3

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);
}
Aadil Keshwani
  • 1,385
  • 1
  • 18
  • 29
Reid
  • 1,109
  • 1
  • 12
  • 27
  • Hello Reid, have you finally fought a solution? I have exactly the same problem, and you code works good sometimes, but not often. Have you tried a snippet below, it works? – Ivan Kozlov Sep 12 '14 at 15:43
  • Ivan, the project I needed this for was shelved so I never did actually figure it out. It doesn't seem like rocket science, but nothing I tried ever worked. It seems like your best bet is to go with the answer below. – Reid Sep 12 '14 at 20:54

1 Answers1

5

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);
}
Community
  • 1
  • 1
bradenm
  • 2,150
  • 1
  • 17
  • 10
  • Thanks very much for taking the time to answer! I actually never devised a workable solution to this, as I got transferred to other projects. Definitely bookmarking this for future reference! – Reid Jul 11 '14 at 16:16
  • @bradenm could you explain how did you calculate localDTimeOffset? – Ivan Kozlov Sep 12 '14 at 08:33
  • Just spent 3 hours trying to use this snippet. Could you provide an example of usage please? Thanks. – Ivan Kozlov Sep 12 '14 at 13:25
  • 3
    Ivan, the algorithm I use is proprietary. For a basic version, imagine you have two devices, A & B. A tells B "My DTime_A is 514", then B immediately tells A, "I received your message at DTime_B = 240". When A gets the reply from B, DTime_A will now be something like 530. So A estimates that B's reply was sent at DTime_A = (530+514)/2 = 522, and the offset is 522 - 240 = 282 s. – bradenm Sep 12 '14 at 17:50
  • Thank you. I'll try to implement it. – Ivan Kozlov Sep 12 '14 at 19:56
  • @bradenm can you tell me what is DTimeVal ? Is it custom class you have created ? Can you please guide ? – Aadil Keshwani May 14 '16 at 04:46
  • @bradenm can you please show demo of how to use `atExactDTime:(DTimeVal)val runBlock:(dispatch_block_t)block` method ? – Aadil Keshwani May 14 '16 at 05:04
  • @AadilKeshwani, DTimeVal is just `typedef int64_t DTimeVal;` and represents a synchronized time value, in nanoseconds from some arbitrary starting point. – bradenm May 14 '16 at 18:03
  • @bradenm Ok thanks but can you please show demo of how to use atExactDTime:(DTimeVal)val runBlock:(dispatch_block_t)block method ? – Aadil Keshwani May 16 '16 at 07:00
  • If the code from my answer is put into a class called `DTime` then: If `DTimeVal moment = 5000000000;` defines the exact time when you want the code to run, use `[DTime atExactDTime:moment runBlock:^{ /* the code that you want to run goes here */ }];` to execute code at that time. If you're asking how to get the value of `localDTimeOffset` which is required for these to be synchronized, I can't provide a detailed example, but you can use NTP or the outline of an approach I described above. – bradenm May 16 '16 at 22:54