[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:
@@ -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/регистров
|
||||
|
||||
Reference in New Issue
Block a user