Files
obsidian-vault/personal/tech/modbus-rtu-sniffer-debugging.md
T

103 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (накопления нет).