CoreBluetooth - CBMutableCharacteristic or CBMutableService with meaningful UUID - ios

I am developing an open source block based wrapper for Core Bluetooth.
Currently I am developing the Peripheral Manager part.
I have it broadcasting and I can see the service and its characteristic. When looking at other services I can see they have been given meaningful names such as Battery and Current time.
When I try and provide a meaningful name for any service or characteristic I create it returns the error String Characteristic name does not represent a valid UUID.
My code so far is
[_peripheralManager startAdvertising:#{CBAdvertisementDataLocalNameKey : #"Peripheral name",
CBAdvertisementDataServiceUUIDsKey : [CBUUID UUIDWithNSUUID:[NSUUID UUID]]}];
CBMutableCharacteristic *transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:#"Characteristic name"]
properties:CBCharacteristicPropertyRead
value:[#"Test value" dataUsingEncoding:NSUTF8StringEncoding]
permissions:CBAttributePermissionsReadable];
CBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:#"Service name"]
primary:YES];
[transferService setCharacteristics:#[transferCharacteristic]];
[_peripheralManager addService:transferService];
If I replace any initWithType arguments with [CBUUID UUIDWithNSUUID:[NSUUID UUID]] it works but is not nice to show to the user.
Is there a way to set the UUID of the CBService or CBCharacteristic to a meaningful string?
It is also worth noting that the value of the characteristic is null when viewed, bonus points for seeing why this is.
Thanks

The Bluetooth organisation has accepted some services and their characteristics as standards. These can be referred to by a 4 hex digit assigned number when using the Core Bluetooth library - See the CBUUID documentation
User defined UUIDs are not recognised by the Core Bluetooth library as they are not assigned numbers, so you can only display them with their full UUID.
The value of the characteristic will be null if you haven't issued a read or received a notify.

Use the terminal command uuidgen to create a user defined UUID.
As Paul mentioned, the value will be null until you read the value of the characteristic. It seems like a lot of work but after you discover a peripheral you connect to it (don't forget to retain the peripheral and set the peripheral's delegate property), then discover its services, then discover the characteristics for the services, then you may read the value of the characteristics like so:
[peripheral readValueForCharacteristic:characteristic]; //where peripheral is the connected CBPeripheral
In the CBCentralManagerDelegate method peripheral:didUpdateValueForCharacteristic:error: the value can be read:
NSLog(#"%#",[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]);
Based on the code you included, it is not necessary to include the advertisement data service UUID key in your options dictionary . If you would like to specify a data service UUID that is specific to your app, start advertisementing like this:
[_peripheralManager startAdvertising:#{CBAdvertisementDataLocalNameKey : #"Peripheral name",
CBAdvertisementDataServiceUUIDsKey : #[[CBUUID UUIDWithString:#"generated-string-using-uuidgen"]]}];
You can then set your CBCentralManager to scan for that specific data service uuid like this:
[_centralManager scanForPeripheralsWithServices:#[[CBUUID UUIDWithString:#"generated-string-using-uuidgen"]] options:nil];

Related

What devices does CBCentralManager.retrieveConnectedPeripherals() return?

I want to check which Bluetooth Devices my iPhone is connected to. In order to do that, I use CBCentralManager.retrieveConnectedPeripherals() like this:
let connectedPerphs = centralManager.retrieveConnectedPeripherals(withServices: []);
My problem is that even if my iPhone is connected to a BluetoothDongle (it explicitly says "connected" in the settings), the list that is returned by retriveConnectedPeripherals() is always empty. Am I using the method in a wrong way or can it not be used to detect a bluetooth connection such as the connection to to my dongle? If the latter is the case, how can I detect that connection?
Let me clear, centralManager.retrieveConnectedPeripherals always return empty or nil value, If you are not passing any value into serviceUUIDs
retrieveConnectedPeripherals(withServices:)
Returns a list of the peripherals (containing any of the specified
services) currently connected to the system.
serviceUUIDs:
A list of service UUIDs (represented by CBUUID objects).
Update:
Unfortunately this the long way to do it. You can create Array of CBUUID statically then you can pass it to the method. Please refer below code.
let aryUUID = ["1800","18811"]
var aryCBUUIDS = [CBUUID]()
for uuid in aryUUID{
let uuid = CBUUID(string: "1800")
aryCBUUIDS.append(uuid)
}
let connectedPerphs = centralManager.retrieveConnectedPeripherals(withServices: aryCBUUIDS)
List of available services
First, this works only with BLE devices, thus if your dongle is using a common BT you will not get it from here, but probably using EAAccessoryManager var connectedAccessories: [EAAccessory] method, but as far as I know your app must comply to MFI.
That is why is asking which service your devices are exposing as a filter.

How to get Paired BLE device in iOS?

I connect BLE with CoreBluetooth and paired.
Now when I back to my app screen, I want to make sure that BLE already paired with iOS device.
If I store value in defaults and remove app, this case will not work to fetch device.
If user remove paired bluetooth peripheral from Setting -> Bluetooth -> list of devices this case also not work to identify.
NSArray *ary = [self.bleMgr retrieveConnectedPeripheralsWithServices:#[[CBUUID UUIDWithString:#"180A"]]];
NSUUID *nsUUID = [[NSUUID alloc] initWithUUIDString:identifier];
NSArray *temp = [self.bleMgr retrievePeripheralsWithIdentifiers:#[nsUUID]];
Above both code lines not giving robust result.
How to get that paired BLE device in app?
The first time you discover a peripheral, the system generates an identifier (a UUID, represented by an NSUUID object) to identify the peripheral.
You can then store this identifier (using, for instance, the resources of the NSUserDefaults class), and later use it to try to reconnect to the peripheral using the retrievePeripheralsWithIdentifiers: method of the CBCentralManager class.
The following describes one way to use this method to reconnect to a peripheral you’ve previously connected to.
knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];
The NSUUID class is not toll-free bridged with CoreFoundation’s
CFUUID. Use UUID strings to convert between CFUUIDRef and NSUUID, if
needed. Two NSUUID objects are not guaranteed to be comparable by
pointer value (as CFUUID is); use isEqual(_:) to compare two NSUUID
instances.
Refer this: https://developer.apple.com/documentation/foundation/nsuuid
Refer last two sections: https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForInteractingWithARemotePeripheralDevice/BestPracticesForInteractingWithARemotePeripheralDevice.html#//apple_ref/doc/uid/TP40013257-CH6-SW1

Incorrect BLE Peripheral Name with iOS

I am writing an iOS app to communicate with a BLE device. The device can change names between connections (not during the BLE connection), but iOS refuses to change the device name.
For example: I can connect to the device when its name is SadName. I disconnect it, shut down the app, etc. and change the device's name to HappyName. But, when I scan for devices iOS still shows the peripheral name as SadName.
If I debug the app and look at:
(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
the value of peripheral.name is SadName so I don't think that it is something that I am interpreting incorrectly in code. I should mention that when I scan for devices, my code is:
[self.CM scanForPeripheralsWithServices:nil options:0]; // Start scanning
I am guessing that it is simply because the devices UUID is the same so iOS is pulling it from its cached devices list, but I want to override that.
Thoughts? Sorry, I am new to iOS.
Cheers -
MSchmidtbauer
The CoreBluetooth API of iOS SDK does not provide a way to force refresh the peripheral name.
Currently it is not feasible to use peripheral.name in iOS when the device name in the BLEdevice changes.
Apple suggests to scan for a specific device by specifying a list of CBUUID objects (containing one or more service UUIDs) that you pass to scanForPeripheralsWithServices:
NSArray *services = #[[CBUUID UUIDWithString: #"2456e1b9-26e2-8f83-e744-f34f01e9d701"] ]; // change to your service UUID!
NSDictionary *dictionary = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
[self.manager scanForPeripheralsWithServices:services options:dictionary];
This reduces the number of calls of didDiscoverPeripheral. Do not just pass nil to scanForPeripheralsWithServices. It also allows your app to scan for a peripheral when in background state.
If you are looking for a way to broadcast dynamic information that's available before a connection is established, you can use the Advertise or Scan Response Data. The peripheral can be configured to broadcast the entries called Local Name and Manufacturer Specific Data. This data is availabe in the didDiscoverPeripheral:
- (void)centralManager: (CBCentralManager *)central
didDiscoverPeripheral: (CBPeripheral *)peripheral
advertisementData: (NSDictionary *)advertisementData
RSSI: (NSNumber *)RSSI {
NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
NSData *manufacturerData = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey];
NSLog(#"Local: name: %#", localName);
NSLog(#"Manufact. Data: %#", [manufacturerData description]);
}
Local Name is an NSString, so write only printable characters on the BLE device in this filed. Manufacturer Data is an NSData, this can contain any byte value, so you can even have binary data here.
Depending on the BLE device you use, the length of Local Name and Manufacturer Specific Data is limited.
On my BLE device,I can send the 128 Bit service UUID and a 8 char Local Name with the Advertise Data. The Manufacturer Specific Data goes into the Scan Response Data and can be 29 bytes long.
Good thing about using the Adv./Scan Response Data is, it can change on this BLE device without a power cycle.
Suggestion:
Use the service UUID to filter when scanning (UUID must be part of advertising data! I omitted it in the above description)
Use the Advertise/Scan Response Data for further filtering
Forget about peripheral.name as long as there is no deterministic refresh available
Your guessing is correct.
It is because of the core-blutetooth cache.
Generally changing name / services / characteristics on BLE devices are "not supported". All these parameters are getting cached.
There are two ways of solving this:
restart bluetooth adapter, so bluetooth cache gets cleared (I'm afraid there is no way to do this programatically, but i might be wrong)
your device BLE implements the GATT Service Changed characteristic: read about this here: core_v4.1.zip
Vol 3, Part G, 2.5.2, and Vol 3, Part G, 7.1.
Alternatively check the advertisement data of your BLE device. It might have a name property which should get refreshed every time the BLE device is advertising data (advertising data doesn't get cachced).
The CBPeripheralDelegate protocol contains a method...
- (void)peripheralDidUpdateName:(CBPeripheral *)peripheral NS_AVAILABLE(NA, 6_0);
... which is made for this purpose.
Edit - just realized that the second part of the accepted answer above has the same solution :-( I should have read more closely. I will leave this answer here anyway, since it includes RoboVM code.
I have found a solution to this problem. Adding the GATT Service Changed characteristic didn't work, nor did reading the device name directly from the Device Name characteristic 2A00 since iOS hides the Generic Access service. However, if the peripheral includes its local name in an advertising packet, it is available from the advertisement data dictionary provided on a scan result using the retrieval key CBAdvertisementDataLocalNameKey. I copy this into my BLE device wrapper and use it instead of the name available from the CBPeripheral. Example code in Java for RoboVM is below. The OBJC or Swift equivalent is straightforward.
#Override
public void didDiscoverPeripheral(CBCentralManager cbCentralManager, CBPeripheral cbPeripheral, CBAdvertisementData cbAdvertisementData, NSNumber rssi) {
NSData manufacturerData = cbAdvertisementData.getManufacturerData();
byte[] data = null;
if(manufacturerData != null)
data = manufacturerData.getBytes();
IosBleDevice bleDevice = new IosBleDevice(cbPeripheral);
String name = cbAdvertisementData.getLocalName();
if(name != null && !name.equals(cbPeripheral.getName())) {
CJLog.logMsg("Set local name to %s (was %s)", name, cbPeripheral.getName());
bleDevice.setName(name);
}
deviceList.put(bleDevice.getAddress(), bleDevice);
if(!iosBlueMaxService.getSubscriber().isDisposed()) {
BleScanResult bleScanResult = new IosBleScanResult(bleDevice,
cbAdvertisementData.isConnectable(),
data);
bleScanResult.setRssi(rssi.intValue());
iosBlueMaxService.getSubscriber().onNext(bleScanResult);
}
}

advertising tx power level in iOS

I am currently programming the peripheral side of an app. I want to advertise the tx power level, but all i have found as far as tx documentation is:
CB_EXTERN NSString * const CBAdvertisementDataTxPowerLevelKey; // A NSNumber
I have tried to implement this in the following way:
/** Start advertising
*/
- (IBAction)switchChanged:(id)sender
{
[self.peripheralManager startAdvertising:#{ CBAdvertisementDataServiceUUIDsKey : #[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] }];
[self.peripheralManager startAdvertising: CBAdvertisementDataTxPowerLevelKey];
}
#end
I keep getting a warning on my last line of code saying "Incompatible pointer types sending 'NSString*' to parameter of type 'NSDictionary*'. I understand that my TxPowerLevelKey is a NSString, but what is NSDictionary referring to?
Other answers have addressed how your dictionary is defined, however, you were looking for a higher level issue; How to transmit the txPower level from an iOS device.
The answer is that currently you can't. After you fixed your code, it compiled and ran, but CoreBluetooth is simply ignoring that key.
As stated by the documentation:
An optional dictionary containing the data you want to advertise. The
possible keys of an advertisementData dictionary are detailed in
CBCentralManagerDelegate Protocol Reference. That said, only two of
the keys are supported for peripheral manager objects:
CBAdvertisementDataLocalNameKey and
CBAdvertisementDataServiceUUIDsKey.
Hope that helps
In Objective-C, #{} is shorthand for [NSDictionary dictionaryWithObjectsAndKeys:(id), ..., nil]. The warning indicates the -[PeripheralManager startAdvertising] method is expecting an NSDictionary. Try wrapping the key in a dictionary with a Boolean True value (represented as an NSNumber object with #(YES)):
[self.peripheralManager startAdvertising:#{ CBAdvertisementDataTxPowerLevelKey : #(YES)}];
As you don't seem to know what an NSDictionary* object is please see the Apple Documentation for NSDictionary.
But to answer your question the warning
Incompatible pointer types sending 'NSString*' to parameter of type 'NSDictionary*
which is referring to
[self.peripheralManager startAdvertising: CBAdvertisementDataTxPowerLevelKey];
Is because startAdvertising: will be declared something like
- (void)startAdvertising:(NSDictionary *)start;
So it is expecting you to pass in an NSDictionary* object whereas at the moment you are passing in an NSString* object.
You can resolve this in one of two ways. The first way would be to use the short hand way like you have already done here
[self.peripheralManager startAdvertising:#{ CBAdvertisementDataServiceUUIDsKey : #[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] }];
Notice that the shorthand version of an NSDictionary* object starts at #{ and ends at } so the to declare an NSDictionary* object this way it would be something like #{ Key : Object } so for you it would be #{ CBAdvertisementDataTxPowerLevelKey : #(YES) }
The second way of declaring this would be to do it as I would think of the normal way like :
[NSDictionary dictionaryWithObjectsAndKeys:#(YES), CBAdvertisementDataTxPowerLevelKey, nil]
if you have any questions please just ask.

How do you create a descriptor for a mutable characteristic?

The documentation for CBMutableDescriptor:initWithType:value: says to pass a "128-bit UUID that identifies the characteristic" for the type parameter. It then goes on to say you should only use one of CBUUIDCharacteristicUserDescriptionString or CBUUIDCharacteristicFormatString for the type parameter. Finally, there is no method to add a descriptor to a mutable characteristic.
It appears that the parameter is doing two mutually exclusive things. On the one hand, it is being used to tell the O/S which characteristic the descriptor applies to, and on the other, it is being used to set the type of descriptor. The second makes more sense, but then how do you add the descriptor to the characteristic?
Whether you pass the UUID for the characteristic or CBUUIDCharacteristicUserDescriptionString, iOS crashes with
Assertion failure in -[CBMutableDescriptor initWithType:value:], /SourceCache/CoreBluetooth_Sim/CoreBluetooth-59.3/CBDescriptor.m:25
What's the correct way to create the CBMutableDescriptor and add it to a CBMutableCharacteristic?
You're correct about the docs. But just to be clear for everyone, here is a citation found in CBDescriptor.h:
...Only the Characteristic User Description and Characteristic
Presentation Format descriptors are currently supported. The
Characteristic Extended Properties and Client Characteristic
Configuration descriptors will be created automatically upon
publication of the parent service, depending on the properties of the
characteristic itself.
So in other words, unless you are setting those descriptors, the system will block you (thus why you got the assertion failure).
So say you want to use the Characteristic User Description descriptor, you would do:
CBUUID *yourCharUUID = [CBUUID UUIDWithString:#"c07c5050-15a0-11e3-8ffd-0800200c9a66"];//whatever UUID your using
CBMutableCharacteristic *yourCharacteristic = [[CBMutableCharacteristic alloc]initWithType:yourCharUUID properties:CBCharacteristicPropertyWriteWithoutResponse value:nil permissions:perms];
CBUUID *userDescriptionUUID = [CBUUID UUIDWithString:CBUUIDCharacteristicUserDescriptionString];//or set it to the actual UUID->2901
CBMutableDescriptor *yourDescriptor = [[CBMutableDescriptor alloc]initWithType:userDescriptionUUID value:#"myDescriptorValue"];
yourCharacteristic.descriptors = #[yourDescriptor];
Let me know if you have any questions.

Resources