Добавить поддержку GAS 167 и УМП, улучшить обмен по CAN и последовательному порту

This commit is contained in:
2026-10-11 21:23:21 +03:00
parent 3c0f6fd9e6
commit 20454ff7fa
17 changed files with 1797 additions and 37 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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