feat(sdk): 重命名 owner→brand 并在硬绑前校验设备品牌
- DuooomiBleConfig.owner → brand(语义:设备品牌名),HTTP header x-owner 不变 - bind() 前置 verifyBrand:缺 deviceInfo 先 getDeviceInfo,与 config.brand 严格匹配,不一致主动断连并抛 .brandMismatch - README 抹除软绑/对账/UpgradeRecord 等内部实现细节,仅保留厂商面向的公开 API(功能代码完全保留) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
103
README.md
103
README.md
@@ -23,7 +23,7 @@ dependencies: [
|
|||||||
|
|
||||||
## Demo 初始化(仓库内 demo 目录)
|
## Demo 初始化(仓库内 demo 目录)
|
||||||
|
|
||||||
`apiKey` / `owner` / `scanNamePrefix` 不进 git,由本地 `demo/Sources/DemoSecrets.swift` 提供:
|
`apiKey` / `brand` / `scanNamePrefix` 不进 git,由本地 `demo/Sources/DemoSecrets.swift` 提供:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp demo/Sources/DemoSecrets.template.swift demo/Sources/DemoSecrets.swift
|
cp demo/Sources/DemoSecrets.template.swift demo/Sources/DemoSecrets.swift
|
||||||
@@ -39,7 +39,7 @@ open demo.xcworkspace
|
|||||||
```swift
|
```swift
|
||||||
let sdk = DuooomiBleSDK(config: .init(
|
let sdk = DuooomiBleSDK(config: .init(
|
||||||
apiKey: "your-api-key", // 必填,Loomart x-api-key
|
apiKey: "your-api-key", // 必填,Loomart x-api-key
|
||||||
owner: "your-owner", // 必填,所有 RPC 必带 x-owner header
|
brand: "your-brand", // 必填,设备品牌名(所有 RPC 必带 x-owner header)
|
||||||
scanNamePrefix: "蓝牙广播名前缀" // 必填,大小写不敏感
|
scanNamePrefix: "蓝牙广播名前缀" // 必填,大小写不敏感
|
||||||
// apiHost: URL(...)!, // 默认 https://api.duooomi.com
|
// apiHost: URL(...)!, // 默认 https://api.duooomi.com
|
||||||
// cdnHost: "https://cdn.bowong.cc/", // 默认
|
// cdnHost: "https://cdn.bowong.cc/", // 默认
|
||||||
@@ -51,29 +51,21 @@ sdk.delegate = self
|
|||||||
|
|
||||||
## SDK 能力范围
|
## SDK 能力范围
|
||||||
|
|
||||||
- **内置**:BLE 协议(扫描/连接/硬绑/硬解/播放模式/文件传输/OTA 烧录)+ 软绑(POST `/api/auth/loomart/shipment-sn/device/bind`)+ 软解绑 + 升级对账上报 + 服务端拉最新固件 + 北京时间获取
|
- **内置**:BLE 协议(扫描/连接/绑定/解绑/播放模式/文件传输/OTA 烧录)+ 固件版本检查
|
||||||
- **不内置**:UI、retry 策略;SDK 只提供原子方法和状态属性,由集成方组合
|
- **不内置**:UI、retry 策略;SDK 只提供原子方法和状态属性,由集成方组合
|
||||||
|
|
||||||
### 绑定与固件升级流程
|
### 绑定与固件升级流程
|
||||||
|
|
||||||
绑定按 BLE 链路 / 云端登记拆两步:
|
1. **`bind(userId:)`** — 绑定设备,完成后 `sn` / `version` / `isActivated` 就绪。
|
||||||
|
2. **`fetchLatestFirmware(sn:currentVersion:userId:)`** — 拉服务端最新固件信息,写入 `firmwareUpgrade`。
|
||||||
|
3. **`upgradeFirmware(...)`** — 烧录固件包,进度通过 `didUpdateProgress` 回调。
|
||||||
|
|
||||||
1. **`bind(userId:)`** — 硬绑(BLE 0x0F)+ 同步 `getDeviceVersion`。完成后 `sn` / `version` / `isActivated` 就绪。
|
解绑:`unbind(userId:)` 一步完成。
|
||||||
2. **`fetchLatestFirmware(sn:currentVersion:userId:)`** — 内部串:
|
|
||||||
1. 对账上报本地 pending `UpgradeRecord`(容错失败)
|
|
||||||
2. 软绑 `shipment-sn`(失败 → 自动硬解绑回滚 + 重置 `isActivated`/`sn`,整个 fetch 失败)
|
|
||||||
3. 拉服务端最新固件 → 写入 `firmwareUpgrade`
|
|
||||||
|
|
||||||
升级烧录走 `upgradeFirmware(sn:fileUrl:firmwareId:fromVersion:targetVersion:)`,烧录完自动落 `UpgradeRecord`。
|
|
||||||
|
|
||||||
解绑只一步:`unbind(userId:)` 内部硬解 + 软解(软解失败仅 warn 不阻断)。
|
|
||||||
|
|
||||||
### API 调用风格
|
### API 调用风格
|
||||||
|
|
||||||
所有公开方法**显式传入参**,不依赖 SDK 内部状态自动取参。调用方从 SDK 只读属性(`sdk.sn` / `sdk.version` / `sdk.firmwareUpgrade?.targetFirmware?.*` 等)组装参数后传入。
|
所有公开方法**显式传入参**,不依赖 SDK 内部状态自动取参。调用方从 SDK 只读属性(`sdk.sn` / `sdk.version` / `sdk.firmwareUpgrade?.targetFirmware?.*` 等)组装参数后传入。
|
||||||
|
|
||||||
解绑只一步:`unbind(userId:)` 内部硬解 + 软解(软解失败仅 warn 不阻断)。
|
|
||||||
|
|
||||||
## 代理(Delegate)
|
## 代理(Delegate)
|
||||||
|
|
||||||
通过 `DuooomiBleSDKDelegate` 接收状态变化,所有方法在主线程调用,均有默认空实现:
|
通过 `DuooomiBleSDKDelegate` 接收状态变化,所有方法在主线程调用,均有默认空实现:
|
||||||
@@ -132,7 +124,7 @@ sdk.getVersion { result in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 硬绑 + getVersion;不含软绑/OTA 检查
|
// 绑定设备 + 同步拉版本
|
||||||
sdk.bind(userId: "user-id") { result in
|
sdk.bind(userId: "user-id") { result in
|
||||||
if case .success(let resp) = result {
|
if case .success(let resp) = result {
|
||||||
print("SN: \(resp.sn), Files: \(resp.contents.count)")
|
print("SN: \(resp.sn), Files: \(resp.contents.count)")
|
||||||
@@ -140,7 +132,7 @@ sdk.bind(userId: "user-id") { result in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 硬解 + 软解(一步完成)
|
// 解绑
|
||||||
sdk.unbind(userId: "user-id") { result in /* ... */ }
|
sdk.unbind(userId: "user-id") { result in /* ... */ }
|
||||||
sdk.deleteFile(key: "file-key") { result in /* ... */ }
|
sdk.deleteFile(key: "file-key") { result in /* ... */ }
|
||||||
```
|
```
|
||||||
@@ -189,36 +181,21 @@ sdk.transferMedia(fileUrl: "https://example.com/video.mp4") { result in
|
|||||||
|
|
||||||
### 固件升级
|
### 固件升级
|
||||||
|
|
||||||
完整流程:**扫描 → 连接 → bind → fetchLatestFirmware(含对账上报)→ upgradeFirmware(落 record)→ 重连后再次 fetchLatestFirmware 触发对账**。
|
完整流程:**扫描 → 连接 → bind → fetchLatestFirmware → upgradeFirmware**。设备重启重连后再次走一遍即可。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// ─── 0. 接收升级记录变化(用来观察对账上报结果) ───
|
|
||||||
extension MyController: DuooomiBleSDKDelegate {
|
|
||||||
func sdk(_ sdk: DuooomiBleSDK, didUpdateUpgradeRecords records: [UpgradeRecord]) {
|
|
||||||
for r in records {
|
|
||||||
switch (r.reported, r.outcome) {
|
|
||||||
case (false, _): print("待上报 sn=\(r.sn) fw=\(r.firmwareId)")
|
|
||||||
case (true, .success): print("✅ 正确上报 \(r.fromVersion)→\(r.targetVersion)")
|
|
||||||
case (true, .versionMismatch): print("❌ 错误上报 versionMismatch")
|
|
||||||
case (true, nil): print("过期放弃 (>30 天)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sdk.delegate = self
|
|
||||||
|
|
||||||
// ─── 1. 扫描 + 连接 ───
|
// ─── 1. 扫描 + 连接 ───
|
||||||
sdk.scan()
|
sdk.scan()
|
||||||
// 在 didUpdateDevices 拿到列表后选一台 connect
|
// 在 didUpdateDevices 拿到列表后选一台 connect
|
||||||
sdk.connect(deviceId: device.id) { result in
|
sdk.connect(deviceId: device.id) { result in
|
||||||
guard case .success = result else { return }
|
guard case .success = result else { return }
|
||||||
|
|
||||||
// ─── 2. 硬绑(BLE 0x0F + 同步 getVersion)───
|
// ─── 2. 绑定 ───
|
||||||
sdk.bind(userId: "user-123") { result in
|
sdk.bind(userId: "user-123") { result in
|
||||||
guard case .success(let resp) = result else { return }
|
guard case .success(let resp) = result else { return }
|
||||||
// 此时 sdk.sn = resp.sn / sdk.version 已就绪 / sdk.isActivated = true
|
// 此时 sdk.sn = resp.sn / sdk.version 已就绪 / sdk.isActivated = true
|
||||||
|
|
||||||
// ─── 3. 拉最新固件(内部串:对账上报 → 软绑 → checkUpgrade)───
|
// ─── 3. 拉最新固件信息 ───
|
||||||
sdk.fetchLatestFirmware(
|
sdk.fetchLatestFirmware(
|
||||||
sn: sdk.sn,
|
sn: sdk.sn,
|
||||||
currentVersion: sdk.version,
|
currentVersion: sdk.version,
|
||||||
@@ -226,7 +203,6 @@ sdk.connect(deviceId: device.id) { result in
|
|||||||
) { result in
|
) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .failure(let err):
|
case .failure(let err):
|
||||||
// 软绑失败已自动硬解回滚,可提示重试
|
|
||||||
print("拉取失败:\(err.localizedDescription)")
|
print("拉取失败:\(err.localizedDescription)")
|
||||||
return
|
return
|
||||||
case .success(let info):
|
case .success(let info):
|
||||||
@@ -237,12 +213,7 @@ sdk.connect(deviceId: device.id) { result in
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 4. 强制升级判定(可选)───
|
// ─── 4. 烧录 ───
|
||||||
if target.forceUpgrade == true {
|
|
||||||
// 弹不可关闭弹窗
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 5. 烧录(完成后自动落 UpgradeRecord) ───
|
|
||||||
sdk.upgradeFirmware(
|
sdk.upgradeFirmware(
|
||||||
sn: sdk.sn,
|
sn: sdk.sn,
|
||||||
fileUrl: fileUrl,
|
fileUrl: fileUrl,
|
||||||
@@ -252,8 +223,7 @@ sdk.connect(deviceId: device.id) { result in
|
|||||||
) { result in
|
) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
// OTA 包发完,设备开始烧录并重启。
|
// OTA 包发完,设备开始烧录并重启
|
||||||
// UpgradeRecord 已落本地(reported=false),等设备重启 + 重连。
|
|
||||||
print("烧录命令完成,等待设备重启")
|
print("烧录命令完成,等待设备重启")
|
||||||
case .failure(let err):
|
case .failure(let err):
|
||||||
print("烧录失败:\(err.localizedDescription)")
|
print("烧录失败:\(err.localizedDescription)")
|
||||||
@@ -263,39 +233,8 @@ sdk.connect(deviceId: device.id) { result in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 6. 设备重启 + 重连后,再次走完整流程 ───
|
|
||||||
// 重新 scan/connect/bind 完成后,调一次 fetchLatestFirmware:
|
|
||||||
// 内部 tryReportPendingUpgrade 会拿当前 version 和上次 record.targetVersion 对账,
|
|
||||||
// 命中 → report-success,不命中 → report-failure(VERSION_MISMATCH ...)。
|
|
||||||
// 结果通过 didUpdateUpgradeRecords 主动通知(参见步骤 0)。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 升级对账上报机制
|
|
||||||
|
|
||||||
- **何时落记录**:`upgradeFirmware(...)` 烧录命令发完后,SDK 自动 upsert 一条 `UpgradeRecord` 到 `UserDefaults`(复合主键 `(sn, firmwareId)`),初始 `reported=false`。
|
|
||||||
- **何时触发对账**:每次 `fetchLatestFirmware(...)` 进入时,第一步就跑 `tryReportPendingUpgrade`。
|
|
||||||
- **判定规则**:
|
|
||||||
- `currentVersion == record.targetVersion` → `POST report-success` → `outcome=.success`
|
|
||||||
- `currentVersion != record.targetVersion` → `POST report-failure(VERSION_MISMATCH expected=... actual=...)` → `outcome=.versionMismatch`
|
|
||||||
- `now - triggeredAt > 30 天` → 标记 `outcome=nil` 不再尝试,保留为审计轨迹
|
|
||||||
- HTTP 失败 → 保持 `reported=false`,下次 fetchLatestFirmware 再试
|
|
||||||
- **响应回调**:`DuooomiBleSDKDelegate.didUpdateUpgradeRecords` 在每次 record 状态变化时主线程触发,完整 records 数组。
|
|
||||||
- **手动触发**(仅调试用):
|
|
||||||
|
|
||||||
```swift
|
|
||||||
sdk.tryReportPendingUpgrade(sn: sdk.sn, currentVersion: sdk.version, userId: "user-123")
|
|
||||||
```
|
|
||||||
|
|
||||||
### UpgradeRecord 状态判定
|
|
||||||
|
|
||||||
| `reported` | `outcome` | 含义 |
|
|
||||||
|---|---|---|
|
|
||||||
| `false` | `nil` | 待上报(HTTP 失败/未跑),下次 fetchLatestFirmware 重试 |
|
|
||||||
| `true` | `.success` | ✅ 正确上报:版本命中,业务对账成功 |
|
|
||||||
| `true` | `.versionMismatch` | ❌ 错误上报:设备版本与目标不符,烧录可能失败 |
|
|
||||||
| `true` | `nil` | 过期放弃:30 天 TTL 触发,保留作审计 |
|
|
||||||
|
|
||||||
### 可读取状态
|
### 可读取状态
|
||||||
|
|
||||||
| 属性 | 类型 | 说明 |
|
| 属性 | 类型 | 说明 |
|
||||||
@@ -328,18 +267,6 @@ public struct TargetFirmware {
|
|||||||
let fileMd5: String?
|
let fileMd5: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpgradeRecord — 本地持久化的 OTA 烧录记录(用 UserDefaults,集成方一般不用直接读)
|
|
||||||
public struct UpgradeRecord {
|
|
||||||
let sn: String
|
|
||||||
let firmwareId: String
|
|
||||||
let fromVersion: String
|
|
||||||
let targetVersion: String
|
|
||||||
let triggeredAt: Int64 // 毫秒时间戳
|
|
||||||
let reported: Bool
|
|
||||||
let reportedAt: Int64?
|
|
||||||
let outcome: UpgradeOutcome? // .success / .versionMismatch / nil(TTL 过期)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayMode — 播放模式
|
// PlayMode — 播放模式
|
||||||
public enum PlayMode: Int {
|
public enum PlayMode: Int {
|
||||||
case single = 0 // 单播
|
case single = 0 // 单播
|
||||||
@@ -348,7 +275,7 @@ public enum PlayMode: Int {
|
|||||||
|
|
||||||
// DeviceInfo — getDeviceInfo 返回
|
// DeviceInfo — getDeviceInfo 返回
|
||||||
public struct DeviceInfo {
|
public struct DeviceInfo {
|
||||||
let name: String // 设备名
|
let name: String // 产品名
|
||||||
let brand: String // 品牌
|
let brand: String // 品牌
|
||||||
let size: String // 屏幕尺寸
|
let size: String // 屏幕尺寸
|
||||||
let powerlevel: Int // 电量百分比
|
let powerlevel: Int // 电量百分比
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public enum DuooomiBleError: LocalizedError {
|
|||||||
case permissionDenied
|
case permissionDenied
|
||||||
case unsupportedFormat(String)
|
case unsupportedFormat(String)
|
||||||
case firmwareUpgradeUnavailable(String)
|
case firmwareUpgradeUnavailable(String)
|
||||||
|
case brandMismatch(expected: String, actual: String)
|
||||||
|
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -76,6 +77,8 @@ public enum DuooomiBleError: LocalizedError {
|
|||||||
return "Unsupported file format: \(ext). Supported: jpeg, png, gif, mp4, webm, webp"
|
return "Unsupported file format: \(ext). Supported: jpeg, png, gif, mp4, webm, webp"
|
||||||
case .firmwareUpgradeUnavailable(let reason):
|
case .firmwareUpgradeUnavailable(let reason):
|
||||||
return "Firmware upgrade unavailable: \(reason)"
|
return "Firmware upgrade unavailable: \(reason)"
|
||||||
|
case .brandMismatch(let expected, let actual):
|
||||||
|
return "不支持该品牌设备 (expected=\(expected), actual=\(actual))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ public struct DuooomiBleConfig {
|
|||||||
public let apiHost: URL
|
public let apiHost: URL
|
||||||
/// API 鉴权 key(Loomart x-api-key)
|
/// API 鉴权 key(Loomart x-api-key)
|
||||||
public let apiKey: String
|
public let apiKey: String
|
||||||
/// 多租户 owner 标识,所有 RPC 必带 `x-owner` header
|
/// 设备品牌名(所有 RPC 必带 `x-owner` header)
|
||||||
public let owner: String
|
public let brand: String
|
||||||
/// CDN 资源地址(末尾带 `/`)
|
/// CDN 资源地址(末尾带 `/`)
|
||||||
public let cdnHost: String
|
public let cdnHost: String
|
||||||
/// ANI 转换宽度(像素)
|
/// ANI 转换宽度(像素)
|
||||||
@@ -22,7 +22,7 @@ public struct DuooomiBleConfig {
|
|||||||
public init(
|
public init(
|
||||||
apiHost: URL = URL(string: "https://api.duooomi.com")!,
|
apiHost: URL = URL(string: "https://api.duooomi.com")!,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
owner: String,
|
brand: String,
|
||||||
cdnHost: String = "https://cdn.bowong.cc/",
|
cdnHost: String = "https://cdn.bowong.cc/",
|
||||||
aniWidth: Int = 360,
|
aniWidth: Int = 360,
|
||||||
aniHeight: Int = 360,
|
aniHeight: Int = 360,
|
||||||
@@ -31,7 +31,7 @@ public struct DuooomiBleConfig {
|
|||||||
) {
|
) {
|
||||||
self.apiHost = apiHost
|
self.apiHost = apiHost
|
||||||
self.apiKey = apiKey
|
self.apiKey = apiKey
|
||||||
self.owner = owner
|
self.brand = brand
|
||||||
self.cdnHost = cdnHost.hasSuffix("/") ? cdnHost : cdnHost + "/"
|
self.cdnHost = cdnHost.hasSuffix("/") ? cdnHost : cdnHost + "/"
|
||||||
self.aniWidth = aniWidth
|
self.aniWidth = aniWidth
|
||||||
self.aniHeight = aniHeight
|
self.aniHeight = aniHeight
|
||||||
|
|||||||
@@ -22,6 +22,48 @@ extension DuooomiBleSDK {
|
|||||||
/// - Note: `bind` / `unbind` / `setPlayMode` 共享 `0x0F` / `0x12` opId 通道,**不能并发**。
|
/// - Note: `bind` / `unbind` / `setPlayMode` 共享 `0x0F` / `0x12` opId 通道,**不能并发**。
|
||||||
public func bind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
|
public func bind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
|
||||||
guard ensureConnected(completion: completion) else { return }
|
guard ensureConnected(completion: completion) else { return }
|
||||||
|
verifyBrand { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .failure(let err):
|
||||||
|
completion(.failure(err))
|
||||||
|
case .success:
|
||||||
|
self.performHardBind(userId: userId, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验设备 brand 与 SDK 配置 brand 是否一致;缺失则先拉一次 deviceInfo。
|
||||||
|
/// 不一致时主动断连并回调 `.brandMismatch`。
|
||||||
|
private func verifyBrand(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
if let cached = deviceInfo {
|
||||||
|
checkBrand(actual: cached.brand, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getDeviceInfo { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .failure(let err):
|
||||||
|
completion(.failure(err))
|
||||||
|
case .success(let info):
|
||||||
|
self.checkBrand(actual: info.brand, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkBrand(actual: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
let expected = config.brand
|
||||||
|
if actual == expected {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BleLog.w("Brand mismatch: expected=\(expected), actual=\(actual); disconnecting", "Command")
|
||||||
|
disconnect { _ in
|
||||||
|
completion(.failure(DuooomiBleError.brandMismatch(expected: expected, actual: actual)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performHardBind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
|
||||||
BleLog.d("Sending bind (userId=\(userId))", "Command")
|
BleLog.d("Sending bind (userId=\(userId))", "Command")
|
||||||
|
|
||||||
sendAsyncAndWait(commandType: .bindDevice, completion: { [weak self] result in
|
sendAsyncAndWait(commandType: .bindDevice, completion: { [weak self] result in
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public final class DuooomiBleSDK: NSObject {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
protocolService.delegate = self
|
protocolService.delegate = self
|
||||||
BleLog.i("SDK initialized (apiHost=\(config.apiHost), owner=\(config.owner))", "SDK")
|
BleLog.i("SDK initialized (apiHost=\(config.apiHost), brand=\(config.brand))", "SDK")
|
||||||
setupDisconnectHandler()
|
setupDisconnectHandler()
|
||||||
|
|
||||||
// UpgradeRecord 变化(含对账上报结果)→ delegate 通知
|
// UpgradeRecord 变化(含对账上报结果)→ delegate 通知
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ final class FirmwareUpgradeService {
|
|||||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
|
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
|
||||||
req.setValue(config.owner, forHTTPHeaderField: "x-owner")
|
req.setValue(config.brand, forHTTPHeaderField: "x-owner")
|
||||||
do {
|
do {
|
||||||
req.httpBody = try JSONEncoder().encode(body)
|
req.httpBody = try JSONEncoder().encode(body)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ final class ShipmentSnDeviceService {
|
|||||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
|
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
|
||||||
req.setValue(config.owner, forHTTPHeaderField: "x-owner")
|
req.setValue(config.brand, forHTTPHeaderField: "x-owner")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
req.httpBody = try JSONEncoder().encode(body)
|
req.httpBody = try JSONEncoder().encode(body)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ final class BindPayloadEncodingTests: XCTestCase {
|
|||||||
|
|
||||||
final class SetPlayModeGuardTests: XCTestCase {
|
final class SetPlayModeGuardTests: XCTestCase {
|
||||||
private func makeSDK() -> DuooomiBleSDK {
|
private func makeSDK() -> DuooomiBleSDK {
|
||||||
DuooomiBleSDK(config: DuooomiBleConfig(apiKey: "test", owner: "test-owner", scanNamePrefix: "Duooomi-"))
|
DuooomiBleSDK(config: DuooomiBleConfig(apiKey: "test", brand: "test-brand", scanNamePrefix: "Duooomi-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSetPlayModeFailsWhenNotConnected() {
|
func testSetPlayModeFailsWhenNotConnected() {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
|
|
||||||
// MARK: - SDK Config(demo 内可改,持久化到 UserDefaults)
|
// MARK: - SDK Config(demo 内可改,持久化到 UserDefaults)
|
||||||
|
|
||||||
@Published var configOwner: String {
|
@Published var configBrand: String {
|
||||||
didSet { defaults.set(configOwner, forKey: Self.keyOwner) }
|
didSet { defaults.set(configBrand, forKey: Self.keyBrand) }
|
||||||
}
|
}
|
||||||
@Published var configApiKey: String {
|
@Published var configApiKey: String {
|
||||||
didSet { defaults.set(configApiKey, forKey: Self.keyApiKey) }
|
didSet { defaults.set(configApiKey, forKey: Self.keyApiKey) }
|
||||||
@@ -47,28 +47,28 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
// MARK: - Internals
|
// MARK: - Internals
|
||||||
|
|
||||||
private let defaults = UserDefaults.standard
|
private let defaults = UserDefaults.standard
|
||||||
private static let keyOwner = "demo.config.owner"
|
private static let keyBrand = "demo.config.brand"
|
||||||
private static let keyApiKey = "demo.config.apiKey"
|
private static let keyApiKey = "demo.config.apiKey"
|
||||||
private static let keyScanPrefix = "demo.config.scanPrefix"
|
private static let keyScanPrefix = "demo.config.scanPrefix"
|
||||||
|
|
||||||
/// demo 调试默认值(仅当 UserDefaults 没存过时使用)。秘钥从 DemoSecrets 读,避免泄露到仓库。
|
/// demo 调试默认值(仅当 UserDefaults 没存过时使用)。秘钥从 DemoSecrets 读,避免泄露到仓库。
|
||||||
private static let defaultOwner = DemoSecrets.owner
|
private static let defaultBrand = DemoSecrets.brand
|
||||||
private static let defaultApiKey = DemoSecrets.apiKey
|
private static let defaultApiKey = DemoSecrets.apiKey
|
||||||
private static let defaultScanPrefix = DemoSecrets.scanNamePrefix
|
private static let defaultScanPrefix = DemoSecrets.scanNamePrefix
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
let d = UserDefaults.standard
|
let d = UserDefaults.standard
|
||||||
let owner = d.string(forKey: Self.keyOwner) ?? Self.defaultOwner
|
let brand = d.string(forKey: Self.keyBrand) ?? Self.defaultBrand
|
||||||
let apiKey = d.string(forKey: Self.keyApiKey) ?? Self.defaultApiKey
|
let apiKey = d.string(forKey: Self.keyApiKey) ?? Self.defaultApiKey
|
||||||
let scanPrefix = d.string(forKey: Self.keyScanPrefix) ?? Self.defaultScanPrefix
|
let scanPrefix = d.string(forKey: Self.keyScanPrefix) ?? Self.defaultScanPrefix
|
||||||
|
|
||||||
self.configOwner = owner
|
self.configBrand = brand
|
||||||
self.configApiKey = apiKey
|
self.configApiKey = apiKey
|
||||||
self.configScanPrefix = scanPrefix
|
self.configScanPrefix = scanPrefix
|
||||||
|
|
||||||
self.sdk = DuooomiBleSDK(config: .init(
|
self.sdk = DuooomiBleSDK(config: .init(
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
owner: owner,
|
brand: brand,
|
||||||
scanNamePrefix: scanPrefix
|
scanNamePrefix: scanPrefix
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
func reloadSDK() {
|
func reloadSDK() {
|
||||||
let new = DuooomiBleSDK(config: .init(
|
let new = DuooomiBleSDK(config: .init(
|
||||||
apiKey: configApiKey,
|
apiKey: configApiKey,
|
||||||
owner: configOwner,
|
brand: configBrand,
|
||||||
scanNamePrefix: configScanPrefix
|
scanNamePrefix: configScanPrefix
|
||||||
))
|
))
|
||||||
new.delegate = self
|
new.delegate = self
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
//
|
//
|
||||||
// 首次 clone 后请:
|
// 首次 clone 后请:
|
||||||
// cp demo/Sources/DemoSecrets.template.swift demo/Sources/DemoSecrets.swift
|
// cp demo/Sources/DemoSecrets.template.swift demo/Sources/DemoSecrets.swift
|
||||||
// 然后编辑 DemoSecrets.swift 填入真实 apiKey / owner。
|
// 然后编辑 DemoSecrets.swift 填入真实 apiKey / brand。
|
||||||
//
|
//
|
||||||
// `demo/Sources/DemoSecrets.swift` 已加入 .gitignore,本地修改不会被 commit。
|
// `demo/Sources/DemoSecrets.swift` 已加入 .gitignore,本地修改不会被 commit。
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum DemoSecrets {
|
enum DemoSecrets {
|
||||||
static let apiKey: String = ""
|
static let apiKey: String = ""
|
||||||
static let owner: String = "xmly"
|
static let brand: String = "xmly"
|
||||||
static let scanNamePrefix: String = "Duooomi-"
|
static let scanNamePrefix: String = "Duooomi-"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ struct WrapperTestView: View {
|
|||||||
private var configSection: some View {
|
private var configSection: some View {
|
||||||
Section("SDK Config(修改后点 Reload 生效)") {
|
Section("SDK Config(修改后点 Reload 生效)") {
|
||||||
HStack {
|
HStack {
|
||||||
Text("x-owner").frame(width: 90, alignment: .leading)
|
Text("brand").frame(width: 90, alignment: .leading)
|
||||||
TextField("owner", text: $viewModel.configOwner)
|
TextField("brand", text: $viewModel.configBrand)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
@@ -90,7 +90,7 @@ struct WrapperTestView: View {
|
|||||||
}
|
}
|
||||||
Button("Reload SDK") {
|
Button("Reload SDK") {
|
||||||
viewModel.reloadSDK()
|
viewModel.reloadSDK()
|
||||||
log("SDK reloaded with owner=\(viewModel.configOwner), prefix=\(viewModel.configScanPrefix)", level: .success)
|
log("SDK reloaded with brand=\(viewModel.configBrand), prefix=\(viewModel.configScanPrefix)", level: .success)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user