Files
duooomi-ios-sdk/README.md
km2023 6ed3c555df feat(sdk): 日志等级 logLevel 配置化(默认 .off,对外发布完全静默)
BleLog 之前所有调用都无条件 print(Debug 包),集成方没法关。新增
public enum BleLogLevel(off/error/warn/info/debug),通过
DuooomiBleConfig.logLevel 传入,DuooomiBleSDK.init 第一行写入
BleLog.level。默认 .off 保证对外集成默认静默,排查时显式开 .debug。
保留 #if DEBUG 兜底,Release 任何 level 都不输出。

frameIntervalMs 越界 warn 从 DuooomiBleConfig.init 挪到
DuooomiBleSDK.init 内、level 设置之后,避免在 level 写入前丢日志。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:02:28 +08:00

372 lines
15 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.
# DuooomiBleSDK (iOS)
纯原生 Swift BLE SDK零第三方依赖。
> ⚠️ **请先跑通仓库内 demo 再开始对接 SDK。** demo 已串完整链路(扫描 / 连接 / 绑定 / OTA / 文件传输),是验证设备 + apiKey + brand 是否对的最快途径,避免在自家工程里盲调试。
## 集成
### CocoaPods
```ruby
pod 'DuooomiBleSDK', :git => 'https://gitea.bowong.cc/comi-logic/duooomi-ios-sdk.git', :branch => 'main'
```
然后执行 `pod install`
### Swift Package Manager
```swift
// Package.swift
dependencies: [
.package(url: "https://gitea.bowong.cc/comi-logic/duooomi-ios-sdk.git", branch: "main")
]
```
## Demo 初始化(仓库内 demo 目录)
`apiKey` / `brand` / `scanNamePrefix` 不进 git由本地 `demo/Sources/DemoSecrets.swift` 提供:
```bash
cp demo/Sources/DemoSecrets.template.swift demo/Sources/DemoSecrets.swift
# 然后编辑 DemoSecrets.swift 填真实 apiKey
pod install
open demo.xcworkspace
```
`DemoSecrets.swift` 已加入 `.gitignore`,本地修改不会被 commit。
## 初始化
```swift
let sdk = DuooomiBleSDK(config: .init(
apiKey: "your-api-key", // Loomart x-api-key
brand: "your-brand", // RPC x-owner header
scanNamePrefix: "蓝牙广播名前缀" //
// apiHost: URL(...)!, // https://api.duooomi.com
// cdnHost: "https://cdn.bowong.cc/", //
// aniWidth: 360, aniHeight: 360, aniFps: "24", // ANI
// frameIntervalMs: 35, // (ms) [0,500] warn clamp
// logLevel: .off, // .off .debug
))
sdk.delegate = self
```
### 日志BleLogLevel
SDK 内部所有日志统一走 `BleLog``print`,分级 `off / error / warn / info / debug`。集成方通过 `config.logLevel` 控制输出:
```swift
//
DuooomiBleSDK(config: .init(apiKey: ..., brand: ..., scanNamePrefix: ...))
// debug BLE RPC opId
DuooomiBleSDK(config: .init(apiKey: ..., brand: ..., scanNamePrefix: ..., logLevel: .debug))
```
- 等级单调:`.warn` 会输出 warn + error`.info` 输出 info + warn + error以此类推。
- **Release 构建因 `#if DEBUG` 条件编译,任何 `logLevel` 都不会输出**(双保险)。
- 日志按 category 分桶:`SDK / Scan / Connect / BLE / RPC / Command / Transfer / Firmware / Protocol / Decode / Config`Xcode console 可按 `[DuooomiBleSDK][<category>]` 过滤。
## SDK 能力范围
- **内置**BLE 协议(扫描/连接/绑定/解绑/播放模式/文件传输/OTA 烧录)+ 固件版本检查
- **不内置**UI、retry 策略SDK 只提供原子方法和状态属性,由集成方组合
### 绑定与固件升级流程
1. **`bind(userId:)`** — 绑定设备,完成后 `sn` / `version` / `isActivated` 就绪。
2. **`fetchLatestFirmware(sn:currentVersion:userId:)`** — 拉服务端最新固件信息,写入 `firmwareUpgrade`
3. **`upgradeFirmware(...)`** — 烧录固件包,进度通过 `didUpdateProgress` 回调。
解绑:`unbind(userId:)` 一步完成。
### API 调用风格
所有公开方法**显式传入参**,不依赖 SDK 内部状态自动取参。调用方从 SDK 只读属性(`sdk.sn` / `sdk.version` / `sdk.firmwareUpgrade?.targetFirmware?.*` 等)组装参数后传入。
## 代理Delegate
通过 `DuooomiBleSDKDelegate` 接收状态变化,所有方法在主线程调用,均有默认空实现:
```swift
extension ViewController: DuooomiBleSDKDelegate {
func sdk(_ sdk: DuooomiBleSDK, didChangeState state: ConnectionState) { }
func sdk(_ sdk: DuooomiBleSDK, didUpdateDevices devices: [DiscoveredDevice]) { }
func sdk(_ sdk: DuooomiBleSDK, didConnectDevice device: DiscoveredDevice?) { }
func sdk(_ sdk: DuooomiBleSDK, didUpdateDeviceInfo info: DeviceInfo?) { }
func sdk(_ sdk: DuooomiBleSDK, didUpdateVersion version: String) { }
func sdk(_ sdk: DuooomiBleSDK, didChangeActivation isActivated: Bool) { }
func sdk(_ sdk: DuooomiBleSDK, didUpdateProgress progress: Int) { }
func sdk(_ sdk: DuooomiBleSDK, didEncounterError error: String?) { }
}
```
## API
所有异步方法使用 `completion` 回调,返回 `Result<T, Error>`
### 扫描
| 方法 | 说明 |
|------|------|
| `scan()` | 开始扫描,结果通过 delegate `didUpdateDevices` 回调 |
| `stopScan()` | 停止扫描 |
### 连接
```swift
sdk.connect(deviceId: deviceId) { result in
switch result {
case .success(let device): print("Connected: \(device.name ?? device.id)")
case .failure(let error): print("Failed: \(error)")
}
}
sdk.disconnect { result in
// ...
}
```
### 设备命令
```swift
sdk.getDeviceInfo { result in
if case .success(let info) = result {
print("Brand: \(info.brand), Power: \(info.powerlevel)%")
}
}
sdk.getVersion { result in
if case .success(let info) = result {
print("Version: \(info.version)")
}
}
// +
sdk.bind(userId: "user-id") { result in
if case .success(let resp) = result {
print("SN: \(resp.sn), Files: \(resp.contents.count)")
// sdk.sn / sdk.version / sdk.isActivated
}
}
//
sdk.unbind(userId: "user-id") { result in /* ... */ }
sdk.deleteFile(key: "file-key") { result in /* ... */ }
```
#### 播放模式Play Mode
> ⚠️ **前置条件 — 必须先绑定。** `setPlayMode` 复用 `BIND_DEVICE` (0x0F) 命令通道,固件设计上要求设备先和该 `userId` 完成 `bind`(即 SDK `isActivated == true`才能切播放模式。SDK 在客户端做了守卫:未绑定时直接 fail不会发命令。调用方在 `isActivated == true` 之前不应调用此方法。
```swift
// 0 = / 1 =
sdk.setPlayMode(.loop, userId: "user-id") { result in
switch result {
case .success:
// SDK deviceInfo
// didUpdateDeviceInfo info.loop
break
case .failure(let error):
print("setPlayMode failed: \(error)")
}
}
```
**注意事项:**
- `bind` / `unbind` / `setPlayMode` 共享 `0x0F` / `0x12` 响应通道,**不要并发调用**
- 旧固件可能不支持 `loop` 字段 —— 此时 `DeviceInfo.loop``nil`,调用 `setPlayMode` 设备可能回 `success=0`
### 文件传输
```swift
// ANI prepare transfer
sdk.transferMedia(fileUrl: "https://example.com/video.mp4") { result in
switch result {
case .success: print("Transfer complete")
case .failure(let error): print("Transfer failed: \(error)")
}
}
// delegate didUpdateProgress
```
**支持的媒体格式:** JPEG、PNG、GIF、MP4、WebM、WebP
格式校验规则:
- 优先从 URL path 提取扩展名(自动忽略查询参数)
- 无扩展名时通过 HEAD 请求检查 `Content-Type`
- 不支持的格式返回 `DuooomiBleError.unsupportedFormat`
### 固件升级
完整流程:**扫描 → 连接 → bind → fetchLatestFirmware → upgradeFirmware**。设备重启重连后再次走一遍即可。
```swift
// 1. +
sdk.scan()
// didUpdateDevices connect
sdk.connect(deviceId: device.id) { result in
guard case .success = result else { return }
// 2.
sdk.bind(userId: "user-123") { result in
guard case .success(let resp) = result else { return }
// sdk.sn = resp.sn / sdk.version / sdk.isActivated = true
// 3.
sdk.fetchLatestFirmware(
sn: sdk.sn,
currentVersion: sdk.version,
userId: "user-123"
) { result in
switch result {
case .failure(let err):
print("拉取失败:\(err.localizedDescription)")
return
case .success(let info):
// info sdk.firmwareUpgrade
guard let target = info.targetFirmware,
let fileUrl = target.fileUrl else {
print("无可升级固件")
return
}
// 4.
sdk.upgradeFirmware(
sn: sdk.sn,
fileUrl: fileUrl,
firmwareId: target.id,
fromVersion: sdk.version,
targetVersion: target.version
) { result in
switch result {
case .success:
// OTA
print("烧录命令完成,等待设备重启")
case .failure(let err):
print("烧录失败:\(err.localizedDescription)")
}
}
}
}
}
}
```
### 可读取状态
| 属性 | 类型 | 说明 |
|------|------|------|
| `btState` | `ConnectionState` | idle / scanning / connecting / connected / disconnecting / disconnected |
| `discoveredDevices` | `[DiscoveredDevice]` | 扫描到的设备列表 |
| `connectedDevice` | `DiscoveredDevice?` | 当前连接设备 |
| `deviceInfo` | `DeviceInfo?` | 设备信息 |
| `version` | `String` | 固件版本 |
| `isActivated` | `Bool` | 是否已绑定 |
| `transferProgress` | `Int` | 传输进度 0-100 |
| `error` | `String?` | 最近错误 |
### 汇总:可用方法与属性
> 一张表速查所有 public API。异步方法的失败分支均为 `Result` 的 `.failure(Error)``Error` 多为 `DuooomiBleError`,少数透传 `DecodingError` / `URLError`(详见各方法注释)。
#### 方法
| 方法 | 参数 | 返回 | 说明 |
|------|------|------|------|
| `scan()` | — | 无(`didUpdateDevices` 回调) | 开始扫描 |
| `stopScan()` | — | 无 | 停止扫描 |
| `connect(deviceId:completion:)` | `deviceId: String` | `Result<DiscoveredDevice, Error>` | 连接设备 |
| `disconnect(completion:)` | — | `Result<Void, Error>` | 断开连接 |
| `getConnectedDevices()` | — | `[DiscoveredDevice]`(同步) | 当前已连接设备列表 |
| `bind(userId:completion:)` | `userId: String` | `Result<BindingResponse, Error>` | 绑定 + 同步拉版本,成功后 `sn`/`version`/`isActivated` 就绪 |
| `unbind(userId:completion:)` | `userId: String` | `Result<UnbindResponse, Error>` | 解绑 |
| `getDeviceInfo(completion:)` | — | `Result<DeviceInfo, Error>` | 读品牌/电量/存储/loop写入 `deviceInfo` |
| `getVersion(completion:)` | — | `Result<VersionInfo, Error>` | 读固件版本,写入 `version`/`versionLog` |
| `setPlayMode(_:userId:completion:)` | `_ mode: PlayMode`, `userId: String` | `Result<BindingResponse, Error>` | 切单播/循环;**须先 bind** |
| `deleteFile(key:completion:)` | `key: String` | `Result<DeleteFileResponse, Error>` | 删除设备文件 |
| `prepareTransfer(key:size:completion:)` | `key: String`, `size: Int` | `Result<PrepareTransferResponse, Error>` | 传输前预协商(一般无需直调) |
| `transferMedia(fileUrl:completion:)` | `fileUrl: String` | `Result<Void, Error>` | 一步校验→ANI 转换→prepare→传输 |
| `transferFile(fileUri:commandType:completion:)` | `fileUri: String`, `commandType: CommandType = .transferAniVideo` | `Result<Void, Error>` | 底层文件传输 |
| `fetchLatestFirmware(sn:currentVersion:userId:completion:)` | `sn: String`, `currentVersion: String`, `userId: String` | `Result<FirmwareUpgradeCheckResult, Error>` | 对账上报 + 拉最新固件,写入 `firmwareUpgrade` |
| `upgradeFirmware(sn:fileUrl:firmwareId:fromVersion:targetVersion:completion:)` | `sn`, `fileUrl`, `firmwareId`, `fromVersion`, `targetVersion`: `String` | `Result<Void, Error>` | OTA 烧录;下发前校验实时电量,**< 40% 返回 `.batteryTooLow`,不发任何字节** |
| `tryReportPendingUpgrade(sn:currentVersion:userId:)` | `sn: String`, `currentVersion: String`, `userId: String` | 无fire-and-forget结果见 `upgradeRecords` | 对账上报本地待报记录(`fetchLatestFirmware` 已内置调用) |
#### 属性(除 `delegate` 外均只读)
| 属性 | 类型 | 说明 |
|------|------|------|
| `delegate` | `DuooomiBleSDKDelegate?` | 事件回调代理weak可读写 |
| `config` | `DuooomiBleConfig` | 初始化配置(`let` |
| `btState` | `ConnectionState` | idle / scanning / connecting / connected / disconnecting / disconnected |
| `discoveredDevices` | `[DiscoveredDevice]` | 扫描到的设备列表 |
| `connectedDevice` | `DiscoveredDevice?` | 当前连接设备 |
| `deviceInfo` | `DeviceInfo?` | 设备信息(品牌/电量/存储/loop |
| `version` | `String` | 固件版本bind 后就绪) |
| `versionLog` | `[String: Any]?` | 设备版本附带 JSON透传给 metadata 上报) |
| `isActivated` | `Bool` | 是否已绑定 |
| `sn` | `String` | 设备序列号bind 成功后就绪) |
| `transferProgress` | `Int` | 传输进度 0100 |
| `error` | `String?` | 最近错误描述 |
| `firmwareUpgrade` | `FirmwareUpgradeCheckResult?` | 最近一次 `fetchLatestFirmware` 结果 |
| `upgradeRecords` | `[UpgradeRecord]` | 本地 OTA 对账记录computed |
### 数据类型
```swift
// FirmwareUpgradeCheckResult fetchLatestFirmware sdk.firmwareUpgrade
public struct FirmwareUpgradeCheckResult {
let upgradeAvailable: Bool
let reason: FirmwareUpgradeCheckReason // UPGRADE_AVAILABLE / ALREADY_LATEST / DEVICE_NOT_FOUND ...
let currentSystemVersion: String?
let targetFirmware: TargetFirmware?
}
public struct TargetFirmware {
let id: String // firmwareId使
let version: String //
let fileUrl: String? //
let fileSize: String? //
let fileMd5: String?
}
// PlayMode
public enum PlayMode: Int {
case single = 0 //
case loop = 1 //
}
// DeviceInfo getDeviceInfo
public struct DeviceInfo {
let name: String //
let brand: String //
let size: String //
let powerlevel: Int //
let allspace: UInt64 //
let freespace: UInt64 //
let loop: PlayMode? // nil
}
// BindingResponse bind
public struct BindingResponse {
let success: Int // 1=
let sn: String //
let contents: [String] // key
}
```
## Info.plist
```xml
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth to communicate with your device</string>
```
## 技术规格
- iOS 12.2+ / Swift 5.0+ / 零依赖
- Delegate 回调模式,所有状态变化在主线程通知
- BLE 操作专用串行队列