TTC — Telemetry, Tracking and Command
Reference documentation for nodes/ttc_node.c, the spacecraft’s radio
terminal. See index.md for the platform overview, dhs.md for the DHS
it exchanges packets with, pus.md for the wire format it moves, and
egse.md for what is on the other end of the link.
CAN address 0x2, PUS APID 0x002. One binary, build/bin/ttc_node.
1. Role and boundaries
TTC is the transfer-frame/link layer only — no PUS service semantics. The DHS builds telemetry
packets and parses commands; TTC moves whole PUS space packets across the ground link,
reassembling what the DHS sent over CAN_FUNC_PKT, streaming it to the EGSE, and pushing uplinked
packets back to the DHS the same way.
| Does | Does not |
|---|---|
Frame packet boundaries (pusExtract) | Decode service/subtype, except to spot the high-priority class |
| Buffer telemetry while the link is down | Timestamp anything (the DHS already did) |
| Classify and forward whole packets | Build CAN commands — the endpoint decodes |
| Hold link parameters (SCID, packet counts) | Hold mission parameters (they come from the OBDB) |
RF, modulation, channel coding and CCSDS transfer-frame generation would wrap the same packet stream; here that layer is a deliberate pass-through, so the EGSE sees raw space packets.
TTC dials out to the EGSE rather than listening (:194) — a Posix-port requirement, not a protocol preference, and it lets the EGSE come and go freely.
2. Boot sequence
| Step | Function | What happens |
|---|---|---|
| 1 | prvLoadConfig (:529) | Reads board straps only, [ttc]: log_level, spacecraft_id. |
| 2 | prvInitDataPool (:544) | Registers hk_period_ms, link_retry_ms as pool parameters, applies ROM defaults, obdbInit(CAN_ADDR_TTC, NULL). |
| 3 | main (:564) | Ring mutex, canInit(CAN_ADDR_TTC), three tasks, scheduler start. |
Both pool parameters are read live each cycle, so no apply callback is needed — the DHS’s
database block takes effect on the next pass. Until it arrives the node runs on ROM defaults,
flagged by (rom) on the obdbTag() status line.
3. Tasks
| Task | Priority | Period | Role |
|---|---|---|---|
disp (:430) | PRIO_APP | blocks on CAN queue | Reassembles CAN_FUNC_PKT into whole packets, pushes to the ring; handles CMD_PING, CMD_SET_TIME, CMD_TTC_SWITCH, the OBDB block. |
link (:361) | PRIO_APP | paced by recv() | Sole owner of the TCP socket: connect, receive, transmit, close. |
hk (:499) | PRIO_TLM | hk_period_ms (5000) | Emits the link-parameter HK frame and the console status line. |
Plus canrx from canInit — four tasks, ~512 KiB of heap.
The link task must be both bounded and yielding
(porting.md §1):
the vTaskDelay(1) at the bottom of prvLinkTask is what lets hk run at all. This platform
once shipped without it and TTC emitted no housekeeping for a run’s entire life — caught only
because smoke_sim.sh asserts all five APIDs.
Concurrency. xRingMutex is the only lock. The socket is single-writer by task ownership
(only link touches it); ucLinkDrop is the sole cross-task signal, by which disp asks link
to drop the connection (§8).
4. The downlink ring
DOWNLINK_RING_LEN is 256 whole packets (:27), each up to
PUS_MAX_PACKET bytes — static, not heap. It buffers opaque bytes and adds no timestamp of its
own: the DHS already stamped each packet at build time, so one that waits out a link outage keeps
the time it was generated (same reasoning as dhs.md §11).
| Operation | Behaviour |
|---|---|
prvRingPush (:79) | Full ring: overwrites the oldest, counts a drop — fresh telemetry beats stale. |
prvRingPeek (:99) | Copies the tail slot without retiring it. |
prvRingPop (:116) | Retires only once actually on the wire, so a mid-burst drop doesn’t silently eat telemetry. |
prvLinkTransmit (:249) drains up to LINK_TX_BURST = 16 packets per
pass; a send failure leaves the packet buffered for retry.
prvLinkSendAll (:142) is atomic by construction — a half-written
packet would desync the EGSE’s stream parser — retrying on
EINTR and giving up after 100
EAGAIN stalls (assumed wedged EGSE).
5. The ground link
prvLinkConnect (:194) dials EGSE_ADDR:EGSE_LINK_PORT (TCP :21000)
with SO_RCVTIMEO one tick, SO_SNDTIMEO one second, TCP_NODELAY; on failure it sleeps
link_retry_ms and retries. prvLinkClose (:178) logs once and
reverts to buffering.
Uplink — prvLinkReceive (:275) accumulates into a 1024-byte
staging buffer (TCP is a stream: a read may hold several packets, or half of one). pusExtract
pulls each whole packet; TTC then classifies and forwards with canPktSend:
dst = (service == PUS_SVC_FUNCTION && subtype == PUS_ST_FUNC_PERFORM_HP)
? pusAddrFromApid(tc.apid) & 0x0F /* straight to the subsystem */
: CAN_ADDR_DHS; /* everything else */That two lines is the whole high-priority bypass: TTC never
parses the command, only decides who decodes it. It’s what makes egse_tc EPS OBC B work with
the DHS down, and why redundancy state is a board strap
rather than distributed database content.
Resynchronisation — when pusExtract can’t frame a packet it retires one octet at a time.
resyncing (:307) collapses a run of unframed bytes into a single
warning (usual causes: a bad length field or absent PEC). A buffer that fills entirely with
unframed data is reset.
6. Telemetry
One housekeeping frame every hk_period_ms, sent to the DHS as CAN_FUNC_HK and packetised into
APID-2 PUS TM like any other subsystem’s:
| Octets | Field |
|---|---|
| 0-1 | tm_sent — packets downlinked (low 16 bits) |
| 2-3 | tc_received — packets uplinked and forwarded |
| 4-5 | dropped — packets overwritten in a full ring |
| 6-7 | spacecraft_id (SCID) |
The console line carries the same values plus live buffer depth:
[TTC ] link=DOWN SCID=0x00C5 tm_sent=0 tc_rcvd=0 buffered=15 dropped=0
[TTC ] EGSE link up to 127.0.0.1:21000 (SCID 0x00C5, 21 packet(s) buffered)7. Commands accepted
| Opcode | From | Effect |
|---|---|---|
CMD_PING | anyone | Replies CAN_FUNC_ACK with the opcode |
CMD_SET_TIME | DHS only, dlc >= 7 | Disciplines the local clock (obtSet) |
CMD_TTC_SWITCH | EPS | Cold reboot, only if the board differs (§8) |
CMD_CFG_BEGIN/_ITEM/_END | DHS | obdbHandleFrame — staged, validated, committed whole |
Anything else logs unknown command 0x%02X at debug level rather than being dropped silently.
8. Redundancy — the cold-redundant CPU board
TTC has two integrated CPU boards (A active, B cold); the EPS owns which is powered — see
Redundancy for the model shared by all three pairs. Locally,
xActiveTtc is the board this process runs, so the reboot fires only when the EPS-reported board
differs (:478).
prvTtcReboot (:404) models cutting power to one board and
cold-booting the other:
| Wiped | Survives |
|---|---|
Downlink ring (head, tail, count, dropped) | spacecraft_id — config identity, what the downlink is attributed to |
tm_sent, tc_received | Pool registration and any OBDB values already applied |
Uplink reassembly (xPktReasm) | |
Clock (obtReset → (free)) | |
| Ground link — dropped, then re-dialled |
The socket itself isn’t touched here: the reboot runs in the dispatch task (owner of
xPktReasm), but the socket belongs to the link task, so ucLinkDrop
(:136) hands the drop across, keeping it single-writer. The ring is
wiped under its mutex.
The DHS does not reboot on this switch — it only tracks xPlat.ttc_cpu and raises
EVT_TTC_SWITCH. Asymmetry worth noting: being the uplink path, TC EPS TTC <A|B> only reaches
the spacecraft while the active board is alive — planned rotation, not dead-board recovery.
9. Configuration reference
Board straps — read from [ttc] by this node in main():
| Key | Default | Meaning |
|---|---|---|
log_level | info | off | error | warn | info | debug | trace |
spacecraft_id | 0x00C5 | CCSDS SCID. Board identity, not a mission parameter: it’s what the downlink is attributed to, it survives a CPU-board reboot, and the OBDB can’t change it. |
Database content — read from [ttc] by the DHS, distributed back over the bus:
| Parameter | PID | Default | Range |
|---|---|---|---|
TTC_HK_PER (hk_period_ms) | 0x0280 | 5000 | 500 … 600000 |
TTC_LINK_RETRY (link_retry_ms) | 0x0281 | 1000 | 100 … 60000 |
Both U32, above the 0x80 database threshold. Retune in flight with
egse_tc DHS PARAM SET TTC_HK_PER U32 2000.
10. Known gaps and deliberate simplifications
- No simulated radio yet!
- RF, modulation, channel coding and CCSDS transfer frames are a pass-through stub — the EGSE sees raw space packets. This is the subsystem’s charter and the obvious place to add a real TM/TC transfer-frame layer without touching any other node.
- No link budget, no Doppler, no ranging — “Tracking” is a name here, not a function.
- No downlink rate limiting:
LINK_TX_BURSTpaces the drain per pass but models no bandwidth. - The ring counts drops but never reports them as an event —
droppedrides only in housekeeping, so losing telemetry to a long outage is visible but not alarmed.