feat: 链路控制、延迟校准与长测工具,补全文档
构建客户端 / 检查(类型 + 单元测试) (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

- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、
  温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。
- 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。
- 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增
  延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。
- 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README;
  新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。
- 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
This commit is contained in:
2026-10-19 00:19:07 +08:00
parent 4794bed2cf
commit cc6b82d7ec
60 changed files with 4347 additions and 655 deletions
+178 -4
View File
@@ -1,18 +1,20 @@
/* 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 void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor);
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)
{
@@ -134,7 +136,7 @@ esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_senso
* two is dropping frames. */
#define PROBE_FRAMES 20
static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
static float cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
{
uint32_t total = 0;
int64_t start = esp_timer_get_time();
@@ -158,7 +160,7 @@ static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
if (taken == 0 || elapsed_us <= 0) {
ESP_LOGW(TAG, "fps probe: got no frames");
return;
return 0;
}
double fps = (double)taken * 1000000.0 / (double)elapsed_us;
@@ -167,6 +169,159 @@ static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
"(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)
@@ -190,10 +345,29 @@ esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor)
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) {
@@ -233,4 +407,4 @@ esp_err_t cam_wrapper_deinit(void)
const char *cam_wrapper_sensor_name(void)
{
return s_sensor_name;
}
}