Home | History | Annotate | Download | only in pywalt
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2016 The Chromium OS Authors. All rights reserved.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 """
     19 Runs the touchpad drag latency test using WALT Latency Timer
     20 Usage example:
     21     $ python walt.py 11
     22     Input device   : /dev/input/event11
     23     Serial device  : /dev/ttyACM1
     24     Laser log file : /tmp/WALT_2016_06_23__1714_51_laser.log
     25     evtest log file: /tmp/WALT_2016_06_23__1714_51_evtest.log
     26     Clock zeroed at 1466716492 (rt 0.284ms)
     27     ........................................
     28     Processing data, may take a minute or two...
     29     Drag latency (min method) = 15.37 ms
     30 
     31 Note, before running this script, check that evtest can grab the device.
     32 On some systems it requires running as root.
     33 """
     34 
     35 import argparse
     36 import contextlib
     37 import glob
     38 import os
     39 import random
     40 import re
     41 import socket
     42 import subprocess
     43 import sys
     44 import tempfile
     45 import threading
     46 import time
     47 
     48 import serial
     49 import numpy
     50 
     51 import evparser
     52 import minimization
     53 import screen_stats
     54 
     55 
     56 # Time units
     57 MS = 1e-3  # MS = 0.001 seconds
     58 US = 1e-6  # US = 10^-6 seconds
     59 
     60 # Globals
     61 debug_mode = True
     62 
     63 
     64 def log(msg):
     65     if debug_mode:
     66         print(msg)
     67 
     68 
     69 class Walt(object):
     70     """ A class for communicating with Walt device
     71 
     72     Usage:
     73     with Walt('/dev/ttyUSB0') as walt:
     74         body....
     75 
     76 
     77     """
     78 
     79     # Teensy commands, always singe char. Defined in WALT.ino
     80     # github.com/google/walt/blob/master/arduino/walt/walt.ino
     81     CMD_RESET = 'F'
     82     CMD_PING = 'P'
     83     CMD_SYNC_ZERO = 'Z'
     84     CMD_SYNC_SEND = 'I'
     85     CMD_SYNC_READOUT = 'R'
     86     CMD_TIME_NOW = 'T'
     87     CMD_AUTO_LASER_ON = 'L'
     88     CMD_AUTO_LASER_OFF = 'l'
     89     CMD_AUTO_SCREEN_ON = 'C'
     90     CMD_AUTO_SCREEN_OFF = 'c'
     91     CMD_GSHOCK = 'G'
     92     CMD_VERSION = 'V'
     93     CMD_SAMPLE_ALL = 'Q'
     94     CMD_BRIGHTNESS_CURVE = 'U'
     95     CMD_AUDIO = 'A'
     96 
     97 
     98     def __init__(self, serial_dev, timeout=None, encoding='utf-8'):
     99         self.encoding = encoding
    100         self.serial_dev = serial_dev
    101         self.ser = serial.Serial(serial_dev, baudrate=115200, timeout=timeout)
    102         self.base_time = None
    103         self.min_lag = None
    104         self.max_lag = None
    105         self.median_latency = None
    106 
    107     def __enter__(self):
    108         return self
    109 
    110     def __exit__(self, exc_type, exc_value, traceback):
    111         try:
    112             self.ser.close()
    113         except:
    114             pass
    115 
    116     def close(self):
    117         self.ser.close()
    118 
    119     def readline(self):
    120         return self.ser.readline().decode(self.encoding)
    121 
    122     def sndrcv(self, data):
    123         """ Send a 1-char command.
    124         Return the reply and how long it took.
    125 
    126         """
    127         t0 = time.time()
    128         self.ser.write(data.encode(self.encoding))
    129         reply = self.ser.readline()
    130         reply = reply.decode(self.encoding)
    131         t1 = time.time()
    132         dt = (t1 - t0)
    133         log('sndrcv(): round trip %.3fms, reply=%s' % (dt / MS, reply.strip()))
    134         return dt, reply
    135 
    136     def read_shock_time(self):
    137         dt, s = self.sndrcv(Walt.CMD_GSHOCK)
    138         t_us = int(s.strip())
    139         return t_us
    140 
    141 
    142     def run_comm_stats(self, N=100):
    143         """
    144         Measure the USB serial round trip time.
    145         Send CMD_TIME_NOW to the Teensy N times measuring the round trip each time.
    146         Prints out stats (min, median, max).
    147 
    148         """
    149         log('Running USB comm stats...')
    150         self.ser.flushInput()
    151         self.sndrcv(Walt.CMD_SYNC_ZERO)
    152         tstart = time.time()
    153         times = numpy.zeros((N, 1))
    154         for i in range(N):
    155             dt, _ = self.sndrcv(Walt.CMD_TIME_NOW)
    156             times[i] = dt
    157         t_total = time.time() - tstart
    158 
    159         median = numpy.median(times)
    160         stats = (times.min() / MS, median / MS, times.max() / MS, N)
    161         self.median_latency = median
    162         log('USB comm round trip stats:')
    163         log('min=%.2fms, median=%.2fms, max=%.2fms N=%d' % stats)
    164         if (median > 2):
    165             print('ERROR: the median round trip is too high: %.2f ms' % (median / MS) )
    166             sys.exit(2)
    167 
    168     def zero_clock(self, max_delay=0.001, retries=10):
    169         """
    170         Tell the TeensyUSB to zero its clock (CMD_SYNC_ZERO).
    171         Returns the time when the command was sent.
    172         Verify that the response arrived within max_delay seconds.
    173 
    174         This is the simple zeroing used when the round trip is fast.
    175         It does not employ the same method as Android clock sync.
    176         """
    177 
    178         # Check that we get reasonable ping time with Teensy
    179         # this also 'warms up' the comms, first msg is often slower
    180         self.run_comm_stats(N=10)
    181 
    182         self.ser.flushInput()
    183 
    184         for i in range(retries):
    185             t0 = time.time()
    186             dt, _ = self.sndrcv(Walt.CMD_SYNC_ZERO)
    187             if dt < max_delay:
    188                 print('Clock zeroed at %.0f (rt %.3f ms)' % (t0, dt / MS))
    189                 self.base_time = t0
    190                 self.max_lag = dt
    191                 self.min_lag = 0
    192                 return t0
    193         print('Error, failed to zero the clock after %d retries')
    194         return -1
    195 
    196     def read_remote_times(self):
    197         """ Helper func, see doc string in estimate_lage()
    198         Read out the timestamps taken recorded by the Teensy.
    199         """
    200         times = numpy.zeros(9)
    201         for i in range(9):
    202             dt, reply = self.sndrcv(Walt.CMD_SYNC_READOUT)
    203             num, tstamp = reply.strip().split(':')
    204             # TODO: verify that num is what we expect it to be
    205             log('read_remote_times() CMD_SYNC_READOUT > w >  = %s' % reply)
    206             t = float(tstamp) * US  # WALT sends timestamps in microseconds
    207             times[i] = t
    208         return times
    209 
    210     def estimate_lag(self):
    211         """ Estimate the difference between local and remote clocks
    212 
    213         This is based on:
    214         github.com/google/walt/blob/master/android/WALT/app/src/main/jni/README.md
    215 
    216         self.base_time needs to be set using self.zero_clock() before running
    217         this function.
    218 
    219         The result is saved as self.min_lag and self.max_lag. Assume that the
    220         remote clock lags behind the local by `lag` That is, at a given moment
    221         local_time = remote_time + lag
    222         where local_time = time.time() - self.base_time
    223 
    224         Immediately after this function completes the lag is guaranteed to be
    225         between min_lag and max_lag. But the lag change (drift) away with time.
    226         """
    227         self.ser.flushInput()
    228 
    229         # remote -> local
    230         times_local_received = numpy.zeros(9)
    231         self.ser.write(Walt.CMD_SYNC_SEND)
    232         for i in range(9):
    233             reply = self.ser.readline()
    234             times_local_received[i] = time.time() - self.base_time
    235 
    236         times_remote_sent = self.read_remote_times()
    237         max_lag = (times_local_received - times_remote_sent).min()
    238 
    239         # local -> remote
    240         times_local_sent = numpy.zeros(9)
    241         for i in range(9):
    242             s = '%d' % (i + 1)
    243             # Sleep between the messages to combat buffering
    244             t_sleep = US * random.randint(70, 700)
    245             time.sleep(t_sleep)
    246             times_local_sent[i] = time.time() - self.base_time
    247             self.ser.write(s)
    248 
    249         times_remote_received = self.read_remote_times()
    250         min_lag = (times_local_sent - times_remote_received).max()
    251 
    252         self.min_lag = min_lag
    253         self.max_lag = max_lag
    254 
    255     def parse_trigger(self, trigger_line):
    256         """ Parse a trigger line from WALT.
    257 
    258         Trigger events look like this: "G L 12902345 1 1"
    259         The parts:
    260          * G - common for all trigger events
    261          * L - means laser
    262          * 12902345 is timestamp in us since zeroed
    263          * 1st 1 or 0 is trigger value. 0 = changed to dark, 1 = changed to light,
    264          * 2nd 1 is counter of how many times this trigger happened since last
    265            readout, should always be 1 in our case
    266 
    267         """
    268 
    269         parts = trigger_line.strip().split()
    270         if len(parts) != 5:
    271             raise Exception('Malformed trigger line: "%s"\n' % trigger_line)
    272         t_us = int(parts[2])
    273         val = int(parts[3])
    274         return (t_us / 1e6, val)
    275 
    276 
    277 def array2str(a):
    278     a_strs = ['%0.2f' % x for x in a]
    279     s = ', '.join(a_strs)
    280     return '[' + s + ']'
    281 
    282 
    283 def parse_args(argv):
    284     temp_dir = tempfile.gettempdir()
    285     serial = '/dev/ttyACM0'
    286 
    287     # Try to autodetect the WALT serial port
    288     ls_ttyACM = glob.glob('/dev/ttyACM*')
    289     if len(ls_ttyACM) > 0:
    290         serial = ls_ttyACM[0]
    291 
    292     description = "Run a latency test using WALT Latency Timer"
    293     parser = argparse.ArgumentParser(
    294         description=description,
    295         formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    296 
    297     parser.add_argument('-i', '--input',
    298                         help='input device, e.g: 6 or /dev/input/event6')
    299     parser.add_argument('-s', '--serial', default=serial,
    300                         help='WALT serial port')
    301     parser.add_argument('-t', '--type',
    302                         help='Test type: drag|tap|screen|sanity|curve|bridge|'
    303                              'tapaudio|tapblink')
    304     parser.add_argument('-l', '--logdir', default=temp_dir,
    305                         help='where to store logs')
    306     parser.add_argument('-n', default=40, type=int,
    307                         help='Number of laser toggles to read')
    308     parser.add_argument('-p', '--port', default=50007, type=int,
    309                         help='port to listen on for the TCP bridge')
    310     parser.add_argument('-d', '--debug', action='store_true',
    311                         help='talk more')
    312     args = parser.parse_args(argv)
    313 
    314     if not args.type:
    315         parser.print_usage()
    316         sys.exit(0)
    317 
    318     global debug_mode
    319     debug_mode = args.debug
    320 
    321     if args.input and args.input.isalnum():
    322         args.input = '/dev/input/event' + args.input
    323 
    324     return args
    325 
    326 
    327 def run_drag_latency_test(args):
    328 
    329     if not args.input:
    330         print('Error: --input argument is required for drag latency test')
    331         sys.exit(1)
    332 
    333     # Create names for log files
    334     prefix = time.strftime('WALT_%Y_%m_%d__%H%M_%S')
    335     laser_file_name = os.path.join(args.logdir,  prefix + '_laser.log')
    336     evtest_file_name = os.path.join(args.logdir,  prefix + '_evtest.log')
    337 
    338     print('Starting drag latency test')
    339     print('Input device   : ' + args.input)
    340     print('Serial device  : ' + args.serial)
    341     print('Laser log file : ' + laser_file_name)
    342     print('evtest log file: ' + evtest_file_name)
    343 
    344     with Walt(args.serial) as walt:
    345         walt.sndrcv(Walt.CMD_RESET)
    346         tstart = time.time()
    347         t_zero = walt.zero_clock()
    348         if t_zero < 0:
    349             print('Error: Couldn\'t zero clock, exiting')
    350             sys.exit(1)
    351 
    352         # Fire up the evtest process
    353         cmd = 'evtest %s > %s' % (args.input, evtest_file_name)
    354         evtest = subprocess.Popen(cmd, shell=True)
    355 
    356         # Turn on laser trigger auto-sending
    357         walt.sndrcv(Walt.CMD_AUTO_LASER_ON)
    358         trigger_count = 0
    359         while trigger_count < args.n:
    360             # The following line blocks until a message from WALT arrives
    361             trigger_line = walt.readline()
    362             trigger_count += 1
    363             log('#%d/%d - ' % (trigger_count, args.n) +
    364                 trigger_line.strip())
    365 
    366             if not debug_mode:
    367                 sys.stdout.write('.')
    368                 sys.stdout.flush()
    369 
    370             t, val = walt.parse_trigger(trigger_line)
    371             t += t_zero
    372             with open(laser_file_name, 'at') as flaser:
    373                 flaser.write('%.3f %d\n' % (t, val))
    374         walt.sndrcv(Walt.CMD_AUTO_LASER_OFF)
    375 
    376     # Send SIGTERM to evtest process
    377     evtest.terminate()
    378 
    379     print("\nProcessing data, may take a minute or two...")
    380     # lm.main(evtest_file_name, laser_file_name)
    381     minimization.minimize(evtest_file_name, laser_file_name)
    382 
    383 
    384 def run_screen_curve(args):
    385 
    386     with Walt(args.serial, timeout=1) as walt:
    387         walt.sndrcv(Walt.CMD_RESET)
    388 
    389         t_zero = walt.zero_clock()
    390         if t_zero < 0:
    391             print('Error: Couldn\'t zero clock, exiting')
    392             sys.exit(1)
    393 
    394         # Fire up the walt_blinker process
    395         cmd = 'blink_test 1'
    396         blinker = subprocess.Popen(cmd, shell=True)
    397 
    398         # Request screen brightness data
    399         walt.sndrcv(Walt.CMD_BRIGHTNESS_CURVE)
    400         s = 'dummy'
    401         while s:
    402             s = walt.readline()
    403             print(s.strip())
    404 
    405 
    406 def run_screen_latency_test(args):
    407 
    408     # Create names for log files
    409     prefix = time.strftime('WALT_%Y_%m_%d__%H%M_%S')
    410     sensor_file_name = os.path.join(args.logdir,  prefix + '_screen_sensor.log')
    411     blinker_file_name = os.path.join(args.logdir,  prefix + '_blinker.log')
    412 
    413     print('Starting screen latency test')
    414     print('Serial device  : ' + args.serial)
    415     print('Sensor log file : ' + sensor_file_name)
    416     print('Blinker log file: ' + blinker_file_name)
    417 
    418     with Walt(args.serial, timeout=1) as walt:
    419         walt.sndrcv(Walt.CMD_RESET)
    420 
    421         t_zero = walt.zero_clock()
    422         if t_zero < 0:
    423             print('Error: Couldn\'t zero clock, exiting')
    424             sys.exit(1)
    425 
    426         # Fire up the walt_blinker process
    427         cmd = 'blink_test %d > %s' % (args.n, blinker_file_name, )
    428         blinker = subprocess.Popen(cmd, shell=True)
    429 
    430         # Turn on screen trigger auto-sending
    431         walt.sndrcv(Walt.CMD_AUTO_SCREEN_ON)
    432         trigger_count = 0
    433 
    434         # Iterate while the blinker process is alive
    435         # TODO: re-sync clocks every once in a while
    436         while blinker.poll() is None:
    437             # The following line blocks until a message from WALT arrives
    438             trigger_line = walt.readline()
    439             if not trigger_line:
    440                 # This usually happens when readline timeouts on last iteration
    441                 continue
    442             trigger_count += 1
    443             log('#%d/%d - ' % (trigger_count, args.n) +
    444                 trigger_line.strip())
    445 
    446             if not debug_mode:
    447                 sys.stdout.write('.')
    448                 sys.stdout.flush()
    449 
    450             t, val = walt.parse_trigger(trigger_line)
    451             t += t_zero
    452             with open(sensor_file_name, 'at') as flaser:
    453                 flaser.write('%.3f %d\n' % (t, val))
    454         walt.sndrcv(Walt.CMD_AUTO_SCREEN_OFF)
    455     screen_stats.screen_stats(blinker_file_name, sensor_file_name)
    456 
    457 
    458 def run_tap_audio_test(args):
    459     print('Starting tap-to-audio latency test')
    460     with Walt(args.serial) as walt:
    461         walt.sndrcv(Walt.CMD_RESET)
    462         t_zero = walt.zero_clock()
    463         if t_zero < 0:
    464             print('Error: Couldn\'t zero clock, exiting')
    465             sys.exit(1)
    466 
    467         walt.sndrcv(Walt.CMD_GSHOCK)
    468         deltas = []
    469         while len(deltas) < args.n:
    470             sys.stdout.write('\rWAIT   ')
    471             sys.stdout.flush()
    472             time.sleep(1)  # Wait for previous beep to stop playing
    473             while walt.read_shock_time() != 0:
    474                 pass  # skip shocks during sleep
    475             sys.stdout.write('\rTAP NOW')
    476             sys.stdout.flush()
    477             walt.sndrcv(Walt.CMD_AUDIO)
    478             trigger_line = walt.readline()
    479             beep_time_seconds, val = walt.parse_trigger(trigger_line)
    480             beep_time_ms = beep_time_seconds * 1e3
    481             shock_time_ms = walt.read_shock_time() / 1e3
    482             if shock_time_ms == 0:
    483                 print("\rNo shock detected, skipping this event")
    484                 continue
    485             dt = beep_time_ms - shock_time_ms
    486             deltas.append(dt)
    487             print("\rdt=%0.1f ms" % dt)
    488         print('Median tap-to-audio latency: %0.1f ms' % numpy.median(deltas))
    489 
    490 
    491 def run_tap_blink_test(args):
    492     print('Starting tap-to-blink latency test')
    493     with Walt(args.serial) as walt:
    494         walt.sndrcv(Walt.CMD_RESET)
    495         t_zero = walt.zero_clock()
    496         if t_zero < 0:
    497             print('Error: Couldn\'t zero clock, exiting')
    498             sys.exit(1)
    499 
    500         walt.sndrcv(Walt.CMD_GSHOCK)
    501         walt.sndrcv(Walt.CMD_AUTO_SCREEN_ON)
    502         deltas = []
    503         while len(deltas) < args.n:
    504             trigger_line = walt.readline()
    505             blink_time_seconds, val = walt.parse_trigger(trigger_line)
    506             blink_time_ms = blink_time_seconds * 1e3
    507             shock_time_ms = walt.read_shock_time() / 1e3
    508             if shock_time_ms == 0:
    509                 print("No shock detected, skipping this event")
    510                 continue
    511             dt = blink_time_ms - shock_time_ms
    512             deltas.append(dt)
    513             print("dt=%0.1f ms" % dt)
    514         print('Median tap-to-blink latency: %0.1f ms' % numpy.median(deltas))
    515 
    516 
    517 def run_tap_latency_test(args):
    518 
    519     if not args.input:
    520         print('Error: --input argument is required for tap latency test')
    521         sys.exit(1)
    522 
    523     print('Starting tap latency test')
    524 
    525     with Walt(args.serial) as walt:
    526         walt.sndrcv(Walt.CMD_RESET)
    527         t_zero = walt.zero_clock()
    528         if t_zero < 0:
    529             print('Error: Couldn\'t zero clock, exiting')
    530             sys.exit(1)
    531 
    532         # Fire up the evtest process
    533         cmd = 'evtest ' + args.input
    534         evtest = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
    535         walt.sndrcv(Walt.CMD_GSHOCK)
    536 
    537         taps_detected = 0
    538         taps = []
    539         while taps_detected < args.n:
    540             ev_line = evtest.stdout.readline()
    541             tap_info = evparser.parse_tap_line(ev_line)
    542             if not tap_info:
    543                 continue
    544 
    545             # Just received a tap event from evtest
    546             taps_detected += 1
    547 
    548             t_tap_epoch, direction = tap_info
    549             shock_time_us = walt.read_shock_time()
    550             dt_tap_us = 1e6 * (t_tap_epoch - t_zero) - shock_time_us
    551 
    552             print(ev_line.strip())
    553             print("shock t %d, tap t %f, tap val %d. dt=%0.1f" % (shock_time_us, t_tap_epoch, direction, dt_tap_us))
    554 
    555             if shock_time_us == 0:
    556                 print("No shock detected, skipping this event")
    557                 continue
    558 
    559             taps.append((dt_tap_us, direction))
    560 
    561     evtest.terminate()
    562 
    563     # Process data
    564     print("\nProcessing data...")
    565     dt_down = numpy.array([t[0] for t in taps if t[1] == 1]) / 1e3
    566     dt_up = numpy.array([t[0] for t in taps if t[1] == 0]) / 1e3
    567 
    568     print('dt_down = ' + array2str(dt_down))
    569     print('dt_up = ' + array2str(dt_up))
    570 
    571     median_down_ms = numpy.median(dt_down)
    572     median_up_ms = numpy.median(dt_up)
    573 
    574     print('Median latency, down: %0.1f, up: %0.1f' % (median_down_ms, median_up_ms))
    575 
    576 
    577 def run_walt_sanity_test(args):
    578     print('Starting sanity test')
    579 
    580     with Walt(args.serial) as walt:
    581         walt.sndrcv(Walt.CMD_RESET)
    582 
    583         not_digit = re.compile('\D+')
    584         lows = numpy.zeros(3) + 1024
    585         highs = numpy.zeros(3)
    586         while True:
    587             t, s = walt.sndrcv(Walt.CMD_SAMPLE_ALL)
    588             nums = not_digit.sub(' ', s).strip().split()
    589             if not nums:
    590                 continue
    591             ints = numpy.array([int(x) for x in nums])
    592             lows = numpy.array([lows, ints]).min(axis=0)
    593             highs = numpy.array([highs, ints]).max(axis=0)
    594 
    595             minmax = ' '.join(['%d-%d' % (lows[i], highs[i]) for i in range(3)])
    596             print(s.strip() + '\tmin-max: ' + minmax)
    597             time.sleep(0.1)
    598 
    599 
    600 class TcpServer:
    601     """
    602 
    603 
    604     """
    605     def __init__(self, walt, port=50007, host=''):
    606         self.running = threading.Event()
    607         self.paused = threading.Event()
    608         self.net = None
    609         self.walt = walt
    610         self.port = port
    611         self.host = host
    612         self.last_zero = 0.
    613 
    614     def ser2net(self, data):
    615         print('w>: ' + repr(data))
    616         return data
    617 
    618     def net2ser(self, data):
    619         print('w<: ' + repr(data))
    620         # Discard any empty data
    621         if not data or len(data) == 0:
    622             print('o<: discarded empty data')
    623             return
    624 
    625         # Get a string version of the data for checking longer commands
    626         s = data.decode(self.walt.encoding)
    627         bridge_command = None
    628         while len(s) > 0:
    629             if not bridge_command:
    630                 bridge_command = re.search(r'bridge (sync|update)', s)
    631             # If a "bridge" command does not exist, send everything to the WALT
    632             if not bridge_command:
    633                 self.walt.ser.write(s.encode(self.walt.encoding))
    634                 break
    635             # If a "bridge" command is preceded by WALT commands, send those
    636             # first
    637             if bridge_command.start() > 0:
    638                 before_command = s[:bridge_command.start()]
    639                 log('found bridge command after "%s"' % before_command)
    640                 s = s[bridge_command.start():]
    641                 self.walt.ser.write(before_command.encode(self.walt.encoding))
    642                 continue
    643             # Otherwise, reply directly to the command
    644             log('bridge command: %s, pausing ser2net thread...' %
    645                     bridge_command.group(0))
    646             self.pause()
    647             is_sync = bridge_command.group(1) == 'sync' or not self.walt.base_time
    648             if is_sync:
    649                 self.walt.zero_clock()
    650 
    651             self.walt.estimate_lag()
    652             if is_sync:
    653                 # shift the base so that min_lag is 0
    654                 self.walt.base_time += self.walt.min_lag
    655                 self.walt.max_lag -= self.walt.min_lag
    656                 self.walt.min_lag = 0
    657 
    658             min_lag = self.walt.min_lag * 1e6
    659             max_lag = self.walt.max_lag * 1e6
    660             # Send the time difference between now and when the clock was zeroed
    661             dt0 = (time.time() - self.walt.base_time) * 1e6
    662             reply = 'clock %d %d %d\n' % (dt0, min_lag, max_lag)
    663             self.net.sendall(reply)
    664             print('|custom-reply>: ' + repr(reply))
    665             self.resume()
    666             s = s[bridge_command.end():]
    667             bridge_command = None
    668 
    669     def connections_loop(self):
    670         with contextlib.closing(socket.socket(
    671                 socket.AF_INET, socket.SOCK_STREAM)) as sock:
    672             self.sock = sock
    673             # SO_REUSEADDR is supposed to prevent the "Address already in use" error
    674             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    675             sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    676             sock.bind((self.host, self.port))
    677             sock.listen(1)
    678             while True:
    679                 print('Listening on port %d' % self.port)
    680                 net, addr = sock.accept()
    681                 self.net = net
    682                 try:
    683                     print('Connected by: ' + str(addr))
    684                     self.net2ser_loop()
    685                 except socket.error as e:
    686                     # IO errors with the socket, not sure what they are
    687                     print('Error: %s' % e)
    688                     break
    689                 finally:
    690                     net.close()
    691                     self.net = None
    692 
    693     def net2ser_loop(self):
    694         while True:
    695             data = self.net.recv(1024)
    696             if not data:
    697                 break  # got disconnected
    698             self.net2ser(data)
    699 
    700     def ser2net_loop(self):
    701         while True:
    702             self.running.wait()
    703             data = self.walt.readline()
    704             if self.net and self.running.is_set():
    705                 data = self.ser2net(data)
    706                 data = data.encode(self.walt.encoding)
    707                 self.net.sendall(data)
    708             if not self.running.is_set():
    709                 self.paused.set()
    710 
    711     def serve(self):
    712         t = self.ser2net_thread = threading.Thread(
    713             target=self.ser2net_loop,
    714             name='ser2net_thread'
    715         )
    716         t.daemon = True
    717         t.start()
    718         self.paused.clear()
    719         self.running.set()
    720         self.connections_loop()
    721 
    722     def pause(self):
    723         """ Pause serial -> net forwarding
    724 
    725         The ser2net_thread stays running, but won't read any incoming data
    726         from the serial port.
    727         """
    728 
    729         self.running.clear()
    730         # Send a ping to break out of the blocking read on serial port and get
    731         # blocked on running.wait() instead. The ping response is discarded.
    732         self.walt.ser.write(Walt.CMD_PING)
    733         # Wait until the ping response comes in and we are sure we are no longer
    734         # blocked on ser.read()
    735         self.paused.wait()
    736         print("Paused ser2net thread")
    737 
    738     def resume(self):
    739         self.running.set()
    740         self.paused.clear()
    741         print("Resuming ser2net thread")
    742 
    743     def close(self):
    744         try:
    745             self.sock.close()
    746         except:
    747             pass
    748 
    749         try:
    750             self.walt.close()
    751         except:
    752             pass
    753 
    754     def __exit__(self, exc_type, exc_value, traceback):
    755         self.close()
    756 
    757     def __enter__(self):
    758         return self
    759 
    760 
    761 def run_tcp_bridge(args):
    762 
    763     print('Starting TCP bridge')
    764     print('You may need to run the following to allow traffic from the android container:')
    765     print('iptables -A INPUT -p tcp --dport %d -j ACCEPT' % args.port)
    766 
    767     try:
    768         with Walt(args.serial) as walt:
    769             with TcpServer(walt, port=args.port) as srv:
    770                 walt.sndrcv(Walt.CMD_RESET)
    771                 srv.serve()
    772     except KeyboardInterrupt:
    773         print(' KeyboardInterrupt, exiting...')
    774 
    775 
    776 def main(argv=sys.argv[1:]):
    777     args = parse_args(argv)
    778     if args.type == 'drag':
    779         run_drag_latency_test(args)
    780     if args.type == 'tap':
    781         run_tap_latency_test(args)
    782     elif args.type == 'screen':
    783         run_screen_latency_test(args)
    784     elif args.type == 'sanity':
    785         run_walt_sanity_test(args)
    786     elif args.type == 'curve':
    787         run_screen_curve(args)
    788     elif args.type == 'bridge':
    789         run_tcp_bridge(args)
    790     elif args.type == 'tapaudio':
    791         run_tap_audio_test(args)
    792     elif args.type == 'tapblink':
    793         run_tap_blink_test(args)
    794     else:
    795         print('Unknown test type: "%s"' % args.type)
    796 
    797 
    798 if __name__ == '__main__':
    799     main()
    800