poniedziałek, 13 marca 2017

C# - Usart, program komunikacyjny.

W tym poście chciałbym przedstawić program, który pozwoli na odbieranie danych z układu USART. Tutaj do testów wykorzystam arduino. Dane będą wyświetlane na ekranie w oknie standardowym czyli jak dla programu Terminal bądź innych tego typu. W kolejnym poście przedstawię w jaki sposób wykonać rysowanie wykresu dla danych odpowiednio sformatowanych dla USART'u.

Przedstawiony poniżej program jest wersją wstępną, prototypową, która w kolejnych postach będzie coraz bardziej rozwijana i będą do niej dodawane kolejne elementy.

Program dla Arduino:

Poniżej znajduje się szybki program na Arduino, będący połączeniem kilku przykładowych programów:

  1. byte byteRead;
  2. int thisByte = 33;
  3. void setup()
  4. {                
  5.   pinMode(13, OUTPUT);    
  6.   Serial.begin(115200);
  7.   while (!Serial) { ; }
  8.   Serial.println("ASCII Table ~ Character Map");
  9. }
  10. void loop()
  11. {
  12.   while(thisByte != 126)
  13.   {
  14.     Serial.write(thisByte);
  15.     Serial.print(", dec: ");
  16.     Serial.print(thisByte);
  17.     Serial.print(", hex: ");
  18.     Serial.print(thisByte, HEX);
  19.     Serial.print(", oct: ");
  20.     Serial.print(thisByte, OCT);
  21.     Serial.print(", bin: ");
  22.     Serial.println(thisByte, BIN);
  23.     thisByte++;
  24.   }
  25.   if (Serial.available())
  26.   {
  27.     byteRead = Serial.read();
  28.     Serial.write(byteRead);
  29.     if(byteRead == 'a')
  30.     {
  31.       digitalWrite(13, HIGH);
  32.       delay(800);
  33.       digitalWrite(13, LOW);
  34.     }
  35.   }
  36. }

W części Setup inicjalizowana jest dioda wbudowane w płytkę Arduino, Następnie włączana jest transmisja po porcie szeregowym. Oczekiwanie na jej rozpoczęcie oraz wypisanie tekstu na ekran.

Pętla natomiast zawiera wypisanie znaków w kodzie ASCII na ekran z przedziału od 33 do 125. Po tej operacji następuje przejście do części programu działającego jako echo. Jeśli dane zostały odebrane to wysyła je ponownie na ekran. Jeśli przez układ zostanie odebrany znak a to dioda zostanie zapalona, po krótkim oczekiwaniu zgaszona i program powróci dalej do trybu oczekiwania na odebranie znaku.

Przygotowane okno główne:


W oknie głównym aplikacji dotyczący komunikacji poprzez port COM. Do wyboru trybu działania aplikacji służą kontrolki ComboBox które pozwalają na ustawienie nazwy portu, prędkości, bitów danych, bitów parzystości oraz bity stopu. Przyciski znajdujące się w lewej stronie pozwalają na ustawienie danych dla transmisji, jak wartości domyślne czy odświeżenie transmisji. Duże czerwone pole działa na zasadzie przycisku. Jego kliknięcie powoduje otwarcie wybranego portu komunikacyjnego oraz zmianę jego koloru na zielony. Będzie to oznaczać, że transmisja została poprawnie zainicjalizowana. Dane z portu można także dodatkowo zapisywać do pliku tekstowego. Przycisk wyczyść czyści okno główne, które zawiera okno dane odebrane od układu. Przycisk wyślij przesyła dane jakie zostały wprowadzone do textBoxa znajdującego się poniżej.

Pozostałe opcje zdefiniowane w kolejnych zakładkach będą dodawane w późniejszym czasie. Do każdej z tych zakładek zostanie przygotowany osobny post.


Do projektu zostało dodane także drugie okno RichTextBox, w którym będą wyświetlane dane.

Widok danych odebranych:


Widok dla danych przesłanych:


Program:


Poniżej przejdę po wszystkich ważniejszych elementach aplikacji.

Na samym początku zostaje stworzona obiekt klasy. Bez podanych parametrów, automatycznie zostają podane baudrate 9600, bez bitów parzystości, jeden bit stopu, bez kontroli przepływu danych.

  1.         System.IO.Ports.SerialPort ComPort;

Dalej tworzony jest delegat:

  1.         delegate void Delegate1();
  2.         Delegate1 New_Delegate1;

Jego zadaniem jest odbieranie danych z funkcji WpiszOdebrane. Delegat jest wykorzystywany do przekazywania argumentów innym metodom.

