# 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 内置 API(BLE、JSON、HttpURLConnection、SharedPreferences)。 ## 快速开始 ### 1. 引入 SDK `settings.gradle.kts`: ```kotlin include(":sdk") ``` app 模块的 `build.gradle.kts`: ```kotlin dependencies { implementation(project(":sdk")) } ``` ### 2. 添加权限 `AndroidManifest.xml`: ```xml ``` > 运行时仍需动态申请 `BLUETOOTH_SCAN`、`BLUETOOTH_CONNECT`(Android 12+)和 `ACCESS_FINE_LOCATION`。 ### 3. 初始化 SDK ```kotlin val sdk = DuooomiBleSDK( context = this, config = DuooomiBleConfig( apiKey = "YOUR_API_KEY", brand = "YOUR_BRAND", // 设备品牌名,所有 RPC 必带 scanNamePrefix = "Duooomi-" // 扫描时按广播名前缀过滤 ) ) ``` ### 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"` | ## API 参考 ### 可观察状态(StateFlow) ```kotlin sdk.btState // ConnectionState: IDLE, SCANNING, CONNECTING, CONNECTED, ... sdk.discoveredDevices // List 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? ``` ### 扫描与连接 ```kotlin sdk.scan() // 开始扫描(按 scanNamePrefix 过滤) sdk.stopScan() val device = sdk.connect(deviceId) // suspend sdk.disconnect() // suspend ``` ### 设备操作(均需先连接) ```kotlin 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) ``` ### 播放模式切换 ```kotlin 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 通道,不能并发。 ### 文件传输 ```kotlin 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**。设备重启重连后再次走一遍即可。 ```kotlin // 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 字节**(防烧录中途断电变砖)。 ### 错误处理 ```kotlin 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 可读 */ } ``` ## 运行 Demo `demo/src/main/kotlin/com/duooomi/ble/demo/DemoSecrets.kt` 是本地秘钥文件(已 gitignore),从 `docs/DemoSecrets.template.kt` 拷贝并填入真实 `apiKey` / `brand`。 ```bash ./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