Skip to content

Software Loadout

Nate Gay edited this page Aug 4, 2026 · 36 revisions

⚠️ Work in Progress

If you find something confusing, please let @nateinaction know OR contribute clarification directly to the Wiki.

Need help with:

3.3: need a step-by-step guide to determine coil parameters

5.3: need a step-by-step guide on how to remove existing telemetry (is this only valid with the tlm archive component?)

5.5: need an expert review of OTA procedure AND explanation why you load into a slot but don't boot from it?

How to take a stock proves-core-reference and turn it into the flight build for a specific PROVES Kit satellite, then get that build and its supporting files onto the flight controller before delivery.


0. Before you start

You need, per the reference README:

  • The mission secrets bundle (see §3.1) — signing key + HMAC key

Everything else (fprime-venv, Zephyr workspace, SDK) is set up by make.


1. Fork the reference and create the mission repo

Each satellite gets its own fork. The fork keeps main tracking upstream and holds all mission-specific edits on a long-lived configuration branch, so upstream fixes can be merged in repeatedly without rebasing the mission config away.

  1. Fork Open-Source-Space-Foundation/proves-core-reference into the org, named after the satellite (e.g. david-cubesat).

  2. Clone it and wire up the upstream remote:

    git clone git@github.com:Open-Source-Space-Foundation/<your-cubesat>.git
    cd <your-cubesat>
    git remote add upstream git@github.com:Open-Source-Space-Foundation/proves-core-reference.git
  3. Create the configuration branch:

    git checkout -b configuration
    git push -u origin configuration
  4. Build the toolchain and the stock image once, to confirm the fork builds before you change anything:

    make

To pick up upstream changes later:

git fetch upstream
git checkout configuration
git merge upstream/main

2. Bring up the ground station

Do this before configuring, so you have a working GDS to verify each change against.

2.1 F´ GDS

The GDS must be able to sign uplinked commands, so build the framing plugin first (once per checkout):

make framer-plugin
make gds

To point the GDS at a specific serial device:

make gds UART_DEVICE=/dev/ttyACM0        # Linux
make gds UART_DEVICE=/dev/cu.usbmodem1201 # macOS

The GDS reads the HMAC key from PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h in your checkout. If that file does not match the key compiled into the board's image, every authenticated command will be rejected. See §3.1.

After connecting to a board, sync the anti-replay sequence number so the GDS and flight software agree:

make sync-sequence-number

3. Mission configuration (source changes on configuration)

Everything in this section is a code edit committed to the configuration branch. Work through it in order; the last two items (§3.10, §3.11) turn the radio and antenna live and should be done only when the satellite is otherwise flight-ready.

3.1 HMAC authentication key

Uplinked commands are authenticated per CCSDS 355.0-B-2 by TcSecurityDeframer. The key lives in a generated, git-ignored header:

PROVESFlightControllerReference/Components/TcSecurityDeframer/AuthDefaultKey.h

Two ways to populate it:

# Development: generate a random 16-byte key on this machine
make generate-auth-key

# Flight: install the mission key from the secrets bundle
make copy-secrets SECRETS_DIR=/path/to/mission-secrets

copy-secrets installs three files — keys/proves.pem and keys/proves.pub.pem (the MCUBoot image-signing keypair) and AuthDefaultKey.h (the command HMAC key).

Rules for flight:

  • Generate the mission HMAC key once, store it in the mission secrets store, and use copy-secrets everywhere after that. Never let make generate-auth-key silently mint a different key on an operator's laptop.
  • The same header must be present on every machine that will command the satellite, because the GDS framing plugin reads it.
  • keys/proves.pem must match the key the flashed MCUBoot bootloader was built with, or signed images will not boot.

3.2 Spacecraft orientation → ImuManager.fpp

The IMU is soldered to the board in a fixed orientation, but the board's orientation inside the chassis varies per build. Correct for it with the AXIS_ORIENTATION parameter in PROVESFlightControllerReference/Components/ImuManager/ImuManager.fpp:

param AXIS_ORIENTATION: AxisOrientation default AxisOrientation.STANDARD id 3

Allowed values: STANDARD, ROTATED_90_DEG_CW, ROTATED_90_DEG_CCW, ROTATED_180_DEG.

How to determine it: with the satellite assembled and the GDS connected, place the satellite so that it is sitting on its side (usually on a solar board) and the text of the top cap is right-side-up. Watch the IMU telemetry and pick the orientation that makes the acceleration of gravity read -9.8 on the Y axis. Another way to visualize this is to consider the satellite to be on an X/Y coordinate plane where X is the surface of the table and Y flows from the ceiling to the floor. In this scenario, gravity is a vector flowing through the top face of the satellite through the bottom face of the satellite.