W tej części została jeszcze zdeklarowana zmienna przechowująca odebrane dane, zmienna zawierająca aktualny czas oraz typ wyliczeniowy TransmissionType. On przechowuje jaki tryb transmisji zostanie wybrany czyli czy dane będę wyświetlone jako tekstowe czy w formacie szesnastkowym.

  1.         public enum TransmissionType { Text, Hex }
  2.         private TransmissionType _transType = TransmissionType.Text;
  3.         string rxString;
  4.         private DateTime date_time = DateTime.Now;

Teraz czas na funkcje inicjalizującą parametry głównego okna:

  1.         public Form1()
  2.         {
  3.             InitializeComponent();          //Wlaczenie kontrolek itp.
  4.             ComPort = new SerialPort();     //inicjalizacja zmiennej port
  5.             //timeouty dla programu, określają maksymalny dopuszczalny czas przeznaczony
  6.             //na zapis oraz odczyt poprzez port com, dzieki nim program się nie zawiesi
  7.             ComPort.ReadTimeout = 500;
  8.             ComPort.WriteTimeout = 500;
  9.             //Event dla danych przychodzących do portu COM
  10.             ComPort.DataReceived += new SerialDataReceivedEventHandler(DataRecievedHandler);
  11.             //Event dla klikniecia przycisku odśwież
  12.             this.butRefresh.Click += new System.EventHandler(this.Event_Data_Handler);
  13.             tabPageComPort.Enter += new EventHandler(Event_Data_Handler);,
  14.             //Przypisanie funkcji dla delegata
  15.             New_Delegate1 = new Delegate1(WpiszOdebrane);
  16.             //Ustawienie domyślnych wartości
  17.             this.comboBoxPort.Items.Clear();                    //Wyczyszczenie danych dla portów
  18.             this.comboBoxParity.Items.Clear();             
  19.             this.comboBoxStop.Items.Clear();
  20.             foreach (String s in SerialPort.GetPortNames())         { this.comboBoxPort.Items.Add(s);   }
  21.             foreach (String s in Enum.GetNames(typeof(Parity)))     { this.comboBoxParity.Items.Add(s); }
  22.             foreach (String s in Enum.GetNames(typeof(StopBits)))   { this.comboBoxStop.Items.Add(s);   }
  23.            
  24.             //Wyświetlenie tekstu w kontrolkach
  25.             comboBoxPort.Text = ComPort.PortName.ToString();
  26.             comboBoxBaudRate.Text = ComPort.BaudRate.ToString();
  27.             comboBoxData.Text = ComPort.DataBits.ToString();
  28.             comboBoxParity.Text = ComPort.Parity.ToString();
  29.             comboBoxStop.Text = ComPort.StopBits.ToString();
  30.         }

Dla odebranych danych zdefiniowany jest DataReceiveHandler zostaje on wywołany gdy pojawią się dane do odbioru. Powoduje on wywołania delegata, który wywołuje funkcje odbierające dane:

  1.         private void DataRecievedHandler(object sender, SerialDataReceivedEventArgs e)
  2.         {
  3.             richTextBoxTerminal.Invoke(New_Delegate1);
  4.         }
  
Kolejna funkcja odpowiada za wypisanie danych na ekranie. Sprawdzany jest status wprowadzonych znaków. Jeśli nie zaznaczony żaden znak to automatycznie w formacie tekstu, jeśli zaznaczony blok hex to dane w formacie szesnastkowym.

  1.         private void WpiszOdebrane()
  2.         {
  3.             try
  4.             {
  5.                 if (rdoHex.Checked == true)
  6.                 {
  7.                    richTextBoxTerminal.SelectionColor = System.Drawing.Color.Blue;
  8.                    var StartIndex = richTextBoxTerminal.TextLength;
  9.                    richTextBoxTerminal.AppendText(ComPort.ReadByte().ToString("X") + Environment.NewLine);
  10.                    var EndIndex = richTextBoxTerminal.TextLength;
  11.                    richTextBoxTerminal.Select(StartIndex, EndIndex - StartIndex);
  12.                 }
  13.                 else
  14.                 {
  15.                     richTextBoxTerminal.SelectionColor = System.Drawing.Color.DarkRed;
  16.                     var StartIndex = richTextBoxTerminal.TextLength;
  17.                     richTextBoxTerminal.AppendText(ComPort.ReadLine());
  18.                     var EndIndex = richTextBoxTerminal.TextLength;
  19.                     richTextBoxTerminal.Select((StartIndex), EndIndex - (StartIndex));
  20.                     richTextBoxTerminal.AppendText(Environment.NewLine);
  21.                 }
  22.             }
  23.             catch (Exception ex){ }
  24.             richTextBoxTerminal.SelectionStart = richTextBoxTerminal.Text.Length;
  25.             richTextBoxTerminal.ScrollToCaret();
  26.         }

