jsoncreator_app/JSONCreator_iOS/JSONCreator/5G/MFFGHSBluetoothPair.swift

548 lines
22 KiB
Swift

//
// 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 } ?? []
}
}