Files
templates/python/protocan/protocan.py

726 lines
28 KiB
Python
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.
# -*- 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)