В продолжение своей статьи «Простой электронный самописец» хочу поделится опытом создания терминала для сообщения с разработанным мной девайсом на основе библиотек 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 и поможет в полной мере вкусить все ее прелести при реализации собственных проектов! Удачи!
Автор: granik