⬆️ ⬇️

Using QSerialDevice and Qwt libraries to organize communication with the controller and display data





In continuation of my article “A simple electronic recorder” I want to share the experience of creating a terminal for communication with a device developed by me based on QSerialDevice and Qwt libraries, and of course Qt. QSerialDevice works with any COM port (real or virtual) defined by the operating system, so it does not matter how the controller is connected to the PC: directly via the UART-> RS-232 adapter (MAX-232), via the UART-> USB adapters (FT -232, CP2101) or UART-> Bluetooth (BTM-222), you can also, for example, connect an Arduino-compatible device (the UART-> USB adapter is already soldered to the board). Qwt is a powerful data viewer. Their common advantage is cross-platform, it's Qt, it’s enough to compile the codes under the required platform - and everything works! So, who cares, I ask under the cat!



Introduction



So, what I wanted to get from my terminal:



As a result, the terminal meets all the above requirements (see the picture in the post header). I'll tell you a little about the structure of the application: the main terminal window is built on the basis of the QMainWindow class. There is a toolbar, two graph windows based on the QwtPlot class, and a text data output window based on QTextEdit. Available ports are displayed in a drop-down list based on QComboBox, the Open button opens the current port, the Options button sets data transmission parameters: parity, number of bits, etc., Info displays information about the connected device. See the source for details.

')





Especially for this article, in order to better illustrate the capabilities of libraries (especially QSerialDevice), I wrote a simplified version of my terminal. On his example, I will try to talk about the main points of development, only explaining the source code of the test application.



Let's start



QSerialDevice take here . Next to connect to the project you need to specify the paths to the files in the project file (I prefer unixstyle to write paths):

include($${PWD}/../src/qserialdeviceenumerator/qserialdeviceenumerator.pri) #   include($${PWD}/../src/qserialdevice/qserialdevice.pri) 


Qwt look here . By the way, general information about QWT can be found here in this habpost , including the build process described there, and I will not repeat in this connection. We connect the library as follows: in the command line:

 qmake -set QMAKEFEATURES /path/to/QWT/features #   qwt.prf 


in the project file:

 CONFIG += qwt 


SerialDeviceEnumerator



The QSerialDevice library provides an interesting opportunity - using the methods of the built-in SerialDeviceEnumerator class, you can determine all the serial ports present in the system, as well as get information about each of the connected devices. A detailed description of the class is present in the source code of the library, an example of enumerator in the source folder of the library illustrates well the possibilities of its use.



 //mainwindow.h #include <serialdeviceenumerator.h> ... class MainWindow : public QMainWindow { ... private slots: void procEnumerate(const QStringList &l); ... private: ... SerialDeviceEnumerator *enumerator; ... void initEnumerator(); void deinitEnumerator(); }; //mainwindow.cpp void MainWindow::initEnumerator() { this->enumerator = new SerialDeviceEnumerator(this); connect(this->enumerator, SIGNAL(hasChanged(QStringList)), this, SLOT(procEnumerate(QStringList))); this->enumerator->setEnabled(true); } void MainWindow::deinitEnumerator() { if (this->enumerator && this->enumerator->isEnabled()) this->enumerator->setEnabled(false); } 


where procEnumerate (QStringList) fills the QComboBox class instance located on the main panel with the list of available com-ports:

 //mainwindow.h namespace Ui { class MainWindow; class MainWindow : public QMainWindow { private: ... QComboBox *portBox; }; } //mainwindow.cpp void MainWindow::createToolBars() { portBox = new QComboBox(ui->tb); portBox->setObjectName("Ports"); ui->tb->addWidget(portBox); ... } void MainWindow::procEnumerate(const QStringList &l) { portBox->clear(); portBox->addItems(l); } 


