先得到一张可以真正打开的图
拍照的完成物是一份保留原始字节的JPEG文件,以及能说明它来自哪次运行的记录。串口打印“camera ready”、缓冲开头出现FFD8,分别只能说明初始化或标记;既不能证明文件完整,也不能说明镜头拍到了正确内容。本文把板级成像、USB传输、主机解码和人工看图依次验收。
示例固定官方ESP32-S3-EYE-MB_V2.2、板载OV2640和8MiB OPI PSRAM,只用原生Micro-USB。没有这块板也可复现完整编译和数学合成JPEG软件检查;本站没有实际拍照结果,空观察表不填示例速度或画质数。此页是完整执行配方,不是亲测成像报告。
参考:[1] S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复[2] MB_V2.2原理图:p2电源/USB、p3模组、p4摄像头[4] esp32-camera2.1.8公开结构与帧所有权API
核对主板,再理解已经连接好的针脚
先断开USB,读主板背面的MB_V2.2丝印、模组与排线端点。本文使用板载摄像头,不要求给通用开发板飞线。相机、SD、LCD、麦克风和按钮已占用引脚;指南说只有GPIO3可用于指示灯,本例连GPIO3也不修改。不能把上一篇的外接按钮或LED接到这里。
下表是板内端点映射,不是新增接线清单。PWDN/RESET的-1表示不由额外GPIO控制。旧ESP-EYE连接表和camera仓库通用S3-WROOM示例属于另一组合;固定BSP虽然已改用esp_video,端点仍可对照,不能因此把它的初始化函数、16MHz参数混入camera2.1.8。
| 板载端点 | 本例GPIO | 用途/边界 |
|---|---|---|
| D0/D1/D2/D3 | 11/9/8/10 | 低四位并行像素数据 |
| D4/D5/D6/D7 | 12/18/17/16 | 高四位并行像素数据 |
| XCLK/PCLK | 15/13 | 主控提供时钟/像素同步 |
| VSYNC/HREF | 6/7 | 帧/有效行同步 |
| SCCB SDA/SCL | 4/5 | 传感器配置总线 |
| PWDN/RESET | -1/-1 | 没有外接控制脚 |
| 原生USB | 19/20 | Serial/JTAG,无UART桥 |
参考:[1] S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复[2] MB_V2.2原理图:p2电源/USB、p3模组、p4摄像头[3] 固定BSP板级端点定义;不能混用其esp_video实现
PSRAM、JPEG接收槽与分辨率各负责什么
QVGA是320×240像素;若采用RGB565,每帧未压缩数据就有153600B。这里让OV2640输出JPEG,文件大小随画面细节和压缩质量变化,不能从像素数直接算出JPEG字节数。quality16是驱动的压缩质量参数,0..63中数值越低质量越高,不是16位像素。
固定配置使用8MiB OPI80 PSRAM、一个PSRAM帧缓冲、WHEN_EMPTY和20MHz XCLK,关闭PSRAM DMA。V01的jpeg_buffer_size=0,实际构建AUTO模式按宽×高/5分配15360B接收槽。frame_policy的262144B仅是上层拒绝上限,不能把驱动槽扩为262144B;复杂画面溢出时要保留FB-OVF/空帧和画面条件,再针对新参数另建有SHA的变体。
只读esp_psram_get_size并严格要求8388608,再核OV2640 PID,是避免错误组合继续跑的门槛。通过编译没有执行这些硬件检查。单fb明确规定所有权:取得后先处理,再归还;归还后不能继续访问其中JPEG字节。
参考:[4] esp32-camera2.1.8公开结构与帧所有权API[5] 固定取帧实现:4s SDK等待和帧元数据[6] cam_hal缓冲配置:显式jpeg_buffer_size优先于AUTO
固定组件、配置和原生USB下载
先激活官方IDF5.5.5环境,使用下载包里的完整CMake项目、main/idf_component.yml、sdkconfig.defaults和partitions.csv。锁将camera固定为2.1.8、esp_jpeg固定为1.3.1;首次实际构建生成的lock如实记为构建后输出,没有倒写为编译前输入。
执行idf.py -D IDF_TARGET=esp32s3 -B build -D SDKCONFIG=build/sdkconfig build,再用同一build与配置执行-p PORT flash monitor。PORT应来自本机真实USB枚举,macOS通常cu.usbmodem、Linux ttyACM、Windows COM。BOOT按住,按放RST,再放BOOT可进入下载模式;完成按RST启动。
这块板没有USB转UART桥。程序反复复位导致端口变化时先恢复下载模式、换数据线并关闭占用端口的进程,不给摄像头针脚接一个临时UART。factory4MiB位于0x10000,本文没有OTA或外部供电改造。
# ---- main/idf_component.yml ----
dependencies:
idf: "==5.5.5"
espressif/esp32-camera: "==2.1.8"
# ---- sdkconfig.defaults ----
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_USE_MALLOC=y
# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set
# CONFIG_CAMERA_PSRAM_DMA is not set
CONFIG_OV2640_SUPPORT=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# ---- partitions.csv ----
# Name,Type,SubType,Offset,Size,Flags
nvs,data,nvs,0x9000,0x6000,
phy_init,data,phy,0xf000,0x1000,
factory,app,factory,0x10000,0x400000,
参考:[1] S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复[4] esp32-camera2.1.8公开结构与帧所有权API
完整板级初始化:把范围检查写进程序
下面是下载工程的完整camera_board.h。除了固定端点,它还要求实际8MiB PSRAM与OV2640;初始化失败返回错误,传感器不匹配会执行deinit。固件不初始化LCD、SD和麦克风,USB单独供电,不接电池。所有排线检查先断电。
20MHz来自本固定驱动示例的方案,不代表新BSP的16MHz已经在本程序测过。JPEG尺寸和画质仍需真实取帧与人工看图验收;不能靠修改scope文字宣称其他相机可用。
// ---- main/camera_board.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdio.h>
#include "esp_camera.h"
#include "esp_psram.h"
/* Official ESP32-S3-EYE-MB_V2.2, onboard OV2640. Not generic S3 WROOM. */
static inline esp_err_t eye_camera_init(void){
size_t psram=esp_psram_get_size();printf("PSRAM reported_bytes=%zu required=8388608; physical boot status only when actually run\n",psram);
if(psram!=8388608)return ESP_ERR_INVALID_SIZE;
camera_config_t c={.pin_pwdn=-1,.pin_reset=-1,.pin_xclk=15,.pin_sccb_sda=4,.pin_sccb_scl=5,
.pin_d0=11,.pin_d1=9,.pin_d2=8,.pin_d3=10,.pin_d4=12,.pin_d5=18,.pin_d6=17,.pin_d7=16,
.pin_vsync=6,.pin_href=7,.pin_pclk=13,.xclk_freq_hz=20000000,.ledc_timer=LEDC_TIMER_0,.ledc_channel=LEDC_CHANNEL_0,
.pixel_format=PIXFORMAT_JPEG,.frame_size=FRAMESIZE_QVGA,.jpeg_quality=16,.fb_count=1,.fb_location=CAMERA_FB_IN_PSRAM,.grab_mode=CAMERA_GRAB_WHEN_EMPTY,.sccb_i2c_port=0,.jpeg_buffer_size=0};
esp_err_t e=esp_camera_init(&c);if(e!=ESP_OK)return e;
sensor_t *s=esp_camera_sensor_get();if(!s||s->id.PID!=OV2640_PID){printf("SENSOR_SCOPE_MISMATCH expectedOV2640; not a compatible sensor claim\n");(void)esp_camera_deinit();return ESP_ERR_NOT_SUPPORTED;}
printf("SENSOR PID=0x%04x; no LCD/SD/microphone init; GPIO3 untouched\n",s->id.PID);return ESP_OK;
}
参考:[1] S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复[3] 固定BSP板级端点定义;不能混用其esp_video实现[4] esp32-camera2.1.8公开结构与帧所有权API
完整协议策略:一行命令,一次明确的帧身份
生产frame_policy.h同时用于板端和主机C检查。CAPTURE必须独占一行,CR或LF都作为分隔,CRLF不会重复执行;小写、附带参数、控制字节或超过31个可打印字符的行拒绝,溢出持续丢到分隔符。没有把串口read的一小段当成完整命令。
帧的SOI/EOI和长度检查只验证标记与软件边界,不是JPEG解码器。CRC32-IEEE覆盖原始JPEG全部字节;id和递增seq能发现漏块、错序或不同帧混合。CRC不是消息认证,不能证明拍摄来源或防篡改。
// ---- main/frame_policy.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#define FRAME_CAP 262144U
/* Marker/cap check only; this is NOT a JPEG decoder or image quality test. */
static inline bool frame_markers(const uint8_t *p,size_t n){return p&&n>=4&&n<=FRAME_CAP&&p[0]==0xff&&p[1]==0xd8&&p[n-2]==0xff&&p[n-1]==0xd9;}
static inline uint32_t frame_crc32(const uint8_t *p,size_t n){uint32_t c=0xffffffffU;for(size_t i=0;i<n;i++){c^=p[i];for(unsigned j=0;j<8;j++)c=(c>>1)^(0xedb88320U&(0U-(c&1U)));}return c^0xffffffffU;}
typedef struct {char text[32];size_t used;bool rejected;} CaptureLine;
static inline bool capture_feed(CaptureLine *s,unsigned char c){
if(c=='\r'||c=='\n'){s->text[s->used]=0;bool yes=!s->rejected&&!strcmp(s->text,"CAPTURE");s->used=0;s->rejected=false;return yes;}
if(c<32||c>126)s->rejected=true;
if(!s->rejected){if(s->used<sizeof(s->text)-1)s->text[s->used++]=(char)c;else s->rejected=true;}
return false;
}
参考:[4] esp32-camera2.1.8公开结构与帧所有权API[11] pyserial3.5:端口、timeout与逐行读取接口
完整主程序:取帧、分块传输、统一归还
进入READY后才从原生USB控制台逐字符接命令。capture先取并归还一个待取帧,再计时获取下一帧;持续采集模式中这不等于精确触发曝光,也不建立“按下时刻”的图像新鲜度保证。capture_us只量第二次fb_get调用,固定驱动内部4s等待不构成整条命令的硬截止。
成功帧通过BEGIN声明id/长度/尺寸/CRC,每个DATA对最多180B做Base64,最后END闭合。格式不符、编码或输出失败都归还fb。printf/fflush成功不等于主机收到,所以主机还要独立检查。底部internal/largest用于诊断,不是完整内存健康或无泄漏证明。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "mbedtls/base64.h"
#include "camera_board.h"
#include "frame_policy.h"
static uint32_t capture_id;
static void capture(void){
/* Discard one pending frame, then request another. Not an exposure timestamp guarantee. */
camera_fb_t *old=esp_camera_fb_get();if(!old){puts("V01_ERROR discard_get_null");return;}esp_camera_fb_return(old);
int64_t started=esp_timer_get_time();camera_fb_t *fb=esp_camera_fb_get();int64_t elapsed=esp_timer_get_time()-started;
if(!fb){puts("V01_ERROR frame_get_null");return;}
if(fb->format!=PIXFORMAT_JPEG||fb->width!=320||fb->height!=240||!frame_markers(fb->buf,fb->len)){printf("V01_ERROR format_size_or_markers len=%zu width=%zu height=%zu\n",fb->len,fb->width,fb->height);esp_camera_fb_return(fb);return;}
uint32_t id=++capture_id,crc=frame_crc32(fb->buf,fb->len);
printf("V01_BEGIN id=%"PRIu32" len=%zu width=%zu height=%zu crc=%08"PRIx32" capture_us=%"PRId64"\n",id,fb->len,fb->width,fb->height,crc,elapsed);
bool failed=false;unsigned sequence=0;
for(size_t offset=0;offset<fb->len;offset+=180){size_t n=fb->len-offset;if(n>180)n=180;unsigned char text[244];size_t written=0;
if(mbedtls_base64_encode(text,sizeof text,&written,fb->buf+offset,n)!=0){failed=true;break;}text[written]=0;
if(printf("V01_DATA id=%"PRIu32" seq=%u %s\n",id,sequence++,(char*)text)<0){failed=true;break;}vTaskDelay(1);
}
if(!failed)printf("V01_END id=%"PRIu32"\n",id);else puts("V01_ERROR console_encode_or_write");
esp_camera_fb_return(fb);fflush(stdout);
printf("V01_STATUS free_internal=%zu largest_internal=%zu; USB console completion is not host receipt\n",heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
}
void app_main(void){
esp_err_t e=eye_camera_init();if(e!=ESP_OK){printf("V01_INIT_FAILED %s; retain log, fix board/config/power then RESET\n",esp_err_to_name(e));return;}
puts("V01_READY officialS3EYEMBv2.2 OV2640 JPEG320x240 quality16 onePSRAMfb; exact CAPTURE line. NativeUSBSerialJTAG19/20, no UART bridge.");
CaptureLine line={0};for(;;){int c=getchar();if(c==EOF){clearerr(stdin);vTaskDelay(pdMS_TO_TICKS(20));continue;}if(capture_feed(&line,(unsigned char)c))capture();}
}
参考:[4] esp32-camera2.1.8公开结构与帧所有权API[5] 固定取帧实现:4s SDK等待和帧元数据
完整主机接收器:解码后才创建新文件
退出monitor释放串口,建立桌面Python3.12 venv并安装tools/requirements.txt(Pillow12.3.0、pyserial3.5)。运行python tools/capture.py --port PORT --out first-photo.jpg --timeout 30 --provenance hardware。已有输出拒绝覆盖;Windows用相应COM和venv激活脚本,不能把另一个系统的端口照抄。
工具保留跨250ms串口read timeout的半行,不因一次read返回就截断BEGIN;最长行512B、trace≤2MiB、JPEG≤262144B。收到END后核长度/CRC/标记,再用Pillow真正load为JPEG320×240,以xb创建文件。来源标签由操作人提供,不是相机认证。
工具可读取--trace用于故障重放;包含SYNTHETIC标记的输入不能标hardware。本站用数学网格JPEG验证过工具与真实OS伪终端跨400ms分片接收,这证明主机软件边界,没有测过原生USB电气。串口打开若导致系统复位,保留日志、待READY后用新文件名重试,不许同时运行monitor和接收器。
# ---- tools/capture.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Receive exact V01 framing, decode JPEG on host, never overwrite an output.
A valid payload is not authenticated camera origin or proof of exposure time.
"""
import argparse,base64,binascii,hashlib,io,json,pathlib,re,time,zlib
from PIL import Image
CAP=262144
class Capture:
def __init__(self):self.meta=None;self.parts=bytearray();self.sequence=0;self.complete=False
def feed(self,line):
if len(line)>512:raise ValueError('line exceeds512 bytes')
if b'SYNTHETIC' in line:return 'synthetic-marker'
text=line.decode('ascii').rstrip('\r\n')
if text.startswith('V01_ERROR') or text.startswith('V01_INIT_FAILED'):raise ValueError(text)
if text.startswith('V01_BEGIN'):
if self.meta is not None:raise ValueError('second BEGIN before END')
m=re.fullmatch(r'V01_BEGIN id=(\d+) len=(\d+) width=(\d+) height=(\d+) crc=([0-9a-f]{8}) capture_us=(\d+)',text)
if not m:raise ValueError('malformed BEGIN')
ident,n,width,height,crc,elapsed=m.groups();ident,n,width,height,elapsed=map(int,[ident,n,width,height,elapsed]);
if not 0<=ident<=0xffffffff or not 4<=n<=CAP or (width,height)!=(320,240) or elapsed>0x7fffffffffffffff:raise ValueError('BEGIN outside fixed V01 bounds')
self.meta={'id':ident,'bytes':n,'width':width,'height':height,'crc32':crc,'captureReportedUs':elapsed};return None
if text.startswith('V01_DATA'):
if not self.meta or self.complete:raise ValueError('DATA without active BEGIN')
m=re.fullmatch(r'V01_DATA id=(\d+) seq=(\d+) ([A-Za-z0-9+/]+={0,2})',text)
if not m or int(m[1])!=self.meta['id'] or int(m[2])!=self.sequence:raise ValueError('DATA identity/sequence mismatch')
block=base64.b64decode(m[3],validate=True)
if not 1<=len(block)<=180 or len(self.parts)+len(block)>self.meta['bytes']:raise ValueError('payload exceeds declared limit')
self.parts.extend(block);self.sequence+=1;return None
if text.startswith('V01_END'):
if not self.meta or self.complete or text!='V01_END id='+str(self.meta['id']):raise ValueError('END identity/state mismatch')
data=bytes(self.parts)
if len(data)!=self.meta['bytes'] or f'{zlib.crc32(data):08x}'!=self.meta['crc32']:raise ValueError('payload length/CRC mismatch')
if data[:2]!=b'\xff\xd8' or data[-2:]!=b'\xff\xd9':raise ValueError('JPEG SOI/EOI missing')
with Image.open(io.BytesIO(data)) as image:
if image.format!='JPEG' or image.size!=(320,240):raise ValueError('decoded JPEG outside fixed scope')
image.load() # Actual host decoder, not marker-only validation.
self.complete=True;return data
return None
def main():
p=argparse.ArgumentParser();source=p.add_mutually_exclusive_group(required=True);source.add_argument('--port');source.add_argument('--trace',type=pathlib.Path);p.add_argument('--out',required=True,type=pathlib.Path);p.add_argument('--timeout',type=float,default=30);p.add_argument('--provenance',choices=['hardware','synthetic'],required=True);a=p.parse_args()
if not 0<a.timeout<=300:p.error('timeout must be in(0,300] seconds')
if a.out.exists():p.error('output exists; refusing overwrite')
capture=Capture();deadline=time.monotonic()+a.timeout;serial=None;stream=None;payload=None;synthetic=False
try:
if a.trace:
if a.trace.stat().st_size>2097152:raise ValueError('trace exceeds2MiB')
if b'SYNTHETIC' in a.trace.read_bytes():
synthetic=True
if a.provenance!='synthetic':raise ValueError('SYNTHETIC trace cannot be labeled hardware')
stream=a.trace.open('rb')
else:
import serial as serial_module
serial=serial_module.Serial(a.port,115200,timeout=.25,write_timeout=2);serial.write(b'CAPTURE\n');serial.flush();stream=serial
pending=b''
while time.monotonic()<deadline:
piece=stream.readline(513)
line=pending+piece
if not piece:
if a.trace:break
continue
if len(line)>512:raise ValueError('line exceeds512 bytes')
if not line.endswith(b'\n'):
pending=line
continue
pending=b''
result=capture.feed(line)
if result=='synthetic-marker':
synthetic=True
if a.provenance!='synthetic':raise ValueError('SYNTHETIC trace cannot be labeled hardware')
elif isinstance(result,bytes):payload=result;break
if payload is None:raise ValueError('incomplete frame or deadline; no output written')
if synthetic and a.provenance!='synthetic':raise ValueError('synthetic provenance mismatch')
with a.out.open('xb') as f:f.write(payload)
print(json.dumps({'provenanceProvidedByOperator':a.provenance,'containsSyntheticTraceMarker':synthetic,'scope':'Host JPEG decode and framing/CRC only; camera origin, USB electrical, image quality and exposure time are not authenticated.','reported':capture.meta,'sha256':hashlib.sha256(payload).hexdigest(),'output':str(a.out)},indent=2));return 0
except (ValueError,OSError,UnicodeError,binascii.Error,Image.DecompressionBombError) as error:print(str(error));return 2
finally:
if stream:stream.close()
if __name__=='__main__':raise SystemExit(main())
参考:[10] Pillow12.3.0:Image.open/load及解压尺寸异常[11] pyserial3.5:端口、timeout与逐行读取接口
先跑软件检查,再执行实际拍摄
下载包README给出同源C检查和python host/check_capture.py --out 新目录。C检查直接包含生产frame_policy,ASan/UBSan检查标记/CRC/命令行;Python实际运行16个文件CLI预期结果,包含错误CRC/声明长度/尺寸/序号/Base64、假JPEG、无END、覆盖与错误来源拒绝。合成图片明确带SYNTHETIC,不是手持镜头拍的样张。
本站真实IDF完整构建通过,app306352B;固定前输入、实际编译main、13个bin/ELF/map/配置/日志SHA保留。主机解码通过也不能让硬件维度变成passed。实际板运行必须先看PSRAM和PID、拿到完整文件、最后人工打开检查方向/焦距/内容。
clang -std=c11 -Wall -Wextra -Werror -g -fsanitize=address,undefined -I main host/policy_test.c -o /tmp/v01-policy
/tmp/v01-policy
python host/check_capture.py --out /tmp/v01-SYNTHETIC-check参考:[4] esp32-camera2.1.8公开结构与帧所有权API[10] Pillow12.3.0:Image.open/load及解压尺寸异常[11] pyserial3.5:端口、timeout与逐行读取接口
按失败层级排错,不把坏JPEG修成“成功”
USB枚举失败先恢复BOOT/RST并确认数据线与端口独占;PSRAM或PID不符先停机核板和配置。帧空/FB-OVF先保存相机日志,检查供电、排线、板级脚与实际JPEG槽,再研究压缩参数。不能在错误端点上盲目提高时钟。
CRC或序号失败属于传输/日志完整性,换新文件名重试并保留原始trace,不能删去失败块后继续称通过。解码过但画面方向、焦距或色彩不对属于真实成像链,需要保留原图与光照/物体条件;不是靠“CRC正确”可以解释的事情。
| 现象 | 先保存 | 下一步 |
|---|---|---|
| 无USB/反复复位 | 完整启动/烧录日志 | BOOT/RST、数据线、实际端口 |
| PSRAM/PID不符 | 板背面与模组标记、启动日志 | 停机核型号、OPI80与排线 |
| 空帧/溢出 | 驱动日志和画面条件 | 固定脚/供电/接收槽 |
| CRC/序号不符 | 原trace与接收报错 | 串口独占、完整重试 |
| 可解码但画面不对 | 原JPEG、光照/距离/方向 | 人工成像检查,保留新SHA变体 |
参考:[1] S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复[5] 固定取帧实现:4s SDK等待和帧元数据[6] cam_hal缓冲配置:显式jpeg_buffer_size优先于AUTO
把这张图变成下一步的可信起点
observations.csv只提供列头:板标记、模组、PID、appSHA、PSRAM报告、JPEG长度/尺寸、取帧调用耗时、CRC、人眼看图和原日志。实际运行时保存原JPEG和原串口日志;不能把合成123µs字段填成拍照速度。
第一张真实图的文件链路清楚后,再进入V02局域网预览。加WiFi会增加发送、缓冲与并发问题,不会自动修好错的相机引脚。缺少实板时,完成软件步骤并保持观察表空白即可准确保留尚未完成的成像验收。
资料来源与版本说明
实际读固定S3-EYE指南、原理图p2/p3/p4与BSP端点、camera/HTTP完整所有权和缓冲实现;鲜取与实际编译SDK/组件字节比较,Registry manifest重排并增加发布元数据的差异另记,未假称全部字节相同。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- S3-EYE v2.2官方指南:§3引脚/供电,§4修订与USB恢复
- MB_V2.2原理图:p2电源/USB、p3模组、p4摄像头
- 固定BSP板级端点定义;不能混用其esp_video实现
- esp32-camera2.1.8公开结构与帧所有权API
- 固定取帧实现:4s SDK等待和帧元数据
- cam_hal缓冲配置:显式jpeg_buffer_size优先于AUTO
- IDF5.5.5 HTTP server API与异步请求清理要求
- IDF5.5.5完整异步请求示例
- 固定HTTP异步复制与complete真实实现
- Pillow12.3.0:Image.open/load及解压尺寸异常
- pyserial3.5:端口、timeout与逐行读取接口