Source code for pyrolab.drivers.scopes.rohdeschwarz

# Copyright © PyroLab Project Contributors
# Licensed under the terms of the GNU GPLv3+ License
# (see pyrolab/__init__.py for details)

"""
Rohde & Schwarz Digital Oscilloscopes
=====================================

Submodule containing drivers for each supported laser type.

As stated in the manual (see docs/RTO_UserManaul_en_11.pdf), the following
scopes should be supported:

This manual describes the following R&S®RTO models with firmware version 3.70:
   - R&S®RTO2002 (1329.7002K02)
   - R&S®RTO2004 (1329.7002K04)
   - R&S®RTO2012 (1329.7002K12)
   - R&S®RTO2014 (1329.7002K14)
   - R&S®RTO1024 (1316.1000K24)
   - R&S®RTO2022 (1329.7002K22)
   - R&S®RTO2024 (1329.7002K24)
   - R&S®RTO2032 (1329.7002K32)
   - R&S®RTO2034 (1329.7002K34)
   - R&S®RTO2044 (1329.7002K44)
   - R&S®RTO2064 (1329.7002K64)
   - R&S®RTO1002 (1316.1000K02)
   - R&S®RTO1004 (1316.1000K04)
   - R&S®RTO1012 (1316.1000K12)
   - R&S®RTO1014 (1316.1000K14)
   - R&S®RTO1022 (1316.1000K22)
   - R&S®RTO1044 (1316.1000K44)

If you don't have the NI VISA implementation installed on your computer, be
sure to install the separate dependency ``pyvisa-py``, which is not included
with PyroLab. NI VISA is available for Mac, Windows, and Linux.

Common Issues
-------------
1. Note that if a trigger is set and you try to acquire data but end up with
a timeout warning, it's possible that the acquisition never began because the
trigger level was never reached. The scope will still be waiting to begin
acquisition, but you'll be left without data and with a bad connection.

.. admonition:: Dependencies
   :class: note

   | pyvisa
   | NI-VISA *or* pyvisa-py
"""

# Current Work
# Check out manual, chapter 19.5.2.3
# https://www.rohde-schwarz.com/us/applications/fast-remote-instrument-control-with-hislip-application-note_56280-30881.html
# https://www.google.com/search?channel=tus5&client=firefox-b-1-d&q=pyvisa+hislip
# https://github.com/pyvisa/pyvisa-py/issues/58

# Even though VISAResourceExtensions is not used in this module, the act of
# importing it alone performs some monkey-patching on the pyvisa module,
# required by RTO. Don't remove this seemingly unused import!

import time

import pyvisa as visa

from pyrolab import __version__
from pyrolab.drivers import VISAResourceExtensions
from pyrolab.drivers.scopes import Scope


