Source code for instro.psu.drivers.rigol_dp800

"""Rigol DP800-series PSU driver. Covers DP811, DP821, DP831, DP832."""

from dataclasses import dataclass

from instro.lib.exceptions import FeatureNotSupportedError
from instro.lib.transports.visa import VisaConfig, VisaDriver
from instro.psu import OperatingMode, PSUDriverBase

# Regulation state in the low two bits of :STAT:QUES:INST:ISUM<n>:COND? (bit 2 is OVP, bit 3 OCP).
_CONDITION_MODES = {
    0: OperatingMode.OFF,
    1: OperatingMode.CONSTANT_CURRENT,
    2: OperatingMode.CONSTANT_VOLTAGE,
    3: OperatingMode.UNREGULATED,
}


@dataclass(frozen=True)
class _Range:
    minimum: float
    maximum: float


@dataclass(frozen=True)
class _ChannelLimits:
    voltage: _Range
    current: _Range
    ovp: _Range
    ocp: _Range


[docs] class RigolDP800(PSUDriverBase): """Rigol DP800-series multi-channel PSU (DP811/DP821/DP831/DP832).""" FRIENDLY_NAME = "Rigol DP800-series PSU" def __init__(self, visa_resource: str | VisaConfig) -> None: self._visa = VisaDriver(visa_resource) self.idn = "" self._limits: dict[int, _ChannelLimits] = {}
[docs] def open(self) -> None: self._visa.open() self._load_limits()
[docs] def close(self) -> None: self._visa.close()
[docs] def set_voltage(self, voltage: float, channel: int) -> None: if (limits := self._limits.get(channel)) is not None: self._validate_in_range(voltage, limits.voltage, channel, "voltage") self._write_checked(f":SOUR{channel}:VOLT {voltage:.3f}")
[docs] def get_voltage(self, channel: int) -> float: return self._query_checked_float(f":MEAS:VOLT? CH{channel}")
[docs] def set_current_limit(self, current_limit: float, channel: int) -> None: if (limits := self._limits.get(channel)) is not None: self._validate_in_range(current_limit, limits.current, channel, "current limit") self._write_checked(f":SOUR{channel}:CURR {current_limit:.3f}")
[docs] def get_current(self, channel: int) -> float: return self._query_checked_float(f":MEAS:CURR? CH{channel}")
[docs] def output_enable(self, enable: bool, channel: int) -> None: cmd = f":OUTP CH{channel},ON" if enable else f":OUTP CH{channel},OFF" self._write_checked(cmd)
[docs] def get_output_status(self, channel: int) -> bool: return self._query_checked_bool(f":OUTP? CH{channel}")
[docs] def get_operating_mode(self, channel: int) -> OperatingMode: """Query the regulation state of ``channel``. Args: channel: 1-based output channel. Returns: OperatingMode: ``OFF``, ``CONSTANT_CURRENT``, ``CONSTANT_VOLTAGE``, or ``UNREGULATED``. Raises: RuntimeError: On a non-numeric register reply or a SCPI error. """ reply = self._query_checked(f":STAT:QUES:INST:ISUM{channel}:COND?").strip() try: condition = int(reply) except ValueError: raise RuntimeError(f"Unexpected Rigol condition register for channel {channel}: {reply}") from None return _CONDITION_MODES[condition & 3]
[docs] def get_voltage_setpoint(self, channel: int) -> float: return self._query_checked_float(f":SOUR{channel}:VOLT?")
[docs] def get_current_setpoint(self, channel: int) -> float: return self._query_checked_float(f":SOUR{channel}:CURR?")
[docs] def set_overvoltage_protection_level(self, voltage: float, channel: int) -> None: if (limits := self._limits.get(channel)) is not None: self._validate_in_range(voltage, limits.ovp, channel, "overvoltage protection level") self._write_checked(f":SOUR{channel}:VOLT:PROT {voltage:.3f}")
[docs] def get_overvoltage_protection_level(self, channel: int) -> float: return self._query_checked_float(f":SOUR{channel}:VOLT:PROT:LEV?")
[docs] def set_overvoltage_protection_enabled(self, enabled: bool, channel: int) -> None: self._write_checked(f":SOUR{channel}:VOLT:PROT:STAT {'ON' if enabled else 'OFF'}")
[docs] def get_overvoltage_protection_enabled(self, channel: int) -> bool: return self._query_checked_bool(f":SOUR{channel}:VOLT:PROT:STAT?")
[docs] def set_overvoltage_protection_delay(self, delay: float, channel: int) -> None: raise FeatureNotSupportedError( f"set_overvoltage_protection_delay is not supported by the {self.FRIENDLY_NAME}; " "the DP800 programming guide does not define an OVP delay command" )
[docs] def get_overvoltage_protection_delay(self, channel: int) -> float: raise FeatureNotSupportedError( f"get_overvoltage_protection_delay is not supported by the {self.FRIENDLY_NAME}; " "the DP800 programming guide does not define an OVP delay query" )
[docs] def set_overcurrent_protection_level(self, current: float, channel: int) -> None: if (limits := self._limits.get(channel)) is not None: self._validate_in_range(current, limits.ocp, channel, "overcurrent protection level") self._write_checked(f":SOUR{channel}:CURR:PROT {current:.3f}")
[docs] def get_overcurrent_protection_level(self, channel: int) -> float: return self._query_checked_float(f":SOUR{channel}:CURR:PROT:LEV?")
[docs] def set_overcurrent_protection_enabled(self, enabled: bool, channel: int) -> None: self._write_checked(f":SOUR{channel}:CURR:PROT:STAT {'ON' if enabled else 'OFF'}")
[docs] def get_overcurrent_protection_enabled(self, channel: int) -> bool: return self._query_checked_bool(f":SOUR{channel}:CURR:PROT:STAT?")
[docs] def clear_protection(self, channel: int) -> None: self._write_checked(f":OUTP:OVP:CLEAR CH{channel}") self._write_checked(f":OUTP:OCP:CLEAR CH{channel}")
[docs] def get_overvoltage_protection_tripped(self, channel: int) -> bool: return self._query_checked_tripped(f":OUTP:OVP:QUES? CH{channel}")
[docs] def get_overcurrent_protection_tripped(self, channel: int) -> bool: return self._query_checked_tripped(f":OUTP:OCP:QUES? CH{channel}")
[docs] def set_remote_sense_enabled(self, enabled: bool, channel: int) -> None: self._write_checked(f":OUTP:SENS CH{channel},{'ON' if enabled else 'OFF'}")
[docs] def get_remote_sense_enabled(self, channel: int) -> bool: state = self._query_checked(f":OUTP:SENS? CH{channel}").strip().upper() match state: case "ON" | "1": return True case "OFF" | "0": return False case "NONE": raise FeatureNotSupportedError( f"remote sense is not supported by {self.FRIENDLY_NAME} channel {channel}" ) case _: raise RuntimeError(f"Unexpected Rigol remote-sense state for channel {channel}: {state}")
def _channel_count(self) -> int: if not self.idn: self.idn = self._query_checked("*IDN?") if "DP832" in self.idn or "DP831" in self.idn: return 3 if "DP821" in self.idn: return 2 if "DP811" in self.idn or "DP813" in self.idn: return 1 raise RuntimeError(f"Unrecognized Rigol PSU model: {self.idn}") def _load_limits(self) -> None: self._limits = { channel: _ChannelLimits( voltage=self._query_range(f":SOUR{channel}:VOLT?"), current=self._query_range(f":SOUR{channel}:CURR?"), ovp=self._query_range(f":SOUR{channel}:VOLT:PROT:LEV?"), ocp=self._query_range(f":SOUR{channel}:CURR:PROT:LEV?"), ) for channel in range(1, self._channel_count() + 1) } def _query_range(self, command: str) -> _Range: return _Range( minimum=self._query_checked_float(f"{command} MIN"), maximum=self._query_checked_float(f"{command} MAX"), ) def _validate_in_range(self, value: float, limit: _Range, channel: int, label: str) -> None: if limit.minimum <= value <= limit.maximum: return raise ValueError( f"{label} {value} is out of range for {self.FRIENDLY_NAME} channel {channel}: " f"{limit.minimum} to {limit.maximum}" ) def _write_checked(self, command: str) -> None: with self._visa.lock(): self._visa.write(command) self._check_errors() def _query_checked(self, command: str) -> str: with self._visa.lock(): value = self._visa.query(command) self._check_errors() return value def _query_checked_float(self, command: str) -> float: return float(self._query_checked(command)) def _query_checked_bool(self, command: str) -> bool: return self._query_checked(command).strip().upper() in {"1", "ON"} def _query_checked_tripped(self, command: str) -> bool: reply = self._query_checked(command).strip().upper() match reply: case "YES": return True case "NO": return False case _: raise RuntimeError(f"Unexpected {self.FRIENDLY_NAME} reply to {command}: {reply}") def _check_errors(self) -> None: err = self._visa.query(":SYST:ERR?") code = err.strip().split(",", 1)[0].lstrip("+") if code != "0": raise RuntimeError(f"The {self.FRIENDLY_NAME} reported error: {err.strip()}")