系统架构
1. 系统概览
ESP32-S3 摄像头监控系统基于 FreeRTOS 实时操作系统,在双核 ESP32-S3 上运行 27 个松耦合的 C 模块。系统采用事件驱动与轮询混合架构,摄像头采集帧数据后分两路输出:实时 HTTP MJPEG 流和录像存储。
Camera ──→ Frame Broadcaster ──→ MJPEG Streamer (端口 81) ──→ 浏览器/客户端
└→ Video Recorder ──→ SD Card (AVI 分段)
└→ NAS Uploader ──→ WebDAV / HTTP(S) (互斥)
Config Manager ←→ NVS ←→ SD Card Override (wifi.txt / nas.txt)
WiFi Manager (AP/STA) → Time Sync (SNTP)
Status LED ← LED Controller (GPIO21, 低电平有效)
Watchdog (30s TWDT, panic) → Health Monitor (60s)
ONVIF Discovery (UDP 组播) → NVR 自动发现
音频驱动(I2S PDM DSR_16S, 8kHz)→ DC 去除 → 频谱减法 → 噪声门 → G.711 → 音频广播器
→ RTSP 服务器(端口 554)+ Web 服务器(/api/audio HTTP 流)
核心特性
| 特性 | 说明 |
|---|---|
| 双核分工 | Core 0: 音频采集(优先级 5),录像(优先级 3),RTSP 音频,HTTP/WebSocket;Core 1: MJPEG/RTSP 视频流,SD 写入任务,SD 监控,健康检测,ONVIF |
| PSRAM 依赖 | 摄像头帧缓冲分配在 PSRAM(双缓冲),无 PSRAM 无法工作 |
| 循环存储 | SD 卡剩余空间 < 20% 自动删除最旧录像,恢复到 30% |
| 热插拔 | SD 卡拔出自动停止录像,插入自动恢复 |
| 看门狗 | 30s TWDT,超时触发 panic 重启 |
| ONVIF 发现 | WS-Discovery 组播,NVR 可自动发现 |
2. 启动流程
app_main() 中 21 步顺序初始化,任何关键步骤失败仅记录错误但继续执行(部分功能降级)。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 NVS Flash | 损坏时自动擦除重建 |
| 2 | 配置管理器 | 从 NVS 加载配置,无配置则使用默认值 |
| 3 | 状态 LED | GPIO21 初始化,设为 LED_STARTING(常亮) |
| 4 | SPIFFS 挂载 | 加载 Web UI 静态资源,挂载失败自动格式化 |
| 5 | SD 卡存储 | SPI 模式,FAT32 文件系统 |
| 5a | 清理不完整 AVI | 删除上次异常断电时未正确关闭的录像文件 |
| 6 | SD 卡配置覆盖 | 读取 SD 卡 /sdcard/config/wifi.txt 和 config.txt,覆盖 NVS 配置 |
| 7 | WiFi 初始化 | 已配置 SSID → STA 模式,否则 → AP 模式 |
| 8 | 摄像头初始化 | 自动检测 OV2640/OV3660,配置分辨率/帧率/质量,测试采集验证 |
| 9 | 时间同步 | STA 连接后启动 SNTP,阻塞等待最多 5 秒 |
| 10 | NAS 上传器 | 创建上传队列(容量 16)和上传任务 |
| 11 | 帧广播器 | 初始化帧发布/订阅中心,供 MJPEG 订阅者使用 |
| 12 | 视频录像器 | 初始化 AVI 录像引擎,注册分段回调 |
| 13 | MJPEG 流服务 | 初始化独立 TCP 服务器(端口 81),最大 2 个并发客户端 |
| 14 | Web 服务器 | 端口 80,注册 URI 处理程序 + ONVIF 端点 |
| 14a | OTA 更新器 | 初始化固件 OTA 更新能力 |
| 15 | LED 状态更新 | 根据当前 WiFi 状态更新 LED 模式 |
| 16 | ONVIF 发现 | 启动 WS-Discovery UDP 组播监听(WiFi 连接后) |
| 17 | 等待 STA + 开始录像 | 最多等待 30 秒 WiFi 连接,连接后开始录像 |
| 18 | BOOT 按钮监控 | GPIO0,长按 5 秒触发恢复出厂设置 |
| 19 | 看门狗 | 30s TWDT,双核 idle 任务均纳入监控 |
| 20 | SD 监控任务 | Core 1,每 10 秒轮询 SD 卡状态,处理热插拔 |
| 21 | 健康监控任务 | Core 1,每 60 秒输出堆/栈水位信息 |
RTOS 任务优先级布局
| 任务 | 核心 | 优先级 | 职责 |
|---|---|---|---|
audio_capture |
0 | 5 | I2S PDM 麦克风采集,G.711 编码,音频广播(Core 0 上最高优先级用户任务) |
recorder |
0 | 3 | 摄像头采集,运动检测,帧缓冲分配/发布,入队到 sd_writer |
mjpeg_cli |
1 | 2 | MJPEG HTTP 流客户端任务(端口 81) |
rtsp_video |
1 | 2 | RTSP 视频流(espp) |
rtsp_audio |
0 | 2 | RTSP 音频流(G.711)— Core 0 以降低 I2S 采集的延迟 |
sd_writer |
1 | 2 | 专用 SD 写入任务。从录像器消耗写队列,执行异步 write_avi_frame + 音频复用 |
| WebSocket/httpd | 0/1 | 1 | HTTP 服务器 + WebSocket 处理程序 |
设计原因:
- 音频采集优先级 5 是最高用户任务 — 不会被 SD 写入抢占。
- 录像器优先级 3(原为 5)— 保持在 Core 0,低于音频以避免抢占 Core 1 上的流媒体任务。
- sd_writer 在 Core 1 — I/O 密集型,无需与摄像头保持 CPU 邻近性。
异步 SD 写入架构 (v0.4.0)
录像器采集帧并将 frame_buf 引用入队到深度为 2 的队列。Core 1 上专用的 sd_writer_task 消耗队列并异步执行 write_avi_frame + 音频复用。
关键设计点:
- 队列深度 2:允许录像器在 SD 写入进行时提前采集 1-2 帧
- 哨兵屏障:NULL
frame_buf+xTaskNotifyGive确保分段旋转等待所有排队帧刷新后才执行close_segment - 保持帧同步回退:如果队列已满,录像器同步写入帧(背压)
- 音频复用:音频帧(160 字节 G.711)在 SD 写入期间交错到 AVI 容器中
这消除了 SD I/O 上的采集循环阻塞,以前这会在录制期间导致 MJPEG 流卡顿和音频丢帧。
3. 模块说明
系统共 27 个模块,全部位于 main/ 目录,每个模块一个 .c/.h 文件对。
| 模块 | 文件 | 职责 | 关键函数 |
|---|---|---|---|
| 主入口 | main.c |
启动流程,任务创建 | app_main(), on_segment_complete() |
| 摄像头驱动 | camera_driver.c |
OV2640/OV3660 自动检测,帧采集 | camera_init(), camera_capture() |
| 视频录像器 | video_recorder.c |
AVI MJPEG 分段录像,状态机,延时摄影模式 | recorder_start(), recorder_stop() |
| 帧广播器 | frame_broadcaster.c |
帧发布/订阅中心,向 MJPEG 订阅者分发 JPEG 帧 | fbroadcast_publish(), fbroadcast_subscribe() |
| MJPEG 流服务 | mjpeg_streamer.c |
MJPEG 实时流,独立 TCP 服务器(端口 81) | mjpeg_streamer_init() |
| Web 服务器 | web_server.c |
HTTP 服务器 + REST API + ONVIF 端点 | web_server_start(), web_server_get_handle() |
| WiFi 管理 | wifi_manager.c |
AP/STA 双模式,自动选择 | wifi_init(), wifi_scan() |
| 配置管理 | config_manager.c |
NVS 持久化,SD 卡覆盖 | config_init(), config_save(), config_reset() |
| 存储管理 | storage_manager.c |
SD 卡挂载/卸载,循环清理 | storage_init(), storage_cleanup() |
| NAS 上传 | nas_uploader.c |
互斥上传调度(WebDAV 或 HTTP/HTTPS) | nas_uploader_init(), nas_uploader_enqueue() |
| HTTP 上传客户端 | http_upload_client.c |
HTTP/HTTPS PUT 流式上传 | — |
| WebDAV 客户端 | webdav_client.c |
WebDAV 协议上传实现 | — |
| ONVIF 发现 | onvif_discovery.c |
WS-Discovery UDP 组播,NVR 自动发现 | onvif_discovery_init() |
| ONVIF 服务 | onvif_service.c |
SOAP 处理:GetDeviceInformation, GetCapabilities, GetProfiles, GetStreamUri | onvif_register_handlers() |
| 状态 LED | status_led.c |
5 种 LED 模式控制 | led_init(), led_set_status() |
| 时间同步 | time_sync.c |
SNTP 同步,手动设置时间 | time_sync_init(), time_is_synced() |
| 运动检测 | motion_detector.c |
帧差分析,运动评分,告警触发 | motion_detector_init(), motion_detector_process() |
| OTA 更新器 | ota_updater.c |
固件升级,版本管理 | ota_updater_init(), ota_update_start() |
| Webhook | webhook.c |
HTTP 事件通知 | webhook_send(), webhook_init() |
| WebSocket 服务器 | ws_server.c |
实时推送更新到 WebUI 客户端 | ws_server_init(), ws_broadcast() |
| SHA-256 哈希 | sha256.c |
文件完整性校验 | sha256_hash(), sha256_verify() |
| JSON 解析器 | cJSON.c |
第三方 JSON 库(IDF v6.0 内置) | — |
| 音频驱动 | audio_driver.c |
I2S PDM 麦克风采集,DSR_16S(1.024 MHz),8kHz + DC 去除 + 噪声门 | audio_driver_init(), audio_driver_start() |
| 频谱降噪器 | audio_ns.c |
256 点 FFT 频谱减法,Wiener 增益,最小统计量噪声估计 | audio_ns_init(), audio_ns_process() |
| 音频广播器 | audio_broadcaster.c |
音频帧发布/订阅中心(镜像帧广播器) | abroadcast_publish(), abroadcast_subscribe() |
| G.711 编解码 | g711_codec.c |
G.711 μ-law 编解码器(ITU-T 标准) | g711_encode(), g711_decode() |
| RTSP 服务器 | rtsp_server.cpp |
RTSP 服务器(MJPEG+G.711 双轨道,无认证,端口 554) | rtsp_server_init(), rtsp_server_start() |
| SD 日志 | sd_log.c |
SD 卡结构化事件日志,自动轮转 | sd_log_init(), sd_log_write() |
4. 数据流
录像数据流
摄像头采集的 JPEG 帧由录像器写入 AVI 文件,分段完成后触发回调链:
camera_capture()
│
▼
视频录像器 (recording_task, Core 0)
│ ← 帧数据写入 AVI 文件
│ ← 按 segment_sec 时长分段(连续 vs 延时模式)
│ ← 写入 AVI idx1 索引
│
▼ 分段完成
on_segment_complete(filepath, size)
│
├──→ nas_uploader_enqueue(filepath)
│ │
│ ▼
│ 上传任务 (Core 1)
│ ├── upload_method=1 → WebDAV 上传
│ ├── upload_method=2 → HTTP(S) 上传
│ └── upload_method=0 → 跳过上传
│ 失败重试 3 次,连续失败 10 次后暂停 5 分钟
│
└──→ storage_cleanup()
│
▼
检查剩余空间 < cleanup_low_pct?(默认 20%)
├── 是 → 删除最旧录像文件,直到 ≥ cleanup_high_pct(默认 30%)
│ 写入失败时也尝试清理+重试
└── 否 → 无操作
延时摄影模式:当 timelapse_interval_sec > 0 时,按指定间隔(1-300秒)采集帧而非连续采集。文件使用 TLM_ 前缀,以 15fps 播放。
MJPEG 实时流数据流
MJPEG 流服务运行在端口 81 作为独立 TCP 服务器,与端口 80 的主 httpd 完全分离。详见端口分离设计。
浏览器 → GET /stream (端口 81)
│
▼
mjpeg_listen_task (Core 1, 优先级 3)
└── accept() → 为每个客户端生成 mjpeg_client_task
│
▼
frame_broadcaster → fbroadcast_receive()
│
▼
video_recorder(帧生产者)→ fbroadcast_publish()
│ ← recording_task 中的 camera_capture()
│ ← PSRAM 副本 → 与摄像头缓冲区解耦
│
▼
HTTP 分块传输 (multipart/x-mixed-replace)
│ Content-Type: image/jpeg
│ Boundary: --frame
│
▼
浏览器实时渲染
RTSP 服务器数据流
RTSP Server (端口 554) ← frame_broadcaster + audio_broadcaster → MJPEG 视频轨道(RTP payload type 26) → G.711 μ-law 音频轨道(RTP payload type 0, PCMU)
5. FreeRTOS 任务表
| 任务名 | 函数 | 优先级 | 核心 | 栈大小 | 周期/触发 | 文件 |
|---|---|---|---|---|---|---|
recorder |
recording_task |
5 (configMAX-2) | Core 0 | 4096 B | 连续循环(帧采集) | video_recorder.c |
upload |
upload_task |
3 | Core 1 | 6144 B | 队列阻塞等待 | nas_uploader.c |
mjpeg_listen |
mjpeg_listen_task |
3 | Core 1 | 4096 B | 连接接受循环 | mjpeg_streamer.c |
mjpeg_cli_N |
mjpeg_client_task |
2 | Core 1 | 4096 B | 帧接收+发送循环 | mjpeg_streamer.c |
onvif_disc |
onvif_discovery_task |
2 | Core 1 | 6144 B | UDP 组播监听 | onvif_discovery.c |
sd_monitor |
sd_monitor_task |
2 | Core 1 | 3072 B | 10 秒轮询 | main.c |
boot_btn |
boot_button_monitor |
1 | 任意 | 2048 B | 200ms 轮询 | main.c |
health_mon |
health_monitor_task |
1 | Core 1 | 3072 B | 60 秒轮询 | main.c |
ws_server |
websocket_server_task |
3 | Core 1 | 2048 B | 客户端事件驱动 | ws_server.c |
httpd |
ESP-IDF 内置 | 默认 | — | 16384 B | 事件驱动 | web_server.c |
main |
app_main |
1 | Core 0 | 8192 B | 初始化后返回 | main.c |
任务调度策略
- Core 0:运行录像任务(高优先级),确保帧采集不被抢占导致丢帧
- Core 1:运行上传、MJPEG 流、ONVIF 发现、SD 监控、健康监控、WebSocket 服务器等非实时任务
- BOOT 按钮监控:不绑定核心,低优先级轮询
- Web 服务器:由 ESP-IDF httpd 库管理,16384 字节栈
- WebSocket 服务器:运行在 Core 1,向连接的客户端广播状态更新
6. 分区表
来自 partitions.csv,使用自定义双 OTA 分区表:
| 名称 | 类型 | 子类型 | 偏移 | 大小 | 说明 |
|---|---|---|---|---|---|
nvs |
data | nvs | 0x9000 | 24 KB (0x6000) | 非易失性存储(配置持久化) |
phy_init |
data | phy | 0xF000 | 4 KB (0x1000) | PHY 校准数据 |
ota_0 |
app | ota_0 | 0x10000 | 1792 KB (0x1D0000) | OTA 插槽 A |
ota_1 |
app | ota_1 | 0x1E0000 | 1792 KB (0x1D0000) | OTA 插槽 B |
otadata |
data | ota | 0x3B0000 | 8 KB (0x2000) | OTA 选择数据 |
spiffs |
data | spiffs | 0x3B2000 | 256 KB (0x40000) | Web UI 静态资源 |
Flash 总大小:8 MB
注意:双 OTA 插槽支持无缝固件更新。
esp_https_ota自动处理插槽切换。
7. Web API 端点
服务器运行在端口 80,另含 ONVIF SOAP 端点:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/status |
否 |
| GET | /api/config |
否 |
| POST | /api/config |
|
| GET | /api/files |
否 |
| POST | /api/files/batch |
|
| DELETE | /api/files |
|
| GET | /api/download?name=xxx |
否 |
| GET | /api/scan |
否 |
| POST | /api/time |
|
| POST | /api/record?action=start|stop |
|
| POST | /api/ota |
|
| POST | /api/format |
|
| POST | /api/reset |
|
| GET | /metrics |
否 |
| POST | /onvif/device_service |
否 |
| POST | /onvif/media_service |
否 |
| OPTIONS | /* |
否 |
| GET | /* |
否 |
认证:无——契约 v1.9(2026-09-18)已移除设备级 Web 密码,端点在可信局域网内开放(边界为路由器的 WPA2)。
8. Web UI
6 个 HTML 页面,烧录到 SPIFFS 分区,由通配符 GET 处理程序服务:
| 页面 | 文件 | 功能 |
|---|---|---|
| 首页 | index.html |
状态概览 |
| 配置 | config.html |
WiFi / NAS / 摄像头参数配置(手风琴布局) |
| 文件 | files.html |
录像文件浏览、下载、批量操作,可折叠日期分组 |
| 预览 | preview.html |
MJPEG 实时流预览,支持全屏和截图 |
| OTA | ota.html |
固件更新界面,URL 输入和进度显示 |
| 设置 | setup.html |
首次 WiFi 配置向导 |
Prometheus 集成:/metrics 端点以文本格式提供 15+ 项系统指标。
温度监控:ESP32-S3 芯片温度通过 /api/status 获取,仪表盘颜色编码指示。
性能优化 (v0.4.0)
| 优化 | 影响 |
|---|---|
| 异步 SD 写入(sd_writer_task) | 消除 SD I/O 上的采集循环阻塞 |
| 音频帧使用内部 RAM | 160 字节 G.711 帧使用 MALLOC_CAP_DMA,避免 PSRAM 缓存与 JPEG DMA 的争用 |
| MJPEG TCP_NODELAY + 8KB 分块 | 降低小块 part-headers 的延迟,提高吞吐量 |
| 循环相对帧延时 | 实际交付的 FPS 与配置匹配(以前因发送时间加到延迟上而较低) |
| Gzip 压缩的 HTML | 5 个网页作为 .html.gz 提供,负载减少 60% |
| WebSocket 互斥锁快照模式 | 防止慢客户端发送期间的连接/断开饥饿 |
| 运动检测预分配池缓冲区 | 预分配 388800 字节解码缓冲区,避免每帧分配 |
| TCP 缓冲区 16KB + 接收邮箱 24 | 更大的 TCP 窗口以提高流媒体吞吐量 |