feat: 链路控制、延迟校准与长测工具,补全文档
- 固件:新增 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:
@@ -2,4 +2,4 @@ idf_component_register(SRCS "cam_wrapper.c"
|
||||
INCLUDE_DIRS "include"
|
||||
# esp_timer for the frame-rate probe; the camera component
|
||||
# pulls in the LCD_CAM driver and the FreeRTOS task API.
|
||||
REQUIRES esp32-camera esp_timer)
|
||||
REQUIRES esp32-camera esp_timer esp_driver_tsens)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ typedef struct {
|
||||
size_t height;
|
||||
int format;
|
||||
void *handle; /* internal driver frame; do not touch */
|
||||
int64_t capture_started_us; /* same monotonic clock as esp_timer_get_time */
|
||||
uint32_t skipped_before; /* older completed frames released by latest get */
|
||||
} cam_wrapper_fb_t;
|
||||
|
||||
esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_sensor_t sensor);
|
||||
@@ -119,6 +121,7 @@ esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_senso
|
||||
/* Acquire one frame into `out`. Returns ESP_OK on success, ESP_FAIL if the
|
||||
* driver could not hand out a frame (e.g. timeout or buffer starvation). */
|
||||
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor);
|
||||
esp_err_t cam_wrapper_fb_get_latest(cam_wrapper_fb_t *out, uint32_t timeout_ms);
|
||||
|
||||
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb);
|
||||
|
||||
@@ -129,6 +132,26 @@ esp_err_t cam_wrapper_deinit(void);
|
||||
/* Name of the sensor the driver detected, or "unknown". Never NULL. */
|
||||
const char *cam_wrapper_sensor_name(void);
|
||||
|
||||
/* Keep the sensor's JPEG encoder; effect 2 removes chroma in the sensor ISP. */
|
||||
esp_err_t cam_wrapper_set_grayscale(int enabled);
|
||||
/* Runtime JPEG compression, 0..63, without restarting the camera pipeline. */
|
||||
esp_err_t cam_wrapper_set_quality(unsigned quality);
|
||||
/* OV5640 target bias: 0 = default, -1 / -2 select approximately -1.1 / -1.9 EV. */
|
||||
esp_err_t cam_wrapper_set_exposure_bias(int ev);
|
||||
/* 0 = auto shutter; nonzero holds that exposure in sensor lines, AGC stays on. */
|
||||
esp_err_t cam_wrapper_set_shutter_lines(unsigned lines);
|
||||
/* Keep AEC/AGC automatic, cap exposure lines, and disable night frame extension. */
|
||||
esp_err_t cam_wrapper_set_exposure_limit(unsigned lines);
|
||||
/* 0 keeps the driver timing; nonzero changes OV5640 horizontal total only. */
|
||||
esp_err_t cam_wrapper_set_horizontal_timing(unsigned hts);
|
||||
int cam_wrapper_exposure_lines(void);
|
||||
/* OV5640 gain in 1/16 units; -1 when unavailable. */
|
||||
int cam_wrapper_gain_raw(void);
|
||||
float cam_wrapper_probe_frame_rate(void);
|
||||
/* Silicon temperature, not ambient or camera-sensor temperature. */
|
||||
esp_err_t cam_wrapper_temperature_init(void);
|
||||
esp_err_t cam_wrapper_temperature_read(float *celsius);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user