Skip to content

Ground link bridge

The API service never opens a socket to the ground. A separate host-side process, bin/data_proxy.py, handles both egress and ingress — it’s the only thing that actually talks TCP to the ground link.

bin/data_proxy.py

bin/data_proxy.py runs three threads:

  • TM downlink. Reads the link with minmcs.stream::read_space_packet, which frames a raw TCP stream into whole CCSDS packets using the primary header’s self-describing length field. Each packet is then POSTed to /packet/tm/submit.
  • TC egress. Polls GET /packet/tc/pending, writes each pending command’s raw packet bytes onto the link unchanged, then acks it with POST /packet/tc/{id}/sent. Commands stay in the database until acked, so an uplink survives a proxy or API restart. Delivery is at-least-once: a lost ack can cause a resend, and that gets logged.
  • Heartbeat. Periodically POSTs /links/status, which is how the API learns the ground-link connections are up and can report that to the UI’s Links view.

One socket or two, depending on configuration. By default, MINMCS_TM_SOURCE_HOST/_PORT and MINMCS_TC_TARGET_HOST/_PORT name the same host:port. When they match, the proxy opens a single shared bidirectional TCP connection and uses it for both directions — this matches the real ground-link tool’s protocol, where one socket emits TM, accepts TC, and writes verification reports back on that same connection. Only when the two addresses differ does the proxy open two fully independent connections instead: a read-only TM socket and a write-only TC socket.

bin/egse_link.py — the local dev stub

bin/egse_link.py is not the real EGSE tool. It’s a local Python stand-in for exercising the whole chain — egse_link ↔ data_proxy ↔ API ↔ UI — without spacecraft hardware. It’s built to model the two-socket case, so it binds one listener per role instead of one shared socket:

  • A downlink listener that emits synthetic CCSDS/PUS-C TM_EPS_HK packets on an interval (MINMCS_TM_SOURCE_INTERVAL, default 2s), and writes back service-1 verification reports for any TC it receives on the uplink listener.
  • An uplink listener that receives inbound raw PUS TC space packets, length-framed the same way as the downlink.

The two connections are otherwise indistinguishable TCP sockets. There’s no in-band handshake — role is determined purely by which listener accepted the connection. It always binds two listeners, even if MINMCS_TM_SOURCE_HOST/_PORT and MINMCS_TC_TARGET_HOST/_PORT name the same host:port — which the committed .env.example does (21001/21001, tailored for the real EGSE tool’s single-socket protocol above). With matching ports the second bind fails with Address already in use and the stub never starts, so give it distinct ports for local runs, e.g. the code’s own 9310/9311 fallback. Run egse_link alongside data_proxy and the API to exercise the full local TM/TC loop:

$ uv run python bin/egse_link.py    # synthetic TM out, TC in
$ uv run python bin/data_proxy.py   # bridge: link <-> API
$ uv run uvicorn minmcs.api:app --host 0.0.0.0 --port 9300
Last updated on