[Kurs Qt] Obsługa portów szeregowych

Wyszukiwanie podłączonych urządzeń, odbieranie/wysyłanie danych przez port szeregowy.

Wstęp


Dzisiejsza lekcja prawdopodobnie najbardziej zaciekawi osoby zajmujące się robotyką, czy też podobnymi działami gdzie istnieje potrzeba odbierania, czy też wysyłania danych do zewnętrznego urządzenia przez np. USB.

W późniejszych lekcjach użyjemy dzisiaj zdobytą wiedzę do napisania programu, który pozwala na komunikację laptopa z telefonem czy innym urządzeniem posiadającym bluetooth, lecz dzisiaj zajmiemy się komunikacją przez USB.

 

Przygotowanie


Z racji, że jeszcze nie potrafimy za bardzo się komunikować z innym urządzeniem, to ja wykorzystam Arduino Uno, któro ma już wbudowany mechanizm wysyłania/odbierania danych i nie trzeba ich samemu pisać. Nie będę tutaj dokładnie objaśniał kodu w Arduino, ale o tym możecie poczytać na blogu Łukasza, który jest zdecydowanie większym specjalistą ode mnie jeżeli chodzi o Arduino.

Okno naszej aplikacji może być wam już znane, ponieważ pokazywałem je na Twitterze jakiś czas temu, dzisiaj zajmiemy się jedynie okodowaniem konsoli i wyszukiwaniem portów w Settings->Port.

Nie obędzie się też bez dodania odpowiedniej linii do pliku .pro:

 

Wyszukiwanie urządzeń


Nie da się ukryć, że zanim zaczniemy odbierać/wysyłać dane musimy najpierw połączyć się z danym urządzeniem.

Zaczynamy od pliku nagłówkowego, gdzie dodamy 2 pliki nagłówkowe: QSerialPort  i QSerialPortInfo , gdzie pierwszy pozwala nam obsługiwać dany port, a drugi dostarcza nam o nim informacji (nazwa, opis, etc).

Polecam zwrócić uwagę na podświetlone linie, bo one nie znajdują się w standardowo generowanym pliku nagłówkowym mainwindow.h.

Dodaliśmy tutaj deklaracje dwóch slotów, a także prywatną funkcję. Kolejnym elementem jest dodanie w pliku źródłowym potrzebnych nam wkrótce zmiennych globalnych.

Mamy tutaj listę przechowującą wszystkie obecnie znalezione porty, a także wskaźnik na obecnie używany (wybrany) port.

W konstruktorze ustawiamy domyślnie wartość wskaźnika na NULL , wyszukujemy obecnie podłączone urządzenia, a także łączymy sygnał wciśnięcia przycisku w menu odpowiadającego za żądanie dodatkowego zaktualizowania listy portów z odpowiednim slotem. Destruktor pozostawiamy bez zmian.

Tutaj wyszukujemy obecnie podłączone urządzenia i „wrzucamy” je do wcześniej przygotowanej listy. Wykorzystałem tutaj coś o czym nie mówiłem, a mianowicie statusBar  inaczej pasek statusu, który wyświetla w „stopce” okna jakąś informację, u nas wyświetla ilość znalezionych urządzeń.

W showMessage()  podajemy jako argumenty wiadomość i ewentualnie czas (w ms) po jakim ma wiadomość zniknąć.

Z racji, że odświeżamy listę to wskaźnik ustawiamy wcześniej na NULL  aby nie wskazywał na śmieci po wyczyszczeniu listy i następnie dodajemy wartości tekstowe do Combo Box’a.

Slot jest aktywowany po zmianie wartości przez użytkownika w combo box’ie. Ustawia on wskaźnik na nowy obiekt (lub nic) i wyświetla odpowiednią informację w pasku statusu.

