Введение
В заключительной статье цикла объединим все компоненты, разобранные в предыдущих девяти статьях, в единый, законченный мини-проект — станцию мониторинга с несколькими датчиками, элементами управления и устойчивым к сбоям соединением, демонстрирующую, как все отдельные техники складываются в одно цельное приложение.
Концепция
Архитектура итогового проекта объединяет: ReliableArduinoConnection как основу устойчивого соединения, MessageDispatcher для разбора данных нескольких датчиков, отдельные виджеты для каждого канала данных и управления (графики для датчиков расстояния и температуры, переключатели для реле, слайдеры для серво/мотора), и единый главный экран приложения, объединяющий всё это в согласованный, целостный пользовательский интерфейс мониторинга и управления устройством.
Пример кода
// firmware/src/main.cpp — итоговая прошивка станции мониторинга, объединяющая все компоненты цикла
#include <DHT.h>
#include <Servo.h>
#define DHT_PIN 2
#define TRIG_PIN 9
#define ECHO_PIN 10
#define RELAY_PIN 7
#define SERVO_PIN 11
DHT dht(DHT_PIN, DHT22);
Servo myServo;
int currentAngle = 90, targetAngle = 90;
void setup()
{
Serial.begin(9600);
dht.begin();
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(RELAY_PIN, OUTPUT);
myServo.attach(SERVO_PIN);
myServo.write(currentAngle);
delay(100);
Serial.println("READY");
}
void loop()
{
handleIncomingCommands();
sendPeriodicReadings();
updateServoPosition();
}
void handleIncomingCommands()
{
static String inputBuffer = "";
while (Serial.available() > 0) {
char c = Serial.read();
if (c == 'n') {
processCommand(inputBuffer);
inputBuffer = "";
} else if (inputBuffer.length() < 64) { // защита от неограниченного роста буфера
inputBuffer += c;
}
}
}
void processCommand(String command)
{
command.trim();
if (command == "PING") {
Serial.println("PONG");
} else if (command == "RELAY:ON") {
digitalWrite(RELAY_PIN, HIGH);
} else if (command == "RELAY:OFF") {
digitalWrite(RELAY_PIN, LOW);
} else if (command.startsWith("SERVO:ANGLE:")) {
targetAngle = constrain(command.substring(12).toInt(), 0, 180);
}
}
void sendPeriodicReadings()
{
static unsigned long lastDht = 0, lastDistance = 0;
if (millis() - lastDht >= 2000) {
const float t = dht.readTemperature(), h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
Serial.print("DHT:"); Serial.print(t); Serial.print(","); Serial.println(h);
}
lastDht = millis();
}
if (millis() - lastDistance >= 200) {
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);
}
lastDistance = millis();
}
}
void updateServoPosition()
{
static unsigned long lastStep = 0;
if (millis() - lastStep >= 15 && currentAngle != targetAngle) {
currentAngle += (targetAngle > currentAngle) ? 1 : -1;
myServo.write(currentAngle);
lastStep = millis();
}
}
// mainwindow.h — главное окно станции мониторинга, объединяющее все компоненты цикла
#pragma once
#include <QMainWindow>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "reliableArduinoConnection.h"
#include "messagedispatcher.h"
#include "distancechartwidget.h"
#include "motorcontrolwidget.h"
#include "servocontrolwidget.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow()
{
auto *central = new QWidget(this);
setCentralWidget(central);
m_connectionStatusLabel = new QLabel("Подключение...", this);
m_temperatureLabel = new QLabel("Температура: --", this);
m_humidityLabel = new QLabel("Влажность: --", this);
auto *distanceChart = new DistanceChartWidget(&m_connection.rawConnection(), this);
auto *relayControl = new MotorControlWidget(&m_connection.rawConnection(), this);
auto *servoControl = new ServoControlWidget(&m_connection.rawConnection(), this);
auto *statusLayout = new QHBoxLayout();
statusLayout->addWidget(m_connectionStatusLabel);
statusLayout->addWidget(m_temperatureLabel);
statusLayout->addWidget(m_humidityLabel);
auto *mainLayout = new QVBoxLayout(central);
mainLayout->addLayout(statusLayout);
mainLayout->addWidget(distanceChart);
mainLayout->addWidget(relayControl);
mainLayout->addWidget(servoControl);
auto *dispatcher = new MessageDispatcher(&m_connection.rawConnection(), this);
dispatcher->registerHandler("DHT", [this](const QString &payload) {
const QStringList parts = payload.split(',');
if (parts.size() == 2) {
m_temperatureLabel->setText(QString("Температура: %1 °C").arg(parts[0].toDouble(), 0, 'f', 1));
m_humidityLabel->setText(QString("Влажность: %1 %").arg(parts[1].toDouble(), 0, 'f', 1));
}
});
connect(&m_connection, &ReliableArduinoConnection::connectionStateChanged, this, [this](bool connected) {
m_connectionStatusLabel->setText(connected ? "Подключено" : "Отключено");
m_connectionStatusLabel->setStyleSheet(connected ? "color: green;" : "color: red;");
});
}
private:
ReliableArduinoConnection m_connection;
QLabel *m_connectionStatusLabel;
QLabel *m_temperatureLabel;
QLabel *m_humidityLabel;
};
Пояснения к коду
Итоговая прошивка структурирована как три независимых, явно названных функции, вызываемых из loop() последовательно на каждой итерации — handleIncomingCommands(), sendPeriodicReadings(), updateServoPosition() — каждая отвечает строго за свою область, что делает прошивку, объединяющую несколько ранее разобранных по отдельности возможностей, читаемой и легко расширяемой: добавление, например, ещё одного датчика означает добавление новой функции отправки его показаний и одного дополнительного вызова в sendPeriodicReadings(), без необходимости менять остальную структуру.
MainWindow на Qt-стороне последовательно собирает все ранее разработанные компоненты — ReliableArduinoConnection как единственный источник соединения, передаваемый всем остальным виджетам через rawConnection(), MessageDispatcher для разбора данных датчика температуры/влажности, и уже готовые виджеты графика расстояния, управления реле и серво из предыдущих статей — собранные в одном главном окне через обычную композицию layout’ов. Подписка на connectionStateChanged обновляет визуальный индикатор состояния соединения, давая пользователю немедленную, понятную обратную связь о том, активна ли связь с устройством в данный момент.
Подводные камни
- Совместное использование одного и того же объекта
ArduinoConnectionнесколькими независимыми виджетами одновременно (какdistanceChart,relayControl,servoControlв примере, все получающие ссылку на один и тот жеrawConnection()) требует, чтобы каждый виджет был спроектирован корректно фильтровать и обрабатывать только относящиеся к нему сообщения (что и обеспечиваетMessageDispatcher), а не безусловно реагировать на каждую полученную строку — без такой дисциплины компоненты могут начать конфликтовать друг с другом при разборе одного общего потока входящих данных. - Жёсткая привязка структуры главного окна к конкретному, фиксированному набору датчиков и исполнительных устройств данного мини-проекта ограничивает прямую переносимость кода на другой проект с другим набором модулей без переписывания значительной части
MainWindow— для более универсального, переиспользуемого фреймворка имело бы смысл дополнительно абстрагировать сами виджеты управления через общий интерфейс (по аналогии с паттернами из основного цикла статей про интерфейсы и плагины), что выходило бы за рамки данного учебного мини-проекта, но является логичным следующим шагом для более амбициозного, универсального инструмента мониторинга. - Отсутствие сохранения истории показаний датчиков между запусками приложения — представленный мини-проект показывает только живые, текущие данные, без какого-либо долговременного хранения; для полноценной станции мониторинга, предполагающей анализ исторических трендов (как менялась температура за последние сутки/неделю), нужна интеграция с локальной базой данных, что естественно сочеталось бы с циклом статей про SQL/ORM — отдельное направление развития этого же проекта.
- Тестирование собранного воедино мини-проекта только в идеальных лабораторных условиях разработки, без учёта реальных условий эксплуатации (большая физическая дистанция передачи показаний, электромагнитные помехи от других устройств, изменение температуры окружающей среды, влияющее на точность датчиков) — финальная проверка проекта, объединяющего множество ранее изученных по отдельности техник, должна включать тестирование в условиях, максимально близких к реальным условиям его предполагаемого использования, а не только короткую демонстрацию на столе разработчика.