xref: /aosp_15_r20/external/walt/ios/WALT/DragLatencyController.mm (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 "DragLatencyController.h"
18*bf47c682SAndroid Build Coastguard Worker
19*bf47c682SAndroid Build Coastguard Worker#import <dispatch/dispatch.h>
20*bf47c682SAndroid Build Coastguard Worker#import <math.h>
21*bf47c682SAndroid Build Coastguard Worker#import <numeric>
22*bf47c682SAndroid Build Coastguard Worker#import <vector>
23*bf47c682SAndroid Build Coastguard Worker
24*bf47c682SAndroid Build Coastguard Worker#import "UIAlertView+Extensions.h"
25*bf47c682SAndroid Build Coastguard Worker#import "WALTAppDelegate.h"
26*bf47c682SAndroid Build Coastguard Worker#import "WALTClient.h"
27*bf47c682SAndroid Build Coastguard Worker#import "WALTLogger.h"
28*bf47c682SAndroid Build Coastguard Worker#import "WALTTouch.h"
29*bf47c682SAndroid Build Coastguard Worker
30*bf47c682SAndroid Build Coastguard Workerstatic const NSTimeInterval kGoalpostFrequency = 0.55;  // TODO(pquinn): User-configurable settings.
31*bf47c682SAndroid Build Coastguard Workerstatic const NSUInteger kMinTouchEvents = 100;
32*bf47c682SAndroid Build Coastguard Workerstatic const NSUInteger kMinLaserEvents = 8;
33*bf47c682SAndroid Build Coastguard Workerstatic const char kWALTLaserTag = 'L';
34*bf47c682SAndroid Build Coastguard Worker
35*bf47c682SAndroid Build Coastguard Worker@interface WALTLaserEvent : NSObject
36*bf47c682SAndroid Build Coastguard Worker@property (assign) NSTimeInterval t;
37*bf47c682SAndroid Build Coastguard Worker@property (assign) int value;
38*bf47c682SAndroid Build Coastguard Worker@end
39*bf47c682SAndroid Build Coastguard Worker
40*bf47c682SAndroid Build Coastguard Worker@implementation WALTLaserEvent
41*bf47c682SAndroid Build Coastguard Worker@end
42*bf47c682SAndroid Build Coastguard Worker
43*bf47c682SAndroid Build Coastguard Worker/** Linear interpolation between x0 and x1 at alpha. */
44*bf47c682SAndroid Build Coastguard Workertemplate <typename T>
45*bf47c682SAndroid Build Coastguard Workerstatic T Lerp(const T& x0, const T& x1, double alpha) {
46*bf47c682SAndroid Build Coastguard Worker  NSCAssert(alpha >= 0 && alpha <= 1, @"alpha must be between 0 and 1 (%f)", alpha);
47*bf47c682SAndroid Build Coastguard Worker  return ((1 - alpha) * x0) + (alpha * x1);
48*bf47c682SAndroid Build Coastguard Worker}
49*bf47c682SAndroid Build Coastguard Worker
50*bf47c682SAndroid Build Coastguard Worker/** Linear interpolation of (xp, yp) at x. */
51*bf47c682SAndroid Build Coastguard Workertemplate <typename S, typename T>
52*bf47c682SAndroid Build Coastguard Workerstatic std::vector<T> Interpolate(const std::vector<S>& x,
53*bf47c682SAndroid Build Coastguard Worker                                  const std::vector<S>& xp,
54*bf47c682SAndroid Build Coastguard Worker                                  const std::vector<T>& yp) {
55*bf47c682SAndroid Build Coastguard Worker  NSCAssert(xp.size(), @"xp must contain at least one value.");
56*bf47c682SAndroid Build Coastguard Worker  NSCAssert(xp.size() == yp.size(), @"xp and yp must have matching lengths.");
57*bf47c682SAndroid Build Coastguard Worker
58*bf47c682SAndroid Build Coastguard Worker  std::vector<T> y;
59*bf47c682SAndroid Build Coastguard Worker  y.reserve(x.size());
60*bf47c682SAndroid Build Coastguard Worker
61*bf47c682SAndroid Build Coastguard Worker  size_t i = 0;  // Index into x.
62*bf47c682SAndroid Build Coastguard Worker
63*bf47c682SAndroid Build Coastguard Worker  for (; i < x.size() && x[i] < xp.front(); ++i) {
64*bf47c682SAndroid Build Coastguard Worker    y.push_back(yp.front());  // Pad out y with yp.front() for x values before xp.front().
65*bf47c682SAndroid Build Coastguard Worker  }
66*bf47c682SAndroid Build Coastguard Worker
67*bf47c682SAndroid Build Coastguard Worker  size_t ip = 0;  // Index into xp/yp.
68*bf47c682SAndroid Build Coastguard Worker
69*bf47c682SAndroid Build Coastguard Worker  for (; ip < xp.size() && i < x.size(); ++i) {
70*bf47c682SAndroid Build Coastguard Worker    while (ip < xp.size() && xp[ip] <= x[i]) {  // Find an xp[ip] greater than x[i].
71*bf47c682SAndroid Build Coastguard Worker      ++ip;
72*bf47c682SAndroid Build Coastguard Worker    }
73*bf47c682SAndroid Build Coastguard Worker    if (ip >= xp.size()) {
74*bf47c682SAndroid Build Coastguard Worker      break;  // Ran out of values.
75*bf47c682SAndroid Build Coastguard Worker    }
76*bf47c682SAndroid Build Coastguard Worker
77*bf47c682SAndroid Build Coastguard Worker    const double alpha = (x[i] - xp[ip - 1]) / static_cast<double>(xp[ip] - xp[ip - 1]);
78*bf47c682SAndroid Build Coastguard Worker    y.push_back(Lerp(yp[ip - 1], yp[ip], alpha));
79*bf47c682SAndroid Build Coastguard Worker  }
80*bf47c682SAndroid Build Coastguard Worker
81*bf47c682SAndroid Build Coastguard Worker  for (; i < x.size(); ++i) {
82*bf47c682SAndroid Build Coastguard Worker    y.push_back(yp.back());  // Pad out y with yp.back() for values after xp.back().
83*bf47c682SAndroid Build Coastguard Worker  }
84*bf47c682SAndroid Build Coastguard Worker
85*bf47c682SAndroid Build Coastguard Worker  return y;
86*bf47c682SAndroid Build Coastguard Worker}
87*bf47c682SAndroid Build Coastguard Worker
88*bf47c682SAndroid Build Coastguard Worker/** Extracts the values of y where the corresponding value in x is equal to value. */
89*bf47c682SAndroid Build Coastguard Workertemplate <typename S, typename T>
90*bf47c682SAndroid Build Coastguard Workerstatic std::vector<S> Extract(const std::vector<T>& x, const std::vector<S>& y, const T& value) {
91*bf47c682SAndroid Build Coastguard Worker  NSCAssert(x.size() == y.size(), @"x and y must have matching lengths.");
92*bf47c682SAndroid Build Coastguard Worker  std::vector<S> extracted;
93*bf47c682SAndroid Build Coastguard Worker
94*bf47c682SAndroid Build Coastguard Worker  for (size_t i = 0; i < x.size(); ++i) {
95*bf47c682SAndroid Build Coastguard Worker    if (x[i] == value) {
96*bf47c682SAndroid Build Coastguard Worker      extracted.push_back(y[i]);
97*bf47c682SAndroid Build Coastguard Worker    }
98*bf47c682SAndroid Build Coastguard Worker  }
99*bf47c682SAndroid Build Coastguard Worker
100*bf47c682SAndroid Build Coastguard Worker  return extracted;
101*bf47c682SAndroid Build Coastguard Worker}
102*bf47c682SAndroid Build Coastguard Worker
103*bf47c682SAndroid Build Coastguard Worker/** Returns the standard deviation of the values in x. */
104*bf47c682SAndroid Build Coastguard Workertemplate <typename T>
105*bf47c682SAndroid Build Coastguard Workerstatic T StandardDeviation(const std::vector<T>& x) {
106*bf47c682SAndroid Build Coastguard Worker  NSCAssert(x.size() > 0, @"x must have at least one value.");
107*bf47c682SAndroid Build Coastguard Worker  const T sum = std::accumulate(x.begin(), x.end(), T{});
108*bf47c682SAndroid Build Coastguard Worker  const T mean = sum / x.size();
109*bf47c682SAndroid Build Coastguard Worker  const T ss = std::accumulate(x.begin(), x.end(), T{}, ^(T accum, T value){
110*bf47c682SAndroid Build Coastguard Worker      return accum + ((value - mean) * (value - mean));
111*bf47c682SAndroid Build Coastguard Worker  });
112*bf47c682SAndroid Build Coastguard Worker  return sqrt(ss / (x.size() - 1));
113*bf47c682SAndroid Build Coastguard Worker}
114*bf47c682SAndroid Build Coastguard Worker
115*bf47c682SAndroid Build Coastguard Worker/** Returns the index of the smallest value in x. */
116*bf47c682SAndroid Build Coastguard Workertemplate <typename T>
117*bf47c682SAndroid Build Coastguard Workerstatic size_t ArgMin(const std::vector<T>& x) {
118*bf47c682SAndroid Build Coastguard Worker  NSCAssert(x.size() > 0, @"x must have at least one value.");
119*bf47c682SAndroid Build Coastguard Worker  size_t imin = 0;
120*bf47c682SAndroid Build Coastguard Worker  for (size_t i = 1; i < x.size(); ++i) {
121*bf47c682SAndroid Build Coastguard Worker    if (x[i] < x[imin]) {
122*bf47c682SAndroid Build Coastguard Worker      imin = i;
123*bf47c682SAndroid Build Coastguard Worker    }
124*bf47c682SAndroid Build Coastguard Worker  }
125*bf47c682SAndroid Build Coastguard Worker  return imin;
126*bf47c682SAndroid Build Coastguard Worker}
127*bf47c682SAndroid Build Coastguard Worker
128*bf47c682SAndroid Build Coastguard Worker/**
129*bf47c682SAndroid Build Coastguard Worker * Finds a positive time value that shifting laserTs by will minimise the standard deviation of
130*bf47c682SAndroid Build Coastguard Worker * interpolated touchYs.
131*bf47c682SAndroid Build Coastguard Worker */
132*bf47c682SAndroid Build Coastguard Workerstatic NSTimeInterval FindBestShift(const std::vector<NSTimeInterval>& laserTs,
133*bf47c682SAndroid Build Coastguard Worker                                    const std::vector<NSTimeInterval>& touchTs,
134*bf47c682SAndroid Build Coastguard Worker                                    const std::vector<CGFloat>& touchYs) {
135*bf47c682SAndroid Build Coastguard Worker  NSCAssert(laserTs.size() > 0, @"laserTs must have at least one value.");
136*bf47c682SAndroid Build Coastguard Worker  NSCAssert(touchTs.size() == touchYs.size(), @"touchTs and touchYs must have matching lengths.");
137*bf47c682SAndroid Build Coastguard Worker
138*bf47c682SAndroid Build Coastguard Worker  const NSTimeInterval kSearchCoverage = 0.15;
139*bf47c682SAndroid Build Coastguard Worker  const int kSteps = 1500;
140*bf47c682SAndroid Build Coastguard Worker  const NSTimeInterval kShiftStep = kSearchCoverage / kSteps;
141*bf47c682SAndroid Build Coastguard Worker
142*bf47c682SAndroid Build Coastguard Worker  std::vector<NSTimeInterval> deviations;
143*bf47c682SAndroid Build Coastguard Worker  deviations.reserve(kSteps);
144*bf47c682SAndroid Build Coastguard Worker
145*bf47c682SAndroid Build Coastguard Worker  std::vector<NSTimeInterval> ts(laserTs.size());
146*bf47c682SAndroid Build Coastguard Worker  for (int i = 0; i < kSteps; ++i) {
147*bf47c682SAndroid Build Coastguard Worker    for (size_t j = 0; j < laserTs.size(); ++j) {
148*bf47c682SAndroid Build Coastguard Worker      ts[j] = laserTs[j] + (kShiftStep * i);
149*bf47c682SAndroid Build Coastguard Worker    }
150*bf47c682SAndroid Build Coastguard Worker
151*bf47c682SAndroid Build Coastguard Worker    std::vector<CGFloat> laserYs = Interpolate(ts, touchTs, touchYs);
152*bf47c682SAndroid Build Coastguard Worker    deviations.push_back(StandardDeviation(laserYs));
153*bf47c682SAndroid Build Coastguard Worker  }
154*bf47c682SAndroid Build Coastguard Worker
155*bf47c682SAndroid Build Coastguard Worker  return ArgMin(deviations) * kShiftStep;
156*bf47c682SAndroid Build Coastguard Worker}
157*bf47c682SAndroid Build Coastguard Worker
158*bf47c682SAndroid Build Coastguard Worker@interface DragLatencyController ()
159*bf47c682SAndroid Build Coastguard Worker- (void)updateCountDisplay;
160*bf47c682SAndroid Build Coastguard Worker- (void)processEvent:(UIEvent *)event;
161*bf47c682SAndroid Build Coastguard Worker- (void)receiveTriggers:(id)context;
162*bf47c682SAndroid Build Coastguard Worker- (void)stopReceiver;
163*bf47c682SAndroid Build Coastguard Worker@end
164*bf47c682SAndroid Build Coastguard Worker
165*bf47c682SAndroid Build Coastguard Worker@implementation DragLatencyController {
166*bf47c682SAndroid Build Coastguard Worker  WALTClient *_client;
167*bf47c682SAndroid Build Coastguard Worker  WALTLogger *_logger;
168*bf47c682SAndroid Build Coastguard Worker
169*bf47c682SAndroid Build Coastguard Worker  NSMutableArray<WALTTouch *> *_touchEvents;
170*bf47c682SAndroid Build Coastguard Worker  NSMutableArray<WALTLaserEvent *> *_laserEvents;
171*bf47c682SAndroid Build Coastguard Worker
172*bf47c682SAndroid Build Coastguard Worker  NSThread *_triggerReceiver;
173*bf47c682SAndroid Build Coastguard Worker  dispatch_semaphore_t _receiverComplete;
174*bf47c682SAndroid Build Coastguard Worker}
175*bf47c682SAndroid Build Coastguard Worker
176*bf47c682SAndroid Build Coastguard Worker- (void)dealloc {
177*bf47c682SAndroid Build Coastguard Worker  [self stopReceiver];
178*bf47c682SAndroid Build Coastguard Worker}
179*bf47c682SAndroid Build Coastguard Worker
180*bf47c682SAndroid Build Coastguard Worker- (void)viewDidLoad {
181*bf47c682SAndroid Build Coastguard Worker  [super viewDidLoad];
182*bf47c682SAndroid Build Coastguard Worker
183*bf47c682SAndroid Build Coastguard Worker  _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client;
184*bf47c682SAndroid Build Coastguard Worker  _logger = [WALTLogger sessionLogger];
185*bf47c682SAndroid Build Coastguard Worker}
186*bf47c682SAndroid Build Coastguard Worker
187*bf47c682SAndroid Build Coastguard Worker- (void)viewWillAppear:(BOOL)animated {
188*bf47c682SAndroid Build Coastguard Worker  [super viewWillAppear:animated];
189*bf47c682SAndroid Build Coastguard Worker
190*bf47c682SAndroid Build Coastguard Worker  [self updateCountDisplay];
191*bf47c682SAndroid Build Coastguard Worker
192*bf47c682SAndroid Build Coastguard Worker  [_logger appendString:@"DRAGLATENCY\n"];
193*bf47c682SAndroid Build Coastguard Worker}
194*bf47c682SAndroid Build Coastguard Worker
195*bf47c682SAndroid Build Coastguard Worker- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
196*bf47c682SAndroid Build Coastguard Worker  [self processEvent:event];
197*bf47c682SAndroid Build Coastguard Worker}
198*bf47c682SAndroid Build Coastguard Worker
199*bf47c682SAndroid Build Coastguard Worker- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
200*bf47c682SAndroid Build Coastguard Worker  [self processEvent:event];
201*bf47c682SAndroid Build Coastguard Worker}
202*bf47c682SAndroid Build Coastguard Worker
203*bf47c682SAndroid Build Coastguard Worker- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
204*bf47c682SAndroid Build Coastguard Worker  [self processEvent:event];
205*bf47c682SAndroid Build Coastguard Worker}
206*bf47c682SAndroid Build Coastguard Worker
207*bf47c682SAndroid Build Coastguard Worker- (void)processEvent:(UIEvent *)event {
208*bf47c682SAndroid Build Coastguard Worker  // TODO(pquinn): Pull out coalesced touches.
209*bf47c682SAndroid Build Coastguard Worker
210*bf47c682SAndroid Build Coastguard Worker  WALTTouch *touch = [[WALTTouch alloc] initWithEvent:event];
211*bf47c682SAndroid Build Coastguard Worker  [_touchEvents addObject:touch];
212*bf47c682SAndroid Build Coastguard Worker  [_logger appendFormat:@"TOUCH\t%.3f\t%.2f\t%.2f\n",
213*bf47c682SAndroid Build Coastguard Worker      touch.kernelTime, touch.location.x, touch.location.y];
214*bf47c682SAndroid Build Coastguard Worker  [self updateCountDisplay];
215*bf47c682SAndroid Build Coastguard Worker}
216*bf47c682SAndroid Build Coastguard Worker
217*bf47c682SAndroid Build Coastguard Worker- (void)updateCountDisplay {
218*bf47c682SAndroid Build Coastguard Worker  NSString *counts = [NSString stringWithFormat:@"N ✛ %lu ⇄ %lu",
219*bf47c682SAndroid Build Coastguard Worker                         (unsigned long)_laserEvents.count, (unsigned long)_touchEvents.count];
220*bf47c682SAndroid Build Coastguard Worker  self.countLabel.text = counts;
221*bf47c682SAndroid Build Coastguard Worker}
222*bf47c682SAndroid Build Coastguard Worker
223*bf47c682SAndroid Build Coastguard Worker- (IBAction)start:(id)sender {
224*bf47c682SAndroid Build Coastguard Worker  [self reset:sender];
225*bf47c682SAndroid Build Coastguard Worker
226*bf47c682SAndroid Build Coastguard Worker  self.goalpostView.hidden = NO;
227*bf47c682SAndroid Build Coastguard Worker  self.statusLabel.text = @"";
228*bf47c682SAndroid Build Coastguard Worker
229*bf47c682SAndroid Build Coastguard Worker  [UIView beginAnimations:@"Goalpost" context:NULL];
230*bf47c682SAndroid Build Coastguard Worker  [UIView setAnimationDuration:kGoalpostFrequency];
231*bf47c682SAndroid Build Coastguard Worker  [UIView setAnimationBeginsFromCurrentState:NO];
232*bf47c682SAndroid Build Coastguard Worker  [UIView setAnimationRepeatCount:FLT_MAX];
233*bf47c682SAndroid Build Coastguard Worker  [UIView setAnimationRepeatAutoreverses:YES];
234*bf47c682SAndroid Build Coastguard Worker
235*bf47c682SAndroid Build Coastguard Worker  self.goalpostView.transform =
236*bf47c682SAndroid Build Coastguard Worker      CGAffineTransformMakeTranslation(0.0, -CGRectGetHeight(self.view.frame) + 300);
237*bf47c682SAndroid Build Coastguard Worker
238*bf47c682SAndroid Build Coastguard Worker  [UIView commitAnimations];
239*bf47c682SAndroid Build Coastguard Worker
240*bf47c682SAndroid Build Coastguard Worker  _receiverComplete = dispatch_semaphore_create(0);
241*bf47c682SAndroid Build Coastguard Worker  _triggerReceiver = [[NSThread alloc] initWithTarget:self
242*bf47c682SAndroid Build Coastguard Worker                                             selector:@selector(receiveTriggers:)
243*bf47c682SAndroid Build Coastguard Worker                                               object:nil];
244*bf47c682SAndroid Build Coastguard Worker  [_triggerReceiver start];
245*bf47c682SAndroid Build Coastguard Worker}
246*bf47c682SAndroid Build Coastguard Worker
247*bf47c682SAndroid Build Coastguard Worker- (IBAction)reset:(id)sender {
248*bf47c682SAndroid Build Coastguard Worker  [self stopReceiver];
249*bf47c682SAndroid Build Coastguard Worker
250*bf47c682SAndroid Build Coastguard Worker  self.goalpostView.transform = CGAffineTransformMakeTranslation(0.0, 0.0);
251*bf47c682SAndroid Build Coastguard Worker  self.goalpostView.hidden = YES;
252*bf47c682SAndroid Build Coastguard Worker
253*bf47c682SAndroid Build Coastguard Worker  _touchEvents = [[NSMutableArray<WALTTouch *> alloc] init];
254*bf47c682SAndroid Build Coastguard Worker  _laserEvents = [[NSMutableArray<WALTLaserEvent *> alloc] init];
255*bf47c682SAndroid Build Coastguard Worker
256*bf47c682SAndroid Build Coastguard Worker  [self updateCountDisplay];
257*bf47c682SAndroid Build Coastguard Worker
258*bf47c682SAndroid Build Coastguard Worker  NSError *error = nil;
259*bf47c682SAndroid Build Coastguard Worker  if (![_client syncClocksWithError:&error]) {
260*bf47c682SAndroid Build Coastguard Worker    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
261*bf47c682SAndroid Build Coastguard Worker    [alert show];
262*bf47c682SAndroid Build Coastguard Worker  }
263*bf47c682SAndroid Build Coastguard Worker
264*bf47c682SAndroid Build Coastguard Worker  [_logger appendString:@"RESET\n"];
265*bf47c682SAndroid Build Coastguard Worker}
266*bf47c682SAndroid Build Coastguard Worker
267*bf47c682SAndroid Build Coastguard Worker- (void)receiveTriggers:(id)context {
268*bf47c682SAndroid Build Coastguard Worker  // Turn on laser change notifications.
269*bf47c682SAndroid Build Coastguard Worker  NSError *error = nil;
270*bf47c682SAndroid Build Coastguard Worker  if (![_client sendCommand:WALTLaserOnCommand error:&error]) {
271*bf47c682SAndroid Build Coastguard Worker    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
272*bf47c682SAndroid Build Coastguard Worker    [alert show];
273*bf47c682SAndroid Build Coastguard Worker    dispatch_semaphore_signal(_receiverComplete);
274*bf47c682SAndroid Build Coastguard Worker    return;
275*bf47c682SAndroid Build Coastguard Worker  }
276*bf47c682SAndroid Build Coastguard Worker
277*bf47c682SAndroid Build Coastguard Worker  NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout];
278*bf47c682SAndroid Build Coastguard Worker  if (![_client checkResponse:response forCommand:WALTLaserOnCommand]) {
279*bf47c682SAndroid Build Coastguard Worker    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error"
280*bf47c682SAndroid Build Coastguard Worker                                                    message:@"Failed to start laser probe."
281*bf47c682SAndroid Build Coastguard Worker                                                   delegate:nil
282*bf47c682SAndroid Build Coastguard Worker                                          cancelButtonTitle:@"Dismiss"
283*bf47c682SAndroid Build Coastguard Worker                                          otherButtonTitles:nil];
284*bf47c682SAndroid Build Coastguard Worker    [alert show];
285*bf47c682SAndroid Build Coastguard Worker    dispatch_semaphore_signal(_receiverComplete);
286*bf47c682SAndroid Build Coastguard Worker    return;
287*bf47c682SAndroid Build Coastguard Worker  }
288*bf47c682SAndroid Build Coastguard Worker
289*bf47c682SAndroid Build Coastguard Worker  while (!NSThread.currentThread.isCancelled) {
290*bf47c682SAndroid Build Coastguard Worker    WALTTrigger response = [_client readTriggerWithTimeout:kWALTReadTimeout];
291*bf47c682SAndroid Build Coastguard Worker    if (response.tag == kWALTLaserTag) {
292*bf47c682SAndroid Build Coastguard Worker      WALTLaserEvent *event = [[WALTLaserEvent alloc] init];
293*bf47c682SAndroid Build Coastguard Worker      event.t = response.t;
294*bf47c682SAndroid Build Coastguard Worker      event.value = response.value;
295*bf47c682SAndroid Build Coastguard Worker      [_laserEvents addObject:event];
296*bf47c682SAndroid Build Coastguard Worker      [_logger appendFormat:@"LASER\t%.3f\t%d\n", event.t, event.value];
297*bf47c682SAndroid Build Coastguard Worker    } else if (response.tag != '\0') {  // Don't fail for timeout errors.
298*bf47c682SAndroid Build Coastguard Worker      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error"
299*bf47c682SAndroid Build Coastguard Worker                                                      message:@"Failed to read laser probe."
300*bf47c682SAndroid Build Coastguard Worker                                                     delegate:nil
301*bf47c682SAndroid Build Coastguard Worker                                            cancelButtonTitle:@"Dismiss"
302*bf47c682SAndroid Build Coastguard Worker                                            otherButtonTitles:nil];
303*bf47c682SAndroid Build Coastguard Worker      [alert show];
304*bf47c682SAndroid Build Coastguard Worker    }
305*bf47c682SAndroid Build Coastguard Worker  }
306*bf47c682SAndroid Build Coastguard Worker
307*bf47c682SAndroid Build Coastguard Worker  // Turn off laser change notifications.
308*bf47c682SAndroid Build Coastguard Worker  [_client sendCommand:WALTLaserOffCommand error:nil];
309*bf47c682SAndroid Build Coastguard Worker  [_client readResponseWithTimeout:kWALTReadTimeout];
310*bf47c682SAndroid Build Coastguard Worker
311*bf47c682SAndroid Build Coastguard Worker  dispatch_semaphore_signal(_receiverComplete);
312*bf47c682SAndroid Build Coastguard Worker}
313*bf47c682SAndroid Build Coastguard Worker
314*bf47c682SAndroid Build Coastguard Worker- (void)stopReceiver {
315*bf47c682SAndroid Build Coastguard Worker  // TODO(pquinn): This will deadlock if called in rapid succession -- there is a small delay
316*bf47c682SAndroid Build Coastguard Worker  //               between dispatch_semaphore_signal() and -[NSThread isExecuting] changing.
317*bf47c682SAndroid Build Coastguard Worker  //               Unfortunately, NSThread is not joinable...
318*bf47c682SAndroid Build Coastguard Worker  if (_triggerReceiver.isExecuting) {
319*bf47c682SAndroid Build Coastguard Worker    [_triggerReceiver cancel];
320*bf47c682SAndroid Build Coastguard Worker    dispatch_semaphore_wait(_receiverComplete, DISPATCH_TIME_FOREVER);
321*bf47c682SAndroid Build Coastguard Worker  }
322*bf47c682SAndroid Build Coastguard Worker}
323*bf47c682SAndroid Build Coastguard Worker
324*bf47c682SAndroid Build Coastguard Worker- (IBAction)computeStatistics:(id)sender {
325*bf47c682SAndroid Build Coastguard Worker  if (_touchEvents.count < kMinTouchEvents) {
326*bf47c682SAndroid Build Coastguard Worker    self.statusLabel.text =
327*bf47c682SAndroid Build Coastguard Worker        [NSString stringWithFormat:@"Too few touch events (%lu/%lu).",
328*bf47c682SAndroid Build Coastguard Worker          (unsigned long)_touchEvents.count, (unsigned long)kMinTouchEvents];
329*bf47c682SAndroid Build Coastguard Worker    [self reset:sender];
330*bf47c682SAndroid Build Coastguard Worker    return;
331*bf47c682SAndroid Build Coastguard Worker  }
332*bf47c682SAndroid Build Coastguard Worker
333*bf47c682SAndroid Build Coastguard Worker  // Timestamps are reset to be relative to t0 to make the output easier to read.
334*bf47c682SAndroid Build Coastguard Worker  const NSTimeInterval t0 = _touchEvents.firstObject.kernelTime;
335*bf47c682SAndroid Build Coastguard Worker  const NSTimeInterval tF = _touchEvents.lastObject.kernelTime;
336*bf47c682SAndroid Build Coastguard Worker
337*bf47c682SAndroid Build Coastguard Worker  std::vector<NSTimeInterval> ft(_touchEvents.count);
338*bf47c682SAndroid Build Coastguard Worker  std::vector<CGFloat> fy(_touchEvents.count);
339*bf47c682SAndroid Build Coastguard Worker  for (NSUInteger i = 0; i < _touchEvents.count; ++i) {
340*bf47c682SAndroid Build Coastguard Worker    ft[i] = _touchEvents[i].kernelTime - t0;
341*bf47c682SAndroid Build Coastguard Worker    fy[i] = _touchEvents[i].location.y;
342*bf47c682SAndroid Build Coastguard Worker  }
343*bf47c682SAndroid Build Coastguard Worker
344*bf47c682SAndroid Build Coastguard Worker  // Remove laser events that have a timestamp outside [t0, tF].
345*bf47c682SAndroid Build Coastguard Worker  [_laserEvents filterUsingPredicate:[NSPredicate predicateWithBlock:
346*bf47c682SAndroid Build Coastguard Worker      ^BOOL(WALTLaserEvent *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
347*bf47c682SAndroid Build Coastguard Worker        return evaluatedObject.t >= t0 && evaluatedObject.t <= tF;
348*bf47c682SAndroid Build Coastguard Worker  }]];
349*bf47c682SAndroid Build Coastguard Worker
350*bf47c682SAndroid Build Coastguard Worker  if (_laserEvents.count < kMinLaserEvents) {
351*bf47c682SAndroid Build Coastguard Worker    self.statusLabel.text =
352*bf47c682SAndroid Build Coastguard Worker        [NSString stringWithFormat:@"Too few laser events (%lu/%lu).",
353*bf47c682SAndroid Build Coastguard Worker          (unsigned long)_laserEvents.count, (unsigned long)kMinLaserEvents];
354*bf47c682SAndroid Build Coastguard Worker    [self reset:sender];
355*bf47c682SAndroid Build Coastguard Worker    return;
356*bf47c682SAndroid Build Coastguard Worker  }
357*bf47c682SAndroid Build Coastguard Worker
358*bf47c682SAndroid Build Coastguard Worker  if (_laserEvents.firstObject.value != 0) {
359*bf47c682SAndroid Build Coastguard Worker    self.statusLabel.text = @"First laser crossing was not into the beam.";
360*bf47c682SAndroid Build Coastguard Worker    [self reset:sender];
361*bf47c682SAndroid Build Coastguard Worker    return;
362*bf47c682SAndroid Build Coastguard Worker  }
363*bf47c682SAndroid Build Coastguard Worker
364*bf47c682SAndroid Build Coastguard Worker  std::vector<NSTimeInterval> lt(_laserEvents.count);
365*bf47c682SAndroid Build Coastguard Worker  std::vector<int> lv(_laserEvents.count);
366*bf47c682SAndroid Build Coastguard Worker  for (NSUInteger i = 0; i < _laserEvents.count; ++i) {
367*bf47c682SAndroid Build Coastguard Worker    lt[i] = _laserEvents[i].t - t0;
368*bf47c682SAndroid Build Coastguard Worker    lv[i] = _laserEvents[i].value;
369*bf47c682SAndroid Build Coastguard Worker  }
370*bf47c682SAndroid Build Coastguard Worker
371*bf47c682SAndroid Build Coastguard Worker  // Calculate interpolated touch y positions at each laser event.
372*bf47c682SAndroid Build Coastguard Worker  std::vector<CGFloat> ly = Interpolate(lt, ft, fy);
373*bf47c682SAndroid Build Coastguard Worker
374*bf47c682SAndroid Build Coastguard Worker  // Labels for each laser event to denote those above/below the beam.
375*bf47c682SAndroid Build Coastguard Worker  // The actual side is irrelevant, but events on the same side should have the same label. The
376*bf47c682SAndroid Build Coastguard Worker  // vector will look like [0, 1, 1, 0, 0, 1, 1, 0, 0, ...].
377*bf47c682SAndroid Build Coastguard Worker  std::vector<int> sideLabels(lt.size());
378*bf47c682SAndroid Build Coastguard Worker  for (size_t i = 0; i < lt.size(); ++i) {
379*bf47c682SAndroid Build Coastguard Worker    sideLabels[i] = ((i + 1) / 2) % 2;
380*bf47c682SAndroid Build Coastguard Worker  }
381*bf47c682SAndroid Build Coastguard Worker
382*bf47c682SAndroid Build Coastguard Worker  NSTimeInterval averageBestShift = 0;
383*bf47c682SAndroid Build Coastguard Worker  for (int side = 0; side < 2; ++side) {
384*bf47c682SAndroid Build Coastguard Worker    std::vector<NSTimeInterval> lts = Extract(sideLabels, lt, side);
385*bf47c682SAndroid Build Coastguard Worker    NSTimeInterval bestShift = FindBestShift(lts, ft, fy);
386*bf47c682SAndroid Build Coastguard Worker    averageBestShift += bestShift / 2;
387*bf47c682SAndroid Build Coastguard Worker  }
388*bf47c682SAndroid Build Coastguard Worker
389*bf47c682SAndroid Build Coastguard Worker  self.statusLabel.text = [NSString stringWithFormat:@"%.3f s", averageBestShift];
390*bf47c682SAndroid Build Coastguard Worker
391*bf47c682SAndroid Build Coastguard Worker  [self reset:sender];
392*bf47c682SAndroid Build Coastguard Worker}
393*bf47c682SAndroid Build Coastguard Worker@end
394