Примерка взрослого пальто. Событийно-ориентированный подход на Arduino

от автора

Статья для тех, кто ищет пути как писать иным образом структурированный код.

С чего всё началось

Когда я выбирал платформу, чтобы начать знакомить сына с программной инженерией, выбор оказался предопределенным. Arduino предлагает быстрый старт и минимальный порог входа, начинающий может управлять светодиодом буквально через 15 минут после распаковки.

Только вот структурирование программ на Arduino совсем не похоже на структурирование, которое применяется при разработке десктопных, серверных, мобильных приложений (далее — “большие” приложения). А мне хотелось демонстрировать на “игрушечных” примерах, как в “больших” проектах структурируется код.

Задача, которую решают по частям

В Arduino структурирование строится вокруг функции loop, тогда как “большие” приложения структурируются относительно обработчиков событий.

Существует множество отдельных библиотек, которые фрагментарно реализуют событийно-ориентированный подход в Arduino:

  • cуществуют отдельные событийно-ориентированные библиотеки для кнопок,

  • отдельно для таймеров,

  • есть даже библиотеки для call-back методов классов, а не просто функций.

А где “всё в одном”? Это просто обязано идти одним комплектом!

К самостоятельной сборке “всего в одном” подтолкнула ещё одна деталь: метод обновления состояния, не должен быть задан жестко и не должен требовать протягивания вызовов через декомпозицию пользовательских объектов. Иначе событийно-ориентированный подход заканчивается в том месте, где заканчивается библиотека. Но пользовательский код тоже может быть структурирован событийно!

Собственно “всё в одном” — это три в одном: небольшая библиотека EVA Core | EVA Survival Kit:

  • обратные вызовы (call-back) — методы пользовательских классов

  • любой пользовательский класс может получить свой фрагмент времени в loop() — достаточно унаследовать Tickable и реализовать метод tick()

  • набор для выживания (кнопки, таймеры, джойстики), который реализован на базе первых двух принципов

У библиотеки получилась ещё одна забавная отличительная черта — набор для выживания сам собой оформился в виде конструктора в стиле LEGO, который при желании можно пересобрать.

Что это позволяет демонстрировать?

ОО подход — работу с объектами и методами, контроль за порядком инициализации во время рефакторинга, тестируемость.

Можно показывать как тайминги становятся частью контрактов взаимодействия между пользовательскими объектами. Когда один пользовательский объект использует другой, он может полагаться на то, что тот будет обновляться в нужном ритме.

Композицию без “протягивания” вызовов. Если Вы собрали сложный объект из нескольких — все вложенные объекты знают, когда им нужно обновиться.

Способ мышления, когда Вы описываете, что должно происходить, а не когда, так как вопрос «когда» на себя берет библиотека.

Пример №1: пользовательские классы могут так же получать свой фрагмент времени из loop()

#include <evaTickable.h>#include <evaHandler.h>#include <evaTac.h>using namespace eva;// Сенсор сам управляет своим временемclass MySensor : public Tickable {private:  IHandler* listener = nullptr;  public:  void subscribe(IHandler* handler) {    listener = handler;  }  private:  void tick() override {    // Здесь может быть организована работа со временем:    // контроль retry, таймауты, фильтрация    // Сенсор сам решает, когда и как часто опрашивать железо        if (listener) {      CallbackInfo info;      info.eventType = 0x01;  // DATA_READY      info.eventArg = 42;     // значение      listener->invoke(this, info);    }  }};// Бизнес-логика. Никаких таймингов — только реакция на событияclass AppLogic : public IHandler {private:  MySensor sensor;    void onData(int value) {    // Здесь бизнес-логика    // Без delay(), без millis(), без опросов  }  public:  AppLogic() {    sensor.subscribe(this);  }    void invoke(void* sender, CallbackInfo info) override {    if (info.eventType == 0x01) {      onData(info.eventArg);    }  }};void setup() {  static AppLogic logic;  Serial.begin(9600);}void loop() {  eva::tac();  // Один вызов обновляет всё}