[docs] class RTO(Scope): """ Simple network controller class for R&S RTO oscilloscopes. This class is used to control the R&S RTO oscilloscope. These are not local devices, nor native PyroLab objects. Therefore, network device autodetection is not supported. """
[docs] @staticmethod def detect_devices(): """ Network device detection not supported. Becuase R&S oscilloscopes are connected to using the IP address, this function does not detect them and instead always returns an empty list. """ device_info = [] return device_info
[docs] def connect( self, address: str = "", hislip: bool = False, timeout: float = 1e3 ) -> bool: """ Connects to and initializes the R&S RTO oscilloscope. HiSLIP (High-Speed LAN Instrument Protocol) is a TCP/IP-based protocol for remote instrument control of LAN-based test and measurement instruments. It is intended to replace the older VXI-11 protocol. .. warning:: The HiSLIP protocol is not supported when using the pyvisa-py backend **on the client machine**. To use it, you must use the NI VISA implementation instead. Parameters ---------- address : str The IP address of the instrument. hislip : bool, optional Whether to use the HiSLIP protocol or not (default ``False``). timeout : int, optional The device response timeout in milliseconds (default 1 ms). Pass ``None`` for infinite timeout. """ rm = visa.ResourceManager() if hislip: self.device = rm.open_resource(f"TCPIP::{address}::hislip0") else: self.device = rm.open_resource(f"TCPIP::{address}") self.device.timeout = timeout self.write_termination = "" self.device.ext_clear_status() self.write("*RST;*CLS") self.write("SYST:DISP:UPD ON") self.device.ext_error_checking() return True
[docs] def close(self): self.device.close()
@property def timeout(self): """ Network timeout duration in milliseconds (errors out if no response received within timeout). """ return self.device.timeout @timeout.setter def timeout(self, ms): self.device.timeout = ms
[docs] def query(self, message, delay=None): """ A combination of :py:func:`write` and :py:func:`read`. Parameters ---------- message : str The message to send. delay : float, optional Delay in seconds between write and read operations. If None, defaults to ``self.device.query_delay``. """ return self.device.query(message, delay)
[docs] def write(self, message, termination=None, encoding=None): """ Writes a message to the scope. Parameters ---------- message : str The message to send. termination : str, optional Alternative character termination to use. If None, the value of write_termination is used. Defaults to None. encoding : str, optional Alternative encoding to use to turn str into bytes. If None, the value of encoding is used. Defaults to None. """ self.device.write(message, termination, encoding)
[docs] def write_block(self, message): """ Writes a message to the scope, waits for it to complete, and checks for errors. Parameters ---------- message : str The message to send. Notes ----- This function is blocking. """ self.write(message) self.wait_for_device() self.device.ext_error_checking()
[docs] def wait_for_device(self): """ Waits for the device until last action is complete. .. note:: This function is blocking. """ self.device.ext_wait_for_opc()
[docs] def acquisition_settings(self, sample_rate, duration, force_realtime=False): """ Sets the scope acquisition settings. The exact command this executes is: `'ACQ:POIN:AUTO RES;:ACQ:SRAT {};:TIM:RANG {}'` .. warning:: The oscilloscope has a record length limit. This is system dependent! If you are getting cryptic errors, such as "Data out of range;ACQ:SRAT <some value>", you might be exceeding the record length limit, calculated as SAMPLE_RATE x DURATION. The system enforces this limit to "prevent undersampling and ensure a sufficient resolution to acquire the correct waveform if the time scale is changed." Check your specific system to find the record length limit. See also: RTO User Manual Chapter 4.2 (page 147). Parameters ---------- sample_rate : float Sample rate of device in Sa/s. Range is 2 to 20e+12 in increments of 1. duration : float Length of acquisition in seconds. force_realtime : bool, optional Defaults to False. """ short_command = ( f"ACQ:POIN:AUTO RES;:TIM:RANG {duration};:ACQ:SRAT {sample_rate}" ) if force_realtime: self.write_block("ACQ:MODE RTIM") self.write_block(short_command)
[docs] def set_channel( self, channel: int, state: str = "ON", coupling: str = "DCLimit", range: float = 0.5, position: float = 0, offset: float = 0, invert: str = "OFF", ): """ Configures a channel. Parameters ---------- channel : int The channel number to be added. state : str, optional Switches the channel signal on or off. Acceptable values are ``ON`` and ``OFF``. Default is ``ON`` (see ``CHANnel<m>:STATe``). coupling : str, optional Selects the connection of the indicated channel signal. Valid values are "DC" (direct connection with 50 ohm termination), "DCLimit" (direct connection with 1M ohm termination), or "AC" (connection through DC capacitor). Default is "DCLimit" (see ``CHANnel<m>:COUPling``). range : float, optional Sets the voltage range across the 10 vertical divisions of the diagram in V/div. Possible values depend on the coupling: 10 mV to 10 V for 50 ohm coupling, and 10 mV to 100 V for 1 megaohm coupling. Default is 0.5 (see ``CHANnel<m>:RANGe``). position : float, optional Sets the vertical position of the indicated channel as a graphical value. Valid range is [-5, 5] in increments of 0.01 (units is "divisions"). Default is 0 (see ``CHANnel<m>:POSition``). offset : float, optional The offset voltage is subtracted to correct an offset-affected signal. The offset of a signal is determined and set by the autoset procedure. Valid offset values are dependent on input coupling and vertical voltage range. Default value is 0 (see ``CHANnel<m>:OFFSet``). invert : str, optional Turns the inversion of the signal amplitude on or off. To invert means to reflect the voltage values of all signal components against the ground level. If the inverted channel is the trigger source, the instrument triggers on the inverted signal. Acceptable values are "ON" or "OFF". Default is "OFF" (see ``CHANnel<m>:INVert``). Notes ----- The following two tables give the key to the range of possible offset values given the coupling and vertical voltage range. Offset at 50 :math:`\Omega` Coupling ===================================== ================= Vertical Voltage Range Allowed Offset ===================================== ================= 316 mV/div to :math:`\leq` 1 V/div :math:`\pm` 10 V 100 mV/div to :math:`\leq` 316 mV/div :math:`\pm` 3 V 1 mV/div to :math:`\leq` 100 mV/div :math:`\pm` 1 V ===================================== ================= Offset at 1 M\ :math:`\Omega` Coupling ====================================== =========================================================== Vertical Voltage Range Allowed Offset ====================================== =========================================================== 3.16 V/div to :math:`\leq` 10 V/div :math:`\pm` (115 V :math:`-` vertical voltage range :math:`\\times` 5 div) 1 V/div to :math:`\leq` 3.16 V/div :math:`\pm` 100 V 316 mV/div to :math:`\leq` 1 V/div :math:`\pm` (11.5 V :math:`-` vertical voltage range :math:`\\times` 5 div) 100 mV/div to :math:`\leq` 316 mV/div :math:`\pm` 10 V 31.6 mV/div to :math:`\leq` 100 mV/div :math:`\pm` (1.15 V :math:`-` vertical voltage range :math:`\\times` 5 div) 1 mV/div to :math:`\leq` 31.6 mV/div :math:`\pm` 1 V ====================================== =========================================================== """ cmd = "CHAN{}:STAT {}; COUP {};RANG {};POS {};OFFS {}; INV {}".format( channel, state, coupling, range, position, offset, invert ) self.write_block(cmd)
def __add_trigger( self, type, source, source_num, level, mode, trigger_num=1, settings: str = "", ): short_command = "TRIG{}:MODE {};SOUR {};TYPE {};LEV{} {};".format( trigger_num, mode, source, type, source_num, level ) # Add a trigger. self.write_block(short_command + settings)
[docs] def edge_trigger(self, channel: int, voltage: float, mode: str = "NORM") -> None: """ Add an edge trigger to the specified channel. Parameters ---------- channel : int The channel to configure as a trigger. voltage : float Voltage threshold for positive slope edge trigger. """ # Todo: Edit to allow other trigger sources (ex. External Trigger). self.__add_trigger( type="EDGE", source="CHAN{}".format(channel), source_num=channel, level=voltage, mode=mode, settings="EDGE:SLOP POS", )
[docs] def acquire(self, timeout: int = -1, run: str = "single") -> None: """ Asynchronous command that starts acquisition. Parameters ---------- timeout : int The timeout to use for the given acquisition in milliseconds. The default timeout is used if no timeout is given. The timeout is not permanently set; once the acquisition is complete, the original timeout is kept for any other query/write command. For an infinite timeout, pass in None. If not specified, default timeout is the default for `acquire()`. run : str Specifies the type of run. Allowable values are ``continuous`` (starts the continuous acquisition), ``single`` (starts a defined number of acquisition cycles as set by :py:func:`acquisition_settings`), or ``stop`` (stops a running acquisition). Default is ``single``. Raises ------ ValueError If the run type is not one of the allowed values. """ if run == "single": cmd = "SING" elif run == "continuous": cmd = "RUN" elif run == "stop": cmd = "STOP" else: raise ValueError("%s is not a valid argument" % run) if timeout != -1: default_timeout = self.timeout self.timeout = timeout self.write(cmd) if timeout != -1: self.timeout = default_timeout
[docs] def set_timescale(self, time: float) -> None: """ Sets the horizontal scale--the time per division on the x-axis--for all channel and math waveforms. Parameters ---------- time : float The time (in seconds) per division. Valid range is from 25e-12 to 10000 (RTO) | 5000 (RTE) in increments of 1e-12. (`*RST` value is 10e-9). """ self.write(f"TIM:SCAL {str(time)}") self.device.ext_error_checking()
[docs] def set_auto_measurement( self, measurement: int = 1, source: str = "C1W1", meastype: str = "MAX" ) -> None: """ Convenience function for setting default measurements. Use if you haven't configured any of your own measurements. Parameters ---------- measurement : int The oscope supports storing up to 8 measurements. Default is 1. source : str The source to setup the measurement on. See page 1377 of the User Manual for valid sources. Common ones are of the format "C<m>W<n>", where <m> is the channel and <n> is the waveform (e.g., "C1W1", which is the default). meastype : str The measurement type to associate with the measurement. See page 1381 of the User Manual for valid measurement types. Common ones are 'HIGH', 'LOW', 'AMPLitude', 'MAXimum', 'MINimum', 'MEAN'. Default is 'MAX'. """ self.write(f"MEAS{measurement}:SOUR {source}") # "MAX" will measure the max value in the current view window (Based on time base) self.write(f"MEAS{measurement}:MAIN {meastype}") self.write(f"MEAS{measurement} ON")
[docs] def measure(self, measurement: int = 1) -> float: """ Takes the result of an automatic measurement as setup using `set_auto_measurement()`. Parameters ---------- measurement : int The measurement to take. Default is 1. Returns ------- float The result of the automatic measurement. """ return float(self.query(f"MEAS{measurement}:RES:ACT?"))
[docs] def get_data(self, channel, form="ascii"): """ Retrieves waveform data from the specified channel Data is retrieved in the specified data type. Note that data like this can be transferred in two ways: in ASCII form (slow, but human readable) and binary (fast, but more difficult to debug). Parameters ---------- channel : int The channel to retrieve data for. form : str, optional The data format used for the transmission of waveform data. Allowable values are ``ascii``, ``real``, ``int8``, and ``int16``. Default is ``ascii``. Notes ----- RTO User Manual, commands for ``FORMat[:DATA]`` and ``CHANnel<m>[:WAVeform<n>]:DATA[:VALues]?`` """ if form == "ascii": fmt = "ASC" elif form == "real": fmt = "REAL,32" elif form == "int8": fmt = "INT,8" elif form == "int16": fmt = "INT,16" else: raise ValueError("unexpected value '%s' for argument 'form'." % form) cmd = "FORM {};:CHAN{}:DATA?".format(fmt, channel) # Consider skipping the intermediate "list" step and having pyvisa # automatically convert to a numpy array (see # https://pyvisa.readthedocs.io/en/latest/introduction/rvalues.html#reading-ascii-values) if form == "ascii": return self.device.query_ascii_values(cmd) elif form == "real": return self.device.query_binary_values(cmd) else: return self.query(cmd)
[docs] def screenshot(self, path): """ Takes a screenshot of the scope and saves it to the specified path. Image format is PNG. Parameters ---------- path : str The local path, including filename and extension, of where to save the file. """ instrument_save_path = "'C:\\temp\\Last_Screenshot.png'" self.write("HCOP:DEV:LANG PNG") self.write("MMEM:NAME {}".format(instrument_save_path)) self.write("HCOP:IMM") self.wait_for_device() self.device.ext_error_checking() self.device.ext_query_bin_data_to_file( "MMEM:DATA? {}".format(instrument_save_path), str(path) ) self.device.ext_error_checking()
[docs] def act_filter(self, channel): """ Activates the lowpass filter for a channel. Parameters ---------- channel : int The channel (1-4) on which to activate the filter. """ self.write(f"CHAN{channel}:DIGF:STAT ON")
[docs] def deact_filter(self, channel): """ Deactivates the lowpass filter on a given channel. Parameters ---------- channel : int The channel (1-4) where the new cutoff frequency is applied. """ self.write(f"CHAN{channel}:DIGF:STAT OFF")
[docs] def set_cutoff_freq(self, channel, cutoff_freq): """ Sets the cutoff frequency of one of the two filters: either the filter for channels 1 and 2 or the filter for channels 3 and 4. Cutoff frequencies are set only for either channels 1 and 2 or channels 3 and 4, but you can activate the filter for each channel separately. Parameters ---------- channel : int Specifies which filter's cutoff frequency will be changed. A value of 1 or 2 will set the cutoff frequency for both channels, and a value of 3 or 4 will do the same for both channels 3 and 4. cutoff_freq : int The cutoff frequency enabled on the channel. Must be between 100 kHz and 1 GHz or 2 GHz (depending on the scope). The scope only supports certain discrete cutoff frequencies. Any other frequency will be rounded to the closest frequency the scope supports. """ if cutoff_freq >= 1e5 and cutoff_freq <= 2e9: self.write(f"CHAN{channel}:DIGF:CUT {cutoff_freq}") else: raise ValueError("Cutoff frequency must be between 1e5 and 2e9 Hz")
[docs] class RemoteDisplay: def __init__(self, scope: RTO): self.scope = scope