ブロック送信者

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

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

QSerialPort は2つのプログラミング方法をサポートしています:

  • 非同期(ノンブロッキング)。操作はスケジュールされ、制御が Qt イベントループに戻ったときに実行されます。操作が終了すると、QSerialPort クラスがシグナルを発します。例えば、write ()メソッドはすぐに戻ります。データがシリアル・ポートに送信されると、QSerialPort クラスはbytesWritten() シグナルを発します。
  • 同期(ブロッキング)の選択肢。ヘッドレス・アプリケーションやマルチスレッド・アプリケーションでは、waitメソッド(この例ではwaitForReadyRead())を呼び出すことで、処理が完了するまで呼び出し元のスレッドを一時停止させることができます。

この例では、同期型の代替方法を示します。Terminal の例では、非同期の方法を示します。

この例の目的は、ユーザーインターフェースの応答性を損なうことなく、シリアルプログラミングコードを簡素化する方法を示すことです。ブロッキング・シリアル・プログラミング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クラスは一度に1つのリクエストしか処理できません。

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

警告 ブロッキング転送に関しては、write メソッド呼び出しの後に、waitForBytesWritten () メソッドを使用すること。これにより、Qtイベントループの代わりにすべてのI/Oルーチンが処理されます。

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

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

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 からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細は、Building and Running an Example を参照してください。

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

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

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