// // MFFGHSBluetoothPair.swift // MobileFirstFramework // // Created by Chowdhury, Shohrab on 3/29/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import CoreBluetooth public enum MFFGHSSignalStatus: NSNumber { case noBluetoothConnection = -1, noSignal = 1, poor, good, confirmedGood, confirmedPoor } public enum MFFGHSSignalStrengthCheckStatus: NSNumber { case idle = 0, processing, complete } public protocol MFFGHSBluetoothTestingProtocol { var continueTesting: Bool { get set } } public protocol MFFGHSBluetoothPairDelegate : NSObjectProtocol { /// Receive ble status ON/OFF func bluetoothStatus(isOn: Bool) /// New status that's more reliable as a callback if BLE is off func bluetoothOff() /// Bluetooth does not have access from the user func bluetoothPermissionsDenied() /// BLE found a device that doesn't match what it expects to find. func deviceIMEIMismatch(expected: String, found: String) /// An attempted pairing failed due to a broadcast naming mismatch. func onBluetoothDiscoveredNameFailed(expectedName: String, foundNames: [String]) /// The bluetooth service was discovered. func onBluetoothDiscovered(expectedName: String, foundName: String) /// Check device is paired with phone func pairUpdate(isPaired: Bool) /// Check device is activated or not func updateActivatedStatus(isActivated: Bool) /// Receive current RSSI func updateCurrentSignalStatus(_ status: MFFGHSSignalStatus, strengthCheckStatus: MFFGHSSignalStrengthCheckStatus, rssi: Double?) } extension MFFGHSBluetoothPairDelegate { public func bluetoothStatus(isOn: Bool) {} public func bluetoothOff() {} public func bluetoothPermissionsDenied() {} public func deviceIMEIMismatch(expected: String, found: String) {} public func onBluetoothDiscoveredNameFailed(expectedName: String, foundNames: [String]) {} public func onBluetoothDiscovered(expectedName: String, foundName: String) {} public func pairUpdate(isPaired: Bool) {} public func updateActivatedStatus(isActivated:Bool) {} public func updateCurrentSignalStatus(_ status: MFFGHSSignalStatus, strengthCheckStatus: MFFGHSSignalStrengthCheckStatus, rssi: Double?) {} } class MFFGHSBluetoothPair: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, BluetoothParingProtocol { // !!Major change!! Some controllers are relying on bluetooth object being passed. static weak var instance:MFFGHSBluetoothPair? /// Weak instance returned. Controller that uses it should hold onto strong reference. When all controllers deallocate, this will also. static var sharedInstance: MFFGHSBluetoothPair { get { if let instance = instance { return instance } else { let newInstance = MFFGHSBluetoothPair() instance = newInstance return newInstance } } } // Bluetooth control objects. var centralManager: CBCentralManager? var peripheral: CBPeripheral? // Event delegate. weak var delegate: MFFGHSBluetoothPairDelegate? //debugger for event delegate var bluetoothDebugger: BluetoothDebugger? // Data from bluetooth connection var isPaired = false var isCPEActivated = false var currentSignalStatus: MFFGHSSignalStatus = .noBluetoothConnection var currentRSSIValue: Double? // Configured properties. var bluetoothConfig: BluetoothConfigModel? { didSet { if (oldValue != bluetoothConfig) { // TODO: Needs deep check for config changes. setupBluetoothScanner() } } } var signalStrengthObserveDuration: TimeInterval = 30 var signalPassingPercentage: Double = 100 var signalStrengthCheckStatus: MFFGHSSignalStrengthCheckStatus = .idle var signalStrengthObserverTimer: Timer? var lastSignalStatusTimeStamp: Date? var signalStrengthGoodTotalTime: Double = 0 var signalStrengthFailTotalTime: Double = 0 override init() { super.init() setupBluetoothScanner() } #if DEBUG deinit { print("bluetooth destroy") } #endif var bluetoothEnabled:Bool { #if targetEnvironment(simulator) return true #else return self.centralManager?.state == CBManagerState.poweredOn #endif } func scanForPeripherals(central:CBCentralManager) { let advertiseIds = getBleAdvertiseUUIDs() if advertiseIds.count > 0 { central.scanForPeripherals(withServices: advertiseIds, options: nil) } else { central.stopScan() } } func setupBluetoothScanner(showPowerAlert:Bool = false) { // Reset connection on new config. if let peripheral = self.peripheral { centralManager?.cancelPeripheralConnection(peripheral) self.peripheral = nil self.isPaired = false self.currentSignalStatus = .noBluetoothConnection } if self.centralManager == nil || showPowerAlert { centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [CBCentralManagerOptionShowPowerAlertKey: showPowerAlert]) } else if (bluetoothEnabled) { scanForPeripherals(central: centralManager!) } // else wait for powered on status if GMFGTestScreenData.shared.isCPESimulated { #if DEBUG simulateBluetoothMessaging() #endif } else { #if DEBUG && targetEnvironment(simulator) simulateBluetoothMessaging() #endif } } #if DEBUG // For simulating RSSI changes. var timer: Timer? private func simulateBluetoothMessaging() { // If we don't get advertisement data, technically nothing will come. if getBleAdvertiseUUIDs().count == 0 { return } if timer != nil { return } var signalRotate = 0 _ = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { (timer) in self.delegate?.bluetoothStatus(isOn: true) self.isCPEActivated = true self.delegate?.updateActivatedStatus(isActivated: self.isCPEActivated) self.isPaired = true self.delegate?.pairUpdate(isPaired: self.isPaired) signalRotate = (signalRotate + 1) % 5 self.currentRSSIValue = Double(signalRotate) if (signalRotate >= 3) { self.currentRSSIValue = -76 self.currentSignalStatus = .confirmedGood } else if (signalRotate >= 2) { self.currentRSSIValue = -86 self.currentSignalStatus = .good } else if (signalRotate >= 1) { self.currentRSSIValue = -101 self.currentSignalStatus = .poor } else { self.currentRSSIValue = -999 self.currentSignalStatus = .noSignal } self.delegate?.updateCurrentSignalStatus(self.currentSignalStatus, strengthCheckStatus:self.signalStrengthCheckStatus, rssi: self.currentRSSIValue) self.bluetoothDebugger?.bluetoothStatus = true self.bluetoothDebugger?.isDeviceActivated = true self.bluetoothDebugger?.isDevicePaired = self.isPaired self.bluetoothDebugger?.updateCurrentSignalStatus(self.currentSignalStatus, strengthCheckStatus:self.signalStrengthCheckStatus, rssi: self.currentRSSIValue) } } #endif // Should be used when we detect scanning is no longer required. private func stopScanning() { if let peripheral = self.peripheral { centralManager?.cancelPeripheralConnection(peripheral) self.peripheral = nil; } centralManager?.stopScan() centralManager = nil } open func shutdown() { stopScanning() MFFGHSBluetoothPair.instance = nil } public func stopNotify5G(controller: MFFGHSBluetoothPairDelegate) { if let del = delegate, del === controller { delegate = nil } resetSignalStrengthCheckValue() } public func setup(config: BluetoothConfigModel, delegate: BluetoothDebuggerDelegate) { bluetoothDebugger = BluetoothDebugger(config: config, delegate: delegate) bluetoothConfig = config } public func startNotify5G(controller: MFFGHSBluetoothPairDelegate) { delegate = controller // Initialize delegate?.pairUpdate(isPaired: isPaired) delegate?.updateActivatedStatus(isActivated: isCPEActivated) delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) if let state = centralManager?.state { bluetoothDebugger?.bluetoothState = state } bluetoothDebugger?.bluetoothStatus = bluetoothEnabled bluetoothDebugger?.isDevicePaired = isPaired bluetoothDebugger?.isDeviceActivated = isCPEActivated bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus:signalStrengthCheckStatus, rssi: currentRSSIValue) } // MARK:- Bluetooth delegate public func centralManagerDidUpdateState(_ central: CBCentralManager) { bluetoothDebugger?.bluetoothState = central.state switch (central.state) { case .poweredOn: delegate?.bluetoothStatus(isOn: true) bluetoothDebugger?.bluetoothStatus = true scanForPeripherals(central: central) case .poweredOff, .unsupported, .unauthorized, .unknown: delegate?.bluetoothStatus(isOn: false) bluetoothDebugger?.bluetoothStatus = false default: print("bluetooth not powered on") } } public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { if findPeripheralName(getBleAdvertiseName(), in: advertisementData) { self.peripheral = peripheral peripheral.delegate = self central.connect(peripheral, options: nil) central.stopScan() } else { if let foundName = advertisementData[CBAdvertisementDataLocalNameKey] as? String, foundName.hasPrefix(GMFGConstant.BLE.advertisePrefix) { delegate?.deviceIMEIMismatch(expected: getBleAdvertiseName(), found: foundName) bluetoothDebugger?.addDeviceIMEIMismatch(found: foundName) } } } private func findPeripheralName(_ name: String, in advertisementData: [String : Any]) -> Bool { var found = false var foundAdvertisedNames: [String] = [] // Check the local data name. (Where it should be. Used for testing app.) if let localServiceName = advertisementData[CBAdvertisementDataLocalNameKey] as? String { if localServiceName == name { found = true } else { foundAdvertisedNames.append(localServiceName) } } // Check the service data. (Where it is actually implemented by CPE.) if !found, let avdService = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID:Data] { for key in avdService.keys { if let rawName = avdService[key], let advertisedName = String(bytes: rawName, encoding: .utf8) { if (advertisedName == name) { found = true break } else { foundAdvertisedNames.append(advertisedName) } } } } if found { // Currently exact equality passes. Later can adjust to be actual advertised name if the requirement relaxes. delegate?.onBluetoothDiscovered(expectedName: name, foundName: name) bluetoothDebugger?.deviceDiscovered = name let value: [String : Any] = [ "blePairStatus": "discovered", "expectedBroadcastName": name, "foundBroadcastName": name ] track(value) return true; } else { delegate?.onBluetoothDiscoveredNameFailed(expectedName: name, foundNames: foundAdvertisedNames) bluetoothDebugger?.addDiscoveredNameFailures(foundNames: foundAdvertisedNames) let value: [String : Any] = [ "blePairStatus": "mismatch", "expectedBroadcastName": name, "foundBroadcastName": foundAdvertisedNames.joined(separator: " / ").prefix(500) ] track(value) return false } } public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isPaired = true delegate?.pairUpdate(isPaired: true) bluetoothDebugger?.isDevicePaired = true peripheral.discoverServices(getBleAdvertiseUUIDs()) // For testing app which can only advertise on 1 service. peripheral.discoverServices(getSeviceUUIDs()) } public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { for service in peripheral.services ?? [] { peripheral.discoverCharacteristics(getCharacteristicUUIDs(serviceId: service.uuid), for: service) } } public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { for characteristic in service.characteristics ?? [] { peripheral.setNotifyValue(true, for: characteristic) } } public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { let cpeResponse = formatCharacteristicData(characteristic) handleActivated(response: cpeResponse) handleFiveGSignal(response: cpeResponse) } public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { } public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { // We only connect to 1 peripheral. Using that assumption below. isPaired = false currentSignalStatus = .noBluetoothConnection currentRSSIValue = nil delegate?.pairUpdate(isPaired: false) delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) bluetoothDebugger?.isDevicePaired = false bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) self.peripheral = nil scanForPeripherals(central: central) } func formatCharacteristicData(_ characteristic: CBCharacteristic) -> [String: Any] { guard let advertiseData = characteristic.value, !advertiseData.isEmpty, let advertiseDict = try? JSONSerialization.jsonObject(with: advertiseData, options: []) as? [String: Any] else { return [:] } return advertiseDict } // MARK: Handle Activation func handleActivated(response: [String: Any]) { if let activation = response["Activation"] as? [String: Any] { isCPEActivated = activation.stringForkey("Status") == "Activated" delegate?.updateActivatedStatus(isActivated: isCPEActivated) bluetoothDebugger?.isDeviceActivated = isCPEActivated } } // MARK: Handle FiveGSignal func handleFiveGSignal(response: [String: Any]) { if let fivegSignal = response["5GSignal"] as? [String: Any] { if let rssiValue = parseRSRPFromJSON(fiveGData: fivegSignal) { currentRSSIValue = rssiValue // This need to call before currentSignalStatus set with new status. This is to gather the amount of time on the previous signal status before the update. if signalStrengthCheckStatus == .processing { //lastSignalStatusTimeStamp = lastSignalStatusTimeStamp != nil ? lastSignalStatusTimeStamp : Date() checkSignalStrengthContinueRequired() } if GMFGTestScreenData.shared.isEnable5GSignal { currentSignalStatus = currentSignalStatus == .confirmedGood ? .confirmedGood : .good } else { if rssiValue >= higherThreshold || rssiValue <= lowerThreshold { // Future reference: -999 means no signal from CPE. Need to make sure it will always be included in check. // Invalid value. We should not be above the upper limits or beneath the lower limits. currentSignalStatus = .noSignal } else if rssiValue >= rssiThreshold { currentSignalStatus = currentSignalStatus == .confirmedGood ? .confirmedGood : .good } else { currentSignalStatus = .poor } } } else { currentSignalStatus = .noSignal // Invalid value or doesn't exist. } delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) } } // MARK:- Handle Signal History private func resetSignalStrengthCheckValue() { signalStrengthCheckStatus = .idle signalStrengthObserverTimer?.invalidate() signalStrengthObserverTimer = nil lastSignalStatusTimeStamp = nil signalStrengthGoodTotalTime = 0 signalStrengthFailTotalTime = 0 } func checkSignalStrengthContinueRequired() { let lastTrackDate = lastSignalStatusTimeStamp ?? Date() if [.good, .confirmedGood].contains(currentSignalStatus) { signalStrengthGoodTotalTime += Date().timeIntervalSince(lastTrackDate) } else { signalStrengthFailTotalTime += Date().timeIntervalSince(lastTrackDate) } lastSignalStatusTimeStamp = Date() evaluateSignalStrength() } private func evaluateSignalStrength() { let maxFailPercentage = 100 - signalPassingPercentage let maxFailTime = (maxFailPercentage / 100) * signalStrengthObserveDuration let maxPassTime = (signalPassingPercentage / 100) * signalStrengthObserveDuration #if DEBUG print("[ MFFGHSBluetoothPair ] Good Signal Time >>>> \(signalStrengthGoodTotalTime)") print("[ MFFGHSBluetoothPair ] Bad Signal Time >>>> \(signalStrengthFailTotalTime)") #endif if signalStrengthFailTotalTime > maxFailTime { // already Fail, stop testing resetSignalStrengthCheckValue() signalStrengthCheckStatus = .complete currentSignalStatus = .confirmedPoor delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) } else if signalStrengthGoodTotalTime >= maxPassTime || signalStrengthCheckStatus == .complete { // already PASS. stop testing and handle resetSignalStrengthCheckValue() signalStrengthCheckStatus = .complete currentSignalStatus = .confirmedGood delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue) } else { //contiue } } public func startSignalStrengthCheckValue(duration: TimeInterval, percentage: Double){ if signalStrengthCheckStatus != .processing { resetSignalStrengthCheckValue() signalStrengthObserveDuration = duration signalPassingPercentage = percentage if currentSignalStatus == .confirmedGood { currentSignalStatus = .good } signalStrengthCheckStatus = .processing lastSignalStatusTimeStamp = Date() signalStrengthObserverTimer = Timer(timeInterval: (signalStrengthObserveDuration + 0.3), repeats: false) { (timer) in #if DEBUG print("[ MFFGHSBluetoothPair ] Signal Test Timeout") #endif self.signalStrengthCheckStatus = .complete self.checkSignalStrengthContinueRequired() } RunLoop.main.add(signalStrengthObserverTimer!, forMode: .common) } } // MARK:- Utility Functions func parseRSRPFromJSON(fiveGData: [String: Any]) -> Double? { return fiveGData["SS-RSRP"] as? Double ?? nil } // MARK:- Analytics func track(_ data: [String: Any]) { if let _delegate = delegate { trackGemini(value: data.merging(["delegateName": String(describing: _delegate)]) { $1 }, logType: .BLE) } } // MARK: BluetoothConfigModel methods var rssiThreshold: Double { return bluetoothConfig?.bleSignalThreshold ?? 0 } var lowerThreshold: Double { return bluetoothConfig?.bleSignalLowerBound ?? 0 } var higherThreshold: Double { return bluetoothConfig?.bleSignalUpperBound ?? 0 } func getBleAdvertiseUUIDs() -> [CBUUID] { return bluetoothConfig?.peripherals.map{ $0.uuid } ?? [] } open func getBleAdvertiseName() -> String { return bluetoothConfig?.advertisedData ?? "" } func getSeviceUUIDs() -> [CBUUID] { return bluetoothConfig?.services.map { $0.uuid } ?? [] } func getCharacteristicUUIDs(serviceId: CBUUID) -> [CBUUID] { return bluetoothConfig?.services.first?.characteristics.map { $0.uuid } ?? [] } }