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:
213
src/py_dvt_ate/simulation/virtual/multimeter.py
Normal file
213
src/py_dvt_ate/simulation/virtual/multimeter.py
Normal 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}"
|
||||||
Reference in New Issue
Block a user