This step is important if you do not have 4 equivalent solar boards to control the craft.

TODO: A short video should be added here.

3.3 Detumble wiring → topology.fpp

Which magnetorquer drives which body axis depends on how the face boards are populated and cabled. Setup your satellite the same way as in the previous step: sitting on its side (usually on a solar board) and the text of the top cap is right-side-up. In the GDS, turn on each face. If you're looking at the top cap and the right face lights turns on, it is the X+, left is X-, top is Y+ making the final face is Y-.

This step ensures that detumble is torquing the satellite in the correct direction; without it, detumble may introduce spin to your satellite.

These settings are found in the connections DetumbleManager block of PROVESFlightControllerReference/ReferenceDeployment/Top/topology.fpp:

detumbleManager.xPlusStart  -> drv2605Face0Manager.start
detumbleManager.xMinusStart -> drv2605Face1Manager.start
detumbleManager.yPlusStart  -> drv2605Face2Manager.start
detumbleManager.yMinusStart -> drv2605Face3Manager.start
detumbleManager.zMinusStart -> drv2605Face5Manager.start

…and the matching ...Stop block immediately below it. Both blocks must be remapped identically — a mismatch leaves a coil energized.

detumbleManager.xMinusStart -> drv2605Face3Manager.start
detumbleManager.yPlusStart  -> drv2605Face1Manager.start
detumbleManager.yMinusStart -> drv2605Face2Manager.start

Also set the coil electrical parameter defaults in PROVESFlightControllerReference/Components/DetumbleManager/DetumbleManager.fpp to match the coils actually flown — X_PLUS_RESISTANCE, X_MINUS_RESISTANCE, Y_PLUS_RESISTANCE, Y_MINUS_RESISTANCE (and voltage / length / width / shape if the coil geometry differs). If you do not know these numbers, detumbling may take longer or may not work at all.

# --- X+ Coil ---
param X_PLUS_VOLTAGE: F64 default 3.3 id 9
param X_PLUS_RESISTANCE: F64 default 57.2 id 10
param X_PLUS_LENGTH: F64 default 0.053 id 12
param X_PLUS_WIDTH: F64 default 0.045 id 13
param X_PLUS_SHAPE: CoilShape default CoilShape.RECTANGULAR id 33

TODO: Add a step-by-step guide to determine coil parameters.

3.4 Mode manager load switches → topology.fpp

When ModeManager exits safe mode it turns on the load switches wired to loadSwitchTurnOn. The reference ships with all of those connections commented out, marked TODO(ALLTEAMS) — meaning a stock build never powers a face back on after safe mode. You must wire the faces this satellite actually flies:

This step helps ensure your satellite conserves power when it's low on battery.

# In connections ModeManager, topology.fpp
# TODO(ALLTEAMS): Configure the faces you want to automatically turn on
# modeManager.loadSwitchTurnOn[0] -> face4LoadSwitch.turnOn
# ...
modeManager.loadSwitchTurnOn[0] -> face0LoadSwitch.turnOn
modeManager.loadSwitchTurnOn[1] -> face1LoadSwitch.turnOn
modeManager.loadSwitchTurnOn[2] -> face2LoadSwitch.turnOn
modeManager.loadSwitchTurnOn[3] -> face3LoadSwitch.turnOn
modeManager.loadSwitchTurnOn[4] -> face5LoadSwitch.turnOn

modeManager.loadSwitchTurnOff[0] -> face0LoadSwitch.turnOff
... (same five)

3.5 Per-mission values

Cal Poly — Taygeta

Item Value
Spacecraft ID (See instructions below) 4
Default RTC to proc time (RtcManager.fpp) Set default to TB_PROC_TIME

TXST — Maia

Item Value
Spacecraft ID (See instructions below) 5
Extra Merge the Mosaic component PR into the configuration branch

Set the Spacecraft ID

Set the assigned SCID for this satellite so ground stations can identify your satellite from other PROVES Kit-based satellites.

PROVESFlightControllerReference/project/config/ComCfg.fpp:

module ComCfg {
    @ Spacecraft ID (10 bits) for CCSDS Data Link layer
    dictionary constant SpacecraftId = 0x0044
}

Remember to update the ground side to match the YAMCS adapter's SPACECRAFT_ID (decimal) and yamcs/yamcs-data/etc/yamcs.fprime-project.yaml.

