km2023 f7e1a4769f docs(readme): 初始化 SDK 示例补齐所有默认值注释行(对齐 iOS)
之前只列了 3 个必填 + logLevel,apiHost / cdnHost / ani* / frameIntervalMs
四组默认值的注释行漏了,与 iOS README 不对齐。补齐 + 把 brand /
scanNamePrefix 措辞改成与 iOS 一致("必填,Loomart x-api-key" /
"设备品牌名(所有 RPC 必带 x-owner header)" / "大小写不敏感")。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:28:41 +08:00

Duooomi Android BLE SDK

Duooomi 蓝牙 BLE SDK 的 Android 版本(与 iOS / expo 版业务行为 1:1 对齐)。

⚠️ 请先跑通仓库内 demo 再开始对接 SDK。 demo 已串完整链路(扫描 / 连接 / 绑定 / OTA / 文件传输),是验证设备 + apiKey + brand 是否对的最快途径,避免在自家工程里盲调试。

环境要求

SDK 集成要求(消费方最低版本):

项目 最低要求
Kotlin 1.9.0+
AGP 7.4.2+
minSdk 23 (Android 6.0)
JDK 11+

SDK 开发构建环境:

项目 要求
Android Studio Hedgehog (2023.1.1) 或更高
Kotlin 1.9.22
AGP 8.2.2
Gradle 8.x
JDK 17
compileSdk / targetSdk 34

项目结构

duooomi-android-sdk/
├── sdk/          # SDK 库模块 (com.android.library)
│   └── src/main/kotlin/com/duooomi/ble/
│       ├── DuooomiBleSDK.kt        # 公开 API 入口
│       ├── DuooomiBleConfig.kt     # SDK 配置
│       ├── core/                   # BLE 核心层 (BleClient, 类型定义, 错误类型)
│       ├── protocol/               # BLE 协议层 (帧格式, UUID, 协议管理)
│       ├── services/               # 服务层 (设备命令 / OTA / HTTP)
│       ├── models/                 # 数据模型
│       └── utils/                  # 北京时间 / 日志
├── demo/         # 演示 App (Jetpack Compose)
└── build.gradle.kts

依赖

SDK 仅依赖一个外部库:

org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3

其余全部使用 Android 内置 APIBLE、JSON、HttpURLConnection、SharedPreferences

快速开始

1. 引入 SDK

settings.gradle.kts

include(":sdk")

app 模块的 build.gradle.kts

dependencies {
    implementation(project(":sdk"))
}

2. 添加权限

AndroidManifest.xml

<!-- Android 11 及以下 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 网络 (ANI 转换 / OTA) -->
<uses-permission android:name="android.permission.INTERNET" />

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

运行时仍需动态申请 BLUETOOTH_SCANBLUETOOTH_CONNECTAndroid 12+)和 ACCESS_FINE_LOCATION

3. 初始化 SDK

val sdk = DuooomiBleSDK(
    context = this,
    config = DuooomiBleConfig(
        apiKey = "YOUR_API_KEY",                            // 必填Loomart x-api-key
        brand = "YOUR_BRAND",                               // 必填,设备品牌名(所有 RPC 必带 x-owner header
        scanNamePrefix = "蓝牙广播名前缀"                     // 必填,大小写不敏感
        // apiHost = "https://api.duooomi.com",             // 默认
        // cdnHost = "https://cdn.bowong.cc/",              // 默认
        // aniWidth = 360, aniHeight = 360, aniFps = "24",  // ANI 转换默认参数
        // frameIntervalMs = 35,                            // 协议帧间隔(ms),合法 [0,500],越界 warn 后 clamp
        // logLevel = BleLogLevel.OFF,                      // 日志等级,默认 OFF完全静默排查时改 DEBUG
    )
)

4. Config 字段说明

