Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions .github/workflows/merge-criteria.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,43 @@ jobs:
shell: pwsh
run: platformio run -d ${{ matrix.project }} --target compiledb

- name: Package versioned firmware
id: package
shell: python
working-directory: ./${{ matrix.project }}
env:
PROJECT: ${{ matrix.project }}
run: |
import os
from pathlib import Path
import re
import shutil
from platformio.project.config import ProjectConfig

project = os.environ["PROJECT"]
config = ProjectConfig("platformio.ini")
environment, = config.get("platformio", "default_envs") or list(config.envs())
flags = " ".join(config.get(f"env:{environment}", "build_flags"))
match = re.search(r'''(?:^|\s)-D\s*FIRMWARE_VERSION=['"]*([0-9A-Za-z.+-]+)['"]*(?=\s|$)''', flags)
if not match:
raise RuntimeError(f"Missing FIRMWARE_VERSION for {project}/{environment}")

name = f"{project}-{match.group(1)}"
build = Path(config.get("platformio", "build_dir")) / environment
destination = Path(os.environ["RUNNER_TEMP"]) / name
destination.mkdir(parents=True, exist_ok=True)
extensions = ["bin", "elf"] + (["UF2"] if project == "ground_station" else [])
for extension in extensions:
shutil.copy2(build / f"firmware.{extension}", destination / f"{name}.{extension}")
shutil.copy2(build / "compile_commands.json", destination / "compile_commands.json")

with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"name={name}\npath={destination.as_posix()}\n")

- name: Upload Artifacts
uses: actions/upload-artifact@v7
with:
name: artifacts_${{ matrix.project }}
path: |
./${{ matrix.project }}/.pio/build/**/firmware.UF2
./${{ matrix.project }}/.pio/build/**/firmware.bin
./${{ matrix.project }}/.pio/build/**/firmware.elf
./${{ matrix.project }}/.pio/build/**/compile_commands.json
name: ${{ steps.package.outputs.name }}
path: ${{ steps.package.outputs.path }}
if-no-files-found: error
retention-days: 90
6 changes: 3 additions & 3 deletions ground_station/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ The Sharp Memory LCD is 400x240 and a full refresh blocks the HMI task long enou
the existing debounce logic to observe it rather than refreshing again immediately.
- Keep the 15 ms button debounce unless hardware evidence justifies changing it. Add hold-to-repeat only where repeated
navigation is useful, such as long log lists.
- Bind settings behavior through `settings_button_action_e`, never by a fragile row number. `Enter Bootloader` must remain
the final General settings item; `USB Drive` must invoke USB storage, not bootloader entry.
- Bind settings behavior through `settings_button_action_e`, never by a fragile row number. `Update Firmware` must remain
the final System settings item; `USB Drive` must invoke USB storage, not bootloader entry.
- Keep simulator behavior aligned with production HMI behavior. The simulator is not permission to introduce a second,
independent implementation of a screen.
- Whenever making visual changes, run the relevant simulator scenarios and inspect the rendered screens for clipping,
Expand Down Expand Up @@ -133,7 +133,7 @@ For USB, storage, startup, task-stack, display-driver, or toolchain changes, tes

- immediate behavior after UF2 installation and behavior after a power cycle;
- display output (no snow), buttons, and absence of restart loops;
- Settings > USB Drive, leaving that screen, and the final Enter Bootloader item;
- Settings > System > USB Drive, leaving that screen, and the final Update Firmware item;
- entering the log list without a restart and seeing both current and legacy CSV names;
- USB connect/eject/reconnect, recording reclaim, and retained configuration/logs;
- both receiver modes, enabled-link LEDs, sensors, and recovery navigation when affected.
Expand Down
43 changes: 43 additions & 0 deletions ground_station/RADIO-UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Ground Station receiver firmware update

The Ground Station can install one production STM32G071 telemetry application on both internal receiver MCUs. It uses
the STM32 factory system-memory bootloader over each receiver's existing UART; the ordinary telemetry protocol and the
application flash address (`0x08000000`) are unchanged.

## Prepare and run an update

1. Build the production `telemetry` PlatformIO environment and take its `firmware.bin`. Keep its SHA-256 with the
release record as an informational artifact.
2. Connect the Ground Station USB drive, create `/telemetry_firmware`, and copy the raw `.bin` file into that directory.
Up to 64 `.bin` files are listed alphabetically; subdirectories are not scanned.
3. Safely eject the USB drive from the computer. Keep the Ground Station powered and connected to a stable supply.
4. On the Ground Station, open **Settings > System > Update Firmware > Radio Receivers**, select the file, review the
size and CRC32, and choose **Install**.
5. Do not disconnect power until both links report a verified application version.

The displayed CRC32 detects a file changing between selection and programming. A raw binary has no product identity,
expected version, signature, or downgrade metadata. The operator is responsible for selecting production telemetry
firmware intended for the Ground Station. Reinstalling the same version is allowed.

## Safety and recovery

The update is refused while a log is active, a radio is in test mode, or a recent radio packet reports an airborne
state. USB mass-storage ownership, logging, and both UARTs are handed over before programming. Link 1 is fully written,
read back, started, and version-checked before Link 2 is touched.

The updater checks the STM32 product ID, 128 KiB flash geometry, ROM revision B1-B4, and unprotected option bytes. It
erases individual 2 KiB pages, waits for the affected ROM revisions, writes in at most 256-byte aligned blocks, and
reads every byte back. The first eight vector-table bytes are written only after the rest of the image and its source
CRC are verified. It never changes option bytes or performs read/write unprotect.

An interrupted factory-ROM update has no rollback slot. Most interruptions leave the vector table erased and permit a
new ROM session, but an interruption during final vector activation can require ST-Link recovery. A link whose entry or
programming result is ambiguous is quarantined until Ground Station restart or repair. This is the unavoidable safety
limitation of using the factory ROM instead of a resident custom bootloader.

The bootloader-entry request is `CMD_BOOTLOADER`, a zero payload length, and the usual frame CRC8. The receiver rejects
requests with a nonzero payload length and acknowledges entry before jumping to the ROM bootloader.

Receiver applications predating the `CMD_BOOTLOADER` implementation need a one-time ST-Link installation of a
ROM-capable production telemetry build. Units provisioned with the earlier protected custom-loader proof of concept need
a separate provisioning procedure. Older Vega firmware remains compatible with the ROM-capable telemetry application.
86 changes: 86 additions & 0 deletions ground_station/SELF-TEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Ground Station self-test

Start on battery power with **USB disconnected**. Open **Settings > System > Self-Test**,
rest the Ground Station on the bench with access to the sky, and press **A: Start**.
Leave USB disconnected until the final USB drive check. Results stay on the screen; no reports
or CSV files are saved. Saved settings, flight logs and compass calibration are retained.

## Step 0 - Gyroscope calibration

Keep the Ground Station completely still on the bench. The test averages the three
raw gyro readings over at least five seconds and 200 fresh samples. Acceleration
must remain within 0.8-1.2 g in magnitude, and each raw gyro reading within +/-5 degrees/s.
An acceleration range above 0.1 g on any axis, a gyro range above 0.5 degrees/s,
invalid readings or a sampling gap over 100 ms restart the measurement. It fails
if a stable measurement cannot be collected within 30 seconds.

Successful offsets are saved and read back in the ESP32's NVS under `cats-gyro/bias-v1`.
They are applied to the gyro before compass/orientation processing and the subsequent
IMU checks, and are loaded on future starts. Calibration always uses raw readings,
so rerunning it does not subtract the previous offsets twice. Failed or cancelled
measurements retain the previous offsets. This is a stationary zero-rate correction,
not a temperature compensation or magnetometer calibration.

## Phase 1 - Ground Station

Runs automatically without test Vegas:

- GNSS: fresh positions and advancing time, with up to two minutes to acquire.
- IMU: plausible, stable acceleration and gyro readings while stationary.
- Magnetometer: detected sensor with valid raw readings.
- Battery: five seconds alongside the sensor checks, with every voltage reading
within 3.0-4.35 V and no active USB bus. No separate manual battery step is needed.
- Storage: write, reopen, verify and delete a 256-byte temporary file. USB storage
is reclaimed before accessing files; existing files are never overwritten.

## Phase 2 - Telemetry

Power two test Vegas with phrases **cats_test_1** and **cats_test_2**, then press
**A: Start**. The following checks run automatically:

- Each telemetry chip returns a fresh version reply. Any version is accepted.
- Dual reception, dual reception with phrases swapped, single mode, receiver 1
alone and receiver 2 alone. Each receiver is scored separately.
- Packet rate, link quality and reception gaps meet the bench limits. A failed
radio check retries once. Original receiver settings are restored afterward.
- Watch the receiver LEDs during reception; there is no separate manual LED step.

The limits are at least 8 packets/s over the observation window, mean link quality
at least 80%, SNR strictly greater than -5 dB in every received quality report, and
no gap longer than 1 second. Each active receiver needs at least one quality report.
Dual, swapped and single reception are measured for 15 seconds each; each receiver
alone is measured for 5 seconds, after a 2.5-second settling period.
These limits need checking against
known-good units in the fixture, especially with antennas absent. They do not
measure range. No Vega testing or pyro commands are sent.

