import Foundation /// 协议通信服务:帧收发、分包重组 final class BleProtocolService: @unchecked Sendable { private let client: BleClient /// 分包重组会话 private struct FragmentSession { let total: Int var frames: [Int: Data] // curPage -> data } private var fragments: [String: FragmentSession] = [:] private var listenerTask: Task? private var messageContinuation: AsyncStream<(commandType: UInt8, data: Data)>.Continuation? private(set) var incomingMessages: AsyncStream<(commandType: UInt8, data: Data)>! init(client: BleClient) { self.client = client incomingMessages = AsyncStream { continuation in self.messageContinuation = continuation } BleLog.i("Protocol service created", "Protocol") } // MARK: - Start/Stop Listening /// 开始监听底层通知并进行帧解析与重组。 /// - Note: 重新创建消息流并清空历史分片缓存。 func startListening() { // Recreate the stream in case it was previously finished incomingMessages = AsyncStream { continuation in self.messageContinuation = continuation } fragments.removeAll() listenerTask = Task { [weak self] in guard let self else { return } BleLog.i("Start listening for notifications", "Protocol") for await data in self.client.notifications() { self.handleRawData(data) } } } /// 停止监听,结束消息流并清理缓存。 func stopListening() { listenerTask?.cancel() listenerTask = nil messageContinuation?.finish() messageContinuation = nil fragments.removeAll() BleLog.i("Stop listening; fragments cleared", "Protocol") } // MARK: - Send /// 发送二进制数据(自动拆分为协议帧)。 /// - Parameters: /// - type: 命令类型值。 /// - data: 需要发送的原始负载。 /// - onProgress: 发送进度回调(0.0~1.0)。 /// - Throws: 写入失败或负载过大等错误。 func send( type: UInt8, data: Data, onProgress: ((Double) -> Void)? = nil ) async throws { 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") throw DuooomiBleError.transferFailed("Payload too large: exceeds 65535-page limit") } let total = frames.count BleLog.d("Sending frames: total=\(total), maxChunk=\(safeMaxDataSize)", "Protocol") for (index, frame) in frames.enumerated() { try client.writeWithoutResponse(frame) try await Task.sleep(nanoseconds: FrameConstants.frameIntervalNanos) onProgress?(Double(index + 1) / Double(total)) } BleLog.i("Frames sent: total=\(total)", "Protocol") } /// 发送 JSON 命令,内部完成编码与帧拆分。 /// - Parameters: /// - type: 命令类型。 /// - payload: 可编码的命令体。 /// - Throws: 编码/发送失败时抛出。 func sendJSON(type: CommandType, payload: T) async throws { let jsonData = try JSONEncoder().encode(payload) try await send(type: type.rawValue, data: jsonData) } // 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") messageContinuation?.yield((commandType: frame.type, data: frame.data)) } } private func handleFragment(_ frame: ProtocolFrame) { // Match TS reference behavior: scope reassembly per peripheral so concurrent transfers // (or stale fragments from a previous device after reconnect) cannot pollute each other. let deviceKey = client.connectedPeripheral?.identifier.uuidString ?? "unknown" let key = "\(deviceKey)_\(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") messageContinuation?.yield((commandType: frame.type, data: combined)) } }