[Visual 2015 C# Windows Forms] Terminal - utworzenie komunikacji z wykorzystaniem serialPort - językiem początkującego

W tym miejscu zadajemy pytania na temat języka C#, dzielimy się swoją wiedzą, udzielamy wsparcia, rozwiązujemy problemy programistyczne.
Awatar użytkownika
danielos
Newb
Newb
Posty: 69
Rejestracja: sobota 02 sty 2016, 15:06
Lokalizacja: Pawłowice, Silesia.
Kontaktowanie:

[Visual 2015 C# Windows Forms] Terminal - utworzenie komunikacji z wykorzystaniem serialPort - językiem początkującego

Postautor: danielos » wtorek 12 kwie 2016, 19:00

Witam.

Z racji tej, że chcę zrobić pewien może i ciekawy program, musiałem uruchomić komunikację z wykorzystaniem portu szeregowego w Visual 2015 dla C#. Dlatego chciałbym się z wami podzielić, wiedzą dot. uruchomienia komunikacji poprzez porty COM, na przykładzie prostego terminala.

W sieci jest dużo przykładów, ale nie wszystko w nich jest wytłumaczone - pewne fragmentu kodu nie są objęte komentarzami - szczególnie te dotyczące odbioru danych. Dlatego publikuję tu mój przykład (interfejs i reakcje programu troszeczkę rozbudowane), wraz z opisem - mam nadzieję, że uda mi się wszystko wytłumaczyć w dość prosty i zrozumiały sposób i że nie popełnię jakichś błędów (jeżeli tak się stanie, to pisać co jest nie tak i mnie poprawić).

:!: Środowisko, z którego korzystam to Visual Studio 2015 community w wersji PL.

Tworzymy nowy projekt (Plik->Nowy->Projekt). W nowo wywołanym okienku wybieramy i rozwijamy z menu po lewej stronie Szablony, a następnie Visual C#. Następnie w środkowym oknie należy wybrać Aplikacja Windows Forms. Kolejną czynnością jest nazwanie naszego projektu - dokonujemy tego w polu nazwa. Poniżej na zdjęciu przedstawiam jak to powinno wyglądać (ja projekt nazwałem Cs_01_terminal)
Nowy_projekt.jpg

Zakładam, że pozostałe czynności jak, np. zmiana tekstu wyświetlanego w kontrolkach, jest już znana osobie która to czyta (dla przypomnienia: zmianę wyświetlanej nazwy kontrolki, np. label, dokonuje się w menu ustawienia w parametrze text).

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 -

Ustawienie powyższych komponentów jest dowolne. W moim wykonaniu prezentuje się to następująco:
Wyglad_formatki.jpg

Program będzie umożliwiał m.in. wybranie z menu rozwijalnego prędkości baudrate. Aby możliwy był wybór należy do tej kontrolki 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:
Ustalenie_kolekcji_predkosci.jpg

Zmiany potwierdzamy OK.

:idea: Teraz zacznie się część programistyczna. Większość kodu zawiera opisy w komentarzach, natomiast część kodu, co wymaga więcej uwagi na wytłumaczenie, będzie znajdowała w się w dalszej części wpisu.

W pierwszej kolejności napiszemy kod, który będzie wykonywał się podczas uruchamiania programu. 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.jpg

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:

Kod: Zaznacz cały

        //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.

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ń:

Kod: Zaznacz cały

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:

Kod: Zaznacz cały

        //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 kontrolerk
                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:

Kod: Zaznacz cały

        //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:

Kod: Zaznacz cały

        //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.jpg


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

Kod: Zaznacz cały

        //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 (sedno pisania tego wpisu). 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 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ć w ten sposób nie używając wyrażenia lambda (trochę więcej roboty):

- na początku klasy utworzyć metodę:

Kod: Zaznacz cały

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

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

Kod: Zaznacz cały

                //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:

Kod: Zaznacz cały

        //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 to pisać. Może uda mi się to wyedytować.

Pozdrawiam, oraz zachęcam do wydawania wyroków, ....eee, znaczy się opinii.

Załączam mój projekt.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Można wszystko osiągnąć, wystarczy chcieć.

Wróć do „Pisanie programów w C#”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 6 gości