A link stacks up to three components between the MCS and the target. Each is a process of its own, outside the core, written with the Rust or Python SDK or in any language that speaks the NATS contract. They all register with the reconciler, send heartbeats, and receive their bindings and credentials; the topology decides which ones serve which target.
flowchart LR
MCS["MCS<br/>semantic telecommand"] -->|stellar.tc.encode| D[Driver]
D -->|"unit (packet)"| T[Transport]
T -->|frames| G[Gateway]
G --> M[(Medium:<br/>RF, CAN, serial…)]
M --> G
G -->|stellar.tm.raw| T
T -->|stellar.tm.unit| D
D -->|"stellar.tm.decoded<br/>raw values"| C["MCS<br/>compute stage"]| Layer | Role | Defined by | Example: obc.ping to obc-flatsat-1 over the RF bench |
|---|---|---|---|
| Driver | Telecommand ↔ application unit (PUS packet, CSP packet); bytes → raw values | The ICD of the target | obc-csp builds a whole CSP 1 packet: destination and port fixed by the telecommand, source address read from the link (csp_address) |
| Transport | Application unit ↔ frames of the medium, both ways; optional | The link | csp1-kiss appends the CRC32 and wraps the packet in a KISS frame (FEND, data command, escaped bytes) |
| Gateway | Frames ↔ medium | The gateway | satlink-gateway sends the frame through the radio of the bench |
Without a transport, the driver produces the frames of the medium itself, like tcu-can or
lab-psu. With one, the same driver serves different media: only the transport of the link
changes, and the driver knows nothing of it.
Driver#
The driver is a black box that translates between the semantic catalogue and binary data the MCS never sees decoded:
- it encodes each telecommand and gives ACK 1 (
ENCODEDorENCODE_FAILED); - it decodes the frames (or units) received into raw values, published on
stellar.tm.decoded.<target>; the MCS applies the calibration; - it checks echoes: it compares each frame received with the telecommands it encoded in the
last 60 s and publishes an
ECHOevent, conforming or not; - for file transfers, it extracts the chunks of on-board files, runs the CFDP entity of a platform that uses CFDP, and decodes verified files (LTTM) into deferred samples.
A driver registers with:
| Field | Meaning |
|---|---|
codec | Name of its encoding, part of the encode subject |
catalogue | The catalogue it implements and the versions it accepts: platform-v3@^1.4 (1.4.0 and later 1.x), platform-v3@1.4 (1.4.x), platform-v3@1.4.0 (that one) |
telecommands, measures | What it covers, as <component>.<name> |
output | Optional: what its telecommands become (space-packet, csp1, csp2, can…) |
params_schema | Optional: JSON Schema of the link parameters it reads |
A driver knows a catalogue by its versions, never by a hash: a released catalogue version never changes in place.
Transport#
A transport sits between a driver and a gateway when the link declares one
(transport: ccsds-tc@1). It frames the units of the driver for the medium of the gateway, and
turns the frames received back into units:
- up, it reads units on
stellar.tc.wrap.<transport>.<target>and publishes frames on the uplink subject of the gateway; a unit split into several frames carriesStellar-Fragment: <i>/<n>on each, and the gateway gives ACK 2 after the last one; - down, it reads the raw telemetry of the gateway and publishes each unit a frame completes on
stellar.tm.unit.<transport>.<target>, which the driver decodes. Raw frames stay archived as received; - it keeps a state per link (reassembly across frames); a state that must survive a restart, such as COP-1, lives in JetStream.
A unit the transport cannot frame, or received after its deadline, fails its telecommand with
SEND_FAILED and the reason (the driver already gave ACK 1).
A transport registers with the kind transport, its software and version, its input (what it
frames: the output of the drivers), its output (what it produces: the uplink link type of the
gateways) and an optional params_schema.
Gateway#
The gateway carries frames to and from the medium: a ground station, an RF bench, a CAN bus, a serial line, an EGSE.
- Uplink: it sends the frames of its uplink subject and gives ACK 2 (
SENTorSEND_FAILED); a frame received after its deadline is refused. - Downlink: it publishes every frame received, as is, on
stellar.tm.raw.<gateway>.<target>, stamped with the ground reception time (Stellar-Ground-Time), and marks data replayed from on-board storage as deferred. It publishes the segments of continuous streams. - Link availability: its heartbeat says whether the link is available (satellite in view, station in service). A gateway without that notion, on a bench, always reports it available.
- Throughput: every
metrics_period(5 s by default), a report per active target onstellar.metrics.<gateway>.<target>, also exposed to Prometheus.
A gateway registers with its tags (sband, a region, a station name, bench) and, for each
direction, uplink and downlink: {link_type, rate_bps}, the physical link type (rf, can,
rs422, ethernet, serial, sim…) and its theoretical rate. See the
gateway contract.
Chain compatibility#
The reconciler binds a link only when its chain holds:
| Link | Condition |
|---|---|
| Without transport | output of the driver = uplink link_type of the gateway |
| With a transport | output of the driver = input of the transport, and output of the transport = uplink link_type of the gateway |
| Parameters | In every environment, the parameters satisfy the schemas of the driver and the transport |
What a component does not declare is not checked: a driver without output fits any gateway. A
driver producing space-packet behind a CAN gateway gives the reason … the link needs a transport.
The example components of the repository:
| Component | Kind | Declares |
|---|---|---|
platform-v3-ccsds | driver | platform-v3@^1.4, output space-packet |
obc-csp | driver | obc-v1@^1, output csp1 (or csp2), schema csp_address |
tcu-can | driver | platform-v3@^1.4, covers tcu.* only, no output |
scpi-psu | driver | lab-psu@^1.0 |
satlink-bench | driver | satlink-bench@^1.0 |
ccsds-tc | transport | input space-packet, output rf, COP-1 |
csp1-kiss, csp2-kiss | transports | input csp1 / csp2, output rf (or serial with --output serial), schema crc32 |
csp1-can, csp2-can | transports | input csp1 / csp2, output can, schema crc32, via, can_address, reassembly_timeout_ms |
bench-can | gateway | link type can |
satlink-gateway | gateway | link type rf, tags bench, rf |
bench-gateway | gateway | link type ethernet |
stellar-simulator | driver and gateway | link type sim, tag sim |
Registration and health#
Every instance registers on stellar.ctl.register with its kind, instance name, software and
version, heartbeat period and the public key of an nkey it generates at start. The reconciler
refuses, with the reason, a kind or name that is not a token, a version that is not semantic, a
heartbeat period outside 100 ms to 60 s, a missing or superfluous driver, transport or
gateway section, a catalogue that is not name@requirement, or coverage entries that are not
<component>.<name>. A new registration of an instance replaces the previous one (a restart).
The registration and the last heartbeat live in the stellar_instances bucket with a TTL of three
heartbeat periods. A heartbeat is healthy or degraded with a reason; a degraded instance is not
bound. After three periods without heartbeat, the instance is gone: the reconciler unbinds it and
its targets are no longer ready.
Every instance answers control verbs on stellar.ctl.rpc.<kind>.<instance>.<verb>: status,
bindings, credentials and reregister, plus its own (faults for a simulated gateway, cfdp
for a driver running CFDP). GET /v1/instances and GET /v1/instances/{kind}/{instance} show
them; see Writing a Driver for the full life of an instance.