Add Android BLE SDK design spec

1:1 feature parity with iOS DuooomiBleSDK, pure Kotlin, zero third-party deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
km2023
2026-04-10 16:33:05 +08:00
commit 97aaa6d454

View File

@@ -0,0 +1,248 @@
# DuooomiBleSDK Android — Design Spec
## Overview
Pure Kotlin Android BLE SDK, zero third-party dependencies. 1:1 feature parity with the iOS `DuooomiBleSDK`.
## Project Structure
```
duooomi-android-sdk/
├── build.gradle.kts # Root build
├── settings.gradle.kts
├── gradle.properties
├── gradle/
│ └── wrapper/
├── sdk/
│ ├── build.gradle.kts # Android Library (AAR)
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── kotlin/com/duooomi/ble/
│ ├── DuooomiBleSDK.kt
│ ├── DuooomiBleConfig.kt
│ ├── core/
│ │ ├── BleClient.kt
│ │ └── BleTypes.kt
│ ├── protocol/
│ │ ├── ProtocolConstants.kt
│ │ ├── ProtocolFrame.kt
│ │ └── ProtocolManager.kt
│ ├── services/
│ │ ├── BleProtocolService.kt
│ │ ├── DeviceInfoService.kt
│ │ ├── FileTransferService.kt
│ │ ├── AniConverter.kt
│ │ └── FirmwareService.kt
│ ├── models/
│ │ ├── BindingResponse.kt
│ │ ├── DeleteFileResponse.kt
│ │ ├── DeviceInfo.kt
│ │ ├── PrepareTransferResponse.kt
│ │ ├── UnbindResponse.kt
│ │ ├── VersionInfo.kt
│ │ └── FirmwareInfo.kt
│ └── utils/
│ └── BleLogger.kt
└── demo/ # Demo app (future)
├── build.gradle.kts
└── src/main/...
```
## Technology Mapping (iOS → Android)
| iOS | Android/Kotlin |
|-----|----------------|
| `CoreBluetooth` / `CBCentralManager` | `BluetoothLeScanner` + `BluetoothGatt` |
| `CBPeripheral` | `BluetoothDevice` / `BluetoothGatt` |
| `AsyncStream<T>` | `Flow<T>` / `callbackFlow` |
| `@Published` | `MutableStateFlow<T>` (internal) / `StateFlow<T>` (public) |
| `CheckedContinuation` | `suspendCancellableCoroutine` |
| `ObservableObject` | Direct `StateFlow` properties |
| `@MainActor` | `Dispatchers.Main` |
| `Data` | `ByteArray` |
| `URLSession` | `HttpURLConnection` (zero deps) |
| `Task.sleep(nanoseconds:)` | `delay()` |
| `DispatchQueue(serial)` | `newSingleThreadContext` / `Mutex` |
| `DispatchWorkItem` + `asyncAfter` | `Job` + `delay` |
## Layer Design
### 1. DuooomiBleConfig
```kotlin
data class DuooomiBleConfig(
val apiHost: String = "https://api.mixvideo.bowong.cc",
val apiKey: String,
val cdnHost: String = "https://cdn.bowong.cc/",
val firmwareIdentifier: String = "duomi",
val firmwareStatus: String = "DRAFT"
)
```
### 2. BleTypes (core/BleTypes.kt)
```kotlin
enum class ConnectionState { IDLE, SCANNING, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED }
data class DiscoveredDevice(val id: String, val name: String?, val rssi: Int)
sealed class DuooomiBleError(message: String) : Exception(message) {
class BluetoothNotPoweredOn(state: String) : DuooomiBleError("Bluetooth not powered on: $state")
object NotConnected : DuooomiBleError("No device connected")
class Timeout(command: CommandType) : DuooomiBleError("Timeout: $command")
class ConnectionFailed(reason: String) : DuooomiBleError("Connection failed: $reason")
object ServiceNotFound : DuooomiBleError("BLE service not found")
object CharacteristicNotFound : DuooomiBleError("Characteristic not found")
class BindingFailed(reason: String) : DuooomiBleError("Binding failed: $reason")
object UnbindFailed : DuooomiBleError("Unbind failed")
class DeleteFileFailed(status: Int) : DuooomiBleError("Delete failed: $status")
class PrepareTransferFailed(status: String) : DuooomiBleError("Prepare failed: $status")
class TransferFailed(reason: String) : DuooomiBleError("Transfer failed: $reason")
object InvalidFrame : DuooomiBleError("Invalid protocol frame")
object PermissionDenied : DuooomiBleError("Bluetooth permission denied")
}
```
### 3. ProtocolConstants (protocol/ProtocolConstants.kt)
- BLE UUIDs: same values as iOS (000002c4/c5/c6/c1)
- FrameHead: APP_TO_DEVICE = 0xC7, DEVICE_TO_APP = 0xB0
- FrameConstants: maxDataSize = 496, headerSize = 8, footerSize = 1, frameIntervalMs = 35
- CommandType enum: same values as iOS (0x02..0x14)
### 4. ProtocolFrame + ProtocolManager
- Frame format identical to iOS: [head:1][type:1][subpageTotal:2][curPage:2][dataLen:2][data:N][checksum:1]
- Checksum: `(~sum + 1) & 0xFF`
- `createFrames()` / `parseFrame()` — same logic, ByteArray operations
### 5. BleClient (core/BleClient.kt)
Android BLE wrapper using coroutines:
- `scan()``callbackFlow<DiscoveredDevice>` wrapping `ScanCallback`
- `connect(deviceId)``suspend fun` using `suspendCancellableCoroutine` + `BluetoothGattCallback`
- `disconnect()``suspend fun`
- `writeWithoutResponse(data)` → direct `BluetoothGatt.writeCharacteristic`
- `notifications()``callbackFlow<ByteArray>` from `onCharacteristicChanged`
- `retrieveConnectedDevices()``BluetoothManager.getConnectedDevices`
Key difference from iOS: Android needs `Context` for `BluetoothManager`. Pass via `DuooomiBleSDK` constructor.
### 6. BleProtocolService
Same as iOS:
- `startListening()` / `stopListening()`
- `send(type, data, onProgress)` — frame split + write with 35ms delay
- `sendJSON(type, payload)` — JSON encode + send
- Fragment reassembly with `FragmentSession` map
- `incomingMessages` as `SharedFlow<Pair<UByte, ByteArray>>`
### 7. DeviceInfoService
Same command payloads as iOS:
- `getDeviceInfo()`, `getDeviceVersion()`, `bindDevice(userId)`, `unbindDevice(userId)`, `deleteFile(key)`, `prepareTransfer(key, size)`
### 8. FileTransferService
- `transferFile(fileUri, commandType, onProgress)`
- `loadFileData(uri)` — supports `file://` (local) and `https://` (HttpURLConnection)
### 9. AniConverter
- POST to `{apiHost}/api/auth/loomart/file/convert-to-ani`
- x-api-key header
- Body: `{ videoUrl, width: 360, height: 360, fps: "24" }`
- Retry on connection lost (once)
- Uses `HttpURLConnection`
### 10. FirmwareService
- GET `{apiHost}/api/auth/loomart/firmware/latest-published?identifier=X&status=Y`
- x-api-key header
- Response: `{ success: bool, data: FirmwareInfo? }`
### 11. Models
All using `org.json.JSONObject` for parsing (Android built-in, zero deps):
| Model | Fields |
|-------|--------|
| `BindingResponse` | type: Int, sn: String, success: Int, contents: List<String> |
| `DeleteFileResponse` | type: Int, success: Int |
| `DeviceInfo` | allspace: Long, freespace: Long, name: String, size: String, brand: String, powerlevel: Int |
| `PrepareTransferResponse` | type: Int, key: String, status: String |
| `UnbindResponse` | success: Int |
| `VersionInfo` | version: String, type: String |
| `FirmwareInfo` | version: String, fileUrl: String, description: String?, fileSize: String?, fileMd5: String?, identifier: String?, status: String? |
### 12. DuooomiBleSDK — Public API
```kotlin
class DuooomiBleSDK(context: Context, config: DuooomiBleConfig) {
// Observable state (StateFlow)
val btState: StateFlow<ConnectionState>
val discoveredDevices: StateFlow<List<DiscoveredDevice>>
val connectedDevice: StateFlow<DiscoveredDevice?>
val deviceInfo: StateFlow<DeviceInfo?>
val version: StateFlow<String>
val isActivated: StateFlow<Boolean>
val transferProgress: StateFlow<Int>
val error: StateFlow<String?>
// Scanning
fun scan()
fun stopScan()
// Connection
suspend fun connect(deviceId: String): DiscoveredDevice
suspend fun disconnect()
fun getConnectedDevices(): List<DiscoveredDevice>
// Device commands
suspend fun getDeviceInfo(): DeviceInfo
suspend fun getVersion(): VersionInfo
suspend fun bind(userId: String): BindingResponse
suspend fun unbind(userId: String): UnbindResponse
suspend fun deleteFile(key: String): DeleteFileResponse
suspend fun prepareTransfer(key: String, size: Int): PrepareTransferResponse
// File transfer
suspend fun transferFile(fileUri: String, commandType: CommandType = CommandType.TRANSFER_ANI_VIDEO)
suspend fun transferMedia(fileUrl: String)
// Firmware
suspend fun fetchLatestFirmware(identifier: String? = null, status: String? = null): FirmwareInfo?
suspend fun upgradeFirmware(fileUrl: String)
}
```
## Android-specific Considerations
1. **Context**: `DuooomiBleSDK` constructor requires `Context` (use `applicationContext` internally)
2. **Permissions**: Manifest declares `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`. Runtime permission check is caller's responsibility.
3. **Threading**: All BLE operations on a dedicated coroutine dispatcher. Public API methods are `suspend` functions (no `@MainActor` equivalent needed — caller controls dispatcher).
4. **JSON**: Use `org.json.JSONObject` (Android built-in) instead of third-party JSON libraries.
5. **MTU**: Request MTU 512 after connection to maximize throughput (iOS handles this automatically).
6. **Write queue**: Android BLE requires waiting for `onCharacteristicWrite` callback before next write. Use a serial channel/mutex.
## Build Configuration
- `minSdk`: 26 (Android 8.0+)
- `targetSdk`: 34
- `compileSdk`: 34
- Kotlin: 1.9+
- Gradle: 8.x with KTS
- Zero external dependencies (only `kotlinx-coroutines-android` from Kotlin stdlib)
Note: `kotlinx-coroutines-android` is considered part of the Kotlin ecosystem, not a "third-party" dependency. It provides `Dispatchers.Main` which is essential for Android.
## Permissions in AndroidManifest.xml
```xml
<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" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
```