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