
В продолжение своей статьи «Простой электронный самописец» хочу поделится опытом создания терминала для сообщения с разработанным мной девайсом на основе библиотек QSerialDevice и Qwt, ну и естественно Qt. QSerialDevice работает с любым COM-портом (реальным или виртуальным) определенным операционной системой, поэтому не имеет значения каким способом контроллер подключен к ПК: непосредственно через адаптер UART->RS-232(MAX-232), через адаптеры UART->USB(FT-232, CP2101) или UART->Bluetooth(BTM-222), также можно, например, подключить Arduino-совместимое устройство (адаптер UART->USB уже напаян на плату). Qwt же — мощное средство отображения данных. Их общий плюс — кроссплатформенность, это же Qt, достаточно скомпилировать коды под нужной платформой — и все работает! Так что, кому интересно, прошу под кат!
Введение
Итак, что мне хотелось получить от своего терминала:
- считывание данных с com-порта по их поступлению (данные с 4х канального модуля АЦП на базе контроллера AVR)
- обработка и вывод в виде графиков в реальном времени
- запись обработанных данных в файл(ы)
- безболезненная переносимость с одной платформы на другую
В итоге терминал удовлетворяет всем вышеперечисленным требованиям (см. картинку в заголовоке поста). Расскажу немного о структуре приложения: главное окно терминала построено на основе класса QMainWindow. Присутствует панель инструментов, два окна графиков на основе класса QwtPlot, и окно вывода текстовых данных на основе QTextEdit. Доступные порты отображаются в выпадающем списке на основе QComboBox, кнопкой Open открывается текущий порт, кнопка Options — установка параметров передачи данных: четность, количество бит и.тд., Info — вывод сведений о подключенном устройстве. Подробности смотрите в исходниках.

Специально для этой статьи, в целях лучшей иллюстрации возможностей библиотек (в особенности QSerialDevice), я написал упрощенный вариант своего терминала. На его примере я попытаюсь рассказать об основных моментах разработки, только лишь поясняя исходные коды тестового приложения.
Начнем
QSerialDevice берем здесь. Далее для подключения к проекту необходимо в файле проекта указать пути к файлам (предпочитаю unixstyle написания путей):
include($${PWD}/../src/qserialdeviceenumerator/qserialdeviceenumerator.pri) #пишите свои пути include($${PWD}/../src/qserialdevice/qserialdevice.pri)
Qwt ищите тут. Кстати, общие сведения о QWT можно найти вот в этом хабрапосте, там в том числе описан процесс сборки, в связи с этим повторяться не буду. Подключаем библиотеку следующим образом: в командной строке:
qmake -set QMAKEFEATURES /path/to/QWT/features #путь до файла qwt.prf
в файле проекта:
CONFIG += qwt
SerialDeviceEnumerator
Библиотека QSerialDevice предоставляет интересную возможность — с помощью методов встроенного класса SerialDeviceEnumerator можно определить все присутствующие в системе последовательные порты, а также выудить информацию о каждом из подключенных устройств. Подробное описание класса присутствует в исходных кодах библиотеки, пример enumerator в папке с исходниками библиотеки хорошо иллюстрирует возможности его применения.
//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); }
где procEnumerate (QStringList) заполняет списком доступных com-портов экземпляр класса QComboBox расположенный на главной панели:
//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); }
Для того чтобы использовать все возможности класса SerialDeviceEnumerator нужно передать методу setDeviceName() имя интересующего порта в виде строки, присоединим к сигналу hasChanged(QStringList) еще один слот:
//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(); }
теперь в отладочную консоль выводятся сведения обо всех подключенных устройствах.
AbstractSerial
В то время как класс SerialDeviceEnumerator используется для обнаружения последовательных портов в системе и играет вспомогательную роль, основную функциональность библиотеки QSerialDevice несет класс Аbstract Serial.
//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(); }
В функции initSerial() создается экземпляр класса AbstractSerial, привязываются сигнал состояния signalStatus(QString,QDateTime) и сигнал readyRead() оповещающий о прибытии данных в com-порт к соответствующим слотам. При нажатии на кнопку Open на панели инструментов выполняется функция-слот procControlButtonClick() в которой объекту serial присваивается имя текущего порта в portBox открывается порт, дальше в отладочную консоль выводятся текущие параметры соединения, потом списки возможных значений параметров, а затем устанавливаются нужные параметры соединения.
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; } }
С этого момента по появлению сигнала readyRead() управление передается функции-слоту procSerialDataReceive(), в которой собственно можно и организовать обработку данных. На данный момент функция выводит считываемые данные в текстовое окно вывода на базе элемента textEdit:
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)); }
Функция-слот procSerialMessages(const QString &msg, QDateTime dt) выводит статусные сообщения по срабатыванию на сигнал signalStatus(QString,QDateTime):
void MainWindow::procSerialMessages(const QString &msg, QDateTime dt) { QString s = dt.time().toString() + " > " + msg; textEdit->appendPlainText(s); }
Обработка и вывод данных. Кратко о Qwt.
Итак, как уже стало понятно, основное действо по обработке данных должно происходить в функции-слоте procSerialDataReceive(). Тут возможны варианты в зависимости от того, выводятся данные в реальном времени или же сначала сбор данных, потом — вывод. Если Вам достаточно второго, отсылаю к уже упомянутой статье. В рамках моей задачи необходимо снять сигналы с четырех каналов АЦП микроконтроллера и отобразить их изменения во времени на двух графиках в виде четырех кривых. Здесь рассмотрим простейший случай — одноканальный АЦП, соответственно: один график — одна кривая. Как известно в основе представления данных библиотекой Qwt лежит класс QwtPlot — само полотно графика, для отображения кривой используется класс QwtPlotCurve, для аккумуляции точек кривой — класс QwtArraySeriesData, для прорисовки кривой в реальном времени поточечно — класс QwtPlotDirectPainter, QwrSystemClock — для отсчета времени. Итак, в функции-слоте procSerialDataReceive() класса MainWindow формируем точку и добавляем ее к кривой и в файл при помощи методов appendPoint(QPointF point) и RecToFile(QPointF point), при этом предполагаем, что МК выдает данные в формате «Ch_номер_канала = число» в перемежку с тестовыми сообщениями.
//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; } }
Естественно, в зависимости от того в какой форме микроконтроллер выдает данные в com-порт зависит и содержимое фильтра данных в функции procSerialDataReceive(), смело подстраивайте его под собственные нужды.
Библиотека Qwt — очень мощный инструмент вывода данных, содержит в себе целый набор виджетов для создания интерфейсов типа QwtWheel, кроме того позволяет легко организовать печать, навигацию и зум графиков, так что внимательно смотрите документацию и используйте полностью весь функционал библиотеки.
Заключение
Вот таким образом при помощи библиотек QSerialDevice и Qwt собирается простой(сложный) монитор com-порта, легко переносимый с одной платформу на другую. По мне — так учень удобно и быстро! Надеюсь, этой статья еще раз напомнила Вам о всей мощи и величии Qt и поможет в полной мере вкусить все ее прелести при реализации собственных проектов! Удачи!