diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2019-11-08 22:27:59 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-11-08 22:27:59 +0000 |
commit | 647d282e6ab964c6a5bf8a1c6e623ecf62f45d04 (patch) | |
tree | e690fc8da9354e9458863002071634da27a53135 | |
parent | 21afc1ecc6465eb2ab8a7e63858e7d4e14fdbaaa (diff) | |
parent | e54396ca91c1c9b2327f8e6adf8d40a9795f0798 (diff) | |
download | platform_tools_test_connectivity-647d282e6ab964c6a5bf8a1c6e623ecf62f45d04.tar.gz platform_tools_test_connectivity-647d282e6ab964c6a5bf8a1c6e623ecf62f45d04.tar.bz2 platform_tools_test_connectivity-647d282e6ab964c6a5bf8a1c6e623ecf62f45d04.zip |
Merge "Modifies the existing Monsoon API calls to work with the new Monsoon libraries."
22 files changed, 429 insertions, 1290 deletions
diff --git a/acts/framework/acts/bin/monsoon.py b/acts/framework/acts/bin/monsoon.py index c43eca505e..a76b425dd9 100755 --- a/acts/framework/acts/bin/monsoon.py +++ b/acts/framework/acts/bin/monsoon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2016 - The Android Open Source Project +# Copyright 2019 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,75 +13,100 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Interface for a USB-connected Monsoon power meter (http://msoon.com/LabEquipment/PowerMonitor/). """ import argparse -import sys -import time -import collections -from acts.controllers.monsoon import Monsoon +import acts.controllers.monsoon as monsoon_controller + -def main(FLAGS): +def main(args): """Simple command-line interface for Monsoon.""" - if FLAGS.avg and FLAGS.avg < 0: - print("--avg must be greater than 0") + if args.avg and args.avg < 0: + print('--avg must be greater than 0') return - mon = Monsoon(serial=int(FLAGS.serialno[0])) + mon = monsoon_controller.create([int(args.serialno[0])])[0] - if FLAGS.voltage is not None: - mon.set_voltage(FLAGS.voltage) + if args.voltage is not None: + mon.set_voltage(args.voltage) - if FLAGS.current is not None: - mon.set_max_current(FLAGS.current) + if args.current is not None: + mon.set_max_current(args.current) - if FLAGS.status: + if args.status: items = sorted(mon.status.items()) - print("\n".join(["%s: %s" % item for item in items])) + print('\n'.join(['%s: %s' % item for item in items])) - if FLAGS.usbpassthrough: - mon.usb(FLAGS.usbpassthrough) + if args.usbpassthrough: + mon.usb(args.usbpassthrough) - if FLAGS.startcurrent is not None: - mon.set_max_init_current(FLAGS.startcurrent) + if args.startcurrent is not None: + mon.set_max_initial_current(args.startcurrent) - if FLAGS.samples: - # Have to sleep a bit here for monsoon to be ready to lower the rate of - # socket read timeout. - time.sleep(1) - result = mon.take_samples(FLAGS.hz, FLAGS.samples, - sample_offset=FLAGS.offset, live=True) + if args.samples: + result = mon.measure_power( + args.samples / args.hz, + measure_after_seconds=args.offset, + hz=args.hz, + output_path='monsoon_output.txt') print(repr(result)) + if __name__ == '__main__': - parser = argparse.ArgumentParser(description=("This is a python utility " - "tool to control monsoon power measurement boxes.")) - parser.add_argument("--status", action="store_true", - help="Print power meter status.") - parser.add_argument("-avg", "--avg", type=int, default=0, - help="Also report average over last n data points.") - parser.add_argument("-v", "--voltage", type=float, - help="Set output voltage (0 for off)") - parser.add_argument("-c", "--current", type=float, - help="Set max output current.") - parser.add_argument("-sc", "--startcurrent", type=float, - help="Set max power-up/inital current.") - parser.add_argument("-usb", "--usbpassthrough", choices=("on", "off", - "auto"), help="USB control (on, off, auto).") - parser.add_argument("-sp", "--samples", type=int, - help="Collect and print this many samples") - parser.add_argument("-hz", "--hz", type=int, - help="Sample this many times per second.") - parser.add_argument("-d", "--device", help="Use this /dev/ttyACM... file.") - parser.add_argument("-sn", "--serialno", type=int, nargs=1, required=True, - help="The serial number of the Monsoon to use.") - parser.add_argument("--offset", type=int, nargs='?', default=0, - help="The number of samples to discard when calculating average.") - parser.add_argument("-r", "--ramp", action="store_true", help=("Gradually " - "increase voltage to prevent tripping Monsoon overvoltage")) - args = parser.parse_args() - main(args) + parser = argparse.ArgumentParser( + description='This is a python utility tool to control monsoon power ' + 'measurement boxes.') + parser.add_argument( + '--status', action='store_true', help='Print power meter status.') + parser.add_argument( + '-avg', + '--avg', + type=int, + default=0, + help='Also report average over last n data points.') + parser.add_argument( + '-v', '--voltage', type=float, help='Set output voltage (0 for off)') + parser.add_argument( + '-c', '--current', type=float, help='Set max output current.') + parser.add_argument( + '-sc', + '--startcurrent', + type=float, + help='Set max power-up/initial current.') + parser.add_argument( + '-usb', + '--usbpassthrough', + choices=('on', 'off', 'auto'), + help='USB control (on, off, auto).') + parser.add_argument( + '-sp', + '--samples', + type=int, + help='Collect and print this many samples') + parser.add_argument( + '-hz', '--hz', type=int, help='Sample this many times per second.') + parser.add_argument('-d', '--device', help='Use this /dev/ttyACM... file.') + parser.add_argument( + '-sn', + '--serialno', + type=int, + nargs=1, + required=True, + help='The serial number of the Monsoon to use.') + parser.add_argument( + '--offset', + type=int, + nargs='?', + default=0, + help='The number of samples to discard when calculating average.') + parser.add_argument( + '-r', + '--ramp', + action='store_true', + help='Gradually increase voltage to prevent tripping Monsoon ' + 'overvoltage.') + arguments = parser.parse_args() + main(arguments) diff --git a/acts/framework/acts/controllers/monsoon.py b/acts/framework/acts/controllers/monsoon.py index d5b24a2703..9488837dbb 100644 --- a/acts/framework/acts/controllers/monsoon.py +++ b/acts/framework/acts/controllers/monsoon.py @@ -13,1006 +13,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Interface for a USB-connected Monsoon power meter -(http://msoon.com/LabEquipment/PowerMonitor/). -Based on the original py2 script of kens@google.com -""" -import fcntl -import logging -import os -import select -import struct -import sys -import time -import collections +from acts.controllers.monsoon_lib.api.hvpm.monsoon import Monsoon as HvpmMonsoon +from acts.controllers.monsoon_lib.api.lvpm_stock.monsoon import Monsoon as LvpmStockMonsoon -# http://pyserial.sourceforge.net/ -# On ubuntu, apt-get install python3-pyserial -import serial - -import acts.signals - -from acts import utils -from acts.controllers import android_device - -ACTS_CONTROLLER_CONFIG_NAME = "Monsoon" -ACTS_CONTROLLER_REFERENCE_NAME = "monsoons" +ACTS_CONTROLLER_CONFIG_NAME = 'Monsoon' +ACTS_CONTROLLER_REFERENCE_NAME = 'monsoons' def create(configs): objs = [] - for c in configs: - objs.append(Monsoon(serial=int(c))) - return objs - - -def destroy(objs): - for obj in objs: - fcntl.flock(obj.mon._tempfile, fcntl.LOCK_UN) - obj.mon._tempfile.close() - - -class MonsoonError(acts.signals.ControllerError): - """Raised for exceptions encountered in monsoon lib.""" - - -class MonsoonProxy(object): - """Class that directly talks to monsoon over serial. - - Provides a simple class to use the power meter, e.g. - mon = monsoon.Monsoon() - mon.SetVoltage(3.7) - mon.StartDataCollection() - mydata = [] - while len(mydata) < 1000: - mydata.extend(mon.CollectData()) - mon.StopDataCollection() - - See http://wiki/Main/MonsoonProtocol for information on the protocol. - """ - - def __init__(self, device=None, serialno=None, wait=1): - """Establish a connection to a Monsoon. - - By default, opens the first available port, waiting if none are ready. - A particular port can be specified with "device", or a particular - Monsoon can be specified with "serialno" (using the number printed on - its back). With wait=0, IOError is thrown if a device is not - immediately available. - """ - self._coarse_ref = self._fine_ref = self._coarse_zero = 0 - self._fine_zero = self._coarse_scale = self._fine_scale = 0 - self._last_seq = 0 - self.start_voltage = 0 - self.serial = serialno - - if device: - self.ser = serial.Serial(device, timeout=1) - return - # Try all devices connected through USB virtual serial ports until we - # find one we can use. - while True: - for dev in os.listdir("/dev"): - prefix = "ttyACM" - # Prefix is different on Mac OS X. - if sys.platform == "darwin": - prefix = "tty.usbmodem" - if not dev.startswith(prefix): - continue - tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev) - self._tempfile = open(tmpname, "w") - try: - os.chmod(tmpname, 0o666) - except OSError as e: - pass - - try: # use a lockfile to ensure exclusive access - fcntl.flock(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError as e: - logging.error("device %s is in use", dev) - continue - - try: # try to open the device - self.ser = serial.Serial("/dev/%s" % dev, timeout=1) - self.StopDataCollection() # just in case - self._FlushInput() # discard stale input - status = self.GetStatus() - except Exception as e: - logging.exception("Error opening device %s: %s", dev, e) - continue - - if not status: - logging.error("no response from device %s", dev) - elif serialno and status["serialNumber"] != serialno: - logging.error("Another device serial #%d seen on %s", - status["serialNumber"], dev) - else: - self.start_voltage = status["voltage1"] - return - - self._tempfile = None - if not wait: raise IOError("No device found") - logging.info("Waiting for device...") - time.sleep(1) - - def GetStatus(self): - """Requests and waits for status. - - Returns: - status dictionary. - """ - # status packet format - STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH" - STATUS_FIELDS = [ - "packetType", - "firmwareVersion", - "protocolVersion", - "mainFineCurrent", - "usbFineCurrent", - "auxFineCurrent", - "voltage1", - "mainCoarseCurrent", - "usbCoarseCurrent", - "auxCoarseCurrent", - "voltage2", - "outputVoltageSetting", - "temperature", - "status", - "leds", - "mainFineResistor", - "serialNumber", - "sampleRate", - "dacCalLow", - "dacCalHigh", - "powerUpCurrentLimit", - "runTimeCurrentLimit", - "powerUpTime", - "usbFineResistor", - "auxFineResistor", - "initialUsbVoltage", - "initialAuxVoltage", - "hardwareRevision", - "temperatureLimit", - "usbPassthroughMode", - "mainCoarseResistor", - "usbCoarseResistor", - "auxCoarseResistor", - "defMainFineResistor", - "defUsbFineResistor", - "defAuxFineResistor", - "defMainCoarseResistor", - "defUsbCoarseResistor", - "defAuxCoarseResistor", - "eventCode", - "eventData", - ] - - self._SendStruct("BBB", 0x01, 0x00, 0x00) - while 1: # Keep reading, discarding non-status packets - read_bytes = self._ReadPacket() - if not read_bytes: - raise MonsoonError("Failed to read Monsoon status") - calsize = struct.calcsize(STATUS_FORMAT) - if len(read_bytes) != calsize or read_bytes[0] != 0x10: - raise MonsoonError( - "Wanted status, dropped type=0x%02x, len=%d", - read_bytes[0], len(read_bytes)) - status = dict( - zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, read_bytes))) - p_type = status["packetType"] - if p_type != 0x10: - raise MonsoonError("Package type %s is not 0x10." % p_type) - for k in status.keys(): - if k.endswith("VoltageSetting"): - status[k] = 2.0 + status[k] * 0.01 - elif k.endswith("FineCurrent"): - pass # needs calibration data - elif k.endswith("CoarseCurrent"): - pass # needs calibration data - elif k.startswith("voltage") or k.endswith("Voltage"): - status[k] = status[k] * 0.000125 - elif k.endswith("Resistor"): - status[k] = 0.05 + status[k] * 0.0001 - if k.startswith("aux") or k.startswith("defAux"): - status[k] += 0.05 - elif k.endswith("CurrentLimit"): - status[k] = 8 * (1023 - status[k]) / 1023.0 - return status - - def RampVoltage(self, start, end): - v = start - if v < 3.0: v = 3.0 # protocol doesn't support lower than this - while (v < end): - self.SetVoltage(v) - v += .1 - time.sleep(.1) - self.SetVoltage(end) - - def SetVoltage(self, v): - """Set the output voltage, 0 to disable. - """ - if v == 0: - self._SendStruct("BBB", 0x01, 0x01, 0x00) - else: - self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100)) - - def GetVoltage(self): - """Get the output voltage. - - Returns: - Current Output Voltage (in unit of v). - """ - try: - return self.GetStatus()["outputVoltageSetting"] - # Catch potential errors such as struct.error, TypeError and other - # unknown errors which would bring down the whole test - except Exception as e: - raise MonsoonError("Error getting Monsoon voltage") - - def SetMaxCurrent(self, i): - """Set the max output current. - """ - if i < 0 or i > 8: - raise MonsoonError(("Target max current %sA, is out of acceptable " - "range [0, 8].") % i) - val = 1023 - int((i / 8) * 1023) - self._SendStruct("BBB", 0x01, 0x0a, val & 0xff) - self._SendStruct("BBB", 0x01, 0x0b, val >> 8) - - def SetMaxPowerUpCurrent(self, i): - """Set the max power up current. - """ - if i < 0 or i > 8: - raise MonsoonError(("Target max current %sA, is out of acceptable " - "range [0, 8].") % i) - val = 1023 - int((i / 8) * 1023) - self._SendStruct("BBB", 0x01, 0x08, val & 0xff) - self._SendStruct("BBB", 0x01, 0x09, val >> 8) - - def SetUsbPassthrough(self, val): - """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto. - """ - self._SendStruct("BBB", 0x01, 0x10, val) - - def GetUsbPassthrough(self): - """Get the USB passthrough mode: 0 = off, 1 = on, 2 = auto. - - Returns: - Current USB passthrough mode. - """ - try: - return self.GetStatus()["usbPassthroughMode"] - # Catch potential errors such as struct.error, TypeError and other - # unknown errors which would bring down the whole test - except Exception as e: - raise MonsoonError("Error reading Monsoon USB passthrough status") - - def StartDataCollection(self): - """Tell the device to start collecting and sending measurement data. - """ - self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command - self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) - - def StopDataCollection(self): - """Tell the device to stop collecting measurement data. - """ - self._SendStruct("BB", 0x03, 0x00) # stop - - def CollectData(self): - """Return some current samples. Call StartDataCollection() first. - """ - while 1: # loop until we get data or a timeout - _bytes = self._ReadPacket() - if not _bytes: - raise MonsoonError("Data collection failed due to empty data") - if len(_bytes) < 4 + 8 + 1 or _bytes[0] < 0x20 or _bytes[0] > 0x2F: - logging.warning("Wanted data, dropped type=0x%02x, len=%d", - _bytes[0], len(_bytes)) - continue - - seq, _type, x, y = struct.unpack("BBBB", _bytes[:4]) - data = [ - struct.unpack(">hhhh", _bytes[x:x + 8]) - for x in range(4, - len(_bytes) - 8, 8) - ] - - if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: - logging.warning("Data sequence skipped, lost packet?") - self._last_seq = seq - - if _type == 0: - if not self._coarse_scale or not self._fine_scale: - logging.warning( - "Waiting for calibration, dropped data packet.") - continue - out = [] - for main, usb, aux, voltage in data: - if main & 1: - coarse = ((main & ~1) - self._coarse_zero) - out.append(coarse * self._coarse_scale) - else: - out.append((main - self._fine_zero) * self._fine_scale) - return out - elif _type == 1: - self._fine_zero = data[0][0] - self._coarse_zero = data[1][0] - elif _type == 2: - self._fine_ref = data[0][0] - self._coarse_ref = data[1][0] - else: - logging.warning("Discarding data packet type=0x%02x", _type) - continue - - # See http://wiki/Main/MonsoonProtocol for details on these values. - if self._coarse_ref != self._coarse_zero: - self._coarse_scale = 2.88 / ( - self._coarse_ref - self._coarse_zero) - if self._fine_ref != self._fine_zero: - self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) - - def _SendStruct(self, fmt, *args): - """Pack a struct (without length or checksum) and send it. - """ - # Flush out the input buffer before sending data - self._FlushInput() - data = struct.pack(fmt, *args) - data_len = len(data) + 1 - checksum = (data_len + sum(bytearray(data))) % 256 - out = struct.pack("B", data_len) + data + struct.pack("B", checksum) - self.ser.write(out) - - def _ReadPacket(self): - """Read a single data record as a string (without length or checksum). - """ - len_char = self.ser.read(1) - if not len_char: - raise MonsoonError("Reading from serial port timed out") - - data_len = ord(len_char) - if not data_len: - return "" - result = self.ser.read(int(data_len)) - result = bytearray(result) - if len(result) != data_len: - raise MonsoonError( - "Length mismatch, expected %d bytes, got %d bytes.", data_len, - len(result)) - body = result[:-1] - checksum = (sum(struct.unpack("B" * len(body), body)) + data_len) % 256 - if result[-1] != checksum: - raise MonsoonError( - "Invalid checksum from serial port! Expected %s, got %s", - hex(checksum), hex(result[-1])) - return result[:-1] - - def _FlushInput(self): - """ Flush all read data until no more available. """ - self.ser.reset_input_buffer() - flushed = 0 - while True: - ready_r, ready_w, ready_x = select.select([self.ser], [], - [self.ser], 0) - if len(ready_x) > 0: - raise MonsoonError("Exception from serial port.") - elif len(ready_r) > 0: - flushed += 1 - self.ser.read(1) # This may cause underlying buffering. - self.ser.reset_input_buffer( - ) # Flush the underlying buffer too. - else: - break - # if flushed > 0: - # logging.info("dropped >%d bytes" % flushed) - - -class MonsoonData(object): - """A class for reporting power measurement data from monsoon. - - Data means the measured current value in Amps. - """ - # Number of digits for long rounding. - lr = 8 - # Number of digits for short rounding - sr = 6 - # Delimiter for writing multiple MonsoonData objects to text file. - delimiter = "\n\n==========\n\n" - - def __init__(self, data_points, timestamps, hz, voltage, offset=0): - """Instantiates a MonsoonData object. - - Args: - data_points: A list of current values in Amp (float). - timestamps: A list of epoch timestamps (int). - hz: The hertz at which the data points are measured. - voltage: The voltage at which the data points are measured. - offset: The number of initial data points to discard - in calculations. - """ - self._data_points = data_points - self._timestamps = timestamps - self.offset = offset - num_of_data_pt = len(self._data_points) - if self.offset >= num_of_data_pt: - raise MonsoonError( - ("Offset number (%d) must be smaller than the " - "number of data points (%d).") % (offset, num_of_data_pt)) - self.data_points = self._data_points[self.offset:] - self.timestamps = self._timestamps[self.offset:] - self.hz = hz - self.voltage = voltage - self.tag = None - self._validate_data() - - @property - def average_current(self): - """Average current in the unit of mA. - """ - len_data_pt = len(self.data_points) - if len_data_pt == 0: - return 0 - cur = sum(self.data_points) * 1000 / len_data_pt - return round(cur, self.sr) - - @property - def total_charge(self): - """Total charged used in the unit of mAh. - """ - charge = (sum(self.data_points) / self.hz) * 1000 / 3600 - return round(charge, self.sr) - - @property - def total_power(self): - """Total power used. - """ - power = self.average_current * self.voltage - return round(power, self.sr) - - @staticmethod - def from_string(data_str): - """Creates a MonsoonData object from a string representation generated - by __str__. - - Args: - str: The string representation of a MonsoonData. - - Returns: - A MonsoonData object. - """ - lines = data_str.strip().split('\n') - err_msg = ("Invalid input string format. Is this string generated by " - "MonsoonData class?") - conditions = [ - len(lines) <= 4, "Average Current:" not in lines[1], - "Voltage: " not in lines[2], "Total Power: " not in lines[3], - "samples taken at " not in lines[4], - lines[5] != "Time" + ' ' * 7 + "Amp" - ] - if any(conditions): - raise MonsoonError(err_msg) - """Example string from Monsoon output file, first line is empty. - Line1: - Line2: test_2g_screenoff_dtimx2_marlin_OPD1.170706.006 - Line3: Average Current: 51.87984mA. - Line4: Voltage: 4.2V. - Line5: Total Power: 217.895328mW. - Line6: 150000 samples taken at 500Hz, with an offset of 0 samples. - """ - hz_str = lines[4].split()[4] - hz = int(hz_str[:-3]) - voltage_str = lines[2].split()[1] - voltage = float(voltage_str[:-2]) - lines = lines[6:] - t = [] - v = [] - for l in lines: - try: - timestamp, value = l.split(' ') - t.append(int(timestamp)) - v.append(float(value)) - except ValueError: - raise MonsoonError(err_msg) - return MonsoonData(v, t, hz, voltage) - - @staticmethod - def save_to_text_file(monsoon_data, file_path): - """Save multiple MonsoonData objects to a text file. - - Args: - monsoon_data: A list of MonsoonData objects to write to a text - file. - file_path: The full path of the file to save to, including the file - name. - """ - if not monsoon_data: - raise MonsoonError("Attempting to write empty Monsoon data to " - "file, abort") - utils.create_dir(os.path.dirname(file_path)) - with open(file_path, 'a') as f: - for md in monsoon_data: - f.write(str(md)) - f.write(MonsoonData.delimiter) - - @staticmethod - def from_text_file(file_path): - """Load MonsoonData objects from a text file generated by - MonsoonData.save_to_text_file. - - Args: - file_path: The full path of the file load from, including the file - name. - - Returns: - A list of MonsoonData objects. - """ - results = [] - with open(file_path, 'r') as f: - data_strs = f.read().split(MonsoonData.delimiter) - data_strs = data_strs[:-1] - for data_str in data_strs: - results.append(MonsoonData.from_string(data_str)) - return results - - def _validate_data(self): - """Verifies that the data points contained in the class are valid. - """ - msg = "Error! Expected {} timestamps, found {}.".format( - len(self._data_points), len(self._timestamps)) - if len(self._data_points) != len(self._timestamps): - raise MonsoonError(msg) - - def update_offset(self, new_offset): - """Updates how many data points to skip in caculations. - - Always use this function to update offset instead of directly setting - self.offset. - - Args: - new_offset: The new offset. - """ - self.offset = new_offset - self.data_points = self._data_points[self.offset:] - self.timestamps = self._timestamps[self.offset:] - - def get_data_with_timestamps(self): - """Returns the data points with timestamps. - - Returns: - A list of tuples in the format of (timestamp, data) - """ - result = [] - for t, d in zip(self.timestamps, self.data_points): - result.append(t, round(d, self.lr)) - return result - - def get_average_record(self, n): - """Returns a list of average current numbers, each representing the - average over the last n data points. - - Args: - n: Number of data points to average over. - - Returns: - A list of average current values. - """ - history_deque = collections.deque() - averages = [] - for d in self.data_points: - history_deque.appendleft(d) - if len(history_deque) > n: - history_deque.pop() - avg = sum(history_deque) / len(history_deque) - averages.append(round(avg, self.lr)) - return averages - - def _header(self): - strs = [""] - if self.tag: - strs.append(self.tag) + for serial in configs: + serial_number = int(serial) + if serial_number < 20000: + # This code assumes the LVPM has not been updated to have a + # non-stock firmware. If someone has updated the firmware, + # power measurement will fail. + objs.append(LvpmStockMonsoon(serial=serial_number)) else: - strs.append("Monsoon Measurement Data") - strs.append("Average Current: {}mA.".format(self.average_current)) - strs.append("Voltage: {}V.".format(self.voltage)) - strs.append("Total Power: {}mW.".format(self.total_power)) - strs.append( - ("{} samples taken at {}Hz, with an offset of {} samples.").format( - len(self._data_points), self.hz, self.offset)) - return "\n".join(strs) - - def __len__(self): - return len(self.data_points) - - def __str__(self): - strs = [] - strs.append(self._header()) - strs.append("Time" + ' ' * 7 + "Amp") - for t, d in zip(self.timestamps, self.data_points): - strs.append("{} {}".format(t, round(d, self.sr))) - return "\n".join(strs) - - def __repr__(self): - return self._header() - - -class Monsoon(object): - """The wrapper class for test scripts to interact with monsoon. - """ - - def __init__(self, *args, **kwargs): - serial = kwargs["serial"] - device = None - self.log = logging.getLogger() - if "device" in kwargs: - device = kwargs["device"] - self.mon = MonsoonProxy(serialno=serial, device=device) - self.dev = self.mon.ser.name - self.serial = serial - self.dut = None - - def attach_device(self, dut): - """Attach the controller object for the Device Under Test (DUT) - physically attached to the Monsoon box. - - Args: - dut: A controller object representing the device being powered by - this Monsoon box. - """ - self.dut = dut - - def set_voltage(self, volt, ramp=False): - """Sets the output voltage of monsoon. - - Args: - volt: Voltage to set the output to. - ramp: If true, the output voltage will be increased gradually to - prevent tripping Monsoon overvoltage. - """ - if ramp: - self.mon.RampVoltage(mon.start_voltage, volt) - else: - self.mon.SetVoltage(volt) - - def set_max_current(self, cur): - """Sets monsoon's max output current. - - Args: - cur: The max current in A. - """ - self.mon.SetMaxCurrent(cur) - - def set_max_init_current(self, cur): - """Sets the max power-up/inital current. - - Args: - cur: The max initial current allowed in mA. - """ - self.mon.SetMaxPowerUpCurrent(cur) - - @property - def status(self): - """Gets the status params of monsoon. - - Returns: - A dictionary where each key-value pair represents a monsoon status - param. - """ - return self.mon.GetStatus() - - def take_samples(self, sample_hz, sample_num, sample_offset=0, live=False): - """Take samples of the current value supplied by monsoon. - - This is the actual measurement for power consumption. This function - blocks until the number of samples requested has been fulfilled. - - Args: - hz: Number of points to take for every second. - sample_num: Number of samples to take. - offset: The number of initial data points to discard in MonsoonData - calculations. sample_num is extended by offset to compensate. - live: Print each sample in console as measurement goes on. - - Returns: - A MonsoonData object representing the data obtained in this - sampling. None if sampling is unsuccessful. - """ - sys.stdout.flush() - voltage = self.mon.GetVoltage() - self.log.info("Taking samples at %dhz for %ds, voltage %.2fv.", - sample_hz, (sample_num / sample_hz), voltage) - sample_num += sample_offset - # Make sure state is normal - self.mon.StopDataCollection() - status = self.mon.GetStatus() - native_hz = status["sampleRate"] * 1000 - - # Collect and average samples as specified - self.mon.StartDataCollection() - - # In case sample_hz doesn't divide native_hz exactly, use this - # invariant: 'offset' = (consumed samples) * sample_hz - - # (emitted samples) * native_hz - # This is the error accumulator in a variation of Bresenham's - # algorithm. - emitted = offset = 0 - collected = [] - # past n samples for rolling average - history_deque = collections.deque() - current_values = [] - timestamps = [] - - try: - last_flush = time.time() - while emitted < sample_num or sample_num == -1: - # The number of raw samples to consume before emitting the next - # output - need = int((native_hz - offset + sample_hz - 1) / sample_hz) - if need > len(collected): # still need more input samples - samples = self.mon.CollectData() - if not samples: - break - collected.extend(samples) - else: - # Have enough data, generate output samples. - # Adjust for consuming 'need' input samples. - offset += need * sample_hz - # maybe multiple, if sample_hz > native_hz - while offset >= native_hz: - # TODO(angli): Optimize "collected" operations. - this_sample = sum(collected[:need]) / need - this_time = int(time.time()) - timestamps.append(this_time) - if live: - self.log.info("%s %s", this_time, this_sample) - current_values.append(this_sample) - sys.stdout.flush() - offset -= native_hz - emitted += 1 # adjust for emitting 1 output sample - collected = collected[need:] - now = time.time() - if now - last_flush >= 0.99: # flush every second - sys.stdout.flush() - last_flush = now - except Exception as e: - pass - self.mon.StopDataCollection() - try: - return MonsoonData( - current_values, - timestamps, - sample_hz, - voltage, - offset=sample_offset) - except: - return None - - @utils.timeout(60) - def usb(self, state): - """Sets the monsoon's USB passthrough mode. This is specific to the - USB port in front of the monsoon box which connects to the powered - device, NOT the USB that is used to talk to the monsoon itself. - - "Off" means USB always off. - "On" means USB always on. - "Auto" means USB is automatically turned off when sampling is going on, - and turned back on when sampling finishes. - - Args: - stats: The state to set the USB passthrough to. - - Returns: - True if the state is legal and set. False otherwise. - """ - state_lookup = {"off": 0, "on": 1, "auto": 2} - state = state.lower() - if state in state_lookup: - current_state = self.mon.GetUsbPassthrough() - while (current_state != state_lookup[state]): - self.mon.SetUsbPassthrough(state_lookup[state]) - time.sleep(1) - current_state = self.mon.GetUsbPassthrough() - return True - return False - - def _check_dut(self): - """Verifies there is a DUT attached to the monsoon. - - This should be called in the functions that operate the DUT. - """ - if not self.dut: - raise MonsoonError("Need to attach the device before using it.") - - @utils.timeout(15) - def _wait_for_device(self, ad): - while ad.serial not in android_device.list_adb_devices(): - pass - ad.adb.wait_for_device() - - def execute_sequence_and_measure(self, - step_funcs, - hz, - duration, - offset_sec=20, - *args, - **kwargs): - """@Deprecated. - Executes a sequence of steps and take samples in-between. - - For each step function, the following steps are followed: - 1. The function is executed to put the android device in a state. - 2. If the function returns False, skip to next step function. - 3. If the function returns True, sl4a session is disconnected. - 4. Monsoon takes samples. - 5. Sl4a is reconnected. - - Because it takes some time for the device to calm down after the usb - connection is cut, an offset is set for each measurement. The default - is 20s. - - Args: - hz: Number of samples to take per second. - durations: Number(s) of minutes to take samples for in each step. - If this is an integer, all the steps will sample for the same - amount of time. If this is an iterable of the same length as - step_funcs, then each number represents the number of minutes - to take samples for after each step function. - e.g. If durations[0] is 10, we'll sample for 10 minutes after - step_funcs[0] is executed. - step_funcs: A list of funtions, whose first param is an android - device object. If a step function returns True, samples are - taken after this step, otherwise we move on to the next step - function. - ad: The android device object connected to this monsoon. - offset_sec: The number of seconds of initial data to discard. - *args, **kwargs: Extra args to be passed into each step functions. - - Returns: - The MonsoonData objects from samplings. - """ - self._check_dut() - sample_nums = [] - try: - if len(duration) != len(step_funcs): - raise MonsoonError(("The number of durations need to be the " - "same as the number of step functions.")) - for d in duration: - sample_nums.append(d * 60 * hz) - except TypeError: - num = duration * 60 * hz - sample_nums = [num] * len(step_funcs) - results = [] - oset = offset_sec * hz - for func, num in zip(step_funcs, sample_nums): - try: - self.usb("auto") - step_name = func.__name__ - self.log.info("Executing step function %s.", step_name) - take_sample = func(ad, *args, **kwargs) - if not take_sample: - self.log.info("Skip taking samples for %s", step_name) - continue - time.sleep(1) - self.dut.stop_services() - time.sleep(1) - self.log.info("Taking samples for %s.", step_name) - data = self.take_samples(hz, num, sample_offset=oset) - if not data: - raise MonsoonError("Sampling for %s failed." % step_name) - self.log.info("Sample summary: %s", repr(data)) - data.tag = step_name - results.append(data) - except Exception: - self.log.exception("Exception happened during step %s, abort!" - % func.__name__) - return results - finally: - self.mon.StopDataCollection() - self.usb("on") - self._wait_for_device(self.dut) - # Wait for device to come back online. - time.sleep(10) - self.dut.start_services() - # Release wake lock to put device into sleep. - self.dut.droid.goToSleepNow() - return results - - def disconnect_dut(self): - """Disconnect DUT from monsoon. - - Stop the sl4a service on the DUT and disconnect USB connection - raises: - MonsoonError: monsoon erro trying to disconnect usb - """ - try: - self.dut.stop_services() - time.sleep(1) - self.usb("off") - except Exception as e: - raise MonsoonError( - "Error happended trying to disconnect DUT from Monsoon") - - def monsoon_usb_auto(self): - """Set monsoon USB to auto to ready the device for power measurement. - - Stop the sl4a service on the DUT and disconnect USB connection - raises: - MonsoonError: monsoon erro trying to set usbpassthrough to auto - """ - try: - self.dut.stop_services() - time.sleep(1) - self.usb("auto") - except Exception as e: - raise MonsoonError( - "Error happended trying to set Monsoon usbpassthrough to auto") - - def reconnect_dut(self): - """Reconnect DUT to monsoon and start sl4a services. - - raises: - MonsoonError: monsoon erro trying to reconnect usb - Turn usbpassthrough on and start the sl4a services. - """ - self.log.info("Reconnecting dut.") - try: - # If wait for device failed, reset monsoon and try it again, if - # this still fails, then raise - try: - self._wait_for_device(self.dut) - except acts.utils.TimeoutError: - self.log.info('Retry-reset monsoon and connect again') - self.usb('off') - time.sleep(1) - self.usb('on') - self._wait_for_device(self.dut) - # Wait for device to come back online. - time.sleep(2) - self.dut.start_services() - # Release wake lock to put device into sleep. - self.dut.droid.goToSleepNow() - self.log.info("Dut reconnected.") - except Exception as e: - raise MonsoonError("Error happened trying to reconnect DUT") - - def measure_power(self, hz, duration, tag, offset=30): - """Measure power consumption of the attached device. - - Because it takes some time for the device to calm down after the usb - connection is cut, an offset is set for each measurement. The default - is 30s. The total time taken to measure will be (duration + offset). - - Args: - hz: Number of samples to take per second. - duration: Number of seconds to take samples for in each step. - offset: The number of seconds of initial data to discard. - tag: A string that's the name of the collected data group. - - Returns: - A MonsoonData object with the measured power data. - """ - num = duration * hz - oset = offset * hz - data = None - try: - data = self.take_samples(hz, num, sample_offset=oset) - if not data: - raise MonsoonError( - ("No data was collected in measurement %s.") % tag) - data.tag = tag - self.log.info("Measurement summary: %s", repr(data)) - finally: - self.mon.StopDataCollection() - return data + objs.append(HvpmMonsoon(serial=serial_number)) + return objs - def reconnect_monsoon(self): - """Reconnect Monsoon to serial port. - """ - logging.info("Close serial connection") - self.mon.ser.close() - logging.info("Reset serial port") - time.sleep(5) - logging.info("Open serial connection") - self.mon.ser.open() - self.mon.ser.reset_input_buffer() - self.mon.ser.reset_output_buffer() +def destroy(monsoons): + for monsoon in monsoons: + monsoon.release_monsoon_connection() diff --git a/acts/framework/acts/controllers/monsoon_lib/api/common.py b/acts/framework/acts/controllers/monsoon_lib/api/common.py index fffed4cbd3..f932535467 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/common.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/common.py @@ -40,7 +40,37 @@ PASSTHROUGH_STATES = { } -class MonsoonData(object): +class MonsoonDataRecord(object): + """A data class for Monsoon data points.""" + def __init__(self, time, current): + """Creates a new MonsoonDataRecord. + + Args: + time: the string '{time}s', where time is measured in seconds since + the beginning of the data collection. + current: The current in Amperes as a string. + """ + self._time = float(time[:-1]) + self._current = float(current) + + @property + def time(self): + """The time the record was fetched.""" + return self._time + + @property + def current(self): + """The amount of current in Amperes measured for the given record.""" + return self._current + + @classmethod + def create_from_record_line(cls, line): + """Creates a data record from the line passed in from the output file. + """ + return cls(*line.split(' ')) + + +class MonsoonResult(object): """An object that contains aggregated data collected during sampling. Attributes: @@ -53,19 +83,48 @@ class MonsoonData(object): # The number of decimal places to round a value to. ROUND_TO = 6 - def __init__(self, num_samples, sum_currents, hz, voltage, tag=None): + def __init__(self, num_samples, sum_currents, hz, voltage, datafile_path): + """Creates a new MonsoonResult. + + Args: + num_samples: the number of samples collected. + sum_currents: the total summation of every current measurement. + hz: the number of samples per second. + voltage: the voltage used during the test. + datafile_path: the path to the monsoon data file. + """ self._num_samples = num_samples self._sum_currents = sum_currents self._hz = hz self._voltage = voltage - self.tag = tag + self.tag = datafile_path + + def get_data_points(self): + """Returns an iterator of MonsoonDataRecords.""" + class MonsoonDataIterator: + def __init__(self, file): + self.file = file + + def __iter__(self): + with open(self.file, 'r') as f: + for line in f: + # Remove the newline character. + line.strip() + yield MonsoonDataRecord.create_from_record_line(line) + + return MonsoonDataIterator(self.tag) + + @property + def num_samples(self): + """The number of samples recorded during the test.""" + return self._num_samples @property def average_current(self): """Average current in mA.""" - if self._num_samples == 0: + if self.num_samples == 0: return 0 - return round(self._sum_currents * 1000 / self._num_samples, + return round(self._sum_currents * 1000 / self.num_samples, self.ROUND_TO) @property @@ -79,8 +138,14 @@ class MonsoonData(object): """Total power used.""" return round(self.average_current * self._voltage, self.ROUND_TO) + @property + def voltage(self): + """The voltage during the measurement (in Volts).""" + return self._voltage + def __str__(self): return ('avg current: %s\n' 'total charge: %s\n' - 'total power: %s' % (self.average_current, self.total_charge, - self.total_power)) + 'total power: %s\n' + 'total samples: %s' % (self.average_current, self.total_charge, + self.total_power, self._num_samples)) diff --git a/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py index 6dc72c8f91..f1b03c9114 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py @@ -20,7 +20,7 @@ import time from Monsoon import HVPM from Monsoon import Operations as op -from acts.controllers.monsoon_lib.api.common import MonsoonData +from acts.controllers.monsoon_lib.api.common import MonsoonResult from acts.controllers.monsoon_lib.api.monsoon import BaseMonsoon from acts.controllers.monsoon_lib.sampling.engine.assembly_line import AssemblyLineBuilder from acts.controllers.monsoon_lib.sampling.engine.assembly_line import ThreadAssemblyLine @@ -137,8 +137,9 @@ class Monsoon(BaseMonsoon): manager.shutdown() self._mon.setup_usb(self.serial) - monsoon_data = MonsoonData(aggregator.num_samples, - aggregator.sum_currents, hz, voltage) + monsoon_data = MonsoonResult(aggregator.num_samples, + aggregator.sum_currents, hz, voltage, + output_path) self._log.info('Measurement summary:\n%s', str(monsoon_data)) return monsoon_data diff --git a/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py index b54b6794fc..e8d116d83f 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py @@ -16,7 +16,7 @@ import multiprocessing import time -from acts.controllers.monsoon_lib.api.common import MonsoonData +from acts.controllers.monsoon_lib.api.common import MonsoonResult from acts.controllers.monsoon_lib.api.lvpm_stock.monsoon_proxy import MonsoonProxy from acts.controllers.monsoon_lib.api.monsoon import BaseMonsoon from acts.controllers.monsoon_lib.sampling.engine.assembly_line import AssemblyLineBuilder @@ -119,8 +119,9 @@ class Monsoon(BaseMonsoon): manager.shutdown() - monsoon_data = MonsoonData(aggregator.num_samples, - aggregator.sum_currents, hz, voltage) + monsoon_data = MonsoonResult(aggregator.num_samples, + aggregator.sum_currents, hz, voltage, + output_path) self._log.info('Measurement summary:\n%s', str(monsoon_data)) return monsoon_data diff --git a/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py index 4916da2f15..a55fc8a0e0 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py @@ -160,6 +160,8 @@ class BaseMonsoon(object): """Deprecated. Use the connection callbacks instead.""" def on_reconnect(): + # Make sure the device is connected and available for commands. + android_device.wait_for_boot_completion() android_device.start_services() # Release wake lock to put device into sleep. android_device.droid.goToSleepNow() @@ -180,15 +182,6 @@ class BaseMonsoon(object): """Sets the callback to be called when Monsoon reconnects USB.""" self.on_reconnect = callback - def monsoon_usb_auto(self): - """Deprecated. Use usb('auto') or usb(PassthroughStates.AUTO) instead. - """ - self.usb(PassthroughStates.AUTO) - - def reconnect_dut(self): - """Deprecated. Use usb('on') or usb(PassthroughStates.ON) instead.""" - self.usb(PassthroughStates.ON) - def take_samples(self, assembly_line): """Runs the sampling procedure based on the given assembly line.""" # Sampling is always done in a separate process. Release the Monsoon diff --git a/acts/framework/acts/test_utils/power/PowerBaseTest.py b/acts/framework/acts/test_utils/power/PowerBaseTest.py index 928c6bfab1..2334702828 100644 --- a/acts/framework/acts/test_utils/power/PowerBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerBaseTest.py @@ -17,16 +17,19 @@ import json import logging import math import os +import re import time + import acts.controllers.iperf_server as ipf from acts import asserts from acts import base_test from acts import utils -from acts.controllers import monsoon +from acts.controllers.monsoon_lib.api.common import MonsoonError +from acts.controllers.monsoon_lib.api.common import PassthroughStates from acts.metrics.loggers.blackbox import BlackboxMetricLogger from acts.test_utils.power.loggers.power_metric_logger import PowerMetricLogger -from acts.test_utils.wifi import wifi_test_utils as wutils from acts.test_utils.wifi import wifi_power_test_utils as wputils +from acts.test_utils.wifi import wifi_test_utils as wutils RESET_BATTERY_STATS = 'dumpsys batterystats --reset' IPERF_TIMEOUT = 180 @@ -49,6 +52,7 @@ class ObjNew(): """Create a random obj with unknown attributes and value. """ + def __init__(self, **kwargs): self.__dict__.update(kwargs) @@ -67,6 +71,7 @@ class PowerBaseTest(base_test.BaseTestClass): """Base class for all wireless power related tests. """ + def __init__(self, controllers): base_test.BaseTestClass.__init__(self, controllers) @@ -108,6 +113,7 @@ class PowerBaseTest(base_test.BaseTestClass): # Setup the must have controllers, phone and monsoon self.dut = self.android_devices[0] self.mon_data_path = os.path.join(self.log_path, 'Monsoon') + os.makedirs(self.mon_data_path, exist_ok=True) self.mon = self.monsoons[0] self.mon.set_max_current(8.0) self.mon.set_voltage(self.mon_voltage) @@ -123,7 +129,7 @@ class PowerBaseTest(base_test.BaseTestClass): self.network_file = file elif 'rockbottom_' + self.dut.model in file: self.rockbottom_script = file - #Abort the class if threshold and rockbottom file is missing + # Abort the class if threshold and rockbottom file is missing asserts.abort_class_if( not self.threshold_file, 'Required test pass/fail threshold file is missing') @@ -138,7 +144,7 @@ class PowerBaseTest(base_test.BaseTestClass): self.mon_info = self.create_monsoon_info() # Sync device time, timezone and country code - utils.require_sl4a((self.dut, )) + utils.require_sl4a((self.dut,)) utils.sync_device_time(self.dut) self.dut.droid.wifiSetCountryCode('US') @@ -213,6 +219,8 @@ class PowerBaseTest(base_test.BaseTestClass): Args: file: the common file containing pass fail threshold. + test_specific: if True, returns the JSON element within the file + that starts with the test class name. """ with open(file, 'r') as f: params = json.load(f) @@ -260,20 +268,23 @@ class PowerBaseTest(base_test.BaseTestClass): """The actual test flow and result processing and validate. """ - self.collect_power_data() - self.pass_fail_check() + result = self.collect_power_data() + self.pass_fail_check(result.average_current) def collect_power_data(self): """Measure power, plot and take log if needed. + Returns: + A MonsoonResult object. """ - tag = '' # Collecting current measurement data and plot - self.file_path, self.test_result = self.monsoon_data_collect_save() - self.power_result.metric_value = self.test_result * self.mon_voltage - wputils.monsoon_data_plot(self.mon_info, self.file_path, tag=tag) + result = self.monsoon_data_collect_save() + self.power_result.metric_value = (result.average_current * + self.mon_voltage) + wputils.monsoon_data_plot(self.mon_info, result) + return result - def pass_fail_check(self): + def pass_fail_check(self, average_current=None): """Check the test result and decide if it passed or failed. The threshold is provided in the config file. In this class, result is @@ -286,18 +297,18 @@ class PowerBaseTest(base_test.BaseTestClass): return current_threshold = self.threshold[self.test_name] - if self.test_result: + if average_current: asserts.assert_true( - abs(self.test_result - current_threshold) / current_threshold < + abs(average_current - current_threshold) / current_threshold < self.pass_fail_tolerance, 'Measured average current in [{}]: {:.2f}mA, which is ' 'out of the acceptable range {:.2f}±{:.2f}mA'.format( - self.test_name, self.test_result, current_threshold, + self.test_name, average_current, current_threshold, self.pass_fail_tolerance * current_threshold)) asserts.explicit_pass( 'Measurement finished for [{}]: {:.2f}mA, which is ' 'within the acceptable range {:.2f}±{:.2f}'.format( - self.test_name, self.test_result, current_threshold, + self.test_name, average_current, current_threshold, self.pass_fail_tolerance * current_threshold)) else: asserts.fail( @@ -334,8 +345,12 @@ class PowerBaseTest(base_test.BaseTestClass): logging.info('Monsoon recovered from unexpected error') time.sleep(2) return True - except monsoon.MonsoonError: - logging.info(self.mon.mon.ser.in_waiting) + except MonsoonError: + try: + self.log.info(self.mon_info.dut._mon.ser.in_waiting) + except AttributeError: + # This attribute does not exist for HVPMs. + pass logging.warning('Unable to recover monsoon from unexpected error') return False @@ -346,94 +361,70 @@ class PowerBaseTest(base_test.BaseTestClass): log file. Take bug report if requested. Returns: - data_path: the absolute path to the log file of monsoon current - measurement - avg_current: the average current of the test + A MonsoonResult object containing information about the gathered + data. """ tag = '{}_{}_{}'.format(self.test_name, self.dut.model, self.dut.build_info['build_id']) + data_path = os.path.join(self.mon_info.data_path, '{}.txt'.format(tag)) - total_expected_samples = self.mon_info.freq * (self.mon_info.duration + - self.mon_info.offset) - min_required_samples = total_expected_samples * MIN_PERCENT_SAMPLE / 100 - # Retry counter for monsoon data aquisition - retry_measure = 1 - # Indicator that need to re-collect data - need_collect_data = 1 - result = None - while retry_measure <= MEASUREMENT_RETRY_COUNT: - try: - # If need to retake data - if need_collect_data == 1: - #Resets the battery status right before the test started - self.dut.adb.shell(RESET_BATTERY_STATS) - self.log.info( - 'Starting power measurement with monsoon box, try #{}'. - format(retry_measure)) - #Start the power measurement using monsoon - self.mon_info.dut.monsoon_usb_auto() - result = self.mon_info.dut.measure_power( - self.mon_info.freq, - self.mon_info.duration, - tag=tag, - offset=self.mon_info.offset) - self.mon_info.dut.reconnect_dut() - # Reconnect to dut - else: - self.mon_info.dut.reconnect_dut() - # Reconnect and return measurement results if no error happens - avg_current = result.average_current - monsoon.MonsoonData.save_to_text_file([result], data_path) - self.log.info('Power measurement done within {} try'.format( + + # If the specified Monsoon data file already exists (e.g., multiple + # measurements in a single test), write the results to a new file with + # the postfix "_#". + if os.path.exists(data_path): + highest_value = 1 + for filename in os.listdir(os.path.dirname(data_path)): + match = re.match(r'{}_(\d+).txt'.format(tag), filename) + if match: + highest_value = int(match.group(1)) + + data_path = os.path.join(self.mon_info.data_path, + '%s_%s.txt' % (tag, highest_value + 1)) + + total_expected_samples = self.mon_info.freq * self.mon_info.duration + min_required_samples = (total_expected_samples + * MIN_PERCENT_SAMPLE / 100) + for retry_measure in range(1, MEASUREMENT_RETRY_COUNT + 1): + # Resets the battery status right before the test starts. + self.dut.adb.shell(RESET_BATTERY_STATS) + self.log.info( + 'Starting power measurement, attempt #{}.'.format( retry_measure)) - return data_path, avg_current - # Catch monsoon errors during measurement - except monsoon.MonsoonError: - self.log.info(self.mon_info.dut.mon.ser.in_waiting) - # Break early if it's one count away from limit - if retry_measure == MEASUREMENT_RETRY_COUNT: - self.log.error( - 'Test failed after maximum measurement retry') - break - - self.log.warning('Monsoon error happened, now try to recover') - # Retry loop to recover monsoon from error - retry_monsoon = 1 - while retry_monsoon <= RECOVER_MONSOON_RETRY_COUNT: - mon_status = self.monsoon_recover() - if mon_status: - break - else: - retry_monsoon += 1 - self.log.warning( - 'Wait for {} second then try again'.format( - MONSOON_RETRY_INTERVAL)) - time.sleep(MONSOON_RETRY_INTERVAL) - - # Break the loop to end test if failed to recover monsoon - if not mon_status: - self.log.error( - 'Tried our best, still failed to recover monsoon') - break - else: - # If there is no data, or captured samples are less than min - # required, re-take - if not result: - self.log.warning('No data taken, need to remeasure') - elif len(result._data_points) <= min_required_samples: - self.log.warning( - 'More than {} percent of samples are missing due to monsoon error. Need to remeasure' - .format(100 - MIN_PERCENT_SAMPLE)) - else: - need_collect_data = 0 - self.log.warning( - 'Data collected is valid, try reconnect to DUT to finish test' - ) - retry_measure += 1 - - if retry_measure > MEASUREMENT_RETRY_COUNT: - self.log.error('Test failed after maximum measurement retry') + # Start the power measurement using monsoon. + self.mon_info.dut.usb(PassthroughStates.AUTO) + result = self.mon_info.dut.measure_power( + self.mon_info.duration, + measure_after_seconds=self.mon_info.offset, + hz=self.mon_info.freq, + output_path=data_path) + self.mon_info.dut.usb(PassthroughStates.ON) + + self.log.debug(result) + self.log.debug('Samples Gathered: %s. Max Samples: %s ' + 'Min Samples Required: %s.' % + (result.num_samples, total_expected_samples, + min_required_samples)) + + if result.num_samples <= min_required_samples: + retry_measure += 1 + self.log.warning( + 'More than {} percent of samples are missing due to ' + 'dropped packets. Need to remeasure.'.format( + 100 - MIN_PERCENT_SAMPLE)) + continue + + self.log.info('Measurement successful after {} attempt(s).'.format( + retry_measure)) + return result + else: + try: + self.log.info(self.mon_info.dut._mon.ser.in_waiting) + except AttributeError: + # This attribute does not exist for HVPMs. + pass + self.log.error('Unable to gather enough samples to run validation.') def process_iperf_results(self): """Get the iperf results and process. @@ -458,7 +449,7 @@ class PowerBaseTest(base_test.BaseTestClass): throughput = (math.fsum( iperf_result.instantaneous_rates[self.start_meas_time:-1] ) / len(iperf_result.instantaneous_rates[self.start_meas_time:-1]) - ) * 8 * (1.024**2) + ) * 8 * (1.024 ** 2) self.log.info('The average throughput is {}'.format(throughput)) except ValueError: diff --git a/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py b/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py index 14c95975c9..5f38aec086 100644 --- a/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py @@ -17,6 +17,8 @@ import logging import time +import os + import acts.test_utils.power.PowerBaseTest as PBT from acts import base_test @@ -38,40 +40,50 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): """ def collect_power_data(self): - """Measure power and plot. - - """ - tag = '_Power' - super().collect_power_data() - self.monsoon_data_plot_power(self.mon_info, self.file_path, tag=tag) + """Measure power and plot.""" + result = super().collect_power_data() + self.monsoon_data_plot_power(self.mon_info, result, tag='_Power') + return result - def monsoon_data_plot_power(self, mon_info, file_path, tag=""): + def monsoon_data_plot_power(self, mon_info, monsoon_results, tag=''): """Plot the monsoon power data using bokeh interactive plotting tool. Args: - mon_info: Dictionary with the monsoon packet config - file_path: the path to the monsoon log file with current data + mon_info: Dictionary with the monsoon packet config. + monsoon_results: a MonsoonResult or list of MonsoonResult objects to + to plot. + tag: an extra tag to append to the resulting filename. """ - self.log.info("Plot the power measurement data") - # Get results as monsoon data object from the input file - results = monsoon.MonsoonData.from_text_file(file_path) - # Decouple current and timestamp data from the monsoon object - current_data = [] - timestamps = [] - voltage = results[0].voltage - for result in results: - current_data.extend(result.data_points) - timestamps.extend(result.timestamps) - period = 1 / mon_info.freq - time_relative = [x * period for x in range(len(current_data))] - # Calculate the average current for the test - - current_data = [x * 1000 for x in current_data] - power_data = [x * voltage for x in current_data] - avg_current = sum(current_data) / len(current_data) - color = ['navy'] * len(current_data) + if not isinstance(monsoon_results, list): + monsoon_results = [monsoon_results] + logging.info('Plotting the power measurement data.') + + voltage = monsoon_results[0].voltage + + total_current = 0 + total_samples = 0 + for result in monsoon_results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + avg_current = total_current / total_samples + + time_relative = [ + data_point.time + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + power_data = [ + data_point.current * voltage + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + total_data_points = sum( + result.num_samples for result in monsoon_results) + color = ['navy'] * total_data_points # Preparing the data and source link for bokehn java callback source = ColumnDataSource( @@ -94,18 +106,20 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): dt = DataTable( source=s2, columns=columns, width=1300, height=60, editable=True) - plot_title = file_path[file_path.rfind('/') + 1:-4] + tag - output_file("%s/%s.html" % (mon_info.data_path, plot_title)) - TOOLS = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' + plot_title = (os.path.basename( + os.path.splitext(monsoon_results[0].tag)[0]) + + tag) + output_file(os.path.join(mon_info.data_path, plot_title + '.html')) + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' # Create a new plot with the datatable above plot = figure( plot_width=1300, plot_height=700, title=plot_title, - tools=TOOLS, - output_backend="webgl") - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) + tools=tools, + output_backend='webgl') + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='width')) + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='height')) plot.line('x0', 'y0', source=source, line_width=2) plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color') plot.xaxis.axis_label = 'Time (s)' @@ -119,8 +133,7 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): code=customjsscript) # Layout the plot and the datatable bar - l = layout([[dt], [plot]]) - save(l) + save(layout([[dt], [plot]])) def disconnect_usb(self, ad, sleeptime): """Disconnect usb while device is on sleep and diff --git a/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py b/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py index 9592cb6643..937656e352 100644 --- a/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py @@ -115,10 +115,11 @@ class PowerWiFiBaseTest(PBT.PowerBaseTest): If IPERF is run, need to pull iperf results and attach it to the plot. """ - super().collect_power_data() + result = super().collect_power_data() tag = '' if self.iperf_duration: throughput = self.process_iperf_results() tag = '_RSSI_{0:d}dBm_Throughput_{1:.2f}Mbps'.format( self.RSSI, throughput) - wputils.monsoon_data_plot(self.mon_info, self.file_path, tag=tag) + wputils.monsoon_data_plot(self.mon_info, result, tag=tag) + return result diff --git a/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py b/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py index 34a820b934..ac18a8f3e8 100644 --- a/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py +++ b/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py @@ -16,8 +16,8 @@ import logging import time +import os from acts import utils -from acts.controllers import monsoon from acts.libs.proc import job from acts.controllers.ap_lib import bridge_interface as bi from acts.test_utils.wifi import wifi_test_utils as wutils @@ -39,7 +39,7 @@ ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM=' MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM=' -def monsoon_data_plot(mon_info, file_path, tag=""): +def monsoon_data_plot(mon_info, monsoon_results, tag=''): """Plot the monsoon current data using bokeh interactive plotting tool. Plotting power measurement data with bokeh to generate interactive plots. @@ -50,9 +50,10 @@ def monsoon_data_plot(mon_info, file_path, tag=""): Args: mon_info: obj with information of monsoon measurement, including - monsoon device object, measurement frequency, duration and - offset etc. - file_path: the path to the monsoon log file with current data + monsoon device object, measurement frequency, duration, etc. + monsoon_results: a MonsoonResult or list of MonsoonResult objects to + to plot. + tag: an extra tag to append to the resulting filename. Returns: plot: the plotting object of bokeh, optional, will be needed if multiple @@ -60,25 +61,35 @@ def monsoon_data_plot(mon_info, file_path, tag=""): dt: the datatable object of bokeh, optional, will be needed if multiple datatables will be combined to one html file. """ + if not isinstance(monsoon_results, list): + monsoon_results = [monsoon_results] + logging.info('Plotting the power measurement data.') + + voltage = monsoon_results[0].voltage + + total_current = 0 + total_samples = 0 + for result in monsoon_results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + avg_current = total_current / total_samples + + time_relative = [ + data_point.time + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] - log = logging.getLogger() - log.info("Plot the power measurement data") - #Get results as monsoon data object from the input file - results = monsoon.MonsoonData.from_text_file(file_path) - #Decouple current and timestamp data from the monsoon object - current_data = [] - timestamps = [] - voltage = results[0].voltage - [current_data.extend(x.data_points) for x in results] - [timestamps.extend(x.timestamps) for x in results] - period = 1 / float(mon_info.freq) - time_relative = [x * period for x in range(len(current_data))] - #Calculate the average current for the test - current_data = [x * 1000 for x in current_data] - avg_current = sum(current_data) / len(current_data) - color = ['navy'] * len(current_data) - - #Preparing the data and source link for bokehn java callback + current_data = [ + data_point.current * 1000 + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + total_data_points = sum(result.num_samples for result in monsoon_results) + color = ['navy'] * total_data_points + + # Preparing the data and source link for bokehn java callback source = ColumnDataSource( data=dict(x0=time_relative, y0=current_data, color=color)) s2 = ColumnDataSource( @@ -88,7 +99,7 @@ def monsoon_data_plot(mon_info, file_path, tag=""): x0=[round(avg_current * voltage, 2)], z1=[round(avg_current * voltage * mon_info.duration, 2)], z2=[round(avg_current * mon_info.duration, 2)])) - #Setting up data table for the output + # Setting up data table for the output columns = [ TableColumn(field='z0', title='Total Duration (s)'), TableColumn(field='y0', title='Average Current (mA)'), @@ -99,26 +110,29 @@ def monsoon_data_plot(mon_info, file_path, tag=""): dt = DataTable( source=s2, columns=columns, width=1300, height=60, editable=True) - plot_title = file_path[file_path.rfind('/') + 1:-4] + tag - output_file("%s/%s.html" % (mon_info.data_path, plot_title)) - TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') + plot_title = (os.path.basename(os.path.splitext(monsoon_results[0].tag)[0]) + + tag) + output_file(os.path.join(mon_info.data_path, plot_title + '.html')) + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' # Create a new plot with the datatable above plot = figure( - plot_width=1300, plot_height=700, title=plot_title, tools=TOOLS) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) + plot_width=1300, + plot_height=700, + title=plot_title, + tools=tools, + output_backend='webgl') + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='width')) + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='height')) plot.line('x0', 'y0', source=source, line_width=2) plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color') plot.xaxis.axis_label = 'Time (s)' plot.yaxis.axis_label = 'Current (mA)' plot.title.text_font_size = {'value': '15pt'} - #Callback Java scripting + # Callback JavaScript source.selected.js_on_change( "indices", - CustomJS( - args=dict(source=source, mytable=dt), - code=""" + CustomJS(args=dict(source=source, mytable=dt), code=""" var inds = cb_obj.indices; var d1 = source.data; var d2 = mytable.source.data; @@ -154,10 +168,9 @@ def monsoon_data_plot(mon_info, file_path, tag=""): mytable.change.emit(); """)) - #Layout the plot and the datatable bar - l = layout([[dt], [plot]]) - save(l) - return [plot, dt] + # Layout the plot and the datatable bar + save(layout([[dt], [plot]])) + return plot, dt def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10): @@ -287,12 +300,12 @@ def bokeh_plot(data_sets, Returns: plot: bokeh plot figure object """ - TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' plot = figure( plot_width=1300, plot_height=700, title=fig_property['title'], - tools=TOOLS, + tools=tools, output_backend="webgl") plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) @@ -324,7 +337,7 @@ def bokeh_plot(data_sets, legend=str(legend), fill_color=color) - #Plot properties + # Plot properties plot.xaxis.axis_label = fig_property['x_label'] plot.yaxis.axis_label = fig_property['y_label'] plot.legend.location = "top_right" diff --git a/acts/framework/setup.py b/acts/framework/setup.py index b5e4467bb6..32b8a93cad 100755 --- a/acts/framework/setup.py +++ b/acts/framework/setup.py @@ -39,6 +39,7 @@ install_requires = [ 'xlsxwriter', 'mobly', 'grpcio', + 'Monsoon', # paramiko-ng is needed vs paramiko as currently paramiko does not support # ed25519 ssh keys, which is what Fuchsia uses. 'paramiko-ng', diff --git a/acts/framework/tests/acts_import_unit_test.py b/acts/framework/tests/acts_import_unit_test.py index 3dd4857fd0..38dc3bf103 100755 --- a/acts/framework/tests/acts_import_unit_test.py +++ b/acts/framework/tests/acts_import_unit_test.py @@ -48,6 +48,12 @@ else: PY_FILE_REGEX = re.compile('.+\.py$') BLACKLIST = [ + # TODO(markdr): Remove these after BT team evaluates these tests. + 'acts/test_utils/bt/PowerBaseTest.py', + 'tests/google/ble/power/GattPowerTest.py', + 'tests/google/bt/power/A2dpPowerTest.py', + 'tests/google/ble/power/BleScanPowerTest.py', + 'acts/controllers/rohdeschwarz_lib/contest.py', 'acts/controllers/native.py', 'acts/controllers/native_android_device.py', diff --git a/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py b/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py index ec274b956e..9d628959d9 100755 --- a/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py +++ b/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py @@ -209,15 +209,6 @@ class BaseMonsoonTest(unittest.TestCase): 'usbPassthroughMode should not be called when the ' 'state does not change.') - def test_monsoon_usb_auto_sets_usb_state_to_auto(self): - monsoon = MonsoonImpl() - - monsoon.monsoon_usb_auto() - - self.assertEqual(monsoon.status.usbPassthroughMode, - PassthroughStates.AUTO, - 'monsoon_usb_auto() did not disconnect USB.') - def take_samples_always_reestablishes_the_monsoon_connection(self): monsoon = MonsoonImpl() assembly_line = mock.Mock() diff --git a/acts/tests/google/power/PowerBaselineTest.py b/acts/tests/google/power/PowerBaselineTest.py index b75514423d..d8ef277b2e 100644 --- a/acts/tests/google/power/PowerBaselineTest.py +++ b/acts/tests/google/power/PowerBaselineTest.py @@ -31,7 +31,7 @@ class PowerBaselineTest(PowerBaseTest): self.dut.droid.goToSleepNow() # Measure power - self.collect_power_data() + result = self.collect_power_data() # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py b/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py index d24e398fd6..9c2ce9a2ef 100644 --- a/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py +++ b/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py @@ -35,8 +35,8 @@ class PowerGnssDpoSimTest(GBT.PowerGnssBaseTest): Decode the test config from the test name, set device to desired state. Measure power and plot results. """ - self.collect_power_data() - self.pass_fail_check() + result = self.collect_power_data() + self.pass_fail_check(result.average_current) # Test cases def test_gnss_dpoOFF_measurement(self): diff --git a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py index 66aa45815c..e5aabb76f9 100644 --- a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py @@ -119,7 +119,7 @@ class PowerTelHotspotTest(PowerTelTrafficTest): iperf_helpers = self.start_tel_traffic(self.android_devices[1]) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Wait for iPerf to finish time.sleep(self.IPERF_MARGIN + 2) @@ -129,7 +129,7 @@ class PowerTelHotspotTest(PowerTelTrafficTest): iperf_helpers) # Checks if power is below the required threshold. - self.pass_fail_check() + self.pass_fail_check(result.average_current) def setup_test(self): """ Executed before every test case. diff --git a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py index 934fe20dc0..f1197988c6 100644 --- a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py @@ -33,7 +33,7 @@ class PowerTelIdleTest(PWCEL.PowerCellularLabBaseTest): self.cellular_simulator.wait_until_idle_state(idle_wait_time) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py index 0abee5050a..b373fdb3d0 100644 --- a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py @@ -158,7 +158,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): iperf_helpers = self.start_tel_traffic(self.dut) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Wait for iPerf to finish time.sleep(self.IPERF_MARGIN + 2) @@ -167,9 +167,9 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): self.iperf_results = self.get_iperf_results(self.dut, iperf_helpers) # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) - return self.test_result, self.iperf_results + return result.average_current, self.iperf_results def get_iperf_results(self, device, iperf_helpers): """ Pulls iperf results from the device. @@ -196,7 +196,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): return throughput - def pass_fail_check(self): + def pass_fail_check(self, average_current=None): """ Checks power consumption and throughput. Uses the base class method to check power consumption. Also, compares @@ -230,7 +230,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): direction, round(throughput, 3), round(expected_t, 3), round(throughput / expected_t, 3))) - super().pass_fail_check() + super().pass_fail_check(average_current) def start_tel_traffic(self, client_host): """ Starts iPerf in the indicated device and initiates traffic. diff --git a/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py b/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py index f0d0eda6ce..93b69c6e2b 100644 --- a/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py @@ -70,10 +70,10 @@ class PowerTelVoiceCallTest(PWCEL.PowerCellularLabBaseTest): self.dut.droid.goToSleepNow() # Measure power - self.collect_power_data() + result = self.collect_power_data() # End the call hangup_call(self.log, self.dut) # Check if power measurement is within the required values - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py b/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py index f554d12bde..57e926376a 100644 --- a/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py +++ b/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py @@ -158,7 +158,7 @@ class PowerWiFiHotspotTest(PWBT.PowerWiFiBaseTest): time.sleep(2) # Measure power - self.collect_power_data() + result = self.collect_power_data() if traffic: # Wait for iperf to finish @@ -168,7 +168,7 @@ class PowerWiFiHotspotTest(PWBT.PowerWiFiBaseTest): self.client_iperf_helper.process_iperf_results( self.dut, self.log, self.iperf_servers, self.test_name) - self.pass_fail_check() + self.pass_fail_check(result.average_current) def power_idle_tethering_test(self): """ Start power test when Hotspot is idle diff --git a/acts/tests/google/power/wifi/PowerWiFiroamingTest.py b/acts/tests/google/power/wifi/PowerWiFiroamingTest.py index 98409895f4..66110a1f34 100644 --- a/acts/tests/google/power/wifi/PowerWiFiroamingTest.py +++ b/acts/tests/google/power/wifi/PowerWiFiroamingTest.py @@ -81,22 +81,30 @@ class PowerWiFiroamingTest(PWBT.PowerWiFiBaseTest): time.sleep(5) # Toggle between two networks begin_time = utils.get_current_epoch_time() + results = [] for i in range(self.toggle_times): self.dut.log.info('Connecting to %s' % network_main[wc.SSID]) self.dut.droid.wifiConnect(network_main) - file_path, avg_current = self.monsoon_data_collect_save() + results.append(self.monsoon_data_collect_save()) self.dut.log.info('Connecting to %s' % network_aux[wc.SSID]) self.dut.droid.wifiConnect(network_aux) - file_path, avg_current = self.monsoon_data_collect_save() - [plot, dt] = wputils.monsoon_data_plot(self.mon_info, file_path) - self.test_result = dt.source.data['y0'][0] - self.power_result.metric_value = ( - self.test_result * PHONE_BATTERY_VOLTAGE) + results.append(self.monsoon_data_collect_save()) + wputils.monsoon_data_plot(self.mon_info, results) + + total_current = 0 + total_samples = 0 + for result in results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + average_current = total_current / total_samples + + self.power_result.metric_value = [result.total_power for result in + results] # Take Bugreport if self.bug_report: self.dut.take_bug_report(self.test_name, begin_time) # Path fail check - self.pass_fail_check() + self.pass_fail_check(average_current) @test_tracker_info(uuid='e5ff95c0-b17e-425c-a903-821ba555a9b9') def test_screenon_toggle_between_AP(self): @@ -119,22 +127,30 @@ class PowerWiFiroamingTest(PWBT.PowerWiFiBaseTest): time.sleep(5) # Toggle between two networks begin_time = utils.get_current_epoch_time() + results = [] for i in range(self.toggle_times): self.dut.log.info('Connecting to %s' % network_main[wc.SSID]) self.dut.droid.wifiConnect(network_main) - file_path, avg_current = self.monsoon_data_collect_save() + results.append(self.monsoon_data_collect_save()) self.dut.log.info('Connecting to %s' % network_aux[wc.SSID]) self.dut.droid.wifiConnect(network_aux) - file_path, avg_current = self.monsoon_data_collect_save() - [plot, dt] = wputils.monsoon_data_plot(self.mon_info, file_path) - self.test_result = dt.source.data['y0'][0] - self.power_result.metric_value = ( - self.test_result * PHONE_BATTERY_VOLTAGE) + results.append(self.monsoon_data_collect_save()) + wputils.monsoon_data_plot(self.mon_info, results) + + total_current = 0 + total_samples = 0 + for result in results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + average_current = total_current / total_samples + + self.power_result.metric_value = [result.total_power for result in + results] # Take Bugreport if self.bug_report: self.dut.take_bug_report(self.test_name, begin_time) # Path fail check - self.pass_fail_check() + self.pass_fail_check(average_current) @test_tracker_info(uuid='a16ae337-326f-4d09-990f-42232c3c0dc4') def test_screenoff_wifi_wedge(self): diff --git a/acts/tests/google/wifi/WifiSensitivityTest.py b/acts/tests/google/wifi/WifiSensitivityTest.py index 19476843cd..a982cc6972 100644 --- a/acts/tests/google/wifi/WifiSensitivityTest.py +++ b/acts/tests/google/wifi/WifiSensitivityTest.py @@ -194,7 +194,7 @@ class WifiSensitivityTest(WifiRvrTest, WifiPingTest): Args: result: dict containing attenuation, throughput and other meta - data + data """ try: golden_path = next(file_name @@ -207,7 +207,7 @@ class WifiSensitivityTest(WifiRvrTest, WifiPingTest): except: golden_sensitivity = float('nan') - result_string = ('Througput = {}%, Sensitivity = {}.' + result_string = ('Throughput = {}%, Sensitivity = {}.' 'Target Sensitivity = {}'.format( result['peak_throughput_pct'], result['sensitivity'], golden_sensitivity)) |