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