Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
photo-demo-ios
iOS 原生 Demo:通过 BLE 连接相机硬件,接收其自动/手动拍摄的照片并显示。
上层数据帧遵循《相机传图协议 V1.02_20260707》——完整协议已沉淀在 PROTOCOL.md(含实测勘误),直接查询用。
BLE:广播 Service 0xFF10(当前固件实际广播为字节序反的 10FF,已兼容)、蓝牙名 CmLXXXXXX、传图通道 = 服务 FF10 / 特征 FFF2(WRITE+NOTIFY 双向)。
结构
photo-demo-ios/
├── PhotoProtocol/ # 纯 Foundation 协议层(可离线 swift test)
│ └── Sources/PhotoProtocol/
│ ├── CameraProtocol.swift # 常量、cmd 枚举、ack 码
│ ├── CameraFrame.swift # 帧结构 / 编码 / 累加和校验 / 常用帧构造
│ └── FrameParser.swift # 流式拆帧(magic + data_len,抗噪重同步)
├── Sources/
│ ├── BLE/BleTypes.swift # UUID(沿用旧固件)、状态、错误
│ ├── BLE/BleClient.swift # CoreBluetooth 封装(零依赖)
│ ├── Transfer/PhotoTransferManager.swift # 传图状态机 + ObservableObject
│ └── App/ # SwiftUI 入口与单页 UI
├── project.yml # XcodeGen 工程定义
└── Info.plist
构建
# 1. 协议层单测(无需设备/模拟器)
cd PhotoProtocol && swift test
# 2. 生成 Xcode 工程并编译
cd .. && xcodegen generate
open photo-demo.xcodeproj # 选真机运行(BLE 无法在模拟器测)
协议帧格式
magic(2)=0xABCD | cmd(1) | packet_idx(2) | total_packet(2) | data_len(2) | ack_code(1) | reserve(1) | check_sum(1) | DATA(0~200)
- 多字节字段 wire 上为大端;
check_sum= 前 11 头字节 + 全部 DATA 累加 & 0xFF。 - cmd:0x10/0x11/0x12 查数量;0x20
0x23 请求传图(ACK/连续);0x300x33 图片数据;0x40 完成;0x50 对时。
交互流程(ACK 模式)
查数量(0x10) → 请求(0x20) → 设备 ACK(有图) → 逐包 0x30 / 每包回 ACK → 0x40 完成 → 回 ACK(设备删图) → 自动请求下一张 → 直至设备返回「无图」。
⚠️ 卡点 / 待确认(Blockers)
状态标记:🔴 阻塞进行中的一步 · 🟡 影响运行正确性(不影响编译) · 🟢 已解决
| # | 卡点 | 状态 | 说明 / 当前处理 |
|---|---|---|---|
| B1 | 飞书协议文档需登录,WebFetch 抓不到 | 🟢 | 用户直接粘贴正文解决 |
| B2 | BLE 传输层用哪套 UUID | 🟢 | 已确认:沿用旧 duooomi 固件(service 000002c4 / write 000002c5 / read 000002c6) |
| B3 | 真机能否扫到目标设备 | 🟢 | 已扫到设备 |
| B3b | 设备真实的 Service/写/通知特征 UUID | 🟢 | 硬件日志确认:服务 FF10 → 特征 FFF2(WRITE+NOTIFY,一特征双向)。App 默认已选 FFF2,另可手动改选 |
| B4 | 图片编码格式(JPEG/PNG/裸流) | 🟡 | 现用 UIImage(data:) 自动识别;CLI 落盘后按文件头判格式。待实传一张确认 |
| B5 | 一次请求传一张还是全部 | 🟡 | 现按「每张 0x40 后自动再请求下一张」直到设备回非成功 ACK。若固件一次性发全部需调终止判定 |
| B6 | ack_code 错误码表 |
🟡 | 已知 0x80 成功 / 0xF8 忙(传图中) / 0xF6 无照片(0x23 实测);其余按「无更多照片/出错」结束会话 |
| B7 | 写特征响应类型 | 🟢 | 已自动探测:支持带响应写用 withResponse,否则退化 withoutResponse |
| B8 | 固件广播 UUID 字节序反:广播的是 10FF 而非 FF10 |
🟡 | 客户端已双向兼容(FF10/10FF 均识别)。待硬件修正 |
| B9 | 固件带 data 回包 check_sum 错误(如 0x10 回包 F0,协议算 3C) | 🟢 | 固件已于 2026-07-22 修复(实测 219 包零校验错误)。客户端暂保持宽容模式观察,稳定后改回严格校验 |
| B10 | 固件断开连接后不恢复广播,需重启设备才能再连(用户实测确认) | 🔴 | BLE 外设断开后应自动重新 advertising。影响重连体验,待硬件修正 |
| B11 | 按拍照键后 0x11 手动未上传仍=0,照片未计入「手动」类 | 🟡 | 待确认按键拍照的归类逻辑;CLI 已加降级:无手动图自动改拉自动图(0x21) |
| B12 | 连续模式丢包:一张图 219 包丢约 30 包,固件侧认为全部已发出(2026-07-22 实测) | 🔴 | 判断为固件调 notify 过快,协议栈 TX 缓冲满被丢弃(notify 返回失败未检查)。建议固件检查发送返回值失败重发,或加大发包间隔。0x26 有 0x24 补包兜底;0x23 无补包通道 |
| B13 | GATT UUID 变更:2026-07-22 固件实测 服务 FF12 / 写 FF13 / 通知 FF14(文档写 FF10/FFF2) | 🟡 | App 按属性自动兜底选中,不受影响;文档与固件需统一口径 |
| B14 | Android 收完一张后卡「接收中 N/N 包」转圈 | 🟢 | App bug:Android GATT 单在途写限制,「完成ACK+请求下一张」连发丢第二条。已加串行写队列修复(2026-07-22),同时修复 0x26 连发 0x24 补包只出第一条的隐患 |
📋 进度日志(Changelog)
2026-07-22 · 首次实传成功 + B9 修复确认 + Android 写队列修复
- 首次真机实传成功(Android):3 张手动照片收到并可见缩略图;固件 check_sum 修复后 219 包零校验错误(B9 🟢)。
- 新发现:连续模式一张图丢约 30 包而固件认为已发完(B12 🔴,疑似 notify 过快栈缓冲溢出);GATT 实测变为 FF12/FF13/FF14(B13)。
- 修复 Android 卡转圈(B14):GATT 单在途写限制导致「完成ACK+请求下一张」连发丢第二条 → BleClient 加串行写队列;断开时清 SD 会话;发送失败自动收尾会话。
- ⏭️ 固件改发送侧(查 notify 返回值/加间隔)后复测 0x23 丢包率;跑通 0x26+0x24 补包全流程。
2026-07-21 · 照片详情(EXIF) + 上传接口 + CLI 降级拉自动图
- 照片详情页:点缩略图弹 sheet(修复 List 行内 NavigationLink 手势失效),大图 + 基本信息 + EXIF/TIFF/GPS 元数据分组展示。
- 上传接口(仿 duooomi-ios-sdk demo):
POST {apiHost}/api/auth/loomart/file/upload-s3(multipartfile+x-api-key);详情页「上传照片」→ 返回 CDN URL 可复制。token 在Sources/Upload/UploadSecrets.swift(gitignore,模板入库)。 - CLI:查全三个数量;0x23 被拒(0xF6 无照片)自动降级 0x21 拉自动图;落盘后打印 EXIF。
- 实测新增:错误码 0xF6=无照片(B6);B10 固件断开后不恢复广播需重启(用户确认,🔴);B11 按键拍照未计入手动类。
- ✅ CLI build / App BUILD SUCCEEDED。
- ⏭️ 设备重启后跑 0x21/0x23 实传,确认图片格式(B4)与上传链路。
2026-07-21 · Mac 实测连通 + 指令全通 + 对齐最新文档
- 新固件(广播 FF10、蓝牙名 CmLXXXXXX、SD 卡自动拍照、手动长按关机)实测:Mac CLI 扫到
CmL3D8C64并连接成功。 - 指令实测全通:0x10=50 张自动、0x11=0 张手动未传、0x12=10 张手动总数、0x50 对时成功(ack 均 0x80)。
- 发现两个固件问题(B8/B9):广播 UUID 字节序反(
10FF);带 data 回包 check_sum 错(疑似 struct 对齐填充混入求和)。客户端均已兼容。 - 对齐最新文档:新增 0x13 删除SD卡所有照片(App 加红色按钮,0xF8=设备忙);
FrameParser支持宽容校验模式(告警不丢帧)。 - App 补「拉取手动未上传照片(连续模式 0x23)」按钮;扫描识别兼容 10FF。
- CLI 重构为集中测 0x23:连续收 0x33 → 拼包 → 0x40 → 落盘
/tmp/manual_photo_N.<ext>(按文件头识别 JPEG/PNG/BMP),带包序跳变告警与静默看门狗。 - ✅ 协议层单测通过;CLI build 通过;App BUILD SUCCEEDED。
- ⏭️ 手动拍几张后跑 0x23 实传,确认图片格式(B4)与多张传输行为(B5)。
2026-07-20 · 确认传图通道 FF10/FFF2 + 手动改选特征
- 硬件端调试日志确认:相机协议通道 = 服务 FF10 / 特征 FFF2(WRITE+NOTIFY,一特征双向)。
- App 默认自动选 FFF2 为写+通知;GATT 列表每个特征加「设为写/设为通知」按钮,可手动改选(备选 FF13 写 / FF14 通知)。
- BleClient 支持运行时切换写/通知特征并重建报告。
- ⏭️ 真机连上点「查询数量」验证收发;通了即做拉图落地。
2026-07-20 · 自动取 UUID + 连接发指令一条龙
- 设备实际用 FFFx 族 UUID(非旧 000002c4),改为不依赖固定 UUID:连上后遍历全部服务/特征,按属性自动挑写特征(Write/WriteNoResp)与通知特征(Notify/Indicate)。
- 连接逻辑改为「只要连上就成功」,不再因找不到旧 UUID 失败,从而能看到 GATT。
- App 内新增「GATT 服务/特征」展示区:列出所有 UUID + 属性,自动选中的写/通知打标签,UUID 可长按复制;日志同步打印整棵 GATT 树。
- 用户操作闭环:点设备→自动连+自动取值→点按钮发指令(0x10/0x50/0x20…)→日志看响应。
- ✅
xcodebuildiphonesimulator BUILD SUCCEEDED。 - ⏭️ 真机连上后截图 GATT,确认自动识别的写/通知是否正确(B3b)。
2026-07-20 · 扫描环节强化
- 顶部实时显示蓝牙适配器状态(就绪/关闭/未授权),不再静默失败。
- 蓝牙未就绪时点扫描,记下意图待
poweredOn自动补开扫。 - 扫全部设备(不按 service 过滤),避免目标设备未广播 service 被漏;广播了目标 service 的设备标 ⭐️ 并置顶。
- 优先用广播
localName;新增 RSSI 信号强度条(AllowDuplicates 持续刷新)。 - ✅
xcodebuildiphonesimulator BUILD SUCCEEDED。 - ⏭️ 下一步:真机实测扫描,反馈 B3。
2026-07-20 · 项目搭建 + 协议层
- 参照
duooomi-ios-sdk的 demo 模式新建工程(XcodeGen + 本地 SwiftPM 协议包 + 零依赖 CoreBluetooth 封装)。 - 实现协议层:帧编解码 / 累加和校验 / 流式拆帧;BLE 层复用旧 duooomi UUID;传图状态机;SwiftUI 单页 UI。
- 完整链路:扫描→连接→对时(0x50)→查数量(0x10/11/12)→拉图(0x20
23)→逐包接收(0x3033)+逐帧 ACK→0x40 完成回 ACK→自动拉下一张→拼包→显示。 - ✅ 协议层
swift test7/7 通过(测试向量取自文档示例校验和 0x88 / 0x0F)。 - ✅ App
xcodebuildiphonesimulator BUILD SUCCEEDED。
维护约定:每次迭代在「进度日志」置顶追加一条(日期·标题 + 要点 + 验证结果 + 下一步);卡点变化同步更新上表状态。
Description
Languages
Swift
100%