## Phase 3 - Manual checks

**Every numbered check waits for a fresh A: Start press.** Waiting has no timeout;
measurements, patterns and test timers begin only after confirmation. Completing
one check opens the next confirmation screen. Holding A cannot start the next check.

| Check | What to do after pressing Start |
| --- | --- |
| 3.1 Sensor movement | Tilt and rotate around all three axes; completion is detected automatically. |
| 3.2 Buttons | Press and release all six buttons: Up, Down, Left, Right, A and B. The Start press does not count. |
| 3.3 Display | Inspect black, white and two checkerboard patterns, held for four seconds each. Then A = good, B = fault. |
| 3.4 USB drive | After Start, connect USB and open the CATS GS drive on the computer. The GS shares storage and requires a fresh successful host read within 30 seconds. Connection alone does not pass. |

Receiver settings are restored after telemetry testing. USB storage is shared during
the USB drive check and remains shared afterward. The battery check assumes the
operator starts with USB physically disconnected; bus state alone cannot prove
removal of USB power. Normal storage ownership still treats host suspension
separately from disconnection. The power checks do not measure charge current or
battery capacity.

Hold **B for two seconds** to cancel. Available Skip actions leave a check
**NOT TESTED**. The final screen shows **PASS** only if every check passed;
otherwise it shows **FAIL**, with individual results and reasons available using
up/down. Press A to rerun or B to return to Settings.

The simulator uses the production state machine and display renderer with fixture
inputs. Automated tests cover all confirmation gates, held buttons, timeouts,
receiver faults, storage faults, cancellation and USB sequencing. Physical sensors,
radio quality, LCD/LED appearance, battery operation and USB/storage behavior still
require verification on a Ground Station.
61 changes: 34 additions & 27 deletions ground_station/bootloader/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
# Ground Station TinyUF2 updater
# Ground Station WROVER bootloader

## Current release

