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 22import serial.tools.list_ports # pylint: disable=F0401,E0611 23 24 25Power = collections.namedtuple('Power', ['amps', 'volts']) 26 27 28class Monsoon: 29 """Provides a simple class to use the power meter. 30 31 mon = monsoon.Monsoon() 32 mon.SetVoltage(3.7) 33 mon.StartDataCollection() 34 mydata = [] 35 while len(mydata) < 1000: 36 mydata.extend(mon.CollectData()) 37 mon.StopDataCollection() 38 """ 39 40 def __init__(self, device=None, serialno=None, wait=True): 41 """Establish a connection to a Monsoon. 42 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. 47 """ 48 assert float(serial.VERSION) >= 2.7, \ 49 'Monsoon requires pyserial v2.7 or later. You have %s' % serial.VERSION 50 51 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0 52 self._coarse_scale = self._fine_scale = 0 53 self._last_seq = 0 54 self._voltage_multiplier = None 55 56 if device: 57 self.ser = serial.Serial(device, timeout=1) 58 return 59 60 while 1: 61 for (port, desc, _) in serial.tools.list_ports.comports(): 62 if not desc.lower().startswith('mobile device power monitor'): 63 continue 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. 68 import fcntl 69 fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 70 except IOError: 71 logging.error('device %s is in use', port) 72 continue 73 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() 79 except IOError, e: 80 logging.error('error opening device %s: %s', port, e) 81 continue 82 83 if not status: 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']) 87 else: 88 if status['hardwareRevision'] == 1: 89 self._voltage_multiplier = 62.5 / 10**6 90 else: 91 self._voltage_multiplier = 125.0 / 10**6 92 return 93 94 self._tempfile = None 95 if not wait: 96 raise IOError('No device found') 97 logging.info('waiting for device...') 98 time.sleep(1) 99 100 def GetStatus(self): 101 """Requests and waits for status. Returns status dictionary.""" 102 103 # status packet format 104 STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH' 105 STATUS_FIELDS = [ 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', 120 ] 121 122 self._SendStruct('BBB', 0x01, 0x00, 0x00) 123 while 1: # Keep reading, discarding non-status packets. 124 data = self._ReadPacket() 125 if not data: 126 return None 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)) 130 continue 131 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'): 146 status[k] += 0.05 147 elif k.endswith('CurrentLimit'): 148 status[k] = 8 * (1023 - status[k]) / 1023.0 149 return status 150 151 152 def SetVoltage(self, v): 153 """Set the output voltage, 0 to disable.""" 154 if v == 0: 155 self._SendStruct('BBB', 0x01, 0x01, 0x00) 156 else: 157 self._SendStruct('BBB', 0x01, 0x01, int((v - 2.0) * 100)) 158 159 160 def SetMaxCurrent(self, i): 161 """Set the max output current.""" 162 assert i >= 0 and i <= 8 163 164 val = 1023 - int((i/8)*1023) 165 self._SendStruct('BBB', 0x01, 0x0a, val & 0xff) 166 self._SendStruct('BBB', 0x01, 0x0b, val >> 8) 167 168 def SetUsbPassthrough(self, val): 169 """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto.""" 170 self._SendStruct('BBB', 0x01, 0x10, val) 171 172 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) 177 178 179 def StopDataCollection(self): 180 """Tell the device to stop collecting measurement data.""" 181 self._SendStruct('BB', 0x03, 0x00) # Stop. 182 183 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() 188 if not data: 189 return None 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)) 193 continue 194 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)] 198 199 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: 200 logging.info('data sequence skipped, lost packet?') 201 self._last_seq = seq 202 203 if packet_type == 0: 204 if not self._coarse_scale or not self._fine_scale: 205 logging.info('waiting for calibration, dropped data packet') 206 continue 207 208 out = [] 209 for main, usb, _, voltage in data: 210 main_voltage_v = self._voltage_multiplier * (voltage & ~3) 211 sample = 0.0 212 if main & 1: 213 sample += ((main & ~1) - self._coarse_zero) * self._coarse_scale 214 else: 215 sample += (main - self._fine_zero) * self._fine_scale 216 if usb & 1: 217 sample += ((usb & ~1) - self._coarse_zero) * self._coarse_scale 218 else: 219 sample += (usb - self._fine_zero) * self._fine_scale 220 out.append(Power(sample, main_voltage_v)) 221 return out 222 223 elif packet_type == 1: 224 self._fine_zero = data[0][0] 225 self._coarse_zero = data[1][0] 226 227 elif packet_type == 2: 228 self._fine_ref = data[0][0] 229 self._coarse_ref = data[1][0] 230 231 else: 232 logging.debug('discarding data packet type=0x%02x', packet_type) 233 continue 234 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) 239 240 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) 247 self.ser.write(out) 248 249 250 def _ReadPacket(self): 251 """Read a single data record as a string (without length or checksum).""" 252 len_char = self.ser.read(1) 253 if not len_char: 254 logging.error('timeout reading from serial port') 255 return None 256 257 data_len = struct.unpack('B', len_char) 258 data_len = ord(len_char) 259 if not data_len: 260 return '' 261 262 result = self.ser.read(data_len) 263 if len(result) != data_len: 264 return None 265 body = result[:-1] 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') 269 return None 270 return result[:-1] 271 272 def _FlushInput(self): 273 """Flush all read data until no more available.""" 274 self.ser.flush() 275 flushed = 0 276 while True: 277 ready_r, _, ready_x = select.select([self.ser], [], [self.ser], 0) 278 if len(ready_x) > 0: 279 logging.error('exception from serial port') 280 return None 281 elif len(ready_r) > 0: 282 flushed += 1 283 self.ser.read(1) # This may cause underlying buffering. 284 self.ser.flush() # Flush the underlying buffer too. 285 else: 286 break 287 if flushed > 0: 288 logging.debug('dropped >%d bytes', flushed) 289