xref: /aosp_15_r20/external/walt/ios/WALT/WALTClient.m (revision bf47c6829f95be9dd55f4c5bbc44a71c90aad403)
1*bf47c682SAndroid Build Coastguard Worker/*
2*bf47c682SAndroid Build Coastguard Worker * Copyright (C) 2016 The Android Open Source Project
3*bf47c682SAndroid Build Coastguard Worker *
4*bf47c682SAndroid Build Coastguard Worker * Licensed under the Apache License, Version 2.0 (the "License");
5*bf47c682SAndroid Build Coastguard Worker * you may not use this file except in compliance with the License.
6*bf47c682SAndroid Build Coastguard Worker * You may obtain a copy of the License at
7*bf47c682SAndroid Build Coastguard Worker *
8*bf47c682SAndroid Build Coastguard Worker *      http://www.apache.org/licenses/LICENSE-2.0
9*bf47c682SAndroid Build Coastguard Worker *
10*bf47c682SAndroid Build Coastguard Worker * Unless required by applicable law or agreed to in writing, software
11*bf47c682SAndroid Build Coastguard Worker * distributed under the License is distributed on an "AS IS" BASIS,
12*bf47c682SAndroid Build Coastguard Worker * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*bf47c682SAndroid Build Coastguard Worker * See the License for the specific language governing permissions and
14*bf47c682SAndroid Build Coastguard Worker * limitations under the License.
15*bf47c682SAndroid Build Coastguard Worker */
16*bf47c682SAndroid Build Coastguard Worker
17*bf47c682SAndroid Build Coastguard Worker#import "WALTClient.h"
18*bf47c682SAndroid Build Coastguard Worker
19*bf47c682SAndroid Build Coastguard Worker#include <ctype.h>
20*bf47c682SAndroid Build Coastguard Worker#include <dispatch/dispatch.h>
21*bf47c682SAndroid Build Coastguard Worker#include <mach/clock.h>
22*bf47c682SAndroid Build Coastguard Worker#include <mach/mach.h>
23*bf47c682SAndroid Build Coastguard Worker#include <mach/mach_host.h>
24*bf47c682SAndroid Build Coastguard Worker#include <stdlib.h>
25*bf47c682SAndroid Build Coastguard Worker#include <time.h>
26*bf47c682SAndroid Build Coastguard Worker
27*bf47c682SAndroid Build Coastguard Worker#import "MIDIEndpoint.h"
28*bf47c682SAndroid Build Coastguard Worker#import "MIDIMessage.h"
29*bf47c682SAndroid Build Coastguard Worker
30*bf47c682SAndroid Build Coastguard WorkerNSString * const WALTClientErrorDomain = @"WALTClientErrorDomain";
31*bf47c682SAndroid Build Coastguard Worker
32*bf47c682SAndroid Build Coastguard Workerstatic NSString * const kWALTVersion = @"v 4";
33*bf47c682SAndroid Build Coastguard Worker
34*bf47c682SAndroid Build Coastguard Workerstatic const MIDIChannel kWALTChannel = 1;
35*bf47c682SAndroid Build Coastguard Workerstatic const MIDIByte kWALTSerialOverMIDIProgram = 1;
36*bf47c682SAndroid Build Coastguard Workerstatic const MIDIMessageType kWALTCommandType = MIDIMessageChannelPressure;
37*bf47c682SAndroid Build Coastguard Worker
38*bf47c682SAndroid Build Coastguard Workerconst NSTimeInterval kWALTReadTimeout = 0.2;
39*bf47c682SAndroid Build Coastguard Workerstatic const NSTimeInterval kWALTDuplicateTimeout = 0.01;
40*bf47c682SAndroid Build Coastguard Worker
41*bf47c682SAndroid Build Coastguard Workerstatic const int kWALTSyncIterations = 7;
42*bf47c682SAndroid Build Coastguard Worker#define kWALTSyncDigitMax 9  // #define to avoid variable length array warnings.
43*bf47c682SAndroid Build Coastguard Worker
44*bf47c682SAndroid Build Coastguard Worker/** Similar to atoll(), but only reads a maximum of n characters from s. */
45*bf47c682SAndroid Build Coastguard Workerstatic unsigned long long antoull(const char *s, size_t n) {
46*bf47c682SAndroid Build Coastguard Worker  unsigned long long result = 0;
47*bf47c682SAndroid Build Coastguard Worker  while (s && n-- && *s && isdigit(*s)) {
48*bf47c682SAndroid Build Coastguard Worker    result = result * 10 + (*s - '0');
49*bf47c682SAndroid Build Coastguard Worker    ++s;
50*bf47c682SAndroid Build Coastguard Worker  }
51*bf47c682SAndroid Build Coastguard Worker  return result;
52*bf47c682SAndroid Build Coastguard Worker}
53*bf47c682SAndroid Build Coastguard Worker
54*bf47c682SAndroid Build Coastguard Worker/** Converts a mach_timespec_t to its equivalent number of microseconds. */
55*bf47c682SAndroid Build Coastguard Workerstatic int64_t TimespecToMicroseconds(const mach_timespec_t ts) {
56*bf47c682SAndroid Build Coastguard Worker  return ((int64_t)ts.tv_sec) * USEC_PER_SEC + ts.tv_nsec / NSEC_PER_USEC;
57*bf47c682SAndroid Build Coastguard Worker}
58*bf47c682SAndroid Build Coastguard Worker
59*bf47c682SAndroid Build Coastguard Worker/** Returns the current time (in microseconds) on a clock. */
60*bf47c682SAndroid Build Coastguard Workerstatic int64_t CurrentTime(clock_serv_t clock) {
61*bf47c682SAndroid Build Coastguard Worker  mach_timespec_t time = {0};
62*bf47c682SAndroid Build Coastguard Worker  clock_get_time(clock, &time);
63*bf47c682SAndroid Build Coastguard Worker  return TimespecToMicroseconds(time);
64*bf47c682SAndroid Build Coastguard Worker}
65*bf47c682SAndroid Build Coastguard Worker
66*bf47c682SAndroid Build Coastguard Worker/** Sleeps the current thread for us microseconds. */
67*bf47c682SAndroid Build Coastguard Workerstatic void Sleep(int64_t us) {
68*bf47c682SAndroid Build Coastguard Worker  const struct timespec ts = {
69*bf47c682SAndroid Build Coastguard Worker    .tv_sec = (long)(us / USEC_PER_SEC),
70*bf47c682SAndroid Build Coastguard Worker    .tv_nsec = (us % USEC_PER_SEC) * NSEC_PER_USEC,
71*bf47c682SAndroid Build Coastguard Worker  };
72*bf47c682SAndroid Build Coastguard Worker  nanosleep(&ts, NULL);
73*bf47c682SAndroid Build Coastguard Worker}
74*bf47c682SAndroid Build Coastguard Worker
75*bf47c682SAndroid Build Coastguard Worker@interface WALTClient ()
76*bf47c682SAndroid Build Coastguard Worker@property (readwrite, nonatomic, getter=isConnected) BOOL connected;
77*bf47c682SAndroid Build Coastguard Worker
78*bf47c682SAndroid Build Coastguard Worker- (void)drainResponseQueue;
79*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveSyncBoundsWithError:(NSError **)error;
80*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveMinBoundWithError:(NSError **)error;
81*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveMaxBoundWithError:(NSError **)error;
82*bf47c682SAndroid Build Coastguard Worker- (BOOL)readRemoteTimestamps:(uint64_t[kWALTSyncDigitMax])times error:(NSError **)error;
83*bf47c682SAndroid Build Coastguard Worker- (WALTTrigger)readTrigger:(NSData *)response;
84*bf47c682SAndroid Build Coastguard Worker@end
85*bf47c682SAndroid Build Coastguard Worker
86*bf47c682SAndroid Build Coastguard Worker@implementation WALTClient {
87*bf47c682SAndroid Build Coastguard Worker  MIDIClient *_client;
88*bf47c682SAndroid Build Coastguard Worker
89*bf47c682SAndroid Build Coastguard Worker  // Responses from the MIDIClient are queued up here with a signal to the semaphore.
90*bf47c682SAndroid Build Coastguard Worker  NSMutableArray<NSData *> *_responseQueue;  // TODO(pquinn): Lock-free circular buffer?
91*bf47c682SAndroid Build Coastguard Worker  dispatch_semaphore_t _responseSemaphore;
92*bf47c682SAndroid Build Coastguard Worker
93*bf47c682SAndroid Build Coastguard Worker  BOOL _syncCompleted;
94*bf47c682SAndroid Build Coastguard Worker
95*bf47c682SAndroid Build Coastguard Worker  clock_serv_t _clock;
96*bf47c682SAndroid Build Coastguard Worker
97*bf47c682SAndroid Build Coastguard Worker  NSData *_lastData;
98*bf47c682SAndroid Build Coastguard Worker  NSTimeInterval _lastDataTimestamp;
99*bf47c682SAndroid Build Coastguard Worker
100*bf47c682SAndroid Build Coastguard Worker  struct {
101*bf47c682SAndroid Build Coastguard Worker    // All microseconds.
102*bf47c682SAndroid Build Coastguard Worker    int64_t base;
103*bf47c682SAndroid Build Coastguard Worker    int64_t minError;
104*bf47c682SAndroid Build Coastguard Worker    int64_t maxError;
105*bf47c682SAndroid Build Coastguard Worker  } _sync;
106*bf47c682SAndroid Build Coastguard Worker}
107*bf47c682SAndroid Build Coastguard Worker
108*bf47c682SAndroid Build Coastguard Worker- (instancetype)initWithError:(NSError **)error {
109*bf47c682SAndroid Build Coastguard Worker  if ((self = [super init])) {
110*bf47c682SAndroid Build Coastguard Worker    _responseQueue = [[NSMutableArray<NSData *> alloc] init];
111*bf47c682SAndroid Build Coastguard Worker    _responseSemaphore = dispatch_semaphore_create(0);
112*bf47c682SAndroid Build Coastguard Worker
113*bf47c682SAndroid Build Coastguard Worker    // NB: It's important that this is the same clock used as the base for UIEvent's -timestamp.
114*bf47c682SAndroid Build Coastguard Worker    kern_return_t result = host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &_clock);
115*bf47c682SAndroid Build Coastguard Worker
116*bf47c682SAndroid Build Coastguard Worker    if (result != KERN_SUCCESS || ![self checkConnectionWithError:error]) {
117*bf47c682SAndroid Build Coastguard Worker      self = nil;
118*bf47c682SAndroid Build Coastguard Worker    }
119*bf47c682SAndroid Build Coastguard Worker  }
120*bf47c682SAndroid Build Coastguard Worker  return self;
121*bf47c682SAndroid Build Coastguard Worker}
122*bf47c682SAndroid Build Coastguard Worker
123*bf47c682SAndroid Build Coastguard Worker- (void)dealloc {
124*bf47c682SAndroid Build Coastguard Worker  [self drainResponseQueue];
125*bf47c682SAndroid Build Coastguard Worker  mach_port_deallocate(mach_task_self(), _clock);
126*bf47c682SAndroid Build Coastguard Worker}
127*bf47c682SAndroid Build Coastguard Worker
128*bf47c682SAndroid Build Coastguard Worker// Ensure only one KVO notification is sent when the connection state is changed.
129*bf47c682SAndroid Build Coastguard Worker+ (BOOL)automaticallyNotifiesObserversOfConnected {
130*bf47c682SAndroid Build Coastguard Worker  return NO;
131*bf47c682SAndroid Build Coastguard Worker}
132*bf47c682SAndroid Build Coastguard Worker
133*bf47c682SAndroid Build Coastguard Worker- (void)setConnected:(BOOL)connected {
134*bf47c682SAndroid Build Coastguard Worker  if (_connected != connected) {
135*bf47c682SAndroid Build Coastguard Worker    [self willChangeValueForKey:@"connected"];
136*bf47c682SAndroid Build Coastguard Worker    _connected = connected;
137*bf47c682SAndroid Build Coastguard Worker    [self didChangeValueForKey:@"connected"];
138*bf47c682SAndroid Build Coastguard Worker  }
139*bf47c682SAndroid Build Coastguard Worker}
140*bf47c682SAndroid Build Coastguard Worker
141*bf47c682SAndroid Build Coastguard Worker- (BOOL)checkConnectionWithError:(NSError **)error {
142*bf47c682SAndroid Build Coastguard Worker  if (_client.source.isOnline && _client.destination.isOnline && _syncCompleted) {
143*bf47c682SAndroid Build Coastguard Worker    self.connected = YES;
144*bf47c682SAndroid Build Coastguard Worker    return YES;  // Everything's fine.
145*bf47c682SAndroid Build Coastguard Worker  }
146*bf47c682SAndroid Build Coastguard Worker
147*bf47c682SAndroid Build Coastguard Worker  _syncCompleted = NO;  // Reset the sync state.
148*bf47c682SAndroid Build Coastguard Worker  [self drainResponseQueue];
149*bf47c682SAndroid Build Coastguard Worker
150*bf47c682SAndroid Build Coastguard Worker  // Create a new client.
151*bf47c682SAndroid Build Coastguard Worker  // This probably isn't strictly necessary, but solves some of the flakiness on iOS.
152*bf47c682SAndroid Build Coastguard Worker  _client.delegate = nil;
153*bf47c682SAndroid Build Coastguard Worker  _client = [[MIDIClient alloc] initWithName:@"WALT" error:error];
154*bf47c682SAndroid Build Coastguard Worker  _client.delegate = self;
155*bf47c682SAndroid Build Coastguard Worker  if (!_client) {
156*bf47c682SAndroid Build Coastguard Worker    self.connected = NO;
157*bf47c682SAndroid Build Coastguard Worker    return NO;
158*bf47c682SAndroid Build Coastguard Worker  }
159*bf47c682SAndroid Build Coastguard Worker
160*bf47c682SAndroid Build Coastguard Worker  if (!_client.source.isOnline) {
161*bf47c682SAndroid Build Coastguard Worker    // Try to connect to the first available input source.
162*bf47c682SAndroid Build Coastguard Worker    // TODO(pquinn): Make this user-configurable.
163*bf47c682SAndroid Build Coastguard Worker    NSArray<MIDISource *> *sources = [MIDISource allSources];
164*bf47c682SAndroid Build Coastguard Worker    if (sources.count) {
165*bf47c682SAndroid Build Coastguard Worker      if (![_client connectToSource:sources.firstObject error:error]) {
166*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
167*bf47c682SAndroid Build Coastguard Worker        return NO;
168*bf47c682SAndroid Build Coastguard Worker      }
169*bf47c682SAndroid Build Coastguard Worker    }
170*bf47c682SAndroid Build Coastguard Worker  }
171*bf47c682SAndroid Build Coastguard Worker
172*bf47c682SAndroid Build Coastguard Worker  if (!_client.destination.isOnline) {
173*bf47c682SAndroid Build Coastguard Worker    // Try to connect to the first available input source.
174*bf47c682SAndroid Build Coastguard Worker    // TODO(pquinn): Make this user-configurable.
175*bf47c682SAndroid Build Coastguard Worker    NSArray<MIDIDestination *> *destinations = [MIDIDestination allDestinations];
176*bf47c682SAndroid Build Coastguard Worker    if (destinations.count) {
177*bf47c682SAndroid Build Coastguard Worker      if (![_client connectToDestination:destinations.firstObject error:error]) {
178*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
179*bf47c682SAndroid Build Coastguard Worker        return NO;
180*bf47c682SAndroid Build Coastguard Worker      }
181*bf47c682SAndroid Build Coastguard Worker    }
182*bf47c682SAndroid Build Coastguard Worker
183*bf47c682SAndroid Build Coastguard Worker    if (_client.destination.isOnline) {
184*bf47c682SAndroid Build Coastguard Worker      // Switch to Serial-over-MIDI mode.
185*bf47c682SAndroid Build Coastguard Worker      NSData *message = MIDIMessageCreateSimple1(MIDIMessageProgramChange,
186*bf47c682SAndroid Build Coastguard Worker                                                 kWALTChannel,
187*bf47c682SAndroid Build Coastguard Worker                                                 kWALTSerialOverMIDIProgram);
188*bf47c682SAndroid Build Coastguard Worker      if (![_client sendData:message error:error]) {
189*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
190*bf47c682SAndroid Build Coastguard Worker        return NO;
191*bf47c682SAndroid Build Coastguard Worker      }
192*bf47c682SAndroid Build Coastguard Worker
193*bf47c682SAndroid Build Coastguard Worker      // Make sure it's using a known protocol version.
194*bf47c682SAndroid Build Coastguard Worker      message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, WALTVersionCommand);
195*bf47c682SAndroid Build Coastguard Worker      if (![_client sendData:message error:error]) {
196*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
197*bf47c682SAndroid Build Coastguard Worker        return NO;
198*bf47c682SAndroid Build Coastguard Worker      }
199*bf47c682SAndroid Build Coastguard Worker
200*bf47c682SAndroid Build Coastguard Worker      NSData *response = [self readResponseWithTimeout:kWALTReadTimeout];
201*bf47c682SAndroid Build Coastguard Worker      NSString *versionString = [[NSString alloc] initWithData:response
202*bf47c682SAndroid Build Coastguard Worker                                                      encoding:NSASCIIStringEncoding];
203*bf47c682SAndroid Build Coastguard Worker      if (![versionString isEqualToString:kWALTVersion]) {
204*bf47c682SAndroid Build Coastguard Worker        if (error) {
205*bf47c682SAndroid Build Coastguard Worker          *error = [NSError errorWithDomain:WALTClientErrorDomain
206*bf47c682SAndroid Build Coastguard Worker                                       code:0
207*bf47c682SAndroid Build Coastguard Worker                                   userInfo:@{NSLocalizedDescriptionKey:
208*bf47c682SAndroid Build Coastguard Worker                                                [@"Unknown WALT version: "
209*bf47c682SAndroid Build Coastguard Worker                                                  stringByAppendingString:versionString]}];
210*bf47c682SAndroid Build Coastguard Worker        }
211*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
212*bf47c682SAndroid Build Coastguard Worker        return NO;
213*bf47c682SAndroid Build Coastguard Worker      }
214*bf47c682SAndroid Build Coastguard Worker
215*bf47c682SAndroid Build Coastguard Worker      if (![self syncClocksWithError:error]) {
216*bf47c682SAndroid Build Coastguard Worker        self.connected = NO;
217*bf47c682SAndroid Build Coastguard Worker        return NO;
218*bf47c682SAndroid Build Coastguard Worker      }
219*bf47c682SAndroid Build Coastguard Worker
220*bf47c682SAndroid Build Coastguard Worker      _syncCompleted = YES;
221*bf47c682SAndroid Build Coastguard Worker    }
222*bf47c682SAndroid Build Coastguard Worker  }
223*bf47c682SAndroid Build Coastguard Worker
224*bf47c682SAndroid Build Coastguard Worker  self.connected = (_client.source.isOnline && _client.destination.isOnline && _syncCompleted);
225*bf47c682SAndroid Build Coastguard Worker  return YES;
226*bf47c682SAndroid Build Coastguard Worker}
227*bf47c682SAndroid Build Coastguard Worker
228*bf47c682SAndroid Build Coastguard Worker#pragma mark - Clock Synchronisation
229*bf47c682SAndroid Build Coastguard Worker
230*bf47c682SAndroid Build Coastguard Worker- (BOOL)syncClocksWithError:(NSError **)error {
231*bf47c682SAndroid Build Coastguard Worker  _sync.base = CurrentTime(_clock);
232*bf47c682SAndroid Build Coastguard Worker
233*bf47c682SAndroid Build Coastguard Worker  if (![self sendCommand:WALTZeroSyncCommand error:error]) {
234*bf47c682SAndroid Build Coastguard Worker    return NO;
235*bf47c682SAndroid Build Coastguard Worker  }
236*bf47c682SAndroid Build Coastguard Worker
237*bf47c682SAndroid Build Coastguard Worker  NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
238*bf47c682SAndroid Build Coastguard Worker  if (![self checkResponse:data forCommand:WALTZeroSyncCommand]) {
239*bf47c682SAndroid Build Coastguard Worker    if (error) {
240*bf47c682SAndroid Build Coastguard Worker      *error = [NSError errorWithDomain:WALTClientErrorDomain
241*bf47c682SAndroid Build Coastguard Worker                                   code:0
242*bf47c682SAndroid Build Coastguard Worker                               userInfo:@{NSLocalizedDescriptionKey:
243*bf47c682SAndroid Build Coastguard Worker          [NSString stringWithFormat:@"Bad acknowledgement for WALTZeroSyncCommand: %@", data]}];
244*bf47c682SAndroid Build Coastguard Worker    }
245*bf47c682SAndroid Build Coastguard Worker    return NO;
246*bf47c682SAndroid Build Coastguard Worker  }
247*bf47c682SAndroid Build Coastguard Worker
248*bf47c682SAndroid Build Coastguard Worker  _sync.maxError = CurrentTime(_clock) - _sync.base;
249*bf47c682SAndroid Build Coastguard Worker  _sync.minError = 0;
250*bf47c682SAndroid Build Coastguard Worker
251*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncIterations; ++i) {
252*bf47c682SAndroid Build Coastguard Worker    if (![self improveSyncBoundsWithError:error]) {
253*bf47c682SAndroid Build Coastguard Worker      return NO;
254*bf47c682SAndroid Build Coastguard Worker    }
255*bf47c682SAndroid Build Coastguard Worker  }
256*bf47c682SAndroid Build Coastguard Worker
257*bf47c682SAndroid Build Coastguard Worker  // Shift the time base so minError == 0
258*bf47c682SAndroid Build Coastguard Worker  _sync.base += _sync.minError;
259*bf47c682SAndroid Build Coastguard Worker  _sync.maxError -= _sync.minError;
260*bf47c682SAndroid Build Coastguard Worker  _sync.minError = 0;
261*bf47c682SAndroid Build Coastguard Worker  return YES;
262*bf47c682SAndroid Build Coastguard Worker}
263*bf47c682SAndroid Build Coastguard Worker
264*bf47c682SAndroid Build Coastguard Worker- (BOOL)updateSyncBoundsWithError:(NSError **)error {
265*bf47c682SAndroid Build Coastguard Worker  // Reset the bounds to unrealistic values
266*bf47c682SAndroid Build Coastguard Worker  _sync.minError = -1e7;
267*bf47c682SAndroid Build Coastguard Worker  _sync.maxError =  1e7;
268*bf47c682SAndroid Build Coastguard Worker
269*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncIterations; ++i) {
270*bf47c682SAndroid Build Coastguard Worker    if (![self improveSyncBoundsWithError:error]) {
271*bf47c682SAndroid Build Coastguard Worker      return NO;
272*bf47c682SAndroid Build Coastguard Worker    }
273*bf47c682SAndroid Build Coastguard Worker  }
274*bf47c682SAndroid Build Coastguard Worker
275*bf47c682SAndroid Build Coastguard Worker  return YES;
276*bf47c682SAndroid Build Coastguard Worker}
277*bf47c682SAndroid Build Coastguard Worker
278*bf47c682SAndroid Build Coastguard Worker- (int64_t)minError {
279*bf47c682SAndroid Build Coastguard Worker  return _sync.minError;
280*bf47c682SAndroid Build Coastguard Worker}
281*bf47c682SAndroid Build Coastguard Worker
282*bf47c682SAndroid Build Coastguard Worker- (int64_t)maxError {
283*bf47c682SAndroid Build Coastguard Worker  return _sync.maxError;
284*bf47c682SAndroid Build Coastguard Worker}
285*bf47c682SAndroid Build Coastguard Worker
286*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveSyncBoundsWithError:(NSError **)error {
287*bf47c682SAndroid Build Coastguard Worker  return ([self improveMinBoundWithError:error] && [self improveMaxBoundWithError:error]);
288*bf47c682SAndroid Build Coastguard Worker}
289*bf47c682SAndroid Build Coastguard Worker
290*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveMinBoundWithError:(NSError **)error {
291*bf47c682SAndroid Build Coastguard Worker  if (![self sendCommand:WALTResetCommand error:error]) {
292*bf47c682SAndroid Build Coastguard Worker    return NO;
293*bf47c682SAndroid Build Coastguard Worker  }
294*bf47c682SAndroid Build Coastguard Worker
295*bf47c682SAndroid Build Coastguard Worker  NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
296*bf47c682SAndroid Build Coastguard Worker  if (![self checkResponse:data forCommand:WALTResetCommand]) {
297*bf47c682SAndroid Build Coastguard Worker    if (error) {
298*bf47c682SAndroid Build Coastguard Worker      *error = [NSError errorWithDomain:WALTClientErrorDomain
299*bf47c682SAndroid Build Coastguard Worker                                   code:0
300*bf47c682SAndroid Build Coastguard Worker                               userInfo:@{NSLocalizedDescriptionKey:
301*bf47c682SAndroid Build Coastguard Worker          [NSString stringWithFormat:@"Bad acknowledgement for WALTResetCommand: %@", data]}];
302*bf47c682SAndroid Build Coastguard Worker    }
303*bf47c682SAndroid Build Coastguard Worker    return NO;
304*bf47c682SAndroid Build Coastguard Worker  }
305*bf47c682SAndroid Build Coastguard Worker
306*bf47c682SAndroid Build Coastguard Worker  const uint64_t kMaxSleepTime = 700; // µs
307*bf47c682SAndroid Build Coastguard Worker  const uint64_t kMinSleepTime = 70;  // µs
308*bf47c682SAndroid Build Coastguard Worker  const uint64_t kSleepTimeDivider = 10;
309*bf47c682SAndroid Build Coastguard Worker
310*bf47c682SAndroid Build Coastguard Worker  uint64_t sleepTime = (_sync.maxError - _sync.minError) / kSleepTimeDivider;
311*bf47c682SAndroid Build Coastguard Worker  if (sleepTime > kMaxSleepTime) { sleepTime = kMaxSleepTime; }
312*bf47c682SAndroid Build Coastguard Worker  if (sleepTime < kMinSleepTime) { sleepTime = kMinSleepTime; }
313*bf47c682SAndroid Build Coastguard Worker
314*bf47c682SAndroid Build Coastguard Worker  struct {
315*bf47c682SAndroid Build Coastguard Worker    uint64_t local[kWALTSyncDigitMax];
316*bf47c682SAndroid Build Coastguard Worker    uint64_t remote[kWALTSyncDigitMax];
317*bf47c682SAndroid Build Coastguard Worker  } digitTimes = {0};
318*bf47c682SAndroid Build Coastguard Worker
319*bf47c682SAndroid Build Coastguard Worker  // Send the digits 1 through 9 and record the times they were sent in digitTimes.local.
320*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncDigitMax; ++i) {
321*bf47c682SAndroid Build Coastguard Worker    digitTimes.local[i] = CurrentTime(_clock) - _sync.base;
322*bf47c682SAndroid Build Coastguard Worker
323*bf47c682SAndroid Build Coastguard Worker    char c = '1' + i;
324*bf47c682SAndroid Build Coastguard Worker    if (![self sendCommand:c error:error]) {
325*bf47c682SAndroid Build Coastguard Worker      if (error) {
326*bf47c682SAndroid Build Coastguard Worker        *error = [NSError errorWithDomain:WALTClientErrorDomain
327*bf47c682SAndroid Build Coastguard Worker                                     code:0
328*bf47c682SAndroid Build Coastguard Worker                                 userInfo:@{NSLocalizedDescriptionKey:
329*bf47c682SAndroid Build Coastguard Worker            [NSString stringWithFormat:@"Error sending digit %d", i + 1],
330*bf47c682SAndroid Build Coastguard Worker                                            NSUnderlyingErrorKey: *error}];
331*bf47c682SAndroid Build Coastguard Worker      }
332*bf47c682SAndroid Build Coastguard Worker      return NO;
333*bf47c682SAndroid Build Coastguard Worker    }
334*bf47c682SAndroid Build Coastguard Worker    // Sleep between digits
335*bf47c682SAndroid Build Coastguard Worker    Sleep(sleepTime);
336*bf47c682SAndroid Build Coastguard Worker  }
337*bf47c682SAndroid Build Coastguard Worker
338*bf47c682SAndroid Build Coastguard Worker  if (![self readRemoteTimestamps:digitTimes.remote error:error]) {
339*bf47c682SAndroid Build Coastguard Worker    return NO;
340*bf47c682SAndroid Build Coastguard Worker  }
341*bf47c682SAndroid Build Coastguard Worker
342*bf47c682SAndroid Build Coastguard Worker  // Adjust minError to be the largest delta between local and remote.
343*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncDigitMax; ++i) {
344*bf47c682SAndroid Build Coastguard Worker    int64_t delta = digitTimes.local[i] - digitTimes.remote[i];
345*bf47c682SAndroid Build Coastguard Worker    if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta > _sync.minError) {
346*bf47c682SAndroid Build Coastguard Worker      _sync.minError = delta;
347*bf47c682SAndroid Build Coastguard Worker    }
348*bf47c682SAndroid Build Coastguard Worker  }
349*bf47c682SAndroid Build Coastguard Worker  return YES;
350*bf47c682SAndroid Build Coastguard Worker}
351*bf47c682SAndroid Build Coastguard Worker
352*bf47c682SAndroid Build Coastguard Worker- (BOOL)improveMaxBoundWithError:(NSError **)error {
353*bf47c682SAndroid Build Coastguard Worker  struct {
354*bf47c682SAndroid Build Coastguard Worker    uint64_t local[kWALTSyncDigitMax];
355*bf47c682SAndroid Build Coastguard Worker    uint64_t remote[kWALTSyncDigitMax];
356*bf47c682SAndroid Build Coastguard Worker  } digitTimes = {0};
357*bf47c682SAndroid Build Coastguard Worker
358*bf47c682SAndroid Build Coastguard Worker  // Ask the WALT to send the digits 1 through 9, and record the times they are received in
359*bf47c682SAndroid Build Coastguard Worker  // digitTimes.local.
360*bf47c682SAndroid Build Coastguard Worker  if (![self sendCommand:WALTSendSyncCommand error:error]) {
361*bf47c682SAndroid Build Coastguard Worker    return NO;
362*bf47c682SAndroid Build Coastguard Worker  }
363*bf47c682SAndroid Build Coastguard Worker
364*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncDigitMax; ++i) {
365*bf47c682SAndroid Build Coastguard Worker    NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
366*bf47c682SAndroid Build Coastguard Worker    if (data.length != 1) {
367*bf47c682SAndroid Build Coastguard Worker      if (error) {
368*bf47c682SAndroid Build Coastguard Worker        *error = [NSError errorWithDomain:WALTClientErrorDomain
369*bf47c682SAndroid Build Coastguard Worker                                     code:0
370*bf47c682SAndroid Build Coastguard Worker                                 userInfo:@{NSLocalizedDescriptionKey:
371*bf47c682SAndroid Build Coastguard Worker            [NSString stringWithFormat:@"Error receiving digit %d: %@", i + 1, data]}];
372*bf47c682SAndroid Build Coastguard Worker      }
373*bf47c682SAndroid Build Coastguard Worker      return NO;
374*bf47c682SAndroid Build Coastguard Worker    }
375*bf47c682SAndroid Build Coastguard Worker
376*bf47c682SAndroid Build Coastguard Worker    char c = ((const char *)data.bytes)[0];
377*bf47c682SAndroid Build Coastguard Worker    if (!isdigit(c)) {
378*bf47c682SAndroid Build Coastguard Worker      if (error) {
379*bf47c682SAndroid Build Coastguard Worker        *error = [NSError errorWithDomain:WALTClientErrorDomain
380*bf47c682SAndroid Build Coastguard Worker                                     code:0
381*bf47c682SAndroid Build Coastguard Worker                                 userInfo:@{NSLocalizedDescriptionKey:
382*bf47c682SAndroid Build Coastguard Worker            [NSString stringWithFormat:@"Error parsing digit response: %c", c]}];
383*bf47c682SAndroid Build Coastguard Worker      }
384*bf47c682SAndroid Build Coastguard Worker      return NO;
385*bf47c682SAndroid Build Coastguard Worker    }
386*bf47c682SAndroid Build Coastguard Worker
387*bf47c682SAndroid Build Coastguard Worker    int digit = c - '0';
388*bf47c682SAndroid Build Coastguard Worker    digitTimes.local[digit - 1] = CurrentTime(_clock) - _sync.base;
389*bf47c682SAndroid Build Coastguard Worker  }
390*bf47c682SAndroid Build Coastguard Worker
391*bf47c682SAndroid Build Coastguard Worker  if (![self readRemoteTimestamps:digitTimes.remote error:error]) {
392*bf47c682SAndroid Build Coastguard Worker    return NO;
393*bf47c682SAndroid Build Coastguard Worker  }
394*bf47c682SAndroid Build Coastguard Worker
395*bf47c682SAndroid Build Coastguard Worker  // Adjust maxError to be the smallest delta between local and remote
396*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncDigitMax; ++i) {
397*bf47c682SAndroid Build Coastguard Worker    int64_t delta = digitTimes.local[i] - digitTimes.remote[i];
398*bf47c682SAndroid Build Coastguard Worker    if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta < _sync.maxError) {
399*bf47c682SAndroid Build Coastguard Worker      _sync.maxError = delta;
400*bf47c682SAndroid Build Coastguard Worker    }
401*bf47c682SAndroid Build Coastguard Worker  }
402*bf47c682SAndroid Build Coastguard Worker  return YES;
403*bf47c682SAndroid Build Coastguard Worker}
404*bf47c682SAndroid Build Coastguard Worker
405*bf47c682SAndroid Build Coastguard Worker- (BOOL)readRemoteTimestamps:(uint64_t [9])times error:(NSError **)error {
406*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kWALTSyncDigitMax; ++i) {
407*bf47c682SAndroid Build Coastguard Worker    // Ask the WALT for each digit's recorded timestamp
408*bf47c682SAndroid Build Coastguard Worker    if (![self sendCommand:WALTReadoutSyncCommand error:error]) {
409*bf47c682SAndroid Build Coastguard Worker      return NO;
410*bf47c682SAndroid Build Coastguard Worker    }
411*bf47c682SAndroid Build Coastguard Worker
412*bf47c682SAndroid Build Coastguard Worker    NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
413*bf47c682SAndroid Build Coastguard Worker    if (data.length < 3) {
414*bf47c682SAndroid Build Coastguard Worker      if (error) {
415*bf47c682SAndroid Build Coastguard Worker        *error = [NSError errorWithDomain:WALTClientErrorDomain
416*bf47c682SAndroid Build Coastguard Worker                                     code:0
417*bf47c682SAndroid Build Coastguard Worker                                 userInfo:@{NSLocalizedDescriptionKey:
418*bf47c682SAndroid Build Coastguard Worker            [NSString stringWithFormat:@"Error receiving sync digit %d: %@", i + 1, data]}];
419*bf47c682SAndroid Build Coastguard Worker      }
420*bf47c682SAndroid Build Coastguard Worker      return NO;
421*bf47c682SAndroid Build Coastguard Worker    }
422*bf47c682SAndroid Build Coastguard Worker
423*bf47c682SAndroid Build Coastguard Worker    // The reply data is formatted as n:xxxx, where n is a digit between 1 and 9, and xxxx
424*bf47c682SAndroid Build Coastguard Worker    // is a microsecond timestamp.
425*bf47c682SAndroid Build Coastguard Worker    int digit = (int)antoull(data.bytes, 1);
426*bf47c682SAndroid Build Coastguard Worker    uint64_t timestamp = antoull(((const char *)data.bytes) + 2, data.length - 2);
427*bf47c682SAndroid Build Coastguard Worker
428*bf47c682SAndroid Build Coastguard Worker    if (digit != (i + 1) || timestamp == 0) {
429*bf47c682SAndroid Build Coastguard Worker      if (error) {
430*bf47c682SAndroid Build Coastguard Worker        *error = [NSError errorWithDomain:WALTClientErrorDomain
431*bf47c682SAndroid Build Coastguard Worker                                     code:0
432*bf47c682SAndroid Build Coastguard Worker                                 userInfo:@{NSLocalizedDescriptionKey:
433*bf47c682SAndroid Build Coastguard Worker            [NSString stringWithFormat:@"Error parsing remote time response for %d: %@", i, data]}];
434*bf47c682SAndroid Build Coastguard Worker      }
435*bf47c682SAndroid Build Coastguard Worker      return NO;
436*bf47c682SAndroid Build Coastguard Worker    }
437*bf47c682SAndroid Build Coastguard Worker    times[digit - 1] = timestamp;
438*bf47c682SAndroid Build Coastguard Worker  }
439*bf47c682SAndroid Build Coastguard Worker  return YES;
440*bf47c682SAndroid Build Coastguard Worker}
441*bf47c682SAndroid Build Coastguard Worker
442*bf47c682SAndroid Build Coastguard Worker#pragma mark - MIDIClient Delegate
443*bf47c682SAndroid Build Coastguard Worker
444*bf47c682SAndroid Build Coastguard Worker// TODO(pquinn): Errors from these callbacks aren't propoagated anywhere.
445*bf47c682SAndroid Build Coastguard Worker
446*bf47c682SAndroid Build Coastguard Worker- (void)MIDIClientEndpointAdded:(MIDIClient *)client {
447*bf47c682SAndroid Build Coastguard Worker  [self performSelectorOnMainThread:@selector(checkConnectionWithError:)
448*bf47c682SAndroid Build Coastguard Worker                         withObject:nil
449*bf47c682SAndroid Build Coastguard Worker                      waitUntilDone:NO];
450*bf47c682SAndroid Build Coastguard Worker}
451*bf47c682SAndroid Build Coastguard Worker
452*bf47c682SAndroid Build Coastguard Worker- (void)MIDIClientEndpointRemoved:(MIDIClient *)client {
453*bf47c682SAndroid Build Coastguard Worker  [self performSelectorOnMainThread:@selector(checkConnectionWithError:)
454*bf47c682SAndroid Build Coastguard Worker                         withObject:nil
455*bf47c682SAndroid Build Coastguard Worker                      waitUntilDone:NO];
456*bf47c682SAndroid Build Coastguard Worker}
457*bf47c682SAndroid Build Coastguard Worker
458*bf47c682SAndroid Build Coastguard Worker- (void)MIDIClientConfigurationChanged:(MIDIClient *)client {
459*bf47c682SAndroid Build Coastguard Worker  [self performSelectorOnMainThread:@selector(checkConnectionWithError:)
460*bf47c682SAndroid Build Coastguard Worker                         withObject:nil
461*bf47c682SAndroid Build Coastguard Worker                      waitUntilDone:NO];
462*bf47c682SAndroid Build Coastguard Worker}
463*bf47c682SAndroid Build Coastguard Worker
464*bf47c682SAndroid Build Coastguard Worker- (void)MIDIClient:(MIDIClient *)client receivedError:(NSError *)error {
465*bf47c682SAndroid Build Coastguard Worker  // TODO(pquinn): What's the scope of these errors?
466*bf47c682SAndroid Build Coastguard Worker  NSLog(@"WALTClient received unhandled error: %@", error);
467*bf47c682SAndroid Build Coastguard Worker}
468*bf47c682SAndroid Build Coastguard Worker
469*bf47c682SAndroid Build Coastguard Worker- (void)MIDIClient:(MIDIClient *)client receivedData:(NSData *)message {
470*bf47c682SAndroid Build Coastguard Worker  NSData *body = MIDIMessageBody(message);
471*bf47c682SAndroid Build Coastguard Worker  @synchronized (_responseQueue) {
472*bf47c682SAndroid Build Coastguard Worker    // Sometimes a message will be received twice in quick succession. It's not clear where the bug
473*bf47c682SAndroid Build Coastguard Worker    // is (the WALT, CoreMIDI, or this application), and it cannot be reliably reproduced. As a
474*bf47c682SAndroid Build Coastguard Worker    // hack, simply ignore messages that appear to be duplicates and arrive within
475*bf47c682SAndroid Build Coastguard Worker    // kWALTDuplicateTimeout seconds.
476*bf47c682SAndroid Build Coastguard Worker    if (self.currentTime - _lastDataTimestamp <= kWALTDuplicateTimeout &&
477*bf47c682SAndroid Build Coastguard Worker        [body isEqualToData:_lastData]) {
478*bf47c682SAndroid Build Coastguard Worker      NSLog(@"Ignoring duplicate response within kWALTDuplicateTimeout: %@", message);
479*bf47c682SAndroid Build Coastguard Worker      return;
480*bf47c682SAndroid Build Coastguard Worker    }
481*bf47c682SAndroid Build Coastguard Worker
482*bf47c682SAndroid Build Coastguard Worker    [_responseQueue addObject:body];
483*bf47c682SAndroid Build Coastguard Worker    _lastData = body;
484*bf47c682SAndroid Build Coastguard Worker    _lastDataTimestamp = self.currentTime;
485*bf47c682SAndroid Build Coastguard Worker  }
486*bf47c682SAndroid Build Coastguard Worker  dispatch_semaphore_signal(_responseSemaphore);
487*bf47c682SAndroid Build Coastguard Worker}
488*bf47c682SAndroid Build Coastguard Worker
489*bf47c682SAndroid Build Coastguard Worker#pragma mark - Send/Receive
490*bf47c682SAndroid Build Coastguard Worker
491*bf47c682SAndroid Build Coastguard Worker- (void)drainResponseQueue {
492*bf47c682SAndroid Build Coastguard Worker  @synchronized (_responseQueue) {
493*bf47c682SAndroid Build Coastguard Worker    // Drain out any stale responses or the semaphore destructor will assert.
494*bf47c682SAndroid Build Coastguard Worker    while (_responseQueue.count) {
495*bf47c682SAndroid Build Coastguard Worker      dispatch_semaphore_wait(_responseSemaphore, DISPATCH_TIME_FOREVER);
496*bf47c682SAndroid Build Coastguard Worker      [_responseQueue removeObjectAtIndex:0];
497*bf47c682SAndroid Build Coastguard Worker    }
498*bf47c682SAndroid Build Coastguard Worker  }
499*bf47c682SAndroid Build Coastguard Worker}
500*bf47c682SAndroid Build Coastguard Worker
501*bf47c682SAndroid Build Coastguard Worker- (NSData *)readResponseWithTimeout:(NSTimeInterval)timeout {
502*bf47c682SAndroid Build Coastguard Worker  if (dispatch_semaphore_wait(_responseSemaphore,
503*bf47c682SAndroid Build Coastguard Worker                              dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC))) {
504*bf47c682SAndroid Build Coastguard Worker    return nil;
505*bf47c682SAndroid Build Coastguard Worker  }
506*bf47c682SAndroid Build Coastguard Worker
507*bf47c682SAndroid Build Coastguard Worker  @synchronized (_responseQueue) {
508*bf47c682SAndroid Build Coastguard Worker    NSAssert(_responseQueue.count > 0, @"_responseQueue is empty!");
509*bf47c682SAndroid Build Coastguard Worker    NSData *response = _responseQueue.firstObject;
510*bf47c682SAndroid Build Coastguard Worker    [_responseQueue removeObjectAtIndex:0];
511*bf47c682SAndroid Build Coastguard Worker    return response;
512*bf47c682SAndroid Build Coastguard Worker  }
513*bf47c682SAndroid Build Coastguard Worker}
514*bf47c682SAndroid Build Coastguard Worker
515*bf47c682SAndroid Build Coastguard Worker- (BOOL)sendCommand:(WALTCommand)command error:(NSError **)error {
516*bf47c682SAndroid Build Coastguard Worker  NSData *message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, command);
517*bf47c682SAndroid Build Coastguard Worker  [self drainResponseQueue];
518*bf47c682SAndroid Build Coastguard Worker  return [_client sendData:message error:error];
519*bf47c682SAndroid Build Coastguard Worker}
520*bf47c682SAndroid Build Coastguard Worker
521*bf47c682SAndroid Build Coastguard Worker- (BOOL)checkResponse:(NSData *)response forCommand:(WALTCommand)command {
522*bf47c682SAndroid Build Coastguard Worker  const WALTCommand flipped = isupper(command) ? tolower(command) : toupper(command);
523*bf47c682SAndroid Build Coastguard Worker  if (response.length < 1 || ((const char *)response.bytes)[0] != flipped) {
524*bf47c682SAndroid Build Coastguard Worker    return NO;
525*bf47c682SAndroid Build Coastguard Worker  } else {
526*bf47c682SAndroid Build Coastguard Worker    return YES;
527*bf47c682SAndroid Build Coastguard Worker  }
528*bf47c682SAndroid Build Coastguard Worker}
529*bf47c682SAndroid Build Coastguard Worker
530*bf47c682SAndroid Build Coastguard Worker#pragma mark - Specific Commands
531*bf47c682SAndroid Build Coastguard Worker
532*bf47c682SAndroid Build Coastguard Worker- (NSTimeInterval)lastShockTimeWithError:(NSError **)error {
533*bf47c682SAndroid Build Coastguard Worker  if (![self sendCommand:WALTGShockCommand error:error]) {
534*bf47c682SAndroid Build Coastguard Worker    return -1;
535*bf47c682SAndroid Build Coastguard Worker  }
536*bf47c682SAndroid Build Coastguard Worker
537*bf47c682SAndroid Build Coastguard Worker  NSData *response = [self readResponseWithTimeout:kWALTReadTimeout];
538*bf47c682SAndroid Build Coastguard Worker  if (!response) {
539*bf47c682SAndroid Build Coastguard Worker    if (error) {
540*bf47c682SAndroid Build Coastguard Worker      *error = [NSError errorWithDomain:WALTClientErrorDomain
541*bf47c682SAndroid Build Coastguard Worker                                   code:0
542*bf47c682SAndroid Build Coastguard Worker                               userInfo:@{NSLocalizedDescriptionKey:
543*bf47c682SAndroid Build Coastguard Worker                                            @"Error receiving shock response."}];
544*bf47c682SAndroid Build Coastguard Worker    }
545*bf47c682SAndroid Build Coastguard Worker    return -1;
546*bf47c682SAndroid Build Coastguard Worker  }
547*bf47c682SAndroid Build Coastguard Worker
548*bf47c682SAndroid Build Coastguard Worker  uint64_t microseconds = antoull(response.bytes, response.length);
549*bf47c682SAndroid Build Coastguard Worker  return ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC;
550*bf47c682SAndroid Build Coastguard Worker}
551*bf47c682SAndroid Build Coastguard Worker
552*bf47c682SAndroid Build Coastguard Worker- (WALTTrigger)readTrigger:(NSData *)response {
553*bf47c682SAndroid Build Coastguard Worker  NSString *responseString =
554*bf47c682SAndroid Build Coastguard Worker      [[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding];
555*bf47c682SAndroid Build Coastguard Worker  NSArray<NSString *> *components = [responseString componentsSeparatedByString:@" "];
556*bf47c682SAndroid Build Coastguard Worker
557*bf47c682SAndroid Build Coastguard Worker  WALTTrigger result = {0};
558*bf47c682SAndroid Build Coastguard Worker
559*bf47c682SAndroid Build Coastguard Worker  if (components.count != 5 ||
560*bf47c682SAndroid Build Coastguard Worker      ![[components objectAtIndex:0] isEqualToString:@"G"] ||
561*bf47c682SAndroid Build Coastguard Worker      [components objectAtIndex:1].length != 1) {
562*bf47c682SAndroid Build Coastguard Worker    return result;
563*bf47c682SAndroid Build Coastguard Worker  }
564*bf47c682SAndroid Build Coastguard Worker
565*bf47c682SAndroid Build Coastguard Worker  result.tag = [[components objectAtIndex:1] characterAtIndex:0];
566*bf47c682SAndroid Build Coastguard Worker
567*bf47c682SAndroid Build Coastguard Worker  uint64_t microseconds = atoll([components objectAtIndex:2].UTF8String);
568*bf47c682SAndroid Build Coastguard Worker  result.t = ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC;
569*bf47c682SAndroid Build Coastguard Worker  result.value = (int)atoll([components objectAtIndex:3].UTF8String);
570*bf47c682SAndroid Build Coastguard Worker  result.count = (unsigned int)atoll([components objectAtIndex:4].UTF8String);
571*bf47c682SAndroid Build Coastguard Worker  return result;
572*bf47c682SAndroid Build Coastguard Worker}
573*bf47c682SAndroid Build Coastguard Worker
574*bf47c682SAndroid Build Coastguard Worker- (WALTTrigger)readTriggerWithTimeout:(NSTimeInterval)timeout {
575*bf47c682SAndroid Build Coastguard Worker  return [self readTrigger:[self readResponseWithTimeout:timeout]];
576*bf47c682SAndroid Build Coastguard Worker}
577*bf47c682SAndroid Build Coastguard Worker
578*bf47c682SAndroid Build Coastguard Worker#pragma mark - Time
579*bf47c682SAndroid Build Coastguard Worker
580*bf47c682SAndroid Build Coastguard Worker- (NSTimeInterval)baseTime {
581*bf47c682SAndroid Build Coastguard Worker  return ((NSTimeInterval)_sync.base) / USEC_PER_SEC;
582*bf47c682SAndroid Build Coastguard Worker}
583*bf47c682SAndroid Build Coastguard Worker
584*bf47c682SAndroid Build Coastguard Worker- (NSTimeInterval)currentTime {
585*bf47c682SAndroid Build Coastguard Worker  return ((NSTimeInterval)CurrentTime(_clock)) / USEC_PER_SEC;
586*bf47c682SAndroid Build Coastguard Worker}
587*bf47c682SAndroid Build Coastguard Worker@end
588