Embedded

Итоговый мини-проект: умная панель управления домом на Raspberry Pi с Qt

Введение

В заключительной статье цикла собираем итоговый проект — kiosk-панель умного дома на Raspberry Pi, объединяющая темы всего цикла: GPIO для локальных датчиков и реле, аппаратно ускоренный QML-интерфейс через EGLFS, автозапуск через systemd, и связь с MQTT-инфраструктурой из статьи 320 для управления удалёнными ESP32-узлами, логически замыкая Raspberry Pi-цикл на уже написанную IoT-тему.

Концепция

Архитектура проекта: панель на Raspberry Pi подписывается на MQTT-топики ESP32-узлов (статья 315) для отображения показаний датчиков по всему дому, отображает локальное реле/освещение через прямой GPIO-доступ (статья 343), запускается в kiosk-режиме (статья 348) с полноэкранным QML-интерфейсом, и поддерживает удалённое обновление по сети (статья 349) для развёртывания новых версий интерфейса без необходимости физического доступа к устройству.

Пример кода: Итоговый мини-проект

// main.qml — основной интерфейс панели, объединяющий MQTT-данные и локальное GPIO-управление
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 800
    height: 480 // типичное разрешение небольшого сенсорного дисплея для панели умного дома

    GridView {
        anchors.fill: parent
        cellWidth: 200
        cellHeight: 150
        model: roomTemperatureModel // заполняется из C++ через MQTT-подписку

        delegate: Rectangle {
            width: 190; height: 140
            color: "#2c3e50"
            radius: 8

            Column {
                anchors.centerIn: parent
                Text { text: model.roomName; color: "white"; font.pixelSize: 18 }
                Text { text: model.temperature + "°C"; color: "#3498db"; font.pixelSize: 32 }
            }
        }
    }

    Switch {
        anchors.bottom: parent.bottom
        anchors.right: parent.right
        text: "Свет в гостиной"
        onToggled: gpioController.setLivingRoomLight(checked) // прямой вызов C++ через контекст
    }
}
// homepanel.h — связующий C++ слой между MQTT, GPIO и QML-интерфейсом
class HomePanel : public QObject
{
    Q_OBJECT
public:
    HomePanel(QObject *parent = nullptr) : QObject(parent)
    {
        // MQTT-клиент, аналогичный MqttSensorClient из статьи 315 —
        // подписка на топики всех ESP32-узлов дома
        connect(&m_mqttClient, &MqttSensorClient::readingReceived,
                this, &HomePanel::onReadingReceived);
        m_mqttClient.connectToHost("mqtt.home.local");

        // Прямой GPIO-доступ для локального реле освещения, аналогично статье 343
        m_lightRelay = std::make_unique<GpioController>("gpiochip0", 27);
        m_lightRelay->setupAsOutput();
    }

    Q_INVOKABLE void setLivingRoomLight(bool on)
    {
        m_lightRelay->setValue(on);
    }

private slots:
    void onReadingReceived(const QString &roomName, double temperature)
    {
        m_roomTemperatureModel->updateRoom(roomName, temperature);
    }

private:
    MqttSensorClient m_mqttClient;
    std::unique_ptr<GpioController> m_lightRelay;
    RoomTemperatureModel *m_roomTemperatureModel;
};
# kiosk-app.service — итоговая конфигурация автозапуска, объединяющая все настройки цикла
[Unit]
Description=Smart Home Panel
After=network-online.target  # важно дождаться сети ПЕРЕД стартом — приложению нужен MQTT-брокер
Wants=network-online.target

[Service]
Type=simple
User=pi
Environment=QT_QPA_PLATFORM=eglfs
ExecStart=/home/pi/SmartHomePanel/SmartHomePanel
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

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

HomePanel объединяет два независимых источника данных в одном связующем C++ классе — MqttSensorClient (статья 315) для удалённых показаний от ESP32-узлов по всему дому, и прямой GpioController (статья 343) для локального реле, физически подключённого непосредственно к самой панели — оба пути выставлены в QML через Q_INVOKABLE методы и модели данных, давая интерфейсу единообразный доступ к разнородным по природе источникам данных и управления. Конфигурация systemd-сервиса явно зависит от network-online.target (а не просто multi-user.target, как в более простом примере статьи 348) — это критично важная деталь именно для этого проекта: приложению нужно установленное сетевое соединение для подключения к MQTT-брокеру при самом первом запуске, и без этой зависимости сервис мог бы попытаться запуститься до того, как сеть будет полностью готова, особенно на медленно загружающихся сетевых интерфейсах (Wi-Fi, требующий времени на ассоциацию с точкой доступа).

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

  • Отсутствие явной обработки временной потери сетевого соединения уже во время работы приложения (а не только при самом старте, для чего предусмотрена зависимость network-online.target) — если Wi-Fi-соединение временно прерывается во время работы панели (что для домашней сети случается регулярно), MQTT-клиент должен корректно обнаружить разрыв и автоматически переподключиться при восстановлении сети, аналогично подводным камням из статьи 314 основного MQTT-цикла, и интерфейс должен явно показывать пользователю состояние «данные могут быть устаревшими» при потере соединения, а не молча отображать последние, потенциально устаревшие значения как актуальные.
  • Смешивание прямого, синхронного доступа к GPIO из слота, вызываемого из QML (setLivingRoomLight), без вынесения в отдельный поток — хотя одна операция gpiod_line_set_value() сама по себе очень быстрая и не блокирует интерфейс заметно, общий принцип, разбиравшийся для gRPC (статья 327) и сетевых вызовов, остаётся в силе: любое потенциально медленное взаимодействие с аппаратным обеспечением, вызываемое непосредственно из UI-потока, стоит сначала явно проверить на реальную задержку, прежде чем считать его безопасным для вызова напрямую из обработчика событий интерфейса.
  • Хранение пароля/учётных данных MQTT-брокера непосредственно в исходном коде приложения (упрощённо показанное в примере как просто connectToHost() без аутентификации) — для реального домашнего проекта, особенно если панель должна поддерживать TLS и аутентификацию для MQTT-соединения (статья 318), учётные данные должны браться из защищённого конфигурационного файла или хранилища, а не быть зашиты непосредственно в исходный код, который может быть случайно опубликован или передан третьим лицам вместе с проектом.
  • Развёртывание единственной, не резервируемой панели управления домом как единственной точки управления всеми устройствами, без какого-либо резервного способа управления тем же освещением/устройствами при выходе из строя самой панели Raspberry Pi (физическая поломка SD-карты, зависание системы) — для критичных функций (например, освещение, а не просто отображение показаний температуры) стоит сохранить независимый от панели способ управления (обычный физический выключатель, параллельный кнопке на самой панели), чтобы единая точка отказа панели не оставляла пользователя без какого-либо способа управления базовыми функциями дома.

Цикл «Raspberry Pi + Qt» завершён — план дополнен до 350 статей; далее последуют циклы STM32/bare-metal (351-360) и CAN-шина/автомобильная диагностика (361-370).