Skip to content

Serve a diagnostic frame when camera init fails - #40

Open
misonyah wants to merge 3 commits into
EyeTrackVR:mainfrom
misonyah:feature/camera-fault-diagnostic-frame
Open

Serve a diagnostic frame when camera init fails#40
misonyah wants to merge 3 commits into
EyeTrackVR:mainfrom
misonyah:feature/camera-fault-diagnostic-frame

Conversation

@misonyah

@misonyah misonyah commented Aug 8, 2026

Copy link
Copy Markdown

Summary

  • When esp_camera_init() fails (e.g. sensor not seated in its socket), the stream server currently skips registering / entirely, so clients just lose the video feed with no indication of why.
  • This renders the returned esp_err_t as a small hex code using a hand-rolled 5x7 pixel font, tiled in a 3x3 grid across the frame with each tile independently rotated 0/90/180/270 degrees, and serves it through the existing multipart stream path instead.
  • A single large centered label isn't reliable: eye-tracking clients (Baballonia, EyeTrackVR, etc.) crop/zoom toward wherever they guess the pupil is, which can clip a label entirely or blow it up into an unreadable blob depending on the crop's rotation. Tiling with per-tile rotation means at least one instance should land inside the crop window right-side-up.
  • Adds components/CameraManager/DIAGNOSTIC_ERROR_CODES.md documenting which esp_err_t codes are actually reachable through this path.
  • Camera init now auto-retries every 5s while it hasn't succeeded, instead of only running once at boot. StreamServer also re-checks camera state on every loop iteration of an open connection (not just once when the connection starts), so an already-connected client automatically switches from the diagnostic frame to live video the moment the camera recovers - no reboot or reconnect needed.
  • Fixes a bug the retry feature exposed: StateManager never sent an LED event on camera recovery, and LEDManager::updateState() unconditionally refused to leave any error state once entered - together the status LED would show CameraError forever even after the camera actually recovered and was streaming live video. Now the LED clears back to idle on recovery, while still staying sticky against other errors overriding a real camera fault.

Known limitation

Currently hardcoded to the 240x240 grayscale frame this board's CameraManager config uses, not derived from the configured camera_config_t. Boards with a different resolution/pixel format would need to adjust FRAME_DIM/PIXFORMAT_GRAYSCALE accordingly - happy to make this dynamic if that's a blocker for merging.

Test plan

  • Built and flashed to a XIAO ESP32S3 Sense board with a physically disconnected/unseated camera sensor
  • Confirmed via serial log that the diagnostic frame is generated on init failure
  • Confirmed via curl and by pulling a frame out of the live MJPEG stream that the tiled, rotated hex code renders correctly
  • Confirmed in Baballonia that at least one tile is readable regardless of its crop/zoom window
  • Confirmed via serial log that the retry task fires every 5s and re-attempts esp_camera_init() while the camera hasn't recovered
  • Confirmed on a second, known-working camera module that live streaming and the LED recovery both work correctly

When esp_camera_init() fails (e.g. sensor not seated in its socket), the
stream server used to just skip registering "/" entirely, so clients lost
the video feed with no indication of why.

CameraManager now renders the esp_err_t as a small hex code using a
hand-rolled 5x7 pixel font, tiled in a 3x3 grid across the frame with each
tile independently rotated 0/90/180/270 degrees. A single large centered
label isn't reliable here: eye-tracking clients (Baballonia, EyeTrackVR,
etc.) crop/zoom toward wherever they guess the pupil is, which can clip a
single label entirely or blow it up into an unreadable blob at whatever
rotation the crop applies. Tiling with per-tile rotation means at least
one instance should land inside the crop window right-side-up. The frame
is encoded once via fmt2jpg and served on a loop through the existing
StreamServer multipart path.

Adds DIAGNOSTIC_ERROR_CODES.md documenting which esp_err_t codes are
actually reachable through this path (and which esp32-camera codes
aren't, since the frame only renders 3 hex digits).

Currently hardcoded to the 240x240 grayscale frame this board's
CameraManager config uses - not derived from the configured
camera_config_t, so boards with a different resolution/pixel format
would need to adjust FRAME_DIM/PIXFORMAT_GRAYSCALE accordingly.
Camera init previously only ran once at boot; if it failed there was no
way to recover without a manual reboot. CameraManager now spawns a
background task that retries setupCamera() every 5s while it hasn't
succeeded, and StreamServer re-checks camera state on every loop
iteration (not just once at connection start) so an already-open stream
picks up live video automatically the moment the camera comes up, no
client reconnect required.
StateManager never sent an LED event on Camera_Success, and
LEDManager::updateState() unconditionally refused to leave any error
state once entered. Both were fine when a camera failure was permanent
until reboot, but now that CameraManager retries and can genuinely
recover, the LED needs to reflect that instead of showing CameraError
forever after the camera is already back and streaming live video.

StateManager now sends LedStateNone on Camera_Success. LEDManager's
error guard switches to the already-sketched-out "recoverable" variant:
error states still block other error states from overriding them, but a
non-error transition (like the recovery signal above) is allowed through.
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