From b9beaf6fbd2ce2f4990b5e130c6e9bc6720477f9 Mon Sep 17 00:00:00 2001 From: Christian Baars Date: Sat, 7 Mar 2026 11:39:36 +0100 Subject: [PATCH] Webcam CSI: allow more complex ISP setups with config files (#24531) Co-authored-by: Christian Baars --- lib/libesp32/berry_tasmota/src/be_isp_lib.c | 34 - tasmota/berry/camera_sensor/imx219_isp.json | 1 + .../xdrv_52_3_berry_isp.ino | 233 ------- .../xdrv_81_0_esp32_webcam_CSI_core.ino | 96 ++- .../xdrv_81_0_esp32_webcam_CSI_isp.ino | 622 ++++++++++++++++++ 5 files changed, 683 insertions(+), 303 deletions(-) delete mode 100644 lib/libesp32/berry_tasmota/src/be_isp_lib.c create mode 100644 tasmota/berry/camera_sensor/imx219_isp.json delete mode 100644 tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_isp.ino create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_isp.ino diff --git a/lib/libesp32/berry_tasmota/src/be_isp_lib.c b/lib/libesp32/berry_tasmota/src/be_isp_lib.c deleted file mode 100644 index aae89a71b..000000000 --- a/lib/libesp32/berry_tasmota/src/be_isp_lib.c +++ /dev/null @@ -1,34 +0,0 @@ -/******************************************************************** - * Tasmota lib for the ISP of the ESP32-P4 (and maybe successors) - * - * To use: import ISP` - *******************************************************************/ -#include "be_constobj.h" -#include "be_mapping.h" - -#if !defined(USE_BERRY_ISP) - -#include "be_constobj.h" -#include "be_mapping.h" - -extern int be_ISP_init(bvm *vm); -extern int be_ISP_set_ccm(bvm *vm); -extern int be_ISP_set_gamma(bvm *vm); -extern int be_ISP_set_sharpen(bvm *vm); -extern int be_ISP_set_color(bvm *vm); -extern int be_ISP_set_bf(bvm *vm); - -/* @const_object_info_begin -module ISP (scope: global) { - init, func(be_ISP_init) - set_ccm, func(be_ISP_set_ccm) - set_gamma, func(be_ISP_set_gamma) - set_sharpen, func(be_ISP_set_sharpen) - set_color, func(be_ISP_set_color) - set_bf, func(be_ISP_set_bf) -} -@const_object_info_end */ - -#include "be_fixed_ISP.h" - -#endif // USE_BERRY_ULP diff --git a/tasmota/berry/camera_sensor/imx219_isp.json b/tasmota/berry/camera_sensor/imx219_isp.json new file mode 100644 index 000000000..7ad79afea --- /dev/null +++ b/tasmota/berry/camera_sensor/imx219_isp.json @@ -0,0 +1 @@ +{"IMX219":{"awb":{"model":0,"min_red_gain_step":0.034,"min_blue_gain_step":0.034,"min_counted":1000,"range":{"green":{"min":128,"max":240},"rg":{"min":0.7,"max":1.3},"bg":{"min":0.7,"max":1.3}},"green_luma_env":"dummy_awb_luma","green_luma_init":140,"green_luma_step_ratio":0.3},"acc":{"ccm":{"low_luma":{"luma_env":"ae.luma.avg","threshold":28,"matrix":[1,0,0,0,1,0,0,0,1]},"table":[{"color_temp":6500,"matrix":[1.9049,-0.5453,-0.3596,-0.2035,1.4471,-0.2436,-0.0028,-0.4219,1.4247]},{"color_temp":2800,"matrix":[1.485,-0.273,-0.212,-0.19,1.337,-0.147,0.127,-0.785,1.658]}]}},"adn":{"bf":[{"gain":1,"param":{"level":2,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":8,"param":{"level":4,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":32,"param":{"level":6,"matrix":[1,1,1,1,2,1,1,1,1]}}]},"aen":{"gamma":{"use_gamma_param":false,"luma_env":"dummy_gamma_luma","luma_min_step":5,"table":[{"luma":20,"y":[0,66,90,109,125,138,150,161,171,181,190,198,207,215,222,230]},{"luma":50,"y":[0,72,99,119,136,151,164,175,186,197,206,216,224,233,241,248]},{"luma":100,"y":[0,80,113,138,160,179,196,211,226,239,248,253,255,255,255,255]}]},"sharpen":[{"gain":1,"param":{"h_thresh":10,"l_thresh":2,"h_coeff":2.8,"m_coeff":3,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":2,"param":{"h_thresh":12,"l_thresh":3,"h_coeff":2.2,"m_coeff":2.5,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":4,"param":{"h_thresh":16,"l_thresh":5,"h_coeff":1.5,"m_coeff":1.8,"matrix":[1,2,1,2,4,2,1,2,1]}},{"gain":8,"param":{"h_thresh":24,"l_thresh":7,"h_coeff":0.8,"m_coeff":1.1,"matrix":[1,1,1,1,2,1,1,1,1]}}],"contrast":[{"gain":1,"value":128},{"gain":4,"value":120},{"gain":8,"value":112}]},"ext":{"hue":0,"brightness":0}}} \ No newline at end of file diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_isp.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_isp.ino deleted file mode 100644 index 08f30183a..000000000 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_isp.ino +++ /dev/null @@ -1,233 +0,0 @@ -/* - xdrv_52_3_berry_isp.ino - Berry scripting language, native functions - - Copyright (C) 2025 Christian Baars & Stephan Hadinger, Berry language by Guan Wenliang https://github.com/Skiars/berry - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - - -/* - xdrv_52_3_berry_isp.ino - Native Berry ISP Implementation for ESP-IDF 5.5+ - FIXED: - - Replaced designated initializers with direct assignment to fix ordering errors. - - Corrected Gamma point assignment (setting .y and .x members). -*/ - -#ifdef USE_BERRY -#include - -#ifdef USE_CSI_WEBCAM -// Global ISP Processor Handle (Must be shared with xdrv_81) -#include "driver/isp.h" -isp_proc_handle_t tasmota_wc_isp_handle = NULL; // we do not have stable IDF 5.5 yet, but we need this symbol - -#ifdef PLACEHOLDER_UNTIL_IDF55_ISMATURED //USE_CSI_WEBCAM - -#include "driver/isp.h" -#include "driver/isp_bf.h" -#include "driver/isp_sharpen.h" -#include "driver/isp_color.h" -#include "driver/isp_ccm.h" -#include "driver/isp_gamma.h" -#include - - -extern "C" { - - // Helper: Parse Berry list to flat float array - static int ParseFloatList(bvm *vm, int index, float* out, int max_len) { - if (!be_islist(vm, index)) return 0; - int count = be_data_size(vm, index); - if (count > max_len) count = max_len; - be_pushvalue(vm, index); - for (int i = 0; i < count; i++) { - be_pushint(vm, i); - be_getindex(vm, -2); - out[i] = be_isnumber(vm, -1) ? be_toreal(vm, -1) : 0.0f; - be_pop(vm, 1); - } - be_pop(vm, 1); - return count; - } - - // ISP.init(width, height) - int be_ISP_init(bvm *vm) { - int width = be_toint(vm, 1); - int height = be_toint(vm, 2); - - if (tasmota_wc_isp_handle) { - be_pushbool(vm, true); - return 1; - } - - // FIXED: Use memset + direct assignment to bypass C++ designated initializer ordering issues - esp_isp_processor_cfg_t isp_config; - memset(&isp_config, 0, sizeof(isp_config)); - - isp_config.clk_hz = 80 * 1000 * 1000; - isp_config.clk_src = ISP_CLK_SRC_DEFAULT; - isp_config.input_data_source = ISP_INPUT_DATA_SOURCE_CSI; - isp_config.input_data_color_type = ISP_COLOR_RAW8; - isp_config.output_data_color_type = ISP_COLOR_YUV422; - isp_config.h_res = (uint32_t)width; - isp_config.v_res = (uint32_t)height; - isp_config.has_line_start_packet = false; - isp_config.has_line_end_packet = false; - - esp_err_t ret = esp_isp_new_processor(&isp_config, &tasmota_wc_isp_handle); - if (ret == ESP_OK) { - esp_isp_enable(tasmota_wc_isp_handle); - } - - be_pushbool(vm, (ret == ESP_OK)); - return 1; - } - - // ISP.set_ccm(matrix_list) - int be_ISP_set_ccm(bvm *vm) { - if (!tasmota_wc_isp_handle) return 0; - - esp_isp_ccm_config_t cfg = {0}; - float temp_matrix[9]; - ParseFloatList(vm, 1, temp_matrix, 9); - - int idx = 0; - for(int r=0; r<3; r++) { - for(int c=0; c<3; c++) { - cfg.matrix[r][c] = temp_matrix[idx++]; - } - } - - esp_isp_ccm_configure(tasmota_wc_isp_handle, &cfg); - esp_isp_ccm_enable(tasmota_wc_isp_handle); - return 0; - } - - // ISP.set_gamma(curve_points_list) - // curve_points_list: List of 16 Y-values (Output values) - int be_ISP_set_gamma(bvm *vm) { - if (!tasmota_wc_isp_handle) return 0; - - float temp_points[16]; - ParseFloatList(vm, 1, temp_points, 16); - - isp_gamma_curve_points_t pts; - memset(&pts, 0, sizeof(pts)); - - // P4 Gamma Curve has 16 points. - // We map the 16 input values to Y, and generate linear X steps. - // Assuming 8-bit input (0-255), step is 16. - for(int i=0; i<16; i++) { - pts.pt[i].x = i * 16; // Auto-generate Input X (0, 16, 32...) - pts.pt[i].y = (uint32_t)temp_points[i]; // FIXED: Assign to .y member - } - - esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_R, &pts); - esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_G, &pts); - esp_isp_gamma_configure(tasmota_wc_isp_handle, COLOR_COMPONENT_B, &pts); - esp_isp_gamma_enable(tasmota_wc_isp_handle); - - return 0; - } - - // ISP.set_sharpen(h_thresh, l_thresh, h_coeff, m_coeff, template_list) - int be_ISP_set_sharpen(bvm *vm) { - if (!tasmota_wc_isp_handle) return 0; - - esp_isp_sharpen_config_t cfg; - memset(&cfg, 0, sizeof(cfg)); - - cfg.h_thresh = be_toint(vm, 1); - cfg.l_thresh = be_toint(vm, 2); - - float h_val = be_toreal(vm, 3); - cfg.h_freq_coeff.integer = (uint8_t)h_val; - cfg.h_freq_coeff.decimal = (uint8_t)((h_val - (int)h_val) * 256); - - float m_val = be_toreal(vm, 4); - cfg.m_freq_coeff.integer = (uint8_t)m_val; - cfg.m_freq_coeff.decimal = (uint8_t)((m_val - (int)m_val) * 256); - - float temp[9]; - ParseFloatList(vm, 5, temp, 9); - int idx = 0; - for(int r=0; r<3; r++) { - for(int c=0; c<3; c++) { - cfg.sharpen_template[r][c] = (uint8_t)temp[idx++]; - } - } - - cfg.padding_mode = ISP_SHARPEN_EDGE_PADDING_MODE_SRND_DATA; - - esp_isp_sharpen_configure(tasmota_wc_isp_handle, &cfg); - esp_isp_sharpen_enable(tasmota_wc_isp_handle); - return 0; - } - - // ISP.set_color(contrast, saturation, hue, brightness) - int be_ISP_set_color(bvm *vm) { - if (!tasmota_wc_isp_handle) return 0; - - esp_isp_color_config_t cfg; - memset(&cfg, 0, sizeof(cfg)); - - float contrast = be_toreal(vm, 1); - cfg.color_contrast.integer = (uint8_t)contrast; - cfg.color_contrast.decimal = (uint8_t)((contrast - (int)contrast) * 256); - - float saturation = be_toreal(vm, 2); - cfg.color_saturation.integer = (uint8_t)saturation; - cfg.color_saturation.decimal = (uint8_t)((saturation - (int)saturation) * 256); - - cfg.color_hue = (be_top(vm) >= 3) ? be_toint(vm, 3) : 0; - cfg.color_brightness = (be_top(vm) >= 4) ? be_toint(vm, 4) : 0; - - esp_isp_color_configure(tasmota_wc_isp_handle, &cfg); - esp_isp_color_enable(tasmota_wc_isp_handle); - return 0; - } - - // ISP.set_bf(denoising_level, template_list) - int be_ISP_set_bf(bvm *vm) { - if (!tasmota_wc_isp_handle) return 0; - - esp_isp_bf_config_t cfg; - memset(&cfg, 0, sizeof(cfg)); - - cfg.denoising_level = be_toint(vm, 1); - - if (be_top(vm) >= 2 && be_islist(vm, 2)) { - float temp[9]; - ParseFloatList(vm, 2, temp, 9); - int idx = 0; - for(int r=0; r<3; r++) { - for(int c=0; c<3; c++) { - cfg.bf_template[r][c] = (uint8_t)temp[idx++]; - } - } - } - - cfg.padding_mode = ISP_BF_EDGE_PADDING_MODE_SRND_DATA; - - esp_isp_bf_configure(tasmota_wc_isp_handle, &cfg); - esp_isp_bf_enable(tasmota_wc_isp_handle); - return 0; - } - -} // extern C - -#endif //PLACEHOLDER_UNTIL_IDF55_ISMATURED -#endif // USE_CSI_WEBCAM -#endif // USE_BERRY diff --git a/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_core.ino b/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_core.ino index 135764082..7589b0469 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_core.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_core.ino @@ -137,6 +137,7 @@ struct { SemaphoreHandle_t resume_sem; camera_fail_reason_t fail_reason; esp_err_t fail_esp_err; + uint32_t isp_gamma_y[16]; } core; // --- 2. JPEG Session (POD) --- @@ -236,6 +237,10 @@ struct { uint32_t compression_ratio_x100; // Compression ratio * 100 (e.g., 1500 = 15.00x) } WcStats; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +bool WcIspApplyConfig(isp_proc_handle_t handle, const char* sensor_name, int width, int height); +#endif + #define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e" // Command definitions @@ -266,11 +271,14 @@ static volatile uint32_t cb_finished_count = 0; void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Entering FAILED state, reason=%d, esp_err=0x%x"), reason, esp_err); - // 1. Stop task if running + // 1. Stop sensor streaming (harmless if not yet started) + callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0); + + // 2. Gate ISR and wake/join processing task if (Wc.core.cam_task_handle) { Wc.core.state = CAM_STOPPING; if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem); - if (Wc.core.frame_mutex) xSemaphoreGive(Wc.core.frame_mutex); + // NOTE: no xSemaphoreGive(frame_mutex) — mutex, not a signal xTaskNotifyGive(Wc.core.cam_task_handle); for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) { @@ -281,11 +289,10 @@ void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) { vTaskDelete(Wc.core.cam_task_handle); Wc.core.cam_task_handle = NULL; } + } else { + Wc.core.state = CAM_STOPPING; } - // 2. Stop sensor streaming - callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0); - // 3. Deinitialize hardware pipeline WcDeinitPipeline(); @@ -322,6 +329,7 @@ void WcSetFailed(camera_fail_reason_t reason, esp_err_t esp_err = ESP_OK) { // Callback: Provide new buffer for next frame (Ping-Pong Logic) static bool IRAM_ATTR csi_on_get_new_vb(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) { + if (Wc.core.state != CAM_STREAMING) return false; // ← ADDED cb_get_new_count++; // Switch to the OTHER buffer for the next write @@ -350,7 +358,9 @@ static bool IRAM_ATTR csi_on_trans_finished(esp_cam_ctlr_handle_t handle, esp_ca // Wake up the processing task immediately BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(Wc.core.cam_task_handle, &xHigherPriorityTaskWoken); + TaskHandle_t task = Wc.core.cam_task_handle; // safe capture + if (!task) return false; + vTaskNotifyGiveFromISR(task, &xHigherPriorityTaskWoken); return xHigherPriorityTaskWoken == pdTRUE; } @@ -367,7 +377,6 @@ uint32_t WcInitPipeline() { Wc.core.frame_buffer[i] = (uint8_t*)heap_caps_aligned_calloc(64, 1, Wc.core.frame_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!Wc.core.frame_buffer[i]) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Frame buffer %d allocation failed"), i); - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_MEMORY); return 0; } @@ -393,7 +402,6 @@ uint32_t WcInitPipeline() { ret = esp_cam_new_csi_ctlr(&csi_config, &Wc.core.cam_handle); if (ret != ESP_OK) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI init failed (0x%x)"), ret); - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_CSI_INIT, ret); return 0; } @@ -405,23 +413,18 @@ uint32_t WcInitPipeline() { ret = esp_cam_ctlr_register_event_callbacks(Wc.core.cam_handle, &cbs, &Wc.core.cam_trans); if (ret != ESP_OK) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Callback registration failed (0x%x)"), ret); - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_CSI_INIT, ret); return 0; } ret = esp_cam_ctlr_enable(Wc.core.cam_handle); if (ret != ESP_OK) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI enable failed (0x%x)"), ret); - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_CSI_INIT, ret); return 0; } - // 4. ISP - if (tasmota_wc_isp_handle) { - Wc.core.isp_handle = tasmota_wc_isp_handle; - esp_isp_enable(Wc.core.isp_handle); - } else { + // 4. ISP — always created here with correct output format for session type + { isp_color_t isp_output_format = (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) ? ISP_COLOR_YUV420 : ISP_COLOR_YUV422; esp_isp_processor_cfg_t isp_config = { .clk_hz = 120 * 1000 * 1000, @@ -434,23 +437,25 @@ uint32_t WcInitPipeline() { ret = esp_isp_new_processor(&isp_config, &Wc.core.isp_handle); if (ret != ESP_OK) { AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP init failed (0x%x)"), ret); - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_ISP_INIT, ret); return 0; } + esp_isp_enable(Wc.core.isp_handle); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + WcIspApplyConfig(Wc.core.isp_handle, Wc.core.config.name, Wc.core.config.width, Wc.core.config.height); +#endif } // 5. Encoders if (Wc.core.session_type == SESSION_MJPEG_HTTP) { if (!WcSetupJpegEncoder()) { - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_ENCODER_INIT); return 0; } } else if (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) { if (!WcSetupH264Encoder()) { - WcDeinitPipeline(); WcSetFailed(CAM_FAIL_ENCODER_INIT); return 0; } @@ -462,6 +467,9 @@ uint32_t WcInitPipeline() { // De-initialize only the resolution-dependent hardware void WcDeinitPipeline() { + // 0. AWB must go before ISP processor + WcIspDeinitAWB(); + // 1. Delete Encoder if (Wc.h264.handle) { esp_h264_enc_del(Wc.h264.handle); // Only if using helper that doesn't double-free @@ -483,12 +491,10 @@ void WcDeinitPipeline() { Wc.core.cam_handle = NULL; } - // 3. Delete ISP (Only if we own it) + // 3. Delete ISP if (Wc.core.isp_handle) { esp_isp_disable(Wc.core.isp_handle); - if (!tasmota_wc_isp_handle) { - esp_isp_del_processor(Wc.core.isp_handle); - } + esp_isp_del_processor(Wc.core.isp_handle); Wc.core.isp_handle = NULL; } @@ -585,8 +591,8 @@ uint32_t WcSetup(bool reset_config) { AddLog(LOG_LEVEL_INFO, PSTR("CAM: Mutexes created")); // 5. Create processing task based on session type - TaskFunction_t task_func; - const char *task_name; + TaskFunction_t task_func = nullptr; // ← was uninitialized + const char *task_name = nullptr; // ← was uninitialized void WebRTCProcessingTask(void *pvParameters); if (Wc.core.session_type == SESSION_MJPEG_HTTP) { @@ -604,6 +610,17 @@ uint32_t WcSetup(bool reset_config) { task_name = "WebRTCTask"; } + // ← ADDED: guard against unknown/unhandled session type + if (!task_func) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Unknown session type %d, no task function"), Wc.core.session_type); + WcDeinitPipeline(); + vSemaphoreDelete(Wc.core.frame_mutex); + vSemaphoreDelete(Wc.jpeg.mutex); + vSemaphoreDelete(Wc.core.resume_sem); + WcSetFailed(CAM_FAIL_TASK); + return 0; + } + BaseType_t task_created = xTaskCreatePinnedToCore( task_func, task_name, @@ -694,20 +711,24 @@ uint32_t WcStop(void) { AddLog(LOG_LEVEL_INFO, PSTR("CAM: Stopping (state=%d)"), Wc.core.state); - // 1. Set state to STOPPING - task will exit on next iteration + // 1. Stop sensor streaming (MIPI stops; harmless if already idle) + callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Called Berry stream stop")); + + // 2. Gate ISR immediately Wc.core.state = CAM_STOPPING; - // 2. If task was paused, release it so it can see STOPPING and exit + // 3. Wake paused task so it can see STOPPING and exit if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem); - if (Wc.core.frame_mutex) xSemaphoreGive(Wc.core.frame_mutex); + // NOTE: no xSemaphoreGive(frame_mutex) — mutex not a signal; task unblocked by notify below - // 3. Wake task and wait for it to exit + // 4. Wake task and wait for it to exit if (Wc.core.cam_task_handle) { xTaskNotifyGive(Wc.core.cam_task_handle); // Wait for task to exit (task sets handle to NULL before deleting itself) for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) { - delay(10); + delay(25); } // Force delete if task didn't exit @@ -718,10 +739,6 @@ uint32_t WcStop(void) { } AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Task stopped")); } - - // 4. Stop sensor streaming via Berry - callBerryEventDispatcher(PSTR("camera"), PSTR("stream"), 0, nullptr, 0); - AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Called Berry stream stop")); // 5. Deinitialize hardware pipeline (Buffers, CSI, ISP, Encoders) WcDeinitPipeline(); @@ -735,10 +752,10 @@ uint32_t WcStop(void) { // 7. Stop RTSP server and close client WcRtspStop(); - // 7b. Stop WebRTC (UDP, DTLS task, state) + // 8. Stop WebRTC (UDP, DTLS task, state) WcWebRTCStop(); - // 8. Delete mutexes + // 9. Delete mutexes if (Wc.core.frame_mutex) { vSemaphoreDelete(Wc.core.frame_mutex); Wc.core.frame_mutex = NULL; @@ -752,7 +769,7 @@ uint32_t WcStop(void) { Wc.core.resume_sem = NULL; } - // 9. Final state + // 10. Final state Wc.core.state = CAM_IDLE; Wc.jpeg.stream_active = 0; @@ -871,6 +888,13 @@ bool Xdrv81(uint32_t function) { WcSetup(true); // First boot - reset config to defaults } break; + case FUNC_EVERY_250_MSECOND: + if (Wc.core.state == CAM_STREAMING) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + WcIspAwbProcess(); +#endif + } + break; case FUNC_EVERY_SECOND: // Auto-start streaming once WiFi is available (only from INIT, not after FAILED) if (Wc.core.state == CAM_INIT && !TasmotaGlobal.global_state.network_down) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_isp.ino b/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_isp.ino new file mode 100644 index 000000000..3167ca7f3 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_81_0_esp32_webcam_CSI_isp.ino @@ -0,0 +1,622 @@ +/* + xdrv_81_0_esp32_webcam_CSI_isp.ino - ESP32-P4 CSI ISP Configuration + + Copyright (C) 2025 Christian Baars and Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/*********************************************************************************************\ + * ESP32-P4 Hardware ISP Configuration + * + * Pure C++ implementation - no Berry dependencies + * + * Responsibilities: + * - Load and parse /isp.json from filesystem + * - Apply ISP settings: CCM, Gamma, Sharpen, Color, BF (Bilateral Filter) + * - Initialize and manage AWB (Auto White Balance) controller + * - Run AWB processing loop (called from xdrv_81 core at 4Hz) + * + * Graceful degradation: + * - If isp.json is missing or malformed, ISP runs with bare minimum configuration + * - Individual section failures are logged but don't stop other sections +\*********************************************************************************************/ + +#ifdef ESP32 +#ifdef USE_CSI_WEBCAM + +#include "esp_idf_version.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + +#include "driver/isp.h" +#include "driver/isp_bf.h" +#include "driver/isp_sharpen.h" +#include "driver/isp_color.h" +#include "driver/isp_ccm.h" +#include "driver/isp_gamma.h" +#include "driver/isp_awb.h" + +/*********************************************************************************************/ + +// AWB state - file-static, used by init and C processing loop +static struct { + isp_awb_ctlr_t handle; + bool enabled; + float base_ccm[3][3]; // Base CCM matrix (before AWB correction) + float gain_r, gain_b; // Current WB correction gains + float min_r_step; // Minimum gain delta to trigger update + float min_b_step; + uint32_t min_counted; // Minimum white patches before updating +} isp_awb_state = { NULL, false, {{1,0,0},{0,1,0},{0,0,1}}, 1.0f, 1.0f, 0.031f, 0.031f, 2000 }; + +/*********************************************************************************************/ +// Forward declarations + +void WcIspApplyCCM(isp_proc_handle_t handle, JsonParserObject &sensor); +void WcIspApplyGamma(isp_proc_handle_t handle, JsonParserObject &sensor); +void WcIspApplySharpen(isp_proc_handle_t handle, JsonParserObject &sensor); +void WcIspApplyColor(isp_proc_handle_t handle, JsonParserObject &sensor); +void WcIspApplyBF(isp_proc_handle_t handle, JsonParserObject &sensor); +void WcIspInitAWB(isp_proc_handle_t handle, JsonParserObject &sensor, int width, int height); + +/*********************************************************************************************/ + +// Main entry point: Load isp.json and apply all ISP settings +// Called from WcSetup() in xdrv_81_0 after ISP handle is created +// Returns true if configuration was applied, false otherwise +bool WcIspApplyConfig(isp_proc_handle_t handle, const char* sensor_name, int width, int height) { + if (!handle) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP handle is NULL")); + return false; + } + + // Check if file exists + if (!TfsFileExists("/isp.json")) { + AddLog(LOG_LEVEL_INFO, PSTR("CAM: No isp.json found, using bare ISP")); + return false; + } + + // Get file size and allocate buffer + size_t file_size = TfsFileSize("/isp.json"); + if (file_size == 0 || file_size > 2047) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json invalid size: %d"), file_size); + return false; + } + + size_t buf_size = file_size + 1; // +1 for null terminator + char *buf = (char*)malloc(buf_size); + if (!buf) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Failed to allocate %d bytes for JSON"), buf_size); + return false; + } + + // Load file directly into buffer + memset(buf, 0, buf_size); + if (!TfsLoadFile("/isp.json", (uint8_t*)buf, file_size)) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Failed to load isp.json")); + free(buf); + return false; + } + + // Parse JSON + JsonParser parser(buf); + JsonParserToken root = parser.getRoot(); + if (!root) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json parse failed")); + free(buf); + return false; + } + + JsonParserObject root_obj = root.getObject(); + if (!root_obj) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: isp.json root is not an object")); + free(buf); + return false; + } + + // Find sensor section + JsonParserToken sensor_tok = root_obj[sensor_name]; + if (!sensor_tok || !sensor_tok.isObject()) { + AddLog(LOG_LEVEL_INFO, PSTR("CAM: No ISP section for sensor %s"), sensor_name); + free(buf); + return false; + } + + JsonParserObject sensor = sensor_tok.getObject(); + + // Apply all ISP settings (each function handles its own errors gracefully) + WcIspApplyCCM(handle, sensor); + WcIspApplyGamma(handle, sensor); + WcIspApplySharpen(handle, sensor); + WcIspApplyColor(handle, sensor); + WcIspApplyBF(handle, sensor); + WcIspInitAWB(handle, sensor, width, height); + + free(buf); + return true; +} + +/*********************************************************************************************/ + +// Helper: Find nearest entry in array by numeric key +// Returns index of nearest entry, or -1 if not found +int WcIspFindNearest(JsonParserArray &arr, const char* key, int target_value) { + if (!arr) return -1; + + int best_idx = -1; + int best_dist = INT_MAX; + int idx = 0; + + for (auto entry_tok : arr) { + if (!entry_tok.isObject()) { + idx++; + continue; + } + + JsonParserObject entry = entry_tok.getObject(); + JsonParserToken val_tok = entry[key]; + if (!val_tok) { + idx++; + continue; + } + + int val = val_tok.getInt(); + int dist = abs(val - target_value); + if (dist < best_dist) { + best_dist = dist; + best_idx = idx; + } + idx++; + } + + return best_idx; +} + +/*********************************************************************************************/ + +// Apply CCM (Color Correction Matrix) +// JSON: {"acc": {"ccm": {"table": [{"color_temp": 6500, "matrix": [flat 9 floats]}, ...]}} +void WcIspApplyCCM(isp_proc_handle_t handle, JsonParserObject &sensor) { + // Navigate: sensor["acc"]["ccm"]["table"] + JsonParserToken acc_tok = sensor["acc"]; + if (!acc_tok || !acc_tok.isObject()) return; + JsonParserObject acc = acc_tok.getObject(); + + JsonParserToken ccm_tok = acc["ccm"]; + if (!ccm_tok || !ccm_tok.isObject()) return; + JsonParserObject ccm_obj = ccm_tok.getObject(); + + JsonParserToken table_tok = ccm_obj["table"]; + if (!table_tok || !table_tok.isArray()) return; + JsonParserArray table = table_tok.getArray(); + + // Find nearest color temperature to 6500K (daylight) + int best_idx = WcIspFindNearest(table, "color_temp", 6500); + if (best_idx < 0) return; + + JsonParserToken best_tok = table[best_idx]; + if (!best_tok.isObject()) return; + JsonParserObject best = best_tok.getObject(); + + JsonParserToken mat_tok = best["matrix"]; + if (!mat_tok || !mat_tok.isArray()) return; + JsonParserArray mat = mat_tok.getArray(); + + // Flat 9-element array + esp_isp_ccm_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + int i = 0; + for (auto val : mat) { + if (i >= 9) break; + float fval = val.getFloat(0.0f); + cfg.matrix[i/3][i%3] = fval; + isp_awb_state.base_ccm[i/3][i%3] = fval; + i++; + } + + esp_isp_ccm_configure(handle, &cfg); + esp_isp_ccm_enable(handle); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP CCM applied")); +} + +/*********************************************************************************************/ + +uint32_t WcIspGammaInterp(uint32_t x) { + uint32_t seg = (x * 15) / 255; + if (seg >= 15) return Wc.core.isp_gamma_y[15]; + uint32_t x0 = seg * 255 / 15; + uint32_t x1 = (seg + 1) * 255 / 15; + uint32_t y0 = Wc.core.isp_gamma_y[seg]; + uint32_t y1 = Wc.core.isp_gamma_y[seg + 1]; + uint32_t dx = x1 - x0; + return dx ? y0 + (y1 - y0) * (x - x0) / dx : y0; +} + +// Apply Gamma Curve +// JSON: {"aen": {"gamma": {"table": [{"luma": 50, "y": [0,...15 values...]}, ...]}}} +void WcIspApplyGamma(isp_proc_handle_t handle, JsonParserObject &sensor) { + JsonParserToken aen_tok = sensor["aen"]; + if (!aen_tok || !aen_tok.isObject()) { + return; + } + + JsonParserObject aen = aen_tok.getObject(); + JsonParserToken gamma_tok = aen["gamma"]; + if (!gamma_tok || !gamma_tok.isObject()) { + return; + } + + JsonParserObject gamma_obj = gamma_tok.getObject(); + JsonParserToken table_tok = gamma_obj["table"]; + if (!table_tok || !table_tok.isArray()) { + return; + } + + JsonParserArray table = table_tok.getArray(); + + // Find nearest luma to 50 (normal scene) + int best_idx = WcIspFindNearest(table, "luma", 50); + if (best_idx < 0) { + return; + } + + JsonParserToken best_tok = table[best_idx]; + if (!best_tok.isObject()) { + return; + } + + JsonParserObject best = best_tok.getObject(); + JsonParserToken y_tok = best["y"]; + if (!y_tok || !y_tok.isArray()) { + return; + } + + JsonParserArray y_arr = y_tok.getArray(); + + int i = 0; + for (auto v : y_arr) { + if (i >= 16) break; + Wc.core.isp_gamma_y[i++] = (uint32_t)v.getInt(0); + } + + isp_gamma_curve_points_t pts = {}; + esp_isp_gamma_fill_curve_points(WcIspGammaInterp, &pts); + + // Apply to all color components + esp_isp_gamma_configure(handle, COLOR_COMPONENT_R, &pts); + esp_isp_gamma_configure(handle, COLOR_COMPONENT_G, &pts); + esp_isp_gamma_configure(handle, COLOR_COMPONENT_B, &pts); + esp_isp_gamma_enable(handle); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Gamma applied (luma=50)")); +} + +/*********************************************************************************************/ + +// Apply Sharpen Filter +// JSON: {"aen": {"sharpen": [{"gain": 1, "param": {"h_thresh":..., "l_thresh":..., "h_coeff":..., "m_coeff":..., "matrix":[...]}}]}} +void WcIspApplySharpen(isp_proc_handle_t handle, JsonParserObject &sensor) { + JsonParserToken aen_tok = sensor["aen"]; + if (!aen_tok || !aen_tok.isObject()) { + return; + } + + JsonParserObject aen = aen_tok.getObject(); + JsonParserToken sh_tok = aen["sharpen"]; + if (!sh_tok || !sh_tok.isArray()) { + return; + } + + JsonParserArray sh_arr = sh_tok.getArray(); + + // Find nearest gain to 1 + int best_idx = WcIspFindNearest(sh_arr, "gain", 1); + if (best_idx < 0) { + return; + } + + JsonParserToken best_tok = sh_arr[best_idx]; + if (!best_tok.isObject()) { + return; + } + + JsonParserObject best = best_tok.getObject(); + JsonParserToken param_tok = best["param"]; + if (!param_tok || !param_tok.isObject()) { + return; + } + + JsonParserObject param = param_tok.getObject(); + + // Parse sharpen parameters + esp_isp_sharpen_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + + cfg.h_thresh = param.getInt("h_thresh", 255); + cfg.l_thresh = param.getInt("l_thresh", 128); + + float h_coeff = param.getFloat("h_coeff", 1.0f); + cfg.h_freq_coeff.integer = (uint8_t)h_coeff; + cfg.h_freq_coeff.decimal = (uint8_t)((h_coeff - (int)h_coeff) * 256); + + float m_coeff = param.getFloat("m_coeff", 1.0f); + cfg.m_freq_coeff.integer = (uint8_t)m_coeff; + cfg.m_freq_coeff.decimal = (uint8_t)((m_coeff - (int)m_coeff) * 256); + + // Parse 3x3 matrix + JsonParserToken mat_tok = param["matrix"]; + if (mat_tok && mat_tok.isArray()) { + JsonParserArray mat = mat_tok.getArray(); + int i = 0; + for (auto v : mat) { + if (i >= 9) break; + cfg.sharpen_template[i/3][i%3] = (uint8_t)v.getFloat(0); + i++; + } + } + + cfg.padding_mode = ISP_SHARPEN_EDGE_PADDING_MODE_SRND_DATA; + + esp_isp_sharpen_configure(handle, &cfg); + esp_isp_sharpen_enable(handle); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Sharpen applied (gain=1)")); +} + +/*********************************************************************************************/ + +// Apply Color Adjustment +// JSON: {"ext": {"hue":0, "brightness":0}, "aen": {"contrast": [{"gain":1, "value":128}]}} +void WcIspApplyColor(isp_proc_handle_t handle, JsonParserObject &sensor) { + esp_isp_color_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + + // contrast from aen.contrast[] nearest gain=1 + cfg.color_contrast.integer = 1; + JsonParserToken aen_tok = sensor["aen"]; + if (aen_tok && aen_tok.isObject()) { + JsonParserObject aen = aen_tok.getObject(); + JsonParserToken con_tok = aen["contrast"]; + if (con_tok && con_tok.isArray()) { + JsonParserArray con_arr = con_tok.getArray(); + int best_idx = WcIspFindNearest(con_arr, "gain", 1); + if (best_idx >= 0) { + int val = con_arr[best_idx].getObject().getInt("value", 128); + cfg.color_contrast.integer = (uint8_t)(val >> 7); // scale 0-255 → ISP fixed-point + cfg.color_contrast.decimal = (uint8_t)((val & 0x7F) << 1); + } + } + } + + // hue + brightness from ext + JsonParserToken ext_tok = sensor["ext"]; + if (ext_tok && ext_tok.isObject()) { + JsonParserObject ext = ext_tok.getObject(); + cfg.color_hue = ext.getInt("hue", 0); + cfg.color_brightness = ext.getInt("brightness", 0); + } + + // TODO: read from acc.saturation[] once color temperature estimation is available + cfg.color_saturation.integer = 1; + + esp_isp_color_configure(handle, &cfg); + esp_isp_color_enable(handle); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP Color applied")); +} + +/*********************************************************************************************/ + +// Apply Bilateral Filter (Denoising) +// JSON: {"adn": {"bf": [{"gain":1, "param": {"level":2, "matrix":[flat 9]}}]}} +void WcIspApplyBF(isp_proc_handle_t handle, JsonParserObject &sensor) { + JsonParserToken adn_tok = sensor["adn"]; + if (!adn_tok || !adn_tok.isObject()) return; + JsonParserObject adn = adn_tok.getObject(); + + JsonParserToken bf_tok = adn["bf"]; + if (!bf_tok || !bf_tok.isArray()) return; + JsonParserArray bf_arr = bf_tok.getArray(); + + int best_idx = WcIspFindNearest(bf_arr, "gain", 1); // gain=1 = no AGC, normal light + if (best_idx < 0) return; + + JsonParserObject best = bf_arr[best_idx].getObject(); + JsonParserToken param_tok = best["param"]; + if (!param_tok || !param_tok.isObject()) return; + JsonParserObject param = param_tok.getObject(); + + esp_isp_bf_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.denoising_level = param.getInt("level", 2); + + JsonParserToken mat_tok = param["matrix"]; + if (mat_tok && mat_tok.isArray()) { + JsonParserArray mat = mat_tok.getArray(); + int i = 0; + for (auto v : mat) { + if (i >= 9) break; + cfg.bf_template[i/3][i%3] = (uint8_t)v.getFloat(0); + i++; + } + } + cfg.padding_mode = ISP_BF_EDGE_PADDING_MODE_SRND_DATA; + esp_isp_bf_configure(handle, &cfg); + esp_isp_bf_enable(handle); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP BF applied")); +} + +/*********************************************************************************************/ + +// Initialize AWB (Auto White Balance) +// JSON: {"awb": {"range": {"green":{min,max}, "rg":{min,max}, "bg":{min,max}}, "min_red_gain_step":..., "min_blue_gain_step":..., "min_counted":...}} +void WcIspInitAWB(isp_proc_handle_t handle, JsonParserObject &sensor, int width, int height) { + JsonParserToken awb_tok = sensor["awb"]; + if (!awb_tok || !awb_tok.isObject()) return; + JsonParserObject awb = awb_tok.getObject(); + + if (isp_awb_state.handle) { + esp_isp_awb_controller_disable(isp_awb_state.handle); + esp_isp_del_awb_controller(isp_awb_state.handle); + isp_awb_state.handle = NULL; + isp_awb_state.enabled = false; + } + + // Read nested range object + JsonParserToken range_tok = awb["range"]; + if (!range_tok || !range_tok.isObject()) return; + JsonParserObject range = range_tok.getObject(); + + JsonParserObject green_obj = range["green"].getObject(); + JsonParserObject rg_obj = range["rg"].getObject(); + JsonParserObject bg_obj = range["bg"].getObject(); + + int lum_min = green_obj.getInt("min", 128); + int lum_max = green_obj.getInt("max", 240); + float rg_min = rg_obj.getFloat("min", 0.7f); + float rg_max = rg_obj.getFloat("max", 1.3f); + float bg_min = bg_obj.getFloat("min", 0.7f); + float bg_max = bg_obj.getFloat("max", 1.3f); + + int x1 = width / 10; + int y1 = height / 10; + int x2 = width - width / 10; + int y2 = height - height / 10; + + esp_isp_awb_config_t awb_cfg; + memset(&awb_cfg, 0, sizeof(awb_cfg)); + awb_cfg.sample_point = (isp_awb_sample_point_t)awb.getInt("sample_point", 1); + awb_cfg.window.top_left.x = x1; + awb_cfg.window.top_left.y = y1; + awb_cfg.window.btm_right.x = x2; + awb_cfg.window.btm_right.y = y2; + awb_cfg.subwindow = awb_cfg.window; + awb_cfg.white_patch.luminance.min = (uint32_t)lum_min; + awb_cfg.white_patch.luminance.max = (uint32_t)lum_max; + awb_cfg.white_patch.red_green_ratio.min = rg_min; + awb_cfg.white_patch.red_green_ratio.max = rg_max; + awb_cfg.white_patch.blue_green_ratio.min = bg_min; + awb_cfg.white_patch.blue_green_ratio.max = bg_max; + + isp_awb_ctlr_t ctlr = NULL; + esp_err_t ret = esp_isp_new_awb_controller(handle, &awb_cfg, &ctlr); + if (ret != ESP_OK) { + AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP AWB init failed (0x%x)"), ret); + return; + } + ret = esp_isp_awb_controller_enable(ctlr); + if (ret != ESP_OK) { + esp_isp_del_awb_controller(ctlr); + return; + } + + isp_awb_state.handle = ctlr; + isp_awb_state.enabled = true; + isp_awb_state.gain_r = 1.0f; + isp_awb_state.gain_b = 1.0f; + isp_awb_state.min_counted = (uint32_t)awb.getInt("min_counted", 2000); + isp_awb_state.min_r_step = awb.getFloat("min_red_gain_step", 0.034f); + isp_awb_state.min_b_step = awb.getFloat("min_blue_gain_step", 0.034f); + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP AWB initialized")); +} + +/*********************************************************************************************/ + +// AWB Processing Loop - called from xdrv_81 FUNC_EVERY_250_MSECOND (4Hz) +// Grey-world algorithm: gain_r = avg_G/avg_R, gain_b = avg_G/avg_B +// Applied as diagonal correction on base CCM +void WcIspAwbProcess(void) { + if (Wc.core.state != CAM_STREAMING) return; + if (!isp_awb_state.enabled || !isp_awb_state.handle) return; + isp_proc_handle_t isp = Wc.core.isp_handle; + if (!isp) return; + + isp_awb_stat_result_t stats; + memset(&stats, 0, sizeof(stats)); + + esp_err_t ret = esp_isp_awb_controller_get_oneshot_statistics( + isp_awb_state.handle, 100, &stats); + if (ret != ESP_OK) { + return; + } + + if (stats.white_patch_num < isp_awb_state.min_counted) { + return; + } + + float avg_r = (float)stats.sum_r / (float)stats.white_patch_num; + float avg_g = (float)stats.sum_g / (float)stats.white_patch_num; + if (avg_g < 8.0f) return; // too dark?? + float avg_b = (float)stats.sum_b / (float)stats.white_patch_num; + + if (avg_r < 1.0f) avg_r = 1.0f; + if (avg_b < 1.0f) avg_b = 1.0f; + + float new_gain_r = avg_g / avg_r; + float new_gain_b = avg_g / avg_b; + + // Clamp gains to reasonable range + if (new_gain_r < 0.5f) new_gain_r = 0.5f; + if (new_gain_r > 2.0f) new_gain_r = 2.0f; + if (new_gain_b < 0.5f) new_gain_b = 0.5f; + if (new_gain_b > 2.0f) new_gain_b = 2.0f; + + // Check if delta exceeds minimum step + if (fabsf(new_gain_r - isp_awb_state.gain_r) < isp_awb_state.min_r_step && + fabsf(new_gain_b - isp_awb_state.gain_b) < isp_awb_state.min_b_step) { + return; + } + + isp_awb_state.gain_r = new_gain_r; + isp_awb_state.gain_b = new_gain_b; + + // Apply: corrected_ccm = diag(gain_r, 1, gain_b) * base_ccm + esp_isp_ccm_config_t ccm_cfg; + memset(&ccm_cfg, 0, sizeof(ccm_cfg)); + for (int c = 0; c < 3; c++) { + ccm_cfg.matrix[0][c] = isp_awb_state.base_ccm[0][c] * isp_awb_state.gain_r; + ccm_cfg.matrix[1][c] = isp_awb_state.base_ccm[1][c]; + ccm_cfg.matrix[2][c] = isp_awb_state.base_ccm[2][c] * isp_awb_state.gain_b; + } + + esp_isp_ccm_configure(isp, &ccm_cfg); +} + +/*********************************************************************************************/ + +// Deinitialize AWB - called when streaming stops +void WcIspDeinitAWB(void) { + if (!isp_awb_state.handle) return; + + isp_awb_state.enabled = false; + + memset(isp_awb_state.base_ccm, 0, sizeof(isp_awb_state.base_ccm)); + isp_awb_state.base_ccm[0][0] = 1.0f; + isp_awb_state.base_ccm[1][1] = 1.0f; + isp_awb_state.base_ccm[2][2] = 1.0f; + + isp_proc_handle_t isp = Wc.core.isp_handle; + if (!isp) { + isp_awb_state.handle = NULL; + return; + } + + esp_isp_awb_controller_disable(isp_awb_state.handle); + esp_isp_del_awb_controller(isp_awb_state.handle); + isp_awb_state.handle = NULL; + isp_awb_state.gain_r = 1.0f; + isp_awb_state.gain_b = 1.0f; + AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: ISP AWB deinitialized")); +} + +#endif // ESP_IDF_VERSION >= 5.5.0 +#endif // USE_CSI_WEBCAM +#endif // ESP32