Skip to content

Add TcpServer and TcpReceiver for inbound node connections #6

Description

@jonnyspicer

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

  • TCP server accepts multiple concurrent node connections
  • Token validated on first message per connection
  • Invalid tokens cause socket close + warning log
  • Frames routed to correct TcpReceiver by node_id
  • Heartbeats discarded (don't reach TcpReceiver)
  • Queue bounded at 2000, drops oldest on overflow
  • TCP_NODELAY set on accepted sockets
  • Max 1MB per message enforced
  • Connection/disconnection events update TcpReceiver health
  • Unit tests for all above scenarios

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions