Files
ESP32S3_vt/components/cam_wrapper/cam_wrapper.c
T
Horldsence 4f893ce140 ESP32-S3 摄像头图传:固件 + Slint 客户端 + 共享协议
两块 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/ 是上游的按芯片分目录,里面是
源码,必须保留。
2026-10-05 18:22:19 +08:00

236 lines
6.9 KiB
C

/* Implementation of the cam_wrapper shim around esp32-camera. See cam_wrapper.h. */
#include "cam_wrapper.h"
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor);
static const char *TAG = "cam_wrapper";
static sensor_t *s_sensor;
static char s_sensor_name[16] = "unknown";
static void fill_config(const cam_wrapper_config_t *in, camera_config_t *out)
{
memset(out, 0, sizeof(*out));
out->pin_pwdn = in->pin_pwdn;
out->pin_reset = in->pin_reset;
out->pin_xclk = in->pin_xclk;
out->pin_sccb_sda = in->pin_sccb_sda;
out->pin_sccb_scl = in->pin_sccb_scl;
out->pin_d0 = in->pin_d[0];
out->pin_d1 = in->pin_d[1];
out->pin_d2 = in->pin_d[2];
out->pin_d3 = in->pin_d[3];
out->pin_d4 = in->pin_d[4];
out->pin_d5 = in->pin_d[5];
out->pin_d6 = in->pin_d[6];
out->pin_d7 = in->pin_d[7];
out->pin_vsync = in->pin_vsync;
out->pin_href = in->pin_href;
out->pin_pclk = in->pin_pclk;
out->xclk_freq_hz = in->xclk_freq_hz;
out->ledc_timer = (ledc_timer_t)in->ledc_timer;
out->ledc_channel = (ledc_channel_t)in->ledc_channel;
out->pixel_format = (pixformat_t)in->pixel_format;
out->frame_size = (framesize_t)in->frame_size;
out->jpeg_quality = in->jpeg_quality;
out->fb_count = in->fb_count;
out->fb_location = (camera_fb_location_t)in->fb_location;
out->grab_mode = (camera_grab_mode_t)in->grab_mode;
out->sccb_i2c_port = in->sccb_i2c_port;
}
esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_sensor_t sensor)
{
camera_config_t cfg;
(void)sensor;
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
fill_config(config, &cfg);
ESP_LOGI(TAG, "config: pwdn=%d reset=%d xclk=%d sda=%d scl=%d "
"d=[%d %d %d %d %d %d %d %d] vsync=%d href=%d pclk=%d "
"fmt=%d size=%d q=%d fb=%u loc=%d grab=%d i2c=%d vsync_inv=%d",
cfg.pin_pwdn, cfg.pin_reset, cfg.pin_xclk,
cfg.pin_sccb_sda, cfg.pin_sccb_scl,
cfg.pin_d0, cfg.pin_d1, cfg.pin_d2, cfg.pin_d3,
cfg.pin_d4, cfg.pin_d5, cfg.pin_d6, cfg.pin_d7,
cfg.pin_vsync, cfg.pin_href, cfg.pin_pclk,
(int)cfg.pixel_format, (int)cfg.frame_size, cfg.jpeg_quality,
(unsigned)cfg.fb_count, (int)cfg.fb_location, (int)cfg.grab_mode,
cfg.sccb_i2c_port, config->vsync_invert);
/* Must be set before init: cam_init copies it into the driver object, and
* ll_cam_set_pin applies it to the GPIO matrix for the VSYNC input. */
cam_set_vsync_invert(config->vsync_invert != 0);
esp_err_t err = esp_camera_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_camera_init failed: 0x%x", err);
return err;
}
/* The driver probes the SCCB bus and binds a driver by the sensor's
* PID/ID, so name the sensor from the matched entry of its table rather
* than reporting something the driver does not actually know. */
s_sensor = esp_camera_sensor_get();
if (s_sensor != NULL) {
sensor_id_t id = s_sensor->id;
const camera_sensor_info_t *info = esp_camera_sensor_get_info(&id);
if (info != NULL && info->name != NULL) {
strncpy(s_sensor_name, info->name, sizeof(s_sensor_name) - 1);
s_sensor_name[sizeof(s_sensor_name) - 1] = '\0';
}
ESP_LOGI(TAG, "sensor detected: %s (pid=0x%04x, ver=0x%02x)",
s_sensor_name, id.PID, id.VER);
} else {
ESP_LOGW(TAG, "camera initialized but no sensor handle is available");
}
/* Measure what the sensor actually produces, here, with no WiFi and no HTTP
* in the picture. Everything downstream (socket writes, TCP, the browser) is
* downstream of this number, so when the stream is slow this is what says
* whether the problem is capture or transport. */
cam_wrapper_fb_t probe;
memset(&probe, 0, sizeof(probe));
if (cam_wrapper_fb_get(&probe, sensor) == ESP_OK) {
ESP_LOGI(TAG, "first frame: %ux%u, %u bytes, format %d",
(unsigned)probe.width, (unsigned)probe.height,
(unsigned)probe.len, probe.format);
cam_wrapper_fb_release(&probe);
cam_wrapper_measure_fps(sensor);
} else {
ESP_LOGW(TAG, "camera initialized but returned no frame");
}
return ESP_OK;
}
/* Grabs PROBE_FRAMES frames back to back and logs the achieved rate.
*
* This runs on the capture path with nothing else contending for the sensor, so
* it is the ceiling the stream can ever reach. Comparing it against the rate the
* stream actually achieves localises the bottleneck: equal means the transport is
* keeping up and the sensor is the limit, much lower means something between the
* two is dropping frames. */
#define PROBE_FRAMES 20
static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
{
uint32_t total = 0;
int64_t start = esp_timer_get_time();
int taken = 0;
for (int i = 0; i < PROBE_FRAMES; i++) {
cam_wrapper_fb_t fb;
memset(&fb, 0, sizeof(fb));
if (cam_wrapper_fb_get(&fb, sensor) != ESP_OK) {
ESP_LOGW(TAG, "fps probe: no frame at index %d", i);
continue;
}
total += (uint32_t)fb.len;
taken++;
cam_wrapper_fb_release(&fb);
}
int64_t elapsed_us = esp_timer_get_time() - start;
if (taken == 0 || elapsed_us <= 0) {
ESP_LOGW(TAG, "fps probe: got no frames");
return;
}
double fps = (double)taken * 1000000.0 / (double)elapsed_us;
ESP_LOGI(TAG, "fps probe: %d frames in %lld ms -> %.1f fps, avg %u bytes "
"(sensor ceiling, no network involved)",
taken, (long long)(elapsed_us / 1000), fps,
(unsigned)(total / (uint32_t)taken));
}
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor)
{
(void)sensor;
if (out == NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(out, 0, sizeof(*out));
camera_fb_t *fb = esp_camera_fb_get();
if (fb == NULL) {
return ESP_FAIL;
}
out->data = fb->buf;
out->len = fb->len;
out->width = fb->width;
out->height = fb->height;
out->format = (int)fb->format;
out->handle = fb;
return ESP_OK;
}
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb)
{
if (fb == NULL || fb->handle == NULL) {
return;
}
esp_camera_fb_return((camera_fb_t *)fb->handle);
fb->data = NULL;
fb->len = 0;
fb->handle = NULL;
}
esp_err_t cam_wrapper_reconfigure(const cam_wrapper_config_t *config)
{
camera_config_t cfg;
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
fill_config(config, &cfg);
return esp_camera_reconfigure(&cfg);
}
esp_err_t cam_wrapper_deinit(void)
{
esp_err_t err = esp_camera_deinit();
s_sensor = NULL;
strcpy(s_sensor_name, "unknown");
return err;
}
const char *cam_wrapper_sensor_name(void)
{
return s_sensor_name;
}