• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
19Runs the touchpad drag latency test using WALT Latency Timer
20Usage 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
31Note, before running this script, check that evtest can grab the device.
32On some systems it requires running as root.
33"""
34
35import argparse
36import contextlib
37import glob
38import os
39import random
40import re
41import socket
42import subprocess
43import sys
44import tempfile
45import threading
46import time
47
48import serial
49import numpy
50
51import evparser
52import minimization
53import screen_stats
54
55
56# Time units
57MS = 1e-3  # MS = 0.001 seconds
58US = 1e-6  # US = 10^-6 seconds
59
60# Globals
61debug_mode = True
62
63
64def log(msg):
65    if debug_mode:
66        print(msg)
67
68
69class 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
277def array2str(a):
278    a_strs = ['%0.2f' % x for x in a]
279    s = ', '.join(a_strs)
280    return '[' + s + ']'
281
282
283def 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
327def 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
384def 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
406def 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
458def 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
491def 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
517def 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
577def 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
600class 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
761def 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
776def 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
798if __name__ == '__main__':
799    main()
800