[Visual Studio 2015] Obsługa komponentu serialPort – tworzymy prosty terminal dla portu szeregowego

Witam.

W pewnym momencie dla hobbysty elektronika przychodzi taki moment w którym potrzebne jest, aby wykonane urządzenie skomunikowało się z komputerem – np. aby wysłać wartości zmierzonej temperatury. Dodatkowo chcemy aby odbieraniem i interpretacją danych zajmował się dedykowany program, najlepiej jakby był napisany przez nas. Aby tego dokonać, w programie Visual Studio w języku C# wykorzystuje się komponent serialPort, który w dość łatwy sposób umożliwia nam odbieranie i wysyłanie danych.

W tym wpisie chciałbym przedstawić sposób konfiguracji tego komponentu na przykładzie prostego terminala. Możliwa będzie zmiana portu szeregowego, oraz wybór prędkości baudrate. Postaram się również w jak najlepszy, oraz w prosty i zrozumiały sposób opisać kod, który zostanie wykorzystany. W tym momencie również chciałbym przeprosić za ewentualne błędy, oraz prosić o sugestie dot. lepszego opisania danego fragmentu programu.


Zakładam, że tworzenie nowego projektu jak i wstawianie nowych komponentów do formatki jest znana.

Przykład terminala, realizowany jest w programie Visual Studio 2015 Community w języku C#.

Większość kodu (część łatwiejsza) zostanie opisana w komentarzach. Pozostałe fragmenty kodu wymagające większej uwagi zostaną dodatkowo opisane.


W programie zostanę użyte następujące kontrolki:

  • label – 3 sztuki – zastosowane do opisu innych kontrolek
  • button – 3 sztuki – przyciski połącz, rozłącz wyślij
  • comboBox – 2 sztuki – wybór portu, oraz prędkości transmisji
  • checkBox – 2 sztuki – wybór, czy wysyłany tekst ma na końcu zawierać znak końca linii i/lub powrotu karetki
  • richTextBox – 1 sztuka – okienko w której będą pojawiać się odebrane znaki
  • textBox – 1 sztuka – okienko do którego będą wpisywane znaki, które mają zostać wysłane
  • serialPort – 1 sztuka – komponent potrzebny do obsługi komunikacji szeregowej

Ustawienie komponentów jest dowolne. W moim przykładzie wygląda to następująco:

Ułożenie komponentów
Ułożenie komponentów

Tak jak już wspomniałem we wstępie, program będzie umożliwiał wybranie prędkości baudrate, oraz portu do którego podłączone będzie nasze urządzenie. Aby możliwy był wybór prędkości należy do kontrolki z rozwijanym menu wpisać żądane prędkości – dokonuje się tego poprzez wskazanie rozwijanej listy (comboBox) i odnalezieniu we właściwościach (okienko właściwości domyślnie znajduje się na dole po prawej stronie) do tej kontroli parametru Items. Wybranie tego parametru powoduje otwarcie nowego okna, gdzie w każdej nowej linii, można umieścić w tym przypadku żądane prędkości. Wygląda to w ten sposób:

Wpisanie dostępnych prędkości do rozwijanej listy.
Wpisanie dostępnych prędkości do rozwijanej listy.

Natomiast dostępne porty, uzupełniane będą automatycznie podczas ładowania/uruchamiania się naszego programu. Tego dokonamy pisząc odpowiedni kod w zdarzeniu wywoływanym podczas ładowania naszego formularza. W tym celu należy wybrać formularz i w oknie właściwości zmienić wyświetlany tryb na zdarzenia, a następnie dwa razy kliknąć obok zdarzenia LOAD:

Zdarzenia formatki
Zdarzenia formatki

Otworzy nam się plik programu z rozszerzeniem *.cs, a znak kursora będzie ustawiony na zawartość metody uruchamianej po załadowaniu formatki. Ciało tej zostanie uzupełnione następującym kodem:

//załadowanie FORM
private void Form1_Load(object sender, EventArgs e)
{
         //odczytanie dostępnych portów wraz z wpisanie ich do rozwijanej listy
            comboBox1.Items.AddRange(SerialPort.GetPortNames());

          //sortowanie wyswietlanych nazw dostępnych portów
          comboBox1.Sorted = true;   //true oznacza, że zawartość tego komponenty ma być posortowana

          //przypisanie wartosci domyslnych w rozwijanych listach wyboru
          comboBox1.SelectedIndex = 0;   //pierwszy dostępny port
          comboBox2.SelectedIndex = 6;   //prędkość 19200 - jest 6 z kolei - liczymy od 0

          //aktywacja i deaktywacja odpowiednich kontrolek
          comboBox1.Enabled = true;   //lista z portami
          comboBox2.Enabled = true;   //lista z prędkością
          button1.Enabled = false;    //przycisk wyślij
          button2.Enabled = true;     //przycisk połącz
          button3.Enabled = false;    //przycisk rozłącz
          textBox1.Enabled = false;   //edit box dla wyślij
}

