📜 ⬆️ ⬇️

Qt + MVP + QThread. Build your bike

image
Good day everyone!

Recently, I faced a rather interesting task, to make the user interface for communicating with one piece of iron. Communication with it was carried out through the COM port. Since the work was assumed with a real-time system (this is the piece of hardware) in the Windows operating system, in order for the GUI not to slow down, it was decided to put the work with the COM port into a separate thread. Since the requirements for the system itself were constantly changing, in order to minimize and speed up patches, it was also decided to write using the MVP design pattern. Qt was chosen as the development environment. Just because I like this environment and its capabilities. This is how the Qt + MVP + Qthread bundle turned out. Who cares what came of it all and for what rake I went, I ask under the cat.

Planning


We have a certain device with which we want to communicate through the COM port. Moreover, we complicate the task a little, we want to communicate both with the help of commands entered from the keyboard, “ala-console”, and in automatic mode, when requests to the device are sent on a timer. We also need to take a response from the device, process it and display it on the form. All work on receiving and sending messages to the COM port should be performed in a separate thread. Plus, it should be possible to send messages to a device connected to a computer from different places in the program. And it is desirable to still have the opportunity to write tests (hello TDD).

On Habré already was an article about a bunch of Qt + MVP [1] . There were also several articles about using threads in Qt [2] , [3] . In short, the essence of MVP is that the application logic is separated from its appearance, which allows testing this logic separately from the rest of the program, using Unit tests, used in other applications, and also making changes faster to meet changing requirements .
')
A little about terms:
View - is responsible for the appearance of the program, it will be our form.
Model - is responsible for the logic of the program, our messages will be sent from it to the COM port, and the received reply packet will be understood in it.
Presenter is the link between View and Model.
Briefly on this all, in more detail you can read here and here . It's time to get down to the code.
We will write a small application (link to the finished project at the end) and in the course of writing we will implement what is in the article title.

Model


I like to start writing programs from logic, not from the interface.
So, we start the work by writing the ModelComPort class.
To begin, we implement the sending of messages to the COM port.

Our class should:
  1. Automatically detect available COM ports in the system.
  2. Connect to the specified COM port at the specified speed.
  3. Send messages to the COM port.
  4. Decrypt the received message from the COM port.

Here is what it will look like:
ModelComPort.h
class ModelComPort { public: ModelComPort(); ~ModelComPort(); //   COM- void connectToComPort(); //   void setPortName(QString portName); QString getPortName() const; //   void setBaudrate(int baudrate); int getBaudrate() const; //   COM- QList<QString> getListNamePorts() const; //    bool isConnect() const; //   COM- void onCommand(QString command); //    COM- void response(QByteArray msg); private: //   COM-   void searchComPorts(); //     void sendCommand(int command); private: bool m_connected; //     COM- QString m_portName; //  COM- QList<QString> m_listPorts; //  COM-   //   int m_baudrate; int m_dataBits; int m_parity; int m_stopBits; int m_flowControl; QByteArray m_inBuf; //   ComPortThread thread; //      }; 


As you can see, for those properties that are subject to change, we set get and set methods. Do not pay attention to the object of type ComPortThread, it will be described below.

I will not completely bring up the ModelComPort.cpp file, I’ll only focus on some of the nuances:
Constructor
 ModelComPort::ModelComPort() : m_portName(""), m_baudrate(QSerialPort::Baud9600), m_dataBits(QSerialPort::Data8), m_parity(QSerialPort::NoParity), m_stopBits(QSerialPort::OneStop), m_flowControl(QSerialPort::NoFlowControl), m_connected(false) { searchComPorts(); } 


As you can see, in the constructor, I immediately configure the default communication parameters, as well as determine which COM ports are installed in the system. I list the names of all available COM ports into an array. Let me explain why this is done. The fact is that our form, on which we will continue to display connection parameters, does not know anything about the available COM ports in the system, this is not in its competence. But, since we have to give the user a choice of which particular COM port to connect to, then in the future we will transfer this list to our form.

The method for determining the available COM ports is quite simple:

