显示如何在工作线程中使用QSerialPort的同步API。
Blocking Sender显示如何在工作线程中使用QSerialPort的同步API为串行接口创建应用程序。
QSerialPort支持两种编程替代方案:
异步(非阻塞)替代方案:当控制返回到Qt事件循环时,安排并执行操作。QSerialPort类在操作完成时发出一个信号。例如,write()方法会立即返回。当数据被发送到串行端口时,QSerialPort类会发出bytesWritten()信号。
同步(阻塞)替代方案。在GUI和多线程应用程序中,可以调用wait方法(在本例中为waitForReadyRead())来挂起调用线程,直到操作完成。
在本例中,演示了同步替代方案。终端示例说明了异步替代方案。
此示例的目的是演示如何在不损失用户界面响应性的情况下简化串行编程代码。阻塞串行编程API通常会导致更简单的代码,但它只能用于非GUI线程,以保持用户界面的响应性。
此应用程序是演示与接收器应用程序Blocking receiver示例配对的工作的发送器。
发送器应用程序通过串行端口向接收器应用程序发起传输请求,并等待响应。
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()方法。这将处理所有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();
}