- 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>
262 lines
8.3 KiB
Markdown
262 lines
8.3 KiB
Markdown
# 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
|
||
<!-- 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
|