mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-09 10:23:01 +02:00
feat(companion): echo injected TX to companion clients as raw RX (0x88)
Push locally-injected TX packets to connected companion frame server clients as PUSH_CODE_LOG_RX_DATA (0x88) with snr=0/rssi=0, so apps that decrypt locally from raw RX (e.g. RemoteTerm) see companion-originated channel traffic. The originating companion is excluded so a node never hears its own transmission, matching physical firmware behavior. inject_packet now takes an origin_hash (threaded per-companion via the packet_injector partial); _on_raw_rx_for_companions gains exclude_hash to skip that companion's frame server. OTA RX is unaffected.
This commit is contained in:
@@ -143,6 +143,15 @@ async def test_raw_rx_and_duplicate_logging_hooks():
|
||||
await daemon._on_raw_rx_for_companions(b"abc", rssi=-90, snr=2.0)
|
||||
fs_ok.push_rx_raw.assert_called_once()
|
||||
|
||||
# exclude_hash skips the matching companion's own frame server (no self-echo)
|
||||
fs_self = SimpleNamespace(companion_hash="0x1a", push_rx_raw=MagicMock())
|
||||
fs_other = SimpleNamespace(companion_hash="0x2b", push_rx_raw=MagicMock())
|
||||
daemon.companion_frame_servers = [fs_self, fs_other]
|
||||
await daemon._on_raw_rx_for_companions(b"xyz", rssi=0, snr=0.0, exclude_hash="0x1a")
|
||||
fs_self.push_rx_raw.assert_not_called()
|
||||
fs_other.push_rx_raw.assert_called_once()
|
||||
daemon.companion_frame_servers = [fs_ok, fs_fail]
|
||||
|
||||
engine = SimpleNamespace(
|
||||
is_duplicate=MagicMock(side_effect=[False, True]),
|
||||
record_duplicate=MagicMock(),
|
||||
|
||||
@@ -561,3 +561,70 @@ class TestPacketRouterRoutingBranches(unittest.IsolatedAsyncioTestCase):
|
||||
await router._route_packet(pkt)
|
||||
b1.process_received_packet.assert_awaited_once()
|
||||
daemon.repeater_handler.assert_awaited_once()
|
||||
|
||||
|
||||
class TestInjectedTxRawEcho(unittest.IsolatedAsyncioTestCase):
|
||||
"""inject_packet echoes local TX to companion clients as raw RX (0x88)."""
|
||||
|
||||
async def test_inject_packet_echoes_raw_tx_to_companions(self):
|
||||
"""Successful local TX is pushed via _on_raw_rx_for_companions with snr=0/rssi=0."""
|
||||
daemon = _make_daemon()
|
||||
daemon._on_raw_rx_for_companions = AsyncMock()
|
||||
router = PacketRouter(daemon)
|
||||
pkt = _make_packet()
|
||||
pkt.write_to.return_value = b"\x10\x20\x30"
|
||||
|
||||
ok = await router.inject_packet(pkt)
|
||||
|
||||
self.assertTrue(ok)
|
||||
daemon._on_raw_rx_for_companions.assert_awaited_once_with(
|
||||
b"\x10\x20\x30", 0, 0.0, exclude_hash=None
|
||||
)
|
||||
|
||||
async def test_inject_packet_excludes_originating_companion(self):
|
||||
"""A companion's own TX is echoed with its hash excluded (no self-echo)."""
|
||||
daemon = _make_daemon()
|
||||
daemon._on_raw_rx_for_companions = AsyncMock()
|
||||
router = PacketRouter(daemon)
|
||||
pkt = _make_packet()
|
||||
pkt.write_to.return_value = b"\xaa\xbb"
|
||||
|
||||
ok = await router.inject_packet(pkt, origin_hash="0x1a")
|
||||
|
||||
self.assertTrue(ok)
|
||||
daemon._on_raw_rx_for_companions.assert_awaited_once_with(
|
||||
b"\xaa\xbb", 0, 0.0, exclude_hash="0x1a"
|
||||
)
|
||||
|
||||
async def test_inject_packet_no_echo_when_tx_fails(self):
|
||||
"""A failed local transmission must not echo a raw RX frame."""
|
||||
daemon = _make_daemon()
|
||||
daemon.repeater_handler = AsyncMock(return_value=False)
|
||||
daemon._on_raw_rx_for_companions = AsyncMock()
|
||||
router = PacketRouter(daemon)
|
||||
|
||||
ok = await router.inject_packet(_make_packet())
|
||||
|
||||
self.assertFalse(ok)
|
||||
daemon._on_raw_rx_for_companions.assert_not_awaited()
|
||||
|
||||
async def test_inject_packet_survives_echo_failure(self):
|
||||
"""An error while echoing must not fail the injection."""
|
||||
daemon = _make_daemon()
|
||||
daemon._on_raw_rx_for_companions = AsyncMock(side_effect=RuntimeError("boom"))
|
||||
router = PacketRouter(daemon)
|
||||
|
||||
ok = await router.inject_packet(_make_packet())
|
||||
|
||||
self.assertTrue(ok)
|
||||
daemon._on_raw_rx_for_companions.assert_awaited_once()
|
||||
|
||||
async def test_inject_packet_without_echo_hook(self):
|
||||
"""Injection succeeds even if the daemon has no raw-RX companion hook."""
|
||||
daemon = _make_daemon()
|
||||
daemon._on_raw_rx_for_companions = None
|
||||
router = PacketRouter(daemon)
|
||||
|
||||
ok = await router.inject_packet(_make_packet())
|
||||
|
||||
self.assertTrue(ok)
|
||||
|
||||
Reference in New Issue
Block a user