From a15403ae3656f203ff1b5c331af331bb101e7c86 Mon Sep 17 00:00:00 2001 From: km2023 Date: Tue, 19 May 2026 17:19:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20upgradeFirmware=20=E4=B8=8B?= =?UTF-8?q?=E5=8F=91=E5=89=8D=E6=A0=A1=E9=AA=8C=E7=94=B5=E9=87=8F=20?= =?UTF-8?q?=E2=89=A540%=EF=BC=8C=E4=B8=8D=E8=B6=B3=E6=8A=9B=20.batteryTooL?= =?UTF-8?q?ow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OTA 烧录前主动拉新鲜 getDeviceInfo 取实时电量,<40% 直接以 .batteryTooLow(current:required:) 失败、不发任何字节,防烧录中途断电变砖。 新增常量 minBatteryForUpgrade=40;烧录体抽出私有 performFirmwareTransfer。 README 补可用方法/属性汇总表。 Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 45 +++++++++++++++++++ Sources/DuooomiBleSDK/Core/BleTypes.swift | 3 ++ .../DuooomiBleSDK+Firmware.swift | 45 ++++++++++++++++++- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ea49c9..47db371 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,51 @@ sdk.connect(deviceId: device.id) { result in | `transferProgress` | `Int` | 传输进度 0-100 | | `error` | `String?` | 最近错误 | +### 汇总:可用方法与属性 + +> 一张表速查所有 public API。异步方法的失败分支均为 `Result` 的 `.failure(Error)`,`Error` 多为 `DuooomiBleError`,少数透传 `DecodingError` / `URLError`(详见各方法注释)。 + +#### 方法 + +| 方法 | 参数 | 返回 | 说明 | +|------|------|------|------| +| `scan()` | — | 无(`didUpdateDevices` 回调) | 开始扫描 | +| `stopScan()` | — | 无 | 停止扫描 | +| `connect(deviceId:completion:)` | `deviceId: String` | `Result` | 连接设备 | +| `disconnect(completion:)` | — | `Result` | 断开连接 | +| `getConnectedDevices()` | — | `[DiscoveredDevice]`(同步) | 当前已连接设备列表 | +| `bind(userId:completion:)` | `userId: String` | `Result` | 绑定 + 同步拉版本,成功后 `sn`/`version`/`isActivated` 就绪 | +| `unbind(userId:completion:)` | `userId: String` | `Result` | 解绑 | +| `getDeviceInfo(completion:)` | — | `Result` | 读品牌/电量/存储/loop,写入 `deviceInfo` | +| `getVersion(completion:)` | — | `Result` | 读固件版本,写入 `version`/`versionLog` | +| `setPlayMode(_:userId:completion:)` | `_ mode: PlayMode`, `userId: String` | `Result` | 切单播/循环;**须先 bind** | +| `deleteFile(key:completion:)` | `key: String` | `Result` | 删除设备文件 | +| `prepareTransfer(key:size:completion:)` | `key: String`, `size: Int` | `Result` | 传输前预协商(一般无需直调) | +| `transferMedia(fileUrl:completion:)` | `fileUrl: String` | `Result` | 一步:校验→ANI 转换→prepare→传输 | +| `transferFile(fileUri:commandType:completion:)` | `fileUri: String`, `commandType: CommandType = .transferAniVideo` | `Result` | 底层文件传输 | +| `fetchLatestFirmware(sn:currentVersion:userId:completion:)` | `sn: String`, `currentVersion: String`, `userId: String` | `Result` | 对账上报 + 拉最新固件,写入 `firmwareUpgrade` | +| `upgradeFirmware(sn:fileUrl:firmwareId:fromVersion:targetVersion:completion:)` | `sn`, `fileUrl`, `firmwareId`, `fromVersion`, `targetVersion`: `String` | `Result` | OTA 烧录;下发前校验实时电量,**< 40% 返回 `.batteryTooLow`,不发任何字节** | +| `tryReportPendingUpgrade(sn:currentVersion:userId:)` | `sn: String`, `currentVersion: String`, `userId: String` | 无(fire-and-forget,结果见 `upgradeRecords`) | 对账上报本地待报记录(`fetchLatestFirmware` 已内置调用) | + +#### 属性(除 `delegate` 外均只读) + +| 属性 | 类型 | 说明 | +|------|------|------| +| `delegate` | `DuooomiBleSDKDelegate?` | 事件回调代理(weak,可读写) | +| `config` | `DuooomiBleConfig` | 初始化配置(`let`) | +| `btState` | `ConnectionState` | idle / scanning / connecting / connected / disconnecting / disconnected | +| `discoveredDevices` | `[DiscoveredDevice]` | 扫描到的设备列表 | +| `connectedDevice` | `DiscoveredDevice?` | 当前连接设备 | +| `deviceInfo` | `DeviceInfo?` | 设备信息(品牌/电量/存储/loop) | +| `version` | `String` | 固件版本(bind 后就绪) | +| `versionLog` | `[String: Any]?` | 设备版本附带 JSON(透传给 metadata 上报) | +| `isActivated` | `Bool` | 是否已绑定 | +| `sn` | `String` | 设备序列号(bind 成功后就绪) | +| `transferProgress` | `Int` | 传输进度 0–100 | +| `error` | `String?` | 最近错误描述 | +| `firmwareUpgrade` | `FirmwareUpgradeCheckResult?` | 最近一次 `fetchLatestFirmware` 结果 | +| `upgradeRecords` | `[UpgradeRecord]` | 本地 OTA 对账记录(computed) | + ### 数据类型 ```swift diff --git a/Sources/DuooomiBleSDK/Core/BleTypes.swift b/Sources/DuooomiBleSDK/Core/BleTypes.swift index a5da05f..9bd96c3 100644 --- a/Sources/DuooomiBleSDK/Core/BleTypes.swift +++ b/Sources/DuooomiBleSDK/Core/BleTypes.swift @@ -39,6 +39,7 @@ public enum DuooomiBleError: LocalizedError { case permissionDenied case unsupportedFormat(String) case firmwareUpgradeUnavailable(String) + case batteryTooLow(current: Int, required: Int) case brandMismatch(expected: String, actual: String) public var errorDescription: String? { @@ -77,6 +78,8 @@ public enum DuooomiBleError: LocalizedError { return "Unsupported file format: \(ext). Supported: jpeg, png, gif, mp4, webm, webp" case .firmwareUpgradeUnavailable(let reason): return "Firmware upgrade unavailable: \(reason)" + case .batteryTooLow(let current, let required): + return "设备电量不足,无法升级 (当前 \(current)%,需 ≥ \(required)%)" case .brandMismatch(let expected, let actual): return "不支持该品牌设备 (expected=\(expected), actual=\(actual))" } diff --git a/Sources/DuooomiBleSDK/DuooomiBleSDK+Firmware.swift b/Sources/DuooomiBleSDK/DuooomiBleSDK+Firmware.swift index 230b5f1..b6e5d0d 100644 --- a/Sources/DuooomiBleSDK/DuooomiBleSDK+Firmware.swift +++ b/Sources/DuooomiBleSDK/DuooomiBleSDK+Firmware.swift @@ -7,6 +7,9 @@ import Foundation extension DuooomiBleSDK { + /// OTA 烧录前置电量门槛(百分比)。低于此值拒绝下发,避免烧录中途断电变砖。 + static let minBatteryForUpgrade = 40 + /// 拉取服务端最新固件信息。**内部串两步业务编排**(顺序固定): /// /// 1. **对账上报**:把本地 pending `UpgradeRecord` 逐条报给服务端(容错失败,不阻断) @@ -61,7 +64,10 @@ extension DuooomiBleSDK { /// - targetVersion: 升级目标版本(一般从 `sdk.firmwareUpgrade?.targetFirmware?.version` 取) /// - completion: 回调 `Result` /// - 成功:OTA 包已发完,设备会重启完成升级;UpgradeRecord 已落本地 - /// - 失败:`.notConnected` / `.transferFailed(...)` + /// - 失败:`.notConnected` / `.batteryTooLow(current:required:)`(电量 < `minBatteryForUpgrade`, + /// 未下发任何字节)/ `.transferFailed(...)` / `getDeviceInfo` 的 `.timeout` / 解码错误 + /// - Important: 下发前会**主动拉一次新鲜 `getDeviceInfo`** 校验实时电量,低于 + /// `minBatteryForUpgrade`(40%)直接以 `.batteryTooLow` 失败,不发任何 OTA 字节。 public func upgradeFirmware( sn: String, fileUrl: String, @@ -74,6 +80,43 @@ extension DuooomiBleSDK { completion(.failure(DuooomiBleError.notConnected)) return } + // OTA 前置电量校验:取实时电量(不用缓存,可能过期),低于门槛拒绝下发。 + getDeviceInfo { [weak self] infoResult in + guard let self = self else { return } + switch infoResult { + case .failure(let error): + BleLog.w("OTA blocked: getDeviceInfo failed: \(error.localizedDescription)", "Firmware") + completion(.failure(error)) + case .success(let info): + guard info.powerlevel >= Self.minBatteryForUpgrade else { + BleLog.w("OTA blocked: battery \(info.powerlevel)% < \(Self.minBatteryForUpgrade)%", "Firmware") + completion(.failure(DuooomiBleError.batteryTooLow( + current: info.powerlevel, + required: Self.minBatteryForUpgrade + ))) + return + } + self.performFirmwareTransfer( + sn: sn, + fileUrl: fileUrl, + firmwareId: firmwareId, + fromVersion: fromVersion, + targetVersion: targetVersion, + completion: completion + ) + } + } + } + + /// `upgradeFirmware` 通过电量门槛后的实际烧录 + 落 `UpgradeRecord`。 + private func performFirmwareTransfer( + sn: String, + fileUrl: String, + firmwareId: String, + fromVersion: String, + targetVersion: String, + completion: @escaping (Result) -> Void + ) { BleLog.i("OTA upgrade start: sn=\(sn) \(fromVersion)→\(targetVersion) (fw=\(firmwareId))", "Firmware") transferFile(fileUri: fileUrl, commandType: .otaPackage) { [weak self] result in if case .success = result {