Добавить поддержку GAS 167 и УМП, улучшить обмен по CAN и последовательному порту
This commit is contained in:
@@ -53,8 +53,8 @@ def decode(raw_id: int, data: bytes, *, ide: bool = True,
|
||||
"0x%04X %s" % (item.address, item.name or "регистр"),
|
||||
"0x%04X (%d)" % (item.value, item.signed_value),
|
||||
))
|
||||
if not frame.from_device and item.address == 0x7F:
|
||||
result.fields.append(("Команды ПМ", frame.command_description(item.value)))
|
||||
if item.address == 0x7F:
|
||||
result.fields.append(("Активные биты Commands", frame.command_description(item.value)))
|
||||
if frame.present_mask == 0:
|
||||
result.warnings.append("В адресном слове не отмечен ни один регистр")
|
||||
return result
|
||||
|
||||
@@ -13,15 +13,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
from . import can_transport as tr
|
||||
from protocan import protocan as pc
|
||||
from protocan.gas_167 import REGISTERS
|
||||
|
||||
#: Типы сообщений, которые генератор выдаёт по кругу случайно.
|
||||
KINDS = ("pulse", "analog", "modbus", "gas", "status", "error", "discrete")
|
||||
|
||||
|
||||
class UmpDemoSource:
|
||||
"""Two simulated UMP nodes using the assigned global GAS map.
|
||||
|
||||
Call next_frames every 250 ms: four words per node per tick and a
|
||||
heartbeat every second. Device Type=3, Device=5/6 are demo addresses,
|
||||
not discovery results or a claim about firmware 167/v1 support.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tick = 0
|
||||
self._seq = 0
|
||||
|
||||
def _frame(self, node: int, kind: int, body: int, data: bytes) -> bytes:
|
||||
self._seq = (self._seq + 1) & 0xFF
|
||||
identifier = pc.ProtoCanId.build(1, 1, 3, node, kind, body)
|
||||
return tr.Frame(seq=self._seq, flags=tr.FLAG_IDE,
|
||||
can_id=identifier, data=data).encode()
|
||||
|
||||
def next_frames(self) -> list[bytes]:
|
||||
"""Return encoded bridge frames; one full map takes eight seconds."""
|
||||
packets = []
|
||||
# Show LOAD state immediately, then walk the entire dictionary.
|
||||
start = (124 + self._tick * 4) % len(REGISTERS)
|
||||
for node, key in ((5, "ump1_id"), (6, "ump2_id")):
|
||||
if self._tick % 4 == 0:
|
||||
packets.append(self._frame(
|
||||
node, pc.MsgType.PULSE, 0,
|
||||
bytes([(self._tick // 4) & 0xFF])))
|
||||
values = []
|
||||
for entry in REGISTERS[start:start + 4]:
|
||||
offset = entry["modbus"]
|
||||
value = int(2000 + 700 * math.sin(self._tick / 12 + offset / 8 + node))
|
||||
command = 0x80 if (self._tick // 32) % 2 == 0 else 0x40
|
||||
value = {82: 1000, 124: 8, 125: 0, 126: node, 127: command}.get(offset, value)
|
||||
values.append(value.to_bytes(2, "little"))
|
||||
packets.append(self._frame(
|
||||
node, pc.MsgType.GENERAL_ADDRESS_SPACE,
|
||||
REGISTERS[start][key], b"".join(values)))
|
||||
self._tick += 1
|
||||
return packets
|
||||
|
||||
|
||||
class BusDemoSource:
|
||||
"""@brief Источник демонстрационных кадров шины.
|
||||
|
||||
@@ -58,11 +102,15 @@ class BusDemoSource:
|
||||
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
|
||||
for _ in range(cnt))
|
||||
elif kind == "gas":
|
||||
adr = self._rng.randint(0, 0x400)
|
||||
# Exercise the shared named map in the ordinary demo too.
|
||||
dev_type, dev_id = 3, self._rng.choice((5, 6))
|
||||
key = "ump1_id" if dev_id == 5 else "ump2_id"
|
||||
count = self._rng.randint(1, 4)
|
||||
adr = REGISTERS[self._rng.randrange(len(REGISTERS) - count + 1)][key]
|
||||
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id,
|
||||
pc.MsgType.GENERAL_ADDRESS_SPACE, adr)
|
||||
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
|
||||
for _ in range(self._rng.randint(1, 4)))
|
||||
for _ in range(count))
|
||||
elif kind == "status":
|
||||
now = datetime.now()
|
||||
cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.BROADCAST,
|
||||
|
||||
@@ -32,6 +32,8 @@ from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import chain
|
||||
from typing import Iterable
|
||||
from protocan import protocan as pc
|
||||
from protocan.gas_167 import REGISTERS
|
||||
|
||||
#: Распространённые скорости UART для подключения платы и RS485-приёмника.
|
||||
UART_BAUDS = (
|
||||
@@ -596,7 +598,14 @@ class BoardDemoSource:
|
||||
"""
|
||||
|
||||
#: Идентификаторы, по которым генератор идёт по кругу.
|
||||
IDS = ((True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF))
|
||||
IDS = (
|
||||
(True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF),
|
||||
# Assigned UMP map, simulated to demonstrate the common GAS decoder.
|
||||
(True, pc.ProtoCanId.build(1, 1, 3, 5, pc.MsgType.GENERAL_ADDRESS_SPACE,
|
||||
REGISTERS[124]['ump1_id'])),
|
||||
(True, pc.ProtoCanId.build(1, 1, 3, 6, pc.MsgType.GENERAL_ADDRESS_SPACE,
|
||||
REGISTERS[124]['ump2_id'])),
|
||||
)
|
||||
|
||||
def __init__(self, rng: random.Random | None = None) -> None:
|
||||
self._rng = rng or random.Random()
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
|
||||
from . import can_transport as tr
|
||||
from protocan import protocan as pc
|
||||
from protocan.gas_167 import assigned_metadata
|
||||
from . import balsam_can
|
||||
|
||||
#: Расшифровка поля LEC регистра CAN_ESR.
|
||||
@@ -64,8 +65,27 @@ def decode_frame(frame: tr.Frame) -> pc.Decoded:
|
||||
decoded_balsam = balsam_can.decode(
|
||||
frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
if decoded_balsam is not None:
|
||||
decoded_balsam.fields.append(("Автоопределение", "Balsam 167: идентификатор legacy CAN"))
|
||||
return decoded_balsam
|
||||
return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
if (frame.ide and not frame.rtr
|
||||
and result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE
|
||||
and len(frame.data) <= 8 and len(frame.data) % 2 == 0
|
||||
and result.registers is not None):
|
||||
addresses = [address for address, _value in result.registers] or [result.id.body]
|
||||
entries = [assigned_metadata(address) for address in addresses]
|
||||
if all(entries) and len({entry['node'] for entry in entries}) == 1:
|
||||
node = entries[0]['node'] - 4
|
||||
result.protocol = "УМП / ProtoCAN GAS (назначенные ID)"
|
||||
result.device_label = "УМП %d" % node
|
||||
result.message_label = "GAS · УМП %d" % node
|
||||
result.fields.append(("Автоопределение",
|
||||
"УМП %d: все GAS ID входят в назначенную карту; поддержка прошивкой не подтверждена" % node))
|
||||
return result
|
||||
result.fields.append(("Автоопределение", "Разбор ProtoCAN; профиль прибора не определён"))
|
||||
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
|
||||
result.summary += " · профиль/узел не определены — выберите протокол вручную"
|
||||
return result
|
||||
|
||||
dec = pc.Decoded(id=pc.ProtoCanId.parse(frame.can_id), dlc=len(frame.data),
|
||||
data=frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
|
||||
@@ -10,14 +10,33 @@ from .can_bridge import decode_frame
|
||||
|
||||
PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"),
|
||||
("devboard", "DevBoard_V1 / SETCAN"),
|
||||
("deif", "DEIF XDi-192 / CANopen"))
|
||||
("deif", "DEIF XDi-192 / CANopen"),
|
||||
("protocan", "ProtoCAN / GAS"),
|
||||
("ump_balsam", "УМП / Balsam 167 (legacy CAN)"),
|
||||
("ump1_gas167", "УМП 1 / ProtoCAN — карта GAS 167/v1"),
|
||||
("ump2_gas167", "УМП 2 / ProtoCAN — карта GAS 167/v1"),
|
||||
("ump_gas", "УМП / ProtoCAN GAS — новые ID 5000/6000"))
|
||||
|
||||
|
||||
def decode_protocol(frame, protocol="auto"):
|
||||
if protocol == "deif":
|
||||
from .deif_xdi import decode
|
||||
return decode(frame)
|
||||
if protocol == "devboard":
|
||||
if protocol == "ump_balsam":
|
||||
from .balsam_can import decode
|
||||
result = decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
if result is not None:
|
||||
return result
|
||||
result = pc.Decoded(pc.ProtoCanId.parse(frame.can_id), len(frame.data),
|
||||
bytes(frame.data), ide=frame.ide, rtr=frame.rtr,
|
||||
protocol="BALZAM 167", message_label="Не Balsam 167")
|
||||
result.summary = "УМП / Balsam 167: CAN ID или формат кадра не соответствует протоколу"
|
||||
result.warnings.append(result.summary)
|
||||
return result
|
||||
if protocol in ("ump1_gas167", "ump2_gas167"):
|
||||
from .ump_can import decode_gas167
|
||||
return decode_gas167(frame, node=1 if protocol == "ump1_gas167" else 2)
|
||||
if protocol in ("devboard", "protocan", "ump_gas"):
|
||||
return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
return decode_frame(frame)
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ class MonitorControls(QWidget):
|
||||
self.protocol = QComboBox()
|
||||
for key, label in PROTOCOLS:
|
||||
self.protocol.addItem(label, key)
|
||||
self.protocol.setMinimumContentsLength(28)
|
||||
self.protocol.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
|
||||
self.protocol.setToolTip(
|
||||
"Выберите формат CAN вручную. УМП / Balsam — legacy CAN; "
|
||||
"карта 167/v1 — имена слов 0…127 и 0x5000…0x507F в ProtoCAN; "
|
||||
"новые ID — назначенные диапазоны 5000…5127 и 6000…6127.")
|
||||
self.protocol.setCurrentIndex(self.protocol.findData(default))
|
||||
layout.addWidget(QLabel("Протокол"))
|
||||
layout.addWidget(self.protocol)
|
||||
|
||||
50
python/set_devices/ump_can.py
Normal file
50
python/set_devices/ump_can.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Explicit interpretation of ProtoCAN registers with the UMP 167/v1 map.
|
||||
|
||||
This is a manually selected dictionary, not detection of the legacy service
|
||||
CAN packet format or a claim that a particular firmware implements ProtoCAN.
|
||||
"""
|
||||
from protocan import protocan as pc
|
||||
from protocan.gas_167 import REGISTERS, describe_word
|
||||
|
||||
|
||||
def decode_gas167(frame, node):
|
||||
result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||||
result.device_label = "УМП %d (выбран вручную)" % node
|
||||
result.protocol = "УМП %d / ProtoCAN GAS 167/v1" % node
|
||||
if not frame.ide or frame.rtr or frame.is_error:
|
||||
return result
|
||||
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE,
|
||||
pc.MsgType.MODBUS_HOLDING):
|
||||
return result
|
||||
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
|
||||
start = result.id.body
|
||||
else:
|
||||
start, _count = pc.split_modbus(result.id.body)
|
||||
words = result.registers
|
||||
if words is None:
|
||||
return result
|
||||
labels = []
|
||||
for address, value in words or [(start, None)]:
|
||||
offset = address if 0 <= address < 128 else address - 0x5000
|
||||
label = "0x%04X" % address
|
||||
if 0 <= offset < len(REGISTERS):
|
||||
entry = REGISTERS[offset]
|
||||
label += " · " + entry['description']
|
||||
result.fields += [
|
||||
(" УМП 167/v1 0x%04X" % address, entry['description']),
|
||||
(" Назначенные ID УМП 1/2", "%d / %d" % (entry['ump1_id'], entry['ump2_id'])),
|
||||
(" Доступ", entry['access']),
|
||||
]
|
||||
else:
|
||||
label += " · вне карты 167/v1"
|
||||
if value is not None:
|
||||
label += " = %d [0x%04X]" % (value, value)
|
||||
description = describe_word(offset, value)
|
||||
if description:
|
||||
label += " — " + description
|
||||
result.fields.append((" Биты 0x%04X" % address, description))
|
||||
labels.append(label)
|
||||
result.summary = "УМП %d · GAS 167/v1%s: %s" % (
|
||||
node, " запрос" if not words else "", "; ".join(labels))
|
||||
result.fields.append((" Карта", "167/v1 выбрана вручную"))
|
||||
return result
|
||||
Reference in New Issue
Block a user