Home | History | Annotate | Download | only in controllers
      1 #!/usr/bin/env python3.4
      2 #
      3 #   Copyright 2016 - The Android Open Source Project
      4 #
      5 #   Licensed under the Apache License, Version 2.0 (the "License");
      6 #   you may not use this file except in compliance with the License.
      7 #   You may obtain a copy of the License at
      8 #
      9 #       http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 #   Unless required by applicable law or agreed to in writing, software
     12 #   distributed under the License is distributed on an "AS IS" BASIS,
     13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 #   See the License for the specific language governing permissions and
     15 #   limitations under the License.
     16 
     17 """Interface for a USB-connected Monsoon power meter
     18 (http://msoon.com/LabEquipment/PowerMonitor/).
     19 """
     20 
     21 _new_author_ = 'angli (at] google.com (Ang Li)'
     22 _author_ = 'kens (at] google.com (Ken Shirriff)'
     23 
     24 import fcntl
     25 import os
     26 import select
     27 import struct
     28 import sys
     29 import time
     30 import traceback
     31 import collections
     32 
     33 # http://pyserial.sourceforge.net/
     34 # On ubuntu, apt-get install python3-pyserial
     35 import serial
     36 
     37 import acts.logger
     38 import acts.signals
     39 
     40 from acts import utils
     41 from acts.controllers import android_device
     42 
     43 ACTS_CONTROLLER_CONFIG_NAME = "Monsoon"
     44 ACTS_CONTROLLER_REFERENCE_NAME = "monsoons"
     45 
     46 def create(configs, logger):
     47     objs = []
     48     for c in configs:
     49         objs.append(Monsoon(serial=c, logger=logger))
     50     return objs
     51 
     52 def destroy(objs):
     53     return
     54 
     55 class MonsoonError(acts.signals.ControllerError):
     56     """Raised for exceptions encountered in monsoon lib."""
     57 
     58 class MonsoonProxy:
     59     """Class that directly talks to monsoon over serial.
     60 
     61     Provides a simple class to use the power meter, e.g.
     62     mon = monsoon.Monsoon()
     63     mon.SetVoltage(3.7)
     64     mon.StartDataCollection()
     65     mydata = []
     66     while len(mydata) < 1000:
     67         mydata.extend(mon.CollectData())
     68     mon.StopDataCollection()
     69 
     70     See http://wiki/Main/MonsoonProtocol for information on the protocol.
     71     """
     72 
     73     def __init__(self, device=None, serialno=None, wait=1):
     74         """Establish a connection to a Monsoon.
     75 
     76         By default, opens the first available port, waiting if none are ready.
     77         A particular port can be specified with "device", or a particular
     78         Monsoon can be specified with "serialno" (using the number printed on
     79         its back). With wait=0, IOError is thrown if a device is not
     80         immediately available.
     81         """
     82         self._coarse_ref = self._fine_ref = self._coarse_zero = 0
     83         self._fine_zero = self._coarse_scale = self._fine_scale = 0
     84         self._last_seq = 0
     85         self.start_voltage = 0
     86         self.serial = serialno
     87 
     88         if device:
     89             self.ser = serial.Serial(device, timeout=1)
     90             return
     91         # Try all devices connected through USB virtual serial ports until we
     92         # find one we can use.
     93         while True:
     94             for dev in os.listdir("/dev"):
     95                 prefix = "ttyACM"
     96                 # Prefix is different on Mac OS X.
     97                 if sys.platform == "darwin":
     98                     prefix = "tty.usbmodem"
     99                 if not dev.startswith(prefix):
    100                     continue
    101                 tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev)
    102                 self._tempfile = open(tmpname, "w")
    103                 try:
    104                     os.chmod(tmpname, 0o666)
    105                 except OSError as e:
    106                     pass
    107 
    108                 try:  # use a lockfile to ensure exclusive access
    109                     fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
    110                 except IOError as e:
    111                     # TODO(angli): get rid of all print statements.
    112                     print("device %s is in use" % dev, file=sys.stderr)
    113                     continue
    114 
    115                 try:  # try to open the device
    116                     self.ser = serial.Serial("/dev/%s" % dev, timeout=1)
    117                     self.StopDataCollection()  # just in case
    118                     self._FlushInput()  # discard stale input
    119                     status = self.GetStatus()
    120                 except Exception as e:
    121                     print("error opening device %s: %s" % (dev, e),
    122                         file=sys.stderr)
    123                     print(traceback.format_exc())
    124                     continue
    125 
    126                 if not status:
    127                     print("no response from device %s" % dev, file=sys.stderr)
    128                 elif serialno and status["serialNumber"] != serialno:
    129                     print(("Note: another device serial #%d seen on %s" %
    130                         (status["serialNumber"], dev)), file=sys.stderr)
    131                 else:
    132                     self.start_voltage = status["voltage1"]
    133                     return
    134 
    135             self._tempfile = None
    136             if not wait: raise IOError("No device found")
    137             print("Waiting for device...", file=sys.stderr)
    138             time.sleep(1)
    139 
    140     def GetStatus(self):
    141         """Requests and waits for status.
    142 
    143         Returns:
    144             status dictionary.
    145         """
    146         # status packet format
    147         STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH"
    148         STATUS_FIELDS = [
    149                 "packetType", "firmwareVersion", "protocolVersion",
    150                 "mainFineCurrent", "usbFineCurrent", "auxFineCurrent",
    151                 "voltage1", "mainCoarseCurrent", "usbCoarseCurrent",
    152                 "auxCoarseCurrent", "voltage2", "outputVoltageSetting",
    153                 "temperature", "status", "leds", "mainFineResistor",
    154                 "serialNumber", "sampleRate", "dacCalLow", "dacCalHigh",
    155                 "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime",
    156                 "usbFineResistor", "auxFineResistor",
    157                 "initialUsbVoltage", "initialAuxVoltage",
    158                 "hardwareRevision", "temperatureLimit", "usbPassthroughMode",
    159                 "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor",
    160                 "defMainFineResistor", "defUsbFineResistor",
    161                 "defAuxFineResistor", "defMainCoarseResistor",
    162                 "defUsbCoarseResistor", "defAuxCoarseResistor", "eventCode",
    163                 "eventData", ]
    164 
    165         self._SendStruct("BBB", 0x01, 0x00, 0x00)
    166         while 1:  # Keep reading, discarding non-status packets
    167             read_bytes = self._ReadPacket()
    168             if not read_bytes:
    169                 return None
    170             calsize = struct.calcsize(STATUS_FORMAT)
    171             if len(read_bytes) != calsize or read_bytes[0] != 0x10:
    172                 print("Wanted status, dropped type=0x%02x, len=%d" % (
    173                     read_bytes[0], len(read_bytes)), file=sys.stderr)
    174                 continue
    175             status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT,
    176                 read_bytes)))
    177             p_type = status["packetType"]
    178             if p_type != 0x10:
    179                 raise MonsoonError("Package type %s is not 0x10." % p_type)
    180             for k in status.keys():
    181                 if k.endswith("VoltageSetting"):
    182                     status[k] = 2.0 + status[k] * 0.01
    183                 elif k.endswith("FineCurrent"):
    184                     pass # needs calibration data
    185                 elif k.endswith("CoarseCurrent"):
    186                     pass # needs calibration data
    187                 elif k.startswith("voltage") or k.endswith("Voltage"):
    188                     status[k] = status[k] * 0.000125
    189                 elif k.endswith("Resistor"):
    190                     status[k] = 0.05 + status[k] * 0.0001
    191                     if k.startswith("aux") or k.startswith("defAux"):
    192                         status[k] += 0.05
    193                 elif k.endswith("CurrentLimit"):
    194                     status[k] = 8 * (1023 - status[k]) / 1023.0
    195             return status
    196 
    197     def RampVoltage(self, start, end):
    198         v = start
    199         if v < 3.0: v = 3.0 # protocol doesn't support lower than this
    200         while (v < end):
    201             self.SetVoltage(v)
    202             v += .1
    203             time.sleep(.1)
    204         self.SetVoltage(end)
    205 
    206     def SetVoltage(self, v):
    207         """Set the output voltage, 0 to disable.
    208         """
    209         if v == 0:
    210             self._SendStruct("BBB", 0x01, 0x01, 0x00)
    211         else:
    212             self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100))
    213 
    214     def GetVoltage(self):
    215         """Get the output voltage.
    216 
    217         Returns:
    218             Current Output Voltage (in unit of v).
    219         """
    220         return self.GetStatus()["outputVoltageSetting"]
    221 
    222     def SetMaxCurrent(self, i):
    223         """Set the max output current.
    224         """
    225         if i < 0 or i > 8:
    226             raise MonsoonError(("Target max current %sA, is out of acceptable "
    227                 "range [0, 8].") % i)
    228         val = 1023 - int((i/8)*1023)
    229         self._SendStruct("BBB", 0x01, 0x0a, val & 0xff)
    230         self._SendStruct("BBB", 0x01, 0x0b, val >> 8)
    231 
    232     def SetMaxPowerUpCurrent(self, i):
    233         """Set the max power up current.
    234         """
    235         if i < 0 or i > 8:
    236             raise MonsoonError(("Target max current %sA, is out of acceptable "
    237                 "range [0, 8].") % i)
    238         val = 1023 - int((i/8)*1023)
    239         self._SendStruct("BBB", 0x01, 0x08, val & 0xff)
    240         self._SendStruct("BBB", 0x01, 0x09, val >> 8)
    241 
    242     def SetUsbPassthrough(self, val):
    243         """Set the USB passthrough mode: 0 = off, 1 = on,  2 = auto.
    244         """
    245         self._SendStruct("BBB", 0x01, 0x10, val)
    246 
    247     def GetUsbPassthrough(self):
    248         """Get the USB passthrough mode: 0 = off, 1 = on,  2 = auto.
    249 
    250         Returns:
    251             Current USB passthrough mode.
    252         """
    253         return self.GetStatus()["usbPassthroughMode"]
    254 
    255     def StartDataCollection(self):
    256         """Tell the device to start collecting and sending measurement data.
    257         """
    258         self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command
    259         self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
    260 
    261     def StopDataCollection(self):
    262         """Tell the device to stop collecting measurement data.
    263         """
    264         self._SendStruct("BB", 0x03, 0x00) # stop
    265 
    266     def CollectData(self):
    267         """Return some current samples. Call StartDataCollection() first.
    268         """
    269         while 1:  # loop until we get data or a timeout
    270             _bytes = self._ReadPacket()
    271             if not _bytes:
    272                 return None
    273             if len(_bytes) < 4 + 8 + 1 or _bytes[0] < 0x20 or _bytes[0] > 0x2F:
    274                 print("Wanted data, dropped type=0x%02x, len=%d" % (
    275                     _bytes[0], len(_bytes)), file=sys.stderr)
    276                 continue
    277 
    278             seq, _type, x, y = struct.unpack("BBBB", _bytes[:4])
    279             data = [struct.unpack(">hhhh", _bytes[x:x+8])
    280                             for x in range(4, len(_bytes) - 8, 8)]
    281 
    282             if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
    283                 print("Data sequence skipped, lost packet?", file=sys.stderr)
    284             self._last_seq = seq
    285 
    286             if _type == 0:
    287                 if not self._coarse_scale or not self._fine_scale:
    288                     print("Waiting for calibration, dropped data packet.",
    289                         file=sys.stderr)
    290                     continue
    291                 out = []
    292                 for main, usb, aux, voltage in data:
    293                     if main & 1:
    294                         coarse = ((main & ~1) - self._coarse_zero)
    295                         out.append(coarse * self._coarse_scale)
    296                     else:
    297                         out.append((main - self._fine_zero) * self._fine_scale)
    298                 return out
    299             elif _type == 1:
    300                 self._fine_zero = data[0][0]
    301                 self._coarse_zero = data[1][0]
    302             elif _type == 2:
    303                 self._fine_ref = data[0][0]
    304                 self._coarse_ref = data[1][0]
    305             else:
    306                 print("Discarding data packet type=0x%02x" % _type,
    307                     file=sys.stderr)
    308                 continue
    309 
    310             # See http://wiki/Main/MonsoonProtocol for details on these values.
    311             if self._coarse_ref != self._coarse_zero:
    312                 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
    313             if self._fine_ref != self._fine_zero:
    314                 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
    315 
    316     def _SendStruct(self, fmt, *args):
    317         """Pack a struct (without length or checksum) and send it.
    318         """
    319         data = struct.pack(fmt, *args)
    320         data_len = len(data) + 1
    321         checksum = (data_len + sum(data)) % 256
    322         out = bytes([data_len]) + data + bytes([checksum])
    323         self.ser.write(out)
    324 
    325     def _ReadPacket(self):
    326         """Read a single data record as a string (without length or checksum).
    327         """
    328         len_char = self.ser.read(1)
    329         if not len_char:
    330             print("Reading from serial port timed out.", file=sys.stderr)
    331             return None
    332 
    333         data_len = ord(len_char)
    334         if not data_len:
    335             return ""
    336         result = self.ser.read(int(data_len))
    337         if len(result) != data_len:
    338             print("Length mismatch, expected %d bytes, got %d bytes." % data_len,
    339                 len(result))
    340             return None
    341         body = result[:-1]
    342         checksum = (sum(result[:-1]) + data_len) % 256
    343         if result[-1] != checksum:
    344             print("Invalid checksum from serial port!", file=sys.stderr)
    345             print("Expected {}, got {}".format(hex(checksum), hex(result[-1])))
    346             return None
    347         return result[:-1]
    348 
    349     def _FlushInput(self):
    350         """ Flush all read data until no more available. """
    351         self.ser.flush()
    352         flushed = 0
    353         while True:
    354             ready_r, ready_w, ready_x = select.select([self.ser], [],
    355                 [self.ser], 0)
    356             if len(ready_x) > 0:
    357                 print("exception from serial port", file=sys.stderr)
    358                 return None
    359             elif len(ready_r) > 0:
    360                 flushed += 1
    361                 self.ser.read(1)  # This may cause underlying buffering.
    362                 self.ser.flush()  # Flush the underlying buffer too.
    363             else:
    364                 break
    365         # if flushed > 0:
    366         #     print("dropped >%d bytes" % flushed, file=sys.stderr)
    367 
    368 class MonsoonData:
    369     """A class for reporting power measurement data from monsoon.
    370 
    371     Data means the measured current value in Amps.
    372     """
    373     # Number of digits for long rounding.
    374     lr = 8
    375     # Number of digits for short rounding
    376     sr = 6
    377     # Delimiter for writing multiple MonsoonData objects to text file.
    378     delimiter = "\n\n==========\n\n"
    379 
    380     def __init__(self, data_points, timestamps, hz, voltage, offset=0):
    381         """Instantiates a MonsoonData object.
    382 
    383         Args:
    384             data_points: A list of current values in Amp (float).
    385             timestamps: A list of epoch timestamps (int).
    386             hz: The hertz at which the data points are measured.
    387             voltage: The voltage at which the data points are measured.
    388             offset: The number of initial data points to discard
    389                 in calculations.
    390         """
    391         self._data_points = data_points
    392         self._timestamps = timestamps
    393         self.offset = offset
    394         num_of_data_pt = len(self._data_points)
    395         if self.offset >= num_of_data_pt:
    396             raise MonsoonError(("Offset number (%d) must be smaller than the "
    397                 "number of data points (%d).") % (offset, num_of_data_pt))
    398         self.data_points = self._data_points[self.offset:]
    399         self.timestamps = self._timestamps[self.offset:]
    400         self.hz = hz
    401         self.voltage = voltage
    402         self.tag = None
    403         self._validate_data()
    404 
    405     @property
    406     def average_current(self):
    407         """Average current in the unit of mA.
    408         """
    409         len_data_pt = len(self.data_points)
    410         if len_data_pt == 0:
    411             return 0
    412         cur = sum(self.data_points) * 1000 / len_data_pt
    413         return round(cur, self.sr)
    414 
    415     @property
    416     def total_charge(self):
    417         """Total charged used in the unit of mAh.
    418         """
    419         charge = (sum(self.data_points) / self.hz) * 1000 / 3600
    420         return round(charge, self.sr)
    421 
    422     @property
    423     def total_power(self):
    424         """Total power used.
    425         """
    426         power = self.average_current * self.voltage
    427         return round(power, self.sr)
    428 
    429     @staticmethod
    430     def from_string(data_str):
    431         """Creates a MonsoonData object from a string representation generated
    432         by __str__.
    433 
    434         Args:
    435             str: The string representation of a MonsoonData.
    436 
    437         Returns:
    438             A MonsoonData object.
    439         """
    440         lines = data_str.strip().split('\n')
    441         err_msg = ("Invalid input string format. Is this string generated by "
    442                    "MonsoonData class?")
    443         conditions = [len(lines) <= 4,
    444                       "Average Current:" not in lines[1],
    445                       "Voltage: " not in lines[2],
    446                       "Total Power: " not in lines[3],
    447                       "samples taken at " not in lines[4],
    448                       lines[5] != "Time" + ' ' * 7 + "Amp"]
    449         if any(conditions):
    450             raise MonsoonError(err_msg)
    451         hz_str = lines[4].split()[2]
    452         hz = int(hz_str[:-2])
    453         voltage_str = lines[2].split()[1]
    454         voltage = int(voltage[:-1])
    455         lines = lines[6:]
    456         t = []
    457         v = []
    458         for l in lines:
    459             try:
    460                 timestamp, value = l.split(' ')
    461                 t.append(int(timestamp))
    462                 v.append(float(value))
    463             except ValueError:
    464                 raise MonsoonError(err_msg)
    465         return MonsoonData(v, t, hz, voltage)
    466 
    467     @staticmethod
    468     def save_to_text_file(monsoon_data, file_path):
    469         """Save multiple MonsoonData objects to a text file.
    470 
    471         Args:
    472             monsoon_data: A list of MonsoonData objects to write to a text
    473                 file.
    474             file_path: The full path of the file to save to, including the file
    475                 name.
    476         """
    477         if not monsoon_data:
    478             raise MonsoonError("Attempting to write empty Monsoon data to "
    479                                "file, abort")
    480         utils.create_dir(os.path.dirname(file_path))
    481         with open(file_path, 'w') as f:
    482             for md in monsoon_data:
    483                 f.write(str(md))
    484                 f.write(MonsoonData.delimiter)
    485 
    486     @staticmethod
    487     def from_text_file(file_path):
    488         """Load MonsoonData objects from a text file generated by
    489         MonsoonData.save_to_text_file.
    490 
    491         Args:
    492             file_path: The full path of the file load from, including the file
    493                 name.
    494 
    495         Returns:
    496             A list of MonsoonData objects.
    497         """
    498         results = []
    499         with open(file_path, 'r') as f:
    500             data_strs = f.read().split(MonsoonData.delimiter)
    501             for data_str in data_strs:
    502                 results.append(MonsoonData.from_string(data_str))
    503         return results
    504 
    505     def _validate_data(self):
    506         """Verifies that the data points contained in the class are valid.
    507         """
    508         msg = "Error! Expected {} timestamps, found {}.".format(
    509             len(self._data_points), len(self._timestamps))
    510         if len(self._data_points) != len(self._timestamps):
    511             raise MonsoonError(msg)
    512 
    513     def update_offset(self, new_offset):
    514         """Updates how many data points to skip in caculations.
    515 
    516         Always use this function to update offset instead of directly setting
    517         self.offset.
    518 
    519         Args:
    520             new_offset: The new offset.
    521         """
    522         self.offset = new_offset
    523         self.data_points = self._data_points[self.offset:]
    524         self.timestamps = self._timestamps[self.offset:]
    525 
    526     def get_data_with_timestamps(self):
    527         """Returns the data points with timestamps.
    528 
    529         Returns:
    530             A list of tuples in the format of (timestamp, data)
    531         """
    532         result = []
    533         for t, d in zip(self.timestamps, self.data_points):
    534             result.append(t, round(d, self.lr))
    535         return result
    536 
    537     def get_average_record(self, n):
    538         """Returns a list of average current numbers, each representing the
    539         average over the last n data points.
    540 
    541         Args:
    542             n: Number of data points to average over.
    543 
    544         Returns:
    545             A list of average current values.
    546         """
    547         history_deque = collections.deque()
    548         averages = []
    549         for d in self.data_points:
    550             history_deque.appendleft(d)
    551             if len(history_deque) > n:
    552                 history_deque.pop()
    553             avg = sum(history_deque) / len(history_deque)
    554             averages.append(round(avg, self.lr))
    555         return averages
    556 
    557     def _header(self):
    558         strs = [""]
    559         if self.tag:
    560             strs.append(self.tag)
    561         else:
    562             strs.append("Monsoon Measurement Data")
    563         strs.append("Average Current: {}mA.".format(self.average_current))
    564         strs.append("Voltage: {}V.".format(self.voltage))
    565         strs.append("Total Power: {}mW.".format(self.total_power))
    566         strs.append(("{} samples taken at {}Hz, with an offset of {} samples."
    567                     ).format(len(self._data_points),
    568                              self.hz,
    569                              self.offset))
    570         return "\n".join(strs)
    571 
    572     def __len__(self):
    573         return len(self.data_points)
    574 
    575     def __str__(self):
    576         strs = []
    577         strs.append(self._header())
    578         strs.append("Time" + ' ' * 7 + "Amp")
    579         for t, d in zip(self.timestamps, self.data_points):
    580             strs.append("{} {}".format(t, round(d, self.sr)))
    581         return "\n".join(strs)
    582 
    583     def __repr__(self):
    584         return self._header()
    585 
    586 class Monsoon:
    587     """The wrapper class for test scripts to interact with monsoon.
    588     """
    589     def __init__(self, *args, **kwargs):
    590         serial = kwargs["serial"]
    591         device = None
    592         if "logger" in kwargs:
    593             self.log = acts.logger.LoggerProxy(kwargs["logger"])
    594         else:
    595             self.log = acts.logger.LoggerProxy()
    596         if "device" in kwargs:
    597             device = kwargs["device"]
    598         self.mon = MonsoonProxy(serialno=serial, device=device)
    599         self.dut = None
    600 
    601     def attach_device(self, dut):
    602         """Attach the controller object for the Device Under Test (DUT)
    603         physically attached to the Monsoon box.
    604 
    605         Args:
    606             dut: A controller object representing the device being powered by
    607                 this Monsoon box.
    608         """
    609         self.dut = dut
    610 
    611     def set_voltage(self, volt, ramp=False):
    612         """Sets the output voltage of monsoon.
    613 
    614         Args:
    615             volt: Voltage to set the output to.
    616             ramp: If true, the output voltage will be increased gradually to
    617                 prevent tripping Monsoon overvoltage.
    618         """
    619         if ramp:
    620             self.mon.RampVoltage(mon.start_voltage, volt)
    621         else:
    622             self.mon.SetVoltage(volt)
    623 
    624     def set_max_current(self, cur):
    625         """Sets monsoon's max output current.
    626 
    627         Args:
    628             cur: The max current in A.
    629         """
    630         self.mon.SetMaxCurrent(cur)
    631 
    632     def set_max_init_current(self, cur):
    633         """Sets the max power-up/inital current.
    634 
    635         Args:
    636             cur: The max initial current allowed in mA.
    637         """
    638         self.mon.SetMaxPowerUpCurrent(cur)
    639 
    640     @property
    641     def status(self):
    642         """Gets the status params of monsoon.
    643 
    644         Returns:
    645             A dictionary where each key-value pair represents a monsoon status
    646             param.
    647         """
    648         return self.mon.GetStatus()
    649 
    650     def take_samples(self, sample_hz, sample_num, sample_offset=0, live=False):
    651         """Take samples of the current value supplied by monsoon.
    652 
    653         This is the actual measurement for power consumption. This function
    654         blocks until the number of samples requested has been fulfilled.
    655 
    656         Args:
    657             hz: Number of points to take for every second.
    658             sample_num: Number of samples to take.
    659             offset: The number of initial data points to discard in MonsoonData
    660                 calculations. sample_num is extended by offset to compensate.
    661             live: Print each sample in console as measurement goes on.
    662 
    663         Returns:
    664             A MonsoonData object representing the data obtained in this
    665             sampling. None if sampling is unsuccessful.
    666         """
    667         sys.stdout.flush()
    668         voltage = self.mon.GetVoltage()
    669         self.log.info("Taking samples at %dhz for %ds, voltage %.2fv." % (
    670             sample_hz, sample_num/sample_hz, voltage))
    671         sample_num += sample_offset
    672         # Make sure state is normal
    673         self.mon.StopDataCollection()
    674         status = self.mon.GetStatus()
    675         native_hz = status["sampleRate"] * 1000
    676 
    677         # Collect and average samples as specified
    678         self.mon.StartDataCollection()
    679 
    680         # In case sample_hz doesn't divide native_hz exactly, use this
    681         # invariant: 'offset' = (consumed samples) * sample_hz -
    682         # (emitted samples) * native_hz
    683         # This is the error accumulator in a variation of Bresenham's
    684         # algorithm.
    685         emitted = offset = 0
    686         collected = []
    687         # past n samples for rolling average
    688         history_deque = collections.deque()
    689         current_values = []
    690         timestamps = []
    691 
    692         try:
    693             last_flush = time.time()
    694             while emitted < sample_num or sample_num == -1:
    695                 # The number of raw samples to consume before emitting the next
    696                 # output
    697                 need = int((native_hz - offset + sample_hz - 1) / sample_hz)
    698                 if need > len(collected):     # still need more input samples
    699                     samples = self.mon.CollectData()
    700                     if not samples:
    701                         break
    702                     collected.extend(samples)
    703                 else:
    704                     # Have enough data, generate output samples.
    705                     # Adjust for consuming 'need' input samples.
    706                     offset += need * sample_hz
    707                     # maybe multiple, if sample_hz > native_hz
    708                     while offset >= native_hz:
    709                         # TODO(angli): Optimize "collected" operations.
    710                         this_sample = sum(collected[:need]) / need
    711                         this_time = int(time.time())
    712                         timestamps.append(this_time)
    713                         if live:
    714                             self.log.info("%s %s" % (this_time, this_sample))
    715                         current_values.append(this_sample)
    716                         sys.stdout.flush()
    717                         offset -= native_hz
    718                         emitted += 1 # adjust for emitting 1 output sample
    719                     collected = collected[need:]
    720                     now = time.time()
    721                     if now - last_flush >= 0.99: # flush every second
    722                         sys.stdout.flush()
    723                         last_flush = now
    724         except Exception as e:
    725             pass
    726         self.mon.StopDataCollection()
    727         try:
    728             return MonsoonData(current_values, timestamps, sample_hz,
    729                 voltage, offset=sample_offset)
    730         except:
    731             return None
    732 
    733     @utils.timeout(60)
    734     def usb(self, state):
    735         """Sets the monsoon's USB passthrough mode. This is specific to the
    736         USB port in front of the monsoon box which connects to the powered
    737         device, NOT the USB that is used to talk to the monsoon itself.
    738 
    739         "Off" means USB always off.
    740         "On" means USB always on.
    741         "Auto" means USB is automatically turned off when sampling is going on,
    742         and turned back on when sampling finishes.
    743 
    744         Args:
    745             stats: The state to set the USB passthrough to.
    746 
    747         Returns:
    748             True if the state is legal and set. False otherwise.
    749         """
    750         state_lookup = {
    751             "off": 0,
    752             "on": 1,
    753             "auto": 2
    754         }
    755         state = state.lower()
    756         if state in state_lookup:
    757             current_state = self.mon.GetUsbPassthrough()
    758             while(current_state != state_lookup[state]):
    759                 self.mon.SetUsbPassthrough(state_lookup[state])
    760                 time.sleep(1)
    761                 current_state = self.mon.GetUsbPassthrough()
    762             return True
    763         return False
    764 
    765     def _check_dut(self):
    766         """Verifies there is a DUT attached to the monsoon.
    767 
    768         This should be called in the functions that operate the DUT.
    769         """
    770         if not self.dut:
    771             raise MonsoonError("Need to attach the device before using it.")
    772 
    773     @utils.timeout(15)
    774     def _wait_for_device(self, ad):
    775         while ad.serial not in android_device.list_adb_devices():
    776             pass
    777         ad.adb.wait_for_device()
    778 
    779     def execute_sequence_and_measure(self, step_funcs, hz, duration, offset_sec=20, *args, **kwargs):
    780         """@Deprecated.
    781         Executes a sequence of steps and take samples in-between.
    782 
    783         For each step function, the following steps are followed:
    784         1. The function is executed to put the android device in a state.
    785         2. If the function returns False, skip to next step function.
    786         3. If the function returns True, sl4a session is disconnected.
    787         4. Monsoon takes samples.
    788         5. Sl4a is reconnected.
    789 
    790         Because it takes some time for the device to calm down after the usb
    791         connection is cut, an offset is set for each measurement. The default
    792         is 20s.
    793 
    794         Args:
    795             hz: Number of samples to take per second.
    796             durations: Number(s) of minutes to take samples for in each step.
    797                 If this is an integer, all the steps will sample for the same
    798                 amount of time. If this is an iterable of the same length as
    799                 step_funcs, then each number represents the number of minutes
    800                 to take samples for after each step function.
    801                 e.g. If durations[0] is 10, we'll sample for 10 minutes after
    802                 step_funcs[0] is executed.
    803             step_funcs: A list of funtions, whose first param is an android
    804                 device object. If a step function returns True, samples are
    805                 taken after this step, otherwise we move on to the next step
    806                 function.
    807             ad: The android device object connected to this monsoon.
    808             offset_sec: The number of seconds of initial data to discard.
    809             *args, **kwargs: Extra args to be passed into each step functions.
    810 
    811         Returns:
    812             The MonsoonData objects from samplings.
    813         """
    814         self._check_dut()
    815         sample_nums = []
    816         try:
    817             if len(duration) != len(step_funcs):
    818                 raise MonsoonError(("The number of durations need to be the "
    819                     "same as the number of step functions."))
    820             for d in duration:
    821                 sample_nums.append(d * 60 * hz)
    822         except TypeError:
    823             num = duration * 60 * hz
    824             sample_nums = [num] * len(step_funcs)
    825         results = []
    826         oset = offset_sec * hz
    827         for func, num in zip(step_funcs, sample_nums):
    828             try:
    829                 self.usb("auto")
    830                 step_name = func.__name__
    831                 self.log.info("Executing step function %s." % step_name)
    832                 take_sample = func(ad, *args, **kwargs)
    833                 if not take_sample:
    834                     self.log.info("Skip taking samples for %s" % step_name)
    835                     continue
    836                 time.sleep(1)
    837                 self.dut.terminate_all_sessions()
    838                 time.sleep(1)
    839                 self.log.info("Taking samples for %s." % step_name)
    840                 data = self.take_samples(hz, num, sample_offset=oset)
    841                 if not data:
    842                     raise MonsoonError("Sampling for %s failed." % step_name)
    843                 self.log.info("Sample summary: %s" % repr(data))
    844                 # self.log.debug(str(data))
    845                 data.tag = step_name
    846                 results.append(data)
    847             except Exception:
    848                 msg = "Exception happened during step %s, abort!" % func.__name__
    849                 self.log.exception(msg)
    850                 return results
    851             finally:
    852                 self.mon.StopDataCollection()
    853                 self.usb("on")
    854                 self._wait_for_device(self.dut)
    855                 # Wait for device to come back online.
    856                 time.sleep(10)
    857                 droid, ed = self.dut.get_droid(True)
    858                 ed.start()
    859                 # Release wake lock to put device into sleep.
    860                 droid.goToSleepNow()
    861         return results
    862 
    863     def measure_power(self, hz, duration, tag, offset=30):
    864         """Measure power consumption of the attached device.
    865 
    866         Because it takes some time for the device to calm down after the usb
    867         connection is cut, an offset is set for each measurement. The default
    868         is 20s.
    869 
    870         Args:
    871             hz: Number of samples to take per second.
    872             duration: Number of seconds to take samples for in each step.
    873             offset: The number of seconds of initial data to discard.
    874             tag: A string that's the name of the collected data group.
    875 
    876         Returns:
    877             A MonsoonData object with the measured power data.
    878         """
    879         if offset >= duration:
    880             raise MonsoonError(("Measurement duration (%ds) should be larger "
    881                 "than offset (%ds) for measurement %s."
    882                 ) % (duration, offset, tag))
    883         num = duration * hz
    884         oset = offset * hz
    885         data = None
    886         try:
    887             self.usb("auto")
    888             time.sleep(1)
    889             self.dut.terminate_all_sessions()
    890             time.sleep(1)
    891             data = self.take_samples(hz, num, sample_offset=oset)
    892             if not data:
    893                 raise MonsoonError(("No data was collected in measurement %s."
    894                     ) % tag)
    895             data.tag = tag
    896             self.log.info("Measurement summary: %s" % repr(data))
    897         finally:
    898             self.mon.StopDataCollection()
    899             self.log.info("Finished taking samples, reconnecting to dut.")
    900             self.usb("on")
    901             self._wait_for_device(self.dut)
    902             # Wait for device to come back online.
    903             time.sleep(10)
    904             droid, ed = self.dut.get_droid(True)
    905             ed.start()
    906             # Release wake lock to put device into sleep.
    907             droid.goToSleepNow()
    908             self.log.info("Dut reconncted.")
    909             return data