Status: working — receiver decodes live Vaisala RS41 telemetry end-to-end (radio → frame parser → Reed-Solomon ECC → GPS → OLED + BLE + web UI). Verified against a real sonde at 403.500 MHz:
state=TRACKING, valid serial, lat/lon/alt accurate to GPS noise,rs_errs=0on clean codewords. Specification at docs/fsd/Stratos FSD.md. Companion app: BalloonHunter.
Every day, weather services launch hundreds of small radiosondes — single-use sensor packages dangling under helium balloons. Each one transmits its GPS position on 433 MHz as it ascends, drifts, bursts, and falls. To find one after it lands, you need a receiver: a box that listens to the right frequency, decodes the position, and feeds it to a phone you can carry into the field.
A good field receiver is small, runs on a battery, configures over a phone, and shows live data — local on its own screen, and remote on the phone's map.
Stratos is open-source firmware for a ~€20 LILYGO T3 V1.6 (TTGO LoRa32 V2.1_1.6) board. It turns that board into a self-contained, pocket-sized receiver for Vaisala RS41 radiosondes and feeds the decoded position to BalloonHunter, the companion iOS/Android app, over Bluetooth Low Energy.
Buy the board, flash this firmware, attach a 433 MHz antenna, open BalloonHunter — done.
Compatibility note. Stratos's BLE payload format is wire-compatible with the MySondyGo API v3.0 ASCII protocol, so other MySondyGo-aware apps connect without modification. This is a side effect of using a stable, documented protocol — not the project's reason for being.
- Tunes a single 433 MHz channel and decodes Vaisala RS41 radiosondes — full pipeline: SX1276 FSK RX → bit-reverse → PRBS de-whitening → Reed-Solomon ECC (RS(255, 231) over GF(2⁸)) → frame field extraction → ECEF→WGS84 conversion.
- Streams the decoded telemetry — serial, latitude, longitude, altitude, horizontal/vertical velocity, signal strength — over Bluetooth Low Energy to BalloonHunter (or any MySondyGo-aware app).
- Shows the same data locally on the on-board OLED with a 60-second sticky display so marginal-signal flicker doesn't reach the user.
- Exposes a configuration web page on its own WiFi access point. The receiver never joins a home network — it is its own AP.
- Saves all settings in NVS; recovers gracefully from a wiped or corrupted config.
- Updates its own firmware over WiFi by uploading a
.binfile from the browser (with automatic rollback if the new image fails to boot).
Limitations (v1, by design):
- One frequency at a time — no scanner or auto-search.
- RS41 sondes only. Hooks exist for M10, M20, DFM, PILOT, but no decoders are wired up yet.
- AP-only — no STA mode, no GitHub-pull OTA. Updates are operator-driven from a phone or laptop on the AP.
- No deep-sleep / battery optimisation.
- No outbound uploaders (no SondeHub, APRS-IS, MQTT).
╭───────────╮
│ balloon │ ╭────────────────────────────╮
│ ╱╲╱╲ │ │ LILYGO T3 V1.6 │
│ ▽ ◯ RS41 │ ESP32 + SX1276 + OLED │
╰───┊───────╯ │ │
│ 433 MHz ╭┤ SX1276 (FSK RX) │
│ ────────────────▶│ │
│└─▶ RS41 decoder │
│ │ │
│ ▼ │
│ state ─┬──▶ BLE NUS ──┐ │
│ │ (Stratos │ │
│ │ protocol) │ │
│ └──▶ HTTP UI ─┼─╮ │
│ OLED │ │ │
╰────────────────────────┴─┴─╯
│ │
┌───────────┘ │
│ │
▼ ▼
┌───────────┐ ┌──────────┐
│ phone │ │ browser │
│ (Balloon- │ │ (config) │
│ Hunter) │ │ │
└───────────┘ └──────────┘
The radio chip on the board listens to the sonde's transmission. Software on the ESP32 turns the bits into GPS coordinates. Those coordinates are notified over Bluetooth in the format BalloonHunter expects. The OLED on the board shows the same data locally; a web page on the same WiFi network is there for configuration.
| Item | Notes |
|---|---|
| LILYGO T3 V1.6 (TTGO LoRa32 V2.1_1.6) | 433 MHz variant. Identifiable by the silkscreen LoRa32 V2.1 1.6. ~€20 from LilyGO direct or major resellers. The board includes the ESP32, SX1276 radio, 0.96″ SSD1306 OLED, USB-C, and 18650 battery holder. Pinout reference: LilyGO utilities.h. |
| 433 MHz antenna | λ/4 (~17 cm whip) or λ/2, SMA connector. Any cheap dipole works for nearby sondes; a tuned antenna helps at distance. |
| USB cable | For flashing and primary power. |
| 18650 Li-Ion cell (optional) | Slots into the on-board holder for portable use. |
- ESP-IDF v5.5 or newer (install guide)
- A serial port to the board (
/dev/ttyUSB0,/dev/ttyACM0, or similar)
. $IDF_PATH/export.sh
idf.py set-target esp32
idf.py build
idf.py -p /dev/ttyUSB0 flash monitorOn power-up the device opens an open WiFi access point named Stratos-XXXX (last 4 hex of its MAC) on 192.168.4.1. Connect a phone or laptop, browse to http://192.168.4.1 (the captive portal redirects there automatically), and set the listen frequency, sonde type, and battery calibration. Save → done.
The receiver does not join any other WiFi network.
After provisioning, configuration is reachable two ways:
- Web page at
http://192.168.4.1after joining the device's AP from your phone or laptop. - BalloonHunter app — connect via Bluetooth and use the app's frequency/sonde-type controls. The device honours the API v3.0 commands documented in
docs/fsd/Stratos FSD.md§6.
Long-press the on-board button (≥ 5 s) to factory-reset and re-enter provisioning mode.
OTA is operator-driven and AP-only:
- Power up the receiver and join its AP.
- Browse to
http://192.168.4.1. - Click Upload .bin, pick the new
stratos.bin, wait for the green checkmark. - The device verifies the image, swaps OTA slots, reboots, and rolls back automatically if the new firmware fails to boot.
There is no STA mode, no GitHub-pull update path, and no auto-update.
| Symptom | Likely cause | Fix |
|---|---|---|
OLED stays blank, log shows no SSD1306 ACK |
I²C bus issue (loose pin, wrong board variant) | Check SDA=21, SCL=22 are connected; confirm the board is a T3 V1.6 (this firmware does not support older T3 V1.3 pinout). |
rst:0x1 (POWERON_RESET) reboot loop right after boot |
Board profile lists a non-existent OLED reset pin | Confirm oled_rst is GPIO_NUM_NC in components/board_profile/src/board_profile.c. T3 V1.6 has no OLED reset wire. |
| BalloonHunter sees the device but no telemetry | Frequency mis-set; antenna disconnected; no sonde in range | Open the web UI, verify frequency matches a known active sonde launch. RSSI on the OLED status screen confirms signal. |
state stays NAME_ONLY even with strong signal — lat/lon = 0 and rs_errs = -1 |
RS41 frames being truncated/spliced by the radio driver | Confirm SX1276 PAYLOAD_LEN is 312 (9-bit form: PACKET_CONFIG2[2:0] = 1, PAYLOAD_LEN = 0x38) and the FIFO drain task polls every ~30 ms (the 64-byte FIFO fills in ~107 ms at 4800 bps). |
state = TRACKING but h_kmh shows tens of km/h on a stationary sonde |
Velocity unit assumed cm/s when it should be mm/s | Verified empirically: RS41 ECEF velocity is mm/s. Already fixed in decoder_rs41.c. |
Browser upload of .bin fails |
Image too large, or wrong target | The OTA slot is 1.5 MB. Confirm you're uploading the stratos.bin for ESP32 (not a different target). |
Image hash failed - image is corrupt after a flash |
esptool's "Hash of data verified" can pass while one byte is unwritten if you use erase=1 then a partial write right after |
idf.py fullclean && idf.py build then full chip erase via esptool, then re-flash with erase=1. Single missing byte in the SHA256 trailer is enough to trigger this. |
.
├── CMakeLists.txt top-level ESP-IDF project (project name: stratos)
├── partitions.csv 4 MB layout: nvs, phy_init, otadata, ota_0, ota_1
├── sdkconfig.defaults NimBLE on, custom partition, 60s task WDT
├── main/
│ ├── app_main.c boot sequence, component wiring
│ └── CMakeLists.txt
├── components/
│ ├── board_profile/ per-board pin/feature table
│ ├── config_store/ NVS-backed configuration
│ ├── platform_common/ version, watchdog, reboot, heap monitor
│ ├── wifi_manager/ SoftAP setup
│ ├── http_ui/ web UI, REST API, OTA
│ ├── ota/ OTA receive + verify
│ ├── oled_ui/ SSD1306 driver (i2c_master) + render loop
│ ├── button/ short/long press handler
│ ├── battery/ ADC calibration, percentage curves
│ ├── ble_nus/ NimBLE host, NUS service
│ ├── stratos_codec/ BLE wire-format codec (MySondyGo v3.0 wire-compatible)
│ ├── rf_sx1276/ FSK RX driver for Semtech SX1276
│ ├── decoder_core/ per-sonde-type decoder vtable
│ ├── decoder_rs41/ Vaisala RS41 frame decoder
│ ├── sonde_state/ decoded-frame state machine
│ └── sonde_types/ shared types and event base
├── docs/fsd/Stratos FSD.md canonical specification
├── LICENSE GPL-2.0
└── README.md
| Concern | Choice |
|---|---|
| RTOS / framework | ESP-IDF v5.5 (FreeRTOS). No Arduino-core compatibility shim. |
| Radio | Semtech SX1276 in raw FSK continuous-RX mode (not LoRa). Custom SPI driver, sync-word matching, FIFO drain on DIO0 interrupt. |
| Decoder | Original RS41 parser + Reed-Solomon ECC. Two interleaved RS(255, 231) codewords over GF(2⁸), primitive polynomial 0x11D, generator roots α⁰…α²³. Protocol facts (PRBS whitening, sync word, frame layout, ECEF→WGS84, mm/s velocity scale) per the publicly documented air protocol; rs1729/RS is the canonical reference. |
| Bluetooth | NimBLE host stack (built into ESP-IDF). Exposes the Nordic UART Service (NUS); Stratos's MySondyGo-v3.0-compatible ASCII protocol rides on top. |
| OLED | SSD1306 driver via the IDF i2c_master API (page-addressing mode). |
| Web | ESP-IDF esp_http_server. Single-page HTML embedded in firmware via EMBED_TXTFILES — no SPIFFS / LittleFS partition. |
| Persistence | ESP-IDF NVS (nvs_flash). |
| Updates | Browser-uploaded .bin → esp_ota_* + dual A/B partitions, automatic rollback on bad firmware. |
| Port | Service | Notes |
|---|---|---|
192.168.4.1:80 |
HTTP UI | Captive portal, REST API, OTA upload |
192.168.4.1:53/udp |
Captive DNS | Redirects all DNS lookups to the device |
| BLE NUS | 6E400001-…-… |
RX …0002 (write), TX …0003 (notify) |
Stratos's BLE payload format is identical to the MySondyGo API v3.0 ASCII protocol, so any MySondyGo-aware app (BalloonHunter included) parses the frames natively. The frame types 0/, 1/, 2/, 3/ and inbound o{...}o command grammar are documented in Stratos FSD §6.
- BalloonHunter — companion iOS/Android app (Andreas Spiess, HB9BLA). Stratos's primary BLE consumer.
rs1729/RS— public protocol reference for the Vaisala RS41 (and other radiosondes). Stratos's RS41 parser is original; no code is copied.dl9rdz/rdz_ttgo_sonde— sister project, cross-referenced for hardware quirks and SX1276 RS41 register values. Not a code dependency.- SondeHub — global radiosonde tracking network.
- Mirko Dalmonte (IZ4PNN) — author of the MySondyGo API v3.0 specification, which Stratos's BLE wire format implements (no code copied — implemented from the published spec).
GPL-2.0 — see LICENSE. Licensed under GPL-2.0 by choice; no third-party derived code is incorporated.