Teraz opisanie kontrolki odpowiedzialnej za wybranie trybu pracy:

  1.         private void rdoHex_CheckedChanged(object sender, EventArgs e)
  2.         {
  3.             if (rdoHex.Checked == true)
  4.             {
  5.                 _transType = TransmissionType.Hex;
  6.             }
  7.             else
  8.             {
  9.                 _transType = TransmissionType.Text;
  10.             }
  11.         }

Sprawdzanie ustawiania dla zmiennej TransmissionType:

  1.         public TransmissionType CurrentTransmissionType
  2.         {
  3.             get { return _transType; }
  4.             set { _transType = value; }
  5.         }

Poniżej przedstawię opis funkcji obsługi zdarzenia, jakie jest wywoływane na kliknięcie przycisku.

  1.         void Event_Data_Handler(object sender, EventArgs e)
  2.         {
  3.             //aktualizacja list
  4.             this.comboBoxPort.Items.Clear();
  5.             this.comboBoxParity.Items.Clear();
  6.             this.comboBoxStop.Items.Clear();
  7.             foreach (String s in SerialPort.GetPortNames()) this.comboBoxPort.Items.Add(s);
  8.             foreach (String s in Enum.GetNames(typeof(Parity))) this.comboBoxParity.Items.Add(s);
  9.             foreach (String s in Enum.GetNames(typeof(StopBits))) this.comboBoxStop.Items.Add(s);
  10.             //aktualizacja nazw
  11.             comboBoxPort.Text = ComPort.PortName.ToString();
  12.             comboBoxBaudRate.Text = ComPort.BaudRate.ToString();
  13.             comboBoxData.Text = ComPort.DataBits.ToString();
  14.             comboBoxParity.Text = ComPort.Parity.ToString();
  15.             comboBoxStop.Text = ComPort.StopBits.ToString();
  16.         }

Najpierw czyszczone są kontrolki comboBox przechowujące dane. Następnie przechowywane są wartości dostępnych portów com, bitów parzystości oraz bitów stopu. Na samym końcu te dane są wprowadzane do poszczególnych kontrolek.

