Собрано из 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 не собиралась.
520 lines
20 KiB
Python
520 lines
20 KiB
Python
# -*- 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)
|