Arduino

Множество одновременных датчиков: протокол с разделением каналов

Введение

Реальные проекты часто включают несколько разных датчиков одновременно (температура, влажность, расстояние, освещённость) — нужен протокол, позволяющий Qt-стороне надёжно различать, какое значение от какого датчика пришло, и масштабируемая архитектура парсинга на стороне Qt, не превращающаяся в нечитаемый набор разрозненных if-проверок при добавлении каждого нового датчика.

Концепция

Расширим текстовый протокол явным префиксом канала для каждого типа данных (DHT:, DISTANCE:, LIGHT: и так далее, что уже частично использовалось в предыдущих статьях) и реализуем на стороне Qt диспетчер сообщений — единый компонент, разбирающий префикс каждой входящей строки и направляющий оставшиеся данные соответствующему обработчику конкретного типа датчика, вместо разбора всех типов сообщений в одном большом, монолитном методе.

Пример кода

// firmware/src/main.cpp — несколько датчиков с явными префиксами каналов
#include <DHT.h>

#define DHT_PIN 2
#define TRIG_PIN 9
#define ECHO_PIN 10
#define LIGHT_SENSOR_PIN A0

DHT dht(DHT_PIN, DHT22);

void setup()
{
    Serial.begin(9600);
    dht.begin();
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    delay(100);
    Serial.println("READY");
}

void loop()
{
    static unsigned long lastDhtTime = 0, lastDistanceTime = 0, lastLightTime = 0;

    // Каждый датчик опрашивается со своей собственной, физически разумной частотой
    if (millis() - lastDhtTime >= 2000) {
        sendDhtReading();
        lastDhtTime = millis();
    }
    if (millis() - lastDistanceTime >= 200) {
        sendDistanceReading();
        lastDistanceTime = millis();
    }
    if (millis() - lastLightTime >= 500) {
        sendLightReading();
        lastLightTime = millis();
    }
}

void sendDhtReading()
{
    const float t = dht.readTemperature(), h = dht.readHumidity();
    if (!isnan(t) && !isnan(h)) {
        Serial.print("DHT:"); Serial.print(t); Serial.print(","); Serial.println(h);
    }
}

void sendDistanceReading()
{
    digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
    digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);
    const unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000);
    if (duration > 0) {
        Serial.print("DISTANCE:"); Serial.println((duration * 0.0343) / 2.0);
    }
}

void sendLightReading()
{
    const int lightValue = analogRead(LIGHT_SENSOR_PIN);
    Serial.print("LIGHT:"); Serial.println(lightValue);
}
// messagedispatcher.h — диспетчер сообщений на стороне Qt, масштабируемый на любое число каналов
#pragma once
#include <QObject>
#include <QHash>
#include <QString>
#include <functional>
#include "arduinoconnection.h"

class MessageDispatcher : public QObject
{
    Q_OBJECT
public:
    using Handler = std::function<void(const QString &payload)>;

    explicit MessageDispatcher(ArduinoConnection *connection, QObject *parent = nullptr)
        : QObject(parent)
    {
        connect(connection, &ArduinoConnection::lineReceived, this, &MessageDispatcher::onLineReceived);
    }

    // Регистрация обработчика для конкретного префикса канала —
    // добавление нового датчика не требует изменения этого класса вовсе
    void registerHandler(const QString &channelPrefix, Handler handler)
    {
        m_handlers[channelPrefix] = handler;
    }

private slots:
    void onLineReceived(const QString &line)
    {
        const int colonIndex = line.indexOf(':');
        if (colonIndex == -1) {
            handleUnrecognized(line);
            return;
        }

        const QString prefix = line.left(colonIndex);
        const QString payload = line.mid(colonIndex + 1);

        if (m_handlers.contains(prefix)) {
            m_handlers[prefix](payload);
        } else {
            handleUnrecognized(line);
        }
    }

    void handleUnrecognized(const QString &line)
    {
        qWarning() << "Неизвестное сообщение от Arduino:" << line;
    }

private:
    QHash<QString, Handler> m_handlers;
};
// Регистрация обработчиков для каждого канала — добавление нового датчика
// сводится к одному дополнительному вызову registerHandler, без изменений где-либо ещё
auto *dispatcher = new MessageDispatcher(arduinoConnection, this);

dispatcher->registerHandler("DHT", [this](const QString &payload) {
    const QStringList parts = payload.split(',');
    if (parts.size() == 2) {
        emit temperatureUpdated(parts[0].toDouble());
        emit humidityUpdated(parts[1].toDouble());
    }
});

dispatcher->registerHandler("DISTANCE", [this](const QString &payload) {
    emit distanceUpdated(payload.toDouble());
});

dispatcher->registerHandler("LIGHT", [this](const QString &payload) {
    emit lightLevelUpdated(payload.toInt());
});

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

Прошивка опрашивает три разных датчика с тремя независимыми интервалами времени (lastDhtTime, lastDistanceTime, lastLightTime), каждый соответствующий физическим ограничениям конкретного датчика — это естественное расширение неблокирующего паттерна millis() из предыдущих статей на случай нескольких одновременных источников данных, без необходимости в более сложной логике планирования.

MessageDispatcher решает проблему масштабируемости разбора протокола — вместо одного большого метода с цепочкой if (line.startsWith("DHT")) {...} else if (line.startsWith("DISTANCE")) {...}, который рос бы линейно с каждым новым типом датчика и постепенно становился всё менее читаемым, диспетчер хранит отображение префикса канала на обрабатывающую функцию (std::function) в QHash, и добавление поддержки нового типа сообщения сводится к одному вызову registerHandler() где угодно в коде приложения, без необходимости модифицировать сам класс MessageDispatcher.

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

  • Слишком много одновременно опрашиваемых датчиков с разными интервалами на одной плате Arduino может создать перегруженный loop(), особенно если опрос некоторых датчиков (как HC-SR04 с его pulseIn() и таймаутом до 30 мс) занимает заметное время — при достаточно большом количестве датчиков и совпадении моментов, когда нужно опросить сразу несколько из них, суммарное время одной итерации loop() может стать заметным, что нужно явно учитывать при проектировании прошивки с большим количеством датчиков.
  • Использование : как разделителя префикса канала может конфликтовать с данными, которые сами содержат символ : (например, временная метка в формате чч:мм:сс) — для протоколов, где передаваемые данные потенциально могут содержать символ-разделитель, нужен либо более устойчивый формат разделения (например, JSON, хотя это увеличивает накладные расходы по сравнению с простым текстовым протоколом), либо явное экранирование специальных символов внутри данных.
  • Захват состояния по ссылке в лямбда-обработчиках, зарегистрированных через registerHandler, требует аккуратности с временем жизни захваченных объектов — если лямбда захватывает this указателем на объект, который может быть удалён раньше, чем MessageDispatcher (например, если порядок удаления объектов в дереве владения не гарантирует этого), вызов уже невалидного обработчика приведёт к неопределённому поведению; для надёжности стоит явно продумывать время жизни объектов, участвующих в подобных захватах через лямбды.
  • Отсутствие explicit обработки ситуации, когда конкретный датчик физически отключился во время работы устройства (отвалившийся провод, неисправность) — без отдельного механизма обнаружения «тишины» по конкретному каналу (отсутствие сообщений данного префикса дольше ожидаемого интервала) Qt-приложение может продолжать показывать последнее полученное, но уже давно неактуальное значение, не сообщая пользователю, что конкретный датчик перестал отвечать — эта тема подробнее разбирается в следующей статье цикла про надёжность связи.