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: PersistentStorage var hasInfo: Bool { deviceInfo != nil } var isConnected: Bool { if case .configured = deviceState { return true } return false } private var isUpdatingFlag = false @Published var shouldConnect: Bool { didSet { isUpdatingFlag = true connection.shouldConnectIfPossible = shouldConnect log.info("Should connect: \(shouldConnect)") isUpdatingFlag = false } } init(storage: PersistentStorage, shouldConnect: Bool = false, deviceInfo: DeviceInfo? = nil) { self.storage = storage self.deviceInfo = deviceInfo self.shouldConnect = shouldConnect connection.shouldConnectIfPossible = shouldConnect 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 { // 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 func clearDeviceStorage() { guard let count = deviceInfo?.numberOfRecordedBytes else { log.info("Can't clear device data without device info") return } addRequest(.clearRecordingBuffer(byteCount: count)) } 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: 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, previous: storage.lastDeviceTime) 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") self.runningRequest = nil return // TODO: Start new transfer? } guard runningTransfer.add(data: data, offset: offset, count: count) else { self.runningRequest = nil return // TODO: Start new transfer } 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(shouldConnectToDevice: Bool) { guard !isUpdatingFlag else { return } self.shouldConnect = shouldConnectToDevice } 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 { log.error("Request \(runningRequest) received non-matching response about number of bytes to delete") return } // 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) case .responseTooLarge: guard case .getRecordingData = runningRequest else { log.error("Unexpectedly exceeded payload size for request \(runningRequest)") return } // 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) 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() } } private func didClearDeviceStorage() { guard let runningTransfer else { log.warning("No running transfer after clearing device storage") return } defer { self.runningTransfer = nil } guard runningTransfer.completeTransfer() else { return } storage.add(runningTransfer.measurements) storage.lastDeviceTime = runningTransfer.time } func deviceManager(didChangeState state: DeviceState) { DispatchQueue.main.async { self.deviceState = state } } } */