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 import serial
# pylint: disable=import-error,no-name-in-module
19 import serial
.tools
.list_ports
# pylint: disable=import-error,no-name-in-module
22 Power
= collections
.namedtuple('Power', ['amps', 'volts'])
25 class Monsoon(object):
26 """Provides a simple class to use the power meter.
28 mon = monsoon.Monsoon()
30 mon.StartDataCollection()
32 while len(mydata) < 1000:
33 mydata.extend(mon.CollectData())
34 mon.StopDataCollection()
37 def __init__(self
, device
=None, serialno
=None, wait
=True):
38 """Establish a connection to a Monsoon.
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.
45 assert float(serial
.VERSION
) >= 2.7, \
46 'Monsoon requires pyserial v2.7 or later. You have %s' % serial
.VERSION
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)
58 for (port
, desc
, _
) in serial
.tools
.list_ports
.comports():
59 if not desc
.lower().startswith('mobile device power monitor'):
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
)
68 logging
.error('device %s is in use', port
)
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()
77 logging
.error('error opening device %s: %s', port
, e
)
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'])
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))
156 def SetStartupCurrent(self
, a
):
157 """Set the max startup output current. the unit of |a| : Amperes """
158 assert a
>= 0 and a
<= 8
160 val
= 1023 - int((a
/8.0)*1023)
161 self
._SendStruct
('BBB', 0x01, 0x08, val
& 0xff)
162 self
._SendStruct
('BBB', 0x01, 0x09, val
>> 8)
164 def SetMaxCurrent(self
, a
):
165 """Set the max output current. the unit of |a| : Amperes """
166 assert a
>= 0 and a
<= 8
168 val
= 1023 - int((a
/8.0)*1023)
169 self
._SendStruct
('BBB', 0x01, 0x0a, val
& 0xff)
170 self
._SendStruct
('BBB', 0x01, 0x0b, val
>> 8)
172 def SetUsbPassthrough(self
, val
):
173 """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto."""
174 self
._SendStruct
('BBB', 0x01, 0x10, val
)
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)
183 def StopDataCollection(self
):
184 """Tell the device to stop collecting measurement data."""
185 self
._SendStruct
('BB', 0x03, 0x00) # Stop.
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
()
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
))
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)]
203 if self
._last
_seq
and seq
& 0xF != (self
._last
_seq
+ 1) & 0xF:
204 logging
.info('data sequence skipped, lost packet?')
208 if not self
._coarse
_scale
or not self
._fine
_scale
:
209 logging
.info('waiting for calibration, dropped data packet')
213 for main
, usb
, _
, voltage
in data
:
214 main_voltage_v
= self
._voltage
_multiplier
* (voltage
& ~
3)
217 sample
+= ((main
& ~
1) - self
._coarse
_zero
) * self
._coarse
_scale
219 sample
+= (main
- self
._fine
_zero
) * self
._fine
_scale
221 sample
+= ((usb
& ~
1) - self
._coarse
_zero
) * self
._coarse
_scale
223 sample
+= (usb
- self
._fine
_zero
) * self
._fine
_scale
224 out
.append(Power(sample
, main_voltage_v
))
227 elif packet_type
== 1:
228 self
._fine
_zero
= data
[0][0]
229 self
._coarse
_zero
= data
[1][0]
231 elif packet_type
== 2:
232 self
._fine
_ref
= data
[0][0]
233 self
._coarse
_ref
= data
[1][0]
236 logging
.debug('discarding data packet type=0x%02x', packet_type
)
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
)
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
)
254 def _ReadPacket(self
):
255 """Read a single data record as a string (without length or checksum)."""
256 len_char
= self
.ser
.read(1)
258 logging
.error('timeout reading from serial port')
261 data_len
= struct
.unpack('B', len_char
)
262 data_len
= ord(len_char
)
266 result
= self
.ser
.read(data_len
)
267 if len(result
) != data_len
:
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')
276 def _FlushInput(self
):
277 """Flush all read data until no more available."""
281 ready_r
, _
, ready_x
= select
.select([self
.ser
], [], [self
.ser
], 0)
283 logging
.error('exception from serial port')
285 elif len(ready_r
) > 0:
287 self
.ser
.read(1) # This may cause underlying buffering.
288 self
.ser
.flush() # Flush the underlying buffer too.
292 logging
.debug('dropped >%d bytes', flushed
)