[2026-09-15] eagle: family/how-to/home-automation.md family/tech/zigbee-t610-z2m-i-zha.md personal/tech/modbus-rtu-sniffer-debugging.md
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# Modbus RTU sniffer / RS-485 — отладка
|
||||
|
||||
> Создано 2026-09-15. Зеркало скила `modbus-rtu-sniffer-debugging`.
|
||||
> Применение на живом железе: [[family/how-to/home-automation]] §9 (баг CO2), `<addons>/modbus-bridge`.
|
||||
|
||||
## Когда это нужно
|
||||
|
||||
Пассивный Modbus RTU сниффер:
|
||||
- публикует часть датчиков, часть — нет;
|
||||
- датчик **отвечает в шине** (валидный CRC), но bridge его не публикует;
|
||||
- чтения приходят **разрезанными** на куски;
|
||||
- лог **замирает** на часы, а процесс жив (`started`);
|
||||
- **или** кадр собирается нормально, но значение абсурдное (CO2 отрицательный, 1692 °C) — см. раздел «Неверные значения».
|
||||
|
||||
## Ключевое правило (канон, не фольклор)
|
||||
|
||||
Кадры Modbus RTU разделяются **тишиной в шине**, а не длиной. Кадр завершён
|
||||
только после **≥ T3.5 (3.5 символа)** тишины.
|
||||
|
||||
```
|
||||
t0 = (1 start + 8 data + 2 stop) / baudrate # сек на символ
|
||||
T3.5 = 3.5 * t0 при baud <= 19200
|
||||
T3.5 = 1.75 ms (фикс.) при baud > 19200
|
||||
```
|
||||
|
||||
На **9600 бод**: `t0 = 11/9600 = 1.146 ms` → **T3.5 = 4.01 ms**.
|
||||
|
||||
## Классический баг: сироты в буфере
|
||||
|
||||
Симптом: 12-байтные ответы публикуются, длинный (19 байт) — никогда.
|
||||
|
||||
1. Чтения разрезают кадр между итерациями.
|
||||
2. **Сирота** от предыдущего кадра залипает в начале буфера → сдвиг выравнивания.
|
||||
3. Сканер стартует с `frame[0] = 0x00` → CRC не совпадает никогда.
|
||||
4. Буфер чистится только при **успешном** матче → мусор копится вечно.
|
||||
|
||||
**Доказательство:** ответ датчика ЕСТЬ в шине с валидным CRC → это баг читателя, не устройства.
|
||||
|
||||
**Фикс — порядок обязателен: сначала гейт по тишине, потом скан, потом сброс мусора.**
|
||||
Скан во время приёма байт или очистка до скана — молча ломает всё.
|
||||
|
||||
## 🟠 Неверные значения: кадр парсится, число всё равно мусор
|
||||
|
||||
Отдельный класс багов. Кадр собрался, CRC OK, температура/влажность верные —
|
||||
но одно поле читается абсурдно (−76 ppm CO2, 1692 °C). Баг **не в парсере, а в конфиге**.
|
||||
|
||||
Цепочка:
|
||||
1. Раньше был баг фрейминга → парсер читал мало/не те байты.
|
||||
2. Симптом «починили» **ручной подгонкой `correction_offset`** (напр. `-925`).
|
||||
3. Фрейминг позже починили правильно → сырое значение уже в нужных единицах.
|
||||
4. Оставшийся `correction_offset` теперь **портит корректное значение**:
|
||||
`846.9 ppm − 925 = −78`. Молча, без ошибок.
|
||||
|
||||
**Как отличить от бага парсинга:**
|
||||
|
||||
```
|
||||
1. Взять сырые байты ответа из лога аддона ("Raw RTU: ...").
|
||||
2. Декодировать поле вручную (float32 BE / int16 + divider).
|
||||
3. Сырое значение уже правдоподобно?
|
||||
ДА → баг в correction_offset. УДАЛИТЬ, а не переподбирать.
|
||||
НЕТ → реальный баг offset'ов поля, править их.
|
||||
```
|
||||
|
||||
**Правило: коррекция — это мелкая калибровка (доли единицы), НЕ число размером с показание.**
|
||||
`-925` на ~850 ppm — красный флаг. `-4.5` на 28 °C — нормальная калибровка.
|
||||
Большие offset'ы = окаменевшие workaround'ы.
|
||||
|
||||
Также сверять **имя поля с реальностью**: конфиг звал slave 2 «Bedroom», а MQTT-топики — `kids/*`.
|
||||
Верить живому MQTT-топику, а не `device_name` в конфиге — имена дрейфуют.
|
||||
|
||||
**Проверять по СЫРОМУ источнику, а не по производной сущности.**
|
||||
`sensor.x_summary` (шаблон, склеивающий другие сенсоры) может выглядеть мусором,
|
||||
когда значения под ним исправны — и наоборот. Смотреть `modbus/sensors/<room>/<param>`
|
||||
на брокере или сырые байты, прежде чем объявлять датчик сломанным.
|
||||
Доклад «сводка сломана» вместо «датчик сломан» — потерянная сессия.
|
||||
|
||||
**Конфиг лежит там, где шаблон, а не там, где опции.**
|
||||
Для HA-аддонов: `config.yaml` = только опции (device, baud). Карта регистров и коррекции —
|
||||
в `data/config.template.tmpl`, откуда `run.sh` рендерит `/app/config.yml` при старте.
|
||||
Правка `config.yaml` не делает ничего. Шаблон **впекается в образ** → нужен `rebuild`, не `restart`.
|
||||
|
||||
**Инструменты на целевом хосте.** Минимальные HAOS/BusyBox хосты **не имеют `perl`**.
|
||||
Правка текста там → `awk`, либо писать новый файл локально и `scp`. Всегда `diff`
|
||||
старого с новым и проверять, что diff непустой, перед перезаписью.
|
||||
|
||||
## Питфоллы
|
||||
|
||||
- **Не хардкодить T3.5** — выводить из baud.
|
||||
- **Не сканировать, пока байты ещё идут** — иначе не отличить «частичный кадр» от «битого».
|
||||
- **`timeout` pyserial ≠ таймаут кадра.** T3.5 — мера тишины в шине, из baud.
|
||||
- **Одиночный байт — это норма**, а не поломка.
|
||||
- **Не винить устройство раньше логов сырых байт.**
|
||||
- **Dockerfile HA-аддона обычно `COPY app.py`** → правка файла без `rebuild` ничего не меняет.
|
||||
- **Диагностика за env-флагом требует rebuild** для переключения (env_vars в манифесте нет) — ставить в `run.sh` и пересобирать.
|
||||
|
||||
## Чеклист проверки
|
||||
|
||||
1. Сырой лог показывает целевой кадр целиком (`[RAW n]`).
|
||||
2. `Slave: N Func: 0x3 CRC OK: True`.
|
||||
3. Значения опубликованы (MQTT `[OK]` / HA push).
|
||||
4. Независимая проверка — читать **потребителя** (HA API states), а не только лог продюсера.
|
||||
5. Счётчики: `BUF-LEFT` = 0 (накопления нет).
|
||||
Reference in New Issue
Block a user