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