Каждый мой проект на ESP32 заканчивался одинаково: микроконтроллер готов, датчики читаются, реле щёлкают — а дальше надо руками писать HTML, CSS, JS и протокол поверх веб‑сокета, чтобы увидеть пару графиков в браузере. На третий раз я решил, что хватит, и сделал библиотеку RisalDash: интерфейс описывается на C++ в несколько строк, а весь фронтенд и протокол генерируются сами и отдаются самим контроллером.
cpp
dash.gauge ("Voltage", &volts, 0, 14, "V");
dash.chart ("Temperature", &temp, "C");
dash.toggle("Pump", &pump, [](bool on){ digitalWrite(PUMP, on); });
dash.begin(); // сохранённый Wi-Fi → подключиться; первый запуск → captive-портал
Под катом — два сюжета. Первый инженерный: как линкер помогает уместить 26 типов виджетов в килобайты и почему дашборд работает полностью офлайн. Второй честный: библиотеку я спроектировал сам, а большую часть кода написал в паре с ИИ под жёсткие лимиты памяти — расскажу, где это сработало, а где модель галлюцинировала и её приходилось ловить.
Почему не Blynk / ESPUI
— Blynk — это облако. Мне нужен был дашборд, который работает без интернета и без чужого сервера.
— ESPUI — ближе по духу (UI на самой плате), но визуально это «админка из 2010-х», и набор виджетов ограничен.
Хотелось: дизайн «как в приложении», полностью офлайн, real‑time, и чтобы добавление виджета было одной строкой — а не правкой HTML‑шаблона.
Идея Zero‑Waste: платить только за то, что используешь
Главное ограничение, которое я себе поставил: ничего лишнего не должно попадать во флеш. Если в скетче нет графика — в бинарнике не должно быть ни строчки CSS/JS графика.
Реализация опирается на то, как линкер выбрасывает мёртвый код (-ffunction-sections -fdata-sections + --gc-sections, в Arduino‑тулчейне это включено). Каждый тип виджета — отдельный класс со своими статическими PROGMEM‑строками CSS и JS. Пока виджет не инстанцирован, на него нет ссылок — и линкер выкидывает и класс, и его строки.
Замерил на ESP32 (Arduino), прирост флеша от первого использования каждого типа над голым ESPAsyncWebServer:
| Виджет | +флеш | Виджет | +флеш |
| led | ~1.3 КБ | metric | ~2.3 КБ |
| badge | ~1.6 КБ | table | ~2.4 КБ |
| ai | ~1.8 КБ | slider | ~2.4 КБ |
| number | ~1.8 КБ | gauge | ~3.1 КБ |
| toggle | ~2.0 КБ | chart | ~3.4 КБ |
Второй и последующие экземпляры того же типа — это уже единицы байт (объект + указатель). Неиспользуемый тип — ровно 0 байт. Это и есть весь смысл.
Отдельная боль — не держать ничего в RAM: HTML стримится прямо в сокет из PROGMEM кусками, без сборки большой String в памяти.
Offline‑first и captive‑портал
begin() на чистом устройстве поднимает точку доступа с captive‑порталом: телефон сам открывает страницу настройки, выбираешь сеть, вводишь пароль — креды и таймзона сохраняются в NVS, устройство перезагружается уже в STA. Системные шрифты, ноль внешних запросов, ноль CDN — дашборд живёт целиком во флеше.
Для real‑time — WebSocket, причём пушатся только изменившиеся значения; элементы управления зовут колбэки обратно в твой код.
Дизайн, который не раздувает флеш
UI сделан в стиле OKLCH «liquid glass»: полупрозрачные стеклянные карточки, статус‑бар в стиле iOS, многостраничность со свайп‑листом, мультиязык EN/RU/AR с полным RTL. Важный момент — всё это строки в PROGMEM, а не рантайм‑фреймворк: дизайн «дорогой» визуально, но дешёвый по флешу, и языки компилируются только те, что используешь.
ИИ как со‑разработчик: где сработало, где врал
Теперь честная часть. Библиотеку я архитектурно спроектировал сам — модульность виджетов, стратегию Zero‑Waste, разбиение на классы, контроль размера бинарника. Но рутину — генерацию 26 типов виджетов, парсинг пресетов сенсоров, мультиязычные строки, компактный CSS стеклянного UI — писал в связке с ИИ (Claude Code).
Что зашло хорошо:
— Однотипная масса. 26 виджетов по одному шаблону (css/js/html + сериализация) — ИИ генерит быстро и ровно.
— CSS/SVG. Компактные стеклянные стили и SVG‑иконки/дуги — экономия часов.
— Рефакторинг под ограничения. «Перепиши так, чтобы не держать String в RAM, стримь в Print&» — выполняется аккуратно.
Где приходилось ловить за руку:
— Указатели и время жизни в C++. Модель уверенно предлагала хранить ссылки на временные объекты — ровно тот класс багов, что молча роняет МК.
— Веб‑сокеты и асинхронность. Путала колбэки ESPAsyncWebServer, придумывала несуществующие сигнатуры.
— «Оптимистичные» цифры. В тексты лезли заниженные оценки размера («ядро <8 КБ, виджет ~200 байт») — я их перемерил и поправил на реальные (таблица выше). Технический читатель такое проверит первым делом.
Вывод без хайпа: ИИ под надзором архитектора — это сильный junior+, который кратно ускоряет рутину, но не заменяет того, кто ставит задачу, держит инварианты по памяти и проверяет каждое «магическое» число. Эффективный embedded в этой связке писать можно — но решает не модель, а постановка и контроль.
Бонус: устройство как инструмент для ИИ‑агента (MCP)
Раз уж про ИИ — добавил Model Context Protocol: dash.enableMCP("token") превращает каждый виджет в тулзу get_*/set_*. Через мост npx risal-dash-mcp Claude может прочитать датчик и щёлкнуть реле на реальной плате. Плюс есть автодискавери в Home Assistant через MQTT. То есть из тех же строк C++ получаешь и дашборд для человека, и интерфейс для агента.
Итог
— Zero‑Waste: неиспользуемый тип виджета = 0 байт; используемый = ~1.3–3.4 КБ.
— Offline‑first: AP + captive‑портал, ноль внешних запросов.
— Real‑time по WebSocket, 26 виджетов, пресеты сенсоров, EN/RU/AR + RTL, OTA, MQTT + Home Assistant, MCP.
— ESP32 + ESP8266, MIT.
Живое демо (код ↔ дашборд, без железа)
Буду рад фидбеку — особенно по API и по тому, какие виджеты/пресеты добавить. И отдельно интересно мнение про связку «архитектор + ИИ» в embedded: где у вас она реально экономила время, а где ломалась?
ссылка на оригинал статьи https://habr.com/ru/articles/1053508/