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