Source code for msl.equipment.resources.aim_tti.mx_series

"""
Establishes a connection to an MX100QP, MX100TP or MX180TP DC power supply
from `Aim and Thurlby Thandar Instruments`_

.. _Aim and Thurlby Thandar Instruments: https://www.aimtti.com/
"""
from __future__ import annotations

import re
import time

from msl.equipment.connection_prologix import ConnectionPrologix
from msl.equipment.connection_serial import ConnectionSerial
from msl.equipment.connection_socket import ConnectionSocket
from msl.equipment.constants import Interface
from msl.equipment.exceptions import AimTTiError
from msl.equipment.resources import register

EXECUTION_ERROR_CODES = {
    0: ('OK',
        'No error has occurred since this register was last read.'),
    100: ('NumericError',
          'The parameter value sent was outside the permitted range for the command in the present circumstances.'),
    102: ('RecallError',
          'A recall of set up data has been requested but the store specified does not contain any data.'),
    103: ('CommandInvalid',
          'The command is recognised but is not valid in the current circumstances. '
          'Typical examples would be trying to change V2 directly while the outputs are '
          'in voltage tracking mode with V1 as the master.'),
    104: ('RangeChangeError',
          'An operation requiring a range change was requested but could not be completed. '
          'Typically this occurs because >0.5V was still present on output 1 and/or output 2 '
          'terminals at the time the command was executed.'),
    200: ('AccessDenied',
          'An attempt was made to change the instrument\'s settings from an interface which is '
          'locked out of write privileges by a lock held by another interface.')
}


