UniSat is built so the same firmware, flight-software stack, and ground station can fly any of the following vehicles with nothing more than a template swap and a build-target choice:
| Family | Variants |
|---|---|
| CanSat | cansat_minimal, cansat_standard, cansat_advanced |
| CubeSat | 1U, 1.5U, 2U, 3U, 6U, 12U |
| Other | suborbital rocket payload, HAB, small drone, small rover |
This document describes the three collaborating registries that make that possible and the end-to-end flow from template to firmware.
flight-software/core/form_factors.pyAuthoritative source for physical envelopes:
min_kg, max_kg, nominal_kg)shape, dimensions_mm, volume_cm3)peak_w, average_w, battery_capacity_wh_typical, solar_capable)Each entry is a frozen dataclass with helpers such as check_mass() and
is_adcs_tier_supported().
flight-software/core/feature_flags.pyRegistry of optional capabilities (orbit_predictor, reaction_wheels,
descent_controller, parachute_pyro, s_band_radio, camera, …).
Each FeatureDescriptor declares:
At runtime resolve_flags(profile, form_factor, config) returns a
ResolvedFlags object that records, for every flag, whether it is
enabled / disabled and a human-readable reason. The decision
pipeline is deterministic:
explicit override → platform match → form-factor match
→ ADCS tier match
→ radio band match → default
An explicit features.<flag> = false in mission_config.json always
wins; an explicit true wins against the gates (the configurator
surfaces envelope conflicts separately so the operator is never
silently overruled).
flight-software/core/mission_types.pyBuilt-in MissionProfile objects for every supported mission type,
including a full phase graph (pre_launch → ascent → apogee → descent →
landed for CanSat; startup → deployment → detumbling → nominal →
science → comm_window → safe_mode → low_power for CubeSat LEO).
The size-specific CubeSat profiles (CUBESAT_1U … CUBESAT_12U) reuse
the LEO phase graph and differ only in telemetry rate, module list, and
the competition.form_factor key consumed by the form-factor
registry.
mission_templates/ (e.g. cubesat_3u.json)
and copy it to mission_config.json at the repository root.flight-software/flight_controller.py
parses the config, resolves the profile, and dynamically loads only
the modules declared in profile.core_modules + enabled optional
modules.Build the firmware with the matching profile target:
make target-cubesat-3u # → firmware/build-arm-cubesat-3u/
Each target defines -DMISSION_PROFILE_<NAME>=1 so
firmware/stm32/Core/Inc/mission_profile.h brings in exactly the
PROFILE_FEATURE_* macros for that vehicle.
ground-station/app.py also reads
mission_config.json; every page calls utils.profile_gate.page_applies()
and the irrelevant ones (orbit tracker for CanSat, image viewer
without a camera, ADCS monitor without an ADCS subsystem) auto-hide
with a short notice.| Template | MissionType | Form factor | Firmware target | Nominal mass |
|---|---|---|---|---|
cansat_minimal.json |
CANSAT_MINIMAL |
cansat_minimal |
target-cansat-minimal |
~130 g |
cansat_standard.json |
CANSAT_STANDARD |
cansat_standard |
target-cansat-standard |
~170 g |
cansat_advanced.json |
CANSAT_ADVANCED |
cansat_advanced |
target-cansat-advanced |
~250 g |
cubesat_1u.json |
CUBESAT_1U |
cubesat_1u |
target-cubesat-1u |
~0.42 kg |
cubesat_1_5u.json |
CUBESAT_1_5U |
cubesat_1_5u |
target-cubesat-1-5u |
~0.7 kg |
cubesat_2u.json |
CUBESAT_2U |
cubesat_2u |
target-cubesat-2u |
~1.2 kg |
cubesat_3u.json |
CUBESAT_3U |
cubesat_3u |
target-cubesat-3u |
~1.28 kg |
cubesat_6u.json |
CUBESAT_6U |
cubesat_6u |
target-cubesat-6u |
~7.1 kg |
cubesat_12u.json |
CUBESAT_12U |
cubesat_12u |
target-cubesat-12u |
~16 kg |
_register(FormFactor(...)) in flight-software/core/form_factors.py.MissionType entry and factory in
flight-software/core/mission_types.py.mission_templates/.hardware/bom/by_form_factor/.target-cubesat-* rules).firmware/stm32/Core/Inc/mission_profile.h with a
new MISSION_PROFILE_<NAME> block.Tests under flight-software/tests/test_form_factors.py,
test_feature_flags.py, and test_new_profiles.py should be extended
to cover the new entry.