Arduino

Мини-проект: станция мониторинга/умный дом на Qt + Arduino

Введение

В заключительной статье цикла объединим все компоненты, разобранные в предыдущих девяти статьях, в единый, законченный мини-проект — станцию мониторинга с несколькими датчиками, элементами управления и устойчивым к сбоям соединением, демонстрирующую, как все отдельные техники складываются в одно цельное приложение.

Концепция

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