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