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>
197 lines
6.5 KiB
Swift
197 lines
6.5 KiB
Swift
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)
|
||
}
|
||
}
|