Files
obsidian-vault/Modbus/RTU_Framing_Source_Analysis.md
T

134 lines
6.7 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 Frame Delimiting: pymodbus vs libmodbus (source analysis)
> Источник кода — реальные файлы с GitHub, скачаны при разборе бага `modbus-bridge`
> (см. [[family/plans/t610-home-automation]] §5-кватер-З и §5-кватер-Ж).
> Локальные копии: `/Users/admin/modbus_src/`.
## Границы (URL)
- pymodbus dev (новый фреймер): `https://raw.githubusercontent.com/pymodbus-dev/pymodbus/dev/pymodbus/framer/rtu.py`
- pymodbus v3.4.1 (классический ModbusRtuFramer): `.../v3.4.1/pymodbus/framer/rtu_framer.py`
- pymodbus v3.0.2: `.../v3.0.2/pymodbus/framer/rtu_framer.py`
- pymodbus serial client (t35): `.../v3.4.1/pymodbus/client/serial.py`, `.../v3.6.9/pymodbus/client/serial.py`
- libmodbus: `https://raw.githubusercontent.com/stephane/libmodbus/master/src/modbus-rtu.c`, `.../src/modbus.c`, `.../src/modbus-rtu-private.h`
## Константы
pymodbus v3.0.2/3.4.1 `ModbusRtuFramer`: `_min_frame_size = 4`, `_hsize = 0x01`.
Таблица baud (докстринг, 1.5c = 18 бит, 3.5c = 38 бит):
```
Baud 1.5c (18 bits) 3.5c (38 bits)
1200 13333.3 us 31666.7 us
4800 3333.3 us 7916.7 us
9600 1666.7 us 3958.3 us
19200 833.3 us 1979.2 us
38400 416.7 us 989.6 us
1 Byte = start + 8 bits + parity + stop = 11 bits
```
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/регистров