Skip to content

Latest commit

 

History

History
498 lines (387 loc) · 61.5 KB

CHANGELOG.md

File metadata and controls

498 lines (387 loc) · 61.5 KB

Changelog

Формат основан на Keep a Changelog, проект использует Semantic Versioning.

[Unreleased]

[5.0.71] — 2026-09-12 — Excel-выгрузка стала книгой из трёх листов

Проблема

В v5.0.69 весы начали отдавать раскрашенную таблицу, но на одном листе, тогда как скрипт tools/csv_to_xlsx.py делал четыре. HTML-формат иначе и не умеет: Excel хранит в нём каждый лист отдельным файлом, в одном документе несколько листов не собрать.

Решение

Формат сменён на SpreadsheetML 2003 (XML) — единственный, который держит несколько листов в одном файле и при этом собирается на ESP32 обычной печатью в поток. Эндпоинт и кнопка те же.

Три листа:

  1. Сводка — период, число замеров, вес начало → конец и мин/макс, температура мин/макс, батарея начало → конец в вольтах и процентах, расход заряда в сутки, остаток дней работы и длина полного цикла.
  2. Замеры — как раньше: вес жёлтой заливкой, изменение к предыдущему замеру зелёным/красным, температура пятью ступенями, батарея с красной отсечкой на 3.6 В. Добавлено закрепление шапки.
  3. По дням — замеров за сутки, вес на конец дня, привес за сутки (зелёным/красным), мин/макс веса и температуры, батарея на конец дня.

Файл собирается за три прохода по логу: статистика для «Сводки» нужна до того, как пишется первый лист; «По дням» группируется на лету — строки в логе идут по возрастанию даты, достаточно отследить смену первых 10 символов.

Значения на «Сводке» — строки, локаль Excel к ним не применяется, поэтому точка заменяется на запятую вручную (_ru_dec()): иначе рядом с настоящими числами из «Замеров» выглядело чужеродно.

Чего по-прежнему нет

Графиков — SpreadsheetML их не поддерживает. Нужны графики — tools/csv_to_xlsx.py, он делает настоящий XLSX с четырьмя листами и тремя диаграммами.

Предупреждение Excel про формат файла осталось: оно появляется из-за расширения .xls при не-бинарном содержимом. Нажать «Да».

Проверено

Формат проверен до прошивки: сгенерирован файл по реальной выгрузке + искусственные крайние случаи (мороз −5.4 °C, жара +41 °C, разряд 3.38 В), открыт настоящим Excel через COM. Три листа на месте, заливки применяются: шапка #1F3A2E, вес #FFF3CD, жара #E06666, разряд #FFC7CE.

[5.0.70] — 2026-09-11 — Кнопка «Весь лог CSV» не работала вообще

Проблема

Нажатие «📥 Весь лог CSV» отдавало 500 Cannot open log. Выгрузка за период при этом работала — разница и указала на причину.

Веб-слой открывал файл сам:

f = LOG_FS.open(LOG_FILE, "r");   // LOG_FS на ESP32 = SPIFFS

А лог пишется в LittleFS (Logger.cpp: _fs_open_read()). Макрос LOG_FS остался с тех времён, когда на ESP32 использовалась SPIFFS, и после перехода на LittleFS указывал в пустоту: файла там нет, открытие падает всегда. Выгрузки за период и за дату шли через Logger и потому работали.

Решение

Веб-слой больше не лезет в файловую систему. Полная выгрузка идёт тем же путём, что и остальные — log_stream_csv_range(cs, "", "") с пустым диапазоном. Макрос LOG_FS удалён, чтобы не выстрелил ещё раз.

[5.0.69] — 2026-09-11 — Excel с подсветкой прямо из весов

Проблема

Единственным экспортом был голый CSV: колонка цифр, в которой ничего не разглядеть — где улей набрал, где просел, когда села батарея. Раскрасить можно было скриптом (tools/csv_to_xlsx.py), но он живёт на одном конкретном компьютере: подключишься с другого ноутбука — снова получишь безликий CSV.

Решение

Раскраску делает сама прошивка. Новый эндпоинт GET /api/log/xls?from=&to= отдаёт HTML-таблицу с инлайн-стилями под именем beehive_<период>.xls — Excel и LibreOffice открывают её как книгу и сохраняют заливку. Кнопка «📊 Excel с подсветкой (за период)» добавлена в раздел «⬇ Скачать» на вкладке Архив, период берётся тот же, что выбран выше.

Что раскрашено (всё считается на устройстве, Logger.cpp: log_stream_xls_range()):

  • вес — жёлтая заливка, жирный;
  • изменение к предыдущему замеру — новая колонка, рост зелёным, падение красным (порог ±0.05 кг);
  • температура — 5 ступеней от синего (<10 °C) до красного (>35 °C);
  • батарея — зелёная при заряде >80%, бледно-зелёная 50-80%, жёлтая <50%, красная ниже 3.6 В (порог алерта разряда);
  • заряд в процентах — новая колонка, считается той же кривой LiPo, что в Battery.cpp:53-59, поэтому сходится с показаниями на экране и в Telegram.

Настоящий XLSX (zip + несколько XML) на ESP32 не собрать без заметного heap, поэтому выбран HTML-вариант. Плата: Excel при открытии показывает предупреждение «формат файла не соответствует расширению» — надо нажать «Да». Об этом написано прямо под кнопкой.

CSV-выгрузка осталась на месте — она удобнее, когда данные идут в расчёты, а не на просмотр.

[5.0.68] — 2026-08-16 — Дубли TG-отчётов + обрыв загрузки архива и CSV

Проблема 1 — отчёты каждые 2 минуты, хотя никто не просил

16.08 весы прислали отчёты в 14:00:19, 14:02:39, 14:04:43, 14:06:02 — интервал совпадает с auto-sleep = 120 сек. Значит плата циклически перезагружалась, и каждый cold boot попадал в окно ±10 мин от слота 14:00 → отправлялся ещё один «отчёт при включении» (логика v5.0.62, BeehiveScale.ino:577-583).

