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 thenPOSTed 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 withPOST /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_HKpackets 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