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