1. О документе
Этот документ описывает внутреннее устройство прошивки IoTManager и объясняет, как написать собственный модуль — например, драйвер нового датчика, исполнительного устройства или виртуального элемента. Материал рассчитан на начинающего разработчика: предполагается, что вы знакомы с языком C++ на базовом уровне (классы, функции) и умеете писать простые скетчи для Arduino.
Главная идея: вам не нужно понимать всю прошивку целиком. Модуль — это один самодостаточный C++ класс плюс один файл описания modinfo.json. Ядро само найдёт ваш модуль, добавит его в веб-интерфейс и будет вызывать ваш код в нужные моменты.
1.1. Что вам понадобится
- Установленный PlatformIO (расширение для VS Code) — среда сборки прошивки.
- Python 3 — используется скриптом подготовки проекта
PrepareProject.py. - Плата esp8266 или esp32, USB-кабель и сам датчик/устройство, для которого пишется модуль.
- Готовый шаблон
ExampleModule— он уже лежит в проекте и подробно прокомментирован. Это ваша стартовая точка.
2. Как устроена прошивка (кратко)
Чтобы писать модули уверенно, достаточно понять несколько ключевых понятий. Не нужно разбираться в сетевом стеке, MQTT или веб-сервере — всё это ядро берёт на себя.
2.1. Ядро и модули
Прошивка состоит из ядра (папки src/, include/) и модулей (папка src/modules/). Ядро отвечает за Wi-Fi, MQTT, веб-интерфейс, хранение настроек, выполнение сценариев и периодический опрос модулей. Модули отвечают только за конкретное «железо».
Модули разложены по категориям (это же деление вы увидите в меню веб-интерфейса):
| Папка | Раздел меню | Назначение | Кол-во |
|---|---|---|---|
sensors | Сенсоры | Чтение датчиков (температура, давление, ток…) | 49 |
exec | Исполнительные | Управление устройствами (реле, ШИМ, серво, Telegram…) | 30 |
display | Экраны | Вывод на дисплеи и адресные ленты | 12 |
virtual | Виртуальные | Расчёты без железа (математика, таймеры, логирование…) | 18 |
2.2. Базовый класс IoTItem
Любой модуль — это класс, унаследованный от IoTItem (объявлен в include/classes/IoTItem.h). Этот базовый класс даёт вашему модулю всё необходимое: хранение значения, интервал опроса, регистрацию событий и связь с ядром.
Самые важные части, которые вы будете переопределять или использовать:
| Элемент | Что делает |
|---|---|
Конструктор(String parameters) | Аналог setup(): читает настройки из веб-интерфейса, инициализирует железо. |
doByInterval() | Вызывается каждые int секунд. Здесь читаете датчик и регистрируете значение. |
loop() | Полный аналог loop() Arduino. Переопределять только при необходимости. |
regEvent(...) | Сообщает ядру новое значение. Обязательный вызов. |
value | Структура IoTValue с полями valD (число) и valS (строка) — текущее значение. |
execute(cmd, param) | Обрабатывает команды, вызванные из сценариев. |
onModuleOrder(key, value) | Обрабатывает нажатия кнопок с панели модуля. |
~Деструктор() | Освобождает ресурсы при переконфигурации. |
2.3. Структура значения IoTValue
Всё, что модуль «отдаёт» ядру, хранится в простой структуре:
struct IoTValue {
float valD = 0; // числовое значение (температура, ток...)
String valS = ""; // строковое значение (текст, статус...)
bool isDecimal = true; // true = используется valD, false = valS
};
3. Жизненный цикл модуля
Понимание того, когда и в каком порядке ядро вызывает ваш код, — ключ к написанию рабочего модуля. Вот полный путь от старта прошивки до получения значения:
- Пользователь добавляет элемент в веб-интерфейсе и заполняет поля (пин, интервал и т. д.). Настройки сохраняются как JSON.
- Ядро вызывает функцию
getAPI_ИмяМодуля(), передавая ейsubtypeи строку параметров. - Функция создаёт объект вашего класса — срабатывает конструктор, в котором вы читаете параметры и инициализируете железо.
- Каждые
intсекунд ядро вызываетdoByInterval()— вы читаете датчик и вызываетеregEvent(). - Ядро распространяет значение: показывает в веб-интерфейсе, шлёт в приложение через MQTT, делает доступным сценариям.
- При переконфигурации объект удаляется — срабатывает деструктор.
4. Анатомия модуля: три файла
Каждый модуль живёт в своей папке внутри нужной категории, например src/modules/sensors/МойДатчик/. Внутри — обычно три файла:
| Файл | Обязателен | Назначение |
|---|---|---|
МойДатчик.cpp | Да | Код модуля: классы, логика чтения, функция getAPI. |
modinfo.json | Да | Описание модуля: поля настроек, автор, библиотеки, меню. |
Библиотека.h / .cpp | Нет | Своя или сторонняя библиотека, если нужна. |
4.1. Файл modinfo.json — паспорт модуля
Этот файл ядро читает до компиляции. Из него строятся: пункт меню, форма настроек в веб-интерфейсе, список подключаемых библиотек и справка. Разберём его по частям на примере из ExampleModule.
"menuSection": "sensors",
Определяет, в какой раздел меню попадёт модуль. Допустимые значения: sensors, executive_devices, screens, virtual_elments.
Блок configItem — поля настроек
Массив описывает элементы, которые пользователь сможет добавить. Каждый объект — это один параметр (один класс в .cpp):
"configItem": [
{
"name": "Пример датчика А", // название в списке
"type": "Reading", // тип элемента
"subtype": "ExampleModule_A", // связь с классом в .cpp !!!
"id": "Tmp", // идентификатор по умолчанию
"widget": "anydataTmp", // виджет отображения
"page": "Сенсоры", // вкладка в интерфейсе
"descr": "Температура", // подпись
"int": 15, // интервал опроса, сек
"pin": "32", // пользовательский параметр
"round": 1
}
]
Любое поле, добавленное в configItem (например "pin": "32"), автоматически появится как поле ввода в веб-форме, и его можно будет прочитать в конструкторе через jsonReadInt(parameters, "pin").
Блок about — метаданные и справка
"about": {
"authorName": "Иван Иванов",
"authorContact": "https://t.me/ivan",
"moduleName": "ExampleModule", // = имя в getAPI_ !!!
"moduleVersion": "1.0",
"usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 },
"title": "Название модуля",
"moduleDesc": "Что делает модуль и его особенности",
"propInfo": { // всплывающие подсказки к полям
"pin": "GPIO, к которому подключён датчик",
"int": "Секунд между опросами"
},
"funcInfo": [ ... ] // описание функций для сценариев
}
Блоки defActive и usedLibs
"defActive": false, // включён ли модуль по умолчанию
"usedLibs": { // сторонние библиотеки для каждой платы
"esp32*": [ "milesburton/DallasTemperature @ ^4.0.4" ],
"esp82*": [ "milesburton/DallasTemperature @ ^4.0.4" ]
}
В usedLibs перечисляются зависимости PlatformIO. Ключи esp32* и esp82* — маски для семейств плат. Если библиотека не нужна — оставьте пустые массивы, как в шаблоне.
5. Файл .cpp — код модуля
Разберём структуру ExampleModule.cpp сверху вниз. Это готовый рабочий шаблон — вы копируете его и заменяете имена на свои.
5.1. Подключения в начале файла
#include "Global.h" // ядро IoTM (обязательно)
#include "classes/IoTItem.h" // базовый класс (обязательно)
#include <DallasTemperature.h> // сторонняя библиотека (если нужна)
extern IoTGpio IoTgpio; // объект для работы с GPIO
Первые два include — обязательны всегда. Сторонние библиотеки подключаются в угловых скобках < > и должны быть перечислены в usedLibs. Объект IoTgpio даёт кроссплатформенные методы analogRead(), digitalWrite() и т. п.
5.2. Класс модуля
Класс наследуется от IoTItem. Имя класса = subtype из JSON. Минимальный рабочий сенсор выглядит так:
class ExampleModule_A : public IoTItem {
private:
unsigned int _pin; // храним номер пина
public:
// --- КОНСТРУКТОР (аналог setup) ---
ExampleModule_A(String parameters) : IoTItem(parameters) {
_pin = jsonReadInt(parameters, "pin"); // читаем поле "pin"
jsonRead(parameters, "int", _interval, false);
// здесь же: sensor.begin(), pinMode() и т. п.
}
// --- ЧТЕНИЕ ПО ИНТЕРВАЛУ (аналог loop) ---
void doByInterval() {
value.valD = analogRead(_pin) * 0.1; // считаем значение
regEvent(value.valD, "ExampleModule"); // отдаём ядру
}
// --- ДЕСТРУКТОР ---
~ExampleModule_A() { /* освобождаем ресурсы */ };
};
Чтение параметров из настроек
Все поля из configItem приходят в конструктор одной JSON-строкой parameters. Для чтения есть готовые функции:
| Функция | Возвращает | Пример |
|---|---|---|
jsonReadInt(p, "pin") | int | номер пина 32 |
jsonReadStr(p, "id") | String | идентификатор |
jsonReadBool(p, "flag") | bool | true / false |
jsonRead(p, "int", var, false) | в переменную | интервал, множитель… |
5.3. Функция-мост getAPI
В самом конце файла — функция, через которую ядро создаёт объекты вашего класса. Её имя должно быть getAPI_ + moduleName из JSON:
void *getAPI_ExampleModule(String subtype, String param) {
if (subtype == "ExampleModule_A") {
return new ExampleModule_A(param);
}
else if (subtype == "ExampleModule_B") {
return new ExampleModule_B(param);
}
else {
return nullptr; // этот subtype не наш
}
}
Если у модуля несколько параметров (несколько классов), перечислите их все через else if. Каждый subtype в проверке должен совпадать с subtype в modinfo.json.
6. Дополнительные возможности
Базового сенсора достаточно для чтения данных. Но ядро позволяет модулю делать больше — реагировать на кнопки, участвовать в сценариях, читать значения других модулей.
6.1. Кнопки на панели модуля (onModuleOrder)
Чтобы у модуля появилась кнопка, добавьте в configItem поле "btn-ИмяКнопки", а в классе переопределите:
void onModuleOrder(String &key, String &value) {
if (key == "Example") { // имя из "btn-Example"
SerialPrint("i", "MyModule", "Нажата кнопка: " + value);
// выполняем калибровку, сброс и т. п.
}
}
6.2. Команды из сценариев (execute)
Сценарии IoTManager могут вызывать функции вашего модуля. Для этого переопределите execute() и опишите функции в блоке funcInfo файла JSON:
IoTValue execute(String command, std::vector<IoTValue> ¶m) {
if (command == "myFunc") {
if (param.size() >= 1) {
String otherId = param[0].valS;
String v = getItemValue(otherId); // значение другого модуля
// делаем что-то полезное
}
}
return {}; // возвращаем пустое значение или результат
}
Аргументы приходят в векторе param; каждый — это IoTValue с полями valS и valD. Функция getItemValue(id) читает текущее значение любого другого элемента по его идентификатору.
6.3. Свой loop() и общие библиотеки
Если нужен непрерывный опрос (не по интервалу), переопределите loop(). Обязательно вызовите в конце IoTItem::loop(), иначе перестанет работать doByInterval():
void loop() {
adc = IoTgpio.analogRead(_pin); // ваша непрерывная логика
IoTItem::loop(); // ОБЯЗАТЕЛЬНО в конце
}
Когда несколько экземпляров используют один физический интерфейс (I2C, шину), заведите общий объект библиотеки через функцию-инициализатор, как показано в шаблоне (instanceLibXX(pin)). В деструкторе такой объект удаляют, чтобы при переконфигурации обновились параметры.
7. Практика: создаём модуль с нуля
Соберём всё вместе. Создадим простой аналоговый датчик освещённости на фоторезисторе, подключённом к аналоговому пину.
Шаг 1. Скопировать шаблон
Скопируйте папку src/modules/sensors/ExampleModule в новую src/modules/sensors/LightSensor. Переименуйте файл .cpp в LightSensor.cpp.
Шаг 2. Заполнить modinfo.json
{
"menuSection": "sensors",
"configItem": [{
"name": "Датчик освещённости",
"type": "Reading",
"subtype": "LightSensor",
"id": "Light",
"widget": "anydataLux",
"page": "Сенсоры",
"descr": "Освещённость",
"int": 10,
"pin": "34",
"round": 1
}],
"about": {
"authorName": "Ваше имя",
"moduleName": "LightSensor",
"moduleVersion": "1.0",
"usedRam": { "esp32_4mb": 5, "esp8266_4mb": 5 },
"title": "Датчик света",
"moduleDesc": "Читает фоторезистор с аналогового пина",
"propInfo": {
"pin": "Аналоговый GPIO",
"int": "Секунд между опросами"
}
},
"defActive": false,
"usedLibs": { "esp32*": [], "esp82*": [] }
}
Шаг 3. Написать LightSensor.cpp
#include "Global.h"
#include "classes/IoTItem.h"
extern IoTGpio IoTgpio;
class LightSensor : public IoTItem {
private:
unsigned int _pin;
public:
LightSensor(String parameters) : IoTItem(parameters) {
_pin = jsonReadInt(parameters, "pin");
}
void doByInterval() {
int raw = IoTgpio.analogRead(_pin); // 0..4095
value.valD = raw / 40.95; // грубо в проценты
regEvent(value.valD, "LightSensor");
}
~LightSensor() {};
};
void *getAPI_LightSensor(String subtype, String param) {
if (subtype == "LightSensor") {
return new LightSensor(param);
}
return nullptr;
}
Шаг 4. Подготовить проект
Запустите скрипт подготовки — он найдёт новый модуль и обновит списки:
python PrepareProject.py --update
Затем включите модуль в вашем профиле (myProfile.json) или через веб-конфигуратор и снова выполните PrepareProject.py для нужной платы, например:
python PrepareProject.py -b esp32_4mb
Шаг 5. Скомпилировать и прошить
Соберите и загрузите прошивку через PlatformIO (кнопка Upload или pio run -t upload). После перезагрузки новый датчик появится в списке элементов веб-интерфейса.
7.1. Что делает PrepareProject.py «под капотом»
Понимание этого шага помогает отлаживать проблемы «модуль не появился»:
- Сканирует
src/modules/в поисках всехmodinfo.json. - Для активных модулей собирает список библиотек и пути и записывает их в
platformio.ini. - Генерирует
src/modules/API.cpp— файл, который вызывает всеgetAPI_…функции по очереди. - Формирует
data_svelte/items.json— список элементов для меню веб-интерфейса.
8. Чек-лист и типичные ошибки
8.1. Контрольный список перед сборкой
- Имя папки, файла
.cppиmoduleName— согласованы. - Имя функции
getAPI_X=getAPI_+moduleName. - Каждый
subtypeвmodinfo.jsonимеет свой класс и ветку вgetAPI. - В
doByInterval()есть хотя бы одинregEvent(). - Все сторонние библиотеки перечислены в
usedLibs. - Нет
delay()и долгих циклов. - Запущен
PrepareProject.pyпосле изменений.
8.2. Частые проблемы
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Модуль не появился в меню | не запущен PrepareProject / defActive=false | запустить скрипт, включить модуль |
| Ошибка линковки getAPI | moduleName ≠ имя функции | привести имена к единому виду |
| Значение всегда 0 / пустое | нет regEvent или не тот пин | проверить doByInterval и параметр pin |
| Виснут другие датчики | delay или долгий цикл в модуле | убрать задержки, разбить на такты |
| Библиотека не найдена | не указана в usedLibs | добавить зависимость для нужной платы |
| Старые параметры после смены пина | общий объект не пересоздан | удалять объект библиотеки в деструкторе |
9. Справочник
9.1. Полезные методы IoTItem
| Метод | Назначение |
|---|---|
regEvent(val, info) | зарегистрировать новое значение (число или строка) |
getItemValue(id) | получить значение другого элемента по id |
findIoTItem(name) | найти объект другого модуля |
isItemExist(name) | проверить, существует ли элемент |
getID() | получить идентификатор текущего элемента |
getSubtype() | получить subtype текущего элемента |
setInterval(ms) | изменить интервал опроса |
SerialPrint("i", tag, msg) | вывод в лог/консоль |
9.2. Поддерживаемые платы (профили сборки)
Задаются параметром -b при запуске PrepareProject.py. Основные:
| Профиль | Платформа |
|---|---|
esp8266_4mb / esp8266_16mb | ESP8266 с 4/16 МБ флеш |
esp8266_1mb / esp8285_1mb | компактные ESP8266/8285 |
esp32_4mb / esp32_16mb | ESP32 классический |
esp32cam_4mb | ESP32-CAM (с камерой) |
esp32s2_4mb / esp32c3m_4mb | ESP32-S2 / C3 |
esp32s3_16mb / esp32c6_4mb | ESP32-S3 / C6 |
9.3. Полезные ресурсы
- Репозиторий проекта: github.com/IoTManagerProject/IoTManager
- Готовые модули как примеры: папки
src/modules/sensors/— изучайтеNtc,Ds18b20,AnalogAdc. - Шаблон-заготовка:
src/modules/sensors/ExampleModule/— подробно прокомментирован. - Учебные примеры очередей и JSON: папка
training/.
ver4stableИсходный код: github.com/IoTManagerProject/IoTManager