• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2019 - 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"""The interface for a USB-connected Monsoon power meter.
17
18Details on the protocol can be found at
19(http://msoon.com/LabEquipment/PowerMonitor/)
20
21Based on the original py2 script of kens@google.com.
22"""
23import collections
24import fcntl
25import logging
26import os
27import select
28import struct
29import sys
30import time
31
32import errno
33import serial
34
35from acts.controllers.monsoon_lib.api.common import MonsoonError
36
37
38class LvpmStatusPacket(object):
39    """The data received from asking an LVPM Monsoon for its status.
40
41    Attributes names with the same values as HVPM match those defined in
42    Monsoon.Operations.statusPacket.
43    """
44
45    def __init__(self, values):
46        iter_value = iter(values)
47        self.packetType = next(iter_value)
48        self.firmwareVersion = next(iter_value)
49        self.protocolVersion = next(iter_value)
50        self.mainFineCurrent = next(iter_value)
51        self.usbFineCurrent = next(iter_value)
52        self.auxFineCurrent = next(iter_value)
53        self.voltage1 = next(iter_value)
54        self.mainCoarseCurrent = next(iter_value)
55        self.usbCoarseCurrent = next(iter_value)
56        self.auxCoarseCurrent = next(iter_value)
57        self.voltage2 = next(iter_value)
58        self.outputVoltageSetting = next(iter_value)
59        self.temperature = next(iter_value)
60        self.status = next(iter_value)
61        self.leds = next(iter_value)
62        self.mainFineResistor = next(iter_value)
63        self.serialNumber = next(iter_value)
64        self.sampleRate = next(iter_value)
65        self.dacCalLow = next(iter_value)
66        self.dacCalHigh = next(iter_value)
67        self.powerupCurrentLimit = next(iter_value)
68        self.runtimeCurrentLimit = next(iter_value)
69        self.powerupTime = next(iter_value)
70        self.usbFineResistor = next(iter_value)
71        self.auxFineResistor = next(iter_value)
72        self.initialUsbVoltage = next(iter_value)
73        self.initialAuxVoltage = next(iter_value)
74        self.hardwareRevision = next(iter_value)
75        self.temperatureLimit = next(iter_value)
76        self.usbPassthroughMode = next(iter_value)
77        self.mainCoarseResistor = next(iter_value)
78        self.usbCoarseResistor = next(iter_value)
79        self.auxCoarseResistor = next(iter_value)
80        self.defMainFineResistor = next(iter_value)
81        self.defUsbFineResistor = next(iter_value)
82        self.defAuxFineResistor = next(iter_value)
83        self.defMainCoarseResistor = next(iter_value)
84        self.defUsbCoarseResistor = next(iter_value)
85        self.defAuxCoarseResistor = next(iter_value)
86        self.eventCode = next(iter_value)
87        self.eventData = next(iter_value)
88
89
90class MonsoonProxy(object):
91    """Class that directly talks to monsoon over serial.
92
93    Provides a simple class to use the power meter.
94    See http://wiki/Main/MonsoonProtocol for information on the protocol.
95    """
96
97    # The format of the status packet.
98    STATUS_FORMAT = '>BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH'
99
100    # The list of fields that appear in the Monsoon status packet.
101    STATUS_FIELDS = [
102        'packetType',
103        'firmwareVersion',
104        'protocolVersion',
105        'mainFineCurrent',
106        'usbFineCurrent',
107        'auxFineCurrent',
108        'voltage1',
109        'mainCoarseCurrent',
110        'usbCoarseCurrent',
111        'auxCoarseCurrent',
112        'voltage2',
113        'outputVoltageSetting',
114        'temperature',
115        'status',
116        'leds',
117        'mainFineResistorOffset',
118        'serialNumber',
119        'sampleRate',
120        'dacCalLow',
121        'dacCalHigh',
122        'powerupCurrentLimit',
123        'runtimeCurrentLimit',
124        'powerupTime',
125        'usbFineResistorOffset',
126        'auxFineResistorOffset',
127        'initialUsbVoltage',
128        'initialAuxVoltage',
129        'hardwareRevision',
130        'temperatureLimit',
131        'usbPassthroughMode',
132        'mainCoarseResistorOffset',
133        'usbCoarseResistorOffset',
134        'auxCoarseResistorOffset',
135        'defMainFineResistor',
136        'defUsbFineResistor',
137        'defAuxFineResistor',
138        'defMainCoarseResistor',
139        'defUsbCoarseResistor',
140        'defAuxCoarseResistor',
141        'eventCode',
142        'eventData',
143    ]
144
145    def __init__(self, device=None, serialno=None, connection_timeout=600):
146        """Establish a connection to a Monsoon.
147
148        By default, opens the first available port, waiting if none are ready.
149
150        Args:
151            device: The particular device port to be used.
152            serialno: The Monsoon's serial number.
153            connection_timeout: The number of seconds to wait for the device to
154                connect.
155
156        Raises:
157            TimeoutError if unable to connect to the device.
158        """
159        self.start_voltage = 0
160        self.serial = serialno
161
162        if device:
163            self.ser = serial.Serial(device, timeout=1)
164            return
165        # Try all devices connected through USB virtual serial ports until we
166        # find one we can use.
167        self._tempfile = None
168        self.obtain_dev_port(connection_timeout)
169        self.log = logging.getLogger()
170
171    def obtain_dev_port(self, timeout=600):
172        """Obtains the device port for this Monsoon.
173
174        Args:
175            timeout: The time in seconds to wait for the device to connect.
176
177        Raises:
178            TimeoutError if the device was unable to be found, or was not
179            available.
180        """
181        start_time = time.time()
182
183        while start_time + timeout > time.time():
184            for dev in os.listdir('/dev'):
185                prefix = 'ttyACM'
186                # Prefix is different on Mac OS X.
187                if sys.platform == 'darwin':
188                    prefix = 'tty.usbmodem'
189                if not dev.startswith(prefix):
190                    continue
191                tmpname = '/tmp/monsoon.%s.%s' % (os.uname()[0], dev)
192                self._tempfile = open(tmpname, 'w')
193                if not os.access(tmpname, os.R_OK | os.W_OK):
194                    try:
195                        os.chmod(tmpname, 0o666)
196                    except OSError as e:
197                        if e.errno == errno.EACCES:
198                            raise ValueError(
199                                'Unable to set permissions to read/write to '
200                                '%s. This file is owned by another user; '
201                                'please grant o+wr access to this file, or '
202                                'run as that user.')
203                        raise
204
205                try:  # Use a lock file to ensure exclusive access.
206                    fcntl.flock(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
207                except IOError:
208                    logging.error('Device %s is in use.', repr(dev))
209                    continue
210
211                try:  # try to open the device
212                    self.ser = serial.Serial('/dev/%s' % dev, timeout=1)
213                    self.stop_data_collection()  # just in case
214                    self._flush_input()  # discard stale input
215                    status = self.get_status()
216                except Exception as e:
217                    logging.warning('Error opening device %s: %s', dev, e,
218                                    exc_info=True)
219                    continue
220
221                if not status:
222                    logging.error('No response from device %s.', dev)
223                elif self.serial and status.serialNumber != self.serial:
224                    logging.error('Another device serial #%d seen on %s',
225                                  status.serialNumber, dev)
226                else:
227                    self.start_voltage = status.voltage1
228                    return
229
230            self._tempfile = None
231            logging.info('Waiting for device...')
232            time.sleep(1)
233        raise TimeoutError(
234            'Unable to connect to Monsoon device with '
235            'serial "%s" within %s seconds.' % (self.serial, timeout))
236
237    def release_dev_port(self):
238        """Releases the dev port used to communicate with the Monsoon device."""
239        fcntl.flock(self._tempfile, fcntl.LOCK_UN)
240        self._tempfile.close()
241        self.ser.close()
242
243    def get_status(self):
244        """Requests and waits for status.
245
246        Returns:
247            status dictionary.
248        """
249        self._send_struct('BBB', 0x01, 0x00, 0x00)
250        read_bytes = self._read_packet()
251
252        if not read_bytes:
253            raise MonsoonError('Failed to read Monsoon status')
254        expected_size = struct.calcsize(self.STATUS_FORMAT)
255        if len(read_bytes) != expected_size or read_bytes[0] != 0x10:
256            raise MonsoonError('Wanted status, dropped type=0x%02x, len=%d',
257                               read_bytes[0], len(read_bytes))
258
259        status = collections.OrderedDict(
260            zip(self.STATUS_FIELDS,
261                struct.unpack(self.STATUS_FORMAT, read_bytes)))
262        p_type = status['packetType']
263        if p_type != 0x10:
264            raise MonsoonError('Packet type %s is not 0x10.' % p_type)
265
266        for k in status.keys():
267            if k.endswith('VoltageSetting'):
268                status[k] = 2.0 + status[k] * 0.01
269            elif k.endswith('FineCurrent'):
270                pass  # needs calibration data
271            elif k.endswith('CoarseCurrent'):
272                pass  # needs calibration data
273            elif k.startswith('voltage') or k.endswith('Voltage'):
274                status[k] = status[k] * 0.000125
275            elif k.endswith('Resistor'):
276                status[k] = 0.05 + status[k] * 0.0001
277                if k.startswith('aux') or k.startswith('defAux'):
278                    status[k] += 0.05
279            elif k.endswith('CurrentLimit'):
280                status[k] = 8 * (1023 - status[k]) / 1023.0
281        return LvpmStatusPacket(status.values())
282
283    def set_voltage(self, voltage):
284        """Sets the voltage on the device to the specified value.
285
286        Args:
287            voltage: Either 0 or a value between 2.01 and 4.55 inclusive.
288
289        Raises:
290            struct.error if voltage is an invalid value.
291        """
292        # The device has a range of 255 voltage values:
293        #
294        #     0   is "off". Note this value not set outputVoltageSetting to
295        #             zero. The previous outputVoltageSetting value is
296        #             maintained.
297        #     1   is 2.01V.
298        #     255 is 4.55V.
299        voltage_byte = max(0, round((voltage - 2.0) * 100))
300        self._send_struct('BBB', 0x01, 0x01, voltage_byte)
301
302    def get_voltage(self):
303        """Get the output voltage.
304
305        Returns:
306            Current Output Voltage (in unit of V).
307        """
308        return self.get_status().outputVoltageSetting
309
310    def set_max_current(self, i):
311        """Set the max output current."""
312        if i < 0 or i > 8:
313            raise MonsoonError(('Target max current %sA, is out of acceptable '
314                                'range [0, 8].') % i)
315        val = 1023 - int((i / 8) * 1023)
316        self._send_struct('BBB', 0x01, 0x0a, val & 0xff)
317        self._send_struct('BBB', 0x01, 0x0b, val >> 8)
318
319    def set_max_initial_current(self, current):
320        """Sets the maximum initial current, in mA."""
321        if current < 0 or current > 8:
322            raise MonsoonError(('Target max current %sA, is out of acceptable '
323                                'range [0, 8].') % current)
324        val = 1023 - int((current / 8) * 1023)
325        self._send_struct('BBB', 0x01, 0x08, val & 0xff)
326        self._send_struct('BBB', 0x01, 0x09, val >> 8)
327
328    def set_usb_passthrough(self, passthrough_mode):
329        """Set the USB passthrough mode.
330
331        Args:
332            passthrough_mode: The mode used for passthrough. Must be the integer
333                value. See common.PassthroughModes for a list of values and
334                their meanings.
335        """
336        self._send_struct('BBB', 0x01, 0x10, passthrough_mode)
337
338    def get_usb_passthrough(self):
339        """Get the USB passthrough mode: 0 = off, 1 = on,  2 = auto.
340
341        Returns:
342            The mode used for passthrough, as an integer. See
343                common.PassthroughModes for a list of values and their meanings.
344        """
345        return self.get_status().usbPassthroughMode
346
347    def start_data_collection(self):
348        """Tell the device to start collecting and sending measurement data."""
349        self._send_struct('BBB', 0x01, 0x1b, 0x01)  # Mystery command
350        self._send_struct('BBBBBBB', 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
351
352    def stop_data_collection(self):
353        """Tell the device to stop collecting measurement data."""
354        self._send_struct('BB', 0x03, 0x00)  # stop
355
356    def _send_struct(self, fmt, *args):
357        """Pack a struct (without length or checksum) and send it."""
358        # Flush out the input buffer before sending data
359        self._flush_input()
360        data = struct.pack(fmt, *args)
361        data_len = len(data) + 1
362        checksum = (data_len + sum(bytearray(data))) % 256
363        out = struct.pack('B', data_len) + data + struct.pack('B', checksum)
364        self.ser.write(out)
365
366    def _read_packet(self):
367        """Returns a single packet as a string (without length or checksum)."""
368        len_char = self.ser.read(1)
369        if not len_char:
370            raise MonsoonError('Reading from serial port timed out')
371
372        data_len = ord(len_char)
373        if not data_len:
374            return ''
375        result = self.ser.read(int(data_len))
376        result = bytearray(result)
377        if len(result) != data_len:
378            raise MonsoonError(
379                'Length mismatch, expected %d bytes, got %d bytes.', data_len,
380                len(result))
381        body = result[:-1]
382        checksum = (sum(struct.unpack('B' * len(body), body)) + data_len) % 256
383        if result[-1] != checksum:
384            raise MonsoonError(
385                'Invalid checksum from serial port! Expected %s, got %s',
386                hex(checksum), hex(result[-1]))
387        return result[:-1]
388
389    def _flush_input(self):
390        """Flushes all read data until the input is empty."""
391        self.ser.reset_input_buffer()
392        while True:
393            ready_r, ready_w, ready_x = select.select([self.ser], [],
394                                                      [self.ser], 0)
395            if len(ready_x) > 0:
396                raise MonsoonError('Exception from serial port.')
397            elif len(ready_r) > 0:
398                self.ser.read(1)  # This may cause underlying buffering.
399                # Flush the underlying buffer too.
400                self.ser.reset_input_buffer()
401            else:
402                break
403