Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create visa_dmm custom driver and updated nivisa_dmm_measurement #439

Merged
merged 11 commits into from
Oct 13, 2023
68 changes: 0 additions & 68 deletions examples/nivisa_dmm_measurement/NIInstrumentSimulatorV2_0.yaml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PinMap xmlns="http://www.ni.com/TestStand/SemiconductorModule/PinMap.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="1.1">
<Instruments>
<Instrument name="GPIB0::3::INSTR" instrumentTypeId="DigitalMultimeterSimulator">
<Instrument name="GPIB0::3::INSTR" instrumentTypeId="VisaDmm">
<Channel id="0" />
</Instrument>
</Instruments>
Expand Down
20 changes: 13 additions & 7 deletions examples/nivisa_dmm_measurement/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## NI-VISA DMM Measurement

This is a MeasurementLink example that performs a DMM measurement using NI-VISA
and an NI Instrument Simulator v2.0.
and a DMM that supports SCPI commands.

### Features

Expand All @@ -24,29 +24,35 @@ and an NI Instrument Simulator v2.0.
- NI-VISA
- Optional: NI Instrument Simulator software

Note: there is no Python instrument driver for the NI Instrument Simulator, so
this example directly performs low-level, device-specific commands and queries.
> **Note:**
>
> This example uses the custom instrument driver `_visa_dmm.py` to perform the device-specific commands and queries.

### Required Hardware

By default, this example does not require hardware; it uses PyVISA-sim to
simulate the instrument in software.
[`NIInstrumentSimulatorV2_0.yaml`](./NIInstrumentSimulatorV2_0.yaml) defines the
[`_visa_dmm_sim.yaml`](./_visa_dmm_sim.yaml) defines the
behavior of the simulated instrument.

To use NI Instrument Simulator hardware:
Supported instrument models:
- NI Instrument Simulator v2.0
- HP/Agilent/Keysight 34401A DMM

To use a physical instrument:
- Disable software simulation by setting `USE_SIMULATION = False` in
[`_constants.py`](./_constants.py).
- Connect the NI Instrument Simulator over GPIB or serial.
- Connect the instrument to a supported interface, such as GPIB or serial.
- By default, the pin map included with this example uses the resource name
`GPIB0::3::INSTR`, which matches the NI Instrument Simulator's factory default
settings when connected via GPIB.
- If this doesn't match your configuration, edit
- If this doesn't match your instrument's configuration, edit
[`NIVisaDmmMeasurement.pinmap`](./NIVisaDmmMeasurement.pinmap) and replace
`GPIB0::3::INSTR` with the desired resource name (e.g. `ASRL1::INSTR`).
- To modify the NI Instrument Simulator configuration (e.g. GPIB address,
serial configuration), use the `Instrument Simulator Wizard` included with
the NI Instrument Simulator software.
- To configure third party instruments, see the documentation provided with the instrument.

### Session Management

Expand Down
11 changes: 9 additions & 2 deletions examples/nivisa_dmm_measurement/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def update_pin_map(self, pin_map_path: str) -> str:
pin_map_path_obj = pathlib.Path(pin_map_path)
# By convention, the pin map id is the .pinmap file path.
request = pin_map_service_pb2.UpdatePinMapFromXmlRequest(
pin_map_id=pin_map_path, pin_map_xml=pin_map_path_obj.read_text(encoding="utf-8")
pin_map_id=pin_map_path,
pin_map_xml=pin_map_path_obj.read_text(encoding="utf-8"),
)
response: pin_map_service_pb2.PinMap = self._client.UpdatePinMapFromXml(request)
return response.pin_map_id
Expand Down Expand Up @@ -158,7 +159,13 @@ def resolve_file_path(self, file_path: str) -> str:
"""
if pathlib.Path(file_path).is_absolute():
return file_path
(_, absolute_path, _, _, user_canceled) = self._sequence_context.Engine.FindFileEx(
(
_,
absolute_path,
_,
_,
user_canceled,
) = self._sequence_context.Engine.FindFileEx(
fileToFind=file_path,
absolutePath=None,
srchDirType=None,
Expand Down
136 changes: 136 additions & 0 deletions examples/nivisa_dmm_measurement/_visa_dmm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Custom instrument driver for MeasurementLink NI-VISA DMM examples."""
from __future__ import annotations

import pathlib
import sys
from enum import Enum
from types import TracebackType
from typing import (
TYPE_CHECKING,
Optional,
Type,
)

