diff --git a/esp32/include/peripherals/camera_service.h b/esp32/include/peripherals/camera_service.h index 44e4364..debc84d 100644 --- a/esp32/include/peripherals/camera_service.h +++ b/esp32/include/peripherals/camera_service.h @@ -11,26 +11,22 @@ namespace Camera { -#if USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4 +#define USE_DVP_CAMERA (USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4) +#define USE_CSI_CAMERA (USE_CAMERA && CONFIG_IDF_TARGET_ESP32P4) + +#if USE_DVP_CAMERA #include #include -#else -typedef struct { - uint8_t *buf; - size_t len; -} camera_fb_t; -typedef struct { -} sensor_t; -#endif - -#define PART_BOUNDARY "frame" camera_fb_t *safe_camera_fb_get(); sensor_t *safe_sensor_get(); void safe_sensor_return(); +#endif + +#define PART_BOUNDARY "frame" class CameraService -#if USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4 +#if USE_DVP_CAMERA : public StatefulService #endif { @@ -42,7 +38,7 @@ class CameraService esp_err_t cameraStill(httpd_req_t *request); esp_err_t cameraStream(httpd_req_t *request); -#if USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4 +#if USE_DVP_CAMERA StatefulProtoEndpoint protoEndpoint; private: diff --git a/esp32/include/peripherals/i2c_bus.h b/esp32/include/peripherals/i2c_bus.h index 46732b6..050a17a 100644 --- a/esp32/include/peripherals/i2c_bus.h +++ b/esp32/include/peripherals/i2c_bus.h @@ -1,11 +1,12 @@ #pragma once -#include +#include #include #include #include #include #include +#include class I2CBus { public: @@ -24,30 +25,17 @@ class I2CBus { _scl = scl; _freq = freq; - i2c_config_t conf = {}; - conf.mode = I2C_MODE_MASTER; - conf.sda_io_num = sda; - conf.scl_io_num = scl; - conf.sda_pullup_en = GPIO_PULLUP_ENABLE; - conf.scl_pullup_en = GPIO_PULLUP_ENABLE; - conf.master.clk_speed = freq; + i2c_master_bus_config_t bus_cfg = {}; + bus_cfg.i2c_port = port; + bus_cfg.sda_io_num = sda; + bus_cfg.scl_io_num = scl; + bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT; + bus_cfg.glitch_ignore_cnt = 7; + bus_cfg.flags.enable_internal_pullup = true; - esp_err_t err = i2c_param_config(_port, &conf); + esp_err_t err = i2c_new_master_bus(&bus_cfg, &_bus); if (err != ESP_OK) { - ESP_LOGE(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); - return err; - } - - err = i2c_driver_install(_port, I2C_MODE_MASTER, 0, 0, 0); - if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGW(TAG, "I2C driver already installed for port %d, reconfiguring", _port); - i2c_driver_delete(_port); - err = i2c_param_config(_port, &conf); - if (err != ESP_OK) return err; - err = i2c_driver_install(_port, I2C_MODE_MASTER, 0, 0, 0); - } - if (err != ESP_OK) { - ESP_LOGE(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err)); return err; } @@ -57,73 +45,51 @@ class I2CBus { void end() { if (_initialized) { - i2c_driver_delete(_port); + if (_dev) { + i2c_master_bus_rm_device(_dev); + _dev = NULL; + _dev_addr = 0xFF; + } + i2c_del_master_bus(_bus); + _bus = NULL; _initialized = false; } } bool isInitialized() const { return _initialized; } + i2c_master_bus_handle_t busHandle() const { return _bus; } + esp_err_t writeBytes(uint8_t addr, const uint8_t* data, size_t len) { if (!_initialized) return ESP_ERR_INVALID_STATE; - - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); - if (len > 0 && data != nullptr) { - i2c_master_write(cmd, data, len, true); - } - i2c_master_stop(cmd); - esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100)); - i2c_cmd_link_delete(cmd); - return ret; + esp_err_t err = ensureDevice(addr); + if (err != ESP_OK) return err; + return i2c_master_transmit(_dev, data, len, pdMS_TO_TICKS(100)); } esp_err_t writeReg(uint8_t addr, uint8_t reg, const uint8_t* data, size_t len) { if (!_initialized) return ESP_ERR_INVALID_STATE; + esp_err_t err = ensureDevice(addr); + if (err != ESP_OK) return err; - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); - i2c_master_write_byte(cmd, reg, true); + uint8_t buf[len + 1]; + buf[0] = reg; if (len > 0 && data != nullptr) { - i2c_master_write(cmd, data, len, true); + memcpy(buf + 1, data, len); } - i2c_master_stop(cmd); - esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100)); - i2c_cmd_link_delete(cmd); - return ret; + return i2c_master_transmit(_dev, buf, len + 1, pdMS_TO_TICKS(100)); } esp_err_t readReg(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) { if (!_initialized) return ESP_ERR_INVALID_STATE; - - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); - i2c_master_write_byte(cmd, reg, true); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, true); - if (len > 1) { - i2c_master_read(cmd, data, len - 1, I2C_MASTER_ACK); - } - i2c_master_read_byte(cmd, data + len - 1, I2C_MASTER_NACK); - i2c_master_stop(cmd); - esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100)); - i2c_cmd_link_delete(cmd); - return ret; + esp_err_t err = ensureDevice(addr); + if (err != ESP_OK) return err; + return i2c_master_transmit_receive(_dev, ®, 1, data, len, pdMS_TO_TICKS(100)); } bool probe(uint8_t addr) { if (!_initialized) return false; - - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); - i2c_master_stop(cmd); - esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(50)); - i2c_cmd_link_delete(cmd); - return ret == ESP_OK; + return i2c_master_probe(_bus, addr, pdMS_TO_TICKS(50)) == ESP_OK; } std::vector scan(uint8_t lower = 1, uint8_t upper = 127) { @@ -157,4 +123,23 @@ class I2CBus { gpio_num_t _scl = GPIO_NUM_NC; uint32_t _freq = 100000; bool _initialized = false; + + i2c_master_bus_handle_t _bus = NULL; + i2c_master_dev_handle_t _dev = NULL; + uint8_t _dev_addr = 0xFF; + + esp_err_t ensureDevice(uint8_t addr) { + if (_dev && _dev_addr == addr) return ESP_OK; + if (_dev) { + i2c_master_bus_rm_device(_dev); + _dev = NULL; + } + i2c_device_config_t dev_cfg = {}; + dev_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + dev_cfg.device_address = addr; + dev_cfg.scl_speed_hz = _freq; + esp_err_t err = i2c_master_bus_add_device(_bus, &dev_cfg, &_dev); + if (err == ESP_OK) _dev_addr = addr; + return err; + } }; diff --git a/esp32/src/CMakeLists.txt b/esp32/src/CMakeLists.txt index bda5e5a..7815da0 100644 --- a/esp32/src/CMakeLists.txt +++ b/esp32/src/CMakeLists.txt @@ -1,5 +1,6 @@ set(COMPONENT_REQUIRES driver + esp_driver_i2c esp_http_server nvs_flash esp_wifi @@ -14,7 +15,7 @@ set(COMPONENT_REQUIRES ) if(IDF_TARGET STREQUAL "esp32p4") - list(APPEND COMPONENT_REQUIRES esp_wifi_remote esp_hosted) + list(APPEND COMPONENT_REQUIRES esp_wifi_remote esp_hosted esp_driver_cam esp_driver_isp esp_driver_jpeg espressif__esp_sccb_intf espressif__esp_cam_sensor) else() list(APPEND COMPONENT_REQUIRES esp32-camera) endif() diff --git a/esp32/src/idf_component.yml b/esp32/src/idf_component.yml index a48db62..9ed81ff 100644 --- a/esp32/src/idf_component.yml +++ b/esp32/src/idf_component.yml @@ -27,5 +27,10 @@ dependencies: espressif/esp_hosted: version: ">=0.0.6" + rules: + - if: "target in [esp32p4]" + + espressif/esp_cam_sensor: + version: ">=0.5.0" rules: - if: "target in [esp32p4]" \ No newline at end of file diff --git a/esp32/src/main.cpp b/esp32/src/main.cpp index ca9c2a8..b42f81a 100644 --- a/esp32/src/main.cpp +++ b/esp32/src/main.cpp @@ -59,11 +59,13 @@ void setupServer() { server.on("/api/camera/still", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStill(request); }); server.on("/api/camera/stream", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStream(request); }); +#if USE_DVP_CAMERA server.on("/api/camera/settings", HTTP_GET, [&](httpd_req_t *request) { return cameraService.protoEndpoint.getState(request); }); server.on("/api/camera/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) { return cameraService.protoEndpoint.handleStateUpdate(request, protoReq); }); +#endif #endif server.on("/api/servo/config", HTTP_GET, [&](httpd_req_t *request) { return servoController.protoEndpoint.getState(request); }); diff --git a/esp32/src/peripherals/camera_service.cpp b/esp32/src/peripherals/camera_service.cpp index 6d37e36..6c8f7a1 100644 --- a/esp32/src/peripherals/camera_service.cpp +++ b/esp32/src/peripherals/camera_service.cpp @@ -5,11 +5,14 @@ namespace Camera { static const char *const TAG = "CameraService"; -#if USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4 +#if USE_DVP_CAMERA || USE_CSI_CAMERA static constexpr const char *_STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; static constexpr const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; static constexpr const char *_STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; +#endif + +#if USE_DVP_CAMERA SemaphoreHandle_t cameraMutex = xSemaphoreCreateMutex(); @@ -179,11 +182,359 @@ void CameraService::updateCamera() { safe_sensor_return(); } -#else +#elif USE_CSI_CAMERA -camera_fb_t *safe_camera_fb_get() { return nullptr; } -sensor_t *safe_sensor_get() { return nullptr; } -void safe_sensor_return() {} +} + +extern "C" { +#include "esp_cam_ctlr.h" +#include "esp_cam_ctlr_csi.h" +#include "esp_cam_ctlr_types.h" +#include "driver/isp.h" +#include "driver/jpeg_encode.h" +#include "esp_sccb_intf.h" +#include "esp_sccb_i2c.h" +#include "esp_cam_sensor.h" +#include "ov5647.h" +#include "esp_ldo_regulator.h" +} + +#include + +namespace Camera { + +#ifndef MIPI_CSI_HRES +#define MIPI_CSI_HRES 640 +#endif +#ifndef MIPI_CSI_VRES +#define MIPI_CSI_VRES 480 +#endif +#ifndef MIPI_CSI_LANE_BITRATE_MBPS +#define MIPI_CSI_LANE_BITRATE_MBPS 200 +#endif +#ifndef MIPI_CSI_DATA_LANES +#define MIPI_CSI_DATA_LANES 2 +#endif +#ifndef CAM_SCCB_FREQ_HZ +#define CAM_SCCB_FREQ_HZ 100000 +#endif +#ifndef CAM_SENSOR_ADDR +#define CAM_SENSOR_ADDR 0x36 +#endif +#ifndef CAM_XCLK_PIN +#define CAM_XCLK_PIN -1 +#endif +#ifndef CAM_XCLK_FREQ_HZ +#define CAM_XCLK_FREQ_HZ 25000000 +#endif +#ifndef CAM_RESET_PIN +#define CAM_RESET_PIN -1 +#endif +#ifndef CAM_PWDN_PIN +#define CAM_PWDN_PIN -1 +#endif +#ifndef CSI_JPEG_QUALITY +#define CSI_JPEG_QUALITY 80 +#endif + +static SemaphoreHandle_t s_cam_mutex = NULL; +static SemaphoreHandle_t s_frame_done = NULL; +static esp_cam_ctlr_handle_t s_cam_handle = NULL; +static isp_proc_handle_t s_isp_proc = NULL; +static jpeg_encoder_handle_t s_jpeg_enc = NULL; + +static uint8_t *s_frame_buf = NULL; +static size_t s_frame_buf_size = 0; +static uint8_t *s_jpeg_buf = NULL; +static size_t s_jpeg_buf_size = 0; +static bool s_cam_initialized = false; + +static bool on_trans_finished(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) { + if (trans->buffer != s_frame_buf) return false; + BaseType_t woken = pdFALSE; + xSemaphoreGiveFromISR(s_frame_done, &woken); + return (woken == pdTRUE); +} + +static bool capture_and_encode(uint8_t **jpeg_out, size_t *jpeg_len) { + if (!s_cam_initialized) return false; + + esp_cam_ctlr_trans_t trans = {}; + trans.buffer = s_frame_buf; + trans.buflen = s_frame_buf_size; + + esp_err_t err = esp_cam_ctlr_receive(s_cam_handle, &trans, 2000); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Frame capture failed: %s", esp_err_to_name(err)); + return false; + } + + if (xSemaphoreTake(s_frame_done, pdMS_TO_TICKS(2000)) != pdTRUE) { + ESP_LOGE(TAG, "Frame receive timed out"); + return false; + } + + jpeg_encode_cfg_t enc_cfg = {}; + enc_cfg.src_type = JPEG_ENCODE_IN_FORMAT_RGB565; + enc_cfg.sub_sample = JPEG_DOWN_SAMPLING_YUV422; + enc_cfg.image_quality = CSI_JPEG_QUALITY; + enc_cfg.width = MIPI_CSI_HRES; + enc_cfg.height = MIPI_CSI_VRES; + + uint32_t out_size = 0; + err = jpeg_encoder_process(s_jpeg_enc, &enc_cfg, + s_frame_buf, trans.received_size, + s_jpeg_buf, s_jpeg_buf_size, &out_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "JPEG encode failed: %s", esp_err_to_name(err)); + return false; + } + + *jpeg_out = s_jpeg_buf; + *jpeg_len = out_size; + return true; +} + +CameraService::CameraService() { + s_cam_mutex = xSemaphoreCreateMutex(); + s_frame_done = xSemaphoreCreateBinary(); +} + +esp_err_t CameraService::begin() { + ESP_LOGI(TAG, "Initializing MIPI-CSI camera for ESP32-P4"); + + esp_ldo_channel_handle_t ldo_mipi_phy = NULL; + esp_ldo_channel_config_t ldo_cfg = {}; + ldo_cfg.chan_id = 3; + ldo_cfg.voltage_mv = 2500; + esp_err_t ldo_err = esp_ldo_acquire_channel(&ldo_cfg, &ldo_mipi_phy); + if (ldo_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to acquire MIPI PHY LDO: %s", esp_err_to_name(ldo_err)); + return ldo_err; + } + + i2c_master_bus_handle_t i2c_bus = I2CBus::instance().busHandle(); + if (!i2c_bus) { + ESP_LOGE(TAG, "I2C bus not initialized, cannot init camera SCCB"); + return ESP_ERR_INVALID_STATE; + } + + esp_sccb_io_handle_t sccb_io = NULL; + sccb_i2c_config_t sccb_cfg = {}; + sccb_cfg.scl_speed_hz = CAM_SCCB_FREQ_HZ; + sccb_cfg.device_address = CAM_SENSOR_ADDR; + sccb_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + + esp_err_t err = sccb_new_i2c_io(i2c_bus, &sccb_cfg, &sccb_io); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create SCCB I/O handle: %s", esp_err_to_name(err)); + return err; + } + + esp_cam_sensor_config_t cam_sensor_cfg = {}; + cam_sensor_cfg.sccb_handle = sccb_io; + cam_sensor_cfg.reset_pin = static_cast(CAM_RESET_PIN); + cam_sensor_cfg.pwdn_pin = static_cast(CAM_PWDN_PIN); + cam_sensor_cfg.xclk_pin = static_cast(CAM_XCLK_PIN); + cam_sensor_cfg.xclk_freq_hz = CAM_XCLK_FREQ_HZ; + cam_sensor_cfg.sensor_port = ESP_CAM_SENSOR_MIPI_CSI; + + esp_cam_sensor_device_t *cam_sensor = ov5647_detect(&cam_sensor_cfg); + if (!cam_sensor) { + ESP_LOGE(TAG, "OV5647 detection failed"); + return ESP_FAIL; + } + ESP_LOGI(TAG, "OV5647 camera sensor detected"); + + esp_cam_sensor_format_array_t fmt_array = {}; + err = esp_cam_sensor_query_format(cam_sensor, &fmt_array); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to query sensor formats: %s", esp_err_to_name(err)); + return err; + } + + bool format_set = false; + for (uint32_t i = 0; i < fmt_array.count; i++) { + if (fmt_array.format_array[i].width == MIPI_CSI_HRES && + fmt_array.format_array[i].height == MIPI_CSI_VRES) { + err = esp_cam_sensor_set_format(cam_sensor, &fmt_array.format_array[i]); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Sensor format set: %dx%d", + MIPI_CSI_HRES, MIPI_CSI_VRES); + format_set = true; + break; + } + } + } + if (!format_set && fmt_array.count > 0) { + err = esp_cam_sensor_set_format(cam_sensor, &fmt_array.format_array[0]); + if (err == ESP_OK) { + ESP_LOGW(TAG, "Using fallback sensor format: %dx%d", + fmt_array.format_array[0].width, fmt_array.format_array[0].height); + } + } + + int stream_on = 1; + err = esp_cam_sensor_ioctl(cam_sensor, ESP_CAM_SENSOR_IOC_S_STREAM, &stream_on); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start sensor stream: %s", esp_err_to_name(err)); + return err; + } + + esp_isp_processor_cfg_t isp_cfg = {}; + isp_cfg.clk_src = static_cast(0); + isp_cfg.clk_hz = 80 * 1000 * 1000; + isp_cfg.input_data_source = ISP_INPUT_DATA_SOURCE_CSI; + isp_cfg.input_data_color_type = ISP_COLOR_RAW8; + isp_cfg.output_data_color_type = ISP_COLOR_RGB565; + isp_cfg.has_line_start_packet = false; + isp_cfg.has_line_end_packet = false; + isp_cfg.h_res = MIPI_CSI_HRES; + isp_cfg.v_res = MIPI_CSI_VRES; + isp_cfg.bayer_order = COLOR_RAW_ELEMENT_ORDER_BGGR; + + err = esp_isp_new_processor(&isp_cfg, &s_isp_proc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ISP processor init failed: %s", esp_err_to_name(err)); + return err; + } + + err = esp_isp_enable(s_isp_proc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ISP enable failed: %s", esp_err_to_name(err)); + return err; + } + + esp_cam_ctlr_csi_config_t csi_cfg = {}; + csi_cfg.ctlr_id = 0; + csi_cfg.h_res = MIPI_CSI_HRES; + csi_cfg.v_res = MIPI_CSI_VRES; + csi_cfg.lane_bit_rate_mbps = MIPI_CSI_LANE_BITRATE_MBPS; + csi_cfg.input_data_color_type = CAM_CTLR_COLOR_RAW8; + csi_cfg.output_data_color_type = CAM_CTLR_COLOR_RGB565; + csi_cfg.data_lane_num = MIPI_CSI_DATA_LANES; + csi_cfg.byte_swap_en = false; + csi_cfg.queue_items = 1; + + err = esp_cam_new_csi_ctlr(&csi_cfg, &s_cam_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "CSI controller init failed: %s", esp_err_to_name(err)); + return err; + } + + esp_cam_ctlr_evt_cbs_t cbs = {}; + cbs.on_trans_finished = on_trans_finished; + err = esp_cam_ctlr_register_event_callbacks(s_cam_handle, &cbs, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "CSI register callbacks failed: %s", esp_err_to_name(err)); + return err; + } + + static constexpr size_t CACHE_LINE_SIZE = 64; + s_frame_buf_size = MIPI_CSI_HRES * MIPI_CSI_VRES * 2; + s_frame_buf_size = (s_frame_buf_size + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1); + s_frame_buf = (uint8_t *)heap_caps_aligned_alloc(CACHE_LINE_SIZE, s_frame_buf_size, MALLOC_CAP_SPIRAM); + if (!s_frame_buf) { + ESP_LOGE(TAG, "Failed to allocate frame buffer (%d bytes)", (int)s_frame_buf_size); + return ESP_ERR_NO_MEM; + } + + jpeg_encode_memory_alloc_cfg_t jpeg_mem_cfg = {}; + jpeg_mem_cfg.buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER; + s_jpeg_buf = (uint8_t *)jpeg_alloc_encoder_mem( + MIPI_CSI_HRES * MIPI_CSI_VRES, &jpeg_mem_cfg, &s_jpeg_buf_size); + if (!s_jpeg_buf) { + ESP_LOGE(TAG, "Failed to allocate JPEG buffer"); + return ESP_ERR_NO_MEM; + } + + jpeg_encode_engine_cfg_t enc_eng_cfg = {}; + enc_eng_cfg.timeout_ms = 40; + + err = jpeg_new_encoder_engine(&enc_eng_cfg, &s_jpeg_enc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "JPEG encoder init failed: %s", esp_err_to_name(err)); + return err; + } + + err = esp_cam_ctlr_enable(s_cam_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "CSI controller enable failed: %s", esp_err_to_name(err)); + return err; + } + + err = esp_cam_ctlr_start(s_cam_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "CSI controller start failed: %s", esp_err_to_name(err)); + return err; + } + + s_cam_initialized = true; + ESP_LOGI(TAG, "MIPI-CSI camera initialized (%dx%d, %d-lane, %d Mbps)", + MIPI_CSI_HRES, MIPI_CSI_VRES, MIPI_CSI_DATA_LANES, MIPI_CSI_LANE_BITRATE_MBPS); + return ESP_OK; +} + +esp_err_t CameraService::cameraStill(httpd_req_t *request) { + if (!s_cam_initialized) { + return WebServer::sendError(request, 503, "Camera not initialized"); + } + + xSemaphoreTake(s_cam_mutex, portMAX_DELAY); + + uint8_t *jpeg_buf = NULL; + size_t jpeg_len = 0; + + if (!capture_and_encode(&jpeg_buf, &jpeg_len)) { + xSemaphoreGive(s_cam_mutex); + return WebServer::sendError(request, 500, "Camera capture failed"); + } + + httpd_resp_set_type(request, "image/jpeg"); + httpd_resp_set_hdr(request, "Content-Disposition", "inline; filename=capture.jpg"); + esp_err_t res = httpd_resp_send(request, (const char *)jpeg_buf, jpeg_len); + + xSemaphoreGive(s_cam_mutex); + return res; +} + +esp_err_t CameraService::cameraStream(httpd_req_t *request) { + if (!s_cam_initialized) { + return WebServer::sendError(request, 503, "Camera not initialized"); + } + + httpd_resp_set_type(request, _STREAM_CONTENT_TYPE); + + char part_buf[64]; + esp_err_t res = ESP_OK; + + while (res == ESP_OK) { + xSemaphoreTake(s_cam_mutex, portMAX_DELAY); + + uint8_t *jpeg_buf = NULL; + size_t jpeg_len = 0; + + if (!capture_and_encode(&jpeg_buf, &jpeg_len)) { + xSemaphoreGive(s_cam_mutex); + break; + } + + size_t hlen = snprintf(part_buf, 64, _STREAM_PART, (unsigned int)jpeg_len); + res = httpd_resp_send_chunk(request, part_buf, hlen); + if (res == ESP_OK) res = httpd_resp_send_chunk(request, (const char *)jpeg_buf, jpeg_len); + if (res == ESP_OK) res = httpd_resp_send_chunk(request, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); + + xSemaphoreGive(s_cam_mutex); + + vTaskDelay(pdMS_TO_TICKS(30)); + } + + ESP_LOGI(TAG, "Stream ended"); + httpd_resp_send_chunk(request, NULL, 0); + return ESP_OK; +} + +#else CameraService::CameraService() {} esp_err_t CameraService::begin() { return ESP_ERR_NOT_SUPPORTED; }