Files
duooomi-ios-sdk/docs/superpowers/specs/2026-05-20-frame-interval-config-design.md
km2023 b967471532 docs(spec): 帧间隔 frameIntervalMs 配置化设计稿
设计 DuooomiBleConfig.frameIntervalMs(默认 35,clamp 到 [0,500])。
含跨端契约对齐项、demo 改动清单、死代码清理计划、决策记录。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:54:03 +08:00

6.8 KiB
Raw Blame History

设计稿:可配置蓝牙帧间隔

  • 日期:2026-05-20
  • 范围:duooomi-ios-sdk · duooomi-android-sdk(权威上游 iOS,Android 1:1 跟随)
  • 状态:待实现

背景

BleProtocolService.sendFrames 在分包后逐帧 writeWithoutResponse,帧间停顿固定 35ms,防止 GATT TX 队列拥塞导致丢帧。

当前实现两端不对齐:

位置 现状
iOS Protocol/ProtocolConstants.swift:20 static let frameIntervalNanos: UInt64 = 35_000_000 —— 定义存在但全仓无任何引用(死代码)
iOS Services/BleProtocolService.swift:94 实际使用字面量 0.035(magic number)
Android protocol/ProtocolConstants.kt:23 const val FRAME_INTERVAL_MS = 35L
Android services/BleProtocolService.kt:74 delay(FrameConstants.FRAME_INTERVAL_MS) —— 正确引用

集成方现在没有任何途径调节该值。

目标

  1. 暴露帧间隔为 DuooomiBleConfig 的字段,默认 35,集成方可在 SDK 初始化时覆盖
  2. 修复 iOS 死代码 + 字面量两个不一致点,改为读 config
  3. demo 提供 UI 控件,与现有 apiKey / scanNamePrefix 同套持久化机制
  4. 两端字段名、默认值、越界文案逐字一致

非目标:

  • Android BleClient.kt:201delay(30) —— GATT 忙的写重试退避,语义与帧间隔无关,本次不动
  • 运行时可变 setter(YAGNI)
  • ANI / 连接 / 超时等其他参数的配置化

设计

1. API 形态

iOS DuooomiBleConfig.swift:

public init(
    apiHost: URL = URL(string: "https://api.duooomi.com")!,
    apiKey: String,
    brand: String,
    cdnHost: String = "https://cdn.bowong.cc/",
    aniWidth: Int = 360,
    aniHeight: Int = 360,
    aniFps: String = "24",
    scanNamePrefix: String,
    frameIntervalMs: Int = 35       // ← 新增
)

public let frameIntervalMs: Int     // ← 新增,存储原始值

Android DuooomiBleConfig.kt:

data class DuooomiBleConfig(
    val apiKey: String,
    val brand: String,
    val scanNamePrefix: String,
    val apiHost: String = "https://api.duooomi.com",
    val cdnHost: String = "https://cdn.bowong.cc/",
    val aniWidth: Int = 360,
    val aniHeight: Int = 360,
    val aniFps: String = "24",
    val frameIntervalMs: Int = 35    // ← 新增
)

2. 校验策略

  • 合法区间:[0, 500] ms
  • 越界:BleLog.w("frameIntervalMs=<value> out of [0,500], will clamp at use site", "Config"),不抛错
  • 存储:原始值(便于诊断),使用点 coerceIn(0, 500) / min(max(...))
  • 校验时机:Config 构造完成时(iOS 在 init 末尾、Android 在 init {} 块)

警告字符串两端逐字一致(集成方过滤日志时跨端一致)。

3. 消费路径

BleProtocolService 改为构造期接收 DuooomiBleConfig(或仅 frameIntervalMs 一个 Int)。

sendFrames 帧间 sleep 改动
iOS 0.035Double(min(max(config.frameIntervalMs, 0), 500)) / 1000.0(秒)
Android delay(FrameConstants.FRAME_INTERVAL_MS)delay(config.frameIntervalMs.coerceIn(0, 500).toLong())

