Arduino

Надёжность связи: переподключение, тайм-ауты, контроль целостности протокола

Введение

Любое реальное устройство, подключённое через физический кабель, может быть случайно отсоединено, испытать электрические помехи, искажающие передаваемые данные, или просто временно «зависнуть» по аппаратной причине. В заключительной перед итоговым проектом статье разберём, как сделать связь между Qt-приложением и Arduino устойчивой к таким сбоям — автоматическое переподключение, обнаружение «зависшего» устройства через тайм-ауты, и базовая проверка целостности данных.

Концепция

Надёжное соединение строится на нескольких независимых механизмах: периодической проверке живости соединения через регулярный обмен heartbeat-сообщениями (PING/PONG из статьи 282, но теперь используемый систематически, а не только вручную), обнаружении физического отключения устройства через сигналы ошибок QSerialPort и попытке автоматического переподключения при появлении устройства снова, и тайм-ауте ожидания ответа — если ожидаемый ответ на команду или периодическое сообщение датчика не приходит в течение разумного времени, соединение считается подозрительным и инициируется проверка/переподключение.

Пример кода

// reliableArduinoConnection.h — надстройка над ArduinoConnection с переподключением и heartbeat
#pragma once
#include <QObject>
#include <QTimer>
#include <QSerialPortInfo>
#include "arduinoconnection.h"

class ReliableArduinoConnection : public QObject
{
    Q_OBJECT
public:
    explicit ReliableArduinoConnection(QObject *parent = nullptr) : QObject(parent)
    {
        connect(&m_connection, &ArduinoConnection::lineReceived, this, &ReliableArduinoConnection::onLineReceived);

        // Heartbeat — регулярная проверка живости соединения
        connect(&m_heartbeatTimer, &QTimer::timeout, this, &ReliableArduinoConnection::sendHeartbeat);
        m_heartbeatTimer.setInterval(3000);

        // Тайм-аут — если PONG не пришёл в ответ на PING за разумное время, соединение подозрительно
        connect(&m_heartbeatTimeoutTimer, &QTimer::timeout, this, &ReliableArduinoConnection::onHeartbeatTimeout);
        m_heartbeatTimeoutTimer.setSingleShot(true);

        // Периодическая попытка найти и подключиться к устройству, если соединение не активно
        connect(&m_reconnectTimer, &QTimer::timeout, this, &ReliableArduinoConnection::tryConnect);
        m_reconnectTimer.start(2000);
    }

    ArduinoConnection &rawConnection() { return m_connection; }

signals:
    void connectionStateChanged(bool connected);

private slots:
    void tryConnect()
    {
        if (m_isConnected) return;

        const QString port = findArduinoPort();
        if (port.isEmpty()) return;

        if (m_connection.connectToPort(port)) {
            m_isConnected = true;
            m_heartbeatTimer.start();
            emit connectionStateChanged(true);
            qDebug() << "Соединение установлено на порту" << port;
        }
    }

    void sendHeartbeat()
    {
        m_connection.sendCommand("PING");
        m_heartbeatTimeoutTimer.start(2000); // ожидаем PONG в течение 2 секунд
    }

    void onHeartbeatTimeout()
    {
        qWarning() << "Устройство не ответило на PING — считаем соединение потерянным";
        handleDisconnection();
    }

    void onLineReceived(const QString &line)
    {
        if (line == "PONG") {
            m_heartbeatTimeoutTimer.stop(); // живой ответ получен, тайм-аут не нужен
        }
    }

    void handleDisconnection()
    {
        m_isConnected = false;
        m_heartbeatTimer.stop();
        m_heartbeatTimeoutTimer.stop();
        m_connection.disconnect(); // закрытие порта на Qt-стороне
        emit connectionStateChanged(false);
        // m_reconnectTimer продолжает работать в фоне и попытается переподключиться автоматически
    }

private:
    static QString findArduinoPort()
    {
        const auto ports = QSerialPortInfo::availablePorts();
        for (const auto &portInfo : ports) {
            if (portInfo.description().contains("Arduino", Qt::CaseInsensitive) ||
                portInfo.manufacturer().contains("Arduino", Qt::CaseInsensitive)) {
                return portInfo.portName();
            }
        }
        return QString();
    }

    ArduinoConnection m_connection;
    QTimer m_heartbeatTimer;
    QTimer m_heartbeatTimeoutTimer;
    QTimer m_reconnectTimer;
    bool m_isConnected = false;
};

Пояснения к коду

ReliableArduinoConnection объединяет три независимых таймера, каждый отвечающий за свой аспект надёжности: m_heartbeatTimer периодически отправляет PING, m_heartbeatTimeoutTimer запускается сразу после отправки PING и, если не остановлен своевременным получением PONG (что происходит в onLineReceived), считает соединение потерянным, и m_reconnectTimer работает постоянно в фоне, пытаясь найти и подключиться к устройству каждые несколько секунд, но реально выполняя подключение только если в данный момент m_isConnected ложно — это позволяет одной и той же логике переподключения сработать как при первом запуске приложения (устройство ещё не подключалось), так и после обнаруженного разрыва связи.

handleDisconnection() централизует логику обработки потери соединения — останавливает heartbeat-таймеры (поскольку нет смысла продолжать пинговать заведомо недоступное устройство), закрывает порт, и сигнализирует остальной части приложения об изменении состояния, позволяя интерфейсу, например, показать индикатор «Соединение потеряно» — при этом m_reconnectTimer продолжает работать, и при следующем срабатывании автоматически попробует найти и заново подключиться к устройству, если оно к этому моменту снова окажется доступным (переподключен кабель, либо плата перезагрузилась после временного сбоя).

Подводные камни

  • Слишком частые heartbeat-запросы (короткий интервал m_heartbeatTimer) создают избыточный, постоянный трафик через последовательный порт, конкурирующий с реальными данными датчиков за то же физическое соединение — интервал heartbeat должен быть выбран как компромисс между быстротой обнаружения проблемы соединения и избыточной нагрузкой на канал связи; для большинства практических случаев интервал в несколько секунд (как в примере) разумен.
  • Попытка переподключения сразу после обнаружения отключения без какой-либо задержки может создать цикл быстрых, безуспешных попыток подключения, если устройство физически остаётся недоступным продолжительное время (например, провод действительно отсоединён, а не временный сбой) — фиксированный интервал m_reconnectTimer в примере (2 секунды) предотвращает это, но для более устойчивой реализации можно рассмотреть экспоненциальное увеличение интервала между попытками при многократных неудачах подряд, аналогично паттернам переподключения из основного цикла статей про сеть.
  • Отсутствие различения между «устройство физически отсутствует» и «устройство есть, но не отвечает корректно» (например, неправильная версия прошивки, не поддерживающая команду PING) — оба случая в примере обрабатываются одинаково (через тайм-аут), но для более информативной диагностики, особенно на этапе разработки и тестирования, может быть полезно различать эти ситуации и сообщать пользователю более конкретную причину проблемы.
  • Гонка между обработкой обычных данных датчиков и heartbeat-логикой, если, например, PONG от предыдущего heartbeat-цикла приходит с заметной задержкой и обрабатывается уже после того, как был запущен следующий цикл PING/тайм-аут — для протокола с единственным, простым heartbeat без явного порядкового номера запроса такая путаница обычно не критична (поскольку любой полученный PONG просто останавливает текущий активный тайм-аут), но для более сложных протоколов с несколькими параллельными запросами-ответами стоит явно вводить идентификаторы запросов для надёжного сопоставления ответов с конкретными запросами.