177 lines
6.5 KiB
Swift
177 lines
6.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?
|
||
|
|
||
|
private var connectingDevice: CBPeripheral?
|
||
|
|
||
|
var isScanningForDevices: Bool {
|
||
|
get {
|
||
|
manager.isScanning
|
||
|
}
|
||
|
set {
|
||
|
if newValue {
|
||
|
guard !manager.isScanning else {
|
||
|
return
|
||
|
}
|
||
|
manager.scanForPeripherals(withServices: [serviceUUID])
|
||
|
log.info("Scanner: Started scanning for devices")
|
||
|
} else {
|
||
|
guard manager.isScanning else {
|
||
|
return
|
||
|
}
|
||
|
manager.stopScan()
|
||
|
log.info("Scanner: Stopped scanning for devices")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
override init() {
|
||
|
connectionState = .noDeviceFound
|
||
|
super.init()
|
||
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
return
|
||
|
}
|
||
|
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.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
|
||
|
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
|
||
|
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
|
||
|
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)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
||
|
}
|
||
|
}
|