Webcam CSI: allow more complex ISP setups with config files (#24531)

Co-authored-by: Christian Baars <christianbaars@MacStudio-von-Christian.local>
This commit is contained in:
Christian Baars
2026-03-07 11:39:36 +01:00
committed by GitHub
co-authored by Christian Baars
parent b57ae100ba
commit b9beaf6fbd
5 changed files with 683 additions and 303 deletions
@@ -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
@@ -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}}}
@@ -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 <http://www.gnu.org/licenses/>.
*/
/*
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 <berry.h>
#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 <cstring>
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
@@ -137,6 +137,7 @@ struct {
SemaphoreHandle_t resume_sem; SemaphoreHandle_t resume_sem;
camera_fail_reason_t fail_reason; camera_fail_reason_t fail_reason;
esp_err_t fail_esp_err; esp_err_t fail_esp_err;
uint32_t isp_gamma_y[16];
} core; } core;
// --- 2. JPEG Session (POD) --- // --- 2. JPEG Session (POD) ---
@@ -236,6 +237,10 @@ struct {
uint32_t compression_ratio_x100; // Compression ratio * 100 (e.g., 1500 = 15.00x) uint32_t compression_ratio_x100; // Compression ratio * 100 (e.g., 1500 = 15.00x)
} WcStats; } 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" #define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e"
// Command definitions // 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) { 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); 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) { if (Wc.core.cam_task_handle) {
Wc.core.state = CAM_STOPPING; Wc.core.state = CAM_STOPPING;
if (Wc.core.resume_sem) xSemaphoreGive(Wc.core.resume_sem); 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); xTaskNotifyGive(Wc.core.cam_task_handle);
for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) { 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); vTaskDelete(Wc.core.cam_task_handle);
Wc.core.cam_task_handle = NULL; 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 // 3. Deinitialize hardware pipeline
WcDeinitPipeline(); 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) // 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) { 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++; cb_get_new_count++;
// Switch to the OTHER buffer for the next write // 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 // Wake up the processing task immediately
BaseType_t xHigherPriorityTaskWoken = pdFALSE; 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; 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); 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]) { if (!Wc.core.frame_buffer[i]) {
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Frame buffer %d allocation failed"), i); AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Frame buffer %d allocation failed"), i);
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_MEMORY); WcSetFailed(CAM_FAIL_MEMORY);
return 0; return 0;
} }
@@ -393,7 +402,6 @@ uint32_t WcInitPipeline() {
ret = esp_cam_new_csi_ctlr(&csi_config, &Wc.core.cam_handle); ret = esp_cam_new_csi_ctlr(&csi_config, &Wc.core.cam_handle);
if (ret != ESP_OK) { if (ret != ESP_OK) {
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI init failed (0x%x)"), ret); AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI init failed (0x%x)"), ret);
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_CSI_INIT, ret); WcSetFailed(CAM_FAIL_CSI_INIT, ret);
return 0; 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); ret = esp_cam_ctlr_register_event_callbacks(Wc.core.cam_handle, &cbs, &Wc.core.cam_trans);
if (ret != ESP_OK) { if (ret != ESP_OK) {
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Callback registration failed (0x%x)"), ret); AddLog(LOG_LEVEL_ERROR, PSTR("CAM: Callback registration failed (0x%x)"), ret);
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_CSI_INIT, ret); WcSetFailed(CAM_FAIL_CSI_INIT, ret);
return 0; return 0;
} }
ret = esp_cam_ctlr_enable(Wc.core.cam_handle); ret = esp_cam_ctlr_enable(Wc.core.cam_handle);
if (ret != ESP_OK) { if (ret != ESP_OK) {
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI enable failed (0x%x)"), ret); AddLog(LOG_LEVEL_ERROR, PSTR("CAM: CSI enable failed (0x%x)"), ret);
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_CSI_INIT, ret); WcSetFailed(CAM_FAIL_CSI_INIT, ret);
return 0; return 0;
} }
// 4. ISP // 4. ISP — always created here with correct output format for session type
if (tasmota_wc_isp_handle) { {
Wc.core.isp_handle = tasmota_wc_isp_handle;
esp_isp_enable(Wc.core.isp_handle);
} else {
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; 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 = { esp_isp_processor_cfg_t isp_config = {
.clk_hz = 120 * 1000 * 1000, .clk_hz = 120 * 1000 * 1000,
@@ -434,23 +437,25 @@ uint32_t WcInitPipeline() {
ret = esp_isp_new_processor(&isp_config, &Wc.core.isp_handle); ret = esp_isp_new_processor(&isp_config, &Wc.core.isp_handle);
if (ret != ESP_OK) { if (ret != ESP_OK) {
AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP init failed (0x%x)"), ret); AddLog(LOG_LEVEL_ERROR, PSTR("CAM: ISP init failed (0x%x)"), ret);
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_ISP_INIT, ret); WcSetFailed(CAM_FAIL_ISP_INIT, ret);
return 0; return 0;
} }
esp_isp_enable(Wc.core.isp_handle); 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 // 5. Encoders
if (Wc.core.session_type == SESSION_MJPEG_HTTP) { if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
if (!WcSetupJpegEncoder()) { if (!WcSetupJpegEncoder()) {
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_ENCODER_INIT); WcSetFailed(CAM_FAIL_ENCODER_INIT);
return 0; return 0;
} }
} else if (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) { } else if (Wc.core.session_type == SESSION_RTSP_AND_WS || Wc.core.session_type == SESSION_WEBRTC) {
if (!WcSetupH264Encoder()) { if (!WcSetupH264Encoder()) {
WcDeinitPipeline();
WcSetFailed(CAM_FAIL_ENCODER_INIT); WcSetFailed(CAM_FAIL_ENCODER_INIT);
return 0; return 0;
} }
@@ -462,6 +467,9 @@ uint32_t WcInitPipeline() {
// De-initialize only the resolution-dependent hardware // De-initialize only the resolution-dependent hardware
void WcDeinitPipeline() { void WcDeinitPipeline() {
// 0. AWB must go before ISP processor
WcIspDeinitAWB();
// 1. Delete Encoder // 1. Delete Encoder
if (Wc.h264.handle) { if (Wc.h264.handle) {
esp_h264_enc_del(Wc.h264.handle); // Only if using helper that doesn't double-free 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; Wc.core.cam_handle = NULL;
} }
// 3. Delete ISP (Only if we own it) // 3. Delete ISP
if (Wc.core.isp_handle) { if (Wc.core.isp_handle) {
esp_isp_disable(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; Wc.core.isp_handle = NULL;
} }
@@ -585,8 +591,8 @@ uint32_t WcSetup(bool reset_config) {
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Mutexes created")); AddLog(LOG_LEVEL_INFO, PSTR("CAM: Mutexes created"));
// 5. Create processing task based on session type // 5. Create processing task based on session type
TaskFunction_t task_func; TaskFunction_t task_func = nullptr; // ← was uninitialized
const char *task_name; const char *task_name = nullptr; // ← was uninitialized
void WebRTCProcessingTask(void *pvParameters); void WebRTCProcessingTask(void *pvParameters);
if (Wc.core.session_type == SESSION_MJPEG_HTTP) { if (Wc.core.session_type == SESSION_MJPEG_HTTP) {
@@ -604,6 +610,17 @@ uint32_t WcSetup(bool reset_config) {
task_name = "WebRTCTask"; 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( BaseType_t task_created = xTaskCreatePinnedToCore(
task_func, task_func,
task_name, task_name,
@@ -694,20 +711,24 @@ uint32_t WcStop(void) {
AddLog(LOG_LEVEL_INFO, PSTR("CAM: Stopping (state=%d)"), Wc.core.state); 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; 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.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) { if (Wc.core.cam_task_handle) {
xTaskNotifyGive(Wc.core.cam_task_handle); xTaskNotifyGive(Wc.core.cam_task_handle);
// Wait for task to exit (task sets handle to NULL before deleting itself) // 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++) { for (int i = 0; i < 50 && Wc.core.cam_task_handle != NULL; i++) {
delay(10); delay(25);
} }
// Force delete if task didn't exit // Force delete if task didn't exit
@@ -718,10 +739,6 @@ uint32_t WcStop(void) {
} }
AddLog(LOG_LEVEL_DEBUG, PSTR("CAM: Task stopped")); 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) // 5. Deinitialize hardware pipeline (Buffers, CSI, ISP, Encoders)
WcDeinitPipeline(); WcDeinitPipeline();
@@ -735,10 +752,10 @@ uint32_t WcStop(void) {
// 7. Stop RTSP server and close client // 7. Stop RTSP server and close client
WcRtspStop(); WcRtspStop();
// 7b. Stop WebRTC (UDP, DTLS task, state) // 8. Stop WebRTC (UDP, DTLS task, state)
WcWebRTCStop(); WcWebRTCStop();
// 8. Delete mutexes // 9. Delete mutexes
if (Wc.core.frame_mutex) { if (Wc.core.frame_mutex) {
vSemaphoreDelete(Wc.core.frame_mutex); vSemaphoreDelete(Wc.core.frame_mutex);
Wc.core.frame_mutex = NULL; Wc.core.frame_mutex = NULL;
@@ -752,7 +769,7 @@ uint32_t WcStop(void) {
Wc.core.resume_sem = NULL; Wc.core.resume_sem = NULL;
} }
// 9. Final state // 10. Final state
Wc.core.state = CAM_IDLE; Wc.core.state = CAM_IDLE;
Wc.jpeg.stream_active = 0; Wc.jpeg.stream_active = 0;
@@ -871,6 +888,13 @@ bool Xdrv81(uint32_t function) {
WcSetup(true); // First boot - reset config to defaults WcSetup(true); // First boot - reset config to defaults
} }
break; 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: case FUNC_EVERY_SECOND:
// Auto-start streaming once WiFi is available (only from INIT, not after FAILED) // Auto-start streaming once WiFi is available (only from INIT, not after FAILED)
if (Wc.core.state == CAM_INIT && !TasmotaGlobal.global_state.network_down) { if (Wc.core.state == CAM_INIT && !TasmotaGlobal.global_state.network_down) {
@@ -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 <http://www.gnu.org/licenses/>.
*/
/*********************************************************************************************\
* 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