Skip to content

Latest commit

 

History

History
680 lines (536 loc) · 34.4 KB

File metadata and controls

680 lines (536 loc) · 34.4 KB

QuoteImage 开发者指南

本文面向移动端、嵌入式、协议、测试和发布维护人员。内容以当前仓库代码为准,说明 系统边界、开发环境、核心实现和可验证的工作流。字节级协议细节以 BLE 协议规范为唯一规范来源。

1. 系统职责与设计目标

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 电子纸"]
Loading

主要设计约束:

  • 固定输出 152 x 296 / 8 = 5624 字节,避免设备解码大图;
  • 手机预览和发送帧来自同一次像素判定,避免预览与实物不一致;
  • 只有通过版本、长度、偏移和 CRC32 校验的完整帧才能刷新;
  • BLE IRQ 只处理短操作,耗时屏幕刷新由主循环执行;
  • 传输帧只驻留 RAM,不磨损设备闪存;
  • 设备只接受一个系统 bond owner 和一个活动连接,状态和恢复模型保持可审计。

2. 仓库结构

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。构建产物和 工具缓存不是架构的一部分,不应作为源代码提交。

3. 环境要求

3.1 通用工具

  • 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 --version

Makefile 直接调用激活环境中的 idf.pyparttool.pyesptool.py,以及当前 PATH 中的 python3cmakeninjaflutter

3.2 移动平台工具

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、权限和照片选择流程。

3.3 Quote/0 固件环境

主线固件直接生成 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 固件互通。

4. 初始化与日常命令

在仓库根目录执行:

make bootstrap
make check

make 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、身份和设置状态

5. 移动端开发

5.1 运行

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 声明蓝牙和照片库用途。修改插件或平台权限后,应在对应系统的真机上重新验证首次授权、 拒绝后重试和系统设置恢复。

5.2 UI 与状态所有权

EditorScreen 当前承担完整单屏工作流:

  • 加载设备列表和最后选择项;
  • 选择源图片及触发后台处理;
  • 管理 fit、rotation、dither、threshold;
  • 添加、切换、重命名和解除配对;
  • 查询设备、电量和电量显示 capability,提交按设备持久化的显示开关;
  • 发送帧并把 BLE 阶段映射到界面进度。

它通过构造参数接收可选 QuoteBleClientDeviceStore,Widget 测试可注入 fake。 新增业务逻辑时优先保持协议、存储、图像处理与 UI 的现有边界;若单屏状态继续增长, 再引入与当前测试注入方式兼容的状态控制层,而不是把平台依赖直接放进 Widget。

并发相关状态:

  • _processGeneration 防止较早的异步处理结果覆盖新参数结果;
  • 图片处理、读取电量、提交屏幕设置和发送期间会禁用冲突控件;
  • 扫描和发送期间不能打开新的设备切换流程;
  • 每次 BLE 操作打开短连接,操作完成后断开。

5.3 图片处理管线

epaper_image_processor.dart 使用 compute 在后台 isolate 执行:

  1. 解码源图片;
  2. 烘焙 EXIF orientation;
  3. 将用户旋转与面板物理方向的 270° 变换合并;
  4. 使用 cubic interpolation 等比缩放;
  5. contain 时白底居中,cover 时中心裁切;
  6. 以 Rec. 709 系数计算亮度,透明像素与白色背景合成;
  7. 阈值判定,或使用蛇形扫描的 Atkinson 误差扩散和确定性阈值 jitter;
  8. 同时生成 MSB-first 帧和预览 PNG;
  9. 将预览旋转回用户看到的 296 x 152 横向方向。

位布局为:

byte_index = y * (152 / 8) + floor(x / 8)
bit_mask   = 0x80 >> (x mod 8)
黑色       = 清零该位
白色       = 保持该位为 1

修改图像算法时,必须同时保持以下不变量:帧恰好 5624 字节、预览与帧像素一致、透明 区域按白色处理、输出确定性,以及 0° 对应产品定义的横向方向。

5.4 BLE 客户端

QuoteBleClientQuoteDeviceId 为持久身份,以 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: 刷新完成
Loading

扫描只按 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 和显示错误的差异,不能统一折叠为“设备离线”。

5.5 本地存储

