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