阻塞接收器

展示了如何在非 GUI 线程中使用 QSerialPort 的同步 API。

阻塞接收器 展示了如何使用在非 GUI 线程中 QSerialPort 的同步 API 创建用于串行接口的应用程序。

QSerialPort 支持两种通用的编程方法

  • 异步(非阻塞)方法。 操作会在将控制权返回 Qt 的事件循环时被安排和执行。在操作完成后,QSerialPort 会发出一个信号。例如,QSerialPort::write() 将立即返回。当数据发送到串行端口时,QSerialPort 会发出 bytesWritten()。
  • 同步(阻塞)方法。 在非 GUI 和多线程应用程序中,可以调用(即 QSerialPort::waitForReadyRead())来挂起调用线程,直到操作完成。

在这个示例中,演示了同步方法。终端 示例说明了异步方法。

这个示例的目的在于展示一种你可以用来简化你的串行编程代码,同时又不牺牲用户界面响应性的模式。使用 Qt 的阻塞串行编程 API 常常会导致代码更加简洁,但由于它的阻塞行为,它应该只在非 GUI 线程中使用,以避免用户界面冻结。但与许多人认为的相反,在应用程序中使用线程(例如 QThread)并不一定会给你的应用程序添加难以管理的复杂性。

这个应用程序是一个接收器,演示了与发送应用程序 阻塞发送器示例 配合工作的功能。

接收器应用程序从发送应用程序通过串行端口接收请求,并向其发送响应。

我们将从处理串行编程代码的 ReceiverThread 类开始。

class ReceiverThread : public QThread
{
    Q_OBJECT

public:
    explicit ReceiverThread(QObject *parent = nullptr);
    ~ReceiverThread();

    void startReceiver(const QString &portName, int waitTimeout, const QString &response);

signals:
    void request(const QString &s);
    void error(const QString &s);
    void timeout(const QString &s);

private:
    void run() override;

    QString m_portName;
    QString m_response;
    int m_waitTimeout = 0;
    QMutex m_mutex;
    bool m_quit = false;
};

ReceiverThread 是 QThread 的子类,它提供了从发送器接收请求的 API,并具有发送响应和报告错误的信号。

你应该调用 startReceiver() 来启动接收器应用程序。此方法将所需的参数传递给 ReceiverThread 以配置和启动串行接口。当 ReceiverThread 接收到来自发送器的任何请求时,将发出 request() 信号。如果发生任何错误,将发出 error() 或 timeout() 信号。

值得注意的是,startReceiver() 是从主,GUI 线程中调用的,但响应数据和其他参数将从一个线程访问 ReceiverThread 的线程。同时从不同的线程读取和写入 ReceiverThread 的数据成员是合理的,因此建议使用 QMutex 来同步访问。

void ReceiverThread::startReceiver(const QString &portName, int waitTimeout, const QString &response)
{
    const QMutexLocker locker(&m_mutex);
    m_portName = portName;
    m_waitTimeout = waitTimeout;
    m_response = response;
    if (!isRunning())
        start();
}

startReceiver() 函数将串行端口名称、超时和响应数据存储起来,并使用 QMutexLocker 锁定互斥锁以保护这些数据。然后我们启动线程,除非它已经在运行。稍后将讨论 QWaitCondition::wakeOne()。

void ReceiverThread::run()
{
    bool currentPortNameChanged = false;

    m_mutex.lock();
    QString currentPortName;
    if (currentPortName != m_portName) {
        currentPortName = m_portName;
        currentPortNameChanged = true;
    }

    int currentWaitTimeout = m_waitTimeout;
    QString currentRespone = m_response;
    m_mutex.unlock();

在 run() 函数中,首先获取互斥锁,从成员数据中获取串行端口名称、超时时间和响应数据,然后再释放锁。在任何情况下,都不应该同时调用方法 startReceiver() 和读取这些数据的过程。由于 QString 是可重入但非线程安全的,因此不建议从一个启动读取串行端口名称,然后调用另一个的超时或响应数据。ReceiverThread 每次只能处理一个启动。

我们在进入循环之前,在 run() 函数的栈中构造了一个 QSerialPort 对象。

    QSerialPort serial;

    while (!m_quit) {

这使得我们只需要创建一次对象,同时运行循环,这也意味着对象的全部方法都将运行在 run() 线程的上下文中。

在循环中,检查当前启用的串行端口名称是否发生变化。如果已更改,则重新打开和重新配置串行端口。

        if (currentPortName.isEmpty()) {
            emit error(tr("Port not set"));
            return;
        } else 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;
            }
        }

        if (serial.waitForReadyRead(currentWaitTimeout)) {

循环将继续等待请求数据。

            // read request
            QByteArray requestData = serial.readAll();
            while (serial.waitForReadyRead(10))
                requestData += serial.readAll();

警告:对于阻塞方法,应在每个 read() 调用之前使用 waitForReadyRead() 方法,因为它处理所有的 I/O 例程,而不是 Qt 事件循环。

如果在读取数据时发生错误,将发出 timeout() 信号。

        } else {
            emit timeout(tr("Wait read request timeout %1")
                         .arg(QTime::currentTime().toString()));
        }

成功读取后,尝试发送响应并等待传输完成。

            // write response
            const QByteArray responseData = currentRespone.toUtf8();
            serial.write(responseData);
            if (serial.waitForBytesWritten(m_waitTimeout)) {
                const QString request = QString::fromUtf8(requestData);
                emit this->request(request);

警告:对于阻塞方法,在每个 write() 调用之后应使用 waitForBytesWritten() 方法,因为它处理所有的 I/O 例程,而不是 Qt 事件循环。

如果在写入数据时发生错误,将发出 timeout() 信号。

            } else {
                emit timeout(tr("Wait write response timeout %1")
                             .arg(QTime::currentTime().toString()));
            }

成功写入后,发出包含来自 Sender 应用程序数据的 request() 信号。

                emit this->request(request);

接下来,线程切换到读取串行接口的当前参数,因为它们可能已经更新,然后从循环的开始处重新运行。

        m_mutex.lock();
        if (currentPortName != m_portName) {
            currentPortName = m_portName;
            currentPortNameChanged = true;
        } else {
            currentPortNameChanged = false;
        }
        currentWaitTimeout = m_waitTimeout;
        currentRespone = m_response;
        m_mutex.unlock();
    }

运行示例

要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。更多信息,请访问 构建和运行示例

示例项目 @ code.qt.io

另请参阅 串行终端阻塞发送器

© 2024 Qt 公司有限版权公司。此处包含的文档贡献归各自所有者所有。本提供的文档是根据自由软件基金会发布的 GNU 自由文档许可版本 1.3 的条款进行许可的。Qt 及相关标识是芬兰和/或全球其他国家的 Qt 公司的商标。所有其他商标均为其各自所有者的财产。