Reputation: 11
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