 void ModelComPort::searchComPorts() { foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { m_listPorts.append(info.portName()); } } 

Next, we consider the method by which we create a connection:

connectToComPort
 void ModelComPort::connectToComPort() { if (!m_connected) { if (m_portName == "") { return; } if (!thread->isRunning()) { thread.connectCom(m_portName, m_baudrate, m_dataBits, m_dataBits, m_stopBits, m_flowControl); thread.wait(500); //     if (thread.isConnect()) { m_connected = true; } } } else { if (thread.isConnect()) { thread.disconnectCom(); } m_connected = false; } } 


It's simple. First we determine whether we already have a connection or not. This is done so that when you click on the same button, the user can connect to the port and disconnect from it. That is, for example, when loading we are disabled. The first click on the connect button connects us, the second click on the connect button disconnects us. And so in a circle.
Next, we determine whether we know the name of the COM port to which we are connecting. Then we look, whether the flow which will carry out work with port is started at us. If the stream is not running, then create it, run it and it is already connected to the COM port. Here, probably, it is worth staying in more detail. The fact is that in order to work with a COM port in a separate stream, this thread must create a connection during its operation. Therefore, we in the ModelComPort class do not create the connection ourselves, but tell the stream that we want to create a connection and pass it the parameters with which we would like to connect.
Next, we give the stream time to create a connection and check whether it was possible to create it. If all is well, set the flag that we are connected.

Finally, we have methods by which we can establish or get the current connection settings, as well as get the current status of our connection.
The code is very simple
 void ModelComPort::setPortName(QString portName) { m_portName = portName; } QString ModelComPort::getPortName() const { return m_portName; } void ModelComPort::setBaudrate(int baudrate) { m_baudrate = baudrate; } int ModelComPort::getBaudrate() const { return m_baudrate; } bool ModelComPort::isConnect() const { return m_connected; } 


Since one of the conditions was that commands can be sent either automatically by timer and from the console, then we need a method that will receive a text command from the console, decrypt it and send it to the COM port.
The input method receives a string from the console and sends the corresponding command:
 void ModelComPort::onCommand(QString command) { if (command == "On") { sendommand(ON); } else if (command == "Off") { sendommand(OFF); } .... } 

All commands we will have is in a separate file and have a three-digit code:
 Enum Commands { ON = 101, OFF = 102 .... } 

Well, the sendCommand method will form a packet and give it to the stream for sending:
sendCommand
 void ModelComPort::sendCommand(int command) { QByteArray buffer; quint8 checkSumm = 0; buffer[0] = '#'; buffer[1] = '<'; buffer[2] = 0; checkSumm ^= buffer[2]; buffer[3] = command; checkSumm ^= buffer[3]; buffer[4] = checkSumm; thread.transaction(buffer, 250); } 


The number 250 in the string thread.transaction (buffer, 250); This is the wait time in ms to send our package. If during this time the package could not be sent, we assume that we have no connection with the device and display an error.
We have everything with the ModelComPort class, now we are going to create the PresenterComPort class.

Presenter


As mentioned earlier, we have Presenter as an intermediary between View and Model. That is, it has a dual function. On the one hand, this class must respond to all user actions performed with the GUI. On the other hand, it should provide synchronization of all our View and Model. That is, if we have several View, the general information displayed on them should be the same. This is, firstly, and secondly, the data entered on our (our) View must be synchronized with the data with which our Model works.
So, look at our Presenter.
PresenterComPort
 class PresenterComPort : public QObject { Q_OBJECT public: explicit PresenterComPort(QObject *parent = 0); ~PresenterComPort(); void appendView(IViewComPort *view); private slots: //   Com- void processConnect(); //   Com- void processNameComPortChanged(QString portName); //   Com- void processBaudratePortChanged(int baudrate); //    COM- void onCommand(QString command); //    COM- void response(const QByteArray& msg); private: void refreshView() const; void refreshView(IViewComPort *view) const; void setPortInfo() const; void setPortInfo(IViewComPort *view) const; private: ModelComPort *m_model; QList<IViewComPort*> m_viewList; ComPortThread thread; }; 


As you can see, there is only one public method here, it is used to bind our (our) View to the Presenter. In order for us to work with any View as a single object, all our View must be inherited from one interface. In this case, I use one View and inherit it from the IViewComPort interface. Details of this implementation can be found here [1] . Let's take a closer look at the appendView () method.
appendView
 void PresenterComPort::appendView(IViewComPort *view) { //       if (m_viewList.contains(view)) { return; } m_viewList.append(view); QObject *view_obj = dynamic_cast<QObject*>(view); //   COM- QObject::connect(view_obj, SIGNAL(processConnect()), this, SLOT(processConnect())); //   COM- QObject::connect(view_obj, SIGNAL(processNameComPortChanged(QString)), this, SLOT(processNameComPortChanged(QString))); //    QObject::connect(view_obj, SIGNAL(processBaudratePortChanged(int)), this, SLOT(processBaudratePortChanged(int))); //    COM- QObject::connect(view_obj, SIGNAL(onCommand(QString)), this, SLOT(onCommand(QString))); refreshView(view); setPortInfo(view); } 


In it, the transmitted View is recorded in the list, and our Presenter connects to the signals that can come from this View. This is done just so that our Presenter knows about all the changes on the form.

I will not talk about all the methods, the code there is not complicated, I’ll dwell on an example of only one method that sets connection parameters to our (our) View. As I said above, our form does not know what COM ports are in the system, but the user needs to display this information before connecting. All this makes the method
setPortInfo
 void PresenterComPort::setPortInfo(IViewComPort *view) const { //   COM-   QList<QString> tempList = m_model->getListNamePorts(); //     COM-   for (int i = 0; i < tempList.count(); i++) { view->addPortName(tempList.at(i)); } //       view->addBaudrate(9600); view->addBaudrate(34800); view->addBaudrate(115200); } 


As you can see from it, we request from our Model a list of all COM ports, and then enter this information on our form.
I fixed the possible connection speeds rigidly, in my work I mostly use the 9600, but just in case I added a couple more.
The rest of the code can be viewed in the project laid out at the end of the article, but then, so, it has already stretched strongly, and there is still a lot to discuss.

View


On the form, we will have 2 comboBox for setting the connection settings, one button that will be responsible for connecting / disconnecting to the COM port. We will also have a console in which we will write commands. And there will be another LED that will display the current connection status. If we are connected will turn green.

The final form of the form can be seen below.
image

The code of the form of special interest does not represent, we simply send signals when the selected item is changed in each of the ComboBox, the signal when the connection button is pressed, and also emit a signal if Enter is pressed in the console.
All these signals are intercepted by our Presenter, and transmits data to our Model for further processing.

It's time to move on to implementing our stream, which will be responsible for working with the COM port.
There are several opinions on how to better organize work with threads in Qt. Someone creates a stream and puts data into it, someone inherits from Qthread and overrides the run () method. Each method has its advantages and disadvantages. In this case, we will follow the second path, inheriting from Qthread.

ComPortThread


So, consider our ComPortThread class:
ComPortThread.h
 class ComPortThread : public QThread { Q_OBJECT public: ComPortThread(QObject *parent = 0); ~ComPortThread(); //    COM- void transaction(const QByteArray& request, int waitTimeout); //   COM- void connectCom(QString namePort, int baudRate, int m_dataBits, int m_parity, int m_stopBits, int m_flowControl); //   COM- void disconnectCom(); //    bool isConnect(); signals: //   void responseMsg(const QByteArray &s); //    COM- void error(const QString &s); //     void timeout(const QString &s); protected: //   void run(); private: int m_waitTimeout; //        COM- QMutex mutex; QWaitCondition cond; //  COM- QString m_portName; int m_baudrate; int m_dataBits; int m_parity; int m_stopBits; int m_flowControl; //   COM- QByteArray m_request; //   bool m_isConnect; //  bool m_isDisconnecting; //   bool m_isConnecting; //   bool m_isQuit; //     }; 


As you can see, in it we have connection settings with a COM port that will be transferred to us from the Model, the current state of the COM port (connected or not) and the stage (connection / disconnection).

We proceed to the implementation.
Constructor
 ComPortThread::ComPortThread(QObject *parent) : QThread(parent), m_waitTimeout(0), m_isQuit(false), m_isConnect(false), m_isDisconnecting(false), m_isConnecting(false) { } 


Here I think nothing new for those who have ever worked with synchronous streams, who are not familiar with them, I advise you to consult the Qt documentation.
Go to the connection method:
connectCom
 void ComPortThread::connectCom(QString namePort, int baudRate, int dataBits, int parity, int stopBits, int flowControl) { mutex.lock(); m_portName = namePort; m_baudrate = baudRate; m_dataBits = dataBits; m_parity = parity; m_stopBits = stopBits; m_flowControl = flowControl; mutex.unlock(); //     -   if (!isRunning()) { m_isConnecting = true; start(); m_isQuit = false; } else { //   ,   cond.wakeOne(); } } 


As you can see, here we do not create the connection as such, here we just check if we have a workflow, if not, we create a new flow and set the intention flag that we want to create a connection. Disconnecting from the COM port is the same expose the intention that we want to disconnect. All work that will be done by the stream will be in the run () method, which we will override.
disconnectCom
 void ComPortThread::disconnectCom() { mutex.lock(); m_isDisconnecting = true; mutex.unlock(); cond.wakeOne(); } 


Please note that before changing the variables of the flow, you need to block the flow, and after that you must unlock it. Well, it is advisable to wake him up if you want your changes to take effect immediately.

We turn to the main method in which all the useful work is accomplished.
run
 void ComPortThread::run() { QSerialPort serial; //   COM- QString currentPortName = m_portName; //    int currentWaitTimeout = m_waitTimeout; // ,   COM- QByteArray currentRequest = m_request; while (!m_isQuit) { //     COM- if (m_isConnecting) { //   COM- serial.setPortName(currentPortName); //  COM- if (serial.open(QIODevice::ReadWrite)) { //   if ((serial.setBaudRate(m_baudrate) && serial.setDataBits((QSerialPort::DataBits)m_dataBits) && serial.setParity((QSerialPort::Parity)m_parity) && serial.setStopBits((QSerialPort::StopBits)m_stopBits) && serial.setFlowControl((QSerialPort::FlowControl)m_flowControl))) { m_isConnect = true; m_isConnecting = false; } else { m_isConnect = false; m_isConnecting = false; emit error(tr("Can't open %1, error code %2") .arg(m_portName) .arg(serial.error())); return; } } else { m_isConnect = false; m_isConnecting = false; emit error(tr("Can't open %1, error code %2") .arg(m_portName) .arg(serial.error())); return; } } else if (m_isDisconnecting) { serial.close(); m_isDisconnecting = false; m_request.clear(); m_isQuit = true; } else { //   COM-  if (!currentRequest.isEmpty()) { serial.write(currentRequest); //     if (serial.waitForBytesWritten(m_waitTimeout)) { //      if (serial.waitForReadyRead(currentWaitTimeout)) { //   QByteArray responseFromPort = serial.readAll(); while (serial.waitForReadyRead(10)) { responseFromPort += serial.readAll(); } //    ,    emit responseMsg(responseFromPort); } else { //      emit timeout(tr("Wait read response timeout %1") .arg(QTime::currentTime().toString())); } } else { //       emit timeout(tr("Wait write request timeout %1") .arg(QTime::currentTime().toString())); } //    currentRequest.clear(); } else { mutex.lock(); //     cond.wait(&mutex); currentWaitTimeout = m_waitTimeout; currentRequest = m_request; mutex.unlock(); } } } } 


First of all, we create local variables, in which we will enter information that may change during the work of the stream. Next, we go into an infinite loop in which our stream will spin until we set the flag that we want to exit.
Turning in the stream, we watch the flags and according to them we make certain actions. A sort of state machine. That is, if there is a flag that means that we want to connect to the COM port, we connect, reset this flag and fall asleep until another command follows. Further, if a command arrives to send a message to the COM port, the thread wakes up, takes the message to be transmitted, and then tries to transmit it within the specified time. If the transfer failed, then the stream sends a signal to which any external object can subscribe and thus find out that the transfer failed.
If the transfer is successful, the stream waits for a response for the specified time. If the answer does not come, the thread issues a signal to which any object can again subscribe, so we can find out that our piece of hardware is not responding and deal with it already.
If the answer is received, then the stream again emits a signal that the data is ready, it can be taken and processed.

Rake


In general, in words it sounds quite easy, but there are nuances. The fact is that our Model cannot receive signals. That is, the package came to us, but Model does not know about it. On the other hand, Presenter can receive signals (since it is inherited from Qobject), but Presenter does not have access to a stream that works with a COM port. There are two possible solutions (maybe more, who knows, write in the comments), the first option is to work with the stream in the Presenter. It seemed to me that it was not a very good idea, because then we would have to carry out the work on packing / unpacking messages to Presenter too, that is, we will have part of the program logic not in our Model, but in Presenter. I dropped the idea. The second option is to make the class ComPortThread Singleton. And our Presenter should subscribe to its signals, and all processing should be carried out in the Model. For this, the ComPortThread class needs to be slightly redone:
ComPortThread
 class ComPortThread : public QThread { Q_OBJECT public: static ComPortThread* getInstance() { static QMutex mutex; if (!m_instance) { mutex.lock(); if (!m_instance) { m_instance = new ComPortThread; } m_refCount++; mutex.unlock(); } return m_instance; } void run(); void transaction(const QByteArray& request, int waitTimeout); void connectCom(QString namePort, int baudRate, int m_dataBits, int m_parity, int m_stopBits, int m_flowControl); void disconnectCom(); bool isConnect(); void free(); signals: void responseMsg(const QByteArray &s); void error(const QString &s); void timeout(const QString &s); private: ComPortThread(QObject *parent = 0); ~ComPortThread(); ComPortThread(const ComPortThread&); ComPortThread& operator=(const ComPortThread&); private: int m_waitTimeout; QMutex mutex; QWaitCondition cond; QString m_portName; int m_baudrate; int m_dataBits; int m_parity; int m_stopBits; int m_flowControl; QByteArray m_request; bool m_isConnect; bool m_isDisconnecting; bool m_isConnecting; bool m_isQuit; static ComPortThread* m_instance; static int m_refCount; }; 


Hiding constructors and destructor from external access, we implement the method of getting links, and in the ModelComPort and PresenterComPort classes we add the line to the constructors:
 thread = ComPortThread::getInstance(); 


Do not forget to add lines to the destructors of these classes:
 if (thread) { thread->free(); thread = 0; } 

The free () method of the thread object counts references to itself, and as soon as there are zero of them, it will allow its deletion. This is done to protect against deletion of an object to which hanging links are possible. Accordingly, in all classes where we used the ComPortThread object, we change the object declaration to the pointer declaration and work with the stream through the pointer. More details can be found in the source.

Well, finally we put everything together, the main.cpp file
main.cpp
 int main(int argc, char *argv[]) { QApplication a(argc, argv); CopterGUI w = new CopterGUI; PresenterComPort *presenterComPort = new PresenterComPort(); presenterComPort->appendView(&w); w.show(); return a.exec(); } 



Conclusion


Well, in general, that's all.

Interested in your feedback, suggestions, criticism.

The volume was quite large, I apologize, I wanted to highlight as many implementation details as possible, if there were any questions left, I will try to answer in the comments. About errors, please write in a personal.

From myself I want to note that this implementation does not pretend to be absolutely correct, it is rather a description of one of the options, how to do it. This article is the first for me, so please do not kick much. The project code, as promised, can be found here .

PS Corrected some minor notes on the code given by the respected kulinich and semlanik in the comments.

PPS In the course of the discussion, thanks to the links Part 1 and Part 2 , which the respected Singerofthefall gave, it turned out that the approach to working with streams described in the article may not always meet the expectations of the programmer and work quite differently than expected.
It is safer to use the Qobject.moveToThread () method for working with threads, as advised in the first comments.

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


All Articles