Перенести настройки из EEPROM в LittleFS
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>
This commit is contained in:
@@ -105,11 +105,16 @@ SSD1306 128×64, I2C, адрес по умолчанию `0x3C` (констан
|
||||
* Adafruit SSD1306
|
||||
* Adafruit GFX Library
|
||||
* WiFiManager (tzapu)
|
||||
* ArduinoJson (v7)
|
||||
|
||||
Входят в ESP8266 core: `ESP8266WiFi`, `ESP8266WebServer`, `ESP8266HTTPClient`,
|
||||
`WiFiClientSecure`, `EEPROM`, `Wire`.
|
||||
`WiFiClientSecure`, `LittleFS`, `Wire`.
|
||||
|
||||
Board: **LOLIN(WEMOS) D1 R2 & mini**, Upload speed 921600, Flash size 4MB.
|
||||
Board: **LOLIN(WEMOS) D1 R2 & mini**, Upload speed 921600.
|
||||
|
||||
**Flash Size обязательно с файловой системой** — например
|
||||
`4MB (FS:2MB OTA:~1019KB)`. При `FS:none` настройки сохранять некуда; прошивка
|
||||
сообщит об этом в лог при старте и покажет предупреждение в веб-панели.
|
||||
|
||||
---
|
||||
|
||||
@@ -180,7 +185,7 @@ pE = стоит -> реле «Резерв» выключено
|
||||
|
||||
## 5. Webhook
|
||||
|
||||
`POST` на `webhook_url`, `Content-Type: application/json`:
|
||||
`POST` на `settings.webhookUrl`, `Content-Type: application/json`:
|
||||
|
||||
```json
|
||||
{"event":"MAIN PUMPS","status":"ALARM","device":"Wemos_D1_Pump"}
|
||||
@@ -191,7 +196,7 @@ pE = стоит -> реле «Резерв» выключено
|
||||
| `MAIN PUMPS` | `ALARM` / `OK` |
|
||||
| `EMERGENCY PUMP` | `STARTED` / `STOPPED` |
|
||||
|
||||
Поле `device` берётся из настройки `device_name` (задаётся через веб-панель,
|
||||
Поле `device` берётся из настройки `settings.deviceName` (задаётся через веб-панель,
|
||||
по умолчанию `Wemos_D1_Pump`). Значение отфильтровано при вводе до
|
||||
`[A-Za-z0-9_-]`, поэтому экранирование в JSON не требуется — сломать тело
|
||||
запроса ему нечем.
|
||||
@@ -241,44 +246,99 @@ BearSSL ограничен тем же `_timeout`. Чтение ответа о
|
||||
|
||||
---
|
||||
|
||||
## 6. Хранение настроек (EEPROM)
|
||||
## 6. Хранение настроек (LittleFS)
|
||||
|
||||
Эмулируемая EEPROM, 512 байт. Занято 258.
|
||||
Настройки лежат в файловой системе LittleFS, в файле `/settings.json`:
|
||||
|
||||
| Адрес | Размер | Содержимое | По умолчанию |
|
||||
|---|---|---|---|
|
||||
| `0` | 2 | Magic `0xA55B` — версия раскладки 2 | — |
|
||||
| `2` | 220 | `webhook_url`, `\0`-терминированный | пусто |
|
||||
| `222` | 31 | `device_name` | `Wemos_D1_Pump` |
|
||||
| `254` | 2 | `alarm_delay_sec` (`uint16`) | `5` |
|
||||
| `256` | 2 | `emerg_delay_sec` (`uint16`) | `0` |
|
||||
```json
|
||||
{"webhook":"https://...","device":"Wemos_D1_Pump","alarm_delay":5,"emerg_delay":0}
|
||||
```
|
||||
|
||||
Значения по умолчанию воспроизводят поведение прошивки до появления настроек.
|
||||
| Поле структуры `AppSettings` | Ключ JSON | По умолчанию |
|
||||
|---|---|---|
|
||||
| `webhookUrl` (`char[221]`) | `webhook` | пусто |
|
||||
| `deviceName` (`char[32]`) | `device` | `Wemos_D1_Pump` |
|
||||
| `alarmDelaySec` (`uint16`) | `alarm_delay` | `5` |
|
||||
| `emergDelaySec` (`uint16`) | `emerg_delay` | `0` |
|
||||
|
||||
### 6.1 Версионирование и миграция
|
||||
Имена ключей вынесены в константы `KEY_*` и используются и при чтении, и при
|
||||
записи — рассинхронизация имён между load и save самый частый источник багов в
|
||||
таком коде, а через константу компилятор их разойтись не даст.
|
||||
|
||||
Magic одновременно служит номером версии раскладки:
|
||||
Строки хранятся как `char[]`, а не `String`: прибор работает месяцами без
|
||||
перезагрузки, и глобальные `String` фрагментировали бы кучу.
|
||||
|
||||
| Прочитанный magic | Действие |
|
||||
|---|---|
|
||||
| `0xA55B` | читаются все поля |
|
||||
| `0xA55A` | **миграция v1 → v2**: `webhook_url` сохраняется, новые поля получают значения по умолчанию, magic перезаписывается |
|
||||
| иное | первый старт, все поля по умолчанию |
|
||||
### 6.1 Почему не EEPROM
|
||||
|
||||
Миграция нужна, чтобы уже прошитые приборы не потеряли настроенный webhook при
|
||||
обновлении прошивки.
|
||||
Изначально настройки хранились в эмулируемой EEPROM. На целевой плате это **не
|
||||
работает**: `EEPROM.commit()` возвращает `false`, то есть падает сам вызов SDK
|
||||
(`spi_flash_erase_sector` либо `spi_flash_write`), и настройки молча теряются.
|
||||
|
||||
При чтении v2 значения дополнительно проверяются: пустое `device_name` и
|
||||
выдержка больше `DELAY_MAX_SEC` заменяются значениями по умолчанию — иначе мусор
|
||||
во флеше дал бы заведомо неверную выдержку.
|
||||
Проверкой исходников исключены: ошибка в библиотеке EEPROM, перекрытие адресов,
|
||||
стирание сектора со стороны `WiFiManager::resetSettings()`. Физический размер
|
||||
флеша тоже ни при чём — LittleFS на том же чипе работает.
|
||||
|
||||
Пустой `webhook_url` — легальное состояние. Пока URL не задан,
|
||||
Наиболее вероятный механизм: при раскладке `4MB (FS:2MB)` сектор EEPROM (1019,
|
||||
смещение `0x3FB000`) попадает в узкую щель между концом ФС (`0x3FA000`) и
|
||||
служебной областью SDK (`0x3FC000`) и на этой связке платы и SDK оказывается
|
||||
защищён от стирания. Доказать это не удалось, поэтому формулировка гипотетическая.
|
||||
Переход на LittleFS выбран не как обход симптома, а как переезд на механизм,
|
||||
работоспособность которого на этом железе подтверждена.
|
||||
|
||||
Практическое следствие: **в IDE обязателен вариант `Flash Size` с ненулевой
|
||||
файловой системой**. При `FS:none` сохранять настройки будет некуда, и прошивка
|
||||
скажет об этом в лог при старте.
|
||||
|
||||
### 6.2 Отказоустойчивость
|
||||
|
||||
Загрузка не падает на неполном или повреждённом файле. Порядок в
|
||||
`settingsBegin()`:
|
||||
|
||||
1. `settingsDefaults()` — заполнить структуру значениями по умолчанию;
|
||||
2. `settingsLoad()` — перезаписать полями из JSON; отсутствующий или битый ключ
|
||||
**оставляет значение по умолчанию**;
|
||||
3. если файла нет или JSON не разобрался — `settingsSave()` создаёт его заново.
|
||||
|
||||
Благодаря пункту 2 файл, записанный прошлой версией прошивки, не ломается при
|
||||
добавлении новых полей — старый `/settings.json` просто не содержит нового ключа,
|
||||
и подставляется значение по умолчанию.
|
||||
|
||||
`settingsClamp()` дополнительно приводит значения к допустимым: пустое
|
||||
`deviceName` и выдержка больше `DELAY_MAX_SEC` заменяются значениями по
|
||||
умолчанию. Вызывается и после чтения, и перед записью.
|
||||
|
||||
Если ФС не смонтировалась, выполняется `LittleFS.format()` и повторная попытка.
|
||||
При окончательной неудаче взводится `settingsFsReady = false`: прибор продолжает
|
||||
работать на значениях по умолчанию, но **веб-панель показывает предупреждение**,
|
||||
что настройки не переживут перезагрузку. Молчаливой потери настроек больше нет.
|
||||
|
||||
### 6.3 Ресурс флеша
|
||||
|
||||
Запись происходит только по факту изменения — из обработчика `/set_config`, и
|
||||
только если хотя бы одно поле реально изменилось. Периодической записи нет и
|
||||
добавлять её не следует.
|
||||
|
||||
### 6.4 Как добавить новую настройку
|
||||
|
||||
Синхронно правятся четыре места:
|
||||
|
||||
1. константа `*_DEFAULT` рядом с остальными значениями по умолчанию;
|
||||
2. поле в `struct AppSettings` и строка в `settingsDefaults()`;
|
||||
3. константа `KEY_*`, строка в `settingsLoad()` и строка в `settingsSave()`;
|
||||
4. поле формы в `getHTML()` и его разбор в обработчике `/set_config`.
|
||||
|
||||
Старый `/settings.json` без нового ключа не сломается — подставится значение по
|
||||
умолчанию.
|
||||
|
||||
### 6.5 Что не хранится здесь
|
||||
|
||||
Учётные данные Wi-Fi хранит WiFiManager в своей области флеша, поэтому сброс
|
||||
Wi-Fi настройки из `/settings.json` не затрагивает, и наоборот.
|
||||
|
||||
Пустой `webhookUrl` — легальное состояние. Пока URL не задан,
|
||||
`sendPostWebhook()` сразу выходит и пишет в лог
|
||||
`[HTTP] Webhook URL не задан — отправка пропущена`.
|
||||
|
||||
Учётные данные Wi-Fi хранит WiFiManager в своей области флеша, не в этой EEPROM,
|
||||
поэтому сброс Wi-Fi настройки из этой таблицы не затрагивает.
|
||||
|
||||
---
|
||||
|
||||
## 7. Веб-интерфейс
|
||||
@@ -295,7 +355,7 @@ Magic одновременно служит номером версии раск
|
||||
### 7.1 `/set_config`
|
||||
|
||||
Одна форма на все настройки. **Пустое или неверное поле означает «не менять»** —
|
||||
так частично заполненная форма не обнуляет остальные параметры. Запись в EEPROM
|
||||
так частично заполненная форма не обнуляет остальные параметры. Запись в файл
|
||||
выполняется только если что-то реально изменилось.
|
||||
|
||||
| Параметр | Валидация | При отказе |
|
||||
@@ -403,9 +463,9 @@ STATUS: OK
|
||||
портал придётся своим таймером.
|
||||
* Логика аварии — «оба стоят»: остановка одного насоса штатной ситуацией
|
||||
не считается и никак не сигнализируется.
|
||||
* Webhook URL хранится в EEPROM в открытом виде и отдаётся веб-панелью всем,
|
||||
кто может её открыть. Если URL содержит секретный токен — доступ к панели
|
||||
равносилен доступу к токену.
|
||||
* Webhook URL хранится в `/settings.json` в открытом виде и отдаётся
|
||||
веб-панелью всем, кто может её открыть. Если URL содержит секретный токен —
|
||||
доступ к панели равносилен доступу к токену.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user