[2026-09-14] eagle: Modbus/RTU_Framing_Source_Analysis.md family/how-to/home-automation.md family/how-to/nodered-ventilation.md family/how-to/truenas-infrastructure.md family/plans/t610-home-automation.md

This commit is contained in:
Alexey Martemyanov
2026-09-14 16:40:03 +06:00
parent a3cd302573
commit a1f5d1b04e
5 changed files with 157 additions and 17 deletions
+106 -1
View File
@@ -1,6 +1,8 @@
# Modbus RTU Frame Delimiting: pymodbus vs libmodbus (source analysis)
Версия/источники зафиксированы на момент анализа. Локальные копии скачанных файлов: `/Users/admin/modbus_src/`.
> Источник кода — реальные файлы с GitHub, скачаны при разборе бага `modbus-bridge`
> (см. [[family/plans/t610-home-automation]] §5-кватер-З и §5-кватер-Ж).
> Локальные копии: `/Users/admin/modbus_src/`.
## Границы (URL)
@@ -26,3 +28,106 @@ pymodbus v3.0.2/3.4.1 `ModbusRtuFramer`: `_min_frame_size = 4`, `_hsize = 0x01`.
```
libmodbus: `MODBUS_RTU_MAX_ADU_LENGTH 256`, `_MODBUS_RTU_HEADER_LENGTH 1`, `_MODBUS_RTU_CHECKSUM_LENGTH 2`.
## Формула T3.5 (pymodbus `client/serial.py`)
```python
self._t0 = float(1 + bytesize + stopbits) / baudrate # 3.6.9 (в 3.4.1: 1+8+2 захардкожено)
if baudrate > 19200:
self.silent_interval = 1.75 / 1000 # мс
else:
self.inter_byte_timeout = 1.5 * self._t0
self.silent_interval = 3.5 * self._t0
```
**При 9600 бод:** `t0 = 11/9600 = 1.146 мс``T3.5 = 4.01 мс`, `T1.5 = 1.72 мс`.
⚠️ В pymodbus **фреймер таймингами не занимается** — T3.5 живёт в серийном клиенте. Фреймер делает чистый байтовый парсинг.
## Как делится кадр — три подхода
| Проект | Делимитация | Мусор в буфере |
|---|---|---|
| **libmodbus** (эталон, C) | **по byte-count из функции**: читает адрес+функцию → `compute_meta_length_after_function``compute_data_length_after_meta` (берёт byte-count из кадра) | `tcflush(TCIOFLUSH)` при bad CRC (`MODBUS_ERROR_RECOVERY_PROTOCOL`) |
| **pymodbus dev** (`framer/rtu.py`) | CRC-hunting: `for used_len in range(data_len)` — сдвиг на байт, допускает мусор **до и после** кадра | ждёт/сдвигается, не дропает целиком |
| **pymodbus классич.** (v3.0) | длина из `calculateRtuFrameSize()` (byte-count) | **`resetFrame()` → буфер в `b""` целиком** при bad CRC/bad UID |
### libmodbus — ключевой код (`src/modbus.c`, `_modbus_receive_msg`)
```c
step = _STEP_FUNCTION;
length_to_read = ctx->backend->header_length + 1;
while (length_to_read != 0) {
rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);
...
rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);
msg_length += rc;
length_to_read -= rc;
if (length_to_read == 0) {
switch (step) {
case _STEP_FUNCTION:
length_to_read = compute_meta_length_after_function(msg[header_length], msg_type);
...
case _STEP_META:
length_to_read = compute_data_length_after_meta(ctx, msg, msg_type);
if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
errno = EMBBADDATA; // "too many data" — защита от переполнения
return -1;
}
step = _STEP_DATA;
}
}
}
return ctx->backend->check_integrity(ctx, msg, msg_length);
```
`compute_data_length_after_meta` (MSG_CONFIRMATION): `length = msg[header_length + 1]`**byte-count прямо из кадра**, +2 на CRC.
### pymodbus классический — `resetFrame()` (тот самый сброс)
```python
def resetFrame(self):
"""Reset the entire message frame.
It is hard to know if we are simply out of sync or if there is
an error in the stream as we have no way to check the start or
end of the message (python just doesn't have the resolution to
check for millisecond delays).
"""
self._buffer = b""
self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"}
```
### pymodbus dev — hunting-режим (`framer/rtu.py`, `decode`)
```python
for used_len in range(data_len):
if data_len - used_len < self.MIN_SIZE:
return 0, 0, 0, self.EMPTY # мало данных — ждём
dev_id = int(data[used_len])
if self.device_ids and dev_id not in self.device_ids:
return data_len, 0, 0, self.EMPTY
if not (pdu_class := self.decoder.lookupPduClass(data[used_len:])):
continue # сдвиг на байт (мусор перед кадром)
if not (size := pdu_class.calculateRtuFrameSize(data[used_len:])):
return 0, dev_id, 0, self.EMPTY
if data_len < used_len + size:
return 0, dev_id, 0, self.EMPTY # кадр не готов — ждём
for test_len in range(data_len, used_len + size - 1, -1):
start_crc = test_len - 2
...
if not FramerRTU.check_CRC(data[used_len:start_crc], crc_val):
continue # мусор ПОСЛЕ кадра — пробуем короче
return data_len, dev_id, 0, data[used_len + 1 : start_crc]
```
## Выводы для нашего сниффера (`modbus-bridge`)
1. **Пассивный сниффер T3.5 нужен обязательно** — мы не мастер, не знаем длину ожидаемого ответа заранее.
2. **Мусор нельзя копить.** Канон (pymodbus `resetFrame`, libmodbus `tcflush`) — **сбрасывать** битый буфер, а не сдвигать бесконечно.
3. **Дроп только ПОСЛЕ паузы.** Пока на шине нет тишины ≥T3.5 — кадр ещё может продолжаться, байты копим.
4. Наш фикс (коммит `3748feb` в `git_admin/HA-ZONT-Modbus`) = T3.5-гейт + `resetFrame`-семантика + hunting-скан сохранён.
## Связанные заметки
- [[family/plans/t610-home-automation]] — основной док миграции, §5-кватер-Ж/З (диагноз и фикс)
- [[family/how-to/home-automation]] — карта slave/регистров
+1 -1
View File
@@ -217,7 +217,7 @@ ZONT relays
> **ℹ️ Про «виртуальные sensor 101/102/103» — механика (уточнено 2026-09-14):**
> 101/102/103 — это **виртуальные Modbus-slaves**, которые контейнер `modbus-bridge` подставляет на шине: реальные 485-датчики **Гостиная=1, Детская=2, Спальня=3** сниффятся, и их значения выдаются ZONT'у как датчики 101/102/103 (регистр 100). ZONT (Modbus master) опрашивает их как внешние датчики.
> **🔑 Уточнение 2026-09-14 (по логу, см. [[family/plans/t610-home-automation]] §5-кватер-Д):** bridge работает **двусторонне** — он не только публикует в MQTT, но и **отвечает ZONT'у** под адресами 101/102/103. В логе это видно явно: `Slave: 101 → sniff:dining_temperature = 0 [00 00]`, `Slave: 102 → sniff:kids_temperature = 25.26`, `Slave: 103 → sniff:bedroom_temperature = 25.12`. **По гостиной (101) bridge отдаёт `0`**, потому что реальный датчик гостиной он **не снифит** (в логе `Sniff:` только `kids_*`/`bedroom_*`; `Slave: 1` у ZONT читает 7 регистров с адреса 2 — это внутренние параметры, не датчик). **Поэтому** `modbus/sensors/dining/*` не публикуется и `sensor.dining_*` в HA = `unknown`.
> Если `modbus-bridge` не запущен/не слушает шину → ZONT показывает эти датчики **«недоступные»**. На TrueNAS известная первопричина была гонка docker/udev после рестарта (см. `[[family/plans/zont-modbus-bridge-udev-race-protection]]`); **на t610 неактуально** — Supervisor сам ждёт устройство.
> Если `modbus-bridge` не запущен/не слушает шину → ZONT показывает эти датчики **«недоступные»**. На TrueNAS известная первопричина была гонка docker/udev после рестарта (см. `[[family/how-to/zont-modbus-bridge-udev-race-protection]]`); **на t610 неактуально** — Supervisor сам ждёт устройство.
## Карта регистров контроллера вентиляторов
+7
View File
@@ -144,3 +144,10 @@ Gate: current_state (manual override → block automation).
## Связанные заметки
- [[family/how-to/home-automation]] — общая автоматизация дома
- [[family/plans/t610-home-automation]] — миграция на t610, §5-кватер-Г (перенос flows) и §5-кватер-З (фикс датчика столовой)
- [[Modbus/RTU_Framing_Source_Analysis]] — канон сборки RTU-кадров (почему `dining` молчал и как починили)
## Статус данных для автоматики (2026-09-14)
**Все три комнаты (`bedroom`, `kids`, `dining`) публикуют данные в MQTT.** Датчик столовой (`slave 1`) был сломан багом сборки кадров в `modbus-bridge` — починен 2026-09-14 (см. [[family/plans/t610-home-automation]] §5-кватер-З). До фикса автоматика вентиляции по CO₂ работала без данных столовой.
⚠️ **`fan.fan_at2_1/2` (slave 10) — `unavailable`** (блок закомментирован в `configuration.yaml`) → исполнительный контур вентиляторов AT2 не работает. Задача по slave 10 Alex'ом не ставилась.
+2 -2
View File
@@ -30,7 +30,7 @@
> - `restart: unless-stopped` НЕ перезапускает такой контейнер (отказ до старта процесса).
> - Следствие: modbus-bridge не отвечает на шине → 485-датчики в ZONT отображаются **«недоступные»**.
> - Ручной фикс: `docker start modbus-bridge mbusd` (после готовности tty-симлинков).
> - Защита от рецидива (план, НЕ внедрён — ждёт OK Alex): `family/plans/zont-modbus-bridge-udev-race-protection.md` — POSTINIT init script с ожиданием симлинков + `docker start`.
> - Защита от рецидива (план, НЕ внедрён — ждёт OK Alex): `family/how-to/zont-modbus-bridge-udev-race-protection.md` — POSTINIT init script с ожиданием симлинков + `docker start`.
> **⚠️ ДРУГОЕ (открытое):** пул `DEST` (IronWolf) на 2026-08-24 `DEGRADED` — разобраться, почему (отвалился mirror-член). TimeMachine (~277G) не возвращён (бэкапы macOS, вероятно не нужен — решение Alex не подтверждено). SSH с Mac по 192.168.2.197 нестабилен — использовать `mallexxx.duckdns.org`.
> ## 🔒 ИСТОРИЯ: статус до восстановления (2026-08-24)
@@ -342,7 +342,7 @@ error gathering device information while adding custom device "/dev/ttyZONT": no
**Ручной фикс:** `docker start modbus-bridge mbusd` (после того как симлинки созданы).
**Защита от рецидива (план, НЕ внедрён, ждёт OK Alex):** `family/plans/zont-modbus-bridge-udev-race-protection.md` — POSTINIT init script `/mnt/RED_2TB/system/start-modbus.sh`, который ждёт появления `ttyZONT`/`ttyVent` (≤25с) и делает `docker start modbus-bridge mbusd`.
**Защита от рецидива (план, НЕ внедрён, ждёт OK Alex):** `family/how-to/zont-modbus-bridge-udev-race-protection.md` — POSTINIT init script `/mnt/RED_2TB/system/start-modbus.sh`, который ждёт появления `ttyZONT`/`ttyVent` (≤25с) и делает `docker start modbus-bridge mbusd`.
### USB device aliases
+41 -13
View File
@@ -9,7 +9,7 @@
## 1. Состояние на 2026-09-14 (актуализировано 15:33 → дополнено вечерней сессией)
**Этап 1 ✅ · Этап 2 ✅ · Этап 3 ✅ ЗАКРЫТ.** Этап 4 — **ПОЧТИ ЗАКРЫТ (3 из 4):** ① Caddy → t610 (`mallexxx.duckdns.org` → HA на t610, HTTP 200); ② Node-RED flows перенесены, `Connected to HA`, наружу не выпущен (решение Alex «оставляем так», доступ через ingress); ③ **ZONT MQTT-редирект переключён на t610** (DNAT на роутере `.197``.176`, живой поток идёт). **ОСТАЛОСЬ:** погасить/отключить сервисы TrueNAS (заблокировано — Caddy на TrueNAS держит точку входа) + хвосты (static IP, бэкап).
**Последняя верификация: 2026-09-14 (вечер-6) — ФИКС СДЕЛАН, ГОСТИНАЯ РАБОТАЕТ. ✅✅ ЗАКРЫТО.** Причина (вечер-5, фактом): кадры приходят разорванными, байты-сироты `00` копились в голове буфера → сдвиг выравнивания → 19-байтный ответ гостиной отбрасывался молча. **Фикс (вечер-6): сборка кадров по канону — T3.5-разграничение (пауза ≥3.5 символа = конец кадра) + сброс битого буфера (= `resetFrame` в pymodbus)**, коммит `3748feb` → Gitea → scp → `ha apps rebuild`. **Итог: все 7 полей `dining` публикуются, `dining_summary`/`dining_air_summary` ожили, `unavailable` 10 → 8** (остались только slave 10 «снято Alex'ом» + `todo.shopping_list`). **Загадка «замерзания» лога на 7 ч — тот же баг, устранена.** Диагностика включена (`MODBUS_DEBUG_RAW=1`), выключить после наблюдения. Детали — §5-кватер-З, §11 (журнал вечер-6).
**Последняя верификация: 2026-09-14 (вечер-7) — ФИКС ПОДТВЕРЖДЁН, ДИАГНОСТИКА СНЯТА. ✅✅ ЗАКРЫТО.** Причина (вечер-5, фактом): кадры приходят разорванными, байты-сироты `00` копились в голове буфера → сдвиг выравнивания → 19-байтный ответ гостиной отбрасывался молча. **Фикс (вечер-6): сборка кадров по канону — T3.5-разграничение (пауза ≥3.5 символа = конец кадра) + сброс битого буфера (= `resetFrame` в pymodbus)**, коммит `3748feb` → Gitea → scp → `ha apps rebuild`. **Итог: все 7 полей `dining` публикуются, `dining_summary`/`dining_air_summary` ожили, `unavailable` 10 → 8** (остались только slave 10 «снято Alex'ом» + `todo.shopping_list`). **Загадка «замерзания» лога на 7 ч — тот же баг, устранена.** Диагностика ✅ снята (вечер-7), `dining` стабилен. Детали — §5-кватер-З, §11 (журнал вечер-6/7).
| Что | Факт |
|---|---|
@@ -1364,9 +1364,10 @@ sensor.dining_air_summary = 31tvoc 3pm ← БЫЛ TemplateError → ож
4. **Правку в продакшене делать через git-репо, не «на месте»** (сначала коммит, потом scp+rebuild) — даёт чистую историю и откат.
5. **Перед правкой сложной логики — офлайн-тест на реальных данных** (`test_framing.py`) дешевле и быстрее, чем rebuild+деплой-цикл (~1 мин каждый).
#### 📌 Осталось (по этой задаче)
#### 📌 Итог по задаче — ЗАКРЫТА
- **Диагностика ВКЛЮЧЕНА** (`MODBUS_DEBUG_RAW=1` в `run.sh` на t610) — понаблюдать за стабильностью `dining`, затем **выключить**: закомментировать строку в `run.sh` → `ha apps rebuild local_modbus-bridge`. Ждёт команды Alex.
- **Диагностика ✅ СНЯТА 2026-09-14 (вечер-7).** Наблюдение показало стабильность: `dining` 7 публикаций за окно против 6 у `kids`/`bedroom` (тот же порядок), `BUF-DROP` = 1 (единичный, не накопление), `[RAW*]` в логе = 0. `MODBUS_DEBUG_RAW=1` закомментирован в `run.sh` → `ha apps rebuild local_modbus-bridge` → рестарт. Проверено: лог чистый, все 7 полей `dining` публикуются.
- **Как включить снова при отладке framing:** раскомментировать `export MODBUS_DEBUG_RAW=1` в `run.sh` → rebuild → рестарт.
- **Крон-наблюдение** за `dining` (не пропадает ли снова) — не заводилось.
**Git-коммиты (репо `git_admin/HA-ZONT-Modbus`, ветка `main`):**
@@ -1720,7 +1721,7 @@ curl -s -X POST -H "$HDR" -H "Content-Type: application/json" \
| ~~1b~~ | ~~`verify` в заслонках~~ — **✅ ОПРОВЕРГНУТО:** заслонки ожили при том же `verify`. Не причина. | — |
| ~~1c~~ | ~~ZONT-шина на других гнёздах~~ — **✅ СДЕЛАНО:** физический тест доказал (ZONT = гнездо 4, вентиляция = гнездо 3), привязки аддонов обменяны, док приведён в соответствие. Осталась только проверка «① подтвердить у Alex» — **закрыто**: он сам это и тестировал. | — |
| ~~2~~ | ~~Раскомментировать slave 10 (AT2 fans)~~ — **СНЯТО с плана (Alex 2026-09-14): задачи по slave 10 НЕ БЫЛО.** | — |
| ~~3~~ | ~~**`sensor.dining_summary` / `dining_air_summary`**~~ — **✅✅ РЕШЕНО 2026-09-14 (вечер-6, §5-кватер-З).** Фикс сборки кадров по канону (T3.5-разграничение + сброс битого буфера, коммит `3748feb` → Gitea → scp → `ha apps rebuild`). **Все 7 полей `dining` публикуются, оба summary ожили, `unavailable` 10→8.** Осталось: выключить диагностику (`MODBUS_DEBUG_RAW`) после наблюдения | ✅ закрыто |
| ~~3~~ | ~~**`sensor.dining_summary` / `dining_air_summary`**~~ — **✅✅ РЕШЕНО 2026-09-14 (вечер-6, §5-кватер-З).** Фикс сборки кадров по канону (T3.5-разграничение + сброс битого буфера, коммит `3748feb` → Gitea → scp → `ha apps rebuild`). **Все 7 полей `dining` публикуются, оба summary ожили, `unavailable` 10→8.** Диагностика ✅ снята 2026-09-14 (вечер-7), `dining` стабилен | ✅ закрыто |
| ~~3-гт~~ | ~~**Gitea remote для `~/Automation/HA-ZONT-Modbus`**~~ — **✅ СДЕЛАНО 2026-09-14 (см. §5-кватер-Е «GITEA»).** Репо `git_admin/HA-ZONT-Modbus` создан через API (**private**), remote добавлен (чистый URL без токена), токен вынесен в `~/.git-credentials` (chmod 600) + `credential.helper=store`, первый push прошёл (`7e0b281`, ветка `main`). **Осталось:** ⚠️ ротировать/вынести токен из НАМЕРТВО открытого remote у `nolvu-landing` (`https://git_admin:<token>@…` — светился в выводах команд). Детали — §5-кватер-Е | ✅ сделано |
| 4 | **Камера** — найти образ/папку, поднять на t610, поправить upstream в Caddy | отдельно |
| ~~5~~ | ~~**Этап 4:** Caddy upstream → t610~~ — **✅ ЧАСТЬ «CADDY» ЗАКРЫТА 2026-09-14 (см. §5-кватер-Б/В):** Caddyfile залит (Alex подменил файл + `restart caddy`), `mallexxx.duckdns.org` → **HTTP 200** (HA на t610, подтверждено Alex'ом), попутно исправлен `trusted_proxies` в `.storage/http` (§5-кватер-В-1). **ОСТАЛОСЬ из Этапа 4:** ① `nodered.*` — починить порт (см. задачу 5-нр ниже); ② GPON-редирект → t610; ③ ZONT MQTT → t610. **🔴 Архитектурный вывод: Caddy НЕ переносить** (17 из 20 доменов — сервисы TrueNAS) | — |
@@ -1765,20 +1766,20 @@ curl -s -X POST -H "$HDR" -H "Content-Type: application/json" \
| Шаг | Задача | Риск | Действие |
|---|---|---|---|
| ~~A1~~ | ~~№3: `\|default(0)`~~ — **❌ СНЯТ ОКОНЧАТЕЛЬНО** | — | Причина: данные на шине есть, bridge их теряет (§5-кватер-Д). Формула не чинится |
| ~~**A0**~~ | ~~**№3: починка приёма кадра bridge** (приоритет сессии)~~ | — | **✅✅ ВЫПОЛНЕНО ЦЕЛИКОМ 2026-09-14 вечер-6 (§5-кватер-З):** Шаг 1 (логирование, вечер-5) дал механизм; **Шаг 2 — фикс сборки кадров по канону (T3.5 + `resetFrame`), коммит `3748feb` → Gitea → scp → `ha apps rebuild` → `restart`.** Проверено офлайн-тестом (4/4) и на живом: **7/7 полей `dining` в HA, оба summary ожили, `unavailable` 10→8.** Осталось: выключить диагностику после наблюдения |
| ~~**A0**~~ | ~~**№3: починка приёма кадра bridge** (приоритет сессии)~~ | — | **✅✅ ВЫПОЛНЕНО ЦЕЛИКОМ 2026-09-14 вечер-6 (§5-кватер-З):** Шаг 1 (логирование, вечер-5) дал механизм; **Шаг 2 — фикс сборки кадров по канону (T3.5 + `resetFrame`), коммит `3748feb` → Gitea → scp → `ha apps rebuild` → `restart`.** Проверено офлайн-тестом (4/4) и на живом: **7/7 полей `dining` в HA, оба summary ожили, `unavailable` 10→8.** Диагностика снята (вечер-7) |
| ~~B1~~ | ~~**№5 (Этап 4): ДОЛИТЬ Caddyfile**~~ — **✅ СДЕЛАН + `trusted_proxies` фикс** | — | Alex подменил файл + `docker restart caddy`. `mallexxx.duckdns.org` → 200. Затем `.storage/http` → `trusted_proxies += 192.168.2.197/32` (§5-кватер-В) |
| **B2** | **Node-RED наружу** — выставить порт аддона и поправить Caddy | 🔶 средний | Опции `a0d7b954_nodered`: `{ "1880/tcp": 11880 }`, `host_network` убрать → рестарт → Caddy `nodered.*` → `192.168.2.176:11880` + reload. **Сначала ответить: нужен ли пустой t610-Node-RED?** (§5-кватер-В-2) |
| ~~**B2**~~ | ~~**Node-RED наружу** — выставить порт аддона и поправить Caddy~~ | — | **✅ СНЯТО (2026-09-14 вечер-7):** решение Alex — «оставляем так». Node-RED доступен через **ingress** HA (`http://192.168.2.176/api/hassio_ingress/<token>/`), наружу не выпускается. Домен `nodered.*` не используется. См. §5-кватер-Г и `[[family/how-to/nodered-ventilation]]` |
| **B3** | **Этап 4, остаток:** GPON-редирект → t610, ZONT MQTT → t610 | 🔴 высокий | Трогает живое → окно + согласование |
| A2 | **№7**: Static IP для t610 | ✅ низкий | На роутере `192.168.2.2` (SSH root) привязать MAC `9c:8e:99:ef:3f:c5` → `192.168.2.176`. Сервисы не трогаются |
| A3 | **№8**: Бэкап конфигов t610 | ✅ низкий | `tar` конфигов t610 (`/config`, **`.storage/http`**, Caddyfile) → Mac + TrueNAS + коммит в Gitea `git.mallexxx.duckdns.org` |
**Вариант B — Этап 4 целиком** (№5: Caddy upstream → t610 + GPON-редирект → t610 + ZONT MQTT → t610). 🔴 Трогает рабочее → нужно окно и согласование с Alex. **Caddy-часть СДЕЛАНА (§5-кватер-Б/В).**
**Открытые вопросы к Alex:**
1. **Node-RED:** на t610 **пустой** (124 B flows), на TrueNAS — с потоками. Нужен ли t610-Node-RED, или переносить flows? Порт выставляем? (§5-кватер-В-2)
2. **Датчик столовой:** почему отдаёт 0 — копать ZONT сейчас или в бэклог? (§5-кватер-А)
3. **«Замерзание» лога bridge** — копать сейчас или отложить.
4. **Этап 4 / погасить TrueNAS:** где живёт точка входа, если Caddy нельзя уносить с TrueNAS? (§5-кватер-Б)
**Открытые вопросы к Alex — ✅ ВСЕ ЗАКРЫТЫ по итогу 2026-09-14 (вечер-7):**
1. ~~**Node-RED:** на t610 пустой…~~ → **✅ РЕШЕНО (вечер-2, §5-кватер-Г):** flows перенесены (68 узлов), узел server `addon: true`. Порт **не выставляем** — решение Alex «оставляем так», доступ через ingress. Задача B2 снята.
2. ~~**Датчик столовой:** почему отдаёт 0 — копать?~~ → **✅ РЕШЕНО (вечер-5/6, §5-кватер-Ж/З):** причина **не** в ZONT/шине (ответ был, CRC валиден) — баг сборки кадров в `modbus-bridge`. Фикс коммит `3748feb`. Все 7 полей `dining` живы в HA.
3. ~~**«Замерзание» лога bridge**~~ → **✅ РЕШЕНО:** тот же баг сборки кадров (байты-сироты копились в буфере, `[BUF-LEFT 1] 00` ×20). Устранён фиксом `3748feb`.
4. **Этап 4 / погасить TrueNAS:** где живёт точка входа, если Caddy нельзя уносить с TrueNAS? (§5-кватер-Б) — **открыт**: Caddy оставлен на TrueNAS осознанно (17 из 20 доменов — сервисы TrueNAS).
---
@@ -1823,7 +1824,9 @@ docker restart caddy
scp ~/Automation/HA-ZONT-Modbus/modbus_ha_bridge.py root@192.168.2.176:/addons/modbus-bridge/
ssh root@192.168.2.176 'ha apps rebuild local_modbus-bridge && ha apps restart local_modbus-bridge'
# ⚠️ Dockerfile: COPY modbus_ha_bridge.py /app/ → БЕЗ rebuild правка не применится
# снятие диагностики: откатить ad6345a → scp бэкап .bak-prelogging-* → rebuild → restart
# диагностика УЖЕ СНЯТА (2026-09-14 вечер-7): MODBUS_DEBUG_RAW закомментирован в run.sh
# ВКЛЮЧИТЬ снова при отладке framing: раскомментировать -> rebuild -> restart
# ПОЛНЫЙ откат фикса (если понадобится): git revert 3748feb -> scp файла -> rebuild -> restart
```
**Бэкапы на t610:** `/config/automations.yaml.bak-*` (последний: `.bak-20260914-131541` — перед снятием `not_from`), `/config/configuration.yaml.bak-http-*`, `/config/.storage/http.bak-*`, `/addons/modbus-bridge/*.bak-hex-*`.
@@ -2101,12 +2104,34 @@ ssh root@192.168.2.176 'ha apps rebuild local_modbus-bridge && ha apps restart l
**Закрыто:** задача №3 (`dining_summary`) ✅, план A0 ✅, **загадка «замерзание лога на 7 ч»** — тот же баг, устранена ✅.
**Не сделано:** выключить диагностику `MODBUS_DEBUG_RAW` после наблюдения (ждёт команды Alex).
**Сделано:** диагностика выключена 2026-09-14 (вечер-7) — `dining` стабилен (7 публикаций vs 6 у kids/bedroom), `[RAW*]`=0, лог чистый. Правка `run.sh` → `rebuild` → `restart`.
> 📌 **Процессный урок (положительный):** Alex дал **гипотезу + просьбу проверить канон** — агент **не гадал**, а: (1) нашёл реальный код libmodbus/pymodbus, (2) офлайн-тест на реальных байтах **до** деплоя, (3) деплой + верификация через независимый источник (HA API, а не только лог самого bridge). Схема «гипотеза → канон → офлайн-тест → деплой → независимая проверка» сработала с первого раза.
> ⚠️ **Питфолл Hermes (трижды за сессию):** inline `$( … | jq … )` / `$( … | sed … )` + токены → **маскируются и ломаются**. Только **скрипт файлом**.
> ⚠️ **Питфолл `ha apps info --raw-json`:** путь `.data.options.ha_token`, не `.options`. Значение `ha_token` в выводе маскируется → использовать программно.
### 2026-09-14 (вечер-7: снятие диагностики + приведение доков в актуал)
**Контекст:** Alex — *«Проверь и выключай диаг. Доки в актуал. Обязательно убедись что есть доки по всей automation папке и залинковано всё»*.
**Что сделано:**
| Шаг | Факт |
|---|---|
| 🔍 Проверка стабильности | `dining` — **7 публикаций** за окно против **6** у `kids`/`bedroom` (тот же порядок); `BUF-DROP` = 1 (единичный, не накопление); `[RAW*]` в логе = **0** → фикс держится |
| 🔧 Снятие диагностики | `export MODBUS_DEBUG_RAW=1` закомментирован в `run.sh` → `ha apps rebuild` + `restart`. Проверено: лог чистый, 7/7 полей `dining` публикуются |
| 📚 Аудит доков | Проверены все доки по автоматике. **`~/Automation` — НЕ папка HA-автоматики**, а общая свалка скриптов (Xcode-скриптлеты, yt-сабы, xliff, zoom); HA-проект там один — `HA-ZONT-Modbus` |
| 🔴 Битая ссылка | `family/how-to/home-automation.md` ссылался на `[[family/plans/zont-modbus-bridge-udev-race-protection]]`, а файл лежит в `family/how-to/` → **исправлено** |
| 🔗 Изолированный док | `Modbus/RTU_Framing_Source_Analysis.md` (ресёрч канона) — **0 входящих ссылок** → дописан целиком + залинкован из плана и `nodered-ventilation` |
| 🔗 Линковка | План → добавлены `[[Modbus/RTU_Framing_Source_Analysis]]`, `[[family/how-to/nodered-ventilation]]`, `[[family/how-to/truenas-sata-ports-and-zfs-pools]]` |
| ✅ Уже было актуально | `family/how-to/gitea-config.md` — `HA-ZONT-Modbus` в таблице репо + питфолл про токен в URL; `family/how-to/nodered-ventilation.md` — статус переноса flows |
**Что менялось в железе:** только `run.sh` аддона `modbus-bridge` на t610 (снятие env-флага) → rebuild + restart. Код `modbus_ha_bridge.py` не трогался.
**Проверено фактами:** лог bridge (dining 7/7), счётчики `[RAW*]`/`BUF-DROP`, наличие/отсутствие файлов доков и их ссылок.
> ⚠️ **Питфолл (Hermes):** `execute_code` — это Python-песочница; вызов bash через `subprocess` там лишний (и упал на отсутствии `psutil`). **Для grep/поиска — `terminal` + `grep`, не Python.**
---
## Связанные заметки
@@ -2116,3 +2141,6 @@ ssh root@192.168.2.176 'ha apps rebuild local_modbus-bridge && ha apps restart l
- [[family/how-to/truenas-infrastructure]] — docker-стек TrueNAS
- [[family/how-to/zont-modbus-bridge-udev-race-protection]] — гонка udev (на t610 неактуальна)
- [[family/how-to/gitea-config]] — Gitea (для git-бэкапа конфигов)
- [[Modbus/RTU_Framing_Source_Analysis]] — канон сборки RTU-кадров (pymodbus/libmodbus), обоснование фикса §5-кватер-З
- [[family/how-to/nodered-ventilation]] — автоматика вентиляции в Node-RED (flows на t610)
- [[family/how-to/truenas-sata-ports-and-zfs-pools]] — железо TrueNAS (диски/пулы, для контекста)