feat(sdk): upgradeFirmware 下发前校验电量 ≥40%,不足抛 .batteryTooLow

OTA 烧录前主动拉新鲜 getDeviceInfo 取实时电量,<40% 直接以
.batteryTooLow(current:required:) 失败、不发任何字节,防烧录中途断电变砖。
新增常量 minBatteryForUpgrade=40;烧录体抽出私有 performFirmwareTransfer。
README 补可用方法/属性汇总表。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
km2023
2026-05-19 17:19:23 +08:00
parent 4bc8fdb64d
commit a15403ae36
3 changed files with 92 additions and 1 deletions

View File

@@ -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<DiscoveredDevice, Error>` | 连接设备 |
| `disconnect(completion:)` | — | `Result<Void, Error>` | 断开连接 |
| `getConnectedDevices()` | — | `[DiscoveredDevice]`(同步) | 当前已连接设备列表 |
| `bind(userId:completion:)` | `userId: String` | `Result<BindingResponse, Error>` | 绑定 + 同步拉版本,成功后 `sn`/`version`/`isActivated` 就绪 |
| `unbind(userId:completion:)` | `userId: String` | `Result<UnbindResponse, Error>` | 解绑 |
| `getDeviceInfo(completion:)` | — | `Result<DeviceInfo, Error>` | 读品牌/电量/存储/loop写入 `deviceInfo` |
| `getVersion(completion:)` | — | `Result<VersionInfo, Error>` | 读固件版本,写入 `version`/`versionLog` |
| `setPlayMode(_:userId:completion:)` | `_ mode: PlayMode`, `userId: String` | `Result<BindingResponse, Error>` | 切单播/循环;**须先 bind** |
| `deleteFile(key:completion:)` | `key: String` | `Result<DeleteFileResponse, Error>` | 删除设备文件 |
| `prepareTransfer(key:size:completion:)` | `key: String`, `size: Int` | `Result<PrepareTransferResponse, Error>` | 传输前预协商(一般无需直调) |
| `transferMedia(fileUrl:completion:)` | `fileUrl: String` | `Result<Void, Error>` | 一步校验→ANI 转换→prepare→传输 |
| `transferFile(fileUri:commandType:completion:)` | `fileUri: String`, `commandType: CommandType = .transferAniVideo` | `Result<Void, Error>` | 底层文件传输 |
| `fetchLatestFirmware(sn:currentVersion:userId:completion:)` | `sn: String`, `currentVersion: String`, `userId: String` | `Result<FirmwareUpgradeCheckResult, Error>` | 对账上报 + 拉最新固件,写入 `firmwareUpgrade` |
| `upgradeFirmware(sn:fileUrl:firmwareId:fromVersion:targetVersion:completion:)` | `sn`, `fileUrl`, `firmwareId`, `fromVersion`, `targetVersion`: `String` | `Result<Void, Error>` | 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` | 传输进度 0100 |
| `error` | `String?` | 最近错误描述 |
| `firmwareUpgrade` | `FirmwareUpgradeCheckResult?` | 最近一次 `fetchLatestFirmware` 结果 |
| `upgradeRecords` | `[UpgradeRecord]` | 本地 OTA 对账记录computed |
### 数据类型
```swift

View File

@@ -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))"
}

View File

@@ -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<Void, Error>`
/// - 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, Error>) -> 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 {