Stellar ControlMission control · by Stellar Systems v0.1.0

Operations

Direct Telecommands and Values

Send a telecommand by hand, follow its acknowledgements, and watch the current values of a target.

A direct telecommand is a telecommand sent by hand, outside any procedure: from the CLI (stellar send), from the target page of the web console, or by POST /v1/tc. The executor runs it as an implicit one-step procedure, with the verification the catalogue declares for it: requires, then ACK 1 from the driver, ACK 2 from the gateway and ACK 3 from verify. The states and acknowledgements are described in Telecommand Lifecycle.

The current values of a target — the latest sample of each of its measures — are read the same way, once or as a live feed: stellar watch, the web console, or the values API.

Sending a telecommand#

Shell
stellar send sim-1 'tcu[TCU1].ping'
stellar send sim-1 'tcu[TCU1].set_anode_voltage' 'voltage=100 V'
stellar send sim-1 'tcu[TCU1].set_mode' mode=SAFE_MODE
stellar send flatsat-1 'tcu[TCU2].ping' --link direct
stellar send psu-lab-2 psu.set_voltage voltage="28 V"

stellar send submits the telecommand, prints its identifier, then follows its events until a final state, one line per event:

text
01JB2Y1C8ZP1N2B8H3M4K5Q6R7 submitted to sim-1
2026-10-02T10:14:40.120Z  PENDING
2026-10-02T10:14:40.131Z  ENCODED
2026-10-02T10:14:40.142Z  SENT
2026-10-02T10:14:40.371Z  VERIFIED

It exits with code 0 when the telecommand ends VERIFIED or COMPLETE, and 1 on any other final state (REJECTED, ENCODE_FAILED, SEND_FAILED, VERIFY_FAILED, VERIFY_TIMEOUT) or when the API refuses the submission. An ECHO event is printed with conforming or NOT conforming; it is not a state.

Writing the telecommand#

FormMeaning
pingThe telecommand alone, when its name is unique in the platform
tcu.pingcomponent.telecommand, when several components have a telecommand of that name
tcu[TCU1].pingWith the instance of a multi-instance component

The instance can also be given with --instance TCU1; an instance written in the telecommand wins.

The standard link component is addressed the same way: 'link[nominal].unlock' sends the COP-1 directive of the nominal link (see COP-1 and the Link Component).

Writing the arguments#

Arguments are always named, name=value, one per shell word:

ValueRead as
true, falseBoolean
A plain number (10, 2.5)Number without unit, in the unit of the argument
Anything elseText: a quantity with its unit (100 V, 10 V/s), an enum value (SAFE_MODE), hexadecimal bytes

Quote a quantity so that the shell keeps its space: 'voltage=100 V' or voltage="100 V".

The MCS converts each value into the unit of the argument (0.1 kV becomes 100 V), checks it against its range, and fills missing arguments with their default. An argument without a value nor a default, an unknown argument or a value out of range refuses the telecommand before anything is sent.

Options#

OptionDefaultMeaning
--instance <instance>—Instance of a multi-instance component, when not written in the telecommand
--link <link>the default linkLink of the target to send through (direct)
--environment <env>—Environment of the telecommand, which selects the parameters of the link
--api <url>http://localhost:8080 (STELLAR_API)API of the MCS
--as <user>$USERIdentity declared to the API
--role <roles>STELLAR_ROLERoles declared with it, comma-separated
--token <jwt>STELLAR_TOKENOIDC token, preferred to the declared identity
--api-ca <pem>system roots (STELLAR_API_CA)CA certificates of an https:// API

A link may carry only some components of the catalogue (components: [tcu]). A telecommand of another component sent through that link is refused (tc::link-component). The default link always carries the whole catalogue.

Targets held by a run#

A run holds a lease on each of its targets (see Leases and Crash Recovery). While it does, a direct telecommand to that target is refused with 409 tc::target-held, naming the runs that hold it. One exception: under a shared lease (a run that only reads values and sends telecommands with changes_state: false), a direct telecommand with changes_state: false is accepted.

The API: POST /v1/tc#

The CLI and the web console go through the API. A client can do the same:

Shell
curl -s -X POST http://localhost:8080/v1/tc \
  -H 'Content-Type: application/json' -H 'X-Stellar-User: alice' \
  -d '{"target": "sim-1", "telecommand": "tcu.set_anode_voltage", "instance": "TCU1",
       "args": {"voltage": "100 V"}}'
FieldRequiredMeaning
targetyesTarget, as named in the topology
telecommandyesIts name alone when unique in the platform, else component.name
instancenoInstance of a multi-instance component, or file identifier for files
argsnoArguments by name: "100 V", enum values, booleans, numbers, hexadecimal bytes
linknoLink, when not the default one
environmentnoEnvironment, which selects the parameters of the link
idnoIdentifier chosen by the client (a ULID), to make the submission idempotent

Unknown fields are refused. The reply is 202 Accepted:

