Карточка устройства¶
Это главная глава раздела. Здесь устройство получает интерфейс на портале и в мобильном приложении — без единой строчки кода на их стороне.
Как это работает¶
Устройство публикует card-манифест — машиночитаемое описание «что показать и чем управлять». Портал и приложение читают манифест и строят карточку: сенсоры становятся ячейками с живыми значениями, контролы — кнопками, полями ввода и списками. Разметку тоже можно задать из прошивки.
Вам не нужно ничего публиковать вручную: вы объявляете сущности через link.card(), а ядро само собирает манифест и отправляет его при подключении.
1. Объявляем сущности¶
Все объявления делаются в setup(), после s_link.begin(). У нашего фильтра три сущности: показание VOC, список режимов и поле порога. Разберём каждую отдельно, а в конце соберём блок целиком.
Общий принцип: id и label¶
У каждой сущности есть два имени, не путайте их:
- id — внутреннее, машинное имя (
"voc","mode"). Латиница, цифры, подчёркивание, без пробелов. По id сущность узнают разметка, команды и портал между собой. Придумали один раз — не меняете; - label — подпись для человека (
"VOC index","Mode"). Что напишете, то пользователь и увидит на карточке. Менять можно свободно.
Сенсор: показание VOC¶
s_link.card().sensor(
"voc", // id: внутреннее имя сущности
"VOC index", // label: подпись на карточке
"", // unit: единица измерения справа от числа ("°C", "%", "g");
// у VOC-индекса единицы нет — пустая строка
"units[0].vocIndex" // path: откуда брать значение — путь внутри JSON телеметрии.
// Это ТО САМОЕ поле, которое мы дописали в главе 5:
// doc["units"][0]["vocIndex"]. Имена должны совпадать
// буква в букву, иначе на карточке будет прочерк.
);
Сенсор — это ячейка «только чтение»: портал берёт значение из телеметрии по path и показывает. Никакой команды у сенсора нет.
Список выбора: режим работы¶
// Варианты списка. Пользователь увидит их в выпадающем меню как есть.
static const char* kModes[] = { "auto", "on", "off" };
s_link.card().select(
"mode", // id: внутреннее имя сущности
"Mode", // label: подпись на карточке
kModes, // options: массив вариантов (объявлен строкой выше)
3, // количество вариантов в массиве — auto, on, off = три.
// C++ сам не знает длину массива, её сообщаем мы
[](const char* opt) { // колбэк: функция, которую ядро вызовет, когда
// пользователь выберет вариант на портале.
// opt — выбранная строка, например "on"
onModeSelected(opt); // передаём её в нашу логику (напишем в главе 7)
}
);
Здесь появляется вторая половина механизма: управление. Когда пользователь выбирает вариант на портале, устройству прилетает команда, ядро само её принимает, проверяет (чужие строки, которых нет в options, до вас не дойдут) и вызывает ваш колбэк с выбранным значением. Разбирать MQTT-сообщения руками не нужно — ваша зона ответственности начинается внутри onModeSelected.
Числовое поле: порог срабатывания¶
s_link.card().number(
"threshold", // id: внутреннее имя сущности
"VOC threshold", // label: подпись на карточке
100, // min: меньше этого портал ввести не даст
400, // max: больше этого — тоже; ядро дополнительно
// обрежет значение по этим границам на своей стороне
10, // step: шаг изменения значения стрелками поля
"", // unit: единица измерения; у индекса её нет
[](float v) { // колбэк: вызывается, когда пользователь отправил
// новое значение; v — число в границах min..max
onThresholdChanged(v); // передаём в нашу логику (напишем в главе 7)
}
);
Собираем вместе¶
Финальный вид блока в setup() — то, что должно остаться в вашем коде. Функции onModeSelected и onThresholdChanged мы напишем в главе 7; чтобы код компилировался уже сейчас, объявите их заглушками выше setup():
// Заглушки: настоящие тела напишем в главе 7 (логика автоматики).
static void onModeSelected(const char* opt) {}
static void onThresholdChanged(float v) {}
void setup() {
Serial.begin(115200);
s_link.begin();
initVocSensor();
// Телеметрия: своё поле vocIndex (глава 5).
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// Карточка: сенсор + два органа управления.
s_link.card().sensor("voc", "VOC index", "", "units[0].vocIndex");
static const char* kModes[] = { "auto", "on", "off" };
s_link.card().select("mode", "Mode", kModes, 3, [](const char* opt) {
onModeSelected(opt);
});
s_link.card().number("threshold", "VOC threshold", 100, 400, 10, "", [](float v) {
onThresholdChanged(v);
});
}
А вентилятор? Его объявлять не нужно: флаг hasFan = true в Config уже добавил ячейку «Вентилятор» в манифест автоматически — это словарный навык, ядро знает о нём всё само.
Квадратные скобки в колбэках — всегда пустые
[](const char* opt) { ... } — это лямбда, безымянная функция; подробно мы разобрали её во врезке главы 5. Напомним правило ядра: скобки захвата всегда пустые ([]), внутрь лямбды ничего «с собой» не берём, всё нужное храним в глобальных переменных — как g_mode и g_threshold из следующей главы.
2. Автоматическая разметка карточки¶
Разметку можно вообще не задавать. Портал сам соберёт карточку из объявленных сущностей — и соберёт аккуратно: показания-ячейки группируются в ряды (до трёх в ряд, дальше перенос), органы управления идут ниже, каждый на своей строке, всё в фирменном оформлении портала. Для большинства устройств этого достаточно — интерфейс получается опрятным без единой мысли о вёрстке.
Порядок сущностей на карточке — порядок их объявления в setup().
3. Своя разметка карточки (по желанию)¶
Сначала — как устроена карточка. Карточка — это вертикальная стопка рядов. Ряд — горизонтальная полоса, в которой стоят от одной до четырёх сущностей; ширину карточки они делят поровну: одна сущность в ряду займёт всю ширину, две — по половине, три — по трети.
Автоматическая разметка из предыдущего раздела раскладывает сущности по этим рядам сама. Если хотите решать самостоятельно, что с чем стоит рядом, — задайте ряды вручную вызовами layoutRow. Один вызов = один ряд, порядок вызовов = порядок рядов сверху вниз:
// Ряд 1: две ячейки — индекс VOC и вентилятор, каждая по половине ширины.
s_link.card().layoutRow("voc", "fan");
// Ряд 2: два органа управления — режим и порог, тоже пополам.
s_link.card().layoutRow("mode", "threshold");
В layoutRow передаются id сущностей — те самые внутренние имена, которые вы дали им при объявлении (вот зачем id был нужен). "fan" — id словарной сущности вентилятора, её создал флаг hasFan.
На карточке это даст такую компоновку:
┌─ DIY Air Filter ────────────────┐
│ VOC index │ Вентилятор │ ← ряд 1: voc, fan
│ 103 │ Выкл │
├───────────────────┼─────────────┤
│ Mode [auto ▾] │ Threshold [150] │ ← ряд 2: mode, threshold
└─────────────────────────────────┘
Сущности, которые вы не упомянули ни в одном ряду, не пропадут — портал дорисует их ниже автоматическим списком. Так можно разметить только «главное», а остальное оставить автоматике.
4. Что уходит в эфир¶
Ядро опубликует в топик idryer/{serial}/card (retained):
{
"v": 1,
"entities": [
{ "id": "fan", "type": "binary_sensor", "device_class": "fan",
"source": "telemetry", "path": "units[0].fanStatus" },
{ "id": "voc", "type": "sensor", "label": "VOC index",
"source": "telemetry", "path": "units[0].vocIndex" },
{ "id": "mode", "type": "select", "label": "Mode",
"options": ["auto", "on", "off"], "action": "card.mode", "arg": "value" },
{ "id": "threshold", "type": "number", "label": "VOC threshold",
"min": 100, "max": 400, "step": 10, "action": "card.threshold", "arg": "value" }
],
"layout": [ ["voc", "fan"], ["mode", "threshold"] ]
}
Разбираться в этом JSON не обязательно — ядро генерирует его из ваших вызовов. Но знать о нём полезно: если вы пишете прошивку не на idryer-core (Rust, MicroPython, что угодно), достаточно опубликовать такой JSON самостоятельно — портал всеядный, лишь бы формат совпадал.
5. Проверка¶
Прошейте и откройте устройство на портале:
- ячейка VOC index показывает живой индекс (дуньте на датчик — число растёт при следующем обновлении);
- ячейка Вентилятор — Вкл/Выкл;
- Mode — выпадающий список, VOC threshold — поле с кнопкой отправки.
Выбор режима и порога пока ничего не делает — колбэки-заглушки. Оживим их в следующей главе.
Это и есть та самая концепция
Заметьте, что произошло: вы описали интерфейс пятью строками в прошивке — и он появился на портале и в приложении. Тот же приём работает для любого вашего устройства: меняются только id, подписи и колбэки.