Arduino

Объединение нескольких выходных модулей в единую систему индикации

1 просмотров

Введение

Симметрично предыдущей статье, разберём агрегацию вывода — единый способ показать пользователю информацию или состояние приложения сразу через несколько физических индикаторов (LCD-дисплей, RGB-светодиод, зуммер), не дублируя логику обновления каждого индикатора в разных местах кода приложения.

Концепция

Идея агрегации вывода — единый интерфейс OutputIndicator, через который любой конкретный физический индикатор реализует метод showStatus(status), и центральный StatusBroadcaster, владеющий списком всех зарегистрированных индикаторов и пересылающий им одно и то же изменение статуса приложения — каждый конкретный индикатор сам решает, как именно выразить полученный статус доступными ему средствами (текстом на LCD, цветом RGB-светодиода, звуковым сигналом).

Пример кода

// outputindicator.h — единый интерфейс для любого физического индикатора состояния
#pragma once
#include <QObject>

enum class AppStatus { Idle, Processing, Success, Error };

class OutputIndicator : public QObject
{
    Q_OBJECT
public:
    explicit OutputIndicator(QObject *parent = nullptr) : QObject(parent) {}
    virtual void showStatus(AppStatus status) = 0;
};
// lcdoutputindicator.h, rgboutputindicator.h, buzzeroutputindicator.h —
// три разные реализации одного интерфейса, каждая со своим способом выражения статуса
#pragma once
#include "outputindicator.h"
#include "arduinoconnection.h"

class LcdOutputIndicator : public OutputIndicator
{
public:
    explicit LcdOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
        : OutputIndicator(parent), m_connection(connection) {}

    void showStatus(AppStatus status) override
    {
        QString text;
        switch (status) {
            case AppStatus::Idle: text = "Ожидание..."; break;
            case AppStatus::Processing: text = "Обработка..."; break;
            case AppStatus::Success: text = "Готово!"; break;
            case AppStatus::Error: text = "Ошибка!"; break;
        }
        m_connection->sendCommand("LCD:LINE1:" + text);
    }

private:
    ArduinoConnection *m_connection;
};

class RgbOutputIndicator : public OutputIndicator
{
public:
    explicit RgbOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
        : OutputIndicator(parent), m_connection(connection) {}

    void showStatus(AppStatus status) override
    {
        QString rgbCommand;
        switch (status) {
            case AppStatus::Idle: rgbCommand = "RGB:0,0,255"; break;       // синий — спокойное ожидание
            case AppStatus::Processing: rgbCommand = "RGB:255,255,0"; break; // желтый — что-то происходит
            case AppStatus::Success: rgbCommand = "RGB:0,255,0"; break;    // зелёный — успех
            case AppStatus::Error: rgbCommand = "RGB:255,0,0"; break;      // красный — ошибка
        }
        m_connection->sendCommand(rgbCommand);
    }

private:
    ArduinoConnection *m_connection;
};

class BuzzerOutputIndicator : public OutputIndicator
{
public:
    explicit BuzzerOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
        : OutputIndicator(parent), m_connection(connection) {}

    void showStatus(AppStatus status) override
    {
        // Зуммер реагирует только на значимые, требующие внимания события,
        // намеренно оставаясь молчаливым для повседневных, ожидаемых статусов
        if (status == AppStatus::Success) {
            m_connection->sendCommand("BUZZER:BEEP");
        } else if (status == AppStatus::Error) {
            m_connection->sendCommand("BUZZER:ALARM");
        }
    }

private:
    ArduinoConnection *m_connection;
};
// statusbroadcaster.h — единая точка изменения статуса, рассылающая его всем индикаторам
#pragma once
#include <QObject>
#include <QList>
#include "outputindicator.h"

class StatusBroadcaster : public QObject
{
    Q_OBJECT
public:
    explicit StatusBroadcaster(QObject *parent = nullptr) : QObject(parent) {}

    void registerIndicator(OutputIndicator *indicator) { m_indicators.append(indicator); }

    void setStatus(AppStatus status)
    {
        for (OutputIndicator *indicator : m_indicators) {
            indicator->showStatus(status);
        }
    }

private:
    QList<OutputIndicator *> m_indicators;
};

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

Каждая из трёх реализаций OutputIndicator интерпретирует один и тот же абстрактный статус (AppStatus::Success, например) совершенно по-своему, в соответствии с собственными физическими возможностями — LCD выводит текст, RGB-светодиод меняет цвет, а зуммер издаёт короткий звук подтверждения, причём зуммер сознательно остаётся «молчаливым» для статусов Idle/Processing, поскольку постоянные звуковые сигналы на каждое незначительное изменение статуса были бы избыточны и раздражающи.

StatusBroadcaster хранит список всех зарегистрированных индикаторов и при вызове setStatus() просто вызывает showStatus() у каждого из них по очереди — остальной код приложения, инициирующий изменение статуса (например, после завершения какой-то операции), вызывает единственный метод broadcaster.setStatus(AppStatus::Success), не заботясь о том, сколько и какие конкретно физические индикаторы в данный момент зарегистрированы и активны — добавление или удаление конкретного индикатора (скажем, временное отключение зуммера) требует изменения только в одном месте — где индикаторы регистрируются, а не во всех местах, где статус устанавливается.

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

  • Синхронная, последовательная рассылка статуса всем индикаторам внутри setStatus() означает, что если один из индикаторов выполняет относительно долгую операцию (например, ожидание подтверждения от Arduino), остальные индикаторы в списке получат обновление статуса с задержкой, ожидая своей очереди — для большинства практических случаев с быстрой отправкой команд через последовательный порт эта задержка незаметна, но для потенциально медленных операций стоит рассмотреть асинхронную рассылку, не блокирующую обработку оставшихся индикаторов в списке ожиданием медленного.
  • Отсутствие обработки ошибок на уровне отдельных индикаторов — если, например, LCD-дисплей физически отключён или вышел из строя, его реализация showStatus() всё равно будет вызвана StatusBroadcaster без какой-либо проверки успешности, и без явного механизма обратной связи об ошибке остальная часть приложения может не узнать, что конкретный индикатор не смог фактически отразить переданный статус.
  • Жёстко закодированное соответствие между абстрактным статусом и конкретным визуальным/звуковым выражением внутри каждого индикатора (например, конкретные цвета RGB для каждого статуса) затрудняет последующую настройку этого соответствия пользователем (например, изменение цветовой схемы по предпочтению) без изменения кода — для проектов, где такая настраиваемость важна, соответствие между статусом и конкретным выражением можно было бы сделать конфигурируемым (через настройки приложения), а не зафиксированным напрямую в коде индикатора.
  • Расширение перечисления AppStatus новыми значениями без соответствующего обновления всех существующих реализаций индикаторов — аналогично подводному камню предыдущей статьи про входные источники, добавление нового статуса требует явной проверки (и, возможно, обновления) каждой существующей реализации OutputIndicator, чтобы убедиться, что новый статус обрабатывается осознанно (даже если решение — «этот конкретный индикатор не реагирует на этот статус»), а не просачивается в switch без обработки default-случая, что для компилируемого enum class обычно явно подсвечивается предупреждением компилятора о неполном покрытии switch, но это предупреждение легко проигнорировать.