fix: URL-decode WiFi passwords before truncating to buffer size#182
fix: URL-decode WiFi passwords before truncating to buffer size#182muness wants to merge 19 commits into
Conversation
* feat: manifest-driven UI renderer (ADR-0003)
Add manifest_parse.c/h and manifest_ui.c/h — a fresh renderer that
builds screens from JSON manifest served by the bridge. Gated behind
USE_MANIFEST compile flag (default OFF, zero regression).
- manifest_parse: cJSON parser for full manifest, fast-only, sha-only
- manifest_ui: media/list/card screen renderers, screen manager,
chrome (header, status dot, status bar), zone picker, volume overlay
- bridge_client: dispatch macros route all ui_* calls through
USE_MANIFEST conditional, fetch_manifest() with SHA-based 304
- CMakeLists: USE_MANIFEST option, new source files
- Wired main_idf.c, app_main.c, platform_input_idf.c
- Added .cache/ to .gitignore
* fix: optimize build for DRAM fit and UI performance
- Switch from -Og (debug) to -O2 (IDF libs) + -O3 (our code)
- Exclude ui.c when USE_MANIFEST=ON, add compat shims in manifest_ui.c
- Reduce MANIFEST_MAX_SCREENS 8→4, MAX_LIST_ITEMS 64→16 for DRAM
- Enable SPIRAM BSS segment for overflow safety
- Add missing bridge_client.h include in platform_input_idf.c
* feat(manifest-ui): progress/status screens, art mode, volume gradient, circular artwork
- Add progress and status screen renderers (build + update + dispatch)
- Implement art mode: hide chrome/controls, full opacity artwork
- Volume arc gradient: blue → purple → red based on level
- Artwork fetched at full display size with bridge-side circular clipping (clip_radius param)
- Progress arc hidden when no track duration
- Remove scrollable flag for proper image centering
- UI_UPDATE noop in manifest mode (stops IP flash)
- Remove debug fetch_manifest logs
- bridge_client_get_artwork_url gains clip_radius parameter
* feat(bridge-client): UDP fast-path with HTTP fallback
Poll bridge via UDP (48-byte binary) instead of HTTP+JSON for fast
state. Falls back to HTTP on any UDP failure (timeout, bad response,
bridge without UDP support). SHA mismatch triggers HTTP manifest fetch.
Wire format: 54-byte request (magic+SHA+zone_id), 48-byte response
(flags+SHA+volume+seek+transport). Zero parsing — memcpy into struct.
* feat(ui): smooth arc transitions for volume and progress
Replace instant lv_arc_set_value jumps with 300ms ease-out animations
using lv_anim_t. Volume arc interpolates gradient color during animation.
Optimistic volume change (encoder turn) stays instant for snappy feel.
Progress arcs (media seek + standalone progress screen) also animated.
* polish(ui): volume suffix dB/%, smoother 500ms ease-in-out arcs
- Show 'dB' suffix for negative-min zones (Roon), '%' for percentage zones (LMS)
- Bump arc animation from 300ms ease-out to 500ms ease-in-out for smoother feel
* tweak: arc animation 550ms
* feat: manifest UI is now the default
USE_MANIFEST option flipped to ON. Plain 'idf.py build' builds
manifest mode. Pass -DUSE_MANIFEST=OFF for legacy UI.
* feat: UDP fast-path logging for verification
Firmware logs 'UDP fast-path OK' on first success and every 30th poll.
Logs 'falling back to HTTP' when UDP fails. Quiet during normal operation.
* fix: suppress polled volume during optimistic guard window
After encoder volume change, ignore polled volume updates for 1.5s.
Prevents jitter where fast UDP poll returns stale server value before
the volume command has been processed.
* quiet: remove per-poll manifest_ui_update log
* feat: UDP volume commands + remove optimistic guard
Volume changes sent via non-blocking UDP sendto instead of blocking
HTTP POST. UI thread never blocks on volume. HTTP fallback if UDP
socket not ready.
Removed volume guard window — poll shows truth, not optimistic cache.
With UDP commands, bridge knows real volume almost immediately.
* feat: OTA update UI for manifest mode
Full-screen overlay with progress arc, percentage, status text.
Tap overlay to start update (disabled during download).
Thread-safe via platform_task_post_to_ui callbacks.
Replaces the no-op stubs.
* fix: reduce ART_SIZE 510→336 — less crop, less waste
Old 510px was based on incorrect geometry (D×√2). 336px gives
minimal overflow past the 360px display circle while preserving
more of the album art. Transfer drops from 520KB to 226KB per image.
* fix: address CodeRabbit review findings
- Fix critical nav index bug: find_screen_index returned screens[] index
but current_screen is a nav index. Now searches nav.order[] directly.
- Add upper-bound clamp to zone picker scroll selection.
- Null-terminate s_mgr.sha after strncpy.
- SHA fields in UDP structs: uint8_t→char (text, not binary).
- Fix misleading comments: CMakeLists default, sdkconfig -O2, parse_fast doc.
- Reduce ART_SIZE 510→336 for less crop waste.
* fix: mDNS race — don't query before init completes
Bridge client poll loop was calling mDNS discovery before
platform_mdns_init() ran, burning through all 10 retries with
ESP_ERR_INVALID_STATE. After that, it permanently gave up.
Add platform_mdns_is_ready() gate — discovery attempts are
skipped until mDNS service is initialized.
* feat: UDP broadcast bridge discovery — no mDNS required
When bridge_base is empty, sends a UDP poll to 255.255.255.255:8089.
The bridge already listens on that port and responds normally.
Source IP from the response reveals the bridge address.
Tried before mDNS — works on networks where multicast is blocked
but broadcast isn't (most consumer gear). Falls back to mDNS if
broadcast gets no response.
* fix: flash-size 8MB→16MB in merge-bin — matches sdkconfig
Firmware compiled with CONFIG_ESPTOOLPY_FLASHSIZE=16MB but the
merge-bin header said 8MB. Bootloader sees the mismatch and the
device becomes unresponsive after web flash.
* fix: show network/setup messages in manifest UI mode
After erase+flash, WiFi provisioning messages were invisible because
manifest_ui_set_network_status was a no-op stub. No bridge = no
manifest = black screen with no text.
Implements a fullscreen overlay banner that shows setup messages
("Setup: Connect to 'roon-knob-setup'", "WiFi: Connecting...", etc.)
centered on a black background. Created on first use, hidden when
ui_set_network_status(NULL) fires after WiFi connects.
* fix: setup/provisioning messages visible in manifest UI
- AP mode banner now shows full instructions: WiFi SSID to connect
to AND the fallback URL (192.168.4.1) for when captive portal
auto-popup doesn't trigger
- Captive portal save/reboot countdown uses persistent network
banner instead of transient 3-second status messages that
disappeared before the user could read them
* fix: zone list items respond to touch
List buttons had user_data set but no click handler. Tap now sets
selected index and fires UI_INPUT_PLAY_PAUSE to confirm — same
path as encoder press.
* fix: OTA overlay says 'Tap to install' — knob has touchscreen
* fix: force full-screen invalidation on init — clear stale GRAM
SH8601 OLED retains its framebuffer across ESP reset. After
erase+flash the old screen content persists because LVGL's partial
rendering mode may not redraw pixels it considers unchanged.
lv_obj_invalidate(screen) at end of manifest_ui_init() forces LVGL
to flush the entire display on the first lv_task_handler() call.
* fix: remove lv_refr_now (stack overflow), add init+banner logging
* fix: show connection progress and distinguish first-time vs fallback AP
- CONNECTING shows SSID and attempt count (e.g. 'Attempt 2/5')
- AP_STARTED after failed retries shows 'Could not connect to <ssid>'
- First-time AP (no saved SSID) shows original setup instructions
* fix: WiFi setup/retry flow — three bugs
1. Missing break in AP_STARTED case — fell through to AP_STOPPED,
overwriting setup banner with 'WiFi: Connecting...'
2. bridge_client set_network_ready(false) competed with WiFi handler
for the banner during boot. Now only sets banner for RECONNECTING
(was operational, lost network), not during initial BOOT.
3. esp_wifi_stop() in start_ap_mode fired STA_DISCONNECTED which
triggered spurious retry. Set s_ap_mode before stopping STA,
guard schedule_retry_with_reason.
Also: remove debug logging (flush count, ui_loop_iter, network_status).
* ci: update ESP-IDF to release-v5.5 to match local build
CI was building with v5.4.3, local with v5.5.2. The v5.4 binary
doesn't flush LVGL after esp_restart (post-captive-portal reboot),
causing stale GRAM content to persist. v5.5 works.
* fix: apply display defaults (rotation 180) on fresh config
platform_storage_defaults() did memset(0) but never called
rk_cfg_set_display_defaults(), so rotation_charging stayed 0
instead of the default 180. Screen wasn't flipped when charging.
* fix: captive portal preserves display defaults (rotation, timeouts)
On fresh device, captive portal saved config with rotation=0/0
because it loaded an empty config and never applied defaults.
Now calls rk_cfg_set_display_defaults() before saving when no
valid config exists. Also uses RK_CFG_CURRENT_VER instead of
hardcoded 1.
* fix: don't regress from OPERATIONAL to CONNECTED on WiFi reconnect
set_network_ready(true) unconditionally set state to CONNECTED and
showed 'Loading zones...' banner even when device was already
OPERATIONAL. On transient WiFi drops during art mode, this caused
the banner to appear on wake. Now preserves OPERATIONAL state.
* fix: don't force-show network banner on art mode wake
ui_set_controls_visible(true) unconditionally unhid the network
banner, resurrecting a stale 'Loading zones...' message that had
already been cleared. The banner manages its own visibility via
set_network_status — art mode wake should not touch it.
* feat: WiFi signal strength indicator in chrome
4 vertical bars (top-left area), updated every 2s from ESP32 RSSI.
Bars light up based on signal: ≥-50 dBm = 4, ≥-60 = 3, ≥-70 = 2,
≥-80 = 1. Hidden when not connected. Hidden in art mode, reappears
on wake via periodic update. PC build guarded.
* fix: WiFi indicator — horizontal fan bars, remove RSSI log
Replaced vertical bars (mobile signal look) with horizontal bars
in fan shape (widest at top, narrowest at bottom). 4 bars: 16/12/8/4px
wide, 2px tall, centered. Adjusted thresholds for ESP32 antenna.
* fix: WiFi bars fill from bottom (narrowest) up
* fix: nudge WiFi indicator down 3px to clear volume arc
* fix: WiFi indicator position — 2px up, 2px right
* feat: WiFi indicator — colored dot at top center
Replaced horizontal signal bars with a single 8px colored dot.
Color encodes signal strength: blue (excellent ≥-55), green (good
≥-65), yellow (fair ≥-75), red (weak). Black border for contrast
against album art. Hidden in art mode, reappears on wake.
* feat: progress bar with local seek interpolation
Bridge sends seek_position as a snapshot (LMS doesn't push
continuous seek updates like Roon). Firmware now interpolates
locally: stores last seek + timestamp, ticks forward every 1s
while playing. Progress arc animates smoothly instead of staying
frozen at the initial position.
Also: WiFi TX power bumped 11→17 dBm for better range on boards
with weak PCB antennas. Negligible battery impact (~1-2mA avg).
Add ui_show_settings() call to zone_label_long_press_cb in manifest_ui.c, matching legacy ui.c behavior. The callback already sets s_zone_long_pressed to prevent click-after-long-press from also opening the zone picker. Closes #127 Co-authored-by: Miranda <miranda@example.com>
* feat: render list item sublabels with selection highlight - Add flex column layout to list item buttons for label + sublabel vertical stacking - Render sublabel in font_small() with COLOR_TEXT_DIM when present - Items without sublabel render unchanged (single flex child) - Change selected item highlight from grey (0x333333) to blue-tinted (0x2a4a6a), matching legacy zone picker - Add LV_LABEL_LONG_DOT truncation for long sublabels Closes #128 * fix: add width constraints for label DOT truncation in column flex Per CodeRabbit review: LV_LABEL_LONG_DOT requires explicit width to trigger ellipsis. Also fix primary label width which loses auto-fill when button flex switches from ROW to COLUMN. --------- Co-authored-by: Miranda <miranda@example.com>
Port battery display from ui.c into manifest_ui.c: - Battery icon in header flex column above zone label - 4 discrete levels (critical/low/medium/high) with hysteresis - Red color for critical/low, grey for normal - Charging state shows charging icon - 30-second poll timer + immediate refresh on ui_update_battery() - Art mode hides/shows icon with hysteresis reset Closes #130 Co-authored-by: Miranda <miranda@example.com>
- Port fractional step detection from legacy: use (step_abs - (int)step_abs) > 0.01f instead of vol_step < 1.0f which incorrectly treats integer steps as fractional - Extend fractional formatting to percentage mode (symmetric with dB mode) - Fix play button border width: 2px -> 3px to match legacy visual weight - Document that manifest color choices (muted green, red offline, OPA_90) are intentional improvements over legacy, not bugs Closes #131 Co-authored-by: Miranda <miranda@example.com>
* fix: long-press zone label toggles settings panel * fix: shrink album art to 320px — pull inside circle edge * fix: remove progress debug logging
…139) In manifest mode, UI_UPDATE is a noop so error state details (attempt counts, header text) were lost. The UI_SET_NETWORK_STATUS calls existed but used terse messages like 'mDNS: 3/10' or 'Bridge: Retry 2/10'. Enrich all four error path network status messages to include the full context previously carried by UI_UPDATE line1/line2: - Just lost connection: 'Testing Bridge\nAttempt X of Y...' - mDNS still searching: 'Searching for Bridge\nAttempt X of Y...' - Bridge configured, max retries with IP: 'Bridge unreachable\nUpdate at http://...' - Bridge configured, still retrying: 'Testing Bridge\nAttempt X of Y...' The network banner is already persistent and fullscreen, so these messages now satisfy the acceptance criteria for persistent error state display during the connection sequence. Co-authored-by: Miranda <miranda@example.com>
* fix: long-press zone label toggles settings panel * fix: shrink album art to 320px — pull inside circle edge * fix: remove progress debug logging * feat: album art edge color as media screen background Bridge extracts average color from outer 15% border strip of album art during image conversion and caches it by image_key. Manifest includes background_color on MediaScreen. Firmware parses hex string and applies it to the media container background. Also shrinks ART_SIZE from 336 to 280 to pull art inside circle edge, making the colored background visible around the artwork.
…142) * fix: long-press zone label toggles settings panel * fix: shrink album art to 320px — pull inside circle edge * fix: remove progress debug logging
…144) - Progress gutter arc (8px black) behind progress arc for contrast - Progress indicator color derived from album art edge color - brighten_floor() ensures minimum channel value of 80 for dark art - Gutter visibility synced with progress arc (hidden when no track length)
* feat: multi-WiFi support — store up to 4 networks, try in order Config v3 adds rk_wifi_entry_t wifi[4] array to rk_cfg_t. On boot, wifi_manager tries each stored network in order. After STA_FAIL_THRESHOLD failures on one network, advances to the next. After all exhausted, enters AP mode for provisioning. Captive portal adds new credentials to the wifi[] list. Config web page (http://<knob-ip>/) shows saved networks with add/remove controls. Migration: v2 configs auto-migrate ssid/pass into wifi[0]. Active ssid/pass fields kept for runtime use by bridge_client. * fix: clear auto-discovered bridge on WiFi connect for location switching When network becomes ready, clear bridge_base if it was from mDNS. Forces fresh discovery on every WiFi connect. Solves two cases: - Different locations with different SSIDs (different wifi[] entries) - Different locations with the SAME SSID (mDNS finds local bridge) * fix: config_server.c compile error — missing cfg declaration and if block * feat: UI polish — zone selector fix, art-themed accents, progress arc stability - Fix zone selector showing status instead of names: reset flex_grow(0) and LV_LABEL_LONG_DOT on primary label after switching lv_list_add_btn to column flow (lv_list_add_btn sets flex_grow(1) + SCROLL_CIRCULAR which breaks in column layout) - Theme entire UI from album art edge color: volume arc, progress arc, and transport button borders all use brightness-floored accent color derived from album art. Falls back to default blues when no art color. - Remove volume arc gradient (blue→purple→red) — flat accent color, volume number provides the loudness indication - Add black gutter behind volume arc (12px black behind 8px colored) for visibility against art backgrounds - Move WiFi indicator dot into progress arc ring at 12 o'clock - Fix progress arc stalling: remove backward snap logic (diff < -3) that dragged local interpolation back to stale server seek values. Now only resets on: first update, server ahead, or state change. - Reduce RK_MAX_WIFI from 4 to 2 (rk_cfg_t 766→570 bytes) * fix: address CodeRabbit review — zone re-discovery, input validation, buffer sizes - Reset zone_resolved + s_bridge_verified when clearing mDNS bridge on WiFi reconnect, ensuring zones are re-discovered at new location - Increase wifi_html buffer 512→1024 for HTML rendering headroom - Increase POST body buffer 256→384 for URL-encoded SSID+password - Replace atoi with strtol+validation in wifi_remove_handler - Return 500 on platform_storage_save failure in add/remove handlers - Clear wifi[] array in platform_storage_reset_wifi_only * fix: hide volume gutter arc in art mode * fix: accent color — uniform brightness scaling preserving hue Per-channel floor clamping desaturated dark colors (e.g. #052062 became grayish #505062). Now scales all channels uniformly so the brightest reaches the floor (180), preserving hue and saturation. #052062 becomes a vivid blue rgb(9,59,180) instead of gray. * fix: accent color matches background — no brightness manipulation Use the same parsed hex color for both background and accent. The brightness floor was distorting the hue. Remove dead parse_hex_color_bright function. * fix: remove default theme shadow from transport buttons LVGL default theme applies a grey shadow to all buttons, creating a second border-like ring around the transport controls. * fix: arc track colors adapt to background brightness Compute perceived luma of album art edge color. Dark backgrounds (luma < 80) get lighter arc tracks (0x5a5a5a/0x4a4a4a) for contrast. Light backgrounds keep the default dark tracks. Resets to defaults when no art color. * feat: adaptive button shadows — light glow on dark, dark shadow on light Dark backgrounds (luma < 80) get a subtle grey glow around buttons. Light backgrounds get a dark drop shadow. No art color = no shadow (transparent). Play button slightly larger shadow (8px vs 6px). * fix: play button clipping — allow overflow on controls container * feat: adaptive text colors — light text on dark bg, dark text on light bg Title and volume use near-white on dark backgrounds (default). Light backgrounds flip to near-black title/volume and dark gray artist. * fix: explicit 84px height for controls row — prevent play button clipping LV_SIZE_CONTENT may undercount cross-axis height in flex row when children have different sizes, causing the 80px play button to be clipped by the 60px-height siblings. * feat: adaptive zone label color — same light/dark treatment as title * fix: remove dead last_seek<0 check, fix else brace indentation CodeRabbit review round 3 — both minor cleanups.
* fix: detect stale sdkconfig after defaults change ESP-IDF only applies sdkconfig.defaults when generating a fresh sdkconfig. If a user already has a sdkconfig from a previous build, pulling new code with updated defaults silently ignores the changes. This causes confusing build failures: DRAM overflow, missing LVGL symbols, wrong optimization level. Add a CMake guard that hashes sdkconfig.defaults and stamps it in the build directory. On subsequent configures, if the hash has changed and an existing sdkconfig is present, the build fails with a clear message telling the user to delete sdkconfig and rebuild. The guard is bypassed when no sdkconfig exists (fresh build), so the normal first-build flow is unaffected. * fix: add CMAKE_CONFIGURE_DEPENDS so idf.py build triggers the check Without this, CMake skips reconfiguration when only sdkconfig.defaults changes (it's not in the default dependency set). The staleness guard would only fire on explicit reconfigure or fullclean, not on a normal idf.py build after git pull. * fix: remove unused HTTPS/basic-auth config from install.sh install.sh injected CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y and CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y into sdkconfig.local. Neither HTTPS nor basic auth is used anywhere in the firmware — the bridge communicates over plain HTTP on the local network. These options pull in the full mbedTLS stack, adding significant DRAM and flash usage. Combined with the manifest UI, PSRAM artwork buffers, LVGL, and WiFi, this pushes the firmware over the DRAM budget, causing boot crashloops on the device. Also make build_flash_idf.sh skip set-target when already configured, matching the pattern install.sh already uses. The unconditional set-target was triggering a full rebuild on every invocation. * fix: add missing stack size and SPIRAM BSS to sdkconfig.defaults Two settings that were only in local sdkconfigs, never committed to defaults. Clean builds from sdkconfig.defaults alone would crash: - CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096: IDF default 3584 is too small for the manifest UI init path, causing stack overflow on boot. - CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y: allows large BSS segments to spill to PSRAM. Without this, DRAM overflows when the manifest UI allocates screen/list buffers in static storage. * refactor: split setup and install into separate scripts setup_mac.sh — one-time after clone or IDF version change: brew deps, fullclean, set-target esp32s3 install.sh — build and flash with baked-in WiFi credentials: writes sdkconfig.local, reconfigure, build, flash Bails early if setup hasn't been run. Previously install.sh tried to do both, and build_flash_idf.sh ran set-target on every invocation (triggering full rebuilds). Now setup is explicit and only done once. * fix: increase MANIFEST_MAX_ID from 32 to 64 for Roon zone IDs Roon zone IDs are UUIDs (36 chars, e.g. 1601a5e3-9b43-4b64-8f44-3964e129d3ee). MANIFEST_MAX_ID was 32, truncating them during manifest parsing. The truncated ID didn't match anything in bridge_client's zone list, so zone selection silently failed. Without a valid zone_id, fetch_manifest bails, no manifest loads, and subsequent taps of the zone label find no list screen — does nothing. Reverts the MANIFEST_MAX_SCREENS bump (4→8) from the previous commit — the bridge always sends exactly 2 screens (media + zones list), regardless of zone count. That was a red herring. * fix: address review nits from CodeRabbit - Quote CMake variable expansions in staleness guard for path robustness - Remove redundant idf.py reconfigure from install.sh (build suffices) - Remove duplicate CONFIG_LWIP_NETIF_HOSTNAME=y entry - Remove extra blank line in CMakeLists.txt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Muness Castle <931+muness@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: increase UDP zone_id from 32 to 64 bytes for Roon IDs Roon zone IDs are 41 characters (e.g., roon:1601...c231) but the UDP wire protocol used zone_id[32], truncating them to 31 chars. This broke UDP fast-path polling and volume commands for Roon zones. Closes #150 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review findings for UDP zone_id change - Update test_udp.py to use 64-byte zone_id (was hardcoded to 32) - Add _Static_assert for all three UDP struct sizes - Improve comments: mention max backend ID size, not just Roon Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add _Static_assert for UDP_CMD_SIZE (without value field) CodeRabbit caught that UDP_CMD_SIZE (command without float value) was not covered by compile-time assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: WiFi failover race condition + captive portal network management 1. Race condition: connect_now() called esp_wifi_set_config() while the driver was still connecting to the previous SSID, causing "sta is connecting, cannot set config" errors. Fix: disconnect before applying new config. 2. Captive portal (AP mode) now shows saved WiFi networks with Remove buttons, so users can delete stale/wrong SSIDs when the device can't connect. Previously this was only available in the config server (STA mode), which is unreachable when WiFi is broken. Refs #152 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review — XSS, POST for removal, assert comment - Escape SSIDs in HTML output (captive portal + config server) to prevent XSS from rogue AP names containing <, >, &, ", ' - Change captive portal wifi-remove from GET to POST to prevent accidental deletion by prefetchers/link scanners - Clarify UDP_CMD_SIZE assert assumes 'value' is the last field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Zone picker fixes: - Rotary encoder scroll moves visual highlight and auto-scrolls - Current zone pre-selected and scrolled into view when picker opens - List padding prevents item clipping at circular display bezel edges - Selecting the current zone closes picker silently (no "Loading zone...") - Zone label stays hidden when display wakes while picker is open WiFi fix: - Cancel pending retry timer on successful connection (GOT_IP) — the timer could fire after WiFi connected, disconnecting a working link and causing a reconnect loop Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add hiphi frame — e-ink now-playing display for PhotoPainter board New ESP-IDF app (frame_app/) targeting the Waveshare ESP32-S3-PhotoPainter with its 7.3" 800x480 4-color e-ink display. Reuses common/ bridge client and platform HAL. Includes: SPI e-ink driver, Floyd-Steinberg dithering to 4-color palette, bitmap font renderer, 3-button input, AXP2101 PMIC battery monitoring, WiFi provisioning via captive portal, and debounced e-ink refresh (180s cooldown per Waveshare recommendation). Tested on hardware: boots, connects to bridge, downloads and dithers album art, renders to e-ink panel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: BLE media remote, zone/BLE web UI, art-centered e-ink layout Add BLE HID host (NimBLE HOGP) for media remote control — scan, pair, bonding with NVS persistence. Consumer Control HID keys (play/pause, next, prev, volume) mapped to bridge commands. Extend captive portal with STA-mode web server for zone picker and BLE remote configuration. Zone API exposed from bridge_client for web UI. Overhaul e-ink layout: ~450x450 centered album art with slim track/artist bar at bottom, BLE/bridge status icons in bottom-right corner. Builds clean for ESP32-S3 with WiFi + BLE coexistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review findings — thread safety, XSS, favicon - bridge_client: copy zone_id/cfg under lock instead of returning raw ptr - bridge_client: set_zone copies locals before unlocking for save/UI call - ble_remote: add mutex protecting all BLE state, volatile for cross-task bools - ble_remote: fix unpair double-free with s_unpair_pending flag - ble_remote: bounded reconnect attempts (max 20), device_name copies under lock - captive_portal: XSS prevention via is_safe_url() + html_escape() - captive_portal: WiFi form uses POST, password no longer logged - captive_portal: strtol for BLE pair index, larger zone page buffer - captive_portal: add favicon (same icon as unified-hifi-control) - eink_ui: bridge/BLE icons use red when disconnected for visibility - sdkconfig: document CONFIG_BT_NIMBLE_HID_SERVICE requirement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit findings — 4 critical, 8 major Critical: - dns_server: bounds check before writing 16-byte answer section - eink_font: fix font_24 scaling (12/8=1 truncation) with nearest-neighbor sampling - eink_ui: cache dithered artwork in PSRAM, blit on non-art-dirty renders - platform_storage: use sizeof(*out) not stored_len for nvs_get_blob capacity Major: - ble_remote: clear s_scanning on semaphore timeout to prevent stuck state - dns_server: bounds check before rx_buf[qpos] in domain logging - dns_server: use semaphore instead of blind vTaskDelay for task stop - eink_display: null check s_fb in set_pixel/get_pixel - platform_http: dynamic buffering for chunked transfer encoding - platform_input: split GPIO config — pull-down for active-high PWR pin - platform_input: promote timer handle to static, implement proper shutdown - platform_mdns: check addr.type == ESP_IPADDR_TYPE_V4 before using ip4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reduce e-ink cooldown, strip buttons, add web restart - Reduce render cooldown from 180s to 30s, debounce from 5s to 3s - Strip physical buttons to essentials: BOOT long=WiFi AP, GP4 long=restart - Add POST /api/restart endpoint and restart button to zones + BLE pages - Add cooldown diagnostic logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: enable PMIC ALDO power rails for e-ink panel, add 180° rotation Root cause: e-ink panel had no power — AXP2101 ALDO1-4 outputs were never enabled, so the panel ignored all SPI commands and BUSY pin stayed HIGH. Display appeared to "refresh" in 40ms but nothing actually happened on screen. - Configure PMIC ALDO1-4 to 3.3V and enable before display init - Move PMIC init before e-ink init in boot sequence - Add 180° framebuffer rotation so display is right-side up with the frame leg at the bottom - Panel refresh now takes ~19s (real ACeP refresh) with working BUSY pin signaling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: enable all 6 ACeP colors (add blue+green), stop volume-triggered refreshes The dither palette only had 4 colors (black, white, yellow, red), causing blue to render as gray. Expanded to full 6-color ACeP palette with correct panel hardware indices (Blue=5, Green=6). Also removed volume changes from triggering full e-ink refreshes since volume isn't displayed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: deep sleep, checkerboard dither, 180s cooldown, e-ink hardware docs - Panel enters deep sleep (0x07/0xA5) after each refresh, wakes via hardware reset before next refresh — reduces idle power to ~µA - Add checkerboard dithering as alternative to Floyd-Steinberg; cleaner pattern at 127 PPI where individual pixels are visible - Increase refresh cooldown from 30s to 180s per Waveshare recommendation for panel longevity - Add docs/frame/hw-reference/eink_panel.md with full Spectra 6 panel reference (commands, color indices, limitations, dither notes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * revert: switch back to Floyd-Steinberg dithering Checkerboard dithering produced visibly worse quality. Revert to Floyd-Steinberg error diffusion. Checkerboard function remains available in eink_dither.c for future experimentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: BLE pair button layout, revert to Floyd-Steinberg dithering - Add missing .device form CSS rule (display:inline;margin:0) matching .zone form — fixes Pair button being invisible/non-functional in flex layout - Revert from checkerboard to Floyd-Steinberg dithering (better quality) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: BLE pairing — initiate security before GATT reads esp_hidh's nimble_hidh.c has no security handling. When connecting to HID devices that require encryption (like the FiiO RM3), GATT reads of the HID Report Map fail with INSUFFICIENT_ENCRYPTION (status 271) and the connection drops. Fix: register a system-wide NimBLE GAP event listener that calls ble_gap_security_initiate() immediately on BLE_GAP_EVENT_CONNECT. This triggers pairing/bonding before esp_hidh attempts protected reads. Also enable USB Serial/JTAG console so logs appear over USB. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: BLE remote commands — skip usage check, revert to UART console The esp_hidh report map parsing fails due to GATT reads happening before encryption completes (status=14), leaving all reports with usage=0. Since BLE remotes only send consumer control, pass all HID input directly to the consumer control handler. Also revert console from USB_SERIAL_JTAG to UART — USB_SERIAL_JTAG causes flash reliability issues (connection drops mid-write). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address 24 CodeRabbit review findings across 11 files BLE remote: - Read bonded device state under mutex in reconnect_task (#22) - Check xTaskCreate return in scan_start to avoid stuck s_scanning (#23) - Increase unpair delay and keep s_unpair_pending until free (#20) Captive portal: - Fix get_form_field substring matching (check preceding '&') (#21) - Check save result before sending success response (#24) - HTML-escape bridge_url in href attributes to prevent XSS (#25) - Clamp snprintf pos to prevent OOB writes on truncation (#26) E-ink: - Only clear art_dirty on successful artwork render (#29) - Fix stale refresh time comments, bounds-check doc (#27, #43) - Guard eink_scale_bilinear against zero dimensions (#44) - Suppress unused volume_step parameter warning (#45) HTTP: - Cap response buffer growth at 512KB (#30) - Fix signed integer overflow in gzip shift operations (#31) - Cap gzip uncompressed size at 2MB (#32) - Add missing error log on image read failure (#33) Platform: - Check esp_timer_create/start return values (#34) - Make s_mdns_ready volatile for cross-task visibility (#35) - Fix .local suffix check to match end-of-string only (#36) - Log oversized NVS blob case (#37) Main: - Guard against double-init of BLE and STA server on reconnect (#39) - Check xTaskCreate return for UI loop task (#40) PMIC: - Initialize ldo_ctrl and check I2C read result (#41) - Fix ADC comment: 1mV/LSB, not 1.1mV (#42) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: don't blank e-ink when artwork download fails If artwork download fails and there's no cached art, skip the render entirely. It's e-ink — whatever's on screen stays, which is always better than a blank white panel. Closes #159 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: e-ink blank screen, add bridge eink_acep6 pre-processing, full-width art Three changes: 1. Fix e-ink going blank (#159): eink_display_init_panel() was clearing the framebuffer on every panel wake from deep sleep, wiping artwork that had just been drawn. Also stop text-only state changes (status, zone, message, track name, battery) from triggering 20s e-ink refreshes — only artwork changes trigger renders now. Cancel pending renders when artwork is cleared (nothing playing) to keep last image on screen. 2. Bridge pre-processing: request format=eink_acep6 (4-bit packed, panel hardware color indices) from the bridge. Falls back to format=rgb565 with on-device Floyd-Steinberg dithering if the bridge returns an unexpected size. Eliminates all image processing on the ESP32 when bridge supports it. 3. Full-width artwork: expand from 450x450 centered square to 800x450 (full panel width, 30px text bar at bottom). Bridge handles crop-to-fill for non-matching aspect ratios. Closes #159 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address 6 CodeRabbit review findings - bridge_client: null/size guard in bridge_client_get_zones - ble_remote: check ble_gap_conn_find return before using desc - platform_input: delete timer handle if start_periodic fails - eink_ui: free img_data on rgb565 download failure (leak fix) - eink_dither: guard src_w/src_h against zero in scale_bilinear - docs: fix SPI typo CPHL → CPHA Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address remaining 9 CodeRabbit review findings - captive_portal: increase wifi_html buffer 512→1024, loop strstr for substring-safe field matching - platform_input: clarify poll_button calls update state for restart check - pmic_axp2101: log failures on all pmic_write_reg calls - main_frame: add s_mdns_initialized double-init guard - platform_storage: fix stale "identical" comment, verify bridge_base and zone_id on config save (not just ssid) - eink_panel.md: fix CPHL→CPHA typo, add language IDs to code blocks, update dither pipeline docs for eink_acep6 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: eliminate double-write in artwork render pipeline render_artwork() was writing pixels to both the framebuffer AND the art cache, then render_full_screen() would clear the FB and re-blit from cache — effectively writing every artwork pixel twice. Now cache_packed_artwork() and decode_rgb565_artwork() only populate the cache; render_full_screen() handles the single write to FB. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: show album name in e-ink text bar Parse line3 (album) from bridge /now_playing response and display it in the bottom text bar as "Track - Artist - Album". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Auto-detect firmware directory (esp_dial or idf_app) so CI works for both master (idf_app) and v4/feature branches (esp_dial rename). Also adds v4 to pull_request branch targets and workflow_dispatch trigger. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WiFi passwords with special characters were silently corrupted during captive portal setup. URL-encoded data (e.g. ! -> %21) was truncated to fit the 65-byte output buffer before decoding, so a 63-char password with special chars could expand to ~107 encoded bytes, get chopped, then decode to ~40 garbage characters causing "Wrong Password" failures. Fix: decode into an intermediate 256-byte buffer first, then truncate the decoded result to fit the output buffer. Closes #179 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughget_form_field() in multiple apps now copies the URL-encoded substring into a fixed temporary buffer, URL-decodes there, then truncates/copies the decoded text into the caller buffer. Several places switched from Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
idf_app/main/config_server.c (1)
95-100: Prefer sizing the temporary encoded buffer from max decoded field size (not a magic 256).For this file,
get_form_field()is used with buffers up to 129 bytes (bridge). A fixed 256-byte encoded buffer is not a full 3x worst-case envelope for that decoded size, so pre-decode truncation risk can reappear.Suggested fix
- char encoded[256]; + // 3x expansion worst case for percent-encoding + NUL + char encoded[129 * 3 + 1];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@idf_app/main/config_server.c` around lines 95 - 100, The temporary encoded buffer is a fixed 256 bytes which can be too small for URL-encoded forms when the decoded field (e.g. buffers used by get_form_field() such as bridge) approaches its maximum; change the encoded buffer sizing to derive from the incoming decoded length instead of the magic 256—allocate (or ensure) encoded_size = decoded_len * 3 + 1 (or dynamically allocate that size) and then use that encoded_size in the subsequent checks that currently reference encoded/sizeof(encoded); update references in get_form_field() to use the computed encoded buffer and keep the existing truncation logic that sets len = encoded_size - 1 when needed so pre-decode truncation cannot reoccur.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frame_app/main/captive_portal.c`:
- Around line 178-183: The truncation math can underflow when out_len == 0;
before computing decoded_len = out_len - 1 or doing memcpy/out[decoded_len] =
'\0', add a guard that checks if out_len == 0 and handle it (e.g., return 0 or
set decoded_len = 0 and skip copies/writes); update the block that manipulates
decoded_len, memcpy(out, encoded, decoded_len) and out[decoded_len] = '\0' to
only perform the subtraction, copy and null-terminate when out_len > 0 to avoid
underflow and invalid writes (refer to variables decoded_len, out_len, encoded,
out in this function).
---
Nitpick comments:
In `@idf_app/main/config_server.c`:
- Around line 95-100: The temporary encoded buffer is a fixed 256 bytes which
can be too small for URL-encoded forms when the decoded field (e.g. buffers used
by get_form_field() such as bridge) approaches its maximum; change the encoded
buffer sizing to derive from the incoming decoded length instead of the magic
256—allocate (or ensure) encoded_size = decoded_len * 3 + 1 (or dynamically
allocate that size) and then use that encoded_size in the subsequent checks that
currently reference encoded/sizeof(encoded); update references in
get_form_field() to use the computed encoded buffer and keep the existing
truncation logic that sets len = encoded_size - 1 when needed so pre-decode
truncation cannot reoccur.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3671a5e4-37ba-4e9f-89a5-5f9ec51068d8
📒 Files selected for processing (3)
frame_app/main/captive_portal.cidf_app/main/captive_portal.cidf_app/main/config_server.c
| size_t decoded_len = strlen(encoded); | ||
| if (decoded_len >= out_len) { | ||
| decoded_len = out_len - 1; | ||
| } | ||
| memcpy(out, encoded, decoded_len); | ||
| out[decoded_len] = '\0'; |
There was a problem hiding this comment.
Guard zero-length destination buffers before truncation math.
At Line 179, decoded_len = out_len - 1 underflows when out_len == 0, which can turn this into an invalid large copy/write path.
Suggested fix
+ if (out_len == 0) {
+ return false;
+ }
size_t decoded_len = strlen(encoded);
if (decoded_len >= out_len) {
decoded_len = out_len - 1;
}
memcpy(out, encoded, decoded_len);
out[decoded_len] = '\0';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frame_app/main/captive_portal.c` around lines 178 - 183, The truncation math
can underflow when out_len == 0; before computing decoded_len = out_len - 1 or
doing memcpy/out[decoded_len] = '\0', add a guard that checks if out_len == 0
and handle it (e.g., return 0 or set decoded_len = 0 and skip copies/writes);
update the block that manipulates decoded_len, memcpy(out, encoded, decoded_len)
and out[decoded_len] = '\0' to only perform the subtraction, copy and
null-terminate when out_len > 0 to avoid underflow and invalid writes (refer to
variables decoded_len, out_len, encoded, out in this function).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
idf_app/main/captive_portal.c (1)
130-147: LGTM - correctly fixes the URL-decoding truncation issue.The new approach properly decodes into the 256-byte intermediate buffer first, then truncates the decoded result to fit the output buffer. This prevents percent-encoded sequences from being split during truncation.
One minor defensive consideration: if
out_lenwere ever 0, line 143 would underflow (out_len - 1wraps toSIZE_MAX). Current callers always pass valid sizes (33 and 65), so this isn't exploitable today, but a guard could prevent future misuse.🛡️ Optional defensive check
static bool get_form_field(const char *data, const char *field, char *out, size_t out_len) { + if (out_len == 0) return false; char search[64];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@idf_app/main/captive_portal.c` around lines 130 - 147, Add a defensive guard so out_len cannot underflow when truncated: in the function that does URL decoding (the block using encoded[], url_decode(encoded), size_t decoded_len, and out_len), check if out_len == 0 before computing out_len - 1 and either return false or handle it safely; then compute decoded_len = min(decoded_len, out_len - 1) and proceed to memcpy/out[decoded_len] = '\0'. This prevents the underflow when out_len is zero while preserving the existing truncation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@idf_app/main/captive_portal.c`:
- Around line 130-147: Add a defensive guard so out_len cannot underflow when
truncated: in the function that does URL decoding (the block using encoded[],
url_decode(encoded), size_t decoded_len, and out_len), check if out_len == 0
before computing out_len - 1 and either return false or handle it safely; then
compute decoded_len = min(decoded_len, out_len - 1) and proceed to
memcpy/out[decoded_len] = '\0'. This prevents the underflow when out_len is zero
while preserving the existing truncation behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4dd6dd50-9862-459e-9f97-ec4ea88d7c81
📒 Files selected for processing (4)
common/rk_cfg.hframe_app/main/captive_portal.cidf_app/main/captive_portal.cidf_app/main/config_server.c
🚧 Files skipped from review as they are similar to previous changes (2)
- idf_app/main/config_server.c
- frame_app/main/captive_portal.c
Newer GCC (esp-14.2.0_20260121 in CI) raises -Wstringop-truncation on strncpy calls where it can prove the source length might equal the copy limit. The existing strncpy usage is correct (always null-terminated), so demote to warning rather than doing a risky global strncpy replacement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
002b5c1 to
9a0408e
Compare
Firmware PreviewFlash this PR's firmware directly in your browser: Updated: 2026-03-21T13:45:20.728Z |
|
Superseded — fix applied directly to master after branch reorganization. Current master is now based on v2.5.0 (stable, compatible with bridge 3.3.2). The manifest UI work is on the v4-manifest-ui branch. |
Summary
get_form_field()— no other refactoringReplaces #180 (which bundled an unrelated strncpy→rk_strlcpy refactor that broke bridge communication).
Test plan
!@#$%^&*) — should now connect/wifi-addPOST endpoint also works with long passwordsCloses #179
🤖 Generated with Claude Code
Summary by CodeRabbit