mirror of
https://github.com/ARM-software/devlib.git
synced 2025-01-30 17:50:46 +00:00
Instrument/BaylibreAcme: Add IIO-based ACME instr.
Add BaylibreAcmeInstrument, a new instrument providing support for the Baylibre ACME board as a wrapper for the IIO interface. This class provides better access to the ACME hardware (e.g. the ability to control the sampling frequency) and to the retrieved samples than what the other instrument, AcmeCapeInstrument, provides. Furthermore, it removes an unnecessary and limiting dependency by interfacing directly with the IIO drivers instead of relying on an intermediate script ("iio-capture") potentially introducing unexpected bugs. Finally, it allows handling multiple probes (the ACME can have up to 8) through an easy-to-use single instance of this class instead of having to have an instance of AcmeCapeInstrument per channel potentially (untested) leading to race conditions between the underlying scripts for accessing the hardware. This commit does not overwrite AcmeCapeInstrument as BaylibreAcmeInstrument does not provide interface compatibility with that class. Anyhow, we believe that anything that can be achieved with AcmeCapeInstrument can be done with BaylibreAcmeInstrument (the reciprocal is not true) so that BaylibreAcmeInstrument might eventually replace AcmeCapeInstrument. Add BaylibreAcmeInstrument documentation detailing the class interface and the ACME instrument itself and discussing the way it works and its potential limitations.
This commit is contained in:
parent
30dc161f12
commit
63d2fb53fc
@ -34,6 +34,12 @@ from devlib.instrument.hwmon import HwmonInstrument
|
||||
from devlib.instrument.monsoon import MonsoonInstrument
|
||||
from devlib.instrument.netstats import NetstatsInstrument
|
||||
from devlib.instrument.gem5power import Gem5PowerInstrument
|
||||
from devlib.instrument.baylibre_acme import (
|
||||
BaylibreAcmeNetworkInstrument,
|
||||
BaylibreAcmeXMLInstrument,
|
||||
BaylibreAcmeLocalInstrument,
|
||||
BaylibreAcmeInstrument,
|
||||
)
|
||||
|
||||
from devlib.derived import DerivedMeasurements, DerivedMetric
|
||||
from devlib.derived.energy import DerivedEnergyMeasurements
|
||||
|
557
devlib/instrument/baylibre_acme.py
Normal file
557
devlib/instrument/baylibre_acme.py
Normal file
@ -0,0 +1,557 @@
|
||||
#pylint: disable=attribute-defined-outside-init
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import re
|
||||
import threading
|
||||
|
||||
from past.builtins import basestring
|
||||
|
||||
try:
|
||||
import iio
|
||||
except ImportError as e:
|
||||
iio_import_failed = True
|
||||
iio_import_error = e
|
||||
else:
|
||||
iio_import_failed = False
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from devlib import CONTINUOUS, Instrument, HostError, MeasurementsCsv, TargetError
|
||||
from devlib.utils.ssh import SshConnection
|
||||
|
||||
class IIOINA226Channel(object):
|
||||
|
||||
def __init__(self, iio_channel):
|
||||
|
||||
channel_id = iio_channel.id
|
||||
channel_type = iio_channel.attrs['type'].value
|
||||
|
||||
re_measure = r'(?P<measure>\w+)(?P<index>\d*)$'
|
||||
re_dtype = r'le:(?P<sign>\w)(?P<width>\d+)/(?P<size>\d+)>>(?P<align>\d+)'
|
||||
|
||||
match_measure = re.search(re_measure, channel_id)
|
||||
match_dtype = re.search(re_dtype, channel_type)
|
||||
|
||||
if not match_measure:
|
||||
msg = "IIO channel ID '{}' does not match expected RE '{}'"
|
||||
raise ValueError(msg.format(channel_id, re_measure))
|
||||
|
||||
if not match_dtype:
|
||||
msg = "'IIO channel type '{}' does not match expected RE '{}'"
|
||||
raise ValueError(msg.format(channel_type, re_dtype))
|
||||
|
||||
self.measure = match_measure.group('measure')
|
||||
self.iio_dtype = 'int{}'.format(match_dtype.group('width'))
|
||||
self.iio_channel = iio_channel
|
||||
# Data is reported in amps, volts, watts and microseconds:
|
||||
self.iio_scale = (1. if 'scale' not in iio_channel.attrs
|
||||
else float(iio_channel.attrs['scale'].value))
|
||||
self.iio_scale /= 1000
|
||||
# As calls to iio_store_buffer will be blocking and probably coming
|
||||
# from a loop retrieving samples from the ACME, we want to provide
|
||||
# consistency in processing timing between iterations i.e. we want
|
||||
# iio_store_buffer to be o(1) for every call (can't have that with []):
|
||||
self.sample_buffers = collections.deque()
|
||||
|
||||
def iio_store_buffer_samples(self, iio_buffer):
|
||||
# IIO buffers receive and store their data as an interlaced array of
|
||||
# samples from all the IIO channels of the IIO device. The IIO library
|
||||
# provides a reliable function to extract the samples (bytes, actually)
|
||||
# corresponding to a channel from the received buffer; in Python, it is
|
||||
# iio.Channel.read(iio.Buffer).
|
||||
#
|
||||
# NB: As this is called in a potentially tightly timed loop, we do as
|
||||
# little work as possible:
|
||||
self.sample_buffers.append(self.iio_channel.read(iio_buffer))
|
||||
|
||||
def iio_get_samples(self, absolute_timestamps=False):
|
||||
# Up to this point, the data is not interpreted yet i.e. these are
|
||||
# bytearrays. Hence the use of np.dtypes.
|
||||
buffers = [np.frombuffer(b, dtype=self.iio_dtype)
|
||||
for b in self.sample_buffers]
|
||||
|
||||
must_shift = (self.measure == 'timestamp' and not absolute_timestamps)
|
||||
samples = np.concatenate(buffers)
|
||||
return (samples - samples[0] if must_shift else samples) * self.iio_scale
|
||||
|
||||
def iio_forget_samples(self):
|
||||
self.sample_buffers.clear()
|
||||
|
||||
|
||||
# Decorators for the attributes of IIOINA226Instrument:
|
||||
|
||||
def only_set_to(valid_values, dynamic=False):
|
||||
def validating_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, value):
|
||||
values = (valid_values if not dynamic
|
||||
else getattr(self, valid_values))
|
||||
if value not in values:
|
||||
msg = '{} is invalid; expected values are {}'
|
||||
raise ValueError(msg.format(value, valid_values))
|
||||
return func(self, value)
|
||||
return wrapper
|
||||
return validating_wrapper
|
||||
|
||||
|
||||
def with_input_as(wanted_type):
|
||||
def typecasting_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, value):
|
||||
return func(self, wanted_type(value))
|
||||
return wrapper
|
||||
return typecasting_wrapper
|
||||
|
||||
|
||||
def _IIODeviceAttr(attr_name, attr_type, writable=False, dyn_vals=None, stat_vals=None):
|
||||
|
||||
def getter(self):
|
||||
return attr_type(self.iio_device.attrs[attr_name].value)
|
||||
|
||||
def setter(self, value):
|
||||
self.iio_device.attrs[attr_name].value = str(attr_type(value))
|
||||
|
||||
if writable and (dyn_vals or stat_vals):
|
||||
vals, dyn = dyn_vals or stat_vals, dyn_vals is not None
|
||||
setter = with_input_as(attr_type)(only_set_to(vals, dyn)(setter))
|
||||
|
||||
return property(getter, setter if writable else None)
|
||||
|
||||
|
||||
def _IIOChannelIntTime(chan_name):
|
||||
|
||||
attr_name, attr_type = 'integration_time', float
|
||||
|
||||
def getter(self):
|
||||
ch = self.iio_device.find_channel(chan_name)
|
||||
return attr_type(ch.attrs[attr_name].value)
|
||||
|
||||
@only_set_to('INTEGRATION_TIMES_AVAILABLE', dynamic=True)
|
||||
@with_input_as(attr_type)
|
||||
def setter(self, value):
|
||||
ch = self.iio_device.find_channel(chan_name)
|
||||
ch.attrs[attr_name].value = str(value)
|
||||
|
||||
return property(getter, setter)
|
||||
|
||||
|
||||
def _setify(x):
|
||||
return {x} if isinstance(x, basestring) else set(x) #Py3: basestring->str
|
||||
|
||||
|
||||
class IIOINA226Instrument(object):
|
||||
|
||||
IIO_DEVICE_NAME = 'ina226'
|
||||
|
||||
def __init__(self, iio_device):
|
||||
|
||||
if iio_device.name != self.IIO_DEVICE_NAME:
|
||||
msg = 'IIO device is {}; expected {}'
|
||||
raise TargetError(msg.format(iio_device.name, self.IIO_DEVICE_NAME))
|
||||
|
||||
self.iio_device = iio_device
|
||||
self.absolute_timestamps = False
|
||||
self.high_resolution = True
|
||||
self.buffer_samples_count = None
|
||||
self.buffer_is_circular = False
|
||||
|
||||
self.collector = None
|
||||
self.work_done = threading.Event()
|
||||
self.collector_exception = None
|
||||
|
||||
self.data = collections.OrderedDict()
|
||||
|
||||
channels = {
|
||||
'timestamp': 'timestamp',
|
||||
'shunt' : 'voltage0',
|
||||
'voltage' : 'voltage1', # bus
|
||||
'power' : 'power2',
|
||||
'current' : 'current3',
|
||||
}
|
||||
self.computable_channels = {'current' : {'shunt'},
|
||||
'power' : {'shunt', 'voltage'}}
|
||||
self.uncomputable_channels = set(channels) - set(self.computable_channels)
|
||||
self.channels = {k: IIOINA226Channel(self.iio_device.find_channel(v))
|
||||
for k, v in channels.items()}
|
||||
# We distinguish between "output" channels (as seen by the user of this
|
||||
# class) and "hardware" channels (as requested from the INA226).
|
||||
# This is necessary because of the 'high_resolution' feature which
|
||||
# requires outputting computed channels:
|
||||
self.active_channels = set() # "hardware" channels
|
||||
self.wanted_channels = set() # "output" channels
|
||||
|
||||
|
||||
# Properties
|
||||
|
||||
OVERSAMPLING_RATIOS_AVAILABLE = (1, 4, 16, 64, 128, 256, 512, 1024)
|
||||
INTEGRATION_TIMES_AVAILABLE = _IIODeviceAttr('integration_time_available',
|
||||
lambda x: tuple(map(float, x.split())))
|
||||
|
||||
sample_rate_hz = _IIODeviceAttr('in_sampling_frequency', int)
|
||||
shunt_resistor = _IIODeviceAttr('in_shunt_resistor' , int, True)
|
||||
oversampling_ratio = _IIODeviceAttr('in_oversampling_ratio', int, True,
|
||||
dyn_vals='OVERSAMPLING_RATIOS_AVAILABLE')
|
||||
|
||||
integration_time_shunt = _IIOChannelIntTime('voltage0')
|
||||
integration_time_bus = _IIOChannelIntTime('voltage1')
|
||||
|
||||
def list_channels(self):
|
||||
return self.channels.keys()
|
||||
|
||||
def activate(self, channels=None):
|
||||
all_channels = set(self.channels)
|
||||
requested_channels = (all_channels if channels is None
|
||||
else _setify(channels))
|
||||
|
||||
unknown = ', '.join(requested_channels - all_channels)
|
||||
if unknown:
|
||||
raise ValueError('Unknown channel(s): {}'.format(unknown))
|
||||
|
||||
self.wanted_channels |= requested_channels
|
||||
|
||||
def deactivate(self, channels=None):
|
||||
unwanted_channels = (self.wanted_channels if channels is None
|
||||
else _setify(channels))
|
||||
|
||||
unknown = ', '.join(unwanted_channels - set(self.channels))
|
||||
if unknown:
|
||||
raise ValueError('Unknown channel(s): {}'.format(unknown))
|
||||
|
||||
unactive = ', '.join(unwanted_channels - self.wanted_channels)
|
||||
if unactive:
|
||||
raise ValueError('Already unactive channel(s): {}'.format(unactive))
|
||||
|
||||
self.wanted_channels -= unwanted_channels
|
||||
|
||||
def sample_collector(self):
|
||||
class Collector(threading.Thread):
|
||||
def run(collector_self):
|
||||
for name, ch in self.channels.items():
|
||||
ch.iio_channel.enabled = (name in self.active_channels)
|
||||
|
||||
samples_count = self.buffer_samples_count or self.sample_rate_hz
|
||||
|
||||
iio_buffer = iio.Buffer(self.iio_device, samples_count,
|
||||
self.buffer_is_circular)
|
||||
# NB: This buffer creates a communication pipe to the
|
||||
# BeagleBone (or is it between the BBB and the ACME?)
|
||||
# that locks down any configuration. The IIO drivers
|
||||
# do not limit access when a buffer exists so that
|
||||
# configuring the INA226 (i.e. accessing iio.Device.attrs
|
||||
# or iio.Channel.attrs from iio.Device.channels i.e.
|
||||
# assigning to or reading from any property of this class
|
||||
# or calling its setup or reset methods) will screw up the
|
||||
# whole system and will require rebooting the BBB-ACME board!
|
||||
|
||||
self.collector_exception = None
|
||||
try:
|
||||
refilled_once = False
|
||||
while not (refilled_once and self.work_done.is_set()):
|
||||
refilled_once = True
|
||||
iio_buffer.refill()
|
||||
for name in self.active_channels:
|
||||
self.channels[name].iio_store_buffer_samples(iio_buffer)
|
||||
except Exception as e:
|
||||
self.collector_exception = e
|
||||
finally:
|
||||
del iio_buffer
|
||||
for ch in self.channels.values():
|
||||
ch.enabled = False
|
||||
|
||||
return Collector()
|
||||
|
||||
def start_capturing(self):
|
||||
if not self.wanted_channels:
|
||||
raise TargetError('No active channel: aborting.')
|
||||
|
||||
self.active_channels = self.wanted_channels.copy()
|
||||
if self.high_resolution:
|
||||
self.active_channels &= self.uncomputable_channels
|
||||
for channel, dependencies in self.computable_channels.items():
|
||||
if channel in self.wanted_channels:
|
||||
self.active_channels |= dependencies
|
||||
|
||||
self.work_done.clear()
|
||||
self.collector = self.sample_collector()
|
||||
self.collector.daemon = True
|
||||
self.collector.start()
|
||||
|
||||
def stop_capturing(self):
|
||||
self.work_done.set()
|
||||
self.collector.join()
|
||||
|
||||
if self.collector_exception:
|
||||
raise self.collector_exception
|
||||
|
||||
self.data.clear()
|
||||
for channel in self.active_channels:
|
||||
ch = self.channels[channel]
|
||||
self.data[channel] = ch.iio_get_samples(self.absolute_timestamps)
|
||||
ch.iio_forget_samples()
|
||||
|
||||
if self.high_resolution:
|
||||
res_ohm = 1e-6 * self.shunt_resistor
|
||||
current = self.data['shunt'] / res_ohm
|
||||
if 'current' in self.wanted_channels:
|
||||
self.data['current'] = current
|
||||
if 'power' in self.wanted_channels:
|
||||
self.data['power'] = current * self.data['voltage']
|
||||
for channel in set(self.data) - self.wanted_channels:
|
||||
del self.data[channel]
|
||||
|
||||
self.active_channels.clear()
|
||||
|
||||
def get_data(self):
|
||||
return self.data
|
||||
|
||||
|
||||
class BaylibreAcmeInstrument(Instrument):
|
||||
|
||||
mode = CONTINUOUS
|
||||
|
||||
MINIMAL_ACME_SD_IMAGE_VERSION = (2, 1, 3)
|
||||
MINIMAL_ACME_IIO_DRIVERS_VERSION = (0, 6)
|
||||
MINIMAL_HOST_IIO_DRIVERS_VERSION = (0, 15)
|
||||
|
||||
def __init__(self, target=None, iio_context=None,
|
||||
use_base_iio_context=False, probe_names=None):
|
||||
|
||||
if iio_import_failed:
|
||||
raise HostError('Could not import "iio": {}'.format(iio_import_error))
|
||||
|
||||
super(BaylibreAcmeInstrument, self).__init__(target)
|
||||
|
||||
if isinstance(probe_names, basestring):
|
||||
probe_names = [probe_names]
|
||||
|
||||
self.iio_context = (iio_context if not use_base_iio_context
|
||||
else iio.Context(iio_context))
|
||||
|
||||
self.check_version()
|
||||
|
||||
if probe_names is not None:
|
||||
if len(probe_names) != len(set(probe_names)):
|
||||
msg = 'Probe names should be unique: {}'
|
||||
raise ValueError(msg.format(probe_names))
|
||||
|
||||
if len(probe_names) != len(self.iio_context.devices):
|
||||
msg = ('There should be as many probe_names ({}) '
|
||||
'as detected probes ({}).')
|
||||
raise ValueError(msg.format(len(probe_names),
|
||||
len(self.iio_context.devices)))
|
||||
|
||||
probes = [IIOINA226Instrument(d) for d in self.iio_context.devices]
|
||||
|
||||
self.probes = (dict(zip(probe_names, probes)) if probe_names
|
||||
else {p.iio_device.id : p for p in probes})
|
||||
self.active_probes = set()
|
||||
|
||||
for probe in self.probes:
|
||||
for measure in ['voltage', 'power', 'current']:
|
||||
self.add_channel(site=probe, measure=measure)
|
||||
self.add_channel('timestamp', 'time_us')
|
||||
|
||||
self.data = pd.DataFrame()
|
||||
|
||||
def check_version(self):
|
||||
msg = ('The IIO drivers running on {} ({}) are out-of-date; '
|
||||
'devlib requires {} or later.')
|
||||
|
||||
if iio.version[:2] < self.MINIMAL_HOST_IIO_DRIVERS_VERSION:
|
||||
ver_str = '.'.join(map(str, iio.version[:2]))
|
||||
min_str = '.'.join(map(str, self.MINIMAL_HOST_IIO_DRIVERS_VERSION))
|
||||
raise HostError(msg.format('this host', ver_str, min_str))
|
||||
|
||||
if self.version[:2] < self.MINIMAL_ACME_IIO_DRIVERS_VERSION:
|
||||
ver_str = '.'.join(map(str, self.version[:2]))
|
||||
min_str = '.'.join(map(str, self.MINIMAL_ACME_IIO_DRIVERS_VERSION))
|
||||
raise TargetError(msg.format('the BBB', ver_str, min_str))
|
||||
|
||||
# properties
|
||||
|
||||
def probes_unique_property(self, property_name):
|
||||
probes = self.active_probes or self.probes
|
||||
try:
|
||||
# This will fail if there is not exactly one single value:
|
||||
(value,) = {getattr(self.probes[p], property_name) for p in probes}
|
||||
except ValueError:
|
||||
msg = 'Probes have different values for {}.'
|
||||
raise ValueError(msg.format(property_name) if probes else 'No probe')
|
||||
return value
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self.iio_context.version
|
||||
|
||||
@property
|
||||
def OVERSAMPLING_RATIOS_AVAILABLE(self):
|
||||
return self.probes_unique_property('OVERSAMPLING_RATIOS_AVAILABLE')
|
||||
|
||||
@property
|
||||
def INTEGRATION_TIMES_AVAILABLE(self):
|
||||
return self.probes_unique_property('INTEGRATION_TIMES_AVAILABLE')
|
||||
|
||||
@property
|
||||
def sample_rate_hz(self):
|
||||
return self.probes_unique_property('sample_rate_hz')
|
||||
|
||||
@sample_rate_hz.setter
|
||||
# This setter is required for compliance with the inherited methods
|
||||
def sample_rate_hz(self, value):
|
||||
if value is not None:
|
||||
raise AttributeError("can't set attribute")
|
||||
|
||||
# initialization and teardown
|
||||
|
||||
def setup(self, shunt_resistor,
|
||||
integration_time_bus,
|
||||
integration_time_shunt,
|
||||
oversampling_ratio,
|
||||
buffer_samples_count=None,
|
||||
buffer_is_circular=False,
|
||||
absolute_timestamps=False,
|
||||
high_resolution=True):
|
||||
|
||||
def pseudo_list(v, i):
|
||||
try:
|
||||
return v[i]
|
||||
except TypeError:
|
||||
return v
|
||||
|
||||
for i, p in enumerate(self.probes.values()):
|
||||
for attr, val in locals().items():
|
||||
if attr != 'self':
|
||||
setattr(p, attr, pseudo_list(val, i))
|
||||
|
||||
self.absolute_timestamps = all(pseudo_list(absolute_timestamps, i)
|
||||
for i in range(len(self.probes)))
|
||||
|
||||
def reset(self, sites=None, kinds=None, channels=None):
|
||||
|
||||
# populate self.active_channels:
|
||||
super(BaylibreAcmeInstrument, self).reset(sites, kinds, channels)
|
||||
|
||||
for ch in self.active_channels:
|
||||
if ch.site != 'timestamp':
|
||||
self.probes[ch.site].activate(['timestamp', ch.kind])
|
||||
self.active_probes.add(ch.site)
|
||||
|
||||
def teardown(self):
|
||||
del self.active_channels[:]
|
||||
self.active_probes.clear()
|
||||
|
||||
def start(self):
|
||||
for p in self.active_probes:
|
||||
self.probes[p].start_capturing()
|
||||
|
||||
def stop(self):
|
||||
for p in self.active_probes:
|
||||
self.probes[p].stop_capturing()
|
||||
|
||||
max_rate_probe = max(self.active_probes,
|
||||
key=lambda p: self.probes[p].sample_rate_hz)
|
||||
|
||||
probes_dataframes = {
|
||||
probe: pd.DataFrame.from_dict(self.probes[probe].get_data())
|
||||
.set_index('timestamp')
|
||||
for probe in self.active_probes
|
||||
}
|
||||
|
||||
for df in probes_dataframes.values():
|
||||
df.set_index(pd.to_datetime(df.index, unit='us'), inplace=True)
|
||||
|
||||
final_index = probes_dataframes[max_rate_probe].index
|
||||
|
||||
df = pd.concat(probes_dataframes, axis=1).sort_index()
|
||||
df.columns = ['_'.join(c).strip() for c in df.columns.values]
|
||||
|
||||
self.data = df.interpolate('time').reindex(final_index)
|
||||
|
||||
if not self.absolute_timestamps:
|
||||
epoch_index = self.data.index.astype(np.int64) // 1000
|
||||
self.data.set_index(epoch_index, inplace=True)
|
||||
# self.data.index is in [us]
|
||||
# columns are in volts, amps and watts
|
||||
|
||||
def get_data(self, outfile=None, **to_csv_kwargs):
|
||||
if outfile is None:
|
||||
return self.data
|
||||
|
||||
self.data.to_csv(outfile, **to_csv_kwargs)
|
||||
return MeasurementsCsv(outfile, self.active_channels)
|
||||
|
||||
class BaylibreAcmeLocalInstrument(BaylibreAcmeInstrument):
|
||||
|
||||
def __init__(self, target=None, probe_names=None):
|
||||
|
||||
if iio_import_failed:
|
||||
raise HostError('Could not import "iio": {}'.format(iio_import_error))
|
||||
|
||||
super(BaylibreAcmeLocalInstrument, self).__init__(
|
||||
target=target,
|
||||
iio_context=iio.LocalContext(),
|
||||
probe_names=probe_names
|
||||
)
|
||||
|
||||
class BaylibreAcmeXMLInstrument(BaylibreAcmeInstrument):
|
||||
|
||||
def __init__(self, target=None, xmlfile=None, probe_names=None):
|
||||
|
||||
if iio_import_failed:
|
||||
raise HostError('Could not import "iio": {}'.format(iio_import_error))
|
||||
|
||||
super(BaylibreAcmeXMLInstrument, self).__init__(
|
||||
target=target,
|
||||
iio_context=iio.XMLContext(xmlfile),
|
||||
probe_names=probe_names
|
||||
)
|
||||
|
||||
class BaylibreAcmeNetworkInstrument(BaylibreAcmeInstrument):
|
||||
|
||||
def __init__(self, target=None, hostname=None, probe_names=None):
|
||||
|
||||
if iio_import_failed:
|
||||
raise HostError('Could not import "iio": {}'.format(iio_import_error))
|
||||
|
||||
super(BaylibreAcmeNetworkInstrument, self).__init__(
|
||||
target=target,
|
||||
iio_context=iio.NetworkContext(hostname),
|
||||
probe_names=probe_names
|
||||
)
|
||||
|
||||
try:
|
||||
self.ssh_connection = SshConnection(hostname, username='root', password=None)
|
||||
except TargetError as e:
|
||||
msg = 'No SSH connexion could be established to {}: {}'
|
||||
self.logger.debug(msg.format(hostname, e))
|
||||
self.ssh_connection = None
|
||||
|
||||
def check_version(self):
|
||||
super(BaylibreAcmeNetworkInstrument, self).check_version()
|
||||
|
||||
cmd = r"""sed -nr 's/^VERSION_ID="(.+)"$/\1/p' < /etc/os-release"""
|
||||
try:
|
||||
ver_str = self._ssh(cmd).rstrip()
|
||||
ver = tuple(map(int, ver_str.split('.')))
|
||||
except Exception as e:
|
||||
self.logger.debug('Unable to verify ACME SD image version through SSH: {}'.format(e))
|
||||
else:
|
||||
if ver < self.MINIMAL_ACME_SD_IMAGE_VERSION:
|
||||
min_str = '.'.join(map(str, self.MINIMAL_ACME_SD_IMAGE_VERSION))
|
||||
msg = ('The ACME SD image for the BBB (ver. {}) is out-of-date; '
|
||||
'devlib requires {} or later.')
|
||||
raise TargetError(msg.format(ver_str, min_str))
|
||||
|
||||
def _ssh(self, cmd=''):
|
||||
"""Connections are assumed to be rare."""
|
||||
if self.ssh_connection is None:
|
||||
raise TargetError('No SSH connection; see log.')
|
||||
return self.ssh_connection.execute(cmd)
|
||||
|
||||
def _reboot(self):
|
||||
"""Always delete the object after calling its _reboot method"""
|
||||
try:
|
||||
self._ssh('reboot')
|
||||
except:
|
||||
pass
|
@ -31,6 +31,9 @@ import shlex
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.mathjax',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
@ -104,7 +107,7 @@ pygments_style = 'sphinx'
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
BIN
doc/images/instrumentation/baylibre_acme/bottleneck.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/bottleneck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
doc/images/instrumentation/baylibre_acme/buffer.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/buffer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
BIN
doc/images/instrumentation/baylibre_acme/cape.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/cape.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
doc/images/instrumentation/baylibre_acme/ina226_circuit.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/ina226_circuit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
BIN
doc/images/instrumentation/baylibre_acme/ina226_functional.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/ina226_functional.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
doc/images/instrumentation/baylibre_acme/int_time.png
Normal file
BIN
doc/images/instrumentation/baylibre_acme/int_time.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
@ -13,7 +13,7 @@ Example
|
||||
The following example shows how to use an instrument to read temperature from an
|
||||
Android target.
|
||||
|
||||
.. code-block:: ipython
|
||||
.. code-block:: python
|
||||
|
||||
# import and instantiate the Target and the instrument
|
||||
# (note: this assumes exactly one android target connected
|
||||
@ -51,7 +51,7 @@ API
|
||||
Instrument
|
||||
~~~~~~~~~~
|
||||
|
||||
.. class:: Instrument(target, **kwargs)
|
||||
.. class:: Instrument(target, \*\*kwargs)
|
||||
|
||||
An ``Instrument`` allows collection of measurement from one or more
|
||||
channels. An ``Instrument`` may support ``INSTANTANEOUS`` or ``CONTINUOUS``
|
||||
@ -88,7 +88,7 @@ Instrument
|
||||
Returns channels for a particular ``measure`` type. A ``measure`` can be
|
||||
either a string (e.g. ``"power"``) or a :class:`MeasurmentType` instance.
|
||||
|
||||
.. method:: Instrument.setup(*args, **kwargs)
|
||||
.. method:: Instrument.setup(\*args, \*\*kwargs)
|
||||
|
||||
This will set up the instrument on the target. Parameters this method takes
|
||||
are particular to subclasses (see documentation for specific instruments
|
||||
@ -115,7 +115,7 @@ Instrument
|
||||
If none of ``sites``, ``kinds`` or ``channels`` are provided then all
|
||||
available channels are enabled.
|
||||
|
||||
.. method:: Instrument.take_measurment()
|
||||
.. method:: Instrument.take_measurement()
|
||||
|
||||
Take a single measurement from ``active_channels``. Returns a list of
|
||||
:class:`Measurement` objects (one for each active channel).
|
||||
@ -178,7 +178,7 @@ Instrument
|
||||
Instrument Channel
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: InstrumentChannel(name, site, measurement_type, **attrs)
|
||||
.. class:: InstrumentChannel(name, site, measurement_type, \*\*attrs)
|
||||
|
||||
An :class:`InstrumentChannel` describes a single type of measurement that may
|
||||
be collected by an :class:`Instrument`. A channel is primarily defined by a
|
||||
@ -228,9 +228,9 @@ Measurement Types
|
||||
|
||||
In order to make instruments easer to use, and to make it easier to swap them
|
||||
out when necessary (e.g. change method of collecting power), a number of
|
||||
standard measurement types are defined. This way, for example, power will always
|
||||
be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently
|
||||
defined measurement types are
|
||||
standard measurement types are defined. This way, for example, power will
|
||||
always be reported as "power" in Watts, and never as "pwr" in milliWatts.
|
||||
Currently defined measurement types are
|
||||
|
||||
|
||||
+-------------+-------------+---------------+
|
||||
@ -269,4 +269,644 @@ Available Instruments
|
||||
|
||||
This section lists instruments that are currently part of devlib.
|
||||
|
||||
TODO
|
||||
.. todo:: Add other instruments
|
||||
|
||||
|
||||
Baylibre ACME BeagleBone Black Cape
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _official project page: http://baylibre.com/acme/
|
||||
.. _image built for using the ACME: https://gitlab.com/baylibre-acme/ACME-Software-Release/blob/master/README.md
|
||||
.. _libiio (the Linux IIO interface): https://github.com/analogdevicesinc/libiio
|
||||
.. _Linux Industrial I/O Subsystem: https://wiki.analog.com/software/linux/docs/iio/iio
|
||||
.. _Texas Instruments INA226: http://www.ti.com/lit/ds/symlink/ina226.pdf
|
||||
|
||||
From the `official project page`_:
|
||||
|
||||
[The Baylibre Another Cute Measurement Equipment (ACME)] is an extension for
|
||||
the BeagleBone Black (the ACME Cape), designed to provide multi-channel power
|
||||
and temperature measurements capabilities to the BeagleBone Black (BBB). It
|
||||
comes with power and temperature probes integrating a power switch (the ACME
|
||||
Probes), turning it into an advanced all-in-one power/temperature measurement
|
||||
solution.
|
||||
|
||||
The ACME initiative is completely open source, from HW to SW drivers and
|
||||
applications.
|
||||
|
||||
|
||||
The Infrastructure
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieving measurement from the ACME through devlib requires:
|
||||
|
||||
- a BBB running the `image built for using the ACME`_ (micro SD card required);
|
||||
|
||||
- an ACME cape on top of the BBB;
|
||||
|
||||
- at least one ACME probe [#acme_probe_variants]_ connected to the ACME cape;
|
||||
|
||||
- a BBB-host interface (typically USB or Ethernet) [#acme_name_conflicts]_;
|
||||
|
||||
- a host (the one running devlib) with `libiio (the Linux IIO interface)`_
|
||||
installed, and a Python environment able to find the libiio Python wrapper
|
||||
*i.e.* able to ``import iio`` as communications between the BBB and the
|
||||
host rely on the `Linux Industrial I/O Subsystem`_ (IIO).
|
||||
|
||||
The ACME probes are built on top of the `Texas Instruments INA226`_ and the
|
||||
data acquisition chain is as follows:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph target {
|
||||
rankdir = LR
|
||||
bgcolor = transparent
|
||||
|
||||
subgraph cluster_target {
|
||||
|
||||
subgraph cluster_BBB {
|
||||
node [style = filled, color = white];
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
label = "BeagleBone Black";
|
||||
|
||||
drivers -> "IIO Daemon" [dir = both]
|
||||
}
|
||||
|
||||
subgraph cluster_INA226 {
|
||||
node [style = filled, color = white];
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
label = INA226;
|
||||
|
||||
ADC -> Processing
|
||||
Processing -> Registers
|
||||
}
|
||||
|
||||
subgraph cluster_inputs {
|
||||
node [style = filled, color = white];
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
label = Inputs;
|
||||
|
||||
"Bus Voltage" -> ADC;
|
||||
"Shunt Voltage" -> ADC;
|
||||
}
|
||||
|
||||
Registers -> drivers [dir = both, label = I2C];
|
||||
}
|
||||
|
||||
subgraph cluster_IIO {
|
||||
style = none
|
||||
"IIO Daemon" -> "IIO Interface" [dir = both, label = "Eth./USB"]
|
||||
}
|
||||
}
|
||||
|
||||
For reference, the software stack on the host is roughly given by:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph host {
|
||||
rankdir = LR
|
||||
bgcolor = transparent
|
||||
|
||||
subgraph cluster_host {
|
||||
|
||||
subgraph cluster_backend {
|
||||
node [style = filled, color = white];
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
label = Backend;
|
||||
|
||||
"IIO Daemon" -> "C API" [dir = both]
|
||||
}
|
||||
|
||||
subgraph cluster_Python {
|
||||
node [style = filled, color = white];
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
label = Python;
|
||||
|
||||
"C API" -> "iio Wrapper" [dir = both]
|
||||
"iio Wrapper" -> devlib [dir = both]
|
||||
devlib -> "User" [dir = both]
|
||||
}
|
||||
}
|
||||
|
||||
subgraph cluster_IIO {
|
||||
style = none
|
||||
"IIO Interface" -> "IIO Daemon" [dir = both, label = "Eth./USB"]
|
||||
}
|
||||
}
|
||||
|
||||
Ethernet was the only IIO Interface used and tested during the development of
|
||||
this instrument. However,
|
||||
`USB seems to be supported<https://gitlab.com/baylibre-acme/ACME/issues/2>`_.
|
||||
The IIO library also provides "Local" and "XML" connections but these are to be
|
||||
used when the IIO devices are directly connected to the host *i.e.* in our
|
||||
case, if we were to run Python and devlib on the BBB. These are also untested.
|
||||
|
||||
Measuring Power
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
In IIO terminology, the ACME cape is an *IIO context* and ACME probes are *IIO
|
||||
devices* with *IIO channels*. An input *IIO channel* (the ACME has no *output
|
||||
IIO channel*) is a stream of samples and an ACME cape can be connected to up to
|
||||
8 probes *i.e.* have 8 *IIO devices*. The probes are discovered at startup by
|
||||
the IIO drivers on the BBB and are indexed according to the order in which they
|
||||
are connected to the ACME cape (with respect to the "Probe *X*" connectors on
|
||||
the cape).
|
||||
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/cape.png
|
||||
:width: 50%
|
||||
:alt: ACME Cape
|
||||
:align: center
|
||||
|
||||
ACME Cape on top of a BBB: Notice the numbered probe connectors (
|
||||
`source <https://baylibre.com/wp-content/uploads/2015/11/20150916_BayLibre_ACME_RevB-010-1030x599.png>`_)
|
||||
|
||||
|
||||
Please note that the numbers on the PCB do not represent the index of a probe
|
||||
in IIO; on top of being 1-based (as opposed to IIO device indexing being
|
||||
0-based), skipped connectors do not result in skipped indices *e.g.* if three
|
||||
probes are connected to the cape at ``Probe 1``, ``Probe 3`` and ``Probe 7``,
|
||||
IIO (and therefore the entire software stack, including devlib) will still
|
||||
refer to them as devices ``0``, ``1`` and ``2``, respectively. Furthermore,
|
||||
probe "hot swapping" does not seem to be supported.
|
||||
|
||||
INA226: The probing spearhead
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
An ACME probe has 5 *IIO channels*, 4 of which being "IIO wrappers" around what
|
||||
the INA226 outputs (through its I2C registers): the bus voltage, the shunt
|
||||
voltage, the shunt current and the load power. The last channel gives the
|
||||
timestamps and is probably added further down the pipeline. A typical circuit
|
||||
configuration for the INA226 (useful when shunt-based ACME probes are used as
|
||||
their PCB does not contain the full circuit unlike the USB and jack variants)
|
||||
is given by its datasheet:
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/ina226_circuit.png
|
||||
:width: 90%
|
||||
:alt: Typical circuit configuration, INA226
|
||||
:align: center
|
||||
|
||||
Typical Circuit Configuration (source: `Texas Instruments INA226`_)
|
||||
|
||||
|
||||
The analog-to-digital converter (ADC)
|
||||
'''''''''''''''''''''''''''''''''''''
|
||||
|
||||
The digital time-discrete sampled signal of the analog time-continuous input
|
||||
voltage signal is obtained through an analog-to-digital converter (ADC). To
|
||||
measure the "instantaneous input voltage", the ADC "charges up or down" a
|
||||
capacitor before measuring its charge.
|
||||
|
||||
The *integration time* is the time spend by the ADC acquiring the input signal
|
||||
in its capacitor. The longer this time is, the more resilient the sampling
|
||||
process is to unwanted noise. The drawback is that, if the integration time is
|
||||
increased then the sampling rate decreases. This effect can be somewhat
|
||||
compared to a *low-pass filter*.
|
||||
|
||||
As the INA226 alternatively connects its ADC to the bus voltage and shunt
|
||||
voltage (see previous figure), samples are retrieved at a frequency of
|
||||
|
||||
.. math::
|
||||
\frac{1}{T_{bus} + T_{shunt}}
|
||||
|
||||
where :math:`T_X` is the integration time for the :math:`X` voltage.
|
||||
|
||||
As described below (:meth:`BaylibreAcmeInstrument.reset`), the integration
|
||||
times for the bus and shunt voltage can be set separately which allows a
|
||||
tradeoff of accuracy between signals. This is particularly useful as the shunt
|
||||
voltage returned by the INA226 has a higher resolution than the bus voltage
|
||||
(2.5 μV and 1.25 mV LSB, respectively) and therefore would benefit more from a
|
||||
longer integration time.
|
||||
|
||||
As an illustration, consider the following sampled sine wave and notice how
|
||||
increasing the integration time (of the bus voltage in this case) "smoothes"
|
||||
out the signal:
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/int_time.png
|
||||
:alt: Illustration of the impact of the integration time
|
||||
:align: center
|
||||
|
||||
Increasing the integration time increases the resilience to noise
|
||||
|
||||
|
||||
Internal signal processing
|
||||
''''''''''''''''''''''''''
|
||||
|
||||
The INA226 is able to accumulate samples acquired by its ADC and output to the
|
||||
ACME board (technically, to its I2C registers) the average value of :math:`N`
|
||||
samples. This is called *oversampling*. While the integration time somewhat
|
||||
behaves as an analog low-pass filter, the oversampling feature is a digital
|
||||
low-pass filter by definition. The former should be set to reduce sampling
|
||||
noise (*i.e.* noise on a single sample coming from the sampling process) while
|
||||
the latter should be used to filter out high-frequency noise present in the
|
||||
input signal and control the sampling frequency.
|
||||
|
||||
Therefore, samples are available at the output of the INA226 at a frequency
|
||||
|
||||
.. math::
|
||||
\frac{1}{N(T_{bus} + T_{shunt})}
|
||||
|
||||
and oversampling ratio provides a way to control the output sampling frequency
|
||||
(*i.e.* to limit the required output bandwidth) while making sure the signal
|
||||
fidelity is as desired.
|
||||
|
||||
|
||||
The 4 IIO channels coming from the INA226 can be grouped according to their
|
||||
respective origins: the bus and shunt voltages are measured (and, potentially
|
||||
filtered) while the shunt current and load power are computed. Indeed, the
|
||||
INA226 contains on-board fixed-point arithmetic units to compute the trivial
|
||||
expressions:
|
||||
|
||||
.. math::
|
||||
|
||||
I_{shunt} = \frac{V_{shunt}}{R_{shunt}}
|
||||
,\ \
|
||||
P_{load} = V_{load}\ I_{load}
|
||||
\approx V_{bus} \ I_{shunt}
|
||||
|
||||
A functional block diagram of this is also given by the datasheet:
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/ina226_functional.png
|
||||
:width: 60%
|
||||
:alt: Functional block diagram, INA226
|
||||
:align: center
|
||||
|
||||
Acquisition and Processing: Functional Block Diagram
|
||||
(source: `Texas Instruments INA226`_)
|
||||
|
||||
In the end, there are therefore 3 channels (bus voltage, shunt voltage and
|
||||
timestamps) that are necessary to figure out the load power consumption, while
|
||||
the others are being provided for convenience *e.g.* in case the rest of the
|
||||
hardware does not have the computing power to make the computation.
|
||||
|
||||
|
||||
Sampling Frequency Issues
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
It looks like the INA226-ACME-BBB setup has a bottleneck preventing the
|
||||
sampling frequency to go higher than ~1.4 kHz (the maximal theoretical sampling
|
||||
frequency is ~3.6 kHz). We know that this issue is not internal to the ADC
|
||||
itself (inside of the INA226) because modifying the integration time affects
|
||||
the output signal even when the sampling frequency is capped (as shown above)
|
||||
but it may come from anywhere after that.
|
||||
|
||||
Because of this, there is no point in using a (theoretical) sampling frequency
|
||||
that is larger than 1.4 kHz. But it is important to note that the ACME will
|
||||
still report the theoretical sampling rate (probably computed with the formula
|
||||
given above) through :attr:`BaylibreAcmeInstrument.sample_rate_hz` and
|
||||
:attr:`IIOINA226Instrument.sample_rate_hz` even if it differs from the actual
|
||||
sampling rate.
|
||||
|
||||
Note that, even though this is obvious for the theoretical sampling rate, the
|
||||
specific values of the bus and shunt integration times do not seem to have an
|
||||
influence on the measured sampling rate; only their sum matters. This further
|
||||
points toward a data-processing bottleneck rather than a hardware bug in the
|
||||
acquisition device.
|
||||
|
||||
The following chart compares the evolution of the measured sampling rate with
|
||||
the expected one as we modify it through :math:`T_{shunt}`, :math:`T_{bus}` and
|
||||
:math:`N`:
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/bottleneck.png
|
||||
:alt: Sampling frequency does not go higher than 1.4 kHz
|
||||
:align: center
|
||||
|
||||
Theoretical vs measured sampling rates
|
||||
|
||||
|
||||
Furthermore, because the transactions are done through a buffer (see next
|
||||
section), if the sampling frequency is too low, the connection may time-out
|
||||
before the buffer is full and ready to be sent. This may be fixed in an
|
||||
upcoming release.
|
||||
|
||||
Buffer-based transactions
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
Samples made available by the INA226 are retrieved by the BBB and stored in a
|
||||
buffer which is sent back to the host once it is full (see
|
||||
``buffer_samples_count`` in :meth:`BaylibreAcmeInstrument.setup` for setting
|
||||
its size). Therefore, the larger the buffer is, the longer it takes to be
|
||||
transmitted back but the less often it has to be transmitted. To illustrate
|
||||
this, consider the following graphs showing the time difference between
|
||||
successive samples in a retrieved signal when the size of the buffer changes:
|
||||
|
||||
.. figure:: images/instrumentation/baylibre_acme/buffer.png
|
||||
:alt: Buffer size impact on the sampled signal
|
||||
:align: center
|
||||
|
||||
Impact of the buffer size on the sampling regularity
|
||||
|
||||
devlib API
|
||||
^^^^^^^^^^
|
||||
|
||||
ACME Cape + BBB (IIO Context)
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
devlib provides wrapper classes for all the IIO connections to an IIO context
|
||||
given by `libiio (the Linux IIO interface)`_ however only the network-based one
|
||||
has been tested. For the other classes, please refer to the official IIO
|
||||
documentation for the meaning of their constructor parameters.
|
||||
|
||||
.. class:: BaylibreAcmeInstrument(target=None, iio_context=None, use_base_iio_context=False, probe_names=None)
|
||||
|
||||
Base class wrapper for the ACME instrument which itself is a wrapper for the
|
||||
IIO context base class. This class wraps around the passed ``iio_context``;
|
||||
if ``use_base_iio_context`` is ``True``, ``iio_context`` is first passed to
|
||||
the :class:`iio.Context` base class (see its documentation for how this
|
||||
parameter is then used), else ``iio_context`` is expected to be a valid
|
||||
instance of :class:`iio.Context`.
|
||||
|
||||
``probe_names`` is expected to be a string or list of strings; if passed,
|
||||
the probes in the instance are named according to it in the order in which
|
||||
they are discovered (see previous comment about probe discovery and
|
||||
:attr:`BaylibreAcmeInstrument.probes`). There should be as many
|
||||
``probe_names`` as there are probes connected to the ACME. By default, the
|
||||
probes keep their IIO names.
|
||||
|
||||
To ensure that the setup is reliable, ``devlib`` requires minimal versions
|
||||
for ``iio``, the IIO drivers and the ACME BBB SD image.
|
||||
|
||||
.. class:: BaylibreAcmeNetworkInstrument(target=None, hostname=None, probe_names=None)
|
||||
|
||||
Child class of :class:`BaylibreAcmeInstrument` for Ethernet-based IIO
|
||||
communication. The ``hostname`` should be the IP address or network name of
|
||||
the BBB. If it is ``None``, the ``IIOD_REMOTE`` environment variable will be
|
||||
used as the hostname. If that environment variable is empty, the server will
|
||||
be discovered using ZeroConf. If that environment variable is not set, a
|
||||
local context is created.
|
||||
|
||||
.. class:: BaylibreAcmeXMLInstrument(target=None, xmlfile=None, probe_names=None)
|
||||
|
||||
Child class of :class:`BaylibreAcmeInstrument` using the XML backend of the
|
||||
IIO library and building an IIO context from the provided ``xmlfile`` (a
|
||||
string giving the path to the file is expected).
|
||||
|
||||
.. class:: BaylibreAcmeLocalInstrument(target=None, probe_names=None)
|
||||
|
||||
Child class of :class:`BaylibreAcmeInstrument` using the Local IIO backend.
|
||||
|
||||
.. attribute:: BaylibreAcmeInstrument.mode
|
||||
|
||||
The collection mode for the ACME is ``CONTINUOUS``.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.setup(shunt_resistor, integration_time_bus, integration_time_shunt, oversampling_ratio, buffer_samples_count=None, buffer_is_circular=False, absolute_timestamps=False, high_resolution=True)
|
||||
|
||||
The ``shunt_resistor`` (:math:`R_{shunt}` [:math:`\mu\Omega`]),
|
||||
``integration_time_bus`` (:math:`T_{bus}` [s]), ``integration_time_shunt``
|
||||
(:math:`T_{shunt}` [s]) and ``oversampling_ratio`` (:math:`N`) are copied
|
||||
into on-board registers inside of the INA226 to be used as described above.
|
||||
Please note that there exists a limited set of accepted values for these
|
||||
parameters; for the integration times, refer to
|
||||
``IIOINA226Instrument.INTEGRATION_TIMES_AVAILABLE`` and for the
|
||||
``oversampling_ratio``, refer to
|
||||
``IIOINA226Instrument.OVERSAMPLING_RATIOS_AVAILABLE``. If all probes share
|
||||
the same value for these attributes, this class provides
|
||||
:attr:`BaylibreAcmeInstrument.OVERSAMPLING_RATIOS_AVAILABLE` and
|
||||
:attr:`BaylibreAcmeInstrument.INTEGRATION_TIMES_AVAILABLE`.
|
||||
|
||||
The ``buffer_samples_count`` is the size of the IIO buffer expressed **in
|
||||
samples**; this is independent of the number of active channels! By default,
|
||||
if ``buffer_samples_count`` is not passed, the IIO buffer of size
|
||||
:attr:`IIOINA226Instrument.sample_rate_hz` is created meaning that a buffer
|
||||
transfer happens roughly every second.
|
||||
|
||||
If ``absolute_timestamps`` is ``False``, the first sample from the
|
||||
``timestamps`` channel is substracted from all the following samples of this
|
||||
channel, effectively making its signal start at 0.
|
||||
|
||||
``high_resolution`` is used to enable a mode where power and current are
|
||||
computed offline on the host machine running ``devlib``: even if the user
|
||||
asks for power or current channels, they are not enabled in hardware
|
||||
(INA226) and instead the necessary voltage signal(s) are enabled to allow
|
||||
the computation of the desired signals using the FPU of the host (which is
|
||||
very likely to be much more accurate than the fixed-point 16-bit unit of the
|
||||
INA226).
|
||||
|
||||
A circular buffer can be used by setting ``buffer_is_circular`` to ``True``
|
||||
(directly passed to :class:`iio.Buffer`).
|
||||
|
||||
Each one of the arguments of this method can either be a single value which
|
||||
will be used for all probes or a list of values giving the corresponding
|
||||
setting for each probe (in the order of ``probe_names`` passed to the
|
||||
constructor) with the exception of ``absolute_timestamps`` (as all signals
|
||||
are resampled onto a common time signal) which, if passed as an array, will
|
||||
be ``True`` only if all of its elements are ``True``.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.reset(sites=None, kinds=None, channels=None)
|
||||
|
||||
:meth:`BaylibreAcmeInstrument.setup` should **always** be called before
|
||||
calling this method so that the hardware is correctly configured. Once this
|
||||
method has been called, :meth:`BaylibreAcmeInstrument.setup` can only be
|
||||
called again once :meth:`BaylibreAcmeInstrument.teardown` has been called.
|
||||
|
||||
This method inherits from :meth:`Instrument.reset`; call
|
||||
:meth:`list_channels` for a list of available channels from a given
|
||||
instance.
|
||||
|
||||
Please note that the size of the transaction buffer is proportional to the
|
||||
number of active channels (for a fixed ``buffer_samples_count``). Therefore,
|
||||
limiting the number of active channels allows to limit the required
|
||||
bandwidth. ``high_resolution`` in :meth:`BaylibreAcmeInstrument.setup`
|
||||
limits the number of active channels to the minimum required.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.start()
|
||||
|
||||
:meth:`BaylibreAcmeInstrument.reset` should **always** be called before
|
||||
calling this method so that the right channels are active,
|
||||
:meth:`BaylibreAcmeInstrument.stop` should **always** be called after
|
||||
calling this method and no other method of the object should be called
|
||||
in-between.
|
||||
|
||||
This method starts the sampling process of the active channels. The samples
|
||||
are stored but are not available until :meth:`BaylibreAcmeInstrument.stop`
|
||||
has been called.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.stop()
|
||||
|
||||
:meth:`BaylibreAcmeInstrument.start` should **always** be called before
|
||||
calling this method so that samples are being captured.
|
||||
|
||||
This method stops the sampling process of the active channels and retrieves
|
||||
and pre-processes the samples. Once this function has been called, the
|
||||
samples are made available through :meth:`BaylibreAcmeInstrument.get_data`.
|
||||
Note that it is safe to call :meth:`BaylibreAcmeInstrument.start` after this
|
||||
method returns but this will discard the data previously acquired.
|
||||
|
||||
When this method returns, It is guaranteed that the content of at least one
|
||||
IIO buffer will have been captured.
|
||||
|
||||
If different sampling frequencies were used for the different probes, the
|
||||
signals are resampled to share the time signal with the highest sampling
|
||||
frequency.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.teardown()
|
||||
|
||||
This method can be called at any point (unless otherwise specified *e.g.*
|
||||
:meth:`BaylibreAcmeInstrument.start`) to deactive any active probe once
|
||||
:meth:`BaylibreAcmeInstrument.reset` has been called. This method does not
|
||||
affect already captured samples.
|
||||
|
||||
The following graph gives a summary of the allowed calling sequence(s) where
|
||||
each edge means "can be called directly after":
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph acme_calls {
|
||||
rankdir = LR
|
||||
bgcolor = transparent
|
||||
|
||||
__init__ -> setup -> reset -> start -> stop -> teardown
|
||||
|
||||
teardown:sw -> setup [style=dashed]
|
||||
teardown -> reset [style=dashed]
|
||||
|
||||
stop -> reset [style=dashed]
|
||||
stop:nw -> start [style=dashed]
|
||||
|
||||
reset -> teardown [style=dashed]
|
||||
}
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.get_data(outfile=None)
|
||||
|
||||
Inherited from :meth:`Instrument.get_data`. If ``outfile`` is ``None``
|
||||
(default), the samples are returned as a `pandas.DataFrame` with the
|
||||
channels as columns. Else, it behaves like the parent class, returning a
|
||||
``MeasurementCsv``.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.add_channel()
|
||||
|
||||
Should not be used as new channels are discovered through the IIO context.
|
||||
|
||||
.. method:: BaylibreAcmeInstrument.list_channels()
|
||||
|
||||
Inherited from :meth:`Instrument.list_channels`.
|
||||
|
||||
.. attribute:: BaylibreAcmeInstrument.sample_rate_hz
|
||||
.. attribute:: BaylibreAcmeInstrument.OVERSAMPLING_RATIOS_AVAILABLE
|
||||
.. attribute:: BaylibreAcmeInstrument.INTEGRATION_TIMES_AVAILABLE
|
||||
|
||||
These attributes return the corresponding attributes of the probes if they
|
||||
all share the same value (and are therefore provided to avoid reading from a
|
||||
single probe and expecting the others to share this value). They should be
|
||||
used whenever the assumption that all probes share the same value for the
|
||||
accessed attribute is made. For this reason, an exception is raised if it is
|
||||
not the case.
|
||||
|
||||
If probes are active (*i.e.* :meth:`BaylibreAcmeInstrument.reset` has been
|
||||
called), only these are read for the value of the attribute (as others have
|
||||
been tagged to be ignored). If not, all probes are used.
|
||||
|
||||
.. attribute:: BaylibreAcmeInstrument.probes
|
||||
|
||||
Dictionary of :class:`IIOINA226Instrument` instances representing the probes
|
||||
connected to the ACME. If provided to the constructor, the keys are the
|
||||
``probe_names`` that were passed.
|
||||
|
||||
ACME Probes (IIO Devices)
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
The following class is not supposed to be instantiated by the user code: the
|
||||
API is provided as the ACME probes can be accessed through the
|
||||
:attr:`BaylibreAcmeInstrument.probes` attribute.
|
||||
|
||||
.. class:: IIOINA226Instrument(iio_device)
|
||||
|
||||
This class is a wrapper for the :class:`iio.Device` class and takes a valid
|
||||
instance as ``iio_device``. It is not supposed to be instantiated by the
|
||||
user and its partial documentation is provided for read-access only.
|
||||
|
||||
.. attribute:: IIOINA226Instrument.shunt_resistor
|
||||
.. attribute:: IIOINA226Instrument.sample_rate_hz
|
||||
.. attribute:: IIOINA226Instrument.oversampling_ratio
|
||||
.. attribute:: IIOINA226Instrument.integration_time_shunt
|
||||
.. attribute:: IIOINA226Instrument.integration_time_bus
|
||||
.. attribute:: IIOINA226Instrument.OVERSAMPLING_RATIOS_AVAILABLE
|
||||
.. attribute:: IIOINA226Instrument.INTEGRATION_TIMES_AVAILABLE
|
||||
|
||||
These attributes are provided *for reference* and should not be assigned to
|
||||
but can be used to make the user code more readable, if needed. Please note
|
||||
that, as reading these attributes reads the underlying value from the
|
||||
hardware, they should not be read when the ACME is active *i.e* when
|
||||
:meth:`BaylibreAcmeInstrument.setup` has been called without calling
|
||||
:meth:`BaylibreAcmeInstrument.teardown`.
|
||||
|
||||
|
||||
Examples
|
||||
""""""""
|
||||
|
||||
The following example shows a basic use of an ACME at IP address
|
||||
``ACME_IP_ADDR`` with 2 probes connected, capturing all the channels during
|
||||
(roughly) 10 seconds at a sampling rate of 613 Hz and outputing the
|
||||
measurements to the CSV file ``acme.csv``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import time
|
||||
import devlib
|
||||
|
||||
acme = devlib.BaylibreAcmeNetworkInstrument(hostname=ACME_IP_ADDR,
|
||||
probe_names=['battery', 'usb'])
|
||||
|
||||
int_times = acme.INTEGRATION_TIMES_AVAILABLE
|
||||
ratios = acme.OVERSAMPLING_RATIOS_AVAILABLE
|
||||
|
||||
acme.setup(shunt_resistor=20000,
|
||||
integration_time_bus=int_times[1],
|
||||
integration_time_shunt=int_times[1],
|
||||
oversampling_ratio=ratios[1])
|
||||
|
||||
acme.reset()
|
||||
acme.start()
|
||||
time.sleep(10)
|
||||
acme.stop()
|
||||
acme.get_data('acme.csv')
|
||||
acme.teardown()
|
||||
|
||||
It is common to have different resistances for different probe shunt resistors.
|
||||
Furthermore, we may want to have different sampling frequencies for different
|
||||
probes (*e.g.* if it is known that the USB voltage changes rather slowly).
|
||||
Finally, it is possible to set the integration times for the bus and shunt
|
||||
voltages of a same probe to different values. The following call to
|
||||
:meth:`BaylibreAcmeInstrument.setup` illustrates these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acme.setup(shunt_resistor=[20000, 10000],
|
||||
integration_time_bus=[int_times[2], int_times[3]],
|
||||
integration_time_shunt=[int_times[3], int_times[4]],
|
||||
oversampling_ratio=[ratios[0], ratios[1]])
|
||||
|
||||
for n, p in acme.probes.iteritems():
|
||||
print('{}:'.format(n))
|
||||
print(' T_bus = {} s'.format(p.integration_time_bus))
|
||||
print(' T_shn = {} s'.format(p.integration_time_shunt))
|
||||
print(' N = {}'.format(p.oversampling_ratio))
|
||||
print(' freq = {} Hz'.format(p.sample_rate_hz))
|
||||
|
||||
# Output:
|
||||
#
|
||||
# battery:
|
||||
# T_bus = 0.000332 s
|
||||
# T_shn = 0.000588 s
|
||||
# N = 1
|
||||
# freq = 1087 Hz
|
||||
# usb:
|
||||
# T_bus = 0.000588 s
|
||||
# T_shn = 0.0011 s
|
||||
# N = 4
|
||||
# freq = 148 Hz
|
||||
|
||||
Please keep in mind that calling ``acme.get_data('acme.csv')`` after capturing
|
||||
samples with this setup will output signals with the same sampling frequency
|
||||
(the highest one among the sampling frequencies) as the signals are resampled
|
||||
to output a single time signal.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#acme_probe_variants] There exist different variants of the ACME probe (USB, Jack, shunt resistor) but they all use the same probing hardware (the TI INA226) and don't differ from the point of view of the software stack (at any level, including devlib, the highest one)
|
||||
|
||||
.. [#acme_name_conflicts] Be careful that in cases where multiple ACME boards are being used, it may be required to manually handle name conflicts
|
||||
|
3
setup.py
3
setup.py
@ -94,11 +94,14 @@ params = dict(
|
||||
'pyserial', # Serial port interface
|
||||
'wrapt', # Basic for construction of decorator functions
|
||||
'future', # Python 2-3 compatibility
|
||||
'pandas',
|
||||
'numpy',
|
||||
],
|
||||
extras_require={
|
||||
'daq': ['daqpower'],
|
||||
'doc': ['sphinx'],
|
||||
'monsoon': ['python-gflags'],
|
||||
'acme': ['pandas', 'numpy'],
|
||||
},
|
||||
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
|
Loading…
x
Reference in New Issue
Block a user