What I am trying to do
I am trying to connect my app to a Bluetooth LE device which needs to be paired.
Current behaviour
There is no problem without pairing the device and my iPhone application. I am able to connect, reconnect and read/write characteristics without any problem.
But, if the device need to be paired, I am only able to read/write characteristics the first time, right after the pairing popup confirmation. The next time, I discover and connect the app to my device, but I don't have the rights to read/write characteristics data because (I guess) I am not using the pairing information.
Finally...
After spending few hours searching around the web with no luck here are my questions :
How can I connect my app to a Bluetooth LE device from my iPhone app using the pairing data stored in my phone? Am I missing something?
Is it possible that it is not an IOS problem because if pairing data are present in the phone for the connecting device, it is automatically used?
Is there someone with experience with Bluetooth LE and IOS to help me?
Update 2013-10-27
I have discovered that you can't read a protected characteristic by pairing authentication right after that the characteristic has been discovered if a pairing exists (no confirmation popup). No problem with non-protected characteristic! I don't know exactly why is happening, but the behavior is that the IOS app never receive answers from the device.
So if the first reading is done after, it doesn't cause problem. Here is the code I am using to discover characteristics with the data reading in comment.
- (void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error;
{
NSArray *characteristics = [service characteristics];
CBCharacteristic *characteristic;
if (peripheral != servicePeripheral) {
NSLog(#"Wrong Peripheral.\n");
return ;
}
if (service != batteryService) {
NSLog(#"Wrong Service.\n");
return ;
}
if (error != nil) {
NSLog(#"Error %#\n", error);
return ;
}
for (characteristic in characteristics) {
NSLog(#"discovered characteristic %#", [characteristic UUID]);
if ([[characteristic UUID] isEqual:[CBUUID UUIDWithString:kBatteryCharacteristicUUIDString]]) { // Bat
NSLog(#"Discovered Bat Characteristic");
batteryCharacteristic = [characteristic retain];
//--> generate problem when pairing exists between IOS app and device
//[peripheral readValueForCharacteristic:batteryCharacteristic];
}
}
}
You don't have to do anything in your app for pairing management.
If your app runs in LE Central mode, and the peripheral sends an Insufficient Authentication error code in response to a read / write request, iOS will automatically pair with your device and will retry the request.
If you disconnect from the device, and later reconnect again, the peripheral needs to send the Insufficient Authentication error code again for the iPhone to restart encryption. Again, you don't have to do anything special in your app here.
If your app runs in LE Peripheral mode, things are a bit different. When you set up your GATT database, make sure to set correct flags for both the CBAttributePermissions and CBCharacteristicProperties. This will tell iOS that it should send the Insufficient Authentication error code itself, if it is not paired. It is then the responsibility of the central device to start the encryption process.
In the Bluetooth Accessory Design Guidelines for Apple Products, further restrictions are described.
Your accessory needs the capability to resolve private Bluetooth addresses. The iPhone will change its public Bluetooth address every now and then, and only paired devices will have the correct key to resolve that public address and recognize the iPhone.
"Section 3.9 Pairing" is also interesting.
Note that if you pair without man-in-the-middle (MITM) protection, your peripheral can use the resulting key to resolve the private Bluetooth address of the iPhone. However, you won't be able to encrypt the channel.
Pairing with MITM protection on iOS involves entering a PIN code that is displayed by the remote device. Out-of-band (OOB) pairing where you send pairing data over an external channel is not supported by iOS as far as I know (at least there's no public APIs to set OOB data).
Long story short: if you have only a "Pair" / "Cancel" pairing, you cannot encrypt the LE channel but only recognize the iPhone in future connections. The nice thing is that you can still recognize the iPhone even if you unpair it on the iPhone side, and even after restoring the iPhone firmware ;-).
Regarding LE encryption in general: it's not secure anyways (see http://eprint.iacr.org/2013/309).
Related
I want to remove paired bluetooth device from system settings or "unpair". I know that apple doesn't provide public api to do this so I tried private class CBPairingAgent. I found headers on github
CBCentralManager.h
CBPairingAgent.h and here is my code:
- (void)unpair:(CBPeripheral *)peer {
id agent = [self.centralManager valueForKey:#"sharedPairingAgent"];
// [agent performSelector:#selector(setDelegate:) withObject:self.centralManager];
[agent performSelector:#selector(unpairPeer:) withObject:peer];
}
This code runs without errors but also without any effect for peripheral.
Can somebody point me in right direction? Or suggest better way
The private API to remove a device pairing works if the process has enough entitlements, which is only possible on a JB device.
On JB, I've added these entitlements (found in Settings & SpringBord):
com.apple.BTServer.allowRestrictedServices
com.apple.BTServer.le
com.apple.BTServer.programmaticPairing
Most of time, this works normally in my application. Unfortunately, sometimes it never been triggered after discoverservice is called.
My code is:
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(#"Did connect to peripheral: %#", peripheral);
[self.delegate statusMessage:[NSString stringWithFormat:#"Did connect to peripheral: %#\n", peripheral]];
peripheral.Delegate = self;
NSArray *serviceArray = [NSArray arrayWithObject:_uuid_tpms_sensor_service];
[_peripheral discoverServices:serviceArray];
[peripheral discoverServices:serviceArray ];
}
Some posts relative with this are
CoreBluetooth never calls didDiscoverServices on iPhone5S
iOS CoreBluetooth not scanning for services in iPad Air
iPhone does not discover Services on a Bluetooth LE tag on reconnection
The final conclusion should be an issue in iOS. My question is that, given that it's iOS problem, how to work around this?
Many thanks to Larme and henrik as I got many ideas from your reply.
After three days verification, it seems I have find a work around for this problem (the problem is more likely a limitation of Bluetooth stack of iOS rather than an issue)
I'd like to summarize my findings and work around here:
[root cause]
Bluetooth stack in iOS is not robust enough, it's therefore the internal state machine become corrupted after some unexpected API calling.
As the BTLE radio follows a certain pattern where it interacts with one device connection query at a time, BLE application should follow the API sequence of connectperiperal-->discover-->read-->disconnect (trigger by local, peer device or supervision timeout on link layer).
Apple admitted that this was an issue in iOS.
[resolution]
follow the API calling sequence described in the above
Hope the summary is useful for other person.
Too many rogue app's not handling the BLE stack properly can cause it to crash. Then the phone needs to be restarted. iOS7 and 8 are much more robust than in the early life of iOS BLE.
Android are still less robust. Programmers forget to release resources or might try to write new data before the old has been transmitted.
This can happen a lot if you use X-code and you stop your app at a point before it releases resources etc. This can still crash iOS BLE stack.
I need to implement a self bluetooth app to connect with all bluetooth devices to iPhone. I know it is not possible with CoreBluetooth framework.
I use private API and added header files of DeviceManager and BluetoothManager to private frameworks and downloaded BeeTee Project from here
This app runs and finds all bluetooth device near me but when I have tried to connect to a device by this code:
[self.bluetoothManager connectDevice:bluetoothDevice];
and this
[bluetoothDevice connect];
When a cell is selected, Both of above codes request to connect but BTM returns this message:
BeeTee[5473:60b] BTM: connection to service 0xffffffff on device "Nokia 500" F4:xx:xx:xx:xx:xx failed with error 109
What is error 109? Which would be set service number?
I guess I should pair devices before connecting but how can I do that?
I am just guessing, but I think the problem is that the BluetoothManager.framework is made for the External Accessory Program by Apple. And this allows (among others) SPP Bluetooth connection to certificated devices. But there is the problem: you need to have a device with a authentication chip inside.
I don't know on which level/layer Apple implemented the authentication, but I fear the did it one layer under the private framework BeeTee is using.
UPDATE: Maybe this is helpful for you:
BluetoothManager *bluetoothManager = //...
[bluetoothManager setDevicePairingEnabled:YES];
[bluetoothManager connectDevice:bluetoothDevice withServices:0x00002000];
Credits
BluetoothManager *bluetoothManager = //...
[bluetoothManager setDevicePairingEnabled:YES];
[btManager setPincode:#"111111" forDevice:bluetoothDevice.deviceRef];
//where 111111 is your device PIN
[bluetoothManager connectDevice:bluetoothDevice withServices:0x00002000];
I know you can use this service to have devices like smart watches intercept notifications from iOS devices. But can you receive these iOS notifications on a Mac through OS X?
I want to be able to have my OS X program detect a specific notification type that is received in iOS. I tried browsing for the ANCS device on my Mac, but it didn't show up. I know you can't do this between iOS devices, so I was wondering if maybe the same was true between iOS and OS X or not?
Thanks!
It's definitely possible!
Here's what you need:
An app on your iOS device which imports CoreBluetooth and uses CBPeripheralManager to advertise a dummy service with some custom UUID (not the ANCS UUID, it won't work). This dummy service is required for your Mac to "see" the ANCS service.*
An app on your Mac which imports IOBluetooth, initiates a CBCentralManager object, and starts a scan. You can do this as so:
[self.centralManager scanForPeripheralsWithServices:#[[CBUUID UUIDWithString:YOUR_CUSTOM_SERVICE_UUID]] options:#{CBCentralManagerScanOptionSolicitedServiceUUIDsKey:#[[CBUUID UUIDWithString:ANCS_SERVICE_UUID]]];
Make sure you set yourself up as a delegate to CBCentralManager to receive the delegate callbacks.
When your Mac discovers your device in didDiscoverPeripheral, connect to it:
[self.centralManager connectPeripheral:peripheral options:nil];
1 very important thing to note here is you need to retain your peripheral to a property if you wish to connect to it, otherwise it will be dealloc'ed. See this answer for a more detailed discussion.
In didConnectPeripheral, you need to set yourself up as a delegate to the CBPeripheral you're connected to then start discovering services:
[peripheral discoverServices:nil];
(All the callbacks from this point onward are for CBPeripheral)
In didDiscoverServices, you should get a list of available services. Loop through them as so and discover each service's characteristics:
for (CBService *service in peripheral.services) {
if ([service.UUID isEqual:[CBUUID UUIDWithString:YOUR_CUSTOM_SERVICE_UUID]]) {
NSLog(#"Found your Custom Service");
}
if ([service.UUID isEqual:[CBUUID UUIDWithString:ANCS_UUID]]) {
NSLog(#"Found ANCS Service");
}
[peripheral discoverCharacteristics:nil forService:service];
}
In didDiscoverCharacteristicsForService, you want to look for 3 characteristics:
ANCS Notification Source: UUID 9FBF120D-6301-42D9-8C58-25E699A21DBD (notifiable)
ANCS Control Point: UUID 69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9 (writeable with response)
ANCS Data Source: UUID 22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB (notifiable)
For those notifiable characteristics, subscribe to them for updates:
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:ANCS_CHARACTERISTIC_UUID]]) {
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
If you want to check if those characteristics are have started notifying, do a if(characteristic.isNotifying) in didUpdateNotificationStateForCharacteristic.
You will get the actual NSData updates in didUpdateValueForCharacteristic with characteristic.value. The important thing to note here is that you will get informed of notification events by the Notification Source characteristic, but these will not contain that much information. If you want your Mac to play a sound or flash some Hue lights or something like that for every iOS notification, this will suffice. However, for the actual notification details, it will need to come from the Data Source characteristic, but you need to request for them by making very specific calls to the Control Point characteristic. This is where it gets really complicated, and you'll be able to get more information in the official ANCS Specification document.
If you want a shortcut or a look at how others have done it, check out these Github repos:
jamiepinkham/BTLE_ANCS
KhaosT/ANCS-Mac
indragiek/INDANCSClient
Just be careful as you may find bugs in some of these implementations, mainly in the processing of data sent by the ANCS Data Source (I had to get creative with my own error handling).
*** Some may argue that you can use "Service Solicitation" to expose ANCS without having an app running on the iOS device and/or without advertising a dummy Service (see options parameter in Step 2), but I haven't had that much success with it so perhaps there's something I'm missing.
I am currently working on an iOS application based on bluetooth low energy devices. In order to get a unique identifier to compare the peripherals got, I have to get the MAC address of the peripherals.
It is observed that the UUID property of a peripheral device varies across iOS devices and also for the peripheral device to get a UUID, it will have to get connected to a master device at least once. Since I have to deal with check-in's I don't want to establish a connection. As I went through the bluetooth services portal, I found that the device information itself is a service, which couldn't be retrieved unless a connection has been established between the master iOS device and the peripheral bluetooth low energy device.
I found that in Android we get the entire information of the device, including its MAC address (using getAddress()) when we get the response from the device on scanning itself.
I didn't find any properties in CBPeripheral class related to the device address.
Another way to get a unique parameter would be to customize the advertisement data to send additional information regarding the device, which requires more work on firmware side.
So is there any way in iOS that I could get the MAC address of the bluetooth low energy peripheral without establishing a connection?
Any help would be greatly appreciated.
CBPeripheral's identifier property will serve your purpose, available from a still-unconnected device in CBCentralManager's didDiscoverPeripheral delegate method:
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
CBPeripheral *peripheral ...
NSUUID* serverId = [peripheral identifier];
I have a half dozen LE devices I'm experimenting with, including multiple sets of identical devices. I just confirmed that across two iOS devices, the identifiers for all of these LE widgets were different, but for EACH iOS device, the identifier for each widget was retained across application launches and even across app deletes and reinstalls. I would say this definitively proves that the OS is storing enough info internally that for a given iThing, you'll be able to distinguish between, and re-identify, all the peripherals your app encounters, without actually connecting to them.
Also note that the advertisementData, also available before connecting, is full of useful info like CBAdvertisementDataLocalNameKey, CBAdvertisementDataManufacturerDataKey, CBAdvertisementDataServiceUUIDsKey, CBAdvertisementDataSolicitedServiceUUIDsKey, and others, although none as certain to uniquely identify the device as [peripheral identifier] is.
I didn't try doing a device backup and restore to prove the UUIDs were retained, but I'd wager they are, and if they're not, it's something Apple would consider a bug.
Updated Answer :-
After iOS 12 we can get UDID
print(UIDevice.current.identifierForVendor)
print(UIDevice.current.identifierForVendor?.uuidString)
Before iOS 12**
There is no public API to get this information.
If this is an internal or jailbreak application you can get the value of the kLockdownBluetoothAddressKey key via liblockdown.dylib
Low energy peripherals may use privacy feature which hides the MAC address, so it is not necessarily even possible to get the address before connection or bonding. If you somehow get the MAC address which goes over the air, you need to handle privacy or you have interoperability problems.
Apple uses UUIDs to abstract these privacy features out so users do not need to worry about those.
Correct way to do that like you wrote is to either add some vendor specific data to advertisement packet or use the Device Information service.
On-behalf of the discussion of the other professionals I've found some facts which says -
“iOS hides the MAC address of the device and generates a UUID. The UUID on iOS is generated by the iOS device. Different iOS devices will get different UUIDs for the same peripheral. The MAC address is usually based on the hardware. If we both have iPhones and scan the same peripheral, we'll see different UUIDs. iOS generates the UUID on the device and hides the MAC address.
Summary - iOS doesn't let you get the MAC address of a device, it gives you a random UUID instead.“
Source - https://github.com/don/cordova-plugin-ble-central/issues/77
As per above study I've found that there’s not such a unique way to get connect to the board so far, Every board has a MAC address, which Doesn’t changes and easy to access in (only) Android, while iOS doesn’t allow to access MAC Address of the peripheral, however iOS use this MAC address to create a peripheral identifier (UUID), which is unique on (unique) device only. The peripheral identifier for a single Board is different for different iPhones devices (but unique on single device).
However we can connect to a board by searching with Peripheral's Bluetooth Service UUID, but this service UUID is same for all the boards of a kind say- “Adafruit Feather M0”. It means the App will look around the BLE boards of the same type (“Adafruit Feather M0”) and will get connect to ANY one of them. In order to connect to a particular user to a specific Board doesn’t seems to be possible so far due to the inaccessibility of MAC and giving the random UUID in iOS.
You can access to the MAC ADDRESS without problem in iOS 12.
To get the mac address you have to follow the next steps.
Parse the Data received by the BLE device to String.
extension Data{
func hexEncodedString() -> String {
let hexDigits = Array("0123456789abcdef".utf16)
var hexChars = [UTF16.CodeUnit]()
hexChars.reserveCapacity(count * 2)
for byte in self {
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
hexChars.insert(hexDigits[index2], at: 0)
hexChars.insert(hexDigits[index1], at: 0)
}
return String(utf16CodeUnits: hexChars, count: hexChars.count)
}
}
Add a separator ":" to the address.
extension String {
func separate(every stride: Int = 4, with separator: Character = " ") -> String {
return String(enumerated().map { $0 > 0 && $0 % stride == 0 ? [separator, $1] : [$1]}.joined())
}
}
In didReadValueForCharacteristic( characteristic: CBCharacteritic) you can use the previous 2 functions to get the mac address.
func didReadValueForCharacteristic(_ characteristic: CBCharacteristic) {
if characteristic.uuid == BleDeviceProfile.MAC_ADDRESS, let mac_address = characteristic.value?.hexEncodedString().uppercased(){
let macAddress = mac_address.separate(every: 2, with: ":")
print("MAC_ADDRESS: \(macAddress)")
}
}
enjoy your mac address:
"MAC_ADDRESS: 00:0A:57:4E:86:F2"