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/).
18Based on the original py2 script of kens@google.com
19"""
20
21import fcntl
22import logging
23import os
24import select
25import struct
26import sys
27import time
28import collections
29
30# http://pyserial.sourceforge.net/
31# On ubuntu, apt-get install python3-pyserial
32import serial
33
34import acts.signals
35
36from acts import utils
37from acts.controllers import android_device
38
39ACTS_CONTROLLER_CONFIG_NAME = "Monsoon"
40ACTS_CONTROLLER_REFERENCE_NAME = "monsoons"
41
42
43def create(configs):
44    objs = []
45    for c in configs:
46        objs.append(Monsoon(serial=c))
47    return objs
48
49
50def destroy(objs):
51    for obj in objs:
52        fcntl.flock(obj.mon._tempfile, fcntl.LOCK_UN)
53        obj.mon._tempfile.close()
54
55
56class MonsoonError(acts.signals.ControllerError):
57    """Raised for exceptions encountered in monsoon lib."""
58
59
60class 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
410class 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
638class 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