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.
This commit is contained in:
2025-12-02 13:43:56 +00:00
parent 40792c848d
commit 73f792f6bc

View File

@@ -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}"