Najbardziej trywialny slot, po prostu aktywujący odpowiednią funkcję do wyszukiwania portów. Co ważne wyszukane porty nie są puste i mogą się komunikować z komputerem, tzn. jeśli mamy podłączoną myszkę przez USB to program jej nie wykryje, jeżeli Arduino to zostanie ono wykryte.

Nasz program potrafi już wyszukiwać urządzenia, wbrew pozorom kodu jaki napisaliśmy jest niewiele. Najwyższy czas wysłać jakieś dane :) .

 

Wysyłanie danych


Zanim nauczymy się wysyłać informację to po prostu napiszmy sobie system, który będzie wyświetlał nasze komunikaty po ich napisaniu i wciśnięciu przycisku Enter (zarówno klawiaturze jak i tym w programie).

Wymaga to dopisania kolejnych deklaracji funkcji:

Pierwszy ze slotów aktywuje się po wciśnięciu „fizycznego” klawisza Enter, a drugi po wciśnięciu wewnątrz programu.

Funkcja  addTextToConsole jako argumenty przyjmuje wiadomość, którą ma wstawić na naszą „konsolę” oraz informację o tym czy jest ona od użytkownika (a więc należy ją jeszcze wysłać do urządzenia), czy od urządzenia.

Deklaracja slotów jest prosta i identyczna, w zasadzie można by było napisać tylko raz ten kod w jednym slocie, a w drugim sztucznie utworzyć sygnał, który spowodowałby działanie kodu.

Najciekawszą z tych funkcji jest samo wysłanie wiadomości.

Blokujemy tutaj możliwość wysłania pustej wiadomości (ale już nie złożonej z samych znaków białych, np spacji) i tworzymy prostą komendę do czyszczenia konsoli, z racji że nasz program ma jedynie wysyłać wiadomości/odbierać, to nie tworzymy specjalnego systemu pod obsługę komend.

Przed dodaniem tekstu dodajemy informację kto jest jej autorem (my czy urządzenie). Po wstawieniu tekstu przewijamy scroll na koniec, aby najświeższe info zawsze było „na wierzchu”.

Skończyliśmy tą nudniejszą część, a więc czas na wysyłanie wiadomości, ja przyjmuję konwencję, że koniec wiadomości oznaczam znakiem $. Tzn ten symbol będzie dawał znać Arduino, że to już koniec jednego ciągu komendy/wiadomości. Przykładowy wygląd wiadomości wysłanej do arduino:

"Hello World!$"

Wszystko co będzie po $ będzie oznaczało nową wiadomość, pomaga to w sprawdzaniu czy została wysłana/odebrana cała wiadomość, a nie jedynie jej część. Kod do Arduino:

Kod sprawia, że wiadomość jest odbierana dopóki nie napotkamy znaku $, gdy napotka ten znak zapala diodę 13. na 2 sekundy.

Czas na właściwy kod do wysyłania danych, najpierw musimy sobie dołożyć nową deklarację funkcji w pliku nagłówkowym (przy okazji dokładamy deklarację do odbierania).

A także dodajemy nowy plik nagłówkowy oraz zmienną globalną.

Czas na edycję, wcześniej pisanych funkcji (zmiany mają podświetlone linie).

W tej funkcji jeżeli od razu otwieramy wybrany port, który wybraliśmy i w razie problemów dostajemy błąd w postaci MessageBox’a. Jeżeli tutaj wystąpi błąd to nie będziemy w stanie wysłać żadnej wiadomości.

addTextToConsole(...) dodajemy po prostu wywołanie funkcji i dodajemy na końcu wiadomości informację ($) końca wiadomości.

Funkcja wysyłania danych jest wręcz frustrująco krótka, ponieważ tyle musieliśmy się narobić aby do niej w ogóle dojść :).

Sprawdzamy w niej czy port oby na pewno jest otwarty i jeżeli tak  to wysyłamy („piszemy”) dane, co ważne dane muszą być w postaci QBytesArray  lub char* . Jako dodatkowy argument możemy podać maksymalną wielkość 1 pakietu.

