Firmware for the Pyro MK1B Rocket Flight Computer
🚀 Try the web interface — no hardware needed
- Dual Pyrotechnic Control - AP2192 power switches with overcurrent detection
- Four Firing Modes - Fallen distance, AGL altitude, descent speed, timed delay
- Fault Detection - Overcurrent monitoring and post-fire verification
- Fire-and-Forget Flight Log - 50Hz streaming CSV to LittleFS via
hal_log_sample(); ISR-driven async flush; non-blocking - Real-time Telemetry -
$PYRONMEA or JSON format via UART (non-blocking ISR TX ring), 10Hz in flight; event sentences at apogee, fire, landing - Pressure Sensing - MS5607-02BA03 or BMP280 (auto-detected)
- Altitude Calculation - Integer-only barometric formula
- Continuity Checking - ADC oversampling for pre-flight verification
- Web Interface - Live dashboard via USB network (192.168.7.1)
- OTA Firmware Updates - A/B bootloader with automatic rollback
- WASM Simulation - Full flight computer runs in browser with physics engine
- Status Beep Codes - Field-diagnosable error reporting
- Altitude Beep-out - Max altitude announced after landing (m/ft/ft100)
- Configurable Deployment - INI file configuration via USB or web
- Ground Test Interface - Safe pre-launch pyro test via serial jack
- MCU: Raspberry Pi Pico (RP2040)
- Pressure Sensor: MS5607-02BA03 or BMP280 (auto-detected)
- Pyro Control: AP2192 dual channel power switch (1.5A per channel)
- Storage: littlefs on Pico flash (984KB)
- Interfaces: USB, UART0, I2C0
- PAD_IDLE - Ground calibration, ascent detection (+10m in <1s)
- ASCENT - Boost/coast phase, 50Hz logging, apogee detection
- DESCENT - Pyro deployment, 50Hz logging, landing detection
- LANDED - Post-flight altitude beep-out; flight log finalized
Apogee is an event within ASCENT, not a separate state. Pyros arm only after vertical speed drops below 10 m/s.
| Mode | Description |
|---|---|
| fallen | Distance fallen from apogee (in configured units) |
| agl | Altitude above ground level (in configured units) |
| speed | Downward vertical speed threshold (configured units/second) |
| delay | Seconds after apogee event |
Edit config.ini on the USB drive (appears when connected to PC):
[pyro]
id=PYRO001
name=My Rocket
pyro1_mode=delay
pyro1_value=0
pyro2_mode=agl
pyro2_value=300
units=m
beep_mode=digitsPyro Modes:
- fallen: Distance fallen from apogee (in configured units)
- agl: Altitude above ground level (in configured units)
- speed: Downward vertical speed threshold (configured units/second)
- delay: Seconds after apogee event
Units: cm (centimeters), m (meters), ft (feet) - applies to pyro values and altitude reporting
Beep Mode: digits (each digit beeped) or hundreds (value / 100, then each digit)
Defaults: If config.ini is missing, defaults are created automatically.
Two-digit codes (1-5 beeps per digit):
- 100ms beep, 200ms pause within digit
- 300ms pause between digits
- 500ms pause between codes
- Repeats after 1 second
| Code | Meaning |
|---|---|
| 1-1 | All systems good ✓ |
| 2-1 | Pyro 1 open circuit |
| 2-2 | Pyro 1 short circuit |
| 2-3 | Pyro 1 fault during fire |
| 2-4 | Pyro 1 failed to open after fire |
| 3-1 | Pyro 2 open circuit |
| 3-2 | Pyro 2 short circuit |
| 3-3 | Pyro 2 fault during fire |
| 3-4 | Pyro 2 failed to open after fire |
| 4-1 | Pressure sensor failure |
| 4-2 | Filesystem failure |
| 4-3 | Pyro altitude setting exceeds sensor limit |
| 5-5 | Critical system failure |
- GPIO 0: UART0 TX (telemetry output)
- GPIO 1: UART0 RX
- GPIO 8: I2C0 SDA (pressure sensor) / Test input (active low, internal pull-up)
- GPIO 9: I2C0 SCL (pressure sensor)
- MS5607: GPIO 10 (SDA), GPIO 7 (SCL)
- BMP280: GPIO 6 (SDA), GPIO 7 (SCL)
- Auto-detected at boot with I2C pin release between attempts
- GPIO 15: Common enable (master switch)
- GPIO 21: Pyro 1 enable (AP2192 EN1)
- GPIO 22: Pyro 2 enable (AP2192 EN2)
- GPIO 17: Pyro 1 fault (AP2192 FLAG1, active low)
- GPIO 18: Pyro 2 fault (AP2192 FLAG2, active low)
- GPIO 26 (ADC0): Pyro 1 continuity sense
- GPIO 27 (ADC1): Pyro 2 continuity sense
- GPIO 25: Onboard LED (status)
- GPIO 16: Buzzer (GPIO on/off, external circuit produces tone)
The primary in-flight record is written by hal_log_sample() (v2-9) to flight_log.csv in LittleFS — one CSV line per pressure sample from launch to landing.
- Primary log:
flight_log.csvin LittleFS, 50Hz during ASCENT/DESCENT, streaming append (ISR-flushed every 200ms) - Events: LAUNCH, ARMED, APOGEE, PYRO1_FIRE, PYRO2_FIRE, LANDING tagged inline as an extra CSV column
- Fields:
time_ms, pressure_pa, altitude_cm, state, thrust, event - RAM ring buffer: 64 entries × 16 bytes = 1KB (retained for launch-backdate and
/api/flight.csvHTTP endpoint) - CSV export:
/api/flight.csvservesflight_log.csvdirectly from LittleFS
Format: $PYRO NMEA sentences (default) or JSON (set telem_format=1 in config.ini), 115200 baud
$PYRO,seq,state,thrust,alt_cm,vel_cms,maxalt_cm,press_pa,time_ms,flags_hex,p1adc,p2adc,batt,temp*XX\r\n
- Rate: 10Hz during ASCENT/DESCENT, 1Hz during PAD_IDLE/LANDED
- State: 0=PAD_IDLE, 1=ASCENT, 2=DESCENT, 3=LANDED
- Flags: bit0=P1_CONT, bit1=P2_CONT, bit2=P1_FIRED, bit3=P2_FIRED, bit4=ARMED, bit5=APOGEE
- Checksum: XOR of all bytes between
$and*
Event sentences (NMEA format):
$PYRO_APO,flight_time_ms,max_alt_cm*XX\r\n
$PYRO_FIRE,channel,flight_time_ms,alt_cm*XX\r\n
$PYRO_LAND,flight_time_ms,max_alt_cm*XX\r\n
JSON format (set telem_format=1 in config.ini):
{"t":"state","seq":42,"state":1,"alt":1500,"vel":-200,"press":95000}
{"t":"apogee","alt":15240,"time":8500}
{"t":"fire","ch":1,"alt":15240,"time":8501}Example (NMEA):
$PYRO,42,1,1,150000,-200,150000,95000,8500,13,48,52,0,0*4A
From a PC or handset connected to UART0 RX (TRRS jack, 115200 baud), send plain-text commands while the device is in PAD_IDLE:
| Command | Description |
|---|---|
STATUS |
Report continuity state for both channels |
BEEP STATUS |
Replay last continuity beep code from buzzer |
BEEP ALT <n> |
Beep a specific altitude value (cm, for field calibration) |
ARM 1 or ARM 2 |
Arm the selected pyro channel for 3 seconds |
FIRE 1 or FIRE 2 |
Fire the armed channel (must ARM first within 3s) |
Responses are NMEA-style $GT,...*XX sentences with XOR checksum:
$GT,ARMED,1*3A
$GT,FIRED,1*38
$GT,ERR,not_pad_idle*XX
All commands are silently ignored if the device is not in PAD_IDLE (ASCENT/DESCENT/LANDED). The ARM→FIRE sequence has a 3-second auto-disarm timeout for safety.
- Pre-flight Continuity Check - ADC oversampling (256 samples, 16-bit effective)
- Two-part Pyro Safety - Common enable + channel enable
- AP2192 Protection - Hardware current limiting, thermal shutdown, short circuit protection
- Fault Monitoring - FLAG pins monitored during fire
- Post-fire Verification - ADC confirms pyro opened successfully
- Ground Test Safety - 3-second ARM→FIRE window, commands rejected in flight states
- Circuit: 100kΩ pull-up to 3.3V on each pyro output
- Method: ADC oversampling (256 samples) for 16-bit effective resolution
- Thresholds:
- Open: ADC > 60000 (3.3V)
- Good: ADC 1-100 (very low but non-zero)
- Short: ADC = 0 (exactly 0V)
The Pyro MK1B flight computer and physics engine compile to WebAssembly, enabling any web project to run closed-loop rocket flight simulations entirely in the browser. The WASM module contains the real firmware state machine, pyro logic, and telemetry — identical to hardware.
# Build the WASM module (requires Emscripten SDK)
./scripts/build_wasm.shCopy 3 files into your project:
docs/wasm/pyro.js ← Emscripten glue (generated)
docs/wasm/pyro.wasm ← WASM binary (generated)
docs/wasm/pyro-sim.js ← ES module API wrapper
Then in your JavaScript:
import { createPyroSim } from './wasm/pyro-sim.js';
const sim = await createPyroSim();
sim.init("pyro1_mode=delay\npyro1_value=0\nunits=ft\n");
sim.setContinuity(1, 50, true, false);
sim.setContinuity(2, 50, true, false);
sim.physics.init(1524); // 5000 ft target apogee
// Closed-loop: physics feeds pressure → flight computer fires pyros → physics deploys chutes
for (let t = 0; t <= 120000; t++) {
if (sim.pyroFireCount > 0 && sim.lastFireChannel === 1) sim.physics.deployDrogue();
if (t >= 2000) sim.physics.step((t - 2000) / 1000);
sim.setPressure(sim.physics.pressurePa);
sim.clearPyroFiring();
sim.tick(t);
if (sim.state === 7) break; // LANDED
}
console.log("Apogee:", sim.physics.apogeeM.toFixed(0), "m");| Document | Audience | Contents |
|---|---|---|
| sim/INTEGRATION.md | AI assistants & developers | Step-by-step integration guide, full API reference, troubleshooting, code examples |
| sim/README.md | Developers | Architecture diagram, API tables, build instructions, C library usage |
| docs/sim.html | Anyone | Interactive browser demo — working example of WASM integration |
The simulation also works as a plain C library:
cc -I sim/ -I src/ sim/physics.c sim/main_sim.c sim/hal_sim.c \
src/flight_states.c src/telemetry_formatter.c src/buzzer.c src/config.c \
my_test.c -lm -o my_testSee sim/pyro_sim.h and sim/physics.h for the C API.
Requires Pico SDK 2.2.0 or later:
mkdir build && cd build
cmake -G Ninja ..
ninjaThis produces:
build/_deps/pico_fota_bootloader-build/pico_fota_bootloader.uf2— A/B bootloaderbuild/pyro_fw_c.uf2— application firmwarebuild/pyro_fw_c_fota_image.bin— OTA update image
0x000000 Bootloader 36 KB (pico_fota_bootloader)
0x009000 Info block 4 KB (swap flags, rollback state)
0x00A000 App Slot A 512 KB (active firmware)
0x08A000 App Slot B 512 KB (OTA download target)
0x10A000 LittleFS 984 KB (config, web files, flight data)
0x200000 End of flash
- Hold BOOTSEL, plug in USB
- Copy
pico_fota_bootloader.uf2to the Pico drive - Hold BOOTSEL again
- Copy
pyro_fw_c.uf2to the Pico drive - Upload web files:
./support/upload_www.sh
After the initial flash, use picotool for all subsequent flashing (no BOOTSEL button needed):
./support/flash_picotool.shThis forces BOOTSEL via the vendor reset interface, loads both bootloader and app, reboots, and waits for the network to come up.
For routine updates without reflashing the bootloader:
./support/upload_fw.shOr use the "Firmware Update" button in the web interface at http://pyro.local/.
The A/B bootloader (pico_fota_bootloader) provides:
- Safe updates — new firmware is written to the inactive slot while the device keeps running
- Automatic rollback — if new firmware doesn't call
pfb_firmware_commit(), the bootloader reverts on next reboot - No bricking — a failed or interrupted OTA leaves the current firmware intact
Connect the Pico via USB. It appears as a network adapter with DHCP.
- Address: http://pyro.local/ (or http://192.168.7.1/)
- Status tab: Live state, altitude, speed, pressure in configured units
- Config tab: Guided pyro editor with range warnings and tips
- Flight Data tab: Summary, CSV download, altitude graph
- Update tab: Firmware/web upload, GitHub release checker
- APIs: /api/status, /api/config, /api/reboot, /api/ota, /api/flight.csv (all CORS enabled)
Each tracker advertises a _pyro._tcp DNS-SD service via mDNS.
Single device: Browse to http://pyro.local/ — just works.
Multiple devices: A data collection server browses for _pyro._tcp to discover all attached trackers automatically.
A comprehensive test suite validates the device's network stack, HTTP server, mDNS, and picotool integration.
# Interactive mode — guided setup for new users
python3 support/test_network.py
# Quick test against direct IP
python3 support/test_network.py 192.168.7.1
# Full suite with UART monitoring and log file
python3 support/test_network.py --all --uart /dev/tty.usbmodem201202 --log test.log
# Analyze a log file from a remote user
python3 support/test_network.py --analyze test.logThe test suite features a live TUI with colored results, UART monitoring via pyserial, timestamped diagnostic logging, and automated failure analysis. See support/README.md for full documentation.
All development and deployment scripts are in the support/ directory:
| Script | Purpose |
|---|---|
support/test_network.py |
Network/API test suite with TUI |
support/flash_picotool.sh |
Flash via picotool (no BOOTSEL button) |
support/upload_fw.sh |
OTA firmware update (local build) |
support/upload_www.sh |
Upload web files |
support/update_from_release.py |
Update firmware from GitHub releases |
Releases are automated via GitHub Actions:
- Every push to main builds firmware and uploads artifacts
- Git tags (
v*) create a GitHub Release with downloadable binaries
To create a release:
# Set version in VERSION file
echo "2.0.0" > VERSION
git add VERSION && git commit -m "release: v2.0.0"
git tag v2.0.0
git push && git push --tagsGitHub Actions will build and publish pyro_fw_c.uf2, pyro_fw_c_fota_image.bin, and pico_fota_bootloader.uf2 as release assets.
Update a device to the latest release directly from GitHub:
# Check for updates
python3 support/update_from_release.py --check
# Update to latest
python3 support/update_from_release.py
# Update to specific version
python3 support/update_from_release.py --version 2.0.0
# Update device at specific address
python3 support/update_from_release.py --host 192.168.7.1The tool checks the device's current version, compares with GitHub releases, downloads the OTA binary, pushes it to the device, and verifies the update.
- Power on - System initializes
- Wait 3 seconds - Continuity check performed
- Listen for beeps - 1-1 = good, others = fault
- Connect USB - Open http://pyro.local/ to view status or edit config
- Arm system - Place in rocket
- Launch - Automatic detection and logging
- Retrieve - Download flight data from web interface
99 C tests, 22 web UI tests — all passing.
The firmware is feature-complete and fully implements the v2 autonomous I/O architecture. All v2 milestones are complete:
- v2-1/2: X-macro config system;
hal_config_load/save - v2-2b: Ground test serial command interface (
ground_test.c) - v2-3: CPU sleep (
hal_sleep_until_event/__wfe) - v2-4: Autonomous 50Hz pressure sampling (
async_task.h+hal_pressure_fifo_*) - v2-5: Telemetry formatter module (dual NMEA/JSON, event sentences)
- v2-7: Buzzer parallel state machine (
buzzer_play_code/altitude,async_task_t-driven, no main-loop involvement) - v2-8: Batch
flight_process_samples()at 50Hz - v2-9: Fire-and-forget
hal_log_sample()with ISR-flushed streaming LittleFS writer - v2-10: Non-blocking UART TX ring buffer (UART0 ISR, 512-byte SPSC queue)
See ARCHITECTURE_V2.md and STATUS.md for implementation details.
See SPECIFICATION.md for detailed requirements and implementation notes.
See LICENSE file for details.