feat(sdk): 绑定时上报设备 log 到 update-metadata 并内聚双层绑定
1:1 复刻 iOS commit 4bc8fdb。 - bind(userId) 内聚为硬绑 + getVersion + 软绑 + updateMetadata 一次原子调用, 软绑失败自动硬解绑回滚 + 清状态;集成方无需感知双层绑定 - fetchLatestFirmware 简化为对账上报 + checkUpgrade 两步,不再重复软绑 - VersionInfo 增加 log: JSONObject?,fromJson 用 optJSONObject 解析; SDK 暴露只读 versionLog: StateFlow<JSONObject?>,断连/解绑/软绑回滚时清空 - ShipmentSnDeviceService 新增 updateMetadata(sn, data: JSONObject), HttpHelper.post 已支持 JSONObject 作为 body 子节点透传 - 老固件不返回 log → versionLog == null → 自动跳过 updateMetadata,前向兼容 - 同步更新 .wiki/tech-architecture.md(状态清理表 + OTA 编排图) + product-decisions.md(新增决策条目)+ README.md(versionLog 状态) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
**影响**:集成方升级时构造函数签名报错,必须显式传两个参数。可接受,因为 demo / iOS / Harmony 都同步改了。
|
||||
|
||||
## bind 改瘦 + fetchLatestFirmware 串三步
|
||||
## bind 改瘦 + fetchLatestFirmware 串三步(已废弃,见下条)
|
||||
|
||||
**决策时间**:2026-05-08(commit `2dde274`)
|
||||
|
||||
@@ -27,6 +27,28 @@
|
||||
|
||||
**集成方迁移指南**:bind 之后必须显式调 `fetchLatestFirmware(sn, currentVersion, userId)` 才能拿到固件信息和触发对账。
|
||||
|
||||
## bind 内聚双层绑定 + updateMetadata 上报
|
||||
|
||||
**决策时间**:2026-05-18(同步 iOS commit `4bc8fdb` 复刻)
|
||||
|
||||
把软绑 + updateMetadata 从 `fetchLatestFirmware` 重新搬回 `bind` 内部,集成方一次调用完成"完整绑定"。
|
||||
|
||||
| 阶段 | 上一版(2026-05-08) | 本版(2026-05-18) |
|
||||
|---|---|---|
|
||||
| `bind(userId)` | BLE 硬绑 + getVersion | BLE 硬绑 + getVersion + **软绑** + **updateMetadata** + 失败回滚 |
|
||||
| `fetchLatestFirmware(sn, currentVersion, userId)` | 对账 + 软绑 + checkUpgrade | 对账 + checkUpgrade(不再软绑) |
|
||||
|
||||
**原因**:
|
||||
- 软绑是"绑定"语义的一部分,集成方不该感知双层之分(与 iOS HEAD 对齐)
|
||||
- updateMetadata 在 bind 时 versionLog 刚新鲜,语义上"绑定时一并上报状态"更连贯
|
||||
- 上一版"bind 改瘦"是过度拆分;事实证明拆开反而让集成方容易遗漏软绑
|
||||
|
||||
**集成方迁移指南**:bind 调用方式不变,但**不再需要**调 `fetchLatestFirmware` 才完成软绑。检查更新仍调 `fetchLatestFirmware`,但它只做对账+checkUpgrade。
|
||||
|
||||
**新增 SDK 状态**:`versionLog: StateFlow<JSONObject?>` —— 设备 BLE getVersion 响应中 `log` 字段(任意 JSON 对象,老固件不返回则 null)。bind 时 fire-and-forget 透传到 `POST /api/auth/loomart/shipment-sn/device/update-metadata`。**前向兼容**:老固件 versionLog == null → 自动跳过 updateMetadata。
|
||||
|
||||
**JSON object 选型**:直接用 `org.json.JSONObject?` 透传(不解析、不转 Map),HttpHelper.post 已支持 `JSONObject` 作为 body 子节点序列化。对应 iOS 的 `[String: Any]?` 方案。
|
||||
|
||||
## disconnect 时不清 sn / firmwareUpgrade
|
||||
|
||||
**决策时间**:2026-05-08(commit `0dbfab9`)
|
||||
|
||||
@@ -32,9 +32,9 @@ sdk/src/main/kotlin/com/duooomi/ble/
|
||||
└── BeijingTimeProvider.kt # 三级 fallback 时间获取
|
||||
```
|
||||
|
||||
## OTA 三段式编排(核心架构)
|
||||
## OTA 编排(核心架构 · 2026-05-18 重构)
|
||||
|
||||
`bind` 改瘦 + `unbind` 改胖 + `fetchLatestFirmware` 串三步,是与 expo / iOS HEAD 1:1 对齐的核心。**不要回退到"bind 一键搞定全套"的旧设计**。
|
||||
`bind` 内聚(硬绑+getVersion+软绑+updateMetadata)+ `fetchLatestFirmware` 瘦身(对账+checkUpgrade)+ `unbind` 改胖(硬解+软解),与 iOS HEAD 1:1 对齐。
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
@@ -44,17 +44,21 @@ sdk/src/main/kotlin/com/duooomi/ble/
|
||||
connect(deviceId)
|
||||
└─ BLE 连接 + 启动协议监听
|
||||
|
||||
bind(userId) ← 只做 BLE 硬绑 + getVersion
|
||||
├─ sendAndWait(BIND_DEVICE) payload = {type, userId, time}
|
||||
├─ 写入 sdk.sn / sdk.isActivated
|
||||
└─ runCatching { getVersion() } // 失败不阻断
|
||||
bind(userId) ← 完整绑定(4 步内部编排,对集成方原子)
|
||||
├─ Step 1: verifyBrand
|
||||
├─ Step 2: sendAndWait(BIND_DEVICE) BLE 硬绑 0x0F
|
||||
│ └─ 写 sdk.sn / sdk.isActivated
|
||||
├─ Step 3: runCatching { getVersion() } // 失败不阻断
|
||||
│ └─ 写 sdk.version / sdk.versionLog
|
||||
├─ Step 4: shipmentService.bind(sn, userId) 软绑
|
||||
│ └─ 失败 → 自动硬解回滚 + 清 sn/isActivated/versionLog → 整体失败抛 SoftBindFailed
|
||||
└─ Step 5: scope.launch { updateMetadata(sn, versionLog) } (fire-and-forget)
|
||||
└─ 仅当 versionLog 非 null 且非空对象;失败仅 warn
|
||||
|
||||
fetchLatestFirmware(sn, currentVersion, userId) ← 三步编排
|
||||
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)
|
||||
└─ Step 2: firmwareUpgradeService.checkUpgrade(sn, currentVersion)
|
||||
└─ 写 sdk.firmwareUpgrade
|
||||
|
||||
upgradeFirmware(sn, fileUrl, firmwareId, fromVersion, targetVersion)
|
||||
@@ -64,7 +68,7 @@ upgradeFirmware(sn, fileUrl, firmwareId, fromVersion, targetVersion)
|
||||
unbind(userId) ← 硬解 + 软解(容错)
|
||||
├─ 缓存 cachedSn = sdk.sn
|
||||
├─ sendAndWait(UNBIND_DEVICE)
|
||||
├─ 写 sdk.isActivated = false / sdk.sn = ""
|
||||
├─ 写 sdk.isActivated = false / sdk.sn = "" / sdk.versionLog = null
|
||||
└─ if cachedSn 非空 → shipmentService.unbind(...)(失败仅 warn)
|
||||
```
|
||||
|
||||
@@ -74,10 +78,11 @@ unbind(userId) ← 硬解 + 软解(容错)
|
||||
|
||||
| 状态字段 | 写入时机 | 清理时机 |
|
||||
|---|---|---|
|
||||
| `sn` | bind 成功(BindingResponse.sn) | unbind 成功;fetchLatestFirmware 软绑失败回滚 |
|
||||
| `sn` | bind 硬绑成功(BindingResponse.sn) | unbind 成功;**bind 软绑失败回滚** |
|
||||
| `firmwareUpgrade` | fetchLatestFirmware 成功 | (**从不主动清**) |
|
||||
| `isActivated` | bind 成功(true) / unbind 成功(false) / 软绑失败回滚(false) | disconnect handler / 主动 disconnect |
|
||||
| `isActivated` | bind 硬绑成功(true) / unbind 成功(false) / **bind 软绑失败回滚(false)** | disconnect handler / 主动 disconnect |
|
||||
| `version` | getVersion / bind | disconnect handler / 主动 disconnect |
|
||||
| `versionLog` | getVersion 成功(log 非 null) | unbind 成功 / **bind 软绑失败回滚** / disconnect handler / 主动 disconnect |
|
||||
| `deviceInfo` | getDeviceInfo / setPlayMode 后自动刷新 | disconnect handler / 主动 disconnect |
|
||||
| `connectedDevice` | connect 成功 | disconnect handler / 主动 disconnect |
|
||||
| `transferProgress` | transferFile 进度回调 | (持续写,无显式清) |
|
||||
@@ -89,6 +94,8 @@ unbind(userId) ← 硬解 + 软解(容错)
|
||||
|
||||
之前 Android 这两个字段在 disconnect 时被清,与 iOS 不一致;commit `0dbfab9` 修正。
|
||||
|
||||
⚠️ **2026-05-18 重构**:把软绑从 `fetchLatestFirmware` 搬到 `bind` 内部。理由:软绑是"绑定"语义的一部分,不该让"检查更新"承担。同步新增 `versionLog` 状态 + `updateMetadata` 上报。详见 `product-decisions.md`。
|
||||
|
||||
## sn 级 in-flight 互斥锁(UpgradeRecordStore)
|
||||
|
||||
`tryReportPendingUpgrade` 是 fire-and-forget。如果 demo 短时间内连续两次调 `fetchLatestFirmware`(用户狂点),会出现两个协程并行处理同一 sn 的 pending records 重复 markReported。
|
||||
|
||||
Reference in New Issue
Block a user