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.
Related
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 am new to iOS development, and study about Bluetooth Low Energy (BLE, Bluetooth 4.0) for IOS.
I studied the sample code of this link BTLE Central Peripheral Transfer.
And there is another similar sample in this link iOS 7 SDK: Core Bluetooth - Practical Lesson
The applications on the above two links talk about send and receive the text data between two IOS device base on BLE.
The App can select to be a central or Peripheral , and the central will receive the text data send from the Peripheral.
It define the UUID like the following code in header file.
#define TRANSFER_CHARACTERISTIC_UUID #"08590F7E-DB05-467E-8757-72F6FAEB13D4"
And after the Central connect to the Peripheral , it discover the characteristic from Peripheral.
If the UUID is equal to TRANSFER_CHARACTERISTIC_UUID ,then subscribe it by using setNotifyValue:YES like the following code.
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
// Again, we loop through the array, just in case.
for (CBCharacteristic *characteristic in service.characteristics) {
// And check if it's the right one
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
// If it is, subscribe to it
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
}
// Once this is complete, we just need to wait for the data to come in.
}
The question is like the following:
First Question:
I can not find this UUID:#"08590F7E-DB05-467E-8757-72F6FAEB13D4" in Bluetooth Development Portal.
Is that create by uuidgen in terminal ?
The second Question:
If I am Central ,and I have subscribe the characteristic by using setNotifyValue:YES like the above code.
The BLE will tell the Central there has new data send from Peripheral by following code , is the concept correct ?
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
I am new in IOS development and BLE.
Thanks in advance.
First question:
Yes, Apple even suggests generating those UUIDs using uuidgen in the various WWDC video. The 128-bit UUIDs are not standardized by the Bluetooth SIG and you can use those to run your own profiles.
Second question:
Yes, you first discover the services, then the characteristics, then setNotifyValue:YES. From now on, you will receive notifications from the peripheral via [-CBPeripheralDelegate didUpdateValueForCharacteristic:error:]. The same callback will be invoked when you read a characteristic manually (there's no way to distinguish the read response from a notification in Core Bluetooth).
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).
I have an app that simulates heart rate monitor peripheral (The peripheral app).
I also have an app that receives the data and present it (The central app).
The central app decided to connect to the discovered peripheral based on its name.
The problem is that both app work perfectly good, except that the name is always "iPhone".
The advertising is done this way:
- (IBAction)switchChanged:(id)sender
{
if (self.advertisingSwitch.on) {
NSDictionary *advData =
#{CBAdvertisementDataLocalNameKey:#"Custom Name",
CBAdvertisementDataServiceUUIDsKey:#[[CBUUID UUIDWithString:#"180D"]]};
[self.peripheralManager startAdvertising:advData];
NSLog(#"Advertising");
}
else {
[self.peripheralManager stopAdvertising];
[[self timerInterval] invalidate];
NSLog(#"Stopped advertising");
}
}
But on the central side, inside
- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
The name property never changed.
Is there anything that should be done?
What I have observed with CBPeripheral.name is that the device will, in fact, set the name to the name you select with CBAdvertisementDataLocalNameKey. This name is not persistent, though. If you disconnect the master and reconnect,the name will generally have been switched to "iPhone". If the peripheral disconnects due to an error, I have seen it reconnect with the correct peripheral name, but a new UUID.
There may be other situations where the name also switches to iPhone.
This appears to be a bug in iOS. I'm looking for confirmation before reporting it.
CBAdvertisementDataLocalNameKey only change kCBAdvDataLocalName in advertisementData.
When you nslog advertisementData, you will see some data like this:
{
kCBAdvDataIsConnectable = 1;
kCBAdvDataLocalName = Custom Name;
kCBAdvDataServiceUUIDs = (
"FB694B90-F49E-4597-8306-171BBA78F846"
);
}
Unfortunately, there is no other way to set the peripheral name. iPhone will always have the name: iPhone.
The advertisement is probably correctly seen on the central side. You may check by NSLogging the advertisementData. However, if you rely on the peripheral.name property, then that will either be empty (if you connect first) or contain the "iPhone" string.
I remember it is used to happen to me and I figured it was something to do with the way Core Bluetooth handles caching and service discovery. What Happened to me was that at first I received a default name like iPhone, iPad or nothing at all. But then after discovering services or trying to establish a connection the key magically changes to the value I had set on the other end.
Moreover, it seems it only happens the first time, afterwards, even between launches and subsequent runs of the app Core Bluetooth will try its best to return those values to you on the advertisement stage even on first discovery, but those might as well be outdated values.
my current implementation looks as follows:
NSString * baconName = [[UIDevice currentDevice] name];
NSDictionary *advertisementData = #{CBAdvertisementDataServiceUUIDsKey:#[[CBUUIDUUIDWithString:BACON_SERVICE_UUID]],
CBAdvertisementDataLocalNameKey:baconName};
And It just works for me, iPhones love Bacon, everybody does ;).
Hence, the best way to ensure you get any data you want, is to create another characteristic to transmit your flag and do constant discoveries of services and characteristics for Peripherals you are discovering, and accordingly minimise the discovery of existing or cached peripherals by caching or keeping a reference to them, CB is supposed to do this for ya, and they do their best effort but only you know the business logic of your app and what is important to you. I am overly paranoid and keep references to the discovered peripherals I am interested all the time. That is just me: it ensures I have the right information, and that I minimise scanning and constant re discovery of services and characteristics.
I Hope this helps.
In most such applications instead of identifying the peripheral by name, the client app should be identifying it by a service ID, and the server (peripheral), should be providing a either a standard service ID, as defined at bluetooth.org, or a proprietary service ID/name.
I have the same problem. I argee with Mike, this really seems like bug in IOS. If you discover your peripheral with TI multitool (for example) first, then your device will be discovered as you setuped in CBAdvertisementDataLocalNameKey.
to Dan1one:
you should use [[UIDevice currentDevice] model], not name, to get the string same to default.
I just wrote a simple app where I scan and connect to peripheral (which is also an IOS device). However the CBPeripheral object I get back from ConnectPeripheral function does not have the device UUID and it's always null. Now I am trying to understand where do I set it so that it gets passed.
Here is what I am doing.
To advertise my service I am doing
NSDictionary *advertisingDict=[NSDictionary dictionaryWithObject: services forKey:CBAdvertisementDataServiceUUIDsKey]
[manager startAdvertising:advertisingDicts];
(From the framework I understand that I cannot pass the device UUID in the advertising packet. Correct me if I am wrong here)
My client scan for the service and gets into the function
centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
And as expected the peripheral.uuid is null. If I call connectPeripheral on this peripheral it works fine too. I am not sure how it understands which device to connect when the uuid is null. Also what would I need to do If I want to reconnect later. How do I fill this uuid?
It has a work around. You can pass it instead of the local name of the device like this:
[self.peripheralManager startAdvertising:#{CBAdvertisementDataLocalNameKey : *the UUID*}];
And in the other device in the didDiscoverPeripheral function you can get it like this:
[advertisementData objectForKey:#"kCBAdvDataLocalName"]