From 3cfbad19bdc3f001d7b682db27b8fea5dd99e24c Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Tue, 11 Apr 2017 15:42:06 +0100 Subject: [PATCH] instrument: Add support for Monsoon Power Monitor This adds support for the Monsoon Power Monitor: https://www.msoon.com/LabEquipment/PowerMonitor/ This device officialy supports only MS Windows so we use a tool from AOSP to communicate with it from Unix targets: https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py The user is required to install this tool and its dependencies, then pass the path to it as the monsoon_bin parameter. --- devlib/__init__.py | 1 + devlib/instrument/monsoon.py | 132 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 devlib/instrument/monsoon.py diff --git a/devlib/__init__.py b/devlib/__init__.py index 1ea54e2..51a8e47 100644 --- a/devlib/__init__.py +++ b/devlib/__init__.py @@ -14,6 +14,7 @@ from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS from devlib.instrument.daq import DaqInstrument from devlib.instrument.energy_probe import EnergyProbeInstrument from devlib.instrument.hwmon import HwmonInstrument +from devlib.instrument.monsoon import MonsoonInstrument from devlib.instrument.netstats import NetstatsInstrument from devlib.trace.ftrace import FtraceCollector diff --git a/devlib/instrument/monsoon.py b/devlib/instrument/monsoon.py new file mode 100644 index 0000000..9d544fd --- /dev/null +++ b/devlib/instrument/monsoon.py @@ -0,0 +1,132 @@ +import csv +import os +import signal +from subprocess import Popen, PIPE +from tempfile import NamedTemporaryFile +from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv +from devlib.exception import HostError +from devlib.host import PACKAGE_BIN_DIRECTORY +from devlib.utils.misc import which + +INSTALL_INSTRUCTIONS=""" +MonsoonInstrument requires the monsoon.py tool, available from AOSP: + +https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py + +Download this script and put it in your $PATH (or pass it as the monsoon_bin +parameter to MonsoonInstrument). `pip install gflags pyserial` to install the +dependencies. +""" + +class MonsoonInstrument(Instrument): + """Instrument for Monsoon Solutions power monitor + + To use this instrument, you need to install the monsoon.py script available + from the Android Open Source Project. As of May 2017 this is under the CTS + repository: + + https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py + + Collects power measurements only, from a selection of two channels, the USB + passthrough channel and the main output channel. + + :param target: Ignored + :param monsoon_bin: Path to monsoon.py executable. If not provided, + ``$PATH`` is searched. + :param tty_device: TTY device to use to communicate with the Power + Monitor. If not provided, a sane default is used. + """ + + mode = CONTINUOUS + + def __init__(self, target, monsoon_bin=None, tty_device=None): + super(MonsoonInstrument, self).__init__(target) + self.monsoon_bin = monsoon_bin or which('monsoon.py') + if not self.monsoon_bin: + raise HostError(INSTALL_INSTRUCTIONS) + + self.tty_device = tty_device + + self.process = None + self.output = None + + self.sample_rate_hz = 500 + self.add_channel('output', 'power') + self.add_channel('USB', 'power') + + def reset(self, sites=None, kinds=None, channels=None): + super(MonsoonInstrument, self).reset(sites, kinds) + + def start(self): + if self.process: + self.process.kill() + + cmd = [self.monsoon_bin, + '--hz', str(self.sample_rate_hz), + '--samples', '-1', # -1 means sample indefinitely + '--includeusb'] + if self.tty_device: + cmd += ['--device', self.tty_device] + + self.logger.debug(' '.join(cmd)) + self.buffer_file = NamedTemporaryFile(prefix='monsoon', delete=False) + self.process = Popen(cmd, stdout=self.buffer_file, stderr=PIPE) + + def stop(self): + process = self.process + self.process = None + if not process: + raise RuntimeError('Monsoon script not started') + + process.poll() + if process.returncode is not None: + stdout, stderr = process.communicate() + raise HostError( + 'Monsoon script exited unexpectedly with exit code {}.\n' + 'stdout:\n{}\nstderr:\n{}'.format(process.returncode, + stdout, stderr)) + + process.send_signal(signal.SIGINT) + + stderr = process.stderr.read() + + self.buffer_file.close() + with open(self.buffer_file.name) as f: + stdout = f.read() + os.remove(self.buffer_file.name) + self.buffer_file = None + + self.output = (stdout, stderr) + + def get_data(self, outfile): + if self.process: + raise RuntimeError('`get_data` called before `stop`') + + stdout, stderr = self.output + + with open(outfile, 'wb') as f: + writer = csv.writer(f) + active_sites = [c.site for c in self.active_channels] + + # Write column headers + row = [] + if 'output' in active_sites: + row.append('output_power') + if 'USB' in active_sites: + row.append('USB_power') + writer.writerow(row) + + # Write data + for line in stdout.splitlines(): + # Each output line is a main_output, usb_output measurement pair. + # (If our user only requested one channel we still collect both, + # and just ignore one of them) + output, usb = line.split() + row = [] + if 'output' in active_sites: + row.append(output) + if 'USB' in active_sites: + row.append(usb) + writer.writerow(row) + + return MeasurementsCsv(outfile, self.active_channels)