Что здесь происходит:

  • MySensor наследует Tickable > получает свой кусок времени в loop()

  • AppLogic ничего не знает о времени > только реагирует на события

  • В tick() сенсора можно делать любую временную логику (retry, фильтрацию, таймауты)

  • Пользовательский код структурирован так же, как “взрослые” приложения

Пример №2: “Хулиганство” — сборка обработчиков из кирпичиков

EVA Core | EVA Survival Kit внутри собирает классы работы с кнопками как цепочку шаблонов:

// Три кнопки на одном аналоговом входе через резисторную лестницуtemplate <int PIN, int PIN_MODE, signed short... LEVELS>using PinMultiButton = Button<QuantizeDecor<DebounceDecor<AnalogPinReader<PIN, PIN_MODE>>, LEVELS...>>;

Что происходит? Библиотека в compile-time собирает цепочку преобразователей:

Аналоговый сигнал > Дебаунс > Квантование по уровням > Логика кнопки > События

Так как послойная архитектура открыта, возможно конструировать подобные цепочки на базе определённых пользователем классов.

Можно собрать обработчик своей клавиатуры, задействуя отдельные компоненты :

#include <evaSwitch.h>#include <evaTac.h>#include <evaHandler.h>using namespace eva;class MyBankReader {public:  MyBankReader() {    pinMode(2, INPUT_PULLUP);    pinMode(3, INPUT_PULLUP);    pinMode(4, INPUT_PULLUP);    pinMode(5, INPUT_PULLUP);  }    signed short getValue() {    if (digitalRead(2) == LOW) return 'u';  // Вверх (Up button)    if (digitalRead(3) == LOW) return 'd';  // Вниз (Down button)    if (digitalRead(4) == LOW) return 'l';  // Влево (Left button)    if (digitalRead(5) == LOW) return 'r';  // Вправа (Right button)    return 0;    }};class App {private:  // Сборка собственной цепочки  Switch<DebounceDecor<MyBankReader>> navButtons;    void onButtonPress(void* sender, CallbackInfo cbInfo) {    char button = cbInfo.eventArg;  // 'u', 'd', 'l', или 'r'        Serial.print("Pressed: ");    switch(button) {      case 'u': Serial.println("UP"); break;      case 'd': Serial.println("DOWN"); break;      case 'l': Serial.println("LEFT"); break;      case 'r': Serial.println("RIGHT"); break;    }  }  public:  App() : navButtons() {    navButtons.setListener(      new Handler<App>(this, &App::onButtonPress),      ON_PRESS    );  }};void setup() {  Serial.begin(9600);  static App app;}void loop() {  eva::tac();}

Логика чтения, антидребезга и реакции собирается в одном месте. Каждый слой делает своё дело, и слои можно переставлять и заменять.

Это — как конструктор в стиле LEGO. Библиотека даёт детали, вы собираете из них нужный механизм.

Позиционирование библиотеки

Библиотека в классическом понимании, как инструмент разработки — вопрос. Перед её публикацией я предпринял некоторые шаги, чтобы сократить потребление ОЗУ, но накладные расходы, связанные с гибкостью, в ней неизбежно присутствуют.

Я позиционирую EVA Core | EVA Survival Kit как методическое упражнение, а не серебряную пулю, отлично понимая её ограничения, если смотреть на неё как на инструмент разработки скетчей:

  1. Порог входа выше, чем у “чистого” Arduino. Придётся разобраться с классами, шаблонами, указателями на методы.

  2. Не для всех проектов. Если у Вас три датчика и один светодиод — овчинка выделки не стоит.

  3. Стиль написания кода меняется. Это пугает. Но это и есть главная цель обучения.

  4. Встраивание в чужой код. Требует усилий над собой.

Вместо заключения

Я призываю посмотреть на Arduino иначе. С небольшим тюнингом эта платформа идеально подходит, чтобы объяснить:

  • Что такое событийное программирование

  • Зачем нужны колбэки

  • Как собирать сложные компоненты из простых

  • Почему архитектура важнее, чем “лишь бы работало”


Ссылки:

ссылка на оригинал статьи https://habr.com/ru/articles/1024730/