Защиты не было: tgSentSinceBoot — статик в RAM, обнуляется на каждом boot; persist.* лежит в RTC-памяти, которую power-cut стирает полностью. Единственное, что переживает снятие питания, — EEPROM. Функции save_last_tg_report_unix() / load_last_tg_report_unix() написаны ещё в v5.0.44, но не вызывались ниоткуда.

Исправлено: отметка времени успешной отправки пишется в EEPROM (addr 356-360) и проверяется перед каждым отчётом. Порог — половина минимального интервала между слотами расписания, максимум 15 минут. При расписании 08:00/11:00/14:00/… это 15 мин, при слотах через 10 мин — 5 мин, так что настоящие слоты не глушатся.

Проблема 2 — «Ошибка загрузки» в Архиве и сорванное скачивание CSV

ESP32 WebServer.h синхронный: обрабатывает один запрос за раз. Дашборд опрашивает /api/data каждые 5 секунд, и этот поллинг влезал в тот же сокет посреди длинных операций:

  • /api/period отдаёт лог одним потоком (CONTENT_LENGTH_UNKNOWN + чанки) — поток обрывался, JSON.parse падал → «Ошибка загрузки»;
  • window.open('/api/log') — передача файла срывалась на середине;
  • вдобавок сам archLoad() дёргал /api/config параллельно с /api/period — тот же конфликт, только внутри одной функции.

Исправлено:

  • флаг _ioBusy — на время длинной операции 5-секундный поллинг молчит;
  • /api/config в архиве запрашивается до стрима периода, а не параллельно;
  • все четыре точки скачивания CSV переведены на dlOpen() — глушит поллинг на 30 сек;
  • текст ошибки стал осмысленным: подсказывает сузить период.

Диагностика — причина последнего сброса

Чтобы понять, почему плата перезагружается, esp_reset_reason() теперь отдаётся в /api/data полем resetReason и выводится строкой «Причина старта» в карточке «Статус системы». POWERON (питание подал DS3231), SW и EXT — норма, показываются обычным цветом. PANIC, INT_WDT, TASK_WDT, BROWNOUT, DEEPSLEEP — подсвечиваются красным.

DEEPSLEEP здесь означает, что hardware power-cut не сработал и прошивка ушла в fallback-сон (SleepManager.cpp:130-137) — сон в этом режиме ест ~14 мА вместо ~2 мА.

[5.0.67] — 2026-08-16 — IP-адрес на LCD + короткое mDNS-имя

Проблема

IP-адрес весов печатался только в Serial. Чтобы открыть веб-интерфейс, приходилось заходить в админку роутера и искать устройство в списке DHCP-клиентов. После долгого простоя (весы были офлайн 2 недели) аренда истекает и адрес меняется. mDNS в прошивке был (beehivescale.local), но нигде не задокументирован — про него просто не знали.

Решение

  1. Новый экран меню 7/9 — IP (display_screen_ip()): строка 1 — адрес (192.168.0.145 в STA, AP 192.168.4.1 в AP-режиме), строка 2 — mDNS: vesy.local. Нет связи — WiFi: net svyazi.
  2. Показ адреса 3 сек сразу после подключения к WiFi в setup() — видно при включении, без нажатий.
  3. mDNS-имя укорочено: beehivescale → vesy (MDNS_HOSTNAME в Connectivity.h). Влезает на 16-символьную строку LCD вместе с подписью и легче набирается с телефона. Тем же именем представляется ArduinoOTA (порт в Arduino IDE теперь vesy).

Изменения в навигации

  • MENU_SCREENS 8 → 9. Порядок: 1 Вес, 2 Дельта, 3 Батарея, 4 Температура, 5 Дата/время, 6 Статус, 7 IP, 8 Калибровка, 9 Диагностика.
  • Магические числа экранов заменены на SCR_IP / SCR_CALIB / SCR_DIAG — индексы калибровки и диагностики сдвинулись с 6/7 на 7/8.

Алерт низкой батареи включён по умолчанию

load_alerts() при пустом EEPROM-блоке отдавал batLowV = 0.0 — то есть предупреждение о разряде было выключено. Реальные последствия: банка села в улье 30.07.2026, весы молча умерли, потеря замеров обнаружена только 15.08 по тишине в Telegram. Новый дефолт — 3.6 В (≈15% заряда, остаётся 1-2 дня на снятие и зарядку). Кто сохранял настройки алертов раньше — значение в EEPROM уцелеет, порог надо выставить вручную на вкладке «Telegram».

Инструкция (manual.html + pages/) — сверка с прошивкой

Инструкция отставала на 46 версий (заголовки v5.0.21). Исправлено:

  • DS3231 SQW → D33 везде описывался как «задел на будущее, в коде не используется». На деле с v5.0.47 на нём держится весь power-cut: без припаянного SQW и живой CR2032 весы не проснутся по расписанию вообще. Переписано в manual.html, 01_components, 02_connection, 03_settings, 04_operation, 07_troubleshooting.
  • Автономность заявлялась как «30–40 дней». Приведена к расчёту под реальную банку 21700 4500 мА·ч: ~17–19 дней с текущим AMS1117 и 40–44 дня после замены LDO на HT7333.
  • Алерты батареи / температуры / RTC / роения не были описаны нигде — добавлен раздел в 03_settings.html.
  • Как узнать IP — новый раздел в 05_web.html (mDNS / Экран 7 / резервация DHCP в роутере).
  • Битая навигация: все 7 страниц в pages/ ссылались на несуществующие ../index.html и ../User_Guide.html, а manual.html не ссылался на pages/ вообще — страницы были осиротевшими. Ссылки сшиты в обе стороны. Удалён <link> на несуществующий styles/main.css.
  • Экраны LCD перенумерованы (8 → 9 штук, IP вставлен седьмым).

[5.0.21] — 2026-05-17 — Отложенный WiFi (фикс защёлки MT3608 при wake-up)

Проблема