3.6 Verify the authentication bypass allowlist

ProvesRouter accepts a small set of opcodes without authentication (so an unsynced ground station can still no-op the satellite and read back its sequence number). That allowlist is a hard-coded array in PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp:

static constexpr uint32_t kBypassOpCodes[] = {
    0x01000000,  //!< CdhCore.cmdDisp.CMD_NO_OP
    0x2100B000,  //!< ComCcsdsUart.tcSecurityDeframer.GET_SEQ_NUM
    0x2200B000,  //!< ComCcsdsLora.tcSecurityDeframer.GET_SEQ_NUM
    0x2300B000,  //!< ComCcsdsSband.tcSecurityDeframer.GET_SEQ_NUM
    0x10065000,  //!< ReferenceDeployment.amateurRadio.TELL_JOKE
};

Opcodes may shift whenever component base IDs or the topology change, so after make build verify every entry still resolves to the command its comment claims. Run this from the repo root:

python3 - <<'PY'
import json, re
dict_path = "build-artifacts/zephyr/fprime-zephyr-deployment/dict/ReferenceDeploymentTopologyDictionary.json"
src_path = "PROVESFlightControllerReference/Components/ProvesRouter/Bypasser.cpp"
d = json.load(open(dict_path))
want = {int(m, 16) for m in re.findall(r"0x([0-9A-Fa-f]{8}),", open(src_path).read())}
byop = {c["opcode"]: c["name"] for c in d["commands"] if c["opcode"] in want}
for op in sorted(want):
    print(f"0x{op:08X}  {byop.get(op, '*** NOT IN DICTIONARY ***')}")
PY

Any *** NOT IN DICTIONARY *** line is either a stale opcode (fix Bypasser.cpp) or a component that is not in this build (e.g. S-band is not compiled into every deployment — confirm which before deciding it is harmless). A stale entry is a security-relevant defect: it either silently disables a needed bypass, or leaves an unauthenticated hole pointing at whatever command inherited that opcode.

3.7 Radio downlink delay

When multiple PROVES satellites deploy into the same orbit on the same frequency allocation, set ComDelay so they do not transmit on top of each other.

How it works. ComDelay sits between the radio's flow control and the framer. It counts rate-group ticks and releases a "ready to send" token only once every DIVIDER + 1 ticks, holding the downlink off in between:

period = (DIVIDER + 1) x tick interval

There are two instances, on different rate groups:

Instance Rate group Divider Period
downlinkDelay 10 Hz 299 30 s
telemetryDelay 1 Hz 29 30 s

Why it separates satellites. Nothing synchronizes the constellation — each board counts ticks from its own boot on its own crystal. If two satellites run the same cadence, then once they drift into alignment they stay aligned for hours, because only crystal error pulls them apart again. Give them cadences that are not simple multiples of one another and any overlap drifts back out within a cycle or two instead of beating in lockstep.

Because the period is DIVIDER + 1 ticks, the value that must be distinct and co-prime across the pod is DIVIDER + 1, not the divider itself. So pick a prime and subtract one. Near 30 s on the 10 Hz group:

DIVIDER Period ticks (prime) Period
292 293 29.3 s
306 307 30.7 s
310 311 31.1 s
312 313 31.3 s
316 317 31.7 s

This spreads collisions out and shares them fairly; it does not make them rare. Collision frequency is driven by how much airtime each satellite uses, so keep relying on retransmission.

The ComDelay divides the 1 Hz rate group to set the beacon/downlink cadence. PROVESFlightControllerReference/Components/ComDelay/ComDelay.fpp:

constant DEFAULT_DIVIDER = 299 # On a 1Hz input, outputs every ~30s

There are two dividers in play, and the startup sequence sets both at runtime:

R00:00:00 ReferenceDeployment.downlinkDelay.DIVIDER_PRM_SET, 299
R00:00:00 ReferenceDeployment.telemetryDelay.DIVIDER_PRM_SET, 299

Set the flight value in both places — the FPP default (what runs if the startup sequence never executes) and sequences/startup.seq — or the two will disagree.

3.8 Mission-specific extras

Anything else this satellite needs: payload components merged in, camera configuration, additional sequences. Keep these as separate commits on configuration so upstream merges stay readable.

3.9 Enable hard-coded startup — flight build only

PROVESFlightControllerReference/Components/StartupManager/HardCodedStartup.h:

