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
7http://msoon.com/LabEquipment/PowerMonitor/
8Currently Unix-only. Relies on fcntl, /dev, and /tmp.
9"""
10
11import collections
12import logging
13import os
14import select
15import struct
16import time
17
18import serial  # pylint: disable=import-error,no-name-in-module
19import serial.tools.list_ports  # pylint: disable=import-error,no-name-in-module
20
21
22Power = collections.namedtuple('Power', ['amps', 'volts'])
23
24
25class 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