ESP32 после wake-up из deep sleep одновременно запускала: ядро, LCD-инициализацию, Wi-Fi подключение. Суммарный пик потребления 600-700 мА за 100мс. MT3608 Boost (заявлено 2А пик, реально ~1.5А) уходил в защиту по току (OCP) и защёлкивался — выходное напряжение падало до 1В, ESP32 крашила, на LCD остаётся только подсветка («синий экран»). Telegram не отправлялся, расписание не срабатывало. Симптом проявлялся именно после wake-up по расписанию в улье — никто не мог нажать reset/подключить USB.

Решение (программное, без перепайки)

  1. Отложенный WiFi: ESP32 + LCD + датчики стартуют первыми (~200мА — легко тянет MT3608), показывают splash «WiFi cherez 8s..», ждут 8 секунд. Капы заряжаются, банка восстанавливается. Только потом вызывается wifi_init() отдельным пиком ~200мА (вдвое меньше прежних 500мА).
  2. WiFi TX power 11dBm (WiFi.setTxPower(WIFI_POWER_11dBm)) — снижает пиковый ток передачи с ~500мА до ~200мА. Дальность падает на ~30%, но в улье или рядом с домашним роутером хватает.
  3. Modem-sleep (WiFi.setSleep(true)) — между передачами WiFi-чип засыпает. Снижает средний ток в active фазе.

Эффект

  • Пик при wake-up: 600-700мА → ~200мА (×3 меньше)
  • 8 секунд паузы дают времени MT3608 восстановиться после холодного старта
  • LCD показывает диагностику с первой секунды (раньше — кубики до WiFi init)
  • Boot loop / защёлка MT3608 не возникают

Что НЕ исправляет

  • Если железо принципиально слабое (плохая банка с большим внутренним сопротивлением, либо MT3608 нерабочий) — поможет только замена компонентов
  • При сильно посаженной банке (<3.3В) — DW01 будет отрубать OUT независимо от софта

Альтернатива (если фикс не помог)

  • Капы 2200µF на VOUT MT3608 + 470µF на VIN
  • Замена MT3608 → XL6009 (3А, нет защёлки)

[5.0.20] — 2026-05-13 — Настраиваемые Telegram-алерты

Добавлено

  • Новый модуль Alerts.h/cpp — проверка пороговых условий и отправка алертов через tg_send_message(). Анти-спам: каждый алерт шлётся один раз, флаг хранится в RAM. Сброс с гистерезисом (например, низкая батарея сбросится только когда напряжение поднимется на 0.1 В выше порога).
  • 4 типа алертов, все настраиваются через сайт:
    1. 🔋 Низкая батарея — порог в вольтах (например 3.6В). Текст: «Низкая батарея! Напряжение: 3.58 В (18%). Пора зарядить». Сброс при ≥порог+0.1В.
    2. ❄️ Низкая температура — например -10°C (для зимовки). Сброс при ≥порог+2°C.
    3. 🔥 Высокая температура — например 38°C (перегрев). Сброс при ≤порог-2°C.
    4. ⏰ RTC error — автоматический алерт когда DS3231 теряет валидность (села CR2032). Включается/выключается чекбоксом.
  • EEPROM addr 342-356 (magic 0xAD): AlertSettings { batLowV, tempLow, tempHigh, rtcEn }. Дефолты: всё выключено кроме RTC error.
  • Memory.h/cpp: save_alerts(AlertSettings) / load_alerts(&) с валидацией диапазонов.
  • Endpoint /api/alerts (GET/POST):
    • GET → {batLowV, tempLow, tempHigh, rtcEn}
    • POST → сохраняет в EEPROM (поля опциональные — можно прислать только то что меняется)
  • UI на вкладке Telegram — новый блок «⚠ Пороги алертов» с 3 числовыми полями + чекбокс RTC + кнопки Сохранить/Загрузить.
  • Интеграция в main loop: alerts_check() вызывается при каждой записи в лог (schedLog/bootLog/intervalLog) — точно когда есть свежие значения батареи/температуры.

Мотивация

Раньше единственный способ узнать о низкой батарее — увидеть «!LOW BATTERY!» на LCD когда подойдёшь к улью. Но к этому моменту банка уже на 10% и через сутки разрядится. Теперь Telegram сам напишет когда напряжение упадёт до твоего порога (например 3.6 В) — есть несколько дней чтобы приехать. То же для температурных аномалий зимой/летом.

Wi-Fi 24h-watchdog не добавляли — на пасеке Wi-Fi может отсутствовать долго, ложные срабатывания.

[5.0.19] — 2026-05-13 — Калибровка делителя батареи через сайт

Добавлено

  • Калибровочный коэффициент делителя батареи в EEPROM (addr 336-340, magic 0xAC). Прежний BAT_DIVIDER_RATIO = 2.0f был захардкожен в Battery.h, теперь читается из EEPROM при старте, дефолт 2.0 если запись пустая. Валидация диапазона 1.5–3.0 (защита от мусора).
  • Memory.h/cpp: save_bat_calib(float) / load_bat_calib() с валидацией.
  • Battery.h/cpp: глобальная _batRatio вместо #define, новые функции bat_get_ratio(), bat_set_ratio(r), bat_read_raw_adc_voltage() (для калибровки).
  • Новый endpoint /api/battery/calib (GET/POST):
    • GET → возвращает {ratio, adcV, batV, smoothed} для отображения текущих значений
    • POST {realVoltage: 4.10} → авто-расчёт коэффициента из реального напряжения с мультиметра
    • POST {ratio: 2.124} → ручная установка точного коэффициента
  • UI на вкладке «Калибровка» — новый блок «🔋 Калибровка батареи»:
    • Показывает текущее напряжение (с EMA), сырое ADC, текущий коэффициент
    • Поле ввода «Реальное напряжение (с мультиметра)»
    • Кнопка «⚙ Откалибровать автоматически» — вычисляет и сохраняет новый коэф.
    • Кнопка «↺ Обновить» — перечитать текущие показания
    • Кнопка «↻ Сбросить на 2.0» — заводское значение
  • При установке нового коэффициента сбрасывается EMA-сглаживание чтобы новые показания применились сразу.

Мотивация

