import Foundation import SwiftUI final class BluetoothClient: ObservableObject { private let updateInterval = 3.0 private let minimumOffsetToUpdateDeviceClock = 5.0 private let connection = DeviceManager() private let storage: TemperatureStorage var hasInfo: Bool { deviceInfo != nil } var isConnected: Bool { if case .configured = deviceState { return true } return false } init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) { self.storage = storage self.deviceInfo = deviceInfo connection.delegate = self } func connect() -> Bool { connection.connect() } @Published private(set) var deviceState: DeviceState = .disconnected { didSet { log.info("State: \(deviceState)") if case .configured = deviceState { startRegularUpdates() } else { endRegularUpdates() } } } @Published private(set) var deviceInfo: DeviceInfo? { didSet { updateDeviceTimeIfNeeded() // collectRecordedData() if let deviceInfo, let runningTransfer { runningTransfer.update(info: deviceInfo) let next = runningTransfer.nextRequest() addRequest(next) } } } private var openRequests: [BluetoothRequest] = [] private var runningRequest: BluetoothRequest? private var runningTransfer: TemperatureDataTransfer? // MARK: Regular updates func updateDeviceInfo() { guard case .configured = deviceState else { return } addRequest(.getInfo) } private var dataUpdateTimer: Timer? private func startRegularUpdates() { guard dataUpdateTimer == nil else { return } log.info("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 log.info("Ending updates") } // MARK: Requests private func performNextRequest() { guard runningRequest == nil else { return } guard !openRequests.isEmpty else { return } let next = openRequests.removeFirst() guard connection.send(next.serialized) else { log.warning("Failed to start request \(next)") performNextRequest() return } runningRequest = next } func addRequest(_ request: BluetoothRequest) { defer { performNextRequest() } let type = request.byte if let runningRequest, runningRequest.byte == type { log.info("Skipping duplicate request \(request)") return } guard !openRequests.contains(where: { $0.byte == type }) else { log.info("Skipping duplicate request \(request)") return } openRequests.append(request) } // MARK: Device time private func updateDeviceTimeIfNeeded() { guard let deviceInfo else { return } guard !deviceInfo.hasDeviceStartTimeSet || abs(deviceInfo.clockOffset) > minimumOffsetToUpdateDeviceClock else { return } guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else { return } let time = deviceInfo.calculatedDeviceStartTime.seconds addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time)) log.info("Setting device start time to \(time) s (correcting offset of \(Int(deviceInfo.clockOffset)) s)") } // MARK: Data transfer @discardableResult func collectRecordedData() -> Bool { guard runningTransfer == nil else { log.info("Transfer already running") return false } guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else { log.info("Transfer already scheduled") return false } guard let info = deviceInfo else { log.warning("No device info to start transfer") return false } guard info.numberOfStoredMeasurements > 0 else { return false } let transfer = TemperatureDataTransfer(info: info) runningTransfer = transfer let next = transfer.nextRequest() log.info("Starting transfer") addRequest(next) return true } private func didReceive(data: Data, offset: Int, count: Int) { guard let runningTransfer else { log.warning("No running transfer to process device data") return // TODO: Start new transfer? } runningTransfer.add(data: data, offset: offset, count: count) let next = runningTransfer.nextRequest() addRequest(next) } private func decode(info: Data) { guard let newInfo = try? DeviceInfo(info: info) else { log.error("Failed to decode device info") return } self.deviceInfo = newInfo } } extension BluetoothClient: DeviceManagerDelegate { func deviceManager(didReceive data: Data) { defer { performNextRequest() } guard let runningRequest else { log.warning("No request active, but \(data) received") return } self.runningRequest = nil guard data.count > 0 else { log.error("No response data for request \(runningRequest)") return } guard let type = BluetoothResponseType(rawValue: data[0]) else { log.error("Unknown response \(data[0]) for request \(runningRequest)") return } switch type { case .success: break case .responseInProgress: log.info("Device is busy for \(runningRequest)") // Retry the request addRequest(runningRequest) return case .invalidNumberOfBytesToDelete: guard case .clearRecordingBuffer = runningRequest else { // If clearing the recording buffer fails due to byte mismatch, // then requesting new info will resolve the mismatch, and the transfer will be resumed addRequest(.getInfo) return } log.error("Request \(runningRequest) received non-matching responde about number of bytes to delete") case .responseTooLarge: guard case .getRecordingData = runningRequest else { // 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 } log.error("Unexpectedly exceeded payload size for request \(runningRequest)") default: log.error("Unknown 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 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: didClearDeviceStorage() case .setDeviceStartTime: log.info("Device time set") break } } private func didClearDeviceStorage() { guard let runningTransfer else { log.warning("No running transfer after clearing device storage") return } runningTransfer.completeTransfer() storage.add(runningTransfer.measurements) self.runningTransfer = nil updateDeviceTimeIfNeeded() } func deviceManager(didChangeState state: DeviceState) { DispatchQueue.main.async { self.deviceState = state } } }