In order to use all the features of the SerialDeviceEnumerator class, you need to pass to the setDeviceName () method the name of the port of interest as a string, let's add another slot to the hasChanged (QStringList) signal:

 //mainwindow.h #include <serialdeviceenumerator.h> ... class MainWindow : public QMainWindow { ... private slots: void procEnumerate(const QStringList &l); void slotPrintAllDevices(const QStringList &list) ... }; //mainwindow.cpp void MainWindow::initEnumerator() { this->enumerator = new SerialDeviceEnumerator(this); connect(this->enumerator, SIGNAL(hasChanged(QStringList)), this, SLOT(procEnumerate(QStringList))); connect(this->enumerator, SIGNAL(hasChanged(QStringList)), this, SLOT(slotPrintAllDevices(QStringList))); this->enumerator->setEnabled(true); } void slotPrintAllDevices(const QStringList &list) { qDebug() << "\n ===> All devices: " << list; //   foreach (QString s, list) { this->enumerator->setDeviceName(s);//   ,           qDebug() << "\n <<< info about: " << this->enumerator->name() << " >>>"; qDebug() << "-> description : " << this->enumerator->description(); ... qDebug() << "-> is busy : " << this->enumerator->isBusy(); } 


Now in the debug console displays information about all connected devices.



AbstractSerial



While the SerialDeviceEnumerator class is used to detect serial ports in the system and plays an auxiliary role, the main functionality of the QSerialDevice library is the Abstract Serial class.

 //mainwindow.h #include <abstractserial.h> ... class MainWindow : public QMainWindow { ... private slots: ... void procSerialMessages(const QString &msg, QDateTime dt); void procSerialDataReceive(); void printTrace(const QByteArray &data); void RecToFile(QPointF point); ... void procControlButtonClick(); private: ... AbstractSerial *serial; QAction *controlButton; ... void initSerial(); void deinitSerial(); }; //mainwindow.cpp void MainWindow::createToolBars() { ... ui->tb->addAction(controlButton); ... } void MainWindow::initSerial() { this->serial = new AbstractSerial(this); connect(this->serial, SIGNAL(signalStatus(QString,QDateTime)), this, SLOT(procSerialMessages(QString,QDateTime))); connect(this->serial, SIGNAL(readyRead()), this, SLOT(procSerialDataReceive())); //   this->serial->enableEmitStatus(true); } void MainWindow::deinitSerial() { if (this->serial && this->serial->isOpen()) this->serial->close(); } 


