|  | #!/usr/bin/env python | 
|  |  | 
|  | # Copyright (C) 2014 The Android Open Source Project | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #       http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | """Interface for a USB-connected Monsoon power meter | 
|  | (http://msoon.com/LabEquipment/PowerMonitor/). | 
|  | This file requires gflags, which requires setuptools. | 
|  | To install setuptools: sudo apt-get install python-setuptools | 
|  | To install gflags, see http://code.google.com/p/python-gflags/ | 
|  | To install pyserial, see http://pyserial.sourceforge.net/ | 
|  |  | 
|  | Example usages: | 
|  | Set the voltage of the device 7536 to 4.0V | 
|  | python monsoon.py --voltage=4.0 --serialno 7536 | 
|  |  | 
|  | Get 5000hz data from device number 7536, with unlimited number of samples | 
|  | python monsoon.py --samples -1 --hz 5000 --serialno 7536 | 
|  |  | 
|  | Get 200Hz data for 5 seconds (1000 events) from default device | 
|  | python monsoon.py --samples 100 --hz 200 | 
|  |  | 
|  | Get unlimited 200Hz data from device attached at /dev/ttyACM0 | 
|  | python monsoon.py --samples -1 --hz 200 --device /dev/ttyACM0 | 
|  |  | 
|  | Output columns for collection with --samples, separated by space: | 
|  |  | 
|  | TIMESTAMP OUTPUT OUTPUT_AVG USB USB_AVG | 
|  | |                |          |   | | 
|  | |                |          |   ` (if --includeusb and --avg) | 
|  | |                |          ` (if --includeusb) | 
|  | |                ` (if --avg) | 
|  | ` (if --timestamp) | 
|  | """ | 
|  |  | 
|  | import fcntl | 
|  | import os | 
|  | import select | 
|  | import signal | 
|  | import stat | 
|  | import struct | 
|  | import sys | 
|  | import time | 
|  | import collections | 
|  |  | 
|  | import gflags as flags  # http://code.google.com/p/python-gflags/ | 
|  |  | 
|  | import serial           # http://pyserial.sourceforge.net/ | 
|  |  | 
|  | FLAGS = flags.FLAGS | 
|  |  | 
|  | class Monsoon: | 
|  | """ | 
|  | Provides a simple class to use the power meter, e.g. | 
|  | mon = monsoon.Monsoon() | 
|  | mon.SetVoltage(3.7) | 
|  | mon.StartDataCollection() | 
|  | mydata = [] | 
|  | while len(mydata) < 1000: | 
|  | mydata.extend(mon.CollectData()) | 
|  | mon.StopDataCollection() | 
|  | """ | 
|  |  | 
|  | def __init__(self, device=None, serialno=None, wait=1): | 
|  | """ | 
|  | Establish a connection to a Monsoon. | 
|  | By default, opens the first available port, waiting if none are ready. | 
|  | A particular port can be specified with "device", or a particular Monsoon | 
|  | can be specified with "serialno" (using the number printed on its back). | 
|  | With wait=0, IOError is thrown if a device is not immediately available. | 
|  | """ | 
|  |  | 
|  | self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0 | 
|  | self._coarse_scale = self._fine_scale = 0 | 
|  | self._last_seq = 0 | 
|  | self.start_voltage = 0 | 
|  |  | 
|  | if device: | 
|  | self.ser = serial.Serial(device, timeout=1) | 
|  | return | 
|  |  | 
|  | while True:  # try all /dev/ttyACM* until we find one we can use | 
|  | for dev in os.listdir("/dev"): | 
|  | if not dev.startswith("ttyACM"): continue | 
|  | tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev) | 
|  | self._tempfile = open(tmpname, "w") | 
|  | try: | 
|  | os.chmod(tmpname, 0666) | 
|  | except OSError: | 
|  | pass | 
|  | try:  # use a lockfile to ensure exclusive access | 
|  | fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) | 
|  | except IOError as e: | 
|  | print >>sys.stderr, "device %s is in use" % dev | 
|  | continue | 
|  |  | 
|  | try:  # try to open the device | 
|  | self.ser = serial.Serial("/dev/%s" % dev, timeout=1) | 
|  | self.StopDataCollection()  # just in case | 
|  | self._FlushInput()  # discard stale input | 
|  | status = self.GetStatus() | 
|  | except Exception as e: | 
|  | print >>sys.stderr, "error opening device %s: %s" % (dev, e) | 
|  | continue | 
|  |  | 
|  | if not status: | 
|  | print >>sys.stderr, "no response from device %s" % dev | 
|  | elif serialno and status["serialNumber"] != serialno: | 
|  | print >>sys.stderr, ("Note: another device serial #%d seen on %s" % | 
|  | (status["serialNumber"], dev)) | 
|  | else: | 
|  | self.start_voltage = status["voltage1"] | 
|  | return | 
|  |  | 
|  | self._tempfile = None | 
|  | if not wait: raise IOError("No device found") | 
|  | print >>sys.stderr, "waiting for device..." | 
|  | time.sleep(1) | 
|  |  | 
|  |  | 
|  | def GetStatus(self): | 
|  | """ Requests and waits for status.  Returns status dictionary. """ | 
|  |  | 
|  | # status packet format | 
|  | STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH" | 
|  | STATUS_FIELDS = [ | 
|  | "packetType", "firmwareVersion", "protocolVersion", | 
|  | "mainFineCurrent", "usbFineCurrent", "auxFineCurrent", "voltage1", | 
|  | "mainCoarseCurrent", "usbCoarseCurrent", "auxCoarseCurrent", "voltage2", | 
|  | "outputVoltageSetting", "temperature", "status", "leds", | 
|  | "mainFineResistor", "serialNumber", "sampleRate", | 
|  | "dacCalLow", "dacCalHigh", | 
|  | "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime", | 
|  | "usbFineResistor", "auxFineResistor", | 
|  | "initialUsbVoltage", "initialAuxVoltage", | 
|  | "hardwareRevision", "temperatureLimit", "usbPassthroughMode", | 
|  | "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor", | 
|  | "defMainFineResistor", "defUsbFineResistor", "defAuxFineResistor", | 
|  | "defMainCoarseResistor", "defUsbCoarseResistor", "defAuxCoarseResistor", | 
|  | "eventCode", "eventData", ] | 
|  |  | 
|  | self._SendStruct("BBB", 0x01, 0x00, 0x00) | 
|  | while True:  # Keep reading, discarding non-status packets | 
|  | bytes = self._ReadPacket() | 
|  | if not bytes: return None | 
|  | if len(bytes) != struct.calcsize(STATUS_FORMAT) or bytes[0] != "\x10": | 
|  | print >>sys.stderr, "wanted status, dropped type=0x%02x, len=%d" % ( | 
|  | ord(bytes[0]), len(bytes)) | 
|  | continue | 
|  |  | 
|  | status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, bytes))) | 
|  | assert status["packetType"] == 0x10 | 
|  | for k in status.keys(): | 
|  | if k.endswith("VoltageSetting"): | 
|  | status[k] = 2.0 + status[k] * 0.01 | 
|  | elif k.endswith("FineCurrent"): | 
|  | pass # needs calibration data | 
|  | elif k.endswith("CoarseCurrent"): | 
|  | pass # needs calibration data | 
|  | elif k.startswith("voltage") or k.endswith("Voltage"): | 
|  | status[k] = status[k] * 0.000125 | 
|  | elif k.endswith("Resistor"): | 
|  | status[k] = 0.05 + status[k] * 0.0001 | 
|  | if k.startswith("aux") or k.startswith("defAux"): status[k] += 0.05 | 
|  | elif k.endswith("CurrentLimit"): | 
|  | status[k] = 8 * (1023 - status[k]) / 1023.0 | 
|  | return status | 
|  |  | 
|  | def RampVoltage(self, start, end): | 
|  | v = start | 
|  | if v < 3.0: v = 3.0       # protocol doesn't support lower than this | 
|  | while (v < end): | 
|  | self.SetVoltage(v) | 
|  | v += .1 | 
|  | time.sleep(.1) | 
|  | self.SetVoltage(end) | 
|  |  | 
|  | def SetVoltage(self, v): | 
|  | """ Set the output voltage, 0 to disable. """ | 
|  | if v == 0: | 
|  | self._SendStruct("BBB", 0x01, 0x01, 0x00) | 
|  | else: | 
|  | self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100)) | 
|  |  | 
|  |  | 
|  | def SetMaxCurrent(self, i): | 
|  | """Set the max output current.""" | 
|  | assert i >= 0 and i <= 8 | 
|  |  | 
|  | val = 1023 - int((i/8)*1023) | 
|  | self._SendStruct("BBB", 0x01, 0x0a, val & 0xff) | 
|  | self._SendStruct("BBB", 0x01, 0x0b, val >> 8) | 
|  |  | 
|  | def SetUsbPassthrough(self, val): | 
|  | """ Set the USB passthrough mode: 0 = off, 1 = on,  2 = auto. """ | 
|  | self._SendStruct("BBB", 0x01, 0x10, val) | 
|  |  | 
|  |  | 
|  | def StartDataCollection(self): | 
|  | """ Tell the device to start collecting and sending measurement data. """ | 
|  | self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command | 
|  | self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) | 
|  |  | 
|  |  | 
|  | def StopDataCollection(self): | 
|  | """ Tell the device to stop collecting measurement data. """ | 
|  | self._SendStruct("BB", 0x03, 0x00) # stop | 
|  |  | 
|  |  | 
|  | def CollectData(self): | 
|  | """ Return some current samples.  Call StartDataCollection() first. """ | 
|  | while True:  # loop until we get data or a timeout | 
|  | bytes = self._ReadPacket() | 
|  | if not bytes: return None | 
|  | if len(bytes) < 4 + 8 + 1 or bytes[0] < "\x20" or bytes[0] > "\x2F": | 
|  | print >>sys.stderr, "wanted data, dropped type=0x%02x, len=%d" % ( | 
|  | ord(bytes[0]), len(bytes)) | 
|  | continue | 
|  |  | 
|  | seq, type, x, y = struct.unpack("BBBB", bytes[:4]) | 
|  | data = [struct.unpack(">hhhh", bytes[x:x+8]) | 
|  | for x in range(4, len(bytes) - 8, 8)] | 
|  |  | 
|  | if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: | 
|  | print >>sys.stderr, "data sequence skipped, lost packet?" | 
|  | self._last_seq = seq | 
|  |  | 
|  | if type == 0: | 
|  | if not self._coarse_scale or not self._fine_scale: | 
|  | print >>sys.stderr, "waiting for calibration, dropped data packet" | 
|  | continue | 
|  |  | 
|  | def scale(val): | 
|  | if val & 1: | 
|  | return ((val & ~1) - self._coarse_zero) * self._coarse_scale | 
|  | else: | 
|  | return (val - self._fine_zero) * self._fine_scale | 
|  |  | 
|  | out_main = [] | 
|  | out_usb = [] | 
|  | for main, usb, aux, voltage in data: | 
|  | out_main.append(scale(main)) | 
|  | out_usb.append(scale(usb)) | 
|  | return (out_main, out_usb) | 
|  |  | 
|  | elif type == 1: | 
|  | self._fine_zero = data[0][0] | 
|  | self._coarse_zero = data[1][0] | 
|  | # print >>sys.stderr, "zero calibration: fine 0x%04x, coarse 0x%04x" % ( | 
|  | #     self._fine_zero, self._coarse_zero) | 
|  |  | 
|  | elif type == 2: | 
|  | self._fine_ref = data[0][0] | 
|  | self._coarse_ref = data[1][0] | 
|  | # print >>sys.stderr, "ref calibration: fine 0x%04x, coarse 0x%04x" % ( | 
|  | #     self._fine_ref, self._coarse_ref) | 
|  |  | 
|  | else: | 
|  | print >>sys.stderr, "discarding data packet type=0x%02x" % type | 
|  | continue | 
|  |  | 
|  | if self._coarse_ref != self._coarse_zero: | 
|  | self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero) | 
|  | if self._fine_ref != self._fine_zero: | 
|  | self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) | 
|  |  | 
|  |  | 
|  | def _SendStruct(self, fmt, *args): | 
|  | """ Pack a struct (without length or checksum) and send it. """ | 
|  | data = struct.pack(fmt, *args) | 
|  | data_len = len(data) + 1 | 
|  | checksum = (data_len + sum(struct.unpack("B" * len(data), data))) % 256 | 
|  | out = struct.pack("B", data_len) + data + struct.pack("B", checksum) | 
|  | self.ser.write(out) | 
|  |  | 
|  |  | 
|  | def _ReadPacket(self): | 
|  | """ Read a single data record as a string (without length or checksum). """ | 
|  | len_char = self.ser.read(1) | 
|  | if not len_char: | 
|  | print >>sys.stderr, "timeout reading from serial port" | 
|  | return None | 
|  |  | 
|  | data_len = struct.unpack("B", len_char) | 
|  | data_len = ord(len_char) | 
|  | if not data_len: return "" | 
|  |  | 
|  | result = self.ser.read(data_len) | 
|  | if len(result) != data_len: return None | 
|  | body = result[:-1] | 
|  | checksum = (data_len + sum(struct.unpack("B" * len(body), body))) % 256 | 
|  | if result[-1] != struct.pack("B", checksum): | 
|  | print >>sys.stderr, "invalid checksum from serial port" | 
|  | return None | 
|  | return result[:-1] | 
|  |  | 
|  | def _FlushInput(self): | 
|  | """ Flush all read data until no more available. """ | 
|  | self.ser.flush() | 
|  | flushed = 0 | 
|  | while True: | 
|  | ready_r, ready_w, ready_x = select.select([self.ser], [], [self.ser], 0) | 
|  | if len(ready_x) > 0: | 
|  | print >>sys.stderr, "exception from serial port" | 
|  | return None | 
|  | elif len(ready_r) > 0: | 
|  | flushed += 1 | 
|  | self.ser.read(1)  # This may cause underlying buffering. | 
|  | self.ser.flush()  # Flush the underlying buffer too. | 
|  | else: | 
|  | break | 
|  | if flushed > 0: | 
|  | print >>sys.stderr, "dropped >%d bytes" % flushed | 
|  |  | 
|  | def main(argv): | 
|  | """ Simple command-line interface for Monsoon.""" | 
|  | useful_flags = ["voltage", "status", "usbpassthrough", "samples", "current"] | 
|  | if not [f for f in useful_flags if FLAGS.get(f, None) is not None]: | 
|  | print __doc__.strip() | 
|  | print FLAGS.MainModuleHelp() | 
|  | return | 
|  |  | 
|  | if FLAGS.includeusb: | 
|  | num_channels = 2 | 
|  | else: | 
|  | num_channels = 1 | 
|  |  | 
|  | if FLAGS.avg and FLAGS.avg < 0: | 
|  | print "--avg must be greater than 0" | 
|  | return | 
|  |  | 
|  | mon = Monsoon(device=FLAGS.device, serialno=FLAGS.serialno) | 
|  |  | 
|  | if FLAGS.voltage is not None: | 
|  | if FLAGS.ramp is not None: | 
|  | mon.RampVoltage(mon.start_voltage, FLAGS.voltage) | 
|  | else: | 
|  | mon.SetVoltage(FLAGS.voltage) | 
|  |  | 
|  | if FLAGS.current is not None: | 
|  | mon.SetMaxCurrent(FLAGS.current) | 
|  |  | 
|  | if FLAGS.status: | 
|  | items = sorted(mon.GetStatus().items()) | 
|  | print "\n".join(["%s: %s" % item for item in items]) | 
|  |  | 
|  | if FLAGS.usbpassthrough: | 
|  | if FLAGS.usbpassthrough == 'off': | 
|  | mon.SetUsbPassthrough(0) | 
|  | elif FLAGS.usbpassthrough == 'on': | 
|  | mon.SetUsbPassthrough(1) | 
|  | elif FLAGS.usbpassthrough == 'auto': | 
|  | mon.SetUsbPassthrough(2) | 
|  | else: | 
|  | sys.exit('bad passthrough flag: %s' % FLAGS.usbpassthrough) | 
|  |  | 
|  | if FLAGS.samples: | 
|  | # Make sure state is normal | 
|  | mon.StopDataCollection() | 
|  | status = mon.GetStatus() | 
|  | native_hz = status["sampleRate"] * 1000 | 
|  |  | 
|  | # Collect and average samples as specified | 
|  | mon.StartDataCollection() | 
|  |  | 
|  | # In case FLAGS.hz doesn't divide native_hz exactly, use this invariant: | 
|  | # 'offset' = (consumed samples) * FLAGS.hz - (emitted samples) * native_hz | 
|  | # This is the error accumulator in a variation of Bresenham's algorithm. | 
|  | emitted = offset = 0 | 
|  | chan_buffers = tuple([] for _ in range(num_channels)) | 
|  | # past n samples for rolling average | 
|  | history_deques = tuple(collections.deque() for _ in range(num_channels)) | 
|  |  | 
|  | try: | 
|  | last_flush = time.time() | 
|  | while emitted < FLAGS.samples or FLAGS.samples == -1: | 
|  | # The number of raw samples to consume before emitting the next output | 
|  | need = (native_hz - offset + FLAGS.hz - 1) / FLAGS.hz | 
|  | if need > len(chan_buffers[0]):     # still need more input samples | 
|  | chans_samples = mon.CollectData() | 
|  | if not all(chans_samples): break | 
|  | for chan_buffer, chan_samples in zip(chan_buffers, chans_samples): | 
|  | chan_buffer.extend(chan_samples) | 
|  | else: | 
|  | # Have enough data, generate output samples. | 
|  | # Adjust for consuming 'need' input samples. | 
|  | offset += need * FLAGS.hz | 
|  | while offset >= native_hz:  # maybe multiple, if FLAGS.hz > native_hz | 
|  | this_sample = [sum(chan[:need]) / need for chan in chan_buffers] | 
|  |  | 
|  | if FLAGS.timestamp: print int(time.time()), | 
|  |  | 
|  | if FLAGS.avg: | 
|  | chan_avgs = [] | 
|  | for chan_deque, chan_sample in zip(history_deques, this_sample): | 
|  | chan_deque.appendleft(chan_sample) | 
|  | if len(chan_deque) > FLAGS.avg: chan_deque.pop() | 
|  | chan_avgs.append(sum(chan_deque) / len(chan_deque)) | 
|  | # Interleave channel rolling avgs with latest channel data | 
|  | data_to_print = [datum | 
|  | for pair in zip(this_sample, chan_avgs) | 
|  | for datum in pair] | 
|  | else: | 
|  | data_to_print = this_sample | 
|  |  | 
|  | fmt = ' '.join('%f' for _ in data_to_print) | 
|  | print fmt % tuple(data_to_print) | 
|  |  | 
|  | sys.stdout.flush() | 
|  |  | 
|  | offset -= native_hz | 
|  | emitted += 1              # adjust for emitting 1 output sample | 
|  | chan_buffers = tuple(c[need:] for c in chan_buffers) | 
|  | now = time.time() | 
|  | if now - last_flush >= 0.99:  # flush every second | 
|  | sys.stdout.flush() | 
|  | last_flush = now | 
|  | except KeyboardInterrupt: | 
|  | print >>sys.stderr, "interrupted" | 
|  |  | 
|  | mon.StopDataCollection() | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | # Define flags here to avoid conflicts with people who use us as a library | 
|  | flags.DEFINE_boolean("status", None, "Print power meter status") | 
|  | flags.DEFINE_integer("avg", None, | 
|  | "Also report average over last n data points") | 
|  | flags.DEFINE_float("voltage", None, "Set output voltage (0 for off)") | 
|  | flags.DEFINE_float("current", None, "Set max output current") | 
|  | flags.DEFINE_string("usbpassthrough", None, "USB control (on, off, auto)") | 
|  | flags.DEFINE_integer("samples", None, | 
|  | "Collect and print this many samples. " | 
|  | "-1 means collect indefinitely.") | 
|  | flags.DEFINE_integer("hz", 5000, "Print this many samples/sec") | 
|  | flags.DEFINE_string("device", None, | 
|  | "Path to the device in /dev/... (ex:/dev/ttyACM1)") | 
|  | flags.DEFINE_integer("serialno", None, "Look for a device with this serial number") | 
|  | flags.DEFINE_boolean("timestamp", None, | 
|  | "Also print integer (seconds) timestamp on each line") | 
|  | flags.DEFINE_boolean("ramp", True, "Gradually increase voltage") | 
|  | flags.DEFINE_boolean("includeusb", False, "Include measurements from USB channel") | 
|  |  | 
|  | main(FLAGS(sys.argv)) |