feat(protocan-py): хостовые кодеки ProtoCAN и каталога на Python
Собрано из SETGUI/src/gui_desktop/core (protocan, can_transport, protocol, gas_catalog); в CAN_to_RS485/template/python лежала такая же копия. Только stdlib: ни Qt, ни pyserial — модулям передают bytes, порт и таймауты остаются делом вызывающего кода. Кодировщики совпадают побайтово с c/protocan-transport, что зафиксировано эталонами в его тестах. Добавлен __init__.py: gas_catalog импортирует protocol относительным импортом, без пакета копия в CAN_to_RS485/template не собиралась.
This commit is contained in:
519
python/protocan/protocan.py
Normal file
519
python/protocan/protocan.py
Normal file
@@ -0,0 +1,519 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Разбор и сборка сообщений ProtoCAN — прикладного уровня шины CAN.
|
||||
|
||||
Раскладка полей повторяет ``ProtoCanId_t`` из SETCAN/Inc/protocan.h:
|
||||
битовые поля укладываются от младших бит к старшим.
|
||||
|
||||
Модуль не импортирует Qt: разбор переносим и проверяется host-тестами.
|
||||
Источник контракта — CAN_to_RS485/docs/PROTOCOL.md, §1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Перечисления протокола
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class Priority(IntEnum):
|
||||
CRITICAL = 0
|
||||
STANDARD = 1
|
||||
|
||||
|
||||
class Route(IntEnum):
|
||||
FROM_PM = 0
|
||||
FROM_DEVICE = 1
|
||||
|
||||
|
||||
class MsgType(IntEnum):
|
||||
BROADCAST = 0b0000
|
||||
DISCRETE = 0b0001
|
||||
ANALOG = 0b0010
|
||||
GENERAL_ADDRESS_SPACE = 0b0011
|
||||
MODBUS_COIL = 0b0100
|
||||
MODBUS_DISCRETE = 0b0101
|
||||
MODBUS_HOLDING = 0b0110
|
||||
MODBUS_INPUT = 0b0111
|
||||
ERROR = 0b1000
|
||||
PULSE = 0b1111
|
||||
|
||||
|
||||
class BroadcastType(IntEnum):
|
||||
STATUS = 0
|
||||
ONOFF = 1
|
||||
RESTARTDEVICE = 2
|
||||
RTCSETUP = 3
|
||||
END = 0xFFF
|
||||
|
||||
|
||||
class DiscreteType(IntEnum):
|
||||
ACCIDENT = 0
|
||||
WARNING = 1
|
||||
CONTROL_SIGNALS = 2
|
||||
FLAGS = 3
|
||||
RESET = 4
|
||||
CHANGE_MODE = 5
|
||||
REQUEST_LIST_OF_PARAMETERS = 6
|
||||
END = 0xF
|
||||
|
||||
|
||||
class AnalogType(IntEnum):
|
||||
UNIVERSAL = 0
|
||||
SETTINGS = 1
|
||||
U = 2
|
||||
I = 3
|
||||
T = 4
|
||||
END = 0xF
|
||||
|
||||
|
||||
PRIORITY_RU = {
|
||||
Priority.CRITICAL: "критический",
|
||||
Priority.STANDARD: "стандартный",
|
||||
}
|
||||
|
||||
ROUTE_RU = {
|
||||
Route.FROM_PM: "от мастера (PM)",
|
||||
Route.FROM_DEVICE: "от устройства",
|
||||
}
|
||||
|
||||
MSGTYPE_RU = {
|
||||
MsgType.BROADCAST: "Broadcast",
|
||||
MsgType.DISCRETE: "Discrete",
|
||||
MsgType.ANALOG: "Analog",
|
||||
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
|
||||
MsgType.MODBUS_COIL: "Modbus Coil",
|
||||
MsgType.MODBUS_DISCRETE: "Modbus Discrete",
|
||||
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
||||
MsgType.MODBUS_INPUT: "Modbus Input",
|
||||
MsgType.ERROR: "Error",
|
||||
MsgType.PULSE: "Pulse",
|
||||
}
|
||||
|
||||
BROADCAST_RU = {
|
||||
BroadcastType.STATUS: "запрос статуса",
|
||||
BroadcastType.ONOFF: "вкл/выкл пульса",
|
||||
BroadcastType.RESTARTDEVICE: "перезапуск устройства",
|
||||
BroadcastType.RTCSETUP: "установка RTC",
|
||||
BroadcastType.END: "конец диапазона",
|
||||
}
|
||||
|
||||
DISCRETE_RU = {
|
||||
DiscreteType.ACCIDENT: "авария",
|
||||
DiscreteType.WARNING: "предупреждение",
|
||||
DiscreteType.CONTROL_SIGNALS: "управляющие сигналы",
|
||||
DiscreteType.FLAGS: "флаги",
|
||||
DiscreteType.RESET: "сброс",
|
||||
DiscreteType.CHANGE_MODE: "смена режима",
|
||||
DiscreteType.REQUEST_LIST_OF_PARAMETERS: "запрос списка параметров",
|
||||
DiscreteType.END: "конец диапазона",
|
||||
}
|
||||
|
||||
ANALOG_RU = {
|
||||
AnalogType.UNIVERSAL: "универсальный",
|
||||
AnalogType.SETTINGS: "уставки",
|
||||
AnalogType.U: "напряжение U",
|
||||
AnalogType.I: "ток I",
|
||||
AnalogType.T: "температура T",
|
||||
AnalogType.END: "конец диапазона",
|
||||
}
|
||||
|
||||
WEEKDAY_RU = {
|
||||
1: "Пн", 2: "Вт", 3: "Ср", 4: "Чт", 5: "Пт", 6: "Сб", 7: "Вс", 0: "?",
|
||||
}
|
||||
|
||||
|
||||
def _name(enum_cls, value, table):
|
||||
"""Имя элемента перечисления либо «резерв» для неизвестного кода."""
|
||||
try:
|
||||
item = enum_cls(value)
|
||||
except ValueError:
|
||||
return "резерв 0x%X" % value
|
||||
return "%s (%s)" % (item.name, table.get(item, ""))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Идентификатор
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class ProtoCanId:
|
||||
"""Разобранный 29-битный идентификатор."""
|
||||
|
||||
raw: int
|
||||
msg_body: int
|
||||
msg_type: int
|
||||
device_id: int
|
||||
device_type: int
|
||||
route: int
|
||||
priority: int
|
||||
|
||||
@staticmethod
|
||||
def parse(raw: int) -> "ProtoCanId":
|
||||
raw &= 0x1FFFFFFF
|
||||
return ProtoCanId(
|
||||
raw=raw,
|
||||
msg_body=raw & 0xFFFF,
|
||||
msg_type=(raw >> 16) & 0xF,
|
||||
device_id=(raw >> 20) & 0xF,
|
||||
device_type=(raw >> 24) & 0x7,
|
||||
route=(raw >> 27) & 0x1,
|
||||
priority=(raw >> 28) & 0x1,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build(priority: int, route: int, device_type: int, device_id: int,
|
||||
msg_type: int, msg_body: int) -> int:
|
||||
return (((priority & 0x1) << 28)
|
||||
| ((route & 0x1) << 27)
|
||||
| ((device_type & 0x7) << 24)
|
||||
| ((device_id & 0xF) << 20)
|
||||
| ((msg_type & 0xF) << 16)
|
||||
| (msg_body & 0xFFFF))
|
||||
|
||||
@property
|
||||
def msg_type_name(self) -> str:
|
||||
try:
|
||||
return MSGTYPE_RU[MsgType(self.msg_type)]
|
||||
except ValueError:
|
||||
return "резерв 0x%X" % self.msg_type
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Раскладки MsgBody
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def split_broadcast(body: int):
|
||||
"""BROADCAST: Body[3:0], Type[15:4]."""
|
||||
return body & 0xF, (body >> 4) & 0xFFF
|
||||
|
||||
|
||||
def split_discrete(body: int):
|
||||
"""DISCRETE: Body[11:0], Type[15:12]."""
|
||||
return body & 0xFFF, (body >> 12) & 0xF
|
||||
|
||||
|
||||
def split_analog(body: int):
|
||||
"""ANALOG: SensorID[11:0], Type[15:12]."""
|
||||
return body & 0xFFF, (body >> 12) & 0xF
|
||||
|
||||
|
||||
def split_modbus(body: int):
|
||||
"""MODBUS: RegCount[3:0], StrAdr[15:4]."""
|
||||
return (body >> 4) & 0xFFF, body & 0xF
|
||||
|
||||
|
||||
def split_error(body: int):
|
||||
"""ERROR: ErrorCode[7:0], Info[15:8]."""
|
||||
return body & 0xFF, (body >> 8) & 0xFF
|
||||
|
||||
|
||||
def merge_broadcast(bcast_type: int, body: int) -> int:
|
||||
return ((bcast_type & 0xFFF) << 4) | (body & 0xF)
|
||||
|
||||
|
||||
def merge_discrete(disc_type: int, body: int) -> int:
|
||||
return ((disc_type & 0xF) << 12) | (body & 0xFFF)
|
||||
|
||||
|
||||
def merge_analog(an_type: int, sensor_id: int) -> int:
|
||||
return ((an_type & 0xF) << 12) | (sensor_id & 0xFFF)
|
||||
|
||||
|
||||
def merge_modbus(str_adr: int, reg_count: int) -> int:
|
||||
return ((str_adr & 0xFFF) << 4) | (reg_count & 0xF)
|
||||
|
||||
|
||||
def merge_error(info: int, code: int) -> int:
|
||||
return ((info & 0xFF) << 8) | (code & 0xFF)
|
||||
|
||||
|
||||
def sensor_to_modbus_register(sensor_type: int, sensor_id: int) -> int:
|
||||
"""Макрос SensorToModbusRegister из protocan.h."""
|
||||
return ((sensor_type << 11) | sensor_id) & 0xFFFF
|
||||
|
||||
|
||||
def regs_le(data: bytes) -> List[int]:
|
||||
"""Пары байт -> регистры u16 little-endian (порядок из PROTOCAN_SEND_*)."""
|
||||
return [data[i] | (data[i + 1] << 8) for i in range(0, len(data) - 1, 2)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Результат разбора
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Decoded:
|
||||
"""Полный разбор одного кадра."""
|
||||
|
||||
id: ProtoCanId
|
||||
dlc: int
|
||||
data: bytes
|
||||
ide: bool = True
|
||||
rtr: bool = False
|
||||
#: Короткая сводка для колонки таблицы
|
||||
summary: str = ""
|
||||
#: Пары (поле, значение) для панели подробностей
|
||||
fields: List[tuple] = field(default_factory=list)
|
||||
#: Регистры, если сообщение их несёт
|
||||
registers: Optional[List[tuple]] = None
|
||||
#: Замечания о нарушениях протокола
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _ascii(data: bytes) -> str:
|
||||
return "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|
||||
|
||||
|
||||
def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Decoded:
|
||||
"""Разбирает кадр ProtoCAN в структуру Decoded."""
|
||||
pid = ProtoCanId.parse(raw_id)
|
||||
data = bytes(data[:8])
|
||||
res = Decoded(id=pid, dlc=len(data), data=data, ide=ide, rtr=rtr)
|
||||
|
||||
res.fields.append(("Priority", "%d — %s" % (
|
||||
pid.priority, PRIORITY_RU.get(Priority(pid.priority), ""))))
|
||||
res.fields.append(("Route", "%d — %s" % (
|
||||
pid.route, ROUTE_RU.get(Route(pid.route), ""))))
|
||||
res.fields.append(("DeviceType", "%d (0b{:03b})".format(pid.device_type) % pid.device_type))
|
||||
res.fields.append(("DeviceID", "%d (0b{:04b})".format(pid.device_id) % pid.device_id))
|
||||
res.fields.append(("MsgType", "0x%X — %s" % (pid.msg_type, pid.msg_type_name)))
|
||||
res.fields.append(("MsgBody", "0x%04X" % pid.msg_body))
|
||||
|
||||
if not ide:
|
||||
res.warnings.append("Стандартный ID: ProtoCAN использует расширенный (29 бит)")
|
||||
if rtr:
|
||||
res.warnings.append("Remote frame: полезная нагрузка отсутствует")
|
||||
|
||||
handler = {
|
||||
MsgType.BROADCAST: _decode_broadcast,
|
||||
MsgType.DISCRETE: _decode_discrete,
|
||||
MsgType.ANALOG: _decode_analog,
|
||||
MsgType.GENERAL_ADDRESS_SPACE: _decode_gas,
|
||||
MsgType.MODBUS_COIL: _decode_modbus_bit,
|
||||
MsgType.MODBUS_DISCRETE: _decode_modbus_bit,
|
||||
MsgType.MODBUS_HOLDING: _decode_modbus_reg,
|
||||
MsgType.MODBUS_INPUT: _decode_modbus_reg,
|
||||
MsgType.ERROR: _decode_error,
|
||||
MsgType.PULSE: _decode_pulse,
|
||||
}
|
||||
try:
|
||||
fn = handler[MsgType(pid.msg_type)]
|
||||
except ValueError:
|
||||
res.summary = "Неизвестный MsgType 0x%X" % pid.msg_type
|
||||
res.warnings.append("MsgType 0x%X не описан в protocan.h" % pid.msg_type)
|
||||
if data:
|
||||
res.fields.append(("Data ASCII", _ascii(data)))
|
||||
return res
|
||||
fn(res)
|
||||
return res
|
||||
|
||||
|
||||
def _decode_broadcast(res: Decoded) -> None:
|
||||
body, btype = split_broadcast(res.id.msg_body)
|
||||
res.fields.append((" Broadcast.Type", "0x%03X — %s" % (
|
||||
btype, _name(BroadcastType, btype, BROADCAST_RU))))
|
||||
res.fields.append((" Broadcast.Body", "0x%X (%d)" % (body, body)))
|
||||
|
||||
if btype == BroadcastType.STATUS:
|
||||
res.summary = "BROADCAST STATUS"
|
||||
if res.dlc == 7:
|
||||
h, m, s, yy, mo, dd, wd = res.data
|
||||
res.summary = "BROADCAST STATUS: 20%02d-%02d-%02d %02d:%02d:%02d" % (
|
||||
yy, mo, dd, h, m, s)
|
||||
res.fields += [
|
||||
(" Время", "%02d:%02d:%02d" % (h, m, s)),
|
||||
(" Дата", "20%02d-%02d-%02d" % (yy, mo, dd)),
|
||||
(" День недели", "%d (%s)" % (wd, WEEKDAY_RU.get(wd, "?"))),
|
||||
]
|
||||
_check_datetime(res, h, m, s, yy, mo, dd, wd)
|
||||
elif res.dlc == 0:
|
||||
res.summary = "BROADCAST STATUS (запрос)"
|
||||
else:
|
||||
res.warnings.append("STATUS-ответ должен иметь DLC = 7, получено %d" % res.dlc)
|
||||
elif btype == BroadcastType.ONOFF:
|
||||
res.summary = "BROADCAST ONOFF (инверсия флага пульса)"
|
||||
elif btype == BroadcastType.RESTARTDEVICE:
|
||||
page = int.from_bytes(res.data, "little") if res.data else 0
|
||||
ids = [i for i in range(res.dlc * 8) if (page >> i) & 1]
|
||||
res.summary = "BROADCAST RESTART: устройства %s" % (ids if ids else "нет")
|
||||
res.fields.append((" Битовая карта", "0x%X" % page))
|
||||
res.fields.append((" Перезапустить ID", ", ".join(map(str, ids)) or "—"))
|
||||
if res.dlc == 0:
|
||||
res.warnings.append("RESTART с DLC = 0 отвергается устройством")
|
||||
elif btype == BroadcastType.RTCSETUP:
|
||||
res.summary = "BROADCAST RTCSETUP"
|
||||
if res.dlc == 7:
|
||||
h, m, s, yy, mo, dd, wd = res.data
|
||||
res.summary = "BROADCAST RTCSETUP: 20%02d-%02d-%02d %02d:%02d:%02d" % (
|
||||
yy, mo, dd, h, m, s)
|
||||
res.fields += [
|
||||
(" Время", "%02d:%02d:%02d" % (h, m, s)),
|
||||
(" Дата", "20%02d-%02d-%02d" % (yy, mo, dd)),
|
||||
(" День недели", "%d (%s)" % (wd, WEEKDAY_RU.get(wd, "?"))),
|
||||
]
|
||||
_check_datetime(res, h, m, s, yy, mo, dd, wd)
|
||||
else:
|
||||
res.warnings.append("RTCSETUP принимается только при DLC = 7, получено %d" % res.dlc)
|
||||
else:
|
||||
res.summary = "BROADCAST 0x%03X" % btype
|
||||
|
||||
|
||||
def _is_leap(year: int) -> bool:
|
||||
"""Совпадает с IsLeapYear() в protocan.c: год двузначный, 2000-е."""
|
||||
y = 2000 + year
|
||||
return (y % 4 == 0 and y % 100 != 0) or y % 400 == 0
|
||||
|
||||
|
||||
_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
|
||||
|
||||
def _check_datetime(res: Decoded, h, m, s, yy, mo, dd, wd) -> None:
|
||||
if h > 23:
|
||||
res.warnings.append("Часы %d > 23" % h)
|
||||
if m > 59:
|
||||
res.warnings.append("Минуты %d > 59" % m)
|
||||
if s > 59:
|
||||
res.warnings.append("Секунды %d > 59" % s)
|
||||
if yy > 99:
|
||||
res.warnings.append("Год %d > 99" % yy)
|
||||
if mo == 0 or mo > 12:
|
||||
res.warnings.append("Месяц %d вне диапазона 1..12" % mo)
|
||||
else:
|
||||
limit = _DAYS[mo - 1] + (1 if (mo == 2 and _is_leap(yy)) else 0)
|
||||
if dd == 0 or dd > limit:
|
||||
res.warnings.append("Число %d вне диапазона 1..%d" % (dd, limit))
|
||||
if wd > 6:
|
||||
res.warnings.append("День недели %d > 6" % wd)
|
||||
|
||||
|
||||
def _decode_discrete(res: Decoded) -> None:
|
||||
body, dtype = split_discrete(res.id.msg_body)
|
||||
res.fields.append((" Discrete.Type", "0x%X — %s" % (
|
||||
dtype, _name(DiscreteType, dtype, DISCRETE_RU))))
|
||||
res.fields.append((" Discrete.Body", "0x%03X (%d)" % (body, body)))
|
||||
res.summary = "DISCRETE %s, body=%d, %d байт" % (
|
||||
_name(DiscreteType, dtype, DISCRETE_RU), body, res.dlc)
|
||||
if res.data:
|
||||
bits = " ".join("{:08b}".format(b) for b in reversed(res.data))
|
||||
res.fields.append((" Data биты (MSB..LSB)", bits))
|
||||
res.fields.append((" Data ASCII", _ascii(res.data)))
|
||||
if dtype == DiscreteType.REQUEST_LIST_OF_PARAMETERS:
|
||||
mask = int.from_bytes(res.data, "little") if res.data else 0
|
||||
idx = [i for i in range(res.dlc * 8) if (mask >> i) & 1]
|
||||
res.fields.append((" Запрошены параметры", ", ".join(map(str, idx)) or "—"))
|
||||
|
||||
|
||||
def _decode_analog(res: Decoded) -> None:
|
||||
sensor_id, atype = split_analog(res.id.msg_body)
|
||||
res.fields.append((" Analog.Type", "0x%X — %s" % (
|
||||
atype, _name(AnalogType, atype, ANALOG_RU))))
|
||||
res.fields.append((" Analog.SensorID", "%d (0x%03X)" % (sensor_id, sensor_id)))
|
||||
res.fields.append((" Modbus-регистр", "0x%04X" % sensor_to_modbus_register(atype, sensor_id)))
|
||||
text = _ascii(res.data)
|
||||
res.summary = "ANALOG %s, sensor %d" % (_name(AnalogType, atype, ANALOG_RU), sensor_id)
|
||||
if res.data:
|
||||
res.fields.append((" Data ASCII", text))
|
||||
res.summary += ", '%s'" % text
|
||||
if len(res.data) >= 6 and res.data[:2] in (b"US", b"IS", b"TS"):
|
||||
digits = res.data[2:6].decode("ascii", "replace")
|
||||
if digits.isdigit():
|
||||
res.fields.append((" Ответ по датчику", "%s №%d" % (
|
||||
res.data[:2].decode(), int(digits))))
|
||||
if int(digits) != sensor_id:
|
||||
res.warnings.append(
|
||||
"SensorID в ASCII (%d) не совпадает с ID в MsgBody (%d)"
|
||||
% (int(digits), sensor_id))
|
||||
if res.dlc == 0:
|
||||
res.summary += " (запрос)"
|
||||
|
||||
|
||||
def _decode_gas(res: Decoded) -> None:
|
||||
start = res.id.msg_body
|
||||
res.fields.append((" Адрес первого регистра", "0x%04X (%d)" % (start, start)))
|
||||
if res.dlc % 2:
|
||||
res.warnings.append("DLC = %d нечётный: регистры GAS передаются парами байт" % res.dlc)
|
||||
vals = regs_le(res.data)
|
||||
if len(vals) > 4:
|
||||
res.warnings.append("В одном кадре GAS не более 4 регистров, получено %d" % len(vals))
|
||||
res.registers = [(start + i, v) for i, v in enumerate(vals)]
|
||||
if res.data[:4] == b"GAS-":
|
||||
res.summary = "GAS отклик '%s'" % _ascii(res.data)
|
||||
res.fields.append((" Data ASCII", _ascii(res.data)))
|
||||
res.registers = None
|
||||
return
|
||||
for adr, v in res.registers:
|
||||
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v)))
|
||||
if vals:
|
||||
res.summary = "GAS: %d рег. с 0x%04X = %s" % (
|
||||
len(vals), start, " ".join("%04X" % v for v in vals))
|
||||
else:
|
||||
res.summary = "GAS: запрос по адресу 0x%04X" % start
|
||||
|
||||
|
||||
def _decode_modbus_bit(res: Decoded) -> None:
|
||||
str_adr, reg_count = split_modbus(res.id.msg_body)
|
||||
kind = "COIL" if res.id.msg_type == MsgType.MODBUS_COIL else "DISCRETE"
|
||||
res.fields.append((" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)))
|
||||
res.fields.append((" Modbus.RegCount", "%d" % reg_count))
|
||||
expected = reg_count % 8 + 1
|
||||
if res.dlc and res.dlc != expected:
|
||||
res.warnings.append(
|
||||
"Для %s ожидается DLC = RegCount %% 8 + 1 = %d, получено %d"
|
||||
% (kind, expected, res.dlc))
|
||||
if len(res.data) >= 2:
|
||||
val = res.data[0] | (res.data[1] << 8)
|
||||
bits = ["%d" % ((val >> i) & 1) for i in range(16)]
|
||||
res.registers = [(str_adr, val)]
|
||||
res.fields.append((" Значение", "0x%04X" % val))
|
||||
res.fields.append((" Биты 0..15", " ".join(bits)))
|
||||
res.summary = "MODBUS %s @0x%03X x%d = 0x%04X" % (kind, str_adr, reg_count, val)
|
||||
else:
|
||||
res.summary = "MODBUS %s @0x%03X x%d (запрос)" % (kind, str_adr, reg_count)
|
||||
|
||||
|
||||
def _decode_modbus_reg(res: Decoded) -> None:
|
||||
str_adr, reg_count = split_modbus(res.id.msg_body)
|
||||
kind = "HOLDING" if res.id.msg_type == MsgType.MODBUS_HOLDING else "INPUT"
|
||||
res.fields.append((" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)))
|
||||
res.fields.append((" Modbus.RegCount", "%d" % reg_count))
|
||||
if res.dlc and res.dlc != reg_count * 2:
|
||||
res.warnings.append(
|
||||
"Для %s ожидается DLC = 2 * RegCount = %d, получено %d"
|
||||
% (kind, reg_count * 2, res.dlc))
|
||||
if reg_count > 4:
|
||||
res.warnings.append("В одном кадре не более 4 регистров, в ID указано %d" % reg_count)
|
||||
vals = regs_le(res.data)
|
||||
res.registers = [(str_adr + i, v) for i, v in enumerate(vals)]
|
||||
for adr, v in res.registers:
|
||||
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v)))
|
||||
if vals:
|
||||
res.summary = "MODBUS %s @0x%03X: %s" % (
|
||||
kind, str_adr, " ".join("%04X" % v for v in vals))
|
||||
else:
|
||||
res.summary = "MODBUS %s @0x%03X x%d (запрос)" % (kind, str_adr, reg_count)
|
||||
|
||||
|
||||
def _decode_error(res: Decoded) -> None:
|
||||
code, info = split_error(res.id.msg_body)
|
||||
res.fields.append((" Error.Code", "0x%02X (%d)" % (code, code)))
|
||||
res.fields.append((" Error.Info", "0x%02X (%d)" % (info, info)))
|
||||
res.summary = "ERROR code=0x%02X info=0x%02X" % (code, info)
|
||||
if code == 0xFF and info == 0x00:
|
||||
res.summary += " — необработанный запрос (PROTOCAN_RequestError)"
|
||||
if res.dlc:
|
||||
res.warnings.append("ERROR передаётся с DLC = 0, получено %d" % res.dlc)
|
||||
|
||||
|
||||
def _decode_pulse(res: Decoded) -> None:
|
||||
if res.dlc >= 1:
|
||||
res.summary = "PULSE, счётчик %d" % res.data[0]
|
||||
res.fields.append((" Счётчик пульса", "%d (0x%02X)" % (res.data[0], res.data[0])))
|
||||
else:
|
||||
res.summary = "PULSE (без счётчика)"
|
||||
res.warnings.append("PULSE передаётся с DLC = 1")
|
||||
if res.id.msg_body:
|
||||
res.warnings.append("MsgBody у PULSE должен быть 0, получено 0x%04X" % res.id.msg_body)
|
||||
Reference in New Issue
Block a user