[docs] @register( manufacturer=r'(Aim)?\s*[-&_]?\s*(and)?\s*T(hurlby)?\s*T(handar)?\s*I(nstruments)?', model=r'MX1[80]0[TQ]P', flags=re.IGNORECASE ) class MXSeries(object): def __new__(cls, record): """ Establishes a connection to an MX100QP, MX100TP or MX180TP DC power supply from `Aim and Thurlby Thandar Instruments`_ for different interfaces: * :obj:`.Interface.PROLOGIX` * :obj:`.Interface.SERIAL` * :obj:`.Interface.SOCKET` Do not instantiate this class directly. Use the :meth:`~.EquipmentRecord.connect` method to connect to the equipment. Parameters ---------- record : :class:`~.EquipmentRecord` A record from an :ref:`equipment-database`. """ interface = record.connection.interface if interface == Interface.SOCKET: base = ConnectionSocket elif interface == Interface.SERIAL: base = ConnectionSerial elif interface == Interface.PROLOGIX: base = ConnectionPrologix else: raise AimTTiError('Unsupported interface {!r}'.format(interface)) dict_ = dict((k, v) for k, v in vars(cls).items() if not k.startswith('__')) type_ = type(cls.__name__, (base,), dict_) instance = type_(record) instance.set_exception_class(AimTTiError) return instance
[docs] def clear(self): """Send the clear, ``*CLS``, command.""" self.write('*CLS')
[docs] def decrement_current(self, channel): """Decrement the current limit by step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). See Also -------- :meth:`.set_current_step_size` """ self._write_and_check('DECI{}'.format(channel))
[docs] def decrement_voltage(self, channel, verify=True): """Decrement the voltage by step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). verify : :class:`bool`, optional Whether to verify that the output voltage has stabilized at the decremented voltage before returning to the calling program. See Also -------- :meth:`.set_voltage_step_size` """ command = 'DECV{}{}'.format(channel, 'V' if verify else '') self._write_and_check(command)
[docs] def event_status_register(self, as_integer=True): """Read and clear the standard event status register. Parameters ---------- as_integer : :class:`bool`, optional Whether to return the value as an :class:`int`. Returns ------- :class:`int` or :class:`str` The event status register value. The data type depends on the value of `as_integer`. If a :class:`str` is returned then it will have a length of 8. For example, * '10000000' or the integer value 128 * '00100000' or the integer value 32 """ value = int(self.query('*ESR?')) if as_integer: return value return format(value, '08b')
[docs] def get_current(self, channel): """Get the output current of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The output current, in Amps. """ reply = self._query_and_check('I{}O?'.format(channel)) return float(reply[:-1])
[docs] def get_current_limit(self, channel): """Get the current limit of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The current limit, in Amps. """ reply = self._query_and_check('I{}?'.format(channel)) return float(reply[2:])
[docs] def get_current_step_size(self, channel): """Get the current limit step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The current limit step size, in Amps. """ reply = self._query_and_check('DELTAI{}?'.format(channel)) return float(reply[7:])
[docs] def get_over_current_protection(self, channel): """Get the over-current protection trip point of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` or :data:`None` If the trip point is enabled then returns the trip point value, in Amps. Returns :data:`None` if the over-current protection is disabled. """ reply = self._query_and_check('OCP{}?'.format(channel)) if reply.endswith('OFF'): return return float(reply[3:])
[docs] def get_over_voltage_protection(self, channel): """Get the over-voltage protection trip point of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` or :data:`None` If the trip point is enabled then returns the trip point value, in Volts. Returns :data:`None` if the over-voltage protection is disabled. """ reply = self._query_and_check('OVP{}?'.format(channel)) if reply.endswith('OFF'): return return float(reply[3:])
[docs] def get_voltage(self, channel): """Get the output voltage of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The output voltage, in Volts. """ reply = self._query_and_check('V{}O?'.format(channel)) return float(reply[:-1])
[docs] def get_voltage_range(self, channel): """Get the output voltage range index of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`int` The output voltage range index. See the manual for more details. For example, 2 = 35V/3A. """ return int(self._query_and_check('VRANGE{}?'.format(channel)))
[docs] def get_voltage_setpoint(self, channel): """Get the set-point voltage of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The set-point voltage, in Volts. """ reply = self._query_and_check('V{}?'.format(channel)) return float(reply[2:])
[docs] def get_voltage_step_size(self, channel): """Get the voltage step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`float` The voltage step size, in Volts. """ reply = self._query_and_check('DELTAV{}?'.format(channel)) return float(reply[7:])
[docs] def get_voltage_tracking_mode(self): """Get the voltage tracking mode of the unit. Returns ------- :class:`int` The voltage tracking mode. See the manual for more details. """ return int(self._query_and_check('CONFIG?'))
[docs] def increment_current(self, channel): """Increment the current limit by step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). See Also -------- :meth:`.set_current_step_size` """ self._write_and_check('INCI{}'.format(channel))
[docs] def increment_voltage(self, channel, verify=True): """Increment the voltage by step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). verify : :class:`bool`, optional Whether to verify that the output voltage has stabilized at the incremented voltage before returning to the calling program. See Also -------- :meth:`.set_voltage_step_size` """ command = 'INCV{}{}'.format(channel, 'V' if verify else '') self._write_and_check(command)
[docs] def is_output_on(self, channel): """Check if the output channel is on or off. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). Returns ------- :class:`bool` Whether the output channel is on (:data:`True`) or off (:data:`False`). """ reply = self._query_and_check('OP{}?'.format(channel)) return reply == '1'
[docs] def turn_on(self, channel): """Turn the output channel on. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). """ self._write_and_check('OP{} 1'.format(channel))
[docs] def turn_on_multi(self, options=None): """Turn multiple output channels on (the Multi-On feature). Parameters ---------- options : :class:`dict`, optional Set the Multi-On option for each output channel before setting Multi-On. If not specified then uses the pre-programmed options. If a particular output channel is not included in `options` then uses the pre-programmed option for that channel. The keys are the output channel number and the value can be :data:`False` (set the channel to ``NEVER``, see the manual for more details), :data:`True` (set the channel to ``QUICK``, see the manual for more details) or a delay in milliseconds (as an :class:`int`). Examples: * ``{1: False}`` :math:`\\rightarrow` channel 1 does not turn on * ``{2: 100}`` :math:`\\rightarrow` channel 2 has a 100-ms delay * ``{1: 100, 3: True}`` :math:`\\rightarrow` channel 1 has a 100-ms delay and channel 3 turns on immediately * ``{1: 100, 2: 200, 3: 300}`` :math:`\\rightarrow` channel 1 has a 100-ms delay, channel 2 has a 200-ms delay and channel 3 has a 300-ms delay See Also -------- :meth:`.set_multi_on_delay` :meth:`.set_multi_on_action` """ if options: if not isinstance(options, dict): self.raise_exception('The Multi-On options must be a dict, got {}'.format(type(options))) for channel, value in options.items(): if isinstance(value, bool): self.set_multi_on_action(channel, 'QUICK' if value else 'NEVER') else: self.set_multi_on_action(channel, 'DELAY') self.set_multi_on_delay(channel, value) time.sleep(0.1) # otherwise the power supply may not set the delay properly self._write_and_check('OPALL 1')
[docs] def turn_off(self, channel): """Turn the output channel off. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). """ self._write_and_check('OP{} 0'.format(channel))
[docs] def turn_off_multi(self, options=None): """Turn multiple output channels off (the Multi-Off feature). Parameters ---------- options : :class:`dict`, optional Set the Multi-Off option for each output channel before setting Multi-Off. If not specified then uses the pre-programmed options. If a particular output channel is not included in `options` then uses the pre-programmed option for that channel. The keys are the output channel number and the value can be :data:`False` (set the channel to ``NEVER``, see the manual for more details), :data:`True` (set the channel to ``QUICK``, see the manual for more details) or a delay in milliseconds (as an :class:`int`). Examples: * ``{1: False}`` :math:`\\rightarrow` channel 1 does not turn off * ``{2: 100}`` :math:`\\rightarrow` channel 2 has a 100-ms delay * ``{1: 100, 3: True}`` :math:`\\rightarrow` channel 1 has a 100-ms delay and channel 3 turns off immediately * ``{1: 100, 2: 200, 3: 300}`` :math:`\\rightarrow` channel 1 has a 100-ms delay, channel 2 has a 200-ms delay and channel 3 has a 300-ms delay See Also -------- :meth:`.set_multi_off_delay` :meth:`.set_multi_off_action` """ if options: if not isinstance(options, dict): self.raise_exception('The Multi-Off options must be a dict, got {}'.format(type(options))) for channel, value in options.items(): if isinstance(value, bool): self.set_multi_off_action(channel, 'QUICK' if value else 'NEVER') else: self.set_multi_off_action(channel, 'DELAY') self.set_multi_off_delay(channel, value) time.sleep(0.1) # otherwise the power supply may not set the delay properly self._write_and_check('OPALL 0')
[docs] def recall(self, channel, index): """Recall the settings of the output channel from the store. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). index : :class:`int` The store index number, can be 0-49. See Also -------- :meth:`.save` """ self._write_and_check('RCL{} {}'.format(channel, index))
[docs] def recall_all(self, index): """Recall the settings for all output channels from the store. Parameters ---------- index : :class:`int` The store index number, can be 0-49. See Also -------- :meth:`.save_all` """ self._write_and_check('*SAV {}'.format(index))
[docs] def reset(self): """Send the reset, ``*RST``, command.""" self.write('*RST')
[docs] def reset_trip(self): """Attempt to clear all trip conditions.""" self.write('TRIPRST')
[docs] def save(self, channel, index): """Save the present settings of the output channel to the store. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). index : :class:`int` The store index number, can be 0-49. See Also -------- :meth:`.recall` """ self._write_and_check('SAV{} {}'.format(channel, index))
[docs] def save_all(self, index): """Save the settings of all output channels to the store. Parameters ---------- index : :class:`int` The store index number, can be 0-49. See Also -------- :meth:`.recall_all` """ self._write_and_check('*RCL {}'.format(index))
[docs] def set_current_limit(self, channel, value): """Set the current limit of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). value : :class:`float` The current limit, in Amps. """ self._write_and_check('I{} {}'.format(channel, value))
[docs] def set_current_meter_averaging(self, channel, value): """Set the current meter measurement averaging of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). value : :class:`str` Can be ``ON``, ``OFF``, ``LOW``, ``MED`` or ``HIGH``. """ self._write_and_check('DAMPING{} {}'.format(channel, value))
[docs] def set_current_step_size(self, channel, size): """Set the current limit step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). size : :class:`float` The current limit step size, in Amps. """ self._write_and_check('DELTAI{} {}'.format(channel, size))
[docs] def set_multi_on_action(self, channel, action): """Set the Multi-On action of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). action : :class:`str` The Multi-On action, one of ``QUICK``, ``NEVER`` or ``DELAY``. """ self._write_and_check('ONACTION{} {}'.format(channel, action))
[docs] def set_multi_on_delay(self, channel, delay): """Set the Multi-On delay, in milliseconds, of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). delay : :class:`int` The delay, in milliseconds. """ self._write_and_check('ONDELAY{} {}'.format(channel, delay))
[docs] def set_multi_off_action(self, channel, action): """Set the Multi-Off action of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). action : :class:`str` The Multi-Off action, one of ``QUICK``, ``NEVER`` or ``DELAY``. """ self._write_and_check('OFFACTION{} {}'.format(channel, action))
[docs] def set_multi_off_delay(self, channel, delay): """Set the Multi-Off delay, in milliseconds, of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). delay : :class:`int` The delay, in milliseconds. """ self._write_and_check('OFFDELAY{} {}'.format(channel, delay))
[docs] def set_over_current_protection(self, channel, enable, value=None): """Set the over-current protection trip point of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). enable : :class:`bool` Whether to enable (:data:`True`) or disable (:data:`False`) the over-current protection trip point. value : :class:`float`, optional If the trip point is enabled then you must specify a value, in Amps. """ if enable: if value is None: self.raise_exception('Must specify the trip point value if the trip point is enabled') command = 'OCP{channel} ON;OCP{channel} {value}'.format(channel=channel, value=value) else: command = 'OCP{} OFF'.format(channel) self._write_and_check(command)
[docs] def set_over_voltage_protection(self, channel, enable, value=None): """Set the over-voltage protection trip point of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). enable : :class:`bool` Whether to enable (:data:`True`) or disable (:data:`False`) the over-voltage protection trip point. value : :class:`float`, optional If the trip point is enabled then you must specify a value, in Volts. """ if enable: if value is None: self.raise_exception('Must specify the trip point value if the trip point is enabled') command = 'OVP{channel} ON;OVP{channel} {value}'.format(channel=channel, value=value) else: command = 'OVP{} OFF'.format(channel) self._write_and_check(command)
[docs] def set_voltage(self, channel, value, verify=True): """Set the output voltage of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). value : :class:`float` The value, in Volts. verify : :class:`bool`, optional Whether to verify that the output voltage has stabilized at `value` before returning to the calling program. """ if verify: command = 'V{}V {}'.format(channel, value) else: command = 'V{} {}'.format(channel, value) self._write_and_check(command)
[docs] def set_voltage_range(self, channel, index): """Set the output voltage range of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). index : :class:`int` The output voltage range index. See the manual for more details. For example, 2 = 35V/3A. """ self._write_and_check('VRANGE{} {}'.format(channel, index))
[docs] def set_voltage_step_size(self, channel, size): """Set the voltage step size of the output channel. Parameters ---------- channel : :class:`int` The output channel. The first output channel is 1 (not 0). size : :class:`float` The voltage step size, in Volts. """ self._write_and_check('DELTAV{} {}'.format(channel, size))
[docs] def set_voltage_tracking_mode(self, mode): """Set the voltage tracking mode of the unit. Parameters ---------- mode : :class:`int` The voltage tracking mode. See the manual for more details. """ self._write_and_check('CONFIG {}'.format(mode))
def _check_event_status_register(self, command): """Check the value of the standard event status register for an error. Parameters ---------- command : :class:`str` The command that was sent prior to checking for an error. """ status = self.event_status_register(as_integer=False) # Bit 7 - Power On. Set when power is first applied to the instrument. # Bit 1 and 6 - Not used, permanently 0. # Bit 0 - Operation Complete. Set in response to the *OPC command. bit5, bit4, bit3, bit2 = status[2:-2] if bit5 == '1': # Bit 5 - Command Error err_type = 'CommandError' err_msg = 'A syntax error is detected in a command or parameter' elif bit4 == '1': # Bit 4 - Execution Error error_code = int(self.query('EER?').rstrip()) try: err_type, err_msg = EXECUTION_ERROR_CODES[error_code] except KeyError: err_type = 'UndefinedError' err_msg = 'The error code {} has not been defined in the Python dict'.format(error_code) elif bit3 == '1': # Bit 3 - Verify Timeout Error err_type = 'VerifyTimeoutError' err_msg = 'A parameter has been set with "verify" specified ' \ 'and the value has not been reached within 5 seconds, ' \ 'e.g. the output voltage is slowed by a load with a large capacitance' elif bit2 == '1': # Bit 2 - Query Error err_type = 'QueryError' err_msg = 'The controller has not issued commands and read ' \ 'response messages in the correct sequence' else: return self.raise_exception('{}: {} -> command={!r}'.format(err_type, err_msg, command)) def _query_and_check(self, command): """ Query the command. If there is an error when querying then check the event status register for an error. """ try: return self.query(command).rstrip() except: self._check_event_status_register(command) # if checking the event status register does not raise an exception # then raise the query exception raise def _write_and_check(self, command): """Write the command and check the event status register for an error.""" self.write(command) self._check_event_status_register(command)