Home | History | Annotate | Download | only in profiler
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Interface for a USB-connected Monsoon power meter.
      6 
      7 http://msoon.com/LabEquipment/PowerMonitor/
      8 Currently Unix-only. Relies on fcntl, /dev, and /tmp.
      9 """
     10 
     11 import collections
     12 import logging
     13 import os
     14 import select
     15 import struct
     16 import time
     17 
     18 import serial  # pylint: disable=import-error,no-name-in-module
     19 import serial.tools.list_ports  # pylint: disable=import-error,no-name-in-module
     20 
     21 
     22 Power = collections.namedtuple('Power', ['amps', 'volts'])
     23 
     24 
     25 class Monsoon(object):
     26   """Provides a simple class to use the power meter.
     27 
     28   mon = monsoon.Monsoon()
     29   mon.SetVoltage(3.7)
     30   mon.StartDataCollection()
     31   mydata = []
     32   while len(mydata) < 1000:
     33     mydata.extend(mon.CollectData())
     34   mon.StopDataCollection()
     35   """
     36 
     37   def __init__(self, device=None, serialno=None, wait=True):
     38     """Establish a connection to a Monsoon.
     39 
     40     By default, opens the first available port, waiting if none are ready.
     41     A particular port can be specified with 'device', or a particular Monsoon
     42     can be specified with 'serialno' (using the number printed on its back).
     43     With wait=False, IOError is thrown if a device is not immediately available.
     44     """
     45     assert float(serial.VERSION) >= 2.7, \
     46      'Monsoon requires pyserial v2.7 or later. You have %s' % serial.VERSION
     47 
     48     self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0
     49     self._coarse_scale = self._fine_scale = 0
     50     self._last_seq = 0
     51     self._voltage_multiplier = None
     52 
     53     if device:
     54       self.ser = serial.Serial(device, timeout=1)
     55       return
     56 
     57     while 1:
     58       for (port, desc, _) in serial.tools.list_ports.comports():
     59         if not desc.lower().startswith('mobile device power monitor'):
     60           continue
     61         tmpname = '/tmp/monsoon.%s.%s' % (os.uname()[0], os.path.basename(port))
     62         self._tempfile = open(tmpname, 'w')
     63         try:  # Use a lockfile to ensure exclusive access.
     64           # Put the import in here to avoid doing it on unsupported platforms.
     65           import fcntl  # pylint: disable=import-error
     66           fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
     67         except IOError:
     68           logging.error('device %s is in use', port)
     69           continue
     70 
     71         try:  # Try to open the device.
     72           self.ser = serial.Serial(port, timeout=1)
     73           self.StopDataCollection()  # Just in case.
     74           self._FlushInput()  # Discard stale input.
     75           status = self.GetStatus()
     76         except IOError, e:
     77           logging.error('error opening device %s: %s', port, e)
     78           continue
     79 
     80         if not status:
     81           logging.error('no response from device %s', port)
     82         elif serialno and status['serialNumber'] != serialno:
     83           logging.error('device %s is #%d', port, status['serialNumber'])
     84         else:
     85           if status['hardwareRevision'] == 1:
     86             self._voltage_multiplier = 62.5 / 10**6
     87           else:
     88             self._voltage_multiplier = 125.0 / 10**6
     89           return
     90 
     91       self._tempfile = None
     92       if not wait:
     93         raise IOError('No device found')
     94       logging.info('waiting for device...')
     95       time.sleep(1)
     96 
     97   def GetStatus(self):
     98     """Requests and waits for status.  Returns status dictionary."""
     99 
    100     # status packet format
    101     STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH'
    102     STATUS_FIELDS = [
    103         'packetType', 'firmwareVersion', 'protocolVersion',
    104         'mainFineCurrent', 'usbFineCurrent', 'auxFineCurrent', 'voltage1',
    105         'mainCoarseCurrent', 'usbCoarseCurrent', 'auxCoarseCurrent', 'voltage2',
    106         'outputVoltageSetting', 'temperature', 'status', 'leds',
    107         'mainFineResistor', 'serialNumber', 'sampleRate',
    108         'dacCalLow', 'dacCalHigh',
    109         'powerUpCurrentLimit', 'runTimeCurrentLimit', 'powerUpTime',
    110         'usbFineResistor', 'auxFineResistor',
    111         'initialUsbVoltage', 'initialAuxVoltage',
    112         'hardwareRevision', 'temperatureLimit', 'usbPassthroughMode',
    113         'mainCoarseResistor', 'usbCoarseResistor', 'auxCoarseResistor',
    114         'defMainFineResistor', 'defUsbFineResistor', 'defAuxFineResistor',
    115         'defMainCoarseResistor', 'defUsbCoarseResistor', 'defAuxCoarseResistor',
    116         'eventCode', 'eventData',
    117     ]
    118 
    119     self._SendStruct('BBB', 0x01, 0x00, 0x00)
    120     while 1:  # Keep reading, discarding non-status packets.
    121       data = self._ReadPacket()
    122       if not data:
    123         return None
    124       if len(data) != struct.calcsize(STATUS_FORMAT) or data[0] != '\x10':
    125         logging.debug('wanted status, dropped type=0x%02x, len=%d',
    126                       ord(data[0]), len(data))
    127         continue
    128 
    129       status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, data)))
    130       assert status['packetType'] == 0x10
    131       for k in status.keys():
    132         if k.endswith('VoltageSetting'):
    133           status[k] = 2.0 + status[k] * 0.01
    134         elif k.endswith('FineCurrent'):
    135           pass  # Needs calibration data.
    136         elif k.endswith('CoarseCurrent'):
    137           pass  # Needs calibration data.
    138         elif k.startswith('voltage') or k.endswith('Voltage'):
    139           status[k] = status[k] * 0.000125
    140         elif k.endswith('Resistor'):
    141           status[k] = 0.05 + status[k] * 0.0001
    142           if k.startswith('aux') or k.startswith('defAux'):
    143             status[k] += 0.05
    144         elif k.endswith('CurrentLimit'):
    145           status[k] = 8 * (1023 - status[k]) / 1023.0
    146       return status
    147 
    148 
    149   def SetVoltage(self, v):
    150     """Set the output voltage, 0 to disable."""
    151     if v == 0:
    152       self._SendStruct('BBB', 0x01, 0x01, 0x00)
    153     else:
    154       self._SendStruct('BBB', 0x01, 0x01, int((v - 2.0) * 100))
    155 
    156   def SetStartupCurrent(self, a):
    157     """Set the max startup output current. the unit of |a| : Amperes """
    158     assert a >= 0 and a <= 8
    159 
    160     val = 1023 - int((a/8.0)*1023)
    161     self._SendStruct('BBB', 0x01, 0x08, val & 0xff)
    162     self._SendStruct('BBB', 0x01, 0x09, val >> 8)
    163 
    164   def SetMaxCurrent(self, a):
    165     """Set the max output current. the unit of |a| : Amperes """
    166     assert a >= 0 and a <= 8
    167 
    168     val = 1023 - int((a/8.0)*1023)
    169     self._SendStruct('BBB', 0x01, 0x0a, val & 0xff)
    170     self._SendStruct('BBB', 0x01, 0x0b, val >> 8)
    171 
    172   def SetUsbPassthrough(self, val):
    173     """Set the USB passthrough mode: 0 = off, 1 = on,  2 = auto."""
    174     self._SendStruct('BBB', 0x01, 0x10, val)
    175 
    176 
    177   def StartDataCollection(self):
    178     """Tell the device to start collecting and sending measurement data."""
    179     self._SendStruct('BBB', 0x01, 0x1b, 0x01)  # Mystery command.
    180     self._SendStruct('BBBBBBB', 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
    181 
    182 
    183   def StopDataCollection(self):
    184     """Tell the device to stop collecting measurement data."""
    185     self._SendStruct('BB', 0x03, 0x00)  # Stop.
    186 
    187 
    188   def CollectData(self):
    189     """Return some current samples.  Call StartDataCollection() first."""
    190     while 1:  # Loop until we get data or a timeout.
    191       data = self._ReadPacket()
    192       if not data:
    193         return None
    194       if len(data) < 4 + 8 + 1 or data[0] < '\x20' or data[0] > '\x2F':
    195         logging.debug('wanted data, dropped type=0x%02x, len=%d',
    196             ord(data[0]), len(data))
    197         continue
    198 
    199       seq, packet_type, x, _ = struct.unpack('BBBB', data[:4])
    200       data = [struct.unpack(">hhhh", data[x:x+8])
    201               for x in range(4, len(data) - 8, 8)]
    202 
    203       if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
    204         logging.info('data sequence skipped, lost packet?')
    205       self._last_seq = seq
    206 
    207       if packet_type == 0:
    208         if not self._coarse_scale or not self._fine_scale:
    209           logging.info('waiting for calibration, dropped data packet')
    210           continue
    211 
    212         out = []
    213         for main, usb, _, voltage in data:
    214           main_voltage_v = self._voltage_multiplier * (voltage & ~3)
    215           sample = 0.0
    216           if main & 1:
    217             sample += ((main & ~1) - self._coarse_zero) * self._coarse_scale
    218           else:
    219             sample += (main - self._fine_zero) * self._fine_scale
    220           if usb & 1:
    221             sample += ((usb & ~1) - self._coarse_zero) * self._coarse_scale
    222           else:
    223             sample += (usb - self._fine_zero) * self._fine_scale
    224           out.append(Power(sample, main_voltage_v))
    225         return out
    226 
    227       elif packet_type == 1:
    228         self._fine_zero = data[0][0]
    229         self._coarse_zero = data[1][0]
    230 
    231       elif packet_type == 2:
    232         self._fine_ref = data[0][0]
    233         self._coarse_ref = data[1][0]
    234 
    235       else:
    236         logging.debug('discarding data packet type=0x%02x', packet_type)
    237         continue
    238 
    239       if self._coarse_ref != self._coarse_zero:
    240         self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
    241       if self._fine_ref != self._fine_zero:
    242         self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
    243 
    244 
    245   def _SendStruct(self, fmt, *args):
    246     """Pack a struct (without length or checksum) and send it."""
    247     data = struct.pack(fmt, *args)
    248     data_len = len(data) + 1
    249     checksum = (data_len + sum(struct.unpack('B' * len(data), data))) % 256
    250     out = struct.pack('B', data_len) + data + struct.pack('B', checksum)
    251     self.ser.write(out)
    252 
    253 
    254   def _ReadPacket(self):
    255     """Read a single data record as a string (without length or checksum)."""
    256     len_char = self.ser.read(1)
    257     if not len_char:
    258       logging.error('timeout reading from serial port')
    259       return None
    260 
    261     data_len = struct.unpack('B', len_char)
    262     data_len = ord(len_char)
    263     if not data_len:
    264       return ''
    265 
    266     result = self.ser.read(data_len)
    267     if len(result) != data_len:
    268       return None
    269     body = result[:-1]
    270     checksum = (data_len + sum(struct.unpack('B' * len(body), body))) % 256
    271     if result[-1] != struct.pack('B', checksum):
    272       logging.error('invalid checksum from serial port')
    273       return None
    274     return result[:-1]
    275 
    276   def _FlushInput(self):
    277     """Flush all read data until no more available."""
    278     self.ser.flush()
    279     flushed = 0
    280     while True:
    281       ready_r, _, ready_x = select.select([self.ser], [], [self.ser], 0)
    282       if len(ready_x) > 0:
    283         logging.error('exception from serial port')
    284         return None
    285       elif len(ready_r) > 0:
    286         flushed += 1
    287         self.ser.read(1)  # This may cause underlying buffering.
    288         self.ser.flush()  # Flush the underlying buffer too.
    289       else:
    290         break
    291     if flushed > 0:
    292       logging.debug('dropped >%d bytes', flushed)
    293