Receive data packets via BLE Notifications - ios

When receiving multiple packets via BLE notifications, iOS is only giving me access to the final packet sent. I am using YMSCoreBluetooth to connect to a BLE peripheral with multiple services, each of which has multiple characteristics. I connect to the peripheral, discover the services and discover the characteristics of those services without a problem. My goal is to subscribe to a certain characteristic's notifications and receive via the notifications a series of data packets. My subscription is successful and I can see through use of NSLogs within my code that I am receiving the notifications containing the data. The issue is that when I go to access the data from each notification as it comes in, every notification gives me only the data contained in the last packet sent.
My code for receiving notifications is as follows:
- (void)notifyCharacteristicHandler:(YMSCBCharacteristic *)characteristic error:(NSError *)error
{
if (error) {
NSLog(#"Error: Error in handling notification.\n%#", error);
}
else if ([characteristic.name isEqualToString:#"InterestingChar"]) {
if (self.firstNotify) {
self.mutableData = [[NSMutableData alloc] init];
self.firstNotify = NO;
}
NSData *data = [[NSData alloc] init];
data = characteristic.cbCharacteristic.value;
[self.mutableData appendData:data];
self.notifyCounter++;
NSLog(#"Notify received! Count: %ld \nData =%#",(long)self.notifyCounter,self.mutableData);
}
else NSLog(#"Other notification received");
}
For instance, if I receive 5 notifications with the following data:
1 ababababab
2 bcbcbcbcbc
3 cdcdcdcdcd
4 dedededede
5 efefefefef
My NSLog would print out efefefefef for the first notify data, efefefefef efefefefef for the second, and so on appending the last data value for each subsequent notify.
I am trying to send the notifications as quickly as possible from the peripheral using BLE. The connection interval is between 20ms and 40ms (iOS demands a range of at least 20ms) and three packets are being sent per connection interval.
EDIT:
Paulw11's suggestion worked beautifully. I fixed the issue by amending the YMSCB 'didUpdateValueForCharacteristic' method to obtain the value of the characteristic and pass it along with the pointer to the characteristic itself onto the 'notifyCharacteristicHandler' method. The amended method now looks as follows:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
__weak YMSCBPeripheral *this = self;
NSData *value = characteristic.value;
_YMS_PERFORM_ON_MAIN_THREAD(^{
YMSCBService *btService = [this findService:characteristic.service];
YMSCBCharacteristic *yc = [btService findCharacteristic:characteristic];
if (yc.cbCharacteristic.isNotifying) {
[btService notifyCharacteristicHandler:yc value:value error:error];
} else {
if ([yc.readCallbacks count] > 0) {
[yc executeReadCallback:characteristic.value error:error];
}
}
if ([this.delegate respondsToSelector:#selector(peripheral:didUpdateValueForCharacteristic:error:)]) {
[this.delegate peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error];
}
});
}
You obviously also need to amend the 'notifyCharacteristicHandler' method to accept the new argument.

Looking at the internal didUpdateValueForCharacteristic delegate method of the YMSCoreBluetooth library, it sends the data to your method using a "perform on main thread" and it doesn't capture the data - it just sends a reference to the characteristic. Also, it performs a "findCharacteristic" on the characteristic by executing a linear search through the array on the main thread even though this could have been done immediately on entering the delegate method on the current thread. Granted this isn't going to be a very big array but it seems that this library hasn't been created with performance in mind.
I suspect that you have a timing problem - by the time your method executes the data in the characteristic has been over written. If you have control over your peripheral, slow it right down for a test to see if the problem goes away.
If it is timing related then you could try a straight Core-Bluetooth implementation, or try a modification to YMSCoreBluetooth so that it captures the data earlier - perhaps if it created a copy of the peripheral at the start of didUpdateValueForCharacteristic and sent that to your method it would work.

Related

Block a for loop until a delegate method gets fired

I'm currently working with CBPeripheralDelegate to exchange messages between an iOS device and a Bluetooth Low Energy USB dongle. I have to implement the sendMessage: method that writes data bytes using a serial emulation service. This method has to send a frame of 15 bytes (or less) at the time, waiting for an ack from dongle before sending the next one.
Below is my code:
- (void)sendMessage:(NSData *)message {
NSArray *chuncks = [self splitMessage:message];
for (NSUInteger i = 0; i < chunks.count; i++) {
NSData *chunk = [chunks objectAtIndex:i];
[self sendChunk:chunk withId:i ofChunks:chances.count];
// Wait for the ack to be received
}
}
- (void)sendChunk:(NSData *)chunk withId:(NSInteger)id ofChunks:(NSInteger)count {
NSMutableData *frame = [NSMutableData new];
// Here I build my frame, adding header, current chunk ID and total number of chunks, then I call...
[serialEmulationService writeValue:frame forCharacteristic:serialEmulationServiceCharacteristic type:CBCharacteristicWriteWithResponse];
}
Now the issue: the for loop within the sendMessage: method has to be blocked until the peripheral won't receive the ack, possibly with a timeout. This ack is received inside the delegate method - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error, so here I have to restart the for loop previously blocked.
What's the best practice for this particular situation? I'd like to use GCD's semaphores, but I cannot figure out how to implement synchronous calls and cannot manage to understand any of the many online examples that explain this technique.
Could someone please give me a hand?
How about skipping the for loop entirely…
#property (nonatomic) NSMutableArray *chunks;
#property (nonatomic) NSInteger chunkId;
- (void)sendMessage:(NSData *)message {
self.chunks = [[self splitMessage:message] mutableCopy];
self.chunkId = 0;
[self sendNextChunk];
}
- (void sendNextChunk {
NSData * chunk = self.chunks.firstObject;
if chunk == nil {
return
}
[self.chunks removeObjectAtIndex: 0];
[self sendChunk:chunk withId:chunkId++ ofChunks:chances.count];
}
- (void)sendChunk:(NSData *)chunk withId:(NSInteger)id ofChunks:(NSInteger)count {
NSMutableData *frame = [NSMutableData new];
// Here I build my frame, adding header, current chunk ID and total number of chunks, then I call...
[serialEmulationService writeValue:frame forCharacteristic:serialEmulationServiceCharacteristic type:CBCharacteristicWriteWithResponse];
}
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error {
[self sendNextChunk];
}
The approach would be to use the Notification. Create separate method to run your for loop. This method will observer for the Notification, inside the delegate method post the Notification. In the sendMessage: keep adding the message to a property.

Central to communicate with multiple peripherals at once

I'm trying to get my central (iOS device) to communicate with two peripherals (one iOS device, one not). Individually they work fine but I'm finding that once I get both peripherals involved, only the peripheral that was connected to the most recently is able to receive data from the central device. Is there a way to send data from the central to each peripheral without disconnecting and reconnecting the peripheral?
This is my code for writing to peripheral:
- (void) peripheral: (CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBChatacteristic *)characteristic error:(NSError *)error{
NSString *newValue = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(#"Received: %# from %#", newValue, peripheral.name);
CBPeripheral *sender = peripheral;
if([newValue isEqualToString:#"ready"]){
NSData *messageValue = [#"challenge dataUsingNSUTF8StringEncoding];
[sender writeValue:messageValue forCharacteristic:_writeCharacteristic type:CBCharacteristicWriteWithResponse];
NSLog(#"Challenge sent to %#", sender.name);
}
Breakpoints indicate the code is being executed and the log shows "challenge sent" to the correct peripheral, it's just that peripheral never receives it.
Code for peripheral receiving:
-(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *) requests{
for(int i=0; i<requests.count; i++){
CBATTRequest *request = requests[i];
if([request.characteristic.UUID isEqual:_writeCharacteristic.UUID]){
NSString *stringValue = [[NSString alloc] initWithData:request.value encoding:NSASCIIStringEncoding];
NSLog(#"Write Request: %#", stringValue);
}
}
To answer your question: Yes it is possible that you can have multiple connections to different peripherals and read/write to them. iOS devices can handle up to 8 simultaneous connections.
For the implementation, have a look at the Core Bluetooth Programming Guide from Apple. All the things you need are explained there.
Just as a suggestion: If multiple devices (let's call them B and C) should receive data from 1 device (called A), I would use the peripheral role on device A that needs to send the data to the others. Because then devices B and C can scan, connect and subscribe to a characteristic and receive updates without having to read again.

iOS CoreBluetooth losing local peripheral.name after connection

I'm building a bluetooth app using CoreBluetooth on iOS. Every time an app is launch user receives an ID, which is saved in Peripheral's LocalNameKey, begin to advertise with it and start to search for other users using CentralManager. Every user is identified by his local name and that's work fine.
Using CentralManager every user can write a value to another's user Peripheral's characteristics, and notify them about the change. That is also working.
Problem occurs here, after the connection is done and Peripheral executed didRecieveWriteRequests method, both CBPeripheralManager and CBCentralMange are being reseted and reinitialised. After that Peripheral's LocalNameKey is no longer my specific ID, but an iPhone's Given Name or (null). It ruins the whole idea of the app, but I'm pretty sure it can be done.
It works again, when I turn off and on bluetooth.
This how I cleanup Central after connection:
- (void)cleanup
{
// See if we are subscribed to a characteristic on the peripheral
if (self.peripheral.services != nil) {
for (CBService *service in self.peripheral.services) {
if (service.characteristics != nil) {
for (CBCharacteristic *characteristic in service.characteristics) {
if (characteristic.isNotifying) {
// It is notifying, so unsubscribe
[self.peripheral setNotifyValue:NO forCharacteristic:characteristic];
// And we're done.
return;
}
}
}
}
}
// If we've got this far, we're connected, but we're not subscribed, so we just disconnect
[self.manager cancelPeripheralConnection:self.peripheral];
}
This is how I reinitialise bluetooth's central and peripheral:
self.bluetoothPeripheral = nil;
self.bluetoothCentral = nil;
self.bluetoothPeripheral = [[BluetoothPeripheral alloc] initWithID:idNumber];
[self.bluetoothPeripheral loadManager];
self.bluetoothCentral = [[BluetoothCentral alloc] init];

Not able to receive response of the sent UDP packets using GCDAsyncSocket

I am making an app to send UDP packets in order to switch on a LED bulb. I have been able to perform all the actions when I am connecting to the Ad-hoc created by the Wifi bridge.
Now, I want to configure the Wifi bridge so that it can connect to my main router. I have the AT command set to perform this procedure but somehow I am not able to receive the response form the Wifi bridge for the commands which I am sending to it.
The procedure is as follows:-
Step 1 : Send UDP message to the LAN broadcast IP address of "10.10.100.255" and port of 48899 => "Link_Wi-Fi"
All Wifi bridges on the LAN will respond with their details. Response is "10.10.100.254, ACCF232483E8"
Step 2 : (optional for changing settings on the wifi bridge): Then send "+ok" to the LimitlessLED Wifi Bridge. Send UDP message to the response IP address returned from step 1 "10.10.100.254" => "+ok"
Step 3 : (optional for changing settings on the wifi bridge): After that you may send AT commands (ending with \r\n) to the module.
The code for sending the UDP packets is as follows
-(void)configureWifi{
counter++;
NSString *host = #"10.10.100.255";
if ([host length] == 0)
{
[self logError:#"Address required"];
return;
}
int port = 48899; //[portField.text intValue];
if (port <= 0 || port > 65535)
{
[self logError:#"Valid port required"];
return;
}
NSString *msg = #"Link_Wi-Fi";
NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];
NSLog(#"the message sent is %#", data);
[udpSocket sendData:data toHost:host port:port withTimeout:-1 tag:tag];
}
Now in order to setup the socket and to receive the data I am using these two delegate methods:
- (void)setupSocket
{
// Setup our socket.
// The socket will invoke our delegate methods using the usual delegate paradigm.
// However, it will invoke the delegate methods on a specified GCD delegate dispatch queue.
//
// Now we can configure the delegate dispatch queues however we want.
// We could simply use the main dispatc queue, so the delegate methods are invoked on the main thread.
// Or we could use a dedicated dispatch queue, which could be helpful if we were doing a lot of processing.
//
// The best approach for your application will depend upon convenience, requirements and performance.
//
// For this simple example, we're just going to use the main thread.
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
if (![udpSocket bindToPort:0 error:&error])
{
[self logError:FORMAT(#"Error binding: %#", error)];
return;
}
if (![udpSocket beginReceiving:&error])
{
[self logError:FORMAT(#"Error receiving: %#", error)];
return;
}
[self logInfo:#"Ready"];
}
and to Receive data this is the method which is note getting called after sending the UDP packets. This is the delegate method of the GCDAsyncUdpSocket class which I have used in my project in order to send and receive the UDP packets.
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext
{
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (msg)
{
[self logMessage:FORMAT(#"RECV: %#", msg)];
}
else
{
NSString *host = nil;
uint16_t port = 0;
[GCDAsyncUdpSocket getHost:&host port:&port fromAddress:address];
[self logInfo:FORMAT(#"RECV: Unknown message from: %#:%hu", host, port)];
}
}
Once I am able to receive the response I will be able to send the next AT commands in order to configure the Bridge.
Thanks. Any help will be appreciated.
Here are the troubleshooting steps I recommend that you use :
1- I'm assuming you are using ARC so make sure that your udpSocket variable has a strong reference throughout the asynchronous communication. If it is being freed, then that could explain the absence of a callback.
2- Make sure the communication is really happening the way you think it is. Use a software such as Wireshark to capture the packets being exchanged on the network. This should allow you to confirm that your packets do get sent upon calling sendData: and it will also allow you to confirm whether or not you are getting a reply back.
3- Make sure you are using the GCDAsyncUdpSocket properly. Considering you want to broadcast a message, you shouldn't be calling bindToPort:error: in your setupSocket method. Instead you should be calling enableBroadcast:error:. Considering you also want to receive packets after broadcasting, you should use the connectToHost:onPort:error: method to change the state of the socket to allow for bidirectional communication. After that is done, you can replace your usage of sendData:toHost:port:withTimeout:tag: by sendData:withTimeout:tag:. Finally, you can call beginReceiving: so that the delegate gets called for any incoming packets.
4- If this still doesn't get you through it, I recommend that you read throughly the documentation of the GCDAsyncUdpSocket which is very well documented.
You can trouble shoot the problem using Wireshark or any network capture tool.
We use to work in similar kind of project where we used Wireshark extensively.
If packet has reached device(Z-Wave ) it will send out some sort of Ack.
this will help to make sure packets are getting out.

CKFetchNotificationChangesOperation returning old notifications

I'm working on a CloudKit-based app that uses CKSubscription notifications to keep track of changes to a public database. Whenever the app receives a push notification I check the notification queue with CKFetchNotificationChangesOperation and mark each notification read after processing it:
__block NSMutableArray *notificationIds = [NSMutableArray new];
CKFetchNotificationChangesOperation *operation = [[CKFetchNotificationChangesOperation alloc] initWithPreviousServerChangeToken:self.serverChangeToken];
operation.notificationChangedBlock = ^(CKNotification *notification) {
[notificationIds addObject:notification.notificationID];
[self processRemoteNotification:notification withCompletionHandler:completionHandler];
};
__weak CKFetchNotificationChangesOperation *operationLocal = operation;
operation.fetchNotificationChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to fetch queued notifications: %#", operationError);
}
else {
self.serverChangeToken = serverChangeToken;
completionHandler(UIBackgroundFetchResultNewData);
// Mark the processed notifications as read so they're not delivered again if the token gets reset.
CKMarkNotificationsReadOperation *markReadOperation = [[CKMarkNotificationsReadOperation alloc] initWithNotificationIDsToMarkRead:[notificationIds copy]];
[notificationIds removeAllObjects];
markReadOperation.markNotificationsReadCompletionBlock = ^(NSArray *notificationIDsMarkedRead, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to mark notifications read: %#", operationError);
}
else {
NSLog(#"%lu notifications marked read.", (unsigned long)[notificationIDsMarkedRead count]);
}
};
[[CKContainer defaultContainer] addOperation:markReadOperation];
if (operationLocal.moreComing) {
NSLog(#"Fetching more");
[self checkNotificationQueueWithCompletionHandler:completionHandler];
}
}
};
[[CKContainer defaultContainer] addOperation:operation];
As I understand it marking a notification read will keep it from showing up in future queue fetches, even if the server change token is reset to nil. Instead I'm getting a lot of old notifications in every fetch with a non-nil change token when there should only be 1 or 2 new ones. I can detect the old ones from the notificationType flag, but I'm concerned that they're showing up at all. Am I missing a step somewhere?
I know this is a bit old, but I was running into the same issue. I think I figured it out (at least for my case).
In my code, I was doing the same as you: that is, adding all the notificationIDs to an array and using that in my CKMarkNotificationsReadOperation, and was also getting all the notifications returned each time (although, as you noted, with a type of "ReadNotification").
I changed my code so that I was only adding "new" notifications to my array, and not the "ReadNotification" items, and sending those. That fixed it.
It seems that sending a notification back to the server to be marked as read, even if it already has been marked as such, will cause it to be returned again as "ReadNotification."
I hope this helps someone.
The documentation isn't very clear it should say: "Marking a notification as read prevents it from being returned by subsequent fetch operations"...as a query notification type. Further clarification it should say the notifications will instead be returned as read type.
If it wasn't returned at all then other devices that missed the push wouldn't know that something has changed!

Resources