ブロック・レシーバー

QSerialPort の同期 API を非 GUI スレッドで使用する方法を示します。

Blocking ReceiverはQSerialPort の同期APIを使用して、非GUIスレッドでシリアル・インターフェース用のアプリケーションを作成する方法を示します。

QSerialPort は、2 つの一般的なプログラミング・アプローチをサポートしています:

  • 非同期(ノンブロッキング)アプローチ。操作はスケジュールされ、コントロールがQtのイベントループに戻ったときに実行されます。QSerialPort 、操作が終了するとシグナルが出力されます。例えば、QSerialPort::write ()はすぐに戻ります。データがシリアル・ポートに送信されると、QSerialPortbytesWritten() を発信します。
  • 同期(ブロッキング)アプローチ。非GUIアプリケーションやマルチスレッド・アプリケーションでは、waitFor...() 関数を呼び出す(つまり、QSerialPort::waitForReadyRead())ことで、操作が完了するまで呼び出し元のスレッドを一時停止させることができます。

この例では、同期アプローチを示します。ターミナルの例では、非同期のアプローチを示します。

この例の目的は、ユーザーインターフェイスの応答性を損なうことなく、シリアルプログラミングコードを簡素化するために使用できるパターンを示すことです。Qtのブロッキングシリアル・プログラミングAPIを使用することで、コードがシンプルになることがよくありますが、ブロッキング動作のため、ユーザーインターフェイスがフリーズするのを防ぐために、非GUIスレッドでのみ使用する必要があります。しかし、多くの人が考えているのとは逆に、QThread でスレッドを使用しても、必ずしもアプリケーションに手に負えないほどの複雑さが加わるわけではありません。

このアプリケーションはReceiverであり、SenderアプリケーションのBlocking Senderの例と対になって動作を示します。

Receiverアプリケーションは、Senderアプリケーションからシリアルポートを介してリクエストを受信し、レスポンスを送信します。

まず、ReceiverThreadクラスから始めます。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 、Senderからのリクエストを受け取るためのAPIを提供するサブクラスで、レスポンスを送ったりエラーを報告するためのシグナルを持っています。

startReceiver()を呼び出してReceiverアプリケーションを起動する必要があります。このメソッドは、ReceiverThreadにシリアルインターフェースの設定と起動に必要なパラメータを転送します。ReceiverThreadがSenderから何らかのリクエストを受信すると、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::wakeOnerun()関数については後述する。

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は一度に1つの起動しか処理できません。

QSerialPort 、ループに入る前にrun()関数でスタック上にオブジェクトを構築する:

    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();

警告 WaitForReadyRead() メソッドは、Qt のイベント・ループの代わりにすべての I/O ルーチンを処理するため、ブロッキング・アプローチの各 read() 呼び出しの前に使用する必要があります。

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);

警告 ブロッキング・アプローチでは、waitForBytesWritten() メソッドを各 write() 呼び出しの後に使用する必要があります。

データ書き込み時にエラーが発生すると、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 からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳しくは、Building and Running an Example を参照してください。

サンプルプロジェクト @ code.qt.io

Serial TerminalBlocking Senderも参照してください

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。