summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2019-11-08 22:27:59 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2019-11-08 22:27:59 +0000
commit647d282e6ab964c6a5bf8a1c6e623ecf62f45d04 (patch)
treee690fc8da9354e9458863002071634da27a53135
parent21afc1ecc6465eb2ab8a7e63858e7d4e14fdbaaa (diff)
parente54396ca91c1c9b2327f8e6adf8d40a9795f0798 (diff)
downloadplatform_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."
-rwxr-xr-xacts/framework/acts/bin/monsoon.py131
-rw-r--r--acts/framework/acts/controllers/monsoon.py1010
-rw-r--r--acts/framework/acts/controllers/monsoon_lib/api/common.py79
-rw-r--r--acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py7
-rw-r--r--acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py7
-rw-r--r--acts/framework/acts/controllers/monsoon_lib/api/monsoon.py11
-rw-r--r--acts/framework/acts/test_utils/power/PowerBaseTest.py191
-rw-r--r--acts/framework/acts/test_utils/power/PowerGnssBaseTest.py85
-rw-r--r--acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py5
-rw-r--r--acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py95
-rwxr-xr-xacts/framework/setup.py1
-rwxr-xr-xacts/framework/tests/acts_import_unit_test.py6
-rwxr-xr-xacts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py9
-rw-r--r--acts/tests/google/power/PowerBaselineTest.py4
-rw-r--r--acts/tests/google/power/gnss/PowerGnssDpoSimTest.py4
-rw-r--r--acts/tests/google/power/tel/lab/PowerTelHotspotTest.py4
-rw-r--r--acts/tests/google/power/tel/lab/PowerTelIdleTest.py4
-rw-r--r--acts/tests/google/power/tel/lab/PowerTelTrafficTest.py10
-rw-r--r--acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py4
-rw-r--r--acts/tests/google/power/wifi/PowerWiFiHotspotTest.py4
-rw-r--r--acts/tests/google/power/wifi/PowerWiFiroamingTest.py44
-rw-r--r--acts/tests/google/wifi/WifiSensitivityTest.py4
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))