import Foundation import CoreBluetooth final class DeviceManager: NSObject, CBCentralManagerDelegate { static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001") static let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002") private var manager: CBCentralManager! = nil private(set) var lastRSSI: Int = 0 weak var delegate: DeviceManagerDelegate? var state: DeviceState = .disconnected { didSet { delegate?.deviceManager(didChangeState: state) } } override init() { super.init() self.manager = CBCentralManager(delegate: self, queue: nil) } @discardableResult func connect() -> Bool { switch state { case .bluetoothDisabled: log.info("Can't connect, bluetooth disabled") return false case .disconnected: break default: return true } guard !manager.isScanning else { state = .scanning return true } if !shouldConnectIfPossible { shouldConnectIfPossible = true } state = .scanning manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID]) return true } var shouldConnectIfPossible = true { didSet { updateConnectionOnChange() delegate?.deviceManager(shouldConnectToDevice: shouldConnectIfPossible) } } private func updateConnectionOnChange() { if shouldConnectIfPossible { ensureConnection() } else { disconnectIfNeeded() } } private func ensureConnection() { switch state { case .disconnected: connect() default: return } } private func disconnectIfNeeded() { switch state { case .bluetoothDisabled, .disconnected: return default: disconnect() } } func disconnect() { if shouldConnectIfPossible { shouldConnectIfPossible = false } switch state { case .bluetoothDisabled, .disconnected: return case .scanning: manager.stopScan() state = .disconnected return case .connecting(let device), .discoveringCharacteristic(let device), .discoveringServices(device: let device), .configured(let device, _): manager.cancelPeripheralConnection(device) manager.stopScan() state = .disconnected return } } @discardableResult func send(_ data: Data) -> Bool { guard case .configured(let device, let characteristic) = state else { return false } device.writeValue(data, for: characteristic, type: .withResponse) return self.read() } @discardableResult private func read() -> Bool { guard case .configured(let device, let characteristic) = state else { return false } device.readValue(for: characteristic) return true } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { guard shouldConnectIfPossible else { return } peripheral.delegate = self manager.connect(peripheral) manager.stopScan() state = .connecting(device: peripheral) } func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOff: state = .bluetoothDisabled case .poweredOn: state = .disconnected connect() case .unsupported: state = .bluetoothDisabled log.info("Bluetooth is not supported") case .unknown: state = .bluetoothDisabled log.info("Bluetooth state is unknown") case .resetting: state = .bluetoothDisabled log.info("Bluetooth is resetting") case .unauthorized: state = .bluetoothDisabled log.info("Bluetooth is not authorized") @unknown default: state = .bluetoothDisabled log.warning("Unknown state \(central.state)") } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { log.info("Connected to " + peripheral.name!) peripheral.discoverServices([DeviceManager.serviceUUID]) state = .discoveringServices(device: peripheral) } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { log.info("Disconnected from " + peripheral.name!) state = .disconnected // Attempt to reconnect if shouldConnectIfPossible { connect() } } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'") if let error = error { log.warning(error.localizedDescription) } state = manager.isScanning ? .scanning : .disconnected // Attempt to reconnect if shouldConnectIfPossible { connect() } } } extension DeviceManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services, !services.isEmpty else { log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'") manager.cancelPeripheralConnection(peripheral) return } guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else { log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})") manager.cancelPeripheralConnection(peripheral) return } peripheral.discoverCharacteristics([DeviceManager.characteristicUUID], for: service) state = .discoveringCharacteristic(device: peripheral) } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { log.error("Failed to discover characteristics: \(error)") manager.cancelPeripheralConnection(peripheral) return } guard let characteristics = service.characteristics, !characteristics.isEmpty else { log.error("No characteristics found for device") manager.cancelPeripheralConnection(peripheral) return } for characteristic in characteristics { guard characteristic.uuid == DeviceManager.characteristicUUID else { log.warning("Unused characteristic \(characteristic.uuid.uuidString)") continue } state = .configured(device: peripheral, characteristic: characteristic) peripheral.setNotifyValue(true, for: characteristic) } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)") } } func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { if let error = error { log.warning("Failed to get RSSI: \(error)") return } lastRSSI = RSSI.intValue log.info("RSSI: \(lastRSSI)") } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { log.error("Failed to read value update: \(error)") return } guard case .configured(device: _, characteristic: let storedCharacteristic) = state else { log.warning("Received data while not properly configured") return } guard characteristic.uuid == storedCharacteristic.uuid else { log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)") return } guard let data = characteristic.value else { log.warning("No data") return } delegate?.deviceManager(didReceive: data) } }