Question

I've been working at this problem for a few days and none of my solutions have been adequate. I'm lacking the theoretical knowledge to make this happen, I think, and would love some advice (does not have to be iOS specific--I can translate C, pseudocode, whatever, into what I need).

Basically, I have two iPhones. Either one can trigger a repeating action when the user presses a button. It then needs to notify the other iPhone (via the MultiPeer framework) to trigger the same action...but they both need to start at the same instant and stay in step. I really need to get 1/100sec accuracy, which I think is achievable on this platform.

As a semi-rough gauge of how well in synch I am, I use AudioServices to play a "tick" sound on each device...you can very easily tell by ear how well in synch they are (ideally you would not be able to discern multiple sound sources).

Of course, I have to account for the MultiPeer latency somehow...and it's highly variable, anywhere from .1 sec to .8 sec in my testing.

Having found that the system clock is totally unreliable for my purposes, I found an iOS implementation of NTP and am using that. So I'm reasonably confident that the two phones have an accurate common reference for time (though I haven't figured out a way to test this assumption short of continuously displaying NTP time on both devices, which I do, and it seems nicely in synch to my eye).

What I was trying before was sending the "start time" with the P2P message, then (on the recipient end) subtracting that latency from a 1.5sec constant, and performing the action after that delay. On the sender end, I would simply wait for that constant to elapse and then perform the action. This didn't work at all. I was way off.

My next attempt was to wait, on both ends, for a whole second divisible by three, Since latency always seems to be <1sec, I thought this would work. I use the "delay" method to simply block the thread. It's a cudgel, I know, but I just want to get the timing working period before I worry about a more elegant solution. So, my "sender" (the device where the button is pressed) does this:

-(void)startActionAsSender
{
    [self notifyPeerToStartAction];
    [self delay];
    [self startAction];
}

And the recipient does this, in response to a delegate call:

-(void)peerDidStartAction
{
    [self delay];
    [self startAction];
}

My "delay" method looks like this:

-(void)delay
{
    NSDate *NTPTimeNow = [[NetworkClock sharedInstance] networkTime];
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components = [calendar components:NSSecondCalendarUnit 
    fromDate:NTPTimeNow];
    NSInteger seconds = [components second];

    // If this method gets called on a second divisible by three, wait a second...
    if (seconds % 3 == 0) { 
        sleep(1);
    }

    // Spinlock
    while (![self secondsDivideByThree]) {} 
}

-(BOOL)secondsDivideByThree
{
    NSDate *NTPTime = [[NetworkClock sharedInstance] networkTime];
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSInteger seconds = [[calendar components:NSSecondCalendarUnit fromDate:NTPTime] 
    second];

    return (seconds % 3 == 0);
}
Was it helpful?

Solution

This is old, so I hope you were able to get something working. I faced a very similar problem. In my case, I found that the inconsistency was almost entirely due to timer coalescing, which causes timers to be wrong by up to 10% on iOS devices in order to save battery usage.

For reference, here's a solution that I've been using in my own app. First, I use a simple custom protocol that's essentially a rudimentary NTP equivalent to synchronize a monotonically increasing clock between the two devices over the local network. I call this synchronized time "DTime" in the code below. With this code I'm able to tell all peers "perform action X at time Y", and it happens in sync.

+ (DTimeVal)getCurrentDTime
{
    DTimeVal baseTime = mach_absolute_time();
    // Convert from ticks to nanoseconds:
    static mach_timebase_info_data_t s_timebase_info;
    if (s_timebase_info.denom == 0) {
        mach_timebase_info(&s_timebase_info);
    }
    DTimeVal timeNanoSeconds = (baseTime * s_timebase_info.numer) / s_timebase_info.denom;
    return timeNanoSeconds + localDTimeOffset;
}

+ (void)atExactDTime:(DTimeVal)val runBlock:(dispatch_block_t)block
{
    // Use the most accurate timing possible to trigger an event at the specified DTime.
    // This is much more accurate than dispatch_after(...), which has a 10% "leeway" by default.
    // However, this method will use battery faster as it avoids most timer coalescing.
    // Use as little as necessary.
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, dispatch_get_main_queue());
    dispatch_source_set_event_handler(timer, ^{
        dispatch_source_cancel(timer); // one shot timer
        while (val - [self getCurrentDTime] > 1000) {
            // It is at least 1 microsecond too early...
            [NSThread sleepForTimeInterval:0.000001]; // Change this to zero for even better accuracy
        }
        block();
    });
    // Now, we employ a dirty trick:
    // Since even with DISPATCH_TIMER_STRICT there can be about 1ms of inaccuracy, we set the timer to
    // fire 1.3ms too early, then we use an until(time) { sleep(); } loop to delay until the exact time
    // that we wanted. This takes us from an accuracy of ~1ms to an accuracy of ~0.01ms, i.e. two orders
    // of magnitude improvement. However, of course the downside is that this will block the main thread
    // for 1.3ms.
    dispatch_time_t at_time = dispatch_time(DISPATCH_TIME_NOW, val - [self getCurrentDTime] - 1300000);
    dispatch_source_set_timer(timer, at_time, DISPATCH_TIME_FOREVER /*one shot*/, 0 /* minimal leeway */);
    dispatch_resume(timer);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top