TempTrack-iOS/TempTrack/Bluetooth/DeviceManager.swift

259 lines
8.6 KiB
Swift
Raw Normal View History

2023-06-03 08:15:00 +02:00
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:
2023-06-14 16:16:56 +02:00
log.info("Can't connect, bluetooth disabled")
2023-06-03 08:15:00 +02:00
return false
case .disconnected:
2023-06-03 08:15:00 +02:00
break
default:
return true
}
guard !manager.isScanning else {
state = .scanning
return true
}
if !shouldConnectIfPossible {
shouldConnectIfPossible = true
}
2023-06-03 08:15:00 +02:00
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()
}
}
2023-06-03 08:15:00 +02:00
func disconnect() {
if shouldConnectIfPossible {
shouldConnectIfPossible = false
}
2023-06-03 08:15:00 +02:00
switch state {
case .bluetoothDisabled, .disconnected:
2023-06-03 08:15:00 +02:00
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
}
2023-06-03 08:15:00 +02:00
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
2023-06-03 08:15:00 +02:00
connect()
case .unsupported:
state = .bluetoothDisabled
2023-06-14 16:16:56 +02:00
log.info("Bluetooth is not supported")
2023-06-03 08:15:00 +02:00
case .unknown:
state = .bluetoothDisabled
2023-06-14 16:16:56 +02:00
log.info("Bluetooth state is unknown")
2023-06-03 08:15:00 +02:00
case .resetting:
state = .bluetoothDisabled
2023-06-14 16:16:56 +02:00
log.info("Bluetooth is resetting")
2023-06-03 08:15:00 +02:00
case .unauthorized:
state = .bluetoothDisabled
2023-06-14 16:16:56 +02:00
log.info("Bluetooth is not authorized")
2023-06-03 08:15:00 +02:00
@unknown default:
state = .bluetoothDisabled
2023-06-14 16:16:56 +02:00
log.warning("Unknown state \(central.state)")
2023-06-03 08:15:00 +02:00
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
2023-06-14 16:16:56 +02:00
log.info("Connected to " + peripheral.name!)
2023-06-03 08:15:00 +02:00
peripheral.discoverServices([DeviceManager.serviceUUID])
state = .discoveringServices(device: peripheral)
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
2023-06-14 16:16:56 +02:00
log.info("Disconnected from " + peripheral.name!)
2023-06-03 08:15:00 +02:00
state = .disconnected
// Attempt to reconnect
if shouldConnectIfPossible {
connect()
}
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
2023-06-14 16:16:56 +02:00
log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
2023-06-03 08:15:00 +02:00
if let error = error {
2023-06-14 16:16:56 +02:00
log.warning(error.localizedDescription)
2023-06-03 08:15:00 +02:00
}
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 {
2023-06-14 16:16:56 +02:00
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
2023-06-03 08:15:00 +02:00
manager.cancelPeripheralConnection(peripheral)
return
}
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
2023-06-14 16:16:56 +02:00
log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
2023-06-03 08:15:00 +02:00
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 {
2023-06-14 16:16:56 +02:00
log.error("Failed to discover characteristics: \(error)")
2023-06-03 08:15:00 +02:00
manager.cancelPeripheralConnection(peripheral)
return
}
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
2023-06-14 16:16:56 +02:00
log.error("No characteristics found for device")
2023-06-03 08:15:00 +02:00
manager.cancelPeripheralConnection(peripheral)
return
}
for characteristic in characteristics {
guard characteristic.uuid == DeviceManager.characteristicUUID else {
2023-06-14 16:16:56 +02:00
log.warning("Unused characteristic \(characteristic.uuid.uuidString)")
2023-06-03 08:15:00 +02:00
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 {
2023-06-14 16:16:56 +02:00
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
2023-06-03 08:15:00 +02:00
}
}
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
if let error = error {
2023-06-14 16:16:56 +02:00
log.warning("Failed to get RSSI: \(error)")
2023-06-03 08:15:00 +02:00
return
}
lastRSSI = RSSI.intValue
2023-06-14 16:16:56 +02:00
log.info("RSSI: \(lastRSSI)")
2023-06-03 08:15:00 +02:00
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
2023-06-14 16:16:56 +02:00
log.error("Failed to read value update: \(error)")
2023-06-03 08:15:00 +02:00
return
}
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
2023-06-14 16:16:56 +02:00
log.warning("Received data while not properly configured")
2023-06-03 08:15:00 +02:00
return
}
guard characteristic.uuid == storedCharacteristic.uuid else {
2023-06-14 16:16:56 +02:00
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
2023-06-03 08:15:00 +02:00
return
}
guard let data = characteristic.value else {
2023-06-14 16:16:56 +02:00
log.warning("No data")
2023-06-03 08:15:00 +02:00
return
}
delegate?.deviceManager(didReceive: data)
}
}