TempTrack-iOS/TempTrack/Connection/BluetoothScanner.swift
2023-07-03 13:28:51 +02:00

218 lines
7.5 KiB
Swift

import Foundation
import SwiftUI
import CoreBluetooth
final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObject {
enum ConnectionState {
case noDeviceFound
case connecting
case discoveringService
case discoveringCharacteristic
}
private let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
private let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
private var manager: CBCentralManager! = nil
@Published
var bluetoothIsAvailable = false
@Published
var connectionState: ConnectionState
@Published
var configuredDevice: BluetoothDevice?
@Published
var lastDeviceInfo: DeviceInfo?
private var connectingDevice: CBPeripheral?
@Published
var isScanningForDevices: Bool = false {
didSet {
if isScanningForDevices {
startScanning()
} else {
stopScanning()
}
}
}
private func startScanning() {
guard !manager.isScanning else {
return
}
manager.scanForPeripherals(withServices: [serviceUUID])
log.info("Scanner: Started scanning for devices")
}
private func stopScanning() {
guard manager.isScanning else {
return
}
manager.stopScan()
log.info("Scanner: Stopped scanning for devices")
}
var isConnectingOrConnected: Bool {
configuredDevice != nil || connectingDevice != nil
}
func disconnect() {
if let configuredDevice {
manager.cancelPeripheralConnection(configuredDevice.peripheral)
}
if let connectingDevice {
manager.cancelPeripheralConnection(connectingDevice)
}
}
override init() {
connectionState = .noDeviceFound
super.init()
self.manager = CBCentralManager(delegate: self, queue: nil)
self.isScanningForDevices = manager.isScanning
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
guard connectionState == .noDeviceFound && configuredDevice == nil && connectingDevice == nil else {
log.info("Scanner: Discovered additional device '\(peripheral.name ?? "No Name")'")
return
}
log.info("Scanner: Connecting to discovered device '\(peripheral.name ?? "No Name")'")
connectingDevice = peripheral
manager.connect(peripheral)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
break
case .poweredOn:
bluetoothIsAvailable = true
return
case .unsupported:
log.info("Bluetooth state: Not supported")
case .unknown:
log.info("Bluetooth state: Unknown")
case .resetting:
log.info("Bluetooth state: Resetting")
case .unauthorized:
log.info("Bluetooth state: Not authorized")
@unknown default:
log.warning("Bluetooth state: Unknown (\(central.state))")
}
bluetoothIsAvailable = false
// TODO: Disconnect devices?
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
log.info("Scanner: Connected to '\(peripheral.name ?? "No Name")'")
connectionState = .discoveringService
peripheral.delegate = self
peripheral.discoverServices([serviceUUID])
connectingDevice = peripheral
configuredDevice = nil
isScanningForDevices = false
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
log.info("Scanner: Disconnected from '\(peripheral.name ?? "No Name")'")
connectionState = .noDeviceFound
configuredDevice = nil
connectingDevice = nil
// TODO: Check if peripheral matches the connected device(s)
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
log.warning("Scanner: Failed to connect to device '\(peripheral.name ?? "No Name")' (\(error?.localizedDescription ?? "No error"))")
isScanningForDevices = true
connectionState = .noDeviceFound
connectingDevice = nil
}
}
extension BluetoothScanner: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services, !services.isEmpty else {
log.error("Connected device '\(peripheral.name ?? "No Name")': No services found")
manager.cancelPeripheralConnection(peripheral)
connectionState = .noDeviceFound
connectingDevice = nil
isScanningForDevices = true
return
}
guard let service = services.first(where: { $0.uuid.uuidString == serviceUUID.uuidString }) else {
log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
manager.cancelPeripheralConnection(peripheral)
connectionState = .noDeviceFound
connectingDevice = nil
isScanningForDevices = true
return
}
peripheral.delegate = self
peripheral.discoverCharacteristics([characteristicUUID], for: service)
connectionState = .discoveringCharacteristic
connectingDevice = peripheral
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
log.error("Failed to discover characteristics: \(error)")
manager.cancelPeripheralConnection(peripheral)
connectionState = .noDeviceFound
connectingDevice = nil
isScanningForDevices = true
return
}
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
log.error("Connected device '\(peripheral.name ?? "No Name")': No characteristics found")
manager.cancelPeripheralConnection(peripheral)
connectionState = .noDeviceFound
connectingDevice = nil
isScanningForDevices = true
return
}
var desiredCharacteristic: CBCharacteristic? = nil
for characteristic in characteristics {
guard characteristic.uuid == characteristicUUID else {
log.warning("Connected device '\(peripheral.name ?? "No Name")': Unused characteristic \(characteristic.uuid.uuidString)")
continue
}
desiredCharacteristic = characteristic
}
connectionState = .noDeviceFound
connectingDevice = nil
guard let desiredCharacteristic else {
log.error("Connected device '\(peripheral.name ?? "No Name")': Characteristic not found")
manager.cancelPeripheralConnection(peripheral)
isScanningForDevices = true
return
}
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
Task {
await configuredDevice?.set(delegate: self)
}
}
}
extension BluetoothScanner: BluetoothDeviceDelegate {
func bluetoothDevice(didUpdate info: DeviceInfo?) {
DispatchQueue.main.async {
self.lastDeviceInfo = info
}
}
}