Реальные резисторы делителя 100 кΩ × 2 имеют разброс ±1-5% по точности, плюс ADC ESP32 нелинейный (±2-3%). Суммарно показания батареи могут отличаться от реальности на 0.1-0.3 В (например, у Геннадия мультиметр показывал 4.10 В, а сайт — 3.86 В). Раньше для исправления нужно было править BAT_DIVIDER_RATIO в коде и перепрошивать. Теперь — один POST на сайт, коэф. живёт в EEPROM, прошивка не трогается.

Изменено

  • BAT_DIVIDER_RATIO в Battery.h переименован в BAT_DIVIDER_RATIO_DEFAULT (дефолт при пустом EEPROM).
  • bat_read_raw() теперь использует динамический _batRatio вместо макроса.

[5.0.18] — 2026-05-11 — Обновлённая PWA-иконка

Изменено

  • PWA-иконка /icon.svg — заменена на минималистичную стилизованную пчелу на скруглённом медовом квадрате. Прежний простой геометрический SVG (3 чёрные полосы на оранжевом круге + 2 белых эллипса как крылья) заменён на полноценный bee-symbol: голова с антеннами и точками, овальное тельце с тремя медовыми полосками, заострённое жало внизу, два повернутых белых полупрозрачных крыла, тонкая платформа-весы под пчелой. Скруглённый медовый квадрат как фон даёт характерный «app icon» вид. ~1.5 КБ SVG, маскируемая иконка для PWA-манифеста (purpose: any maskable).
  • Источник дизайна: AI-сгенерированный логотип docs/assets/icon.png (вариант №2 из Gemini Imagen). PNG используется в README/GitHub, SVG-версия — в прошивке (компактная, масштабируемая).

[5.0.17] — 2026-05-11 — Пресет «С прошлого визита» в Архиве

Добавлено

  • EEPROM хранит timestamp прошлого открытия web (addr 331..336, magic 0xAB). При первом открытии вкладки Архив за boot возвращается СТАРОЕ значение, EEPROM обновляется на «сейчас» если разрыв >1ч. Кеш _lastVisitCached в RAM держит старое значение для всего boot — повторные открытия вкладки не сбрасывают пресет на «только что».
  • Кнопка ↩ С прошлого визита в архивной секции — появляется только если visit timestamp валиден. Подпись под пресетами: «Прошлый визит: вчера в 18:42» / «X дн. назад».
  • Авто-выставление дат при заходе во вкладку Архив: если прошлый визит был ≥1 дня назад, поля «с/по» автоматически устанавливаются на интервал «с прошлого визита по сегодня» и список загружается с этими параметрами.
  • Новый endpoint GET /api/last-visit → {lastVisit: unix_seconds, serverNow: unix_seconds}.
  • Memory.h/cpp: save_last_visit(uint32_t) / load_last_visit() с валидацией ts < 2019-01-01 = мусор.

Размер прошивки

1.27 МБ (40.5%, +3 КБ от v5.0.16). RAM 17.5%.

[5.0.16] — 2026-05-11 — Вкладка «Архив» + PWA

Добавлено

  • Новая вкладка 📅 Архив в веб-интерфейсе (8-я по счёту, между Главной и Графиком).
  • Выбор периода: два HTML date-input («С» / «По») + 5 пресетов (Сегодня / 3 дня / Неделя / Месяц / Всё). Любой произвольный диапазон.
  • Лента дней сверху-вниз (новые первыми), сгруппированы по месяцам. Каждая карточка дня содержит:
    • Дату + день недели (Пн/Вт/Ср…)
    • Цветной маркер аномалий (🟢 прирост, 🟡 ±0.1 кг, 🔴 падение > alert_delta, ⚪ нет данных предыдущего дня) — переиспользует существующий порог alert_delta, новых настроек не вводится
    • Все измерения дня (время + вес + температура)
    • Мин/Макс веса и температуры за день
    • Дельту к предыдущему дню в кг
  • Сводка периода сверху ленты: диапазон дат, кол-во записей и дней, суммарная дельта Σ за период (плюс/минус кг с цветом).
  • Скачивание: кнопки «CSV за выбранный период» (через /api/log?from=&to=) и «Весь лог CSV» (старый /api/log).

