From 144e80f87a01a82e50935650766691692fe95ce1 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Fri, 16 May 2025 23:48:11 +0000 Subject: [PATCH] Add multimeter simulator Implement SCPI-based virtual DMM with DC voltage and current measurement. Supports MEAS, CONF, and READ commands. Integrates with physics engine for DUT output measurements. --- .../simulation/virtual/multimeter.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/py_dvt_ate/simulation/virtual/multimeter.py diff --git a/src/py_dvt_ate/simulation/virtual/multimeter.py b/src/py_dvt_ate/simulation/virtual/multimeter.py new file mode 100644 index 0000000..aa0a076 --- /dev/null +++ b/src/py_dvt_ate/simulation/virtual/multimeter.py @@ -0,0 +1,213 @@ +"""Virtual digital multimeter (DMM) simulator. + +This module implements a SCPI-based virtual multimeter that interfaces +with the physics engine to measure DUT electrical parameters. +""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from py_dvt_ate.instruments.scpi import SCPICommand +from py_dvt_ate.simulation.virtual.base import BaseInstrument + +if TYPE_CHECKING: + from py_dvt_ate.simulation.physics.engine import PhysicsEngine + + +class MeasurementFunction(Enum): + """Available measurement functions.""" + + VOLTAGE_DC = "VOLT:DC" + CURRENT_DC = "CURR:DC" + + +class MultimeterSim(BaseInstrument): + """Virtual digital multimeter simulator. + + Simulates a digital multimeter with SCPI control interface. The DMM + measures DUT output voltage and load current via the physics engine. + + SCPI Commands: + MEAS:VOLT:DC? - Measure DC voltage (shortcut) + MEAS:CURR:DC? - Measure DC current (shortcut) + CONF:VOLT:DC - Configure for DC voltage measurement + CONF:CURR:DC - Configure for DC current measurement + CONF? - Query current configuration + READ? - Take measurement with current configuration + + Attributes: + manufacturer: "PyDVTATE" + model: "DMM-SIM-001" + """ + + manufacturer = "PyDVTATE" + model = "DMM-SIM-001" + serial_number = "DMMSIM001" + firmware_version = "1.0.0" + + def __init__(self, physics_engine: PhysicsEngine | None = None) -> None: + """Initialise the multimeter simulator. + + Args: + physics_engine: Reference to physics engine for measurement values. + """ + self._function = MeasurementFunction.VOLTAGE_DC + super().__init__(physics_engine) + + def _setup_commands(self) -> None: + """Register multimeter SCPI commands.""" + self.register_command("MEAS:VOLT:DC", self._handle_meas_volt_dc) + self.register_command("MEAS:CURR:DC", self._handle_meas_curr_dc) + self.register_command("CONF:VOLT:DC", self._handle_conf_volt_dc) + self.register_command("CONF:CURR:DC", self._handle_conf_curr_dc) + self.register_command("CONF", self._handle_conf) + self.register_command("READ", self._handle_read) + + def reset(self) -> None: + """Reset multimeter to default state.""" + self._function = MeasurementFunction.VOLTAGE_DC + + def _handle_meas_volt_dc(self, command: SCPICommand) -> str: + """Handle MEAS:VOLT:DC? query. + + Configures for DC voltage and takes measurement in one command. + + Args: + command: Parsed SCPI command. + + Returns: + Measured DC voltage. + + Raises: + ValueError: If used as command (not query). + """ + if not command.is_query: + raise ValueError("MEAS:VOLT:DC is query only") + + self._function = MeasurementFunction.VOLTAGE_DC + return self._measure_voltage_dc() + + def _handle_meas_curr_dc(self, command: SCPICommand) -> str: + """Handle MEAS:CURR:DC? query. + + Configures for DC current and takes measurement in one command. + + Args: + command: Parsed SCPI command. + + Returns: + Measured DC current. + + Raises: + ValueError: If used as command (not query). + """ + if not command.is_query: + raise ValueError("MEAS:CURR:DC is query only") + + self._function = MeasurementFunction.CURRENT_DC + return self._measure_current_dc() + + def _handle_conf_volt_dc(self, command: SCPICommand) -> str: + """Handle CONF:VOLT:DC command. + + Configures multimeter for DC voltage measurement. + + Args: + command: Parsed SCPI command. + + Returns: + Empty string (no response for configuration). + + Raises: + ValueError: If used as query. + """ + if command.is_query: + raise ValueError("CONF:VOLT:DC is command only") + + self._function = MeasurementFunction.VOLTAGE_DC + return "" + + def _handle_conf_curr_dc(self, command: SCPICommand) -> str: + """Handle CONF:CURR:DC command. + + Configures multimeter for DC current measurement. + + Args: + command: Parsed SCPI command. + + Returns: + Empty string (no response for configuration). + + Raises: + ValueError: If used as query. + """ + if command.is_query: + raise ValueError("CONF:CURR:DC is command only") + + self._function = MeasurementFunction.CURRENT_DC + return "" + + def _handle_conf(self, command: SCPICommand) -> str: + """Handle CONF? query. + + Args: + command: Parsed SCPI command. + + Returns: + Current measurement configuration. + + Raises: + ValueError: If used as command without subcommand. + """ + if not command.is_query: + raise ValueError("CONF requires a function (e.g., CONF:VOLT:DC)") + + return f'"{self._function.value}"' + + def _handle_read(self, command: SCPICommand) -> str: + """Handle READ? query. + + Takes measurement using current configuration. + + Args: + command: Parsed SCPI command. + + Returns: + Measured value. + + Raises: + ValueError: If used as command (not query). + """ + if not command.is_query: + raise ValueError("READ is query only") + + if self._function == MeasurementFunction.VOLTAGE_DC: + return self._measure_voltage_dc() + else: + return self._measure_current_dc() + + def _measure_voltage_dc(self) -> str: + """Measure DC voltage from physics engine. + + Returns: + Formatted voltage reading. + """ + if self._physics_engine is None: + return "0.000000" + + electrical_state = self._physics_engine.get_electrical_state() + return f"{electrical_state.output_voltage:.6f}" + + def _measure_current_dc(self) -> str: + """Measure DC current from physics engine. + + Returns: + Formatted current reading. + """ + if self._physics_engine is None: + return "0.000000" + + electrical_state = self._physics_engine.get_electrical_state() + return f"{electrical_state.load_current:.6f}"