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