JSON
{"id": "01JB2Y1C8ZP1N2B8H3M4K5Q6R7", "duplicate": false}

The API resolves the request against the current configuration, then publishes the semantic telecommand on stellar.tc.submit.<target> (stream TC_COMMANDS) with the identifier as Nats-Msg-Id, the configuration revision as Stellar-Config and the caller as Stellar-User. The executor records the caller in the PENDING event.

Idempotency#

Without id, the API draws a new ULID. With id, a client that retries after a network error submits the same telecommand again under the same identifier: JetStream deduplicates it, and the reply says "duplicate": true. Nothing is sent twice.

Errors#

StatusCodeCause
400api::invalid-body, api::invalid-idMalformed body, or id is not a ULID
404tc::unknown-targetNo such target in the topology (with a suggestion)
409tc::target-heldA run holds the target
422tc::unknown-telecommand, tc::ambiguous-telecommandNo such telecommand, or its name is not unique: write component.name
422tc::missing-instance, tc::unexpected-instance, tc::unknown-instanceInstance missing, superfluous or unknown
422tc::missing-argument, tc::unexpected-argument, tc::invalid-argument, tc::out-of-rangeArguments
422tc::unknown-link, tc::link-componentLink unknown, or not carrying the component
422tc::unknown-environment, tc::environment-requiredEnvironment unknown, or required
422tc::missing-catalogueThe catalogue of the target is not in the snapshot
503api::unavailableNATS is unavailable

A 422 reply lists every error found, each with its help when there is one. See Error Codes.

Following a telecommand#

GET /v1/tc/{target}/{id}/events upgrades to a WebSocket. It replays the events already recorded in TC_EVENTS, follows the new ones, and closes after a final state. Each message is a telecommand event:

JSON
{"tc": "01JB2Y1C8ZP1N2B8H3M4K5Q6R7", "state": "SENT", "at": "2026-10-02T10:14:40.142Z"}
FieldMeaning
tcTelecommand identifier
statePENDING, REJECTED, ENCODED, ENCODE_FAILED, SENT, SEND_FAILED, VERIFIED, VERIFY_FAILED, VERIFY_TIMEOUT, COMPLETE, or ECHO
atTime of the transition
detailExplanation: failed precondition, error of the driver or the gateway
conformingFor ECHO: whether the echo matches the telecommand sent
telecommandFor PENDING: {component, instance, telecommand}

Opening the socket after the telecommand ended still gives its whole history. See WebSocket Streams.

Current values#

The compute stage keeps the latest sample of every measure and derived measure of a target in the current value table (see Measures and Current Values).

stellar watch#

Shell
stellar watch sim-1                                   # every measure of the target
stellar watch sim-1 'tcu[TCU1].anode_voltage'         # one instance
stellar watch sim-1 tcu.responding                    # every instance of tcu
stellar watch sim-1 responding                        # that measure of any component
stellar watch sim-1 'tcu[TCU1].mode' --until STANDBY --timeout 30

It prints the current values, then every update, one line per sample: the time of the sample, the measure and its value.

text
2026-10-02T10:14:41.000Z  tcu[TCU1].anode_voltage = 42.5
OptionMeaning
measures…Measures to show; every measure without
--until <value>Stop with code 0 as soon as a watched value equals this: true, STANDBY, 28.0 (JSON when it parses, else text)
--timeout <seconds>Give up with code 1 after this many seconds

--until and --timeout make stellar watch usable in scripts: wait for a mode, a flag, a value. The comparison is exact on the JSON value of the sample: 28.0 does not match 28.1.

The values API#

GET /v1/targets/{target}/values returns the current values of a target, by key order:

JSON
{
  "values": [
    {
      "component": "tcu",
      "instance": "TCU1",
      "measure": "anode_voltage",
      "sample": {
        "value": 100.2,
        "raw": 20040,
        "time": "2026-10-02T10:14:41Z",
        "ground_time": "2026-10-02T10:14:41.012Z",
        "link": "nominal",
        "delivery": "realtime"
      }
    }
  ]
}
Sample fieldMeaning
valuePhysical value: number in the unit of the measure, boolean, enum value, hexadecimal bytes; null when a temporal derived measure becomes unknown
rawRaw value, for a measure calibrated by the MCS
timeTime of the sample: on-board time when known, else ground reception time
ground_timeGround reception time, reference of freshness (max_age)
linkLink the frame came through
deliveryrealtime, or deferred for telemetry stored on board and dumped later

GET /v1/targets/{target}/values/watch?measures=… upgrades to a WebSocket: it sends the current values, then each update of the table, one value per message. measures is a comma-separated list of selectors in the three forms above (tcu[TCU1].responding,psu.voltage); every measure without. An invalid selector answers 400 api::invalid-measure.

A sample older than the known value never replaces it: the table always shows the most recent state, even while a file of deferred telemetry is being decoded.

See also#

Stellar Control · v0.1.0

↑↓ to moveEnter to open