//! Thin safe wrapper over the `cam_wrapper` C component (see //! `components/cam_wrapper`). //! //! The pin map matches the XIAO ESP32S3 Sense camera connector, which is the //! common OV2640/OV3660 wiring on an ESP32-S3-N16R8. Adjust `PINS` at the //! bottom of this file if your board wires the DVP bus differently. use std::ffi::CStr; use anyhow::{anyhow, Result}; use esp_idf_svc::sys; // bindgen names C enum variants `_`, so alias them to // something readable here instead of scattering those prefixes around. use esp_idf_svc::sys::camera::{ cam_wrapper_fb_location_t_CAM_WRAPPER_FB_IN_PSRAM as FB_IN_PSRAM, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_CIF as FRAMESIZE_CIF, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_HVGA as FRAMESIZE_HVGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_QQVGA as FRAMESIZE_QQVGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_QVGA as FRAMESIZE_QVGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_SVGA as FRAMESIZE_SVGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_SXGA as FRAMESIZE_SXGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_UXGA as FRAMESIZE_UXGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_VGA as FRAMESIZE_VGA, cam_wrapper_framesize_t_CAM_WRAPPER_FRAMESIZE_XGA as FRAMESIZE_XGA, cam_wrapper_grab_mode_t_CAM_WRAPPER_GRAB_LATEST as GRAB_LATEST, cam_wrapper_pixel_format_t_CAM_WRAPPER_PIXFORMAT_JPEG as PIXFORMAT_JPEG, cam_wrapper_sensor_t_CAM_WRAPPER_SENSOR_AUTO as SENSOR_AUTO, cam_wrapper_sensor_t_CAM_WRAPPER_SENSOR_OV2640 as SENSOR_OV2640, cam_wrapper_sensor_t_CAM_WRAPPER_SENSOR_OV3660 as SENSOR_OV3660, cam_wrapper_sensor_t_CAM_WRAPPER_SENSOR_OV5640 as SENSOR_OV5640, }; /// Capture resolution. /// /// Anything above CIF needs PSRAM, which the N16R8 has; the bandwidth cost of /// the bigger sizes is real, so start at VGA and only go higher if the link /// keeps up. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Resolution { Qqvga, Qvga, Cif, Hvga, Vga, Svga, Xga, Sxga, Uxga, } impl Resolution { fn framesize(self) -> sys::camera::cam_wrapper_framesize_t { match self { Resolution::Qqvga => FRAMESIZE_QQVGA, Resolution::Qvga => FRAMESIZE_QVGA, Resolution::Cif => FRAMESIZE_CIF, Resolution::Hvga => FRAMESIZE_HVGA, Resolution::Vga => FRAMESIZE_VGA, Resolution::Svga => FRAMESIZE_SVGA, Resolution::Xga => FRAMESIZE_XGA, Resolution::Sxga => FRAMESIZE_SXGA, Resolution::Uxga => FRAMESIZE_UXGA, } } } impl std::str::FromStr for Resolution { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "qqvga" => Ok(Resolution::Qqvga), "qvga" => Ok(Resolution::Qvga), "cif" => Ok(Resolution::Cif), "hvga" => Ok(Resolution::Hvga), "vga" => Ok(Resolution::Vga), "svga" => Ok(Resolution::Svga), "xga" => Ok(Resolution::Xga), "sxga" => Ok(Resolution::Sxga), "uxga" => Ok(Resolution::Uxga), other => Err(anyhow!("unknown resolution {other:?}")), } } } use log::info; /// Sensor override. `Auto` lets esp32-camera probe the SCCB bus, which is what /// you want unless the probe is ambiguous. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SensorHint { Auto, Ov2640, Ov3660, Ov5640, } impl From for sys::camera::cam_wrapper_sensor_t { fn from(hint: SensorHint) -> Self { match hint { SensorHint::Auto => SENSOR_AUTO, SensorHint::Ov2640 => SENSOR_OV2640, SensorHint::Ov3660 => SENSOR_OV3660, SensorHint::Ov5640 => SENSOR_OV5640, } } } /// One captured JPEG frame. Holds the driver frame buffer, so the data is only /// valid until the `Frame` is dropped. pub struct Frame { fb: sys::camera::cam_wrapper_fb_t, } impl Frame { pub fn data(&self) -> &[u8] { if self.fb.data.is_null() || self.fb.len == 0 { return &[]; } // SAFETY: `data` points at `len` bytes owned by esp32-camera, which // stays valid until this Frame is dropped (cam_wrapper_fb_release). unsafe { std::slice::from_raw_parts(self.fb.data, self.fb.len) } } /// Size of the JPEG in bytes. /// /// Reported in the sender's statistics, which is how a frame rate problem gets /// attributed to the capture side or the transport side. #[allow(dead_code)] pub fn len(&self) -> usize { self.fb.len } /// Capture width in pixels, as the sensor actually produced it (which can be /// smaller than the requested frame size). pub fn width(&self) -> usize { self.fb.width } /// Capture height in pixels. pub fn height(&self) -> usize { self.fb.height } } impl Drop for Frame { fn drop(&mut self) { unsafe { sys::camera::cam_wrapper_fb_release(&mut self.fb) } } } /// The driver could not hand out a frame. #[derive(Debug, Clone, Copy)] pub struct FrameError; impl std::fmt::Display for FrameError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("camera returned no frame") } } impl std::error::Error for FrameError {} /// An initialized camera. /// /// Held for the lifetime of the program: esp32-camera keeps a task and the /// frame buffers running, and tearing them down would only be needed to power /// the sensor down, which this program never does. pub struct Camera { sensor: SensorHint, } impl Camera { /// Brings up the sensor and blocks until it has produced frames. /// /// `vsync_invert` should be true for a directly connected DVP VSYNC (the /// esp32-camera default) and false if the board inverts the line in hardware. pub fn init( sensor: SensorHint, resolution: Resolution, jpeg_quality: u8, vsync_invert: bool, ) -> Result { if jpeg_quality > 63 { return Err(anyhow!("jpeg_quality must be 0-63, got {jpeg_quality}")); } let config = sys::camera::cam_wrapper_config_t { pin_pwdn: -1, pin_reset: pins::RESET, pin_xclk: pins::XCLK, pin_sccb_sda: pins::SIOD, pin_sccb_scl: pins::SIOC, pin_d: pins::DATA, pin_vsync: pins::VSYNC, pin_href: pins::HREF, pin_pclk: pins::PCLK, // 20 MHz is what the OV5640 datasheet calls for, and esp32-camera's // OV5640 driver is only PLL-tuned for it; raising this without // retuning the sensor's own PLL just corrupts frames. xclk_freq_hz: 20_000_000, ledc_timer: 0, ledc_channel: 0, pixel_format: PIXFORMAT_JPEG, frame_size: resolution.framesize(), jpeg_quality: jpeg_quality as i32, // Three buffers, not two: esp32-camera needs a free buffer at every // VSYNC, and with two a client that is still draining the previous // frame costs a whole capture period. One is spent filling, one is // being written to the socket, and one is spare for the moment the // write blocks. fb_count: 3, fb_location: FB_IN_PSRAM, // LATEST keeps the newest frame in the queue instead of handing out // whatever was captured first, which matters once a client is slow // enough for frames to pile up. grab_mode: GRAB_LATEST, sccb_i2c_port: 0, // esp32-camera inverts VSYNC by default, which is right for the // large majority of DVP sensors. A board that inverts the line // itself needs this cleared: a mismatch here does not fail, it // makes the driver latch frames on the wrong edge and the frame rate // collapses while the pictures still look plausible. vsync_invert: if vsync_invert { 1 } else { 0 }, }; let rc = unsafe { sys::camera::cam_wrapper_init(&config, sensor.into()) }; if rc != 0 { return Err(anyhow!( "camera init failed (esp_err 0x{:x}) - check the DVP/SCCB wiring and that \ octal PSRAM is enabled", rc )); } info!("camera ready: {}", Self::sensor_name()); Ok(Self { sensor }) } pub fn sensor_name() -> String { unsafe { let ptr = sys::camera::cam_wrapper_sensor_name(); if ptr.is_null() { String::from("unknown") } else { CStr::from_ptr(ptr).to_string_lossy().into_owned() } } } /// Grabs the next frame. /// /// This blocks until the sensor has one, which is what paces the stream. /// `Err` means the driver could not hand out a frame at all; the camera is /// still usable afterwards, so callers should keep going rather than treat /// it as fatal. pub fn next_frame(&self) -> Result { let mut fb: sys::camera::cam_wrapper_fb_t = unsafe { std::mem::zeroed() }; let rc = unsafe { sys::camera::cam_wrapper_fb_get(&mut fb, self.sensor.into()) }; if rc != 0 || fb.data.is_null() || fb.len == 0 { // Nothing was handed out, but be defensive in case a future // driver version sets a partial frame. unsafe { sys::camera::cam_wrapper_fb_release(&mut fb) }; return Err(FrameError); } Ok(Frame { fb }) } } /// Camera connector of the Freenove ESP32-S3-WROOM CAM board. /// /// Every pin here is one the ESP32-S3 leaves free: 26..37 are bonded to the /// module's SPI flash and octal PSRAM, 19/20 are the native USB pair this /// firmware logs over, and 43/44 are the console UART. Touching IO_MUX on any /// of the flash/PSRAM pins hangs the CPU mid-transfer, which is what happens if /// you reuse a pin map written for a different board. /// /// The data bus is the sensor's Y2..Y9, i.e. the low byte of the 10-bit pixel /// word, so Y2 is D0 and Y9 is D7. mod pins { pub const RESET: i32 = 0; pub const XCLK: i32 = 15; pub const PCLK: i32 = 13; pub const VSYNC: i32 = 6; pub const HREF: i32 = 7; pub const SIOD: i32 = 4; pub const SIOC: i32 = 5; /// D0..D7 == Y2..Y9, in the order `camera_config_t` expects. pub const DATA: [i32; 8] = [11, 9, 8, 10, 12, 18, 17, 16]; }