The core of Stellar Control never touches a byte of a target. Everything that depends on a platform, a medium or a downstream system lives in external components: processes outside the MCS that talk to it through NATS only (or, for pass feeders, through the HTTP API). This section explains how to write them.
The components you can write#
| Component | What it does | Talks through | Contract |
|---|---|---|---|
| Driver | Encodes semantic telecommands into bytes (ACK 1), decodes telemetry into raw values, recognises echoes, extracts file chunks, decodes recorded files | NATS | Driver contract |
| Transport | Frames the units of a driver for a medium (CCSDS TC frames under COP-1, CSP over KISS…), and back | NATS | Driver contract, Transports |
| Gateway | Carries frames to and from a ground station, a bench or an EGSE (ACK 2), reports link availability and throughput | NATS | Gateway contract |
| Output connector | Consumes the data of the MCS to store it (a time-series database) or relay it | NATS (JetStream consumers) | Connector contract |
| Pass feeder | Pushes the passes of a flight dynamics system or a ground station provider | HTTP API | POST /v1/passes, POST /v1/passes/replace_window |
flowchart LR
subgraph MCS["Core of the MCS"]
EX[Executor]
CO[Compute stage]
RE[Reconciler]
end
EX -- "SemanticTc<br/>stellar.tc.encode.*" --> D[Driver]
D -- "unit<br/>stellar.tc.wrap.*" --> T[Transport]
T -- "frame<br/>stellar.tc.uplink.*" --> G[Gateway]
D -. "frame (no transport)" .-> G
G -- "raw frame<br/>stellar.tm.raw.*" --> T
T -- "unit<br/>stellar.tm.unit.*" --> D
D -- "RawSample[]<br/>stellar.tm.decoded.*" --> CO
RE -- "bindings, credentials" --> D & T & G
MCS -- "JetStream consumers" --> C[Output connector]
F[Pass feeder] -- "HTTP" --> API[API]The contract is the interface#
The contract is JSON over NATS, independent of any language: registration, heartbeats,
control verbs, bindings and the data plane. The MCS checks it at registration and binds only
the instances that fit the topology. The NATS contract reference lists
every subject, header and message; stellar schema <kind> prints the JSON Schema of each
message.
Two SDKs implement it, so that a component only declares what it does:
| Rust SDK | Python SDK | |
|---|---|---|
| Package | crate stellar-sdk (sdk/rust/sdk), with stellar-contract and stellar-common | stellar-mcs on PyPI (sdk/python) |
| Driver | Driver trait, driver_main / run_driver | Driver with @driver.encode, @driver.decode, @driver.echo, @driver.chunks, @driver.decode_file |
| Transport | Transport trait, transport_main / run_transport | Transport with @transport.wrap, @transport.unwrap |
| Gateway | Gateway trait, run_gateway | Gateway with @gateway.send, @gateway.receive, @gateway.link_available |
| Output connector | Connector trait, run_connector | Connector with @connector.on("<kind>") |
| Pass feeder | — (plain HTTP) | PassFeeder |
| Own control verbs | Rpc trait | @component.rpc("verb") |
| COP-1 in a transport | yes (Transport::cop1, Transport::clcw) | no |
| CFDP entity in a driver | yes (Driver::cfdp and friends) | no |
| Protocol helpers | CCSDS space packets and frames, CLCW, COP-1 FOP and FARM, CFDP entity, CSP 1/2, KISS, CAN frames, CSP over CAN (CFP) | CSP 1/2, CAN frames, CSP over CAN (CFP) |
| Prometheus endpoint | yes (STELLAR_METRICS_ADDR) | no |
Both SDKs handle the same chores: the nkey key pair, registration with retries, the JWT issued
by the reconciler and its renewal, heartbeats, the common control verbs (status,
credentials, bindings, reregister), subscriptions derived from the bindings, JetStream
consumers, telecommand events (ACK 1 and ACK 2), deadlines, fragments and reconnection.
A component in another language sends the same messages: nothing in the MCS knows which SDK, if any, a component uses.
Choosing#
- Python for a quick integration, a bench instrument, a text protocol, a connector or a
feeder script. The examples in
examples/pythoncarry a SCPI power supply into the MCS in a few dozen lines each. - Rust for flight links: CCSDS framing, COP-1, CFDP, strict timing, test vectors replayed bit for bit, and a Prometheus endpoint.
- Any language when a component must live in an existing code base: follow the NATS contract.
Layering#
The repository enforces one-way dependencies (scripts/check-layers.py, run in CI):
sdk ← core ← tooling, and examples depend on the SDK only. A driver embeds the
contract and the SDK, never the core; the core is shipped without the SDK. What an example does,
a team outside the MCS can do.
Example components#
All of them are built on the SDK alone, and are the best starting points.
| Path | Kind | Binary | What it shows |
|---|---|---|---|
examples/rust/drivers/lab-psu | driver | scpi-psu | The smallest driver: SCPI-like text lines of the lab-psu catalogue |
examples/rust/drivers/platform-v3 | driver | platform-v3-ccsds | PUS-C packets behind the ccsds-tc transport: echoes from verification reports, file chunks, LTTM decoding, test vectors (examples/icd/platform-v3.md) |
examples/rust/drivers/obc-csp | driver | obc-csp | Whole CSP 1 packets, the ground address read from the link parameters (csp_address), with any transport (examples/icd/obc-csp.md) |
examples/rust/drivers/tcu-can | driver | tcu-can | A partial driver covering the tcu component only, on the CAN bench bus (examples/icd/tcu-can.md) |
examples/rust/drivers/satlink-bench | driver | satlink-bench | An RF bench as a target, through its control plane (examples/icd/satlink-bench.md) |
examples/rust/transports/ccsds-tc | transport | ccsds-tc | Space packets in TC frames under COP-1, packets reassembled from TM frames |
examples/rust/transports/csp-kiss | transport | csp1-kiss, csp2-kiss | CSP packets in KISS frames, CRC32 added and checked |
examples/rust/transports/csp-can | transport | csp1-can, csp2-can | CSP packets on a CAN bus in the CFP of libcsp, checked against libcsp's own frames |
examples/rust/gateways/can | gateway | bench-can | A SocketCAN bus; an in-memory bus for tests |
examples/rust/gateways/satlink | gateway | satlink-gateway | A bench with a REST control plane and a ZeroMQ data plane |
examples/rust/gateways/bench | gateway | bench-gateway | The smallest gateway: logs frames, publishes a status frame every second |
examples/rust/connectors/timescale | connector | stellar-timescale | The reference output connector, to TimescaleDB |
examples/python/driver.py | driver | — | The scpi-psu driver in Python |
examples/python/gateway.py | gateway | — | A TCP gateway to an instrument: polling, link state, reconnection, a control verb |
examples/python/connector.py | connector | — | Measures and telecommand events to CSV files, idempotently |
examples/python/csp_can.py | transport | — | csp1-can and csp2-can in Python |
examples/python/pass_feeder.py | pass feeder | — | An FDS planning or provider bookings pushed as snapshots |
How a component joins a target#
- The topology declares a link of a target with a driver (by name and
version,
platform-v3-ccsds@4), an optional transport and a gateway (by name, or a tag constraint such asany(tag: sband)). - The component starts with bootstrap credentials limited to registration and heartbeats, and registers: software name and version, and what it implements.
- The reconciler checks the registration, binds the instance to the links it fits, issues it a JWT with exactly the permissions of its bindings, and sends it its bindings.
- The component subscribes to the subjects of its bindings; the target becomes ready when every link has a driver, a transport when declared, and a gateway bound.
See Drivers, Transports and Gateways for the binding rules seen from the topology.