QT serial sending and receiving messages to external device
-
Hello all. I am working on QT application that will be used for testing external microcontroller devices using serial commands.
Lets say I have a device that will respond to certain serial commands. If response is as expected, that means the device is OK, if not, that means device is faulty and should be put aside for repairs.
I have a structure:
struct testing_s{ bool waiting_for_response; QString command; QString expected_response; }; testing_s test_struct;
The structure above contains a flag (waiting_for_response) that allows me to know if I have received a response or not.
Initially when the program starts, this is set to 0 but when I send a command, I set it to 1 which means I will be waiting for the response.In my QT application, I am listening for any incoming serial data from the external device using
connect(&serial_local->serial_connection, &QSerialPort::readyRead, this, &TestTool::Command_parser);
When I want to send some command and expect certain response, I use the following command:
void TestTool::Send_and_wait(QString command,QString expected_response){ test_struct.command = command; test_struct.expected_response = expected_response; serial_local->serial_connection.write((command.toUtf8())); // send serial data test_struct.waiting_for_response = 1; }
The command above will set the test_struct with the required information and send serial data to the device, then in my command_parser I am parsing all incoming data and filtering for my expected response:
void TestTool::Command_parser() { while(serial_local->is_data_available(&serial_local->serial_connection)){ QByteArray line = serial_local->read_data(&serial_local->serial_connection); QString DataAsString = QString(line); if(test_struct.waiting_for_response == 1){ qDebug("response received = %s \n",DataAsString.toStdString().c_str()); qDebug("expected response = %s \n",test_struct.expected_response.toStdString().c_str()); if(strncmp(DataAsString.toStdString().c_str(),test_struct.expected_response.toStdString().c_str(),test_struct.expected_response.length()) == 0){ qDebug("response matched, ready to send new command \n"); test_struct.waiting_for_response = 0; } } } }
If expected response is received, I am ready to send another command, if it is not received within certain time (lets say 5 seconds, then I should stop the testing and assume the device is faulty).
In my QT application, I have a button "Start test". Once this button is pressed, I want to testing to start. I have tested this with only 1 command and it seemed to work. I send "ping" to the device and my device responded "pong".
void TestTool::on_start_test_button_clicked(){ qDebug("on_start_test_button_clicked \n"); Send_and_wait("ping\n","pong"); }
The application looks like:
When the expected response is received, I will mark the radio button so the user can see at which stage the test is at any time.
My question
I am not fully understanding how can I ensure that more than 1 command is send one by one after I click START_TEST button. I have tried the following:
void TestTool::on_start_test_button_clicked(){ while(1){ if(test_struct.waiting_for_response == 0){ // only allow sending command if we are currently not waiting for any respose Send_and_wait("ping\n","pong\n"); } else{ QThread::msleep(100); } if(test_struct.waiting_for_response == 0){ // only allow sending command if we are currently not waiting for any respose Send_and_wait("test_nvs\n","OK\n"); } else{ QThread::msleep(100); } if(test_struct.waiting_for_response == 0){ // only allow sending command if we are currently not waiting for any respose Send_and_wait("ping\n","pong\n"); } else{ QThread::msleep(100); } } }
The function of the above is quite simple. When the button is clicked, I enter a testing loop. I check if I am already waiting for the response, if not I send the command and wait for the response. If the response is received, move to another command, if it is not received wait for 100ms.
Unfortunately, the above logic does not work since I have infinite while loop which causes the rest of my application to hang. I would like to get some advice on how this can be achieved. Thank you in advance.
Keep in mind that I have provided example with only 3 commands. The real tests that I will he doing might have 100 or so commands so I want to be able to scale up after I get this to work with simple 3 command example.
I have made a very simple flowchart on how things should be working:
-
@lukutis222
If you want the UI to remain responsive after you click the "Start test" button you will not want awhile(1)
loop in it. Nor do I know what you expect to happen with yourQThread::msleep()
calls, which will also block the thread.One way to " ensure that more than 1 command is send one by one after I click START_TEST button" is to send the first command and have a slot connected to a signal emitted when the command finishes. Then the slot can move onto the next test in the sequence and repeat this.
If you have a "large" number of commands and your nice flowchart, you might consider using Qt's QStateMachine Class as the neatest way of implementing it.
-
Thanks for a very detailed outline of what you are attempting.
Two major differences with how I would approach this:
- Use Qt asynchronous processing rather than trying to force synchronous behaviour. This is the key to responsiveness.
- Maintain a list of pending commands. Process the first until completion (good or bad) then move it to a completed list (or maintain an index to the current command). Repeat until list empty/finished.
The lists allow for arbitrary test count and probably map better to a list view for your GUI.
Something roughly like this untested brain dump:
#ifndef TESTRUNNER_H #define TESTRUNNER_H #include <QByteArray> #include <QObject> #include <QSerialPort> struct TestCase { public: TestCase(const QString &command, const QString &expect); QString m_command; QString m_expect; QString m_actual; // what we actually got int m_failed; }; class TestRunner : public QObject { Q_OBJECT public: explicit TestRunner(QObject *parent = nullptr); void run(const QList<TestCase> &tests); signals: void finished(); private: void initSerial(); private slots: void sendCommand(); void dataReceived(); private: QSerialPort m_serial; QList<TestCase> m_queue; QByteArray m_receiveBuffer; }; #endif // TESTRUNNER_H
#include "testrunner.h" #include <QTimer> TestCase::TestCase(const QString &command, const QString &expect): m_command(command), m_expect(expect), m_actual(), m_failed(0) {} TestRunner::TestRunner(QObject *parent) : QObject{parent} { initSerial(); } void TestRunner::initSerial() { // setup comm port etc. connect(&m_serial, &QSerialPort::readyRead, this, &TestRunner::dataReceived); } void TestRunner::sendCommand() { if (m_queue.count() > 0) { // send the command at the top of the list m_receiveBuffer.clear(); m_serial.write(m_queue.at(0).m_command.toUtf8()); } else emit finished(); } void TestRunner::run(const QList<TestCase> &tests) { m_queue = tests; sendCommand(); } void TestRunner::dataReceived() { m_receiveBuffer.append(m_serial.readAll()); if (int index = m_receiveBuffer.indexOf('\n') != -1) { // we have accumulated a complete response m_queue.first().m_actual = QString(m_receiveBuffer.left(index-1)); if (m_queue.at(0).m_actual == m_queue.at(0).m_expect) { // success, do something with that here m_queue.removeFirst(); } else { // failure, do something with that here m_queue.first().m_failed++; if ( m_queue.at(0).m_failed >= 5) { // maximum fail count m_queue.removeFirst(); } } // Try the first command in the queue again after a short pause. QTimer::singleShot(100, this, &TestRunner::sendCommand); } }
-
Thank you both for good suggestions. That put me on a right track :)