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 22 23 24Power = collections.namedtuple('Power', ['amps', 'volts']) 25 26 27class Monsoon: 28 """Provides a simple class to use the power meter. 29 30 mon = monsoon.Monsoon() 31 mon.SetVoltage(3.7) 32 mon.StartDataCollection() 33 mydata = [] 34 while len(mydata) < 1000: 35 mydata.extend(mon.CollectData()) 36 mon.StopDataCollection() 37 """ 38 39 def __init__(self, device=None, serialno=None, wait=True): 40 """Establish a connection to a Monsoon. 41 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. 46 """ 47 assert float(serial.VERSION) >= 2.7, \ 48 'Monsoon requires pyserial v2.7 or later. You have %s' % serial.VERSION 49 50 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0 51 self._coarse_scale = self._fine_scale = 0 52 self._last_seq = 0 53 self._voltage_multiplier = None 54 55 if device: 56 self.ser = serial.Serial(device, timeout=1) 57 return 58 59 while 1: # Try all /dev/ttyACM* until we find one we can use. 60 for dev in os.listdir('/dev'): 61 if not dev.startswith('ttyACM'): 62 continue 63 tmpname = '/tmp/monsoon.%s.%s' % (os.uname()[0], dev) 64 self._tempfile = open(tmpname, 'w') 65 try: # Use a lockfile to ensure exclusive access. 66 # Put the import in here to avoid doing it on unsupported platforms. 67 import fcntl 68 fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 69 except IOError: 70 logging.error('device %s is in use', dev) 71 continue 72 73 try: # Try to open the device. 74 self.ser = serial.Serial('/dev/%s' % dev, timeout=1) 75 self.StopDataCollection() # Just in case. 76 self._FlushInput() # Discard stale input. 77 status = self.GetStatus() 78 except IOError, e: 79 logging.error('error opening device %s: %s', dev, e) 80 continue 81 82 if not status: 83 logging.error('no response from device %s', dev) 84 elif serialno and status['serialNumber'] != serialno: 85 logging.error('device %s is #%d', dev, status['serialNumber']) 86 else: 87 if status['hardwareRevision'] == 1: 88 self._voltage_multiplier = 62.5 / 10**6 89 else: 90 self._voltage_multiplier = 125.0 / 10**6 91 return 92 93 self._tempfile = None 94 if not wait: 95 raise IOError('No device found') 96 logging.info('waiting for device...') 97 time.sleep(1) 98 99 def GetStatus(self): 100 """Requests and waits for status. Returns status dictionary.""" 101 102 # status packet format 103 STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH' 104 STATUS_FIELDS = [ 105 'packetType', 'firmwareVersion', 'protocolVersion', 106 'mainFineCurrent', 'usbFineCurrent', 'auxFineCurrent', 'voltage1', 107 'mainCoarseCurrent', 'usbCoarseCurrent', 'auxCoarseCurrent', 'voltage2', 108 'outputVoltageSetting', 'temperature', 'status', 'leds', 109 'mainFineResistor', 'serialNumber', 'sampleRate', 110 'dacCalLow', 'dacCalHigh', 111 'powerUpCurrentLimit', 'runTimeCurrentLimit', 'powerUpTime', 112 'usbFineResistor', 'auxFineResistor', 113 'initialUsbVoltage', 'initialAuxVoltage', 114 'hardwareRevision', 'temperatureLimit', 'usbPassthroughMode', 115 'mainCoarseResistor', 'usbCoarseResistor', 'auxCoarseResistor', 116 'defMainFineResistor', 'defUsbFineResistor', 'defAuxFineResistor', 117 'defMainCoarseResistor', 'defUsbCoarseResistor', 'defAuxCoarseResistor', 118 'eventCode', 'eventData', 119 ] 120 121 self._SendStruct('BBB', 0x01, 0x00, 0x00) 122 while 1: # Keep reading, discarding non-status packets. 123 data = self._ReadPacket() 124 if not data: 125 return None 126 if len(data) != struct.calcsize(STATUS_FORMAT) or data[0] != '\x10': 127 logging.debug('wanted status, dropped type=0x%02x, len=%d', 128 ord(data[0]), len(data)) 129 continue 130 131 status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, data))) 132 assert status['packetType'] == 0x10 133 for k in status.keys(): 134 if k.endswith('VoltageSetting'): 135 status[k] = 2.0 + status[k] * 0.01 136 elif k.endswith('FineCurrent'): 137 pass # Needs calibration data. 138 elif k.endswith('CoarseCurrent'): 139 pass # Needs calibration data. 140 elif k.startswith('voltage') or k.endswith('Voltage'): 141 status[k] = status[k] * 0.000125 142 elif k.endswith('Resistor'): 143 status[k] = 0.05 + status[k] * 0.0001 144 if k.startswith('aux') or k.startswith('defAux'): 145 status[k] += 0.05 146 elif k.endswith('CurrentLimit'): 147 status[k] = 8 * (1023 - status[k]) / 1023.0 148 return status 149 150 151 def SetVoltage(self, v): 152 """Set the output voltage, 0 to disable.""" 153 if v == 0: 154 self._SendStruct('BBB', 0x01, 0x01, 0x00) 155 else: 156 self._SendStruct('BBB', 0x01, 0x01, int((v - 2.0) * 100)) 157 158 159 def SetMaxCurrent(self, i): 160 """Set the max output current.""" 161 assert i >= 0 and i <= 8 162 163 val = 1023 - int((i/8)*1023) 164 self._SendStruct('BBB', 0x01, 0x0a, val & 0xff) 165 self._SendStruct('BBB', 0x01, 0x0b, val >> 8) 166 167 def SetUsbPassthrough(self, val): 168 """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto.""" 169 self._SendStruct('BBB', 0x01, 0x10, val) 170 171 172 def StartDataCollection(self): 173 """Tell the device to start collecting and sending measurement data.""" 174 self._SendStruct('BBB', 0x01, 0x1b, 0x01) # Mystery command. 175 self._SendStruct('BBBBBBB', 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) 176 177 178 def StopDataCollection(self): 179 """Tell the device to stop collecting measurement data.""" 180 self._SendStruct('BB', 0x03, 0x00) # Stop. 181 182 183 def CollectData(self): 184 """Return some current samples. Call StartDataCollection() first.""" 185 while 1: # Loop until we get data or a timeout. 186 data = self._ReadPacket() 187 if not data: 188 return None 189 if len(data) < 4 + 8 + 1 or data[0] < '\x20' or data[0] > '\x2F': 190 logging.debug('wanted data, dropped type=0x%02x, len=%d', 191 ord(data[0]), len(data)) 192 continue 193 194 seq, packet_type, x, _ = struct.unpack('BBBB', data[:4]) 195 data = [struct.unpack(">hhhh", data[x:x+8]) 196 for x in range(4, len(data) - 8, 8)] 197 198 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: 199 logging.info('data sequence skipped, lost packet?') 200 self._last_seq = seq 201 202 if packet_type == 0: 203 if not self._coarse_scale or not self._fine_scale: 204 logging.info('waiting for calibration, dropped data packet') 205 continue 206 207 out = [] 208 for main, usb, _, voltage in data: 209 main_voltage_v = self._voltage_multiplier * (voltage & ~3) 210 sample = 0.0 211 if main & 1: 212 sample += ((main & ~1) - self._coarse_zero) * self._coarse_scale 213 else: 214 sample += (main - self._fine_zero) * self._fine_scale 215 if usb & 1: 216 sample += ((usb & ~1) - self._coarse_zero) * self._coarse_scale 217 else: 218 sample += (usb - self._fine_zero) * self._fine_scale 219 out.append(Power(sample, main_voltage_v)) 220 return out 221 222 elif packet_type == 1: 223 self._fine_zero = data[0][0] 224 self._coarse_zero = data[1][0] 225 226 elif packet_type == 2: 227 self._fine_ref = data[0][0] 228 self._coarse_ref = data[1][0] 229 230 else: 231 logging.debug('discarding data packet type=0x%02x', packet_type) 232 continue 233 234 if self._coarse_ref != self._coarse_zero: 235 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero) 236 if self._fine_ref != self._fine_zero: 237 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) 238 239 240 def _SendStruct(self, fmt, *args): 241 """Pack a struct (without length or checksum) and send it.""" 242 data = struct.pack(fmt, *args) 243 data_len = len(data) + 1 244 checksum = (data_len + sum(struct.unpack('B' * len(data), data))) % 256 245 out = struct.pack('B', data_len) + data + struct.pack('B', checksum) 246 self.ser.write(out) 247 248 249 def _ReadPacket(self): 250 """Read a single data record as a string (without length or checksum).""" 251 len_char = self.ser.read(1) 252 if not len_char: 253 logging.error('timeout reading from serial port') 254 return None 255 256 data_len = struct.unpack('B', len_char) 257 data_len = ord(len_char) 258 if not data_len: 259 return '' 260 261 result = self.ser.read(data_len) 262 if len(result) != data_len: 263 return None 264 body = result[:-1] 265 checksum = (data_len + sum(struct.unpack('B' * len(body), body))) % 256 266 if result[-1] != struct.pack('B', checksum): 267 logging.error('invalid checksum from serial port') 268 return None 269 return result[:-1] 270 271 def _FlushInput(self): 272 """Flush all read data until no more available.""" 273 self.ser.flush() 274 flushed = 0 275 while True: 276 ready_r, _, ready_x = select.select([self.ser], [], [self.ser], 0) 277 if len(ready_x) > 0: 278 logging.error('exception from serial port') 279 return None 280 elif len(ready_r) > 0: 281 flushed += 1 282 self.ser.read(1) # This may cause underlying buffering. 283 self.ser.flush() # Flush the underlying buffer too. 284 else: 285 break 286 if flushed > 0: 287 logging.debug('dropped >%d bytes', flushed) 288