• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import csv
2import os
3import signal
4from subprocess import Popen, PIPE
5from tempfile import NamedTemporaryFile
6from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
7from devlib.exception import HostError
8from devlib.host import PACKAGE_BIN_DIRECTORY
9from devlib.utils.misc import which
10
11INSTALL_INSTRUCTIONS="""
12MonsoonInstrument requires the monsoon.py tool, available from AOSP:
13
14https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py
15
16Download this script and put it in your $PATH (or pass it as the monsoon_bin
17parameter to MonsoonInstrument). `pip install python-gflags pyserial` to install
18the dependencies.
19"""
20
21class MonsoonInstrument(Instrument):
22    """Instrument for Monsoon Solutions power monitor
23
24    To use this instrument, you need to install the monsoon.py script available
25    from the Android Open Source Project. As of May 2017 this is under the CTS
26    repository:
27
28    https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py
29
30    Collects power measurements only, from a selection of two channels, the USB
31    passthrough channel and the main output channel.
32
33    :param target: Ignored
34    :param monsoon_bin: Path to monsoon.py executable. If not provided,
35                        ``$PATH`` is searched.
36    :param tty_device: TTY device to use to communicate with the Power
37                       Monitor. If not provided, a sane default is used.
38    """
39
40    mode = CONTINUOUS
41
42    def __init__(self, target, monsoon_bin=None, tty_device=None):
43        super(MonsoonInstrument, self).__init__(target)
44        self.monsoon_bin = monsoon_bin or which('monsoon.py')
45        if not self.monsoon_bin:
46            raise HostError(INSTALL_INSTRUCTIONS)
47
48        self.tty_device = tty_device
49
50        self.process = None
51        self.output = None
52
53        self.sample_rate_hz = 500
54        self.add_channel('output', 'power')
55        self.add_channel('USB', 'power')
56
57    def reset(self, sites=None, kinds=None, channels=None):
58        super(MonsoonInstrument, self).reset(sites, kinds)
59
60    def start(self):
61        if self.process:
62            self.process.kill()
63
64        os.system(self.monsoon_bin + ' --usbpassthrough off')
65
66        cmd = [self.monsoon_bin,
67               '--hz', str(self.sample_rate_hz),
68               '--samples', '-1', # -1 means sample indefinitely
69               '--includeusb']
70        if self.tty_device:
71            cmd += ['--device', self.tty_device]
72
73        self.logger.debug(' '.join(cmd))
74        self.buffer_file = NamedTemporaryFile(prefix='monsoon', delete=False)
75        self.process = Popen(cmd, stdout=self.buffer_file, stderr=PIPE)
76
77    def stop(self):
78        process = self.process
79        self.process = None
80        if not process:
81            raise RuntimeError('Monsoon script not started')
82
83        process.poll()
84        if process.returncode is not None:
85            stdout, stderr = process.communicate()
86            raise HostError(
87                'Monsoon script exited unexpectedly with exit code {}.\n'
88                'stdout:\n{}\nstderr:\n{}'.format(process.returncode,
89                                                  stdout, stderr))
90
91        process.send_signal(signal.SIGINT)
92
93        stderr =  process.stderr.read()
94
95        self.buffer_file.close()
96        with open(self.buffer_file.name) as f:
97            stdout = f.read()
98        os.remove(self.buffer_file.name)
99        self.buffer_file = None
100
101        self.output = (stdout, stderr)
102        os.system(self.monsoon_bin + ' --usbpassthrough on')
103
104        # Wait for USB connection to be restored
105        print ('waiting for usb connection to be back')
106        os.system('adb wait-for-device')
107
108    def get_data(self, outfile):
109        if self.process:
110            raise RuntimeError('`get_data` called before `stop`')
111
112        stdout, stderr = self.output
113
114        with open(outfile, 'wb') as f:
115            writer = csv.writer(f)
116            active_sites = [c.site for c in self.active_channels]
117
118            # Write column headers
119            row = []
120            if 'output' in active_sites:
121                row.append('output_power')
122            if 'USB' in active_sites:
123                row.append('USB_power')
124            writer.writerow(row)
125
126            # Write data
127            for line in stdout.splitlines():
128                # Each output line is a main_output, usb_output measurement pair.
129                # (If our user only requested one channel we still collect both,
130                # and just ignore one of them)
131                output, usb = line.split()
132                row = []
133                if 'output' in active_sites:
134                    row.append(output)
135                if 'USB' in active_sites:
136                    row.append(usb)
137                writer.writerow(row)
138
139        return MeasurementsCsv(outfile, self.active_channels)
140