PWA — установка на главный экран телефона

  • /manifest.json — name: BeehiveScale, short_name: Beehive, display: standalone, theme_color: #f5a623, иконка /icon.svg.
  • /sw.js — service worker, кеш beehive-v1 для /, /manifest.json, /icon.svg. API-запросы (/api/*) НЕ кешируются. Прочее — network-first с fallback на cache (оффлайн доступ к UI).
  • /icon.svg — SVG-иконка пчелы (амбер круг + 3 чёрные полоски + 2 белых крыла) 192×192. Один файл, браузер сам растеризует под нужный размер.
  • Meta-теги в <head> PAGE_HTML: theme-color, apple-mobile-web-app-capable, apple-mobile-web-app-status-bar-style: black-translucent, apple-mobile-web-app-title: Beehive, link rel="manifest", link rel="apple-touch-icon", link rel="icon" type="image/svg+xml".
  • Регистрация SW в window.onload: navigator.serviceWorker.register('/sw.js').
  • UX установки: на пасеке → подключиться к Wi-Fi весов → открыть сайт в браузере телефона → меню → «Добавить на главный экран» → иконка 🐝 Beehive появляется как у нативного приложения.

Новые API endpoints

  • GET /archive — alias на главный SPA (страница та же, открывается на вкладке Архив).
  • GET /api/period?from=YYYY-MM-DD&to=YYYY-MM-DD — JSON-стрим записей за диапазон. Формат [{dt,w,t,b},...], влажность пропущена. Стримит через _WebChunkStream без heap-аккумуляции.
  • GET /api/log?from=&to= — CSV за диапазон. Старый ?date=YYYY-MM-DD совместим. Если заданы оба from/to — приоритет у диапазона.
  • GET /manifest.json, /sw.js, /icon.svg — PWA-артефакты, без _auth() (по дизайну: SW должен регистрироваться без логина).

Logger

  • log_stream_csv_range(out, from, to) — стрим CSV за диапазон в Stream. from/to принимают оба формата дат (YYYY-MM-DD и DD.MM.YYYY).
  • log_stream_period_json(out, from, to) — JSON-стрим за диапазон.
  • _stream_filter_range() — внутренний единый обход файла + _date_to_yyyymmdd() конвертер для лекс-сравнения дат. Без heap-аллокаций в цикле.

WebServerModule

  • _WebChunkStream вынесен на файловый уровень — переиспользуется между /api/log, /api/period и будущими стриминговыми эндпоинтами. Буфер 256 байт, flush каждые 512 + yield().
  • В nav() JS обновлён idx map: добавлен archive:1, остальные индексы сдвинуты (chart:2, wifi:3, settings:4, calib:5, tg:6, api:7).

Размер прошивки

1.27 МБ (40.4%, +90 КБ от v5.0.15 за счёт ~150 строк HTML+JS архивной секции и PWA-артефактов).

Та же сессия что и 5.0.0 — продолжение по результатам тестирования с Геннадием.

Добавлено

  • Auto-sleep timeout настраивается через сайт — поле «Уйти в сон после бездействия (сек, 0=не засыпать)». Раньше захардкодено 180 сек. Можно выставить 0 (отладка), 30 (экономный), 600 (медленно). EEPROM addr 317-319 (uint16_t + magic 0xA8).
  • Auto-return на главный экран через 20 сек бездействия. Не применяется на экране 6 (calib menu) и 7 (diag).
  • Двойная дельта в Telegram-отчёте + на сайте:
    • 📈 «С прошлого замера» (от lastReportWeight в RTC RAM, обновляется при каждой отправке отчёта)
    • 🎯 «От зафиксированной точки» (от prevWeight в EEPROM, меняется только ручной кнопкой)
  • Дата фиксации опорного веса — Unix timestamp в EEPROM addr 313 (uint32_t). Записывается при ручном нажатии «📍 Зафиксировать». В Telegram и на сайте показывается «зафикс. 4.67 от 05.05.2026, N дн назад».
  • scale.power_down() перед deep sleep — HX711 в энергосбережение (1.5 мА → 1 мкА).
  • lcd.clear() + noDisplay() перед сном — на LCD не остаются "квадратики" от старых символов.
  • Wake-grace через sticky флаг — в момент начала нажатия (когда подсветка ещё была off) ставится wakeEventPending = true, держится до релиза. Гарантированно срабатывает: первое нажатие при погашенном экране = просто будит, не открывает диалог тары. Раньше был баг с проверкой wasBacklightOff после обработки кнопки — она уже включалась через lcd_backlight_activity.
  • Окно подтверждения тары увеличено 3 → 4 сек.
  • Текст в Telegram переведён на русский (Отчёт, Тревога, Время, Вес, Эталон, Δ, Температура).
  • JSON-буфер для Telegram 384 → 2048 байт — большие сообщения с кириллицей и <b> теперь не обрезаются.
  • Подробная диагностика TG-ошибок — расшифровка кодов (-1 CONNECTION_REFUSED, -5 CONNECTION_LOST, 401 Unauthorized и т.д.) + free heap before TLS.

Изменено

  • PlatformIO partition: default.csv → huge_app.csv (3MB code / 1MB SPIFFS) — без OTA, но iOS прошивка ~1.36 МБ не влезала в default.
  • Точность веса 3 → 2 знака на LCD и в JSON веб-API.
  • Период чтения HX711 1500 → 600 мс — отклик веса ускорился с 15 сек до 5-7 сек.
  • Кнопка MAIN — возвращена старая логика (двойное нажатие подтверждения тары + удержание 5 сек = калибровка). Промежуточная попытка с MEDIUM_PRESS (3 сек = тара) откачена по просьбе пользователя.
  • Названия дельт в TG/web для прозрачности:
    • «За период» → «📈 С прошлого замера»
    • «От опорного» → «🎯 От зафиксированной точки» + дата фиксации
  • Кнопка «💾 Сохранить эталон» → «📍 Зафиксировать вес как точку отсчёта» — название самообъясняющее.

Исправлено

  • 🔥 КРИТИЧНЫЙ ОТКАТ ОПАСНЫХ ОПТИМИЗАЦИЙ СНА в SleepManager.cpp::sleep_enter():
    • Удалён esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF) — конфликтовал с esp_sleep_enable_ext0_wakeup(GPIO27). Симптом: assert failed: esp_sleep_pd_config sleep_modes.c:1974 (refs >= 0) при попытке уйти в сон → SW_CPU_RESET.
    • Удалён Wire.end() — после assert/panic в WiFi context вызывал NULL-pointer panic (Guru Meditation InstrFetchProhibited PC=0x00000000).
    • Удалён esp_wifi_deinit() — двойной вызов после WiFi.mode(WIFI_OFF) крашил.
    • Удалён esp_bt_controller_disable() — BT не был инициализирован, попытка disable пустого = краш.
    • Теперь sleep_enter() минимальный: esp_sleep_enable_timer_wakeup() + esp_deep_sleep_start(). ESP32 сам корректно отключает peripherals.
  • Step cfg-sleep в HTML 30 → 1 — браузер требовал кратное 30, не давал сохранить 120 сек.
  • WebData структура расширена полями lastReportWeight* + hasLastReport*.
  • tg_send_report сигнатура расширена на prevWeightDate, lastReportWeight, hasLastReport.

Известные ограничения (для P4)

  • Потребление в "deep sleep" 70 мА (измерено USB-тестером RYKEN). ESP32-чип в deep sleep ест ~50 мкА; остальные 70 мА уходят на:
    • AMS1117 quiescent ~5-10 мА
    • LCD I2C контроллер PCF8574 ~3 мА
    • Подсветка LCD: «логически off» через noBacklight(), но физически модуль через VCC всё равно ест
    • HX711 модуль (вне чипа): ~1 мА на обвязке
    • Реальные ~50 мА уходят в LCD backlight который не отключается полностью через PCF8574 P3-пин — нужен P-MOSFET high-side switch.
  • Schottky-диод USB→VIN на ESP32 DevKit V1 спалён — Геннадий замкнул его перемычкой. Без диода защиты от обратной полярности нет. Нужно купить SS14 (Schottky 1A 40V SMA, на корпусе маркировка «S4»). Полоска корпуса = катод, должна смотреть в сторону регулятора.
  • Светодиоды выпаяны частично — экономия ~7 мА.
  • Battery monitoring — делитель 100к+100к не подключён физически на GPIO34.

Тестовые результаты этой версии

  • Cloudflare Worker beehive-relay.darkvolg.workers.dev работает, обходит блокировку Beeline
  • Telegram-сообщения с эмодзи и жирной кириллицей приходят
  • HX711 калибровка точная: 4.67 кг показывает 4.67 (5л бутылка воды). Раньше 5.7 кг было потому что не убрал гирю 1 кг с платформы.
  • Расписание 17:00 сработало точно — wake в 17:00:10
  • DS18B20 27.5°C, RTC 28.0°C
  • LCD все 8 экранов

[5.0.0] — 2026-05-05 — Полный port на ESP32

Целая сессия порта с ESP8266 NodeMCU v0.1 на ESP32-WROOM-32 DevKit V1. Причина перехода — на ESP8266 не получилось завести deep sleep wake (ни через GPIO16→RST, ни через DS3231 alarm, перепробованы все варианты резисторов/диодов/режимов). На ESP32 deep sleep работает железно из коробки.

Pin mapping ESP32

  • HX711: DT=GPIO16, SCK=GPIO17 (питание от 3V3 — стабильнее показания)
  • LCD I2C: SDA=GPIO21, SCL=GPIO22 (адрес 0x27, питание 5V через VIN)
  • DS3231 RTC: общий I2C bus с LCD (адрес 0x68), SQW=GPIO33 (для будущего EXT0 wake)
  • DS18B20: GPIO4 + резистор 4.7кОм подтяжка к 3V3
  • Кнопки MAIN=GPIO27, MENU=GPIO26 (RTC GPIO — поддерживают wake-from-sleep)
  • Battery ADC: GPIO34 (через делитель 100к+100к, пока не подключено физически)

Добавлено

  • Cloudflare Worker relay для Telegram (beehive-relay.darkvolg.workers.dev) — обход блокировки api.telegram.org российскими провайдерами. ESP шлёт через Worker по HTTPS с заголовком X-Beehive-Secret, Worker форвардит в Telegram. См. cloudflare-worker/ESP32_INTEGRATION.md.
  • Telegram отчёты привязаны к расписанию замеров — каждое срабатывание расписания (например, в 09:00) автоматически шлёт отчёт. Раньше шло по интервалу, что не работало в deep sleep.
  • Сообщения Telegram переведены на нормальную кириллицу + жирный HTML (<b>Отчёт: улей</b>, Время:, Вес:, Эталон:, Δ:, Температура:).
  • Дельта в Telegram-отчёте — добавлены поля Эталон (опорный вес) и Δ (изменение от эталона). Раньше было только текущий вес.
  • Wake-grace для тарирования — при пробуждении LCD (подсветка погасла) первое короткое нажатие MAIN не открывает диалог тары, только зажигает экран. Это решает старую проблему "случайно протарировал когда хотел просто посмотреть вес".
  • Логи sleep — [AutoSleep] Sleep for X sec показывают сколько ESP проспит, помогает диагностировать расписание.
  • Диагностика TLS-ошибок Telegram — при failure печатается free heap + расшифровка кода (CONNECTION_REFUSED, CONNECTION_LOST, 401 Unauthorized, 400 Bad Request).
  • Подробное логирование причин WiFi STA timeout (NO_SSID_AVAIL, CONNECT_FAILED, IDLE и т.д.).

Изменено

  • Точность веса до сотых (2 знака после запятой) — на LCD Ves: 4.42kg и в JSON веб-API. Раньше было 1 знак на LCD и 3 знака в JSON.
  • Период чтения HX711 1500ms → 600ms — отклик веса с EMA-фильтром α=0.3 теперь ~5-7 сек до стабильного значения вместо 15.
  • Auto-format LittleFS при первом boot — LittleFS.begin(true) для ESP32. Без этого на свежем чипе FS не монтировалась и web-панель отдавала ошибку.
  • Эффективный режим WiFi определяется через WiFi.getMode(), а не только через EEPROM. Это решает баг "после fallback STA→AP веб-сервер останавливается через 7 сек после Setup Done".
  • wifi_ensure_connected() не дёргает реконнект если фактически в AP-режиме — раньше каждые 10 сек рвал сеть после неудачного STA.
  • Sleep guard 60 сек — если sched_next_sec() вернул < 60 (попали ровно в минуту расписания), используем 60 сек минимум. Без этого был цикл "проснулся-уснул-проснулся" в минуту срабатывания расписания.
  • JSON-буфер для Telegram 384 → 2048 байт — большие сообщения (приветствие на ~500 байт UTF-8) обрезались, parse_mode HTML терялся, теги <b> показывались как текст.
  • Вёрстка веб-панели — мобильная адаптация: .tabs с горизонтальным скроллом, flex-shrink:0 на табах, при max-width:760px шапка flex-wrap:wrap, версия прошивки переносится под логотип, лишние строки скрыты. Раньше пункты меню (РОСТ/WI-FI/...) уезжали за край экрана и не были доступны.
  • Step Deep Sleep input в HTML 30 → 1 — браузер не давал сохранить значения вроде 120 (требовал кратное 30). Теперь любое целое число.
  • Шапка веб-панели "ESP8266" → "ESP32" + описание /api/reboot.
  • Battery.cpp: ADC_ATTEN_DB_11 → ADC_11db — совместимость с ESP32 Arduino Core 3.0+, где константа была переименована.

Исправлено

  • Logger.cpp: ESP8266 LittleFS Dir/openDir/dir.next() → ESP32 File/openNextFile/isDirectory() для перебора файлов лога.
  • Logger.cpp: убран ESP.wdtFeed() (нет на ESP32) — оставлен только yield(), который кормит таск-watchdog корректно.
  • WebServerModule.cpp: collectHeaders("X-CSRF-Token") → collectHeaders(arr, 1) — на ESP32 другая сигнатура (массив + размер).
  • ThingSpeak очередь автоочистка — если API key дефолтный (YOUR_*), queue_add не пишет в файл, а queue_process удаляет старый /queue.bin. Раньше копились 50+ элементов с [Queue] Send failed, saving remaining каждую минуту.

Удалено

  • Поддержка PERIPHERAL_POWER_PIN (был = 26 для ESP32, конфликт с MENU button) — пока заглушено -1. Вернётся в P4 (финализация) когда будет P-MOSFET для отключения периферии в deep sleep.

Известные ограничения

  • 94.5% Flash в default partition — пришлось использовать Huge APP partition (3MB No OTA / 1MB SPIFFS) в Arduino IDE.
  • AUTO_SLEEP_MS = 3 минуты захардкожено — нельзя настроить через сайт.
  • Battery monitoring — делитель 100к+100к не подключён физически, web показывает 0.00В.
  • Потребление в "deep sleep" — измерено 70 мА (со стола, через USB-тестер). Основные потребители: AMS1117 quiescent, LCD I2C контроллер PCF8574, HX711 standby. Для нормального автономного режима нужен P-MOSFET (P4).

Тестовые результаты

  • HX711 откалиброван точностью ±10 граммов (эталон 4.43, текущий 4.42, Δ -0.01 кг).
  • WiFi STA подключается к домашнему роутеру (DHCP IP 192.168.0.58).
  • Расписание 17:00 сработало точно — wake в 17:00:10, отчёт в Telegram через 5 сек.
  • Cloudflare Worker отдаёт 200 OK, Telegram сообщения приходят в чат 1371763893.

[4.2.9] — 2026-05-04

Изменено — двойное подтверждение тарирования

  • TARE_COUNT 1 → 2 (BeehiveScale.ino). Случайное нажатие при пробуждении LCD из режима ожидания больше не запускает тарирование. Защита от потери данных в улье при случайном касании.
  • Первое нажатие D3 → показывает запрос на LCD: «Tara? Nazhmite esche 1 raz/3sek».
  • Второе нажатие в течение 3 секунд → выполняется тарирование.
  • Таймаут 3 сек без второго нажатия → показывает «Tara: otmena» и возвращается к обычному экрану.
  • Калибровка (D3 удержание 5 сек) — без изменений.

[4.2.8] — 2026-05-04

Исправлено — авто-тарирование при включении (регрессия v4.2.6)

  • При включении весов GPIO0 (D3, главная кнопка) — boot-strap пин ESP8266, в момент инициализации даёт переходные процессы. FALLING-прерывание ловило их как «нажатие», и в v4.2.6 (после уменьшения TARE_COUNT 4→1) это автоматически запускало тарирование с грузом на платформе.
  • Фикс 1: в конце setup() сбрасываем ISR-флаги обоих кнопок (btnMain.irqFell, btnMenu.irqFell).
  • Фикс 2: в handle_buttons() добавлен boot grace period — первые 2 секунды после старта кнопки игнорируются, ISR-флаги периодически чистятся.

[4.2.7] — 2026-05-04

Изменено — порог LONG_PRESS

  • LONG_PRESS_MS 2000мс → 5000мс в Button.cpp. Калибровка (D3 hold) и «домой» из меню (D4 hold) теперь требуют удержания 5 секунд вместо 2. Защита от случайного запуска опасной калибровки. Тарирование (D3 одно короткое нажатие) — без изменений.

[4.2.6] — 2026-05-04

Изменено — UX кнопки тарирования

  • TARE_COUNT 4 → 1 (BeehiveScale.ino). Раньше для тарирования требовалось 4 коротких нажатия за 5 секунд (без визуальной обратной связи на LCD), что воспринималось пользователем как «работает через раз». Теперь — одно короткое нажатие = тарирование, как на стандартных весах.
  • Долгое нажатие главной кнопки = по-прежнему калибровка (защита от случайного запуска опасной операции).
  • Двойное нажатие меню-кнопки = по-прежнему отмена тары (undo_tare).

[4.2.5] — 2026-05-04

Изменено — точность отображения LCD

  • Главный экран веса: %6.2f kg → %6.1f kg (сотые → десятые). Для пасеки точность 100г более чем достаточно (прирост 0.5–3 кг/день видно отлично), убирает «дрожание» младшей цифры.
  • Экран разницы (D:): аналогично %6.2f → %6.1f для текущей и предыдущей разницы.
  • В CSV-логах и в Telegram точность сохранена на уровне %.2f — для аналитики и графиков.
  • В экране ручной подстройки калибровочного фактора (CF) точность веса оставлена %6.2f — там нужно видеть мелкие изменения для точной настройки.

Активировано LEGACY_HX711_PINS

  • В этой сборке HX711 на пинах D0 (GPIO16) / TX (GPIO1) — соответствует физической распиновке весов Геннадия. Serial Monitor не работает (TX занят SCK).

[4.2.4] — 2026-05-04

Изменено — UX скорости отклика и стабильности нуля

  • Интервал чтения HX711: 2500мс → 1500мс (SCALE_READ_INTERVAL_MS в BeehiveScale.ino). Время отклика на полное изменение веса упало с ~25 сек до ~10 сек.
  • EMA α по умолчанию: 0.2 → 0.3 (Memory.cpp). Более быстрый отклик при сохранении фильтрации импульсов от пчёл (<1 сек длительности).
  • Auto-zero deadband: 0.001 кг (1 г) → 0.05 кг (50 г) (ZERO_DEADBAND_KG в BeehiveScale.ino). Убирает дрожание показаний возле нуля (-0.01 / +0.02 → теперь стабильно 0.000).

Улучшено — Веб-интерфейс

  • Поле «EMA сглаживание α» переименовано в «Скорость отклика весов» с понятным описанием: 0.1 = медленно (улей), 0.3 = средне (рекомендуется), 0.6 = быстро (тесты).
  • Подсказка в блоке «Описание» теперь объясняет рекомендуемые значения для разных режимов работы.

Техническое

  • Существующие EEPROM-настройки сохраняются: если у пользователя уже стоит α=0.2, она не перезапишется. Чтобы получить новый дефолт 0.3 — задать вручную через веб-интерфейс или через POST /api/settings.

[4.2.3] — 2026-05-03

Добавлено

  • Build-флаг LEGACY_HX711_PINS в BeehiveScale.ino — позволяет временно вернуть HX711 на старые пины D0 (DT) / TX (SCK) для диагностики. По умолчанию выключен. При активации автоматически делает Serial.end() перед scale_init() чтобы UART TX не конфликтовал с HX711 SCK.

Зафиксировано (диагностика 2026-05-03)

  • HX711 модуль конкретного экземпляра весов вышел из строя. Симптомы: тарирование показывает близкие к 0 значения (-0.7 кг шум), но нагрузка не считывается (90 кг → реакции нет). Воспроизведено на двух разных тензодатчиках и двух распиновках (D5/D6 и D0/TX). Решение — замена HX711 модуля. Подтверждается мультиметром: между E+ и E− должно быть ~3.3 В (excitation), при смерти HX711 = 0 В.

[4.2.2] — 2026-05-03

Исправлено

  • Splash на LCD теперь показывает реальную версию из Version.h (FW_VERSION) — раньше был хардкод Versiya 4.1, не обновлялся при бампе версии. Теперь будет автоматически.
  • В JSON /api/backup поле _ver тоже завязано на FW_VERSION (было хардкод "4.1").

[4.2.1] — 2026-05-03

Исправлено — корневая причина нестабильного веса

  • HX711 перенесён с GPIO16/GPIO1 на GPIO14/GPIO12 (D5/D6) — стандартные пины ESP8266 для HX711, как в эталонной прошивке Bee_Lite_D1 v1.1. Прежняя распиновка не имела внутреннего pull-up на DOUT (GPIO16) и использовала UART TX как SCK (GPIO1), что давало плавающий вес и ложные срабатывания spike-фильтра / watchdog.
  • Убран костыль Serial.end() перед scale_init() — больше не нужен, UART TX свободен.
  • Удалён anti-stuck блок из scale_read_weight() (3 лишних raw чтения после get_units) — удваивал окно ловли наводок и не давал реальной защиты.
  • Удалён дублирующий вызов set_scale()/set_offset() после wifi_init() — это были no-op в RAM, не имели смысла.
  • Интервал чтения веса 800мс → 2500мс — снижает температурный дрейф HX711 и даёт WebServer окно на обработку запросов.

Изменено

  • Эталонный груз калибровки: верхний предел 5 кг → 50 кг (для калибровки по тяжёлым гирям при больших ульях).

Удалено

  • SD-карта отключена (#define USE_SD_CARD закомментирован в Logger.h). Логи и бэкапы теперь только в LittleFS (внутренний flash ESP8266). Причина: D5/D6/D7/D8 освобождаются от SPI, чтобы HX711 мог встать на стандартную D5/D6. В полевых условиях LittleFS надёжнее SD (нет окисления контактов слота, выдерживает -40..+85°C, FS с journaling).

Изменено

  • LOG_INTERVAL_MS: 1 мин → 5 мин (для пасеки достаточно, снижает износ LittleFS в 5x).

Hardware

  • ⚠ Требуется перекоммутация HX711: DT с D0 на D5, SCK с TX на D6. SD-карту физически можно оставить на коннекторе — она просто не будет использоваться.
  • ⚠ При прошивке в Arduino IDE выбрать: Tools → Flash Size → 4MB (FS:2MB OTA:~512KB) — для запаса места в LittleFS.
  • Admin-пароль и OTA-пароль вынесены в EEPROM (больше не хардкод в исходниках)
  • Добавлена CSRF-защита на все POST endpoints
  • Санитизация CSV (защита от формула-инъекций в Excel/LibreOffice)
  • _maskSecret больше не обрезает токены >63 байт, а маскирует середину
  • Rate limit на /api/log и /api/data

Исправлено

  • start_webserver() теперь идемпотентный при Wi-Fi reconnect
  • Guard на sleepDur=0 в loop()
  • /api/reboot отдаёт JSON до ESP.restart(), а не после
  • RTC fallback больше не использует __DATE__ — берёт последнее время из EEPROM
  • Atomic rotate лог-файла на SD (write→verify→remove вместо copy+rename)
  • Убран автоматический LittleFS.format() при ошибке begin()
  • Сброс spikeRejectCnt при NaN
  • Partial queue send: позиция снимается до закрытия файла

Изменено

  • String заменён на char[] + snprintf в горячих путях (Logger, WebServer, RTC)
  • /api/backup/restore делает один EEPROM.commit() вместо 18 подряд (износ flash)
  • yield() добавлен в BearSSL handshake
  • Log rotate: при ошибке ротации лог truncate'ится вместо роста в бесконечность
  • apPassBuf стал локальной переменной (race fix)

Удалено

  • Dummy-поле humidity из API (нет физического датчика)
  • Посторонний сервер.txt (документация VPN от другого проекта)

Документация

  • Добавлен LICENSE (MIT)
  • Добавлен CHANGELOG.md
  • Добавлен platformio.ini для сборки через PlatformIO
  • Добавлен .github/workflows/build.yml (CI на каждый push)
  • Расширен .gitignore (корректные правила)
  • Исправлена распиновка DS18B20 в README.md (GPIO3 вместо GPIO13)

[4.1.0] — 2026-03-09

Baseline версия. Полный функционал:

  • HX711 тензодатчик с EMA-сглаживанием и spike-фильтром
  • DS18B20 термометр, DS3231 RTC
  • LCD 1602 I2C с 7 экранами меню
  • SD-карта CSV логирование
  • Веб-интерфейс с REST API (Basic Auth admin/beehive)
  • Telegram уведомления
  • ThingSpeak облако
  • Deep Sleep через 3 мин неактивности
  • EEPROM калибровка с magic bytes
  • OTA обновление (пароль ota_beehive)