Files
duooomi-android-sdk/.wiki/tech-architecture.md

160 lines
7.4 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.
# 技术架构duooomi-android-sdk
## 总体分层
```
sdk/src/main/kotlin/com/duooomi/ble/
├── DuooomiBleSDK.kt # 公开 API 入口state + 业务编排)
├── DuooomiBleConfig.kt # 不可变配置
├── core/ # BleClient / BleTypes / 错误枚举
├── protocol/ # 帧格式 / UUID / ProtocolManager
├── services/ # 协议层 RPC + 持久化 + 共享 HTTP
│ ├── BleProtocolService.kt # 协议层(帧组包/拆包,与平台无关)
│ ├── DeviceInfoService.kt # 设备命令getDeviceInfo / bind / setPlayMode 等)
│ ├── FileTransferService.kt # 文件传输
│ ├── AniConverter.kt # ANI 转换HTTP
│ ├── ShipmentSnDeviceService.kt # 软绑/软解 HTTP
│ ├── FirmwareUpgradeService.kt # checkUpgrade / report-success / report-failure
│ ├── UpgradeRecordStore.kt # SharedPreferences 持久化升级记录
│ └── HttpHelper.kt # 共享 POST 工具(统一 header / unwrap / 错误剥离)
├── models/
│ ├── DeviceInfo.kt # 含可选 PlayMode? loop
│ ├── PlayMode.kt # SINGLE=0 / LOOP=1
│ ├── BindingResponse.kt
│ ├── UnbindResponse.kt
│ ├── DeleteFileResponse.kt
│ ├── PrepareTransferResponse.kt
│ ├── VersionInfo.kt
│ ├── FirmwareUpgradeCheckResult.kt # 含 TargetFirmware (id/version/fileUrl/forceUpgrade...)
│ └── UpgradeRecord.kt # 含 UpgradeOutcomesuccess / version_mismatch
└── utils/
├── BleLogger.kt
└── BeijingTimeProvider.kt # 三级 fallback 时间获取
```
## OTA 三段式编排(核心架构)
`bind` 改瘦 + `unbind` 改胖 + `fetchLatestFirmware` 串三步,是与 expo / iOS HEAD 1:1 对齐的核心。**不要回退到"bind 一键搞定全套"的旧设计**。
```
┌──────────────────────────────────────────────────────────────┐
│ 集成方调用流 │
└──────────────────────────────────────────────────────────────┘
connect(deviceId)
└─ BLE 连接 + 启动协议监听
bind(userId) ← 只做 BLE 硬绑 + getVersion
├─ sendAndWait(BIND_DEVICE) payload = {type, userId, time}
├─ 写入 sdk.sn / sdk.isActivated
└─ runCatching { getVersion() } // 失败不阻断
fetchLatestFirmware(sn, currentVersion, userId) ← 三步编排
├─ Step 1: tryReportPendingUpgrade(...) (fire-and-forget)
│ └─ 把本地 pending UpgradeRecord 报给服务端
├─ Step 2: shipmentService.bind(sn, userId) 软绑
│ └─ 失败 → 自动硬解回滚 + 清 sn / isActivated
└─ Step 3: firmwareUpgradeService.checkUpgrade(sn, currentVersion)
└─ 写 sdk.firmwareUpgrade
upgradeFirmware(sn, fileUrl, firmwareId, fromVersion, targetVersion)
├─ transferFile(OTA_PACKAGE)
└─ upgradeRecordStore.upsert(record) // 落记录等下次对账
unbind(userId) ← 硬解 + 软解(容错)
├─ 缓存 cachedSn = sdk.sn
├─ sendAndWait(UNBIND_DEVICE)
├─ 写 sdk.isActivated = false / sdk.sn = ""
└─ if cachedSn 非空 → shipmentService.unbind(...)(失败仅 warn
```
## 状态清理时机表
**严格对齐 iOS HEAD**。新增清理点前先确认 iOS 是否同步清。
| 状态字段 | 写入时机 | 清理时机 |
|---|---|---|
| `sn` | bind 成功BindingResponse.sn | unbind 成功fetchLatestFirmware 软绑失败回滚 |
| `firmwareUpgrade` | fetchLatestFirmware 成功 | **从不主动清** |
| `isActivated` | bind 成功(true) / unbind 成功(false) / 软绑失败回滚(false) | disconnect handler / 主动 disconnect |
| `version` | getVersion / bind | disconnect handler / 主动 disconnect |
| `deviceInfo` | getDeviceInfo / setPlayMode 后自动刷新 | disconnect handler / 主动 disconnect |
| `connectedDevice` | connect 成功 | disconnect handler / 主动 disconnect |
| `transferProgress` | transferFile 进度回调 | (持续写,无显式清) |
| `upgradeRecords` | upsert / markReportedstore.onChange 推) | (持久化,不清) |
⚠️ **关键决策**`sn``firmwareUpgrade` 在 disconnect 时**不清空**。理由:
- `sn` 是设备身份,断线重连同设备时还有效,避免每次重连后丢失 OTA 上下文
- `firmwareUpgrade` 是上次拉取结果UI 可继续展示直到下次 fetchLatestFirmware 覆盖
之前 Android 这两个字段在 disconnect 时被清,与 iOS 不一致commit `0dbfab9` 修正。
## sn 级 in-flight 互斥锁UpgradeRecordStore
`tryReportPendingUpgrade` 是 fire-and-forget。如果 demo 短时间内连续两次调 `fetchLatestFirmware`(用户狂点),会出现两个协程并行处理同一 sn 的 pending records 重复 markReported。
**方案**sn 级互斥(不是全局锁)。
```kotlin
fun tryAcquire(sn: String): Boolean = synchronized(lock) {
if (inFlight.contains(sn)) return false // 后到的协程直接放弃
inFlight.add(sn); true
}
fun release(sn: String) = synchronized(lock) { inFlight.remove(sn) }
```
调用方约定:
```kotlin
if (!upgradeRecordStore.tryAcquire(sn)) return
scope.launch {
try { /* 上报逻辑 */ } finally { upgradeRecordStore.release(sn) }
}
```
> 不同 sn 的并发上报互不干扰;这点比单全局锁更细粒度,符合多设备场景。
## BeijingTimeProvider 三级 fallback
bind / unbind / setPlayMode payload 必带 `time` 字段(北京时间 ISO 8601
```
fetch():
if 5s 缓存命中 → return
Mutex.withLock:
1. GET https://quan.suning.com/getSysTime.do (timeout=3s)
→ JSON body { sysTime2 / sysTime1 } → 解析 ISO 8601
2. response Date header (RFC 1123) → 转北京时间
3. fallback: System.currentTimeMillis() + 8h offset
写缓存 + 返回
```
`Mutex.withLock` 起单飞作用 —— 多协程同时 fetch 只有第一个真请求,其余等锁释放后命中缓存。
## HTTP 约定
所有 RPC 走 `HttpHelper.post()`
- 必带 `x-api-key` + `x-owner` header来自 `DuooomiBleConfig`
- 响应 `{ success: true, data: {...} }` 包装 → unwrap 后取 `data` 字段unwrap=true默认
- 错误:剥离 `API error:` 前缀 + 尝试从 JSON 抽 `message` 字段
-`HttpHelper.HttpError(code, message)` —— Service 层负责转成 `DuooomiBleError.SoftBindFailed` / `TransferFailed`
## Demo 架构
```
MainActivity
├─ 持有 mutableStateOf<DuooomiBleSDK>(支持 reload
├─ SharedPreferences 持久化 owner / apiKey / scanPrefix
└─ 提供 onReload 回调 → 旧 SDK fire-and-forget disconnect + 新建实例
DemoScreen(sdk, config, onReload)
└─ 通过 sdk.xxx.collectAsState() 订阅 SDK StateFlow
reload 时 sdk 引用变化 → collectAsState 重新订阅新实例
```
⚠️ **DemoSecrets 文件**
- `demo/src/main/kotlin/com/duooomi/ble/demo/DemoSecrets.kt` —— 真实秘钥,**gitignored**
- `docs/DemoSecrets.template.kt` —— 模板,**编译路径外**(不放在 demo/src 否则 redeclare 冲突)
来源commit `b439851` / `2dde274` / `0dbfab9` / 复刻对话 2026-05-08。