1#!/usr/bin/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
18import fcntl
19import logging
20logging.getLogger().setLevel(logging.ERROR)
21
22import os.path
23import select
24import stat
25import struct
26import sys
27import time
28import collections
29import socket
30import glob
31import signal
32import serial           # http://pyserial.sourceforge.net/
33
34#Set to True if you want log output to go to screen:
35LOG_TO_SCREEN = False
36
37TIMEOUT_SERIAL = 1 #seconds
38
39#ignore SIG CONTINUE signals
40for signum in [signal.SIGCONT]:
41  signal.signal(signum, signal.SIG_IGN)
42
43try:
44  from . import Abstract_Power_Monitor
45except:
46  sys.exit("You cannot run 'monsoon.py' directly.  Run 'execut_power_tests.py' instead.")
47
48class Power_Monitor(Abstract_Power_Monitor):
49  """
50  Provides a simple class to use the power meter, e.g.
51  mon = monsoon.Power_Monitor()
52  mon.SetVoltage(3.7)
53  mon.StartDataCollection()
54  mydata = []
55  while len(mydata) < 1000:
56    mydata.extend(mon.CollectData())
57  mon.StopDataCollection()
58  """
59  _do_log = False
60
61  @staticmethod
62  def lock( device ):
63      tmpname = "/tmp/monsoon.%s.%s" % ( os.uname()[0],
64                                         os.path.basename(device))
65      lockfile = open(tmpname, "w")
66      try:  # use a lockfile to ensure exclusive access
67          fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
68          logging.debug("Locked device %s"%device)
69      except IOError as e:
70          self.log("device %s is in use" % dev)
71          sys.exit('device in use')
72      return lockfile
73
74  def to_string(self):
75      return self._devicename
76
77  def __init__(self, device = None, wait = False, log_file_id= None ):
78    """
79    Establish a connection to a Power_Monitor.
80    By default, opens the first available port, waiting if none are ready.
81    A particular port can be specified with "device".
82    With wait=0, IOError is thrown if a device is not immediately available.
83    """
84    self._lockfile = None
85    self._logfile = None
86    self.ser = None
87    for signum in [signal.SIGALRM, signal.SIGHUP, signal.SIGINT,
88                   signal.SIGILL, signal.SIGQUIT,
89                   signal.SIGTRAP,signal.SIGABRT, signal.SIGIOT, signal.SIGBUS,
90                   signal.SIGFPE, signal.SIGSEGV, signal.SIGUSR2, signal.SIGPIPE,
91                   signal.SIGTERM]:
92      signal.signal(signum, self.handle_signal)
93
94    self._coarse_ref = self._fine_ref = self._coarse_zero = self._fine_zero = 0
95    self._coarse_scale = self._fine_scale = 0
96    self._last_seq = 0
97    self.start_voltage = 0
98
99    if device:
100      if isinstance( device, serial.Serial ):
101        self.ser = device
102
103    else:
104        device_list = None
105        while not device_list:
106            device_list = Power_Monitor.Discover()
107            if not device_list and wait:
108                time.sleep(1.0)
109                logging.info("No power monitor serial devices found.  Retrying...")
110            elif not device_list and not wait:
111                logging.error("No power monitor serial devices found.  Exiting")
112                self.Close()
113                sys.exit("No power monitor serial devices found")
114
115        if device_list:
116            if len(device_list) > 1:
117                logging.error("=======================================")
118                logging.error("More than one power monitor discovered!")
119                logging.error("Test may not execute properly.Aborting test.")
120                logging.error("=======================================")
121                sys.exit("More than one power monitor connected.")
122            device = device_list[0].to_string() # choose the first one
123            if len(device_list) > 1:
124                logging.info("More than one device found.  Using %s"%device)
125            else:
126                logging.info("Power monitor @ %s"%device)
127        else: raise IOError("No device found")
128
129    self._lockfile = Power_Monitor.lock( device )
130    if log_file_id is not None:
131        self._logfilename = "/tmp/monsoon_%s_%s.%s.log" % (os.uname()[0], os.path.basename(device),
132                                                            log_file_id)
133        self._logfile = open(self._logfilename,'a')
134    else:
135        self._logfile = None
136    try:
137        self.ser = serial.Serial(device, timeout= TIMEOUT_SERIAL)
138    except Exception as e:
139      self.log( "error opening device %s: %s" % (dev, e))
140      self._lockfile.close()
141      raise
142    logging.debug("Setting up power monitor...")
143    self._devicename = device
144    #just in case, stop any active data collection on monsoon
145    self._dataCollectionActive = True
146    self.StopDataCollection()
147    logging.debug("Flushing input...")
148    self._FlushInput()  # discard stale input
149    logging.debug("Getting status....")
150    status = self.GetStatus()
151
152    if not status:
153      self.log( "no response from device %s" % device)
154      self._lockfile.close()
155      raise IOError("Failed to get status from device")
156    self.start_voltage = status["voltage1"]
157
158  def __del__(self):
159    self.Close()
160
161  def Close(self):
162    if self._logfile:
163      print("=============\n"+\
164            "Power Monitor log file can be found at '%s'"%self._logfilename +
165            "=============\n")
166      self._logfile.close()
167      self._logfile = None
168    if (self.ser):
169      #self.StopDataCollection()
170      self.ser.flush()
171      self.ser.close()
172      self.ser = None
173    if self._lockfile:
174      self._lockfile.close()
175
176  def log(self, msg , debug = False):
177    if self._logfile: self._logfile.write( msg + "\n")
178    if not debug and LOG_TO_SCREEN:
179      logging.error( msg )
180    else:
181      logging.debug(msg)
182
183  def handle_signal( self, signum, frame):
184    if self.ser:
185      self.ser.flush()
186      self.ser.close()
187      self.ser = None
188    self.log("Got signal %d"%signum)
189    sys.exit("\nGot signal %d\n"%signum)
190
191  @staticmethod
192  def Discover():
193    monsoon_list = []
194    elapsed = 0
195    logging.info("Discovering power monitor(s)...")
196    ser_device_list = glob.glob("/dev/ttyACM*")
197    logging.info("Seeking devices %s"%ser_device_list)
198    for dev in ser_device_list:
199        try:
200            lockfile = Power_Monitor.lock( dev )
201        except:
202            logging.info( "... device %s in use, skipping"%dev)
203            continue
204        tries = 0
205        ser = None
206        while ser is None and tries < 100:
207             try:  # try to open the device
208                ser = serial.Serial( dev, timeout=TIMEOUT_SERIAL)
209             except Exception as e:
210                logging.error(  "error opening device %s: %s" % (dev, e) )
211                tries += 1
212                time.sleep(2);
213                ser = None
214        logging.info("... found device %s"%dev)
215        lockfile.close()#will be re-locked once monsoon instance created
216        logging.debug("unlocked")
217        if not ser:
218            continue
219        if ser is not None:
220            try:
221                monsoon = Power_Monitor(device = dev)
222                status = monsoon.GetStatus()
223
224                if not status:
225                    monsoon.log("... no response from device %s, skipping")
226                    continue
227                else:
228                    logging.info("... found power monitor @ %s"%dev)
229                    monsoon_list.append( monsoon )
230            except:
231                import traceback
232                traceback.print_exc()
233                logging.error("... %s appears to not be a monsoon device"%dev)
234    logging.debug("Returning list of %s"%monsoon_list)
235    return monsoon_list
236
237  def GetStatus(self):
238    """ Requests and waits for status.  Returns status dictionary. """
239
240    # status packet format
241    self.log("Getting status...", debug = True)
242    STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH"
243    STATUS_FIELDS = [
244        "packetType", "firmwareVersion", "protocolVersion",
245        "mainFineCurrent", "usbFineCurrent", "auxFineCurrent", "voltage1",
246        "mainCoarseCurrent", "usbCoarseCurrent", "auxCoarseCurrent", "voltage2",
247        "outputVoltageSetting", "temperature", "status", "leds",
248        "mainFineResistor", "serialNumber", "sampleRate",
249        "dacCalLow", "dacCalHigh",
250        "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime",
251        "usbFineResistor", "auxFineResistor",
252        "initialUsbVoltage", "initialAuxVoltage",
253        "hardwareRevision", "temperatureLimit", "usbPassthroughMode",
254        "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor",
255        "defMainFineResistor", "defUsbFineResistor", "defAuxFineResistor",
256        "defMainCoarseResistor", "defUsbCoarseResistor", "defAuxCoarseResistor",
257        "eventCode", "eventData", ]
258
259    self._SendStruct("BBB", 0x01, 0x00, 0x00)
260    while True:  # Keep reading, discarding non-status packets
261      bytes = self._ReadPacket()
262      if not bytes: return None
263      if len(bytes) != struct.calcsize(STATUS_FORMAT) or bytes[0] != "\x10":
264        self.log("wanted status, dropped type=0x%02x, len=%d" % (
265                ord(bytes[0]), len(bytes)))
266        continue
267
268      status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, bytes)))
269      assert status["packetType"] == 0x10
270      for k in status.keys():
271        if k.endswith("VoltageSetting"):
272          status[k] = 2.0 + status[k] * 0.01
273        elif k.endswith("FineCurrent"):
274          pass # needs calibration data
275        elif k.endswith("CoarseCurrent"):
276          pass # needs calibration data
277        elif k.startswith("voltage") or k.endswith("Voltage"):
278          status[k] = status[k] * 0.000125
279        elif k.endswith("Resistor"):
280          status[k] = 0.05 + status[k] * 0.0001
281          if k.startswith("aux") or k.startswith("defAux"): status[k] += 0.05
282        elif k.endswith("CurrentLimit"):
283          status[k] = 8 * (1023 - status[k]) / 1023.0
284      #self.log( "Returning requested status: \n %s"%(status), debug = True)
285      return status
286
287  def RampVoltage(self, start, end):
288    v = start
289    if v < 3.0: v = 3.0       # protocol doesn't support lower than this
290    while (v < end):
291      self.SetVoltage(v)
292      v += .1
293      time.sleep(.1)
294    self.SetVoltage(end)
295
296  def SetVoltage(self, v):
297    """ Set the output voltage, 0 to disable. """
298    self.log("Setting voltage to %s..."%v, debug = True)
299    if v == 0:
300      self._SendStruct("BBB", 0x01, 0x01, 0x00)
301    else:
302      self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100))
303    self.log("...Set voltage", debug = True)
304
305  def SetMaxCurrent(self, i):
306    """Set the max output current."""
307    assert i >= 0 and i <= 8
308    self.log("Setting max current to %s..."%i, debug = True)
309    val = 1023 - int((i/8)*1023)
310    self._SendStruct("BBB", 0x01, 0x0a, val & 0xff)
311    self._SendStruct("BBB", 0x01, 0x0b, val >> 8)
312    self.log("...Set max current.", debug = True)
313
314  def SetUsbPassthrough(self, val):
315    """ Set the USB passthrough mode: 0 = off, 1 = on,  2 = auto. """
316    self._SendStruct("BBB", 0x01, 0x10, val)
317
318  def StartDataCollection(self):
319    """ Tell the device to start collecting and sending measurement data. """
320    self.log("Starting data collection...", debug = True)
321    self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command
322    self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
323    self.log("...started", debug = True)
324    self._dataCollectionActive = True
325
326  def StopDataCollection(self):
327    """ Tell the device to stop collecting measurement data. """
328    self._SendStruct("BB", 0x03, 0x00) # stop
329    if self._dataCollectionActive:
330      while self.CollectData(False) is not None:
331        pass
332    self._dataCollectionActive = False
333
334  def CollectData(self, verbose = True):
335    """ Return some current samples.  Call StartDataCollection() first. """
336    #self.log("Collecting data ...", debug = True)
337    while True:  # loop until we get data or a timeout
338      bytes = self._ReadPacket(verbose)
339
340      if not bytes: return None
341      if len(bytes) < 4 + 8 + 1 or bytes[0] < "\x20" or bytes[0] > "\x2F":
342        if verbose: self.log( "wanted data, dropped type=0x%02x, len=%d" % (
343          ord(bytes[0]), len(bytes)), debug=verbose)
344        continue
345
346      seq, type, x, y = struct.unpack("BBBB", bytes[:4])
347      data = [struct.unpack(">hhhh", bytes[x:x+8])
348              for x in range(4, len(bytes) - 8, 8)]
349
350      if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
351        self.log( "data sequence skipped, lost packet?" )
352      self._last_seq = seq
353
354      if type == 0:
355        if not self._coarse_scale or not self._fine_scale:
356          self.log("waiting for calibration, dropped data packet")
357          continue
358
359        out = []
360        for main, usb, aux, voltage in data:
361          if main & 1:
362            out.append(((main & ~1) - self._coarse_zero) * self._coarse_scale)
363          else:
364            out.append((main - self._fine_zero) * self._fine_scale)
365        #self.log("...Collected %d samples"%(len(out)), debug = True)
366        return out
367
368      elif type == 1:
369        self._fine_zero = data[0][0]
370        self._coarse_zero = data[1][0]
371
372      elif type == 2:
373        self._fine_ref = data[0][0]
374        self._coarse_ref = data[1][0]
375
376      else:
377        self.log( "discarding data packet type=0x%02x" % type)
378        continue
379
380      if self._coarse_ref != self._coarse_zero:
381        self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
382      if self._fine_ref != self._fine_zero:
383        self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
384
385
386  def _SendStruct(self, fmt, *args):
387    """ Pack a struct (without length or checksum) and send it. """
388    data = struct.pack(fmt, *args)
389    data_len = len(data) + 1
390    checksum = (data_len + sum(struct.unpack("B" * len(data), data))) % 256
391    out = struct.pack("B", data_len) + data + struct.pack("B", checksum)
392    self.ser.write(out)
393    self.ser.flush()
394
395  def _ReadPacket(self, verbose = True):
396    """ Read a single data record as a string (without length or checksum). """
397    len_char = self.ser.read(1)
398    if not len_char:
399      if verbose: self.log( "timeout reading from serial port" )
400      return None
401
402    data_len = struct.unpack("B", len_char)
403    data_len = ord(len_char)
404    if not data_len: return ""
405
406    result = self.ser.read(data_len)
407    if len(result) != data_len: return None
408    body = result[:-1]
409    checksum = (data_len + sum(struct.unpack("B" * len(body), body))) % 256
410    if result[-1] != struct.pack("B", checksum):
411      self.log( "Invalid checksum from serial port" )
412      return None
413    return result[:-1]
414
415  def _FlushInput(self):
416    """ Flush all read data until no more available. """
417    self.ser.flushInput()
418    flushed = 0
419    self.log("Flushing input...", debug = True)
420    while True:
421      ready_r, ready_w, ready_x = select.select([self.ser], [], [self.ser], 0)
422      if len(ready_x) > 0:
423        self.log( "exception from serial port" )
424        return None
425      elif len(ready_r) > 0:
426        flushed += 1
427        self.ser.read(1)  # This may cause underlying buffering.
428        self.ser.flush()  # Flush the underlying buffer too.
429      else:
430        break
431    if flushed > 0:
432      self.log( "flushed >%d bytes" % flushed, debug = True )
433
434