Source code for inspy.instrument.powersupply.aimtti_psu

"""Aim TTi Power Supply.

NOTE: The USB interface to some TTi power supplies doesn't flush correctly on
Linux, instead use the RS-232 or Ethernet interface.

NOTE: The Ethernet interface requires a raw socket using a VISA address like:
TCPIP0::169.254.136.181::9221::SOCKET

Example VISA resource names:

- RS-232: ``ASRL/dev/ttyUSB0``
- USB: ``ASRL/dev/ttyACM0``
- Ethernet: ``TCPIP0::169.254.136.181::9221::SOCKET``

"""

from time import monotonic
from typing import Literal

from pyvisa.errors import VisaIOError

from inspy.instrument._base import VisaInstrumentMixin

from ._base import PowerSupply


class PsuAimTTiBase(VisaInstrumentMixin, PowerSupply):
    """Aim TTi Power supply base class.

    Based on Aim TTi CPX400DP power supply
    Should work with other TTi power supplies, but not tested yet.

    """

    reset_timeout = 5.0

    def set_channel_state(self, enable: bool = True, channel: int = 1) -> None:
        """Set a specific channel on or off."""
        assert channel in self.channels, self.channel_error_message

        self.transport.command(f"OP{channel} {1 if enable else 0}")

    def get_channel_state(self, channel: int = 1) -> bool:
        """Get info on whether a channel is on or off."""
        assert channel in self.channels, self.channel_error_message

        return bool(int(self.transport.query(f"OP{channel}?")))

    def set_voltage(self, voltage: float, channel: int = 1) -> None:
        """Set the voltage of the power supply."""
        assert channel in self.channels, self.channel_error_message

        self.transport.command(f"V{channel} {voltage:.2f}")

    def get_voltage(self, channel: int = 1) -> float:
        """Set the voltage of the power supply."""
        assert channel in self.channels, self.channel_error_message

        # The query returns a string with the format "V1 5.00V"
        # We split the string using partition which results in [V1, ' ', 5.00V]
        # We then take the last element of the partition and convert it to a float
        return float(self.transport.query(f"V{channel}?").partition(" ")[2])

    def set_current(self, current: float, channel: int = 1) -> None:
        """Set the current of the power supply."""
        assert channel in self.channels, self.channel_error_message

        self.transport.command(f"I{channel} {current:.2f}")

    def get_current(self, channel: int = 1) -> float:
        """Get the current of the power supply."""
        assert channel in self.channels, self.channel_error_message

        # The query returns a string with the format "I1 5.00A"
        # We split the string using partition which results in [I1, ' ', 5.00A]
        # We then take the last element of the partition and convert it to a float
        return float(self.transport.query(f"I{channel}?").partition(" ")[2])

    def measure_voltage(self, channel: int = 1) -> float:
        """Get the measured output voltage of the power supply on a specific channel."""
        assert channel in self.channels, self.channel_error_message

        # The query returns a string with the format "5.00V"
        # We take a substring of the query result excluding the last 'V' character
        return float(self.transport.query(f"V{channel}O?")[:-1])

    def measure_current(self, channel: int = 1) -> float:
        """Get the measured output current of the power supply on a specific channel."""
        assert channel in self.channels, self.channel_error_message

        # The query returns a string with the format "5.00A"
        # We take a substring of the query result excluding the last 'A' character
        return float(self.transport.query(f"I{channel}O?")[:-1])

    def set_output_voltage_trip_point(self, voltage: float, channel: int = 1) -> None:
        """Set the output voltage value at which to trip the power supply.

        This is for a specific channel.
        """
        assert channel in self.channels, self.channel_error_message

        self.transport.command(f"OVP{channel} {voltage:.2f}")

    def get_output_voltage_trip_point(self, channel: int = 1) -> float:
        """Get the output voltage value for tripping the power supply.

        This is for a specific channel.
        """
        assert channel in self.channels, self.channel_error_message

        return float(self.transport.query(f"OVP{channel}?"))

    def set_output_current_trip_point(self, current: float, channel: int = 1) -> None:
        """Set the output current value at which to trip the power supply.

        This is for a specific channel.
        """
        assert channel in self.channels, self.channel_error_message

        self.transport.command(f"OCP{channel} {current:.2f}")

    def get_output_current_trip_point(self, channel: int = 1) -> float:
        """Get the output current value at which to trip the power supply.

        This is for a specific channel.
        """
        assert channel in self.channels, self.channel_error_message

        return float(self.transport.query(f"OCP{channel}?"))

    def await_completion(self) -> None:
        """Wait for the power supply to complete its operation."""
        self.transport.query("*OPC?")

    def reset(self) -> None:
        """Reset the power supply."""
        self.transport.clear()
        self.transport.command("*CLS")
        self.transport.command("*RST")

        # Poll await_completion until reset done
        deadline = monotonic() + self.reset_timeout
        while True:
            self.transport.clear()
            try:
                self.await_completion()
                return
            except VisaIOError:
                if monotonic() >= deadline:
                    raise


