W poprzedniej części tego kursu powiedzieliśmy sobie o synchronicznej wymianie informacji pomiędzy portami, które było całkiem przyjemne i odbywało się na zasadzie: wyślij_dane-odbierz_dane-wyślij_dane-…. Takie rozwiązanie nie zawsze jest praktyczne ponieważ zakłada że oczekiwać na dane będziemy jeżeli po wysłaniu jakiegoś sygnału.
Dzisiaj zajmiemy się asynchroniczną komunikacją pomiędzy portami, czyli taką która zakłada, że będziemy w stanie odebrać dane w dowolnej chwili działania programu.
Przygotowanie
Tradycyjnie przygotujmy sobie okienko naszego programu, którego zadaniem będzie mierzenie temperatury w losowych momentach + nasz program będzie mógł wysłać żądanie o zmierzenie jej w tym momencie. Jak można się domyśleć wykorzystam tutaj Arduino Uno, z którego korzystaliśmy ostatnim razem oraz czujnik temperatury LM35.
Nasze okienko jest maksymalnie proste, składa się z widgetu imitującego wyświetlacz LCD, który będzie pokazywał aktualną temperaturę, TimeEdit do wyświetlania ostatniej aktualizacji (wyłączyłem możliwość edytowania czasu) oraz przycisku do natychmiastowego mierzenia czasu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
int czujnik = 0; // pin pod którym jest termometr unsigned long lastUpdate; // ostatnia aktualizacja czasu int timeToWait; // co ile czasu będzie sprawdzana temperatura float minimum = 1; // min/max oczekiwania na kolejną aktualizację float maximum = 20; // wartości w sekundach void setup() { Serial.begin(9600); lastUpdate = 0; timeToWait = 2000; } void loop() { if(millis() - lastUpdate >= timeToWait) { odczytajTemperature(); } // opóźnienie pomiaru max 100ms delay(100); } // funkcja odczytująca temperaturę void odczytajTemperature() { float temperatura = analogRead(czujnik); temperatura = (temperatura*0.0048828125)/0.01; // wysłanie temperatury int t = temperatura + 0.5f; String str = '@' + String(t) + '$'; Serial.write(str.c_str()); //Serial.print("*Cn"); // aktualizaowanie ostatniej aktualizacji timeToWait = random(minimum,maximum+1) * 1000; lastUpdate = millis(); } // jeżeli nastąpi "rozkaz z góry" void serialEvent() { char buffer; if(Serial.available() > 0) { buffer = Serial.read(); } if(buffer == '!') odczytajTemperature(); } |
Wyjątkowo słowo komentarza do tego kodu: z racji, nie możemy pauzować programu za pomocą delay() bo chcemy mieć możliwość w dowolnej chwili interakcję z programem, tzn. wymuszenie w każdej chwili pobrania temperatury, którą wyślemy jako string.
Tym razem jako znak specjalny dla arduino dostajemy !, co oznacza rozkaz podesłania temperatury oraz @ jako początek wiadomości i $ oznaczający koniec transmisji.
Nie zapomnijmy o dodaniu linii do pliku .pro: QT += serialport w naszym projekcie.
Potrzebne funkcje, zmienne
Czyli coś w rodzaju, krótkiego przypomnienia tego co będziemy używać, a także wstępny wygląd plików nagłówkowych/źródłowych.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QSerialPort> #include <QSerialPortInfo> namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_zmierz_clicked(); void readData(); private: Ui::MainWindow *ui; void searchDevice(); void update(int v=0); QSerialPort *port; }; #endif // MAINWINDOW_H |
Na razie chyba nie potrzeba, żadnego komentarza ponieważ to wszystko znamy z poprzedniej lekcji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); port = new QSerialPort(this); searchDevice(); update(); connect(port,SIGNAL(readyRead()),this,SLOT(readData())); } MainWindow::~MainWindow() { if(port->isOpen()) port->close(); delete port; delete ui; } // wyszukiwanie pojedynczego urządzenia void MainWindow::searchDevice() { QList <QSerialPortInfo> list; list = QSerialPortInfo::availablePorts(); if(list.size() == 1) { port->setPort(list.at(0)); port->open(QIODevice::ReadWrite); } else { int answer = QMessageBox::warning(this,"Błąd!","Nie znaleziono pojedynczego portu, spróbować ponownie?", QMessageBox::Yes,QMessageBox::No); switch(answer) { case QMessageBox::Yes: return searchDevice(); break; case QMessageBox::No: qApp->deleteLater(); break; } } } // prosimy o natychmiastowe podesłanie temperatury void MainWindow::on_zmierz_clicked() { if(port->isOpen()) { char c = '!'; port->write(&c); port->waitForBytesWritten(-1); } else QMessageBox::information(this,"Błąd","Odłączono urządzenie!"); } // aktualizacja widgetów, po odebraniu danych void MainWindow::update(int value) { } |
Do tego całego pliku przyda się kilka słów wyjaśnienia.
Nie ukrywam, że znowu poszedłem na łatwiznę i aby nie zajmować się wyborem portu z menu, otwieraniem go, itp czyli zamiast zajmować się rzeczami, które zrobiliśmy ostatnio tak tutaj zakładam, że do laptopa mamy podłączone dokładnie 1 urządzenie, które może się w ten sposób komunikować inaczej dostajemy prośbę o podłączenie urządzenia lub w przypadku odmowy zamykamy program.
Pozostały kod powinien być jasny dlaczego pojawił się w tej, a nie innej formie. Czas na meritum tej lekcji.
Asynchroniczna komunikacja portów
W asynchronicznej komunikacji charakterystyczną cechą jest to, że bloki wiadomości posiadają znak początku i końca bloku, jest to o tyle ważne, że przy jej odpieraniu należy w odpowiedni sposób ją „skleić”, czyli musimy wiedzieć kiedy ona się zaczęła, a kiedy się kończy, np zdanie: „Hello World”, może być wysłane jako: „He” „ll” „o ” „Wor” „ld”, „Hello World”, albo na wiele innych sposobów, w momencie gdy mamy kilka wiadomości może się zdarzyć, że zostanie sklejone ze sobą kilka wiadomości.
1 2 3 4 5 6 7 8 |
// aktualizacja widgetów, po odebraniu danych void MainWindow::update(int value) { // ustawiamy wartość temperatury ui->temperatura->display(value); // zmieniamy date ostatniej aktualizacji ui->lastUpdate->setDateTime(QDateTime(QDate::currentDate(),QTime::currentTime())); } |
Ta funkcja jest dość łatwa i zostaje wywołana już po zakończeniu wczytywania danych. Jest chyba dość trywialna więc zachęcam do samodzielnej analizy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// wczytywanie danych void MainWindow::readData() { array = port->readAll(); while(port->waitForReadyRead(100)) array += port->readAll(); QString a(array); if(a.at(0) == '@' && a.at(a.size()-1) == '$') { a.remove('$'); a.remove('@'); update(a.toInt()); array.clear(); } } |
Ta funkcja ma za zadanie wczytanie całej liczby, jej konstrukcja jest już nam w jakiś sposób znana z poprzedniej lekcji. Zostaje ona uruchomiona przez sygnał.
Jej konstrukcja jest dość wadliwa bo zakłada, że zawsze pierwszym elementem jest @ (i tak powinno być, chyba że jakieś dane by się zagubiły), a ostatnim $, co już nie zawsze może się zdarzyć i można to nieco poprawić poprzez wyszukanie fraz zaczynających się od @ i kończących $ (możesz to potraktować jako zadanie domowe).
Podsumowanie
Dziś w tej nieco krótkiej lekcji wykonaliśmy program, który może komunikować się w sposób asynchroniczny z innym urządzeniem.
W kolejnej lekcji najprawdopodobniej odstawimy tymczasowo Arduino, bo zajmiemy się komunikacją z bazą danych (chyba że macie inną propozycję).
Jak zwykle w razie pytań do tej lekcji, problemów, albo po prostu chęci wyrażenia swojego zdania zapraszam do systemu komentarzy.
[GitHub]