W ostatniej części tego kodu aktywujemy, bądź dezaktywujemy niektóre kontrolki – w zależności od tego czy po załadowaniu programu mają one być dostępne dla użytkownika.

Należy dodać jeszcze jedną przestrzeń nazw (zbiór dostępnych klas – biblioteki), która będzie nam potrzebna do odczytania dostępnych portów. Dodajemy ją na samej górze za domyślnie dodanymi przestrzeniami nazw, a przed ciałem naszej klasy. Jest to przestrzeń:

using System.IO.Ports;                  //Dodanie nowej przestrzeni nazw związanej z obsługą wejść/wyjść dla portów

W następnym kroku ustanowimy połączenie programu z wybranym portem, poprzez wciśnięcie przycisku – Połącz. Aby napisać kod dla przycisku, należy przejść do widoku projektanta formularza i dwa razy kliknąć na przycisk który ma powodować połączenie z danym portem. Kod dla tego przycisku wygląda następująco:

//połącz
     private void button2_Click(object sender, EventArgs e)
     {

         //zabezpieczenie przed wystąpieniem wyjątku/problemu z otwarciem portu
         try
         {
            //ustawiany jest port, który został wybrany z rozwijanej listy
             serialPort1.PortName = comboBox1.Text;
             //konwersja i ustawienie prędkości transmisji (która została wybrana z rozwijanej listy)
             serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text);

             //otwarcie wybranego portu
             serialPort1.Open();

             //wpisanie do "odebrane"
             richTextBox1.Text += "Połączono dla Port " + serialPort1.PortName + " " + serialPort1.BaudRate.ToString() + "bps \n\r";

                //aktywacja i deaktywacja odpowiednich kontrolek
             comboBox1.Enabled = false;      //lista z portami
             comboBox2.Enabled = false;      //lista z prędkością
             button1.Enabled = true;         //przycisk wyślij
             button2.Enabled = false;        //przycisk połącz
             button3.Enabled = true;         //przycisk rozłącz
             textBox1.Enabled = true;        //edit box dla wyślij
         }
         catch
         {
             //jeżeli wystąpi błąd
             richTextBox1.Text += "Błąd połączenia\n\r";
         }

}

W obsłudze tego przycisku została umieszczona instrukcja try, catch. Powoduje ona przechwycenie wyjątku/błędu który wystąpi w którejś części programu który się w niej znajduje. Jeżeli taki błąd zostanie przechwycony, wykonywanie reszty kodu w instrukcji try zostanie przerwane i nastąpi wykonanie kodu w instrukcji catch. W przypadku braku wystąpieniu jakiegoś błędu, kod w instrukcji catch nie zostanie w ogóle wykonany.

W podobny sposób postępujemy z przyciskiem rozłącz:

       //rozłącz
        private void button3_Click(object sender, EventArgs e)
        {

            //zabezpieczenie przed wystąpieniem wyjątku/problemu z zamknięciem portu
            try
            {
                //zamknięciu portu - odłączenie
                serialPort1.Close();

                //wpisanie do "odebrane"
                richTextBox1.Text += "Rozłączono\n\r";

                //aktywacja i deaktywacja odpowiednich kontrolerk
                comboBox1.Enabled = true;   //lista z portami
                comboBox2.Enabled = true;   //lista z prędkością
                button1.Enabled = false;    //przycisk wyślij
                button2.Enabled = true;     //przycisk połącz
                button3.Enabled = false;    //przycisk rozłącz
                textBox1.Enabled = false;   //edit box dla wyślij
            }
            catch
            {
                //jeżeli wystąpi błąd
                richTextBox1.Text += "Błąd z rozłączeniem\n\r";
            }

        }

oraz wyślij:

        //przycisk wyślij
        private void button1_Click(object sender, EventArgs e)
        {
            //wsyłanie danych z kontroli textbox przez port szeregowy
            serialPort1.Write(textBox1.Text.ToString());

            //czy ma wysyłać polecenie powrotu karetki
            if (checkBox1.Checked)
            {
                //wysłanie polecenia powrotu
                serialPort1.Write("\r");
            }

            //czy ma wysyłać polecenie nowej lini
            if (checkBox2.Checked)
            {
                //wysłanie polecenia nowej linii
                serialPort1.Write("\n");
            }

        }

Aby terminal odbierał dane należy napisać kod reagujący na to zdarzenie (odbioru danych). Żeby tego dokonać, należy przejść od okna projektanta, wybrać kontrolką serialport1 z formularza, w właściwościach zmienić widok na zdarzenia i dwa razy kliknąć na zdarzenie DataReceived:

