porty-szeregowe

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


  • marianexyx

    Wspaniała lekcja. Dzięki 🙂

  • purper nikiel

    Funkcje searchDevices() i refresh() są to funkcje biblioteczne? Bo u mnie nie działają.

    • Nie, ich definicje są opisane w tej lekcji (wręcz na samym początku).

  • martintro

    Odbieram po jednym bajcie, zapisuję go jak w przykładzie r_data. Jeśli wyślę na konsolę nie ma problemu, jak to przekonwertować by wyświetlało w QLcdNumber- Zakres 0-255. Funkcja toInt nic mi nie zmienia. W zasadzie dane wysyłane do portu z AVr są w postaci int (1bajt) odbieram to ale nie jestem w stanie go odzyskać (krzaczki). Jak zrobić by nie było konwersji danych do ASCII bo to pewnie się dzieje?

    • Hmm, jak rozumiem robisz coś takiego z 1 daną: program Qt -> AVR -> program Qt. Może wina leży po stronie AVR przy odbieraniu danej? Co z kolei implikuje fakt że jest wysyłana ona błędnie do Qt i źle ją interpretuje?

      • martintro

        W terminalu systemowym mam poprawne odczyty, komunikacja jest tylko w 1 stronę wysyłam bajt co jakiś czas (sygnał z potencjometru (zakres zmian 00-FF). Poprawność wysyłania bajtu przez avr jest poprawna, sprawdzane na różnych terminalach. Qlcd wyświetla w pewnych momentach liczby kręcąc potencjometrem po pewnym czasie zostaje wyświetlona liczba 1,2,3 itd do 9 , ale jest to wąski zakres bo w tym czasie gdy wyrzucę to na konsoli to mam poprostu całą tablicę ASCII. Dokumentację przeglądam już 3 dzień QString, QByteArray,QLcd ale nie mogę natrafić na żaden trop.

        • Ciężko mi coś poradzić, ponieważ zagadnieniem portów niewiele się interesowałem i w sumie nie wiele więcej zrobiłem niż to co tutaj przedstawiłem,a obecnie nie mam kiedy pobawić się tym problemem.

          • martintro

            Rozumiem i dzięki i tak dzięki tobie w ogóle ten temat ruszyłem bo dużego doświadczenia nie mam.
            Na razie pomogło w pewnym stopniu:
            QByteArray r_data = port.readAll();
            QString str(r_data);
            QChar znak=str.at(0);
            int lcd=znak.toLatin1();

            ui->map->display(lcd);

            Tylko jak wartość lcd schodzi poniżej 0 i powyżej 128 (generalnie jak pojawia się -(minus)) to aplikacja się wyłącza a „unsignet int” nie mogę użyć.

            Jeszcze raz dzięki za lekcję

          • martintro

            Hej gdybyś był ciekawy to sprawę załatwiło mi to:

            QByteArray r_data = port.readAll();
            int lcd=r_data.at(0);
            if (lcdmap->display(lcd*0.0043);

          • ok, fajnie że sobie poradziłeś

  • VOLT

    Witam.
    podczas kompilacji na qt5.4 dostaję błędy
    : błąd: invalid use of incomplete type ‚class QScrollBar’ scroll->setValue(scroll->maximum());
    : błąd: forward declaration of ‚class QScrollBar’ class QScrollBar;
    udało mi się sprawę „wyciszyć” przez dodanie verticalScrolBar`a ,ale ten z kolei zasłania scrola pojawiającego się po „przepełnieniu” pola plaintextedit.
    Dodam, że sytuacja jest identyczna i w Ubuntu i w Win7, problemem na pewno jest moja niewiedza, przyznaje się bez bicia, że ledwo zaczynam i z QT i z C++, dotychczas pisałem tylko w C i to głównie na AVR troszkę STM. Jeśli ten temat był poruszany w którymś z poprzednich odcinków kursu to przepraszam za niecierpliwość i proszę o wskazanie żródła/wyjaśnienia.

    • Hmm, a zaincludowałeś ? Pytam ponieważ na tej samej wersji Qt ten sam projekt bezproblemowo się kompiluje.

      • VOLT

        HA. Nawet mi to do głowy nie przyszło, po prostu do „pustego” projektu kopiowałem po kolei fragmenty Twojego kodu, i cieszyłem się że coś działa.
        Już dodałem includa i jest OK. Dzięki.
        Mam jeszcze takie pytania:
        funkcja setPort uruchamia ustawienia domyślne? czyli jakie?
        Czy w destruktorze „aplikacji” nie trzeba zamknąć portu?

        • Domyślne, czyli dokładnie takie jak standardowo ma ten port system, albo jeżeli jest podłączony jakieś urządzenie i ono zmieniło ustawienia portu. Ten kod po prostu nie ingeruje w to co zostało zmienione i ustawione.

          A co do drugiej uwagi, to można, ale destruktor QSerialPort zamyka port za nas jeżeli jest otwarty.

  • Marcin

    Witam.
    Próbuję połączyć owy kodzik wraz z STM, jednak przy próbie wyboru (otwarcia) portu,
    program zwraca mi błąd. Zastanawiam się gdzie szukać przyczyny, bo STM komunikuje się z programem RealTerm bez zarzutów, odbiera i zwraca dane.
    Liczę na pomoc i sugestie 🙁

    • Marcin

      Problem generował RealTerm, który działał równolegle do Qt.
      Najwyraźniej dany port może być otwarty tylko przez jedną aplikację.

      • Cieszę się że sam rozwiązałeś ten problem i faktycznie 1 port może być zajęty w danej chwili przez 1 program