• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@google.com (Ang Li)'
22_author_ = 'kens@google.com (Ken Shirriff)'
23
24import fcntl
25import os
26import select
27import struct
28import sys
29import time
30import traceback
31import collections
32
33# http://pyserial.sourceforge.net/
34# On ubuntu, apt-get install python3-pyserial
35import serial
36
37import acts.logger
38import acts.signals
39
40from acts import utils
41from acts.controllers import android_device
42
43ACTS_CONTROLLER_CONFIG_NAME = "Monsoon"
44ACTS_CONTROLLER_REFERENCE_NAME = "monsoons"
45
46def create(configs, logger):
47    objs = []
48    for c in configs:
49        objs.append(Monsoon(serial=c, logger=logger))
50    return objs
51
52def destroy(objs):
53    return
54
55class MonsoonError(acts.signals.ControllerError):
56    """Raised for exceptions encountered in monsoon lib."""
57
58class 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
368class 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
586class 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