Files
duooomi-android-sdk/README.md
km2023 e63865092c feat(sdk): 同步 iOS 改动 — owner→brand 重命名 + 硬绑前 brand 校验 + README 内部细节抹除
- DuooomiBleConfig.owner → brand(语义:设备品牌名),HTTP header x-owner 不变
- bind() 前置 verifyBrand:缺 deviceInfo 先 getDeviceInfo,与 config.brand 严格匹配,
  不一致主动 disconnect 并抛 DuooomiBleError.BrandMismatch
- README 抹除软绑/对账/UpgradeRecord 等内部实现细节,仅保留厂商面向的公开 API
  (功能代码完全保留:fetchLatestFirmware 内部三段串、unbind 软解、tryReportPendingUpgrade)
- README 顶部加 demo 优先验证提示

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:03:26 +08:00

262 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```kotlin
include(":sdk")
```
app 模块的 `build.gradle.kts`
```kotlin
dependencies {
implementation(project(":sdk"))
}
```
### 2. 添加权限
`AndroidManifest.xml`
```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_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<DiscoveredDevice>
sdk.connectedDevice // DiscoveredDevice?
sdk.deviceInfo // DeviceInfo? (含可选 loop: PlayMode?)
sdk.version // String (设备固件版本)
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
// 绑定设备 —— 完成后写入 sdk.sn / sdk.version / sdk.isActivated
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 包发完,设备开始烧录并重启
```
### 错误处理
```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 为空等前置不满足 */ }
```
## 运行 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