I'm using the iOS 7 Multipeer framework in my app but I'm experiencing a problem with devices disconnecting. If I open the app in two devices: device A and device B the two devices connect to each other automatically. However, after several seconds device A disconnects from device B. i.e. At first the connection is like this:
A ---> B
A <--- B
After several seconds:
A ---> B
A B
Device A maintains it's connection but device B get's a MCSessionStateNotConnected.
This means that A can send data to B but B can't reply. I tried to get around this by checking if the device is connected and if it's not, re-initiating the connection using:
[browser invitePeer:peerID toSession:_session withContext:Nil timeout:10];
But the didChangeState callback just get's called with MCSessionStateNotConnected.
Strangely if I send app A to the background, then re-open it, B reconnects to it and the connection is maintained.
The Multipeer API (and documentation) seems a bit sparse so I was assuming that it would just work. In this situation how should I re-connect the device?
I was having the same problem, and it seems to have been related to my app browsing and advertising at the same time, and two invitations being sent/accepted. When I stopped doing this and let one peer defer to the other for invitations the devices stayed connected.
In my browser delegate I'm checking the hash value of the discovered peer's displayName and only sending an invitation if my peer has a higher hash value:
Edit
As pointed out by #Masa the hash value of an NSString will be different on 32 and 64 bit devices, so it's safer to use the compare: method on displayName.
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info {
NSLog(#"Browser found peer ID %#",peerID.displayName);
//displayName is created with [[NSUUID UUID] UUIDString]
BOOL shouldInvite = ([_myPeerID.displayName compare:peerID.displayName]==NSOrderedDescending);
if (shouldInvite){
[browser invitePeer:peerID toSession:_session withContext:nil timeout:1.0];
}
else {
NSLog(#"Not inviting");
}
}
As you say, the documentation is sparse so who knows what Apple really wants us to do, but I've experimented with both sending and accepting invitations using a single session, and also creating a new session for each invitation accepted/sent, but this particular way of doing things has given me the most success.
For anyone interested, I created MCSessionP2P, a demo app that illustrates the ad-hoc networking features of MCSession. The app both advertises itself on the local network and programmatically connects to available peers, establishing a peer-to-peer network. Hat tip to #ChrisH for his technique of comparing hash values for inviting peers.
I liked ChrisH's solution, which reveals the key insight that only one peer should connect to the other peer, not both. Mutual connection attempts results in mutual disconnection (though not that a single-sided connection actually is, counter-intuitively, a mutual connection in terms of status and communication, so that works fine).
However, I think a better approach than one peer inviting is for both peers to invite but only one peer to accept. I use this method now and it works great, because both peers have an opportunity to pass rich information to the other via the context parameter of the invitation, as opposed to having to rely on scant information available in the foundPeer delegate method.
Therefore, I recommend a solution like so:
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
[self invitePeer:peerID];
}
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL accept, MCSession *session))invitationHandler
{
NSDictionary *hugePackageOfInformation = [NSKeyedUnarchiver unarchiveObjectWithData:context];
BOOL shouldAccept = ([hugePackageOfInformation.UUID.UUIDString compare:self.user.UUID.UUIDString] == NSOrderedDescending);
invitationHandler(shouldAccept && ![self isPeerConnected:peerID], [self openSession]);
}
I have the same issue when devices trying to connect to each other at the same time and I don't know how to find a reason because we don't have any errors with MCSessionStateNotConnected.
We can use some crafty way to solve this issue:
Put into txt records ( discovery info ) a time [[NSDate date] timeIntervalSince1970] when app started. Who started first - send invitation to others.
But I think it's not a right way ( if apps start at the same time, unlikely... :) ). We need to figure out the reason.
This is the result of a bug, which I've reported to Apple. I've explained how to fix it in my response to another question: Why does my MCSession peer disconnect randomly?
I have not flagged these questions for merging, because while the underlying bug and solution are the same, the two questions describe different problems.
Save the hash of the peer B. Using a timer check the state of the connection continuously if is not connected try to reconnect with each given period of time.
According to apple document Choosing an inviter when using Multipeer Connectivity
“In iOS 7, sending simultaneous invites can cause both invites to fail, leaving both peers unable to communicate with each other.”
But iOS 8 has fixed it.
It seems that the .notConnected message is a false positive in that the device is still receiving data. So, I manually updated local connection state to .connected
It was hard to factor out other state from other examples. So, I wrote a bare bones MCSession example for SwiftUI, here: MultiPeer
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.
My setup is this: I have somewhere between 0 and 8 devices running at a time, and devices can be added or removed at any time.
I want to use the iOS 7 Multipeer framework to connect them. I have this working in a controlled environment, I can start 1-7 devices in advertiser mode, then start one in browser mode and they all link up.
What I'm unsure of is how I should know if the device needs to be in advertiser or browser mode when I start it? I've tried defaulting to advertiser mode for X seconds then switching to browser, the problem with this is that it's possible that all the devices started at the same time and turn off advertiser mode at the same time.
I've also considered running devices in both advertiser and browser mode, but the initial problem is that the device discovers itself. Also I believe I have 1 less device to connect this way.
I'm sure there's a recommended way to set this up but I've been unable to find anything that doesn't assume there's a set browser and advertiser, anyone have suggestions for this?
It is easy to make all devices both advertiser and browsers, and it's a normal behavior (I'm pretty sure there's a mention of this in the documentation, I'll search it and add the link later).
I haven't had problems with the device discovering itself, maybe you're creating two peerIDs for the same device?
You may like to check PLPartyTime's implementation of this. It just has a few simple checks to see if it needs to connect/accept a connection.
Advertiser Delegate
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser
didReceiveInvitationFromPeer:(MCPeerID *)peerID
withContext:(NSData *)context
invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler
{
// Only accept invitations with IDs lower than the current host
// If both people accept invitations, then connections are lost
// However, this should always be the case since we only send invites in one direction
if ([peerID.displayName compare:self.peerID.displayName] == NSOrderedDescending)
{
invitationHandler(YES, self.session);
}
}
Browser Delegate
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
// Whenever we find a peer, let's just send them an invitation
// But only send invites one way
// TODO: What if display names are the same?
// TODO: Make timeout configurable
if ([peerID.displayName compare:self.peerID.displayName] == NSOrderedAscending)
{
NSLog(#"Sending invite: Self: %#", self.peerID.displayName);
[browser invitePeer:peerID
toSession:self.session
withContext:nil
timeout:10];
}
}
You may also want to check out my fork, which is a little bit smaller and both browser and advertiser are separate objects.
I have made an iOS multiplayer GameCenter game, but right before publishing found an issue I don't know how to solve. In coding process I used Ray Wenderlich tutorial http://www.raywenderlich.com/3276/how-to-make-a-simple-multiplayer-game-with-game-center-tutorial-part-12
GameCenter view controller is shown, connection creates and game can be played until both devices are on the same Wifi network.
If I turn off Wifi on my phone and use 3G network, then try to start new game - in that case connection isn't made anymore. Both devices find each other, but hangs on "Connecting..." screen. It looks like that
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
is not called. Any ideas how to solve it or at least understand, where exactly is the problem?
I think that in your particular case the problem is that your 3G ISP restricts connections from necessary ports. The Apple docs say:
To use Game Center ... port forwarding must be enabled for ports 443 (TCP), 3478-3497 (UDP), 4398 (UDP), 5223 (TCP), 16384-16387 (UDP), and 16393-16472 (UDP)
I faced this issue too when trying to play on iPad connected via bluetooth to iPhone: there was "Connecting..." screen on each device.
But when I use built-in iPad 3G (with a different tariff plan) everything goes fine.
Just remind, in a normal match-making scenario match:player:didChangeState: may not be called. You should also check match.expectedPlayerCount:
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
//...
if (theMatch.expectedPlayerCount == 0) {
NSLog(#"Ready to start match!");
}
}
Also I expected a similar problem with "Connecting..." screen, but on Wifi network.
And it was reproduced only on iOS6 and after I tried rematch via -[GKMatch rematchWithCompletionHandler:^(GKMatch *match, NSError *error) {}] before.
One device hanged on "Connecting..." screen but on the other matchmakerViewController:didFindMatch: was successfully called, but what is interesting is that match.expectedPlayerCount was 0 and match.playerIDs array was empty at the same time.
I think such error occurred because I tried to find a new match while previous match tried to reconnect on background thread in the same time. And because of that new match was obtained corrupted.
The decision is to wait for rematchCompletion being called and only then try to find new match. There is no interface in GKMatch to cancel rematch, so I use [[GKMatchmaker sharedMatchmaker] cancel] and after several seconds rematchCompletion is called with error and we are ready to start finding new match.
Also I figured out that old unsued GKMatch instances are not deallocated and continue to live somewhere in GameKit framework. And they may probably cause problems if the work with them is not finished correctly (i.e. not disconnected, or rematch is not canceled in my case ). So do not forget to call -[GKMatch disconnect] and finish any other kind of work before removing the last strong reference to the match object .
I want to establish a bluetooth connection between 2 iPhones with a GKSession without GKPeerPickerController and without pushing any "connect button" on both sides.
I'm using the following code:
currentSessionAuto = [[GKSession alloc] initWithSessionID: #"instant-friend-auto"
currentSessionAuto.delegate = self;
currentSessionAuto.available = YES;
currentSessionAuto.disconnectTimeout = 5;
[currentSessionAuto setDataReceiveHandler: self withContext:nil];
When the application is starting on both sides, the
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state is called on both sides with the state "GKPeerStateAvailable".
With a "classic" app, a popup is displayed on both side to ask for connection and most of the time, both "users" does not click on the connect button on the same time.
If I want to have an "Automatic connection" I need a mechanism to only initiate the session on 1 side, because without this mechanism two sessions will be initiated and errors occur.
Any idea / help ?
Take a look at GKSessionP2P, a demo app that illustrates the ad-hoc networking features of GKSession. The app both advertises itself on the local network and automatically connects to available peers, establishing a peer-to-peer network.
Here's an idea: have the peer with the lowest peerID connect. You'll have to convert the PeerID string to an int and compare, but it should be a great tie breaker.
After more than a few hours of searching, I got in what looks like a dead end. In this case, all that I am trying to do, is to get all the iOS Devices of the network with Bonjour. I did so like this
self.serviceBrowser = [[NSNetServiceBrowser alloc] init];
[self.serviceBrowser setDelegate:self];
[self.serviceBrowser searchForServicesOfType:#"_apple-mobdev2._tcp." inDomain:#"local."];
This works fine, though what I get is the following:
local. _apple-mobdev2._tcp. [MAC ADDRESS HERE]
I tried to resolve the connection by using the sync port (62078), since service.port returns -1.
for (NSNetService *service in self.services) {
NSLog(#"%#", service);
NSNetService *newService = [[NSNetService alloc] initWithDomain:service.domain type:service.type name:service.name port:62078];
[newService setDelegate:self];
[newService resolveWithTimeout:30];
}
This in its own turn calls netServiceWillResolve: with no problem at all, but, it doesn't make it to netServiceDidResolveAddress:
But neither does this fail. netService:didNotResolve: isn't called either, I believe it is just waiting for a response to be resolved.
To support this claim, once it did make it to the method and actually [service hostName]; did return Yanniss-iPhone, but that happened at a completely random time that I had left the Mac App running for around half an hour. What could have invoked this to run? Or does anyone know of a different way to get the hostName of the remote device? The other answers do not answer my question, since I am looking for the hostName of the remote device, not of the Mac device.
Relative to that, I've found that when you kill and restart iTunes, along with iTunes Helper, the very log I mentioned below is sent again. Which is why I believe the correct log was an iTunes related event. Any help is very much appreciated!
iTunes search bonjour for wifi sync capability. As for the didNotResolve or resolve delay, bonjour services randomly cast itself anywhere between a few seconds to 30 minutes.
I am actually trying to connect to iOS devices too, but I could not get any response or any devices returned. :\