수신자 차단하기

비-GUI 스레드에서 QSerialPort 의 동기식 API를 사용하는 방법을 보여줍니다.

블로킹 수신기에서는 비-GUI 스레드에서 QSerialPort 의 동기식 API를 사용하여 직렬 인터페이스용 애플리케이션을 만드는 방법을 보여줍니다.

QSerialPort 는 두 가지 일반적인 프로그래밍 접근 방식을 지원합니다:

  • 비동기(비차단) 접근 방식. 제어가 Qt의 이벤트 루프로 돌아갈 때 작업이 예약되고 수행됩니다. QSerialPort 는 작업이 완료되면 신호를 방출합니다. 예를 들어 QSerialPort::write()는 즉시 반환됩니다. 데이터가 직렬 포트로 전송되면 QSerialPortbytesWritten()을 반환합니다.
  • 동기식(블로킹) 접근 방식. 비GUI 및 멀티스레드 애플리케이션에서는 waitFor...() 함수를 호출(예: 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는 Sender의 요청을 수신하기 위한 API를 제공하는 QThread 서브클래스로, 응답을 전달하고 오류를 보고하기 위한 시그널을 가지고 있습니다.

수신기 애플리케이션을 시작하려면 startReceiver()를 호출해야 합니다. 이 메서드는 시리얼 인터페이스 구성 및 시작을 위한 원하는 파라미터를 ReceiverThread로 전송합니다. 수신자 스레드가 전송자로부터 요청을 받으면 요청() 신호를 내보냅니다. 오류가 발생하면 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는 한 번에 하나의 시작만 처리할 수 있습니다.

QSerialPort 객체는 루프가 들어가기 전에 run() 함수에 스택에 생성합니다:

    QSerialPort serial;

    while (!m_quit) {

이렇게 하면 루프를 실행하는 동안 객체를 한 번 생성할 수 있으며, 객체의 모든 메서드가 실행() 스레드의 컨텍스트에서 실행된다는 의미이기도 합니다.

루프에서 현재 시작을 위한 직렬 포트의 이름이 변경되었는지 여부를 확인합니다. 변경된 경우 직렬 포트를 다시 열고 재구성합니다.

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

경고: Qt 이벤트 루프 대신 모든 I/O 루틴을 처리하기 때문에 블로킹 접근 방식에서는 각 read() 호출 전에 waitForReadyRead() 메서드를 사용해야 합니다.

데이터를 읽을 때 오류가 발생하면 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);

경고: Qt 이벤트 루프 대신 모든 I/O 루틴을 처리하기 때문에 블로킹 접근 방식에서는 각 write() 호출 후에 waitForBytesWritten() 메서드를 사용해야 합니다.

데이터를 쓸 때 에러가 발생하면 timeout() 신호가 발생합니다.

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

쓰기에 성공하면 전송자 애플리케이션으로부터 받은 데이터가 포함된 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 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

예제 프로젝트 @ code.qt.io

직렬 터미널발신자 차단도참조하세요 .

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.