2023-07-02 17:29:39 +02:00
|
|
|
import Foundation
|
|
|
|
import CoreBluetooth
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
protocol BluetoothDeviceDelegate: AnyObject {
|
|
|
|
|
|
|
|
func bluetoothDevice(didUpdate info: DeviceInfo?)
|
|
|
|
}
|
|
|
|
|
2023-07-02 17:29:39 +02:00
|
|
|
actor BluetoothDevice: NSObject, ObservableObject {
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
let peripheral: CBPeripheral!
|
2023-07-02 17:29:39 +02:00
|
|
|
|
|
|
|
private let characteristic: CBCharacteristic!
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
@Published
|
2023-07-02 17:29:39 +02:00
|
|
|
var lastDeviceInfo: DeviceInfo?
|
|
|
|
|
|
|
|
@Published
|
|
|
|
private(set) var lastRSSI: Int = 0
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
weak var delegate: BluetoothDeviceDelegate?
|
|
|
|
|
|
|
|
func set(delegate: BluetoothDeviceDelegate?) {
|
|
|
|
self.delegate = delegate
|
|
|
|
}
|
|
|
|
|
2023-07-02 17:29:39 +02:00
|
|
|
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
|
|
|
|
}
|
2023-07-03 13:28:51 +02:00
|
|
|
lastDeviceInfo = info
|
|
|
|
delegate?.bluetoothDevice(didUpdate: info)
|
|
|
|
#warning("Don't use global variable")
|
|
|
|
storage.save(deviceInfo: info)
|
2023-07-02 17:29:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|