本文面向移动端、嵌入式、协议、测试和发布维护人员。内容以当前仓库代码为准,说明 系统边界、开发环境、核心实现和可验证的工作流。字节级协议细节以 BLE 协议规范为唯一规范来源。
QuoteImage 把高成本图片处理放在手机端,把设备端职责限制为身份与认证、严格接收 固定帧、驱动屏幕以及上报状态和电量。
flowchart LR
A["手机相册中的源图片"] --> B["Flutter 图片处理 isolate"]
B --> C["预览 PNG"]
B --> D["5624 字节 1-bit 帧"]
D --> E["QuoteBleClient"]
E -->|"BLE Secure Connections + GATT v2"| F["ESP-IDF NimBLE 固件"]
F --> G["RAM 中的完整帧"]
G --> H["UC8251D 驱动"]
H --> I["152 x 296 电子纸"]
主要设计约束:
- 固定输出
152 x 296 / 8 = 5624字节,避免设备解码大图; - 手机预览和发送帧来自同一次像素判定,避免预览与实物不一致;
- 只有通过版本、长度、偏移和 CRC32 校验的完整帧才能刷新;
- BLE IRQ 只处理短操作,耗时屏幕刷新由主循环执行;
- 传输帧只驻留 RAM,不磨损设备闪存;
- 设备只接受一个系统 bond owner 和一个活动连接,状态和恢复模型保持可审计。
quotex/
├── apps/mobile/ Flutter Android/iOS 应用包
│ ├── lib/main.dart 应用入口与 Material 主题
│ ├── lib/features/editor/ 单屏图片编辑、设备管理和发送 UI
│ ├── lib/core/image/ 图片解码、缩放、抖动和帧编码
│ ├── lib/core/ble/ 协议编解码及 BLE 客户端
│ ├── lib/core/devices/ Device ID 设备记录与 v1 清理迁移
│ ├── test/ Flutter 单元测试和 Widget 测试
│ ├── android/ Android 壳、权限和 Gradle 配置
│ └── ios/ iOS 壳、权限和 CocoaPods 配置
├── firmware/quote0/
│ ├── esp-idf/ ESP-IDF 5.5 / FreeRTOS 主线固件
│ │ ├── main/ NimBLE、任务、状态、ADC 与显示驱动
│ │ ├── host_tests/ 可移植 C 协议与渲染测试
│ │ └── sdkconfig.defaults ESP32-C3、NimBLE 与省电配置
│ └── legacy/micropython/ 旧实现及其 CPython 测试
├── docs/ble-protocol.md 版本化 BLE 应用协议规范
├── tools/scan_quote0.swift macOS CoreBluetooth 诊断工具
├── Makefile 仓库级常用命令
├── pubspec.yaml Dart Pub workspace 根配置
└── pubspec.lock 锁定的 Dart/Flutter 依赖
仓库根 pubspec.yaml 使用 Pub workspace,当前成员只有 apps/mobile。构建产物和
工具缓存不是架构的一部分,不应作为源代码提交。
- Git;
- GNU Make 或兼容的
make; - Flutter 3.41 或更高版本;
- Dart 3.11.x(根包要求
^3.11.0,移动包要求^3.11.4); - Python 3、CMake 和 Ninja,用于固件测试与构建;
- ESP-IDF 5.5.x LTS,构建前须完成安装并激活环境;
mpremote,仅在维护 legacy MicroPython 固件时需要。
当前固件构建基线固定为 ESP-IDF 5.5.x。按照 Espressif 官方说明安装后执行对应的
export.sh,并确认:
idf.py --version
cmake --version
ninja --versionMakefile 直接调用激活环境中的 idf.py、parttool.py、esptool.py,以及当前
PATH 中的 python3、cmake、ninja 和 flutter。
Android 开发需要 Android SDK、可用的 Android toolchain 和启用开发者模式/USB
调试的 BLE 真机。项目使用 Java 17 目标兼容级别;实际 Gradle/JDK 组合以
flutter doctor -v 检查结果为准。
iOS 开发需要 macOS、Xcode、CocoaPods、已配置的 Apple 开发签名和 BLE 真机。 首次构建前 Flutter 会生成 iOS 配置并安装 Pods。
flutter doctor -v
flutter devices桌面模拟器可用于有限的 UI 测试,但不能代表真实 BLE、权限和照片选择流程。
主线固件直接生成 ESP32-C3 bootloader、partition table 和应用镜像,不依赖设备上
预装任何语言运行时。sdkconfig.defaults 固定 NimBLE peripheral、ATT MTU 247、单连接、
单 bond、Secure Connections only、MITM、16 字节链路密钥、RPA,以及 tickless idle 和
automatic light sleep。旧 MicroPython 版本只保留为测试与迁移参考;它的 v1 GATT
协议不与主线 v2 固件互通。
在仓库根目录执行:
make bootstrap
make checkmake bootstrap 运行 flutter pub get 并解析整个 Pub workspace。make check 会执行:
make firmware-host-check
make firmware-idf-build
make firmware-legacy-check
flutter analyze apps/mobile
flutter test apps/mobile常用目标:
| 命令 | 作用 | 是否需要硬件 |
|---|---|---|
make bootstrap |
获取 Flutter/Dart 依赖 | 否 |
make firmware-check |
C 测试、ESP-IDF 构建和 legacy 测试 | 否 |
make mobile-check |
Flutter 静态分析和测试 | 否 |
make check |
执行以上全部检查 | 否 |
make mobile-run |
从移动包目录选择/运行目标设备 | Android/iOS 功能需真机 |
make device-install PORT=... |
同布局 v2 开发升级,保留 NVS | 是 |
make device-migrate PORT=... |
整片擦除并写入完整固件;v1 首次升级强制使用 | 是 |
make device-reset-pairing PORT=... |
USB 清除 v2 owner、bond、身份和设置状态 | 是 |
cd apps/mobile
flutter devices
flutter run -d <device-id>也可从仓库根目录运行 make mobile-run,然后在 Flutter 提示中选择设备。
Android 主清单声明 BLE 硬件必需、Android 12+ 的 BLUETOOTH_SCAN/
BLUETOOTH_CONNECT,以及旧系统的 Bluetooth 和位置兼容权限。iOS Info.plist
声明蓝牙和照片库用途。修改插件或平台权限后,应在对应系统的真机上重新验证首次授权、
拒绝后重试和系统设置恢复。
EditorScreen 当前承担完整单屏工作流:
- 加载设备列表和最后选择项;
- 选择源图片及触发后台处理;
- 管理 fit、rotation、dither、threshold;
- 添加、切换、重命名和解除配对;
- 查询设备、电量和电量显示 capability,提交按设备持久化的显示开关;
- 发送帧并把 BLE 阶段映射到界面进度。
它通过构造参数接收可选 QuoteBleClient 和 DeviceStore,Widget 测试可注入 fake。
新增业务逻辑时优先保持协议、存储、图像处理与 UI 的现有边界;若单屏状态继续增长,
再引入与当前测试注入方式兼容的状态控制层,而不是把平台依赖直接放进 Widget。
并发相关状态:
_processGeneration防止较早的异步处理结果覆盖新参数结果;- 图片处理、读取电量、提交屏幕设置和发送期间会禁用冲突控件;
- 扫描和发送期间不能打开新的设备切换流程;
- 每次 BLE 操作打开短连接,操作完成后断开。
epaper_image_processor.dart 使用 compute 在后台 isolate 执行:
- 解码源图片;
- 烘焙 EXIF orientation;
- 将用户旋转与面板物理方向的 270° 变换合并;
- 使用 cubic interpolation 等比缩放;
contain时白底居中,cover时中心裁切;- 以 Rec. 709 系数计算亮度,透明像素与白色背景合成;
- 阈值判定,或使用蛇形扫描的 Atkinson 误差扩散和确定性阈值 jitter;
- 同时生成 MSB-first 帧和预览 PNG;
- 将预览旋转回用户看到的
296 x 152横向方向。
位布局为:
byte_index = y * (152 / 8) + floor(x / 8)
bit_mask = 0x80 >> (x mod 8)
黑色 = 清零该位
白色 = 保持该位为 1
修改图像算法时,必须同时保持以下不变量:帧恰好 5624 字节、预览与帧像素一致、透明 区域按白色处理、输出确定性,以及 0° 对应产品定义的横向方向。
QuoteBleClient 以 QuoteDeviceId 为持久身份,以 QuoteDiscovery 表示一次扫描中短暂的
平台端点,并以 QuoteSecurityState 区分未拥有、已拥有和安全故障。BLE 地址、
remoteId、RSSI、通用广播名和 setup token 都不能替代 Device ID。
发送工作流如下:
sequenceDiagram
participant UI as EditorScreen
participant Client as QuoteBleClient
participant OS as Android/iOS BLE
participant Device as Quote/0
UI->>Client: locate(deviceId) / upload
Client->>OS: 串行获取全局 BLE operation lease
Client->>Device: 扫描 v2 service,连接并恢复系统 bond
OS->>Device: LE Secure Connections + MITM
Client->>Device: 读取 Status 获取 connection generation
Client->>Device: 读取 Device Info 并核对 Device ID
Client->>Device: START(requestId, generation, transferId, length, CRC32)
loop 连续 offset 分块
Client->>Device: DATA(generation, transferId, offset, bytes)
end
Client->>Device: COMMIT(new requestId, generation, transferId)
Device-->>Client: Status notify: READY / REFRESHING / COMPLETE
Client-->>UI: 刷新完成
扫描只按 7a1e0200-... service UUID 发现候选。通用名称始终是 Quote/0;未配对设备的
VBUS-scoped setup session 可额外携带 8 字节 setup token,用来合并同一 session 内因
RPA/平台标识变化产生的扫描结果,不能写入设备记录。已知设备连接并完成安全访问后,
必须用 Device Info 的 16 字节 ID 核对目标,错误 ID 立即断开。
所有公开 BLE 工作流共用 client-wide operation arbiter。扫描、连接、配对、读取状态、
管理操作和上传不能互相穿插;取消或异常也必须在 finally 中释放 lease。每条连接先从
Status 读取非零 generation;Control 与 Management 共用严格递增的 request ID,回退、
冲突或耗尽后断开重连。数据块按 ATT_MTU - 14 计算 v2 payload,并使用
write-with-response 流控。
Android 在连接后显式调用 createBond 并等待 bonded,UNPAIR 或确认 stale bond 后调用
removeBond。iOS 没有公开的 bond 增删 API:客户端通过访问 authenticated
characteristic 触发系统配对;stale bond 必须提示用户到系统蓝牙设置中“忽略此设备”。
两端都只能在系统安全完成且 Device ID 匹配后把设备标记为在线。
Management 响应只使用 indication。客户端须先订阅,再写入请求,并按 opcode、request ID 和 generation 匹配;在系统确认 indication 之前不允许发起第二个 Management 操作。typed BLE errors 要保留扫描超时、连接、bond、ATT 安全、stale bond、协议格式、 generation、request ID、indication 和显示错误的差异,不能统一折叠为“设备离线”。
PersistentDeviceStore 把 v2 envelope 存在 shared_preferences 的
quoteimage.deviceStore.v2,记录 32 个十六进制字符表示的 16 字节 Device ID、名称、
更新时间和最后选择项。设备 bond 由 Android/iOS 管理;应用不再生成、保存或复制协议
credential。
第一次读取执行可重入迁移:解析旧 quoteimage.devices.v1 和
quoteimage.selectedMac.v1,先写入带 legacyMigrationPending checkpoint 的 v2
envelope,再删除所有 quoteimage.credential.v1.* 安全存储键和旧 preference,最后
清除 checkpoint。清理遍历 secure storage 的全部键,因此也删除没有对应设备记录的
孤儿 credential。中途失败时保留 checkpoint,下次继续;不会把旧 MAC 记录冒充为已
配对的 Device ID。
旧记录只能作为“需要 USB 升级并重新配对”的迁移提示。新 bond 成功并读到 Device ID 后,用 v2 记录替换对应 legacy 记录。损坏的 v2 envelope 必须显式报告存储损坏,同时 仍尝试清除 v1 credential;不能静默返回空列表并掩盖不可追踪的本地状态。
app_main 初始化默认 nvs、nvs_ui、设备配置、16 字节 Device ID、电源管理、ADC、
显示任务和 BLE。未拥有设备只有在 VBUS 存在时才生成六位系统配对 passkey;必须先由
显示任务确认配对画面成功,再在当前 confirmed VBUS-high cycle 内持续开放 setup 广播,
不设 duration timer。确认拔出 VBUS 后立即关闭 setup;已拥有设备不会为了启动 BLE
改写电子纸旧画面。
NimBLE host callback 只做连接 generation 维护、安全事件、有界包解析、帧复制、CRC 和
事件入队。quote_app 任务串行处理管理命令、设备配置 NVS、配对画面编排、电量 deadline
和显示结果;
quote_display 是 SPI、VIN、BUSY、RESET、DC、CS 的唯一所有者,并在屏幕更新前后提交
独立的电量覆盖快照。两个应用任务空闲时都阻塞在 FreeRTOS 队列,不再使用轮询。
禁止在 NimBLE callback 中执行 NVS、ADC 或屏幕刷新。Management characteristic 最多
64 字节并进入固定队列;Control/Data 在 host 上直接推进固定 5624 字节接收器。所有
GATT access 先验证 encrypted、authenticated、bonded、16 字节 key、owner identity 和
exact (connection handle, generation)。显示刷新期间接收器保持 REFRESHING,缓冲区
不可覆盖,断连也不会终止已经开始的刷新。
Management response 使用 reserve-before-send 的单 indication slot。调用 NimBLE 发送前
必须释放安全 mutex,因为 ble_gatts_indicate_custom 可以同步产生 NOTIFY_TX。只有
exact connection generation、reservation、request ID、opcode 和 operation kind 的 ACK
才能完成当前 slot;断连、host reset、send failure 和 timeout 只清除 exact operation。
FrameReceiver 状态路径:
IDLE --START--> RECEIVING --完整数据 + COMMIT/CRC OK--> READY
READY --take_frame--> REFRESHING --finish_refresh--> COMPLETE 或 DISPLAY_ERROR
然后内部重置为 IDLE
START 携带 version、request ID、connection generation、transfer ID、固定长度和 CRC,
复用一个固定 5624 字节 staging 缓冲区。只有同一 generation 且 transfer ID 更大的
START 可以替换 RECEIVING;READY 和 REFRESHING 都不可覆盖。DATA 携带 generation、
transfer ID 和两字节 little-endian offset,offset 必须严格等于当前 received。CRC 使用
标准 reflected CRC-32/ISO-HDLC。断连只丢弃未提交的 RECEIVING;已提交帧继续按不可变
display job 完成,但不会向旧连接发送结果。
默认 nvs 保存低频安全和配置状态:
quote0/state:名称、电量显示标志和 CRC32,不含应用 credential;quote_identity/state:随机 16 字节 Device ID、单 owner identity address 和 CRC32;quote_security/transition:配对/解除配对的事务 journal;- NimBLE bond store:单个 Secure Connections bond。
quote0/state 明确区分 EMPTY、VALID、MIGRATABLE、CORRUPT 和 IO_ERROR。只有 EMPTY 可采用
默认值;损坏或 I/O 错误不能自动降级为未配对状态。身份、bond 和 journal 在 BLE host
sync 时联合审计,任一不一致都关闭广播并置安全故障,而不是擦除全部状态后开放 setup。
独立 nvs_ui 中的 quote0/battery_ui 只保存横向右上角 40 x 16 原始像素、最后成功显示的电量及外部
供电覆盖状态和 CRC。外部供电复用版本 2 blob 的一个保留字节,因此旧快照自然迁移为
“未显示闪电”,大小和版本不变。每次刷新前先提交 pending 记录,成功后再提交
committed 记录;启动时 pending、
长度、版本或 CRC 无效都视为无快照;覆盖区尺寸变更会提升快照版本,使旧快照自然失效。
无可信快照时禁止根据不确定旧画面做局部刷新。完整 5624 字节
用户帧仍不写入 flash。
检测到有效 v1 quote0/state 时,固件先把仅包含名称和电量显示偏好的 v2 blob 暂存到
nvs_ui/quote_migrate/state_v2,再重建默认 nvs,最后恢复该 blob 并清除 checkpoint。
此过程永久删除旧 16 字节 credential、旧 owner/bond 材料并生成新 Device ID;它不迁移
授权。损坏的 v1 blob 不参与迁移。生产设备仍必须按第 10 节执行整片 USB 擦除,因为旧
factory-only partition layout 与 v2 双槽布局不兼容。
设备名限制按 UTF-8 字节计算:去除首尾空白后 1 到 24 字节,禁止 C0 控制字符和
DEL。默认名为 QuoteImage-XXXX,但广播始终使用通用名 Quote/0。MicroPython
device.json 不做自动迁移。
参考 Quote/0 引脚:
| 信号 | GPIO |
|---|---|
| VIN / panel enable | 20 |
| BUSY | 3 |
| RESET | 4 |
| DC | 5 |
| CS | 6 |
| SCK | 10 |
| MOSI | 7 |
| Battery ADC | 1 |
| VBUS detect | 0 |
SPI 使用总线 1、mode 0、8-bit MSB-first、15 MHz。驱动固定为 152 x 296,BUSY 默认
超时 5000 ms;刷新完成后等待控制器要求的延迟,发送 deep sleep 命令并释放 VIN。
改变面板、方向、LUT 或引脚时,应同时检查 quote_config.h、驱动、配对画面坐标、移动端
帧尺寸/旋转和协议版本,不能只修改其中一处。
电量模块在 GPIO1 使用 ESP-IDF ADC oneshot 与 calibration,12 dB attenuation 下每次 读取 8 个样本,去掉最高和最低值后平均,再乘 2.0。结果通过分段线性锂电池曲线映射 为 0 到 100,并约每 30 秒更新。
2.0 系数假设等阻值分压。换硬件或量产前应使用万用表校准,并验证 ADC 输入电压始终位于 ESP32-C3 允许范围内;软件曲线不能弥补错误的硬件分压设计。
常驻电量显示默认关闭。开启后右上角 40 x 16 电池轮廓内绘制无前导零百分比,填充区
使用反白像素保证文字可读;低电和临界值分别加 !、x 前缀。关闭后仍保留 20% 以下
的居中告警符号。告警阈值需要连续两次确认;常规数字还要求连续两次同方向且距屏上值至少 2%。
覆盖请求带递增版本并合并为最后状态,图片上传、App 设置和周期采样都通过同一显示队列。
GPIO0 按 MindReset 官方定义检测 VBUS,高电平仅表示 USB 或外部电源存在。输入不启用
ESP32-C3 内部上拉或下拉。GPIO 使用与当前稳定状态相反的电平中断;ISR 首次触发后立即
屏蔽该管脚,并通过独立二值信号量唤醒应用任务。应用任务在 400 ms 后采样,随后按最终
稳定状态重新武装相反电平。确认变化后立即使用最近一次电量样本更新覆盖层。供电时无论
常驻开关状态都显示数字,并以 3x5 闪电字形替换 %;拔电后恢复百分号、告警符号或原始像素。
固件启用 160/40 MHz 动态调频、tickless idle 和 automatic light sleep。NimBLE 控制器
按广播与连接事件自动唤醒 CPU,因此设备持续可发现,不改变客户端 5 秒扫描。显示和
ADC 操作持有 ESP_PM_NO_LIGHT_SLEEP 锁;本方案不使用会关闭 BLE 的周期 deep sleep。
GPIO0 使用 ESP-IDF GPIO light-sleep wakeup:稳定低电平时等待高电平,稳定高电平时
等待低电平;同一电平配置也用于运行态中断。中断在防抖期间保持屏蔽,确认完成后再按
最终状态重新配置并启用。VBUS 无法区分充电和充满,不得从该信号派生充电阶段。
v2 安全边界建立在 BLE Secure Connections 和持久化 owner/bond 上,不再使用应用层 credential 或 HMAC:
- 仅未拥有且 VBUS 存在的设备生成六位 passkey;配对画面完整刷新成功后才开放 VBUS-scoped setup session,并在 scan response 中加入本 session 的随机 8 字节 setup token;只要该 VBUS-high cycle 持续有效且未锁定,setup 不会因计时自动关闭;
- NimBLE 使用 DisplayOnly、SC-only、MITM、bonding、Security Mode 1 Level 3 和 16 字节 key;legacy SMP 关闭;
- 每条连接在 15 秒内完成系统 pairing/encryption,否则按 exact connection generation 终止;
- 固件确认 pairing complete 与 encryption changed 两个事件、审计 bond 确实落盘,再把 resolved peer identity 写为唯一 owner;
- 后续连接必须同时匹配系统 bond、owner identity 和当前 generation,才可访问任何 characteristic;设备最多一个连接、一个 bond;
- UNPAIR 的
RECONNECT_REQUIREDindication 得到确认后删除 bond 和 owner;两秒内无法 确认时 fail-forward 删除,避免已接受操作留下模糊状态。
Device ID 是应用持久身份,RPA 每 900 秒轮换。广播不含 MAC、Device ID、自定义名称或 其他稳定标识。setup token 只关联当前 VBUS-scoped setup session,不是认证材料。确认 拔出 VBUS 会立即关闭 session、删除 token 并终止未认证连接;三次 pairing failure 锁定 当前 VBUS cycle,保持 VBUS 高电平不会自动解锁,必须拔出并重新插入才能重试。
Android 由应用调用 createBond/removeBond;iOS 通过首次 authenticated characteristic
访问触发系统配对且没有公开 remove-bond API。iOS stale bond 必须让用户在系统设置中
忽略设备。任何平台都不能用删除应用记录代替删除 OS bond 或设备 owner。
开发阶段 setup manufacturer data 的 Company Identifier 使用 0xFFFF。正式构建在该值
未替换时必须直接失败。申请或使用合规 Bluetooth SIG Company Identifier 后,应只同步
固件/移动端对 setup AD 的解析;不能把 Company ID 或 token 当成 Device ID。
BLE 协议规范定义:
- v2 自定义服务和 9 个 characteristic UUID,以及标准 Battery Service;
- 广播及 scan response 格式;
- Device Info、Management 和 Status 包;
- connection generation、client-wide request ID 和 response replay 规则;
- 系统配对、owner/bond、改名、解除配对和电量显示设置;
- START/DATA/COMMIT/ABORT/PING 帧传输;
- Management indication 与确认语义;
- 所有状态码和错误码。
签名断点续传 OTA 协议另行固化 Firmware Info、OTA Control、OTA Data、
OTA Status、192 字节 canonical manifest、P-256 raw r || s、4 KiB durable checkpoint、
END 重读验证、ACTIVATE/pending-verify/rollback 和 Android/iOS long-write 差异。OTA
Control/Data 仅允许可靠 write-with-response;Status 使用 READ+NOTIFY,通知丢失后以
characteristic readback 恢复。成功响应须 exact 匹配 request ID、connection generation、
session ID;远端错误须 exact 匹配前两者,但允许在会话建立前或 session mismatch 时回传
零或设备当前 session ID。Data/checkpoint Status 始终关联最后一次成功 BEGIN/QUERY,失败或
畸形 Control 不能覆盖该 attachment request ID。终态验证错误以 FAILED journal 持久化并在
重启后保留原错误;esp_ota_end() 前先持久化 FINALIZING,重启遇到该阶段必须 fail closed,
不能二次 finalization。FAILED 只允许 exact-session QUERY/ABORT;ACTIVATE 写入开始前冻结
取消。旧 App 遗留 session 的清理由 authenticated OTA Status 取得实际 session ID,不依赖
当前内置包。
修改协议时至少同步:
docs/ble-protocol.md;- OTA 变更还包括
docs/ota-protocol.md; apps/mobile/lib/core/ble/quote_protocol.dart及core/ota/;apps/mobile/lib/core/ble/quote_ble_client.dart;firmware/quote0/esp-idf/main/quote_protocol.c、quote_ota_protocol.c和quote_ble.c;- 两端协议测试和已知向量;
- 兼容性、升级顺序和版本迁移说明。
v2 Control、Data、Status、Device Info、Management、Firmware Info、OTA Control、OTA
Data 和 OTA Status 的 version 都是 2,但各包字段偏移不同;不得只检查 version 而忽略
精确长度。所有多字节整数均为 little-endian;帧 CRC、manifest 和 request fingerprint
跨语言测试向量用于防止实现漂移。协议升级必须使用新 UUID 空间,不允许在同一
UUID/version 下改变字段或安全要求。
v1 7a1e0000-... UUID、四位码、PAIR_BEGIN/PAIR_CONFIRM、AUTH_CHALLENGE/AUTH_PROVE、
credential 和 HMAC 都不能在 v2 客户端或固件中保留 fallback。v2 失败后的降级会把链路
安全故障变成可利用的绕过,应作为发布阻断问题。
flutter analyze apps/mobile
flutter test apps/mobile当前覆盖:
- v2 START/Control/Data/Status/Management 精确长度、little-endian 字段、CRC32、非零 identifier、Device Info flags/Device ID 和所有 typed protocol errors;
- client-wide operation arbiter、request ID/generation 匹配、Management indication 响应;
- OTA 0206..0209 packet codec、manifest/package registry、typed errors、checkpoint resume、 queue backpressure、通知丢失 readback、取消 ABORT 和 pending-verify/rollback 结果;
- 方向定义、黑白位编码、contain/cover、Atkinson 确定性;
QuoteDeviceId、v2 device-store envelope、可重入 v1 record 迁移、孤儿 credential 清除和损坏状态显式上报;- 编辑器完整控件、离线切换设备、电量显示开关、外部供电状态图标、40 x 16 预览覆盖 和六位系统配对流程。
make firmware-host-check
make firmware-legacy-check
make firmware-idf-build可移植 C 测试覆盖 v2 帧/连接/请求状态机、CRC、电量算法、owner journal、bond audit、 配对编排、广告恢复、Management indication FSM,以及 OTA manifest/codec/session、双槽 journal 和 display/OTA arbiter。保留的 MicroPython fake 测试只验证旧行为基线,不能作为 v2 兼容测试;ESP-IDF 构建负责验证 NimBLE、NVS、ADC、PM、SPI 与 ESP OTA API 集成。
- 帧接收状态机、generation/transfer ID、错误偏移、长度、CRC、ABORT 和 immutable READY;
- RPA-safe 广告内容、VBUS-scoped setup token、三次失败锁定当前 cycle 和有界恢复退避;
- 单连接 generation、15 秒安全 deadline、owner/bond/journal 审计和 stale event;
- request ID replay/conflict/rollback/exhaustion、Management indication ACK/失败/断连/reset;
- 改名、解除配对 fail-forward、电量设置和 busy 状态;
- 配置五分类、v1 credential 清除迁移、名称字节限制;
- 电量曲线、trimmed mean、覆盖层像素边界、外部供电字形、2% 节流、400 ms 防抖和 tick 回绕;
- 六位配对画面的方向、居中、前导零和输入校验。
- 192 字节 manifest、raw P-256 签名接口、32 位严格 offset、4 KiB checkpoint、断连回退、
journal commit/readback 错误、FINALIZING 掉电恢复、FAILED terminal error 恢复、
Control/Data request correlation 分离和接近
UINT32_MAX的溢出边界。
自动测试无法覆盖以下项目,发布前应使用真实 Quote/0 和至少一台 Android/iOS 目标 设备验证:
- 首次权限允许、拒绝和系统设置恢复;
- 广播使用 RPA 且约 15 分钟轮换,ADV/scan response 不出现 MAC、Device ID 或自定义名;
- setup token 在单个 VBUS-scoped setup session 内稳定、跨 VBUS cycle 变化,排除全零和 上一 session 重复;
- Android/iOS 六位系统配对、15 秒 security deadline、stale bond、第二 central 拒绝;
- 长时间保持 VBUS 高电平时 setup 不因计时关闭;确认拔出后立即停止未拥有设备的广告并 终止未认证连接;三次 pairing failure 锁定当前 cycle,VBUS fall/rise 后 rearm,旧 queued setup event 不复活;
- Management 只发 indication,系统 confirmation 后完成;快速连续写入不产生第二响应, 断连和 handle reuse 不让 stale ACK 完成新操作;
- UNPAIR 后 OS bond/设备 owner 均清除,iOS 按系统设置恢复流程重新配对;
- 冷启动保留旧画面、USB 恢复后显示新的六位 passkey;
- 多种图片方向、透明图、20 MiB 边界和预览一致性;
- 弱信号/中途断电下不刷新半帧;
- 真实屏幕 BUSY 时序、LUT、黑白位含义和刷新完成通知;
- 电池 ADC 与万用表读数;
- 电量显示开启/关闭、GPIO0 插拔与轻睡眠唤醒、重启恢复、低电阈值、v1 不兼容拒绝与强制迁移及 中途断电快照失效;
- Android/iOS BEGIN long write、低 MTU Status readback、OTA 队列背压、4 KiB 断点恢复、
partial encrypted block、
esp_ota_end()前后、FAILED journal、END 重读验证、旧 session 清理、ACTIVATE 取消边界和 pending-verify/rollback;逐个断电注入点和预期结果以 OTA 协议人工测试表为发布准入; - 连续发送多帧后的内存、温度、供电和 BLE 稳定性。
激活 ESP-IDF 5.5.x,并先用资产记录确认目标串口。device-install 只适用于分区布局兼容的
v2 开发升级并保留 NVS:
make device-install PORT=/dev/cu.usbmodemXXXX任何 v1 固件(legacy MicroPython 或早期 factory-only ESP-IDF)首次迁移到安全 v2 都必须 通过受控 USB 整片擦除并写入 bootloader、partition table 和 app:
make device-migrate PORT=/dev/cu.usbmodemXXXX旧 factory app 占用的地址在双槽布局中会变成 otadata、nvs_ui 或 OTA app 内容;只写
新 partition table 而不擦除可能让旧字节被按新类型解释。因此 v1 首次升级禁止使用普通
device-install,也不存在 BLE OTA bridge 或 v1 fallback。
整片擦除会删除 device.json、旧名称/设置、application credential、Device ID、bond 和
owner。刷写后移动端必须幂等迁移本地 store,删除所有
quoteimage.credential.v1.*(包括孤儿键),不能继续按 MAC 寻址。随后插入并保持 VBUS,
等待六位 passkey 画面,通过 Android/iOS 系统流程创建新 bond,并在 authenticated
Device Info 读取成功后保存新 Device ID。旧 App 记录、OS bond 或设备 owner 任一残留
都不能靠协议降级绕过。
查看串口日志:
make firmware-idf-monitor PORT=/dev/cu.usbmodemXXXX清除配对状态:
make device-reset-pairing PORT=/dev/cu.usbmodemXXXX此命令只擦除默认 nvs partition 并重启,不改写 app 或 nvs_ui。它删除设备配置、
Device ID、owner journal 和 NimBLE bond;下一次启动生成新 Device ID 和默认名。只有
VBUS 存在且新 passkey 已成功显示时才开放 setup,并在 VBUS 保持连接期间持续开放;确认
拔出后立即关闭。手机侧仍须删除旧 OS bond;iOS 需要用户在系统蓝牙设置中忽略设备。
swift tools/scan_quote0.swift当前脚本仍绑定 v1 7a1e0000-... UUID 和旧名称过滤条件,不能用于 v2 验证;在它迁移到
v2 service UUID、通用名和 setup token 解析前,运行结果只能作为 legacy 基线。即使迁移
扫描部分,Device Info 与 Battery Level 也要求 encrypted+authenticated owner link;未
持有系统 bond 的命令行诊断不能读取它们,更不能替代 Android/iOS 的配对、发送、改名和
解除配对测试。
| 现象 | 优先检查 |
|---|---|
| 完全扫描不到 | owner/setup 是否允许广告、v2 service UUID、RPA、系统权限、恢复退避 |
| 能扫描但连接失败 | 单连接占用、VBUS/setup session 状态、三次失败锁定当前 cycle、第二 central、系统 bond |
| 安全访问失败 | 15 秒 deadline、SC/MITM、stale OS bond、owner identity、bond/journal audit |
| 读到错误设备 | 禁止按 MAC/remoteId 判断,核对 authenticated Device Info 的 Device ID |
| Management 超时 | indication 是否已订阅、request ID/generation、是否有未确认 indication |
| 传输中断 | write-with-response、v2 header 后分块大小、generation/transfer ID、严格 offset |
| CRC 错误 | 两端 CRC32、帧是否在发送期间被改动、分块边界 |
| 收到 REFRESHING 后失败 | SPI/引脚/供电、BUSY 超时、面板驱动和 LUT |
| 画面方向错误 | EXIF、用户旋转、270° 物理旋转、预览回转、配对画面坐标 |
| 电量偏差固定 | 分压电阻、BATTERY_DIVIDER、ADC 衰减和万用表校准 |
开发构建:
cd apps/mobile
flutter build apk --debug
flutter build ios --debug --no-codesign发布前在 apps/mobile/pubspec.yaml 更新 version: major.minor.patch+build,并完成平台
签名配置、应用标识、隐私文案和真机回归。
当前 Android release 构建仍显式使用 debug signing key,仅适合开发运行,不能作为
商店或正式分发包。发布负责人必须添加独立的 release keystore、安全的密钥注入方式
和 CI 配置。iOS 也需要在 Xcode 中配置正确的 Team、Bundle Identifier、证书和
provisioning profile。
正式发布还应处理:
- 把开发用 Company Identifier
0xFFFF替换为合规标识; - 确认最低 Android/iOS 版本与 Flutter 插件实际要求;
- 在 Android 和 iOS 上复核蓝牙、照片权限及隐私政策;
- 记录移动端版本、固件版本和 BLE 协议版本的兼容矩阵;
- 锁定 ESP-IDF 5.5.x 工具链并归档可复现的固件构建产物;
- 对出厂设备执行配对、显示、电量和恢复的硬件验收。
提交前至少执行:
make check
git diff --check
git status --short按变更类型追加验证:
| 变更 | 必需的额外检查 |
|---|---|
| UI/权限/插件 | Android 与 iOS 真机流程 |
| 图像处理/方向 | 黄金像素、预览与屏幕实物对照 |
| 协议字段/状态码 | 文档、两端实现、跨端向量、升级兼容 |
| 配对/owner | Android/iOS 系统 bond、第二 central、stale bond、UNPAIR、VBUS-scoped setup cycle、USB 恢复 |
| 驱动/LUT/引脚 | 目标硬件完整刷新、BUSY 超时和休眠 |
| 电池 | 多电压点 ADC 与万用表对照 |
| 发布配置 | 非 debug 签名、版本号、安装升级和权限回归 |
新增功能应同时更新相应用户行为文档。若实现与文档不一致,以修正代码或文档并补充 测试的方式解决,不要依赖只有维护者知道的隐含约定。