1 contributor
//
// BTManager.swift
// USB Meter
//
// Created by Bogdan Timofte on 01/03/2020.
// Copyright © 2020 Bogdan Timofte. All rights reserved.
//
import CoreBluetooth
class BluetoothManager : NSObject, ObservableObject {
private var manager: CBCentralManager?
private var isStarting = false
private var advertisementDataCache = AdvertisementDataCache()
@Published var managerState = CBManagerState.unknown
@Published private(set) var scanStartedAt: Date?
override init () {
super.init()
}
func start() {
guard manager == nil, !isStarting else {
return
}
isStarting = true
track("Starting Bluetooth manager and requesting authorization if needed")
DispatchQueue.main.async { [weak self] in
guard let self else { return }
defer { self.isStarting = false }
guard self.manager == nil else {
return
}
self.manager = CBCentralManager(delegate: self, queue: nil)
}
}
private func scanForMeters() {
guard let manager else {
track("Scan requested before Bluetooth manager was started")
return
}
guard manager.state == .poweredOn else {
track( "Scan requested but Bluetooth state is \(manager.state)")
return
}
//manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
}
func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
guard let peripheralName = resolvedPeripheralName(for: peripheral, advertising: advertismentData) else {
return
}
guard let manufacturerData = resolvedManufacturerData(from: advertismentData), manufacturerData.count > 2 else {
return
}
guard let model = Model.byPeripheralName[peripheralName] else {
return
}
let macAddress = MACAddress(from: manufacturerData.suffix(from: 2))
if appData.meters[peripheral.identifier] == nil {
track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
let btSerial = BluetoothSerial(peripheral: peripheral, radio: model.radio, with: macAddress, managedBy: manager!, RSSI: RSSI.intValue)
var m = appData.meters
m[peripheral.identifier] = Meter(model: model, with: btSerial)
appData.meters = m
} else if let meter = appData.meters[peripheral.identifier] {
meter.lastSeen = Date()
meter.btSerial.RSSI = RSSI.intValue
if peripheral.delegate == nil {
peripheral.delegate = meter.btSerial
}
}
}
private func resolvedPeripheralName(for peripheral: CBPeripheral, advertising advertismentData: [String : Any]) -> String? {
let candidates = [
(advertismentData[CBAdvertisementDataLocalNameKey] as? String),
peripheral.name
]
for candidate in candidates {
if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty {
return trimmed
}
}
return nil
}
private func resolvedManufacturerData(from advertismentData: [String : Any]) -> Data? {
if let data = advertismentData[CBAdvertisementDataManufacturerDataKey] as? Data {
return data
}
if let data = advertismentData["kCBAdvDataManufacturerData"] as? Data {
return data
}
return nil
}
}
extension BluetoothManager : CBCentralManagerDelegate {
// MARK: CBCentralManager state Changed
func centralManagerDidUpdateState(_ central: CBCentralManager) {
managerState = central.state;
track("\(central.state)")
switch central.state {
case .poweredOff:
scanStartedAt = nil
track("Bluetooth is Off. How should I behave?")
case .poweredOn:
scanStartedAt = Date()
track("Bluetooth is On... Start scanning...")
// note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected
// connectedPeripheral = nil
// pendingPeripheral = nil
scanForMeters()
case .resetting:
scanStartedAt = nil
track("Bluetooth is reseting... . Whatever that means.")
case .unauthorized:
scanStartedAt = nil
track("Bluetooth is not authorized.")
case .unknown:
scanStartedAt = nil
track("Bluetooth is in an unknown state.")
case .unsupported:
scanStartedAt = nil
track("Bluetooth not supported by device")
default:
scanStartedAt = nil
track("Bluetooth is in a state never seen before!")
}
}
// MARK: CBCentralManager didDiscover peripheral
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
//track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
}
// MARK: CBCentralManager didConnect peripheral
internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
//track("Connected to peripheral: '\(peripheral.identifier)'")
if let usbMeter = appData.meters[peripheral.identifier] {
usbMeter.btSerial.connectionEstablished()
}
else {
track("Connected to unknown meter with UUID: '\(peripheral.identifier)'")
}
}
// MARK: CBCentralManager didDisconnectPeripheral peripheral
internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
if let usbMeter = appData.meters[peripheral.identifier] {
usbMeter.btSerial.connectionClosed()
}
else {
track("Disconnected from unknown meter with UUID: '\(peripheral.identifier)'")
}
}
internal func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
track("Failed to connect to peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
if let usbMeter = appData.meters[peripheral.identifier] {
usbMeter.btSerial.connectionClosed()
} else {
track("Failed to connect to unknown meter with UUID: '\(peripheral.identifier)'")
}
}
}
private class AdvertisementDataCache {
private var map = [UUID: [String: Any]]()
func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
var ad = (map[peripheral.identifier]) ?? [String: Any]()
if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty {
ad[CBAdvertisementDataLocalNameKey] = localName
}
for (key, value) in advertisementData {
ad[key] = value
}
map[peripheral.identifier] = ad
return ad
}
func clear() {
map.removeAll()
}
}