Добавить протокол DEIF, диагностику CAN и сохранение сессий
This commit is contained in:
159
python/set_devices/deif_xdi.py
Normal file
159
python/set_devices/deif_xdi.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user