I am having a problem with using GCDAsyncUdpSocket. I am using the iPad as a user interface app that interacts with another app - call it Host, the latter running on a separate Windows machine. Both machines are on their own private network, so they are on their own subnet. At certain points, the Host sends UDP packets to the iPad to instruct it which screen to show to the user, and the iPad sends user responses via UDP packets to the Host. Finally, the iPad periodically (at 2 Hz) sends simple "heartbeat" messages to the Host.
This all works fine - for a while. Then, apparently, the iPad abruptly stops accepting the UDP packets from the Host - the latter experiences "Connection reset by peer" errors, while it (the iPad) is still successfully sending, and the Host receiving, the heartbeat messages.
I'm thinking the problem comes from my confusion with respect to how Grand Central Dispatch (GCD) works. My iPad app is pretty simple; I based it off a tutorial on iOS programming (I'm a beginner here, but very experienced with Windows, Linux, embedded/real-time, and networking). It basically consists of a main screen, which creates a second screen from time to time. So the basic structure is this:
main.m
Delegate.m
MainViewController.m
PopupViewController.m
The main.m and Delegate.m were created automagically by the Xcode during the tutorial, and have nothing special in them. The MainViewController.m is my "main screen", and owns the GCDAsyncUdpSocket that is used by the iPad app. The final file, PopupViewController.m, is the second screen, that is used like this:
# MainViewController.m
- (IBAction)sendResponseOne:(id)sender {
// Send a message to Host
[self sendUdpMessage:1];
// Switch to other view
PopupViewController *vc = [[PopupViewController alloc] init];
[vc setMainScreen:self]; // Used to access UDP from 2nd screen
[self presentViewController:vc animated:NO completion:nil];
}
# PopupViewController.m
- (IBAction)confirmAnswers:(id)sender
{
// Send a message to Host - calls same function as above main screen
[self->mainScr sendUdpMessage:2];
[self dismissViewControllerAnimated:NO completion:nil];
}
Now for the code that sems to fail. First, here is the #interface section of MainViewController.m:
# From MainViewController.m
#interface MainViewController ()
{
GCDAsyncUdpSocket *udpSocket;
}
#end
Here is how/where I create the UDP object:
# From MainViewController.m
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
{
// Setup our socket, using the main dispatch queue
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return self;
}
Here is where I bind to the port:
# From MainViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// Start UDP server
int port = 12349;
NSError *error = nil;
if (![udpSocket bindToPort:port error:&error])
{
NSLog(#"Error starting server (bind): %#", error);
return;
}
if (![udpSocket beginReceiving:&error])
{
[udpSocket close];
NSLog(#"Error starting server (recv): %#", error);
return;
}
[self startPingTimer];
isRunning = YES;
}
Here's the code that receives packets. Apparently, this function works fine for awhile, sometimes dozens of times, then unexpectedly fails.
# From MainViewController.m
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext
{
if (data.length == sizeof(MyMessage)) {
MyMessage msg;
[data getBytes:&msg length:sizeof(MyMessage)];
msg.magic = ntohl(msg.magic);
msg.msgId = ntohl(msg.msgId);
for (int i = 0; i < 4; ++i) {
msg.values[i] = ntohl(msg.values[i]);
}
if (msg.magic == 0xdeadcafe) {
switch (msg.msgId) {
case imiStateControl:
self->iceState = (IceState)msg.values[0];
break;
default:
break;
}
}
}
}
I am at a loss as to why the didReceiveData function seems to work correctly for some random amount of time (and random number of messages sent/received). I wonder a couple of things:
Is it valid for me to send a UDP message from the second screen? I think so, and sending never fails - it continues to work even after receiving fails.
How does the didReceiveData get called anyway, and how could that get broken? If I was in Linux or an RTOS, I would probably have created an explicit thread that waits on packets; how does the GCD framework decide where a packet should go?
Why would my app suddenly stop listening on the port? How do I detect/debug that?
Does it matter that the GCDAsyncUdpSocket object is owned by the main screen, as opposed to the Delegate.m module?
Is it appropriate to use the main dispatch queue, as I think I am doing? Indeed, am I doing that, and correctly?
I'm totally at a loss, so of course, any advice would be greatly appreaciated! No need to answer all the questions - especially if your answer to one is the solution!
Thanks!
This post ended about eight hours of torture that I received at the hands of the POSIX/GCDAsyncSocket/NSStream/NSNetService gods. For anyone coming across this: the cause of my GCDAsyncSocket connection reset by peer/remote peer disconnected errors was simply that I was connecting by IP on the LAN, instead of using a host name. (i.e. using the connectToAddress family of methods instead of the connectToHost family of methods). I fired up WireShark, downloaded X11, all that jazz and confirmed I was facing the same issue Bob was seeing – ARP activity around the disconnect time. Trying to connect to a host instead of an address confirmed Seth's take on the situation – it was issues with the router reassigning IP addresses.
This couldn't have been a more innocuous question on SO – question and answer with 0 upvotes, but both of you combined to give more more than enough information to solve what I thought was an intractable problem. Thanks so much!
It sounds like the receiving UDP socket is either being closed or shifted to a different address/port pair.
If its being closed, the only way outgoing packets could still work is if there were actually two sockets. Perhaps the receive one bound to port 12349 and the send one bound to port 0. Then maybe GCD is closing the receive one after some period of time. Given your followup comments about ARP, this seems less likely but is worth keeping in mind.
The ARP activity suggests that the iPad's IP address may be changing. If it were to change, or if it were to switch from the WiFi interface to a cellular interface, then the packets it sends would still get through, but packets sent to it would fail exactly as described.
Whatever is receiving those heart-beat messages, have it check the recvfrom address and make sure that any messages it sends back go to that exact address. And, of course, make sure to pay attention to endian (host vs network byte order).
I am assuming that there are no firewalls or NAT in between the two devices. If there are such things, then a whole different world of possibilities opens up.
Related
I have an iOS app which advertises itself successfully using a CBPeripheralManager. In a Chrome App, I have successfully discovered the device (hence obtaining its address).
I have added a service with a custom UUID to the peripheral manager. didAddService is called (after the peripheral manager is powered on), and so I assume that part is successful.
I call chrome.bluetoothLowEnergy.connect(device address) in the Chrome app. The callback function is called without issue, and so I believe the connection is successful. There is no function called within the app when the connection has occurred - should there be?
I then call chrome.bluetooth.getServices(device address) in the Chrome app. An array of services is returned, however the array is empty. I believe it should return an array of length 1, not 0.
Am I overlooking something? Why are no services being returned? Should any of the peripheralManager functions be called during these operations?
Connect code:
function connect(address) {
/* Connect persistently to the device. */
chrome.bluetoothLowEnergy.connect(address, {persistent: true}, function() {
if(chrome.runtime.lastError) {
/* If we click a green device, we get this. We should try and handle it
earlier. e.g. by removing the listener, or instead using the click
to disconnect the device. */
if(chrome.runtime.lastError.message == 'Already connected')
setDeviceConnected(address);
/* Print error and exit. */
console.log(chrome.runtime.lastError.message);
return;
}
console.log('Successfully connected to ' + address);
setDeviceConnected(address);
// getDeviceService();
getDeviceServices(address);
});
return true;
}
Get services code:
function getDeviceServices(address) {
chrome.bluetoothLowEnergy.getServices(address, function(services) {
console.log('services.length: ' + services.length);
});
}
Peripheral setup code:
- (void)setupPeripheral {
_serviceName = SERVICE_NAME;
_serviceUUID = [CBUUID UUIDWithString:SERVICE_UUID_STRING];
_characteristicUUID = [CBUUID UUIDWithString:CHARACTERISTIC_UUID_STRING];
_peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
_characteristic = [[CBMutableCharacteristic alloc] initWithType:_characteristicUUID
properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify
value:nil
permissions:CBAttributePermissionsReadable];
_service = [[CBMutableService alloc] initWithType:_serviceUUID primary:YES];
_service.characteristics = #[_characteristic];
NSLog(#"Peripheral set up");
}
Start advertising (add the service just before we start advertising):
/* Start advertising */
- (void)startAdvertising {
NSLog(#"Starting advertising...");
NSDictionary *advertisment = #{
CBAdvertisementDataServiceUUIDsKey : #[self.serviceUUID],
CBAdvertisementDataLocalNameKey: self.serviceName
};
[self addServices];
[self.peripheralManager startAdvertising:advertisment];
}
Call to start advertising within didUpdateState:
/* Did update state */
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
switch (peripheral.state) {
case CBPeripheralManagerStatePoweredOn:
NSLog(#"peripheralStateChange: Powered On");
// When the bluetooth has turned on, start advertising.
[self startAdvertising];
break;
Update
On connecting to the peripheral app with OSX app LightBlue, an alert shows up on the app to request a pairing. Then LightBlue can read characteristics from the app. When connecting with the Chrome app, the pairing alert does not show up.
Having chatted to web-bluetooth developers at W3C, I have now been able to discover services from the device, by updating chrome from v51 to v52 (apparantly also v53 works).
However currently (though I am yet to try it out), reading characteristics is currently unsupported for OSX, as seen here.
(I have not yet looked into it but this may provide some options on how to overcome the issues described here.)
I've ran into this problem myself. I've developed a simple app that scans for devices and their services, but all devices found always report back an empty array of services. I have this problem on both OsX and ChromeOS.
There's an exception to this: when the device has previously been paired, I was able to get the services array on ChromeOS. This is confusing to me, since nothing I've read anywhere says a device has to be manually paired first, and there's no programatic way to pair a device one discovered.
EDIT: Upgrading to Chrome 53 fixed the problem. The site linked in louisdeb's answer is related to web-bluetooth, not chrome.bluetooth, so characteristics may actually work.
I am using Reachability class from Apple to detect network events that have impact in the functionality of my app. It is a voip app that uses setKeepAliveTimeout, so every ~10 minutes wakes up reads the network status and decides if the connection should be refreshed.
BOOL res = [app setKeepAliveTimeout:600 handler:^{
[[WIFI instance] isWifiConnected];
[[AClass an_instance] refresh];
}
}];
So every 10 minutes the isWifiConnected is called and the app reads the network status again.
- (BOOL) isWifiConnected {
self.wifiReach = [Reachability reachabilityForLocalWiFi];
NetworkStatus wifiStatus = [self.wifiReach currentReachabilityStatus];
switch (wifiStatus) {
case NotReachable: {
m_wifiConnected = NO;
LOG(#"NetStatus:NotReachable");
break;
}
case ReachableViaWiFi: {
m_wifiConnected = YES;
m_wwanConnected = NO;
LOG(#"NetStatus:ReachableViaWiFi");
break;
}
}
return m_wifiConnected;
}
Although I have WiFi in the device the call returns false, ie no WiFi, and moreover NotReachable for the net status.
However after a very short time interval the reachability callback is called again and the the wifi seems connected. However i have already fired an event due to the error value and the app closes the connection with the server believing that there is no wi-fi.
Doing some research it I found this in the Readme file of the Reachability.m file (provided from Apple)
By default, the application uses www.apple.com for its remote host. You can change the host it uses in APLViewController.m by modifying the value of the remoteHostName variable in -viewDidLoad.
IMPORTANT: Reachability must use DNS to resolve the host name before
it can determine the Reachability of that host, and this may take time
on certain network connections. Because of this, the API will return
NotReachable until name resolution has completed. This delay may be
visible in the interface on some networks
.
Could it this be the problem? The delay in the dns lookup? Or do I need to enhance my code as well?
When I initialize the app I call this
self.hostReach = [Reachability reachabilityWithHostName: #"www.apple.com"];
If I use an IP address like this is correct?
self.hostReach = [Reachability reachabilityWithHostName: #"1.2.3.4"];
Is it safe to use a public IP? eg "17.178.96.59" is the result of an nslookup for apple.com
There is a method in the Reachability class that seems to be used from the Apple's demo.
- (BOOL)connectionRequired
{
NSAssert(_reachabilityRef != NULL, #"connectionRequired called with NULL reachabilityRef");
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
{
return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
}
return NO;
}
Why the connectionRequired is needed? Can be used to resolved the problem?
To preserve battery charge, iOS will close down the networking hardware when it's not being actively used. This means turning off the WiFi and cellular radios. In this situation, Reachability isn't going to be able to report a complete result, because it can't check the situation without turning everything back on again. That is what kSCNetworkReachabilityFlagsConnectionRequired is for -- to tell you that you need to make a connection to wake the hardware back up.
What you are seeing is probably something waking up when the phone is unlocked (your app or some other app with background permissions) and so everything wakes up, you see an error immediately, but then WiFi connects and you are connected again.
You need to handle Reachability as if it can tell you "definitely reachable" or "definitely not reachable" but also "situation unknown". You need to decide what you're going to do in the unknown situation. If you make a network connection immediately then you will drain the battery faster than normal. An alternative might be just to wait for the network to be woken up for some other reason. That's really up to you.
Reachability should be created with a host name, not an explicit address. The whole point of the DNSsystem is that addresses change for hosts sometimes. Linking directly to a name server should provide some security in this regard, but that isn't how it's supposed to work.
Reachability is often a best guess, not a hard and fast. The only way to be sure is to actually try. Connection required is related to this, because it's the device saying 'everything looks ok, I just haven't tried to connect for real'.
So, arguably you should fire the keep alive without checking the reachability status and use the result of the request to decide if there's an error or not. If you need to be sure, send an actual request and review the actual result.
I am just getting started with socket programming on iOS and I am struggling to determine the use of the NSStreamEventHasSpaceAvailable event for NSOutputStreams.
On the one hand, Apple's official documentation (Listing 2) shows that in the -stream:handleEvent: delegate method, data should be written to the output buffer with -write:maxLength: message, passing data continually from a buffer, whenever the NSStreamEventHasSpaceAvailable event is received.
On the other hand, this tutorial from Ray Wenderlich and this iOS TCP socket example on GitHub ignore the NSStreamEventHasSpaceAvailable event altogether, and just go ahead and -write:maxLength: to the buffer whenever they need to (even ignoring -hasSpaceAvailable).
Thirdly, there is this example code which appears to do both...
My question is therefore, what is the correct way(s) to handle writing data to an NSOutputStream that is attached to a socket? And of what use is the NSStreamEventHasSpaceAvailable event code if it can (apparently) be ignored? It seems to me that there is either very fortunate UB happening (in examples 2 and 3), or there are several ways of sending data through a socket-based NSOutputStream...
You can write to a stream at any time, but for network streams, -write:maxLength: returns only until at least one byte has been written to the socket write buffer. Therefore, if the socket write buffer is full (e.g. because the other end of the connection does not read the data fast enough),
this will block the current thread. If you write from the main thread, this will block
the user interface.
The NSStreamEventHasSpaceAvailable event is signalled when you can write to the stream
without blocking. Writing only in response to that event avoids that the current thread
and possibly the user interface is blocked.
Alternatively, you can write to a network stream from a separate "writer thread".
After seeing #MartinR's answer, I re-read the Apple Docs and did some reading up on NSRunLoop events. The solution was not as trivial as I first thought and requires some extra buffering.
Conclusions
While the Ray Wenderlich example works, it is not optimal - as noted by #MartinR, if there is no room in the outgoing TCP window, the call to write:maxLength will block. The reason Ray Wenderlich's example does work is because the messages sent are small and infrequent, and given an error-free and large-bandwidth internet connection, it will 'probably' work. When you start dealing with (much) larger amounts of data being sent (much) more frequently however, the write:maxLength: calls could start to block and the App will start to stall...
For the NSStreamEventHasSpaceAvailable event, Apple's documentation has the following advice:
If the delegate receives an NSStreamEventHasSpaceAvailable event and does not write anything to the stream, it does not receive further space-available events from the run loop until the NSOutputStream object receives more bytes. ... ... You can have the delegate set a flag when it doesn’t write to the stream upon receiving an NSStreamEventHasSpaceAvailable event. Later, when your program has more bytes to write, it can check this flag and, if set, write to the output-stream instance directly.
It is therefore only 'guaranteed to be safe' to call write:maxLength: in two scenarios:
Inside the callback (on receipt of the NSStreamEventHasSpaceAvailable event).
Outside the callback if and only if we have already received the NSStreamEventHasSpaceAvailable but elected not to call write:maxLength: inside the callback itself (e.g. we had no data to actually write).
For scenario (2), we will not receive the callback again until write:maxLength is actually called directly - Apple suggest setting a flag inside the delegate callback (see above) to indicate when we are allowed to do this.
My solution was to use an additional level of buffering - adding an NSMutableArray as a data queue. My code for writing data to a socket looks like this (comments and error checking omitted for brevity, the currentDataOffset variable indicates how much of the 'current' NSData object we have sent):
// Public interface for sending data.
- (void)sendData:(NSData *)data {
[_dataWriteQueue insertObject:data atIndex:0];
if (flag_canSendDirectly) [self _sendData];
}
// NSStreamDelegate message
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
// ...
case NSStreamEventHasSpaceAvailable: {
[self _sendData];
break;
}
}
// Private
- (void)_sendData {
flag_canSendDirectly = NO;
NSData *data = [_dataWriteQueue lastObject];
if (data == nil) {
flag_canSendDirectly = YES;
return;
}
uint8_t *readBytes = (uint8_t *)[data bytes];
readBytes += currentDataOffset;
NSUInteger dataLength = [data length];
NSUInteger lengthOfDataToWrite = (dataLength - currentDataOffset >= 1024) ? 1024 : (dataLength - currentDataOffset);
NSInteger bytesWritten = [_outputStream write:readBytes maxLength:lengthOfDataToWrite];
currentDataOffset += bytesWritten;
if (bytesWritten > 0) {
self.currentDataOffset += bytesWritten;
if (self.currentDataOffset == dataLength) {
[self.dataWriteQueue removeLastObject];
self.currentDataOffset = 0;
}
}
}
Background Info:
I've implemented a Bluetooth LE Peripheral for OSX which exposes two characteristics (using CoreBluetooth). One is readable, and one is writable (both with indications on). I've implemented a Bluetooth LE Central on iOS which will read from the readable characteristic and write to the writable characteristic. I've set it up so that every time the characteristic value is read, the value is updated (in a way similar to this example). The transfer rates I get with this set up are pathetically slow (topping out at a measured sustained speed of roughly 340 bytes / second). This speed is the actual data, and not a measure including the packet details, ACKs and so on.
Problem:
This sustained speed is too slow. I've considered two solutions:
There's some parameter in CoreBluetooth that I've missed that will help me increase the speed.
I'll need to implement a custom Bluetooth LE service using the IOBluetooth classes instead of CoreBluetooth.
I believe, I've exhausted option 1. I don't see any other parameters I can tweak. I'm limited to sending 20 bytes per message. Anything else and I get cryptic errors on the iOS device concerning Unknown Errors, Unlikely Errors, or the value being "Not Long". Since the demo project also indicates a 20 byte MTU, I'll accept that this likely isn't possible.
So I'm left with option 2. I'm trying to somehow modify the connection parameters for Bluetooth LE on OSX to hopefully allow me to increase the transfer speed (by setting the min and max conn intervals to be 20ms and 40ms respectively - as well as sending multiple BT packets per connection interval). It looks like providing my own SDP Service on IOBluetooth is the only way to achieve this on OSX. The problem with this is the documentation for how to do this is negligible to non-existent.
This tells me how to implement my own service (albeit using deprecate API), however, it doesn't explain the required parameters for registering an SDP service. So I'm left wondering:
Where can I find the required parameters for this dictionary?
How do I define these parameters in a way to offer a Bluetooth LE service?
Is there any alternative to providing a Bluetooth LE Peripheral on OSX via another framework (Python library? Linux in a VM with access to the Bluetooth stack? I'd like to avoid this altogether.)
I decided my best course of action was to attempt to use Linux in a VM as there is more documentation available and access to the source code would hopefully guarantee that I could find a solution. For anyone who is also facing this problem, here's how you can issue a Connection Parameter Update Request on OS X (sort of).
Step 1
Install a Linux VM. I used Virtual Box with Linux Mint 15 (64-bit Cinnamon).
Step 2
Allow usage of the OS X Bluetooth device in your VM. Attempting to forward the Bluetooth USB Controller to your VM will give an error message. To allow this, you need to stop everything that is using the controller. On my machine, that included issuing the following commands from the command line:
sudo launchctl unload /System/Library/LaunchDaemons/com.apple.blued.plist
This will kill the OS X Bluetooth daemon. Attempting to kill blued from the Activity Monitor will just cause it to be automatically relaunched.
sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport
On my MacBook, I've got a Broadcom controller and this is the kernel module that OS X uses for it. Don't worry about issuing these commands. To undo the changes, you can power down and reboot your machine (note, in some cases when playing with the BT controller and it got into a bad state, I had to actually leave the machine powered down for ~10 seconds before rebooting to clear volatile memory).
If after running these two commands you still can't mount the BT controller, you can run kextstat | grep Bluetooth and see other Bluetooth related kernel modules and then try to unload them as well. I've got ones named IOBluetoothFamily and IOBluetoothSerialManager that don't need to be unloaded.
Step 3
Launch your VM and get your Linux BT stack. I checked out the bluez Git repo from here. I specifically grabbed the 5.14 release tag using git checkout tags/5.14 just to be sure it was at least a tagged version and less likely to be broken. 5.14 is the newest tag as of writing this answer.
Step 4
Build bluez. This was done using bootstrap, then configure, then make and make install. I used the --prefix=/opt/bluez flag on configure to prevent overwriting the install bluetooth stack. Also, I used the --enable-maintainer-mode configure flag for the reason stated in the next step. You also might need to use --disable-systemd to get it to configure. Bluez has a bunch of tools and utilities you can use for various things. In order to use the built Bluetooth daemon, you need to stop the system daemon using sudo service bluetooth stop. You can then launch the built one using sudo /opt/bluez/libexec/bluetooth/bluetoothd -n -d (this launches in non-daemon mode with debug output).
Step 5
Get your LE service running via bluez. You can view the bluez/plugins/gatt-example.c for how to do this. I directly modified this by removing the unnecessary code and using the battery service code as a template for my own service and characteristics. You need to recompile bluez to have this code added to the bluetooth daemon. One thing to note (that caused my a day or two of trouble getting this working) was that iOS caches the GATT service listing and this is not read/refreshed on each connection. If you add a service or characteristic or change a UUID, you'll need to disable Bluetooth on your iOS device and then re-enable it. This is undocumented in Apples docs and there is no programmatic way to do it.
Step 6
Unfortunately, this is where things get tricky. Bluez doesn't have support built-in for issuing the Connection Parameters Update Request using any of its utilities. I had to write it myself. I'm currently seeing if they want my code to be included in the bluez stack. I can't post the code currently as I'd need to first see if the bluez devs are interested in the code and then get approval from my workplace to give the code. However, I can currently explain what I did to enable support.
Step 7
Prime yourself on the Bluetooth Standard. Any version 4.0 or greater will have the details you need. Read the following sections.
See Vol. 2, Part E, 4.1 for Host to Controller HCI flow.
See Vol. 2, Part E, 5.4.2 for HCI ACL Data Packet format.
See Vol. 3, Part A, 4 for Signalling Packet format.
See Vol. 3, Part A, 4.20 for Connection Parameter Update Request format.
You're basically going to need to write the code to format the packets and then write them to the hci device. The HCI ACL Data Packet header will contain 4 bytes. This is followed by 4 bytes for the Signalling command's length and channel id. This is then followed by your signal payload which in my case was 12 bytes (for the Connection Parameter Update Request).
You can then write them to the device similar to hci_send_cmd in bluez/lib/hci.c. I did each packet header as it's own struct and wrote them each as iovecs to the device. I put my new function in the hci.c file and exposed it with a function prototype in bluez/lib/hci_lib.h. I then modified bluez/tools/hcitool.c to allow me to call this method from the command line. In my case, I made it so that the command was nearly identical to the lecup command as it requires the same parameters (lecup can't be used as it's meant to be called on the master side, not the slave).
Recompiled all of this and then, voila, I can use my new command on hcitool to send the parameters to the bluetooth controller. After sending my command, it then re-negotiates with the iOS device as expected.
Comments
This process is not for the faint of heart. Hopefully, either this, or some other method of setting the connection parameters is added to bluez to simplify this process. Ideally, Apple will allow the ability to do so via CoreBluetooth or IOBluetooth at some point as well (it could be possible, but undocumentated / difficult to do so, I gave up with the Apple libraries). I've journeyed down the rabbit hole and learned much more about the Bluetooth Spec then I thought I'd have to to simply change the connection parameters between a MacBook and an iPhone. Hopefully this will be helpful to somebody at some point (even if it's me checking back on how I did this).
I know I've left out a lot of details in this in order to keep it somewhat brief (i.e. usage on the bluez tools). Please comment if something isn't clear.
If you are implementing your Peripheral using CoreBluetooth, you can request somewhat customized connection parameters by calling -[CBPeripheralManager setDesiredConnectionLatency:forCentral:] to Low, Medium, or High (where Low latency means higher bandwidth). The documentation does not specify what this means, so we have to test it ourselves.
On an OSX Peripheral, when you set the desired latency to Low, the interval is still 22.5ms which is far from the minimum of 7.5ms.
On OSX Yosemite 10.10.4, this is what the CBPeripheralManagerConnectionLatency values mean:
Low: Min Interval: 18 (22.5ms), Max Interval: 18 (22.5ms), Slave Latency: 4 events, Timeout: 200 (2s).
Medium: Min Interval: 32 (40ms), Max Interval: 32 (40ms), Slave Latency: 6 events, Timeout: 200 (2s)
High: Min Interval: 160 (200ms), Max Interval: 160 (200ms), Slave Latency: 2 events, Timeout: 300 (3s)
Here is the code that I used to run a CBPeripheralManager on OSX. I used an Android device as central using BLE Explorer and dumped the Bluetooth traffic to a Btsnoop file.
// clang main.m -framework Foundation -framework IOBluetooth
#import <Foundation/Foundation.h>
#import <IOBluetooth/IOBluetooth.h>
#interface MyPeripheralManagerDelegate: NSObject<CBPeripheralManagerDelegate>
#property (nonatomic, assign) CBPeripheralManager* peripheralManager;
#property (nonatomic) CBPeripheralManagerConnectionLatency nextLatency;
#end
#implementation MyPeripheralManagerDelegate
+ (NSString*)stringFromCBPeripheralManagerState:(CBPeripheralManagerState)state {
switch (state) {
case CBPeripheralManagerStatePoweredOff: return #"PoweredOff";
case CBPeripheralManagerStatePoweredOn: return #"PoweredOn";
case CBPeripheralManagerStateResetting: return #"Resetting";
case CBPeripheralManagerStateUnauthorized: return #"Unauthorized";
case CBPeripheralManagerStateUnknown: return #"Unknown";
case CBPeripheralManagerStateUnsupported: return #"Unsupported";
}
}
+ (CBUUID*)LatencyCharacteristicUuid {
return [CBUUID UUIDWithString:#"B81672D5-396B-4803-82C2-029D34319015"];
}
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
NSLog(#"CBPeripheralManager entered state %#", [MyPeripheralManagerDelegate stringFromCBPeripheralManagerState:peripheral.state]);
if (peripheral.state == CBPeripheralManagerStatePoweredOn) {
NSDictionary* dict = #{CBAdvertisementDataLocalNameKey: #"ConnLatencyTest"};
// Generated with uuidgen
CBUUID *serviceUuid = [CBUUID UUIDWithString:#"7AE48DEE-2597-4B4D-904E-A3E8C7735738"];
CBMutableService* service = [[CBMutableService alloc] initWithType:serviceUuid primary:TRUE];
// value:nil makes it a dynamic-valued characteristic
CBMutableCharacteristic* latencyCharacteristic = [[CBMutableCharacteristic alloc] initWithType:MyPeripheralManagerDelegate.LatencyCharacteristicUuid properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable];
service.characteristics = #[latencyCharacteristic];
[self.peripheralManager addService:service];
[self.peripheralManager startAdvertising:dict];
NSLog(#"startAdvertising. isAdvertising: %d", self.peripheralManager.isAdvertising);
}
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(NSError *)error {
if (error) {
NSLog(#"Error advertising: %#", [error localizedDescription]);
}
NSLog(#"peripheralManagerDidStartAdvertising %d", self.peripheralManager.isAdvertising);
}
+ (CBPeripheralManagerConnectionLatency) nextLatencyAfter:(CBPeripheralManagerConnectionLatency)latency {
switch (latency) {
case CBPeripheralManagerConnectionLatencyLow: return CBPeripheralManagerConnectionLatencyMedium;
case CBPeripheralManagerConnectionLatencyMedium: return CBPeripheralManagerConnectionLatencyHigh;
case CBPeripheralManagerConnectionLatencyHigh: return CBPeripheralManagerConnectionLatencyLow;
}
}
+ (NSString*)describeLatency:(CBPeripheralManagerConnectionLatency)latency {
switch (latency) {
case CBPeripheralManagerConnectionLatencyLow: return #"Low";
case CBPeripheralManagerConnectionLatencyMedium: return #"Medium";
case CBPeripheralManagerConnectionLatencyHigh: return #"High";
}
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqualTo:MyPeripheralManagerDelegate.LatencyCharacteristicUuid]) {
[self.peripheralManager setDesiredConnectionLatency:self.nextLatency forCentral:request.central];
NSString* description = [MyPeripheralManagerDelegate describeLatency: self.nextLatency];
request.value = [description dataUsingEncoding:NSUTF8StringEncoding];
[self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
NSLog(#"didReceiveReadRequest:latencyCharacteristic. Responding with %#", description);
self.nextLatency = [MyPeripheralManagerDelegate nextLatencyAfter:self.nextLatency];
} else {
NSLog(#"didReceiveReadRequest: (unknown) %#", request);
}
}
#end
int main(int argc, const char * argv[]) {
#autoreleasepool {
MyPeripheralManagerDelegate *peripheralManagerDelegate = [[MyPeripheralManagerDelegate alloc] init];
CBPeripheralManager* peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:peripheralManagerDelegate queue:nil];
peripheralManagerDelegate.peripheralManager = peripheralManager;
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
What is the best way to get notification while network interface becomes available/unavailable in iOS, different from Reachability? (maybe there is some good way of doing in SystemConfiguration or CFNetworks framework, or somehow using the native unix sockets networking API?)
Instead of checking network reachability I'm just checking if I'm able to get network info with this function, the only problem is when to check. I don't want to fire up an NSTimer every 0.1 second to do this, though it is solution, I would rather like to somehow get notified when user switches WiFi on/off in settings. (Reachability takes several seconds to nofity me when Im disabling wwan interface in settings.)
- (void)initializeCurrentNetworkInfo
{
NSArray* interfacesSupported = (__bridge_transfer NSArray*) CNCopySupportedInterfaces();
NSLog(#"%s: Supported interfaces: %#", __func__, interfacesSupported);
NSDictionary* interfaceInfo = nil;
for (NSString *interfaceName in interfacesSupported) {
interfaceInfo = (__bridge_transfer NSDictionary*) CNCopyCurrentNetworkInfo((__bridge_retained CFStringRef)interfaceName);
if (interfaceInfo && [interfaceInfo count]) {
self.isInterfaceActive = YES;
self.currentSSID = [NSString stringWithString:interfaceInfo[#"SSID"]];
self.currentBSSID = [NSString stringWithString:interfaceInfo[#"BSSID"]];
break;
} else if (!interfaceInfo){
self.isInterfaceActive = NO;
self.currentBSSID = #"ff:ff:ff:ff:ff:ff";;
self.currentSSID = #"interface is unavailable";
}
}
}
Info: I'm developing the UPNP application, and each time interface becomes available/unavailable I subsequently initialise or nullify my UPNP service object.
The problem is that in some rare occasions my app crashes, while I'm changing Wi-Fi switch on/off consequently for the purpose of testing. (Reachability works fine in the background)
So I could have traced it and found it that it obviously crashes while trying to receive an httpu datagram on socket of no longer available interface, when switching wifi off (recvLen = cg_socket_recv(sock, dgmPkt);), if I understand it right, it means that the background thread listening on socket in my UPNP framework (CyberGarages' CyberLink) is unaware of interface state changes(bug probably?), that's why I really want to stop the upnpService as fast as possible once the interface state changes.
I'm using the Tony Millions' Reachability version, and once I receive networkReachabilityChanged: I check for available interfaces, but I guess it's not the best way of doing it.
Thanks.