You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
13 KiB
394 lines
13 KiB
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "DragLatencyController.h"
|
|
|
|
#import <dispatch/dispatch.h>
|
|
#import <math.h>
|
|
#import <numeric>
|
|
#import <vector>
|
|
|
|
#import "UIAlertView+Extensions.h"
|
|
#import "WALTAppDelegate.h"
|
|
#import "WALTClient.h"
|
|
#import "WALTLogger.h"
|
|
#import "WALTTouch.h"
|
|
|
|
static const NSTimeInterval kGoalpostFrequency = 0.55; // TODO(pquinn): User-configurable settings.
|
|
static const NSUInteger kMinTouchEvents = 100;
|
|
static const NSUInteger kMinLaserEvents = 8;
|
|
static const char kWALTLaserTag = 'L';
|
|
|
|
@interface WALTLaserEvent : NSObject
|
|
@property (assign) NSTimeInterval t;
|
|
@property (assign) int value;
|
|
@end
|
|
|
|
@implementation WALTLaserEvent
|
|
@end
|
|
|
|
/** Linear interpolation between x0 and x1 at alpha. */
|
|
template <typename T>
|
|
static T Lerp(const T& x0, const T& x1, double alpha) {
|
|
NSCAssert(alpha >= 0 && alpha <= 1, @"alpha must be between 0 and 1 (%f)", alpha);
|
|
return ((1 - alpha) * x0) + (alpha * x1);
|
|
}
|
|
|
|
/** Linear interpolation of (xp, yp) at x. */
|
|
template <typename S, typename T>
|
|
static std::vector<T> Interpolate(const std::vector<S>& x,
|
|
const std::vector<S>& xp,
|
|
const std::vector<T>& yp) {
|
|
NSCAssert(xp.size(), @"xp must contain at least one value.");
|
|
NSCAssert(xp.size() == yp.size(), @"xp and yp must have matching lengths.");
|
|
|
|
std::vector<T> y;
|
|
y.reserve(x.size());
|
|
|
|
size_t i = 0; // Index into x.
|
|
|
|
for (; i < x.size() && x[i] < xp.front(); ++i) {
|
|
y.push_back(yp.front()); // Pad out y with yp.front() for x values before xp.front().
|
|
}
|
|
|
|
size_t ip = 0; // Index into xp/yp.
|
|
|
|
for (; ip < xp.size() && i < x.size(); ++i) {
|
|
while (ip < xp.size() && xp[ip] <= x[i]) { // Find an xp[ip] greater than x[i].
|
|
++ip;
|
|
}
|
|
if (ip >= xp.size()) {
|
|
break; // Ran out of values.
|
|
}
|
|
|
|
const double alpha = (x[i] - xp[ip - 1]) / static_cast<double>(xp[ip] - xp[ip - 1]);
|
|
y.push_back(Lerp(yp[ip - 1], yp[ip], alpha));
|
|
}
|
|
|
|
for (; i < x.size(); ++i) {
|
|
y.push_back(yp.back()); // Pad out y with yp.back() for values after xp.back().
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
/** Extracts the values of y where the corresponding value in x is equal to value. */
|
|
template <typename S, typename T>
|
|
static std::vector<S> Extract(const std::vector<T>& x, const std::vector<S>& y, const T& value) {
|
|
NSCAssert(x.size() == y.size(), @"x and y must have matching lengths.");
|
|
std::vector<S> extracted;
|
|
|
|
for (size_t i = 0; i < x.size(); ++i) {
|
|
if (x[i] == value) {
|
|
extracted.push_back(y[i]);
|
|
}
|
|
}
|
|
|
|
return extracted;
|
|
}
|
|
|
|
/** Returns the standard deviation of the values in x. */
|
|
template <typename T>
|
|
static T StandardDeviation(const std::vector<T>& x) {
|
|
NSCAssert(x.size() > 0, @"x must have at least one value.");
|
|
const T sum = std::accumulate(x.begin(), x.end(), T{});
|
|
const T mean = sum / x.size();
|
|
const T ss = std::accumulate(x.begin(), x.end(), T{}, ^(T accum, T value){
|
|
return accum + ((value - mean) * (value - mean));
|
|
});
|
|
return sqrt(ss / (x.size() - 1));
|
|
}
|
|
|
|
/** Returns the index of the smallest value in x. */
|
|
template <typename T>
|
|
static size_t ArgMin(const std::vector<T>& x) {
|
|
NSCAssert(x.size() > 0, @"x must have at least one value.");
|
|
size_t imin = 0;
|
|
for (size_t i = 1; i < x.size(); ++i) {
|
|
if (x[i] < x[imin]) {
|
|
imin = i;
|
|
}
|
|
}
|
|
return imin;
|
|
}
|
|
|
|
/**
|
|
* Finds a positive time value that shifting laserTs by will minimise the standard deviation of
|
|
* interpolated touchYs.
|
|
*/
|
|
static NSTimeInterval FindBestShift(const std::vector<NSTimeInterval>& laserTs,
|
|
const std::vector<NSTimeInterval>& touchTs,
|
|
const std::vector<CGFloat>& touchYs) {
|
|
NSCAssert(laserTs.size() > 0, @"laserTs must have at least one value.");
|
|
NSCAssert(touchTs.size() == touchYs.size(), @"touchTs and touchYs must have matching lengths.");
|
|
|
|
const NSTimeInterval kSearchCoverage = 0.15;
|
|
const int kSteps = 1500;
|
|
const NSTimeInterval kShiftStep = kSearchCoverage / kSteps;
|
|
|
|
std::vector<NSTimeInterval> deviations;
|
|
deviations.reserve(kSteps);
|
|
|
|
std::vector<NSTimeInterval> ts(laserTs.size());
|
|
for (int i = 0; i < kSteps; ++i) {
|
|
for (size_t j = 0; j < laserTs.size(); ++j) {
|
|
ts[j] = laserTs[j] + (kShiftStep * i);
|
|
}
|
|
|
|
std::vector<CGFloat> laserYs = Interpolate(ts, touchTs, touchYs);
|
|
deviations.push_back(StandardDeviation(laserYs));
|
|
}
|
|
|
|
return ArgMin(deviations) * kShiftStep;
|
|
}
|
|
|
|
@interface DragLatencyController ()
|
|
- (void)updateCountDisplay;
|
|
- (void)processEvent:(UIEvent *)event;
|
|
- (void)receiveTriggers:(id)context;
|
|
- (void)stopReceiver;
|
|
@end
|
|
|
|
@implementation DragLatencyController {
|
|
WALTClient *_client;
|
|
WALTLogger *_logger;
|
|
|
|
NSMutableArray<WALTTouch *> *_touchEvents;
|
|
NSMutableArray<WALTLaserEvent *> *_laserEvents;
|
|
|
|
NSThread *_triggerReceiver;
|
|
dispatch_semaphore_t _receiverComplete;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self stopReceiver];
|
|
}
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
|
|
_client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client;
|
|
_logger = [WALTLogger sessionLogger];
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
[super viewWillAppear:animated];
|
|
|
|
[self updateCountDisplay];
|
|
|
|
[_logger appendString:@"DRAGLATENCY\n"];
|
|
}
|
|
|
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
[self processEvent:event];
|
|
}
|
|
|
|
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
[self processEvent:event];
|
|
}
|
|
|
|
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
[self processEvent:event];
|
|
}
|
|
|
|
- (void)processEvent:(UIEvent *)event {
|
|
// TODO(pquinn): Pull out coalesced touches.
|
|
|
|
WALTTouch *touch = [[WALTTouch alloc] initWithEvent:event];
|
|
[_touchEvents addObject:touch];
|
|
[_logger appendFormat:@"TOUCH\t%.3f\t%.2f\t%.2f\n",
|
|
touch.kernelTime, touch.location.x, touch.location.y];
|
|
[self updateCountDisplay];
|
|
}
|
|
|
|
- (void)updateCountDisplay {
|
|
NSString *counts = [NSString stringWithFormat:@"N ✛ %lu ⇄ %lu",
|
|
(unsigned long)_laserEvents.count, (unsigned long)_touchEvents.count];
|
|
self.countLabel.text = counts;
|
|
}
|
|
|
|
- (IBAction)start:(id)sender {
|
|
[self reset:sender];
|
|
|
|
self.goalpostView.hidden = NO;
|
|
self.statusLabel.text = @"";
|
|
|
|
[UIView beginAnimations:@"Goalpost" context:NULL];
|
|
[UIView setAnimationDuration:kGoalpostFrequency];
|
|
[UIView setAnimationBeginsFromCurrentState:NO];
|
|
[UIView setAnimationRepeatCount:FLT_MAX];
|
|
[UIView setAnimationRepeatAutoreverses:YES];
|
|
|
|
self.goalpostView.transform =
|
|
CGAffineTransformMakeTranslation(0.0, -CGRectGetHeight(self.view.frame) + 300);
|
|
|
|
[UIView commitAnimations];
|
|
|
|
_receiverComplete = dispatch_semaphore_create(0);
|
|
_triggerReceiver = [[NSThread alloc] initWithTarget:self
|
|
selector:@selector(receiveTriggers:)
|
|
object:nil];
|
|
[_triggerReceiver start];
|
|
}
|
|
|
|
- (IBAction)reset:(id)sender {
|
|
[self stopReceiver];
|
|
|
|
self.goalpostView.transform = CGAffineTransformMakeTranslation(0.0, 0.0);
|
|
self.goalpostView.hidden = YES;
|
|
|
|
_touchEvents = [[NSMutableArray<WALTTouch *> alloc] init];
|
|
_laserEvents = [[NSMutableArray<WALTLaserEvent *> alloc] init];
|
|
|
|
[self updateCountDisplay];
|
|
|
|
NSError *error = nil;
|
|
if (![_client syncClocksWithError:&error]) {
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
|
|
[alert show];
|
|
}
|
|
|
|
[_logger appendString:@"RESET\n"];
|
|
}
|
|
|
|
- (void)receiveTriggers:(id)context {
|
|
// Turn on laser change notifications.
|
|
NSError *error = nil;
|
|
if (![_client sendCommand:WALTLaserOnCommand error:&error]) {
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
|
|
[alert show];
|
|
dispatch_semaphore_signal(_receiverComplete);
|
|
return;
|
|
}
|
|
|
|
NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout];
|
|
if (![_client checkResponse:response forCommand:WALTLaserOnCommand]) {
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error"
|
|
message:@"Failed to start laser probe."
|
|
delegate:nil
|
|
cancelButtonTitle:@"Dismiss"
|
|
otherButtonTitles:nil];
|
|
[alert show];
|
|
dispatch_semaphore_signal(_receiverComplete);
|
|
return;
|
|
}
|
|
|
|
while (!NSThread.currentThread.isCancelled) {
|
|
WALTTrigger response = [_client readTriggerWithTimeout:kWALTReadTimeout];
|
|
if (response.tag == kWALTLaserTag) {
|
|
WALTLaserEvent *event = [[WALTLaserEvent alloc] init];
|
|
event.t = response.t;
|
|
event.value = response.value;
|
|
[_laserEvents addObject:event];
|
|
[_logger appendFormat:@"LASER\t%.3f\t%d\n", event.t, event.value];
|
|
} else if (response.tag != '\0') { // Don't fail for timeout errors.
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error"
|
|
message:@"Failed to read laser probe."
|
|
delegate:nil
|
|
cancelButtonTitle:@"Dismiss"
|
|
otherButtonTitles:nil];
|
|
[alert show];
|
|
}
|
|
}
|
|
|
|
// Turn off laser change notifications.
|
|
[_client sendCommand:WALTLaserOffCommand error:nil];
|
|
[_client readResponseWithTimeout:kWALTReadTimeout];
|
|
|
|
dispatch_semaphore_signal(_receiverComplete);
|
|
}
|
|
|
|
- (void)stopReceiver {
|
|
// TODO(pquinn): This will deadlock if called in rapid succession -- there is a small delay
|
|
// between dispatch_semaphore_signal() and -[NSThread isExecuting] changing.
|
|
// Unfortunately, NSThread is not joinable...
|
|
if (_triggerReceiver.isExecuting) {
|
|
[_triggerReceiver cancel];
|
|
dispatch_semaphore_wait(_receiverComplete, DISPATCH_TIME_FOREVER);
|
|
}
|
|
}
|
|
|
|
- (IBAction)computeStatistics:(id)sender {
|
|
if (_touchEvents.count < kMinTouchEvents) {
|
|
self.statusLabel.text =
|
|
[NSString stringWithFormat:@"Too few touch events (%lu/%lu).",
|
|
(unsigned long)_touchEvents.count, (unsigned long)kMinTouchEvents];
|
|
[self reset:sender];
|
|
return;
|
|
}
|
|
|
|
// Timestamps are reset to be relative to t0 to make the output easier to read.
|
|
const NSTimeInterval t0 = _touchEvents.firstObject.kernelTime;
|
|
const NSTimeInterval tF = _touchEvents.lastObject.kernelTime;
|
|
|
|
std::vector<NSTimeInterval> ft(_touchEvents.count);
|
|
std::vector<CGFloat> fy(_touchEvents.count);
|
|
for (NSUInteger i = 0; i < _touchEvents.count; ++i) {
|
|
ft[i] = _touchEvents[i].kernelTime - t0;
|
|
fy[i] = _touchEvents[i].location.y;
|
|
}
|
|
|
|
// Remove laser events that have a timestamp outside [t0, tF].
|
|
[_laserEvents filterUsingPredicate:[NSPredicate predicateWithBlock:
|
|
^BOOL(WALTLaserEvent *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
|
|
return evaluatedObject.t >= t0 && evaluatedObject.t <= tF;
|
|
}]];
|
|
|
|
if (_laserEvents.count < kMinLaserEvents) {
|
|
self.statusLabel.text =
|
|
[NSString stringWithFormat:@"Too few laser events (%lu/%lu).",
|
|
(unsigned long)_laserEvents.count, (unsigned long)kMinLaserEvents];
|
|
[self reset:sender];
|
|
return;
|
|
}
|
|
|
|
if (_laserEvents.firstObject.value != 0) {
|
|
self.statusLabel.text = @"First laser crossing was not into the beam.";
|
|
[self reset:sender];
|
|
return;
|
|
}
|
|
|
|
std::vector<NSTimeInterval> lt(_laserEvents.count);
|
|
std::vector<int> lv(_laserEvents.count);
|
|
for (NSUInteger i = 0; i < _laserEvents.count; ++i) {
|
|
lt[i] = _laserEvents[i].t - t0;
|
|
lv[i] = _laserEvents[i].value;
|
|
}
|
|
|
|
// Calculate interpolated touch y positions at each laser event.
|
|
std::vector<CGFloat> ly = Interpolate(lt, ft, fy);
|
|
|
|
// Labels for each laser event to denote those above/below the beam.
|
|
// The actual side is irrelevant, but events on the same side should have the same label. The
|
|
// vector will look like [0, 1, 1, 0, 0, 1, 1, 0, 0, ...].
|
|
std::vector<int> sideLabels(lt.size());
|
|
for (size_t i = 0; i < lt.size(); ++i) {
|
|
sideLabels[i] = ((i + 1) / 2) % 2;
|
|
}
|
|
|
|
NSTimeInterval averageBestShift = 0;
|
|
for (int side = 0; side < 2; ++side) {
|
|
std::vector<NSTimeInterval> lts = Extract(sideLabels, lt, side);
|
|
NSTimeInterval bestShift = FindBestShift(lts, ft, fy);
|
|
averageBestShift += bestShift / 2;
|
|
}
|
|
|
|
self.statusLabel.text = [NSString stringWithFormat:@"%.3f s", averageBestShift];
|
|
|
|
[self reset:sender];
|
|
}
|
|
@end
|