// 0 means no HardCodedStartup, 1 means HardCodedStartup is enabled
#define DEFAULT_STARTUP_VALUE 0

Set to 1 for flight. With it enabled, StartupManager counts down TRANSMIT_ENABLE_TICKS (default 2800 ticks at 1 Hz — the 45-minute quiescence plus 100 s margin), then asserts enableTransmit and logs HardcodedRadioEnable, turning the LoRa transmitter on independently of the startup sequence. That is the backstop that gets the satellite talking even if startup.bin is missing or fails.

Leave it at 0 for all ground testing so the radio cannot key up in the lab. This is the last software change before delivery.


4. Build and flash the flight image

make build

This produces, at the repo root:

Artifact Use
bootable.uf2 Copy to the board in UF2 bootloader mode
bootable.signed.hex Flash over SWD (make debug-install bootable.signed.hex)

Flash by putting the board into UF2 bootloader mode and copying:

cp bootable.uf2 /Volumes/RP2350     # macOS; use findmnt on Linux

Then reconnect the GDS and confirm the version telemetry matches what you just built.


5. Prepare the flight filesystem

With the flight image running and the GDS connected.

5.1 Format the filesystem

Clears every file left over from integration and testing — boot count, mode state, quiescence start, sequence number, old telemetry, stale antenna state:

ReferenceDeployment.fsFormat.FORMAT

⚠️ Destructive, the satellite will not be happy about this and will crash. The hardware watchdog will reboot the satellite after some time.

5.2 Load sequences

Sequences are text in sequences/, compiled to .bin and uplinked as files.

make sequence SEQ=startup     # -> sequences/startup.bin
./tools/bin/make-sequences     # or: compile every sequences/*.seq at once

Then uplink with the GDS file uplink, with the destination path the flight software expects. The startup sequence must land at /startup.bin (the STARTUP_SEQUENCE_FILE parameter of StartupManager); other sequences go where the operator will cmdSeq them from.

For uplink over the radio, slow the GDS down so packets do not overrun the transmitter:

fprime-gds --file-uplink-cooldown 0.8

See Ops Constraint #2 for the LoRa cooldown values.

Sequences shipped in the reference:

File Purpose
startup.seq Post-deploy startup: event filters, antenna deploy at T+45 min, LoRa config, exit safe mode, detumble AUTO for 3 h
enter_safe.seq / radio_enter_safe.seq Force safe mode
radio-fast.seq Switch LoRa to the fast link settings
throttle_amateurs.seq Reduce amateur-radio activity
lose_time.seq, your-face.seq, not-your-face.seq, camera_handler_1.seq Test / operations sequences

Review startup.seq against this satellite's mission before compiling it — the deploy delay, downlink divider (§3.8), LoRa data rate, and detumble duration are all mission decisions baked into that file.

To disable the startup sequence entirely, remove the file:

FileHandling.fileManager.RemoveFile, /startup.bin

5.3 Delete pre-deployment telemetry

Remove telemetry and data files accumulated during ground testing so the first downlink after deployment contains only flight data.

TODO Needs step-by-step guide

5.4 Arm the antenna deployer

Delete the deployer's state file and directory so the satellite deploys on orbit:

FileHandling.fileManager.RemoveFile,      /antenna/antenna_deployer.bin
FileHandling.fileManager.RemoveDirectory, /antenna

FORMAT in §5.1 already removes these; do this explicitly if you formatted earlier and have since run a deploy test. Verify with ListDirectory / that no antenna directory remains — this is the single check that decides whether the antenna deploys in orbit.

5.5 Preload the update image into the second bootloader slot

MCUBoot is built with two image slots so the satellite can be updated on orbit and roll back if the new image misbehaves.

TODO Review guide below.

Uplink the image

Uplink build-artifacts/zephyr.signed.bin via the GDS file uplink, slowed down so packets do not overrun the transmitter:

fprime-gds --file-uplink-cooldown 0.8

Compute the CRC

UPDATE_IMAGE_FROM takes the expected CRC32 as an argument and verifies the file against it, so you need this value before running the command. Use the repo's calculator, not an online one — F´ uses a specific CRC variant (Ops Recommendation #1):

./tools/bin/calculate-crc.py build-artifacts/zephyr.signed.bin

Run the update commands

Command names below are taken from the built dictionary (ReferenceDeploymentTopologyDictionary.json), not from the Update::Updater SDD — the SDD in lib/fprime-extras still documents the older PREPARE_IMAGE / UPDATE_IMAGE_FROM_FILE spellings and is out of date.

