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