Skip to content

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.

DoesDoes not
Frame packet boundaries (pusExtract)Decode service/subtype, except to spot the high-priority class
Buffer telemetry while the link is downTimestamp anything (the DHS already did)
Classify and forward whole packetsBuild 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

StepFunctionWhat happens
1prvLoadConfig (:529)Reads board straps only, [ttc]: log_level, spacecraft_id.
2prvInitDataPool (:544)Registers hk_period_ms, link_retry_ms as pool parameters, applies ROM defaults, obdbInit(CAN_ADDR_TTC, NULL).
3main (: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

TaskPriorityPeriodRole
disp (:430)PRIO_APPblocks on CAN queueReassembles 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_APPpaced by recv()Sole owner of the TCP socket: connect, receive, transmit, close.
hk (:499)PRIO_TLMhk_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).

OperationBehaviour
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.

UplinkprvLinkReceive (: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:

OctetsField
0-1tm_sent — packets downlinked (low 16 bits)
2-3tc_received — packets uplinked and forwarded
4-5dropped — packets overwritten in a full ring
6-7spacecraft_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

OpcodeFromEffect
CMD_PINGanyoneReplies CAN_FUNC_ACK with the opcode
CMD_SET_TIMEDHS only, dlc >= 7Disciplines the local clock (obtSet)
CMD_TTC_SWITCHEPSCold reboot, only if the board differs (§8)
CMD_CFG_BEGIN/_ITEM/_ENDDHSobdbHandleFrame — 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:

WipedSurvives
Downlink ring (head, tail, count, dropped)spacecraft_id — config identity, what the downlink is attributed to
tm_sent, tc_receivedPool 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():

KeyDefaultMeaning
log_levelinfooff | error | warn | info | debug | trace
spacecraft_id0x00C5CCSDS 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:

ParameterPIDDefaultRange
TTC_HK_PER (hk_period_ms)0x02805000500 … 600000
TTC_LINK_RETRY (link_retry_ms)0x02811000100 … 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_BURST paces the drain per pass but models no bandwidth.
  • The ring counts drops but never reports them as an event — dropped rides only in housekeeping, so losing telemetry to a long outage is visible but not alarmed.
Last updated on