Coverage for kv4p/transports/dummy.py: 98%
45 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-13 13:27 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-13 13:27 +0000
1"""In-memory transport that fakes a firmware HELLO reply, for tests/smoke runs without hardware."""
3from __future__ import annotations
5import logging
6import struct
7import threading
8from collections.abc import Callable
10from kv4p.constants.kiss import KISS_CMD_SETHARDWARE
11from kv4p.constants.vendor import COMMAND_HELLO, KV4P_PROTOCOL_VERSION, KV4P_VENDOR_PREFIX
12from kv4p.transports import Kv4pTransport
14logger = logging.getLogger(__name__)
16# Same layouts as messages/version.py and messages/device_state.py from_bytes().
17_VERSION = struct.Struct("<HcIBffB")
18_DEVICE_STATE = struct.Struct("<IiHBffBBBcBBB")
21class DummyTransport(Kv4pTransport):
22 """Transport with no real radio: answers reset() with a synthetic HELLO."""
24 def __init__(
25 self,
26 *,
27 firmware_version: int = 17,
28 window_size: int = 2048,
29 min_radio_freq: float = 134.0,
30 max_radio_freq: float = 174.0,
31 features: int = 0,
32 device_state_flags: int = 0,
33 ) -> None:
34 self._firmware_version = firmware_version
35 self._window_size = window_size
36 self._min_radio_freq = min_radio_freq
37 self._max_radio_freq = max_radio_freq
38 self._features = features
39 self._device_state_flags = device_state_flags
40 self._on_frame: Callable[[int, bytes], None] | None = None
41 self.written: list[tuple[int, bytes]] = []
43 def open(
44 self,
45 on_frame: Callable[[int, bytes], None],
46 on_error: Callable[[Exception], None] | None = None,
47 ) -> None:
48 self._on_frame = on_frame
49 logger.info("dummy transport open")
51 def close(self) -> None:
52 self._on_frame = None
53 logger.info("dummy transport close")
55 def reset(self) -> None:
56 logger.info("dummy transport reset -> synthetic HELLO")
57 payload = _encode_vendor_payload(COMMAND_HELLO, self._build_hello_bytes())
58 threading.Thread(target=self._deliver, args=(payload,), daemon=True).start()
60 def _build_hello_bytes(self) -> bytes:
61 version = _VERSION.pack(
62 self._firmware_version,
63 b"D",
64 self._window_size,
65 0,
66 self._min_radio_freq,
67 self._max_radio_freq,
68 self._features,
69 )
70 device_state = _DEVICE_STATE.pack(
71 0, # applied_sequence
72 -1, # memory_id
73 self._device_state_flags, # flags
74 0, # bw
75 self._min_radio_freq, # freq_tx
76 self._min_radio_freq, # freq_rx
77 0, # ctcss_tx
78 1, # squelch
79 0, # ctcss_rx
80 b"D", # radio_module_status
81 1, # mode (RX)
82 0, # last_error
83 0, # latest_rssi
84 )
85 return version + device_state
87 def _deliver(self, payload: bytes) -> None:
88 if self._on_frame is not None:
89 self._on_frame(KISS_CMD_SETHARDWARE, payload)
91 def write_frame(self, command: int, payload: bytes) -> None:
92 self.written.append((command, payload))
93 logger.debug("dummy transport write command=0x%02x bytes=%d", command, len(payload))
95 def flush(self) -> None:
96 pass
99def _encode_vendor_payload(command: int, payload: bytes = b"") -> bytes:
100 return KV4P_VENDOR_PREFIX + bytes([KV4P_PROTOCOL_VERSION, command]) + payload