feat(sdk): 同步 iOS — upgradeFirmware 下发前校验电量 ≥40%,不足抛 BatteryTooLow

1:1 复刻 iOS:OTA 烧录前主动 getDeviceInfo 取实时电量,<40% 抛
DuooomiBleError.BatteryTooLow(current,required)、不发任何字节。
错误消息字符串与 iOS 逐字一致;新增 MIN_BATTERY_FOR_UPGRADE=40。
协程版线性实现(无 callback 拆分)。README 同步错误处理/升级段。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
km2023
2026-05-19 17:19:34 +08:00
parent 8d83588e0b
commit 69435512d5
3 changed files with 20 additions and 0 deletions

View File

@@ -223,6 +223,8 @@ sdk.upgradeFirmware(
// OTA 包发完,设备开始烧录并重启 // OTA 包发完,设备开始烧录并重启
``` ```
> ⚠️ `upgradeFirmware` 下发前会**主动拉一次新鲜 `getDeviceInfo`** 校验实时电量,低于 40% 直接抛 `DuooomiBleError.BatteryTooLow`**不发任何 OTA 字节**(防烧录中途断电变砖)。
### 错误处理 ### 错误处理
```kotlin ```kotlin
@@ -238,6 +240,7 @@ try {
catch (e: DuooomiBleError.AlreadyBoundByOtherUser) { /* 设备已绑给他人 */ } catch (e: DuooomiBleError.AlreadyBoundByOtherUser) { /* 设备已绑给他人 */ }
catch (e: DuooomiBleError.BrandMismatch) { /* 不支持该品牌设备(已自动断连) */ } catch (e: DuooomiBleError.BrandMismatch) { /* 不支持该品牌设备(已自动断连) */ }
catch (e: DuooomiBleError.FirmwareUpgradeUnavailable) { /* sn 为空等前置不满足 */ } catch (e: DuooomiBleError.FirmwareUpgradeUnavailable) { /* sn 为空等前置不满足 */ }
catch (e: DuooomiBleError.BatteryTooLow) { /* OTA 前电量 < 40%未下发任何字节e.current / e.required 可读 */ }
``` ```
## 运行 Demo ## 运行 Demo

View File

@@ -452,6 +452,9 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
// MARK: - High-Level APIs // MARK: - High-Level APIs
companion object { companion object {
/** OTA 烧录前置电量门槛(百分比)。低于此值拒绝下发,避免烧录中途断电变砖。 */
internal const val MIN_BATTERY_FOR_UPGRADE = 40
private val SUPPORTED_EXTENSIONS = setOf("jpeg", "jpg", "png", "gif", "mp4", "webm", "webp") private val SUPPORTED_EXTENSIONS = setOf("jpeg", "jpg", "png", "gif", "mp4", "webm", "webp")
private val SUPPORTED_MIME_TYPES = setOf( private val SUPPORTED_MIME_TYPES = setOf(
"image/jpeg", "image/png", "image/gif", "image/webp", "image/jpeg", "image/png", "image/gif", "image/webp",
@@ -550,6 +553,13 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
/** /**
* OTA 固件烧录。完成后自动落 [UpgradeRecord] 到本地(复合主键 `(sn, firmwareId)` * OTA 固件烧录。完成后自动落 [UpgradeRecord] 到本地(复合主键 `(sn, firmwareId)`
* 等待下次 [fetchLatestFirmware] 触发 [tryReportPendingUpgrade] 对账上报。 * 等待下次 [fetchLatestFirmware] 触发 [tryReportPendingUpgrade] 对账上报。
*
* 下发前会**主动拉一次新鲜 [getDeviceInfo]** 校验实时电量,低于
* [MIN_BATTERY_FOR_UPGRADE]40%)直接抛 [DuooomiBleError.BatteryTooLow],不发任何 OTA 字节。
*
* @throws DuooomiBleError.NotConnected 未连接
* @throws DuooomiBleError.BatteryTooLow 电量 < [MIN_BATTERY_FOR_UPGRADE](未下发任何字节)
* @throws DuooomiBleError.TransferFailed 烧录阶段失败;[getDeviceInfo] 的 Timeout / 解码错误同样透传
*/ */
suspend fun upgradeFirmware( suspend fun upgradeFirmware(
sn: String, sn: String,
@@ -559,6 +569,12 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) {
targetVersion: String targetVersion: String
) { ) {
ensureConnected() ensureConnected()
// OTA 前置电量校验:取实时电量(不用缓存,可能过期),低于门槛拒绝下发。
val info = getDeviceInfo()
if (info.powerlevel < MIN_BATTERY_FOR_UPGRADE) {
BleLog.w("OTA blocked: battery ${info.powerlevel}% < $MIN_BATTERY_FOR_UPGRADE%", "Firmware")
throw DuooomiBleError.BatteryTooLow(current = info.powerlevel, required = MIN_BATTERY_FOR_UPGRADE)
}
BleLog.i("OTA upgrade start: sn=$sn $fromVersion$targetVersion (fw=$firmwareId)", "Firmware") BleLog.i("OTA upgrade start: sn=$sn $fromVersion$targetVersion (fw=$firmwareId)", "Firmware")
transferFile(fileUri = fileUrl, commandType = CommandType.OTA_PACKAGE) transferFile(fileUri = fileUrl, commandType = CommandType.OTA_PACKAGE)
BleLog.i("OTA upgrade completed", "Firmware") BleLog.i("OTA upgrade completed", "Firmware")

View File

@@ -44,5 +44,6 @@ sealed class DuooomiBleError(message: String) : Exception(message) {
class UnsupportedFormat(val format: String) : DuooomiBleError("Unsupported file format: $format. Supported: jpeg, png, gif, mp4, webm, webp") class UnsupportedFormat(val format: String) : DuooomiBleError("Unsupported file format: $format. Supported: jpeg, png, gif, mp4, webm, webp")
object InvalidFrame : DuooomiBleError("Invalid protocol frame") object InvalidFrame : DuooomiBleError("Invalid protocol frame")
object PermissionDenied : DuooomiBleError("Bluetooth permission denied") object PermissionDenied : DuooomiBleError("Bluetooth permission denied")
class BatteryTooLow(val current: Int, val required: Int) : DuooomiBleError("设备电量不足,无法升级 (当前 ${current}%,需 ≥ ${required}%)")
class BrandMismatch(val expected: String, val actual: String) : DuooomiBleError("不支持该品牌设备 (expected=$expected, actual=$actual)") class BrandMismatch(val expected: String, val actual: String) : DuooomiBleError("不支持该品牌设备 (expected=$expected, actual=$actual)")
} }