QThread + QSerialPort! Крутим в отдельном потоке работу с СOM портом.

В одном из своих проектов столкнулся с небольшой проблемкой: задача заключается в том, что нужно работать с тремя разными портами одновременно(если быть точнее с двумя, а при настройке и калибровке оборудования 3мя), а самая большая опа в том, что все они общаются по разным протоколам… да в придачу два (в некоторых случаях 3) устройство висит на RS485 с протоколом DCON, это первый порт! Одно устройство на RS232 с жутким самопальным протоколом внеочередной переделкой Wake. И настроечно-калибровочное подключается также по RS232-му, протокол неизвестен (занимаюсь расшифровкой)… Ну это все прелюдия и ОФФТОП…
В такой задаче ИМХО целесообразно крутить работу с каждым портом в отдельном потоке. Вот на этом месте и начались все проблемы… Проведя жуткоскучный и мучительный лит. поиск «ГУГЛ в помощь» результаты были весьма и весьма неоднозначны. Мои подзатыльники самому себе и хочу здесь привести на примере создания простейшей ГУЕвины терминалки с использованием QserialPort и Qthread. Надеюсь топик поможет многим, да и мне возможность получить дополнительных подзатыльников=)))

Постановка задачи: Рассмотреть возможные варианты реализации данной задачи и реализовать наиболее приемлемый и работоспособный. Привести пример простейшей терминалки. Получить подзатыльников и исправить свои ошибки (а их много).
Исходя из лит.анализа, основное: Поток должен быть независим, правильно унаследован. Все обращения к потоку и операции с потоком должны организовываться по сигнально-слотовой системе (никакого прямого обращения к методам в классе).
Краткие теоретические сведенья:
Кьют — бесплатный и кроссплатформенный инструмент для разработки ПО на C++.
QserialPort — дополнение к библиотеке Qt для работы с аппаратными и виртуальными СOM портами. Начиная с 5й версии Qt-а официально в него входит.
Qthread — какая-то жуткая магия для работы с потоками. Независимая задача, которая выполняется внутри процесса и разделяет вместе с ним общее адресное пространство, код и глобальные данные.
COM порт (RS232) — интерфейс с последовательной передачей данных стандарта RS232.
RS485 — стандарт физического уровня. Прикручивается к UART…
Цели: обобщить наработанные результаты и поделиться ими. Получить критический анализ и оценку от сообщества для повышения маны.
Моменты создания приложения в Qt пропускаю… Здесь же приведу код класса и то как его обьявлять.

Создаем новый класс:
файл.h

#ifndef PORT_H
#define PORT_H
#include <QObject>
#include <QtSerialPort/QserialPort>//Обьявляем работу с портом

struct Settings {//Структура с настройками порта
    QString name;
    qint32 baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::Parity parity;
    QSerialPort::StopBits stopBits;
    QSerialPort::FlowControl flowControl;
};
class Port : public QObject
{
    Q_OBJECT
public:
    explicit Port(QObject *parent = 0);
    ~Port();
    QSerialPort thisPort;
    Settings SettingsPort;
signals:
    void finished_Port(); //Сигнал закрытия класса
    void error_(QString err);//Сигнал ошибок порта
    void outPort(QString data); //Сигнал вывода полученных данных
public slots:
    void  DisconnectPort(); // Слот отключения порта
    void ConnectPort(void); // Слот подключения порта
    void Write_Settings_Port(QString name, int baudrate, int DataBits, int Parity, int StopBits, int FlowControl);// Слот занесение настроек порта в класс
    void process_Port(); //Тело
    void WriteToPort(QByteArray data); // Слот от правки данных в порт
private slots:
    void handleError(QSerialPort::SerialPortError error);//Слот обработки ощибок
    void ReadInPort(); //Слот чтения из порта по ReadyRead
public:
};
#endif // PORT_H


файл.с