参数 必填 说明
apiKey Loomart x-api-key 鉴权
brand 设备品牌名(所有 RPC 带 x-owner header
scanNamePrefix 扫描时按广播名前缀过滤(大小写不敏感)
apiHost 默认 https://api.duooomi.com
cdnHost 默认 https://cdn.bowong.cc/
aniWidth / aniHeight / aniFps ANI 转换参数,默认 360 / 360 / "24"
frameIntervalMs 协议帧之间发送间隔ms默认 35,合法 [0, 500];越界仅 warn使用点 coerceIn。调高用于排查接收端丢帧
logLevel 日志等级(BleLogLevel.OFF/ERROR/WARN/INFO/DEBUG),默认 OFF。详见下文

5. 日志BleLogLevel

SDK 内部所有日志统一走 BleLogandroid.util.Log,分级 OFF / ERROR / WARN / INFO / DEBUG。集成方通过 config.logLevel 控制输出:

// 对外发布:什么都不传,默认完全静默
DuooomiBleSDK(context, DuooomiBleConfig(apiKey = "...", brand = "...", scanNamePrefix = "..."))

// 本地排查:开 DEBUG 看到所有 BLE 帧、RPC opId、传输进度等
DuooomiBleSDK(context, DuooomiBleConfig(
    apiKey = "...", brand = "...", scanNamePrefix = "...",
    logLevel = BleLogLevel.DEBUG
))
  • 等级单调:WARN 输出 warn + errorINFO 输出 info + warn + error以此类推。
  • 日志按 category 分桶:SDK / Scan / Connect / BLE / RPC / Command / Transfer / Firmware / Protocol / Decode / ConfigLogcat 按 tag DuooomiBleSDK + [<category>] 过滤。
  • Release 包剥离 Log.d/iAndroid 平台没有 iOS #if DEBUG 那样的编译期消除,默认 OFF 已足够静默;如需彻底剥离 android.util.Log 调用,可在宿主 ProGuard/R8 配置中加 -assumenosideeffects class android.util.Log { public static *** d(...); public static *** i(...); }

API 参考

可观察状态StateFlow

sdk.btState              // ConnectionState: IDLE, SCANNING, CONNECTING, CONNECTED, ...
sdk.discoveredDevices    // List<DiscoveredDevice>
sdk.connectedDevice      // DiscoveredDevice?
sdk.deviceInfo           // DeviceInfo?  (含可选 loop: PlayMode?)
sdk.version              // String       (设备固件版本)
sdk.versionLog           // JSONObject?  (设备 log 字段bind 时透传到 update-metadata)
sdk.isActivated          // Boolean      (是否已 bind)
sdk.sn                   // String       (绑定后写入设备序列号)
sdk.firmwareUpgrade      // FirmwareUpgradeCheckResult?  (最新一次拉取结果)
sdk.transferProgress     // Int (0-100)
sdk.error                // String?

扫描与连接

sdk.scan()                       // 开始扫描(按 scanNamePrefix 过滤)
sdk.stopScan()
val device = sdk.connect(deviceId)   // suspend
sdk.disconnect()                     // suspend

设备操作(均需先连接)

val info = sdk.getDeviceInfo()
// info.brand / info.name / info.size / info.powerlevel
// info.freespace / info.allspace / info.loop (PlayMode?)

val versionInfo = sdk.getVersion()
// versionInfo.version / versionInfo.type

// 绑定设备 —— 一次调用完成 BLE 硬绑 + getVersion + 软绑 + metadata 上报
// 完成后写入 sdk.sn / sdk.version / sdk.versionLog / sdk.isActivated
// 软绑失败 → 自动硬解绑回滚 + 抛 SoftBindFailed
val bindResult = sdk.bind(userId)
// bindResult.sn / bindResult.contents (设备文件列表)

// 解绑
sdk.unbind(userId)

// 删除设备文件
sdk.deleteFile(fileKey)

播放模式切换

import com.duooomi.ble.models.PlayMode

// 单播(播完一次停)
sdk.setPlayMode(PlayMode.SINGLE, userId)

// 循环播放
sdk.setPlayMode(PlayMode.LOOP, userId)

复用 BIND_DEVICE (0x0F) 命令通道;前置必须已 bind() 完成(isActivated == true 否则直接抛 BindingFailed("Device not bound; call bind(userId) first")不发送命令bind / unbind / setPlayMode 共享 0x0F / 0x12 opId 通道,不能并发。

文件传输

sdk.transferMedia(fileUrl)
sdk.transferProgress.collect { percent -> /* 0-100 */ }

支持格式: JPEG / PNG / GIF / MP4 / WebM / WebP。先按 URL 扩展名校验,无扩展名时 HEAD 请求查 Content-Type,不支持的格式抛 DuooomiBleError.UnsupportedFormat

OTA 升级流程

完整流程:扫描 → 连接 → bind → fetchLatestFirmware → upgradeFirmware。设备重启重连后再次走一遍即可。

// Step 1: 绑定(前置:已 connect
val bindResult = sdk.bind(userId)
val sn = sdk.sn.value         // 设备序列号
val cur = sdk.version.value   // 当前固件版本

// Step 2: 拉最新固件信息
val result = sdk.fetchLatestFirmware(sn = sn, currentVersion = cur, userId = userId)
// 成功 → 写入 sdk.firmwareUpgrade

val target = result.targetFirmware ?: return  // null 表示已是最新
target.id          // 固件 ID
target.version     // 目标版本字符串
target.fileUrl     // 固件包下载 URL
target.fileSize    // 字节数(字符串编码)
target.fileMd5     // MD5
target.forceUpgrade // 强制升级标记true 时建议弹不可关闭弹窗)

// Step 3: 烧录
sdk.upgradeFirmware(
    sn = sn,
    fileUrl = target.fileUrl!!,
    firmwareId = target.id,
    fromVersion = cur,
    targetVersion = target.version
)
// OTA 包发完,设备开始烧录并重启

⚠️ upgradeFirmware 下发前会主动拉一次新鲜 getDeviceInfo 校验实时电量,低于 40% 直接抛 DuooomiBleError.BatteryTooLow不发任何 OTA 字节(防烧录中途断电变砖)。

错误处理

try {
    sdk.connect(deviceId)
    sdk.bind(userId)
    sdk.fetchLatestFirmware(sn = sdk.sn.value, currentVersion = sdk.version.value, userId = userId)
} catch (e: DuooomiBleError.BluetoothNotPoweredOn) { /* 蓝牙未开启 */ }
  catch (e: DuooomiBleError.ConnectionFailed)      { /* 连接失败 */ }
  catch (e: DuooomiBleError.NotConnected)          { /* 未连接 */ }
  catch (e: DuooomiBleError.Timeout)               { /* 超时 */ }
  catch (e: DuooomiBleError.PermissionDenied)      { /* 权限不足 */ }
  catch (e: DuooomiBleError.AlreadyBoundByOtherUser) { /* 设备已绑给他人 */ }
  catch (e: DuooomiBleError.BrandMismatch)         { /* 不支持该品牌设备(已自动断连) */ }
  catch (e: DuooomiBleError.FirmwareUpgradeUnavailable) { /* sn 为空等前置不满足 */ }
  catch (e: DuooomiBleError.BatteryTooLow)         { /* OTA 前电量 < 40%未下发任何字节e.current / e.required 可读 */ }

汇总:可用方法与属性

一张表速查所有 public API。异步方法均为 suspend fun,失败抛 DuooomiBleError(少数透传 JSONException / IOException)。 与 iOS 的契约一致项错误类型、字段名、HTTP/JSON 字段)严格 1:1API 形态因平台惯例差异iOS Result callback ↔ Android suspendiOS delegate 协议 ↔ Android StateFlow

方法

方法 参数 返回 说明
scan() Unit(结果发到 discoveredDevices Flow 开始扫描
stopScan() Unit 停止扫描
connect(deviceId:) deviceId: String suspend → DiscoveredDevice 连接设备
disconnect() suspend → Unit 断开连接
getConnectedDevices() List<DiscoveredDevice>(同步) 当前已连接设备列表
bind(userId:) userId: String suspend → BindingResponse 绑定 + 同步拉版本,成功后 sn/version/isActivated Flow 就绪
unbind(userId:) userId: String suspend → UnbindResponse 解绑
getDeviceInfo() suspend → DeviceInfo 读品牌/电量/存储/loop写入 deviceInfo Flow
getVersion() suspend → VersionInfo 读固件版本,写入 version/versionLog Flow
setPlayMode(mode:, userId:) mode: PlayMode, userId: String suspend → BindingResponse 切单播/循环;须先 bind
deleteFile(key:) key: String suspend → DeleteFileResponse 删除设备文件
prepareTransfer(key:, size:) key: String, size: Int suspend → PrepareTransferResponse 传输前预协商(一般无需直调)
transferMedia(fileUrl:) fileUrl: String suspend → Unit 一步校验→ANI 转换→prepare→传输
transferFile(fileUri:, commandType:) fileUri: String, commandType: CommandType = TRANSFER_ANI_VIDEO suspend → Unit 底层文件传输
fetchLatestFirmware(sn:, currentVersion:, userId:) sn, currentVersion, userId: String suspend → FirmwareUpgradeCheckResult 对账上报 + 拉最新固件,写入 firmwareUpgrade Flow
upgradeFirmware(sn:, fileUrl:, firmwareId:, fromVersion:, targetVersion:) String suspend → Unit OTA 烧录;下发前校验实时电量,< 40% 抛 BatteryTooLow,不发任何字节
tryReportPendingUpgrade(sn:, currentVersion:, userId:) String Unitfire-and-forget结果见 upgradeRecords Flow 对账上报本地待报记录(fetchLatestFirmware 已内置调用)

属性(除 configval 外均 StateFlow / SharedFlow,集成方 .collect { } 订阅)

属性 类型 说明
config DuooomiBleConfig 初始化配置(val
btState StateFlow<ConnectionState> IDLE / SCANNING / CONNECTING / CONNECTED / DISCONNECTING / DISCONNECTED
discoveredDevices StateFlow<List<DiscoveredDevice>> 扫描到的设备列表
connectedDevice StateFlow<DiscoveredDevice?> 当前连接设备
deviceInfo StateFlow<DeviceInfo?> 设备信息(品牌/电量/存储/loop
version StateFlow<String> 固件版本bind 后就绪)
versionLog StateFlow<JSONObject?> 设备版本附带 JSON透传给 metadata 上报)
isActivated StateFlow<Boolean> 是否已绑定
sn StateFlow<String> 设备序列号bind 成功后就绪)
transferProgress StateFlow<Int> 传输进度 0100
error StateFlow<String?> 最近错误描述
firmwareUpgrade StateFlow<FirmwareUpgradeCheckResult?> 最近一次 fetchLatestFirmware 结果
upgradeRecords StateFlow<List<UpgradeRecord>> 本地 OTA 对账记录SharedPreferences 持久化)
rawJsonEvents SharedFlow<BleRawJsonEvent> 调试用 BLE JSON 协议层 TX/RX 原始事件流iOS 无对应通道,不属业务契约)

运行 Demo

demo/src/main/kotlin/com/duooomi/ble/demo/DemoSecrets.kt 是本地秘钥文件(已 gitignoredocs/DemoSecrets.template.kt 拷贝并填入真实 apiKey / brand

./gradlew :demo:assembleDebug
adb install demo/build/outputs/apk/debug/demo-debug.apk

BLE 协议简述

  • Service UUID: 000002c4-0000-1000-8000-00805f9b34fb
  • Write Characteristic: 000002c5-...
  • Read Characteristic (Notify): 000002c6-...
  • 帧格式: [head:1][type:1][subpageTotal:2][curPage:2][dataLen:2][data:N][checksum:1]
  • 校验和: 二进制补码 (~sum + 1) & 0xFF
  • 大数据分片: 自动拆分,帧间延迟 35ms
  • Service 发现: 不传 UUID 全发现并过滤(兼容部分固件 scoped discovery 返回空的 bug

License

Proprietary - Duooomi

Description
多米安卓 sdk
Readme 208 KiB
Languages
Kotlin 100%