OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2017 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 """Script for automatically measuring motion-to-photon latency for VR. | |
6 | |
7 Doing so requires two specialized pieces of hardware. The first is a Motopho, | |
8 which when used with a VR flicker app, finds the delay between movement and | |
9 the test device's screen updating in response to the movement. The second is | |
10 a ShakerBot, which physically moves the test device and Motopho during the | |
Lei Lei
2017/04/07 21:36:35
I am not sure if it is ok to mention shakerbot pub
bsheedy
2017/04/07 23:24:52
Removed from the comment, and renamed the class to
| |
11 latency test. | |
12 """ | |
13 | |
14 import argparse | |
15 import glob | |
16 import httplib | |
17 import os | |
18 import re | |
19 import serial | |
20 import subprocess | |
21 import sys | |
22 import threading | |
23 import time | |
24 | |
25 # ShakerBot connection constants | |
26 BAUD_RATE = 115200 | |
27 CONNECTION_TIMEOUT = 3.0 | |
28 NUM_TRIES = 5 | |
29 # Motopho constants | |
30 DEFAULT_ADB_PATH = ('/home/gtvchrome/tools/android/android-sdk-linux/' | |
31 'platform-tools/adb') | |
32 DEFAULT_MOTOPHO_PATH = '/home/gtvchrome/motopho/Motopho' | |
33 MOTOPHO_THREAD_TIMEOUT = 30 | |
34 | |
35 class MotophoThread(threading.Thread): | |
36 """Handles the running of the Motopho script and extracting results.""" | |
37 def __init__(self): | |
38 threading.Thread.__init__(self) | |
39 self._latency = None | |
40 self._max_correlation = None | |
41 | |
42 def run(self): | |
Lei Lei
2017/04/07 21:36:35
s/run/Run to keep consistent about naming within f
bsheedy
2017/04/07 23:24:52
It's overriding threading.Thread's run() function.
| |
43 motopho_output = "" | |
44 try: | |
45 motopho_output = subprocess.check_output(["./motophopro_nograph"], | |
46 stderr=subprocess.STDOUT) | |
47 except subprocess.CalledProcessError as e: | |
48 print e.output | |
Lei Lei
2017/04/07 21:36:35
It is better to use logging module for logs instea
bsheedy
2017/04/07 23:24:52
Done.
| |
49 return 1 | |
50 | |
51 if "FAIL" in motopho_output: | |
52 print 'Failed to get latency, printing raw output' | |
53 print motopho_output | |
54 return 1 | |
55 | |
56 self._latency = None | |
57 self._max_correlation = None | |
58 for line in motopho_output.split("\n"): | |
59 if 'Motion-to-photon latency:' in line: | |
60 self._latency = float(line.split(" ")[-2]) | |
61 if 'Max correlation is' in line: | |
62 self._max_correlation = float(line.split(' ')[-1]) | |
63 if self._latency and self._max_correlation: | |
64 break; | |
65 | |
66 @property | |
67 def latency(self): | |
68 return self._latency | |
69 | |
70 @property | |
71 def max_correlation(self): | |
72 return self._max_correlation | |
73 | |
74 | |
75 class ShakerBot(): | |
76 """Handles the serial communication with the ShakerBot.""" | |
77 def __init__(self, device_name, num_tries, baud, timeout): | |
78 self._connection = None | |
79 connected = False | |
80 for attempt in xrange(num_tries): | |
Lei Lei
2017/04/07 21:36:35
nit: attempt is unused, you can add unused prefix
bsheedy
2017/04/07 23:24:52
Replaced with the more standard "_" for unused var
| |
81 try: | |
82 self._connection = serial.Serial('/dev/' + device_name, | |
83 baud, | |
84 timeout=timeout) | |
85 except serial.SerialException as e: | |
86 pass | |
87 if self._connection and 'Enter parameters' in self._connection.read(1024): | |
88 connected = True | |
89 break | |
90 if not connected: | |
91 print 'Failed to connect to ShakerBot' | |
92 sys.exit(1) | |
Lei Lei
2017/04/07 21:36:35
It is better to throw exception here instead of ex
bsheedy
2017/04/07 23:24:52
Done.
| |
93 | |
94 def StartMotophoMovement(self): | |
95 if not self._connection: | |
96 return | |
97 self._connection.write('9\n') | |
98 | |
99 def StopAllMovement(self): | |
100 if not self._connection: | |
101 return | |
102 self._connection.write('0\n') | |
103 | |
104 | |
105 def GetParsedArgs(): | |
106 """Parses the command line arguments passed to the script. | |
107 | |
108 Fails if any unknown arguments are present. | |
109 """ | |
110 parser = argparse.ArgumentParser() | |
111 parser.add_argument('--adb-path', | |
112 type=os.path.realpath, | |
113 help='The absolute path to adb', | |
114 default=DEFAULT_ADB_PATH) | |
115 parser.add_argument('--motopho-path', | |
116 type=os.path.realpath, | |
117 help='The absolute path to the directory with Motopho ' | |
118 'scripts', | |
119 default=DEFAULT_MOTOPHO_PATH) | |
120 (args, unknown_args) = parser.parse_known_args() | |
121 if unknown_args: | |
122 print 'Received unknown args: ' + ' '.join(unknown_args) | |
123 sys.exit(1) | |
Lei Lei
2017/04/07 21:36:35
Considering to use parser.error('unrecognized argu
bsheedy
2017/04/07 23:24:52
Done.
| |
124 return args | |
125 | |
126 | |
127 # Taken from Daydream's perf test suite | |
128 def GetTtyDevices(tty_pattern, vendor_ids): | |
129 """Find all devices connected to tty that match a pattern and device id. | |
130 | |
131 If a serial device is connected to the computer via USB, this function | |
132 will check all tty devices that match tty_pattern, and return the ones | |
133 that have vendor identification number in the list vendor_ids. | |
134 | |
135 Args: | |
136 tty_pattern: The search pattern, such as r'ttyACM\d+'. | |
137 vendor_ids: The list of 16-bit USB vendor ids, such as [0x2a03]. | |
138 | |
139 Returns: | |
140 A list of strings of tty devices, for example ['ttyACM0']. | |
141 """ | |
142 product_string = 'PRODUCT=' | |
143 sys_class_dir = '/sys/class/tty/' | |
144 | |
145 tty_devices = glob.glob(sys_class_dir + '*') | |
146 | |
147 matcher = re.compile('.*' + tty_pattern) | |
148 tty_matches = [x for x in tty_devices if matcher.search(x)] | |
149 tty_matches = [x[len(sys_class_dir):] for x in tty_matches] | |
150 | |
151 found_devices = [] | |
152 for match in tty_matches: | |
153 class_filename = sys_class_dir + match + '/device/uevent' | |
154 with open(class_filename, 'r') as uevent_file: | |
155 # Look for the desired product id in the uevent text. | |
156 for line in uevent_file: | |
157 if product_string in line: | |
158 ids = line[len(product_string):].split('/') | |
159 ids = [int(x, 16) for x in ids] | |
160 | |
161 for desired_id in vendor_ids: | |
162 if desired_id in ids: | |
163 found_devices.append(match) | |
164 | |
165 return found_devices | |
166 | |
167 | |
168 def RunCmdOrFail(cmd): | |
Lei Lei
2017/04/07 21:36:35
Consider to rename it to RunCommand, no need for f
bsheedy
2017/04/07 23:24:52
Done.
| |
169 """Runs the given cmd list. | |
170 | |
171 Prints the command's output and exits if any error occurs. | |
172 """ | |
173 try: | |
174 subprocess.check_output(cmd, stderr=subprocess.STDOUT) | |
175 except subprocess.CalledProcessError as e: | |
176 print e.output | |
177 sys.exit(1) | |
Lei Lei
2017/04/07 21:36:35
It is better to throw the original exception here.
bsheedy
2017/04/07 23:24:52
Done.
| |
178 | |
179 | |
180 def SetChromeCommandLineFlags(adb_path, flags): | |
181 """Sets the given Chrome command line flags. | |
182 | |
183 Puts them in both valid file locations. | |
184 """ | |
185 flag_string = "echo 'chrome " + ' '.join(flags) + "' > " | |
186 RunCmdOrFail([adb_path, | |
187 'shell', flag_string + '/data/local/chrome-command-line']) | |
188 RunCmdOrFail([adb_path, | |
189 'shell', flag_string + '/data/local/tmp/chrome-command-line']) | |
Lei Lei
2017/04/07 21:36:35
Why does it need to set the flags in two files? Ac
bsheedy
2017/04/07 23:24:52
You're right, /data/local/chrome-command-line was
| |
190 | |
191 | |
192 def main(): | |
193 args = GetParsedArgs() | |
194 | |
195 RunCmdOrFail([args.adb_path, 'root']) | |
196 RunCmdOrFail([args.adb_path, 'install', '-r', 'apks/ChromePublic.apk']) | |
197 # Force WebVR support and don't have first run experience | |
198 SetChromeCommandLineFlags(args.adb_path, ['--enable-webvr', '--disable-fre']) | |
199 | |
200 # Motopho scripts use relative paths, so switch to the Motopho directory | |
201 os.chdir(args.motopho_path) | |
202 | |
203 # Connect to the ShakerBot | |
204 devices = GetTtyDevices(r'ttyACM\d+', [0x2a03, 0x2341]) | |
205 if len(devices) != 1: | |
206 print 'Found ' + str(len(devices)) + ' devices, expected 1' | |
207 return 1 | |
208 shaker_bot = ShakerBot(devices[0], NUM_TRIES, BAUD_RATE, CONNECTION_TIMEOUT) | |
209 | |
210 # Wake the device | |
211 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_WAKEUP']) | |
212 # Sleep a bit, otherwise WebGL can crash when Canary starts | |
213 time.sleep(1) | |
214 | |
215 # Start Chrome and go to the flicker app | |
216 # TODO(bsheedy): See about having versioned copies of the flicker app instead | |
217 # of using personal github. | |
218 RunCmdOrFail([args.adb_path, 'shell', 'am', 'start', | |
219 '-a', 'android.intent.action.MAIN', | |
220 '-n', 'org.chromium.chrome/com.google.android.apps.chrome.Main', | |
221 'https://weableandbob.github.io/Motopho/flicker_apps/webvr/webvr- flicker-app-klaus.html?polyfill=0\&canvasClickPresents=1']) | |
222 time.sleep(10) | |
223 | |
224 # Tap the screen to start presenting | |
225 RunCmdOrFail([args.adb_path, 'shell', 'input', 'touchscreen', | |
226 'tap', '800', '800']) | |
227 # Wait for VR to fully start up | |
228 time.sleep(5) | |
229 | |
230 # Start the Motopho script | |
231 motopho_thread = MotophoThread() | |
232 motopho_thread.start() | |
233 # Let the Motopho be stationary so the script can calculate its bias | |
234 time.sleep(3) | |
235 | |
236 # Move so we can measure latency | |
237 shaker_bot.StartMotophoMovement() | |
238 motopho_thread.join(MOTOPHO_THREAD_TIMEOUT) | |
239 if motopho_thread.isAlive(): | |
240 # TODO(bsheedy): Look into ways to prevent Motopho from not sending any | |
241 # data until unplugged and replugged into the machine after a reboot. | |
242 print "Motopho thread join timed out - Motopho might need to be replugged." | |
243 shaker_bot.StopAllMovement() | |
244 | |
245 print "Latency: " + str(motopho_thread.latency) | |
246 print "Max correlation: " + str(motopho_thread.max_correlation) | |
Lei Lei
2017/04/07 21:36:35
The latency and max correlation are also needed to
bsheedy
2017/04/07 23:24:52
Done.
| |
247 | |
248 # Exit VR and Close Chrome | |
249 # TODO(bsheedy): See about closing current tab before exiting so they don't | |
250 # pile up over time. | |
251 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_BACK']) | |
252 RunCmdOrFail([args.adb_path, 'shell', 'am', 'force-stop', | |
253 'org.chromium.chrome']) | |
254 | |
255 # Turn off the screen | |
256 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_POWER']) | |
257 | |
258 return 0 | |
259 | |
260 if __name__ == '__main__': | |
261 sys.exit(main()) | |
OLD | NEW |