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:
km2023
2026-05-20 14:02:04 +08:00
parent b967471532
commit 09088121b5
6 changed files with 35 additions and 11 deletions

View File

@@ -18,6 +18,9 @@ public struct DuooomiBleConfig {
public let aniFps: String
/// 广****广
public let scanNamePrefix: String
/// `[0, 500]` warn使 clamp
/// / 35
public let frameIntervalMs: Int
public init(
apiHost: URL = URL(string: "https://api.duooomi.com")!,
@@ -27,7 +30,8 @@ public struct DuooomiBleConfig {
aniWidth: Int = 360,
aniHeight: Int = 360,
aniFps: String = "24",
scanNamePrefix: String
scanNamePrefix: String,
frameIntervalMs: Int = 35
) {
self.apiHost = apiHost
self.apiKey = apiKey
@@ -37,5 +41,9 @@ public struct DuooomiBleConfig {
self.aniHeight = aniHeight
self.aniFps = aniFps
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
}
}

View File

@@ -110,7 +110,7 @@ public final class DuooomiBleSDK: NSObject {
public init(config: DuooomiBleConfig) {
self.config = config
bleClient = BleClient()
protocolService = BleProtocolService(client: bleClient)
protocolService = BleProtocolService(client: bleClient, frameIntervalMs: config.frameIntervalMs)
timeProvider = BeijingTimeProvider()
deviceInfoService = DeviceInfoService(protocolService: protocolService, timeProvider: timeProvider)
fileTransferService = FileTransferService(protocolService: protocolService)

View File

@@ -16,8 +16,6 @@ public enum FrameConstants {
public static let maxDataSize = 496
public static let headerSize = 8
public static let footerSize = 1
/// Inter-frame delay in nanoseconds (35ms)
public static let frameIntervalNanos: UInt64 = 35_000_000
public static let screenSize = 360
}

View File

@@ -10,6 +10,7 @@ final class BleProtocolService: BleClientDelegate {
weak var delegate: BleProtocolServiceDelegate?
private let client: BleClient
private let frameIntervalMs: Int
private var isListening = false
///
@@ -20,9 +21,10 @@ final class BleProtocolService: BleClientDelegate {
private var fragments: [String: FragmentSession] = [:]
init(client: BleClient) {
init(client: BleClient, frameIntervalMs: Int) {
self.client = client
BleLog.i("Protocol service created", "Protocol")
self.frameIntervalMs = frameIntervalMs
BleLog.i("Protocol service created (frameIntervalMs=\(frameIntervalMs))", "Protocol")
}
// MARK: - Start/Stop Listening
@@ -90,8 +92,9 @@ final class BleProtocolService: BleClientDelegate {
onProgress?(Double(index + 1) / Double(total))
if index + 1 < total {
// Inter-frame delay 35ms
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.035) {
// Inter-frame delay from config (clamped to [0,500]ms)
let delaySec = Double(min(max(self.frameIntervalMs, 0), 500)) / 1000.0
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + delaySec) {
sendFrame(at: index + 1)
}
} else {

View File

@@ -29,6 +29,9 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
@Published var configScanPrefix: String {
didSet { defaults.set(configScanPrefix, forKey: Self.keyScanPrefix) }
}
@Published var configFrameIntervalMs: String {
didSet { defaults.set(configFrameIntervalMs, forKey: Self.keyFrameIntervalMs) }
}
// 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 keyApiKey = "demo.config.apiKey"
private static let keyScanPrefix = "demo.config.scanPrefix"
private static let keyFrameIntervalMs = "demo.config.frameIntervalMs"
/// demo UserDefaults 使 DemoSecrets
private static let defaultBrand = DemoSecrets.brand
private static let defaultApiKey = DemoSecrets.apiKey
private static let defaultScanPrefix = DemoSecrets.scanNamePrefix
private static let defaultFrameIntervalMs = "35"
override init() {
let d = UserDefaults.standard
let brand = d.string(forKey: Self.keyBrand) ?? Self.defaultBrand
let apiKey = d.string(forKey: Self.keyApiKey) ?? Self.defaultApiKey
let scanPrefix = d.string(forKey: Self.keyScanPrefix) ?? Self.defaultScanPrefix
let frameIntervalMs = d.string(forKey: Self.keyFrameIntervalMs) ?? Self.defaultFrameIntervalMs
self.configBrand = brand
self.configApiKey = apiKey
self.configScanPrefix = scanPrefix
self.configFrameIntervalMs = frameIntervalMs
self.sdk = DuooomiBleSDK(config: .init(
apiKey: apiKey,
brand: brand,
scanNamePrefix: scanPrefix
scanNamePrefix: scanPrefix,
frameIntervalMs: Int(frameIntervalMs) ?? 35
))
super.init()
@@ -83,7 +91,8 @@ final class DemoViewModel: NSObject, ObservableObject, DuooomiBleSDKDelegate {
let new = DuooomiBleSDK(config: .init(
apiKey: configApiKey,
brand: configBrand,
scanNamePrefix: configScanPrefix
scanNamePrefix: configScanPrefix,
frameIntervalMs: Int(configFrameIntervalMs) ?? 35
))
new.delegate = self
self.sdk = new

View File

@@ -88,9 +88,15 @@ struct WrapperTestView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
}
HStack {
Text("frameMs").frame(width: 90, alignment: .leading)
TextField("35", text: $viewModel.configFrameIntervalMs)
.textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)
}
Button("Reload SDK") {
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)
}