Skip to content

AmbiSense v6.0.0 — promote v6-idf-rewrite to main#17

Merged
Techposts merged 17 commits into
mainfrom
v6-idf-rewrite
May 5, 2026
Merged

AmbiSense v6.0.0 — promote v6-idf-rewrite to main#17
Techposts merged 17 commits into
mainfrom
v6-idf-rewrite

Conversation

@Techposts
Copy link
Copy Markdown
Owner

Summary

Promotes the v6 ESP-IDF + FreeRTOS rewrite from v6-idf-rewrite to main. After merge, fresh clones default to v6.0.0; the v5 Arduino tree stays accessible on legacy/v5-arduino.

The shipped release is already published at https://github.com/Techposts/AmbiSense/releases/tag/v6.0.0 with prebuilt C3 binaries — this PR just makes main the canonical home for that code.

Recommended merge strategy: merge commit (not squash). Squashing would collapse 7 meaningful commits (PR #1 skeleton, PR #2-#5 rewrite, Kalman + pairing, mobile + boot guard, release docs) into one, losing git bisect granularity for any v6.x regression hunt later.

What lands on main

  • Full ESP-IDF v5.5.2 firmware tree under firmware/ (12 components: board, settings, status_led, button, netmgr, auth, webui, ota, radar, motion, led_engine, topology, mesh)
  • Preact + Vite single-file UI (frontend/) with 7 fully wired screens
  • v5.1.1 Arduino source preserved under legacy/AmbiSense/ (frozen reference)
  • Updated docs: README.md, docs/HARDWARE.md, docs/V6-ROADMAP.md, docs/V6-ARCHITECTURE.md

Recommended hardware tier (per HARDWARE.md)

Tier Board
Recommended ESP32-S3 (DevKitC-1 or S3-Zero)
Supported ESP32-C3 SuperMini
Deprecated ESP32 classic (WROOM-32)
Avoid ESP32-C6

Known issues shipping with 6.0.0 (deferred to 6.1+)

  • ESP-NOW packets unencrypted (PMK/LMK plumbing in place; UI password field deferred)
  • Single-core C3 needs 300 ms client-side debounced save to avoid HTTP saturation under slider drag (already shipped in 0b63b6e); proper fix is dual-core pinning on S3
  • Topology auto-discovery is manual in 6.0; "walk through your stairs" calibration coming in 6.1
  • Sessions are RAM-resident (lost on reboot)
  • LED count tested only to 300 (theoretical max 1500)

Full list and recovery procedures are in the release notes and docs/HARDWARE.md.

Test plan

  • idf.py build clean for esp32c3 target — binary 1.17 MB / 1.4 MB partition (18% free)
  • Both C3s on bench flashed with the v6.0.0 build; web UI loads at http://192.168.0.216/
  • 50× rapid POST /api/settings stress test — all 200 OK (verifies the debounced-save fix)
  • /api/mesh returns pairing_ms_left correctly during 30 s window
  • Motion settings round-trip via /api/settings POST + GET (motion_mode, response, look_ahead_ms, outlier_strength all persist)
  • Mobile responsive — .hide-mobile / .show-mobile rules now defined; sidebar disappears on phones
  • S3-Zero hardware validation (pending S3 unit arriving on bench)

Ravi Singh added 17 commits May 5, 2026 18:22
…sign handoff

Begins the v6 rewrite. Moves the v5.1.1 Arduino source under legacy/
and stands up an ESP-IDF v5.3-compatible project under firmware/.

Highlights:
- Custom partition table: 16K NVS + 8K otadata + 1408K x 2 OTA app slots
  + 960K LittleFS + 64K coredump (4MB layout, dual-bank safe rollback).
- sdkconfig.defaults: task watchdog, brownout detector, coredump-to-flash,
  bootloader rollback, esp_http_server with WS support — applied across
  every target. esp32c3-specific overrides in sdkconfig.defaults.esp32c3.
- components/board: profile struct + four ship-ready profiles
  (esp32c3-supermini validated, esp32-devkit / esp32s3-zero / esp32c6-devkit
  build-clean but untested). unsafe_pin_mask encodes strapping/USB-JTAG/
  flash pins so the upcoming web UI pin-remap can refuse foot-guns.
- components/settings: NVS facade (replaces v5 320-byte EEPROM map);
  exposes board namespace accessors used by main.
- components/status_led: dedicated FreeRTOS task drives the onboard LED;
  pattern API (BOOT, AP_MODE, STA_MODE, OTA, ERROR, PANIC) replaces the
  inline blink-in-loop pattern from the Arduino code.
- main/main.c: resolves board profile (NVS override > compile default),
  applies pin overrides while rejecting unsafe ones, brings up status LED.
- frontend/design-source/: full Claude-Design handoff bundle (tokens.css,
  7 screen JSXs, README, chat). Reference for PR #5 UI build.
- .github/workflows/firmware.yml: IDF v5.3 build matrix across all four
  targets on push/PR; uploads tagged firmware artifacts.
- README banner explaining v5 -> v6 transition, branches, and quickstart.

Verified: idf.py build succeeds for esp32c3. Output ambisense.bin is
~195 KB (86%% free in the 1.4 MB app slot).

Refs #v6 epic. Next: PR #2 — Wi-Fi STA/AP fallback, captive portal,
esp_http_server, OTA, auth scaffold.
Captures the locked architecture decisions, the 5-PR plan with
current status, and the hardware reference + flash troubleshooting
ladder so future sessions/contributors can pick up the v6 rewrite
without reconstructing context.

- docs/V6-ARCHITECTURE.md — locked decisions (peer mesh, modular
  radar drivers, NVS schema, board profiles, FreeRTOS task model,
  HTTP API surface). Single source of truth for "why is it built
  this way".
- docs/V6-ROADMAP.md — PR-by-PR plan (PR #1 done; PR #2-5 scoped),
  done criteria, tag/release cadence (v6.0.0-alpha.N → v6.0.0).
- docs/HARDWARE.md — C3 SuperMini reference wiring, sensor pinouts
  for LD2410/LD2412/LD2420/LD2450, and a tested flash-fails-to-
  connect troubleshooting ladder including the macOS USB-CDC
  stuck-state recovery (Mac restart).
- README pointers to the three new docs.
…th, OTA

Adds the full networking + web layer. AP and STA both run simultaneously
so the device works with OR without an external router — many installs
have no Wi-Fi at all and the AP becomes the only access point.

- components/netmgr — WIFI_MODE_APSTA always on. AP "AmbiSense-XXXX" up
  for the lifetime of the device on a configurable channel (default 6),
  open or WPA2 (NVS-configurable). STA additive: if creds saved, joins
  user's router; on failure, AP stays available so the user can fix
  creds via the captive portal. Captive-portal DNS responder steers
  every query at the device IP so iOS/Android/Win11 auto-pop setup.
  mDNS publishes <hostname>.local + _ambisense._tcp service.
- components/auth — PBKDF2-SHA256 (250k rounds) password hash, 32-byte
  random session tokens, 8-slot in-RAM session store, 24h TTL. Off by
  default; banner in placeholder UI nudges user to set a password.
- components/ota — esp_https_ota wrapper streaming an octet-stream
  upload into the inactive OTA partition, validates, marks for boot,
  reboots in 1s. Bootloader rollback armed via sdkconfig — failed boot
  reverts automatically. ota_mark_valid() called early in main to defuse
  rollback once the running image proves it boots clean.
- components/webui — esp_http_server with full route surface:
    GET  /                        placeholder HTML (PR #5 replaces)
    GET  /generate_204, /hotspot-detect.html, /connecttest.txt, ...
                                   captive portal redirects (302 → /)
    GET  /api/version             firmware/idf/build/uptime/heap/ip/etc.
    GET  /api/wifi/scan           list nearby APs
    POST /api/wifi                set creds, deferred reconnect
    POST /api/auth/login | logout | password
    GET  /api/board/profiles      4 board profiles + unsafe pin masks
    POST /api/board               save board id + per-pin overrides
    GET  /api/radar/kinds         5 radar drivers + active selection
    GET  /api/settings            flat read of every NVS namespace
    POST /api/settings            partial update (any subset)
    GET  /api/distance            placeholder (PR #3 wires real value)
    POST /api/ota                 octet-stream firmware upload
    WS   /api/live                5 Hz JSON: distance/rssi/heap/peers
- components/settings extended: typed get/set for u32/i32/u8/blob/str
  across any namespace + wifi/sys/auth shortcuts.
- main.c brings them all up in order: nvs → board → status_led → auth
  → netmgr → webui → ota_mark_valid.

Built clean; binary 950 KB (33%% free in 1.4 MB app slot). Flashed to
both C3s. AP visible from phone; captive portal expected to pop.

Refs v6 epic. Next: PR #3 — radar driver registry + LED engine.
… engine

Lights and sensors come online. The peer-mesh-shaped target queue from
PR #4 is the next integration; for now the LED engine consumes the
local motion smoother directly.

Components added:
- components/radar — driver registry with all 5 v6.0 drivers compiled
  in: ld2410, ld2412 (alias), ld2420 (alias), ld2450, sim. Selection at
  runtime via NVS board.radar_kind. UART-driven parser task pushes
  radar_frame_t into a 1-slot queue (overwrite-style).
    radar_ld2410.c — 23-byte basic-mode frames, F4F3F2F1...F8F7F6F5
                     header/tail; extracts moving/stationary state +
                     primary distance + energy.
    radar_ld2450.c — 30-byte fixed frames, AA FF 03 00 ... 55 CC; up
                     to 3 targets with x,y,speed,resolution. Decodes
                     the inverted sign convention (bit 15 = positive).
                     Computes primary radial distance via integer sqrt.
    radar_sim.c    — synthetic trace generator. Defaults to a 4 s
                     30..200 cm sine wave; user can POST a scripted
                     trace to /api/sim/trace (PR #5 wires the UI).
- components/motion — v5 PI smoother ported faithfully
  (radar_manager.cpp:38-198). Low-pass on position, EMA on velocity,
  position prediction with PI controller, 50 Hz task. Tunables in NVS
  namespace `motion`, x1000 fixed-point.
- components/led_engine — uses espressif/led_strip managed component
  (RMT-backed) for non-blocking refresh. All 11 v5 visual modes ported:
    standard, rainbow, color_wave, breathing, solid, comet, pulse,
    fire, theater_chase, dual_scan, motion_particles.
  60 Hz render task. NVS-backed parameters; led_engine_reload() called
  from /api/settings POST applies changes without reboot. Strip resize
  triggers a clean re-init of the led_strip handle.
- main.c wires radar_init → motion_init → led_engine_init in order
  using the resolved board profile's pins (UART num, RX/TX, LED data
  pin). Telemetry task pumps motion_get → webui_publish_live at 5 Hz
  so /api/live WS clients receive smoothed distance.

Build: 1.04 MB binary, 28% free in 1.4 MB app slot. Flashed to both
C3 SuperMinis. Default radar driver is ld2450 (matches the bench
hardware on Ravi's setup).

Refs v6 epic. Next: PR #5 — Vite+Preact UI from design source so the
full 7-screen dashboard replaces the placeholder HTML, then PR #4 layers
peer mesh on top.
…ped binary

Replaces the PR #2 placeholder HTML with the full 7-screen dashboard
built from the Claude Design handoff. Single-file Preact bundle
(48 KB raw, 16.9 KB gzipped) embedded via EMBED_FILES; served with
Content-Encoding: gzip — no LittleFS needed.

Frontend (frontend/):
- Vite 5 + Preact 10 + TypeScript + vite-plugin-singlefile + terser.
- src/styles.css lifts design-source/project/tokens.css colors,
  spacing, type, atoms verbatim. Layout adds sidebar/main on desktop,
  bottom tab bar on mobile (≤760px), light/dark theme toggle, prefers-
  reduced-motion respect.
- src/components.tsx — Card, Toggle, Field, Slider, Row, Dot,
  ColorPicker (8 presets + native picker), useToaster.
- src/led_preview.tsx — canvas LED strip preview mirrors the firmware's
  mode logic; on-screen animation matches the wire output. All 11 modes
  rendered live (standard, rainbow, color_wave, breathing, solid,
  comet, pulse, fire, theater_chase, dual_scan, motion_particles).
- src/api.ts — fetch JSON, octet-stream OTA upload with progress,
  WebSocket /api/live with auto-reconnect backoff.
- src/screens.tsx — all 7 screens:
    Live      — distance meter + LED preview + device + mesh cards
    LEDs      — mode picker with animated thumbnails, color picker,
                brightness/count/distance window/effects/trail/dir
    Motion    — PI smoother sliders (live x1k fixed-point readout)
    Mesh      — topology picker (PR #4 wires fully)
    Hardware  — board profile dropdown, radar driver picker, per-pin
                overrides with unsafe-pin-mask filtering
    Network   — STA scan + connect + AP behaviour (auto/always/
                sta_only) + AP password + forget-STA + status badges
    System    — auth password, OTA upload (drag-drop), diagnostics
- Vite dev proxies /api → http://192.168.4.1 for in-browser dev.

Firmware:
- components/webui/ui.html.gz embedded via EMBED_FILES; handle_root
  serves with Content-Encoding: gzip + max-age=300 cache header.
- Linker symbols _binary_ui_html_gz_start/end resolved in webui's own
  translation unit (moved out of main).
- The PR #2 inline HTML kept under #if 0 for reference.

Build: 1.05 MB binary, 27%% free. UI gzipped 16.9 KB. Flashed to both
C3s (ports 21101 and 2122201).

Refs v6 epic. Next: PR #4 (peer mesh) — Mesh screen gets real peer
cards, topology editor, pairing flow.
Adds the mesh layer that lets multiple devices coordinate without a
master/slave relationship. Each device renders only its own LED segment,
broadcasts its smoothed reading at 5 Hz, and runs the same fusion
algorithm locally on the merged peer stream — every device arrives at
the same active position.

components/topology — explicit topology model in NVS:
  - 4 kinds: straight, L_shape, U_shape, custom.
  - Up to 8 segments. Each segment owned by one peer (MAC), with its
    own LED virtual address range and optional distance window.
  - Versioned blob; remote-gossiped updates with higher version win.
  - Defaults to a single-device 30-LED segment on first boot.

components/mesh — ESP-NOW peer mesh:
  - 5 Hz target broadcast (24-byte packet: distance, direction, energy,
    flags, timestamp).
  - Coordinator role auto-elected to lowest-MAC healthy peer; re-runs
    every broadcast tick. Coordinator serves the canonical web UI host
    (mDNS resolves to it).
  - 30-second pairing window opens at boot AND on /api/mesh POST {pair:true}.
    Outside the window, only known peers' broadcasts are accepted.
  - 10-second peer health timeout marks stale peers; logs transitions.
  - Topology gossip via MSG_GOSSIP carrying the full topology_t blob;
    receivers compare versions before accepting.
  - 4 fusion algorithms (port of v5 SENSOR_PRIORITY modes):
      most_recent  — newest reading wins
      slave_first  — non-coordinator readings preferred
      master_first — coordinator's reading preferred
      zone_based   — per-segment dist_min/max windows score candidates;
                     reading inside a segment's window gets +100 score
    Persisted in NVS topo.fuse.

webui adds:
  - GET  /api/mesh        → coordinator role, fusion mode, peer list
                            (mac, distance, direction, rssi, healthy)
  - POST /api/mesh        → set fusion mode, open pairing window
  - GET  /api/topology    → kind, version, total LEDs, segment list
  - POST /api/topology    → replace topology + force gossip to peers

main.c brings them up after netmgr (Wi-Fi must be started before
esp_now_init). Telemetry pump now reports mesh-fused distance + peer
counts to /api/live WS clients.

Build: 1.07 MB binary, 26%% free in app slot.

Refs v6 epic. Next: validate mesh between the two C3s on bench, then
tag v6.0.0.
…r phone-grade perf

Reported issue: web UI is slow on phone. Root cause: the LEDs tab was
mounting 11 simultaneous animated canvases (one per mode card), each
running a 60 Hz requestAnimationFrame loop with per-pixel math (sin/cos
for color_wave, Math.random per pixel for fire). Mobile GPU cooked.

Fixes:
- Replace per-card animated canvases with **static CSS-gradient thumbnails**
  (one custom gradient per mode, hand-tuned to match each mode's feel:
  rainbow strip, comet trail, fire vertical gradient, repeating-stripe for
  theater chase, particle dots, etc.). 11 canvases → 0.
- Hero animated canvas remains: ONE on the Live tab, ONE on the LEDs tab,
  showing the active mode + live distance — exactly the "real-time where
  it matters" win.
- led_preview now respects document.hidden — RAF returns immediately when
  the tab/page is backgrounded. Phone in pocket = zero CPU.
- /api/version polling: 5 s → 30 s, and skipped while hidden. Distance,
  RSSI, peer health continue at 5 Hz over WebSocket — those are the real
  real-time signals.
- Network tab no longer auto-scans WiFi on mount. The 1-second wifi scan
  was stalling first paint. User clicks "Scan" when ready.

Bundle: 50.1 KB raw, 17.3 KB gzipped (+0.4 KB for the new gradient
classes). Same firmware embed path; flashed both C3s.

Real-time vs on-demand split:
- WebSocket /api/live (5 Hz): distance, direction, RSSI, heap, uptime,
  peer count, healthy peer count.
- Animated canvas: only the active mode's preview — mirrors hardware.
- Slider feedback: immediate optimistic write + toast confirm.
- Polled at 30 s: /api/version (uptime + heap).
- On-demand: /api/wifi/scan, /api/board/profiles, /api/topology, etc.
…/ping health endpoint

User reported: at the device's STA IP, only the page header showed —
the body wouldn't render. Reproducible mid-session and not earlier on
the AP IP. Most plausible cause: some browsers/edge cases (private-LAN
detection, captive-portal probes, certain mobile WebViews) don't send
Accept-Encoding: gzip even when the page is plain HTTP. The C3 was
unconditionally returning Content-Encoding: gzip, so those clients got
the gzipped binary as text and parsed only as far as the first non-
ASCII byte.

Fix:
- EMBED both ui.html.gz (17.3 KB) and ui.html (50.1 KB).
- handle_root_real now sniffs Accept-Encoding and serves the gzipped
  blob with Content-Encoding: gzip when supported, raw HTML otherwise.
- Cost: +33 KB flash. Worth it to eliminate this whole bug class.

Also added GET /api/ping → "pong" so a stuck client can curl it and
confirm the server is alive even when HTML rendering misbehaves.

Build: 1.13 MB binary, 22%% free in app slot. Flashed both C3s.

If user's browser issue persists after this flash, the next diagnostic
step is to curl http://<device-ip>/api/ping and check Content-Length
headers on / — that points at TCP/HTTP plumbing rather than encoding.
…eboot button + /api/reboot

Three bugs reported by Ravi during hardware testing, three fixes:

1. **HTML response truncated at ~16 KB** (header-only render at STA IP):
   esp_http_server's single-call httpd_resp_send doesn't reliably push
   large bodies on the C3 — TX buffer can't accept the full 50 KB raw
   HTML in one go. Switched to chunked sending (4 KB chunks +
   terminator). Diagnosed via curl from host: Content-Length: 50172 but
   only 15802 bytes actually arrived. Fix verified — full HTML now
   delivers as Transfer-Encoding: chunked.

2. **Pins "not persisted across reboot"** — actually a UI display bug,
   not a persistence bug. Verified end-to-end via curl: save led_pin=7,
   /api/reboot, ping back, readback returns 7. NVS works fine.
   The lie was the Hardware screen: it only fetched
   /api/board/profiles (which returns profile defaults like GPIO 10)
   and never /api/settings (which has saved overrides). User saw "10"
   in the dropdown after their save and assumed the save was lost.

   Fixed:
   - ScreenHardware now fetches /api/board/profiles + /api/radar/kinds
     + /api/settings in parallel, initializes pin state from saved
     values falling back to profile defaults only when no override exists.
   - Each pin shows "(default)" or "(custom)" label so user can see at
     a glance which pins are overridden.
   - Switching board profile snaps pins to that board's defaults so
     dropdowns can't point at GPIOs that don't exist on the new MCU.

3. **JSON key inconsistency** — /api/settings GET returns "button_pin"
   and "status_led_pin", but /api/board POST expected "button" and
   "status_led". UI couldn't round-trip cleanly. Fixed: server accepts
   both forms; UI uses the canonical *_pin form everywhere.

4. **No way to reboot from the UI**:
   - New POST /api/reboot endpoint (deferred 500 ms so the response
     flushes before esp_restart()).
   - Hardware tab: "Save & reboot" button next to "Save".
   - System tab: dedicated "Reboot device" danger-style button.
   - Both prompt for confirmation before rebooting.

5. **Radar driver versions audit** (Ravi asked):
   - All five drivers (LD2410/LD2412/LD2420/LD2450/sim) are bare-metal
     UART parsers against HiLink's official protocol specs — not
     third-party libraries with versions to upgrade.
   - LD2410 family: F4F3F2F1 + len + type=0x02 + head=AA + state/dist/
     energy + tail=55 + F8F7F6F5; matches every LD2410 firmware ever
     shipped (current v2.04.x).
   - LD2412: same data-report protocol family; per-gate sensitivity
     is config-command only.
   - LD2450: 30-byte fixed frames AA FF 03 00...55 CC, three targets
     with inverted-sign-bit convention. Current spec.

6. **Managed component versions**: IDF 5.5.2, espressif/led_strip 3.0.3,
   espressif/mdns 1.11.1 — all latest stable as of pin date.

Build: 1.13 MB binary, 22%% free in app slot. Flashed both C3s.
…ar diag

User reported the implemented Live screen was missing major elements
shown in the Claude Design handoff. This PR ports the Live screen to
match the design pixel-for-pixel: system enable hero card, gradient
distance number, sparkline + min/max guides, strip preview, 4 stat
tiles, device card with all 8 fields, mesh card with peer info.

Frontend (frontend/):
- src/atoms.tsx: Icon (24 SVG paths from design), Sparkline (gradient
  fill + line, exact port of core.jsx implementation), fmtUptime helper.
- src/screens.tsx Live: full rewrite matching screen-live.jsx layout —
  2-column dash-grid (left: distance + strip + stat tiles; right: device
  + mesh cards), system enable hero with gradient backdrop when active,
  bigger gradient distance number (64 px) with in-window/min/max chips,
  client-side ring-buffer sparkline (80 samples = 16 s @ 5 Hz) with
  dashed min/max guide lines.
- src/styles.css: dist-big gradient text, distance-row, dash-grid +
  stat-row responsive breakpoints (collapse to 1 col at ≤900 px).

Firmware:
- components/led_engine: respects sys.enabled NVS flag — paints all
  black + 100 ms tick when disabled (mesh keeps running, no light out).
- components/webui: GET/POST /api/system endpoints. POST {enabled:bool}
  persists to NVS sys.enabled.
- components/radar: byte counter + last-64-bytes ring + frames-parsed
  counter exposed via new GET /api/radar/diag with hint string.
  Diagnosed Ravi's "distance always 0" → was actually wiring; once
  fixed, diag now shows "OK — radar streaming valid frames" with hex
  dump of the LD2410 frames (e.g. F4 F3 F2 F1 0D 00 02 AA 03 1E 00 64...).
- components/webui: GET/POST /api/system + /api/reboot endpoints.
  /api/reboot defers 500 ms so the response can flush before
  esp_restart(). Used by Hardware tab "Save & reboot" and System tab
  "Reboot device" buttons.

Build: 1.12 MB binary, 21%% free in 1.4 MB app slot. UI bundle 61.8 KB
raw / 20.7 KB gzipped. Both C3s flashed and verified:
- /api/system GET → {"enabled":true}; POST cycle works
- /api/distance → 41 cm (live radar)
- /api/radar/diag → 156 frames parsed in 8 s, hint "OK"
- / → Transfer-Encoding: chunked, full 61936 bytes delivered
…wired to firmware

Comprehensive port from frontend/design-source/. Same look/feel/sizes/
placements as Claude Design handoff; every button, dropdown, field, and
toggle calls a real /api endpoint and persists in NVS or has hardware
effect. Verified all 11 endpoints live on the C3.

Frontend (src/screens.tsx full rewrite + atoms.tsx additions):

LEDs screen — 11 mode cards each with the Claude-design description
("Distance-driven cluster with directional fade", etc), animated mini
LedPreview thumbnails, color preset row + hex input, NumberAndSlider
for brightness/effect speed/intensity/trail, dual-handle distance window
slider, light span / center shift / strip length sliders. Optimistic
writes via /api/settings POST with toast confirms.

Motion screen — large smoothing toggle, real-time raw-vs-smoothed
LineChart (SVG, dashed gridlines, 80-sample ring buffer), filter
sliders (position/velocity/prediction smoothing) + PI gains panel
with explanatory hint card. All values round-trip via /api/settings.

Mesh screen — topology cards with SVG diagrams (straight/L/U/custom,
ported verbatim from design), pair-new-device button opening a 30 s
window with animated pairing card, devices list with healthy dots +
RSSI + lost%, sensor priority cards (most_recent / slave_first /
master_first / zone_based) with descriptions. Wires to /api/topology
and /api/mesh.

Hardware screen — board profile cards (not dropdown), radar driver
cards, pin map with field-label icons + dropdowns filtering unsafe
GPIOs. "Save & reboot" button when reboot is needed; reboots via
/api/reboot.

Network screen — connected status hero card with wifi icon, scan
button + signal-bars indicator + Join/current chip per network, inline
password prompt on Join, hostname input with .local suffix, AP-mode
selector (auto/always/sta_only) + Reset Wi-Fi danger button with
two-click confirmation. All endpoints wired.

System screen — firmware card with version + reboot button, drop-zone
file input with drag-drop or click-to-select, progress bar during
upload, "Flash & reboot" button. Diagnostics with heap/min-heap/
uptime/MAC. Auth section with show/hide password toggle and explicit
"Set password" / "Disable auth" button. JSON config Export downloads
/api/settings JSON. Factory reset with type-the-hostname confirmation
posts to new /api/factory_reset (erases NVS + reboots).

src/atoms.tsx adds: Icon (24 SVG paths from design), Sparkline (gradient
fill + line), NumberAndSlider, DualHandleRange (touch-friendly with
pointermove), TopologyDiagram (4 SVGs), LineChart (raw vs smoothed),
hsv2rgb / rgb2hex / hex2rgb helpers, fmtUptime, useRing.

Firmware:
- /api/factory_reset endpoint: nvs_flash_erase() + esp_restart() in
  deferred task so the JSON response can flush first.
- /api/system GET/POST already shipped.

Bug fixes:
- Set Password now shows real error when < 8 chars (was silent).
- OTA upload uses postBinary() with progress callback (was generic POST).

Build: 1.16 MB binary, 19%% free in app slot. UI bundle 81.8 KB raw /
24.1 KB gzipped. Both C3s flashed.

Verified live on hardware at 192.168.0.216:
- /api/ping, /api/version, /api/distance, /api/system, /api/wifi,
  /api/mesh, /api/topology, /api/board/profiles, /api/radar/kinds,
  /api/radar/diag, /api/settings — all returning expected payloads.
- Radar diag: driver=ld2410, 867 frames parsed in 30 s, last frame 31 ms ago.
- Distance live: 39 cm.
…useEffect imports

Two compounding bugs from the previous comprehensive rewrite that
together produced "distance shows 0 + console useRef error":

1. Server-side route table overflow.
   esp_http_server cfg.max_uri_handlers was 32. The previous PRs added
   /api/mesh GET/POST, /api/topology GET/POST, /api/factory_reset,
   /api/ping, /api/system GET/POST, /api/radar/diag — bringing total
   route count to 33. The WS route is registered last, so it silently
   failed registration. No client could subscribe to live data, so the
   Live screen distance stayed at 0 forever. Bumped to 48.

   Verified post-flash: WS handshake returns 101 Switching Protocols
   from python ws client; chunked HTML still serves intact at 81 KB.

2. Client-side missing imports.
   atoms.tsx uses useRef (in DualHandleRange) and useEffect (in
   DualHandleRange + LineChart) but only imported useState from
   preact/hooks. Errors only fired on LEDs/Motion tabs where those
   components render. Added the missing imports.

Build: same 1.16 MB, both C3s flashed, all 11 endpoints verified live.
…on v2 (median + adaptive)

User reported visible jitter and missing UI elements. Three-pronged fix.

Frontend — full app shell port from design-source/app.jsx:
- LogoMark: animated triangle "A" with 3 concentric pulse rings
  emanating outward, radial proximity-glow that breathes (scale +
  opacity), corner-tick chip details. Pulse strength scales with live
  distance (closer target = brighter glow). Uses lm-grad
  (amber→orange→pink) and lm-core radial gradients verbatim from
  design.
- Wordmark: "Ambi" in slate-gradient + "Sense" in accent-gradient,
  with "v6.x · ESP32" caps subtitle.
- Sticky header (.app-header) with backdrop-filter blur — page name +
  hostname.local breadcrumb + live distance chip + RSSI chip +
  sun/moon theme toggle.
- Sidebar with logo block + nav + bottom IP/board chip
  (10.0.0.x · ESP32-C3 · LD2410C style).
- styles.css: keyframes logo-pulse, logo-breath, pulse-acc; .app-header,
  .brand-block, .sidebar-foot, .btn-icon classes.

Firmware — kill the visible 200ms stair-step jitter:
- ws_broadcast_task: 200 ms (5 Hz) → 50 ms (20 Hz). Bandwidth: ~2.5 KB/s,
  trivial. Browser sparkline now updates 4× more often, no flat dwells.
- main.c telemetry_pump_task: same bump 5 Hz → 20 Hz, also publishes
  raw_cm now (median-filtered but un-smoothed) so the Motion screen
  graph shows what the firmware *actually* feeds the LED engine,
  removing 200 ms client-side simulation lag.
- webui_live_t and the WS JSON gain a 'raw' field.

Firmware — motion algorithm v2:
- Median-of-5 filter on raw radar samples before the smoother. Single-
  sample LD2410 spurious readings (every few minutes) get out-voted;
  rejection is total. Insertion-sort over 5 ints — cheap.
- Adaptive smoothing: alpha scales with motion magnitude. Stationary
  (|delta| < 30 cm/sample) uses configured pos_smooth; fast-moving
  (>= 30 cm/sample) ramps alpha up to 4× the configured value (capped
  at 0.9). Result: calm reading when still, snappy when moving — best
  of both worlds.
- target_t exposes raw_cm alongside distance_cm.

Frontend — Live sparkline + Motion graph fixes:
- ScreenLive: useEffect deps changed from [dist] to [live] so the
  sparkline advances on every WS frame at 20 Hz, even when the integer
  cm value hasn't changed (was freezing during stationary periods).
- ScreenMotion: raw and smoothed lines come straight from firmware
  WS payload, no client-side alpha simulation. Graph now reflects the
  real on-device smoother instead of a 200 ms-lagged copy.

Build: 1.17 MB binary, 19%% free in 1.4 MB app slot. UI gzipped 25.4 KB.
Both C3s flashed.
Pairing flow (PR-A):
- mesh.c broadcasts MSG_PAIR every 1s while pairing window open
- Asymmetric pairing: receiving MSG_PAIR auto-opens own window — clicking
  Pair on either device pulls the other in (no need to tap both)
- mesh_identify(mac) unicasts MSG_IDENTIFY; recipient blinks status LED
  at 10 Hz for 5 s so users can physically locate which board is which
- Coordinator election now has 5 s hysteresis — prevents brief flap when
  a single heartbeat is dropped
- mesh_event_cb_t: peer-joined / pairing-opened/closed / identify-requested
  events surface to main.c which drives the status LED reactions
- POST /api/mesh/identify {mac} endpoint
- GET /api/mesh now returns pairing/pairing_ms_left/my_mac for live UI
  countdown and self-vs-peer card highlighting

Status LED:
- New STATUS_LED_PAIRING (5 Hz) and STATUS_LED_IDENTIFY (10 Hz) patterns
- status_led_oneshot(pattern, ms) — temporary pattern with auto-revert
  to last stable pattern; identify becomes one line in main.c

Button (new component):
- Polling-based long-press detector (50 Hz, 40 ms debounce)
- 3 s long-press → mesh_open_pairing(); 10 s reserved for v6.1 factory reset

Motion v3 (PR-B):
- New motion_kalman.{c,h}: 1-D Kalman over [position, velocity], energy-
  aware observation noise (R *= 4 when energy < 30), ±200 cm/s velocity
  clamp, 3-sample direction hysteresis
- motion.c integrates: motion.mode NVS string ("kalman" default | "pi"),
  motion.resp 0..100 (Calm⇄Snappy), motion.la_ms 0..500, motion.outl 0/1/2
  (Off / median-3 / median-7); legacy ps/vs/pf/pg/ig kept as advanced
- motion_reload() called by /api/settings POST so changes apply live

Frontend:
- ScreenMesh: SVG circular pairing countdown, per-peer Identify button
  with "Blinking…" 5 s cooldown, self-card highlight
- ScreenMotion: Algorithm picker (Kalman/Legacy PI), Response slider with
  context-aware hint text, Look-ahead-ms slider, Outlier segmented control,
  collapsible Advanced (5 PI knobs)
- New atoms icons: link, plus, search

Debounced saves (fixes ERR_CONNECTION_RESET):
- useDebouncedSave hook: 300 ms tail; coalesces multiple slider changes
  into one POST /api/settings batch. Fixes the C3-single-core httpd
  saturation when sliders fired ~30 POST/s during drag (verified with
  50× rapid POST stress test — all 200 OK)

Verified: idf.py build green; ambisense.bin 1.17 MB / 1.4 MB partition
(18% free); both C3s flashed; /api/mesh, /api/settings round-trip works.
Mobile (ERR was: sidebar + bottom-tabs both rendered, scrambling layout):
- Added the missing .hide-mobile / .show-mobile rules — they were
  referenced inline by main.tsx but never defined in styles.css, so on
  phones the desktop sidebar AND mobile bottom-nav both rendered at
  once, overlapping every card and pushing the page off-screen
- Trimmed paddings, reduced page-head gap, and force-collapse all
  inline auto-fit grids (220/280 px min-cols) to 2-column at ≤760 px
  and 1-column at ≤480 px — Hardware/Mesh/Motion screens now lay out
  cleanly on a 360 px viewport

Boot guard (ERR was: 2nd C3 not starting AP after reflash):
- resolve_board_profile now rejects an NVS board.id whose profile->mcu
  doesn't match CONFIG_IDF_TARGET (e.g., "esp32-devkit" pinmap loaded
  on a C3). Wrong pinmap drives USB-JTAG / flash pins as outputs and
  bricks boot before netmgr starts, looking like "device dead". Falls
  back to the compile-time default profile in that case.
README: v6.0.0 shipped — describe the actual delivered features instead
of the "in progress" status. Highlight ESP32-S3 as recommended board.

HARDWARE.md additions:
- Board recommendation tier table at the top (S3 recommended, C3
  supported, classic deprecated, C6 avoid). Plain reasoning so users
  picking a board for a new install pick S3.
- "Second device not visible" recovery — erase-flash + reflash for
  stale NVS with mismatched MCU pinmap. (v6.0 has the boot guard but
  alpha-flashed devices need the manual recovery once.)
- "Slider throws ERR_CONNECTION_RESET" entry — explains the C3
  single-core httpd saturation and the 300 ms client debounce fix
  shipped in 0b63b6e.
- Reordered the supported-boards table by recommendation, not by
  validation status.
README:
- Branch table now shows main = v6.0.0 (post-merge state) with v5
  archived on legacy/v5-arduino. Includes a one-liner for users
  pulling onto an old local main.
- Quickstart split into "easiest path" (flash prebuilt binary from the
  v6.0.0 release page) and "build from source" — the previous
  copy still described the PR #1 skeleton ("waiting for Wi-Fi setup
  in PR #2") which is wrong now that 6.0 is live.
- IDF version bumped from v5.3 LTS to v5.5.2 to match what was
  actually used to build 6.0.0.
- Reference wiring + first-boot AP setup added so a fresh user can
  go from clone to running without bouncing through HARDWARE.md.
- "v5 (Arduino) docs" disclaimer rewritten — v5 is frozen, not
  "until v6 takes over". Points at legacy/v5-arduino branch.

V6-ROADMAP.md:
- Top banner: v6.0.0 RELEASED, links to release page.
- New "v6.x roadmap (post-6.0.0)" section at the bottom captures
  encrypted ESP-NOW, signed OTA, persistent sessions, auto-topology
  learning, S3 dual-core pinning, sim-driver replay, and the few
  smaller follow-ups documented in the v6.0 release notes.
@Techposts Techposts merged commit 646861d into main May 5, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant