Brendan Jackman | 3cfbad1 | 2017-04-11 15:42:06 +0100 | [diff] [blame] | 1 | import csv |
| 2 | import os |
| 3 | import signal |
| 4 | from subprocess import Popen, PIPE |
| 5 | from tempfile import NamedTemporaryFile |
| 6 | from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv |
| 7 | from devlib.exception import HostError |
| 8 | from devlib.host import PACKAGE_BIN_DIRECTORY |
| 9 | from devlib.utils.misc import which |
| 10 | |
| 11 | INSTALL_INSTRUCTIONS=""" |
| 12 | MonsoonInstrument requires the monsoon.py tool, available from AOSP: |
| 13 | |
| 14 | https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py |
| 15 | |
| 16 | Download this script and put it in your $PATH (or pass it as the monsoon_bin |
Brendan Jackman | 3dbd3f7 | 2017-05-22 13:48:47 +0100 | [diff] [blame] | 17 | parameter to MonsoonInstrument). `pip install python-gflags pyserial` to install |
| 18 | the dependencies. |
Brendan Jackman | 3cfbad1 | 2017-04-11 15:42:06 +0100 | [diff] [blame] | 19 | """ |
| 20 | |
| 21 | class 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 | |
Joel Fernandes | 4568bcb | 2017-05-06 10:32:43 -0700 | [diff] [blame] | 64 | os.system(self.monsoon_bin + ' --usbpassthrough off') |
| 65 | |
Brendan Jackman | 3cfbad1 | 2017-04-11 15:42:06 +0100 | [diff] [blame] | 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) |
Joel Fernandes | 4568bcb | 2017-05-06 10:32:43 -0700 | [diff] [blame] | 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') |
Brendan Jackman | 3cfbad1 | 2017-04-11 15:42:06 +0100 | [diff] [blame] | 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 | |
Marc Bonnici | 049b275 | 2017-08-03 16:43:32 +0100 | [diff] [blame] | 139 | return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) |