Następnie funkcja odpowiedzialna za przesyłanie danych wpisanych w komórce textBox1.

  1.         private void butSend_Click(object sender, EventArgs e)
  2.         {
  3.             try
  4.             {
  5.                 if (ComPort.IsOpen)
  6.                 {
  7.                     richTextBoxSendDat.SelectionColor = System.Drawing.Color.Green;
  8.                     richTextBoxSendDat.AppendText(textBox1.Text + Environment.NewLine);
  9.                    
  10.                     ComPort.WriteLine(textBox1.Text);//Wpisanie nowego tekstu
  11.                     richTextBoxSendDat.SelectionStart = richTextBoxSendDat.Text.Length;
  12.                     richTextBoxSendDat.ScrollToCaret();
  13.                 }
  14.                 else
  15.                 {
  16.                     System.Windows.Forms.MessageBox.Show("W celu przesłania danych ustal połączenie");
  17.                 }
  18.             }
  19.             catch(Exception ex)
  20.             {
  21.                 MessageBox.Show(ex.Message"Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
  22.             }
  23.         }

W pierwszej kolejności sprawdzane jest czy port com jest otwarty, jeśli tak to ustawiany jest kolor i wpisywany jest tekst do komórki z danymi przesłanymi. Następnie wysyłane są dane przez port COM. W ostatnich liniach przesuwany jest tekst do najnowszego w rubryce richTextBox.

Przycisk domyślne pozwala na ustawienie domyślnych danych w rubrykach:

  1.         private void butDomyslne_Click(object sender, EventArgs e)
  2.         {
  3.             this.comboBoxPort.Text = "COM1";
  4.             this.comboBoxBaudRate.Text = "9600";
  5.             this.comboBoxData.Text = "8";
  6.             this.comboBoxParity.Text = "None";
  7.             this.comboBoxStop.Text = "One";
  8.         }

Kliknięcie przycisku Anuluj powoduje wprowadzenie domyślnych ustawień w blokach bity danych, bity parzystości oraz bitach stopu.

  1.         private void butCancel_Click(object sender, EventArgs e)
  2.         {
  3.             comboBoxData.Text = ComPort.DataBits.ToString();
  4.             comboBoxParity.Text = ComPort.Parity.ToString();
  5.             comboBoxStop.Text = ComPort.StopBits.ToString();
  6.         }

Aby nawiązać połączenie należy kliknąć w czerwone pole znajdujące się na ekranie.

  1.         private void pushbottonStatus_Click(object sender, EventArgs e)
  2.         {
  3.             if (ComPort.IsOpen)
  4.             {
  5.                 pushbuttonStatus.BackColor = System.Drawing.Color.Red;
  6.                 ComPort.Close();
  7.                 Wyswietl_dane(richTextBoxTerminal, Environment.NewLine + "Zakończono połączenie z " + ComPort.PortName +                               Environment.NewLineSystem.Drawing.Color.Orange);
  8.             }
  9.             else
  10.             {
  11.                 //połączenie może nie być możliwe dlatego należy się zabezpieczyć na wypadek błędu
  12.                 try
  13.                 {
  14.                     //najpierw przepisujemy do portu parametry z opcji
  15.                     ComPort.PortName = this.comboBoxPort.Text;
  16.                     ComPort.BaudRate = Int32.Parse(this.comboBoxBaudRate.Text);
  17.                     ComPort.DataBits = Int32.Parse(this.comboBoxData.Text);
  18.                     ComPort.Parity = (Parity)Enum.Parse(typeof(Parity)this.comboBoxParity.Text);
  19.                     ComPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits)this.comboBoxStop.Text);
  20.                     ComPort.Open();
  21.                     //po uruchomieniu zmieniamy elementy graficzne interfejsu
  22.                     pushbuttonStatus.BackColor = System.Drawing.Color.Green;
  23.                     Wyswietl_dane(richTextBoxTerminal, "Rozpoczęto połączenie z " + ComPort.PortName + Environment.NewLineSystem.Drawing.Color.Orange);
  24.                 }
  25.                 catch (Exception exc)
  26.                 {
  27.                     MessageBox.Show("Błąd połączenia:\n" + exc.Message);
  28.                 }
  29.             }
  30.         }

Klawisz enter odpowiada za wysłanie danych, ma on taką samą funkcjonalność jak przycisk Wyślij.

  1.         private void enter_click(object sender, KeyEventArgs e)
  2.         {
  3.             if(e.KeyCode == Keys.Enter)
  4.             {
  5.                 butSend_Click(sender, e);
  6.             }
  7.         }

W celu zapisania danych przycisk Zapisz dane obudowano w następująco funkcje:

  1.         private void button1_Click(object sender, EventArgs e)
  2.         {
  3.             try
  4.             {
  5.                 string time = "Time: " + date_time.Hour + "-" + date_time.Minute + "-" + date_time.Second;
  6.                 string file_string = @"c:\DaneComPort";
  7.                 string subfolder = System.IO.Path.Combine(file_string, "Dane");
  8.                 string filename = "dane.txt";
  9.                 System.IO.Directory.CreateDirectory(subfolder);
  10.                 subfolder = System.IO.Path.Combine(subfolder, filename);
  11.                 if (!System.IO.File.Exists(subfolder))
  12.                 {
  13.                     using (System.IO.FileStream fs = System.IO.File.Create(subfolder))
  14.                     {}
  15.                 }
  16.                 using (StreamWriter sw = File.AppendText(subfolder))
  17.                 {
  18.                     System.IO.File.WriteAllText(subfolder, time + Environment.NewLine + rtbTerminal.Text);
  19.                 }
  20.                 MessageBox.Show("Dane zapisanie poprawnie do pliku " + subfolder, " Zapisz plik");
  21.             }
  22.             catch (Exception ex)
  23.             {
  24.                 MessageBox.Show("Błąd tworzenia/zapisu pliku\r\n");
  25.             }
  26.         }

Na samym początku zostają stworzone dane do plików a dokładnie jego lokalizacja, która została podzielona na dwie zmienne. Jedna z nich przechowuje ścieżkę do folderu, druga natomiast zawiera informacje o pliku. Następnie tworzony jest tekst zawierający czas oraz datę zapisania pliku. Następnie sprawdzane jest czy plik już jest stworzony, jeśli nie to tworzy go automatycznie. Dalej wpisywana jest data oraz cały tekst jaki został odebrany przez zdefiniowany port COM.

Program w całości można pobrać pod tym linkiem.