Summary
Add a TCP server that accepts inbound connections from radar nodes and routes detection frames to per-node TcpReceiver instances.
Context
Depends on #5 (DetectionSource protocol).
Radar nodes will push detection frames over persistent TCP connections instead of being polled over HTTP. The TcpServer handles all inbound connections on a single port and routes frames by node_id to the correct TcpReceiver.
See design spec: docs/superpowers/specs/2026-03-11-tcp-push-detection-forwarding-design.md in the retina monorepo.
Changes
New: TcpServer (tracker_host/tcp_server.py)
Single asyncio TCP server listening on one port. Responsibilities:
- Accept connections, set
TCP_NODELAY
- Accumulate data per-connection, split on
\n
- Enforce max message size (1 MB per line)
- Parse JSON, extract
node_id and token
- Validate token on first message per connection; close socket on invalid token
- Route detection frames to registered
TcpReceiver by node_id
- Discard heartbeat messages (messages with
"type": "heartbeat")
- Track per-connection state, clean up on disconnect
- Notify
TcpReceiver of connection/disconnection events (for is_healthy)
New: TcpReceiver (tracker_host/tcp_receiver.py)
Implements DetectionSource protocol. Push-based counterpart to DetectionFetcher.
- Backed by bounded
asyncio.Queue(maxsize=2000)
- Drop oldest frame when queue is full (log warning)
- Timestamp dedup (skip if timestamp unchanged)
is_healthy reflects whether TcpServer has an active connection for this node
receive() returns next frame from queue, or None after a short timeout
New: TcpServerConfig (tracker_host/config.py)
@dataclass
class TcpServerConfig:
enabled: bool = False
host: str = "0.0.0.0"
port: int = 30050
token: str = ""
auto_provision: bool = True
auto_provision_port_start: int = 31000
auto_provision_port_end: int = 31099
Protocol
All messages are newline-delimited JSON, node-to-server only:
Detection frame:
{"node_id":"radar3","token":"secret","timestamp":1768932172478,"delay":[40.46],"doppler":[-27.99],"snr":[12.08],"adsb":[null]}
Heartbeat:
{"node_id":"radar3","token":"secret","type":"heartbeat"}
Acceptance criteria
Summary
Add a TCP server that accepts inbound connections from radar nodes and routes detection frames to per-node
TcpReceiverinstances.Context
Depends on #5 (DetectionSource protocol).
Radar nodes will push detection frames over persistent TCP connections instead of being polled over HTTP. The
TcpServerhandles all inbound connections on a single port and routes frames bynode_idto the correctTcpReceiver.See design spec:
docs/superpowers/specs/2026-03-11-tcp-push-detection-forwarding-design.mdin the retina monorepo.Changes
New:
TcpServer(tracker_host/tcp_server.py)Single asyncio TCP server listening on one port. Responsibilities:
TCP_NODELAY\nnode_idandtokenTcpReceiverbynode_id"type": "heartbeat")TcpReceiverof connection/disconnection events (foris_healthy)New:
TcpReceiver(tracker_host/tcp_receiver.py)Implements
DetectionSourceprotocol. Push-based counterpart toDetectionFetcher.asyncio.Queue(maxsize=2000)is_healthyreflects whether TcpServer has an active connection for this nodereceive()returns next frame from queue, orNoneafter a short timeoutNew:
TcpServerConfig(tracker_host/config.py)Protocol
All messages are newline-delimited JSON, node-to-server only:
Detection frame:
{"node_id":"radar3","token":"secret","timestamp":1768932172478,"delay":[40.46],"doppler":[-27.99],"snr":[12.08],"adsb":[null]}Heartbeat:
{"node_id":"radar3","token":"secret","type":"heartbeat"}Acceptance criteria