Tutaj chciałbym zwrócić uwagę na klasę QSerialPort, która ma kilka atrybutów zbyt ważnych aby je pominąć, ponieważ ich niezgodność może spowodować problemy z transmisją danych.

  • BaudRate to wartość szybkości (częstotliwości) transmisji, standardowo używana wartość to 9600.
  • DataBits, czyli sposób w jaki są przekazywane (otrzymywane) dane, zazwyczaj używa się 8 bitowego systemu.
  • Direction kierunek transmisji, możemy np. tylko odbierać dane.
  • FlowControl czyli co ma kontrolować dany port.
  • Parity schemat parzystości wysyłanych bitów.
  • StopBits bity stopu.

My załatwiliśmy sobie ustawienie tych wszystkich wartości przez użycie setPort(...) , które odziedziczyło te wartości z urządzenia stąd nie ustawialiśmy ich.

 

Odbieranie danych


Niewątpliwie nieco trudniejsza część naszej pracy, zanim zajmiemy się odbieraniem danych to trzeba najpierw za-symulować ich wysyłanie, do Arduino dodamy kod, który sprawi, że będziemy dostawać wiadomość zwrotną do naszej wiadomości.

Po odebraniu danych przez Arduino wysyła ono wiadomość zwrotną: „Space”, z różną ilością liter ‚a’.

W naszym programie będziemy oczekiwali danych jedynie wtedy gdy jakieś dane wyślemy, to jest dość ważne ponieważ nasz program nie odbierze danych wysyłanych w losowo wybranych momentach i gdy sam niczego nie wyśle. Jest to tzw synchroniczne odbieranie danych polega na schemacie: wysyłam dane – odbieram dane.

My w tym poradniku pójdziemy nieco na skróty, ponieważ z racji małych danych jakie odbieramy to nie mamy funkcji do odbierania odpalanej na oddzielnym wątku lecz na głównym i większych danych moglibyśmy odczuwać freeze’owanie aplikacji.

O tym jak obierać dane w sposób asynchroniczny powiemy sobie w jednej kolejnych lekcji.

Wracając do tej lekcji, musimy lekko zmienić (dodaliśmy instrukcję warunkową)  addTextToConsole(..) .

Oraz brakuje nam 1 linii w funkcji wysyłającej dane:

Wreszcie doszliśmy do długo wyczekiwanego przez nas momentu, czyli do funkcji void receive() , która odbiera wysyłane przez Arduino dane.

Po wywołaniu tej funkcji program czeka 5 sekund na napłynięcie jakichś danych, jeżeli takie dostanie to je zapisuje, a następnie dopisuje kolejne partie danych, które otrzyma (pamiętamy że dane dostajemy „blokami” i trzeba je jakoś poskładać).

Kolejnym krokiem jest konwersja tych danych na QString, nic nie stoi na przeszkodzie aby zapisać je jako np int . Po czym usuwamy wszystkie powtórzenia znaków końca linii/komendy: „$”. Powinniśmy dodatkowo sprawdzać przed usunięciem czy nie znajduje się kilka znaków ‚$’ aby je odpowiednio rozdzielić i wysłać, bo w obecnej formie cała wiadomość otrzymana w ten sposób zostanie „sklejona” i wysłana do konsoli jako jedna forma. My jednak w naszym programie wykluczamy możliwość wysyłania kilku komend jednocześnie przez Arduino, stąd pozostawiamy to w tej formie.

Ostatnim krokiem jest wyświetlenie wiadomości w konsoli.

 

Podsumowanie


Zdobyliśmy dzisiaj mnóstwo nowej wiedzy: już wiemy w jaki najprostszy sposób możemy komunikować się z innym urządzeniem przez port USB.

Tradycyjnie zapraszam do komentowania, śledzenia  i ogólne tego co pojawia się na nim. Kod jest tradycyjnie dostępny na [GitHub]’ie.