TempTrack-iOS/TempTrack/Connection/BluetoothDevice.swift
2023-07-02 17:29:39 +02:00

151 lines
4.9 KiB
Swift

import Foundation
import CoreBluetooth
actor BluetoothDevice: NSObject, ObservableObject {
private let peripheral: CBPeripheral!
private let characteristic: CBCharacteristic!
@MainActor @Published
var lastDeviceInfo: DeviceInfo?
@Published
private(set) var lastRSSI: Int = 0
init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
self.peripheral = peripheral
self.characteristic = characteristic
super.init()
peripheral.delegate = self
}
override init() {
self.peripheral = nil
self.characteristic = nil
super.init()
}
private var requestContinuation: (id: Int, call: CheckedContinuation<Data?, Never>)?
func updateInfo() async {
guard let info = await getInfo() else {
return
}
Task { @MainActor in
lastDeviceInfo = info
}
}
func getInfo() async -> DeviceInfo? {
await get(DeviceInfoRequest())
}
func getDeviceData(offset: Int, count: Int) async -> Data? {
await get(DeviceDataRequest(offset: offset, count: count))
}
func deleteDeviceData(byteCount: Int) async -> Bool {
await get(DeviceDataResetRequest(byteCount: byteCount)) != nil
}
private func get<Request>(_ request: Request) async -> Request.Response? where Request: DeviceRequest {
guard requestContinuation == nil else {
// Prevent parallel requests
return nil
}
let requestData = Data([request.type.rawValue]) + request.payload
let responseData: Data? = await withCheckedContinuation { continuation in
let id = Int.random(in: .min...Int.max)
requestContinuation = (id, continuation)
peripheral.writeValue(requestData, for: characteristic, type: .withResponse)
peripheral.readValue(for: characteristic)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
Task {
await self?.checkTimeoutForCurrentRequest(request.type, id: id)
}
}
}
guard let responseData else {
return nil
}
guard let responseCode = responseData.first else {
log.error("Request \(request.type) got response of zero bytes")
return nil
}
guard let responseType = BluetoothResponseType(rawValue: responseCode) else {
log.error("Request \(request.type) got unknown response code \(responseCode)")
return nil
}
switch responseType {
case .success, .responseTooLarge, .invalidNumberOfBytesToDelete:
break
case .invalidCommand:
log.error("Request \(request.type) failed: Invalid command")
return nil
case .unknownCommand:
log.error("Request \(request.type) failed: Unknown command")
return nil
case .responseInProgress:
log.info("Request \(request.type) failed: Device is busy")
return nil
}
return request.makeResponse(from: responseData.dropFirst(), responseType: responseType)
}
private func checkTimeoutForCurrentRequest(_ type: BluetoothRequestType, id: Int) {
guard let requestContinuation else { return }
guard requestContinuation.id == id else { return }
log.info("Timed out for request \(type)")
requestContinuation.call.resume(returning: nil)
self.requestContinuation = nil
}
}
extension BluetoothDevice: CBPeripheralDelegate {
nonisolated
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
}
nonisolated
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
Task {
await didUpdateValue(for: characteristic, of: peripheral, error: error)
}
}
private func didUpdateValue(for characteristic: CBCharacteristic, of peripheral: CBPeripheral, error: Error?) {
if let error = error {
log.error("Failed to read value update: \(error)")
continueRequest(with: nil)
return
}
guard characteristic.uuid == self.characteristic.uuid else {
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
continueRequest(with: nil)
return
}
guard let data = characteristic.value else {
log.warning("No data")
continueRequest(with: nil)
return
}
continueRequest(with: data)
}
private func continueRequest(with response: Data?) {
guard let requestContinuation else {
log.error("No continuation to handle request data (\(response?.count ?? 0) bytes)")
return
}
requestContinuation.call.resume(returning: response)
self.requestContinuation = nil
}
}