import pyvisa
import pyvisa.resources
import pyvisa.typing


if TYPE_CHECKING:
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self


# Pin map instrument type constant for VISA DMM
INSTRUMENT_TYPE_VISA_DMM = "VisaDmm"

_SIMULATION_YAML_PATH = pathlib.Path(__file__).resolve().parent / "_visa_dmm_sim.yaml"

_RESOLUTION_DIGITS_TO_VALUE = {"3.5": 0.001, "4.5": 0.0001, "5.5": 1e-5, "6.5": 1e-6}

# Supported NI-VISA DMM instrument IDs, both real and simulated, can be added here
_SUPPORTED_INSTRUMENT_IDS = ["Waveform Generator Simulator", "34401"]


class Function(Enum):
"""Enum that represents the measurement function."""

DC_VOLTS = 0
AC_VOLTS = 1


_FUNCTION_TO_VALUE = {
Function.DC_VOLTS: "VOLT:DC",
Function.AC_VOLTS: "VOLT:AC",
}


class Session:
"""An NI-VISA DMM session."""

def __init__(
self,
resource_name: str,
id_query: bool = True,
reset_device: bool = True,
simulate: bool = False,
) -> None:
"""Open NI-VISA DMM session."""
# Create a real or simulated VISA resource manager."""
visa_library = f"{_SIMULATION_YAML_PATH}@sim" if simulate else ""
resource_manager = pyvisa.ResourceManager(visa_library)

session = resource_manager.open_resource(
resource_name, read_termination="\n", write_termination="\n"
)

if not isinstance(session, pyvisa.resources.MessageBasedResource):
raise TypeError("The 'session' object must be an instance of MessageBasedResource.")
self._session = session

if id_query:
self._validate_id()

if reset_device:
self._reset()

def close(self) -> None:
"""Close the session."""
self._session.close()

def __enter__(self) -> Self:
"""Context management protocol. Returns self."""
return self

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
"""Context management protocol. Calls close()."""
self.close()

def configure_measurement_digits(
self, function: Function, range: float, resolution_digits: float
) -> None:
"""Configure the common properties of the measurement.

These properties include function, range, and resolution_digits.
"""
function_enum = _FUNCTION_TO_VALUE[function]
resolution_value = _RESOLUTION_DIGITS_TO_VALUE[str(resolution_digits)]

self._session.write("CONF:%s %.g,%.g" % (function_enum, range, resolution_value))
self._check_error()

def read(self) -> float:
"""Acquires a single measurement and returns the measured value."""
response = self._session.query("READ?")
self._check_error()
return float(response)

def _check_error(self) -> None:
"""Query the instrument's error queue."""
response = self._session.query("SYST:ERR?")
fields = response.split(",", maxsplit=1)
assert len(fields) >= 1
if int(fields[0]) != 0:
raise RuntimeError("Instrument returned error %s: %s" % (fields[0], fields[1]))

def _validate_id(self) -> None:
"""Check the selected instrument is proper and responding.."""
instrument_id = self._session.query("*IDN?")
if not any(id_check in instrument_id for id_check in _SUPPORTED_INSTRUMENT_IDS):
raise RuntimeError(
"The ID query failed. This may mean that you selected the wrong instrument, your instrument did not respond, "
f"or you are using a model that is not officially supported by this driver. Instrument ID: {instrument_id}"
)

def _reset(self) -> None:
"""Reset the instrument to a known state."""
self._session.write("*CLS")
self._session.write("*RST")
self._check_error()
32 changes: 32 additions & 0 deletions examples/nivisa_dmm_measurement/_visa_dmm_sim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
spec: "1.1"
devices:
VisaDmm:
eom:
GPIB INSTR:
q: "\n"
r: "\n"
dialogues:
- q: "*CLS"
- q: "*IDN?"
r: "National Instruments,Waveform Generator Simulator (simulated with pyvisa-sim),00000000,2.0.1"
- q: "*RST"
- q: "READ?"
r: "1.23456"
error:
error_queue:
- q: 'SYST:ERR?'
default: '0,No Error'
command_error: '-100,Command Error'
query_error: '-400,Query Error'
properties:
configuration:
default: "VOLT:DC 5.000000,0.001000"
getter:
q: "CONF?"
r: "{:s}"
setter:
q: "CONF:{:s}"

resources:
GPIB0::3::INSTR:
device: VisaDmm
62 changes: 0 additions & 62 deletions examples/nivisa_dmm_measurement/_visa_helpers.py

This file was deleted.

Loading