core(M2): машина состояний сессии Ayla LAN + mock-модуль + интеграционные сценарии
- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
(переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
(обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
This commit is contained in:
@@ -30,13 +30,14 @@
|
||||
### 2.1. [ГЛАВНАЯ ПРИЧИНА «РАССИНХРОНИЗАЦИИ»] Keep-alive 1200 с вместо 10–15 с
|
||||
|
||||
`notifier.py:_KEEP_ALIVE_INTERVAL = 1200.0`. APK: 10 с (или `lan.json:keepAlive/3`).
|
||||
Проверено на приборе: **единственный механизм восстановления после расхождения
|
||||
CBC-цепочек — принудительный re-key, который модуль делает при получении
|
||||
`local_reg` для сессии старше ≈44 с. Ответы 400/401 модуль игнорирует.**
|
||||
Следствие для legacy: любая потерянная пара запрос-ответ/обрыв соединения →
|
||||
обе стороны «глохнут» на срок до 20 минут (до следующего local_reg). Наблюдаемый
|
||||
симптом «перестаёт понимать кондиционер» с самопроизвольным восстановлением —
|
||||
именно это.
|
||||
Проверено на приборе: **модуль игнорирует 400/401 на свои POST; переkey
|
||||
происходит при `local_reg` после зазора ≥ ~44–50 с от предыдущего** (при
|
||||
keep-alive 10–15 с re-key вообще не происходит). Следствие для legacy: любая
|
||||
потерянная пара запрос-ответ → обе стороны «глохнут» до следующего local_reg
|
||||
(до 20 минут), который завершится re-key — наблюдаемый симптом «перестаёт
|
||||
понимать кондиционер, потом сам чинится» — именно это. Корректная стратегия
|
||||
для новой реализации: при ошибке расшифровки — пауза ~50 с, затем local_reg
|
||||
(re-key гарантирован).
|
||||
|
||||
Дополнительно: длинные паузы между local_reg держат сессию «полуживой»
|
||||
(модуль не видит keep-alive, но слот может удерживаться), и конфликт за
|
||||
|
||||
@@ -199,9 +199,13 @@ int32_t fgl_convert_from_input(fgl_template, fgl_prop, int32_t disp,
|
||||
### 5.2. Keep-alive и re-key
|
||||
* Таймер `keepalive_ms` (default **15000**); по истечении — `PUT local_reg`
|
||||
c `notify=(очередь непуста)`. Каждый `commands.json` перезапускает таймер.
|
||||
* Re-key (очередной local_reg при возрасте сессии ≥ ~44 с) — штатное
|
||||
событие: перегенерация шифров/цепочек, сессия не пересоздаётся, начальная
|
||||
синхронизация не повторяется; seq_no приложения продолжает глобальный счётчик.
|
||||
* Re-key — событие по инициативе модуля (при зазоре local_reg ≥ ~44–50 с,
|
||||
[ПРОВЕРЕНО НА ПРИБОРЕ]; при штатном keep-alive НЕ происходит): обработать
|
||||
как обычный KE (перегенерация шифров/цепочек), сессию не пересоздавать,
|
||||
начальную синхронизацию не повторять; seq_no приложения продолжает
|
||||
глобальный счётчик.
|
||||
* Восстановление при ошибке расшифровки: тишина > порога (50 с по умолчанию)
|
||||
и возврат — модуль гарантированно ре-кает [ПРОВЕРЕНО НА ПРИБОРЕ].
|
||||
* Анти-спам: ≤1 local_reg/с; notify=1 — один на пакет команд.
|
||||
|
||||
### 5.3. Очередь команд
|
||||
@@ -291,7 +295,7 @@ HA-превью (PLAN_HOME_ASSISTANT §4) и тестами.
|
||||
|---|------------|------------------|
|
||||
| M0 ✅ | Монорепо-каркас: CMake (корень) + IDF-подключение, платслой, лог, CI | Собирается linux+esp-idf; пустой httpd отвечает 404 |
|
||||
| M1 ✅ | `src/ayla`: crypto+envelope, мини-httpd/httpc, jsmn-вендор | Векторы зелёные; httpd-тесты; совместимость с probe_reference.py |
|
||||
| M2 | `src/ayla`: сессия (установка/активация/keep-alive/re-key/слоты/503/delete) с mock-модулем | Все сценарии mock; на приборе: активация ≤5 с, re-key каждые 45–60 с |
|
||||
| M2 ✅ | `src/ayla`: сессия (установка/активация/keep-alive/re-key/слоты/503/delete) с mock-модулем | Все сценарии mock; на приборе: активация ≤5 с; семантика re-key: при зазоре local_reg ≥ ~44–50 с (при честном keep-alive 15 с — 0 re-key за 100 с; при 50 с — 3 re-key) |
|
||||
| M3 | `src/aircon`: шаблоны, конверсии+override, публичный API, batch | `tests/aircon` зелёные; на приборе: чтение всех свойств, batch=1 notify |
|
||||
| M4 | fglctl-пример, `tools/fglair-discover` (в т.ч. `--format esphome-secrets`), README библиотеки (сборка IDF/POSIX, тесты) | 24 ч на приборе: 0 рассинхронов; README готов |
|
||||
| M5 | (Опция) `FglHub` N устройств; mDNS-резолвер как опция host-разрешения | Два устройства одновременно |
|
||||
|
||||
@@ -216,15 +216,21 @@ GET http://<ip приложения>:<порт>/local_lan/commands.json
|
||||
один «пустой» опрос `commands.json` — это признак принятой сессии.
|
||||
2. `local_reg` от endpoint'а с живой сессией **моложе ~40 с** → только
|
||||
keep-alive, без key exchange.
|
||||
3. `local_reg` от endpoint'а с сессией **старше ~44 с** → модуль принудительно
|
||||
инициирует новый key exchange (ротация сессионных ключей). Т.е. при штатном
|
||||
keep-alive каждые 10–15 с ключи ротируются примерно каждые 45–60 с.
|
||||
`time_1` модуля — тикающий счётчик с шагом ≈10 нс (аптайм); порог,
|
||||
вероятно, 44 с в этих единицах либо просто 4.4e9 тиков.
|
||||
3. `local_reg` при **зазоре ≥ ~44–50 с** от предыдущего local_reg →
|
||||
модуль принудительно инициирует новый key exchange («вернувшееся»
|
||||
приложение получает свежие ключи). При штатном keep-alive каждые 10–15 с
|
||||
re-key НЕ происходит — сессия живёт сколь угодно долго (проверено:
|
||||
100 с при 15 с keep-alive — 0 re-key; 125 с при 50 с keep-alive — 3 re-key,
|
||||
оба без потерь). `time_1` модуля — тикающий счётчик с шагом ≈10 нс (аптайм);
|
||||
порог, вероятно, 4.4e9 тиков (~44 с) от последнего local_reg.
|
||||
4. **Ответы 401/400 на POST модуля игнорируются**: сессия продолжает работать,
|
||||
re-key не вызывается. Единственный механизм восстановления после расхождения
|
||||
CBC-цепочек — принудительный re-key по `local_reg` (п. 3). Поэтому интервал
|
||||
keep-alive = интервал потенциального «зависания» при десинхроне.
|
||||
re-key не вызывается. Восстановление после расхождения CBC-цепочек —
|
||||
намеренная «тишина» приложения на > порога из п. 3 с последующим
|
||||
`local_reg`: модуль сочтёт приложение вернувшимся и ре-кает. Т.е. стратегия
|
||||
самовосстановления: при ошибке расшифровки — пауза keep-alive ~50–60 с,
|
||||
затем возобновить (проверено на приборе). Отдельный случай — бракованная
|
||||
подпись при живой цепочке (сообщение расшифровано, подпись не сошлась):
|
||||
цепочка НЕ расходится, следующий push восстанавливает работу без re-key.
|
||||
5. `delete_session` освобождает слот немедленно; следующий `local_reg` того же
|
||||
endpoint'а создаёт новую сессию.
|
||||
6. Наблюдавшийся (не воспроизведённый повторно) режим отказа: модуль отвечает
|
||||
@@ -316,11 +322,12 @@ data: {"id":"<id команды>","ack_status":200,"ack_message":0,"dsn":"..."}
|
||||
|
||||
* **Потеря CBC-цепочки** (§3.4): модуль не может расшифровать ответ приложения /
|
||||
приложение не может расшифровать push модуля. Ответы 401/400 на POST модуля
|
||||
**игнорируются** — модуль продолжает слать в «сломанный» канал. Восстановление
|
||||
происходит только когда очередной `local_reg` (по возрасту ≥ ~44 с или от
|
||||
нового endpoint'а) вызовет новый key exchange. Следствие: **интервал
|
||||
keep-alive = максимальное время «мёртвой» сессии при десинхроне**
|
||||
(10–15 с — незаметно; 1200 с как в legacy-скрипте — 20 минут глухоты).
|
||||
**игнорируются** — модуль продолжает слать в «сломанный» канал. Восстановление:
|
||||
приложение замолкает на > ~44–50 с (порог «возврата» из п. 4.4.3) и шлёт
|
||||
`local_reg` — модуль переkey'ается. Реализация ядра: при ошибке расшифровки
|
||||
пауза keep-alive ~50 с, затем возобновление. В legacy-скрипте пауза получалась
|
||||
«бесплатно» из-за keep-alive 1200 с: каждый цикл завершался re-key при
|
||||
возврате — потому рассинхрон «сам чинился» через ~20 минут.
|
||||
* **Смена lanip_key** (`key_id` не совпал): теоретический путь по APK — 412 +
|
||||
`refreshLanConfig()` из облака. За 5 лет эксплуатации прибора ротации ключа
|
||||
не наблюдалось ни разу; ключ, по-видимому, зашит в модуль, облако лишь хранит
|
||||
|
||||
Reference in New Issue
Block a user