QSerialPort with no GUI, no thread: QObject::startTimer: Timers can only be used with threads started with QThread
Asked Answered
F

1

0

I'm doing a DLL with no GUI (TEMPLATE = lib), using QSerialPort. I don't create threads and I don't need any: I have no GUI and having a blocking serial port operation is no problem, it is what I want.

When doing:

while (!serial_uart->isWritable());
while (!serial_uart->write(frame));

I get:

QObject::startTimer: Timers can only be used with threads started with QThread

Question: how to use QSerialPort in a library without GUI without triggering this error?

Note: I first thought the problem was coming from serial_uart->waitForReadyRead(timeout) but even without this and only serial_uart->write() I already have this problem.


Minimal reproducible DLL example:

test.cpp

#include "test.h"
extern "C" {
    __declspec(dllexport) Test* new_Test() { return new Test(); }
    __declspec(dllexport) void DoTest(Test *t) { t->DoTest(); }
}
Test::Test() :QObject()
{
    qDebug("Hello");
}
void Test::DoTest()
{
    this->serialport = new QSerialPort();
    this->serialport ->setPortName("COM12");
    this->serialport->setBaudRate(QSerialPort::Baud19200);
    this->serialport->open(QIODevice::ReadWrite);
    while (!this->serialport->isWritable());
    while (!this->serialport->write("hello"));
}

test.h

#include <QSerialPort>
class Test : public QObject
{
    Q_OBJECT
public:
    Test();
    void DoTest();
    QSerialPort *serialport;
};

test.pro

TEMPLATE = lib
TARGET = test
QT += serialport
INCLUDEPATH += .
HEADERS += test.h
SOURCES += test.cpp

When I call the release/test.dll from Python I have this:

from ctypes import *
dll = CDLL(r"release\test.dll")
dll.new_Test.restype = c_void_p
dll.new_Test.argtypes = []
dll.DoTest.restype = None
dll.DoTest.argtypes = [c_void_p]
t = dll.new_Test()
dll.DoTest(t)

Hello

QObject::startTimer: Timers can only be used with threads started with QThread

Feriga answered 29/6, 2022 at 9:17 Comment(2)
Check die documentation if QIODevice: doc.qt.io/qt-6/qiodevice.html. The waitFor... functions are supposed to be used without an event loop, or in a separate threadWillowwillowy
@Willowwillowy Yes, you're right, but surprisingly, this error message doesn't happen when I call waitFor..., it happens just when I call serial_uart->write().Feriga
J
1

Most of the QIODevice based classes (like Qt sockets or serial port) want to live in a Qt based thread and also their functions needs to be called from the same thread where the object was created.

For that reason I've usually solved this by:

  1. Create wrapper class (QObject based with Q_OBJECT macro for signal/slot functionality) for the QIODevice based class you are about to use. For each function you are planning on using create a slot function on your wrapper class which then calls the equivalent funtion in the QIODevice:

     qint64 MySerialPort::write(const QByteArray &data)
     {
         // m_serialPort is created with new QSerialPort in constructor of MySerialPort.
         return m_serialPort->write(data);
     }
    
  2. Create a QThread class that in its run function creates an instance of MySerialPort (with new MySerialPort) and just calls exec() after that. Now MySerialPort lives in an event loop and is able to send and receive signals/slots.

     void MySerialPortThread::run()
     {
         m_serialPort = new MySerialPort();
         exec();
         delete m_serialPort; // Automatic deletion after thread is stopped.
     }
    

The thread could also return a pointer to the instance for easier access from outside to connect signals and slots.

MySerialPort* MySerialPortThread::serialPort()
{
    return m_serialPort; // Instance of  MySerialPort class
}
  1. In your main code create signals that match the slots of the MySerialPort and connect them.

     signals:
         qint64 writeSerial(const QByteArray& data);
    
     void MyMainClass::connectSignalsAndSlots()
     {
         MySerialPort* serialPort = m_serialThread->serialPort();
         connect(this, &MyMainClass::writeSerial, serialPort, &MySerialPort::write, Qt::BlockingQueuedConnection); // Use either QueuedConnection or BlockingQueuedConnection to force the execution of the slot to the MySerialThread. 
     }
    
  2. Emit the signals to access the QSerialPort.

     emit writeSerial(dataByteArray);
    
Jigsaw answered 30/6, 2022 at 7:3 Comment(4)
Thank you for your answer. Wow this is complicated, if we really have to do this, this means QSerialPort is not really made for non-GUI apps... Having to do all of this defeats the goal of using Qt, then it's even better to call Winapi directly with CreateFile, GetCommState, SetCommState, SetCommTimeouts, WriteFile. Isn't there an easier way to work with QSerialPort without having to artificially create an event loop or QThread?Feriga
I agree that it can become a bit more complicated at least when dealing with multithreaded apps. In case of single-threaded apps it doesn't necessarily need all these shenaningans but what it probably still needs is a Qt based thread to live it. If you are creating a normal Qt based application which is using QApplication/QCoreApplication then its should also work from the main thread but it has its caveats which can be solved with the aforementioned QThread + wrapper functionality.Jigsaw
I've made a template thread class (just like MySerialPortThread but just with template class instead of hardcoded) to provide a generic threaded option for QObject based classes in case they need one. I'm using that for QSerialPort and QUDPSocket/QTCPSocket in my projects.Jigsaw
I guess the main benefit of using QSerialPort (instead of WinAPI) is the crossplatform support it provides. If you don't need crossplatform compilation then probably using WinAPI is simpler solution for any QIODevice based class usage.Jigsaw

© 2022 - 2024 — McMap. All rights reserved.