1 #!/usr/bin/python 2 3 # Copyright (C) 2014 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 18 import fcntl 19 import logging 20 logging.getLogger().setLevel(logging.ERROR) 21 22 import os.path 23 import select 24 import stat 25 import struct 26 import sys 27 import time 28 import collections 29 import socket 30 import glob 31 import signal 32 import serial # http://pyserial.sourceforge.net/ 33 34 #Set to True if you want log output to go to screen: 35 LOG_TO_SCREEN = False 36 37 TIMEOUT_SERIAL = 1 #seconds 38 39 #ignore SIG CONTINUE signals 40 for signum in [signal.SIGCONT]: 41 signal.signal(signum, signal.SIG_IGN) 42 43 try: 44 from . import Abstract_Power_Monitor 45 except: 46 sys.exit("You cannot run 'monsoon.py' directly. Run 'execut_power_tests.py' instead.") 47 48 class Power_Monitor(Abstract_Power_Monitor): 49 """ 50 Provides a simple class to use the power meter, e.g. 51 mon = monsoon.Power_Monitor() 52 mon.SetVoltage(3.7) 53 mon.StartDataCollection() 54 mydata = [] 55 while len(mydata) < 1000: 56 mydata.extend(mon.CollectData()) 57 mon.StopDataCollection() 58 """ 59 _do_log = False 60 61 @staticmethod 62 def lock( device ): 63 tmpname = "/tmp/monsoon.%s.%s" % ( os.uname()[0], 64 os.path.basename(device)) 65 lockfile = open(tmpname, "w") 66 try: # use a lockfile to ensure exclusive access 67 fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 68 logging.debug("Locked device %s"%device) 69 except IOError as e: 70 self.log("device %s is in use" % dev) 71 sys.exit('device in use') 72 return lockfile 73 74 def to_string(self): 75 return self._devicename 76 77 def __init__(self, device = None, wait = False, log_file_id= None ): 78 """ 79 Establish a connection to a Power_Monitor. 80 By default, opens the first available port, waiting if none are ready. 81 A particular port can be specified with "device". 82 With wait=0, IOError is thrown if a device is not immediately available. 83 """ 84 self._lockfile = None 85 self._logfile = None 86 self.ser = None 87 for signum in [signal.SIGALRM, signal.SIGHUP, signal.SIGINT, 88 signal.SIGILL, signal.SIGQUIT, 89 signal.SIGTRAP,signal.SIGABRT, signal.SIGIOT, signal.SIGBUS, 90 signal.SIGFPE, signal.SIGSEGV, signal.SIGUSR2, signal.SIGPIPE, 91 signal.SIGTERM]: 92 signal.signal(signum, self.handle_signal) 93 94 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0 95 self._coarse_scale = self._fine_scale = 0 96 self._last_seq = 0 97 self.start_voltage = 0 98 99 if device: 100 if isinstance( device, serial.Serial ): 101 self.ser = device 102 103 else: 104 device_list = None 105 while not device_list: 106 device_list = Power_Monitor.Discover() 107 if not device_list and wait: 108 time.sleep(1.0) 109 logging.info("No power monitor serial devices found. Retrying...") 110 elif not device_list and not wait: 111 logging.error("No power monitor serial devices found. Exiting") 112 self.Close() 113 sys.exit("No power monitor serial devices found") 114 115 if device_list: 116 if len(device_list) > 1: 117 logging.error("=======================================") 118 logging.error("More than one power monitor discovered!") 119 logging.error("Test may not execute properly.Aborting test.") 120 logging.error("=======================================") 121 sys.exit("More than one power monitor connected.") 122 device = device_list[0].to_string() # choose the first one 123 if len(device_list) > 1: 124 logging.info("More than one device found. Using %s"%device) 125 else: 126 logging.info("Power monitor @ %s"%device) 127 else: raise IOError("No device found") 128 129 self._lockfile = Power_Monitor.lock( device ) 130 if log_file_id is not None: 131 self._logfilename = "/tmp/monsoon_%s_%s.%s.log" % (os.uname()[0], os.path.basename(device), 132 log_file_id) 133 self._logfile = open(self._logfilename,'a') 134 else: 135 self._logfile = None 136 try: 137 self.ser = serial.Serial(device, timeout= TIMEOUT_SERIAL) 138 except Exception as e: 139 self.log( "error opening device %s: %s" % (dev, e)) 140 self._lockfile.close() 141 raise 142 logging.debug("Setting up power monitor...") 143 self._devicename = device 144 #just in case, stop any active data collection on monsoon 145 self._dataCollectionActive = True 146 self.StopDataCollection() 147 logging.debug("Flushing input...") 148 self._FlushInput() # discard stale input 149 logging.debug("Getting status....") 150 status = self.GetStatus() 151 152 if not status: 153 self.log( "no response from device %s" % device) 154 self._lockfile.close() 155 raise IOError("Failed to get status from device") 156 self.start_voltage = status["voltage1"] 157 158 def __del__(self): 159 self.Close() 160 161 def Close(self): 162 if self._logfile: 163 print("=============\n"+\ 164 "Power Monitor log file can be found at '%s'"%self._logfilename + 165 "=============\n") 166 self._logfile.close() 167 self._logfile = None 168 if (self.ser): 169 #self.StopDataCollection() 170 self.ser.flush() 171 self.ser.close() 172 self.ser = None 173 if self._lockfile: 174 self._lockfile.close() 175 176 def log(self, msg , debug = False): 177 if self._logfile: self._logfile.write( msg + "\n") 178 if not debug and LOG_TO_SCREEN: 179 logging.error( msg ) 180 else: 181 logging.debug(msg) 182 183 def handle_signal( self, signum, frame): 184 if self.ser: 185 self.ser.flush() 186 self.ser.close() 187 self.ser = None 188 self.log("Got signal %d"%signum) 189 sys.exit("\nGot signal %d\n"%signum) 190 191 @staticmethod 192 def Discover(): 193 monsoon_list = [] 194 elapsed = 0 195 logging.info("Discovering power monitor(s)...") 196 ser_device_list = glob.glob("/dev/ttyACM*") 197 logging.info("Seeking devices %s"%ser_device_list) 198 for dev in ser_device_list: 199 try: 200 lockfile = Power_Monitor.lock( dev ) 201 except: 202 logging.info( "... device %s in use, skipping"%dev) 203 continue 204 tries = 0 205 ser = None 206 while ser is None and tries < 100: 207 try: # try to open the device 208 ser = serial.Serial( dev, timeout=TIMEOUT_SERIAL) 209 except Exception as e: 210 logging.error( "error opening device %s: %s" % (dev, e) ) 211 tries += 1 212 time.sleep(2); 213 ser = None 214 logging.info("... found device %s"%dev) 215 lockfile.close()#will be re-locked once monsoon instance created 216 logging.debug("unlocked") 217 if not ser: 218 continue 219 if ser is not None: 220 try: 221 monsoon = Power_Monitor(device = dev) 222 status = monsoon.GetStatus() 223 224 if not status: 225 monsoon.log("... no response from device %s, skipping") 226 continue 227 else: 228 logging.info("... found power monitor @ %s"%dev) 229 monsoon_list.append( monsoon ) 230 except: 231 import traceback 232 traceback.print_exc() 233 logging.error("... %s appears to not be a monsoon device"%dev) 234 logging.debug("Returning list of %s"%monsoon_list) 235 return monsoon_list 236 237 def GetStatus(self): 238 """ Requests and waits for status. Returns status dictionary. """ 239 240 # status packet format 241 self.log("Getting status...", debug = True) 242 STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH" 243 STATUS_FIELDS = [ 244 "packetType", "firmwareVersion", "protocolVersion", 245 "mainFineCurrent", "usbFineCurrent", "auxFineCurrent", "voltage1", 246 "mainCoarseCurrent", "usbCoarseCurrent", "auxCoarseCurrent", "voltage2", 247 "outputVoltageSetting", "temperature", "status", "leds", 248 "mainFineResistor", "serialNumber", "sampleRate", 249 "dacCalLow", "dacCalHigh", 250 "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime", 251 "usbFineResistor", "auxFineResistor", 252 "initialUsbVoltage", "initialAuxVoltage", 253 "hardwareRevision", "temperatureLimit", "usbPassthroughMode", 254 "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor", 255 "defMainFineResistor", "defUsbFineResistor", "defAuxFineResistor", 256 "defMainCoarseResistor", "defUsbCoarseResistor", "defAuxCoarseResistor", 257 "eventCode", "eventData", ] 258 259 self._SendStruct("BBB", 0x01, 0x00, 0x00) 260 while True: # Keep reading, discarding non-status packets 261 bytes = self._ReadPacket() 262 if not bytes: return None 263 if len(bytes) != struct.calcsize(STATUS_FORMAT) or bytes[0] != "\x10": 264 self.log("wanted status, dropped type=0x%02x, len=%d" % ( 265 ord(bytes[0]), len(bytes))) 266 continue 267 268 status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, bytes))) 269 assert status["packetType"] == 0x10 270 for k in status.keys(): 271 if k.endswith("VoltageSetting"): 272 status[k] = 2.0 + status[k] * 0.01 273 elif k.endswith("FineCurrent"): 274 pass # needs calibration data 275 elif k.endswith("CoarseCurrent"): 276 pass # needs calibration data 277 elif k.startswith("voltage") or k.endswith("Voltage"): 278 status[k] = status[k] * 0.000125 279 elif k.endswith("Resistor"): 280 status[k] = 0.05 + status[k] * 0.0001 281 if k.startswith("aux") or k.startswith("defAux"): status[k] += 0.05 282 elif k.endswith("CurrentLimit"): 283 status[k] = 8 * (1023 - status[k]) / 1023.0 284 #self.log( "Returning requested status: \n %s"%(status), debug = True) 285 return status 286 287 def RampVoltage(self, start, end): 288 v = start 289 if v < 3.0: v = 3.0 # protocol doesn't support lower than this 290 while (v < end): 291 self.SetVoltage(v) 292 v += .1 293 time.sleep(.1) 294 self.SetVoltage(end) 295 296 def SetVoltage(self, v): 297 """ Set the output voltage, 0 to disable. """ 298 self.log("Setting voltage to %s..."%v, debug = True) 299 if v == 0: 300 self._SendStruct("BBB", 0x01, 0x01, 0x00) 301 else: 302 self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100)) 303 self.log("...Set voltage", debug = True) 304 305 def SetMaxCurrent(self, i): 306 """Set the max output current.""" 307 assert i >= 0 and i <= 8 308 self.log("Setting max current to %s..."%i, debug = True) 309 val = 1023 - int((i/8)*1023) 310 self._SendStruct("BBB", 0x01, 0x0a, val & 0xff) 311 self._SendStruct("BBB", 0x01, 0x0b, val >> 8) 312 self.log("...Set max current.", debug = True) 313 314 def SetUsbPassthrough(self, val): 315 """ Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto. """ 316 self._SendStruct("BBB", 0x01, 0x10, val) 317 318 def StartDataCollection(self): 319 """ Tell the device to start collecting and sending measurement data. """ 320 self.log("Starting data collection...", debug = True) 321 self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command 322 self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) 323 self.log("...started", debug = True) 324 self._dataCollectionActive = True 325 326 def StopDataCollection(self): 327 """ Tell the device to stop collecting measurement data. """ 328 self._SendStruct("BB", 0x03, 0x00) # stop 329 if self._dataCollectionActive: 330 while self.CollectData(False) is not None: 331 pass 332 self._dataCollectionActive = False 333 334 def CollectData(self, verbose = True): 335 """ Return some current samples. Call StartDataCollection() first. """ 336 #self.log("Collecting data ...", debug = True) 337 while True: # loop until we get data or a timeout 338 bytes = self._ReadPacket(verbose) 339 340 if not bytes: return None 341 if len(bytes) < 4 + 8 + 1 or bytes[0] < "\x20" or bytes[0] > "\x2F": 342 if verbose: self.log( "wanted data, dropped type=0x%02x, len=%d" % ( 343 ord(bytes[0]), len(bytes)), debug=verbose) 344 continue 345 346 seq, type, x, y = struct.unpack("BBBB", bytes[:4]) 347 data = [struct.unpack(">hhhh", bytes[x:x+8]) 348 for x in range(4, len(bytes) - 8, 8)] 349 350 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: 351 self.log( "data sequence skipped, lost packet?" ) 352 self._last_seq = seq 353 354 if type == 0: 355 if not self._coarse_scale or not self._fine_scale: 356 self.log("waiting for calibration, dropped data packet") 357 continue 358 359 out = [] 360 for main, usb, aux, voltage in data: 361 if main & 1: 362 out.append(((main & ~1) - self._coarse_zero) * self._coarse_scale) 363 else: 364 out.append((main - self._fine_zero) * self._fine_scale) 365 #self.log("...Collected %d samples"%(len(out)), debug = True) 366 return out 367 368 elif type == 1: 369 self._fine_zero = data[0][0] 370 self._coarse_zero = data[1][0] 371 372 elif type == 2: 373 self._fine_ref = data[0][0] 374 self._coarse_ref = data[1][0] 375 376 else: 377 self.log( "discarding data packet type=0x%02x" % type) 378 continue 379 380 if self._coarse_ref != self._coarse_zero: 381 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero) 382 if self._fine_ref != self._fine_zero: 383 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) 384 385 386 def _SendStruct(self, fmt, *args): 387 """ Pack a struct (without length or checksum) and send it. """ 388 data = struct.pack(fmt, *args) 389 data_len = len(data) + 1 390 checksum = (data_len + sum(struct.unpack("B" * len(data), data))) % 256 391 out = struct.pack("B", data_len) + data + struct.pack("B", checksum) 392 self.ser.write(out) 393 self.ser.flush() 394 395 def _ReadPacket(self, verbose = True): 396 """ Read a single data record as a string (without length or checksum). """ 397 len_char = self.ser.read(1) 398 if not len_char: 399 if verbose: self.log( "timeout reading from serial port" ) 400 return None 401 402 data_len = struct.unpack("B", len_char) 403 data_len = ord(len_char) 404 if not data_len: return "" 405 406 result = self.ser.read(data_len) 407 if len(result) != data_len: return None 408 body = result[:-1] 409 checksum = (data_len + sum(struct.unpack("B" * len(body), body))) % 256 410 if result[-1] != struct.pack("B", checksum): 411 self.log( "Invalid checksum from serial port" ) 412 return None 413 return result[:-1] 414 415 def _FlushInput(self): 416 """ Flush all read data until no more available. """ 417 self.ser.flushInput() 418 flushed = 0 419 self.log("Flushing input...", debug = True) 420 while True: 421 ready_r, ready_w, ready_x = select.select([self.ser], [], [self.ser], 0) 422 if len(ready_x) > 0: 423 self.log( "exception from serial port" ) 424 return None 425 elif len(ready_r) > 0: 426 flushed += 1 427 self.ser.read(1) # This may cause underlying buffering. 428 self.ser.flush() # Flush the underlying buffer too. 429 else: 430 break 431 if flushed > 0: 432 self.log( "flushed >%d bytes" % flushed, debug = True ) 433 434