Архитектура плагинов¶
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).
Зарегистрировать плагин — макрос:
Под капотом он раскрывается в:
__attribute__((used, section(".sensor_plugins")))
static const SensorPlugin* _sensor_plugin_relay = &_relay_plugin;
Linker собирает всё .sensor_plugins секции в один сегмент, ядро итерируется по нему — это и есть registry.
Контракты других категорий¶
Аналогично: TriggerPlugin, ConditionPlugin, ActionPlugin. Каждая структура — это набор function pointers с дополнениями:
- Trigger —
parse,write,eval,build_summary,on_mesh_event. - Condition —
parse,write,eval. - Action —
parse,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)kind—number/text/select/bool/device_select/time/cron_*label_key— i18n-ключ изdata/public/locales/*.jsonmin/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 критично невыгодно. Поэтому:
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"].
Дальше¶
- Свой плагин: пошагово — практический гайд от пустой папки до working trigger в UI.
- API → Plugins — endpoints
/api/v1/sensors,/api/v1/triggers, ... и schema response'ов.