Every telecommand, whether sent by hand or by a procedure, goes through the same state machine with three acknowledgements: the driver encoded it (ACK 1), the gateway sent it (ACK 2), and the telemetry shows it had the expected effect (ACK 3). Each transition is an event, archived in a replayable stream, so that the fate of every telecommand can be followed live and audited later.
This page describes the lifecycle. To send telecommands by hand, see Direct Telecommands and Values; to declare them, see Telecommands and Verification.
States#
stateDiagram-v2
[*] --> PENDING
PENDING --> REJECTED: requires not met, target held, link not carrying the component
PENDING --> ENCODED: ACK 1 (driver)
PENDING --> ENCODE_FAILED: link not bound, driver error, no ACK 1 in time
ENCODED --> SENT: ACK 2 (gateway)
ENCODED --> SEND_FAILED: gateway or transport error, no ACK 2 in time
SENT --> VERIFIED: ACK 3, every verification holds
SENT --> VERIFY_FAILED: non-conforming echo, condition without window false
SENT --> VERIFY_TIMEOUT: window elapsed
SENT --> COMPLETE: no verification declared| State | Final | Meaning | Published by |
|---|---|---|---|
PENDING | no | Accepted; preconditions not evaluated yet | Executor |
REJECTED | yes | Refused before encoding; detail gives the reason | Executor |
ENCODED | no | ACK 1: encoded by the driver | Driver |
ENCODE_FAILED | yes | The driver could not encode it, the link is not bound, or no ACK 1 in time | Driver or executor |
SENT | no | ACK 2: the gateway sent it (its last frame, when framed in several) | Gateway |
SEND_FAILED | yes | The gateway or the transport could not send it, or no ACK 2 in time | Gateway, transport or executor |
VERIFIED | yes | ACK 3: every verification holds | Executor |
VERIFY_FAILED | yes | A non-conforming echo, or a verification without window found false | Executor |
VERIFY_TIMEOUT | yes | A verification did not hold within its window, or no echo came | Executor |
COMPLETE | yes | Sent, without verification declared | Executor |
A telecommand succeeds on VERIFIED or COMPLETE; every other final state is a failure.
ECHO is an event, not a state: the driver publishes it when it recognises the on-board echo of a
telecommand it encoded, with conforming: true or false.
Events#
Each transition is published on stellar.tc.evt.<target>.<tc_id>, in the TC_EVENTS stream
(30 days of retention):
{"tc": "01J9Z3K8T3YB6N4R5W2Q7XH0AE", "state": "VERIFY_TIMEOUT", "at": "2026-10-02T10:15:04.211Z",
"detail": "verification on responding not met within 5s"}| Field | Meaning |
|---|---|
tc | Identifier of the telecommand (ULID) |
state | New state, or ECHO |
at | Time of the transition |
detail | Explanation: failed precondition, error of the driver or the gateway, timeout… (optional) |
conforming | For an ECHO event: whether the echo conforms |
telecommand | For a PENDING event: {component, instance, telecommand}, which feeds sent_at in derived measures |
Follow a telecommand over WebSocket with GET /v1/tc/{target}/{id}/events: it replays the events
already published, follows the new ones, and closes on a final state. stellar send does exactly
that. See WebSocket Streams.
The chain step by step#
sequenceDiagram
participant C as API / procedure
participant E as Executor
participant D as Driver
participant T as Transport (optional)
participant G as Gateway
participant V as Compute stage
C->>E: telecommand (tc.submit, or in a run)
E->>E: PENDING, then requires, lease, link
E->>D: tc.encode.<codec>.<target>
D-->>E: ENCODED (ACK 1)
D->>T: tc.wrap.<transport>.<target>
T->>G: tc.uplink.<gateway>.<target>
G-->>E: SENT (ACK 2)
G->>D: tm.raw (telemetry, echo)
D-->>E: ECHO
D->>V: tm.decoded
V-->>E: param samples
E->>E: VERIFIED / VERIFY_FAILED / VERIFY_TIMEOUT / COMPLETE1. Submission and PENDING#
A direct telecommand is submitted to the API (POST /v1/tc), which resolves it against the
current snapshot, gives it an identifier and publishes it on stellar.tc.submit.<target>. The
executor runs it as an implicit one-step procedure. A telecommand of a procedure is created by
the executor, which writes it in the log of the run before sending it. Either way, the executor
publishes PENDING, which carries the run and the step for a procedure, and the identity of the
operator for a direct telecommand.
2. Checks before encoding#
Before anything is sent, the executor refuses the telecommand with REJECTED when:
- a precondition does not hold. The
requiresof the telecommand are evaluated on the current values, the samples published but not yet in the table included: a telecommand sent right after await untilor the verification of the previous one sees what they saw. A condition that is false, or that reads a value absent or older than itsmax_age, rejects the telecommand;detailgives the reason and the values read, with their age:precondition on `pressure`, `primary_pump` does not hold: pressure = 101325 Pa (received 0.8 s ago), primary_pump = RUNNING (received 0.3 s ago). - the target is held by a run. See Leases below.
- the link does not carry the component. A link restricted to some components
(
components: [tcu]) refuses a telecommand of another component. - the telecommand or the target is unknown to the configuration revision of the telecommand.
3. Encoding (ACK 1)#
The executor publishes the telecommand on the encoding subject of the binding of its link — the
link of the telecommand (via in a procedure, link for a direct telecommand), else the default
link of the target. A link not bound gives ENCODE_FAILED.
The telecommand is semantic: a component, an instance, a telecommand name and named arguments,
with defaults applied, numbers in the unit of the argument, enum values as strings, bytes as
hexadecimal. The driver encodes it and publishes ENCODED, or ENCODE_FAILED with the reason.
4. Sending (ACK 2)#
The driver hands the encoded unit to the transport of the link, if any, which frames it, or
directly to the gateway. The gateway sends it and publishes SENT, or SEND_FAILED.
- A unit framed into several frames carries
Stellar-Fragment: <i>/<n>on each; the gateway publishesSENTafter the last one,SEND_FAILEDat the first failure. - A unit the transport cannot frame (for instance while COP-1 is locked out) fails the telecommand
with
SEND_FAILEDand the reason: the driver already gave ACK 1.
5. Verification (ACK 3)#
ACK 3 is a predicate on measures, not a correlation of packets. The verify list of the
catalogue combines an echo and conditions on the measures of the component instance:
telecommands:
set_mode:
args:
mode: {type: tcu_operating_mode}
verify: [echo, {mode: args.mode, within: 10s}]- Only samples received after SENT count. A condition is evaluated only on samples of its measures whose ground reception time follows SENT, which avoids a false success on an older value.
- With
within, the condition must become true within the window, which starts at SENT. Otherwise the telecommand endsVERIFY_TIMEOUT. - Without
within, the condition is judged on the first samples received after SENT: false givesVERIFY_FAILED. echois checked by the driver, the only one that knows the bytes: it compares each frame received with the telecommands it encoded recently and publishes anECHOevent. A non-conforming echo givesVERIFY_FAILED; no echo in time givesVERIFY_TIMEOUT.- Every verification must hold for
VERIFIED. - Conditions may reference the arguments (
args.mode).
verify and within are optional. Without them, the telecommand goes from SENT to COMPLETE,
without ACK 3. This is the case of a deferred telecommand executed later by the spacecraft: its
effect is checked afterwards by an explicit check in a procedure, for instance at the next pass.
Timeouts and deadlines#
executor.ack_timeout (10 s by default) bounds each wait of the chain:
| Wait | Limit | State on expiry |
|---|---|---|
| ACK 1 | ack_timeout after PENDING (plus one second of grace) | ENCODE_FAILED (no ACK 1 within …) |
| ACK 2 | ack_timeout after the deadline of ACK 1 | SEND_FAILED (no ACK 2 within …) |
Echo, or first samples of a verification without within | ack_timeout after SENT | VERIFY_TIMEOUT |
Verification with within | within after SENT | VERIFY_TIMEOUT |
The telecommand and its frames carry a deadline, the Stellar-Deadline header (ack_timeout after
PENDING). A driver or a gateway that receives them too late refuses them (ENCODE_FAILED,
SEND_FAILED) instead of sending a stale command: a telecommand not uplinked in time is never sent
later by surprise.
Identifiers and deduplication#
Each telecommand gets a ULID when it is created: by the API for a direct telecommand (or by the
client, which may give its own id to make its submission idempotent), by the executor for a
telecommand of a procedure. The identifier is the Nats-Msg-Id of each stage in the
TC_COMMANDS stream, so that a network duplicate or a resend after a restart of the executor is
deduplicated by JetStream:
| Stage | Nats-Msg-Id |
|---|---|
Submission (stellar.tc.submit.…) | <tc_id> |
| Telecommand to encode | <tc_id>-encode |
| Frame to uplink | <tc_id>-uplink |
| Event | <tc_id>-<STATE> |
A direct submission answers {"id": "…", "duplicate": true} when the same identifier was already
submitted: nothing new is sent. The TC_COMMANDS stream deduplicates over 10 minutes.
A retry is a new attempt, with a new identifier, attached to the telecommand of the first
attempt (retry_of in the run log). Automatic retries are only allowed for telecommands declared
changes_state: false; any other needs an operator decision. See
Retries.
Order#
The telecommands of a target are encoded and sent in the order they were submitted, even when the driver or gateway of the link moves to another instance: each encoding and uplink subject is consumed one message at a time. See Architecture.
Leases and direct telecommands#
A run holds a lease on each of its targets (see Leases and Crash Recovery). While a target is held:
- a direct telecommand is refused:
409 tc::target-heldfrom the API,REJECTEDfrom the executor; - except a telecommand declared
changes_state: falsewhile the lease is shared (a run that only checks and sends telecommands without effect).
The telecommands of a file transfer started by a run carry the Stellar-For-Run header and pass
the lease of that run.
Environment of a telecommand#
A telecommand carries the environment of its run. The environment selects the
parameters of the link, which can differ by environment (a CSP
address in AIT and another in IVV).
- A direct telecommand may declare it (
environmentinPOST /v1/tc,stellar send --environment, a field of the web form). It must when its link overrides its parameters by environment and its target is engaged in several (tc::environment-required); a target engaged in a single environment gives it to its direct telecommands. - A telecommand without environment, such as those of the transfer manager, takes the environment of the last telecommand of its link.
- On reception, the context of the link is also that of its last telecommand, else the base
parameters; decoded values carry the
Stellar-Environmentheader.
Resolution errors of direct telecommands#
POST /v1/tc resolves the request against the current snapshot. An unknown target answers 404;
every other problem answers 422 with the list of errors {code, message, help}:
| Code | Meaning |
|---|---|
tc::unknown-target | No such target (404) |
tc::missing-catalogue | The catalogue of the target is not in the snapshot |
tc::unknown-link | No such link on the target |
tc::unknown-environment | The target is not engaged in that environment |
tc::environment-required | The link has parameters by environment: name the environment |
tc::unknown-telecommand | The platform has no such telecommand |
tc::ambiguous-telecommand | Several components have it: write component.name |
tc::link-component | The link does not carry the component |
tc::missing-instance, tc::unexpected-instance, tc::unknown-instance | Instance of a multi-instance component missing, superfluous or unknown |
tc::missing-argument, tc::unexpected-argument | Argument without default missing, or unknown |
tc::invalid-argument | Value of the wrong type or unit |
tc::out-of-range | Value outside the range of the argument |
tc::target-held | The target is held by a run (409) |
See Error Codes for the codes of the whole API.