C++17 flight software for a LEO spacecraft: attitude determination, attitude control, an autonomous mode director, onboard EPS energy policy, and FDIR (TMR on mode and flags, plus Health on sim-time dt and sensor ranges).
Basilisk is the dynamics environment used to exercise it. A thin software-in-the-loop (SIL) link carries sensors in and actuator commands out. The GNC logic lives in FlightSoftware/ — it does not copy attitude from the simulator.
Basilisk LEO plant → TCP SIL → FlightSoftware::step → RW torque back. Battery SOC and FSW mode come from the status packet, not from truth in the simulator.
ModeDirector::selectNextMode is the only place that changes mode. Each mode only computes torque. EpsManager owns the SOC deadband and can request Safe; FdirManager votes a triplicated ModeId and maps HealthReport to a sensor gate and/or force_safe (TMR fail-safe, bad dt, gyro out of range, or attitude stale). The default demo also holds Standby for 60 s of sim time (launcher separation) via run(boot_standby_s=60); the FSW test scenarios leave this at 0.
stateDiagram-v2
[*] --> Standby: boot
Standby --> Detumble: ||ω|| > 0.015 rad/s
Standby --> Pointing: ||ω|| < 0.005 and ref valid
Detumble --> Pointing: ||ω|| < 0.005 for 5 s and ref valid
Detumble --> Standby: rates settled, ref invalid
Pointing --> Standby: ref invalid
Standby --> Safe: SOC < 0.25
Detumble --> Safe: SOC < 0.25
Pointing --> Safe: SOC < 0.25
Safe --> Detumble: SOC > 0.35 and ||ω|| > 0.015
Safe --> Standby: SOC > 0.35 and ||ω|| ≤ 0.015
| Mode | What it flies | Why it exists |
|---|---|---|
| Standby | Zero RW / MTB | Idle; dispatcher into Detumble or Pointing |
| Detumble | Rate damping (Kp = 0) |
Kill tumble before pointing |
| Pointing | 2-axis PD, nadir (default) or sun | Mission attitude |
| Safe | Sun-point body +Z (SunReference) |
Energy: panels toward the sun when SOC is low |
Battery uses a level deadband in EpsManager (enter < 0.25, exit > 0.35), not a time hold. Rate damping uses a 5 s dwell under 0.005 rad/s so Pointing does not start on a single quiet sample. The ‖ω‖ > 0.015 Detumble entry applies from Standby (and Safe on exit), not from Pointing — the pointing controller can legitimately exceed that rate during acquisition. Pointing/sun references must be valid (nadir_valid or css_valid) before leaving Detumble or Standby for Pointing; if the reference drops in Pointing, the director returns to Standby. While boot Standby is held, neither EPS nor FDIR (force_safe from TMR or Health) can stay in Safe.
Each step():
- Health on the raw
SensorPacket(simdt, gyro/mag/CSS ranges, CSS incoherence). - FDIR majority-vote
ModeId(repair 2-of-3; Safe if no majority or illegal id). - FDIR
evaluate(HealthReport)→ sensor gate + extraforce_safe. - Estimate spacecraft state through the gate (a wild gyro is not copied into the filter).
- Health
noteAttitude(Safe ifattitude_validis false for 50 cycles). - EPS on SOC; TMR on
force_safe/allow_exit_safe/ref_ok. - Choose the next mode (optional boot Standby hold, then
force_safe, then the table above). exit/enterif the mode changed; FDIR commits the threeModeIdreplicas.- Run that mode’s controller.
active_modeis telemetry of the mode that flew, not a request.
flowchart TB
CSS[6-face CSS] --> CssWls[Body sun heading]
Gyro[Gyro] --> Filter
Mag[Magnetometer] --> TRIAD
CssWls --> TRIAD
Kepler[Kepler orbit] --> Nadir
Kepler --> Dipole[Centered dipole B_N]
SunN[Sun model] --> TRIAD
Dipole --> TRIAD
TRIAD --> Filter[Complementary filter + gyro bias]
Filter --> CBN[C_BN]
CBN --> Nadir
Filter --> Omega["ω = gyro − b̂"]
CssWls --> Ref[Sun / nadir reference]
Nadir --> Ref
Omega --> Modes[Mode director + PD]
Ref --> Modes
SOC[Battery SOC] --> EPS[EPS]
EPS --> Modes
Raw[Raw sensors] --> Health[Health]
Health --> FDIR[FDIR TMR + gate]
FDIR --> Filter
FDIR --> Modes
Modes --> RW[Reaction-wheel torque]
Onboard chain (plant truth is not on the sensor packet):
- CSS → unit sun in body
- Kepler two-body
r_BNfrom mission elements → nadir - Analytic sun direction and IGRF-2020 centered dipole in ECI
- TRIAD (sun + mag) seeds / corrects a complementary
C_BNfilter; biasb̂is estimated in sunlight and frozen in eclipse - PD on attitude error + bias-corrected rate; RW mapper saturates and applies the reaction-torque sign
Plant truth stays in Basilisk. The FSW downlinks estimated MRP (PACKET_FSW_ATTITUDE) and status (mode, validity flags, TMR, |b|, Health bits, last-TC ACK). The Python bridge compares FSW MRP to sc.sigma_BN and prints one SIL TM line every 5 s of sim time — not actuator commands.
SIL TM t=...s mode=Pointing Att=0.32 deg |b|=35.2 deg/h TMR=0 flags=0x0f css=1 nadir=1 att=1 triad=1 SOC=0.550 health=0x00 ack=0,0 nH=0
| Metric | Meaning | Typical demo (detumble_to_pointing, sunlight) |
|---|---|---|
| Att | Geodesic angle between filtered C_BN (as MRP) and plant sigma_BN |
< 0.5° after filter settle |
| |b| | Estimated gyro bias norm from the status packet | Converges to ~36 deg/h (planted IMU bias 0.01 °/s) |
| ‖ω‖ | Body rate from gyro − b̂ |
Detumble > 0.015 → < 0.005 rad/s before Pointing |
Control: after boot Standby (60 s in the default demo), expect Standby → Detumble → Pointing on the Mode= console line and Vizard FSW-mode bar. Pointing does not re-enter Detumble on rate alone — only Standby and Safe (on exit) use the 0.015 rad/s tumble threshold.
Estimation: in eclipse, css_valid drops but nadir_valid can remain (filter coasts on gyro); attitude error should not step-change solely because the sun sensors are dark (eclipse_coast.py).
Offline sensor plots (no Vizard): run with log_sensors=True, show_plots=True, real_time=False — see BasiliskSim/README.md.
Basilisk is the LEO SSO plant (sensors, wheels, eclipse, demo battery). SIL is only the link: 56-byte sensors in, 32-byte commands out (status, estimated MRP, RW, optional MTB).
flowchart LR
subgraph env [Basilisk plant]
Sensors[CSS / IMU / TAM]
Plant[Spacecraft + RW + MTB]
SOC[Battery SOC]
Viz[Vizard]
end
subgraph sil [SIL]
TCP[TCP :5557]
TC[TCP :5558]
Inj[FaultInjector]
end
subgraph fsw [Flight software]
Step["FlightSoftware::step"]
end
Sensors -->|SensorPacket 56 B| TCP --> Inj --> Step
TC -->|TelecommandPacket| Step
Step -->|status / MRP / RW / MTB| TCP
TCP --> Plant
TCP -->|ModeId| SOC
SOC --> Viz
Plant --> Viz
| Piece | Role |
|---|---|
FlightSoftware/ |
ADCS, mode director, EPS, Health FDIR (step once per packet) |
SIL/cpp/ |
TCP server (:5557 plant, :5558 TCs), packed protocol, CRC-32, optional fault injection |
BasiliskSim/ |
Plant, SOC, Vizard, Python bridge |
Ports: 5556 Vizard DirectComm, 5557 SIL duplex, 5558 operator TCs (SIL/tc_terminal.py).
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r BasiliskSim\requirements.txt
cmake -S SIL/cpp -B SIL/cpp/build
cmake --build SIL/cpp/build --config Release
ctest --test-dir SIL/cpp/build -C Release --output-on-failure
.\SIL\cpp\build\Release\sensor_receiver.exe
.\.venv\Scripts\python.exe .\BasiliskSim\scenarios\basic_orbit_vizard.pyOr .\run_sil.ps1 from the repo root (VIZARD_EXE optional). Details: FlightSoftware, SIL, BasiliskSim.
Reproducible FSW scenarios — thin scripts under BasiliskSim/scenarios/ with fixed initial conditions (rates, SOC, orbit phase) and docstrings describing the expected mode chain (Detumble, Pointing, Safe, eclipse coast). Same SIL startup; pick a case instead of the default demo, e.g. .\.venv\Scripts\python.exe .\BasiliskSim\scenarios\detumble_to_pointing.py. Full table and thresholds: BasiliskSim/README.md.
FlightSoftware/ ADCS + mode director + EPS + Health FDIR ← this is the product
SIL/cpp/ I/O adapter (TCP, CRC, packed structs, SIL-only fault injection)
BasiliskSim/ Dynamics environment (plant, SOC, Vizard)
tests/ C++ Health + director tests (`ctest`, target `fsw_tests`)
- Demo battery in Basilisk (mode +
|τ|load, small capacity so SOC moves in 1–2 orbits). Onboard EPS is SOC policy only. FDIR TMR protectsModeIdand director flags; Health rejects wilddt/ gyro / mag / CSS and can force Safe. SIL can inject a one-shotModeIdreplica bit-flip or SOC drop (ADCS_INJECT_MODE_AT/ADCS_INJECT_SOC_AT, orinjecton:5558; see SIL). Operator TCs (force/unforce/ …) are on:5558;SET_GAINS/SET_THRESHOLDSare no-ops. No gyro-vs-TRIAD check, wheel-saturation FDIR, or wall-clockstep()watchdog. - Detumble on reaction wheels only; magnetorquers are in the plant but unused.
- Pointing is two-axis (no yaw / ground-track constraint).
- Orbit / sun / mag models match this scenario, not GPS/TLE or full-order IGRF.
- No automated CSV vs plant truth. FDIR/Health unit tests:
ctest --test-dir SIL/cpp/build -C Release. SIL consoleSIL TMand optional offline plots (see Verification).
MIT — see LICENSE.
