Введение
Реальные проекты часто включают несколько разных датчиков одновременно (температура, влажность, расстояние, освещённость) — нужен протокол, позволяющий 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-приложение может продолжать показывать последнее полученное, но уже давно неактуальное значение, не сообщая пользователю, что конкретный датчик перестал отвечать — эта тема подробнее разбирается в следующей статье цикла про надёжность связи.