-
-
Notifications
You must be signed in to change notification settings - Fork 12
Architecture Overview
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.
┌─────────────┐ 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.
| 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.
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.
| 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.
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.
| 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.
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.
| 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% |
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
-
Add a new radar driver — drop a
radar_<kind>.cintofirmware/components/radar/implementingsize_t radar_<kind>_parse(const uint8_t*, size_t, radar_frame_t*), register inradar.c'sk_drivers[], expose via/api/radar/kindsJSON. -
Add an LED mode — extend the
mode_*switch infirmware/components/led_engine/. Modes are render-time only; persistence is just auint8_tin NVS. -
Tweak motion —
firmware/components/motion/motion_kalman.chas the v3 Kalman filter; the legacy PI smoother is inmotion.c. UI knobs map to NVS keys; seehandle_settings_postinwebui.cfor the JSON-key → NVS mapping. -
Add an HTTP endpoint — append to
k_routes[]infirmware/components/webui/webui.c. Don't forget to bumpcfg.max_uri_handlersif you cross the soft limit.
PRs welcome. Open against the v6-idf-rewrite branch.
Get started
Use it
Help
Upgrading
Reference