Files
ESP32S3_vt/components/cam_wrapper/cam_wrapper.c
T
sloray cc6b82d7ec
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
feat: 链路控制、延迟校准与长测工具,补全文档
- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、
  温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。
- 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。
- 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增
  延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。
- 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README;
  新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。
- 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
2026-10-19 00:19:07 +08:00

411 lines
14 KiB
C

/* Implementation of the cam_wrapper shim around esp32-camera. See cam_wrapper.h. */
#include "cam_wrapper.h"
#include "driver/temperature_sensor.h"
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
static float 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 temperature_sensor_handle_t s_temperature;
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 float 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 0;
}
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));
return (float)fps;
}
esp_err_t cam_wrapper_set_grayscale(int enabled)
{
if (!s_sensor || !s_sensor->set_special_effect) return ESP_ERR_NOT_SUPPORTED;
int rc = s_sensor->set_special_effect(s_sensor, enabled ? 2 : 0);
if (rc != 0) return ESP_FAIL;
ESP_LOGI(TAG, "sensor JPEG colour: %s", enabled ? "grayscale" : "colour");
return ESP_OK;
}
esp_err_t cam_wrapper_set_quality(unsigned quality)
{
if (quality > 63) return ESP_ERR_INVALID_ARG;
if (!s_sensor || !s_sensor->set_quality) return ESP_ERR_NOT_SUPPORTED;
return s_sensor->set_quality(s_sensor, (int)quality) == 0 ? ESP_OK : ESP_FAIL;
}
esp_err_t cam_wrapper_set_exposure_bias(int ev)
{
if (ev < -2 || ev > 0) return ESP_ERR_INVALID_ARG;
if (!s_sensor || s_sensor->id.PID != OV5640_PID)
return ev == 0 ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
/* Driver target = (level+5)*10+5, default 55. Targets 25 and 15
* are ~-1.14 and ~-1.87 EV, not literal driver levels -1 and -2. */
int level = ev == -1 ? -3 : ev == -2 ? -4 : 0;
if (s_sensor->set_exposure_ctrl(s_sensor, 1) != 0
|| s_sensor->set_gain_ctrl(s_sensor, 1) != 0
|| s_sensor->set_ae_level(s_sensor, level) != 0) return ESP_FAIL;
ESP_LOGI(TAG, "OV5640 auto exposure bias: requested %d EV, driver level %d, target %d / 55",
ev, level, (level + 5) * 10 + 5);
return ESP_OK;
}
int cam_wrapper_exposure_lines(void)
{
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return -1;
int raw = s_sensor->get_reg(s_sensor, 0x3500, 0xfffff);
return raw < 0 ? -1 : raw >> 4;
}
int cam_wrapper_gain_raw(void)
{
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return -1;
return s_sensor->get_reg(s_sensor, 0x350a, 0x03ff);
}
esp_err_t cam_wrapper_set_shutter_lines(unsigned lines)
{
if (!s_sensor || s_sensor->id.PID != OV5640_PID)
return lines == 0 ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
int vts = s_sensor->get_reg(s_sensor, 0x380e, 0xffff);
if (vts <= 0 || lines > (unsigned)vts) return ESP_ERR_INVALID_ARG;
int previous = cam_wrapper_exposure_lines();
if (s_sensor->set_exposure_ctrl(s_sensor, lines == 0) != 0
|| s_sensor->set_gain_ctrl(s_sensor, 1) != 0) return ESP_FAIL;
if (lines && (s_sensor->set_aec_value(s_sensor, (int)lines) != 0
|| cam_wrapper_exposure_lines() != (int)lines)) return ESP_FAIL;
ESP_LOGI(TAG, "OV5640 shutter: previous %d lines, requested %u lines (0=auto), AGC enabled",
previous, lines);
return ESP_OK;
}
esp_err_t cam_wrapper_set_horizontal_timing(unsigned hts)
{
if (!hts) return ESP_OK;
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return ESP_ERR_NOT_SUPPORTED;
int start = s_sensor->get_reg(s_sensor, 0x3800, 0x0fff);
int end = s_sensor->get_reg(s_sensor, 0x3804, 0x0fff);
int previous = s_sensor->get_reg(s_sensor, 0x380c, 0x0fff);
if (start < 0 || end < start || previous <= 0) return ESP_FAIL;
unsigned active = (unsigned)(end - start + 1) / (s_sensor->status.binning ? 2 : 1);
/* Preserve the active readout window and a conservative blanking margin.
* This is a guard against invalid geometry, not a stability guarantee. */
if ((hts & 1) || hts < active + 64 || hts > 4095) return ESP_ERR_INVALID_ARG;
/* Use the byte order that cannot transiently shorten the line below
* either valid endpoint while updating this live sensor. */
int rc;
if (hts < (unsigned)previous) {
rc = s_sensor->set_reg(s_sensor, 0x380d, 0xff, hts & 0xff)
|| s_sensor->set_reg(s_sensor, 0x380c, 0x0f, hts >> 8);
} else {
rc = s_sensor->set_reg(s_sensor, 0x380c, 0x0f, hts >> 8)
|| s_sensor->set_reg(s_sensor, 0x380d, 0xff, hts & 0xff);
}
if (rc || s_sensor->get_reg(s_sensor, 0x380c, 0x0fff) != (int)hts) return ESP_FAIL;
ESP_LOGI(TAG, "OV5640 horizontal timing: HTS %d -> %u, active %u, VTS %d (PLL/XCLK unchanged)",
previous, hts, active, s_sensor->get_reg(s_sensor, 0x380e, 0xffff));
return ESP_OK;
}
esp_err_t cam_wrapper_set_exposure_limit(unsigned lines)
{
if (!lines) return ESP_OK;
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return ESP_ERR_NOT_SUPPORTED;
int vts = s_sensor->get_reg(s_sensor, 0x380e, 0xffff);
int band50 = s_sensor->get_reg(s_sensor, 0x3a08, 0x03ff);
int band60 = s_sensor->get_reg(s_sensor, 0x3a0a, 0x03ff);
if (vts <= 4 || lines > (unsigned)(vts - 4) || band50 <= 0 || band60 <= 0)
return ESP_ERR_INVALID_ARG;
unsigned max50 = lines / (unsigned)band50;
unsigned max60 = lines / (unsigned)band60;
if (!max50) max50 = 1;
if (!max60) max60 = 1;
if (max50 > 63 || max60 > 63) return ESP_ERR_INVALID_ARG;
int rc = s_sensor->set_exposure_ctrl(s_sensor, 0)
|| s_sensor->set_aec2(s_sensor, 0)
|| s_sensor->set_reg(s_sensor, 0x3a02, 0xff, lines >> 8)
|| s_sensor->set_reg(s_sensor, 0x3a03, 0xff, lines & 0xff)
|| s_sensor->set_reg(s_sensor, 0x3a14, 0x0f, lines >> 8)
|| s_sensor->set_reg(s_sensor, 0x3a15, 0xff, lines & 0xff)
|| s_sensor->set_reg(s_sensor, 0x3a0d, 0x3f, max60)
|| s_sensor->set_reg(s_sensor, 0x3a0e, 0x3f, max50)
|| s_sensor->set_gain_ctrl(s_sensor, 1)
|| s_sensor->set_exposure_ctrl(s_sensor, 1);
if (rc || s_sensor->get_reg(s_sensor, 0x3a02, 0xffff) != (int)lines
|| s_sensor->get_reg(s_sensor, 0x3a14, 0x0fff) != (int)lines)
return ESP_FAIL;
ESP_LOGI(TAG, "OV5640 auto exposure cap: %u lines, band maxima 50Hz=%u / 60Hz=%u, AEC/AGC auto, night mode off",
lines, max50, max60);
return ESP_OK;
}
float cam_wrapper_probe_frame_rate(void)
{
/* Drain the old three-buffer pipeline after changing the colour effect. */
for (int i = 0; i < 4; ++i) {
cam_wrapper_fb_t fb;
if (cam_wrapper_fb_get(&fb, CAM_WRAPPER_SENSOR_AUTO) == ESP_OK)
cam_wrapper_fb_release(&fb);
}
return cam_wrapper_measure_fps(CAM_WRAPPER_SENSOR_AUTO);
}
esp_err_t cam_wrapper_temperature_init(void)
{
if (s_temperature) return ESP_OK;
temperature_sensor_config_t config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 100);
esp_err_t rc = temperature_sensor_install(&config, &s_temperature);
if (rc != ESP_OK) return rc;
rc = temperature_sensor_enable(s_temperature);
if (rc != ESP_OK) {
temperature_sensor_uninstall(s_temperature);
s_temperature = NULL;
}
return rc;
}
esp_err_t cam_wrapper_temperature_read(float *celsius)
{
if (!s_temperature) return ESP_ERR_INVALID_STATE;
return temperature_sensor_get_celsius(s_temperature, celsius);
}
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;
out->capture_started_us = (int64_t)fb->timestamp.tv_sec * 1000000 + fb->timestamp.tv_usec;
return ESP_OK;
}
esp_err_t cam_wrapper_fb_get_latest(cam_wrapper_fb_t *out, uint32_t timeout_ms)
{
if (!out) return ESP_ERR_INVALID_ARG;
memset(out, 0, sizeof(*out));
uint32_t skipped = 0;
camera_fb_t *fb = esp_camera_fb_get_latest(timeout_ms, &skipped);
if (!fb) 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;
out->capture_started_us = (int64_t)fb->timestamp.tv_sec * 1000000 + fb->timestamp.tv_usec;
out->skipped_before = skipped;
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;
}