Order Command Arguments
1 Update.updater.PREPARE_UPDATE none
2 Update.updater.UPDATE_IMAGE_FROM file, crc32
3 Update.updater.CONFIGURE_NEXT_BOOT next = TEST
Update.updater.CONFIRM_UPDATE none — on-orbit only, not at delivery

PREPARE_UPDATE must come first: it erases the target slot, and the FPP annotation states "Users are expected to have run the PREPARE_UPDATE command prior to running [UPDATE_IMAGE_FROM]".

Note Ops Constraint #1: FSW freezes for ~5 s during PREPARE_UPDATE — the watchdog LED and event stream stop, then resume. Expected. Do not run it during anything delicate.

CONFIGURE_NEXT_BOOT, TEST boots the new image exactly once, with automatic reversion to the previous image on the following boot. CONFIRM_UPDATE is what makes it permanent — run it only after the new image has booted and been verified on orbit. Do not run it as part of the delivery loadout.

NEEDS CONFIRMATION: the checklist says "preload but do not use", yet also lists CONFIGURE_NEXT_BOOT, TEST as part of the preload. Those conflict — arming TEST means the satellite boots the preloaded image on its next power cycle. Confirm whether the delivered state should have CONFIGURE_NEXT_BOOT already set to TEST, or whether the image should sit in slot 1 untouched with CONFIGURE_NEXT_BOOT deferred to on-orbit operations. This document assumes the latter is safer.

NEEDS CONFIRMATION: the reference README's OTA notes mention trying regionnumber = 1 instead of 2. Confirm which slot the delivered satellite should have loaded.


6. Record these values at delivery

Capture these from GDS telemetry once the satellite is in its final flight state, and file them with the delivery paperwork. They are the baseline every on-orbit anomaly gets compared against.

Value Where to read it
Boot count StartupManager BootCount telemetry, or the GET_BOOT_COUNT command → CurrentBootCount event
Sequence number tcSecurityDeframer CurrentSequenceNumber telemetry, or GET_SEQ_NUMSequenceNumberGet event (per link: UART / LoRa / S-band)
Full file listing FileHandling.fileManager.ListDirectory, / (check the resulting events); also record FsSpace FreeSpace / TotalSpace
Idle power consumption ina219SysManager Voltage / Current / Power telemetry, with the satellite in its delivery quiescent state

Also record, for the record:

  • Git commit SHA of the configuration branch that was built
  • Spacecraft ID
  • Downlink divider value
  • Whether DEFAULT_STARTUP_VALUE is 1
  • Confirmation that no /antenna directory exists

8. Delivery checklist

  • Fork created, configuration branch pushed, upstream merged to a known-good commit
  • Mission HMAC key and signing key installed via make copy-secrets
  • AXIS_ORIENTATION set and verified against the assembled spacecraft
  • Detumble start and stop wiring remapped; coil resistances match flown coils
  • ModeManager load-switch turn-on connections uncommented for the flown faces
  • Spacecraft ID set in ComCfg.fpp (and matched on the ground side)
  • Bypass opcode allowlist verified against the built dictionary
  • Downlink divider set to a pod-unique prime, in both ComDelay.fpp and startup.seq
  • Amateur radio name resolved (§3.5)
  • DEFAULT_STARTUP_VALUE set to 1
  • make build clean; make check-console-disabled passes
  • Flight image flashed; version telemetry confirms it
  • Filesystem formatted
  • startup.bin uplinked to /startup.bin; other sequences loaded
  • No /antenna directory present
  • Update image (USP) preloaded into the second bootloader slot, CRC verified, CONFIRM_UPDATE not run
  • Delivery values recorded (§6)
  • PreFlight Testing complete

Provenance

This page was written against:

  • proves-core-reference main @ 451c5bcb ("Timebase paramter (#455)")
  • david-cubesat configuration @ 0d1ae5c, mission commits ceb925f, 87b9d1f, 6265d71, d553cfd
  • The built topology dictionary build-artifacts/zephyr/fprime-zephyr-deployment/dict/ReferenceDeploymentTopologyDictionary.json

Command names, parameter defaults, and file paths were taken from those sources rather than from component SDDs, several of which are stale (notably the Update::Updater SDD in lib/fprime-extras, which still documents PREPARE_IMAGE / UPDATE_IMAGE_FROM_FILE).

When main moves, re-check at minimum: the bypass opcode allowlist (§3.7), the Update.updater command names (§5.5), and the TODO(ALLTEAMS) markers in topology.fpp.