DHS — the Data Handling Sub-System
Reference documentation for nodes/dhs_node.c, the on-board data handling
sub-system. See index.md for the platform overview, ttc.md for the link
layer it hands packets to, pus.md for the wire format, and obdb.md for the
parameter model.
CAN address 0x1, PUS APID 0x001. One binary, build/bin/dhs_node.
1. Role and boundaries
The DHS is the CAN↔PUS boundary. Inside the spacecraft everything is an 8-byte CAN frame; the ground link carries CCSDS Space Packets with ECSS PUS-C secondary headers. The DHS is the only node that translates between the two.
It owns seven things:
| Responsibility | Summary |
|---|---|
| Packetisation | Builds every downlink TM packet, attributing each to its producing subsystem by APID |
| Telecommand routing | Decodes uplinked PUS TC, routes CAN commands, answers service-1 verification |
| Mass memory (SSMM) | A static ring of generated packets held for downlink, a file partition for science data, and an event log |
| The mission data link | The RMAP target end of the SpaceWire link, and PUS service 23 as its control plane |
| Clock mastering | Owns on-board time and broadcasts it to every other node |
| The data pool | One addressable, typed, access-controlled view of on-board state, exposed via PUS service 20 |
| The on-board database | Holds every other subsystem’s mission parameters and distributes them over the bus |
Not the DHS’s: RF, modulation, channel coding and CCSDS transfer frames belong to TTC, the link
layer. The DHS hands TTC whole PUS packets, segmented over CAN_FUNC_PKT; TTC buffers them
across link outages and streams them to the EGSE. CAN never leaves the spacecraft.
“OBC” refers only to the redundant computer hosting the DHS — see §12.
2. Boot sequence
Everything that reads a file happens in main() (:4929)
before vTaskStartScheduler(), because a
task blocked in a syscall
starves the Posix port.
logInit("DHS")
└─ prvLoadConfig :6162 also reads [spw] link_rate_bps, a board strap
└─ prvReloadTunables :6049 [dhs] section → xSys, xHkTable, boot mode
└─ prvInitDataPool :6190
├─ prvObdbBuild :2557 materialise DP_CFG_CATALOG, seed ROM defaults
├─ prvObdbLoad(obdb_file) :2747 restore the MRAM image "MOB1"…
│ └─ prvObdbProgramFromFile :2612 …or, if absent/corrupt, program from minscs.conf
├─ prvRegisterPoolParams :3178 bind 99 static params + every database row
├─ dpNvramLoad / dpNvramRestore apply the operator delta over the baseline
└─ dpStart from here the pool is shared between tasks
└─ xSemaphoreCreateMutex × 4 xSsmmMutex, xVerifMutex, xSchedMutex, xSsmmFileMutex
└─ canInit(CAN_ADDR_DHS) creates the "canrx" driver task
└─ spwInit(SPW_LA_DHS, listen) creates the "spwrx" driver task; binds :20100
└─ xTaskCreate × 5 disp, orch, hkpoll, fdir, dnlink
└─ vTaskStartSchedulerprvInitDataPool’s ordering is the whole design in miniature: ROM → MRAM → NVRAM, each layer
applied over the last, so the parameter file is the baseline and the non-volatile mirror is the
operator’s override. See §10.
3. Tasks
main() creates five tasks; canInit() creates a sixth and spwInit() a seventh. All seven are
pthreads under the Posix port, and each costs ~128 KiB of the 4 MiB FreeRTOS heap (stack depth is
counted in words), so the DHS’s task budget is ≈ 896 KiB.
| Task | Priority | Source | Role |
|---|---|---|---|
canrx | PRIO_CAN_RX (5) | common/can_drv.c | Bounded recvfrom into the RX queue; yields every CAN_RX_BATCH frames |
disp | PRIO_APP (3) | :4566 | CAN RX demultiplex — the only consumer of canRxQueue() |
orch | PRIO_APP (3) | :4394 | The decision loop: autonomy, scheduling, periodic telemetry, file writes |
fdir | PRIO_APP (3) | :4253 | FDIR: the heartbeat watchdog and the bus undervoltage monitor |
hkpoll | PRIO_TLM (2) | :4200 | Polls subsystems for housekeeping per xHkTable |
dnlink | PRIO_BULK (1) | :4679 | Drains the SSMM to TTC over CAN_FUNC_PKT |
spwrx | PRIO_CAN_RX (5) | common/spw_drv.c | The SpaceWire link: bounded recvfrom, the initialisation state machine, and the RMAP target callback that writes into the mass memory file partition |
prvDispatchTask — CAN receive demultiplex
Blocks on xQueueReceive (a FreeRTOS primitive, so blocking is safe) and switches on the
frame’s function code:
| Function | Handling |
|---|---|
CAN_FUNC_HK (0x2) | prvIngestHk — parse into the platform mirror, then packetise as TM(3,25) |
CAN_FUNC_SCI (0x3) | Packetised as TM(3,25) too; the APID says which instrument |
CAN_FUNC_XFER_FF (0x4) | prvHandleFirstFrame — open a payload file reassembly |
CAN_FUNC_XFER_CF (0x5) | prvHandleConsecutiveFrame — append, and downlink on completion |
CAN_FUNC_PKT (0x7) | prvHandlePktSegment — reassemble an uplinked PUS TC from TTC |
CAN_FUNC_CMD (0x0) | Only CMD_LOW_POWER is expected inbound (the EPS asserting/clearing) |
CAN_FUNC_ACK (0x1) | Payload file-stored notice, or an OBDB Ready, or a tracked TC completion |
The ACK branch is ordered deliberately: CMD_CFG_END acks are taken by prvObdbAck before
prvVerifMatchAck, or they’d be logged as acks for a telecommand nobody sent.
prvOrchestrateTask — the decision loop
Runs every orchestrate_tick_ms (default 1000). ORCH_TICK is deliberately read-only in the
data pool — it is the divisor of every cadence below, so a wild value from the ground would break
the loop rather than merely retune it.
| Step | Cadence |
|---|---|
| Low-power safing / recovery (§8) | every tick |
| Thermal limit check | every tick |
prvObdbSchedule — database distribution | every tick, after ulTick >= 2 |
| Mode announcement, once, on database convergence | one-shot |
prvVerifSweep — retire timed-out telecommands | every tick |
prvSchedRelease — release due scheduled activities | every tick |
prvBroadcastTime | time_sync_period_ms / orchestrate_tick_ms |
prvEmitOwnHk | hk_tm_period_ms / orchestrate_tick_ms |
prvEmitAdcsHk / prvEmitEpsHk — one housekeeping report each | that subsystem’s xHkTable period_ms / orchestrate_tick_ms |
dpNvramFlush + prvObdbFlush | nvram_flush_period_ms / orchestrate_tick_ms, and only if dirty |
The first two ticks skip distribution because the hub learns routes at runtime and silently drops
a frame to an unlearned address; prvHkPollTask delays its first pass 1500 ms for the same reason.
prvHkPollTask is covered in §6.
prvDownlinkTask — mass memory drain
Takes the oldest stored packet, segments it to TTC with canPktSend, and retires the slot. It is
the sole writer of CAN_FUNC_PKT toward TTC, so segments from different packets never
interleave on the bus. Segmenting releases the SSMM lock for several ticks, so the task records
which slot index it took and only retires that slot if read has not moved underneath it — a full
ring can overwrite and re-advance read in the meantime.
4. Telecommand handling
The two-stage model
The DHS implements the ECSS request-verification model as two functions the uplink path calls back to back:
- Acceptance —
prvAcceptTc(:3979): is the destination routable, the service implemented, the subtype legal, the parameters valid? AnswersTM[1,1]orTM[1,2](+ error code). Pure — validates and translates to a CAN command, emits nothing. - Execution —
prvExecuteTc(:4069): carries it out and answers completion,TM[1,7]orTM[1,8](+ error code).
prvHandleTc (:4146) is just those two with the acceptance report
between them; they stay separate because the command schedule
calls them at two different times with a third report in between.
Every report opens with a 4-octet request identifier echoing the TC’s primary header. Failure
error codes: ILLEGAL_SERVICE, INVALID_PARAMS, INVALID_STATE, EXEC_FAILED, TIMEOUT,
SCHED_FULL, SCHED_PAST.
Destination
The target subsystem is the TC’s APID (low nibble, :4146),
validated against the five node addresses plus CAN_ADDR_BROADCAST (prvIsRoutableAddr,
:2989). An invalid target is rejected INVALID_PARAMS rather than
sent to the hub, which would silently drop it and leave the request to expire into a misleading
TIMEOUT.
Services accepted
Seven. Anything else is rejected ILLEGAL_SERVICE.
| Service | Subtype accepted | Translation | Executed |
|---|---|---|---|
| 17 test | 1 connection test | CMD_PING | on the target |
| 3 housekeeping | 27 one-shot report | CMD_REQ_HK | on the target |
| 8 function | 1 perform, 2 perform-HP | app data is the CAN command (≤ 8 octets) | on the target |
| 9 time | 128 set clock | — | locally |
| 11 scheduling | 1, 2, 3, 4, 5, 16 | — | locally |
| 20 parameters | 1, 3, 133, 134 | — | locally |
| 23 file management | 1, 2, 3, 12, 14 | — | locally (14 also puts a CMD_XFER_GRANT on the bus) |
Subtypes the service emits (17,2; 3,25; 11,17; 20,2; 23,4; 23,13) are also rejected
ILLEGAL_SERVICE. Service 8 subtype 2 (PUS_ST_FUNC_PERFORM_HP) is the
high-priority class TTC hands straight to the addressed
subsystem, so one only reaches the DHS if addressed to the DHS itself.
Three execution paths
| Path | When | Completion |
|---|---|---|
| Local | services 9, 11 and 20; any command with dst == CAN_ADDR_DHS | synchronous — prvVerifCompleteNow |
| Dispatch-is-completion | service 3 polls, and any broadcast (dst == 0xF) | synchronous, on send |
| Parked | everything else routed to a subsystem | asynchronous — on the subsystem’s CAN ack, or TIMEOUT |
The middle row exists because an HK poll’s result comes back on the housekeeping channel, not a
command ack, and a broadcast ack always carries the real sender’s address, never 0xF.
The pending table
xVerif (:390) holds up to VERIF_PENDING_SLOTS (16) parked requests
— { request id, destination, CAN command byte, issue tick } — under xVerifMutex. An ack matches
on (dst == src) && (cmd == data[0]); anything still active after verif_timeout_ms becomes
TM[1,8] TIMEOUT. Static, like the SSMM.
Commands the DHS executes itself
prvExecDhsCommand (:2900), reached when the TC’s APID is the DHS:
| Opcode | Action |
|---|---|
CMD_PING (0x01) | Log only; completion is the answer |
CMD_REQ_HK (0x02) | prvEmitOwnHk |
CMD_SET_MODE (0x03) | Platform mode → broadcast to the whole bus. Refused INVALID_STATE if low power is asserted and the mode is not SAFE |
CMD_CAPTURE (0x10) | Forward to PAYLOAD |
CMD_DOWNLINK_FILE (0x11) | Forward the file id to PAYLOAD |
CMD_FLASH_STATUS (0x12) | Forward to PAYLOAD |
CMD_DUMP_EVENTS (0x21) | prvDumpEvents — replay the whole event log |
CMD_PARAM_DUMP (0x25) | prvParamDumpAll — report the whole pool as chunks |
CMD_CFG_PUSH (0x29) | Invalidate one or all peers, forcing re-distribution |
A platform command addressed to the DHS fans out to the bus (prvSetMode broadcasts); one to a
subsystem routes straight there.
Time-based scheduling (service 11)
A queue of telecommands lodged against an absolute on-board time, so an activity can be commanded
in one contact and executed with nobody listening. A TC[11,4] carries a release time and a
whole embedded telecommand; the DHS verifies and stores it, and the orchestrate loop releases
it when the clock reaches that time.
| Subtype | Verb | Body |
|---|---|---|
11,1 / 11,2 | arm / hold releases (the queue is kept either way) | — |
11,3 | discard every entry | — |
11,4 | insert | N(2) then N × { coarse(4) fine(2) len(2) tc[len] } |
11,5 | delete by request id | N(2) then N × request id(4) |
11,16 → 11,17 | report the queue | — → chunk(1) M(2) then M × 16-octet record |
xSched (:420) is a static array of SCHED_SLOTS (16) × up to
SCHED_TC_BYTES (96) octets, kept sorted by release time.
- The embedded telecommand is deep-copied — its
app_datapoints into the uplink reassembly buffer, which the next telecommand overwrites. - Acceptance runs twice: insert (
prvSchedValidate, :3067) only checks well-formed and routable; release (prvSchedRelease, :3408) re-runs the fullprvAcceptTc, since legal-when-lodged may not be legal now. - No schedule inside the schedule — a
TC[11,4]embedding a service-11 command is rejectedILLEGAL_SERVICE. - Releases gate on
obtIsSynced(): an unsynced clock makes release times meaningless, so a reboot leaves a restored schedule dormant untilTC[9,128]rather than dumping it onto the bus.
The release loop runs right after prvVerifSweep: TM[1,3] against the original request id, then
prvAcceptTc followed by prvExecuteTc with that id, closing the lifecycle the ground opened. An
activity that cannot start produces TM[1,4] plus EVT_SCHED_FAILED and the loop continues
(ECSS-E-ST-40C error isolation) rather than stalling on a malformed entry.
The safe-guard mirror
prvDhsReboot wipes the queue like the pending-verification table, but its statistics —
STAT_SCHED_REL, STAT_SCHED_FAIL, STAT_SCHED_DROP — are read-only with DP_ACC_NVRAM, so
dpRebindVolatile restores them and they survive an OBC switch. SCHED_COUNT does not
persist (a depth is a fact about now); SCHED_ENABLED does, so a disarmed schedule doesn’t come
back armed. Read-only plus NVRAM is a combination nothing else uses — it gates on DP_ACC_NVRAM
alone, never DP_ACC_WRITE, so it’s persisted by the DHS and not forgeable by a TC[20,3].
Uplink reassembly
TTC forwards whole PUS TC packets segmented under CAN_FUNC_PKT: a first segment (start marker
0x80 + 2-octet length + 5 octets) then continuations (7-bit rolling sequence + 7 octets).
canPktFeed reassembles into xTcReasm; a gap, overflow or continuation with no start drops
the whole packet, with no retransmission. On a complete packet, prvHandlePktSegment
(:4178) decodes it, checks the CRC and TC type, and calls
prvHandleTc.
5. Telemetry generated
Every packet is built by prvEmitTmLocked (:486), which stamps the
current on-board time, increments the per-APID sequence and message counters, and stores the
result in the SSMM. An oversized packet is dropped, not truncated.
Provenance: the APID names the producing application, not the DHS — prvIngestHk uses
pusApidFromAddr(CAN_ID_SRC(...)), the CAN frame’s source address, so an ADCS housekeeping
frame becomes an APID-3 packet and a payload science frame an APID-5 packet.
The packet shapes
| Packet | APID | Source |
|---|---|---|
(3,25) housekeeping | producing subsystem | prvIngestHk :685 — per inbound HK frame; TTC/PAYLOAD only |
(3,25) ADCS report | 0x003 | prvEmitAdcsHk :1026, 33 octets, see §6 |
(3,25) EPS report | 0x004 | prvEmitEpsHk :1071, 30 octets, see §6 |
(3,25) science | producing subsystem | prvDispatchTask :4566 — shares the HK path |
(3,25) DHS platform report | 0x001 | prvEmitOwnHk :903, 66 octets, see below |
(5,1..4) event report | 0x001 | prvRaiseEvent :541, prvDumpEvents :578 |
(1,1)/(1,2)/(1,7)/(1,8) verification | 0x001 | prvVerifEmit :1667 |
(1,3)/(1,4) start of execution | 0x001 | prvVerifStart :1692 — scheduled releases only |
(13,1)/(13,2) large data | 0x005 PAYLOAD | prvDownlinkFile :1124 |
(11,17) schedule detail | 0x001 | prvSchedEmitChunk :3222 |
(20,2) parameter values | 0x001 | prvParamEmitChunk :2599 |
(23,4) file attributes | 0x001 | prvFileEmitAttr :3532, 26 octets, see spacewire.md |
(23,13) repository summary | 0x001 | prvFileEmitRepo :3575, chunked, 24-octet records |
CAN_FUNC_XFER_FF/_CF do not map directly onto 13,1/13,2 — the frames are reassembled
first, and only a complete, gap-free file is downlinked (:1206).
The DHS’s own housekeeping report
66 octets, prvEmitOwnHk (:903) — strictly the parameters this
computer owns, the 0x01xx band and nothing else:
| Offset | Field | Pool id |
|---|---|---|
| 0 | Platform mode | SYS_MODE |
| 1 | Low-power flag | SYS_LOW_POWER |
| 2 | Autosafe flag | SYS_AUTOSAFE |
| 3 | Hosting OBC | SYS_OBC |
| 4 | Tracked ADCS board | SYS_ADCS_CPU |
| 5 | Tracked TTC board | SYS_TTC_CPU |
| 6..9 | TM packets built | STAT_TM_BUILT |
| 10..13 | Telecommands accepted | STAT_TC_OK |
| 14..17 | Telecommands rejected | STAT_TC_REJ |
| 18..19 | Mass-memory depth | STAT_SSMM_CNT |
| 20..23 | Mass-memory packets dropped | STAT_SSMM_DROP |
| 24..27 | Verification timeouts | STAT_VERIF_TMO |
| 28..31 | Non-volatile mirror writes | STAT_NVRAM_WR |
| 32..35 | On-board database epoch | CFG_EPOCH |
| 36 | Database readiness mask (bit per peer: TTC, ADCS, EPS, PAYLOAD) | CFG_READY_MSK |
| 37..38 | Schedule depth | SCHED_COUNT |
| 39..42 | Schedule activities released | STAT_SCHED_REL |
| 43..46 | Schedule activities failed | STAT_SCHED_FAIL |
| 47..50 | Schedule activities dropped | STAT_SCHED_DROP |
| 51 | Mission data link state | SPW_LINK |
| 52 | Mission data link load, % | SPW_LOAD_PCT |
| 53..56 | Octets received over the link | SPW_RX_BYTES |
| 57 | File copy state | XFER_STATE |
| 58..61 | Octets received this copy | XFER_BYTES |
| 62 | Files held in the mass memory partition | SSMM_FILE_CNT |
| 63..65 | Software version, major / minor / patch | SYS_SW_VERSION |
The last three octets are the build this spacecraft is running, from
include/version.h, checkable by eye against
mdb/minscs.xtce’s header (see
mdb.md); every other field is read according to
that same file. The read-write thresholds and collection table stay out — they’re database content,
not measurements, the same line drawn in the ADCS and EPS reports.
Locking. xTm/xSsmm (under xSsmmMutex) and the verification/schedule statistics (other
locks, none of which may nest, §13) are snapshotted into locals
before the packet goes out through prvEmitTmLocked. Note the downlinked tm_built is the count
before this report, since emitting it increments the counter.
Unlike the ADCS and EPS reports, a one-shot TC DHS HK produces an immediate downlink —
CMD_REQ_HK addressed to the DHS calls prvEmitOwnHk synchronously, with no CAN round trip.
Timestamps: a CAN housekeeping frame already fills all 8 octets, so a 6-octet CUC timestamp cannot ride alongside the data at the producing subsystem. The DHS stamps each packet with the synchronized time at the moment it builds it — within one CAN hop of the subsystem’s sample time — baked in at generation, so telemetry waiting in TTC’s buffer through a link outage keeps its original time rather than being re-dated at flush.
The mass memory unit
Two stores with opposite retention policies:
xSsmm(:273), a static ring ofSSMM_SLOTS(128) ×SSMM_MAX_PACKET(256 B) = 32 KiB of generated packets. When full it overwrites the oldest undownlinked packet: fresh telemetry beats stale.ucSsmmFile, a 128 KiB partition of science files, which refuses when full — a science file is an observation the ground commanded, so discarding one is the ground’s decision, viaTC[23,2]. See §10.5.
Both sized at compile time, so neither perturbs the heap budget.
6. Housekeeping collection
Every subsystem self-emits housekeeping on its own timer; the DHS additionally polls the
pollable ones as a backstop. The collection table xHkTable (:92) has
one row per remote subsystem, configured by the hk_<sub>_enabled / hk_<sub>_period_ms keys in
§14. enabled gates polling and downlink, but not ingest — a
disabled subsystem isn’t polled or packetised to the ground, but prvIngestHk still parses each
frame into the platform mirror, so autonomous safing is never blinded by a disabled row.
TTC has no on-demand HK responder (poll_cmd == 0), so it’s listed but never polled — only
hk_ttc_enabled has effect. The payload is polled with CMD_FLASH_STATUS rather than
CMD_REQ_HK.
The table is live: every row is registered in the data pool (HK_ADCS_ON…HK_PL_PER), so a
TC[20,3] retunes it with no task restart. The poll task recomputes its base wake quantum — the
smallest enabled, pollable period — on every wake rather than latching it at startup, so a
lowered period takes effect immediately.
What the DHS parses out of each frame
prvIngestHk (:685) writes through dpWriteF32/dpWriteI32 rather
than straight to the field, so the pool stamps validity and update time — the difference between
“the rate is zero” and “we have never heard from the ADCS”.
| Source | Tag | Parsed into |
|---|---|---|
| ADCS | ADCS_RPT_RATE (0xD1) | ADCS_RATE_X/Y/Z (hundredths of °/s) |
| ADCS | ADCS_RPT_ATT (0xD2) | ADCS_QE_X/Y/Z, and ADCS_QE_W reconstructed from the unit norm |
| ADCS | ADCS_RPT_CTRL (0xD3) | ADCS_PLAT_MODE, ADCS_CTRL_MODE, ADCS_LOCKED, ADCS_FDIR, ADCS_ERR_DEG, ADCS_HB (only when dlc >= 8) |
| ADCS | ADCS_RPT_ACT (0xD4) | ADCS_WHL_RPM1/2/3 |
| ADCS | ADCS_RPT_SENS (0xD5) | ADCS_MAG_X/Y/Z, and the validity bits ADCS_SUN_VAL, ADCS_MAG_VAL, ADCS_ST1_VAL, ADCS_ST2_VAL, ADCS_ECLIPSE |
| EPS | (untagged) | EPS_SOC_PCT, EPS_BUS_V, EPS_ARRAY_A, EPS_LOAD_A |
| EPS | EPS_RPT_POWER (0xE1) | EPS_RAIL_INSTR, EPS_RAIL_RADIO, EPS_RAIL_BUS |
| EPS | EPS_RPT_REDUN (0xE2) | SYS_OBC_PWR; a change of active OBC triggers prvDhsReboot |
| EPS | EPS_RPT_REDUN_ADCS (0xE3) | SYS_ACS_PWR; active board tracked and logged, no reboot |
| EPS | EPS_RPT_REDUN_TTC (0xE4) | SYS_TTC_PWR; active board tracked and logged, no reboot |
| EPS | EPS_RPT_THERMAL (0xE5) | EPS_TEMP_BATT, EPS_TEMP_RAD (hundredths of °C) |
| EPS | EPS_RPT_ORBIT (0xE6) | EPS_ECLIPSE, EPS_ORB_VAL, EPS_SUN_INC, EPS_BETA_DEG, EPS_FDIR |
| EPS | EPS_RPT_PL_PWR (0xE7) | EPS_PL_CAM_ON, EPS_PL_GNSS_ON, EPS_PL_MEM_ON |
| PAYLOAD | CMD_FLASH_STATUS (0x12) | PL_FLASH_USED, PL_FLASH_TOTAL, PL_FLASH_FILES |
| TTC | — | not parsed; link parameters are packetised as ordinary APID-2 housekeeping |
The untagged EPS frame is reached by falling through every tag test above, parsed without
checking dlc. That’s safe only because each tag is tested first — a tagged report this table
doesn’t list is read as a state of charge, a bus voltage and two currents, driving autonomous
safing. Any new EPS report tag must be added here in the same change that puts it on the wire,
since the EPS’s ground report is built from these parameters rather than relayed.
One report per subsystem
Two subsystems’ housekeeping is not relayed frame for frame. The emit gate at the tail of
prvIngestHk skips them, and a generator builds one report each from the parameters above, on that
subsystem’s collection-table cadence, in prvOrchestrateTask:
| Generator | APID | Octets | Replaces | Field map |
|---|---|---|---|---|
prvEmitAdcsHk (:1026) | 0x003 | 33 | five tagged frames | adcs.md §8 |
prvEmitEpsHk (:1071) | 0x004 | 30 | eight frames | eps.md §8 |
Each packet describes its subsystem even though this node builds it, so the field maps live there.
The rule is about frame count, not about those two subsystems — eight octets is what a CAN frame holds, so a subsystem with more to say splits it. Building the packet here rather than relaying the frames keeps the picture unskewed in time and stops frame sizes being ground-visible: one container per subsystem instead of several sized to fit a CAN frame. TTC and the payload stay on the relay path, where the frame and the picture are the same thing.
Both generators share two gates through prvHkGenReady (:987): the
row’s enabled flag, and a report withheld until the subsystem has actually reported — asked of
the pool, not a private flag, so a modelled reboot silences the report automatically.
Consolidating required two additions to the mirror: the EPS’s rail currents, and the ADCS’s own
view of the platform mode (ADCS_PLAT_MODE, not SYS_MODE, this computer’s own belief).
7. Events
The event log xEvlog (:362) is a static ring of EVLOG_SLOTS (64)
occurrences. Each is logged and downlinked immediately as a PUS service-5 report at its
severity subtype; TC DHS EVENTS replays the whole log oldest-first.
The identifiers are not declared in this node. They live in
include/events.h as an X-macro catalog, on the same principle as
DP_PID_CATALOG: the DHS takes the EVT_* constants from it and egse_tm takes evtName() from
it, so flight code and ground tool cannot disagree about what 0x0020 is called;
tests/test_events.c asserts the round trip for every row. That header
is the list.
Severity is deliberately not in that catalog — it is the subtype the report is emitted with,
chosen where the event is raised, since the same occurrence can warrant different severities in
different circumstances. Severity and the two parameter octets are what this table adds. The
9-octet report body carries the occurrence time (prvPackEvent, :529),
so a replayed log keeps its original timing even though the replay packets are stamped when
emitted:
| Offset | Field |
|---|---|
| 0..1 | Event identifier |
| 2 | Severity (== the PUS subtype) |
| 3..6 | On-board time of the occurrence (CUC coarse) |
| 7..8 | Two event-specific parameter octets |
| Event | Severity | p0, p1 |
|---|---|---|
MODE_CHANGE | 1 info | new mode, — |
SAFING | 3 medium | SoC, — |
POWER_RECOVER | 1 info | SoC, — |
LOW_POWER | 3 medium | SoC, — |
CLOCK_SET | 1 info | —, — |
TC_REJECTED | 2 low | service, subtype |
FILE_STORED | 1 info | file id hi, lo |
FILE_DOWNLINK | 1 info | file id hi, lo |
OBC_SWITCH | 3 medium | old OBC, new OBC |
ACS_SWITCH | 3 medium | old board, new board |
TTC_SWITCH | 3 medium | old board, new board |
PARAM_SET | 1 info | count, — |
PARAM_DEFINE | 1 info | new pid hi, lo |
PARAM_DELETE | 1 info | count, — |
PARAM_NVRAM | 1 info | restored count, — |
THERM_LIMIT | 3 medium | battery °C, limit °C |
CFG_DISTRIB | 1 info | subsystem, row count |
CFG_READY | 1 info | subsystem, applied count |
CFG_FAILED | 2 low | subsystem, last NAK |
CFG_PROGRAM | — | reserved; declared, not yet raised |
SCHED_INSERT | 1 info | queue depth, inserted count |
SCHED_RELEASE | 1 info | released service, subtype |
SCHED_FAILED | 2 low | error code, service |
SCHED_FULL | 2 low | queue depth, refused count |
SPW_LINK_UP | 1 info | link state, — |
SPW_LINK_DOWN | 3 medium | link state, — |
XFER_START | 1 info | file id hi, lo |
XFER_DONE | 1 info | file id hi, lo |
XFER_FAIL | 3 medium | file id lo, ack status |
SPW_REJECT | 3 medium | offending address, high octets |
FILE_DELETED | 1 info | file id hi, lo |
FDIR_ADCS_TMO | 1 info | frozen heartbeat, miss count |
FDIR_BUS_UV | 4 high | bus volts, limit volts |
8. Autonomy
Four autonomous behaviours, all the DHS’s own judgement rather than relayed commands. Two run in the decision loop; the two FDIR monitors run in their own task and hand their recovery back to the decision loop to actuate.
Low-power safing
The EPS raises CMD_LOW_POWER ({ CMD_LOW_POWER, SoC, asserted }) below its soc_low_pct and
clears it above soc_recover_pct — the third octet lets the DHS learn the battery recovered, since
it holds no power model of its own. The DHS treats it as an edge, so a hysteretic EPS
re-announcing a held state doesn’t fill the event log.
While asserted the DHS safes the platform and refuses every non-SAFE mode telecommand with
INVALID_STATE. Recovery is a separate judgement: the DHS only resumes NOMINAL once
soc_pct > soc_resume_pct. Only an autonomous SAFE auto-recovers, gated by xPlat.autosafe —
a commanded SAFE, or the boot-time SAFE after a reboot, is held until the ground commands
otherwise.
Thermal limit
The battery temperature is compared against THERM_LIMIT_HI read from the pool, so a
TC[20,3] changes what the spacecraft does, not just a number it reports back. The alarm is
latched, raising one EVT_THERM_LIMIT rather than one per tick, and clears when the temperature
falls back.
FDIR — detection and recovery
prvFdirTask :4253 runs two monitors every fdir_period_ms (2 s).
The task detects and isolates; the decision loop recovers, because both recovery actions touch
state the FDIR task must not — prvObdbInvalidate writes the lock-free xObdbPeer[], and
prvSetMode may race the low-power arm’s own bus command — so each recovery is latched in xFdir
and actuated in prvOrchestrateTask, the same hand-off shape the ADCS uses for its reboot.
xFdir needs no mutex: every word has exactly one writer. adcs_reset_req is a monotonic
counter (a set/clear flag could lose an edge to the consumer clearing it); uv_safe is
level-written, only ever 1, so a lost write is simply re-issued next tick.
Both monitors arm on validity, not on a timer — a parameter never written is not one that
stopped being written, which keeps the watchdog quiet through boot and, since dpRebindVolatile
returns every read-only mirror to invalid, disarms both across a modelled reboot with no
reboot-specific code.
Subsystem heartbeat — the ADCS watchdog
The ADCS control task advances a counter once per cycle and the housekeeping task echoes it in the
eighth octet of ADCS_RPT_CTRL, mirrored as ADCS_HB (0x0341) — counting in the control task
while a different task transmits is what lets a stalled control loop be distinguished from a
merely quiet one.
If the value doesn’t change for FDIR_ADCS_TICKS consecutive samples the subsystem is identified
as COMM_TIMEOUT: EVT_FDIR_ADCS_TMO fires once per outage (not per tick); every ADCS mirror
identifier is marked invalid, which stops prvEmitAdcsHk building a fictional packet from
stale values (ADCS_HB itself stays valid — the frozen count is the evidence); and a warm
reset is requested, which the decision loop turns into prvObdbInvalidate(CAN_ADDR_ADCS),
re-pushing the config block on the same tick.
Recovery self-heals with no recovery code: every write from prvIngestHk re-stamps
DP_ST_VALID, so the first report after the ADCS returns is coherent rather than a packet of
zeroes. A TC[20,133] derived parameter with an ADCS source flips to DP_ST_STALE while the trip
stands.
Bus undervoltage
EPS_BUS_V is compared against FDIR_BUS_UV_V read from the pool, the same contract as the
thermal check. Below it for FDIR_UV_TICKS consecutive ticks the DHS raises EVT_FDIR_BUS_UV at
severity 4 (the platform’s first use of TM[5,4]) and latches a safing request the decision loop
turns into MODE_SAFE.
This is a backstop below the EPS’s SoC-based low-power alert: the shipped cell spans 26.0–29.4 V and the EPS asserts low power at 30 % SoC, so the 26.5 V default is only reached if that alert already failed.
The latch is sticky — only a ground mode command lifts it. The auto-recovery arm carries
&& !xFdir.uv_safe :4411: since prvSetMode no-ops when the mode
already matches, without the guard an undervoltage arriving during a low-power SAFE would leave no
trace, and the spacecraft would resume NOMINAL with the bus still under its limit. While the latch
stands, low-power safing cannot auto-recover until the ground clears it, and clearing it while
the fault persists re-trips it three ticks later.
Observability and control
| Identifier | Access | Meaning |
|---|---|---|
FDIR_ENABLED 0x0117 | RW, persisted | Master switch. Disabling holds the counters where they are rather than clearing them, so an operator silencing a monitor can still see what it had reached |
FDIR_ADCS_TICKS 0x0118 | RW, persisted | Unchanged heartbeats before COMM_TIMEOUT, 1..60 |
FDIR_BUS_UV_V 0x0119 | RW, persisted | Bus undervoltage limit in volts, 0..40 |
FDIR_UV_TICKS 0x011A | RW, persisted | Consecutive ticks under the limit before safing, 1..60 |
FDIR_STATUS 0x0155 | RO | 0x01 ADCS timeout, 0x02 bus undervoltage, 0x04 the ADCS has never reported so the watchdog is idle |
FDIR_ADCS_MISS 0x0156 | RO | Consecutive unchanged heartbeats |
FDIR_UV_MISS 0x0157 | RO | Consecutive ticks under the limit |
The four thresholds are ordinary read-write pool parameters, so TC[20,3] retunes them and the
non-volatile mirror carries the change across a reboot. They are deliberately not
DP_CFG_CATALOG rows: the on-board database is what the DHS distributes, and these govern a
decision the DHS makes at its own end. The miss counters are two numbers rather than one
OK/TRIPPED enum so an operator can see how close the monitor runs to its threshold.
9. Parameter management — the data pool
The DHS’s state would otherwise be scattered across three private tables: the platform mirror, the tunables loaded from the parameter file, and the housekeeping-collection table. The data pool gives all of it one addressable, typed, access-controlled face; PUS service 20 is the ground’s window.
The pool binds; it does not copy
Registration points an entry at the C field that already holds the value — &xPlat.soc_pct,
&xSys.verif_timeout_ms, &xHkTable[i].enabled — so a ground write lands in the field flight code
reads. obdb.md §1 is the model. To add a parameter:
one DP_PID_CATALOG row in include/datapool.h, one prvPoolReg call in prvRegisterPoolParams
(:2404). Do not add a config key per parameter — that’s exactly
what the binding exists to avoid.
Identifiers
A 16-bit identifier whose high octet is the producing subsystem’s CAN address, so provenance
reads at a glance the way an APID does: 0x01xx DHS, 0x02xx TTC, 0x03xx ADCS, 0x04xx EPS,
0x05xx PAYLOAD, 0x0Fxx the ground-defined block.
| Group | Range | Access |
|---|---|---|
| Platform state mirror | 0x0101–0x010A | read-only |
| DHS thresholds and cadences | 0x0110–0x0116 | read-write, ranged, persisted (ORCH_TICK read-only) |
| FDIR thresholds | 0x0117–0x011A | read-write, ranged, persisted |
| HK collection table | 0x0120–0x0127 | read-write, ranged, persisted |
| Database status | 0x0130–0x0131 | read-only |
| Workload and schedule statistics | 0x0140–0x014A | read-only (the three schedule counters also persisted) |
| Mission data link and the mass memory file partition | 0x014B–0x0154 | read-only |
| FDIR working state | 0x0155–0x0157 | read-only |
| Remote subsystem measurements | 0x03xx, 0x04xx, 0x05xx (low octet < 0x80) | read-only |
| The on-board database | low octet ≥ 0x80 in 0x02xx–0x05xx | read-write, ranged, persisted |
| Ground-defined | 0x0F00–0x0F1F | dynamic |
The measurement/database split is DP_PID_IS_CFG, and 0x80 is the current threshold —
obdb.md §2 owns that rule and why moving it again invalidates a stored
MRAM image.
Read-only entries start invalid and become valid only when a subsystem actually reports, so the
ground can always tell a real measurement from a subsystem that has never spoken — except the
DHS’s own facts (prvMarkOwnFactsValid, :2371), known from boot.
The wire
TM[20,2] opens with a chunk-control octet and a 2-octet record count, then that many 8-octet
records: identifier (2), type (1), status (1), value (4, right-aligned big-endian) — fixed-width
for cheap chunk arithmetic, at the cost of a few wasted octets on a small parameter. A request for
more parameters than one packet holds is answered in several chunks rather than truncated,
consistent with §5’s rule that an oversized packet is dropped rather than sent short.
| Subtype | Direction | Body |
|---|---|---|
20,1 report values | TC | N(2) then N × identifier(2), max 30 |
20,2 values report | TM | chunk(1), M(2) then M × record(8), max 29 |
20,3 set values | TC | N(2) then N × record(8), reserved octet zero |
20,133 define | TC | count(1)=1, id(2), type(1), op(1), nsrc(1), src0(2), src1(2), name(16) |
20,134 delete | TC | N(2) then N × identifier(2) |
Query versus mutation
TC[20,1] is a query: an unknown identifier is answered with an UNDEFINED record rather
than rejected, and the reply’s record count matches the request’s so correlation stays positional.
TC[20,3] is the opposite: every record is validated at acceptance (exists, write access,
exact type, in range), and any failure rejects the whole command with TM[1,2] INVALID_PARAMS
and nothing written — partial application is exactly what the two-stage model prevents. On success
the new values are echoed back as a TM[20,2], confirming what the spacecraft holds.
Ground-defined parameters
TC[20,133] defines a parameter in the dynamic block, free-standing or derived from one or
two existing parameters through a typed operator; TC[20,134] deletes one. The arena, operators
and derivation rule are obdb.md §4. Define and
delete sit at subtypes 133/134 since ECSS-E-ST-70-41C reserves 1..127 for its own 20,1/2/3.
TC DHS PARAMS (CMD_PARAM_DUMP) reports the whole catalog, static and ground-defined, as a run
of chunks — the parameter equivalent of an event-log replay.
The non-volatile mirror: parameters marked persistable are mirrored outside everything a
reboot wipes, so an operator’s retuning survives an OBC switch (the ground sees an NVRESTORE
flag); naming a file in [dhs] nvram_file persists it across simulator runs too. Nothing else
in the platform ever touches the disk. The telecommand path never writes the file itself, see
The one file-writing site.
10. The on-board database
The parameter file is read by one node. The DHS holds the mission’s parameter database on behalf of the whole spacecraft and distributes it over the bus; the other four subsystems never open the file.
Three layers
ROM (DP_CFG_CATALOG) → the modelled MRAM baseline (MOB1) → the operator’s non-volatile delta
(MNV1), each applied over the last; obdb.md §5
is the model. The two persistent layers land in the same field, so one rule keeps them apart: a
telecommand writes the delta, never the baseline — why prvReloadTunables is [dhs]-only and
prvDhsReboot must not reprogram from the file.
The MRAM image
prvObdbPack/prvObdbLoad (:2006) model an MRAM image, deliberately
the same shape as the parameter mirror — same header length, same 8-octet record, same CRC — so the
difference between the two is semantic rather than structural.
0..3 magic "MOB1"
4..5 format version
6..7 record count
8..11 database epoch
12..13 CRC-16-CCITT over the whole image with these two octets zeroed
14..15 reserved
16.. records, 8 octets: id(2) type(1) width(1) value(4, right-aligned BE)A record whose parameter has gone away or changed type is dropped on load — the catalog is the authority on what exists. An absent, short or CRC-failing image is non-fatal: the DHS programs the database from the parameter file instead, clamping every value into its declared range (a malformed INI value would otherwise distribute platform-wide rather than stay local).
Distribution
Each subsystem gets its own block, selected by the identifier’s high octet. An item carries only the low octet, so a typed parameter fits in a single 8-byte frame.
| Frame | dlc | Layout |
|---|---|---|
CMD_CFG_BEGIN (0x26) | 7 | epoch(4), item count(1), flags(1) |
CMD_CFG_ITEM (0x27) | 8 | sequence(1), pid low octet(1), type(1), value(4) |
CMD_CFG_END (0x28) | 8 | epoch(4), count(1), CRC-16(2) |
The checksum covers what the receiver will reconstruct, not the frames themselves, so a
duplicate or reordered item still checksums alike. The receiver (common/obdb.c, in every node)
stages the items, checks epoch, completeness and CRC, validates every parameter against its
declared type and range, and only then applies all of them, or none — answering Ready, or a
NAK: INCOMPLETE, CRC, EPOCH, PARAM, OVERFLOW.
Convergence
A block is re-sent until acknowledged at obdb_retry_ms; an acknowledged one is refreshed far
more slowly at obdb_refresh_ms, as a backstop — one rule covers a late boot, a missed frame, and
a cold reboot onto a redundant board alike. A subsystem that never answers is reported once as
EVT_CFG_FAILED, then retried quietly.
prvObdbTouch bumps the epoch whenever a telecommand changes a distributed parameter, so a remote
threshold retunes and re-distributes automatically; TC DHS CONFIG <node> forces it by hand.
The readiness mask (one bit per peer) rides in the DHS’s housekeeping. When it first reaches full,
the DHS announces the platform mode once — otherwise it would hold its boot mode while every
subsystem assumed NOMINAL (prvSetMode no-ops when unchanged), and convergence is the first point
the subsystems are known to be listening.
Board straps stay with the box
A node still reads its own log_level, TTC its spacecraft identifier, and the EPS which unit of
each redundant pair is active — facts about the hardware rather than mission parameters. See
obdb.md §7.
10.5. The mass memory file partition and service 23
The file partition (§5) is also the RMAP target window for the SpaceWire mission data link — the only region of this node another card can write, bounds-checked on every access — and PUS service 23 is the link’s control plane. Both are documented in full on that page, including the two copy routes, the repository namespace, and why a file in the payload’s repository reports a length of zero.
Service 23 is DHS-local, like services 20 and 11, for the same reason: a file name alone is twelve octets, too big for the eight-octet CAN-command translation path.
11. On-board time
The DHS is the clock master. On boot no absolute time exists, so every node’s clock free-runs
from zero on its own FreeRTOS tick and telemetry is flagged unsynchronized (the CUC “spacecraft
time reference status” nibble in each PUS TM header, surfaced on the ground as +Ns (unsynced)).
The ground sets it with one telecommand — TC DHS TIME NOW, PUS 9 / subtype 128. The DHS
latches its clock (obtSet), broadcasts it immediately, and raises EVT_CLOCK_SET. Thereafter
prvBroadcastTime (:1639) re-broadcasts CMD_SET_TIME every
time_sync_period_ms; the other four nodes discipline common/obt.c to it. The broadcast is
suppressed while the DHS is itself unsynchronized.
Time is carried everywhere in CCSDS CUC form: 32-bit coarse seconds from the CCSDS epoch
(1958-01-01) plus 16-bit fine sub-seconds, UTC. The epoch bridge to Unix time lives only at the
ground edge (egse_tm rendering, egse_tc TIME setting); on board, time is just a count of
seconds the DHS hands out.
12. Redundancy
All three cold-redundant pairs are owned by the EPS, which switches power;
eps.md §10 is the model. What is local to this node: there is one DHS
process, so “the active OBC” is EPS bookkeeping rather than a second binary, and subsystems only
ever address the logical DHS (0x1).
prvDhsReboot
The DHS detects the switch in prvIngestHk when EPS_RPT_REDUN reports an OBC other than the one
it believes hosts it, and models a cold boot of the backup (:609).
| Wiped | Survives |
|---|---|
| Mass memory, event log, TM sequence and message counters | The non-volatile parameter mirror and the on-board database — by definition not volatile |
| The pending verification table and the command schedule | The schedule’s statistics, via that mirror (The safe-guard mirror) |
| In-flight file reassembly | The [dhs] mission baseline, reloaded by prvReloadTunables, with dpRebindVolatile() re-applying stored values over it |
| The whole platform mirror and every ground-defined parameter | |
The clock (obtReset, back to unsynchronized) | |
| Every peer’s configured state — this computer has no record of who acknowledged what |
Read-only pool entries return to invalid, since their backing fields were just zeroed. The DHS
always comes up in SAFE, forced onto the bus directly rather than through prvSetMode (a no-op
if the mode already matches). Do not add auto-recovery that undoes this — only an autonomous
low-power SAFE auto-recovers. The active ADCS and TTC boards reset to CPU_ID_UNKNOWN rather than
zero, since asserting board A would make the first report from a spacecraft on B look like a switch
that never happened.
ADCS and TTC CPU boards
On an ADCS- or TTC-CPU switch the DHS does not reboot. It tracks xPlat.adcs_cpu /
xPlat.ttc_cpu, raises EVT_ACS_SWITCH / EVT_TTC_SWITCH, and marks that subsystem’s database
block stale so the next pass reconfigures it. The platform clock, mass memory and mode carry on.
13. Concurrency and invariants
Lock hierarchy — the pool mutex is a leaf
| Lock | Guards | Taken by |
|---|---|---|
xSsmmMutex | xTm + xSsmm + xEvlog | every path that builds a packet |
xVerifMutex | xVerif | dispatch (track, match) and orchestrate (sweep) |
xSchedMutex | xSched | dispatch (insert, delete, reset) and orchestrate (release) |
xSsmmFileMutex | ucSsmmFile + xSsmmFiles | the SpaceWire receive task (RMAP writes) and dispatch (service 23) |
| the pool’s own mutex | the parameter table and arena | dp* calls |
None of the five ever nests. A read path snapshots into a dp_sample_t[], releases the pool
lock, and only then builds a packet — holding the pool lock across prvEmitTm/prvRaiseEvent
(which take xSsmmMutex) would create the one cycle that matters. Verification reports are
likewise emitted after xVerifMutex is released, and the schedule copies its due entries out under
xSchedMutex, releases it, then reports — why prvSchedReport snapshots into fixed-width
records rather than copies of the (96-octet) entries.
One mutex covers the three telemetry structures because every path that builds a packet touches
all of them, keeping the sequence counters race-free across the five emitting tasks. xSsmmFileMutex
joins on the same terms and is the easiest to get wrong: the SpaceWire receive task holds it while
writing into the mass memory window and service 23 holds it while reading a file out, so every
service-23 path snapshots the file record (and, for a downlink, its octets) under it, releases, and
only then calls prvEmitTm.
Posix port hazards
The DHS is flight software and carries all of them — see porting.md. Its
heap budget is seven tasks (five in main(), canrx, spwrx) —
why the SSMM, event log, pending table, mass memory file partition and data-pool tables are all
static, and why the pool’s “dynamic” block is a statically sized arena with an allocation
bitmap rather than pvPortMalloc.
The one file-writing site
The NVRAM mirror and the MRAM image are the only files a node writes. dpNvramSync on the
telecommand path only marks the mirror dirty; both files are written from prvOrchestrateTask
on a cadence divider, never from the dispatch path, and reading them is safe only in main()
before the scheduler starts — getting this wrong starves the port, it doesn’t merely slow it.
14. Configuration reference
The [dhs] section of config/minscs.conf. Every key has a compiled-in default, so a missing key
— or the whole file — just leaves the built-in value in place and the platform still boots. Read by
prvReloadTunables (:4754), except log_level, which
prvLoadConfig (:4855) applies before the first banner —
prvReloadTunables runs again on a reboot and is [dhs]-only by design
(§10).
| Key | Default | Pool id | Meaning |
|---|---|---|---|
log_level | info | — | off…trace |
default_mode | NOMINAL | — | Boot mode. The shipped file sets SAFE |
hk_poll_period_ms | 5000 | — | Fallback HK poll cadence and quantum ceiling |
orchestrate_tick_ms | 1000 | ORCH_TICK (RO) | Decision-loop period; divisor of every cadence below |
time_sync_period_ms | 5000 | TIME_SYNC_PER | Clock re-broadcast cadence |
hk_tm_period_ms | 5000 | HK_TM_PERIOD | The DHS’s own HK cadence |
verif_timeout_ms | 3000 | VERIF_TIMEOUT | Wait for a completion ack before TM[1,8] |
soc_resume_pct | 50.0 | SOC_RESUME_PCT | SoC above which the DHS leaves an autonomous SAFE |
therm_limit_hi_c | 45.0 | THERM_LIMIT_HI | Battery over-temperature alarm |
sched_enabled | 1 | SCHED_ENABLED | Boot state of the command schedule; TC[11,1]/[11,2] move it |
sched_past_margin_s | 2 | — | Refuse an insert whose release time is already this far behind the clock |
fdir_period_ms | 2000 | — | How often the FDIR task samples its monitors |
fdir_enabled | 1 | FDIR_ENABLED | Master switch for both monitors |
fdir_adcs_ticks | 5 | FDIR_ADCS_TICKS | Unchanged ADCS heartbeats before COMM_TIMEOUT |
fdir_bus_uv_v | 26.5 | FDIR_BUS_UV_V | Bus undervoltage limit, volts |
fdir_uv_ticks | 3 | FDIR_UV_TICKS | Consecutive ticks under the limit before safing |
nvram_file | (empty) | — | Parameter mirror backing file; empty = memory only |
nvram_flush_period_ms | 10000 | — | How often the decision loop may write it |
obdb_file | (empty) | — | MRAM image backing file; empty = reprogram from the file each boot |
obdb_retry_ms | 5000 | — | Re-send an unacknowledged block |
obdb_refresh_ms | 60000 | — | Re-send to a ready subsystem, as a backstop |
hk_<sub>_enabled | 1 | HK_*_ON | Collect this subsystem: poll it and downlink it |
hk_<sub>_period_ms | hk_poll_period_ms | HK_*_PER | Poll cadence for that subsystem |
<sub> is adcs, eps, ttc or payload (the key names come from canAddrName).
nvram_file and obdb_file take a bare filename, resolved against the config file’s directory
by prvResolveCfgPath (:4729) — CFG_VALUE_LEN silently truncates a
long value, so a full absolute path would be cut off on the way in.
Structural constants that size static arrays — SSMM_SLOTS, EVLOG_SLOTS, VERIF_PENDING_SLOTS,
SCHED_SLOTS, SCHED_TC_BYTES, REASM_MAX_BYTES, OBDB_MAX_ROWS — stay compile-time, because
the heap budget is computed against them.
15. Known gaps and deliberate simplifications
- A payload file with sequence gaps is discarded, not re-requested (
prvHandleConsecutiveFramelogs"would re-request on a real mission") — the ISO-TP-style transport has no flow control by design. - A file larger than
REASM_MAX_BYTES(8192) is refused at the First Frame (:1156), and a new First Frame abandons an in-progress reassembly — one reassembly context, not one per source. EVT_CFG_PROGRAM(0x0014) is declared but never raised — programming the database from the file logs but does not emit an event.prvSetModeno-ops when the mode is unchanged, so a mode telecommand requesting the current mode reportsTM[1,7]without putting anything on the bus; the convergence-time announcement covers the boot case.- The DHS’s own housekeeping carries no other subsystem’s measurements, by design — every octet under APID 1 is a parameter this computer owns, at the cost that no single packet is a whole-spacecraft summary.
- The Posix port is explicitly not real-time. Scheduling is illustrative; the on-board clock is disciplined to whatever UTC the ground sets, not to a real-time guarantee.
ADCS_HBis an octet, so a frozen counter is not the only way to look dead. At the shipped 10 Hz rate consecutive samples are never equal, but a legalTC[20,3]retuningADCS_HK_PERcan put an exact multiple of the control period between reports, aliasing a healthy ADCS into a frozen count. A wider counter pushes the aliasing out rather than removing it; the honest fix is a monotone timestamp, more than this proof of concept needs.- FDIR recovery actions are compiled in, fixed in
prvFdirTaskrather than data the ground can rebind, and neither monitor is a PUS service 12 definition — service-12 shaped (parameter, threshold, persistence count, transition report) but the service is absent. Service 19 (event-action) is likewise absent. Also missing and FDIR-relevant: 5,5–5,8 (inhibiting a chattering monitor’s event reports) and service 15 (anomaly forensics — the SSMM ring is deliberately not modelled as it, and the event log is dumped by a service-8 opcode instead).