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.
5 """Interface for a USB-connected Monsoon power meter.
7 http://msoon.com/LabEquipment/PowerMonitor/
8 Currently Unix-only. Relies on fcntl, /dev, and /tmp.
18 from telemetry.core import util
20 util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'pyserial')
21 import serial # pylint: disable=F0401
24 Power = collections.namedtuple('Power', ['amps', 'volts'])
28 """Provides a simple class to use the power meter.
30 mon = monsoon.Monsoon()
32 mon.StartDataCollection()
34 while len(mydata) < 1000:
35 mydata.extend(mon.CollectData())
36 mon.StopDataCollection()
39 def __init__(self, device=None, serialno=None, wait=True):
40 """Establish a connection to a Monsoon.
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.
48 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0
49 self._coarse_scale = self._fine_scale = 0
51 self._voltage_multiplier = None
54 self.ser = serial.Serial(device, timeout=1)
57 while 1: # Try all /dev/ttyACM* until we find one we can use.
58 for dev in os.listdir('/dev'):
59 if not dev.startswith('ttyACM'):
61 tmpname = '/tmp/monsoon.%s.%s' % (os.uname()[0], dev)
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.
66 fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
68 logging.error('device %s is in use', dev)
71 try: # Try to open the device.
72 self.ser = serial.Serial('/dev/%s' % dev, timeout=1)
73 self.StopDataCollection() # Just in case.
74 self._FlushInput() # Discard stale input.
75 status = self.GetStatus()
77 logging.error('error opening device %s: %s', dev, e)
81 logging.error('no response from device %s', dev)
82 elif serialno and status['serialNumber'] != serialno:
83 logging.error('device %s is #%d', dev, status['serialNumber'])
85 if status['hardwareRevision'] == 1:
86 self._voltage_multiplier = 62.5 / 10**6
88 self._voltage_multiplier = 125.0 / 10**6
93 raise IOError('No device found')
94 logging.info('waiting for device...')
98 """Requests and waits for status. Returns status dictionary."""
100 # status packet format
101 STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH'
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',
119 self._SendStruct('BBB', 0x01, 0x00, 0x00)
120 while 1: # Keep reading, discarding non-status packets.
121 data = self._ReadPacket()
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))
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'):
144 elif k.endswith('CurrentLimit'):
145 status[k] = 8 * (1023 - status[k]) / 1023.0
149 def SetVoltage(self, v):
150 """Set the output voltage, 0 to disable."""
152 self._SendStruct('BBB', 0x01, 0x01, 0x00)
154 self._SendStruct('BBB', 0x01, 0x01, int((v - 2.0) * 100))
157 def SetMaxCurrent(self, i):
158 """Set the max output current."""
159 assert i >= 0 and i <= 8
161 val = 1023 - int((i/8)*1023)
162 self._SendStruct('BBB', 0x01, 0x0a, val & 0xff)
163 self._SendStruct('BBB', 0x01, 0x0b, val >> 8)
165 def SetUsbPassthrough(self, val):
166 """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto."""
167 self._SendStruct('BBB', 0x01, 0x10, val)
170 def StartDataCollection(self):
171 """Tell the device to start collecting and sending measurement data."""
172 self._SendStruct('BBB', 0x01, 0x1b, 0x01) # Mystery command.
173 self._SendStruct('BBBBBBB', 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
176 def StopDataCollection(self):
177 """Tell the device to stop collecting measurement data."""
178 self._SendStruct('BB', 0x03, 0x00) # Stop.
181 def CollectData(self):
182 """Return some current samples. Call StartDataCollection() first."""
183 while 1: # Loop until we get data or a timeout.
184 data = self._ReadPacket()
187 if len(data) < 4 + 8 + 1 or data[0] < '\x20' or data[0] > '\x2F':
188 logging.debug('wanted data, dropped type=0x%02x, len=%d',
189 ord(data[0]), len(data))
192 seq, packet_type, x, _ = struct.unpack('BBBB', data[:4])
193 data = [struct.unpack(">hhhh", data[x:x+8])
194 for x in range(4, len(data) - 8, 8)]
196 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
197 logging.info('data sequence skipped, lost packet?')
201 if not self._coarse_scale or not self._fine_scale:
202 logging.info('waiting for calibration, dropped data packet')
206 for main, usb, _, voltage in data:
207 main_voltage_v = self._voltage_multiplier * (voltage & ~3)
210 sample += ((main & ~1) - self._coarse_zero) * self._coarse_scale
212 sample += (main - self._fine_zero) * self._fine_scale
214 sample += ((usb & ~1) - self._coarse_zero) * self._coarse_scale
216 sample += (usb - self._fine_zero) * self._fine_scale
217 out.append(Power(sample, main_voltage_v))
220 elif packet_type == 1:
221 self._fine_zero = data[0][0]
222 self._coarse_zero = data[1][0]
224 elif packet_type == 2:
225 self._fine_ref = data[0][0]
226 self._coarse_ref = data[1][0]
229 logging.debug('discarding data packet type=0x%02x', packet_type)
232 if self._coarse_ref != self._coarse_zero:
233 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
234 if self._fine_ref != self._fine_zero:
235 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
238 def _SendStruct(self, fmt, *args):
239 """Pack a struct (without length or checksum) and send it."""
240 data = struct.pack(fmt, *args)
241 data_len = len(data) + 1
242 checksum = (data_len + sum(struct.unpack('B' * len(data), data))) % 256
243 out = struct.pack('B', data_len) + data + struct.pack('B', checksum)
247 def _ReadPacket(self):
248 """Read a single data record as a string (without length or checksum)."""
249 len_char = self.ser.read(1)
251 logging.error('timeout reading from serial port')
254 data_len = struct.unpack('B', len_char)
255 data_len = ord(len_char)
259 result = self.ser.read(data_len)
260 if len(result) != data_len:
263 checksum = (data_len + sum(struct.unpack('B' * len(body), body))) % 256
264 if result[-1] != struct.pack('B', checksum):
265 logging.error('invalid checksum from serial port')
269 def _FlushInput(self):
270 """Flush all read data until no more available."""
274 ready_r, _, ready_x = select.select([self.ser], [], [self.ser], 0)
276 logging.error('exception from serial port')
278 elif len(ready_r) > 0:
280 self.ser.read(1) # This may cause underlying buffering.
281 self.ser.flush() # Flush the underlying buffer too.
285 logging.debug('dropped >%d bytes', flushed)