diff --git a/components/CameraManager/CameraManager/CameraManager.cpp b/components/CameraManager/CameraManager/CameraManager.cpp index 835a97a..98b2dbe 100644 --- a/components/CameraManager/CameraManager/CameraManager.cpp +++ b/components/CameraManager/CameraManager/CameraManager.cpp @@ -1,7 +1,112 @@ #include "CameraManager.hpp" +#include "esp_heap_caps.h" +#include "freertos/task.h" +#include "img_converters.h" + +#include +#include +#include const char* CAMERA_MANAGER_TAG = "[CAMERA_MANAGER]"; +namespace +{ +// 5x7 pixel font, hex digits 0-F only. Each glyph is 7 rows, bit4 = leftmost column. +// Hand-drawn (not lifted from any font asset) - legibility over accuracy to any real typeface. +constexpr uint8_t FONT_5X7[16][7] = { + {0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110}, // 0 + {0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110}, // 1 + {0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111}, // 2 + {0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110}, // 3 + {0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010}, // 4 + {0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110}, // 5 + {0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110}, // 6 + {0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000}, // 7 + {0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110}, // 8 + {0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100}, // 9 + {0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001}, // A + {0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110}, // b + {0b01111, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b01111}, // C + {0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100}, // d + {0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111}, // E + {0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000}, // F +}; + +constexpr uint16_t FRAME_DIM = 240; +constexpr uint8_t BG_SHADE = 30; +constexpr uint8_t FG_SHADE = 255; + +void fillRect(uint8_t* buf, int x0, int y0, int x1, int y1, uint8_t shade) +{ + x0 = std::max(x0, 0); + y0 = std::max(y0, 0); + x1 = std::min(x1, (int)FRAME_DIM); + y1 = std::min(y1, (int)FRAME_DIM); + for (int y = y0; y < y1; y++) + { + std::memset(buf + (size_t)y * FRAME_DIM + x0, shade, std::max(0, x1 - x0)); + } +} + +// A 3-hex-digit label is a grid of cells GRID_W (17) x GRID_H (7): each character is 5 cols +// wide with a 1-col gap between characters (5+1+5+1+5 = 17), 7 rows tall. +constexpr int GRID_W = 17; +constexpr int GRID_H = 7; + +// Rotates a label-local cell coordinate by 0/90/180/270 degrees (clockwise) around the label's +// own origin. Since rotations are multiples of 90 degrees, grid cells map to grid cells exactly - +// no interpolation needed. rotation is 0-3 (x90 degrees). +void rotateCell(int gx, int gy, int rotation, int* outGx, int* outGy) +{ + switch (rotation & 0x3) + { + case 0: + *outGx = gx; + *outGy = gy; + break; + case 1: // 90 CW + *outGx = GRID_H - 1 - gy; + *outGy = gx; + break; + case 2: // 180 + *outGx = GRID_W - 1 - gx; + *outGy = GRID_H - 1 - gy; + break; + default: // 270 CW + *outGx = gy; + *outGy = GRID_W - 1 - gx; + break; + } +} + +// Draws a 3-hex-digit label rotated by `rotation` (0-3, x90 degrees clockwise), with its +// rotated bounding box's top-left corner at (originX, originY) in the destination buffer. +void drawLabelRotated(uint8_t* buf, int originX, int originY, int scale, int rotation, const uint8_t nibbles[3]) +{ + for (int charIdx = 0; charIdx < 3; charIdx++) + { + const auto& rows = FONT_5X7[nibbles[charIdx] & 0xF]; + for (int row = 0; row < 7; row++) + { + for (int col = 0; col < 5; col++) + { + if (!(rows[row] & (1 << (4 - col)))) + continue; + + const int gx = charIdx * 6 + col; // 5 cols + 1 gap per character + const int gy = row; + int rgx, rgy; + rotateCell(gx, gy, rotation, &rgx, &rgy); + + const int x = originX + rgx * scale; + const int y = originY + rgy * scale; + fillRect(buf, x, y, x + scale, y + scale, FG_SHADE); + } + } + } +} +} // namespace + CameraManager::CameraManager(std::shared_ptr projectConfig, QueueHandle_t eventQueue) : projectConfig(projectConfig), eventQueue(eventQueue) {} void CameraManager::setupCameraPinout() @@ -156,6 +261,7 @@ bool CameraManager::setupCamera() { ESP_LOGI(CAMERA_MANAGER_TAG, "Camera initialized: %s \r\n", esp_err_to_name(hasCameraBeenInitialized)); + this->cameraOk = true; constexpr auto event = SystemEvent{EventSource::CAMERA, CameraState_e::Camera_Success}; xQueueSend(this->eventQueue, &event, 10); } @@ -164,9 +270,10 @@ bool CameraManager::setupCamera() ESP_LOGE(CAMERA_MANAGER_TAG, "Camera initialization failed with error: %s \r\n", esp_err_to_name(hasCameraBeenInitialized)); ESP_LOGE(CAMERA_MANAGER_TAG, "Camera most likely not seated properly in the socket. " - "Please " - "fix the " - "camera and reboot the device.\r\n"); + "Please fix the camera - it'll be retried automatically, no reboot needed.\r\n"); + this->cameraOk = false; + this->lastCameraError = hasCameraBeenInitialized; + this->generateDiagnosticFrame(); constexpr auto event = SystemEvent{EventSource::CAMERA, CameraState_e::Camera_Error}; xQueueSend(this->eventQueue, &event, 10); return false; @@ -191,6 +298,33 @@ bool CameraManager::setupCamera() return true; } +bool CameraManager::isCameraOk() const +{ + return cameraOk; +} + +namespace +{ +void CameraRetryTask(void* param) +{ + auto* self = static_cast(param); + while (true) + { + vTaskDelay(pdMS_TO_TICKS(5000)); + if (!self->isCameraOk()) + { + ESP_LOGI(CAMERA_MANAGER_TAG, "Camera not initialized, retrying..."); + self->setupCamera(); + } + } +} +} // namespace + +void CameraManager::startAutoRetry() +{ + xTaskCreate(CameraRetryTask, "CameraRetryTask", 1024 * 3, this, 1, nullptr); +} + void CameraManager::loadConfigData() { ESP_LOGD(CAMERA_MANAGER_TAG, "Loading camera config data"); @@ -226,4 +360,96 @@ int CameraManager::setVieWindow(int offsetX, int offsetY, int outputX, int outpu { // todo safariMonkey made a PoC, implement it here return 0; +} + +esp_err_t CameraManager::getLastCameraError() const +{ + return lastCameraError; +} + +bool CameraManager::getDiagnosticFrame(const uint8_t** outBuf, size_t* outLen) const +{ + if (!diagnosticJpegBuf || diagnosticJpegLen == 0) + return false; + + *outBuf = diagnosticJpegBuf; + *outLen = diagnosticJpegLen; + return true; +} + +void CameraManager::generateDiagnosticFrame() +{ + if (diagnosticJpegBuf) + { + free(diagnosticJpegBuf); + diagnosticJpegBuf = nullptr; + diagnosticJpegLen = 0; + } + + auto* raw = static_cast(heap_caps_malloc((size_t)FRAME_DIM * FRAME_DIM, MALLOC_CAP_SPIRAM)); + if (!raw) + { + ESP_LOGE(CAMERA_MANAGER_TAG, "Failed to allocate diagnostic frame buffer"); + return; + } + + std::memset(raw, BG_SHADE, (size_t)FRAME_DIM * FRAME_DIM); + + // The error code magnitude fits comfortably in 3 hex digits for every esp_err_t this + // component can produce (e.g. ESP_ERR_NOT_FOUND = 0x105), so we only render 3. + const uint32_t code = static_cast(std::abs(static_cast(lastCameraError))) & 0xFFF; + const uint8_t nibbles[3] = { + static_cast((code >> 8) & 0xF), + static_cast((code >> 4) & 0xF), + static_cast(code & 0xF), + }; + + // Eye-tracking clients (Baballonia et al.) crop/zoom this frame toward whatever they guess + // is the pupil, so a single centered label isn't reliable - it can land almost anywhere, + // rotated or clipped, and blow up into an unreadable blob. + // Tile small instances of the code across the frame in a grid, each at a different 90-degree + // rotation, so whatever crop/rotation the client applies, at least one instance lands both + // inside the crop window AND right-side-up. + constexpr int scale = 3; // pixels per font cell + constexpr int labelPxW = GRID_W * scale; + constexpr int labelPxH = GRID_H * scale; + // Max footprint across all 4 rotations is a square of the longer dimension, so every tile + // gets the same amount of clearance regardless of which rotation lands there. + constexpr int cellSpan = (labelPxW > labelPxH ? labelPxW : labelPxH); + constexpr int centerXs[3] = {14 + cellSpan / 2, FRAME_DIM / 2, FRAME_DIM - 14 - cellSpan / 2}; + constexpr int centerYs[3] = {14 + cellSpan / 2, FRAME_DIM / 2, FRAME_DIM - 14 - cellSpan / 2}; + + int rotation = 0; + for (int cy : centerYs) + { + for (int cx : centerXs) + { + const bool swapped = (rotation & 1) != 0; // 90/270 swap the bounding box's W/H + const int w = swapped ? labelPxH : labelPxW; + const int h = swapped ? labelPxW : labelPxH; + drawLabelRotated(raw, cx - w / 2, cy - h / 2, scale, rotation, nibbles); + rotation = (rotation + 1) & 0x3; + } + } + + // Thin border so it's obviously a diagnostic card rather than a corrupted stream frame. + fillRect(raw, 0, 0, FRAME_DIM, 4, FG_SHADE); + fillRect(raw, 0, FRAME_DIM - 4, FRAME_DIM, FRAME_DIM, FG_SHADE); + fillRect(raw, 0, 0, 4, FRAME_DIM, FG_SHADE); + fillRect(raw, FRAME_DIM - 4, 0, FRAME_DIM, FRAME_DIM, FG_SHADE); + + uint8_t* jpegOut = nullptr; + size_t jpegLen = 0; + const bool ok = fmt2jpg(raw, (size_t)FRAME_DIM * FRAME_DIM, FRAME_DIM, FRAME_DIM, PIXFORMAT_GRAYSCALE, 30, &jpegOut, &jpegLen); + free(raw); + + if (!ok) + { + ESP_LOGE(CAMERA_MANAGER_TAG, "Failed to encode diagnostic frame to JPEG"); + return; + } + + diagnosticJpegBuf = jpegOut; + diagnosticJpegLen = jpegLen; + ESP_LOGI(CAMERA_MANAGER_TAG, "Generated diagnostic frame for error 0x%03lx (%u bytes)", (unsigned long)code, (unsigned)jpegLen); } \ No newline at end of file diff --git a/components/CameraManager/CameraManager/CameraManager.hpp b/components/CameraManager/CameraManager/CameraManager.hpp index 21c952a..cb19dea 100644 --- a/components/CameraManager/CameraManager/CameraManager.hpp +++ b/components/CameraManager/CameraManager/CameraManager.hpp @@ -24,6 +24,11 @@ class CameraManager QueueHandle_t eventQueue; camera_config_t config; + esp_err_t lastCameraError = ESP_OK; + uint8_t* diagnosticJpegBuf = nullptr; + size_t diagnosticJpegLen = 0; + bool cameraOk = false; + public: CameraManager(std::shared_ptr projectConfig, QueueHandle_t eventQueue); int setCameraResolution(framesize_t frameSize); @@ -32,10 +37,28 @@ class CameraManager int setHFlip(int direction); int setVieWindow(int offsetX, int offsetY, int outputX, int outputY); + // Returns the last esp_err_t returned by esp_camera_init(), only meaningful after setupCamera() fails. + esp_err_t getLastCameraError() const; + + // Fetches a cached JPEG that renders getLastCameraError() as large 7-segment-style hex digits, + // so streaming clients (Baballonia/EyeTrackVR/etc.) see the failure instead of just losing the feed. + // Returns false if no diagnostic frame has been generated (e.g. camera never failed). + bool getDiagnosticFrame(const uint8_t** outBuf, size_t* outLen) const; + + // True once setupCamera() has succeeded. Checked by the retry task below and by + // StreamServer to decide whether to serve live frames or the diagnostic fallback. + bool isCameraOk() const; + + // Spawns a background task that keeps retrying setupCamera() every 5s while it hasn't + // succeeded yet (e.g. the sensor was reseated after a failed boot), so the camera can + // recover on its own without a manual reboot. + void startAutoRetry(); + private: void loadConfigData(); void setupCameraPinout(); void setupCameraSensor(); + void generateDiagnosticFrame(); }; #endif // CAMERAMANAGER_HPP \ No newline at end of file diff --git a/components/CameraManager/DIAGNOSTIC_ERROR_CODES.md b/components/CameraManager/DIAGNOSTIC_ERROR_CODES.md new file mode 100644 index 0000000..8d0cfe1 --- /dev/null +++ b/components/CameraManager/DIAGNOSTIC_ERROR_CODES.md @@ -0,0 +1,69 @@ +# Camera diagnostic frame - error code reference + +When `esp_camera_init()` fails, `CameraManager::generateDiagnosticFrame()` renders the +returned `esp_err_t` as a 3-hex-digit code (e.g. `105`) tiled across the fallback MJPEG +frame instead of a live picture, so eye-tracking clients (Baballonia, EyeTrackVR, etc.) +show *something* explanatory instead of a dead feed. + +**Only the low 3 hex digits are shown.** That's enough to disambiguate every code this +component can realistically produce (see table below), but it isn't unique across the +whole `esp_err_t` space - see [Codes outside this range](#codes-outside-this-range). + +## Codes reachable from `esp_camera_init()` + +These come from `esp_camera_init()` (`managed_components/espressif__esp32-camera/driver/esp_camera.c`), +specifically `camera_probe()` / `SCCB_Probe()` for the sensor detection step. + +| Code | Name | Meaning | Typical cause | +|---------|--------------------------|-----------------------------------------------------------------------|----------------| +| `0x105` | `ESP_ERR_NOT_FOUND` | SCCB (I2C) probe got no ACK from any known sensor address | Camera sensor not seated in its socket, bad ribbon/connector, or dead sensor. | +| `0x101` | `ESP_ERR_NO_MEM` | Frame buffer / DMA allocation failed | PSRAM not detected or exhausted before camera init. | +| `0x102` | `ESP_ERR_INVALID_ARG` | Bad `camera_config_t` (pins, `xclk_freq_hz`, format/framesize combo) | Firmware bug, not a hardware fault - check `CameraManager::setupCameraPinout()` against the board's Kconfig pin defaults. | +| `0x103` | `ESP_ERR_INVALID_STATE` | `esp_camera_init()` called twice without `esp_camera_deinit()` | Firmware bug (double-init) - see the OV5640-specific reinit path in `CameraManager::setupCamera()`. | + +## Full base `esp_err_t` table (for reference) + +Every generic ESP-IDF error code fits in 3 hex digits, so if the frame ever shows one of +these it's unambiguous: + +| Code | Name | +|---------|----------------------------| +| `0x101` | `ESP_ERR_NO_MEM` | +| `0x102` | `ESP_ERR_INVALID_ARG` | +| `0x103` | `ESP_ERR_INVALID_STATE` | +| `0x104` | `ESP_ERR_INVALID_SIZE` | +| `0x105` | `ESP_ERR_NOT_FOUND` | +| `0x106` | `ESP_ERR_NOT_SUPPORTED` | +| `0x107` | `ESP_ERR_TIMEOUT` | +| `0x108` | `ESP_ERR_INVALID_RESPONSE` | +| `0x109` | `ESP_ERR_INVALID_CRC` | +| `0x10A` | `ESP_ERR_INVALID_VERSION` | +| `0x10B` | `ESP_ERR_INVALID_MAC` | +| `0x10C` | `ESP_ERR_NOT_FINISHED` | +| `0x10D` | `ESP_ERR_NOT_ALLOWED` | + +(Source: `esp-idf/components/esp_common/include/esp_err.h`.) + +## Codes outside this range + +`esp32-camera` also defines its own error base, which does **not** fit in 3 hex digits and +will get silently truncated by the diagnostic frame's `& 0xFFF` mask: + +| Full code | Name | Would render as | +|-----------|---------------------------------------------|------------------| +| `0x20001` | `ESP_ERR_CAMERA_NOT_DETECTED` | `001` | +| `0x20002` | `ESP_ERR_CAMERA_FAILED_TO_SET_FRAME_SIZE` | `002` | +| `0x20003` | `ESP_ERR_CAMERA_FAILED_TO_SET_OUT_FORMAT` | `003` | +| `0x20004` | `ESP_ERR_CAMERA_NOT_SUPPORTED` | `004` | + +(Source: `managed_components/espressif__esp32-camera/driver/include/esp_camera.h`.) + +`esp_camera_init()` itself (`esp_camera.c`, ~lines 278-293, via `camera_probe()`/`SCCB_Probe()`) +only ever returns `ESP_ERR_NOT_FOUND` (`0x105`) on a missing sensor. The `ESP_ERR_CAMERA_*` +codes above aren't returned from the init path at all - they live in the unrelated +`esp_camera_save_to_nvs()` / `esp_camera_load_from_nvs()` helpers, which `CameraManager` +doesn't call. So they're currently unreachable through this diagnostic path, not just an +edge case of it. If that ever changes (e.g. a future change starts persisting camera +settings through those NVS helpers and surfacing their errors here), the frame would need +to render 4+ hex digits (or special-case the `0x2xxxx` camera error base) to stay +unambiguous - see `CameraManager::generateDiagnosticFrame()`. diff --git a/components/LEDManager/LEDManager/LEDManager.cpp b/components/LEDManager/LEDManager/LEDManager.cpp index 587d057..9cffc3b 100644 --- a/components/LEDManager/LEDManager/LEDManager.cpp +++ b/components/LEDManager/LEDManager/LEDManager.cpp @@ -110,15 +110,13 @@ void LEDManager::displayCurrentPattern() void LEDManager::updateState(const LEDStates_e newState) { - // If we've got an error state - that's it, keep repeating it indefinitely - if (ledStateMap[this->currentState].isError) + // Error states are sticky against other errors (so e.g. a WiFi hiccup can't paper over a + // camera fault) but must still be clearable by a genuine recovery - CameraManager's retry + // task can bring the camera back on its own, and the LED needs to reflect that instead of + // showing CameraError forever after it already recovered. + if (ledStateMap[this->currentState].isError && ledStateMap[newState].isError) return; - // Alternative (recoverable error states): - // Allow recovery from error states by only blocking transitions when both, current and new states are error. Uncomment to enable recovery. - // if (ledStateMap[this->currentState].isError && ledStateMap[newState].isError) - // return; - // Only update when new state differs and is known. if (!ledStateMap.contains(newState)) return; diff --git a/components/StateManager/StateManager/StateManager.cpp b/components/StateManager/StateManager/StateManager.cpp index a25814f..e82c01e 100644 --- a/components/StateManager/StateManager/StateManager.cpp +++ b/components/StateManager/StateManager/StateManager.cpp @@ -49,6 +49,13 @@ void StateManager::HandleUpdateState() ledStreamState = LEDStates_e::CameraError; xQueueSend(this->ledStateQueue, &ledStreamState, 10); } + else if (this->camera_state == CameraState_e::Camera_Success) + { + // Camera can recover on its own now (CameraManager's retry task), so clear the + // error pattern instead of leaving the LED stuck showing CameraError forever. + ledStreamState = LEDStates_e::LedStateNone; + xQueueSend(this->ledStateQueue, &ledStreamState, 10); + } break; } diff --git a/components/StreamServer/CMakeLists.txt b/components/StreamServer/CMakeLists.txt index 3c0a30a..dc2d0bb 100644 --- a/components/StreamServer/CMakeLists.txt +++ b/components/StreamServer/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "StreamServer/StreamServer.cpp" INCLUDE_DIRS "StreamServer" - REQUIRES esp32-camera StateManager ProjectConfig esp_http_server Helpers WebSocketLogger + REQUIRES esp32-camera StateManager ProjectConfig esp_http_server Helpers WebSocketLogger CameraManager ) \ No newline at end of file diff --git a/components/StreamServer/StreamServer/StreamServer.cpp b/components/StreamServer/StreamServer/StreamServer.cpp index efc0372..cd66088 100644 --- a/components/StreamServer/StreamServer/StreamServer.cpp +++ b/components/StreamServer/StreamServer/StreamServer.cpp @@ -1,4 +1,5 @@ #include "StreamServer.hpp" +#include constexpr static const char* STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; constexpr static const char* STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; @@ -6,7 +7,10 @@ constexpr static const char* STREAM_PART = "Content-Type: image/jpeg\r\nContent- static const char* STREAM_SERVER_TAG = "[STREAM_SERVER]"; -StreamServer::StreamServer(const int STREAM_PORT, StateManager* stateManager) : STREAM_SERVER_PORT(STREAM_PORT), stateManager(stateManager) {} +StreamServer::StreamServer(const int STREAM_PORT, StateManager* stateManager, std::shared_ptr cameraManager) + : STREAM_SERVER_PORT(STREAM_PORT), stateManager(stateManager), cameraManager(cameraManager) +{ +} esp_err_t StreamHelpers::stream(httpd_req_t* req) { @@ -23,8 +27,11 @@ esp_err_t StreamHelpers::stream(httpd_req_t* req) if (!last_frame) last_frame = esp_timer_get_time(); - // Pull event queue from user_ctx to send STREAM on/off notifications - auto* stateManager = static_cast(req->user_ctx); + // Pull state/camera managers from user_ctx to send STREAM on/off notifications and, if the + // camera failed to init, fall back to a diagnostic frame instead of just dropping the feed. + auto* ctx = static_cast(req->user_ctx); + StateManager* stateManager = ctx ? ctx->stateManager : nullptr; + CameraManager* cameraManager = ctx ? ctx->cameraManager : nullptr; QueueHandle_t eventQueue = stateManager ? stateManager->GetEventQueue() : nullptr; bool stream_on_sent = false; @@ -40,22 +47,42 @@ esp_err_t StreamHelpers::stream(httpd_req_t* req) while (true) { - fb = esp_camera_fb_get(); - - if (!fb) + // Re-checked every iteration (not just once at connection start) so an already-open + // stream picks up a camera that recovers mid-connection (see CameraManager's retry + // task) without the client needing to reconnect. + const bool cameraFailed = stateManager && stateManager->GetCameraState() == CameraState_e::Camera_Error; + const uint8_t* diagBuf = nullptr; + size_t diagLen = 0; + const bool haveDiagFrame = cameraFailed && cameraManager && cameraManager->getDiagnosticFrame(&diagBuf, &diagLen); + + if (haveDiagFrame) { - ESP_LOGE(STREAM_SERVER_TAG, "Camera capture failed"); - response = ESP_FAIL; - // Don't break immediately, try to recover - vTaskDelay(pdMS_TO_TICKS(10)); - continue; + // No real camera frames are coming; re-serve the same diagnostic JPEG slowly so + // clients see it as a (mostly static) video feed instead of a broken connection. + gettimeofday(&_timestamp, nullptr); + _jpg_buf_len = diagLen; + _jpg_buf = const_cast(diagBuf); + fb = nullptr; } else { - _timestamp.tv_sec = fb->timestamp.tv_sec; - _timestamp.tv_usec = fb->timestamp.tv_usec; - _jpg_buf_len = fb->len; - _jpg_buf = fb->buf; + fb = esp_camera_fb_get(); + + if (!fb) + { + ESP_LOGE(STREAM_SERVER_TAG, "Camera capture failed"); + response = ESP_FAIL; + // Don't break immediately, try to recover + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + else + { + _timestamp.tv_sec = fb->timestamp.tv_sec; + _timestamp.tv_usec = fb->timestamp.tv_usec; + _jpg_buf_len = fb->len; + _jpg_buf = fb->buf; + } } if (response == ESP_OK) response = httpd_resp_send_chunk(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); @@ -72,14 +99,22 @@ esp_err_t StreamHelpers::stream(httpd_req_t* req) fb = NULL; _jpg_buf = NULL; } - else if (_jpg_buf) + else if (_jpg_buf && !haveDiagFrame) { + // _jpg_buf points at the diagnostic frame's cached buffer when haveDiagFrame is set, + // which is owned by CameraManager and reused every iteration - never free it here. free(_jpg_buf); _jpg_buf = NULL; } if (response != ESP_OK) break; + if (haveDiagFrame) + { + // No real capture rate to pace against; throttle the repeated diagnostic frame. + vTaskDelay(pdMS_TO_TICKS(500)); + } + if (esp_log_level_get(STREAM_SERVER_TAG) >= ESP_LOG_INFO) { static long last_request_time = 0; @@ -130,11 +165,12 @@ esp_err_t StreamServer::startStreamServer() config.send_wait_timeout = 5; // 5 seconds for sending config.lru_purge_enable = true; // Enable LRU purge for better connection handling + this->userCtx = StreamUserCtx{this->stateManager, this->cameraManager.get()}; httpd_uri_t stream_page = { .uri = "/", .method = HTTP_GET, .handler = &StreamHelpers::stream, - .user_ctx = this->stateManager, + .user_ctx = &this->userCtx, }; httpd_uri_t logs_ws = { @@ -154,10 +190,13 @@ esp_err_t StreamServer::startStreamServer() } httpd_register_uri_handler(camera_stream, &logs_ws); - if (this->stateManager->GetCameraState() != CameraState_e::Camera_Success) + + const bool cameraOk = this->stateManager->GetCameraState() == CameraState_e::Camera_Success; + if (!cameraOk) { - ESP_LOGE(STREAM_SERVER_TAG, "Camera not initialized. Cannot start stream server. Logs server will be running."); - return ESP_FAIL; + // Still register "/" - the handler serves a diagnostic frame (error code rendered as a + // JPEG) instead of a real feed, so clients see the failure instead of a dead connection. + ESP_LOGW(STREAM_SERVER_TAG, "Camera not initialized. Stream will serve a diagnostic frame instead of live video."); } httpd_register_uri_handler(camera_stream, &stream_page); diff --git a/components/StreamServer/StreamServer/StreamServer.hpp b/components/StreamServer/StreamServer/StreamServer.hpp index 8161a3e..9d29c3d 100644 --- a/components/StreamServer/StreamServer/StreamServer.hpp +++ b/components/StreamServer/StreamServer/StreamServer.hpp @@ -4,6 +4,7 @@ #define PART_BOUNDARY "123456789000000000000987654321" +#include #include #include #include @@ -14,6 +15,14 @@ extern WebSocketLogger webSocketLogger; +// Passed as httpd_req_t::user_ctx so the stream handler can fall back to a diagnostic +// frame (see CameraManager::getDiagnosticFrame) when the camera failed to init. +struct StreamUserCtx +{ + StateManager* stateManager; + CameraManager* cameraManager; +}; + namespace StreamHelpers { esp_err_t stream(httpd_req_t* req); @@ -25,10 +34,12 @@ class StreamServer private: int STREAM_SERVER_PORT; StateManager* stateManager; + std::shared_ptr cameraManager; + StreamUserCtx userCtx{}; httpd_handle_t camera_stream = nullptr; public: - StreamServer(const int STREAM_PORT, StateManager* StateManager); + StreamServer(const int STREAM_PORT, StateManager* StateManager, std::shared_ptr cameraManager); esp_err_t startStreamServer(); esp_err_t stream(httpd_req_t* req); diff --git a/main/openiris_main.cpp b/main/openiris_main.cpp index 8975d1d..9e78313 100644 --- a/main/openiris_main.cpp +++ b/main/openiris_main.cpp @@ -62,7 +62,7 @@ auto wifiManager = std::make_shared(deviceConfig, eventQueue, state MDNSManager mdnsManager(deviceConfig, eventQueue); std::shared_ptr cameraHandler = std::make_shared(deviceConfig, eventQueue); -StreamServer streamServer(80, stateManager); +StreamServer streamServer(80, stateManager, cameraHandler); std::shared_ptr restAPI = std::make_shared("http://0.0.0.0:81", commandManager); @@ -282,6 +282,7 @@ extern "C" void app_main(void) xTaskCreate(HandleLEDDisplayTask, "HandleLEDDisplayTask", 1024 * 2, ledManager.get(), 3, nullptr); cameraHandler->setupCamera(); + cameraHandler->startAutoRetry(); // let's keep the serial manager running for the duration of the setup // we'll clean it up later if need be