2023-07-02 17:29:39 +02:00
|
|
|
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?
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
@Published
|
|
|
|
var lastDeviceInfo: DeviceInfo?
|
|
|
|
|
2023-07-02 17:29:39 +02:00
|
|
|
private var connectingDevice: CBPeripheral?
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
@Published
|
|
|
|
var isScanningForDevices: Bool = false {
|
|
|
|
didSet {
|
|
|
|
if isScanningForDevices {
|
|
|
|
startScanning()
|
2023-07-02 17:29:39 +02:00
|
|
|
} else {
|
2023-07-03 13:28:51 +02:00
|
|
|
stopScanning()
|
2023-07-02 17:29:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-03 13:28:51 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-02 17:29:39 +02:00
|
|
|
override init() {
|
|
|
|
connectionState = .noDeviceFound
|
|
|
|
super.init()
|
|
|
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
2023-07-03 13:28:51 +02:00
|
|
|
self.isScanningForDevices = manager.isScanning
|
2023-07-02 17:29:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = false
|
2023-07-02 17:29:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = true
|
2023-07-02 17:29:39 +02:00
|
|
|
return
|
|
|
|
}
|
2023-07-03 13:28:51 +02:00
|
|
|
guard let service = services.first(where: { $0.uuid.uuidString == serviceUUID.uuidString }) else {
|
2023-07-02 17:29:39 +02:00
|
|
|
log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
|
|
|
|
manager.cancelPeripheralConnection(peripheral)
|
|
|
|
connectionState = .noDeviceFound
|
|
|
|
connectingDevice = nil
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = true
|
2023-07-02 17:29:39 +02:00
|
|
|
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
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = true
|
2023-07-02 17:29:39 +02:00
|
|
|
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
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = true
|
2023-07-02 17:29:39 +02:00
|
|
|
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)
|
2023-07-03 13:28:51 +02:00
|
|
|
isScanningForDevices = true
|
2023-07-02 17:29:39 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
2023-07-03 13:28:51 +02:00
|
|
|
Task {
|
|
|
|
await configuredDevice?.set(delegate: self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension BluetoothScanner: BluetoothDeviceDelegate {
|
|
|
|
|
|
|
|
func bluetoothDevice(didUpdate info: DeviceInfo?) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.lastDeviceInfo = info
|
|
|
|
}
|
2023-07-02 17:29:39 +02:00
|
|
|
}
|
|
|
|
}
|