I am attempting to have my app scan for BLE devices in the background and to search for a bit of advertisement data in Swift. I've been unable to find any tutorials or questions on here that cover this.
Basically, is there a way of doing this automatically in the background when the app isn't in the foreground and when the user has restarted their phone?:
Obtaining Bluetooth LE scan response data with iOS
I hope you can point me in the right direction. Thank you
Step 1: Enable bluetooth background mode for your projects capabilities
Step 2: Make sure the appropriate stuff was added to your info.plist file
Here is the plist code if it didn't add it:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>bluetooth-central</string>
</array>
Then, when you call "scanForPeripheralsWithServices" on your CBCentralmanager you have to specify an array of services to scan for. You can't pass it an empty array. It will still scan if you pass nil, just not in the background.
So specify an array of service UUID's like this:
let arrayOfServices: [CBUUID] = [CBUUID(string: "8888")]
self.myBluetoothManager?.scanForPeripheralsWithServices(arrayOfServices, options: nil)
Now if you care about options you can pass a dictionary of options in place of the nil I passed above. Mostly, this is used to specify if you want to continuously see a devices RSSI before you connect or if you just want the advertisement packet once. Put a:
println(advertisementData["kCBAdvDataLocalName"] as! String)
println(advertisementData["kCBAdvDataManufacturerData"] as! NSData)
in the "didDiscoverPeripheral" delegate method to observe the different behavior.
Here is the Dictionary you will pass if you want duplicate keys:
let dictionaryOfOptions = [CBCentralManagerScanOptionAllowDuplicatesKey : true]
self.myBluetoothManager?.scanForPeripheralsWithServices(arrayOfServices, options: dictionaryOfOptions)
Now Apple says that when your app goes in the background mode it defaults this scan option to false but as of iOS 8.3 I have see it keep scanning with duplicate keys.
One final note on background execution. If the iOS decides to suspend your app the scanning stops. I have seen this happen as soon as 30 seconds if there are a bunch of apps running.
Related
I have an app demanding to scan BLE devices around in background mode when app is not active in foreground.
I have implemented such functionality using CoreBluetooth framework. This is code, I am using to scan device. First I have all device in DB, fetch and creating array.
for item in self.allItems ?? [] {
let uuid = UUID(uuidString: item.identifier)!
let id = CBUUID(nsuuid: uuid)
self.allServiceIds.append(id)
}
And when start scanning, passing same array in method.
self.centralManager?.scanForPeripherals(withServices: self.allServiceIds, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
Also I have tried to pass service ids in array as I read lots of articles suggesting in background mode it is required.
Also inside Capabilities, I have checked required options. But still it is not scanning in when app is in background.
Any ideas are welcome.
A few tips:
Get your app working in the foreground first. Only once this is working should you try to get it working in the background.
Make sure that bluetooth is turned on in phone settings.
Make sure you have obtained Bluetooth permission from the user. An iPhone will send a dialog to prompt you for this permission the first time an app runs (it is triggered by CBCentralManager usage.) If you deny this permission, you won't see the dialog again and you must go to Settings -> Your App -> Permissions -> Bluetooth to enable it manually.
Take care that you have set the CBCentralManagerDelegate properly and that you are getting callbacks. In particular, log the callback to centralManagerDidUpdateState(_ central: CBCentralManager) and make sure that you see central.state transition to .poweredOn. Only once you reach the poweredOn state should you start scanning. See: https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518888-centralmanagerdidupdatestate
Start off testing in the foreground without filtering self.centralManager?.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true]) (note the withServices: nil). Only once you get callbacks detecting peripherals should you start filtering for specific service UUIDs. This will help you eliminate all other problems before filtering on the service.
If you can get all of the above working, there are tricks you can do to get duplicate detections even in the background.
If you cannot get the above working, please described the results of the above, and show more of your code including where you instantiate the CBCentralManager, where you set the delegate, showing the delegate callback methods, and describing which delegate callbacks get called and when.
I have an iOS application. I can successfully connect to my paired EAAccessory (Bluetooth Classic). I am able to pull information off of the device that is exposed through the EAAccessory object. One thing I noticed is that the name of the device that is paired (in my Settings -> Bluetooth -> My Devices list) does not match the name of the device that my EAAccessory object exposes. I find this very odd.
Is there any way to get the actual name of the device (the one from the Settings page) through my iOS app?
You didn't mention if this is Bluetooth Classic or BLE ?, My answer below is for bluetooth Classic, I recall I've seen something like that before, here's my findings so far:
Take a look at the Accessory Design Guidelines, sections 2.1.5 and 2.1.8 specifically.
2.1.5:
During the Bluetooth discovery process, the Apple product prefers to
display the Friendly Name of discovered accessories. Before the 2.1
version of the Bluetooth specification the Apple product would have to
set up a connection to the accessory and do a Remote Name Request,
which takes power, antenna time, and user's time. The Extended Inquiry
Response feature, introduced in Bluetooth 2.1, lets an accessory send
its Local Name and other information as part of the Inquiry Response
and thereby increase the speed and efficiency of the discovery
process. The Local Name should match the accessory's markings and
packaging and not contain ':' or ';'.
Also review the Class of device section
2.1.8:
Every accessory that is compatible with an Apple product must
accurately set its Class of Device using the Bluetooth SIG defined
Major Device Class and Minor Device Class. See Volume 3, Part C,
Section 3.2.4 in the Bluetooth Core Specification , Version 5.0. For
example, an audio/video accessory intended to operate in a vehicle
should set Major Device Class to audio/video and Minor Device Class to
car-audio .
Your case might be just the fact that the accessory has a friendly name, and iOS did not clear the cache for the name at that point for the settings, or it could be a faulty implementation on the firmware side.
If this doesn't answer your question, please let me know what names do you see on settings and within your app, and what type of accessory this is with FW version if applicable and I'll try to take a further look into it.
It is possible to get the list of paired/connected devices if you have their advertisement UUID.
var centralQueu = DispatchQueue(label: "A_NAME")
centralManager = CBCentralManager(delegate: self, queue: centralQueu, options: [CBCentralManagerOptionRestoreIdentifierKey: "RESTORE_KEY", CBCentralManagerOptionShowPowerAlertKey: true])
ServiceUUIDs = [CBUUID(string: "THE_UUID")]
//the array of CBUUIDs which you are looking for
You need to find the service UUID in which you are interested:
var options = [
CBCentralManagerScanOptionAllowDuplicatesKey : (1)
]
centralManager.scanForPeripherals(withServices: [CBUUID(string: SERVICE_UUID)], options: options)
centralManager.retrieveConnectedPeripherals(withServices: ServiceUUIDs)
handle didDiscoverperipheral in this way:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
discoveredPeripheral = peripheral
if !mRemoteDevices.contains(discoveredPeripheral) {
let peripherels = centralManager.retrievePeripherals(withIdentifiers: [discoveredPeripheral.identifier])
mRemoteDevices.append(peripherels[0])
}
}
this code is converted from objective-C to swift if you are familiar with objective C then the original code is here.
How do I trigger a UILocalNotification from the iPhone which would have no alert but only play a sound / haptic feedback on the Apple Watch?
Further background:
I am making a watchOS2 timer app. I use a UIlocalNotification triggered by the iPhone to tell the user that end of timer is reached (to handle the scenario where the watch is not active).
The problem is that Apple uses its own logic to determine if a notification appears on the watch or the phone. If I trigger a notification with no alert but only sound, this notification always plays on phone, never on the watch.
I'd prefer that notification sound/haptic to play on the watch. How can I achieve this?
The downside of what you're asking:
A sound-only notification on the watch would be confusing.
Without a message associated with the sound, the user wouldn't see any reason for the notification, when they glanced at their watch.
The downside of how to currently do what you're asking:
The WKInterfaceDevice documentation points out Apple's intention for playing haptic feedback:
You can also use this object to play haptic feedback when your app is active.
What follows is a misuse of the API to accomplish something it wasn't intended to do. It's fragile, potentially annoying, and may send users in search of a different timer app.
Changes for iOS 10 prevent this from working, unless your complication is on the active watch face.
How you could currently do what you're asking in watchOS 2:
To provide haptic feedback while your app is not active, you'd need a way for the watch extension to immediately wake up in the background, to provide the feedback:
WKInterfaceDevice.currentDevice().playHaptic(.Notification)
To do this, you could misuse the WCSession transferCurrentComplicationUserInfo method. Its proper use is to immediately transfer complication data from the phone to the watch (that a watch face complication might be updated). As a part of that process, it wakes the watch extension in the background to receive its info.
In your phone's timerDidFire method:
After checking that you have a valid Watch Connectivity session with the watch, use transferCurrentComplicationUserInfo to immediately send a dictionary to the watch extension.
guard let session = session where session.activationState == .Activated && session.paired && session.watchAppInstalled else { // iOS 9.3
return
}
let hapticData = ["hapticType": 0] // fragile WKHapticType.Notification.rawValue
// Every time you misuse an API, an Apple engineer dies
session.transferCurrentComplicationUserInfo(hapticData)
As shown, the dictionary could contain a key/value pair specifying the type of haptic feedback, or simply hold a key indicating that the watch should play a hardcoded notification.
Using an internal raw value is fragile, since it may change. If you do need to specify a specific haptic type from the phone, you should setup an enum instead of using a magic number.
In the watch extension's session delegate:
watchOS will have woken your extension in preparation to receive the complication user info. Here, you'd play the haptic feedback, instead of updating a complication, as would be expected.
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let hapticTypeValue = userInfo["hapticType"] as? Int, hapticType = WKHapticType(rawValue: hapticTypeValue) {
WKInterfaceDevice.currentDevice().playHaptic(hapticType)
}
}
The proper solution:
Request that Apple provide a way to schedule local notifications on the watch. Apple's timer app does this already.
You may also wait to see what is announced at WWDC 2016, to decide if any new functionality available in watchOS 3 would help to create a proper standalone watch timer app.
I am Working on BLE project, everything works fine when the app in the foreground.It can discover and connect to the peripheral, all the call back method work perfectly.
But the problem is that, when the app in the background mode (I press home button). Only the centralManagerDidUpdateState delegate method get called.
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
switch (central.state) {
case CBCentralManagerStatePoweredOn:
[self.cbCentralManager scanForPeripheralsWithServices:nil options:#{ CBCentralManagerScanOptionAllowDuplicatesKey : #YES }];
break;
default:
break;
}
}
I use scanForPeripheralsWithServices:nil option, But when the app in the background, the didDiscoverPeripheral call back never called. I have edit my plist file with "bluetooth-central" option to support ble central role in background.
Any idea why didDiscoverPeripheral method not call when app in the background?
Paulw11 said are right, If your app find the peripherals in the foreground. It will not call the didDiscoverPeripheral for the same peripherals when it enters the background.
For more information about the iOS BLE Behavior in the background mode. You can check this answer
What exactly can CoreBluetooth applications do whilst in the background?
I was working on Estimote Nearable type beacons. After iOS10 SDK update, I encountered exception from CBCentralManager stating :
<CBCentralManager: 0x17009e050> has provided a restore identifier but the delegate doesn't implement the centralManager:willRestoreState: method
To fix this, Turn-On "Background Mode", in Xcode -> Capabilities -> Background Mode
Scan for nil( scanForPeripheralsWithServices:nil) services will not work in background. You must search for some specific service in background.
You have to set the UUID in scanForPeripheralsWithServices: method which Peripherals/BLE device is advertising.
From Official Apple reference
You can provide an array of CBUUID objects—representing service
UUIDs—in the serviceUUIDs parameter. When you do, the central manager
returns only peripherals that advertise the services you specify
(recommended). If the serviceUUIDs parameter is nil, all discovered
peripherals are returned regardless of their supported services (not
recommended). If the central manager is already scanning with
different parameters, the provided parameters replace them. When the
central manager object discovers a peripheral, it calls the
centralManager:didDiscoverPeripheral:advertisementData:RSSI: method of
its delegate object.
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 CBCentralManagerOptionShowPowerAlertKey
scan option is ignored while scanning in the background.
Here
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.
So scanForPeripheralsWithServices:nil with nil it will not work in background , you need to specify list of UUIDS
iOS Multipeer Connectivity question...
My app uses MCNearbyServiceBrowser and MCNearbyServiceAdvertiser (but not simultaneously on a given device).
My MCNearbyServiceAdvertiser always uses the same PeerId ... I store it in NSUserDefaults, per the 2014 WWDC session's advice on this.
When another device is browsing for services, the browsing device gets a foundPeer browser delegate callback, as expected.
However, if on the browsing device I switch away from my app (e.g., via a Home button tap) and then switch back to my app, I get another call to foundPeer for the advertising device, but this time the PeerId is different!
This seems odd, because my advertiser always uses the same PeerId.
Any ideas why this might be happening? Unexpected?
(I was planning to see if a newly-found advertising device with a given PeerId is already in my table view of advertisers, but the above issue kind of messes up that plan.)
Thank you.
-Allan
From the apple docs: “ The Multipeer Connectivity framework is responsible for creating peer objects that represent other devices.”
After pressing the home button and switching back to the app the framework has created a new PeerID object to represent the advertising device. This is another object than the previous one, even though it represents the same advertising device. So you can not rely on PeerID object equality.
To identify peer correctly I suggest you should create an NSUUID string and archive it on disk and reuse it. When you initialise MCPeerID object the display name you should be passing would be displayName+UUID. Use display name for UI elements and UUID for identifying peer.
I hope it helps.
You will not get the same MCPeerID when you create two from the same display name. This ensures uniqueness when you have a name collision. It's common to use the device name as the display name. Not everyone personalizes theirs.
If you want to recognize and be recognized by previously connected peers, then you must save and retrieve the actual MCPeerID.
To see what I mean, paste the following code into a playground and run it.
import MultipeerConnectivity
let hostName = "TestPlaygroundHostName"
let firstPeerID = MCPeerID(displayName: hostName)
let secondPeerID = MCPeerID(displayName: hostName)
firstPeerID.hashValue == secondPeerID.hashValue