# -*- coding: utf-8 -*-
"""
The PyVISA communication with the oscilloscope
See Keysight's Programmer's Guide for reference on the VISA commands.
"""
__docformat__ = "restructuredtext en"
import os
import sys
import pyvisa
import time
import logging
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import keyoscacquire.config as config
import keyoscacquire.visa_utils as visa_utils
import keyoscacquire.fileio as fileio
import keyoscacquire.dataprocessing as dataprocessing
# for backwards compatibility (but rather use the Oscilloscope methods)
from .fileio import save_trace, save_trace_npy
_log = logging.getLogger(__name__)
#: Supported Keysight DSO/MSO InfiniiVision series
_SUPPORTED_SERIES = ['1000', '2000', '3000', '4000', '6000']
#: Datatype is ``'h'`` for 16 bit signed int (``WORD``), ``'b'`` for 8 bit signed bit (``BYTE``).
#: Same naming as for structs `docs.python.org/3/library/struct.html#format-characters`
_DATATYPES = {'BYT':'b', 'WOR':'h', 'BYTE':'b', 'WORD':'h'}
## ========================================================================= ##
[docs]class Oscilloscope:
"""PyVISA communication with the oscilloscope.
Init opens a connection to an instrument and chooses default settings
for the connection and acquisition as specified in :mod:`keyoscacquire.config`.
Leading underscores indicate that an attribute or method is read-only or
suggested to be for interal use only.
Parameters
----------
address : str, default :data:`~keyoscacquire.config._visa_address`
Visa address of instrument. To find the visa addresses of the instruments
connected to the computer run ``list_visa_devices`` in the command line.
Example address ``'USB0::1234::1234::MY1234567::INSTR'``
timeout : int, default :data:`~keyoscacquire.config._timeout`
Milliseconds before timeout on the channel to the instrument
get_errors_on_init : bool
The error queue of the scope is by default cleared when __init__ is called;
however, when this parameter is ``True``, the error queue is extracted
from the scope before the log is cleared, it is printed to the terminal and
the attribute ``errors`` is populated with the errors
verbose : bool, default ``True``
If ``True``: prints when the connection to the device is opened etc,
and sets attr:`verbose_acquistion` to ``True``
Raises
------
:class:`pyvisa.errors.Error`
if no successful connection is made.
Attributes
----------
verbose : bool, default ``True``
If ``True``: prints when the connection to the device is opened, the
acquistion mode, etc
verbose_acquistion : bool, defaulting to ``self.verbose``
If ``True``: prints that the capturing starts, the channels
acquired from and the number of points captured
fname : str, default :data:`keyoscacquire.config._filename`
The filename to which the trace will be saved with :meth:`save_trace()`
ext : str, default :data:`keyoscacquire.config._filetype`
The extension for saving traces, must include the period, e.g. ``.csv``
savepng : bool, default :data:`keyoscacquire.config._export_png`
If ``True``: will save a png of the plot when :meth:`save_trace()`
showplot : bool, default :data:`keyoscacquire.config._show_plot`
If ``True``: will show a matplotlib plot window when :meth:`save_trace()`
_inst : :class:`pyvisa.resources.Resource`
The oscilloscope PyVISA resource
_id : str
The maker, model, serial and firmware version of the scope. Examples::
'AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.567891234'
'KEYSIGHT TECHNOLOGIES,MSO9104A,MY12345678,06.30.00609'
_model : str
The instrument's model name
_serial : str
The instrument's serial number
_address : str
Visa address of instrument
_time : :class:`~numpy.ndarray`
The time axis of the most recent captured trace
_values : :class:`~numpy.ndarray`
The values for the most recent captured trace
_capture_channels : list of ints
The channels of captured for the most recent trace
"""
_capture_channels = None
_raw = None
_metadata = None
_time = None
_values = None
fname = config._filename
ext = config._filetype
savepng = config._export_png
showplot = config._show_plot
verbose_acquistion = False
def __init__(self, address=config._visa_address, timeout=config._timeout,
get_errors_on_init=False, verbose=True):
"""See class docstring"""
self._address = address
self.verbose = verbose
# Connect to the scope
try:
rm = pyvisa.ResourceManager()
self._inst = rm.open_resource(address)
except pyvisa.Error as err:
print(f"\n\nCould not connect to '{address}', see traceback below:\n")
raise
self.timeout = timeout
# For TCP/IP socket connections enable the read Termination Character, or reads will timeout
if self._inst.resource_name.endswith('SOCKET'):
self._inst.read_termination = '\n'
if get_errors_on_init:
self.get_full_error_queue(verbose=True)
# Clear the status data structures, the device-defined error queue, and the Request-for-OPC flag
self.write('*CLS')
# Make sure WORD and BYTE data is transeferred as signed ints and lease significant bit first
self.write(':WAVeform:UNSigned OFF')
self.write(':WAVeform:BYTeorder LSBFirst') # MSBF is default, must be overridden for WORD to work
# Get information about the connected device
self._information_about_device()
# Set standard settings
self.set_acquiring_options(wav_format=config._waveform_format, p_mode=config._p_mode,
num_points=config._num_points)
# Will set channels to the active channels
self.set_channels_for_capture()
self.verbose_acquistion = verbose
def _information_about_device(self):
"""Get the IDN of the instrument and parse it"""
self._id = self.query('*IDN?')
try:
maker, self._model, self._serial, _, self._model_series = visa_utils.interpret_visa_id(self._id)
if self.verbose:
print(f"Connected to:")
print(f" {maker}")
print(f" {self._model} (serial {self._serial})")
except Exception:
if self.verbose:
print(f"Connected to '{self._id}'")
print("(!) Failed to intepret the VISA IDN string")
if not self._model_series in _SUPPORTED_SERIES:
print(f"(!) WARNING: This model ({self._model}) is not yet fully supported by keyoscacquire,")
print( " but might work to some extent. keyoscacquire supports Keysight's")
print( " InfiniiVision X-series oscilloscopes.")
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
# Do not try to set running if an exception ocurred
self.close(set_running=False)
else:
self.close()
[docs] def write(self, command):
"""Write a VISA command to the oscilloscope.
Parameters
----------
command : str
VISA command to be written"""
self._inst.write(command)
[docs] def query(self, command, action=""):
"""Query a VISA command to the oscilloscope. Will ask the oscilloscope
for the latest error if the query times out.
Parameters
----------
command : str
VISA query
action : str, default ""
Optional argument used to customise the error message if there is a
timeout
"""
try:
return self._inst.query(command).strip()
except pyvisa.Error as err:
if action:
msg = f"{action} (command '{command}')"
else:
msg = f"query '{command}'"
print(f"\n\nVisaError: {err}\n When trying {msg} (full traceback below).")
print(f" Have you checked that the timeout (currently "
f"{self.timeout:,d} ms) is sufficently long?")
try:
self.get_full_error_queue(verbose=True)
print("")
except Exception as excep:
print("Could not retrieve errors from the oscilloscope:")
print(excep)
print("")
raise
[docs] def close(self, set_running=True):
"""Closes the connection to the oscilloscope.
Parameters
----------
set_running : bool, default ``True``
``True`` sets the oscilloscope to running before closing the
connection, ``False`` leaves it in its current state
"""
# Set the oscilloscope running before closing the connection
if set_running:
self.run()
self._inst.close()
_log.debug(f"Closed connection to '{self._id}'")
[docs] def get_error(self):
"""Get the first error in the error queue, a FIFO queue of max length 30.
The queue is reset when ``*CLS`` is written to the scope, which happens
in __init__().
Returns
-------
str
error number,description
"""
# Do not use self.query here as that can lead to infinite nesting!
return self._inst.query(":SYSTem:ERRor?").strip()
def get_full_error_queue(self, verbose=True):
"""All the latest errors from the oscilloscope, upto 30 errors
(and store to the attribute ``errors``)"""
self.errors = []
for i in range(30):
err = self.get_error()
if err[:2] == "+0": # No error
# Stop querying
break
else:
# Store the error
self.errors.append(err)
if verbose:
self._print_errors(self.errors)
return self.errors
def _print_errors(self, errors):
"""Print the errors obtained by :func:`Oscilloscope.get_full_error_queue`"""
if not errors:
print("Error queue empty")
else:
print("Latest errors from the oscilloscope (FIFO queue, upto 30 errors)")
for i, err in enumerate(errors):
print(f"{i:>2}: {err}")
[docs] def run(self):
"""Set the oscilloscope to running mode."""
self.write(":RUN")
[docs] def stop(self):
"""Stop the oscilloscope."""
self.write(":STOP")
[docs] def is_running(self):
"""Determine if the oscilloscope is running.
Returns
-------
bool
``True`` if running, ``False`` otherwise
"""
# The third bit of the operation register is 1 if the instrument is running
reg = int(self.query(":OPERegister:CONDition?"))
return (reg & 8) == 8
@property
def timeout(self):
"""The timeout on the VISA communication with the instrument. The
timeout must be longer than the acquisition time.
:getter: Returns the number of milliseconds before timeout of a query command
:setter: Set the number of milliseconds before timeout of a query command
:type: int
"""
return self._inst.timeout
@timeout.setter
def timeout(self, timeout: int):
"""See getter"""
self._inst.timeout = timeout
@property
def active_channels(self):
"""Find the currently active channels on the instrument
.. note:: Changing the active channels will not affect with channels are
captured unless :meth:`set_channels_for_capture()` is subsequently run.
The :meth:`get_traces()` family of methods will make sure of this.
:getter: Returns a list of the active channels, for example ``[1, 3]``
:setter: list of the active channels, for example ``[1, 3]``
:type: list of ints
"""
# querying DISP for each channel to determine which channels are currently displayed
return [i for i in range(1, 5) if bool(int(self.query(f":CHAN{i}:DISP?")))]
@active_channels.setter
def active_channels(self, channels: list):
"""See getter"""
if not isinstance(channels, list):
channels = [channels]
for i in range(1, 5):
self.write(f":CHAN{i}:DISP {int(i in channels)}")
@property
def acq_type(self):
"""Acquisition mode of the oscilloscope
Choose between
* ``'NORMal'`` — sets the oscilloscope in the normal mode.
* ``'AVERage'`` or ``'AVER<m>'`` — sets the oscilloscope in the averaging mode.
The number of averages can be set with :attr:`num_averages`, or
<m> will be used as :attr:`num_averages` if supplied.
<m> can be in the range 2 to 65,536
* ``'HRESolution'`` — sets the oscilloscope in the high-resolution mode
(also known as smoothing). This mode is used to reduce noise at slower
sweep speeds where the digitizer samples faster than needed to fill memory for the displayed time range.
For example, if the digitizer samples at 200 MSa/s, but the effective sample rate is 1 MSa/s
(because of a slower sweep speed), only 1 out of every 200 samples needs to be stored.
Instead of storing one sample (and throwing others away), the 200 samples are averaged
together to provide the value for one display point. The slower the sweep speed, the greater
the number of samples that are averaged together for each display point.
:getter: Returns the current mode (will not return ``<m>`` for ``AVER``)
:setter: Sets the mode, for example ``AVER8``, if :attr:`verbose` will
print the type and the number of averages number
:type: ``{'NORMal', 'AVERage', 'AVER<m>', 'HRES'}``
Raises
------
ValueError
If ``<m>`` in cannot be converted to an int (or is out of range)
"""
return self.query(":ACQuire:TYPE?")
@acq_type.setter
def acq_type(self, a_type: str):
"""See getter"""
acq_type = a_type[:4].upper()
self.write(f":ACQuire:TYPE {acq_type}")
# Handle AVER<m> expressions
if acq_type == 'AVER':
self._handle_aver(a_type)
def _handle_aver(self, a_type: str):
"""Handle ``AVER*`` acquisition types, using a possible int after
``*`` as the number of averages
Raises
------
ValueError
If * cannot be converted to int
"""
if len(a_type) > 4 and not a_type[4:].lower() == 'age':
try:
self.num_averages = int(a_type[4:])
except ValueError:
ValueError(f"\nValueError: Failed to convert '{a_type[4:]}' to an integer, "
"check that acquisition type is on the form AVER or AVER<m> "
f"where <m> is an integer (currently acq. type is '{a_type}').\n")
@property
def num_averages(self):
"""The number of averages taken if the scope is in the ``'AVERage'``
:attr:`acq_type`
:getter: Returns the current number of averages
:setter: Set the number, will print the number if :attr:`verbose`
:type: int, 2 to 65,536
Raises
------
ValueError
If the number is is out of range
"""
return self.query(":ACQuire:COUNt?")
@num_averages.setter
def num_averages(self, num: int):
"""See getter"""
if not (2 <= num <= 65536):
raise ValueError(f"\nThe number of averages {num} is out of range.")
self.write(f":ACQuire:COUNt {num}")
[docs] def print_acq_settings(self):
"""Print the current settings for acquistion from the scope"""
acq_type = self.acq_type
print(f"Acquisition type: {acq_type}")
if acq_type == 'AVER':
print(f"# of averages: {self.num_averages}")
print(f"From channels: {self._capture_channels}")
@property
def p_mode(self):
"""The points mode of the acquistion
``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to
1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital.
:getter: Returns the current mode
:setter: Set the mode, will check if compatible with the :attr:`acq_type`
:type: ``{'NORMal', 'RAW', 'MAXimum'}``
"""
return self.query(":WAVeform:POINts:MODE?")
@p_mode.setter
def p_mode(self, p_mode: str):
"""See getter"""
if (not p_mode[:4] == 'NORM') and self.acq_type == 'AVER':
p_mode = 'NORM'
_log.info(f":WAVeform:POINts:MODE overridden (from {p_mode}) to "
"NORMal due to :ACQuire:TYPE:AVERage.")
self.write(f":WAVeform:POINts:MODE {p_mode}")
_log.debug(f"Points mode set to: {p_mode}")
@property
def num_points(self):
"""The number of points to be acquired for each channel. Use 0 to
get the maximum number given the :attr:`p_mode`, or override with a
lower number than maximum for the given :attr:`p_mode`
.. warning:: If the exact number of points is crucial, always check the
number of points with the getter after performing the setter.
.. note:: The scope must be stopped to get the number of points that
will be transferred when it is in the *stopped* state. As this package
always stops the scope when getting a trace, the getter will also
do this to get the actual number of points that will be
transferred (otherwise the returned number will be capped by the
:attr:`p_mode` ``NORMal`` (which can be transferred without
stopping the scope)).
:getter: Returns the number of points that will be acquired (stopping
and re-running the scope as explained in the note above)
:setter: Set the number, but beware that the scope might change the
number depending on memory depth, time axis settings, etc.
:type: int
Raises
------
ValueError
If a negative integer or other datatypes are given.
"""
# Must stop the scope to be able to read the actual number of points
# that will be transferred in the RAW or MAX mode
self.stop()
points = int(self.query(":WAVeform:POINTs?"))
self.run()
return points
@num_points.setter
def num_points(self, num_points: int):
"""See getter"""
if num_points == 0:
self.write(f":WAVeform:POINts MAXimum")
_log.debug("Number of points set to: MAX")
# If number of points has been specified, tell the instrument to
# use this number of points
elif num_points > 0:
if self._model_series in ['9000']:
self.write(f":ACQuire:POINts {num_points}")
else:
if num_points > 7680:
self.p_mode = 'RAW'
# Must stop the scope to set the number of points to avoid
# getting an error in the scopes' log (however, it seems to
# be working regardless, only the get_error() will return -222)
self.stop()
self.write(f":WAVeform:POINts {num_points}")
self.run()
_log.debug(f"Number of points set to: {num_points}")
else:
ValueError(f"Cannot set points mode ('{num_points}' is not a "
"non-negative integer)")
@property
def wav_format(self):
"""Data transmission mode for waveform data points, i.e. how
the data is formatted when sent from the oscilloscope.
* ``'ASCii'`` formatted data converts the internal integer data values
to real Y-axis values. Values are transferred as ascii digits in
floating point notation, separated by commas.
* ``'WORD'`` formatted data transfers signed 16-bit data as two bytes.
* ``'BYTE'`` formatted data is transferred as signed 8-bit bytes.
:getter: Returns the number of points that will be acquired, however
it does not seem to be fully stable
:setter: Set the number, but beware that the scope might change the
number depending on memory depth, time axis settings, etc.
:type: ``{'WORD', 'BYTE', 'ASCii'}``
"""
return self.query(":WAVeform:FORMat?")
@wav_format.setter
def wav_format(self, wav_format: str):
"""See getter"""
self.write(f":WAVeform:FORMat {wav_format}")
_log.debug(f"Waveform format set to: {wav_format}")
[docs] def set_acquiring_options(self, wav_format=None, acq_type=None,
num_averages=None, p_mode=None, num_points=None,
verbose_acquistion=None):
"""Change acquisition options
Parameters
----------
wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`keyoscacquire.config._waveform_format`
Select the format of the communication of waveform from the
oscilloscope, see :attr:`wav_format`
acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER<m>'``}, default :data:`keyoscacquire.config._acq_type`
Acquisition mode of the oscilloscope. <m> will be used as
num_averages if supplied, see :attr:`acq_type`
num_averages : int, 2 to 65536
Applies only to the ``'AVERage'`` mode: The number of averages applied
p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode`
``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up to 1e6 points.
Use ``'MAXimum'`` for sources that are not analogue or digital
num_points : int, default :data:`keyoscacquire.config._num_points`
Use 0 to get the maximum amount of points for the current :attr:`p_mode`,
otherwise override with a lower number than maximum for the :attr:`p_mode`
verbose_acquistion : bool or ``None``, default ``None``
Temporarily control attribute which decides whether to print
information while acquiring: bool sets it to the bool value,
``None`` leaves as the it is in the Oscilloscope object
Raises
------
ValueError
If num_averages are outside of the range or <m> in acq_type cannot
be converted to int
"""
if verbose_acquistion is not None:
self.verbose_acquistion = verbose_acquistion
if acq_type is not None:
self.acq_type = acq_type
if num_averages is not None:
self.num_averages = num_averages
# Set options for waveform export
self.set_waveform_export_options(wav_format, num_points, p_mode)
## Capture and read functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
[docs] def set_channels_for_capture(self, channels='active'):
"""Decide the channels to be acquired, or determine by checking active
channels on the oscilloscope.
Parameters
----------
channels : list of ints or ``'active'``, default active
list of the channel numbers to be acquired, example ``[1, 3]``.
Use ``'active'`` or ``[]`` to capture all the currently active
channels on the oscilloscope.
Returns
-------
list of ints
the channels that will be captured, example ``[1, 3]``
"""
# If no channels specified, find the channels currently active and acquire from those
if channels is None or np.any(channels in [[], ['active'], 'active']):
self._capture_channels = self.active_channels
else:
self._capture_channels = channels
# Build list of sources
self._sources = [f"CHAN{ch}" for ch in self._capture_channels]
return self._capture_channels
[docs] def capture_and_read(self, set_running=True):
"""Acquire raw data from selected channels according to acquring options
currently set with :func:`set_acquiring_options`.
The parameters are provided by :func:`set_channels_for_capture`.
The populated attributes raw and metadata should be processed
by :func:`keyoscacquire.dataprocessing.process_data`.
raw : :class:`~numpy.ndarray`
An ndarray of ints that can be converted to voltage values using the preamble.
metadata
depends on the :attr:`wav_format`
Parameters
----------
set_running : bool, default ``True``
``True`` leaves oscilloscope running after data capture
Raises
------
ValueError
If :attr:`wav_format` is not one of ``{'BYTE', 'WORD', 'ASCii'}``
See also
--------
:func:`keyoscacquire.dataprocessing.process_data`
"""
wav_format = self.wav_format
if self.verbose_acquistion:
self.print_acq_settings()
print(f"Acquiring (format '{wav_format}').. ", end="", flush=True)
start_time = time.time() # time the acquiring process
# If the instrument is not running, we presumably want the data
# on the screen and hence don't want to use DIGitize as digitize
# will obtain a new trace.
if self.is_running():
# DIGitize is a specialised RUN command.
# Waveforms are acquired according to the settings of the :ACQuire commands.
# When acquisition is complete, the instrument is stopped.
self.write(':DIGitize ' + ", ".join(self._sources))
## Read from the scope
wav_format = wav_format[:3]
if wav_format in ['WOR', 'BYT']:
self._read_binary(datatype=_DATATYPES[wav_format])
elif wav_format[:3] == 'ASC':
self._read_ascii()
else:
raise ValueError(f"\nCould not capture and read data, waveform format "
f"'{wav_format}' is unknown.\n")
if self.verbose_acquistion:
print("done")
to_log = f"Elapsed time capture and read: {(time.time()-start_time)*1e3:.1f} ms"
_log.debug(to_log)
if set_running:
self.run()
def _read_binary(self, datatype='standard'):
"""Read data and metadata from sources of the oscilloscope
when waveform format is ``'WORD'`` or ``'BYTE'``.
The parameters are provided by :func:`set_channels_for_capture`.
The output should be processed by :func:`keyoscacquire.dataprocessing._process_data_binary`.
Populates the following attributes
raw : :class:`~numpy.ndarray`
Raw data to be processed by :func:`keyoscacquire.dataprocessing._process_data_binary`.
An ndarray of ints that can be converted to voltage values using the preamble.
metadata : list of str
List of preamble metadata (comma separated ascii values) for each channel
Parameters
----------
datatype : char or ``'standard'``, optional but must match waveform format used
To determine how to read the values from the oscilloscope depending
on :attr:`wav_format`. Datatype is ``'h'`` for 16 bit signed int
(``'WORD'``), for 8 bit signed bit (``'BYTE'``) (same naming as for
structs, `https://docs.python.org/3/library/struct.html#format-characters`).
``'standard'`` will evaluate :data:`oscilloscope._DATATYPES[self.wav_format]`
to automatically choose according to the waveform format
set_running : bool, default ``True``
``True`` leaves oscilloscope running after data capture
"""
self._raw, self._metadata = [], []
# Loop through all the sources
for source in self._sources:
# Select the channel for which the succeeding WAVeform commands applies to
self.write(f":WAVeform:SOURce {source}")
# obtain comma separated metadata values for processing of raw data for this source
self._metadata.append(self.query(':WAVeform:PREamble?'))
try:
# obtain the data
# read out data for this source
self._raw.append(self._inst.query_binary_values(':WAVeform:DATA?',
datatype=datatype,
container=np.array))
except pyvisa.Error as err:
print(f"\n\nVisaError: {err}\n When trying to obtain the "
f"waveform (full traceback below).")
print(f" Have you checked that the timeout (currently"
f"{self.timeout:,d} ms) is sufficently long?")
try:
self.get_full_error_queue(verbose=True)
print("")
except Exception as excep:
print("Could not retrieve errors from the oscilloscope:")
print(excep)
print("")
raise
def _read_ascii(self):
"""Read data and metadata from sources of the oscilloscope
when waveform format is ASCii.
The parameters are provided by :func:`set_channels_for_capture`.
The output should be processed by :func:`dataprocessing._process_data_ascii`.
Populates the following attributes
raw : str
Raw data to be processed by :func:`dataprocessing._process_data_ascii`.
The raw data is a list of one IEEE block per channel with a head
and then comma separated ascii values.
metadata : tuple of str
Tuple of the preamble for one of the channels to calculate time
axis (same for all channels) and the model series
Parameters
----------
set_running : bool, default ``True``
``True`` leaves oscilloscope running after data capture
"""
self._raw = []
# Loop through all the sources
for source in self._sources:
# Select the channel for which the succeeding WAVeform commands applies to
self.write(f":WAVeform:SOURce {source}")
# Read out data for this source
self._raw.append(self.query(':WAVeform:DATA?', action="obtain the waveform"))
# Get the preamble (used for calculating time axis, which is the same
# for all channels)
preamble = self.query(':WAVeform:PREamble?')
self._metadata = (preamble, self._model_series)
## Building functions to get a trace and various option setting and processing ##
[docs] def get_trace(self, channels=None, verbose_acquistion=None):
"""Obtain one trace with current settings. Will return the values
of the traces, but alos populate a few attributes, including
``_time``, ``_values`` and ``_capture_channels``.
Use :meth:`save_trace()` to save the trace to disk.
Parameters
----------
channels : list of ints or ``'active'``, uses oscilloscope setting by default
Optionally change the list of the channel numbers to be acquired,
example ``[1, 3]``. Use ``'active'`` or ``[]`` to capture all the
currently active channels on the oscilloscope.
verbose_acquistion : bool or ``None``, default ``None``
Optionally change :attr:`verbose_acquistion`
Returns
-------
_time : :class:`~numpy.ndarray`
Time axis for the measurement
_values : :class:`~numpy.ndarray`
Voltage values, same sequence as sources input, each row
represents one channel
_capture_channels : list of ints
list of the channels obtaied from, example ``[1, 3]``
"""
# Possibility to override verbose_acquistion
if verbose_acquistion is not None:
self.verbose_acquistion = verbose_acquistion
self.set_channels_for_capture(channels=channels)
# Capture, read and process data
self.capture_and_read()
self._time, self._values = dataprocessing.process_data(self._raw, self._metadata, self.wav_format,
verbose_acquistion=self.verbose_acquistion)
return self._time, self._values, self._capture_channels
[docs] def set_options_get_trace(self, channels=None, wav_format=None, acq_type=None,
num_averages=None, p_mode=None, num_points=None):
"""Set the options provided by the parameters and obtain one trace.
Parameters
----------
channels : list of ints or ``'active'``, uses active channels by default
list of the channel numbers to be acquired, example ``[1, 3]``.
Use ``'active'`` or ``[]`` to capture all the currently active
channels on the oscilloscope.
wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format`
Select the format of the communication of waveform from the
oscilloscope, see :attr:`wav_format`
acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER<m>'``}, default :data:`~keyoscacquire.config._acq_type`
Acquisition mode of the oscilloscope. <m> will be used as
num_averages if supplied, see :attr:`acq_type`
num_averages : int, 2 to 65536, uses oscilloscope setting by default
Applies only to the ``'AVERage'`` mode: The number of averages applied
p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode`
``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives
up to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue or digital
num_points : int, default :data:`keyoscacquire.config._num_points`
Use 0 to get the maximum amount of points for the current :attr:`p_mode`,
otherwise override with a lower number than maximum for the :attr:`p_mode`
Returns
-------
_time : :class:`~numpy.ndarray`
Time axis for the measurement
_values : :class:`~numpy.ndarray`
Voltage values, same sequence as sources input, each row
represents one channel
_capture_channels : list of ints
list of the channels obtaied from, example ``[1, 3]``
"""
self.set_acquiring_options(wav_format=wav_format, acq_type=acq_type,
num_averages=num_averages, p_mode=p_mode,
num_points=num_points)
self.get_trace(channels=channels)
return self._time, self._values, self._capture_channels
[docs] def set_options_get_trace_save(self, fname=None, ext=None,
channels=None, wav_format=None, acq_type=None,
num_averages=None, p_mode=None,
num_points=None, additional_header_info=None):
"""Get trace and save the trace to a file and plot to png.
Filename is recursively checked to ensure no overwrite.
The file header when capturing ch 1 and 3 in AVER8 is::
# AGILENT TECHNOLOGIES,DSO-X 2024A,MY1234567,12.34.1234567890
# AVER,8
# 2019-09-06 20:01:15.187598
# time,1,3
Parameters
----------
fname : str, default :data:`~keyoscacquire.config._filename`
Filename of trace
ext : str, default :data:`~keyoscacquire.config._filetype`
Choose the filetype of the saved trace
channels : list of ints or ``'active'``, uses active channels by default
list of the channel numbers to be acquired, example ``[1, 3]``.
Use ``'active'`` or ``[]`` to capture all the currently active
channels on the oscilloscope.
wav_format : {``'WORD'``, ``'BYTE'``, ``'ASCii'``}, default :data:`~keyoscacquire.config._waveform_format`
Select the format of the communication of waveform from the
oscilloscope, see :attr:`wav_format`
acq_type : {``'HRESolution'``, ``'NORMal'``, ``'AVERage'``, ``'AVER<m>'``}, default :data:`~keyoscacquire.config._acq_type`
Acquisition mode of the oscilloscope. <m> will be used as
num_averages if supplied, see :attr:`acq_type`
num_averages : int, 2 to 65536, uses oscilloscope setting by default
Applies only to the ``'AVERage'`` mode: The number of averages applied
p_mode : {``'NORMal'``, ``'RAW'``, ``'MAXimum'``}, default :data:`keyoscacquire.config._p_mode`
``'NORMal'`` is limited to 62,500 points, whereas ``'RAW'`` gives up
to 1e6 points. Use ``'MAXimum'`` for sources that are not analogue
or digital
num_points : int, default :data:`keyoscacquire.config._num_points`
Use 0 to get the maximum amount of points for the current :attr:`p_mode`,
otherwise override with a lower number than maximum for the :attr:`p_mode`
additional_header_info : str, default ```None``
Will put this string as a separate line before the column headers
"""
self.set_options_get_trace(channels=channels, wav_format=wav_format,
acq_type=acq_type, num_averages=num_averages,
p_mode=p_mode, num_points=num_points)
self.save_trace(fname, ext, additional_header_info=additional_header_info)
[docs] def save_trace(self, fname=None, ext=None, additional_header_info=None,
savepng=None, showplot=None, nowarn=False):
"""Save the most recent trace to ``fname+ext``. Will check if the filename
exists, and let the user append to the fname if that is the case.
Parameters
----------
fname : str, default :data:`keyoscacquire.config._filename`
Filename of trace
ext : ``{'.csv', '.npy'}``, default :data:`keyoscacquire.config._filetype`
Choose the filetype of the saved trace
additional_header_info : str, default ```None``
Will put this string as a separate line before the column headers
savepng : bool, default :data:`keyoscacquire.config._export_png`
Choose whether to also save a png with the same filename
showplot : bool, default :data:`keyoscacquire.config._show_plot`
Choose whether to show a plot of the trace
"""
if not self._time is None:
if fname is not None:
self.fname = fname
if ext is not None:
self.ext = ext
if savepng is not None:
self.savepng = savepng
if showplot is not None:
self.showplot = showplot
# Remove extenstion if provided in the fname
if self.fname[-4:] in ['.npy', '.csv']:
self.ext = self.fname[-4:]
self.fname = self.fname[:-4]
self.fname = fileio.check_file(self.fname, self.ext)
fileio.plot_trace(self._time, self._values, self._capture_channels, fname=self.fname,
showplot=self.showplot, savepng=self.savepng)
head = self.generate_file_header(additional_line=additional_header_info)
fileio.save_trace(self.fname, self._time, self._values, fileheader=head, ext=self.ext,
print_filename=self.verbose_acquistion, nowarn=nowarn)
else:
print("(!) No trace has been acquired yet, use get_trace()")
_log.info("(!) No trace has been acquired yet, use get_trace()")
[docs] def plot_trace(self):
"""Plot and show the most recent trace"""
if not self._time is None:
fileio.plot_trace(self._time, self._values, self._capture_channels,
savepng=False, showplot=True)
else:
print("(!) No trace has been acquired yet, use get_trace()")
_log.info("(!) No trace has been acquired yet, use get_trace()")
def main():
"""Take a trace using the default settings"""
fname = sys.argv[1] if len(sys.argv) >= 2 else config._filename
ext = config._filetype
with Oscilloscope() as scope:
scope.set_options_get_trace_save(fname, ext)
if __name__ == '__main__':
main()