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:
@@ -33,6 +33,13 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
private val _version = MutableStateFlow("")
|
||||
val version: StateFlow<String> = _version.asStateFlow()
|
||||
|
||||
/**
|
||||
* 设备返回的 `log` 字段(任意 JSON 对象)。`getVersion` 成功时写入,断连 / 解绑 / 软绑回滚时清空。
|
||||
* `bind` 软绑成功后透传给 shipment-sn `update-metadata`,集成方一般无需直接读。
|
||||
*/
|
||||
private val _versionLog = MutableStateFlow<JSONObject?>(null)
|
||||
val versionLog: StateFlow<JSONObject?> = _versionLog.asStateFlow()
|
||||
|
||||
private val _isActivated = MutableStateFlow(false)
|
||||
val isActivated: StateFlow<Boolean> = _isActivated.asStateFlow()
|
||||
|
||||
@@ -107,12 +114,13 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
_connectedDevice.value = null
|
||||
_deviceInfo.value = null
|
||||
_version.value = ""
|
||||
_versionLog.value = null
|
||||
_isActivated.value = false
|
||||
_btState.value = ConnectionState.DISCONNECTED
|
||||
protocolService.stopListening()
|
||||
cancelAllPending(DuooomiBleError.NotConnected)
|
||||
// 与 iOS 一致:sn / firmwareUpgrade 不在断开时重置
|
||||
// —— sn 仅在 unbind 成功 / fetchLatestFirmware 软绑失败回滚时清空
|
||||
// —— sn 仅在 unbind 成功 / bind 软绑失败回滚时清空
|
||||
// —— firmwareUpgrade 仅在 fetchLatestFirmware 成功时覆盖
|
||||
}
|
||||
}
|
||||
@@ -206,6 +214,7 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
_connectedDevice.value = null
|
||||
_deviceInfo.value = null
|
||||
_version.value = ""
|
||||
_versionLog.value = null
|
||||
_isActivated.value = false
|
||||
_btState.value = ConnectionState.DISCONNECTED
|
||||
BleLog.i("Disconnected", "Connect")
|
||||
@@ -241,23 +250,32 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
}
|
||||
val info = VersionInfo.fromJson(parseJsonResponse(data))
|
||||
_version.value = info.version
|
||||
BleLog.i("Version received: ${info.version} (type=${info.type})", "Command")
|
||||
_versionLog.value = info.log
|
||||
BleLog.i(
|
||||
"Version received: ${info.version} (type=${info.type}, log=${if (info.log != null) "present" else "nil"})",
|
||||
"Command"
|
||||
)
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬绑:BLE 协议层把当前用户绑定到设备(命令 0x0F),完成后同步拉一次版本号。
|
||||
* 完整绑定:内部串四步业务编排(顺序固定),对集成方表现为一次原子调用。
|
||||
*
|
||||
* **作用域**:只做 BLE 通道的绑定 + [getVersion]。
|
||||
* 软绑 / 升级对账上报 / 拉最新固件 → 全部移到 [fetchLatestFirmware] 内部串。
|
||||
* 1. **BLE 硬绑**(命令 0x0F):写 [sn] / [isActivated]
|
||||
* 2. **getVersion**:写 [version] / [versionLog](log 为任意 JSON 对象,缺失则 null)
|
||||
* 3. **软绑**:POST `/api/auth/loomart/shipment-sn/device/bind`,登记云端绑定关系
|
||||
* - 软绑失败 → 自动硬解绑(BLE 0x12)回滚 + 重置 `isActivated`/`sn`/`versionLog`,bind 整体失败
|
||||
* 4. **updateMetadata**(fire-and-forget):把 `versionLog` 透传给 `shipment-sn/device/update-metadata`
|
||||
* - 失败只 warn 不阻断;versionLog 为 null / 空对象则跳过
|
||||
*
|
||||
* 完成后:
|
||||
* - [sn] 写入设备序列号
|
||||
* - [version] 写入当前固件版本(getVersion 失败时保持空,但 bind 不算失败)
|
||||
* - [version] / [versionLog] 写入当前设备状态(getVersion 失败时保持空,但 bind 不算失败)
|
||||
* - [isActivated] = true
|
||||
*
|
||||
* @throws DuooomiBleError.NotConnected 未连接
|
||||
* @throws DuooomiBleError.AlreadyBoundByOtherUser BLE 层 success != 1(设备已绑给他人)
|
||||
* @throws DuooomiBleError.SoftBindFailed 软绑失败(含硬解绑回滚后)
|
||||
*/
|
||||
suspend fun bind(userId: String): BindingResponse {
|
||||
ensureConnected()
|
||||
@@ -274,8 +292,43 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
}
|
||||
_sn.value = resp.sn
|
||||
BleLog.i("Hard bind success: sn=${resp.sn}", "Command")
|
||||
// 同步拉版本,让后续 fetchLatestFirmware 的对账上报有 currentVersion 可用
|
||||
|
||||
// Step 2: 同步拉版本(写 version / versionLog 供软绑后的 updateMetadata 透传)
|
||||
runCatching { getVersion() }
|
||||
|
||||
// Step 3: 软绑(失败 → 硬解绑回滚 + 清状态 + 整体失败)
|
||||
val bindSn = resp.sn
|
||||
BleLog.d("Soft bind start: sn=$bindSn", "Command")
|
||||
try {
|
||||
shipmentService.bind(sn = bindSn, bindUserId = userId)
|
||||
} catch (e: Exception) {
|
||||
BleLog.e("Soft bind failed: ${e.message}; rolling back hard unbind", "Command")
|
||||
runCatching {
|
||||
deviceInfoService.unbindDevice(userId)
|
||||
}.onFailure { rb ->
|
||||
BleLog.w("Hard unbind rollback failed (ignored): ${rb.message}", "Command")
|
||||
}
|
||||
_isActivated.value = false
|
||||
_sn.value = ""
|
||||
_versionLog.value = null
|
||||
throw e
|
||||
}
|
||||
BleLog.i("Soft bind success: sn=$bindSn", "Command")
|
||||
|
||||
// Step 4: updateMetadata(fire-and-forget;versionLog 缺失/空则跳过)
|
||||
val log = _versionLog.value
|
||||
if (log != null && log.length() > 0) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
shipmentService.updateMetadata(sn = bindSn, data = log)
|
||||
}.onSuccess {
|
||||
BleLog.i("updateMetadata success: sn=$bindSn", "Command")
|
||||
}.onFailure { e ->
|
||||
BleLog.w("updateMetadata failed (ignored): ${e.message}", "Command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -328,6 +381,7 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
}
|
||||
_isActivated.value = false
|
||||
_sn.value = ""
|
||||
_versionLog.value = null
|
||||
BleLog.i("Hard unbind success", "Command")
|
||||
if (cachedSn.isNotEmpty()) {
|
||||
runCatching {
|
||||
@@ -469,12 +523,12 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取服务端最新固件信息。**内部串三步业务编排**(顺序固定):
|
||||
* 拉取服务端最新固件信息。**内部串两步业务编排**(顺序固定):
|
||||
*
|
||||
* 1. **对账上报**:把本地 pending [UpgradeRecord] 逐条报给服务端(容错失败,不阻断)
|
||||
* 2. **软绑**:POST `/api/auth/loomart/shipment-sn/device/bind`,登记云端绑定关系
|
||||
* - 软绑失败 → 自动硬解绑(BLE 0x12)回滚 + 重置 `isActivated`/`sn`,整个 fetch 失败
|
||||
* 3. **拉服务端最新固件**:写入 [firmwareUpgrade],通过返回值回传
|
||||
* 2. **拉服务端最新固件**:写入 [firmwareUpgrade],通过返回值回传
|
||||
*
|
||||
* 软绑 / metadata 上报 / 硬绑失败回滚 → 已全部内聚到 [bind],本方法不再处理。
|
||||
*/
|
||||
suspend fun fetchLatestFirmware(
|
||||
sn: String,
|
||||
@@ -487,25 +541,7 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
|
||||
// Step 1: 对账上报历史 OTA 记录(容错失败,sn 级 in-flight 互斥)
|
||||
tryReportPendingUpgrade(sn = sn, currentVersion = currentVersion, userId = userId)
|
||||
|
||||
// Step 2: 软绑(shipment-sn HTTP)
|
||||
BleLog.d("Soft bind start: sn=$sn", "Firmware")
|
||||
try {
|
||||
shipmentService.bind(sn = sn, bindUserId = userId)
|
||||
} catch (e: Exception) {
|
||||
BleLog.e("Soft bind failed during fetchLatestFirmware: ${e.message}; rolling back hard unbind", "Firmware")
|
||||
// 硬解绑回滚(fire-and-forget;BLE ack 不影响本次 fetch 错误回传)
|
||||
runCatching {
|
||||
deviceInfoService.unbindDevice(userId)
|
||||
}.onFailure { rb ->
|
||||
BleLog.w("Hard unbind rollback failed (ignored): ${rb.message}", "Firmware")
|
||||
}
|
||||
_isActivated.value = false
|
||||
_sn.value = ""
|
||||
throw e
|
||||
}
|
||||
BleLog.i("Soft bind success: sn=$sn", "Firmware")
|
||||
|
||||
// Step 3: 服务端 checkUpgrade
|
||||
// Step 2: 服务端 checkUpgrade
|
||||
val result = firmwareUpgradeService.checkUpgrade(sn = sn, currentVersion = currentVersion)
|
||||
_firmwareUpgrade.value = result
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user