Files
templates/python/set_devices/bus_demo.py

132 lines
6.4 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.
# Демонстрационная модель шины и устройств для интерфейса без оборудования. Синтетические
# ответы служат проверке пользовательского сценария; они не являются измерениями физической
# линии.
"""@file bus_demo.py
@brief Генератор трафика полевой шины: разбор проверяется без железа.
Повторяет то, что мост выдаёт в RS485: готовые транспортные кадры со
всеми типами сообщений ProtoCAN. Модуль не импортирует Qt — планировщик
живёт во вкладке, а сам генератор проверяется host-тестами.
"""
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 Источник демонстрационных кадров шины.
@param rng Источник случайности; передаётся в тестах для повторяемости.
"""
def __init__(self, rng: random.Random | None = None) -> None:
self._rng = rng or random.Random()
self._seq = 0
def next_frame(self) -> bytes:
"""@brief Собирает очередной кадр так же, как его отдал бы мост.
@return Готовые байты транспортного кадра, включая SOF и CRC.
"""
self._seq = (self._seq + 1) & 0xFF
kind = self._rng.choice(KINDS)
dev_type, dev_id = 1, 2
if kind == "pulse":
cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.PULSE, 0)
data = bytes([self._seq])
elif kind == "analog":
sid = self._rng.randint(1, 40)
atype = self._rng.choice([pc.AnalogType.U, pc.AnalogType.I, pc.AnalogType.T])
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.ANALOG,
pc.merge_analog(atype, sid))
tag = {pc.AnalogType.U: b"US", pc.AnalogType.I: b"IS",
pc.AnalogType.T: b"TS"}[atype]
data = tag + ("%04d" % sid).encode()
elif kind == "modbus":
adr, cnt = self._rng.randint(0, 0x200), self._rng.randint(1, 4)
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.MODBUS_HOLDING,
pc.merge_modbus(adr, cnt))
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
for _ in range(cnt))
elif kind == "gas":
# 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(count))
elif kind == "status":
now = datetime.now()
cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.BROADCAST,
pc.merge_broadcast(pc.BroadcastType.STATUS, 0))
data = bytes([now.hour, now.minute, now.second, now.year % 100,
now.month, now.day, now.isoweekday() % 7])
elif kind == "error":
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.ERROR,
pc.merge_error(self._rng.randint(0, 5), 0xFF))
data = b""
else:
dtype = self._rng.choice(list(pc.DiscreteType)[:7])
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.DISCRETE,
pc.merge_discrete(dtype, self._rng.randint(0, 20)))
data = bytes(self._rng.randint(0, 255)
for _ in range(self._rng.randint(1, 8)))
return tr.Frame(seq=self._seq, flags=tr.FLAG_IDE, can_id=cid,
data=data).encode()