Сверка документации с кодом выявила четыре расхождения, все появились
после вчерашней правки протокола:
- схема в шапке и пример вывода агента показывали строку без ключа M,
хотя агент его шлёт: человек сверил бы свой вывод с образцом и решил,
что у него что-то не так;
- в таблице состава не были упомянуты кнопка и выбор экрана;
- формулировка "короткое нажатие" подразумевала различие с длинным,
которого в коде нет.
Все 16 констант из таблицы настроек сверены с config.h, поведенческие
утверждения - с кодом, расход прошивки подтверждён сборкой: 26% и 4%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Второй экран показывает загрузку ОЗУ крупной цифрой и её график со
шкалой 0-100 - у памяти она естественная, подбирать нечего. В углу свой
значок, планка памяти вместо микросхемы: он же и говорит, какой экран
открыт.
Тревога в приоритете: при перегреве плата принудительно возвращается на
экран процессора и кнопка не работает, пока температура не упадёт. Смысл
тревоги в том, что её нельзя прощёлкать.
Экран памяти выпадает из листания, если агент не шлёт ключ M - со старым
агентом кнопка просто ничего не делает. То же решение подойдёт машинам
без дискретной видеокарты.
Выбранный экран не запоминается при перезагрузке: хранить негде.
Тревоги по памяти нет намеренно - загрузка ОЗУ под 100% здесь обычное
рабочее состояние.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прошивка её пока не показывает - на 72x40 нет места под третий
показатель. Это намеренно: неизвестный ключ прошивка игнорирует, поэтому
агента можно обновлять независимо от плат.
Память берётся у железа с именем Total Memory - это физическая память.
У соседнего Virtual Memory датчик называется точно так же, но считает
вместе с файлом подкачки и показывает заниженную цифру: 87% против
фактических 97%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Выбран приоритетный экран, а не листалка: устройство смотрится боковым
зрением и должно само показывать то, что ближе к своему порогу.
Совмещённый экран отвергнут по измерениям - вторая температура влезает
только шрифтом 5x7, и смысл крупной цифры теряется.
Отдельным пунктом записан опыт, который надо поставить до реализации:
проверить, будит ли опрос дискретную видеокарту и не начнёт ли мониторинг
сам расходовать батарею. Измеритель есть - датчик мощности разряда
батареи, прав администратора он не требует.
Записана и главная ловушка подхода: при близких к порогам показателях
экран начнёт скакать между ними, нужна задержка переключения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено перебором через LibreHardwareMonitorLib после установки PawnIO.
Ключевой вывод: температура видеокарты NVIDIA читается без прав
администратора, потому что идёт через её драйвер, а не через MSR. Админ
нужен только процессору.
Дисков и датчиков материнской платы с вентиляторами на этой машине нет.
Кандидаты на экраны расставлены по возрастанию стоимости: видеокарта,
память, батарея, сеть. Последняя требует автоматической смены масштаба,
потому что скорость гуляет от килобайт до мегабайт.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мгновенное значение плавает на +-1 градус, и цифра мельтешила каждую
секунду при полном покое. На экран теперь идёт среднее за
TEMP_AVG_SAMPLES последних замеров; агент шлёт раз в секунду, так что
это ровно 5 секунд.
Сумма пересчитывается заново на каждом шаге, а не копится: на пяти
числах это бесплатно, зато нет накопления ошибки округления за сутки
непрерывной работы. Окно сбрасывается при обрыве связи, иначе после
паузы среднее считалось бы по значениям до обрыва.
Тревога переведена на усреднённое значение: иначе экран показывал бы 84,
когда мигание уже идёт. Загрузка и график намеренно остались сырыми.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первый пункт - другие показатели (GPU, память, диски) и листалка экранов
кнопкой BOOT. Протокол к этому уже готов: незнакомые ключи прошивка
игнорирует, так что агент может начать слать новые поля, не ломая уже
прошитые платы.
Там же записаны открытые хвосты: обрезание строки при 100 градусах и
100% загрузки, непроверенный перезагрузкой автозапуск, ручной цикл
заливки и вес обзорного изображения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
К изображению дана сноска с двумя поправками: экран на плате 0.42 дюйма,
а не 0.96, и график строится по температуре, а не по загрузке.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Полоса y=0..10 справа выбрана по измерениям на самой плате, а не на глаз:
строка загрузки лежит ниже (y=11..24), а цифры температуры не дотягиваются
туда даже трёхзначными (49 px + градус = 56 из 72). Значок 11x11 не
перекрывает ничего ни при каких значениях и не исчезает под нагрузкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прежний README описывал устройство проекта, но по нему нельзя было
собрать всё с нуля. Теперь это инструкция: что установить, какие пункты
меню выставить, как определить пины дисплея, как поставить агента и
автозапуск, как проверить без агента.
Отдельно вынесены грабли, на которые мы наступили: USB CDC On Boot по
умолчанию выключен, LibreHardwareMonitor требует PawnIO, разделитель
дробной части должен быть точкой, .ps1 нужен UTF-8 с BOM, порт надо
освобождать перед заливкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Настройки собраны в config.h: пины, ориентация, пороги тревоги, шкала
графика, тайминги и скорость Serial. Раньше они были раскиданы по
модулям, и подстройка требовала лазить по коду.
Тревога включается на ALARM_ON_C и отпускает только на ALARM_OFF_C
(85 и 82 градуса). Один порог заставлял бы её дёргаться каждую секунду,
пока температура топчется на границе.
В тревоге экран мигает негативом (инверсия буфера, а не команда
контроллера - не зависит от микросхемы дисплея), светодиод GPIO8 мигает
с ним в общей фазе. Без данных светодиод горит ровно, в норме погашен.
Зажигается низким уровнем: проверено опытом, заодно в простое пин стоит
высоким, что безопасно для GPIO8 как пина начальной загрузки.
Переменная alarm переименована в alarmActive: имя alarm занято функцией
из unistd.h, которая попадает в сборку ESP32.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ориентация вынесена в константу DISPLAY_ROTATION: менять её теперь
правкой одной строки, а не параметра в конструкторе.
Знак градуса поднят с базовой линии 10 на 8 - встал надстрочным знаком
вровень с верхом цифр. Загрузка CPU выводится шрифтом 7x14 вместо 5x7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Плата принимает по USB-CDC строки "T=67.0;L=34" от агента на ПК и рисует
крупную цифру, загрузку и график за последнюю минуту на экране 72x40.
Пины дисплея (SDA=5, SCL=6) определены сканером, а не по шелкографии:
подписи GPIO8/GPIO9 на плате - это дефолтные Wire-пины чипа.
Агент читает сенсоры через LibreHardwareMonitorLib. С версии 0.9.5 она
требует отдельно установленный драйвер PawnIO: без него загрузка CPU
читается, а температуры пусты даже под администратором.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Снимок с работающего прибора: плашка «Подача есть», лампы датчиков и
выходов, раскрытый блок настроек. Вставлен в README (раздел 7) и в
MANUAL (раздел 5).
На снимке нет чувствительных данных: адрес уведомлений показан только
именем хоста и это заглушка, IP приватный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Документация утверждала, что реле при загрузке молчат. На собранном
приборе оба реле кратковременно моргают при перезагрузке; поведение
признано некритичным, прошивка не менялась, исправлен текст.
На собранном приборе оба реле кратковременно моргают при перезагрузке.
README и MANUAL утверждали обратное - что реле при загрузке молчат.
Это было предсказание из рассуждения об уровнях на пинах, а не
наблюдение, и оно не подтвердилось. Формулировки исправлены.
Вероятный механизм описан честно, как вероятный: оба выхода сидят на
служебных пинах (GPIO2 это UART1 TX, GPIO0 - пин выбора режима загрузки),
и пока setup() не выполнился, уровнем управляет не прошивка. Осциллографом
не проверяли, о чём в тексте сказано.
Само решение остаётся верным: альтернативный D8 подтянут к LOW и держал бы
реле уверенно включённым всю загрузку вместо короткого моргания.
Пользователь признал поведение некритичным, прошивка не менялась. В
документации указано, чем лечится, если однажды станет мешать.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Веб-панель переделана в световое табло: главный ответ читается с
расстояния, цвета по конвенции сигнальных ламп, настройки свёрнуты.
Исправлено: лампа выхода «Авария» горела зелёным при срабатывании,
обновление стирало ввод в форме, шрифты тянулись с Google Fonts и на
объекте не загружались.
Страница переехала во флеш (PROGMEM) и в отдельный файл web_page.h -
последнее вынужденно: генератор прототипов Arduino не понимает сырые
строковые литералы и ломался на JavaScript внутри.
OLED: верхняя строка отдана статусу кеглем 2, добавлено состояние реле.
В теле webhook поле status теперь содержит человеческую фразу
(«Насосы стоят, подачу держит резерв»), машиночитаемое значение
переехало в новое поле state.
Прошито и проверено на приборе: COM7, LOLIN D1 ESP-WROOM-02, 2MB FS:64KB.
В теле уведомления status теперь содержит ту же формулировку, что видит
оператор в панели: «Насосы стоят, подачу держит резерв». Прежние
машиночитаемые значения переехали в новое поле state (ALARM / OK /
STARTED / STOPPED), чтобы сценарий на сервере ветвился по нему, а не
разбирал русский текст.
ЛОМАЮЩЕЕ ИЗМЕНЕНИЕ: если сценарий в n8n сравнивал status с "ALARM",
условие надо переключить на state.
Формулировки собраны в одном месте прошивки (statusKind / statusWord /
statusSub / statusPhrase) и оттуда попадают и в webhook, и в панель через
новые поля k, w, s в /status. Раньше та же логика была продублирована в
JS панели; два набора фраз разошлись бы при первой правке. Побочно JS
сократился почти вдвое.
Прошито и проверено на приборе: /status отдаёт
{"k":"ok","w":"Подача есть","s":"работает насос 1"}, /config по-прежнему
отдаёт только имя хоста без токена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Генератор прототипов Arduino не понимает сырые строковые литералы: внутри
JS он нашёл "function draw(d){" и "function poll(){", принял их за
определения функций C++ и вставил для них прототипы в начало файла.
Сборка падала с "error: 'function' does not name a type", а директивы
#line показывали ошибку на строках внутри HTML.
Заголовочные файлы препроцессор не сканирует, поэтому страница переехала
в web_page.h. Тот же приём используется в соседнем проекте.
Ошибка не всплывала раньше потому, что я проверял скетч через
g++ -fsyntax-only по .ino: так файл компилируется как обычный .cpp и
препроцессор Arduino не участвует. Проверять сборку нужно arduino-cli,
это записано в README 11.1.
Прошито и проверено на железе (COM7, LOLIN D1 ESP-WROOM-02, 2MB FS:64KB):
размеры флеша совпадают, LittleFS монтируется, ранее заданные через панель
настройки пережили перепрошивку, Wi-Fi поднимается, датчики читаются.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Панель как световое табло, а не дашборд: главный ответ занимает верх
страницы и читается с расстояния. Прежде самым крупным элементом были
часы браузера - единственное, что не несло информации о приборе, - а
ответ "есть подача или нет" был набран 11 кеглем в панели размером с
кнопку. Норма и авария выглядели почти одинаково.
Исправлены три содержательные ошибки:
- лампа выхода "Авария" горела ЗЕЛЁНЫМ при срабатывании: стиль
"включено" был общим для всех ламп, и сработавшая сигнализация
подсвечивалась цветом "всё хорошо";
- страница делала location.reload() при любом изменении состояния,
стирая недописанный адрес в форме вместе с прокруткой и фокусом;
- CSS подключал шрифты с Google Fonts, тогда как сеть на объекте не
разрешает внешние имена - нарисованный макет там не появлялся никогда.
Страница стала статической и уехала во флеш (PAGE_HTML PROGMEM, ~8 КБ,
отдаётся через send_P). Раньше HTML собирался строкой в куче на каждый
запрос: ~14 КБ при ~40 КБ свободной кучи. Состояние подставляет JS,
забирая /status и новый /config. В /status добавлено поле rdy.
/config отдаёт только имя хоста webhook: полный URL содержит секретный
токен, и выводить его на экран, видимый всем в помещении, незачем.
Цвета по конвенции сигнальных ламп: резерв теперь ЖЁЛТЫЙ, а не красный -
работающий резерв это предупреждение, система ещё справляется. Красный
оставлен для остановки основных. Убраны декоративные коды SYS_OK / 0xFF,
не кодировавшие ничего, и тройное дублирование состояния резерва.
Настройки свёрнуты в details.
OLED: верхняя строка отдана статусу кеглем 2 - прежде её постоянно
занимал заголовок "PUMP CONTROLLER v8", который никогда не менялся.
Три строки датчиков сжаты в одну, освободившееся место отдано состоянию
обоих реле. Текст остаётся латиницей: встроенный шрифт Adafruit GFX
кириллицы не содержит.
Проверено: сборка -Wall -Wextra без предупреждений; все три состояния
панели просмотрены в браузере на 375 и 800 px. На железе не проверялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Добавлен проект контроллера насосной станции на Wemos D1 mini вместе с
его историей: мониторинг двух основных и резервного насоса, два реле
аварий на D3/D4 (модули active-LOW), OLED, веб-панель с настройками и
состоянием реле, уведомления на HTTPS-webhook, настройки в LittleFS.
Также: .gitignore переведён на белый список, чтобы рабочий каталог
D:\Soft\Arduino мог служить рабочим деревом репозитория, не втягивая
в него старые версии скетчей, архивы и драйверы.
Локально все скетчи лежат вперемешку в одной папке: рабочие проекты,
старые версии, эксперименты, архивы и драйверы. Чтобы этот каталог мог
служить рабочим деревом репозитория, .gitignore инвертирован — по
умолчанию игнорируется всё, проекты добавляются точечно строкой
"!/ИмяПапки/".
Прежние правила (артефакты сборки, мусор ОС, secrets.h) сохранены и
перенесены ниже белого списка: более поздний шаблон побеждает, поэтому
они продолжают действовать внутри отслеживаемых папок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Контроллер насосной станции на Wemos D1 mini: мониторинг двух основных
и резервного насоса, два реле аварий, OLED, веб-панель с настройками,
уведомления на HTTPS-webhook.
История проекта влита целиком, в сообщениях коммитов разобраны причины
принятых решений: выбор пинов D3/D4 под реле active-LOW, ограничение
блокировки loop() при отправке webhook, перенос настроек в LittleFS и
найденная причина несохранения настроек (чип 2 МБ при настройке IDE 4 МБ).
Комментарий утверждал, что запись в EEPROM на этой плате проваливается,
что читается как «EEPROM сломана». На деле причина была в неверном
Flash Size, и с верной настройкой работали бы оба механизма. Формулировка
исправлена, чтобы следующий читатель не пошёл менять хранилище вместо
проверки размера чипа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прошивка запущена на целевой плате: LittleFS монтируется и отдаёт
45056 Б, файл настроек читается при старте. В документации указан
фактически использованный вариант Flash Size вместо предполагавшегося
FS:128KB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Плата имеет физически 2 МБ флеша, в IDE было выбрано 4MB (FS:2MB).
При этой раскладке сектор EEPROM лежит по смещению 0x3FB000, а начало
LittleFS — по 0x400000, тогда как чип заканчивается на 0x200000. Обе
области вне микросхемы, поэтому spi_flash_erase_sector() проваливался
и EEPROM.commit() возвращал false.
Лечится настройкой Flash Size = 2MB (FS:128KB OTA:~960KB), а не кодом:
любая раскладка 2MB кладёт файловую систему ниже 0x1FB000, а EEPROM в
сектор 511. С верной настройкой работали бы оба механизма хранения.
Ранее записанная в README гипотеза про «щель между FS и областью SDK»
была неверной и заменена подтверждённым разбором. Переход на LittleFS
остаётся оправданным по своим качествам, но проблему решает настройка
IDE — в документации это разделено явно, чтобы не ввести в заблуждение
при похожем симптоме на другой плате.
Документация исправлена: плата 2 МБ, а не 4 МБ; требование совпадения
выбранного размера с физическим; предупреждение о потере учётных данных
Wi-Fi при смене раскладки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EEPROM.commit() на целевой плате возвращает false — падает сам вызов SDK.
Проверкой исходников исключены ошибка в библиотеке, перекрытие адресов и
стирание сектора со стороны WiFiManager. Физический размер флеша тоже ни
при чём: LittleFS на этом же чипе работает, что подтверждено соседним
проектом на такой же плате.
Наиболее вероятный механизм: при раскладке 4MB/FS:2MB сектор EEPROM (1019,
0x3FB000) попадает в щель между концом ФС (0x3FA000) и служебной областью
SDK (0x3FC000) и оказывается защищён от стирания. Доказать не удалось,
поэтому в документации гипотеза помечена как гипотеза. Переход на LittleFS
выбран не как обход симптома, а как переезд на подтверждённо работающий
на этом железе механизм.
Настройки теперь в /settings.json (ArduinoJson v7). Загрузка
отказоустойчива: отсутствующий или битый ключ оставляет значение по
умолчанию, поэтому файл от прошлой версии прошивки не ломается при
добавлении полей. Имена ключей вынесены в константы KEY_*, используемые и
при чтении, и при записи.
Молчаливой потери настроек больше нет: при недоступной ФС взводится
settingsFsReady = false, и веб-панель показывает предупреждение, что
настройки не переживут перезагрузку. В лог при старте выводится геометрия
ФС и предупреждение при выборе FS:none.
Проверено: xtensa-lx106-elf-g++ -fsyntax-only -Wall -Wextra против ядра
3.1.2 — предупреждений нет. На железе не проверялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit() возвращает false, то есть падает сам вызов SDK
(spi_flash_erase_sector либо spi_flash_write). Самая вероятная причина —
сектор EEPROM за пределами физической микросхемы.
При раскладке 4MB/FS:2MB сектор EEPROM это 1019 (смещение 0x3FB000), что
требует чипа не меньше 4 МБ. Теперь лог сам печатает это смещение и
сравнивает с ESP.getFlashChipRealSize(), а также границы файловой системы —
последнее важно, потому что при том же дефекте LittleFS по адресу
0x400000 окажется недоступен ровно так же.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Настройки не сохраняются после перезагрузки. Причина пока не установлена,
это шаг сбора улик, а не исправление.
Найден дефект, мешавший диагностике: результат EEPROM.commit()
игнорировался, поэтому лог печатал «Конфигурация сохранена» безусловно —
даже при провалившейся записи. Теперь запись подтверждается повторным
чтением сектора из флеша и сравнением всех полей; при расхождении в лог
идут записанные и прочитанные значения.
Добавлена проверка геометрии флеша при старте: несовпадение размера по
настройке IDE с физическим уводит сектор EEPROM за пределы микросхемы,
и запись молча теряется — симптом ровно тот же.
Проверкой чтения исключены: корректность библиотеки EEPROM, раскладка
адресов (258 из 512, без перекрытий), стирание сектора со стороны
WiFiManager::resetSettings() — он вызывает только WiFi.disconnect().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Распиновка. Пина D0, который использовался под выход «Резерв», в доступном
наборе нет. Кнопка сброса Wi-Fi удалена как избыточная: при пропавшей сети
wm.autoConnect() сам поднимает портал. Освободившийся D3 закрыл дефицит.
Выходы переехали на D3 и D4 и стали инверсными. Причина: для реле active-LOW
безопасное состояние — пин в HIGH, а D3/D4 подтянуты к HIGH внешними
резисторами платы и держат этот уровень всю загрузку. D8, наоборот, подтянут
к LOW и щёлкал бы реле при каждом включении питания. В setup() digitalWrite()
идёт до pinMode(), иначе защёлка выхода даёт короткий LOW.
Инверсия живёт только в setRelay(); логическое состояние дублируется в
relayMainOn/relayEmergOn, чтобы панель показывала смысл, а не уровень пина.
Настройки. device, выдержка аварии и выдержка резерва вынесены в /set_config
и EEPROM. Раскладка EEPROM версионирована: magic 0xA55B, старый 0xA55A
распознаётся и переносится, поэтому прошитые приборы не теряют webhook.
Значения по умолчанию воспроизводят прежнее поведение (5 с и 0 с).
Для резерва добавлена выдержка, которой раньше не было, симметрично основной
аварии. Обе выдержки гасят реле и вебхук одновременно.
device_name фильтруется до [A-Za-z0-9_-] вместо экранирования: такой набор
безопасен и в JSON, и в HTML. Пустое или неверное поле формы означает
«не менять», поэтому частичное заполнение не сбрасывает остальное.
Состояние реле выведено в /status и в панель.
Проверено: xtensa-lx106-elf-g++ -fsyntax-only -Wall -Wextra против ядра
3.1.2 — предупреждений в скетче нет (попутно убран неиспользуемый
isEmergency); валидация настроек прогнана на 22 граничных случаях;
раскладка EEPROM проверена на перекрытия. На железе не проверялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Отсутствие ретраев webhook — сознательное решение: автономная работа
без отправки событий это штатный режим.
Пауза мониторинга до 2 минут при старте без сохранённой сети
(блокирующий портал WiFiManager в setup()) — принята как есть,
с указанием способа лечения, если понадобится.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BearSSL делает TLS-хендшейк синхронно, поэтому вместо асинхронности
блокировка ограничивается по времени.
Главный источник залипания — не таймауты HTTPClient, а DNS:
WiFiClientSecureCtx::connect(name, port) резолвит имя через
WiFi.hostByName() без таймаута и подвешивает loop() на ~10 с.
Теперь имя разрешается заранее с лимитом 800 мс, результат попадает
в кэш lwIP, и внутренний резолв возвращается мгновенно. Соединение
по-прежнему идёт по имени, поэтому SNI и заголовок Host сохраняются.
Остаток бюджета уходит в http.setTimeout(): он ограничивает и
connect+TLS (через _client->setTimeout() до connect), и чтение
ответа (собственный цикл HTTPClient по _tcpTimeout).
Худший случай: ~3.5 с вместо прежних 10-15 с. Выходы аварии не
затронуты — digitalWrite() и раньше выполнялся до отправки.
Проверено: xtensa-lx106-elf-g++ -fsyntax-only -Wall -Wextra против
ядра 3.1.2 — в скетче замечаний нет; разбор URL прогнан на 15
граничных случаях.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прошивка Wemos D1 Mini для мониторинга трёх насосов: OLED,
веб-панель, webhook-уведомления, сброс Wi-Fi кнопкой.
Адрес webhook задаётся через веб-панель и хранится в EEPROM;
в исходнике не хранится, так как содержит секретный токен.
Добавлены README.md (техдокументация) и MANUAL.md (руководство).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sun's lowest ray reached y+H, one row below the H-tall erase box,
leaving a stray dot/trail. Erase H+4 so nothing is left behind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On power-up, after the title splash the screen clears and each weather
icon slides in from the right to centre with its colour-coded label, then
slides out to the left — Fine, Fair, Changeable, Rain, Storm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client-side i18n: static labels via data-i18n, dial zone words, and the
26 Zambretti phrases + categories + trend words translated in the browser.
Choice persists in localStorage. Device firmware unchanged (API stays
English; TFT stays Latin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cap the log panel at ~216px with a thin themed scrollbar so a long
forecast history does not stretch the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chunked API handlers and the maxblock<6000 auto-restart from 4b59b89
broke the web UI on-device; restore the working String-based handlers and
plain loop while keeping the scrollable forecast log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cap the log panel at ~216px with a thin themed scrollbar so a long
forecast history does not stretch the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>