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:
45
README.md
45
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<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` | 传输进度 0–100 |
|
||||
| `error` | `String?` | 最近错误描述 |
|
||||
| `firmwareUpgrade` | `FirmwareUpgradeCheckResult?` | 最近一次 `fetchLatestFirmware` 结果 |
|
||||
| `upgradeRecords` | `[UpgradeRecord]` | 本地 OTA 对账记录(computed) |
|
||||
|
||||
### 数据类型
|
||||
|
||||
```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))"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user