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

examples: Update VISA examples to use the new session management API #446

Merged
merged 7 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion examples/nivisa_dmm_measurement/_visa_dmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@
_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"]
_SUPPORTED_INSTRUMENT_IDS = [
# Keysight/Agilent/HP 34401A
"34401",
"34410",
"34411",
"L4411",
# NI Instrument Simulator v2.0
dixonjoel marked this conversation as resolved.
Show resolved Hide resolved
"Instrument Simulator", # single instrument
"Waveform Generator Simulator", # multi-instrument
]


class Function(Enum):
Expand Down
48 changes: 23 additions & 25 deletions examples/nivisa_dmm_measurement/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@
import logging
import pathlib
import sys
from typing import Any, Tuple
from typing import Tuple

import click
import ni_measurementlink_service as nims
from _constants import USE_SIMULATION
from _helpers import (
ServiceOptions,
configure_logging,
create_session_management_client,
get_service_options,
use_simulation_option,
verbosity_option,
)
from _helpers import configure_logging, verbosity_option
from _visa_dmm import INSTRUMENT_TYPE_VISA_DMM, Function, Session
from decouple import AutoConfig
from ni_measurementlink_service.session_management import SessionInformation

script_or_exe = sys.executable if getattr(sys, "frozen", False) else __file__
service_directory = pathlib.Path(script_or_exe).resolve().parent
Expand All @@ -25,7 +19,10 @@
version="0.1.0.0",
ui_file_paths=[service_directory / "NIVisaDmmMeasurement.measui"],
)
service_options = ServiceOptions()

# Search for the `.env` file starting with the current directory.
_config = AutoConfig(str(pathlib.Path.cwd()))
_VISA_DMM_SIMULATE: bool = _config("MEASUREMENTLINK_VISA_DMM_SIMULATE", default=False, cast=bool)


@measurement_service.register_measurement
Expand Down Expand Up @@ -53,31 +50,32 @@ def measure(
resolution_digits,
)

session_management_client = create_session_management_client(measurement_service)

with session_management_client.reserve_session(
context=measurement_service.context.pin_map_context,
pin_or_relay_names=[pin_name],
) as reservation:
with Session(
reservation.session_info.resource_name,
simulate=service_options.use_simulation,
) as session:
with measurement_service.context.reserve_session(pin_name) as reservation:
with reservation.create_session(_create_session, INSTRUMENT_TYPE_VISA_DMM) as session_info:
session = session_info.session
session.configure_measurement_digits(measurement_type, range, resolution_digits)
measured_value = session.read()

logging.info("Completed measurement: measured_value=%g", measured_value)
return (measured_value,)


def _create_session(session_info: SessionInformation) -> Session:
# When this measurement is called from outside of TestStand (session_exists
# == False), reset the instrument to a known state. In TestStand,
# ProcessSetup resets the instrument.
return Session(
session_info.resource_name,
reset_device=not session_info.session_exists,
simulate=_VISA_DMM_SIMULATE,
)


@click.command
@verbosity_option
@use_simulation_option(default=USE_SIMULATION)
def main(verbosity: int, **kwargs: Any) -> None:
def main(verbosity: int) -> None:
"""Perform a DMM measurement using NI-VISA and an NI Instrument Simulator v2.0."""
configure_logging(verbosity)
global service_options
service_options = get_service_options(**kwargs)

with measurement_service.host_service():
input("Press enter to close the measurement service.\n")
Expand Down
10 changes: 9 additions & 1 deletion examples/nivisa_dmm_measurement/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ PyVISA = "^1.13.0"
PyVISA-sim = "^0.5.1"
click = ">=7.1.2, !=8.1.4" # mypy fails with click 8.1.4: https://github.com/pallets/click/issues/2558
grpcio = "*"
python-decouple = ">=3.8"
bkeryan marked this conversation as resolved.
Show resolved Hide resolved

[tool.poetry.group.dev.dependencies]
ni-python-styleguide = ">=0.4.1"
Expand All @@ -24,4 +25,11 @@ requires = ["poetry-core>=1.2.0"]
build-backend = "poetry.core.masonry.api"

[tool.mypy]
disallow_untyped_defs = true
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = [
# https://github.com/HBNetwork/python-decouple/issues/122 - Add support for type stubs
"decouple.*",
]
ignore_missing_imports = true
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<NIDCPowerInstrument name="DCPower1" numberOfChannels="4">
<ChannelGroup name="CommonDCPowerChannelGroup" />
</NIDCPowerInstrument>
<Instrument name="GPIB0::3::INSTR" instrumentTypeId="DigitalMultimeterSimulator">
<Instrument name="GPIB0::3::INSTR" instrumentTypeId="VisaDmm">
<Channel id="0" />
</Instrument>
</Instruments>
Expand Down
145 changes: 145 additions & 0 deletions examples/output_voltage_measurement/_visa_dmm.py
bkeryan marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""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 = [
# Keysight/Agilent/HP 34401A
"34401",
"34410",
"34411",
"L4411",
# NI Instrument Simulator v2.0
"Instrument Simulator", # single instrument
"Waveform Generator Simulator", # multi-instrument
]


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/output_voltage_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
Loading
Loading