Skip to content

Firmware (ESP-IDF)

Firmware source code lives in main/ and is built with ESP-IDF for ESP32‑S3.

Responsibilities

  • Radio telemetry (aircraft → winch)
  • Sensor readout (IAS via MS4525DO)
  • OLED UI (device-local status and (winch) last received telemetry)
  • Web server
    • HTTPS on :443 (UI + REST API)
    • HTTP on :80 redirects to HTTPS
    • Details: HTTPS

Key folders (repo-relative)

  • main/web/: HTTP server, route registration, static file serving, REST handlers
  • main/sensors/: sensor drivers + abstractions
  • main/aircraft/: aircraft configuration and runtime state
  • main/radio/: SX1262 integration
  • main/display/: OLED rendering
  • main/proto/: telemetry protobuf + generated Nanopb sources

Radio (SX1262)

The Sub‑GHz link uses Semtech SX1262 via LibDriver under main/radio/. Aircraft TX bursts and winch continuous RX share the same centre frequency and TX power from NVS (/api/radio); the modem type is compile-time.

Modem backend (OWTS_RADIO_USE_LORA_BACKEND)

Defined in main/radio/owts_radio_settings.h:

Value PHY Notes
0 GFSK Default for bring-up; bitrate / RX bandwidth profiles in the same header (and optional NVS test blob for lab tuning).
1 LoRa SF, BW, CR, preamble, sync word, and CAD parameters in the same header.

Rebuild both ends (aircraft + winch) with the same setting. The application RF payload is always 1 byte length + Nanopb AircraftRadioDownlink; REST and OLED decode the same logical telemetry regardless of PHY.

Telemetry framing: full vs delta

main/proto/owts_aircraft_telemetry.proto defines AircraftRadioDownlink as a oneof:

  • full (AircraftTelemetryFull): registration, aircraft type, \(v_{min}\), \(v_{opt}\), \(v_{max}\), \(v_{current}\), speed unit (identity + static band limits).
  • delta (AircraftTelemetryDelta): identity_hash (metadata digest for registration + type) and v_current only.

Full frames repeat on a fixed schedule so the winch can resynchronise after loss:

  • OWTS_RADIO_AIRCRAFT_TX_HZ — packets per second during a burst (must divide 1000; default 2 Hz in main/owts.h).
  • OWTS_RADIO_TELEMETRY_FULL_INTERVAL_S — wall-clock seconds between mandatory full frames; period in packets is OWTS_RADIO_TELEMETRY_FULL_PERIOD_PKTS = OWTS_RADIO_TELEMETRY_FULL_INTERVAL_S * OWTS_RADIO_AIRCRAFT_TX_HZ.

Between full frames, deltas reduce airtime (important especially for LoRa).

Pre-TX channel access (mandatory)

Always enabled for SX1262 aircraft TX. Runs before protobuf encode on each slot inside an armed burst; if the check says “busy”, the firmware skips that packet, waits one nominal period, and tries again on the next slot (ESP_LOGW).

LBT (GFSK path)listen-before-talk (energy): open a short RX window (OWTS_RADIO_LBT_RX_US, OWTS_RADIO_LBT_SETTLE_US in owts_radio_settings.h), sample instantaneous RSSI, return to standby. If RSSI is warmer (less negative dBm) than OWTS_RADIO_LBT_BUSY_RSSI_DBM, treat the channel as occupied. This is not format-specific: any in-band energy can block; change the defines for your noise floor / bench coupling.

CAD (LoRa path)channel activity detection: hardware looks for LoRa-like on-air energy over a configured symbol count (OWTS_RADIO_LORA_CAD_* in owts_radio_settings.h, programmed in owts_radio_configure_lora). Defer only when the modem reports activity detected. CAD timeouts or driver errors fail open (transmit anyway) with a warning so a broken CAD path does not silence the link.

Logging

Typical messages: [TX] LBT defer: …, [TX] CAD defer: …, and fail-open warnings for RSSI read / CAD timeout / set_rx failures.

Aircraft SRD duty / airtime accounting

In aircraft mode, optional enforcement of the selected SRD profile’s duty_cycle_max_ppm uses per-packet on-air intervals from sx1262_set_tx through TX_DONE, summed into per-calendar-second buckets over a 3600 s sliding window (RAM only since boot); the UI/API expose this as duty_airtime_est_ppm vs duty_cycle_max_ppm_profile. LBT/CAD time is not included. Set duty_cycle_lab_override (NVS, via /api/radio) to skip the gate for bench testing.

