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
22 import serial.tools.list_ports
25 Power = collections.namedtuple('Power', ['amps', 'volts'])
29 """Provides a simple class to use the power meter.
31 mon = monsoon.Monsoon()
33 mon.StartDataCollection()
35 while len(mydata) < 1000:
36 mydata.extend(mon.CollectData())
37 mon.StopDataCollection()
40 def __init__(self, device=None, serialno=None, wait=True):
41 """Establish a connection to a Monsoon.
43 By default, opens the first available port, waiting if none are ready.
44 A particular port can be specified with 'device', or a particular Monsoon
45 can be specified with 'serialno' (using the number printed on its back).
46 With wait=False, IOError is thrown if a device is not immediately available.
48 assert float(serial.VERSION) >= 2.7, \
49 'Monsoon requires pyserial v2.7 or later. You have %s' % serial.VERSION
51 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0
52 self._coarse_scale = self._fine_scale = 0
54 self._voltage_multiplier = None
57 self.ser = serial.Serial(device, timeout=1)
61 for (port, desc, _) in serial.tools.list_ports.comports():
62 if not desc.lower().startswith('mobile device power monitor'):
64 tmpname = '/tmp/monsoon.%s.%s' % (os.uname()[0], os.path.basename(port))
65 self._tempfile = open(tmpname, 'w')
66 try: # Use a lockfile to ensure exclusive access.
67 # Put the import in here to avoid doing it on unsupported platforms.
69 fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
71 logging.error('device %s is in use', port)
74 try: # Try to open the device.
75 self.ser = serial.Serial(port, timeout=1)
76 self.StopDataCollection() # Just in case.
77 self._FlushInput() # Discard stale input.
78 status = self.GetStatus()
80 logging.error('error opening device %s: %s', port, e)
84 logging.error('no response from device %s', port)
85 elif serialno and status['serialNumber'] != serialno:
86 logging.error('device %s is #%d', port, status['serialNumber'])
88 if status['hardwareRevision'] == 1:
89 self._voltage_multiplier = 62.5 / 10**6
91 self._voltage_multiplier = 125.0 / 10**6
96 raise IOError('No device found')
97 logging.info('waiting for device...')
101 """Requests and waits for status. Returns status dictionary."""
103 # status packet format
104 STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH'
106 'packetType', 'firmwareVersion', 'protocolVersion',
107 'mainFineCurrent', 'usbFineCurrent', 'auxFineCurrent', 'voltage1',
108 'mainCoarseCurrent', 'usbCoarseCurrent', 'auxCoarseCurrent', 'voltage2',
109 'outputVoltageSetting', 'temperature', 'status', 'leds',
110 'mainFineResistor', 'serialNumber', 'sampleRate',
111 'dacCalLow', 'dacCalHigh',
112 'powerUpCurrentLimit', 'runTimeCurrentLimit', 'powerUpTime',
113 'usbFineResistor', 'auxFineResistor',
114 'initialUsbVoltage', 'initialAuxVoltage',
115 'hardwareRevision', 'temperatureLimit', 'usbPassthroughMode',
116 'mainCoarseResistor', 'usbCoarseResistor', 'auxCoarseResistor',
117 'defMainFineResistor', 'defUsbFineResistor', 'defAuxFineResistor',
118 'defMainCoarseResistor', 'defUsbCoarseResistor', 'defAuxCoarseResistor',
119 'eventCode', 'eventData',
122 self._SendStruct('BBB', 0x01, 0x00, 0x00)
123 while 1: # Keep reading, discarding non-status packets.
124 data = self._ReadPacket()
127 if len(data) != struct.calcsize(STATUS_FORMAT) or data[0] != '\x10':
128 logging.debug('wanted status, dropped type=0x%02x, len=%d',
129 ord(data[0]), len(data))
132 status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, data)))
133 assert status['packetType'] == 0x10
134 for k in status.keys():
135 if k.endswith('VoltageSetting'):
136 status[k] = 2.0 + status[k] * 0.01
137 elif k.endswith('FineCurrent'):
138 pass # Needs calibration data.
139 elif k.endswith('CoarseCurrent'):
140 pass # Needs calibration data.
141 elif k.startswith('voltage') or k.endswith('Voltage'):
142 status[k] = status[k] * 0.000125
143 elif k.endswith('Resistor'):
144 status[k] = 0.05 + status[k] * 0.0001
145 if k.startswith('aux') or k.startswith('defAux'):
147 elif k.endswith('CurrentLimit'):
148 status[k] = 8 * (1023 - status[k]) / 1023.0
152 def SetVoltage(self, v):
153 """Set the output voltage, 0 to disable."""
155 self._SendStruct('BBB', 0x01, 0x01, 0x00)
157 self._SendStruct('BBB', 0x01, 0x01, int((v - 2.0) * 100))
160 def SetMaxCurrent(self, i):
161 """Set the max output current."""
162 assert i >= 0 and i <= 8
164 val = 1023 - int((i/8)*1023)
165 self._SendStruct('BBB', 0x01, 0x0a, val & 0xff)
166 self._SendStruct('BBB', 0x01, 0x0b, val >> 8)
168 def SetUsbPassthrough(self, val):
169 """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto."""
170 self._SendStruct('BBB', 0x01, 0x10, val)
173 def StartDataCollection(self):
174 """Tell the device to start collecting and sending measurement data."""
175 self._SendStruct('BBB', 0x01, 0x1b, 0x01) # Mystery command.
176 self._SendStruct('BBBBBBB', 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
179 def StopDataCollection(self):
180 """Tell the device to stop collecting measurement data."""
181 self._SendStruct('BB', 0x03, 0x00) # Stop.
184 def CollectData(self):
185 """Return some current samples. Call StartDataCollection() first."""
186 while 1: # Loop until we get data or a timeout.
187 data = self._ReadPacket()
190 if len(data) < 4 + 8 + 1 or data[0] < '\x20' or data[0] > '\x2F':
191 logging.debug('wanted data, dropped type=0x%02x, len=%d',
192 ord(data[0]), len(data))
195 seq, packet_type, x, _ = struct.unpack('BBBB', data[:4])
196 data = [struct.unpack(">hhhh", data[x:x+8])
197 for x in range(4, len(data) - 8, 8)]
199 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
200 logging.info('data sequence skipped, lost packet?')
204 if not self._coarse_scale or not self._fine_scale:
205 logging.info('waiting for calibration, dropped data packet')
209 for main, usb, _, voltage in data:
210 main_voltage_v = self._voltage_multiplier * (voltage & ~3)
213 sample += ((main & ~1) - self._coarse_zero) * self._coarse_scale
215 sample += (main - self._fine_zero) * self._fine_scale
217 sample += ((usb & ~1) - self._coarse_zero) * self._coarse_scale
219 sample += (usb - self._fine_zero) * self._fine_scale
220 out.append(Power(sample, main_voltage_v))
223 elif packet_type == 1:
224 self._fine_zero = data[0][0]
225 self._coarse_zero = data[1][0]
227 elif packet_type == 2:
228 self._fine_ref = data[0][0]
229 self._coarse_ref = data[1][0]
232 logging.debug('discarding data packet type=0x%02x', packet_type)
235 if self._coarse_ref != self._coarse_zero:
236 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
237 if self._fine_ref != self._fine_zero:
238 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
241 def _SendStruct(self, fmt, *args):
242 """Pack a struct (without length or checksum) and send it."""
243 data = struct.pack(fmt, *args)
244 data_len = len(data) + 1
245 checksum = (data_len + sum(struct.unpack('B' * len(data), data))) % 256
246 out = struct.pack('B', data_len) + data + struct.pack('B', checksum)
250 def _ReadPacket(self):
251 """Read a single data record as a string (without length or checksum)."""
252 len_char = self.ser.read(1)
254 logging.error('timeout reading from serial port')
257 data_len = struct.unpack('B', len_char)
258 data_len = ord(len_char)
262 result = self.ser.read(data_len)
263 if len(result) != data_len:
266 checksum = (data_len + sum(struct.unpack('B' * len(body), body))) % 256
267 if result[-1] != struct.pack('B', checksum):
268 logging.error('invalid checksum from serial port')
272 def _FlushInput(self):
273 """Flush all read data until no more available."""
277 ready_r, _, ready_x = select.select([self.ser], [], [self.ser], 0)
279 logging.error('exception from serial port')
281 elif len(ready_r) > 0:
283 self.ser.read(1) # This may cause underlying buffering.
284 self.ser.flush() # Flush the underlying buffer too.
288 logging.debug('dropped >%d bytes', flushed)