Home | History | Annotate | Download | only in power_monitors
      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