两块 ESP32-S3-WROOM-1-N16R8 图传板(DVP 接口接 OV2640/OV3660/OV5640), 向队伍客户端与裁判终端推 UDP 图传。固件是跑在 ESP-IDF 上的 Rust,用官方 esp32-camera 组件采集;客户端是 Rust + Slint 桌面程序。 三个 crate 各自独立构建: . firmware,交叉编译到 Xtensa protocol/ 线路格式与帧重组,两端共用,无 ESP-IDF 依赖,有单元测试 client/ Slint 桌面客户端 视频走 UDP 而非 TCP:实测传感器产出 22 fps,TCP MJPEG 只送出 1.9 fps, 且只占用链路能力的 1%——链路是空的,帧堵在队头阻塞后面等一个实时画面 等不起的重传计时器。改为独立数据报、不重传、缺片丢整帧后达到 20.8 fps。 发现是双向的:板子每 2 秒广播 Hello,客户端广播 Subscribe,板子据此定向 推流。板子身份由 MAC 派生,重启不变,所以客户端可以把绑定存进配置。 裁判端同时收两块板子(每个发送方一个重组器),队伍端绑定其中一块并只显示 自己那一边的血量。装甲板接口已预留(ArmorSource trait + ARMOR_PORT), 当前无数据源,界面显示占位符。 vendor 了 esp32-camera 2.1.7:组件注册表的版本求解器找不到它,且需要打一处 补丁(见 components/esp32-camera/target/esp32s3/ll_cam.c)。 .gitignore 三条 target/ 都锚定在根目录,因为 protocol/ 与 client/ 各自也是 独立 crate;而 components/esp32-camera/target/ 是上游的按芯片分目录,里面是 源码,必须保留。
300 lines
10 KiB
Rust
300 lines
10 KiB
Rust
//! 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 `<enum type>_<VARIANT>`, 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<Self, Self::Err> {
|
|
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<SensorHint> 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<Self> {
|
|
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<Frame, FrameError> {
|
|
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];
|
|
} |