Qt + MVP + QThread. Build your bike

image
Good day to all!

Recently, I faced a rather interesting task, to create a user interface for communication with one piece of iron. Communication with her was carried out through the COM port. Since the work was supposed to be with a real-time system (this 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 stream. Since the requirements for the system itself were constantly changing, in order to minimize and speed up corrections, it was also decided to write using the MVP design pattern. Qt was chosen as the development environment. Just because I like this medium and its features. And so it turned out a bunch of Qt + MVP + Qthread. Who cares what came of it all and what kind of rake I went to, I ask for a cat.

Planning


We have a certain device with which we want to communicate through the COM port. Moreover, we will complicate the task a bit, we want to communicate both with the help of commands entered from the keyboard, “ala-console”, and in automatic mode when requests are sent to the device by timer. We also need to accept the 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 of the program. And it is also desirable to be able to write tests (hi TDD).

There was already an article on Habré about Qt + MVP bunch [1] . There have also been 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 you to test this logic separately from the rest of the program using Unit tests, use it in other applications, and also make changes faster to suit changing requirements .

A little about the terms:
View - is responsible for the appearance of the program, this will be our form.
Model - is responsible for the logic of the program, from it our messages will be sent to the COM port, and the incoming response packet will be analyzed in it.
Presenter is the link between View and Model.
Briefly on this, you can read in more detail here and here . It's time to get down to the code.
We will write a small application (a link to the finished project at the end) and, as we write, we will implement what is placed in the title of the article.

Model


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

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

Here's 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 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 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 the get and set methods. For now, do not pay attention to an object such as ComPortThread, it will be described below.

I won’t cite the ModelComPort.cpp file, I will dwell only on some 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 designer, I immediately configure the default communication parameters, and also determine which COM ports are installed on the system. The names of all the available COM ports I put in an array. Let me explain why this is done. The fact is that our form, on which we will display connection parameters in the future, does not know anything about the available COM ports in the system, it is not in its competence. But, since we must give the user a choice, 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, 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;
    }
}


Everything is simple here. First, we determine if we already have a connection or not. This is done so that when you click on the same button, the user can both connect to the port and disconnect from it. That is, for example, when loading we are disconnected. The first press of the connect button connects us, the second press of 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 to see if we have started a thread that will work with the port. If the thread is not running, then create it, start it, and it is already connected to the COM port. Here, probably, it is worth stopping in more detail. The fact is that in order to work with the COM port in a separate thread, this same 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 thread time to create a connection and check if it was created. If all is well, set the flag that we are connected.

And finally, we have methods with which we can establish or obtain the current connection settings, as well as obtain the current state 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 both automatically by timer and from the console, 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 line from the console and sends the appropriate command:
void ModelComPort::onCommand(QString command)
{ 
    if (command == "On")
    {
        sendСommand(ON);
    }
    else if (command == "Off")
    {
        sendСommand(OFF);
    }
....
}

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

Well, the sendСommand 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 time in ms to send our package. If during this time the packet could not be sent, we assume that we have no communication with the device and display an error.
We have everything with the ModelComPort class, now we move on to creating the PresenterComPort class.

Presenter


As mentioned earlier, Presenter is an intermediate between View and Model. That is, it has a double function. On the one hand, this class should 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 that is entered on our (our) View must be synchronized with the data that our Model works with.
So take a 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 m_viewList;
    ComPortThread thread;
};


As you can see, there is only one public method, it is used to bind our (our) View to Presenter. In order for us to work with any View as a single object, all of our Views 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] . Consider the appendView () method in more detail.
appendView
void PresenterComPort::appendView(IViewComPort *view)
{
    // Проверяем наличие данного вида в списке
    if (m_viewList.contains(view))
    {
        return;
    }
    m_viewList.append(view);
    QObject *view_obj = dynamic_cast(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 listed, and our Presenter is connected to the signals that may come from this View. This is done just to let our Presenter know about all changes on the form.

I won’t talk about all the methods, the code is not complicated there, I will focus on an example of only one method that sets the connection parameters to our (our) View. As I said above, our form does not know which COM ports are available 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 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 we enter this information on our form.
I fixed the possible connection speeds rigidly, in my work I most often use 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, otherwise, it has already stretched so much, and much more has been discussed. Let's move on to our View.

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 also be an LED that will display the current status of the connection. If we are connected it will light green.

The final form view can be seen below.
image

The form code itself is not of particular interest, we just send signals when the selected item in each ComboBox changes, a signal when the connection button is pressed, and also emit a signal if Enter was pressed in the console.
All of these signals are intercepted by our Presenter, and passes the data to our Model for further processing.

It is time to proceed with the implementation of 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 thread and puts data into it, someone inherits from Qthread and redefines the run () method. Each method has its own advantages and disadvantages. In this case, we will go the second way, we will inherit from Qthread.

ComPortThread


So, let's look at 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 the connection settings with the COM port, which will be transmitted to us from the Model, the current status of the COM port (connected or not) and the stage (connection / disconnection).

We pass to 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 threads, 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 only check if we have a workflow, if not, then create a new thread and set the intent flag that we want to create a connection. With disconnection from the COM port, the same thing set the intention that we want to disconnect. All the work that will be done by the thread 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 flow variables, you need to block the stream, and then be sure to 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 useful work is performed.
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 them we will enter information that may change during the flow. Next, we go into an endless cycle in which our stream will spin until we set the flag that we want to exit.
Spinning in the stream, we look at the flags and according to them we perform certain actions. A sort of state machine. That is, if there is a flag meaning that we want to connect to the COM port, we connect, reset this flag and fall asleep until another command is issued. Further, if a command arrives to send a message to the COM port, the thread wakes up, takes the message that needs to be transmitted, and then tries to transmit it within the specified time. If it was not possible to transmit, then the stream sends a signal to which any external object can subscribe and thus find out that the transmission failed.
If the transmission was successful, the stream waits for a specified time for a response. If the answer does not come, the stream gives a signal, which again any object can subscribe to, so we can find out that our piece of iron 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, they can be taken and processed.

Rake


In general, in words it sounds pretty 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 the stream that works with the COM port. There are two solutions (maybe more, who knows, write in the comments), the first option is to work with the stream in Presenter. It seemed to me not a good idea, since then I would have to put the work on packing / unpacking messages to Presenter too, that is, part of the program logic will not be in our Model, but in Presenter. I threw this idea away. The second option is to make the class ComPortThread Singleton. And subscribe to his signals our Presenter, and conduct all processing in Model.
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;
};


We hide the designers and the destructor from external access, we implement the method of obtaining the link, 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 they become zero, it will allow its deletion. This is done to protect against the deletion of an object to which dangling links are possible. Accordingly, in all classes where we used the ComPortThread object, we change the declaration of the object to the declaration of the pointer 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, that's all.

Your feedback, suggestions, criticism are interesting.

The volume turned out to be quite large, I apologize, I wanted to highlight as many implementation details as possible, if there were any questions, I will try to answer in the comments. Please write about errors in PM.

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

PS Corrected small comments on the code given by respected kulinich and semlanik in the comments.

PPS In the discussion process, thanks to the linksPart 1 and Part 2 , which was given by the esteemed Singerofthefall, it turned out that the approach to working with streams described in the article may not always live up to the programmer's expectations and may not work at all as intended.
It is safer to use the Qobject.moveToThread () method for working with threads, as advised in the first comments.

Also popular now: