Skip to content

Architecture Overview

Ravi Singh edited this page May 7, 2026 · 1 revision

Architecture Overview (v6.x)

For tinkerers who want to understand what's happening inside the firmware. This is the user-friendly version; for the locked architectural decisions and PR-by-PR build history, see docs/V6-ARCHITECTURE.md in the repo.

The big picture

┌─────────────┐   UART     ┌──────────────┐    queue    ┌──────────────┐    RMT     ┌──────────┐
│ HiLink      │  256 kbaud │ radar driver │   1-frame   │ motion       │   60 Hz    │ WS2812   │
│ LD2450      │ ─────────→ │ (ld2450.c)   │ ──────────→ │ Kalman / PI  │ ─────────→ │ strip    │
│ (24 GHz)    │            │              │             │ smoother     │            │          │
└─────────────┘            └──────────────┘             └──────────────┘            └──────────┘
                                                              │
                                                              │ 20 Hz
                                                              ▼
                                                       ┌──────────────┐    WS    ┌──────────┐
                                                       │ webui        │ ──────→  │ browser  │
                                                       │ (esp_http_   │          │          │
                                                       │  server)     │ ←──────  │ Preact   │
                                                       └──────────────┘   POST   │ UI       │
                                                                                 └──────────┘

One ESP32-C3 reads one LD2450 over UART, smooths the target stream through a Kalman filter, and drives one WS2812 strip via the RMT peripheral. A web server on port 80 serves the UI bundle and exposes JSON endpoints for every config knob plus a 20 Hz WebSocket telemetry stream.

That's the whole pipeline. There is no peer broadcast, no coordinator, no pairing, no fusion — those were v6.0 features dropped in the v6.x rewrite. See FAQ → Why was the mesh dropped for the reasoning.

FreeRTOS task model

Task Priority Stack Period Job
radar_task 6 4 KB UART event Read radar bytes → parse radar_frame_t
motion_task 5 4 KB 50 Hz Kalman / PI smoother → publishes target_t
led_render_task 4 6 KB 60 Hz Read smoothed target → framebuffer → WS2812
web_task 3 8 KB event HTTPD handler
tele_pump 3 3 KB 20 Hz Publish smoothed target to live WS clients
ws_bcast 3 4 KB 20 Hz Coalesce + emit JSON to all WS clients
status_led_task 2 2 KB pattern Drive onboard LED blink pattern

Higher priority preempts lower. The radar parse is highest because if it falls behind, the FreeRTOS UART driver's ring buffer overruns and we drop frames. LED render is next-highest because dropped frames there look like the strip is stuttering. Wi-Fi / HTTP / WebSocket are lower-priority — slow web requests can never starve the LED render loop, which was a recurring v5-era bug.

Components

The firmware is organized into IDF components under firmware/components/:

Component What it does
board Board profile struct + 4 profiles (C3 SuperMini validated; S3-Zero / classic ESP32 / C6 build-clean). Each profile declares an unsafe_pin_mask covering strapping pins, USB-Serial-JTAG D-/D+, and internal SPI flash.
settings NVS facade — typed accessors for every config namespace (sys, board, led, dist, motion, wifi, auth).
status_led Pattern-driven onboard LED in its own task. Patterns: BOOT, AP_MODE, STA_MODE, OTA, ERROR, PANIC.
button Single-button polling for the BOOT button. Long-press (3 s) reserved for v6.1 factory reset.
netmgr Wi-Fi STA/AP + mDNS + captive DNS portal. AP always-on; STA optional once configured.
auth PBKDF2-SHA256 admin password (250k rounds). Off until configured; UI banner nags until set.
webui esp_http_server with all /api/* routes plus root + captive-portal redirects. UI bundle embedded as ui.html.gz (gzipped to ~25 KB).
ota esp_https_ota-style wrapper with rollback. Bootloader marks new image as pending; firmware calls ota_mark_valid() on successful boot.
radar Driver registry (LD2450 + sim). Drivers compiled in unconditionally; one selected at runtime via NVS board.radar_kind.
motion Kalman smoother + legacy PI smoother. Single output: target_t (smoothed distance + direction + raw value).
led_engine 11 light modes via led_strip RMT. Reads smoothed target each frame; renders into a framebuffer; emits to the strip at 60 Hz.

Removed in v6.x: mesh/ (ESP-NOW peer broadcast) and topology/ (multi-device segment graph). See Migration from v6.0 and FAQ.

NVS schema

Namespace Keys
sys device_name
board id, led_pin, radar_rx, radar_tx, button, status_led, radar_kind
led count, brightness, r/g/b, mode, span, center_shift, trail, dir_light, bg_mode, effect_speed, effect_intensity
dist min_cm, max_cm
motion mode, enabled, response, look_ahead_ms, outlier_strength, pos_smooth, vel_smooth, predict, p_gain, i_gain
wifi ssid, pass, host (hostname), static_ip (optional)
auth pw_hash (PBKDF2-SHA256), pw_salt

NVS is journaled (atomic per-key writes), wear-levelled, and typed. Replaces v5's manual XOR-CRC EEPROM byte layout.

Removed in v6.x: mesh (peer blob, channel, fusion mode) and topo (kind, segments). Devices upgraded from v6.0 may still have orphan keys in those namespaces; the firmware never reads them, so they're harmless.

HTTP API

POST /api/auth/login         → cookie session
POST /api/auth/logout
POST /api/auth/password      → set/change PBKDF2 admin hash
WS   /api/live               → distance + raw + RSSI + heap + uptime @ 20 Hz
GET  /api/distance           → text/plain (legacy compatibility)
GET  /api/version            → app version + git sha + idf version + target
GET  /api/system             → enabled toggle + system metrics
POST /api/system             → mute/unmute LED output
GET  /api/wifi               → current Wi-Fi state
GET  /api/wifi/scan          → list nearby APs
POST /api/wifi               → save creds + reconnect
GET  /api/board/profiles     → board dropdown
POST /api/board              → save board id + pin overrides; reboot
GET  /api/radar/kinds        → ld2450 | sim
GET  /api/radar/diag         → driver id + byte/frame counters + last raw bytes
GET  /api/settings           → flat read of every NVS namespace
POST /api/settings           → batched write
POST /api/ota                → application/octet-stream firmware upload
POST /api/factory_reset
POST /api/reboot
GET  /api/ping               → health check

Removed in v6.x: /api/mesh, /api/mesh/identify, /api/topology. See Migration from v6.0 for what they did and why they're gone.

Partition layout (4 MB flash)

Offset Size Purpose
0x0000 32 KB Bootloader
0x8000 4 KB Partition table
0x9000 16 KB NVS
0xD000 8 KB OTA data (current slot pointer)
0x10000 1408 KB OTA app slot 0
0x170000 1408 KB OTA app slot 1
0x2D0000 960 KB LittleFS
0x3C0000 64 KB Coredump

App slot 1408 KB; current build is ~1158 KB → 20% headroom. Coredump is read out via the C3's USB-Serial-JTAG console after a panic.

Boot flow

1. ROM bootloader → 1st-stage bootloader from 0x0000
2. 2nd-stage bootloader picks current OTA slot
   → if OTA-pending and previous boot failed, fall back to other slot
3. app_main()
   3a. settings_init()         — bring up NVS
   3b. resolve board profile   — NVS-saved board.id wins; MCU-mismatch guard
   3c. apply pin overrides     — unsafe-pin guard rejects strapping/JTAG pins
   3d. status_led_init()       — boot pattern
   3e. auth_init()
   3f. netmgr_init()           — start AP always; STA if creds saved
   3g. webui_init()            — esp_http_server + register all routes
   3h. radar_init()            — UART setup + radar_task
   3i. motion_init()           — motion_task starts smoothing
   3j. led_engine_init()       — RMT + led_render_task
   3k. button_init()           — BOOT-button polling
   3l. xTaskCreate(tele_pump)  — start 20 Hz telemetry
   3m. ota_mark_valid()        — disarm bootloader rollback
   3n. status_led switches to AP_MODE or STA_MODE depending on Wi-Fi state
4. app_main returns; FreeRTOS owns the device.

Build sizes (latest C3 build)

Artifact Size
bootloader.bin 0x5330 (~21 KB)
partition-table.bin 3 KB
ambisense.bin 0x11aa70 (~1158 KB)
UI bundle (gzipped, embedded) ~25 KB
App slot free 20%

Where to read the source

The repo is laid out as:

firmware/
├── main/main.c                 # app_main entry, boot order
├── components/
│   ├── board/                  # board profiles
│   ├── settings/               # NVS facade
│   ├── status_led/
│   ├── button/
│   ├── auth/
│   ├── netmgr/                 # Wi-Fi + mDNS + captive DNS
│   ├── webui/                  # HTTP server + UI bundle
│   ├── ota/
│   ├── radar/                  # LD2450 + sim drivers
│   ├── motion/                 # Kalman + PI smoothers
│   └── led_engine/             # 11 modes via RMT
├── partitions.csv              # custom 4 MB layout
└── CMakeLists.txt              # IDF project root

frontend/
├── src/
│   ├── main.tsx                # App shell — sidebar + header
│   ├── screens.tsx             # All 6 screens (Live, LEDs, Motion, Hardware, Network, System)
│   ├── atoms.tsx               # Icon, Sparkline, sliders, charts
│   ├── components.tsx          # Card, Toggle, Field, etc
│   ├── led_preview.tsx         # Live LED preview rendering
│   ├── api.ts                  # fetch helpers + WS client
│   └── styles.css              # Design tokens
└── vite.config.ts              # Single-file bundle (vite-plugin-singlefile)

docs/
├── V6-ARCHITECTURE.md          # Locked architectural decisions
├── V6-ROADMAP.md               # PR record + v6.x roadmap
└── HARDWARE.md                 # Wiring, mounting, troubleshooting

legacy/
└── AmbiSense/                  # v5.x Arduino source preserved

Want to change something?

  • Add a new radar driver — drop a radar_<kind>.c into firmware/components/radar/ implementing size_t radar_<kind>_parse(const uint8_t*, size_t, radar_frame_t*), register in radar.c's k_drivers[], expose via /api/radar/kinds JSON.
  • Add an LED mode — extend the mode_* switch in firmware/components/led_engine/. Modes are render-time only; persistence is just a uint8_t in NVS.
  • Tweak motionfirmware/components/motion/motion_kalman.c has the v3 Kalman filter; the legacy PI smoother is in motion.c. UI knobs map to NVS keys; see handle_settings_post in webui.c for the JSON-key → NVS mapping.
  • Add an HTTP endpoint — append to k_routes[] in firmware/components/webui/webui.c. Don't forget to bump cfg.max_uri_handlers if you cross the soft limit.

PRs welcome. Open against the v6-idf-rewrite branch.

Clone this wiki locally