In my implementation of the CBCentralManagerDelegate protocol, I have the following function.
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
print(#function)
if error != nil {
print("Error in \(#function) :\n\(error!)")
return
}
......
// More useful code irrelevant to the question.
}
When the above function is called I can see the message below in the Xcode debugging console.
centralManager(_:didDisconnectPeripheral:error:)
Error in centralManager(_:didDisconnectPeripheral:error:) :
Error Domain=CBErrorDomain Code=7 "The specified device has disconnected from us."
UserInfo={NSLocalizedDescription=The specified device has disconnected from us.}
Here is my question:
I must be missing something (because too simple or too subtle), but why does is show an error because "The specified device has disconnected from us."
In the centralManager:didDisconnectPeripheral function, what else could I expect other than the device being disconnected?
I hope some enlighted expert can bring some light an explain why this is so.
As per the Apple documentation:
If the disconnection was not initiated by cancelPeripheralConnection(_:), the cause is detailed in error.
I.e if You disconnect then you get no error but if They disconnect you see that through the error.
Source: https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518791-centralmanager
Related
When doing Bluetooth communications one is often placed in a situation where one does a call which gets a response in a delegate, for example the characteristic discovery shown below:
func discoverCharacteristics(device: CBPeripheral)
{
servicesCount = device.services!.count
for service in device.services!
{
print("Discovering characteristics for service \(service.uuid)")
device.discoverCharacteristics([], for: service)
}
}
Now this discovery is not for a specific device but for health devices following the Bluetooth SIG services/profiles, so I don't know exactly what services they might have and I don't know how many characteristics might be in each service. The method is asynchronous and the answers are signaled in the following delegate method:
// Discovered Characteristics event
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
for characteristic in service.characteristics!
{
print("Found characteristic \(characteristic.uuid)")
}
servicesCount = servicesCount - 1;
print("Characteristics sets left: \(servicesCount)")
if servicesCount == 0
{
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
}
Now I need to wait until the discovery is done before I can take the next step because what I do next often depends on what I got. In Java and on Android what I do is wait on a CountDownLatch in the calling method and I signal the latch in the callback to release that wait.
The iOS equivalent of the CountDownLatch seems to be the DispatchSemaphore. However, doing so apparently blocks the system and no delegate ever gets called. SO what I have done (as shown in the code above) is to initialize a variable servicesCount with the number of services and in the delegate callback decrement it each time it is signaled. When it gets to zero I am done and then I do the next step.
This approach works but it seems hacky; it can't be correct. And its starting to get really messy when I need to do several reads of the DIS characteristics, the features, the various time services, etc. So what I would like to know is what is the proper method to wait for a delegate to get signaled before moving forward? Recall I do not know what services or characteristics these devices might have.
First of all, if you already have an implementation with CountDownLatch for Android, you may just do the same implementation for iOS. Yes, Swift doesn't have a built-in CountDownLatch, but nice folks from Uber created a good implementation.
Another option is to rely on a variable, like you do, but make it atomic. There are various implementations available online, including one in the same Uber library. Another example is in RxSwift library. There are many other variations. Atomic variable will provide a thread-safety to variable's read/write operations.
But probably the most swifty way is to have a DispatchGroup. Will look something like this:
let dispatchGroup = DispatchGroup() // instance-level definition
// ...
func discoverCharacteristics
{
for service in device.services!
{
dispatchGroup.enter()
// ...
}
dispatchGroup.notify(queue: .main) {
// All done
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
// ...
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
// ...
dispatchGroup.leave()
}
In other words, you enter the group when you are about to submit a request, you leave it when request is processed. When all items left the group, notify will execute with the block you provided.
You only get a single call to didDiscoverCharacteristicsFor for each call to device.discoverCharacteristics. That is, the didDiscoverCharacteristicsFor call is only made once all of the service's characteristics have been discovered.
You have the peripheral passed to the delegate call, so you have the information you need to know the context of the delegate call. There is no need to "wait". You can use a simple state machine per peripheral or service if you are discovering data for multiple peripherals/services simultaneously.
You can even keep a set of peripherals that aren't in a completely discovered state if you need to take some action once discovery is complete; e.g. You can remove a peripheral from the set once discovery is complete for that peripheral. If the set is empty, discovery is complete for all devices.
All of this state belongs in your model. All your delegate implementation should need to do is update the model with the data that has been discovered.
In a CoreBluetooth related iOS app written in Swift (4.2), I have the following problem.
The app handles a certain number of Peripherals and Centrals.
Each Peripheral provides a service and has several Centrals connected to it; it keeps an accounting of that and knows how many Centrals are connected.
In the same way each Central is connected to several Peripherals; and also keeps an accounting of that to know how many Peripherals are connected.
The question I have is about the book-keeping to maintain the accounting I was just mentioning, up to date.
From the Peripheral side, it works and I can see when a Central has disconnected.
For that I use:
peripheralManager(_:central:didSubscribeTo:)
and:
peripheralManager(_:central:didUnsubscribeFrom:)
From the Central I want to use:
centralManager(_:didConnect:)
and:
centralManager(_:didDisconnectPeripheral:error:)
But here, for some reason, it does not work. The first function (centralManager(_:didConnect:)) is called as expected but not the second (centralManager(_:didDisconnectPeripheral:error:)).
I suppose this last issue of the function not being called is what I need to solve. But I may be wrong.
As a result, I see when a Peripheral is coming but not when it is leaving.
What am I missing? Or am I just doing it wrong?
I test the app using two devices.
--- Further information --- (Post update)
Here is some of the related code:
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
print(#function)
if cbPerifList == nil {cbPerifList = [CBPeripheral]()}
if let perifIndx = cbPerifList!.index(where:{$0.identifier==peripheral.identifier}) {
central.connect(cbPerifList[perifIndx], options: nil)
} else {
peripheral.delegate = self
cbPerifList.append(peripheral)
central.connect(peripheral, options: nil)
}
}
One more possibly relevant thing I noticed is the following. Instead of switching of the peripheral by a button switch as I should normally do, I force kill the app on the device currently playing the peripheral role, then the centralManager(_:didDisconnectPeripheral:error:) function is called on the other device as I would expect it to be; and the accounting is correctly performed.
In my CoreBluetooth App
func centralManager (_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
}
Is called when ever peripheral is lost.
Maybe check your syntax of your didDisconnectPeripheral func.
I did not find a direct answer to this question.
Reading the comments seems to show there is now answer. In other words what I wanted to do sounds impossible. If some expert happens to know otherwise, please let us know.
In the meanwhile I made a work-around solution:
When shutting of one peripheral, I use one of its characteristics to set some non-sense value which is then used as hint by the central to know that the peripheral has "said good-bye"..... And it works.
We are seeing a case where writing data without response:
peripheral.writeValue(data, forCharacteristic: characteristic, type: .WithoutResponse)
Is still getting a callback to:
func peripheral(peripheral: CBPeripheral, didWriteValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) {
The error is nil and the char is the one we just wrote to (.WithoutResponse). Writing using .WithResponse also gets the callback as one would expect.
What could cause such behavior?
iOS 9.3, iPhone 6s+, Xcode 7.3.1, iOS Central, Ti CC2640 BLE Stack 2.1.0 Peripheral
Update 1:
The properties on the characteristic are: .WriteWithoutResponse, .Write, .Notify (verified via characteristic.properties)
It was exactly the case for me on iOS10.0 and iOS10.1 Things went back to normal on iOS10.2 beta3 released Nov'14th 2016.
More details about CoreBluetooth state machine bugs under iOS10.0 10.1 can be found here: https://forums.developer.apple.com/thread/51960
I am having an issue when instantiating my CBCentralManager. I get a "duplicate issue" message when monitoring it from the iOS console (it does not show in the XCode console).
I've tried updating the queue name and the restoration key id without success. This is how I instantiate my Central Manager:
CBCentralManager *central = [[CBCentralManager alloc] initWithDelegate: self
queue: dispatch_queue_create("com.mydomain.myapp.scanner", NULL)
options: #{
CBCentralManagerOptionRestoreIdentifierKey: #"hexa-string-comes-here"
}];
And those are the errors I am getting:
CKLs-iPhone-5S securityd[78] :
securityd_xpc_dictionary_handler MyApp[2571] add The operation
couldn’t be completed. (OSStatus error -25299 - duplicate item
O,genp,E99372E2,L,ck,X2W6M5UYJ9.com.mydomain.myapp,0,acct,svce,v_Data,20151218165347.298588Z,2CAE5650)
CKLs-iPhone-5S MyApp[2571] : SecOSStatusWith
error:[-25299] The operation couldn’t be completed. (OSStatus error
-25299 - Remote error : The operation couldn’t be completed. (OSStatus error -25299 - duplicate item
O,genp,E99372E2,L,ck,X2W6M5UYJ9.com.mydomain.myapp,0,acct,svce,v_Data,20151218165347.298588Z,2CAE5650))
Any ideas?
If you want to use the CBCentralManagerOptionRestoreIdentifierKey, you must
implement the method
// in Objective-C
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict
// or in Swift
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : AnyObject])
from CBCentralManagerDelegate, and
use the background mode Uses Bluetooth LE accessories set up in Xcode:
This may be related to known Keychain issue. (However, it is only a guess that this is Keychain coming from Security log in your app). OSStatus duplicate item appears when there is already a registered item with all attributes supplied along with the item. So what could happen in here is:
keychain failed to delete last restoration id - item is left in keychain.
register for the restoration with the same id
keychain tries to save item.
it returns duplicate item error
I'm building a simple IOS app for interacting with a Bluetooth LE device.
Main steps ( Connecting, discovering services, discovering characteristics and reading characteristics' values ) are done rightly.
Now i'm searching for a tutorial/example for sending command to the Bluetooth LE device i'm connected on.
I'm searching in the Apple Developer Guide but i'm not finding anything.
You should check out Apple's TemperatureSensor example, specifically where it calls [servicePeripheral writeValue:data ...
When interacting with a CBCharacteristic I am going to assume it is a read/write characteristic (you can confirm this by looking at the value for properties on your characteristic).
The two main methods when interacting with a characteristic are:
func writeValue(_ data: NSData!, forCharacteristic characteristic: CBCharacteristic!, type type: CBCharacteristicWriteType)
func readValueForCharacteristic(_ characteristic: CBCharacteristic!)
Both of these methods are found on your CBPeripheral. Once you have called one of these functions you can utilize the CBPeripheralDelegate to confirm each of these actions in these delegate methods:
optional func peripheral(_ peripheral: CBPeripheral!, didWriteValueForCharacteristic characteristic: CBCharacteristic!, error error: NSError!)
optional func peripheral(_ peripheral: CBPeripheral!, didUpdateValueForCharacteristic characteristic: CBCharacteristic!, error error: NSError!)
These are the places you will look to confirm your read and write's success. (inside of your read you can retrieve the value that was read from the BLE Device off the value property on CBCharacteristic.
Keep in mind the interactions you have (what you can read and write) are entirely dependent on the BLE device you are interrogating/interacting with. Essentially you must know what to read, what to write, and how.
You can send a data with writeValue:forCharacteristic:type: method of CBPeripheral. What characteristic+data match the required commands must be described in device specification.