Reconnecting to disconnected peers
Asked Answered
S

8

30

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?

Subtractive answered 19/10, 2013 at 19:12 Comment(5)
Is it a local or physical problem? Have you tried to do some tracepath between each B to A?Alyciaalyda
I'm pretty sure it's not a physical problem since I've been able to get a stable bluetooth connection manually using DNS-SD and CFSockets. It seems to be a MultiPeer problem.Subtractive
Ah Sorry, I thought it was remotely with internet, but it's bluetooh!Alyciaalyda
Are you browsing and advertising at the same time? Do A and B both invite and accept?Athanor
Yes - just wanted to check you were in the same boat as me before I offered an answer.Athanor
A
24

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.

Athanor answered 22/10, 2013 at 23:8 Comment(10)
Very helpful, thanks. Note that hash on NSString returns an NSUInteger. The results are therefore different on 32 and 64 bit systems. So I just had the case that no one was sending invites. I solved it by simply doing a string compare on the displayNames instead of using hash. (Apart from that, note also that hash on NSString should be used with care: abakia.de/blog/2012/12/05/nsstring-hash-is-bad)Closegrained
@Closegrained that's interesting, thanks. I'll definitely switch to using a string compare instead.Athanor
isn't there a risk that devices have the same display name? what if I have 2 iphones names 'Joon's iPhone' ?Ellerd
@Ellerd the onus there is on you to make sure you are creating unique peer IDs, which they should always be.Athanor
what's the best practice for this? The demo's I've seen just use device name which is not unique afaik.Ellerd
[[NSUUID UUID] UUIDString] will produce a different string regardless of the device nameAthanor
Beware of comparing the displayNames, by default all iPads are named "Users' iPad". Lesser problem with iPhones since the same user tends to only have one. Still high risk of collision.Catenane
Using hashes, as suggested on other threads (if myPeerId.hash > peerID.hash) may not be enough since the result may be different on two terminals (iPhone 7 in my case) running the same OS version (iOS 12.0.1). I have a situation where myPeerId.hash is lower than peerID.hash on both devices and then none send the invitation?Eustace
This worked for me. Here is how did my shouldInvite boolean. let shouldInvite = self.peerID.displayName < peerID.displayNameLancer
for the unique name, I used this UIDevice.modelName + " \(UIDevice.current.identifierForVendor?.uuidString.suffix(8) ?? "")" and model name is here https://mcmap.net/q/44868/-how-to-determine-the-current-iphone-device-model You can hide the UUID from the end to show just the device name.Saeger
F
5

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.

Fawn answered 3/12, 2013 at 19:47 Comment(1)
Nice starting point for someone currently using MCBrowserViewController and MCAdvertiserAssistantAthanor
C
4

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]);
}
Capella answered 4/2, 2014 at 21:52 Comment(0)
H
3

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.

Houdan answered 21/10, 2013 at 17:4 Comment(2)
A better solution is to pass information in the invitation context parameter. Since both devices can invite each other fine, but only one can accept, you can do the discerning over who should accept in the received-invitation delegate method using this context, instead of who should invite in the found-peer delegate method.Capella
I would argue that @SG1's approach is less desirable since it wastes bandwidth.Lhary
M
2

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.

Mcentire answered 31/10, 2013 at 0:26 Comment(2)
This seems to be the missing piece of the puzzle, at least for me. Thanks!Athanor
Even with the certificate handler, I'm still getting disconnectsChuch
E
1

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.

Eijkman answered 31/10, 2013 at 15:49 Comment(4)
– invitePeer:toSession:withContext:timeout: yesEijkman
I tried that and it just sends a PeerNotConnected to the status delegate.Subtractive
What are you passing as a serviceType when looking for the device the first time?Eijkman
A timer isn't necessary. Once you've connected, you will ever-after receive delegate methods when the state changes. If it becomes disconnected then try and reconnect after the timeout period for connection has expired, if the peer has not become connected in the interim. @Ben, this is the same solution to your problem.Capella
K
0

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.

Kristenkristi answered 24/2, 2017 at 7:45 Comment(0)
W
0

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

Wynne answered 5/12, 2022 at 3:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.