I'm developing a BLE application for IOS (SWIFT) and I've found a strange behaviour.. my test has 2 controllers, ONE with the CentralManager Role and the other with the PeripheralManager Role..
Here's my code (summary):
Parameters.swift:
...
// a custome UUID created in console
let TRANSFER_SERVICE_UUID = CBUUID(string: "FB694B90-F49E-....-....-171BBA78F846")
...
Peripheral.swift
...
var pManager = CBPeripheralManager()
var transferService = CBMutableService()
override func viewDidLoad() {
super.viewDidLoad()
pManager = CBPeripheralManager(delegate: self, queue: nil)
}
func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager!) {
if(peripheral.state == CBPeripheralManagerState.PoweredOn) {
transferService = CBMutableService(type: TRANSFER_SERVICE_UUID, primary: true)
// add some characteristic
pManager.addService(transferService)
pManager.startAdvertising(nil)
}
}
...
Central.swift
...
var cManager = CBCentralManager()
override func viewDidLoad() {
super.viewDidLoad()
cManager = CBCentralManager(delegate: self, queue: nil)
}
func centralManagerDidUpdateState(central: CBCentralManager!) {
if central.state == CBCentralManagerState.PoweredOn {
cManager.scanForPeripheralsWithServices([TRANSFER_SERVICE_UUID], options: nil)
}
}
...
Now, if I take 2 device, one with the Central and the other with the Peripheral Role the 2 app can't find each other (but LightBlue app and similar will so the device is emitting)
On the other hand, if I change the code to:
cManager.scanForPeripheralsWithServices(nil, options: nil)
my application works perfectly and the 2 devices can communicate each other.. but at the same time I can't filter only the devices that are emitting TRANSFER_SERVICE_UUID.. I don't want to connect to all peripheral finded in order to search for TRANSFER_SERVICE_UUID.. isn't this the right way to proceed ?? Did I miss something ??
There is only limited space in the Bluetooth advertisement area, so iOS does not automatically advertise all services - A device may have a primary service and several supplementary services. Advertising all services is wasteful when all that is required is to discover the primary service in order to identify a candidate device.
To be able to discover the service in scanForPeripheralsWithServices you need to include the service in the advertisement data.
This is done by including the service's UUID in the dictionary you pass to CBPeripheralManager.startAdvertising -
pManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[TRANSFER_SERVICE_UUID]])
Related
I'm trying to use my MacBook as a peripheral with services for bluetooth scanning so it can be used like this:
var manager:CBCentralManager?
manager?.scanForPeripherals(withServices: nil, options: nil)//the withServices will be replaced with custom service UUID
I tried creating my own custom service UUID and adding it to the peripheralManager like so:
var peripheralManager: CBPeripheralManager?
override func viewDidLoad() {
super.viewDidLoad()
createServiceUUID()
peripheralManager?.startAdvertising(["Test" : "Test"])
}
func createServiceUUID() {
let customServiceUUID = CBUUID(string: customServiceUUIDString)
let mutableService = CBMutableService(type: customServiceUUID, primary: true)
peripheralManager?.add(mutableService)
}
I then tried scanning for the serviceUUID I just created but it doesn't show up:
manager?.scanForPeripherals(withServices: [CBUUID(string: customServiceUUIDString)], options: nil)
I feel like I may be missing a step to turn my Mac into a Peripheral that can be discovered using the
scanForPeripherals(withServices:)
function. Any thoughts on how to make this happen?
I have 3 ViewControllers.
1)Home VC
2) Scan VC
3) Connect VC
Steps:
From Home VC, I push to Scan VC for scanning the available BLE Devices.
Scan VC: I have all the BLE Communication code here:
override func viewDidLoad() {
super.viewDidLoad()
AppConstants.sharedInstance.centralManager = CBCentralManager(delegate: self, queue: nil) // created global Instance for central Manager and assigned it here
}
Above will call centralManagerDidUpdateState(_ central: CBCentralManager) Where I am checking all the state for BLUETOOTH. And if,
case .poweredOn:
btnRefresh.isEnabled = true
print("central.state is .poweredOn...")
startScan()
}
then I Scan for the Devices.
Once I get my device, I connect to it from list of scanned devices,
Then I successfully Connect to that Device it goes to Connect VC.
Till here everything is great.
But real problem starts when
1) I pop from Connect VC to Scan VC and then again Pop to HOMEVC And
2) Then I again push from Home VC to Scan VC
Scan VC: This will call and assign the Delegate to my global CentralManager and as soon as it assign the delegate again, It disconnects the previous connection. (Even I don't get any didDisconnectPeripheral Call)
override func viewDidLoad() {
super.viewDidLoad()
AppConstants.sharedInstance.centralManager = CBCentralManager(delegate: self, queue: nil) // Here when I come again it disconnects old connection.
}
I don't know what's the issue. I am not getting any errors.
Please Help...
I think, Since you are reassigning CBCentralManager to AppConstants.sharedInstance.centralManager in viewDidLoad, CBCentralManager is getting destroyed and disconnected.
Also As it's destroyed, it is not returning any delegate callbacks.
So Can you try to initialize the CBCentralManager only once as like below and disconnect when required?
Example:
override func viewDidLoad() {
super.viewDidLoad()
if AppConstants.sharedInstance.centralManager == nil{
AppConstants.sharedInstance.centralManager = CBCentralManager(delegate: nil, queue: nil)
}
AppConstants.sharedInstance.centralManager.delegate = self
}
No Worries. I found my way to workaround. Thanks to #Natrajan for giving me some idea about it.
override func viewDidLoad() {
super.viewDidLoad()
if AppConstants.sharedInstance.centralManager == nil{
AppConstants.sharedInstance.centralManager = CBCentralManager(delegate: self, queue: nil)
}
else
{
centralManagerDidUpdateState(AppConstants.sharedInstance.centralManager!)
}
}
This serves my purpose and I can Check the Bluetooth State and Scan accordingly (Everytime pushing to the Scan VC)
Yuppieee...
:)
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?
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 am currently writing an app that connects via BLE to an external device. All operations are fine when the app is in foreground.....including connecting, obtaining data, and reconnecting (in cases of the device going out of range) via a reconnect protocol I wrote. The app also functions properly when it is backgrounded but the BLE connection remains alive.
However, the only instance in which the app does not function is if the app is backgrounded and then the BLE device goes out of range. Once the connection is broken, the app seems to be suspended by iOS after a few seconds and none of the code I wrote will continue to function...even if I bring the device back into range. The only way to restore functionality is to bring the app back into the foreground again. (Note: I have the info.plist file and all other settings configured appropriately for centralManager background functionality)
I've read some documentation and it seems that this comes down to not having state preservation/restore code properly implemented. I went ahead and implemented the "willRestoreState" and "didUpdateState" commands, but the app still doesn't reconnect to a device once it has been suspended when in background mode.
I've shown some relevant code below, including the willRestoreState, didUpdateState, and didDisconnect methods. Any ideas or suggestions? Thanks!
//define service+characteristic UUIDS
let serviceUUID = CBUUID(string: "xxxxxxxxx")
let streamingCharacteristicUUID = CBUUID(string: "xxxxxxxxx")
//Local dictionary of UUIDs for connected devices (the ble code updates this var with each connected device)
var devicesUniqueId:[UUID:String] = [UUID:String]()
//Local dictionary of connected peripherals, with respect to each of their UUIDS (the ble code updates this var with each connected device)
var sensorPeripheral = [UUID:CBPeripheral]()
///restoreState function
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
if let peripheralsObject = dict[CBCentralManagerRestoredStatePeripheralsKey] {
let peripherals = peripheralsObject as! Array<CBPeripheral>
print ("starting restorestate code")
if peripherals.count > 0 {
for i in 0 ..< peripherals.count {
print ("starting restorecheck")
//Check if the peripheral exists within our list of connected peripherals, and assign delegate if it does
if self.devicesUniqueId.keys.contains(peripherals[i].identifier) {
peripherals[i].delegate = self
}
}
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager)
{
if central.state != .poweredOn
{
return
}
self.startScanning()
//////Preservation + Restoration code////////
//Iterate through array of connected UUIDS
let keysArray = Array(self.patchDevicesUniqueId.keys)
for i in 0..<keysArray.count {
//Check if peripheral exists for given UUID
if let peripheral = self.sensorPeripheral[keysArray[i]] {
print("peripheral exists")
//Check if services exist within the peripheral
if let services = peripheral.services {
print("services exist")
//Check if predefined serviceUUID exists within services
if let serviceIndex = services.index(where: {$0.uuid == serviceUUID}) {
print("serviceUUID exists within services")
let transferService = services[serviceIndex]
let characteristicUUID = streamingCharacteristicUUID
//Check if predefined characteristicUUID exists within serviceUUID
if let characteristics = transferService.characteristics {
print("characteristics exist within serviceUUID")
if let characteristicIndex = characteristics.index(where: {$0.uuid == characteristicUUID}) {
print("characteristcUUID exists within serviceUUID")
let characteristic = characteristics[characteristicIndex]
//If characteristicUUID exists, begin getting notifications from it
if !characteristic.isNotifying {
print("subscribe if not notifying already")
peripheral.setNotifyValue(true, for: characteristic)
}
else {
print("invoke discover characteristics")
peripheral.discoverCharacteristics([characteristicUUID], for: transferService)
}
}
}
}
else {
print("invoke discover characteristics")
peripheral.discoverServices([serviceUUID])
}
}
}
}
}
//didDisconnect method to handle a connect command issue
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
//commented out unnecessary code
self.removePeripheralData(peripheral: peripheral)
if(sensorCount>0){
sensorCount -= 1
}
}
//removePeripheralData function used in didDisconnect
func removePeripheralData ( peripheral: CBPeripheral) {
//Commented out unnecessary code
//Issue reconnect command
print ("issuing reconnect command")
centralManager.connect(peripheral, options: nil)
//Commented out unnecessary code
handleDidRemoveDevice()
}