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!
// MARK: MacOS split advertisementData generating multiple discoveries with partial data: https://stackoverflow.com/questions/41628114/cbperipheral-advertisementdata-is-different-when-discovering-peripherals-on-osx.
#if targetEnvironment(macCatalyst)
private var advertisementDataCache = AdvertisementDataCache()
#endif
@Published var managerState = CBManagerState.unknown
override init () {
super.init()
manager = CBCentralManager(delegate: self, queue: nil)
}
private func scanForMeters() {
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) {
//track("discovered new USB Meter: (\(peripheral), advertsing \(advertismentData)")
if let peripheralName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines) {
if let kCBAdvDataManufacturerData = advertismentData["kCBAdvDataManufacturerData"] as? Data {
// MARK: MAC Address
let macAddress = MACAddress(from: kCBAdvDataManufacturerData.suffix(from: 2))
// MARK: Model
if let model = ModelByPeriferalName[peripheralName] {
//track("Tetermided model for peripheral name: '\(peripheralName)'")
// MARK: Known Meters Lookup
if appData.meters[peripheral.identifier] == nil {
track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
let btSerial = BluetoothSerial(peripheral: peripheral, radio: modelRadios[model] ?? .UNKNOWN, with: macAddress, managedBy: manager, RSSI: RSSI.intValue)
var m = appData.meters
m[peripheral.identifier] = Meter(model: model, with: btSerial)
appData.meters = m
} else {
// track("Updating USB Meter: \(peripheral.identifier) ")
peripheral.delegate?.peripheral?(peripheral, didReadRSSI: RSSI, error: nil)
}
} else {
track("Unable to determine model for peripheral name: '\(peripheralName)'")
}
} else {
track("Insuficient data to use device!")
}
}
else{
track("Periferal: \(peripheral.identifier) does not have a name")
}
}
}
extension BluetoothManager : CBCentralManagerDelegate {
// MARK: CBCentralManager state Changed
func centralManagerDidUpdateState(_ central: CBCentralManager) {
managerState = central.state;
track("\(central.state)")
switch central.state {
case .poweredOff:
track("Bluetooth is Off. How should I behave?")
case .poweredOn:
track("Bluetooth is On... Start scanning...")
// note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected
// connectedPeripheral = nil
// pendingPeripheral = nil
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.scanForMeters()
}
case .resetting:
track("Bluetooth is reseting... . Whatever that means.")
case .unauthorized:
track("Bluetooth is not authorized.")
case .unknown:
track("Bluetooth is in an unknown state.")
case .unsupported:
track("Bluetooth not supported by device")
default:
track("Bluetooth is in a state never seen before!")
}
}
// MARK: MacOS multiple discoveries advertisementData caching
#if targetEnvironment(macCatalyst)
private class AdvertisementDataCache {
fileprivate var map = [UUID: [String: Any]]()
func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
var ad = (map[peripheral.identifier]) ?? [String: Any]()
for (key, value) in advertisementData {
ad[key] = value
}
map[peripheral.identifier] = ad
return ad
}
func clear() {
map.removeAll()
}
}
#endif
// MARK: CBCentralManager didDiscover peripheral
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
#if targetEnvironment(macCatalyst)
// MARK: MacOS probably assumes that if "kCBAdvDataIsConnectable" is not present in parial advertisment data it nust be 0
// var ad = advertisementData
// if ( ad["kCBAdvDataManufacturerData"] == nil ) {
// ad.removeValue(forKey: "kCBAdvDataIsConnectable")
// }
let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
#else
let completeAdvertisementData = advertisementData
#endif
//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)'")
}
}
}
// MARK: MacOS multiple discoveries advertisementData caching
#if targetEnvironment(macCatalyst)
private class AdvertisementDataCache {
fileprivate var map = [UUID: [String: Any]]()
func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
var ad = (map[peripheral.identifier]) ?? [String: Any]()
for (key, value) in advertisementData {
ad[key] = value
}
map[peripheral.identifier] = ad
return ad
}
func clear() {
map.removeAll()
}
}
#endif