一台 4G AI 相机的完整工程账:576KB 分区、三朵云与一条语音链路

做语音控制 AI 相机(EC600M-CNLE + GC032A + ST7789 + 小智语音)断断续续几个月,专题的坑在之前的帖子里拆过几篇(稳定性防线、MCP 工具集),这篇把它们串起来,站在“整个项目”的高度把账算全:硬件怎么选、576KB 怎么花、三朵云怎么分工、一句话怎么变成一张照片、OTA 怎么在 64KB 的夹缝里做。数据都是实测,欢迎对账。

一、先看全景:五层各司其职

系统全景

分层的目的只有一个:让“换任何一层”都不牵动其余。OTA 只更新固件层+能力层;把小智换成别的语音平台只动通道层;摄像头换型号只动能力层里十几个函数。设备端 14 个 .py 平铺在 /usr,全部代码 ≈256KB——这不是炫技,是 576KB 分区逼出来的纪律。

二、硬件账:两个“文档没写”的坑

GC032A 走 SPI,且 camCapture 隐式依赖 LCD。 相机驱动按官方 camera 库走,但 AI 固件上 open() 即开启预览流,预览依赖 LCD——LCD 不初始化,拍照回调永远不来,现象是“命令发了、文件没有”。ST7789 的 init 序列每段延时都要按屏规格书来,我按官方 drivers 版抄却漏了尾巴段(0x36/2A/2B/2C),整屏发白——这两个坑都只在真机上现形。

三、资源账:一张图决定敢做什么

资源账本

Flash 侧:customer_fs(/usr)576KB,代码 256KB,剩 ~290KB 给照片——所以照片“循环保留最近 N 张 + 上传即删”。fota_pkg 只有 64KB,FOTA 整包想都别想,只能走 SOTA(脚本包),且全量包 ≤255KB 还必须含 main.py,日常只发 50~80KB 增量。

RAM 侧:两条水位线写死在主循环:

free = gc.mem_free()
if free < 120 * 1024:      # 熔断线
    end_session()          # 结束视觉/语音会话,回待机
elif free < 200 * 1024:    # 警戒线
    skip_frame()          # 本帧不拍不传,只保流程

另一条容易忽视的纪律:GC 和 DMA 会打架——摄像头帧缓冲正被 DMA 读的时候 gc.collect() 可能直接 dump。收集只放在“拍照后、上传后”这些硬件不持锁的间隙。

四、三朵云:按“报文大小和时效”分工

通道 承载 为什么是它
EMQX 照片/视频流 公网 broker 自由定 topic,负载不受物模型限制
阿里云 IoT 属性/指令/RRPC 平台规约下行主题+物模型,控制台可视
Gitee 照片归档 raw 外链给网页直接 <img>,302→200 image/jpeg
小智 WS 语音流+MCP call 实时性最好的通道,专供语音交互

照片不走阿里云:物模型报文有负载限制,塞图必然撞墙——这是踩过墙之后的分工。topic 设计也极简:

emqx:  cam/photo/upload    (二进制帧)
       cam/photo/result    (gitee raw url 回传)
ali:   property/set        (cmd + cmd_args,平台规约)
       /rrpc/request/+     (同步指令)

五、语音链路:每一段的毫秒都有出处

语音时序

环节 耗时 备注
PTT → 首帧 Opus 上行 ≈100~150ms 板端录音启动+上行
语音流速率 Opus 60kbps ≈7.5KB/s 4G 无压力
STT + LLM 决策 1~3s 不可控,靠 TTS 兜体验
MCP call 下发→解析 <500ms JSON-RPC 经 WS
拍照执行(含回调) ≈2s camCapture 回调+落盘
上传 + raw 回传 2~5s 看网络
端到端 ≈4~9s 网络决定

两个关键取舍:设备端只做执行器(JSON-RPC 复杂度全在云端桥);先应答后交付(回执先行,照片异步补传,上传失败不影响“已为你拍好”)。LLM 的 tools 有个文档不写的坑:

"inputSchema": {"type": "object", "properties": {}}

写成空 dict {} 会被 schema 校验器推断成 null,报 schema must be 'type: object', got 'type: null'——必须显式声明。

六、稳定性:三条防线一句话版

超时熔断(≥5s/帧,丢帧保流程)、RAM 水位(见第三节)、回调禁令——最后这条是用一次“静默掉线”换的:MQTT 回调里直接调视觉 API,与心跳线程重入同一 socket,底层抛 KeyboardInterrupt(继承 BaseException,except Exception 抓不住),接收线程无声死亡、设备“假在线”。铁律:

def _on_mqtt_cmd(topic, msg):     # 回调线程只做三件事
    with _cmd_lock:
        _cmd_queue.append(msg)    # ① 入队
    _cmd_flag = True              # ② 置标志
# ③ 重活回主循环 process_cmd() 消费,绝不碰 socket/外设

七、OTA:在 64KB 夹缝里做增量

fota_pkg 分区只有 64KB,整包 FOTA 没戏。方案是 SOTA 脚本包:打包工具做白名单+py_compile+强制 main.py+版本递增,增量包 50~80KB。两个坑:progress.params.step 必须字符串(传 int 部分账号静默丢进度);升级目标路径在云端 data.extData._package_udi 的嵌套 JSON 里。最要命的是入口:

# main.py —— 全部两行
import example
example.exec('/usr/_main.py')

_main.py 下划线开头上电不自启,OTA 重启后板上没程序 = LCD 冻结。这两行垫片是“重启变砖”和“重启继续跑”的全部差别。

八、工程方法:让 AI 提效,让真机说话

315 项设备端测试全绿、71 项 TSL 一致性校验脚本、tools/ 下一套串口上传/日志监听/端到端拍照工具链。编码大量用了 AI 提效,但AI 生成不了真机踩坑:GC032A 的时序、4G 传图断流、回调里的静默死亡、PSM 不支持 M 系列——这些只有板子会教你。把每一条都记下来,比多写十个功能更值钱。

小结

  1. 资源账先行:Flash/RAM 两张表决定功能取舍,不先算账的功能都是空中楼阁;
  2. 分层是为了可替换:通道层换平台、能力层换摄像头、OTA 只碰两层;
  3. 先应答后交付:交互体验的关键是“闭环的回执”,不是“完整的交付”;
  4. 真机踩坑记录是最值钱的资产。

专题细节在我之前几篇(稳定性防线 / MCP 工具集 / 多线程笔记)里,欢迎交流。

感谢分享这段完整的项目工程实践记录,硬件选型、Flash/RAM 资源分配、云端分工与语音链路的实测数据都梳理得清晰完整,对做同类方案的伙伴很有参考价值。

本回答仅供参考。