The current bootloader artifacts come from Adafruit TinyUF2 `0.35.0`, tag
commit `8542b474ffa19c0dd66a8ccc99b1d6b8caff8d12`:
These artifacts are for the ESP32-S2 Saola WROVER module. They are unmodified
files from [Adafruit TinyUF2 0.35.0](https://github.com/adafruit/tinyuf2/releases/tag/0.35.0),
tag commit `8542b474ffa19c0dd66a8ccc99b1d6b8caff8d12`.

- `tinyuf2-espressif_saola_1_wroom-0.35.0.zip`, release SHA-256
`f4fc81da5c1c8f4378e5c79dd6685c3d87ebccd701a7e1ecc930595568fa689e`;
- `tinyuf2-espressif_saola_1_wrover-0.35.0.zip`, release SHA-256
`8df90daf8fca821f88304f6e9a94a61dec08b7468d923321668bffa679215d91`.

The checked-in `update-tinyuf2-*.uf2` files are the normal update artifacts for
Ground Stations that already run TinyUF2. Complete factory images are not kept
in this repository because they are not needed for normal application or
bootloader updates.
Source: [tinyuf2-espressif_saola_1_wrover-0.35.0.zip](https://github.com/adafruit/tinyuf2/releases/download/0.35.0/tinyuf2-espressif_saola_1_wrover-0.35.0.zip).
The ZIP SHA-256 matches the official release digest:
`8df90daf8fca821f88304f6e9a94a61dec08b7468d923321668bffa679215d91`.

## Which artifact to use

- Copy the matching `update-tinyuf2-*.uf2` to an existing TinyUF2 drive. It
temporarily replaces the Ground Station application with the updater,
updates only the factory UF2 application, and then opens TinyUF2 again.
- Reinstall the Ground Station application UF2 immediately afterward. The
updater does not replace the second-stage ESP bootloader or partition table.

Always select the WROOM or WROVER artifact that matches the hardware.

## Wired recovery

If a complete factory image is needed for wired recovery, download the matching
0.35.0 WROOM or WROVER release ZIP from
[Adafruit TinyUF2](https://github.com/adafruit/tinyuf2/releases/tag/0.35.0).
Use its `combined-ota.bin`; the plain `combined.bin` has a different partition
layout and is not compatible with the Ground Station application layout.
- `combined-ota-espressif_saola_1_wrover-0.35.0.bin` is the archive's
`combined-ota.bin`, renamed to identify the board and version. Flash it at
**`0x0`** through the ESP ROM bootloader for blank-board provisioning or wired
recovery. Then install the Ground Station application UF2. This factory image
replaces the bootloader, partition table and application regions, including
NVS settings; it is not a normal application update.
- `update-tinyuf2-espressif_saola_1_wrover-0.35.0.uf2` is the archive's
`update-tinyuf2.uf2`. Copy it to an existing TinyUF2 drive. It temporarily
replaces the Ground Station application with the updater, updates only the
factory UF2 application, and then opens TinyUF2 again. Reinstall the Ground
Station application UF2 immediately afterward. This updater does not replace
the second-stage ESP bootloader or partition table.

## Verification

- Factory BIN: 3,116,960 bytes; SHA-256
`12f95fd2814589422b94d943d1a8218da1887fff2b74987d96143d6dd145e51a`.
- Updater UF2: 613,888 bytes; SHA-256
`1bb601567b78adad890b68c4f99a6d572e57b3ada608988e11569798b6c0a9bb`.

The factory image's OTA partition table matches `../partitions_custom.csv`:
`ota_0` at `0x010000`, `ota_1` at `0x170000`, `uf2` at `0x2d0000`, and `ffat`
at `0x310000`. The plain `combined.bin` has a different partition layout and is
not compatible with the Ground Station application layout.

Both artifacts were verified against the release archive. Physical flashing
and bootloader recovery remain hardware checks.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion ground_station/gs-sim.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $true)]
[ValidateSet('setup', 'build', 'fatfs-test', 'serve', 'run', 'test')]
[ValidateSet('setup', 'build', 'browser-test', 'fatfs-test', 'fixture', 'serve', 'run', 'test')]
[string]$Command,
[Parameter(Position = 1)]
[string]$Scenario,
Expand Down
20 changes: 20 additions & 0 deletions ground_station/gs-wsl-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,27 @@ lint_project() {
fi
}

test_rom_bootloader() {
local test_binary="$build_dir/rom-bootloader-test"
mkdir -p "$build_dir"
g++ -std=c++20 -Wall -Wextra -Werror -Wno-sign-compare \
-I"$project_root/src" \
"$project_root/tests/rom_bootloader_test.cpp" \
"$project_root/src/update/rom_bootloader.cpp" \
"$project_root/src/telemetry/crc.cpp" \
-o "$test_binary"
"$test_binary"
}

test_self_test() {
local test_binary="$build_dir/self-test"
g++ -std=c++20 -Wall -Wextra -Werror -I"$project_root/src" \
"$project_root/tests/self_test.cpp" "$project_root/src/self_test.cpp" -o "$test_binary"
"$test_binary"
}
timed dependencies prepare_dependencies
timed rom-bootloader-test test_rom_bootloader
timed self-test test_self_test
timed build build_project
timed lint lint_project

Expand Down
2 changes: 1 addition & 1 deletion ground_station/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ lib_deps =

build_flags =
-fexceptions ; Required by the Nayuki C++ QR encoder
-DFIRMWARE_VERSION='"1.2.1"' ; Enter Firmware Version here
-DFIRMWARE_VERSION='"1.3.0"' ; Enter Firmware Version here
-DUSB_MANUFACTURER='"CATS"' ; USB Manufacturer string
-DUSB_PRODUCT='"CATS Ground Station"' ; USB Product String
-D USB_SERIAL="0" ; Enter Device Serial Number here
Expand Down
5 changes: 4 additions & 1 deletion ground_station/simulator/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ keeps deterministic fixture setup fast by advancing directly to the menu.
Telemetry injection accepts state, timestamp, coordinates, altitude, velocity,
voltage, continuity, connection/update state, LQ, RSSI, and SNR for either
link. Navigation injection accepts home/rocket coordinates, compass north,
azimuth, distance, and sensor/calibration values. Device injection accepts USB
pitch, roll, azimuth, distance, and sensor/calibration values. Device injection accepts USB
state, `usbStorageState` (`firmware`, `preparing`, `host`, or `fault`), plus
recorder write, finalize, and delete failure flags.

Expand All @@ -36,3 +36,6 @@ They also expose `usbStorageState` and `usbStorageMessage` for the automatic
idle-time USB Drive handoff flow.
During startup they also expose `startupElapsedMs` and `startupPhase`.
Configuration snapshots include `startupAnimation`, which defaults to `true`.
Browser snapshots also expose the injected home and rocket coordinates plus
`northRad`, `pitchRad`, and `rollRad` under `navigation`, so fixtures can verify
the location and orientation sent through the WebAssembly boundary.
Loading