The initSerial () function creates an instance of the AbstractSerial class, binds the signal signal state signalStatus (QString, QDateTime) and the readyRead signal () notifying that data arrives at the com port to the corresponding slots. When you click on the Open button on the toolbar, the procControlButtonClick () slot function is executed in which the serial object is assigned the name of the current port in the portBox, the port is opened, the current connection parameters are displayed in the debugging console, then the lists of possible parameter values ​​are set, and then the necessary connection parameters are set.

 void MainWindow::procControlButtonClick() { this->serial->setDeviceName(portBox->currentText()); if (!port->open(AbstractSerial::ReadOnly | AbstractSerial::Unbuffered)) { qDebug() << "Serial device by default: " << port->deviceName() << " open fail."; return; } //   qDebug() << "= Default parameters ="; qDebug() << "Device name : " << port->deviceName(); qDebug() << "Baud rate : " << port->baudRate(); qDebug() << "Data bits : " << port->dataBits(); qDebug() << "Parity : " << port->parity(); qDebug() << "Stop bits : " << port->stopBits(); qDebug() << "Flow : " << port->flowControl(); qDebug() << "Total read timeout constant, msec : " << port->totalReadConstantTimeout(); qDebug() << "Char interval timeout, usec : " << port->charIntervalTimeout(); //    ,     : qDebug() << "List of possible baudrates : " << port->listBaudRate(); ... qDebug() << "List of possible baudrates : " << port->listFlowControl(); //   : if (!port->setBaudRate(AbstractSerial::BaudRate9600)) { qDebug() << "Set baud rate " << AbstractSerial::BaudRate115200 << " error."; return; }; if (!port->setDataBits(AbstractSerial::DataBits8)) { qDebug() << "Set data bits " << AbstractSerial::DataBits8 << " error."; return; } if (!port->setParity(AbstractSerial::ParityNone)) { qDebug() << "Set parity " << AbstractSerial::ParityNone << " error."; return; } if (!port->setStopBits(AbstractSerial::StopBits1)) { qDebug() << "Set stop bits " << AbstractSerial::StopBits1 << " error."; return; } if (!port->setFlowControl(AbstractSerial::FlowControlOff)) { qDebug() << "Set flow " << AbstractSerial::FlowControlOff << " error."; return; } } 


From this point on, when the readyRead () signal appears, control is transferred to the slot function procSerialDataReceive (), in which you can actually organize data processing. At the moment, the function outputs the read data to an output text window based on the textEdit element:

 void MainWindow::procSerialDataReceive() { if (this->serial && this->serial->isOpen()) { QByteArray byte = this->serial->readAll(); this->printTrace(byte, true); } } void MainWindow::printTrace(const QByteArray &data) { textEdit->insertPlainText(QString(data)); } 


The slot function procSerialMessages (const QString & msg, QDateTime dt) outputs status messages when triggered by the signalStatus signal (QString, QDateTime):

 void MainWindow::procSerialMessages(const QString &msg, QDateTime dt) { QString s = dt.time().toString() + " > " + msg; textEdit->appendPlainText(s); } 




Processing and data output. Briefly about Qwt.



So, as has already become clear, the main data processing action should occur in the slot function procSerialDataReceive (). There are possible options depending on whether the data is displayed in real time or first the collection of data, then the output. If the second is enough for you, refer to the article already mentioned. As part of my task, it is necessary to remove the signals from the four channels of the microcontroller's ADC and display their changes over time on two graphs as four curves. Here we consider the simplest case - a single-channel ADC, respectively: one graph - one curve. As is known, QwtPlot class is the basis of Qwt data presentation — the graph canvas itself, QwtPlotCurve class is used to display the curve, QwtArraySeriesData class is used to accumulate curve points, QwtPlotDirectPainter class is used for plotting the curve in real time, QwtPlotDirectPainter class is used for time plotting. So, in the procSerialDataReceive () slot function of the MainWindow class, we create a point and add it to the curve and to the file using the appendPoint (QPointF point) and RecToFile (QPointF point) methods, while assuming that the MC issues data in the format “Ch_number = number "Interspersed with test messages."

 //mainwindow.cpp void MainWindow::procSerialDataReceive() { if (this->serial && this->serial->isOpen()) { QByteArray byte = this->serial->readAll(); this->printTrace(byte); //     if(byte.at(0)!='\n') { dataArray.append(byte); //    } else { if(dataArray.at(0)=='C') //    { if(dataArray.at(3) == '0') //    { double elapsed = (plot -> dclock_elapsed())/ 1000.0;//  QByteArray u; for(int j=5;j<9;j++) { if(dataArray.at(j)!='\r') u[j-5]= dataArray.at(j); //   } QPointF point(elapsed,u.toDouble()*5/1024); //  plot ->appendPoint(point);//    RecToFile(point);//   } } dataArray = 0; } } } void MainWindow::RecToFile(QPointF point) { QFile f("test.dat"); if (f.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&f); out << point.x() << "\t" << point.y() << "\n"; f.close(); } else { qWarning("Can not open file test.dat"); } //plot.cpp void Plot::appendPoint(QPointF point) { CurveData *data = static_cast<CurveData *>(d_curve->data()); data->append(point); const int numPoints = data->size(); if ( numPoints > d_paintedPoints ) { ... d_directPainter->drawSeries(d_curve, d_paintedPoints - 1, numPoints - 1); d_paintedPoints = numPoints; } } 


Naturally, depending on the form in which the microcontroller sends data to the com port, the content of the data filter in the procSerialDataReceive () function depends, feel free to adjust it for your own needs.

The Qwt library is a very powerful output tool, contains a whole set of widgets for creating interfaces like QwtWheel, besides it allows you to easily organize printing, navigation and zoom graphs, so carefully read the documentation and use all the functionality of the library.



Conclusion



In this way, using the QSerialDevice and Qwt libraries, a simple (complex) com-port monitor is assembled, easily portable from one platform to another. For me, it is so convenient and fast! I hope this article once again reminded you of all the power and greatness of Qt and will help you to fully enjoy all its charms in the implementation of their own projects! Good luck!

Source: https://habr.com/ru/post/142476/



All Articles