Two device modes

The same firmware image runs in one of two modes stored in NVS:

  • aircraft: reads sensors and transmits telemetry
  • winch: receives telemetry and serves it via HTTP + UI

See the API spec for mode-specific route behavior and reboot semantics: docs/assets/api/openapi.json.

In winch mode, an optional WS2812 LED strip can be used as a speed indicator (CONFIG_OWTS_WINCH_USE_WS2812). On boot, the strip runs a quick self-test RED → BLUE → GREEN (~500 ms each). The final green state remains on during boot/WiFi setup, then the strip is turned off right before continuous RX is armed.

Same firmware image, two roles

Treat mode (aircraft / winch) as a first-class runtime concern. The frontend and any tooling should assume that some endpoints are mode-specific or may intentionally return 404.

Aircraft runtime state machine (IAS / launch / landing)

OWTS keeps aircraft runtime logic DRY by maintaining a single authoritative runtime state snapshot:

  • IAS input (real sensor or simulator)
  • unit conversion + rounding (km/h ↔ knots for thresholds and UI)
  • launch/burst arming when \(IAS \ge v_{en}\)
  • landing confirmation: after a flight, require \(IAS < v_{en}\) continuously for landing_hold_s before the device considers the aircraft "landed" again
  • OLED runtime rendering of IAS / LANDED

Implementation lives in:

  • main/aircraft/owts_aircraft_state.{h,c}

High-level flow

flowchart TD
  subgraph iasSources["IAS sources"]
    ms4525[MS4525DO task] -->|"ias_update_kmh(kmh, valid)"| aircraftState[owts_aircraft_state]
    sim[IAS simulator task] -->|"ias_update_kmh(kmh, valid)"| aircraftState
  end

  aircraftState -->|snapshot| radioTx[radio_aircraft_tx_task]
  aircraftState -->|snapshot| sensorsRezero[ms4525_auto_rezero]

  aircraftState -->|"poll: burst_active + tick + arm"| launchLoop[aircraft_state_loop_task]
  aircraftState -->|snapshot| liveApi["GET /api/aircraft/live"]
  launchLoop --> oled[OLED_runtime_IAS_LANDED]
  launchLoop --> radioArm["arm_burst(t_send)"]

Landing semantics (boot vs confirmed landing)

The state machine preserves the existing UI semantics:

  • On boot: treat the device as landed until OWTS observes \(IAS \ge v_{en}\) at least once. This prevents "not yet flown" from looking like a spurious in-flight state.
  • After a flight: a landing is confirmed only when:
    • a burst ended (arms the landing-hold window), and
    • \(IAS < v_{en}\) for at least landing_hold_s.
  • One TX burst per launch: while IAS stays above v_en, OWTS does not chain further t_send windows until landing_hold_s below v_en completes (launch_tx_used in owts_aircraft_state).

Live IAS REST snapshot (GET /api/aircraft/live)

Registered only in aircraft mode. Returns the same runtime snapshot as the OLED (no NVS read on every poll beyond the state task’s periodic config refresh):

  • ias / ias_valid / speed_unit
  • is_landed, flight_phase (landed | airborne | landing_hold)
  • landing_hold_elapsed_s / landing_hold_s during landing_hold

Supports ETag + If-None-Match (304). During landing_hold, elapsed whole seconds are part of the ETag so clients see the countdown advance even when IAS stays at 0.

Handler: aircraft_live_get_handler in main/web/owts_web.c. OpenAPI: /api/aircraft/live.

IAS simulator (debug builds)

For development and demo purposes (e.g. exercising the launch/landing FSM without a pitot sensor), the firmware exposes a volatile IAS simulator:

  • Volatile: state is not persisted in NVS; after reboot the simulator is off by default.
  • Aircraft mode only: the REST endpoints are registered only when the device is in aircraft mode.
  • Secured: mutating calls follow the standard API auth rules (Bearer token required only when configured).

REST API

  • GET /api/aircraft/sim{ "enabled": true | false }
  • PUT /api/aircraft/sim → enable simulator (empty body)
  • PATCH /api/aircraft/sim → enable simulator (empty body; alias of PUT)
  • DELETE /api/aircraft/sim → disable simulator (empty body)

Frontend visibility

The web UI shows simulator controls only on debug firmware builds:

  • Device is in aircraft mode, and
  • /api/info.fw_version contains the substring DEBUG (case-insensitive)