[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/регистров