Zdarzenie odbioru z serialPort
Zdarzenie odbioru z serialPort

Po przejściu do pliku z kodem źródłowym, wnętrze metody obsługującej zdarzenie odbioru danych wypełnimy następująco:

        //zdarzenie odbioru danych
        private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            //sprawdzenie czy komponent gdzie wypisywane są odebrane jest w tym samym wątku co odbiór danych
            if (richTextBox1.InvokeRequired)
            {
                //utworzenie delegata (wskaźnika do mikro funkcji) metody do wpisywania danych w komponencie z bufora odbioru danych
                // () => oznacza lambdę
                Action act = () => richTextBox1.Text += serialPort1.ReadExisting();

                //wykonanie delegata dla wątku głównego
                Invoke(act);   //wywołanie delegata
            }
            else
            {//jeżeli jest w tym samym wątku przepisz normalnie dane z bufora do komponentu
                richTextBox1.Text += serialPort1.ReadExisting();
            }

        }

I tutaj znajduje się część kodu która mało gdzie była opisana (dlatego postanowiłem opisać to). Co tu się dzieje – więc tak (prostymi słowami, jak ja to rozumiem):
Zdarzenie obsługujące odbiór danych wykonuje się w osobnym wątku (w tedy gdy nadlecą nowe dane – może ono być w osobnym wątku, ale nie musi, dlatego jest warunek if,else i sprawdzane czy dana kontrolka jest w innym wątku, czy nie). Aby możliwe było wpisanie danych do tej kontrolki, w tym przypadku jest to richtext, należy odnieść się do wątku w którym ta kontrolka się znajduje. Do tego celu należy utworzyć funkcję, która będzie powodowała wpisanie danych do richtext.
Aby to osiągnąć zostanie użyty wbudowany delegat Action. Delegat jest to najprościej mówiąc wskaźnik do metody. Wbudowany delegat Action jest używany gdy chcemy użyć metody która nie będzie zwracała nic (typ void).
Aby zapisać definicję funkcji w jednej linijce (dla wygody) zostanie użyte wyrażenie lambda w postaci () => , dzięki temu nie trzeba deklarować nazwy tej metody w klasie.

Można również zrobić nie używając wyrażenia lambda (trochę więcej roboty):
– na początku klasy utworzyć metodę:

private void writeText()
{
     richTextBox1.Text += serialPort1.ReadExisting();
}

– w obsłudze zdarzenia warunek if miałby postać:

//utworzenie delegata
Action act;
act = writeText;      //przypisanie naszej metody do delegata
//wykonanie delegata dla komponentu
Invoke(act);      //wywołanie delegata

Następnie poprzez metodę Invoke (służy ona do wywołania kontrolki z wątku w którym została ta kontrolka utworzona) zostanie wykonana nasza instrukcja przepisująca odebrane dane do kontrolki – czyli przejście do wątku głównego i tam wykonanie tej instrukcji.

W obsłudze zdarzenia od odbioru jest jeszcze bezpośrednie wpisanie danych do kontrolki, gdyby obsługa zdarzenia odbioru danych, jednak była w tym samym wątku co nasza kontrolka (obsługa else)

Dodamy jeszcze krótki, kod który będzie się wykonywał po zamknięciu aplikacji – aby tak się stało, należy wybrać zdarzenie Closed dla naszego formularza (z właściwości zdarzenia). Kod wygląda następująco:

//po zamknięciu aplikacji/formularza
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{

      //jeżeli dla komponentu serialport1 jest otwarty port należy do zamknąć
      if (serialPort1.IsOpen)
      {
          serialPort1.Close();
      }
}

Powoduje on zamknięciu portu, w przypadku gdy nie został on odłączony przed wykonaniem zamknięcia aplikacji.

Mam nadzieję, że pomogłem w zrozumieniu obsługi tego zdarzenia, oraz że za dużo błędów merytorycznych nie popełniłem – opisywałem to własnymi słowami tak jak Ja (początkujący) to rozumie. Gdyby były jakiejś dość istotne błędy w tłumaczeniu kodu to pisać.

Zachęcam do komentowania i wytykania ewentualnych błędów. W razie jakichkolwiek pytań postaram się na nie odpowiedzieć – na tyle na ile będę potrafił (dopiero z programowanie w C# raczkuję).

W załączniku do tego wpisu udostępniam wam cały projekt.

Pobierz “Cs_01_Terminal.zip”

Cs_01_Terminal.zip – Pobrano 910 razy – 34,96 KB

P.S.
Podobny wpis został również umieszczony przeze mnie na forum microgeek – zachęcam do rejestracji i częstych wizyt.

1 Komentarz

  1. Vizik

    Wartościowy post dla rozpoczynających przygodę z VC # i obsługą portów COM.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *