Использование библиотек QSerialDevice и QWT для организации коммуникации с контроллером и отображения данных

в 21:14, , рубрики: arduino, avr, com-порт, diy или сделай сам, qt, Qt Software, QWT, Программинг микроконтроллеров, метки: , , , ,

Использование библиотек QSerialDevice и QWT для организации коммуникации с контроллером и отображения данных

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

Специально для этой статьи, в целях лучшей иллюстрации возможностей библиотек (в особенности 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js