[docs] class PsuAimTTIPL303P(PsuAimTTiBase): """Power Supply instrument class for the Aim TTi PL303P. AimTTiPSUBase was created for this instrument, this class inherits all methods It adds checks for single channel and adds two new method (set_current_range, get_current_range) This power supply has 1 channel `Aim TTi PL Series User Manual <https://resources.aimtti.com/manuals/New_PL+PL-P_Series_Instruction_Manual-Iss15.pdf>`_ """ # noqa: E501 channels = [1]
[docs] def set_current_range( self, current_range: Literal["Low", "High"], channel: int = 1 ) -> None: """Set the current range of the channel. Requires channel to be switched off before changing. Args: current_range: Low current range or high current range channel: Channel to set range of. Raises: Exception: Changing current range requires channel to be disabled! ValueError: Invalid value for range """ assert channel in self.channels, self.channel_error_message assert self.get_channel_state(channel) is False, ( "Changing current range requires channel to be disabled!" ) if current_range == "Low": self.transport.command(f"IRANGE{channel} 1") elif current_range == "High": self.transport.command(f"IRANGE{channel} 2") else: raise ValueError("Invalid value for current range. Must be 'Low' or 'High'")
[docs] def get_current_range(self, channel: int = 1) -> str: """Get the current range of the channel. Args: channel: Channel to get range of. Raises: ValueError: Invalid value for current range Returns: Current range (Low or High) """ assert channel in self.channels, self.channel_error_message res = int(self.transport.query(f"IRANGE{channel}?")) if res == 1: return "Low" elif res == 2: return "High" else: raise ValueError(f"Invalid value for current range: {res}")
[docs] class PsuAimTTiPL303QMDP(PsuAimTTiBase): """Power Supply instrument class for the Aim TTi PL303QMDP. This class inherits all methods from AimTTiPSUBase. It adds two new mthods (set_current_range, get_current_range) This power supply has 2 channels. CAUTION! The channels have been swapped to match all other standard power supplies: Left hand side = Channel 1, Right hand side = Channel 2 The SCPI programming manual will state CH1 -> Master which is on the right hand side. Due to confusion we have resolved this by applying a conversion for this Class. `Aim TTi PL Series User Manual <https://resources.aimtti.com/manuals/New_PL+PL-P_Series_Instruction_Manual-Iss15.pdf>`_ """ # noqa: E501 channels = [1, 2] swap = {1: 2, 2: 1}
[docs] def set_channel_state(self, enable: bool = True, channel: int = 1) -> None: """Set a specific channel on or off.""" super().set_channel_state(enable, self.swap[channel])
[docs] def get_channel_state(self, channel: int = 1) -> bool: """Get info on whether a channel is on or off.""" return super().get_channel_state(self.swap[channel])
[docs] def set_voltage(self, voltage: float, channel: int = 1) -> None: """Set the voltage of the power supply.""" super().set_voltage(voltage, self.swap[channel])
[docs] def get_voltage(self, channel: int = 1) -> float: """Set the voltage of the power supply.""" return super().get_voltage(self.swap[channel])
[docs] def set_current(self, current: float, channel: int = 1) -> None: """Set the current of the power supply.""" super().set_current(current, self.swap[channel])
[docs] def get_current(self, channel: int = 1) -> float: """Get the current of the power supply.""" return super().get_current(self.swap[channel])
[docs] def measure_voltage(self, channel: int = 1) -> float: """Get the measured output voltage of the power supply on a specific channel.""" return super().measure_voltage(self.swap[channel])
[docs] def measure_current(self, channel: int = 1) -> float: """Get the measured output current of the power supply on a specific channel.""" return super().measure_current(self.swap[channel])
[docs] def set_output_voltage_trip_point(self, voltage: float, channel: int = 1) -> None: """Set the output voltage value at which to trip the power supply. This is for a specific channel. """ super().set_output_voltage_trip_point(voltage, self.swap[channel])
[docs] def get_output_voltage_trip_point(self, channel: int = 1) -> float: """Get the output voltage value for tripping the power supply. This is for a specific channel. """ return super().get_output_voltage_trip_point(self.swap[channel])
[docs] def set_output_current_trip_point(self, current: float, channel: int = 1) -> None: """Set the output current value at which to trip the power supply. This is for a specific channel. """ super().set_output_current_trip_point(current, self.swap[channel])
[docs] def get_output_current_trip_point(self, channel: int = 1) -> float: """Get the output current value at which to trip the power supply. This is for a specific channel. """ return super().get_output_current_trip_point(self.swap[channel])
[docs] def set_current_range( self, current_range: Literal["Low", "High"], channel: int = 1 ) -> None: """Set the current range of the channel. Requires channel to be switched off before changing. Args: current_range: Low current range or high current range channel: Channel to set range of. Raises: Exception: Changing current range requires channel to be disabled! ValueError: Invalid value for range """ assert channel in self.channels, self.channel_error_message assert self.get_channel_state(channel) is False, ( "Changing current range requires channel to be disabled!" ) channel = self.swap[channel] if current_range == "Low": self.transport.command(f"IRANGE{channel} 1") elif current_range == "High": self.transport.command(f"IRANGE{channel} 2") else: raise ValueError("Invalid value for current range. Must be 'Low' or 'High'")
[docs] def get_current_range(self, channel: int = 1) -> str: """Get the current range of the channel. Args: channel: Channel to get range of. Raises: ValueError: Invalid value for current range Returns: Current range (Low or High) """ assert channel in self.channels, self.channel_error_message channel = self.swap[channel] res = int(self.transport.query(f"IRANGE{channel}?")) if res == 1: return "Low" elif res == 2: return "High" else: raise ValueError(f"Invalid value for current range: {res}")