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:
248
docs/superpowers/specs/2026-04-10-android-ble-sdk-design.md
Normal file
248
docs/superpowers/specs/2026-04-10-android-ble-sdk-design.md
Normal 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" />
|
||||
```
|
||||
Reference in New Issue
Block a user