import Foundation import SwiftUI enum BluetoothResponseType: UInt8 { /// The response to the last request is provided case success = 0 /// Invalid command received case invalidCommand = 1 case responseTooLarge = 2 case unknownCommand = 3 case invalidNumberOfBytesToDelete = 4 } final class BluetoothClient: ObservableObject { weak var delegate: TemperatureDataTransferDelegate? private let updateInterval = 3.0 private let connection = DeviceManager() private var didTransferData = false init(deviceInfo: DeviceInfo? = nil) { connection.delegate = self self.deviceInfo = deviceInfo } func connect() -> Bool { connection.connect() } @Published private(set) var deviceState: DeviceState = .disconnected { didSet { print("State: \(deviceState.text)") if case .configured = deviceState { startRegularUpdates() } else { endRegularUpdates() didTransferData = false } } } @Published private(set) var deviceInfo: DeviceInfo? { didSet { guard !didTransferData, runningTransfer == nil else { return } guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else { return } guard collectRecordedData() else { return } } } private var openRequests: [BluetoothRequest] = [] private var runningRequest: BluetoothRequest? private var runningTransfer: TemperatureDataTransfer? func updateDeviceInfo() { guard case .configured = deviceState else { return } addRequest(.getInfo) } private var dataUpdateTimer: Timer? private func startRegularUpdates() { guard dataUpdateTimer == nil else { return } print("Starting updates") dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in guard let self = self else { timer.invalidate() return } self.updateDeviceInfo() } dataUpdateTimer?.fire() } private func endRegularUpdates() { guard let dataUpdateTimer else { return } dataUpdateTimer.invalidate() runningRequest = nil self.dataUpdateTimer = nil print("Ending updates") } private func performNextRequest() { guard runningRequest == nil else { return } guard !openRequests.isEmpty else { return } let next = openRequests.removeFirst() //print("Starting request \(next)") guard connection.send(next.serialized) else { print("Failed to start request \(next)") performNextRequest() return } runningRequest = next } func addRequest(_ request: BluetoothRequest) { openRequests.append(request) performNextRequest() } func collectRecordedData() -> Bool { guard let info = deviceInfo else { return false } let transfer = TemperatureDataTransfer(info: info) runningTransfer = transfer runningTransfer?.delegate = delegate let next = transfer.nextRequest() addRequest(next) return true } private func didReceive(data: Data, offset: Int, count: Int) { guard let runningTransfer else { return // TODO: Start new transfer? } runningTransfer.add(data: data, offset: offset, count: count) continueTransfer() } private func continueTransfer() { guard let runningTransfer else { return // TODO: Start new transfer? } let next = runningTransfer.nextRequest() if case .clearRecordingBuffer = next { runningTransfer.completeTransfer() self.runningTransfer = nil didTransferData = true return } addRequest(next) } private func decode(info: Data) { guard let newInfo = DeviceInfo(info: info) else { return } self.deviceInfo = newInfo guard let runningTransfer else { return } runningTransfer.update(info: newInfo) let next = runningTransfer.nextRequest() addRequest(next) } } extension BluetoothClient: DeviceManagerDelegate { func deviceManager(didReceive data: Data) { defer { self.runningRequest = nil performNextRequest() } guard let runningRequest else { print("No request active, but \(data) received") return } guard data.count > 0 else { print("No response data for request \(runningRequest)") return } guard let type = BluetoothResponseType(rawValue: data[0]) else { print("Unknown response \(data[0]) for request \(runningRequest)") return } guard type == .success else { print("Error response \(data[0]) for request \(runningRequest)") // If clearing the recording buffer fails due to byte mismatch, // then requesting new info will resolve the mismatch, and the transfer will be resumed // If requesting bytes fails due to the response size, // then requesting new info will update the response size, and the transfer will be resumed addRequest(.getInfo) return } let payload = data.dropFirst() switch runningRequest { case .getInfo: decode(info: payload) case .getRecordingData(let offset, let count): didReceive(data: payload, offset: offset, count: count) case .clearRecordingBuffer: runningTransfer?.completeTransfer() runningTransfer = nil } } func deviceManager(didChangeState state: DeviceState) { DispatchQueue.main.async { self.deviceState = state } } }