This question already has answers here:
iOS: didDiscoverPeripheral not called in Background mode
(4 answers)
Closed 2 years ago.
I want to scan for Bluetooth peripheral in the background and make an API Call with found peripherals. I am doing this.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let theViewController = loginVC()
APIManager.sharedInstance.pushtracerData(vc: theViewController, user_id: USERID.sharedInstance.getUserID(), SSID: peripheral.name ?? "", MAC: peripheral.identifier.uuidString, RSSI: "\(RSSI)", TST: "", tracer_or_mobile: "2") {
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey:NSNumber(value: false)]
centralManager?.scanForPeripherals(withServices: nil, options: options)
}
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
self.centralManager = CBCentralManager.init(delegate: self, queue: DispatchQueue.main)
completionHandler(.newData)
}
But in the background, this method is not called. Please help me out
centralManager?.scanForPeripherals(withServices: nil, options: options)
From the docs:
Your app can scan for Bluetooth devices in the background by specifying the bluetooth-central background mode. To do this, your app must explicitly scan for one or more services by specifying them in the serviceUUIDs parameter.
It's recommended in all cases that you scan for specific services and not pass nil here, but in the background it's required.
Related
var peripherals = [CBPeripheral]()
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if let _ = peripheral.name {
if !peripherals.contains(peripheral) {
peripherals.append(peripheral)
table.reloadData()
}
}
}
Using the delegate method above, called after centralManager.scanForPeripherals(withServices: nil), I am showing available peripherals(only those with name).
Since some of the available devices are also mine, I am sure that bluetooth is turned off on them.
So why does this method returns them as available? Are they cached somewhere or?
I want to get notified when something happened at the BLE device.
If BLE device passes some data/Command to the app, then in-app the advertisementData not changed.
But the same thing we can do with android it's working perfectly.
I want to implement functionality like when advertisementData changed I want to get notify.
Please help me to implement this.
Below is my AppDelegate.swift class.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
let kTrackStandardDeviceInfoServiceUUID = CBUUID(string: "180A")
let dictionaryOfOptions = [CBCentralManagerScanOptionAllowDuplicatesKey : true]
let arrayOfServices: [CBUUID] = [kTrackStandardDeviceInfoServiceUUID]
centralManager.scanForPeripherals(withServices: arrayOfServices, options: dictionaryOfOptions)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
For having the application can work in the background, you need to implement some background services in your apps.
Usually, background service is location fetch.
Please note that background service will make your app draining the battery faster
To implement the background service, Click your project, Signing & Capabilities, Background Modes, enable the BLE features.
I want to scan BLE devices on the background as like a foreground. But my iOS app doesn't work as I expect. Below is my AppDelegate class code.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
print(centralManager.state)
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
centralManager.scanForPeripherals(withServices: nil, options: nil)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
}
Please help me to scan app in background state as like a foreground.
From Docs: link
Apps that have specified the bluetooth-central background mode are
allowed to scan while in the background. That said, they must
explicitly scan for one or more services by specifying them in the
serviceUUIDs parameter. The CBCentralManager scan option is ignored
while scanning in the background.
For background scanning to work you need to specify serviceUUIDs.
I'm working on an app which connects to a BLE Peripheral, and I sometimes run into a very strange issue, where the Peripheral is never found by the CentralManager. However, when pulling down Notification Center on the device, or when swiping up Control Center, the Peripheral immediately shows up and connects.
I've tried to find the cause of this problem, but so far I haven't been able to find anything. Apart from willResignActive and didBecomeActive, no other lifecycle functions seem to be called (AFAIK), but in neither of those functions I do anything other than printing that they've been called.
I already made sure to use self.centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) in case I somehow screw up and not detect an initial scan.
Does anyone know what might be the cause of this, and how Control Center or Notification Center's influence on the app might solve this problem?
Thanks!
EDIT: Some extra code that might help find the issue. However, I know that when the problem happens, both discoveredPeripheral and connectedPeripheral are nil.
didConnect
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
if centralManager.isScanning {
centralManager.stopScan()
}
self.connectedPeripheral = peripheral
self.discoveredPeripheral?.delegate = self
debugPrint("\(Date()): Connected \(peripheral)")
peripheral.discoverServices(nil)
DispatchQueue.main.async {
self.rssiTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.readPeripheralRSSI), userInfo: nil, repeats: true)
}
}
didDiscover
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
if peripheral.name == global.qnr && discoveredPeripheral == nil {
centralManager.stopScan()
debugPrint(
"Discovered \(peripheral). " +
"Initiating authentication sequence with \n" +
"\tQNR: \(global.qnr)\n" +
"\tSessionKey: \(global.sessionKey)\n" +
"\tToken: \(global.bluetoothToken)")
if self.discoveredPeripheral == nil || self.discoveredPeripheral != peripheral {
self.discoveredPeripheral = peripheral
self.centralManager.connect(peripheral, options: nil)
}
}
}
startScanning
func scanForPeripherals() {
if !centralManager.isScanning && global.sessionKey != "" {
let services = [CONSTANTS.CBUUID]
self.centralManager.scanForPeripherals(withServices: services, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
}
}
You have to notify the delegate when a device(s) discovered. This is how I handle. I remove the previously added devices when I discover and then add or re-add. Once the discovered devices are added to the array, then reload the table if you are using the tableView to display.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if (peripheral.name != nil) {
// remove previously added devices
items.removeAll()
items.append(peripheral)
}
// Set RSSI to custom peripheral signal property
customPeripheral.signal = RSSI
// Notify delegate for change in data.
NotificationCenter.default.post(name: .reload, object: nil)
}
// Extension for reload tableView when peripheral array is updated
extension Notification.Name {
static let reload = Notification.Name("reload")
}
In your case. You notify the notification to connect to your device once the scan is complete and the device is found.
// Connect with peripheral
func connectPeripheral(peripheral: CBPeripheral) {
centralManager.connect(peripheral, options: nil)
centralManager.stopScan()
print("Connecting")
}
I'm using CoreBluetooth framework in my iOS application to scan and connect to other bluetooth devices. I'm able to scan successfully for devices using below code:
var centralManager: CBCentralManager?
var peripherals = [CBPeripheral]()
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
In the CBCentralManagerDelegate, I implemented below code:
extension ViewController: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if (central.state == .poweredOn){
self.centralManager?.scanForPeripherals(withServices: nil, options: nil)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripherals.contains(peripheral) {
let localName = advertisementData[CBAdvertisementDataLocalNameKey]
print("\(String(describing: localName))" )
peripherals.append(peripheral)
tableView.reloadData()
}
}
}
From above code localName is always nil. Am I missing any code?
To add more info,
When I am scanning for bluetooth devices in my Mac, I'm able to see the names of the devices. But when I am scanning for bluetooth devices on my iPhone, none of them are listed.
I cross checked and made sure that I enabled blue tooth on 4 more devices with me (iPad, 2 Android Phones and 1 Android TV)
Swift with core bluetooth library:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber){
let peripheralLocalName_advertisement = ((advertisementData as NSDictionary).value(forKey: "kCBAdvDataLocalName")) as? String
if (((advertisementData as NSDictionary).value(forKey: "kCBAdvDataLocalName")) != nil)
print(peripheralLocalName_advertisement)//peripheral name from advertismentData
print(peripheral.name)//peripheral name from peripheralData
peripherals.append(peripheral)
arrayPeripheral.append(advertisementData)
}
}
I changed in your code only source of name - instead of:
let localName = advertisementData[CBAdvertisementDataLocalNameKey]
I use:
let localName = peripheral.name
and now I see name of BLE devices around me.
I changed in your code only source of name - instead of:
let localName = advertisementData[CBAdvertisementDataLocalNameKey]
I use:
let localName = peripheral.name
and now I see name of BLE devices around me.
The didDiscoverPeripheral callback unfortunately can return nil for CBPeripheral names and also the advertisementData's local name for the very first callback your app gets after you start a scan.
If you know that the peripheral that you're working with will contain a local name in the Bluetooth advertisement packet, you can choose to ignore the first callback that contains a nil name.
// In didDiscoverPeripheral
guard let peripheralName = peripheral.name else { return }
print("Did discover peripheral: \(peripheralName), RSSI: \(RSSI)")