All notable changes to this project will be documented here. Format follows Keep a Changelog.
- OTA rollback-confirmation crash —
esp_ota_mark_app_valid_cancel_rollback()was deferred via a 2-minute health timer, by which point Wi-Fi/MQTT were already active. The resultingotadataflash erase disables the OPI PSRAM cache; two coredumps confirmed the Wi-Fi task itself was corrupted by the cache-disable race, even with the camera not yet initialized — proving the hazard isn't camera-GDMA-specific. Fix: moved the confirmation intorecovery_mgr_init(), in the same pre-Wi-Fi window the boot-loop NVS write has always used safely. - OTA SHA-256 hashing failure — one-shot
psa_hash_compute()over the ~1.3 MB firmware image failed withPSA_ERROR_INSUFFICIENT_MEMORY(only ~77 KB internal heap free at OTA time). Fix: hash incrementally in 16 KB chunks viapsa_hash_setup/update/finish. /setupPOST body truncation —httpd_req_recv()returned partial reads for bodies spanning multiple TCP segments, silently truncating form fields. Fix: loop until the full (capped) body is received.- Frame pool init leak — a failed allocation partway through
frame_pool_init()leaked the mutex/semaphore/already-allocated PSRAM buffers. Fix: clean up everything allocated so far on failure. - JPEG EOI search UB — the backward
0xFF 0xD9search incam_hal.cbuilt a pointer that could decrement past the start of the buffer when no marker was found. Fix: rewritten with bounded unsigned index arithmetic.
/setupXSS hardening — MQTT URL/user/pass and device ID are now HTML-escaped before being embedded in the/setuppage, preventing a malicious config value from injecting markup.
config_mgrvalidation — resolution/quality are now validated at every setter, not just NVS load;config_mgr_save()aborts without committing if anynvs_set_*call fails.- README — go2rtc example switched to a direct passthrough restream (needed for Frigate VOD/timeline) instead of the ffmpeg transpose re-encode pipeline.
- Frame pool exhaustion under concurrent snapshot + stream — with 3 pool slots,
a simultaneous
GET /snapshot (holding a ref while sending ~150 KB over HTTP) combined with a worker mid-send on the previous frame exhausted all slots. The broadcaster stalled for up to ~5 s, causing Frigate's 20 s ffmpeg watchdog to fire and restart the capture thread. Fix: pool size increased from 3 → 5 slots (2.5 MB PSRAM). With 5 slots, snapshot(1) + worker(1) + broadcaster(1) still leaves 2 free slots, eliminating the exhaustion window entirely.
- Recovery manager crash loop —
recovery_mgr_init()was called after Wi-Fi and MQTT were already running. Itsnvs_commit()disables the OPI PSRAM cache; concurrent PSRAM access by the Wi-Fi task caused ExcCause=7 → crash → boot loop. Fix: movedrecovery_mgr_init()to beforewifi_init_sta(), while no PSRAM-accessing tasks are alive. Root cause confirmed via coredump analysis. - IDF v6 CI build failure —
sha256_buffer()used the legacymbedtls_sha256_*context API removed in mbedTLS 4.0 (IDF v6). Replaced withpsa_hash_compute()from the PSA Crypto API, compatible with both IDF v5 (mbedTLS 3.x) and IDF v6 (mbedTLS 4.0).
- OTA token authentication —
unitcams3/ota/setnow accepts a JSON payload{"url":"...","token":"...","sha256":"..."}. If an OTA token is configured (via/setuporCONFIG_UNITCAMS3_OTA_TOKEN), the token field is required and verified before the OTA is initiated; mismatched or missing tokens are silently rejected. Empty token keeps legacy bare-URL behavior. - OTA SHA-256 verification — the optional
sha256JSON field (64 hex chars) is saved toRTC_NOINIT_ATTRalongside the URL. On the next boot, after the full firmware image is downloaded into a PSRAM buffer, its SHA-256 is computed and compared against the saved hash before any flash write occurs. Mismatch aborts the update. - Coredump Bearer auth —
GET /api/coredumpnow requires anAuthorization: Bearer <token>header when a Coredump Token is configured (via/setuporCONFIG_UNITCAMS3_COREDUMP_TOKEN). Unauthenticated requests receive401 Unauthorizedwith aWWW-Authenticatechallenge. Empty token leaves the endpoint open (default). /setuptoken management — OTA Token and Coredump Token fields added to the browser configuration page. Token values are never reflected in the HTML response (placeholder shows "(saved)" instead of the actual value). Submitting the form without touching a token field preserves the existing value.
- Configurable broadcaster FPS cap —
unitcams3/fps_cap/setMQTT command (payload0–15;0= unlimited). Broadcaster sleeps the remainder of each frame interval;CAMERA_GRAB_LATESTdiscards stale frames while sleeping. HAnumberentity. Current value reflected in/statsasfps_cap. - LED control via MQTT — GPIO 14 exposed as
unitcams3/led/set(1/0). HAlightentity. GPIO access kept inmain.cvia registered callback. - Wi-Fi re-provisioning via MQTT —
unitcams3/reprovisioncommand clears NVS provisioning data and reboots into BLE provisioning mode. broadcast_fpsin/stats— delta of broadcaster frame counter; distinct fromfps(ISR VSYNC rate).frames_deliveredcounter — cumulative frames sent to all MJPEG clients since boot; exposed in/stats.- Long-term soak metrics —
internal_minandpsram_min(minimum free since boot) tracked in/stats;heap_minpublished to MQTT every 10 s. - Task stack HWM — broadcaster and HTTP task HWMs reported in
/statsasstack_hwm.broadcaster_words/http_task_words; per-worker minimum inworker_min_words.
- IDF v6 re-provisioning API —
wifi_v6.cupdated to usenetwork_prov_mgr_reset_wifi_provisioning()(v6 renamed fromnetwork_prov_mgr_reset_provisioning()); fixes v6 CI build failure. - IPC task stack overflow —
CONFIG_ESP_IPC_TASK_STACK_SIZEraised from 1280 to 2048 bytes. The previous size caused a double-exception crash duringnvs_commit()at boot when GPIO was initialized insidemqtt_mgr. esp_driver_gpioremoved frommqtt_mgr— GPIO access moved to a registered callback inmain.c, eliminating the dependency that changed binary layout and triggered the IPC stack overflow.
- Zero-copy MJPEG delivery — refactored
frame_poolto use C11 atomic reference counting. Multiple clients now stream from the same PSRAM buffer simultaneously, reducing bus load by ~80%. - Worker reliability — implemented
SO_SNDTIMEO(10s) on stream sockets. Stalled or slow clients now fail gracefully without hanging worker tasks. - Optimized snapshots — the
/endpoint now grabs a reference to the active broadcaster frame if available, eliminating hardware contention during streams.
- Watchdog safety — re-enabled Task WDT Panic for production auto-recovery.
- Broadcaster stability — added Task WDT registration and removed artificial frame pacing; broadcaster now runs at max camera speed with pull-based worker pacing.
- Memory safety — increased main task stack to 16KB and system event stack to 4096B; forced all task stacks to internal SRAM to prevent flash-write crashes.
- Zero-copy MJPEG streaming —
frame_poolnow uses C11<stdatomic.h>atomic reference counting. The broadcaster captures one frame; all worker tasks share a reference to the same PSRAM buffer. Eliminates 5 concurrentmemcpycalls per frame, reducing PSRAM bus load by ~80%. - Pull-based pacing — removed the artificial 100 ms broadcaster delay. The broadcaster runs at full camera speed (~9.4 fps); each client worker's TCP send latency acts as its own rate limiter. A slow Frigate client always gets the latest available frame via binary semaphore coalescing.
- Unified snapshot (
GET /) — when the broadcaster is active, the snapshot endpoint grabs a reference to the current stream frame instead of callingesp_camera_fb_get()independently. Zero hardware contention with the stream. SO_SNDTIMEOon worker sockets (10 s) — a stalled client causeshttpd_resp_send_chunkto time out and the worker to exit cleanly, freeing its slot and pool reference without hanging the device.- Broadcaster Task WDT —
mjpeg_broadcaster_taskregistered with Task Watchdog; triggers 30 s panic if the broadcaster hangs. IP_EVENT_STA_LOST_IPhandler — device now reconnects if IP is lost silently due to DHCP failure (previously only reconnected on explicit disconnect events).- Stress test script —
test_stream_stress.pyopens N simultaneous streams, reports per-client FPS, bad JPEG count, max frame gap, and device-side stats.
- Wi-Fi TX power cap removed —
esp_wifi_set_max_tx_power(32)(8 dBm) was the primary cause of 105 Wi-Fi disconnects. Removing it restored full 20 dBm TX power. RSSI at AP improved from −56 dBm to −27 dBm; disconnect count dropped to 0. - Disconnect reason logged —
WIFI_EVENT_STA_DISCONNECTEDnow logs the reason code (beacon_timeout,auth_expire, etc.) for field diagnostics. - Reconnect backoff — 1 s delay before
esp_wifi_connect()retry prevents rapid reconnect storms against the AP. - OTA MQTT subscription QoS 0 → 1 —
unitcams3/ota/setsubscription now uses QoS 1 so the broker retries delivery after a brief MQTT reconnect; QoS 0 messages published during a disconnect window were silently lost. - Frame pool reduced 8 → 3 slots — with atomic ref counting, peak simultaneous usage is 2 slots (broadcaster current + workers mid-send, all sharing one ref). 3rd slot covers the transitional swap. Frees 2.5 MB PSRAM (4 MB → 1.5 MB).
- Dead
STREAM_FRAME_INTERVAL_MSdefine removed — unused constant left over from pre-pull-pacing design. - Stale comment in
ota_mgr.c— updated frame pool size reference from4×512KBto3×512KB.
- Production hardening — re-enabled Task Watchdog Panic (
CONFIG_ESP_TASK_WDT_PANIC=y) for auto-recovery from hangs. - Main task stack — increased
main_taskstack size to 16KB to prevent startup overflows during heavy initialization. - Documentation — formalized Memory Segregation rules in ARCHITECTURE.md.
- Multi-client MJPEG streaming — support for up to 5 simultaneous stream clients
(e.g., Frigate
detect+record+ browser view) using asynchronous HTTP handlers and a dedicated broadcaster task. active_streamsmetric — added to/statsJSON and MQTT telemetry; includes Home Assistant auto-discovery as a new sensor.- Broadcaster idling —
mjpeg_broadcaster_tasknow idles when 0 clients are connected, preserving PSRAM bandwidth and reducing heat. - Python test script —
test_mjpeg_concurrency.pyadded for verifying multi-client performance.
- Internal RAM for stacks — forced all task stacks to internal SRAM to prevent Double Exception crashes during flash writes (NVS/OTA).
- Startup stability — increased main task stack to 16KB and system event stack to 4096B to prevent overflows during mDNS and MQTT initialization.
- mDNS unique instances — assigned unique instance names ("HTTP", "Stream") to prevent "Service already exists" errors on port 81.
- Race condition fix — delayed telemetry task until MQTT is connected to ensure all mutexes are initialized before use.
- Thread-safe camera recovery — added a mutex to
camera_reinit()to prevent concurrent driver de-initialization crashes. - Frame pool capacity — increased from 4 to 8 buffers (4MB total PSRAM) to smoothly handle multiple simultaneous consumers.
- mDNS — device advertises at
<device-id>.localusing the espressif/mdns managed component. Hostname tracks the Device ID set on/setup. - Web log viewer (
GET /api/logs) — 16 KB PSRAM ring buffer captures allESP_LOG*output from boot onward; returned as plain text with ANSI colour codes. Implemented viaesp_log_set_vprintf()hook with ISR-safe spinlock. Gracefully disabled if PSRAM is unavailable at boot. reset_reasonin/health— JSON field showing why the device last rebooted (power_on,software,panic,task_watchdog, etc.).- Device ID tooltip on
/setup— explains that Device ID controls the MQTT topic prefix, Home Assistant entity prefix, and mDNS hostname (<id>.local).
- Resolution enum mismatch —
/setupdropdown values now map to PY260-native framesizes only: QVGA (6), VGA (10), HD (13), UXGA (15). Previously the list included CIF and HVGA which silently fail on this sensor. - Invalid NVS cam_res recovery —
config_mgr_init()validates the stored resolution on boot and resets to VGA if the value is not in the supported set, preventing boot loops from stale NVS written by older firmware. mega_ccmunsupported framesize — added anelsebranch that logsESP_LOGEand returns-1instead of silently succeeding.- MQTT HA discovery ranges — corrected brightness to
0–8, contrast and saturation to0–6to match the PY260 driver limits. assert()inll_cam.creplaced withESP_LOGE+ESP_ERR_INVALID_ARGreturn (assertions are compiled out in release builds).- Deinit ordering in
cam_hal.c—ll_cam_deinit()(frees ISR) now called beforevQueueDelete(). CONFIG_CAMERA_TASK_STACK_SIZEKconfig — default raised to 8192; now wired toCAM_TASK_STACKincam_hal.c.- GDMA DMA buffer size range extended to 65536 in Kconfig.
DEFAULT_CAM_REScorrected from 8 (CIF) to 10 (VGA) inconfig_mgr.c.RECOVERY_ERR_RTSP_SENDrenamed toRECOVERY_ERR_STREAM_SEND.- Orphaned
CONFIG_APP_UPDATE=yremoved fromsdkconfig.defaults.
- GitHub Actions now builds both IDF v5.3.2 and IDF v6.0 on every push.
- Release artifacts include
unitcams3_merged.bin(single-file flash) with build provenance attestation viaactions/attest-build-provenance.
Initial public release.
- Camera driver — Forked ESP32-S3 camera driver tuned for the PY260 (
mega_ccm) sensor on the M5Stack Unit CamS3-5MP. JPEG-only capture; all RAW/RGB/YUV paths removed. - ISR stability — VSYNC ISR pinned to Core 1 via
esp_ipc_call_blocking; full GDMA reset sequence prevents spurious double-VSYNC that truncated frames to 4096 bytes. XCLK hard-limited to 10 MHz (PY260 front-porch constraint). - Frame pool — 4 × 512 KB PSRAM ring buffer decouples camera capture from HTTP delivery.
- JPEG validation — SOI/EOI byte-level check with atomic drop counters exposed via
/stats. - HTTP server (port 80) —
GET /snapshot,GET /health(JSON, includes ELF SHA-256),GET /stats,GET /api/coredump(ELF coredump download). - MJPEG stream (port 81) —
GET /streammultipart MJPEG, compatible with Frigate NVR (input_args: -f mjpeg). - MQTT + Home Assistant discovery — Telemetry every 10 s (RSSI, heap, FPS, drop counters); image controls (brightness, contrast, saturation, WB mode); reboot command.
- BLE Wi-Fi provisioning — First-boot BLE provisioning via
wifi_provisioning; credentials stored in NVS. No hardcoded Wi-Fi credentials. - Runtime config (
/setuppage) — Browser form to change MQTT URL/credentials, device ID, MQTT enable toggle, camera resolution, JPEG quality. Values persist in NVS; device restarts to apply. - OTA updates — URL-based OTA triggered by MQTT (
unitcams3/ota/set). Downloads full firmware to PSRAM, stops Wi-Fi, then flashes — avoids OPI cache-disable crash during concurrent camera DMA. - Recovery manager — NVS boot-loop detection (threshold = 3), safe mode, 2-minute health
timer, OTA rollback confirmation via
esp_ota_mark_app_valid_cancel_rollback. - IDF v5 and v6 support —
build.shtargets IDF v5.3.2 LTS;build_v6.shtargets IDF v6 beta. Both auto-detect version mismatch viaCMakeCache.txtand clean before rebuild. - CI — GitHub Actions workflow builds firmware with IDF v5.3.2 on every push and PR.