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