There are a few Stack Overflow questions about this however they are either outdated or have mixed answers on whether 2 central managers are supported. I am using IOS 16.2, the problem also occurs on IOS 16.1. Xcode version 14.1.
PROBLEM:
When attempting to initialize multiple CBCentralManagers, only the first CBCentralManager calls the centralManagerDidUpdateState() function to update its state, the other managers have their state always at unknown.
DESIRED STATE:
All central managers call the centralManagerDidUpdateState() function of their respective delegates. In the code provided, the second central manager should print "SECOND MANAGER UPDATED STATE."
BACKGROUND:
The reason I am using multiple central managers is because I am using a package that initializes its own Core Bluetooth central manager (https://github.com/NordicSemiconductor/IOS-nRF-Mesh-Library/blob/main/nRFMeshProvision/Classes/Bearer/GATT/BaseGattProxyBearer.swift#L109). I noticed the code wasn't doing what it was supposed to (and documented to do). The documentation states that the object in the link I provided above can be initialized within the didDiscover function of a user defined central manager's delegate (https://github.com/NordicSemiconductor/IOS-nRF-Mesh-Library/blob/main/Documentation.docc/nRFMeshProvision.md#provisioning). I investigated enough to find the problem lies in having multiple central managers. A workaround I can pursue is using only 1 central manager and passing it to the package, but that would require me to edit the source and perhaps lead to more cumbersome unsupported workarounds with it. The package was documented to be working in September of 2022, so I don't know if Apple changed the way Core Bluetooth CentralManagers behave to only allow 1 working at a time, or if there may be something wrong with my environment.
Here is my code to replicate the issue easily.
CODE:
ModelData, where the central managers are initialized:
import Foundation
import Combine
import CoreBluetooth
final class ModelData: {
let centralManager: CBCentralManager!
let secondCentralManager: CBCentralManager!
init() {
let centralManagerDelegate = CentralManagerDelegate()
centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: nil)
let secondCMDelegate = SecondCentralManagerDelegate()
secondCentralManager = CBCentralManager(delegate: secondCMDelegate, queue: nil)
print("initialized central managers")
}
}
Central Manager Delegate:
import Foundation
import CoreBluetooth
class CentralManagerDelegate: NSObject, ObservableObject, CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown:
print("Bluetooth Device is UNKNOWN")
case .unsupported:
print("Bluetooth Device is UNSUPPORTED")
case .unauthorized:
print("Bluetooth Device is UNAUTHORIZED")
case .resetting:
print("Bluetooth Device is RESETTING")
case .poweredOff:
print("Bluetooth Device is POWERED OFF")
case .poweredOn:
print("Bluetooth Device is POWERED ON")
#unknown default:
print("Unknown State")
}
}
}
Second Central Manager Delegate:
import Foundation
import CoreBluetooth
class SecondCentralManagerDelegate: NSObject, ObservableObject, CBCentralManagerDelegate {
func centralManagerDidUpdateState(\_ central: CBCentralManager) {
print("SECOND MANAGER UPDATED STATE")
}
}
The view calling the modelData:
struct HomeScreen: View {
#StateObject private var modelData = ModelData()
#State private var showDetail = true
var body: some View {
}
}
Output when running:
initialized central managers
Bluetooth Device is POWERED ON
You are creating your delegate objects as local variables and since the CBCentralManager doesn't retain its delegate, these will be released as soon as init returns. You are probably just lucky that the first delegate has an opportunity to print the state before it is released. The second delegate is released too soon.
You can easily confirm this by adding a deinit method to your delegate classes that prints something - You will see your delegates being de-initialised.
The solution is to use a property to retain a strong reference to your delegates as you do with the CBCentralManager instances
final class ModelData: {
let centralManager: CBCentralManager!
let secondCentralManager: CBCentralManager!
let centralManagerDelegate: CentralManagerDelegate
let secondCMDelegate: SecondCentralManagerDelegate
init() {
centralManagerDelegate = CentralManagerDelegate()
centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: nil)
secondCMDelegate = SecondCentralManagerDelegate()
secondCentralManager = CBCentralManager(delegate: secondCMDelegate, queue: nil)
}
}
Related
I'm learning iOS vs OSX BLE.
I notice that I can't instantiate CBCentralManager in iOS because of:
[CoreBluetooth] XPC connection invalid
Unsupported
Versus via OSX because the iOS platform doesn't have the 'App Sandbox' Characteristic where I can set for BLE use.
Here's my iOS Code:
import SwiftUI
struct ContentView: View {
#ObservedObject var bleManager = BLEManager()
var body: some View {
ZStack {
Color("Background")
Text("Hello")
}
}
}
class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate {
var centralManager: CBCentralManager!
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
print("power is on")
case .resetting:
print("Resetting")
case .unsupported:
print("Unsupported")
case .unauthorized:
print("UnAuthorized")
case .unknown:
print("UnKnown")
case .poweredOff:
print("Powered OFF")
#unknown default:
print("**** Default ****")
}
}
}
Here's the required .plist entry:
I understand that iPhones can be either a BLE center or peripheral.
Simple Question: How do I code for a bona fide CBCentralMaster for iOS?
I'm merely taking baby steps here: coding for peripheral detection.
... then continue from there.
Are you running on the simulator (where it is unsupported) instead of on device?
See the main Core Bluetooth docs:
Your app will crash if its Info.plist doesn’t include usage description keys for the types of data it needs to access. To access Core Bluetooth APIs on apps linked on or after iOS 13, include the NSBluetoothAlwaysUsageDescription key. In iOS 12 and earlier, include NSBluetoothPeripheralUsageDescription to access Bluetooth peripheral data.
Currently I monitor Bluetooth status like this:
class One: NSObject {
private let centralManager = CBCentralManager()
init( ) {
super.init()
centralManager.delegate = self
}
}
extension One: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
NotificationCenter.default.post(
name: .xxx,
object: central.state)
}
}
And then another class listens to this notification, etc. Code above is simplified, as I think everything else is irrelevant, but let me know if something is missing from this picture.
This code works if bluetooth status is changed while application is running. But when application starts I am not getting any notification, so I don't know what is initial status of the Bluetooth.
So how do I get initial status of bluetooth on application startup?
In my app when I launch my app I want to check that device Bluetooth is on or off.
generally, we can get this in CBCentralManagerDelegate. Here is my Code
var manager:CBCentralManager!
in init() or viewDidLoad()
{
manager = CBCentralManager()
manager.delegate = self
}
// Delegate method
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
}
else if central.state == .resetting{
}
else if central.state == .unauthorized
{
}
else if central.state == .unknown
{
}
else if central.state == .unsupported
{
}
else if central.state == .poweredOff{
print("Bluetooth is not Connected.Please Enable it")
}
}
But the issue is that in this method we can get Bluetooth state only when the state is updated during app life cycle.
But we can not check at launch time without any state change.
So how can I get this at launch time to check that Bluetooth is on or off?
Shortly after you instantiate a CBCentralManager you will get a call to the centralManagerDidUpdateState delegate method with the current state. An explicit change in Bluetooth state is not required to trigger this behaviour.
I would suggest that you use appropriate CBCentralManager initialiser so that you can specify your delegate as part of the initialisation:
manager = CBCentralManager(delegate: self, queue: nil)
This is the only way to obtain Bluetooth power state.
There is no synchronous method that reports the current Bluetooth power state.
You need to update your UI/display an alert or take whatever other action you require in response to the delegate method call.
You can specify an option when you instantiate your CBCentralManager that causes iOS to show a system alert if Bluetooth is not currently on.
manager = CBCentralManager(delegate: self, queue: nil, options:
[CBCentralManagerOptionShowPowerAlertKey:1])
I am trying to make an app that will be used as a main control for a bluetooth watch(e.g. fitness bracelets, smart watches). I have done my research about this, and although some people managed to do so, they don't give many details about the process. Below are a few of the "solutions" that I found:
Is it possible to switch central and peripheral each other on iOS?
Can iOS do central and peripheral work on same app at same time?
Peripheral and central at the same time on iOS
All of these are in Objective-C and although I am familiar with it, the posts are 3+ years old so things have changed concerning the code. Another problem is that I need to use the app with another Bluetooth device, not an iOS device as the ones above are doing it, and for the moment the connection request can only come from the iPhone, not from the bluetooth device.
The question is if it's possible to achieve the desired result, and if so, what would be the best way to do it? So far, one of the proposed solutions was to connect to the device, acquire the UUID and then switch the iPhone to peripheral mode so that it can advertise it's services. That is not possible(in my opinion), at least in this current stage.
iOS already has a predefined service that can be discovered and accessed by the device (Current Time Service) when the 2 of them connect, without any modifications from my part so there should be a way to accomplish this.
I hope I made myself clear enough about the problem, if you believe I can add more details to clarify the context, let me know. Thanks for your time.
I am posting below some of the key code from the view in which I discover peripherals:
override func viewDidAppear(_ animated: Bool) {
manager = CBCentralManager(delegate: self, queue: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
peripherals = []
if (manager?.state == CBManagerState.poweredOn) {
scanBLEDevices()
self.tableView.reloadData()
}
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch(peripheral.state)
{
case.unsupported:
print("Peripheral is not supported")
case.unauthorized:
print("Peripheral is unauthorized")
case.unknown:
print("Peripheral is Unknown")
case.resetting:
print("Peripheral is Resetting")
case.poweredOff:
print("Peripheral service is powered off")
case.poweredOn:
print("Peripheral service is powered on")
print("Start advertising.")
let serviceUUID:CBUUID = CBUUID(string: self.service_uuid_string)
let locationUUID:CBUUID = CBUUID(string: self.location_and_speed)
// Start with the CBMutableCharacteristic
self.locationCharacteristic = CBMutableCharacteristic(type: locationUUID, properties: .notify , value: nil, permissions: .readable)
// Then the service
let locationService = CBMutableService(type: serviceUUID, primary: true)
// Add the characteristic to the service
locationService.characteristics?.append(locationCharacteristic!)
// And add it to the peripheral manager
self.peripheralManager?.add(locationService)
peripheralManager?.startAdvertising([CBAdvertisementDataServiceUUIDsKey : serviceUUID])
}
}
I'm getting back with the correct way of achieving the required functionality. After initialising the peripheralManager, create a CBMutableService and hold a reference to it(declared at the top of the class).
var globalService:CBMutableService? = nil
Next step is to check for the peripheralManager state, and do all the required work after you receive the poweredOn state:
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch(peripheral.state)
case.poweredOn:
print("Peripheral service is powered on")
createServiceWithCharacteristics()
}
func createServiceWithCharacteristics(){
let serviceUUID:CBUUID = CBUUID(string: self.service_uuid_string)
let featureCharacteristicUUID:CBUUID = CBUUID(string: self.feature_characteristic_uuid_string)
// Start with the CBMutableCharacteristic
let permissions: CBAttributePermissions = [.readable, .writeable]
let properties: CBCharacteristicProperties = [.notify, .read, .write]
self.featureCharacteristic = CBMutableCharacteristic(type: featureCharacteristicUUID, properties: properties , value: nil, permissions: permissions)
// Then the service
let localService = CBMutableService(type: serviceUUID, primary: true)
// Add the characteristic to the service
localService.characteristics = [featureCharacteristic!]
globalService = localService
// And add it to the peripheral manager
self.peripheralManager?.add(globalService!)
print("Start advertising.")
peripheralManager?.startAdvertising([CBAdvertisementDataLocalNameKey:"Name"])
}
I'm trying to send a process in a background thread using the following code:
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
print("running in the background queue")
btDiscovery
})
but the class is only processing while begin in foreground...any idea ?
EDIT1:
btDiscovery is a class which performs a BLE device scan every X seconds:
let btDiscoverySharedInstance = Beacon();
class Beacon: NSObject, CBCentralManagerDelegate {
private var centralManager: CBCentralManager?
private var peripheralBLE: CBPeripheral?
....
func centralManagerDidUpdateState(central: CBCentralManager) {
switch (central.state) {
case CBCentralManagerState.PoweredOff:
print("BLE powered off")
self.clearDevices()
case CBCentralManagerState.Unauthorized:
// Indicate to user that the iOS device does not support BLE.
print("BLE not supported")
break
case CBCentralManagerState.Unknown:
// Wait for another event
print("BLE unknown event")
break
case CBCentralManagerState.PoweredOn:
print("BLE powered on")
self.startScanning()
break
case CBCentralManagerState.Resetting:
print("BLE reset")
self.clearDevices()
case CBCentralManagerState.Unsupported:
print("BLE unsupported event")
break
}
}
func startScanning() {
print("Start scanning...")
if let central = centralManager {
central.scanForPeripheralsWithServices(nil, options: nil)
}
}
func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {
print("Discovered peripheral \(RSSI) dBM name: \(peripheral.name)")
print("UUID: \(peripheral.identifier.UUIDString)")
...
sleep(delayPolling)
self.startScanning()
}
when the app is launched and remains in foreground, the scan is performed correctly every "delayPolling" seconds.
but as soon as I put my app is background, the scan is paused. it restarts only when it comes back again in foreground.
I would need to leave this scan running in background every time (even if we set a lower priority to this thread).
EDIT2:
by reading the documentation https://developer.apple.com/library/ios/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html
I can see that
When an app that implements the central role includes the UIBackgroundModes key with the bluetooth-central value in its Info.plist file, the Core Bluetooth framework allows your app to run in the background to perform certain Bluetooth-related tasks. While your app is in the background you can still discover and connect to peripherals, and explore and interact with peripheral data. In addition, the system wakes up your app when any of the CBCentralManagerDelegate or CBPeripheralDelegate delegate methods are invoked
I selected the corresponding options in my Info.plist file:
but my app is not running my thread in background.
I realize this is an old question, but scanning in the background requires that you supply a Service UUID.
central.scanForPeripheralsWithServices(nil, options: nil)
needs to be
central.scanForPeripheralsWithServices(serviceUUID, options: nil)