4. 死代码清理

  • iOS:删除 Protocol/ProtocolConstants.swift 中的 frameIntervalNanos 常量
  • Android:删除 protocol/ProtocolConstants.kt 中的 FRAME_INTERVAL_MS 常量
  • 删除前 grep 二次确认无其他引用(本次设计前 grep 已确认)

5. Demo 改动

改动
iOS demo/Sources/DemoApp.swift configFrameIntervalMs 状态字段 + 数字 TextField;与现有 apiKey/scanPrefix 一致地传给 DuooomiBleConfig(...);沿用本仓 demo 既有的持久化方式,不新增机制
Android demo/.../DemoScreen.kt + MainActivity.kt ConfigField("frameIntervalMs", configFrameIntervalMs) { configFrameIntervalMs = it };DemoConfig data class 加字段;SharedPreferences 增加 KEY_FRAME_INTERVAL_MS

Demo 输入按文本 → 解析为 Int → 解析失败回退 35;空字符串当 35。

6. 跨端契约对齐(本次必须 1:1)

字段名 frameIntervalMs(两端)
默认值 35
合法区间 [0, 500] 闭区间
越界 warn 字符串 "frameIntervalMs=<X> out of [0,500], will clamp at use site"
越界处置 warn + 使用点 clamp,不抛错
配置时机 构造期一次性,无运行时 setter

7. 错误处理

无新增 DuooomiBleError case。出范围只是 warn,不影响 catch 分支。

8. 测试

单元(可加):

  • Config 接受 0 / 35 / 500 不告警
  • Config 接受 -1 / 501 告警(若无日志捕获脚手架则跳过,本次不补脚手架)

手动冒烟:

  • demo 设置为 100,触发一次 transferMedia,观察传输耗时约为基线的 ~3 倍
  • demo 设置为 0,触发传输,验证无 crash(可能伴随设备端丢帧 —— 预期行为,集成方自负)

自动化缺口(诚实标注):

  • 两端均无 BLE timing 自动化测试基础设施,本次不补

9. 兼容性 / 破坏性

  • 默认值 35 与现状一致 → 非破坏性
  • 新增参数为 init 末位且有默认值,旧调用方代码不需改动
  • iOS 删除 frameIntervalNanos 常量:public 但 grep 确认零外部引用,理论上仍是破坏性。风险等级:理论破坏 / 实际无影响。如有顾虑可在版本说明里标注

实现顺序建议

  1. iOS Config + BleProtocolService + 删除死代码 + demo
  2. iOS 编译 + 手动冒烟
  3. Android Config + BleProtocolService + 删除常量 + demo
  4. Android debug + release 编译
  5. 两端各一个 commit,提 main(沿用本仓既有 feat(sdk): 直接提 main 的工作流)
  6. 推送

提交信息草稿

iOS commit:

feat(sdk): 帧间隔 frameIntervalMs 配置化(默认 35ms[0,500] clamp

DuooomiBleConfig 新增 frameIntervalMsBleProtocolService 改读 config。
顺手清死代码 frameIntervalNanos 常量、修 0.035 字面量。
demo 加可调输入框UserDefaults 持久化。

Android commit:

feat(sdk): 同步 iOS — 帧间隔 frameIntervalMs 配置化(默认 35ms

1:1 复刻 iOSConfig 加 frameIntervalMsBleProtocolService 改读 config
清掉 FRAME_INTERVAL_MS 常量。demo 加 ConfigFieldSharedPreferences 持久化。
警告文案与 iOS 逐字一致。

决策记录

决策 选项 选了 原因
范围 仅帧间隔 / 帧间隔+写重试 仅帧间隔 30ms retry 是 Android 独有的 GATT busy 退避,语义不同;用户明确确认
越界处置 不校验 / clamp+warn / clamp+throw clamp+warn 用户明确选择;不堵塞集成方但留下诊断信号
Config vs 运行时 setter Config / setter / 双 仅 Config YAGNI;用户没要求运行时可变
范围与覆盖 与现有常量共存 / 取代 取代 + 删常量 单一来源,顺手清死代码
Spec 落地仓 iOS / Android / 各一份 iOS iOS 是上游权威(项目 CLAUDE.md)