J
2026-04-13 11:48:15 -04:00
|
|
|
|
"""End-to-end: local websockets server drives a Streamer with a MockSDR."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
|
|
import websockets
|
|
|
|
|
|
|
|
|
|
|
|
from ria_toolkit_oss.agent.streamer import Streamer
|
|
|
|
|
|
from ria_toolkit_oss.agent.ws_client import WsClient
|
|
|
|
|
|
from ria_toolkit_oss.sdr.mock import MockSDR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_server_start_stream_stop_cycle_over_real_ws():
|
|
|
|
|
|
async def scenario():
|
|
|
|
|
|
control_frames: list[dict] = []
|
|
|
|
|
|
binary_frames: list[bytes] = []
|
|
|
|
|
|
ready = asyncio.Event()
|
|
|
|
|
|
stopped = asyncio.Event()
|
|
|
|
|
|
|
|
|
|
|
|
async def server_handler(ws):
|
|
|
|
|
|
# Agent will open the connection; wait for heartbeat first.
|
|
|
|
|
|
try:
|
|
|
|
|
|
first = await asyncio.wait_for(ws.recv(), timeout=2.0)
|
|
|
|
|
|
control_frames.append(json.loads(first))
|
|
|
|
|
|
await ws.send(
|
|
|
|
|
|
json.dumps(
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "start",
|
|
|
|
|
|
"app_id": "app-1",
|
|
|
|
|
|
"radio_config": {
|
|
|
|
|
|
"device": "mock",
|
|
|
|
|
|
"buffer_size": 32,
|
|
|
|
|
|
"sample_rate": 1_000_000,
|
|
|
|
|
|
"center_frequency": 2.45e9,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
while len(binary_frames) < 3:
|
|
|
|
|
|
msg = await asyncio.wait_for(ws.recv(), timeout=2.0)
|
|
|
|
|
|
if isinstance(msg, bytes):
|
|
|
|
|
|
binary_frames.append(msg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
control_frames.append(json.loads(msg))
|
|
|
|
|
|
ready.set()
|
|
|
|
|
|
await ws.send(json.dumps({"type": "stop", "app_id": "app-1"}))
|
|
|
|
|
|
# Drain final status frame.
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
msg = await asyncio.wait_for(ws.recv(), timeout=0.5)
|
|
|
|
|
|
if isinstance(msg, bytes):
|
|
|
|
|
|
binary_frames.append(msg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
control_frames.append(json.loads(msg))
|
|
|
|
|
|
except (asyncio.TimeoutError, Exception):
|
|
|
|
|
|
pass
|
|
|
|
|
|
stopped.set()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
stopped.set()
|
|
|
|
|
|
|
|
|
|
|
|
server = await websockets.serve(server_handler, "127.0.0.1", 0)
|
|
|
|
|
|
port = server.sockets[0].getsockname()[1]
|
|
|
|
|
|
try:
|
|
|
|
|
|
client = WsClient(
|
|
|
|
|
|
f"ws://127.0.0.1:{port}",
|
|
|
|
|
|
token="",
|
|
|
|
|
|
heartbeat_interval=10.0,
|
|
|
|
|
|
reconnect_pause=0.05,
|
|
|
|
|
|
)
|
|
|
|
|
|
streamer = Streamer(ws=client, sdr_factory=lambda d, i: MockSDR(buffer_size=32, seed=0))
|
2026-04-20 13:51:15 -04:00
|
|
|
|
task = asyncio.create_task(client.run(on_message=streamer.on_message, heartbeat=streamer.build_heartbeat))
|
J
2026-04-13 11:48:15 -04:00
|
|
|
|
await asyncio.wait_for(ready.wait(), timeout=3.0)
|
|
|
|
|
|
await asyncio.wait_for(stopped.wait(), timeout=3.0)
|
|
|
|
|
|
client.stop()
|
|
|
|
|
|
task.cancel()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await task
|
|
|
|
|
|
except (asyncio.CancelledError, Exception):
|
|
|
|
|
|
pass
|
|
|
|
|
|
finally:
|
|
|
|
|
|
server.close()
|
|
|
|
|
|
await server.wait_closed()
|
|
|
|
|
|
return control_frames, binary_frames
|
|
|
|
|
|
|
|
|
|
|
|
controls, binaries = asyncio.run(scenario())
|
|
|
|
|
|
|
|
|
|
|
|
# Heartbeat reached the server.
|
|
|
|
|
|
assert any(f.get("type") == "heartbeat" for f in controls)
|
|
|
|
|
|
# Status transitioned idle -> streaming -> idle.
|
|
|
|
|
|
statuses = [f["status"] for f in controls if f.get("type") == "status"]
|
|
|
|
|
|
assert "streaming" in statuses
|
|
|
|
|
|
assert statuses[-1] == "idle"
|
|
|
|
|
|
# Three binary IQ frames of 32 samples × 2 floats × 4 bytes.
|
|
|
|
|
|
assert len(binaries) >= 3
|
|
|
|
|
|
for b in binaries[:3]:
|
|
|
|
|
|
assert len(b) == 32 * 2 * 4
|