1#!/usr/bin/python2.6 2 3# Copyright (C) 2014 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Interface for a USB-connected Monsoon power meter 18(http://msoon.com/LabEquipment/PowerMonitor/). 19This file requires gflags, which requires setuptools. 20To install setuptools: sudo apt-get install python-setuptools 21To install gflags, see http://code.google.com/p/python-gflags/ 22To install pyserial, see http://pyserial.sourceforge.net/ 23 24Example usages: 25 Set the voltage of the device 7536 to 4.0V 26 python2.6 monsoon.py --voltage=4.0 --serialno 7536 27 28 Get 5000hz data from device number 7536, with unlimited number of samples 29 python2.6 monsoon.py --samples -1 --hz 5000 --serialno 7536 30 31 Get 200Hz data for 5 seconds (1000 events) from default device 32 python2.6 monsoon.py --samples 100 --hz 200 33 34 Get unlimited 200Hz data from device attached at /dev/ttyACM0 35 python2.6 monsoon.py --samples -1 --hz 200 --device /dev/ttyACM0 36""" 37 38import fcntl 39import os 40import select 41import signal 42import stat 43import struct 44import sys 45import time 46import collections 47 48import gflags as flags # http://code.google.com/p/python-gflags/ 49 50import serial # http://pyserial.sourceforge.net/ 51 52FLAGS = flags.FLAGS 53 54class Monsoon: 55 """ 56 Provides a simple class to use the power meter, e.g. 57 mon = monsoon.Monsoon() 58 mon.SetVoltage(3.7) 59 mon.StartDataCollection() 60 mydata = [] 61 while len(mydata) < 1000: 62 mydata.extend(mon.CollectData()) 63 mon.StopDataCollection() 64 """ 65 66 def __init__(self, device=None, serialno=None, wait=1): 67 """ 68 Establish a connection to a Monsoon. 69 By default, opens the first available port, waiting if none are ready. 70 A particular port can be specified with "device", or a particular Monsoon 71 can be specified with "serialno" (using the number printed on its back). 72 With wait=0, IOError is thrown if a device is not immediately available. 73 """ 74 75 self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0 76 self._coarse_scale = self._fine_scale = 0 77 self._last_seq = 0 78 self.start_voltage = 0 79 80 if device: 81 self.ser = serial.Serial(device, timeout=1) 82 return 83 84 while True: # try all /dev/ttyACM* until we find one we can use 85 for dev in os.listdir("/dev"): 86 if not dev.startswith("ttyACM"): continue 87 tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev) 88 self._tempfile = open(tmpname, "w") 89 try: 90 os.chmod(tmpname, 0666) 91 except OSError: 92 pass 93 try: # use a lockfile to ensure exclusive access 94 fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 95 except IOError as e: 96 print >>sys.stderr, "device %s is in use" % dev 97 continue 98 99 try: # try to open the device 100 self.ser = serial.Serial("/dev/%s" % dev, timeout=1) 101 self.StopDataCollection() # just in case 102 self._FlushInput() # discard stale input 103 status = self.GetStatus() 104 except Exception as e: 105 print >>sys.stderr, "error opening device %s: %s" % (dev, e) 106 continue 107 108 if not status: 109 print >>sys.stderr, "no response from device %s" % dev 110 elif serialno and status["serialNumber"] != serialno: 111 print >>sys.stderr, ("Note: another device serial #%d seen on %s" % 112 (status["serialNumber"], dev)) 113 else: 114 self.start_voltage = status["voltage1"] 115 return 116 117 self._tempfile = None 118 if not wait: raise IOError("No device found") 119 print >>sys.stderr, "waiting for device..." 120 time.sleep(1) 121 122 123 def GetStatus(self): 124 """ Requests and waits for status. Returns status dictionary. """ 125 126 # status packet format 127 STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH" 128 STATUS_FIELDS = [ 129 "packetType", "firmwareVersion", "protocolVersion", 130 "mainFineCurrent", "usbFineCurrent", "auxFineCurrent", "voltage1", 131 "mainCoarseCurrent", "usbCoarseCurrent", "auxCoarseCurrent", "voltage2", 132 "outputVoltageSetting", "temperature", "status", "leds", 133 "mainFineResistor", "serialNumber", "sampleRate", 134 "dacCalLow", "dacCalHigh", 135 "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime", 136 "usbFineResistor", "auxFineResistor", 137 "initialUsbVoltage", "initialAuxVoltage", 138 "hardwareRevision", "temperatureLimit", "usbPassthroughMode", 139 "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor", 140 "defMainFineResistor", "defUsbFineResistor", "defAuxFineResistor", 141 "defMainCoarseResistor", "defUsbCoarseResistor", "defAuxCoarseResistor", 142 "eventCode", "eventData", ] 143 144 self._SendStruct("BBB", 0x01, 0x00, 0x00) 145 while True: # Keep reading, discarding non-status packets 146 bytes = self._ReadPacket() 147 if not bytes: return None 148 if len(bytes) != struct.calcsize(STATUS_FORMAT) or bytes[0] != "\x10": 149 print >>sys.stderr, "wanted status, dropped type=0x%02x, len=%d" % ( 150 ord(bytes[0]), len(bytes)) 151 continue 152 153 status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, bytes))) 154 assert status["packetType"] == 0x10 155 for k in status.keys(): 156 if k.endswith("VoltageSetting"): 157 status[k] = 2.0 + status[k] * 0.01 158 elif k.endswith("FineCurrent"): 159 pass # needs calibration data 160 elif k.endswith("CoarseCurrent"): 161 pass # needs calibration data 162 elif k.startswith("voltage") or k.endswith("Voltage"): 163 status[k] = status[k] * 0.000125 164 elif k.endswith("Resistor"): 165 status[k] = 0.05 + status[k] * 0.0001 166 if k.startswith("aux") or k.startswith("defAux"): status[k] += 0.05 167 elif k.endswith("CurrentLimit"): 168 status[k] = 8 * (1023 - status[k]) / 1023.0 169 return status 170 171 def RampVoltage(self, start, end): 172 v = start 173 if v < 3.0: v = 3.0 # protocol doesn't support lower than this 174 while (v < end): 175 self.SetVoltage(v) 176 v += .1 177 time.sleep(.1) 178 self.SetVoltage(end) 179 180 def SetVoltage(self, v): 181 """ Set the output voltage, 0 to disable. """ 182 if v == 0: 183 self._SendStruct("BBB", 0x01, 0x01, 0x00) 184 else: 185 self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100)) 186 187 188 def SetMaxCurrent(self, i): 189 """Set the max output current.""" 190 assert i >= 0 and i <= 8 191 192 val = 1023 - int((i/8)*1023) 193 self._SendStruct("BBB", 0x01, 0x0a, val & 0xff) 194 self._SendStruct("BBB", 0x01, 0x0b, val >> 8) 195 196 def SetUsbPassthrough(self, val): 197 """ Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto. """ 198 self._SendStruct("BBB", 0x01, 0x10, val) 199 200 201 def StartDataCollection(self): 202 """ Tell the device to start collecting and sending measurement data. """ 203 self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command 204 self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) 205 206 207 def StopDataCollection(self): 208 """ Tell the device to stop collecting measurement data. """ 209 self._SendStruct("BB", 0x03, 0x00) # stop 210 211 212 def CollectData(self): 213 """ Return some current samples. Call StartDataCollection() first. """ 214 while True: # loop until we get data or a timeout 215 bytes = self._ReadPacket() 216 if not bytes: return None 217 if len(bytes) < 4 + 8 + 1 or bytes[0] < "\x20" or bytes[0] > "\x2F": 218 print >>sys.stderr, "wanted data, dropped type=0x%02x, len=%d" % ( 219 ord(bytes[0]), len(bytes)) 220 continue 221 222 seq, type, x, y = struct.unpack("BBBB", bytes[:4]) 223 data = [struct.unpack(">hhhh", bytes[x:x+8]) 224 for x in range(4, len(bytes) - 8, 8)] 225 226 if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: 227 print >>sys.stderr, "data sequence skipped, lost packet?" 228 self._last_seq = seq 229 230 if type == 0: 231 if not self._coarse_scale or not self._fine_scale: 232 print >>sys.stderr, "waiting for calibration, dropped data packet" 233 continue 234 235 out = [] 236 for main, usb, aux, voltage in data: 237 if main & 1: 238 out.append(((main & ~1) - self._coarse_zero) * self._coarse_scale) 239 else: 240 out.append((main - self._fine_zero) * self._fine_scale) 241 return out 242 243 elif type == 1: 244 self._fine_zero = data[0][0] 245 self._coarse_zero = data[1][0] 246 # print >>sys.stderr, "zero calibration: fine 0x%04x, coarse 0x%04x" % ( 247 # self._fine_zero, self._coarse_zero) 248 249 elif type == 2: 250 self._fine_ref = data[0][0] 251 self._coarse_ref = data[1][0] 252 # print >>sys.stderr, "ref calibration: fine 0x%04x, coarse 0x%04x" % ( 253 # self._fine_ref, self._coarse_ref) 254 255 else: 256 print >>sys.stderr, "discarding data packet type=0x%02x" % type 257 continue 258 259 if self._coarse_ref != self._coarse_zero: 260 self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero) 261 if self._fine_ref != self._fine_zero: 262 self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) 263 264 265 def _SendStruct(self, fmt, *args): 266 """ Pack a struct (without length or checksum) and send it. """ 267 data = struct.pack(fmt, *args) 268 data_len = len(data) + 1 269 checksum = (data_len + sum(struct.unpack("B" * len(data), data))) % 256 270 out = struct.pack("B", data_len) + data + struct.pack("B", checksum) 271 self.ser.write(out) 272 273 274 def _ReadPacket(self): 275 """ Read a single data record as a string (without length or checksum). """ 276 len_char = self.ser.read(1) 277 if not len_char: 278 print >>sys.stderr, "timeout reading from serial port" 279 return None 280 281 data_len = struct.unpack("B", len_char) 282 data_len = ord(len_char) 283 if not data_len: return "" 284 285 result = self.ser.read(data_len) 286 if len(result) != data_len: return None 287 body = result[:-1] 288 checksum = (data_len + sum(struct.unpack("B" * len(body), body))) % 256 289 if result[-1] != struct.pack("B", checksum): 290 print >>sys.stderr, "invalid checksum from serial port" 291 return None 292 return result[:-1] 293 294 def _FlushInput(self): 295 """ Flush all read data until no more available. """ 296 self.ser.flush() 297 flushed = 0 298 while True: 299 ready_r, ready_w, ready_x = select.select([self.ser], [], [self.ser], 0) 300 if len(ready_x) > 0: 301 print >>sys.stderr, "exception from serial port" 302 return None 303 elif len(ready_r) > 0: 304 flushed += 1 305 self.ser.read(1) # This may cause underlying buffering. 306 self.ser.flush() # Flush the underlying buffer too. 307 else: 308 break 309 if flushed > 0: 310 print >>sys.stderr, "dropped >%d bytes" % flushed 311 312def main(argv): 313 """ Simple command-line interface for Monsoon.""" 314 useful_flags = ["voltage", "status", "usbpassthrough", "samples", "current"] 315 if not [f for f in useful_flags if FLAGS.get(f, None) is not None]: 316 print __doc__.strip() 317 print FLAGS.MainModuleHelp() 318 return 319 320 if FLAGS.avg and FLAGS.avg < 0: 321 print "--avg must be greater than 0" 322 return 323 324 mon = Monsoon(device=FLAGS.device, serialno=FLAGS.serialno) 325 326 if FLAGS.voltage is not None: 327 if FLAGS.ramp is not None: 328 mon.RampVoltage(mon.start_voltage, FLAGS.voltage) 329 else: 330 mon.SetVoltage(FLAGS.voltage) 331 332 if FLAGS.current is not None: 333 mon.SetMaxCurrent(FLAGS.current) 334 335 if FLAGS.status: 336 items = sorted(mon.GetStatus().items()) 337 print "\n".join(["%s: %s" % item for item in items]) 338 339 if FLAGS.usbpassthrough: 340 if FLAGS.usbpassthrough == 'off': 341 mon.SetUsbPassthrough(0) 342 elif FLAGS.usbpassthrough == 'on': 343 mon.SetUsbPassthrough(1) 344 elif FLAGS.usbpassthrough == 'auto': 345 mon.SetUsbPassthrough(2) 346 else: 347 sys.exit('bad passthrough flag: %s' % FLAGS.usbpassthrough) 348 349 if FLAGS.samples: 350 # Make sure state is normal 351 mon.StopDataCollection() 352 status = mon.GetStatus() 353 native_hz = status["sampleRate"] * 1000 354 355 # Collect and average samples as specified 356 mon.StartDataCollection() 357 358 # In case FLAGS.hz doesn't divide native_hz exactly, use this invariant: 359 # 'offset' = (consumed samples) * FLAGS.hz - (emitted samples) * native_hz 360 # This is the error accumulator in a variation of Bresenham's algorithm. 361 emitted = offset = 0 362 collected = [] 363 history_deque = collections.deque() # past n samples for rolling average 364 365 try: 366 last_flush = time.time() 367 while emitted < FLAGS.samples or FLAGS.samples == -1: 368 # The number of raw samples to consume before emitting the next output 369 need = (native_hz - offset + FLAGS.hz - 1) / FLAGS.hz 370 if need > len(collected): # still need more input samples 371 samples = mon.CollectData() 372 if not samples: break 373 collected.extend(samples) 374 else: 375 # Have enough data, generate output samples. 376 # Adjust for consuming 'need' input samples. 377 offset += need * FLAGS.hz 378 while offset >= native_hz: # maybe multiple, if FLAGS.hz > native_hz 379 this_sample = sum(collected[:need]) / need 380 381 if FLAGS.timestamp: print int(time.time()), 382 383 if FLAGS.avg: 384 history_deque.appendleft(this_sample) 385 if len(history_deque) > FLAGS.avg: history_deque.pop() 386 print "%f %f" % (this_sample, 387 sum(history_deque) / len(history_deque)) 388 else: 389 print "%f" % this_sample 390 sys.stdout.flush() 391 392 offset -= native_hz 393 emitted += 1 # adjust for emitting 1 output sample 394 collected = collected[need:] 395 now = time.time() 396 if now - last_flush >= 0.99: # flush every second 397 sys.stdout.flush() 398 last_flush = now 399 except KeyboardInterrupt: 400 print >>sys.stderr, "interrupted" 401 402 mon.StopDataCollection() 403 404 405if __name__ == '__main__': 406 # Define flags here to avoid conflicts with people who use us as a library 407 flags.DEFINE_boolean("status", None, "Print power meter status") 408 flags.DEFINE_integer("avg", None, 409 "Also report average over last n data points") 410 flags.DEFINE_float("voltage", None, "Set output voltage (0 for off)") 411 flags.DEFINE_float("current", None, "Set max output current") 412 flags.DEFINE_string("usbpassthrough", None, "USB control (on, off, auto)") 413 flags.DEFINE_integer("samples", None, "Collect and print this many samples") 414 flags.DEFINE_integer("hz", 5000, "Print this many samples/sec") 415 flags.DEFINE_string("device", None, 416 "Path to the device in /dev/... (ex:/dev/ttyACM1)") 417 flags.DEFINE_integer("serialno", None, "Look for a device with this serial number") 418 flags.DEFINE_boolean("timestamp", None, 419 "Also print integer (seconds) timestamp on each line") 420 flags.DEFINE_boolean("ramp", True, "Gradually increase voltage") 421 422 main(FLAGS(sys.argv)) 423