#include "port.h"
#include <qdebug.h>
Port::Port(QObject *parent) :
    QObject(parent)
{
}
Port::~Port()
{
    qDebug("By in Thread!");
    emit finished_Port();//Сигнал о завершении работы
}
void Port :: process_Port(){//Выполняется при старте класса
    qDebug("Hello World in Thread!");
    connect(&thisPort,SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(handleError(QSerialPort::SerialPortError))); // подключаем проверку ошибок порта
    connect(&thisPort, SIGNAL(readyRead()),this,SLOT(ReadInPort()));//подключаем   чтение с порта по сигналу readyRead()
}
void Port :: Write_Settings_Port(QString name, int baudrate,int DataBits,
                         int Parity,int StopBits, int FlowControl){//заносим параметры порта в структуру данных
    SettingsPort.name = name;
    SettingsPort.baudRate = (QSerialPort::BaudRate) baudrate;
    SettingsPort.dataBits = (QSerialPort::DataBits) DataBits;
    SettingsPort.parity = (QSerialPort::Parity) Parity;
    SettingsPort.stopBits = (QSerialPort::StopBits) StopBits;
    SettingsPort.flowControl = (QSerialPort::FlowControl) FlowControl;
}
void Port :: ConnectPort(void){//процедура подключения
    thisPort.setPortName(SettingsPort.name);
    if (thisPort.open(QIODevice::ReadWrite)) {
        if (thisPort.setBaudRate(SettingsPort.baudRate)
                && thisPort.setDataBits(SettingsPort.dataBits)//DataBits
                && thisPort.setParity(SettingsPort.parity)
                && thisPort.setStopBits(SettingsPort.stopBits)
                && thisPort.setFlowControl(SettingsPort.flowControl))
        {
            if (thisPort.isOpen()){
                error_((SettingsPort.name+ " >> Открыт!\r").toLocal8Bit());
            }
        } else {
            thisPort.close();
            error_(thisPort.errorString().toLocal8Bit());
          }
    } else {
        thisPort.close();
        error_(thisPort.errorString().toLocal8Bit());
    }
}
void Port::handleError(QSerialPort::SerialPortError error)//проверка ошибок при работе
{
    if ( (thisPort.isOpen()) && (error == QSerialPort::ResourceError)) {
        error_(thisPort.errorString().toLocal8Bit());
        DisconnectPort();
    }
}//
void  Port::DisconnectPort(){//Отключаем порт
    if(thisPort.isOpen()){
        thisPort.close();
        error_(SettingsPort.name.toLocal8Bit() + " >> Закрыт!\r");
    }
}
void Port :: WriteToPort(QByteArray data){//Запись данных в порт
    if(thisPort.isOpen()){
        thisPort.write(data);
    }
}
//
void Port :: ReadInPort(){//Чтение данных из порта
    QByteArray data;
    data.append(thisPort.readAll());
    outPort(data);
    //((QString)(adr.toInt())).toLatin1().toHex()
}

Теперь необходимо правильно обьявить поток и подключить к нему наш новый класс (здесь находится вся магия):
QThread *thread_New = new QThread;//Создаем поток для порта платы
    Port *PortNew = new Port();//Создаем обьект по классу
    PortNew->moveToThread(thread_New);//помешаем класс  в поток
    PortNew->thisPort.moveToThread(thread_New);//Помещаем сам порт в поток

И соединяем все нужные нам сигналы и слоты:
connect(PortNew, SIGNAL(error_(QString)), this, SLOT(Print(QString)));//Лог ошибок
    connect(thread_New, SIGNAL(started()), PortNew, SLOT(process_Port()));//Переназначения метода run
    connect(PortNew, SIGNAL(finished_Port()), thread_New, SLOT(quit()));//Переназначение метода выход
    connect(thread_New, SIGNAL(finished()), PortNew, SLOT(deleteLater()));//Удалить к чертям поток
    connect(PortNew, SIGNAL(finished_Port()), thread_New, SLOT(deleteLater()));//Удалить к чертям поток
               connect(this,SIGNAL(savesettings(QString,int,int,int,int,int)),PortNew,SLOT(Write_Settings_Port(QString,int,int,int,int,int)));//Слот - ввод настроек!
    connect(ui->BtnConnect, SIGNAL(clicked()),PortNew,SLOT(ConnectPort()));//по нажатию кнопки подключить порт
    connect(ui->BtnDisconect, SIGNAL(clicked()),PortNew,SLOT(DisconnectPort()));//по нажатию кнопки отключить порт
    connect(PortNew, SIGNAL(outPort(QString)), this, SLOT(Print(QString)));//вывод в текстовое поле считанных данных
 connect(this,SIGNAL(writeData(QByteArray)),PortNew,SLOT(WriteToPort(QByteArray)));//отправка в порт данных

И остается самое главное! Запустить поток:
thread_New->start();

В результате получаем вот такое окошко. Единственное что отправленные и принятые данные отображаются в ASCII кодах

Прилеплен архивчик с исходниками!
  • +3
  • 16 февраля 2014, 19:27
  • kalik
  • 1
Файлы в топике: terminalka.zip

Комментарии (18)

RSS свернуть / развернуть
Писалось под линукс?
На кнопке надпись Serch лучше заменить на Search… Conect = Connect (Disconnect)
0
Да под линукс, но и на винде тоже должно работать. Не проверял… нету сейчас доступа к ней…
Правки внес, спасибо=)) то все мои глубокие знания англиского=))
0
RS485 — стандарт физического уровня. Прикручивается к RS232…
Обычно таки к UART. RS232 — стандарт физического уровня, аналогично RS485. Алсо, в одном месте ты обозвал его RS323.
0
  • avatar
  • Vga
  • 16 февраля 2014, 20:10
Спасибо, поправил
0
СТоит тогда поправить, что RS232 — это тоже физический уровень для UART'а. Хотя, ЕМНИП, он еще специфицирует дополнительные линии вроде RTS-CTS.
0
>>В такой задаче ИМХО целесообразно крутить работу с каждым портом в отдельном потоке. Вот на этом месте и начались все проблемы…

