Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

devlib: add monsoon instrument #100

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions devlib/__init__.py
Expand Up @@ -14,6 +14,7 @@
from devlib.instrument.daq import DaqInstrument
from devlib.instrument.energy_probe import EnergyProbeInstrument
from devlib.instrument.hwmon import HwmonInstrument
from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument

from devlib.trace.ftrace import FtraceCollector
Expand Down
170 changes: 170 additions & 0 deletions devlib/instrument/monsoon.py
@@ -0,0 +1,170 @@
import devlib
import json
import os
import os.path
import psutil
import time
import logging
from subprocess import Popen, STDOUT
from threading import Timer
import pandas as pd
import StringIO
from collections import namedtuple
from subprocess import Popen, PIPE, STDOUT
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError
from devlib.utils.misc import which

class MonsoonInstrument(Instrument):
MONSOON_BIN = "./tools/scripts/monsoon.par"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tool/script is not part of the PR, can it be included?
If the license allows it, you should probably place it under:
devlib/bin/scripts

Otherwise, you can just use an additional mandatory monsoon_bin's __init__ parameter to specify its path.
Client code, e.g. LISA, can provide the path at creation time.


def log_error(self, errmsg, exception=0):
print "ERROR: " + errmsg
self.logger.error(errmsg)
if exception == 1:
raise(Exception(errmsg))

def log_info(self, msg):
print "INFO: " + msg
self.logger.info(msg)

def __init__(self, target, device_entry=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about s/device_entry/device/?
This _entry suffix seems not necessary...

super(MonsoonInstrument, self).__init__(target)

if not os.path.exists(MonsoonInstrument.MONSOON_BIN):
self.log_error("monsoon.par not found", exception=1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, if possible, we should report some tip on how to get this binary.
This is what we did in LISA for the ACME cape:
https://github.com/ARM-software/lisa/blob/master/libs/utils/energy.py#L344
@setrofim: can this work for devlib as well?


if device_entry != None and not os.path.exists(device_entry):
self.log_error(str(device_entry) + " doesn't exist", exception=1)

self.device_entry = device_entry
self.process = None

self.log_info('Found monsoon.par')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not report "it's working" type of comments, let's log only useful information and/or warnings/errors.

In devlib modules are "as much quite as possible". ;-)


def reset(self):
self.log_info('Initiailzing MonsoonInstrument')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some as above, not useful information to be reported here.

# Waiting for monsoon to get ready
self.run_monsoon(['--status'], timeout=3)
self.log_info("MonsoonInstrument ready for action")

def kill_monsoon(self, p):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the Monsoon device we can probably support (at least initially) only the CONTINUOUS interface, thus you need to implement only 4 public methods: reset, start, stop and get_data.

Here is an example:
https://github.com/ARM-software/devlib/blob/master/devlib/instrument/energy_probe.py#L35

try:
p.kill()
except OSError:
pass # ignore

# Args should be a list of arguments ex, ['--status']
# In !stream mode, we continously kill monsoon till it behaves (that is it doesn't block)
# This is for the initial --status command and other commands where we're not streaming
# but just running a simple monsoon command and returning.
# For the streaming case (where we are confident monsoon is working after passing it --status),
# we kill monsoon after timeout but this isn't an error, just termination of data collection.
def run_monsoon(self, args=None, stream=0, async_mode=0, timeout=5, report=0):
if report == 0 and args == None:
raise(Exception("Can't run monsoon without args"))

if report == 0:
popen_args = [MonsoonInstrument.MONSOON_BIN]
if self.device_entry:
popen_args += ['--device', self.device_entry]
popen_args += args

# Give up after 20 trials for !stream case (every attempt is timeout seconds)
n_trys_max = 20
n_trys = n_trys_max

while n_trys > 0:
# When reporting, don't try to run monsoon
if report == 0:
msg = "Running monsoon command: " + str(popen_args)
self.log_info(msg)
p = Popen(popen_args, stdout=PIPE, stderr=PIPE)

if async_mode == 1:
self.process = p
return "ASYNC"

if report == 1:
stream = 1
p = self.process
if p == None:
self.log_error("Monsoon not running")
p.kill()
else:
# In sync mode (async_mode = 0), we kill monsoon after sometime
t = Timer(timeout, self.kill_monsoon, [p])
t.start()

# Get return code for task after either it quit or it was killed
waitret = p.wait()
if waitret != 0:
if report == 0:
t.cancel()

# If we received an error and we're !stream, do a retry
if stream == 0:
n_trys = n_trys - 1
errmsg="timed out waiting or error for monsoon, try resetting it"
self.log_error(errmsg)

errmsg = p.stderr.read()
stderrmsg = ""
for l in errmsg.split(os.linesep):
if l == "":
continue
stderrmsg += "STDERR: " + l + os.linesep

# Always print error buffer
self.log_error(stderrmsg)

if stream == 1:
# if stream, we don't consider -9 as an error, its just test completion timeout
# if we do return for any other reason, raise an exception to catch it later
if waitret != -9:
self.log_error(stderrmsg, exception=1)
self.log_info("MonsoonInstrument stream completed, returning")
return p.stdout.read()

if n_trys == 0:
errmsg = "MonsoonInstrument maximum retries exceeded, please POWER CYCLE your monsoon."
self.log_error(errmsg, exception=1)

errmsg = ("Retrying attempt " + str(n_trys_max - n_trys)) + ", last output:\n" + p.stdout.read() + "\n"
self.log_error(errmsg)
else:
if report == 0:
t.cancel()
self.log_info("MonsoonInstrument commands completed, returning")
return p.stdout.read()
self.log_error("monsoon unreachable code path", exception=1)

def get_status(self):
return self.run_monsoon(['--status'], timeout=3)

def set_voltage(self, volt):
if (volt > 5):
self.log_error("Too high voltage requested: " + str(volt))
return self.run_monsoon(['--voltage', str(volt)])

def set_current(self, current):
return self.run_monsoon(['--current', str(current)])

def set_start_current(self, current):
return self.run_monsoon(['--startcurrent', str(current)])

# Returns pandas dataframe for monsoon output (time and current in mA)
def get_samples_sync(self, duration):
txt = self.run_monsoon(['--hz', '5', '--timestamp', '--samples', '2000000'], stream=1, timeout=duration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These params should probably be exported somehow. Perhaps, since we have a single channel, we can specify them in the __init__?

In that case you can use them to configure the single InstrumentChannel which is going to be registered, as part of the instrument specific attrs:
https://github.com/ARM-software/devlib/blob/master/devlib/instrument/__init__.py#L142

so = StringIO.StringIO(txt)
return pd.read_csv(so, sep=' ', names=['time', 'current'])

# Async stuff
def start(self):
self.run_monsoon(['--hz', '5', '--timestamp', '--samples', '2000000'], async_mode=1)

def report(self):
txt = self.run_monsoon(self, report=1)
so = StringIO.StringIO(txt)
return pd.read_csv(so, sep=' ', names=['time', 'current'])