Files
templates/python/set_devices/deif_xdi.py

160 lines
8.7 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.
"""DEIF XDi-192 observations from CAN analysis dated 2026-10-03.
Based on DEIF 4189350066D, sections 3.2, 11 and appendix 9.
Angle and AC2 percent use the verified working-library resolution of 0.1.
Absolute AC2 power remains RAW until its physical unit is confirmed.
"""
from protocan import protocan as pc
SELECTORS = {
bytes.fromhex("01 01 30 07"): "Азимут / руль — задание",
bytes.fromhex("01 81 30 02"): "RPM / обороты",
bytes.fromhex("01 91 30 02"): "Обороты винта, %",
bytes.fromhex("01 21 34 02"): "Электрическая мощность AC2",
bytes.fromhex("01 31 34 02"): "Электрическая мощность AC2, %",
}
MASTER_SELECTORS = tuple(SELECTORS)
SELECTORS[bytes.fromhex("01 01 30 02")] = "Азимут / руль — фактическое значение"
OBJECTS = {
0x3000: ("Азимут / руль", "°"),
0x3080: ("Обороты винта", "rpm"),
0x3090: ("Обороты винта, %", "%"),
0x3420: ("Электрическая мощность AC2", None),
0x3430: ("Электрическая мощность AC2, %", "%"),
}
def decode(frame):
result = pc.Decoded(pc.ProtoCanId(frame.can_id, 0, 0, 0, 0, 0, 0),
len(frame.data), bytes(frame.data), ide=frame.ide,
rtr=frame.rtr, protocol="DEIF XDi-192")
result.fields = [("Протокол", "DEIF XDi-net / CANopen SAM-MPDO"),
("CAN ID", "0x%03X" % frame.can_id)]
def reject(message):
result.summary = message
result.warnings.append(message)
return result
if frame.ide or frame.rtr:
return reject("DEIF: ожидается STD data frame; EXT/RTR не интерпретируется")
if 0x701 <= frame.can_id <= 0x77F:
node = frame.can_id - 0x700
result.device_label = "Node %d" % node
result.message_label = "Heartbeat / Boot-up"
result.fields.append(("Node-ID", str(node)))
if len(frame.data) != 1:
return reject("DEIF: Heartbeat Node %d — ожидается DLC=1" % node)
state = {0: "Boot-up", 5: "Operational"}.get(frame.data[0])
if state is None:
state = "состояние 0x%02X (не описано в журнале)" % frame.data[0]
result.fields.append(("Состояние", state))
result.summary = "Node %d: %s" % (node, state)
return result
if not 0x201 <= frame.can_id <= 0x57F:
result.summary = "DEIF: CAN ID не описан в журнале"
return result
node = frame.can_id - 0x200
result.message_label = "XDi-net broadcast / SAM-MPDO"
result.fields.append(("COB-ID", "0x%03X" % frame.can_id))
if len(frame.data) != 8:
return reject("DEIF RPDO1: ожидается DLC=8")
selector = frame.data[:4]
result.fields.append(("Селектор Byte0..3", selector.hex(" ").upper()))
source = frame.data[0] & 0x7F
index = int.from_bytes(frame.data[1:3], "little")
subindex = frame.data[3]
result.fields.extend([("Источник Node-ID", str(source)),
("Индекс объекта", "0x%04X" % index),
("Подиндекс", "0x%02X" % subindex)])
dam = bool(frame.data[0] & 0x80)
if dam:
result.fields = [(key, value) for key, value in result.fields if key != "Источник Node-ID"]
result.fields.append(("Получатель Node-ID", str(source)))
result.message_label = "CANopen DAM-MPDO"
if not source or frame.can_id - source not in (0x200, 0x300, 0x400, 0x500):
return reject("DEIF: получатель не соответствует RPDO COB-ID")
elif not source or not 0x201 <= frame.can_id <= 0x27F or (node != 127 and source != node):
return reject("DEIF: источник не соответствует COB-ID")
result.device_label = ("Получатель Node %d" if dam else "Источник Node %d") % source
result.fields[0] = ("Протокол", "CANopen DAM-MPDO" if dam else "DEIF XDi-net / CANopen SAM-MPDO")
family, instance = index & 0xFFF0, index & 0xF
if family not in OBJECTS or not instance or subindex not in (2, 7):
return reject("DEIF: неизвестный объект/подиндекс 0x%04X:%02X" % (index, subindex))
name, unit = OBJECTS[family]
kind = "фактическое значение" if subindex == 2 else "задание"
name = "%s №%d — %s" % (name, instance, kind)
raw = int.from_bytes(frame.data[4:6], "little", signed=True)
result.fields.extend([("Параметр", name),
("RAW INT16 LE", str(raw)),
("Byte6..7", frame.data[6:].hex(" ").upper())])
result.summary = "Node %d · 0x%04X:%02X · %s: RAW=%d" % (source, index, subindex, name, raw)
if raw in (-32768, 32766, 32767):
status = {-32768: "ниже диапазона", 32766: "выше диапазона", 32767: "ошибка данных"}[raw]
result.fields.append(("Статус", status))
result.summary += "; " + status
result.warnings.append(status)
elif unit and (subindex == 2 or family == 0x3000):
value = "%.1f %s" % (raw / 10, unit)
result.fields.append(("Значение", value))
result.fields.append(("Масштаб", "RAW ×0.1"))
result.summary += "; " + value
elif family == 0x3420 and subindex == 2:
result.fields.append(("Масштаб / единицы", "RAW ×1; физическая единица не подтверждена"))
result.summary += "; RAW ×1 (физическая единица не подтверждена)"
else:
result.fields.append(("Масштаб / единицы", "не установлены"))
result.summary += "; масштаб и единицы не установлены"
if frame.data[6:] != b"\x00\x00":
result.warnings.append("Byte6..7: для I16 ожидается нулевое заполнение")
return result
def master_frames(node_id, raw_values, can_id=None, mode="sam", angle_subindex=7):
"""Build a five-parameter STD RPDO1 cycle, using signed INT16 LE values."""
from .can_transport import build_frame
if not isinstance(node_id, int) or not 1 <= node_id <= 127:
raise ValueError("Node-ID должен быть от 1 до 127")
identifier = 0x200 + node_id if can_id is None else can_id
if mode not in ("sam", "dam"):
raise ValueError("Выберите SAM или DAM")
if angle_subindex not in (2, 7):
raise ValueError("Угол: 0x02 — фактический, 0x07 — задание")
if not isinstance(identifier, int) or not 0x201 <= identifier <= 0x57F:
raise ValueError("STD ID вне диапазона RPDO")
if mode == "sam" and identifier not in (0x200 + node_id, 0x27F):
raise ValueError("XDi-net STD ID: 0x200 + источник или 0x27F")
if mode == "dam" and identifier - node_id not in (0x200, 0x300, 0x400, 0x500):
raise ValueError("CANopen DAM: STD ID = база RPDO (200/300/400/500 hex) + получатель")
values = list(raw_values)
if len(values) != len(MASTER_SELECTORS):
raise ValueError("Нужны пять RAW-параметров")
frames = []
for selector, value in zip(MASTER_SELECTORS, values):
if selector[1:] == bytes.fromhex("01 30 07"):
selector = selector[:3] + bytes([angle_subindex])
if not isinstance(value, int) or not -32768 <= value <= 32767:
raise ValueError("RAW должен быть INT16: -32768…32767")
data = bytes([node_id | (0x80 if mode == "dam" else 0)]) + selector[1:] + value.to_bytes(2, "little", signed=True) + b"\x00\x00"
frames.append(build_frame(identifier, data, ide=False))
return frames
# Display binding catalogue. None means library-dependent resolution, not degrees/%.
DISPLAY_PRESETS = {
"power": ("POWER · AC2 (RAW)", 0x3420, 2, None),
"rpm": ("RPM · обороты (×0.1)", 0x3080, 2, 0.1),
"angle": ("Угол · задание (×0.1°)", 0x3000, 7, 0.1),
"angle_actual": ("Угол · фактический (×0.1°)", 0x3000, 2, 0.1),
"power_percent": ("POWER% · AC2 (×0.1%)", 0x3430, 2, 0.1),
"rpm_percent": ("RPM% (×0.1)", 0x3090, 2, 0.1),
}
def display_preset(key, node, instance=1, dam=False):
"""Return a catalogue selector and its known scale, or None for RAW."""
if not 1 <= node <= 127 or not 1 <= instance <= 15:
raise ValueError("Node-ID: 1…127; instance: 1…15")
_, base, subindex, scale = DISPLAY_PRESETS[key]
selector = bytes([node | (0x80 if dam else 0)]) + (base + instance).to_bytes(2, "little") + bytes([subindex])
return selector.hex(" ").upper(), scale