# Демонстрационная модель шины и устройств для интерфейса без оборудования. Синтетические # ответы служат проверке пользовательского сценария; они не являются измерениями физической # линии. """@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()