Перейти к содержанию

Архитектура плагинов

Plug-and-play — это не только «положили в папку, и оно подцепилось». Это компромисс между:

  • Расширяемостью — добавление плагина не должно требовать правок ядра.
  • Размером бинарника — выключенные плагины не должны попадать в .bin.
  • Стоимостью RAM — особенно на ESP8266, где каждый байт на счету.

Слои

            ┌─────────────────────────────────────┐
            │  tasks.cpp (ядро задач)             │
            │  parseTrigger → dispatch.eval(...)  │
            └────────────┬────────────────────────┘
            ┌────────────▼────────────┐
            │  Registry (linker)      │  ← REGISTER_*_PLUGIN macros
            │  trigger_plugin_table[] │
            └────────────┬────────────┘
       ┌─────────┬───────┴─────────┬─────────┐
       ▼         ▼                 ▼         ▼
   interval    cron              sun      device_state
   (.cpp)     (.cpp)            (.cpp)    (.cpp)

Ядро тасков НЕ знает, какие триггеры существуют. Оно умеет только «вызови eval у плагина с указанным именем» — а плагин внутри ему вернёт true/false.

Контракт sensor plugin

Структура function pointers, которую заполняет каждый sensor-плагин:

struct SensorPlugin {
    const char*       name;             // id плагина (= имя папки)
    const char* const* types_provided;  // NULL-terminated массив type-строк
    uint16_t          ram_bytes_estimate;
    uint16_t          flash_bytes_estimate;

    // Жизненный цикл
    void  (*on_pin_setup)(Device& d);                      // pinMode + initial state
    void  (*on_poll_all)(uint32_t now_ms);                 // периодический опрос
    void  (*on_devices_changed)(void);                     // rebuild общих ресурсов

    // UI и действия
    const char*        ui_meta_json;                       // JSON manifest в PROGMEM
    bool (*on_action)(Device& d, JsonObject p, JsonObject r);  // actuator (relay)
    bool (*on_simulate_event)(Device& d, JsonObject p);    // button virtual override

    // State и config
    void  (*on_state_export)(const Device& d, JsonObject out);  // → dashboard/cloud
    void  (*on_config_load)(Device& d, JsonObject cfg);    // парсинг per-device config
};

Не все поля обязательны. Большинство — nullptr для plugin'ов, которым они не нужны (например, у ds18b20 нет on_action, у relay нет on_poll_all).

Зарегистрировать плагин — макрос:

REGISTER_SENSOR_PLUGIN(_relay_plugin);

Под капотом он раскрывается в:

__attribute__((used, section(".sensor_plugins"))) 
static const SensorPlugin* _sensor_plugin_relay = &_relay_plugin;

Linker собирает всё .sensor_plugins секции в один сегмент, ядро итерируется по нему — это и есть registry.

Контракты других категорий

Аналогично: TriggerPlugin, ConditionPlugin, ActionPlugin. Каждая структура — это набор function pointers с дополнениями:

  • Triggerparse, write, eval, build_summary, on_mesh_event.
  • Conditionparse, write, eval.
  • Actionparse, write, execute, build_summary.

Полные сигнатуры — в src/common/{sensors,triggers,conditions,actions}/<category>_plugin.h в репе ctrl-board.

Build-time DCE (Dead Code Elimination)

Plugin может быть «отключен» — тогда его .cpp вообще не попадает в компиляцию.

Делается через scripts/prebuild_<category>.py:

# pseudocode
enabled = json.load(open("data/config/sensors.enabled.json"))["enabled"]
filter_lines = []
for d in os.listdir("src/sensors"):
    if d not in enabled:
        filter_lines.append(f"-<sensors/{d}/*>")
env.Replace(BUILD_SRC_FILTER=env["BUILD_SRC_FILTER"] + filter_lines)

PlatformIO build_src_filter исключает указанные пути из сборки.

После этого linker DCE (с -ffunction-sections -fdata-sections -Wl,--gc-sections) выкидывает unused символы из остальных модулей. Итог: пустой sensors.enabled.json экономит 4 КБ RAM и 54 КБ Flash на ESP8266.

UI manifest

Каждый plugin может (опционально) приложить manifest для фронта — что показывать в форме создания задачи / устройства.

Формат:

{
  "label_key": "trg.type.interval",
  "fields": [
    {
      "name": "interval_sec",
      "kind": "number",
      "label_key": "trg.interval_sec",
      "min": 1, "max": 86400,
      "default": 60,
      "required": true
    }
  ]
}

Поле | Что:

  • name — имя поля в JSON конфига (например, tasks.json[].triggers[].interval_sec)
  • kindnumber / text / select / bool / device_select / time / cron_*
  • label_key — i18n-ключ из data/public/locales/*.json
  • min/max/default/required/maxlength/options — обычные form-validation поля
  • depends_on — условный показ: {"compare": ["above", "below"]} показывает поле только если compare ∈ {above, below}
  • filter_type — для device_select: фильтрует список устройств по device.type

Полный shape — в API → Plugins.

PROGMEM на ESP8266

UI-строки могут быть длинные (3–4 КБ на 17 плагинов = 68 КБ). На ESP8266 хранить их в DRAM критично невыгодно. Поэтому:

static const char UI_META[] PROGMEM = R"json({ ... })json";

ArduinoJson на ESP8266 умеет читать __FlashStringHelper через strncpy_P — UI отдаётся прямо из flash без копирования в RAM.

Без PROGMEM после добавления десяти плагинов плата начнёт падать с heap-exhaustion.

Структура одного плагина

src/sensors/ds18b20/
├── ds18b20.cpp      # сам код + REGISTER_SENSOR_PLUGIN
└── sensor.json      # манифест (ram/flash estimate, lib_deps, depends_on)

sensor.json читается prebuild-скриптом — он влияет на lib_deps (PlatformIO library deps), build_flags, и transit-dependencies между плагинами.

Пример:

{
  "name": "ds18b20",
  "types_provided": ["ds18b20"],
  "ram_bytes_estimate": 96,
  "flash_bytes_estimate": 4800,
  "lib_deps": ["paulstoffregen/OneWire@^2.3.7"],
  "depends_on": []
}

depends_on — другие plugin'ы, которые тоже должны быть enabled (транзитивно). Например, если udp_event зависит от mesh, написать "depends_on": ["mesh"].

Дальше