Telecommand uplink
This page covers how an operator’s command request becomes an uplinked PUS-C TC space packet, and how its execution gets tracked afterward.
Building the packet
command.py::TelecommandHandler owns the
build-then-store pipeline (submit). The wire format itself is mission-specific, and there’s
one implementation of it:
missions/xtce.py::XTCETelecommandHandler.build_sp.
- It looks up the
Telecommandby name, resolves each argument’s raw value (accepting either the raw integer or, for enumerations, the label), andbitstruct.packs the fixed opcode bytes plus the arguments into the payload. - The packet header comes from the XTCE, not from code. A command’s container resolves to
the CCSDS primary header plus a PUS-C TC secondary header (
TC_HEADER_LAYOUT), followed by the payload.derive_tc_headervalidates that shape against every command’s container as the model is built (XTCEMission._resolve_tc_headers). That means an XTCE file that doesn’t describe PUS-C telecommands fails at startup, not on the first uplink attempt. - Some header fields the operator must supply per command — currently just the target APID,
when a command doesn’t fix it via
ArgumentAssignment. These are the one caseimporters.pyleaves markedHEADER_ARG_UNRESOLVED; the mission layer resolves each one to a real field and a UI label (HEADER_ARG_DESCRIPTIONS). minsppfills in everything computed on the ground: sequence count, packet length, and the CRC-16 PEC (SpacePacket.as_bytes(packet_error_control=True)).
The built packet is stored as a PacketTC with sent=False. The API never opens a socket —
uplink is entirely the job of the ground-link bridge.
On-board execution time (PUS 11,4)
A PacketTCSubmit may carry an execution_time. When it does, submit still finishes the
command normally first — its own sequence count, length, and CRC, i.e. exactly the bytes it
would otherwise transmit. Then it wraps that finished command in the model’s schedule-insert
command (build_schedule_insert), which is what actually gets uplinked immediately. That
makes the spacecraft queue the embedded command and run it on board at the requested instant.
_check_schedulable refuses locally what the spacecraft would refuse anyway: service 11
inside a schedule, a high-priority (8,2) command, a packet over SCHED_MAX_TC_OCTETS, or a
time that’s already past.
The feature is gated on Mission.scheduling_enabled, which requires both
MINMCS_TC_SCHEDULING to be set and the model to define a schedule-insert command. It’s
surfaced to the UI on /status.
Verification
TC verification is based on PUS service 1.
XTCETelemetryPacketHandler.VERIF_CONTAINERS
maps report containers to (stage, status) pairs. on_processed decodes the echoed sequence
count from each report and forwards it to
stores.py::PacketStore.tc_apply_verification, which
closes that stage on the matching PacketTC.
Verification of a scheduled command is correlated differently, because two separate
packets are involved: the schedule-insert command the MCS actually uplinked, and the embedded
command it carried. PacketTC.request_id holds the first four octets of the command’s own
packet — its two CCSDS primary-header words, i.e. the PUS request id.
tc_apply_verification tries matching on request_id first, and falls back to the uplinked
sequence count. In practice that means:
- Reports arriving for the uplink itself are found by sequence count, and close a single
queuedstage — they verify the insert, not the embedded command. - The start/completion pair emitted at the release instant quotes the embedded command’s own
id — a packet the MCS never uplinked in its own right — and closes the
start/completestages.