From 0a8d7e5c69f152df30adf12967959cd780822443 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 19 Jul 2025 14:08:46 +0000 Subject: [PATCH] Add instrument interface protocols --- src/py_dvt_ate/instruments/__init__.py | 12 + src/py_dvt_ate/instruments/interfaces.py | 362 +++++++++++++++++++++++ 2 files changed, 374 insertions(+) create mode 100644 src/py_dvt_ate/instruments/interfaces.py diff --git a/src/py_dvt_ate/instruments/__init__.py b/src/py_dvt_ate/instruments/__init__.py index fc2da60..48892df 100644 --- a/src/py_dvt_ate/instruments/__init__.py +++ b/src/py_dvt_ate/instruments/__init__.py @@ -7,3 +7,15 @@ This package provides everything needed to communicate with lab instruments: - Instrument drivers - Factory for creating configured instrument sets """ + +from py_dvt_ate.instruments.interfaces import ( + IMultimeter, + IPowerSupply, + IThermalChamber, +) + +__all__ = [ + "IThermalChamber", + "IPowerSupply", + "IMultimeter", +] diff --git a/src/py_dvt_ate/instruments/interfaces.py b/src/py_dvt_ate/instruments/interfaces.py new file mode 100644 index 0000000..db18082 --- /dev/null +++ b/src/py_dvt_ate/instruments/interfaces.py @@ -0,0 +1,362 @@ +"""Instrument interface protocols. + +This module defines the Hardware Abstraction Layer (HAL) interfaces for all +laboratory instruments used in DVT testing. These protocols allow test code +to be written against abstract interfaces rather than concrete implementations, +enabling seamless switching between simulated and real hardware. + +The interfaces use ABC (Abstract Base Classes) for maximum type safety and +explicit interface implementation. All drivers must inherit from these base +classes and implement all abstract methods. +""" + +from abc import ABC, abstractmethod + + +class IThermalChamber(ABC): + """Hardware abstraction for thermal chambers. + + Defines the interface for controlling environmental temperature during + thermal characterisation tests. Implementations may be virtual instruments + (simulators) or real hardware drivers. + + Temperature units are always degrees Celsius. + """ + + @abstractmethod + def set_temperature(self, setpoint: float) -> None: + """Set the chamber temperature setpoint. + + Args: + setpoint: Target temperature in degrees Celsius. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + """ + pass + + @abstractmethod + def get_temperature(self) -> float: + """Get the actual chamber temperature. + + Returns: + Current chamber air temperature in degrees Celsius. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + """ + pass + + @abstractmethod + def get_setpoint(self) -> float: + """Get the current temperature setpoint. + + Returns: + Current target temperature in degrees Celsius. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + """ + pass + + @abstractmethod + def is_stable(self) -> bool: + """Check if chamber temperature is stable. + + Temperature is considered stable when it has settled within + the instrument's configured stability threshold of the setpoint + for a minimum dwell time. + + Returns: + True if temperature is stable, False if still settling. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + """ + pass + + @abstractmethod + def wait_until_stable( + self, timeout: float = 300.0, poll_interval: float = 1.0 + ) -> bool: + """Wait until chamber temperature stabilises. + + Polls the stability status at regular intervals until stable + or timeout is reached. This is a blocking call. + + Args: + timeout: Maximum time to wait in seconds. Default 300s (5 minutes). + poll_interval: Time between stability checks in seconds. Default 1s. + + Returns: + True if temperature stabilised within timeout, False if timed out. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If communication fails. + ValueError: If timeout or poll_interval are invalid. + """ + pass + + @abstractmethod + def set_ramp_rate(self, rate: float) -> None: + """Set the temperature ramp rate. + + Controls how quickly the chamber changes temperature when moving + to a new setpoint. Slower ramp rates reduce thermal shock to DUT. + + Args: + rate: Ramp rate in degrees Celsius per minute. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + ValueError: If rate is negative or exceeds instrument limits. + """ + pass + + +class IPowerSupply(ABC): + """Hardware abstraction for programmable power supplies. + + Defines the interface for controlling DC power supplies during electrical + characterisation tests. Implementations may be virtual instruments + (simulators) or real hardware drivers. + + Voltage units are always volts (V). + Current units are always amps (A). + """ + + @abstractmethod + def set_voltage(self, channel: int, voltage: float) -> None: + """Set the output voltage setpoint. + + Args: + channel: Output channel number (1-based indexing). + voltage: Target voltage in volts. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + ValueError: If channel is invalid or voltage out of range. + """ + pass + + @abstractmethod + def get_voltage(self, channel: int) -> float: + """Get the voltage setpoint. + + Returns the programmed voltage, not the measured output voltage. + Use measure_voltage() to get the actual output voltage. + + Args: + channel: Output channel number (1-based indexing). + + Returns: + Current voltage setpoint in volts. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + @abstractmethod + def set_current_limit(self, channel: int, current: float) -> None: + """Set the current limit. + + The supply will operate in constant voltage mode until output current + reaches this limit, then transition to constant current mode. + + Args: + channel: Output channel number (1-based indexing). + current: Current limit in amps. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + ValueError: If channel is invalid or current out of range. + """ + pass + + @abstractmethod + def get_current_limit(self, channel: int) -> float: + """Get the current limit. + + Args: + channel: Output channel number (1-based indexing). + + Returns: + Current limit in amps. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + @abstractmethod + def measure_voltage(self, channel: int) -> float: + """Measure the actual output voltage. + + Args: + channel: Output channel number (1-based indexing). + + Returns: + Measured output voltage in volts. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + @abstractmethod + def measure_current(self, channel: int) -> float: + """Measure the actual output current. + + Args: + channel: Output channel number (1-based indexing). + + Returns: + Measured output current in amps. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + @abstractmethod + def enable_output(self, channel: int, enable: bool) -> None: + """Enable or disable the output. + + Args: + channel: Output channel number (1-based indexing). + enable: True to enable output, False to disable. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + @abstractmethod + def is_output_enabled(self, channel: int) -> bool: + """Check if output is enabled. + + Args: + channel: Output channel number (1-based indexing). + + Returns: + True if output is enabled, False if disabled. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If channel is invalid. + """ + pass + + +class IMultimeter(ABC): + """Hardware abstraction for digital multimeters. + + Defines the interface for making precision measurements with DMMs during + electrical characterisation tests. Implementations may be virtual instruments + (simulators) or real hardware drivers. + + Voltage units are always volts (V). + Current units are always amps (A). + Resistance units are always ohms (Ω). + """ + + @abstractmethod + def measure_dc_voltage(self, range: str = "AUTO") -> float: + """Measure DC voltage. + + Configures the meter for DC voltage and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + Specific ranges depend on instrument capabilities. + + Returns: + Measured voltage in volts. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If range is invalid for this instrument. + """ + pass + + @abstractmethod + def measure_dc_current(self, range: str = "AUTO") -> float: + """Measure DC current. + + Configures the meter for DC current and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + Specific ranges depend on instrument capabilities. + + Returns: + Measured current in amps. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If range is invalid for this instrument. + """ + pass + + @abstractmethod + def measure_resistance(self, range: str = "AUTO") -> float: + """Measure resistance. + + Configures the meter for resistance and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + Specific ranges depend on instrument capabilities. + + Returns: + Measured resistance in ohms. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If query fails or instrument reports error. + ValueError: If range is invalid for this instrument. + NotImplementedError: If instrument does not support resistance. + """ + pass + + @abstractmethod + def set_integration_time(self, nplc: float) -> None: + """Set the integration time. + + Integration time affects measurement accuracy and speed. Higher + values (more power line cycles) provide better noise rejection + but take longer to measure. + + Args: + nplc: Integration time in number of power line cycles (NPLC). + Typical values: 0.02, 0.2, 1, 10, 100. + + Raises: + ConnectionError: If not connected to instrument. + IOError: If command fails or instrument reports error. + ValueError: If nplc is invalid for this instrument. + NotImplementedError: If instrument does not support integration time. + """ + pass