两块 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/ 是上游的按芯片分目录,里面是 源码,必须保留。
236 lines
6.9 KiB
C
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;
|
|
} |