/* * 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 "ScreenResponseController.h" #include <stdatomic.h> #import "NSArray+Extensions.h" #import "UIAlertView+Extensions.h" #import "WALTAppDelegate.h" #import "WALTClient.h" #import "WALTLogger.h" static const NSUInteger kMaxFlashes = 20; // TODO(pquinn): Make this user-configurable. static const NSTimeInterval kFlashingInterval = 0.1; static const char kWALTScreenTag = 'S'; @interface ScreenResponseController () - (void)setFlashTimer; - (void)flash:(NSTimer *)timer; @end @implementation ScreenResponseController { WALTClient *_client; WALTLogger *_logger; NSTimer *_flashTimer; NSOperationQueue *_readOperations; // Statistics NSUInteger _initiatedFlashes; NSUInteger _detectedFlashes; _Atomic NSTimeInterval _lastFlashTime; NSMutableArray<NSNumber *> *_deltas; } - (void)dealloc { [_readOperations cancelAllOperations]; [_flashTimer invalidate]; } - (void)viewDidLoad { [super viewDidLoad]; _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; _logger = [WALTLogger sessionLogger]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [_logger appendString:@"SCREENRESPONSE\n"]; [self reset:nil]; } - (IBAction)start:(id)sender { [self reset:nil]; // Clear the screen trigger on the WALT. NSError *error = nil; if (![_client sendCommand:WALTSendLastScreenCommand error:&error]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; [alert show]; return; } WALTTrigger trigger = [_client readTriggerWithTimeout:kWALTReadTimeout]; if (trigger.tag != kWALTScreenTag) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" message:@"Failed to read last screen trigger." delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; [alert show]; return; } // Create a queue for work blocks to read WALT trigger responses. _readOperations = [[NSOperationQueue alloc] init]; _readOperations.maxConcurrentOperationCount = 1; // Start the flash timer and spawn a thread to check for responses. [self setFlashTimer]; } - (void)setFlashTimer { _flashTimer = [NSTimer scheduledTimerWithTimeInterval:kFlashingInterval target:self selector:@selector(flash:) userInfo:nil repeats:NO]; } - (IBAction)computeStatistics:(id)sender { self.flasherView.hidden = YES; self.responseLabel.hidden = NO; NSMutableString *results = [[NSMutableString alloc] init]; for (NSNumber *delta in _deltas) { [results appendFormat:@"%.3f s\n", delta.doubleValue]; } [results appendFormat:@"Median: %.3f s\n", [_deltas medianValue].doubleValue]; self.responseLabel.text = results; } - (IBAction)reset:(id)sender { _initiatedFlashes = 0; _detectedFlashes = 0; _deltas = [[NSMutableArray<NSNumber *> alloc] init]; [_readOperations cancelAllOperations]; [_flashTimer invalidate]; self.flasherView.hidden = NO; self.flasherView.backgroundColor = [UIColor whiteColor]; self.responseLabel.hidden = YES; NSError *error = nil; if (![_client syncClocksWithError:&error]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; [alert show]; } [_logger appendString:@"RESET\n"]; } - (void)flash:(NSTimer *)timer { if (_initiatedFlashes == 0) { // First flash. // Turn on brightness change notifications. NSError *error = nil; if (![_client sendCommand:WALTScreenOnCommand error:&error]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; [alert show]; return; } NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout]; if (![_client checkResponse:response forCommand:WALTScreenOnCommand]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" message:@"Failed to start screen probe." delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; [alert show]; return; } } if (_initiatedFlashes != kMaxFlashes) { // Swap the background colour and record the time. self.flasherView.backgroundColor = ([self.flasherView.backgroundColor isEqual:[UIColor blackColor]] ? [UIColor whiteColor] : [UIColor blackColor]); atomic_store(&_lastFlashTime, _client.currentTime); ++_initiatedFlashes; // Queue an operation to read the trigger. [_readOperations addOperationWithBlock:^{ // NB: The timeout here should be much greater than the expected screen response time. WALTTrigger response = [_client readTriggerWithTimeout:kWALTReadTimeout]; if (response.tag == kWALTScreenTag) { ++_detectedFlashes; // Record the delta between the trigger and the flash time. NSTimeInterval lastFlash = atomic_load(&_lastFlashTime); NSTimeInterval delta = response.t - lastFlash; if (delta > 0) { // Sanity check [_deltas addObject:[NSNumber numberWithDouble:delta]]; [_logger appendFormat:@"O\t%f\n", delta]; } else { [_logger appendFormat:@"X\tbogus delta\t%f\t%f\n", lastFlash, response.t]; } // Queue up another flash. [self performSelectorOnMainThread:@selector(setFlashTimer) withObject:nil waitUntilDone:NO]; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" message:@"Failed to read screen probe." delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; [alert show]; } }]; } if (_initiatedFlashes == kMaxFlashes) { // Queue an operation (after the read trigger above) to turn off brightness notifications. [_readOperations addOperationWithBlock:^{ [_client sendCommand:WALTScreenOffCommand error:nil]; [_client readResponseWithTimeout:kWALTReadTimeout]; [self performSelectorOnMainThread:@selector(computeStatistics:) withObject:nil waitUntilDone:NO]; }]; } } @end