ИМХО тема не раскрыта — в чём проблемы-то были? О том, как создавать потоки в QT не писал только ленивый. В том числе, была куча дебатов, о том, как их писать правильно и неправильно — правда вот однозначных выводов из них сделано не было. У вас используется метод, продвигаемый в статье «QThread — You are doing it wrong», но почему именно так объяснений не дано. А вот я, решая аналогичную задачу и начитавшись ответной статьи, написал более привычным мне по wxWidgets методом — через наследование. А как НА САМОМ ДЕЛЕ правильно и не правильно — фиг разберёшься.
0
тема не раскрыта
Я как бы привел пример работоспособный, а не проводил анализ всех ошибок со способами их решения.
как создавать потоки в QT
Отчасти вы правы=)) Вот только никто не писал про всовывание в поток QSerialPort.
QThread — You are doing it wrong

Именно он! обьяснения 2: 1-проблемы с наследованием всего что в классе; 2 — мне он больше понравился.
начитавшись ответной статьи

Первоначально я сделал тоже как в ответной статье, но куча проблем выскочила, опять с тем же наследованием.
p, li { white-space: pre-wrap; }
QObject: Cannot create children for a parent that is in a different thread. (Parent is QSerialPort(0x912dd50), parent's thread is QThread(0x8f75728), current thread is QThread(0x912dc28) QObject: Cannot create children for a parent that is in a different thread. (Parent is QSerialPort(0x912dd50), parent's thread is QThread(0x8f75728), current thread is QThread(0x912dc28)

Вот самая моя большая головная боль!
А как НА САМОМ ДЕЛЕ

Вот тут Вы правы, черт разберешь=)) Поэтому правильно в данном случае — так как удобно именно Вам и чтобы все работало!
0
>>Первоначально я сделал тоже как в ответной статье, но куча проблем выскочила, опять с тем же наследованием.

Вооо! Вот именно этой информации и не хватает в статье — было бы неплохо дополнить. У меня, кстати, такой проблемы не возникало, потому что я не через QSerialPort работал, а через простую с-шную обёртку вокруг нативного API.

>>Поэтому правильно в данном случае — так как удобно именно Вам и чтобы все работало!

Ещё бы быть уверенным, в том что оно действительно работает, а не только делает вид и любой чих его может сломать…
0
Вот именно этой информации и не хватает в статье
Как чуть расгребусь с делами насущными… постараюсь поправить.
что оно действительно работает
проверяю на разных системах работоспособность кода, с разными устройствами и с использованием разных переходников USB <-> RS232, USB <-> RS485…
+ гонял несколько суток в очень тяжелых условиях (в цеху) со всеми положенными помехами: сварка, станки, лебедки, перепады напряжения и.т.д. вродибы не выскочило никаких критических ощибок…
0
Как подругому как проверить не придумал еще…
0
А зачем вообще отдельные потоки? У QSerialPort есть неблокирующий API, которого должно быть вполне достаточно.
0
  • avatar
  • Thorn
  • 18 февраля 2014, 08:59
Его то достаточно, но куча сбоев при этом? + нужно какую-то службу писать или чегото там… чтобы все в свое нужное время и в нужной очередности по портам…
А если одновременно в 3 порта придут данные? Как там оно реализовано в QSerialPоrt мне не известно… и что в этот момент случится с программой спрогнозировать тоже тяжело… Так что лучше по отдельности.
Предложите Ваш вариант, как бы вы сделали все в одном потоке?
Предыдушее ПО было извояно (хорошо что не мной) на Delphi и все работало в общем потоке… Вот только как оно работало и можно ли назвать это работой или постоянным подвисанием… я хз.
0
>>Предложите Ваш вариант, как бы вы сделали все в одном потоке?

Если бы действительно по каким-то причинам надо было сделать на одном потоке, я бы делал через select(2).
0
Тоже вариант хороший… но мутный=)) Как Вы писали:
работает, а не только делает вид
+ после меня тоже придется кому-то ковыряться в тех кодах… охота не икать приэтом=))
0
Работа на сигналах надёжнее. Уже раза 3 убеждался.
0
  • avatar
  • dekar
  • 19 февраля 2014, 00:11
Взаимно=)) Сам набил шишки уже=))
0
Объявление структуры настроек порта Settings, лучше перенести а public секцию самого класса Port, а объявление экземпляра структуры (Settings SettingsPort;) в pivate секцию.
Передавать настройки порту можно через указатель на структуру типа Port::SettingsPort.

Port::SettingsPort newSetting;
...
connect(this,SIGNAL(savesettings(Port::SettingsPort*)),PortNew,SLOT(Write_Settings_Port(Port::SettingsPort*)));//Слот - ввод настроек!вод настроек!
...
0
На десятке пишет Device is not open
Не могу понять в чем проблема…
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.