发送器阻止
演示如何在工作线程中使用QSerialPort的同步API。
发送器阻止演示如何在工作线程中使用QSerialPort的同步API创建串行接口应用程序。
QSerialPort支持两种编程方案
- 异步(非阻塞)方案。操作在控制权返回到Qt事件循环时被安排和执行。当操作完成时,QSerialPort类会发射一个信号。例如,write()方法立即返回。当数据发送到串行端口时,QSerialPort类会发射bytesWritten()信号。
- 同步(阻塞)方案。在无头和多线程应用程序中,可以调用等待方法(在这种情况下,waitForReadyRead)来挂起调用线程,直到操作完成。
在此示例中,演示了同步方案。终端示例说明了异步方案。
本示例的目的是演示如何简化您的串行编程代码,同时不丢失用户界面的响应性。阻塞串行编程API通常会导致代码更简单,但应仅在非GUI线程中执行,以保持用户界面的响应性。
该应用程序是发送器,演示与接收器应用程序阻塞接收器示例配对的操作。
发送器应用程序通过串行端口向接收器应用程序发起传输请求,并等待响应。
class SenderThread : public QThread { Q_OBJECT public: explicit SenderThread(QObject *parent = nullptr); ~SenderThread(); void transaction(const QString &portName, int waitTimeout, const QString &request); signals: void response(const QString &s); void error(const QString &s); void timeout(const QString &s); private: void run() override; QString m_portName; QString m_request; int m_waitTimeout = 0; QMutex m_mutex; QWaitCondition m_cond; bool m_quit = false; };
SenderThread是QThread的子类,提供用于安排接收器请求的API。此类提供响应和错误报告的信号。可以通过调用transaction()方法启动新的发送器事务并传入所需的请求来启动新的发送器事务。结果通过response()信号提供。如有问题,则发射error()或timeout()信号。
注意,transaction()方法在主线程中调用,但请求是在SenderThread线程中提供。SenderThread的数据成员在多个线程中并发读取和写入,因此使用QMutex类来同步访问。
void SenderThread::transaction(const QString &portName, int waitTimeout, const QString &request) { const QMutexLocker locker(&m_mutex); m_portName = portName; m_waitTimeout = waitTimeout; m_request = request; if (!isRunning()) start(); else m_cond.wakeOne(); }
transaction()方法存储串行端口名称、超时和请求数据。可以使用QMutexLocker锁定互斥锁以保护这些数据。然后可以启动线程,除非它已经运行。稍后讨论wakeOne()方法。
void SenderThread::run() { bool currentPortNameChanged = false; m_mutex.lock(); QString currentPortName; if (currentPortName != m_portName) { currentPortName = m_portName; currentPortNameChanged = true; } int currentWaitTimeout = m_waitTimeout; QString currentRequest = m_request; m_mutex.unlock();
在run()函数中,首先锁定QMutex对象,然后使用成员数据提取串行端口名称、超时和请求数据。完成后,释放QMutex锁定。
在任何情况下,都不应同时调用 transaction()
方法与一个获取数据的进程。注意,虽然 QString 类是可重入的,但它不是线程安全的。因此,不建议在请求线程中读取串口号,并在另一个线程中超时或请求数据。SenderThread 类一次只能处理一个请求。
QSerialPort 对象在进入循环之前都在 run() 方法中构建在栈上
QSerialPort serial; if (currentPortName.isEmpty()) { emit error(tr("No port name specified")); return; } while (!m_quit) {
这使得可以在运行循环时创建对象。这也意味着所有对象方法都是在 run() 方法的范围内执行。
在循环内部检查当前事务的串口号是否已更改。如果已更改,则重新打开串口并进行重新配置。
if (currentPortNameChanged) { serial.close(); serial.setPortName(currentPortName); if (!serial.open(QIODevice::ReadWrite)) { emit error(tr("Can't open %1, error code %2") .arg(m_portName).arg(serial.error())); return; } }
循环将继续请求数据,写入串口,并等待所有数据传输完成。
// write request const QByteArray requestData = currentRequest.toUtf8(); serial.write(requestData); if (serial.waitForBytesWritten(m_waitTimeout)) {
警告:对于阻塞传输,应在每次调用 waitForBytesWritten() 方法和每次 write 方法后使用。这将处理所有 I/O 操作,而不是 Qt 事件循环。
在传输数据时发生超时错误时,将发出 timeout() 信号。
} else { emit timeout(tr("Wait write request timeout %1") .arg(QTime::currentTime().toString())); }
在成功请求后存在等待响应的期间,然后再次读取。
// read response if (serial.waitForReadyRead(currentWaitTimeout)) { QByteArray responseData = serial.readAll(); while (serial.waitForReadyRead(10)) responseData += serial.readAll(); const QString response = QString::fromUtf8(responseData); emit this->response(response);
警告:对于阻塞的替代方案,应在每次调用每个 read() 方法前使用 waitForReadyRead() 方法。这将处理所有 I/O 操作,而不是 Qt 事件循环。
在接收到数据时如果发生超时错误,将发出 timeout() 信号。
} else { emit timeout(tr("Wait read response timeout %1") .arg(QTime::currentTime().toString())); }
当事务成功完成时,response() 信号包含来自接收应用的数据。
emit this->response(response);
随后,线程将休眠,直到出现下一次事务。线程在醒来时通过使用成员读取新数据,并从开始处运行循环。
m_mutex.lock(); m_cond.wait(&m_mutex); if (currentPortName != m_portName) { currentPortName = m_portName; currentPortNameChanged = true; } else { currentPortNameChanged = false; } currentWaitTimeout = m_waitTimeout; currentRequest = m_request; m_mutex.unlock(); }
运行示例
要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。有关更多信息,请访问 构建和运行示例。
© 2024 The Qt Company Ltd. 包含在此的文档贡献来自各自的所有权。提供的文档根据 Free Software Foundation 发布的《GNU 自由文档许可证》版本 1.3 的条款获得许可。Qt 及其相关标志是芬兰和/或其他国家的 The Qt Company Ltd. 的商标。所有其他商标均为其各自所有者的财产。