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

175 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计稿:可配置蓝牙帧间隔
- **日期**: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:201``delay(30)` —— GATT 忙的写重试退避,语义与帧间隔无关,本次不动
- 运行时可变 setter(YAGNI)
- ANI / 连接 / 超时等其他参数的配置化
## 设计
### 1. API 形态
**iOS** `DuooomiBleConfig.swift`:
```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`:
```kotlin
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.035``Double(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) |