# -*- coding: utf-8 -*- """Разбор и сборка сообщений ProtoCAN — прикладного уровня шины CAN. Раскладка полей повторяет ``ProtoCanId_t`` из SETCAN/Inc/protocan.h: битовые поля укладываются от младших бит к старшим. Модуль не импортирует Qt: разбор переносим и проверяется host-тестами. Источник контракта — ``templates/c/set-protocol``. """ from __future__ import annotations from dataclasses import dataclass, field from enum import IntEnum from typing import List, Optional try: from .native import get_native_core _SHARED_PROTOCOL = get_native_core() except (ImportError, OSError, RuntimeError): _SHARED_PROTOCOL = None EXTENDED_ID_MASK = 0x1FFFFFFF # -------------------------------------------------------------------------- # Перечисления протокола # -------------------------------------------------------------------------- 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 BOOT_CONTROL = 0b1001 BOOT_DATA_A = 0b1010 BOOT_DATA_B = 0b1011 BOOT_STATUS = 0b1100 BOOT_DISCOVERY = 0b1101 SETTINGS = 0b1110 PULSE = 0b1111 class SettingsResult(IntEnum): OK = 0x00 INVALID_DLC = 0x01 INVALID_ROM_CRC = 0x02 NOT_FOUND = 0x03 ALREADY_ASSIGNED = 0x04 EEPROM_ERROR = 0x05 INVALID_LOCATION = 0x06 BUSY = 0x07 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.BOOT_CONTROL: "Boot Control", MsgType.BOOT_DATA_A: "Boot Data A", MsgType.BOOT_DATA_B: "Boot Data B", MsgType.BOOT_STATUS: "Boot Status", MsgType.BOOT_DISCOVERY: "Boot Discovery", MsgType.SETTINGS: "Settings", MsgType.PULSE: "Pulse", } #: Имена адресов из листа «Интерфейс протокола» исходного XLSX. #: Для остальных Device Type таблица задаёт только диапазон Device 0x0..0xF, #: но не присваивает приборам имён. DEVICE_TYPE_NAMES = { 0x0: "Верхний уровень", } DEVICE_ADDRESS_NAMES = { (0x7, 0xD): "configurator", } SETTINGS_RESULT_RU = { SettingsResult.OK: "успех", SettingsResult.INVALID_DLC: "неверный DLC", SettingsResult.INVALID_ROM_CRC: "неверный family code или CRC ROM", SettingsResult.NOT_FOUND: "ROM не найден на 1-Wire", SettingsResult.ALREADY_ASSIGNED: "ROM уже назначен другой локации", SettingsResult.EEPROM_ERROR: "ошибка EEPROM", SettingsResult.INVALID_LOCATION: "неверная локация", SettingsResult.BUSY: "устройство занято", } 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, "")) def device_name(device_type: int, device: int) -> str: """Имя пары Device Type/Device из исходной таблицы протокола.""" return DEVICE_ADDRESS_NAMES.get( (device_type, device), DEVICE_TYPE_NAMES.get(device_type, "")) # -------------------------------------------------------------------------- # Идентификатор # -------------------------------------------------------------------------- @dataclass class ProtoCanId: """Разобранный 29-битный идентификатор. Имена полей повторяют исходную таблицу ``Протокол CAN и ОАП.xlsx``: ``Priority | ПМ | Device Type | Device | Msg Type | Body``. Свойства ``route``, ``device_id`` и ``msg_body`` оставлены как совместимые псевдонимы для старого кода. """ raw: int body: int msg_type: int device: int device_type: int pm: int priority: int @staticmethod def parse(raw: int) -> "ProtoCanId": raw &= EXTENDED_ID_MASK if _SHARED_PROTOCOL is not None: priority, pm, device_type, device, msg_type, body = ( _SHARED_PROTOCOL.id_unpack(raw)) return ProtoCanId(raw, body, msg_type, device, device_type, pm, priority) return ProtoCanId( raw=raw, body=raw & 0xFFFF, msg_type=(raw >> 16) & 0xF, device=(raw >> 20) & 0xF, device_type=(raw >> 24) & 0x7, pm=(raw >> 27) & 0x1, priority=(raw >> 28) & 0x1, ) @staticmethod def build(priority: int, pm: Optional[int] = None, device_type: Optional[int] = None, device: Optional[int] = None, msg_type: Optional[int] = None, body: Optional[int] = None, **legacy: int) -> int: """Собирает ID из полей протокола. Старые именованные аргументы ``route``, ``device_id`` и ``msg_body`` принимаются для обратной совместимости. """ aliases = ( ("route", "pm", pm), ("device_id", "device", device), ("msg_body", "body", body), ) resolved = [] for old_name, new_name, value in aliases: old_value = legacy.pop(old_name, None) if value is not None and old_value is not None: raise TypeError("одновременно заданы %s и %s" % (new_name, old_name)) resolved.append(value if value is not None else old_value) if legacy: raise TypeError("неизвестное поле: %s" % next(iter(legacy))) pm, device, body = resolved required = { "pm": pm, "device_type": device_type, "device": device, "msg_type": msg_type, "body": body, } missing = [name for name, value in required.items() if value is None] if missing: raise TypeError("не заданы поля: %s" % ", ".join(missing)) assert pm is not None and device_type is not None and device is not None assert msg_type is not None and body is not None if _SHARED_PROTOCOL is not None: return _SHARED_PROTOCOL.id_pack( priority, pm, device_type, device, msg_type, body) return (((priority & 0x1) << 28) | ((pm & 0x1) << 27) | ((device_type & 0x7) << 24) | ((device & 0xF) << 20) | ((msg_type & 0xF) << 16) | (body & 0xFFFF)) @property def route(self) -> int: """Совместимый псевдоним поля ``ПМ``.""" return self.pm @property def device_id(self) -> int: """Совместимый псевдоним поля ``Device``.""" return self.device @property def msg_body(self) -> int: """Совместимый псевдоним поля ``Body``.""" return self.body @property def msg_type_name(self) -> str: try: return MSGTYPE_RU[MsgType(self.msg_type)] except ValueError: return "резерв 0x%X" % self.msg_type @property def device_name(self) -> str: """Табличное имя адреса Device Type/Device, если оно задано.""" return device_name(self.device_type, self.device) # -------------------------------------------------------------------------- # Раскладки 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 [28]", "%d — %s" % ( pid.priority, PRIORITY_RU.get(Priority(pid.priority), "")))) res.fields.append(("ПМ [27]", "%d — %s" % ( pid.pm, ROUTE_RU.get(Route(pid.pm), "")))) res.fields.append(("Device Type [26:24]", "%d (0b{:03b})".format( pid.device_type) % pid.device_type)) res.fields.append(("Device [23:20]", "%d (0b{:04b})".format( pid.device) % pid.device)) res.fields.append(("Msg Type [19:16]", "0x%X — %s" % ( pid.msg_type, pid.msg_type_name))) res.fields.append(("Body [15:0]", "0x%04X" % pid.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.BOOT_CONTROL: _decode_boot, MsgType.BOOT_DATA_A: _decode_boot, MsgType.BOOT_DATA_B: _decode_boot, MsgType.BOOT_STATUS: _decode_boot, MsgType.BOOT_DISCOVERY: _decode_boot, MsgType.SETTINGS: _decode_settings, 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_boot(res: Decoded) -> None: """Расшифровывает служебные кадры ProtoCAN Boot Protocol 1.0.""" kind = MsgType(res.id.msg_type) if kind == MsgType.BOOT_CONTROL: command = res.id.body & 0xFF session = res.id.body >> 8 names = {1: "IDENTIFY", 2: "ENTER_BOOT", 3: "BEGIN_IMAGE", 4: "BEGIN_COMPAT", 5: "ERASE", 6: "VERIFY", 7: "COMMIT", 8: "CONFIRM", 9: "REBOOT", 10: "ABORT", 11: "QUERY_PROGRESS"} res.summary = "BOOT CONTROL %s, session=%d" % ( names.get(command, "0x%02X" % command), session) elif kind in (MsgType.BOOT_DATA_A, MsgType.BOOT_DATA_B): res.summary = "BOOT DATA slot %s, block=%d" % ( "A" if kind == MsgType.BOOT_DATA_A else "B", res.id.body) elif kind == MsgType.BOOT_STATUS: command = res.id.body & 0xFF session = res.id.body >> 8 status = res.data[0] if res.data else None res.summary = "BOOT STATUS command=0x%02X, session=%d%s" % ( command, session, " status=0x%02X" % status if status is not None else "") else: res.summary = "BOOT DISCOVERY" if res.data[:4] == b"PONG" and len(res.data) == 8: res.summary = "PONG, FW %d.%d, protocol %d" % tuple(res.data[4:7]) def _decode_broadcast(res: Decoded) -> None: body, btype = split_broadcast(res.id.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.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.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.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.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.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.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_settings(res: Decoded) -> None: """SETTINGS: Body[15:8] = сборка Z, Body[7:0] = позиция Y.""" assembly = (res.id.body >> 8) & 0xFF position = res.id.body & 0xFF res.fields += [ (" SETTINGS.Assembly Z", "%d (0x%02X)" % (assembly, assembly)), (" SETTINGS.Position Y", "%d (0x%02X)" % (position, position)), ] if res.id.pm == Route.FROM_PM: if res.dlc == 0: operation = "GET" elif res.dlc == 8 and not any(res.data): operation = "CLEAR" res.fields.append((" ROM", "00 00 00 00 00 00 00 00")) elif res.dlc == 8: operation = "WRITE/REPLACE" res.fields.append((" ROM", " ".join("%02X" % b for b in res.data))) else: operation = "INVALID" res.warnings.append("Запрос SETTINGS должен иметь DLC = 0 или 8") else: if res.dlc == 8: operation = "RESPONSE" res.fields.append((" ROM", " ".join("%02X" % b for b in res.data))) elif res.dlc == 1: operation = "ERROR" code = res.data[0] try: result = SettingsResult(code) result_name = "%s — %s" % (result.name, SETTINGS_RESULT_RU[result]) except ValueError: result_name = "неизвестный код" res.fields.append((" SETTINGS.Result", "0x%02X — %s" % (code, result_name))) else: operation = "INVALID" res.warnings.append("Ответ SETTINGS должен иметь DLC = 1 или 8") res.summary = "SETTINGS %s: сборка %d, позиция %d" % ( operation, assembly, position) 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.body: res.warnings.append("Body у PULSE должен быть 0, получено 0x%04X" % res.id.body)