diff --git a/.env.sample b/.env.sample index 4381aa5c1..4b863abc5 100644 --- a/.env.sample +++ b/.env.sample @@ -5,14 +5,54 @@ # (such as the root of your Git repository or `C:\ProgramData\National # Instruments\MeasurementLink\Services`). # - Rename it to `.env`. -# - Uncomment and edit the settings you want to change. +# - Uncomment and edit the options you want to change. # - Restart any affected services. #---------------------------------------------------------------------- -# Server Configuration +# NI Modular Instrument Options #---------------------------------------------------------------------- -# TBD +# By default, measurement services expect a real device or a simulated device +# configured in NI MAX. +# +# For NI modular instrument drivers, you can also enable simulation via the +# driver's option string. To do this, specify the `options` parameter when +# calling reservation.create_session(s) or uncomment the following options: + +# MEASUREMENTLINK_NIDCPOWER_SIMULATE=1 +# MEASUREMENTLINK_NIDCPOWER_BOARD_TYPE=PXIe +# MEASUREMENTLINK_NIDCPOWER_MODEL=4141 + +# MEASUREMENTLINK_NIDIGITAL_SIMULATE=1 +# MEASUREMENTLINK_NIDIGITAL_BOARD_TYPE=PXIe +# MEASUREMENTLINK_NIDIGITAL_MODEL=6570 + +# MEASUREMENTLINK_NIDMM_SIMULATE=1 +# MEASUREMENTLINK_NIDMM_BOARD_TYPE=PXIe +# MEASUREMENTLINK_NIDMM_MODEL=4081 + +# MEASUREMENTLINK_NIFGEN_SIMULATE=1 +# MEASUREMENTLINK_NIFGEN_BOARD_TYPE=PXIe +# MEASUREMENTLINK_NIFGEN_MODEL=5423 (2CH) + +# MEASUREMENTLINK_NISCOPE_SIMULATE=1 +# MEASUREMENTLINK_NISCOPE_BOARD_TYPE=PXIe +# MEASUREMENTLINK_NISCOPE_MODEL=5162 (4CH) + +# MEASUREMENTLINK_NISWITCH_SIMULATE=1 +# MEASUREMENTLINK_NISWITCH_TOPOLOGY=2567/Independent + +#---------------------------------------------------------------------- +# NI gRPC Device Server Configuration +#---------------------------------------------------------------------- + +# By default, measurement services use the MeasurementLink discovery service to +# activate the NI gRPC Device Server. You probably don't want to change this, +# but if you do, you can uncomment the following options to override this +# behavior: +# +# MEASUREMENTLINK_USE_GRPC_DEVICE=1 +# MEASUREMENTLINK_GRPC_DEVICE_ADDRESS=localhost:31763 #---------------------------------------------------------------------- # Feature Toggles diff --git a/.env.simulation b/.env.simulation new file mode 100644 index 000000000..4387c6193 --- /dev/null +++ b/.env.simulation @@ -0,0 +1,33 @@ +# This is a sample ni-measurementlink-service configuration file that enables +# simulated devices for NI modular instrument drivers. +# +# To use it: +# - Copy this file to your service's directory or one of its parent directories +# (such as the root of your Git repository or `C:\ProgramData\National +# Instruments\MeasurementLink\Services`). +# - Rename it to `.env`. +# - Uncomment and edit the options you want to change. +# - Restart any affected services. + +MEASUREMENTLINK_NIDCPOWER_SIMULATE=1 +MEASUREMENTLINK_NIDCPOWER_BOARD_TYPE=PXIe +MEASUREMENTLINK_NIDCPOWER_MODEL=4141 + +MEASUREMENTLINK_NIDIGITAL_SIMULATE=1 +MEASUREMENTLINK_NIDIGITAL_BOARD_TYPE=PXIe +MEASUREMENTLINK_NIDIGITAL_MODEL=6570 + +MEASUREMENTLINK_NIDMM_SIMULATE=1 +MEASUREMENTLINK_NIDMM_BOARD_TYPE=PXIe +MEASUREMENTLINK_NIDMM_MODEL=4081 + +MEASUREMENTLINK_NIFGEN_SIMULATE=1 +MEASUREMENTLINK_NIFGEN_BOARD_TYPE=PXIe +MEASUREMENTLINK_NIFGEN_MODEL=5423 (2CH) + +MEASUREMENTLINK_NISCOPE_SIMULATE=1 +MEASUREMENTLINK_NISCOPE_BOARD_TYPE=PXIe +MEASUREMENTLINK_NISCOPE_MODEL=5162 (4CH) + +MEASUREMENTLINK_NISWITCH_SIMULATE=1 +MEASUREMENTLINK_NISWITCH_TOPOLOGY=2567/Independent diff --git a/ni_measurementlink_service/_configuration.py b/ni_measurementlink_service/_configuration.py new file mode 100644 index 000000000..f032b721c --- /dev/null +++ b/ni_measurementlink_service/_configuration.py @@ -0,0 +1,108 @@ +"""MeasurementLink configuration options.""" +from __future__ import annotations + +import pathlib +import sys +from typing import TYPE_CHECKING, Any, Callable, Dict, NamedTuple, TypeVar, Union + +from decouple import AutoConfig, Undefined, undefined + +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + + +_PREFIX = "MEASUREMENTLINK" + +# Search for the `.env` file starting with the current directory. +_config = AutoConfig(str(pathlib.Path.cwd())) + +if TYPE_CHECKING: + # Work around decouple's lack of type hints. + _T = TypeVar("_T") + + def _config( + option: str, + default: Union[_T, Undefined] = undefined, + cast: Union[Callable[[str], _T], Undefined] = undefined, + ) -> _T: + ... + + +# ---------------------------------------------------------------------- +# NI Modular Instrument Driver Options +# ---------------------------------------------------------------------- +class MIDriverOptions(NamedTuple): + """Modular instrument driver options.""" + + driver_name: str + """The driver name.""" + + simulate: bool = False + """Specifies whether to simulate session operations.""" + + board_type: str = "" + """The simulated board type (bus).""" + + model: str = "" + """The simulated instrument model.""" + + def update_from_config(self) -> Self: + """Read options from the configuration file and return a new options object.""" + prefix = f"{_PREFIX}_{self.driver_name.upper()}" + return self._replace( + simulate=_config(f"{prefix}_SIMULATE", default=self.simulate, cast=bool), + board_type=_config(f"{prefix}_BOARD_TYPE", default=self.board_type), + model=_config(f"{prefix}_MODEL", default=self.model), + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert options to a dict to pass to nimi-python.""" + options: Dict[str, Any] = {} + if self.simulate: + options["simulate"] = True + if self.board_type or self.model: + options["driver_setup"] = {} + if self.board_type: + options["driver_setup"]["BoardType"] = self.board_type + if self.model: + options["driver_setup"]["Model"] = self.model + return options + + +class NISwitchOptions(NamedTuple): + """NI-SWITCH driver options.""" + + driver_name: str + """The driver name.""" + + simulate: bool = False + """Specifies whether to simulate session operations.""" + + topology: str = "Configured Topology" + """The default topology.""" + + def update_from_config(self) -> Self: + """Read options from the configuration file and return a new options object.""" + prefix = f"{_PREFIX}_{self.driver_name.upper()}" + return self._replace( + simulate=_config(f"{prefix}_SIMULATE", default=self.simulate, cast=bool), + topology=_config(f"{prefix}_TOPOLOGY", default=self.topology), + ) + + +NIDCPOWER_OPTIONS = MIDriverOptions("nidcpower").update_from_config() +NIDIGITAL_OPTIONS = MIDriverOptions("nidigital").update_from_config() +NIDMM_OPTIONS = MIDriverOptions("nidmm").update_from_config() +NIFGEN_OPTIONS = MIDriverOptions("nifgen").update_from_config() +NISCOPE_OPTIONS = MIDriverOptions("niscope").update_from_config() +NISWITCH_OPTIONS = NISwitchOptions("niswitch").update_from_config() + + +# ---------------------------------------------------------------------- +# NI gRPC Device Server Configuration +# ---------------------------------------------------------------------- +USE_GRPC_DEVICE_SERVER: bool = _config(f"{_PREFIX}_USE_GRPC_DEVICE_SERVER", default=True, cast=bool) +GRPC_DEVICE_ADDRESS: str = _config(f"{_PREFIX}_GRPC_DEVICE_ADDRESS", default="") diff --git a/ni_measurementlink_service/_featuretoggles.py b/ni_measurementlink_service/_featuretoggles.py index 6d08ebf36..6ab7c02bd 100644 --- a/ni_measurementlink_service/_featuretoggles.py +++ b/ni_measurementlink_service/_featuretoggles.py @@ -1,12 +1,12 @@ +"""MeasurementLink feature toggles.""" from __future__ import annotations import functools -import pathlib import sys from enum import Enum from typing import TYPE_CHECKING, Callable, TypeVar -from decouple import AutoConfig +from ni_measurementlink_service._configuration import _config, _PREFIX if TYPE_CHECKING: if sys.version_info >= (3, 10): @@ -22,11 +22,6 @@ _P = ParamSpec("_P") _T = TypeVar("_T") -_PREFIX = "MEASUREMENTLINK" - -# Search for the `.env` file starting with the current directory. -_config = AutoConfig(str(pathlib.Path.cwd())) - # Based on the recipe at https://docs.python.org/3/howto/enum.html class _OrderedEnum(Enum): diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py new file mode 100644 index 000000000..330b95fac --- /dev/null +++ b/tests/unit/test_configuration.py @@ -0,0 +1,61 @@ +from typing import Any, Dict +from unittest.mock import Mock + +import pytest +from pytest_mock import MockerFixture + +from ni_measurementlink_service._configuration import MIDriverOptions, NISwitchOptions + + +def test___mi_driver_options___update_from_config___reads_config(config: Mock) -> None: + config_options = { + "MEASUREMENTLINK_NIFAKE_SIMULATE": True, + "MEASUREMENTLINK_NIFAKE_BOARD_TYPE": "PXI", + "MEASUREMENTLINK_NIFAKE_MODEL": "5678", + } + config.side_effect = lambda option, default=None, cast=None: config_options[option] + + options = MIDriverOptions("nifake").update_from_config() + + assert options.simulate + assert options.board_type == "PXI" + assert options.model == "5678" + + +@pytest.mark.parametrize( + "options,expected_dict", + [ + (MIDriverOptions("nifake"), {}), + ( + MIDriverOptions("nifake", True, "", "1234"), + {"simulate": True, "driver_setup": {"Model": "1234"}}, + ), + ( + MIDriverOptions("nifake", True, "PXIe", "1234"), + {"simulate": True, "driver_setup": {"BoardType": "PXIe", "Model": "1234"}}, + ), + ], +) +def test___mi_driver_options___to_dict___returns_options_dict( + options: MIDriverOptions, expected_dict: Dict[str, Any] +) -> None: + assert options.to_dict() == expected_dict + + +def test___niswitch_options___update_from_config___reads_config(config: Mock) -> None: + config_options = { + "MEASUREMENTLINK_NIFAKE_SIMULATE": True, + "MEASUREMENTLINK_NIFAKE_TOPOLOGY": "5678/Independent", + } + config.side_effect = lambda option, default=None, cast=None: config_options[option] + + options = NISwitchOptions("nifake").update_from_config() + + assert options.simulate + assert options.topology == "5678/Independent" + + +@pytest.fixture +def config(mocker: MockerFixture) -> Mock: + """Test fixture that creates a mock decouple config.""" + return mocker.patch("ni_measurementlink_service._configuration._config")