Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/Services/BleProtocolService.swift
km2023 09088121b5 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>
2026-05-20 14:02:04 +08:00

197 lines
6.5 KiB
Swift
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.
import Foundation
///
protocol BleProtocolServiceDelegate: AnyObject {
func protocolService(_ service: BleProtocolService, didReceiveMessage commandType: UInt8, data: Data)
}
///
final class BleProtocolService: BleClientDelegate {
weak var delegate: BleProtocolServiceDelegate?
private let client: BleClient
private let frameIntervalMs: Int
private var isListening = false
///
private struct FragmentSession {
let total: Int
var frames: [Int: Data] // curPage -> data
}
private var fragments: [String: FragmentSession] = [:]
init(client: BleClient, frameIntervalMs: Int) {
self.client = client
self.frameIntervalMs = frameIntervalMs
BleLog.i("Protocol service created (frameIntervalMs=\(frameIntervalMs))", "Protocol")
}
// MARK: - Start/Stop Listening
func startListening() {
isListening = true
client.delegate = self
fragments.removeAll()
BleLog.i("Start listening for notifications", "Protocol")
}
func stopListening() {
isListening = false
fragments.removeAll()
BleLog.i("Stop listening; fragments cleared", "Protocol")
}
// MARK: - Send
/// 使
func send(
type: UInt8,
data: Data,
onProgress: ((Double) -> Void)? = nil,
completion: @escaping (Result<Void, Error>) -> Void
) {
let maxWriteLen = client.maximumWriteLength
let maxDataSize = min(
maxWriteLen - FrameConstants.headerSize - FrameConstants.footerSize,
FrameConstants.maxDataSize
)
let safeMaxDataSize = max(1, maxDataSize)
let frames = ProtocolManager.createFrames(
type: type,
data: data,
head: .appToDevice,
maxDataSize: safeMaxDataSize
)
guard !frames.isEmpty else {
BleLog.e("Payload too large; frames empty", "Protocol")
completion(.failure(DuooomiBleError.transferFailed("Payload too large: exceeds 65535-page limit")))
return
}
let total = frames.count
BleLog.d("Sending frames: total=\(total), maxChunk=\(safeMaxDataSize)", "Protocol")
func sendFrame(at index: Int) {
guard index < total else {
onProgress?(1.0)
BleLog.i("Frames sent: total=\(total)", "Protocol")
completion(.success(()))
return
}
do {
try self.client.writeWithoutResponse(frames[index])
} catch {
completion(.failure(error))
return
}
onProgress?(Double(index + 1) / Double(total))
if index + 1 < total {
// 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 {
onProgress?(1.0)
BleLog.i("Frames sent: total=\(total)", "Protocol")
completion(.success(()))
}
}
sendFrame(at: 0)
}
/// JSON
func sendJSON<T: Encodable>(type: CommandType, payload: T, completion: @escaping (Result<Void, Error>) -> Void) {
do {
let data = try JSONEncoder().encode(payload)
send(type: type.rawValue, data: data, completion: completion)
} catch {
completion(.failure(error))
}
}
/// JSON fire-and-forget
func sendJSON<T: Encodable>(type: CommandType, payload: T) throws {
let jsonData = try JSONEncoder().encode(payload)
let maxWriteLen = client.maximumWriteLength
let maxDataSize = min(
maxWriteLen - FrameConstants.headerSize - FrameConstants.footerSize,
FrameConstants.maxDataSize
)
let safeMaxDataSize = max(1, maxDataSize)
let frames = ProtocolManager.createFrames(
type: type.rawValue,
data: jsonData,
head: .appToDevice,
maxDataSize: safeMaxDataSize
)
for frame in frames {
try client.writeWithoutResponse(frame)
}
}
// MARK: - BleClientDelegate
func bleClient(_ client: BleClient, didDiscoverDevice device: DiscoveredDevice) {
// Not handled here scan results forwarded by SDK
}
func bleClient(_ client: BleClient, didReceiveData data: Data) {
guard isListening else { return }
handleRawData(data)
}
// MARK: - Receive & Reassemble
private func handleRawData(_ data: Data) {
guard let frame = ProtocolManager.parseFrame(data) else {
BleLog.w("Drop invalid frame (len=\(data.count))", "Protocol")
return
}
if frame.subpageTotal > 0 {
handleFragment(frame)
} else {
BleLog.d("Single frame received: type=\(frame.type), len=\(frame.data.count)", "Protocol")
delegate?.protocolService(self, didReceiveMessage: frame.type, data: frame.data)
}
}
private func handleFragment(_ frame: ProtocolFrame) {
let key = "\(frame.type)"
if fragments[key] == nil {
fragments[key] = FragmentSession(total: Int(frame.subpageTotal), frames: [:])
BleLog.d("New fragment session: key=\(key), total=\(frame.subpageTotal)", "Protocol")
}
guard var session = fragments[key] else { return }
guard frame.curPage < session.total else { return }
session.frames[Int(frame.curPage)] = frame.data
fragments[key] = session
// Check if all pages received
guard session.frames.count == session.total else { return }
// Reassemble: from high page to low page
var combined = Data()
for i in stride(from: session.total - 1, through: 0, by: -1) {
if let part = session.frames[i] {
combined.append(part)
}
}
fragments.removeValue(forKey: key)
BleLog.i("Fragment reassembled: key=\(key), size=\(combined.count)", "Protocol")
delegate?.protocolService(self, didReceiveMessage: frame.type, data: combined)
}
}