PersistentDeviceStore 把 v2 envelope 存在 shared_preferencesquoteimage.deviceStore.v2,记录 32 个十六进制字符表示的 16 字节 Device ID、名称、 更新时间和最后选择项。设备 bond 由 Android/iOS 管理;应用不再生成、保存或复制协议 credential。

第一次读取执行可重入迁移:解析旧 quoteimage.devices.v1quoteimage.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;不能静默返回空列表并掩盖不可追踪的本地状态。

6. 固件开发

6.1 启动与任务模型

app_main 初始化默认 nvsnvs_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 队列,不再使用轮询。

6.2 BLE 与耗时操作边界

禁止在 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。

6.3 帧接收状态机

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 可以替换 RECEIVINGREADYREFRESHING 都不可覆盖。DATA 携带 generation、 transfer ID 和两字节 little-endian offset,offset 必须严格等于当前 received。CRC 使用 标准 reflected CRC-32/ISO-HDLC。断连只丢弃未提交的 RECEIVING;已提交帧继续按不可变 display job 完成,但不会向旧连接发送结果。

6.4 设备配置与恢复

默认 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 不做自动迁移。

6.5 电子纸驱动

参考 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、驱动、配对画面坐标、移动端 帧尺寸/旋转和协议版本,不能只修改其中一处。

6.6 电量估算

电量模块在 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 闪电字形替换 %;拔电后恢复百分号、告警符号或原始像素。

6.7 空闲省电

固件启用 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 无法区分充电和充满,不得从该信号派生充电阶段。

7. 配对、认证和安全边界

v2 安全边界建立在 BLE Secure Connections 和持久化 owner/bond 上,不再使用应用层 credential 或 HMAC:

  1. 仅未拥有且 VBUS 存在的设备生成六位 passkey;配对画面完整刷新成功后才开放 VBUS-scoped setup session,并在 scan response 中加入本 session 的随机 8 字节 setup token;只要该 VBUS-high cycle 持续有效且未锁定,setup 不会因计时自动关闭;
  2. NimBLE 使用 DisplayOnly、SC-only、MITM、bonding、Security Mode 1 Level 3 和 16 字节 key;legacy SMP 关闭;
  3. 每条连接在 15 秒内完成系统 pairing/encryption,否则按 exact connection generation 终止;
  4. 固件确认 pairing complete 与 encryption changed 两个事件、审计 bond 确实落盘,再把 resolved peer identity 写为唯一 owner;
  5. 后续连接必须同时匹配系统 bond、owner identity 和当前 generation,才可访问任何 characteristic;设备最多一个连接、一个 bond;
  6. UNPAIR 的 RECONNECT_REQUIRED indication 得到确认后删除 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。

8. BLE 协议维护规则

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,不依赖 当前内置包。

修改协议时至少同步:

  1. docs/ble-protocol.md
  2. OTA 变更还包括 docs/ota-protocol.md
  3. apps/mobile/lib/core/ble/quote_protocol.dartcore/ota/
  4. apps/mobile/lib/core/ble/quote_ble_client.dart
  5. firmware/quote0/esp-idf/main/quote_protocol.cquote_ota_protocol.cquote_ble.c
  6. 两端协议测试和已知向量;
  7. 兼容性、升级顺序和版本迁移说明。

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 失败后的降级会把链路 安全故障变成可利用的绕过,应作为发布阻断问题。

9. 测试策略

9.1 移动端自动测试

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 预览覆盖 和六位系统配对流程。

9.2 固件宿主测试

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 的溢出边界。

9.3 必须保留的真机测试

自动测试无法覆盖以下项目,发布前应使用真实 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 稳定性。

10. 固件部署与设备恢复

激活 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 占用的地址在双槽布局中会变成 otadatanvs_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 需要用户在系统蓝牙设置中忽略设备。

11. 调试方法

11.1 macOS 广播诊断

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 的配对、发送、改名和 解除配对测试。

11.2 分层定位

现象 优先检查
完全扫描不到 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 衰减和万用表校准

12. 构建与发布

开发构建:

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 工具链并归档可复现的固件构建产物;
  • 对出厂设备执行配对、显示、电量和恢复的硬件验收。

13. 变更检查清单

提交前至少执行:

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 签名、版本号、安装升级和权限回归

新增功能应同时更新相应用户行为文档。若实现与文档不一致,以修正代码或文档并补充 测试的方式解决,不要依赖只有维护者知道的隐含约定。