Arduino

Объединение нескольких входных модулей в один Qt-интерфейс управления

1 просмотров

Введение

К этому моменту цикла мы рассмотрели больше десятка отдельных входных модулей (джойстик, кнопки, кнопочная матрица, потенциометр, датчики). В статье разберём, как организовать единый, согласованный Qt-интерфейс, агрегирующий ввод от нескольких разных модулей одновременно, не превращая код в нечитаемый набор разрозненных, специфичных для каждого модуля обработчиков.

Концепция

Идея агрегации — единый интерфейс InputSource, через который любой конкретный модуль ввода (джойстик, клавиатура, кнопка) сообщает о событиях в унифицированной форме, и центральный InputAggregator, подписывающийся на все источники и предоставляющий остальной части приложения единую точку получения пользовательского ввода, не зависящую от того, какой конкретно физический модуль его сгенерировал — аналогично паттерну Repository из цикла про базы данных, здесь применённому к агрегации входных данных вместо доступа к данным БД.

Пример кода

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

enum class InputAction { Up, Down, Left, Right, Confirm, Cancel };

class InputSource : public QObject
{
    Q_OBJECT
signals:
    void actionTriggered(InputAction action);
};
// joystickinputsource.h — джойстик как конкретная реализация InputSource
#pragma once
#include "inputsource.h"
#include "arduinoconnection.h"

class JoystickInputSource : public InputSource
{
    Q_OBJECT
public:
    explicit JoystickInputSource(ArduinoConnection *connection, QObject *parent = nullptr)
        : InputSource(parent)
    {
        connect(connection, &ArduinoConnection::lineReceived, this, &JoystickInputSource::onLineReceived);
    }

private slots:
    void onLineReceived(const QString &line)
    {
        if (line.startsWith("JOYSTICK:") && !line.contains("BUTTON")) {
            const QStringList parts = line.mid(9).split(',');
            if (parts.size() != 2) return;

            const int x = parts[0].toInt(), y = parts[1].toInt();
            const int DEAD_ZONE = 20;

            if (x > DEAD_ZONE) emit actionTriggered(InputAction::Right);
            else if (x < -DEAD_ZONE) emit actionTriggered(InputAction::Left);
            if (y > DEAD_ZONE) emit actionTriggered(InputAction::Down);
            else if (y < -DEAD_ZONE) emit actionTriggered(InputAction::Up);
        } else if (line == "JOYSTICK:BUTTON:PRESSED") {
            emit actionTriggered(InputAction::Confirm);
        }
    }
};
// keypadinputsource.h — кнопочная матрица как другая реализация того же InputSource
#pragma once
#include "inputsource.h"
#include "arduinoconnection.h"

class KeypadInputSource : public InputSource
{
    Q_OBJECT
public:
    explicit KeypadInputSource(ArduinoConnection *connection, QObject *parent = nullptr)
        : InputSource(parent)
    {
        connect(connection, &ArduinoConnection::lineReceived, this, &KeypadInputSource::onLineReceived);
    }

private slots:
    void onLineReceived(const QString &line)
    {
        if (!line.startsWith("KEYPAD:")) return;
        const QChar key = line.mid(7).at(0);

        // Маппинг клавиш матрицы 4х4 на ту же единую систему действий, что и у джойстика
        if (key == 'A') emit actionTriggered(InputAction::Up);
        else if (key == 'B') emit actionTriggered(InputAction::Down);
        else if (key == '#') emit actionTriggered(InputAction::Confirm);
        else if (key == '*') emit actionTriggered(InputAction::Cancel);
    }
};
// Использование — остальная часть приложения работает с абстрактным InputAction,
// не зная и не заботясь, какой конкретно модуль (джойстик или клавиатура) его сгенерировал
void GameController::setupInputSources(ArduinoConnection *connection)
{
    auto *joystick = new JoystickInputSource(connection, this);
    auto *keypad = new KeypadInputSource(connection, this);

    connect(joystick, &InputSource::actionTriggered, this, &GameController::onAction);
    connect(keypad, &InputSource::actionTriggered, this, &GameController::onAction);
}

void GameController::onAction(InputAction action)
{
    switch (action) {
        case InputAction::Up: movePlayerUp(); break;
        case InputAction::Down: movePlayerDown(); break;
        case InputAction::Confirm: confirmSelection(); break;
        default: break;
    }
}

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

InputSource объявляет единственный сигнал actionTriggered(InputAction) — абстрактную, не зависящую от конкретного физического устройства, единицу пользовательского намерения; JoystickInputSource и KeypadInputSource — две совершенно разные по внутренней реализации (одна разбирает аналоговые координаты с мёртвой зоной, другая — единичные символы клавиш), но обе сообщающие о событиях через один и тот же унифицированный интерфейс.

GameController::setupInputSources() подключает оба конкретных источника к одному и тому же обработчику onAction() — это ключевая выгода паттерна: добавление третьего источника ввода (например, ИК-пульта из статьи 302) требует написания только новой реализации InputSource для конкретного модуля, без единого изменения в GameController, который продолжает обрабатывать абстрактные действия одинаково, независимо от их физического происхождения.

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

  • Слишком грубая абстракция входных событий, теряющая важные для конкретного приложения детали — например, если приложению важна не просто «кнопка вправо нажата», а конкретная сила отклонения джойстика (для аналогового, плавного управления скоростью, а не просто дискретного направления), унификация до простого перечисления InputAction теряет эту информацию; для таких случаев абстракция должна быть спроектирована шире, передавая, например, не только направление, но и аналоговую интенсивность действия.
  • Одновременная активация нескольких источников ввода, конфликтующих друг с другом (например, джойстик отклонён влево в тот же момент, когда на клавиатуре нажата клавиша, соответствующая действию «вправо») — без явной политики приоритета источников или какого-то правила разрешения конфликтов, порядок обработки сигналов может зависеть от порядка подключения connect(), что не всегда предсказуемо и интуитивно для конечного поведения приложения.
  • Расширение перечисления InputAction новыми значениями по мере роста приложения требует синхронного обновления всех существующих реализаций InputSource, которые потенциально должны как-то отреагировать на новое действие (даже если просто не генерировать его) — для очень большого количества разных источников ввода это может стать source кода, требующим внимательной синхронизации при каждом расширении набора поддерживаемых действий.
  • Тестирование агрегированной логики через mock-реализацию InputSource (аналогично mock-репозиториям из цикла про базы данных) упрощается именно благодаря этой абстракции — без неё тестирование логики GameController потребовало бы реального физического оборудования или сложной симуляции конкретного протокола Arduino, тогда как с абстракцией достаточно простого тестового InputSource, испускающего нужные actionTriggered сигналы напрямую из тестового кода.