Source code for instruments.generic_scpi.scpi_instrument

#!/usr/bin/env python
"""
Provides support for SCPI compliant instruments
"""

# IMPORTS #####################################################################

from enum import IntEnum

from instruments.abstract_instruments import Instrument
from instruments.units import ureg as u
from instruments.util_fns import assume_units

# CLASSES #####################################################################


[docs] class SCPIInstrument(Instrument): r""" Base class for all SCPI-compliant instruments. Inherits from from `~instruments.Instrument`. This class does not implement any instrument-specific communication commands. What it does add is several of the generic SCPI star commands. This includes commands such as ``*IDN?``, ``*OPC?``, and ``*RST``. Example usage: >>> import instruments as ik >>> inst = ik.generic_scpi.SCPIInstrument.open_tcpip('192.168.0.2', 8888) >>> print(inst.name) """ # PROPERTIES # @property def name(self): """ The name of the connected instrument, as reported by the standard SCPI command ``*IDN?``. :rtype: `str` """ return self.query("*IDN?") @property def scpi_version(self): """ Returns the version of the SCPI protocol supported by this instrument, as specified by the ``SYST:VERS?`` command described in section 21.21 of the SCPI 1999 standard. """ return self.query("SYST:VERS?") @property def op_complete(self): """ Check if all operations sent to the instrument have been completed. :rtype: `bool` """ result = self.query("*OPC?") return bool(int(result)) @property def power_on_status(self): """ Gets/sets the power on status for the instrument. :type: `bool` """ result = self.query("*PSC?") return bool(int(result)) @power_on_status.setter def power_on_status(self, newval): on = ["on", "1", 1, True] off = ["off", "0", 0, False] if isinstance(newval, str): newval = newval.lower() if newval in on: self.sendcmd("*PSC 1") elif newval in off: self.sendcmd("*PSC 0") else: raise ValueError @property def self_test_ok(self): """ Gets the results of the instrument's self test. This lets you check if the self test was sucessful or not. :rtype: `bool` """ result = self.query("*TST?") try: result = int(result) return result == 0 except ValueError: return False # BASIC SCPI COMMANDS ##
[docs] def reset(self): """ Reset instrument. On many instruments this is a factory reset and will revert all settings to default. """ self.sendcmd("*RST")
[docs] def clear(self): """ Clear instrument. Consult manual for specifics related to that instrument. """ self.sendcmd("*CLS")
[docs] def trigger(self): """ Send a software trigger event to the instrument. On most instruments this will cause some sort of hardware event to start. For example, a multimeter might take a measurement. This software trigger usually performs the same action as a hardware trigger to your instrument. """ self.sendcmd("*TRG")
[docs] def wait_to_continue(self): """ Instruct the instrument to wait until it has completed all received commands before continuing. """ self.sendcmd("*WAI")
# SYSTEM COMMANDS ## @property def line_frequency(self): """ Gets/sets the power line frequency setting for the instrument. :return: The power line frequency :units: Hertz :type: `~pint.Quantity` """ return u.Quantity(float(self.query("SYST:LFR?")), "Hz") @line_frequency.setter def line_frequency(self, newval): self.sendcmd( "SYST:LFR {}".format(assume_units(newval, "Hz").to("Hz").magnitude) ) # ERROR QUEUE HANDLING ## # NOTE: This functionality is still quite incomplete, and could be fleshed # out significantly still. One good thing would be to add handling # for SCPI-defined error codes. # # Another good use of this functionality would be to allow users to # automatically check errors after each command or query.
[docs] class ErrorCodes(IntEnum): """ Enumeration describing error codes as defined by SCPI 1999.0. Error codes that are equal to 0 mod 100 are defined to be *generic*. """ # NOTE: this class may be overriden by subclasses, since the only access # to this enumeration from within SCPIInstrument is by "self," # not by explicit name. Thus, if an instrument supports additional # error codes from the SCPI base, they can be added in a natural # way. no_error = 0 # -100 BLOCK: COMMAND ERRORS ## command_error = -100 invalid_character = -101 syntax_error = -102 invalid_separator = -103 data_type_error = -104 get_not_allowed = -105 # -106 and -107 not specified. parameter_not_allowed = -108 missing_parameter = -109 command_header_error = -110 header_separator_error = -111 program_mnemonic_too_long = -112 undefined_header = -113 header_suffix_out_of_range = -114 unexpected_number_of_parameters = -115 numeric_data_error = -120 invalid_character_in_number = -121 exponent_too_large = -123 too_many_digits = -124 numeric_data_not_allowed = -128 suffix_error = -130 invalid_suffix = -131 suffix_too_long = -134 suffix_not_allowed = -138 character_data_error = -140 invalid_character_data = -141 character_data_too_long = -144 character_data_not_allowed = -148 string_data_error = -150 invalid_string_data = -151 string_data_not_allowed = -158 block_data_error = -160 invalid_block_data = -161 block_data_not_allowed = -168 expression_error = -170 invalid_expression = -171 expression_not_allowed = -178 macro_error = -180 invalid_outside_macro_definition = -181 invalid_inside_macro_definition = -183 macro_parameter_error = -184 # pylint: disable=fixme # TODO: copy over other blocks. # -200 BLOCK: EXECUTION ERRORS ## # -300 BLOCK: DEVICE-SPECIFIC ERRORS ## # Note that device-specific errors also include all positive numbers. # -400 BLOCK: QUERY ERRORS ## # OTHER ERRORS ## #: Raised when the instrument detects that it has been turned from #: off to on. power_on = -500 # Yes, SCPI 1999 defines the instrument turning on as # an error. Yes, this makes my brain hurt. user_request_event = -600 request_control_event = -700 operation_complete = -800
[docs] def check_error_queue(self): """ Checks and clears the error queue for this device, returning a list of :class:`SCPIInstrument.ErrorCodes` or `int` elements for each error reported by the connected instrument. """ # pylint: disable=fixme # TODO: use SYST:ERR:ALL instead of SYST:ERR:CODE:ALL to get # messages as well. Should be just a bit more parsing, but the # SCPI standard isn't clear on how the pairs are represented, # so it'd be helpful to have an example first. err_list = map(int, self.query("SYST:ERR:CODE:ALL?").split(",")) return [ self.ErrorCodes[err] if isinstance(err, self.ErrorCodes) else err for err in err_list if err != self.ErrorCodes.no_error ]
# DISPLAY COMMANDS ## @property def display_brightness(self): """ Brightness of the display on the connected instrument, represented as a float ranging from 0 (dark) to 1 (full brightness). :type: `float` """ return float(self.query("DISP:BRIG?")) @display_brightness.setter def display_brightness(self, newval): if newval < 0 or newval > 1: raise ValueError("Display brightness must be a number between 0" " and 1.") self.sendcmd(f"DISP:BRIG {newval}") @property def display_contrast(self): """ Contrast of the display on the connected instrument, represented as a float ranging from 0 (no contrast) to 1 (full contrast). :type: `float` """ return float(self.query("DISP:CONT?")) @display_contrast.setter def display_contrast(self, newval): if newval < 0 or newval > 1: raise ValueError("Display contrast must be a number between 0" " and 1.") self.sendcmd(f"DISP:CONT {newval}")