feat(sdk): 帧间隔 frameIntervalMs 配置化(默认 35ms,[0,500] clamp)
DuooomiBleConfig 新增 frameIntervalMs,BleProtocolService 构造期注入。 顺手清死代码 FrameConstants.frameIntervalNanos、修 sendFrames 0.035 字面量 (统一走 config)。demo 加可调输入框 + UserDefaults 持久化。 越界 warn 不抛错,使用点 min/max 兜底。 设计稿:docs/superpowers/specs/2026-05-20-frame-interval-config-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ public struct DuooomiBleConfig {
|
|||||||
public let aniFps: String
|
public let aniFps: String
|
||||||
/// 扫描时按设备蓝牙广播名前缀过滤(大小写不敏感)。**必填**,由集成方约定固件广播名前缀。
|
/// 扫描时按设备蓝牙广播名前缀过滤(大小写不敏感)。**必填**,由集成方约定固件广播名前缀。
|
||||||
public let scanNamePrefix: String
|
public let scanNamePrefix: String
|
||||||
|
/// 协议帧之间的发送间隔(毫秒)。合法区间 `[0, 500]`,越界仅 warn,使用点会 clamp。
|
||||||
|
/// 集成方一般无需调整;调高用于排查接收端丢帧/拥塞。默认 35。
|
||||||
|
public let frameIntervalMs: Int
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
apiHost: URL = URL(string: "https://api.duooomi.com")!,
|
apiHost: URL = URL(string: "https://api.duooomi.com")!,
|
||||||
@@ -27,7 +30,8 @@ public struct DuooomiBleConfig {
|
|||||||
aniWidth: Int = 360,
|
aniWidth: Int = 360,
|
||||||
aniHeight: Int = 360,
|
aniHeight: Int = 360,
|
||||||
aniFps: String = "24",
|
aniFps: String = "24",
|
||||||
scanNamePrefix: String
|
scanNamePrefix: String,
|
||||||
|
frameIntervalMs: Int = 35
|
||||||
) {
|
) {
|
||||||
self.apiHost = apiHost
|
self.apiHost = apiHost
|
||||||
self.apiKey = apiKey
|
self.apiKey = apiKey
|
||||||
@@ -37,5 +41,9 @@ public struct DuooomiBleConfig {
|
|||||||
self.aniHeight = aniHeight
|
self.aniHeight = aniHeight
|
||||||
self.aniFps = aniFps
|
self.aniFps = aniFps
|
||||||
self.scanNamePrefix = scanNamePrefix
|
self.scanNamePrefix = scanNamePrefix
|
||||||
|
if frameIntervalMs < 0 || frameIntervalMs > 500 {
|
||||||
|
BleLog.w("frameIntervalMs=\(frameIntervalMs) out of [0,500], will clamp at use site", "Config")
|
||||||
|
}
|
||||||
|
self.frameIntervalMs = frameIntervalMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public final class DuooomiBleSDK: NSObject {
|
|||||||
public init(config: DuooomiBleConfig) {
|
public init(config: DuooomiBleConfig) {
|
||||||
self.config = config
|
self.config = config
|
||||||
bleClient = BleClient()
|
bleClient = BleClient()
|
||||||
protocolService = BleProtocolService(client: bleClient)
|
protocolService = BleProtocolService(client: bleClient, frameIntervalMs: config.frameIntervalMs)
|
||||||
timeProvider = BeijingTimeProvider()
|
timeProvider = BeijingTimeProvider()
|
||||||
deviceInfoService = DeviceInfoService(protocolService: protocolService, timeProvider: timeProvider)
|
deviceInfoService = DeviceInfoService(protocolService: protocolService, timeProvider: timeProvider)
|
||||||
fileTransferService = FileTransferService(protocolService: protocolService)
|
fileTransferService = FileTransferService(protocolService: protocolService)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ public enum FrameConstants {
|
|||||||
public static let maxDataSize = 496
|
public static let maxDataSize = 496
|
||||||
public static let headerSize = 8
|
public static let headerSize = 8
|
||||||
public static let footerSize = 1
|
public static let footerSize = 1
|
||||||
/// Inter-frame delay in nanoseconds (35ms)
|
|
||||||
public static let frameIntervalNanos: UInt64 = 35_000_000
|
|
||||||
public static let screenSize = 360
|
public static let screenSize = 360
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ final class BleProtocolService: BleClientDelegate {
|
|||||||
|
|
||||||
weak var delegate: BleProtocolServiceDelegate?
|
weak var delegate: BleProtocolServiceDelegate?
|
||||||
private let client: BleClient
|
private let client: BleClient
|
||||||
|
private let frameIntervalMs: Int
|
||||||
private var isListening = false
|
private var isListening = false
|
||||||
|
|
||||||
/// 分包重组会话
|
/// 分包重组会话
|
||||||
@@ -20,9 +21,10 @@ final class BleProtocolService: BleClientDelegate {
|
|||||||
|
|
||||||
private var fragments: [String: FragmentSession] = [:]
|
private var fragments: [String: FragmentSession] = [:]
|
||||||
|
|
||||||
init(client: BleClient) {
|
init(client: BleClient, frameIntervalMs: Int) {
|
||||||
self.client = client
|
self.client = client
|
||||||
BleLog.i("Protocol service created", "Protocol")
|
self.frameIntervalMs = frameIntervalMs
|
||||||
|
BleLog.i("Protocol service created (frameIntervalMs=\(frameIntervalMs))", "Protocol")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Start/Stop Listening
|
// MARK: - Start/Stop Listening
|
||||||
@@ -90,8 +92,9 @@ final class BleProtocolService: BleClientDelegate {
|
|||||||
onProgress?(Double(index + 1) / Double(total))
|
onProgress?(Double(index + 1) / Double(total))
|
||||||
|
|
||||||
if index + 1 < total {
|
if index + 1 < total {
|
||||||
// Inter-frame delay 35ms
|
// Inter-frame delay from config (clamped to [0,500]ms)
|
||||||
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.035) {
|
let delaySec = Double(min(max(self.frameIntervalMs, 0), 500)) / 1000.0
|
||||||
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + delaySec) {
|
||||||
sendFrame(at: index + 1)
|
sendFrame(at: index + 1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
@Published var configScanPrefix: String {
|
@Published var configScanPrefix: String {
|
||||||
didSet { defaults.set(configScanPrefix, forKey: Self.keyScanPrefix) }
|
didSet { defaults.set(configScanPrefix, forKey: Self.keyScanPrefix) }
|
||||||
}
|
}
|
||||||
|
@Published var configFrameIntervalMs: String {
|
||||||
|
didSet { defaults.set(configFrameIntervalMs, forKey: Self.keyFrameIntervalMs) }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - SDK State (driven by delegate)
|
// MARK: - SDK State (driven by delegate)
|
||||||
|
|
||||||
@@ -50,26 +53,31 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
private static let keyBrand = "demo.config.brand"
|
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"
|
||||||
|
private static let keyFrameIntervalMs = "demo.config.frameIntervalMs"
|
||||||
|
|
||||||
/// demo 调试默认值(仅当 UserDefaults 没存过时使用)。秘钥从 DemoSecrets 读,避免泄露到仓库。
|
/// demo 调试默认值(仅当 UserDefaults 没存过时使用)。秘钥从 DemoSecrets 读,避免泄露到仓库。
|
||||||
private static let defaultBrand = DemoSecrets.brand
|
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
|
||||||
|
private static let defaultFrameIntervalMs = "35"
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
let d = UserDefaults.standard
|
let d = UserDefaults.standard
|
||||||
let brand = d.string(forKey: Self.keyBrand) ?? Self.defaultBrand
|
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
|
||||||
|
let frameIntervalMs = d.string(forKey: Self.keyFrameIntervalMs) ?? Self.defaultFrameIntervalMs
|
||||||
|
|
||||||
self.configBrand = brand
|
self.configBrand = brand
|
||||||
self.configApiKey = apiKey
|
self.configApiKey = apiKey
|
||||||
self.configScanPrefix = scanPrefix
|
self.configScanPrefix = scanPrefix
|
||||||
|
self.configFrameIntervalMs = frameIntervalMs
|
||||||
|
|
||||||
self.sdk = DuooomiBleSDK(config: .init(
|
self.sdk = DuooomiBleSDK(config: .init(
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
brand: brand,
|
brand: brand,
|
||||||
scanNamePrefix: scanPrefix
|
scanNamePrefix: scanPrefix,
|
||||||
|
frameIntervalMs: Int(frameIntervalMs) ?? 35
|
||||||
))
|
))
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@@ -83,7 +91,8 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
|
|||||||
let new = DuooomiBleSDK(config: .init(
|
let new = DuooomiBleSDK(config: .init(
|
||||||
apiKey: configApiKey,
|
apiKey: configApiKey,
|
||||||
brand: configBrand,
|
brand: configBrand,
|
||||||
scanNamePrefix: configScanPrefix
|
scanNamePrefix: configScanPrefix,
|
||||||
|
frameIntervalMs: Int(configFrameIntervalMs) ?? 35
|
||||||
))
|
))
|
||||||
new.delegate = self
|
new.delegate = self
|
||||||
self.sdk = new
|
self.sdk = new
|
||||||
|
|||||||
@@ -88,9 +88,15 @@ struct WrapperTestView: View {
|
|||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
}
|
}
|
||||||
|
HStack {
|
||||||
|
Text("frameMs").frame(width: 90, alignment: .leading)
|
||||||
|
TextField("35", text: $viewModel.configFrameIntervalMs)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
}
|
||||||
Button("Reload SDK") {
|
Button("Reload SDK") {
|
||||||
viewModel.reloadSDK()
|
viewModel.reloadSDK()
|
||||||
log("SDK reloaded with brand=\(viewModel.configBrand), prefix=\(viewModel.configScanPrefix)", level: .success)
|
log("SDK reloaded with brand=\(viewModel.configBrand), prefix=\(viewModel.configScanPrefix), frameMs=\(viewModel.configFrameIntervalMs)", level: .success)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user