ron
ron

Reputation: 11

BLE peripheral gets disconnected after extended period of phone being locked

I implemented bluetooth state preservation and restoration on my iOS app, successfully (I think).

After a few hours about 5+ of the phone being locked it fails to reconnect to the peripheral, I think there is something wrong with the code I am using. It was from an open source app that did't implement restore and relied on gps background activity whereas mine does not.

I added the UIBackgroundMode, and followed the steps on apple's documentation. (app uses only one central manager) Is there anything wrong with my BLEmanager or the way it handles disconnects?

BLEmanager
class BLEController: NSObject, CBPeripheralDelegate, ObservableObject {
 var context: NSManagedObjectContext?
 //var userSettings: UserSettings? 
 private var centralManager: CBCentralManager!
 @Published var peripherals: [Peripheral] = []
 @Published var connectedPeripheral: Peripheral!
 @Published var lastConnectionError: String
 var TORADIO_characteristic: CBCharacteristic!
 var FROMRADIO_characteristic: CBCharacteristic!
 var FROMNUM_characteristic: CBCharacteristic!
 let RavedioServiceCBUUID = CBUUID(string: "0x6992DED8-E048-4499-992B-FF86BE69D834")
 let TORADIO_UUID = CBUUID(string: "0x614614B5-0F05-4013-B3CF-E777505D8BB7")
 let FROMRADIO_UUID = CBUUID(string: "0x444D6B7B-6A32-4718-948D-9217F5442FC5")
 let EOL_FROMRADIO_UUID = CBUUID(string: "0xE7CBCBBF-D642-45AB-8729-3FB23E63E0CC")
 let FROMNUM_UUID = CBUUID(string: "0x100A0727-E3CF-4AE0-8333-AFF4E7BAC1B5")



// MARK: init BLEController
 override init() {
 self.lastConnectionError = ""
 super.init()
 //centralManager = CBCentralManager(delegate: self, queue: nil)
 centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "radioIdentifierRestorationKey"])
    }


// MARK: Scanning for BLE Devices
 func startScanning() {
 if isSwitchedOn {
 centralManager.scanForPeripherals(withServices: [RavedioServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
 print("✅ Scanning Started")
    }
  }

 // Stop Scanning For BLE Devices
 func stopScanning() {
 if centralManager.isScanning {
  centralManager.stopScan()
  print("🛑 Stopped Scanning")
    }
  }



// MARK: BLE Connect functions
 @objc func timeoutTimerFired(timer: Timer) {
  guard let timerContext = timer.userInfo as? [String: String] else { return }
  let name: String = timerContext["name", default: "Unknown"]
  self.timeoutTimerCount += 1
  self.lastConnectionError = ""
  if timeoutTimerCount == 10 {
  if connectedPeripheral != nil {
   self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral)
    }
   connectedPeripheral = nil
   if self.timeoutTimer != nil {
   self.timeoutTimer!.invalidate()
    }
  self.isConnected = false
  self.isConnecting = false
  self.lastConnectionError = String.localizedStringWithFormat("Connection failed after %d attempts to connect to %@. You may need to forget your device under iPhone Settings > Bluetooth.".localized, timeoutTimerCount, name)
  print(lastConnectionError)
  self.timeoutTimerCount = 0
  self.startScanning()
   } else {
   print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)")
    }
   }

// Connect to a specific peripheral
  func connectTo(peripheral: CBPeripheral) {
   stopScanning()
   DispatchQueue.main.async {
   self.isConnecting = true
   self.lastConnectionError = ""
   self.automaticallyReconnect = true
    }
   if connectedPeripheral != nil {
   print("ℹī¸ BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")")
   disconnectPeripheral()
    }
        
  centralManager?.connect(peripheral)
  // Invalidate any existing timer
  if timeoutTimer != nil {
   timeoutTimer!.invalidate()
    }
// Use a timer to keep track of connecting peripherals, context to pass the radio name with the timer and the RunLoop to prevent
// the timer from running on the main UI thread
 let context = ["name": "\(peripheral.name ?? "Unknown")"]
 timeoutTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true)
  RunLoop.current.add(timeoutTimer!, forMode: .common)
  print("ℹī¸ BLE Connecting:")
    }

// Called each time a peripheral is discovered
 func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  isConnecting = false
  isConnected = true
  if UserDefaults.preferredPeripheralId.count < 1 {
   UserDefaults.preferredPeripheralId = peripheral.identifier.uuidString
    }
  // Invalidate and reset connection timer count
  timeoutTimerCount = 0
  if timeoutTimer != nil {
   timeoutTimer!.invalidate()
    }
        
  // remove any connection errors
  self.lastConnectionError = ""
  // Map the peripheral to the connectedPeripheral ObservedObjects
  connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first
   if connectedPeripheral != nil {
    connectedPeripheral.peripheral.delegate = self
    } else {
  // we are null just disconnect and start over
  lastConnectionError = "Bluetooth connection error, please try again."
    disconnectPeripheral()
        return
    }
    // Discover Services
    peripheral.discoverServices([RavedioServiceCBUUID])
    print("✅ BLE Connected:")
  }


// Disconnect Peripheral Event
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
    self.connectedPeripheral = nil
    self.isConnecting = false
    self.isConnected = false
    self.isSubscribed = false
    let manager = LocalNotificationManager()
    if let e = error {

     let errorCode = (e as NSError).code
         // CBError.Code.connectionTimeout The connection has timed out unexpectedly.
     if errorCode == 6 { 

    // Happens when device is manually reset / powered off
    print("🚨 BLE Disconnected")
     } else if errorCode == 7 { 

          // CBError.Code.peripheralDisconnected The specified device has disconnected from us.
     // Seems to be what is received when a radio (different model) sleeps, immediately reconnecting does not work.
     print("🚨 BLE Disconnected:")

     } else if errorCode == 14 { // Peer removed pairing information

     // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
          print("🚨 BLE Disconnected:")
      } else {

        if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString {
                 //send notification
         }
        print("🚨 BLE Disconnected:")
            
                   // I think this is where to implement reconnect????
            connectTo(peripheral: peripheral)
        } else {
            // Disconnected without error which indicates user intent to disconnect
            print("ℹī¸ BLE Disconnected:")
        }

             //This commented out code was left over from previous app, where they didnt have restoration.
        // Start scan so the disconnected peripheral is moved to peripherals[] if awake
        //self.startScanning()
                
     //I tried adding this here but it kep trying to connect even on disconnect so added it above
        //self.connectToPreferredPeripheral() 
    }


   func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
    let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral]
    let navigationServiceUUIDObject = RavedioServiceCBUUID
    let recoverPeripherals = peripherals?.filter({ (peripheral: CBPeripheral) -> Bool in
     var found = false
     if let services = peripheral.services {
       for service in services {
         if service.uuid == navigationServiceUUIDObject {
         found = true
         break
             }
         }
     }
     return found
    })
    foundPeripherals = recoverPeripherals
   }



   // Called each time a peripheral is discovered
   func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        
    if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
    self.connectTo(peripheral: peripheral)
    print("BLE Reconnecting to prefered peripheral:")
    }
    let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
    let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", longName: name ?? "Unknown", firmwareVersion: "Unknown", peripheral: peripheral)
        let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral)
        
        if let peripheralIndex = index {
            peripherals[peripheralIndex] = device
        } else {
            peripherals.append(device)
        }
        let today = Date()
        let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)!
        self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration})
    }

TYIA

Upvotes: 0

Views: 68

Answers (0)

Related Questions