From 1ade2e773547b5845418e3d644010056ea717a36 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Thu, 22 Feb 2024 09:26:07 +0100 Subject: [PATCH 1/6] pkgs/scanf: init at 1.5.2 --- pkgs/default.nix | 4 ++++ pkgs/epnix/tools/scanf/default.nix | 31 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 pkgs/epnix/tools/scanf/default.nix diff --git a/pkgs/default.nix b/pkgs/default.nix index adf63a10..c51664e0 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -9,6 +9,10 @@ in mkEpicsPackage = callPackage ./build-support/mk-epics-package.nix {}; + python3Packages = prev.python3Packages.overrideScope (final: prev: { + scanf = final.callPackage ./epnix/tools/scanf {}; + }); + epnix = recurseExtensible (self: { # EPICS base diff --git a/pkgs/epnix/tools/scanf/default.nix b/pkgs/epnix/tools/scanf/default.nix new file mode 100644 index 00000000..959b33a3 --- /dev/null +++ b/pkgs/epnix/tools/scanf/default.nix @@ -0,0 +1,31 @@ +{ + lib, + buildPythonPackage, + fetchPypi, + setuptools, + wheel, +}: +buildPythonPackage rec { + pname = "scanf"; + version = "1.5.2"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-V2M0QKAqE4zRS2k9CScK8KA7sBfo1M/SSMeYizG4y4E="; + }; + + nativeBuildInputs = [ + setuptools + wheel + ]; + + pythonImportsCheck = ["scanf"]; + + meta = with lib; { + description = "A small scanf implementation"; + homepage = "https://pypi.org/project/scanf/"; + license = licenses.mit; + maintainers = with maintainers; [minijackson]; + }; +} From 51a23e1e59178d9ff4104875573f8c617cbba369 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Thu, 22 Feb 2024 09:26:44 +0100 Subject: [PATCH 2/6] pkgs/lewis: init at 1.3.1 --- pkgs/default.nix | 3 ++ pkgs/epnix/tools/lewis/default.nix | 55 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 pkgs/epnix/tools/lewis/default.nix diff --git a/pkgs/default.nix b/pkgs/default.nix index c51664e0..3a91ab28 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -10,6 +10,7 @@ in mkEpicsPackage = callPackage ./build-support/mk-epics-package.nix {}; python3Packages = prev.python3Packages.overrideScope (final: prev: { + lewis = final.callPackage ./epnix/tools/lewis {}; scanf = final.callPackage ./epnix/tools/scanf {}; }); @@ -50,6 +51,8 @@ in ca-gateway = callPackage ./epnix/tools/ca-gateway {}; + inherit (final.python3Packages) lewis; + pcas = callPackage ./epnix/tools/pcas {}; phoebus = callPackage ./epnix/tools/phoebus/client { diff --git a/pkgs/epnix/tools/lewis/default.nix b/pkgs/epnix/tools/lewis/default.nix new file mode 100644 index 00000000..1a67da55 --- /dev/null +++ b/pkgs/epnix/tools/lewis/default.nix @@ -0,0 +1,55 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + approvaltests, + setuptools, + wheel, + json-rpc, + mock, + pytest, + pyyaml, + pyzmq, + scanf, + semantic-version, +}: +buildPythonPackage rec { + pname = "lewis"; + version = "1.3.1"; + pyproject = true; + + src = fetchFromGitHub { + owner = "ess-dmsc"; + repo = "lewis"; + rev = "v${version}"; + hash = "sha256-7iMREHt6W26IzCFsRmojHqGuqIUHaCuvsKMMHuYflz0="; + }; + + nativeBuildInputs = [ + setuptools + wheel + ]; + + propagatedBuildInputs = [ + json-rpc + pyyaml + pyzmq + scanf + semantic-version + ]; + + checkInputs = [ + approvaltests + mock + pytest + ]; + + pythonImportsCheck = ["lewis"]; + + meta = with lib; { + description = "Let's write intricate simulators"; + homepage = "https://github.com/ess-dmsc/lewis"; + license = licenses.gpl3Only; + maintainers = with maintainers; [minijackson]; + }; +} From 08f87fb899ba92490bb3f5a971d7a0eca16b4894 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Tue, 12 Mar 2024 08:53:01 +0100 Subject: [PATCH 3/6] pkgs: add mkLewisSimulator utility function --- pkgs/default.nix | 1 + pkgs/epnix/tools/lewis/lib.nix | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 pkgs/epnix/tools/lewis/lib.nix diff --git a/pkgs/default.nix b/pkgs/default.nix index 3a91ab28..547eed24 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -52,6 +52,7 @@ in ca-gateway = callPackage ./epnix/tools/ca-gateway {}; inherit (final.python3Packages) lewis; + inherit (callPackage ./epnix/tools/lewis/lib.nix {}) mkLewisSimulator; pcas = callPackage ./epnix/tools/pcas {}; diff --git a/pkgs/epnix/tools/lewis/lib.nix b/pkgs/epnix/tools/lewis/lib.nix new file mode 100644 index 00000000..f6a61586 --- /dev/null +++ b/pkgs/epnix/tools/lewis/lib.nix @@ -0,0 +1,19 @@ +{ + lib, + epnix, + writeShellApplication, +}: { + mkLewisSimulator = { + name, + device ? name, + package, + source, + }: + writeShellApplication { + inherit name; + runtimeInputs = [epnix.lewis]; + text = '' + lewis -a "${source}" -k "${package}" "${device}" "$@" + ''; + }; +} From 9599bacd4cca98e03172575ff1ac4a016e63377b Mon Sep 17 00:00:00 2001 From: Minijackson Date: Wed, 3 Apr 2024 09:12:01 +0200 Subject: [PATCH 4/6] lib/documentation: more robust markdown generation --- lib/documentation.nix | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/documentation.nix b/lib/documentation.nix index aa25dc24..090bcad2 100644 --- a/lib/documentation.nix +++ b/lib/documentation.nix @@ -72,7 +72,7 @@ package2pandoc = headingLevel: path: pkg: let header = lib.fixedWidthString headingLevel "#" ""; in '' - ${header} ${pkg.pname} + ${header} ${pkg.pname or pkg.name} Path : `epnix.${path}` @@ -86,16 +86,17 @@ Homepage : <${pkg.meta.homepage}> - Declared in - : ${let + ${lib.optionalString (pkg.meta ? position) (let filePath = lib.head (lib.splitString ":" pkg.meta.position); relativePath = lib.pipe filePath [ (lib.splitString "/") (lib.sublist 4 255) (lib.concatStringsSep "/") ]; - in - self.markdown.inDefList "[${relativePath}](file://${filePath})"} + in '' + Declared in + : ${self.markdown.inDefList "[${relativePath}](file://${filePath})"} + '')} License(s) : ${self.markdown.inDefList (self.licenseList pkg)} From db030c1b1976c5fa427529efb83225efecb8587c Mon Sep 17 00:00:00 2001 From: Minijackson Date: Tue, 12 Mar 2024 08:52:32 +0100 Subject: [PATCH 5/6] pkgs/psu-simulator: migrate to lewis --- pkgs/doc-support/psu-simulator/default.nix | 15 +- pkgs/doc-support/psu-simulator/poetry.lock | 83 ---- .../psu-simulator/psu_simulator/__init__.py | 206 ---------- .../psu_simulator/psu_simulator.py | 367 ++++++++++++++++++ pkgs/doc-support/psu-simulator/pyproject.toml | 18 +- 5 files changed, 379 insertions(+), 310 deletions(-) delete mode 100644 pkgs/doc-support/psu-simulator/poetry.lock create mode 100644 pkgs/doc-support/psu-simulator/psu_simulator/psu_simulator.py diff --git a/pkgs/doc-support/psu-simulator/default.nix b/pkgs/doc-support/psu-simulator/default.nix index 2f233396..2a850e08 100644 --- a/pkgs/doc-support/psu-simulator/default.nix +++ b/pkgs/doc-support/psu-simulator/default.nix @@ -1,13 +1,20 @@ { - poetry2nix, lib, + epnix, epnixLib, }: -poetry2nix.mkPoetryApplication { - projectDir = ./.; +(epnix.mkLewisSimulator { + name = "psu_simulator"; + package = "psu_simulator"; + source = ./.; +}) +// { + pname = "psu_simulator"; + version = "0.2.0"; meta = { - homepage = "https://epics-extensions.github.io/EPNix/"; + description = "A power supply simulator for the StreamDevice tutorial"; + homepage = "https://epics-extensions.github.io/EPNix/ioc/tutorials/streamdevice.html"; license = lib.licenses.asl20; maintainers = with epnixLib.maintainers; [minijackson]; }; diff --git a/pkgs/doc-support/psu-simulator/poetry.lock b/pkgs/doc-support/psu-simulator/poetry.lock deleted file mode 100644 index fde1b804..00000000 --- a/pkgs/doc-support/psu-simulator/poetry.lock +++ /dev/null @@ -1,83 +0,0 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.7.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, -] - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, -] - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.7.0" -content-hash = "1e70b172657bdbef13c5d18991199035640ab2ff4e638ec047db7a4c7a18602c" diff --git a/pkgs/doc-support/psu-simulator/psu_simulator/__init__.py b/pkgs/doc-support/psu-simulator/psu_simulator/__init__.py index cae3015b..e69de29b 100644 --- a/pkgs/doc-support/psu-simulator/psu_simulator/__init__.py +++ b/pkgs/doc-support/psu-simulator/psu_simulator/__init__.py @@ -1,206 +0,0 @@ -"""A simple power supply simulator.""" - -import logging -import random -import socketserver - -import click - -__version__ = "0.1.0" - -# We don't need cryptographically secure RNG -# ruff: noqa: S311 - -logging.basicConfig(level="INFO", format="%(levelname)s %(message)s") -logger = logging.getLogger(__package__) - -class Server(socketserver.ThreadingMixIn, socketserver.TCPServer): - """TCP server.""" - - allow_reuse_address = True - - current: int = 0 - voltage: int = 0 - resistance: int = 0 - - -class PowerSupply(socketserver.StreamRequestHandler): - """The power supply protocol handler.""" - - def set_current(self: "PowerSupply", val: float) -> None: - """Set the current.""" - self.server.current = val - self.server.voltage = self.server.current * self.server.resistance - - def set_voltage(self: "PowerSupply", val: float) -> None: - """Set the voltage.""" - self.server.voltage = val - self.server.current = self.server.voltage / self.server.resistance - - def handle(self: "PowerSupply") -> None: - """Handle incoming connections.""" - logger.info("received connection") - - self._dispatch = { - b"help": self.cmd_help, - b":idn?": self.cmd_get_identification, - b"meas:curr?": self.cmd_get_measured_current, - b":curr?": self.cmd_get_current, - b":curr": self.cmd_set_current, - b"meas:volt?": self.cmd_get_measured_voltage, - b":volt?": self.cmd_get_voltage, - b":volt": self.cmd_set_voltage, - } - - while True: - try: - args = self.rfile.readline().strip().split() - except BrokenPipeError: - return - - if args == []: - try: - self.wfile.write(b".\n") - except BrokenPipeError: - return - continue - - command = args[0].lower() - params = args[1:] - - decoded_params = [param.decode() for param in params] - logger.info("received command: %s%s", command.decode(), decoded_params) - - if command in self._dispatch: - result = self._dispatch[command](*params) - self.wfile.write(str(result).encode()) - self.wfile.write(b"\n") - else: - self.wfile.write(f"command not found: {command.decode()}\n".encode()) - - def finish(self: "PowerSupply") -> None: - """Clean up connections.""" - logger.info("closed connection") - - def cmd_help(self: "PowerSupply", *args: str) -> str: - """Get help about various commands. - - Usage: help . - """ - if len(args) >= 1: - command = args[0] - if command in self._dispatch: - doc = self._dispatch[command].__doc__ - self.wfile.write(doc.encode()) - else: - self.wfile.write(f"command not found: {command!s}".encode()) - return "" - - self.wfile.write(b"Available commands:\n") - for command, func in self._dispatch.items(): - doc = func.__doc__.splitlines()[0].encode() - self.wfile.write(b" - '" + command + b"': " + doc + b"\n") - - return "" - - def cmd_get_identification(self: "PowerSupply", *_args: str) -> int: - """Return the identification of the power supply. - - Usage: :idn? - Returns: string - """ - return f"psu-simulator {__version__}" - - def cmd_get_measured_current(self: "PowerSupply", *_args: str) -> int: - """Return the measured current, in Amps. - - Usage: meas:curr? - Returns: float - """ - return self.server.current + random.uniform(-1.5, 1.5) - - def cmd_get_current(self: "PowerSupply", *_args: str) -> int: - """Return the current current command, in Amps. - - Usage: :curr? - Returns: float - """ - return self.server.current - - def cmd_set_current(self: "PowerSupply", *args: str) -> str: - """Set the current, in Amps. - - Usage: :curr - Returns: 'OK' | 'ERR' - """ - try: - val = float(args[0]) - except ValueError: - return "ERR" - else: - self.set_current(val) - return "OK" - - def cmd_get_measured_voltage(self: "PowerSupply", *_args: str) -> int: - """Return the measured voltage, in Volts. - - Usage: meas:volt? - Returns: float - """ - return self.server.voltage + random.uniform(-1.5, 1.5) - - def cmd_get_voltage(self: "PowerSupply", *_args: str) -> int: - """Return the voltage voltage command, in Volts. - - Usage: :volt? - Returns: float - """ - return self.server.voltage - - def cmd_set_voltage(self: "PowerSupply", *args: str) -> str: - """Set the voltage, in Volts. - - Usage: :volt - Returns: 'OK' | 'ERR' - """ - try: - val = float(args[0]) - except ValueError: - return "ERR" - else: - self.set_voltage(val) - return "OK" - - -@click.command() -@click.option( - "-l", - "--listen-address", - default="localhost", - show_default=True, - help="Listening address", -) -@click.option( - "-p", - "--port", - default=8727, - show_default=True, - help="Listening TCP port", -) -@click.option( - "--resistance", - default=20, - show_default=True, - help="Resistance of the circuit connected to the power supply, in Ohms.", -) -def main(listen_address: str, port: int, resistance: int) -> None: - """Start a power supply simulator server.""" - with Server((listen_address, port), PowerSupply) as server: - logger.info("Listening on %s:%s", listen_address, port) - server.resistance = resistance - logger.info("Resistance is %s Ohms", resistance) - - try: - server.serve_forever() - except KeyboardInterrupt: - return diff --git a/pkgs/doc-support/psu-simulator/psu_simulator/psu_simulator.py b/pkgs/doc-support/psu-simulator/psu_simulator/psu_simulator.py new file mode 100644 index 00000000..58fc066d --- /dev/null +++ b/pkgs/doc-support/psu-simulator/psu_simulator/psu_simulator.py @@ -0,0 +1,367 @@ +"""A simulated power supply. + +The power supply protocol is inspired by the one from genesys power supplies. +""" + +import random +from collections import OrderedDict +from enum import IntEnum + +from lewis.adapters.stream import Cmd, StreamInterface, scanf +from lewis.core import approaches +from lewis.core.statemachine import State +from lewis.devices import StateMachineDevice + +__version__ = "0.1.0" + +# We don't need cryptographically secure RNG +# ruff: noqa: S311 + + +class ConstantMode(IntEnum): + """Whether the power supply is in constant voltage of constant current.""" + + CONSTANT_VOLTAGE = 1 + CONSTANT_CURRENT = 2 + + +class RampMode(IntEnum): + """The type of ramp for approaching the programmed current / voltage.""" + + IMMEDIATE = 0 + BOTH = 1 + UPWARDS = 2 + DOWNWARDS = 3 + + +class ApproachingCurrentState(State): + """The state when the target current is not the programmed current.""" + + def in_state(self: "ApproachingCurrentState", dt: float) -> None: + """Make a step towards the programmed current.""" + old_actual_current = self._context.actual_current + match self._context.current_ramp_mode: + case RampMode.IMMEDIATE: + self._context.actual_current = self._context.programmed_current + case RampMode.BOTH: + self._context.actual_current = approaches.linear( + old_actual_current, + self._context.programmed_current, + self._context.current_ramp, + dt, + ) + + self.log.info( + "Current changed (%s -> %s), programmed=%s, mode=%s, ramp=%s", + old_actual_current, + self._context.actual_current, + self._context.programmed_current, + self._context.current_ramp_mode.name, + self._context.current_ramp, + ) + + +class ApproachingVoltageState(State): + """The state when the target voltage is not the programmed current.""" + + def in_state(self: "ApproachingVoltageState", dt: float) -> None: + """Make a step towards the programmed voltage.""" + old_actual_voltage = self._context.actual_voltage + match self._context.voltage_ramp_mode: + case RampMode.IMMEDIATE: + self._context.actual_voltage = self._context.programmed_voltage + case RampMode.BOTH: + self._context.actual_voltage = approaches.linear( + old_actual_voltage, + self._context.programmed_voltage, + self._context.voltage_ramp, + dt, + ) + + self.log.info( + "Voltage changed (%s -> %s), programmed=%s, mode=%s, ramp=%s", + old_actual_voltage, + self._context.actual_voltage, + self._context.programmed_voltage, + self._context.voltage_ramp_mode.name, + self._context.voltage_ramp, + ) + + +class SimulatedPowerSupply(StateMachineDevice): + """The simulated power supply.""" + + def _initialize_data(self: "SimulatedPowerSupply") -> None: + self.serial: str = f"psu-simulator {__version__}" + + self.powered: bool = True + self.mode: ConstantMode = ConstantMode.CONSTANT_VOLTAGE + self.resistance: float = 2.0 + + self.programmed_voltage: float = 0.0 + self.actual_voltage: float = 0.0 + self.voltage_ramp_mode: RampMode = RampMode.IMMEDIATE + self.voltage_ramp: float = 0.0 + + self.programmed_current: float = 0.0 + self.actual_current: float = 0.0 + self.current_ramp_mode: RampMode = RampMode.IMMEDIATE + self.current_ramp: float = 0.0 + + def _get_state_handlers(self: "SimulatedPowerSupply") -> dict[str, State]: + return { + "off": State(), + "constant_voltage": State(), + "approaching_voltage": ApproachingVoltageState(), + "constant_current": State(), + "approaching_current": ApproachingCurrentState(), + } + + def _get_initial_state(self: "SimulatedPowerSupply") -> str: + return "off" + + @property + def measured_current(self: "SimulatedPowerSupply") -> float: + """The currently measured output current.""" + if not self.powered: + return 0 + + match self.mode: + case ConstantMode.CONSTANT_VOLTAGE: + return self.actual_voltage / self.resistance + case ConstantMode.CONSTANT_CURRENT: + return self.actual_current + + @property + def measured_voltage(self: "SimulatedPowerSupply") -> float: + """The currently measured output voltage.""" + if not self.powered: + return 0 + + match self.mode: + case ConstantMode.CONSTANT_VOLTAGE: + return self.actual_voltage + case ConstantMode.CONSTANT_CURRENT: + return self.actual_current * self.resistance + + def _get_transition_handlers(self: "SimulatedPowerSupply") -> OrderedDict: + return OrderedDict( + [ + ( + ("off", "constant_voltage"), + lambda: self.powered and self.mode == ConstantMode.CONSTANT_VOLTAGE, + ), + ( + ("off", "constant_current"), + lambda: self.powered and self.mode == ConstantMode.CONSTANT_CURRENT, + ), + (("constant_voltage", "off"), lambda: not self.powered), + (("constant_current", "off"), lambda: not self.powered), + (("approaching_voltage", "off"), lambda: not self.powered), + (("approaching_current", "off"), lambda: not self.powered), + ( + ("constant_voltage", "approaching_voltage"), + lambda: self.programmed_voltage != self.actual_voltage, + ), + ( + ("approaching_voltage", "constant_voltage"), + lambda: self.programmed_voltage == self.actual_voltage, + ), + ( + ("constant_current", "approaching_current"), + lambda: self.programmed_current != self.actual_current, + ), + ( + ("approaching_current", "constant_current"), + lambda: self.programmed_current == self.actual_current, + ), + ] + ) + + +class PowerSupplyInterface(StreamInterface): + """The TCP/IP interface to the power supply.""" + + commands = frozenset( + { + Cmd("help", "help( .+)?"), + Cmd("get_idn", scanf(":idn?")), + Cmd("get_powered", scanf("outp:pon?")), + Cmd("set_powered", scanf("outp:pon %s"), argument_mappings=(bytes,)), + Cmd("get_mode", scanf("outp:mode?")), + Cmd("_set_mode", scanf("outp:mode %s"), argument_mappings=(bytes,)), + Cmd("get_measured_current", scanf("meas:curr?")), + Cmd("get_programmed_current", scanf(":curr?")), + Cmd( + "set_programmed_current", + scanf(r":curr %f"), + argument_mappings=(float,), + ), + Cmd("get_measured_voltage", scanf("meas:volt?")), + Cmd("get_programmed_voltage", scanf(":volt?")), + Cmd( + "_set_programmed_voltage", + scanf(r":volt %f"), + argument_mappings=(float,), + ), + }, + ) + + in_terminator = "\n" + out_terminator = "\n" + + def help(self: "PowerSupplyInterface", arg: bytes) -> str: + """Print help about the various commands. + + Usage: help + Usage: help + """ + result = "" + + if arg is not None: + return self._help_about(arg.decode().strip()) + + def _sort_key(cmd: Cmd) -> str: + return getattr(cmd.pattern, "pattern", "help") + + for cmd in sorted(self.commands, key=_sort_key): + cmd_name = "help (%s)" if cmd.func == "help" else cmd.pattern.pattern + doc = getattr(self, cmd.func).__doc__ + if doc is None: + continue + summary = doc.splitlines()[0] + + result += " - '" + cmd_name + "': " + summary + "\n" + + return result + + def _help_about(self: "PowerSupplyInterface", command: str) -> str: + if command == "help": + return self.help.__doc__ + + doc = None + for cmd in self.commands: + if cmd.func == "help": + continue + cmd_name = cmd.pattern.pattern.split()[0] + if cmd_name == command: + doc = getattr(self, cmd.func).__doc__ + break + + if doc is None: + return "Unknown command" + + return doc + + def get_idn(self: "PowerSupplyInterface") -> str: + """Return the identification of the power supply. + + Usage: :idn? + Returns: string + """ + return self.device.serial + + def get_powered(self: "PowerSupplyInterface") -> str: + """Return whether the output of the power supply is powered on. + + Usage: outp:pon? + Returns: "ON" | "OFF" + """ + return "ON" if self.device.powered else "OFF" + + def set_powered(self: "PowerSupplyInterface", val: bytes) -> str: + """Enable or disable the output. + + Usage: outp:pon <"ON" | "1" | "OFF" | "0"> + Returns: "OK" | "ERR" + """ + match val.lower(): + case b"on" | b"1": + self.device.powered = True + return "OK" + case b"off" | b"0": + self.device.powered = False + return "OK" + case _: + return "ERR" + + def get_mode(self: "PowerSupplyInterface") -> str: + """Return whether the power supply is in constant voltage or constant current. + + Usage: outp:mode? + Returns: "CV" (the default) | "CC" + """ + match self.device.mode: + case ConstantMode.CONSTANT_CURRENT: + return "CC" + case ConstantMode.CONSTANT_VOLTAGE: + return "CV" + + def _set_mode(self: "PowerSupplyInterface", val: bytes) -> str: + """Set whether the power supply is in constant current or constant voltage. + + Usage: outp:mode <"CV" | "CC"> + Returns: "OK" | "ERR" + """ + match val.lower(): + case b"cc": + self.device.mode = ConstantMode.CONSTANT_CURRENT + return "OK" + case b"cv": + self.device.mode = ConstantMode.CONSTANT_VOLTAGE + return "OK" + case _: + return "ERR" + + def get_measured_current(self: "PowerSupplyInterface") -> str: + """Return the measured current, in Amps. + + Usage: meas:curr? + Returns: float + """ + try: + return self.device.measured_current + random.uniform(-1.5, 1.5) + except Exception as e: + return str(e) + + def get_programmed_current(self: "PowerSupplyInterface") -> str: + """Return the current current command, in Amps. + + Usage: :curr? + Returns: float + """ + return self.device.programmed_current + + def set_programmed_current(self: "PowerSupplyInterface", val: float) -> str: + """Set the current, in Amps. + + Usage: :curr + Returns: "OK" | "ERR" + """ + self.device.programmed_current = val + return "OK" + + def get_measured_voltage(self: "PowerSupplyInterface") -> str: + """Return the measured voltage, in Volts. + + Usage: meas:volt? + Returns: float + """ + return self.device.measured_voltage + random.uniform(-1.5, 1.5) + + def get_programmed_voltage(self: "PowerSupplyInterface") -> str: + """Return the voltage voltage command, in Volts. + + Usage: :volt? + Returns: float + """ + return self.device.programmed_voltage + + def _set_programmed_voltage(self: "PowerSupplyInterface", val: float) -> str: + """Set the voltage, in Volts. + + Usage: :volt + Returns: "OK" | "ERR" + """ + self.device.programmed_voltage = val + return "OK" diff --git a/pkgs/doc-support/psu-simulator/pyproject.toml b/pkgs/doc-support/psu-simulator/pyproject.toml index cc599d2f..f5a0ff5f 100644 --- a/pkgs/doc-support/psu-simulator/pyproject.toml +++ b/pkgs/doc-support/psu-simulator/pyproject.toml @@ -1,19 +1,3 @@ -[tool.poetry] -name = "psu-simulator" -version = "0.1.0" -description = "A power supply simulator for the StreamDevice tutorial" -authors = ["Rémi NICOLE "] - -[tool.poetry.scripts] -psu-simulator = "psu_simulator:main" - -[tool.poetry.dependencies] -python = ">=3.7.0" -click = "^8.1.7" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - [tool.ruff] select = ["ALL"] +target-version = "py310" From e7a835f436655f25d46efc3a5cc400f2e6503961 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Wed, 3 Apr 2024 08:54:18 +0200 Subject: [PATCH 6/6] ioc/tests/StreamDevice: use psu-simulator as "mock server" --- .../support/StreamDevice/simple/default.nix | 82 ++++----- .../simple/mock-server/.gitignore | 158 ------------------ .../mock-server/mock_server/__init__.py | 66 -------- .../simple/mock-server/poetry.lock | 8 - .../simple/mock-server/pyproject.toml | 15 -- .../simple/top/iocBoot/iocsimple/st.cmd | 2 +- .../simple/top/simpleApp/Db/simple.db | 91 +++++----- .../simple/top/simpleApp/Db/simple.proto | 71 ++++---- 8 files changed, 113 insertions(+), 380 deletions(-) delete mode 100644 ioc/tests/support/StreamDevice/simple/mock-server/.gitignore delete mode 100644 ioc/tests/support/StreamDevice/simple/mock-server/mock_server/__init__.py delete mode 100644 ioc/tests/support/StreamDevice/simple/mock-server/poetry.lock delete mode 100644 ioc/tests/support/StreamDevice/simple/mock-server/pyproject.toml diff --git a/ioc/tests/support/StreamDevice/simple/default.nix b/ioc/tests/support/StreamDevice/simple/default.nix index dc2ddcc3..45eeaf6e 100644 --- a/ioc/tests/support/StreamDevice/simple/default.nix +++ b/ioc/tests/support/StreamDevice/simple/default.nix @@ -2,10 +2,6 @@ inherit (pkgs) epnixLib; inherit (pkgs.stdenv.hostPlatform) system; - mock_server = pkgs.poetry2nix.mkPoetryApplication { - projectDir = ./mock-server; - }; - result = epnixLib.evalEpnixModules { nixpkgsConfig.system = system; epnixConfig.imports = [./top/epnix.nix]; @@ -19,70 +15,60 @@ in name = "support-StreamDevice-simple"; meta.maintainers = with epnixLib.maintainers; [minijackson]; - nodes.machine = let - listenAddr = "127.0.0.1:1234"; - in - {lib, ...}: { - environment.systemPackages = [pkgs.epnix.epics-base]; + nodes.machine = {lib, ...}: { + environment.systemPackages = [pkgs.epnix.epics-base]; - systemd.sockets.mock-server = { + systemd.services = { + "psu-simulator" = { wantedBy = ["multi-user.target"]; - listenStreams = [listenAddr]; - socketConfig.Accept = true; - }; - - systemd.services = { - "mock-server@".serviceConfig = { - ExecStart = "${mock_server}/bin/mock_server"; - StandardInput = "socket"; - StandardError = "journal"; + serviceConfig = { + ExecStart = lib.getExe pkgs.epnix.psu-simulator; }; - - ioc = lib.mkMerge [ - service - {environment.STREAM_PS1 = listenAddr;} - ]; }; + + ioc = lib.mkMerge [ + service + {environment.STREAM_PS1 = "localhost:9999";} + ]; }; + }; testScript = '' machine.wait_for_unit("default.target") machine.wait_for_unit("ioc.service") - with subtest("getting fixed values"): - machine.wait_until_succeeds("caget -t FLOAT:IN | grep -qxF '42.1234'") - machine.wait_until_succeeds("caget -t FLOAT_WITH_PREFIX:IN | grep -qxF '69.1337'") - machine.wait_until_succeeds("caget -t ENUM:IN | grep -qxF '1'") + def assert_caget(pv: str, expected: str) -> None: + machine.wait_until_succeeds(f"caget -t '{pv}' | grep -qxF '{expected}'", timeout=10) - with subtest("setting values"): - machine.wait_until_succeeds("caget -t VARFLOAT:IN | grep -qxF '0'") - - # Caput can simply not go through - def put_check_varfloat(_) -> bool: - machine.succeed("caput VARFLOAT:OUT 123.456") - status, _output = machine.execute("caget -t VARFLOAT:IN | grep -qxF '123.456'") + def assert_caput(pv: str, value: str) -> None: + def do_caput(_) -> bool: + machine.succeed(f"caput '{pv}' '{value}'") + status, _output = machine.execute(f"caget -t '{pv}' | grep -qxF '{value}'") return status == 0 - retry(put_check_varfloat) + retry(do_caput, timeout=10) - with subtest("calc integration"): - machine.wait_until_succeeds("caget -t SCALC:IN | grep -qxF '10A'") + with subtest("getting initial values"): + assert_caget("UCmd", "0") + assert_caget("URb", "0") + assert_caget("PowerCmd", "ON") + assert_caget("PowerRb", "ON") - def put_check_scalc(_) -> bool: - machine.succeed("caput SCALC:OUT.A 2") - status, _output = machine.execute("caget -t SCALC:IN | grep -qxF '14A'") - return status == 0 - - retry(put_check_scalc) + with subtest("setting values"): + assert_caput("UCmd", "10") + assert_caget("URb", "10") - machine.wait_until_succeeds("caget -t SCALC:OUT.SVAL | grep -qxF 'sent'") + with subtest("calc integration"): + assert_caput("2UCmd.A", "42") + assert_caget("2UCmd.SVAL", "184") + assert_caget("URb", "184") with subtest("regular expressions"): - machine.wait_until_succeeds("caget -t REGEX_TITLE:IN | grep -qxF 'Hello, World!'") - machine.wait_until_succeeds("caget -t REGEX_SUB:IN | grep -qxF 'abcXcXcabc'") + assert_caget("VersionNum", "0.1.0") + assert_caget("VersionCat", "010") ''; passthru = { - inherit mock_server ioc; + inherit ioc; }; } diff --git a/ioc/tests/support/StreamDevice/simple/mock-server/.gitignore b/ioc/tests/support/StreamDevice/simple/mock-server/.gitignore deleted file mode 100644 index 13189553..00000000 --- a/ioc/tests/support/StreamDevice/simple/mock-server/.gitignore +++ /dev/null @@ -1,158 +0,0 @@ -# Created by https://www.toptal.com/developers/gitignore/api/python -# Edit at https://www.toptal.com/developers/gitignore?templates=python - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/ioc/tests/support/StreamDevice/simple/mock-server/mock_server/__init__.py b/ioc/tests/support/StreamDevice/simple/mock-server/mock_server/__init__.py deleted file mode 100644 index 92fb10c6..00000000 --- a/ioc/tests/support/StreamDevice/simple/mock-server/mock_server/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -"""A simple server mocking an ASCII communication.""" - -import sys - -__version__ = "0.1.0" - - -def log(*args: str, **kwargs: str) -> None: - """Print a message to stderr.""" - print(*args, file=sys.stderr, **kwargs) - - -def send(*args: str, **kwargs: str) -> None: - """Send a message.""" - print(*args, end="\r\n", flush=True, **kwargs) - - -def main() -> None: - """Start the mock server.""" - log("received connection") - - varfloat = 0.0 - scalc = "" - - while True: - data = sys.stdin.readline().strip() - - if not data: - break - - log("received command:", data) - - # TODO(minijackson): change that with a command-line parsing tool? - - if data == "FLOAT": - send("42.1234") - elif data == "FLOAT_WITH_PREFIX": - send("VALUE: 69.1337") - elif data == "ENUM": - send("TWO") - elif data.startswith("SET_VARFLOAT "): - varfloat = float(data.split(" ", maxsplit=1)[1]) - elif data == "GET_VARFLOAT": - send(str(varfloat)) - elif data == "REGEX_TITLE": - send( - """ - - - Hello, World! - - -

Hello, World!

- - -""", - ) - elif data == "REGEX_SUB": - send("abcabcabcabc") - elif data.startswith("SET_SCALC "): - send("sent") - scalc = data.split(" ", maxsplit=1)[1] - elif data == "GET_SCALC": - send(scalc) - else: - log("unknown command") diff --git a/ioc/tests/support/StreamDevice/simple/mock-server/poetry.lock b/ioc/tests/support/StreamDevice/simple/mock-server/poetry.lock deleted file mode 100644 index d9af7385..00000000 --- a/ioc/tests/support/StreamDevice/simple/mock-server/poetry.lock +++ /dev/null @@ -1,8 +0,0 @@ -package = [] - -[metadata] -lock-version = "1.1" -python-versions = "*" -content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" - -[metadata.files] diff --git a/ioc/tests/support/StreamDevice/simple/mock-server/pyproject.toml b/ioc/tests/support/StreamDevice/simple/mock-server/pyproject.toml deleted file mode 100644 index d248b538..00000000 --- a/ioc/tests/support/StreamDevice/simple/mock-server/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[tool.poetry] -name = "mock-server" -version = "0.1.0" -description = "Mock Server for the simple StreamDevice support test" -authors = ["Rémi NICOLE "] - -[tool.poetry.scripts] -mock_server = "mock_server:main" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - -[tool.ruff] -select = ["ALL"] diff --git a/ioc/tests/support/StreamDevice/simple/top/iocBoot/iocsimple/st.cmd b/ioc/tests/support/StreamDevice/simple/top/iocBoot/iocsimple/st.cmd index e170880f..d3ca8c76 100755 --- a/ioc/tests/support/StreamDevice/simple/top/iocBoot/iocsimple/st.cmd +++ b/ioc/tests/support/StreamDevice/simple/top/iocBoot/iocsimple/st.cmd @@ -13,6 +13,6 @@ epicsEnvSet("STREAM_PROTOCOL_PATH", ".:${TOP}/db") drvAsynIPPortConfigure("PS1", "${STREAM_PS1}") ## Load record instances -dbLoadRecords("../../db/simple.db", "PORT=PS1") +dbLoadRecords("../../db/simple.db", "PORT=PS1,P=") iocInit() diff --git a/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.db b/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.db index e68e5ebc..3a44589e 100644 --- a/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.db +++ b/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.db @@ -1,81 +1,82 @@ -# Fixed values +# Normal flow -record(ai, "FLOAT:IN") { - field(DESC, "A fixed float") +record(ao, "${P}UCmd") { + field(DESC, "Voltage command") field(DTYP, "stream") - field(INP, "@simple.proto getFloat $(PORT)") - field(PINI, "YES") - field(SCAN, "5 second") + field(OUT, "@simple.proto setUCmd $(PORT)") + field(EGU, "V") + + field(FLNK, "${P}URb") } -record(ai, "FLOAT_WITH_PREFIX:IN") { - field(DESC, "A fixed float with a returned prefix") +record(ai, "${P}URb") { + field(DESC, "Voltage read-back") field(DTYP, "stream") - field(INP, "@simple.proto getFloatWithPrefix $(PORT)") + field(INP, "@simple.proto getURb $(PORT)") + field(EGU, "V") field(PINI, "YES") - field(SCAN, "5 second") } -record(longin, "ENUM:IN") { - field(DESC, "An fixed enum parsed as integer") +record(ai, "${P}UMes") { + field(DESC, "Measure of voltage intensity") field(DTYP, "stream") - field(INP, "@simple.proto getEnum $(PORT)") + field(INP, "@simple.proto getUMes $(PORT)") + field(EGU, "V") field(PINI, "YES") field(SCAN, "5 second") } -# Variable values - -record(ai, "VARFLOAT:IN") { - field(DESC, "A variable float") +record(bo, "${P}PowerCmd") { + field(DESC, "Command for enabling output") field(DTYP, "stream") - field(INP, "@simple.proto getVarFloat $(PORT)") - field(PINI, "YES") - field(SCAN, "5 second") + field(OUT, "@simple.proto setPowerCmd $(PORT)") + field(ZNAM, "OFF") + field(ONAM, "ON") + + field(FLNK, "${P}PowerRb") } -record(ao, "VARFLOAT:OUT") { - field(DESC, "A variable float") +record(bi, "${P}PowerRb") { + field(DESC, "Read back of power supply is enabled") field(DTYP, "stream") - field(OUT, "@simple.proto setVarFloat $(PORT)") - field(FLNK, "VARFLOAT:IN") -} + field(INP, "@simple.proto getPowerRb $(PORT)") + field(PINI, YES) + field(ZNAM, "OFF") + field(ONAM, "ON") -# Calc + # field(FLNK, "${P}Status") +} -record(stringin, "SCALC:IN") { - field(DESC, "Result of scalcout record") +record(stringin, "${P}Version") { + field(DESC, "A fixed float with a returned prefix") field(DTYP, "stream") - field(INP, "@simple.proto getSCalc $(PORT)") + field(INP, "@simple.proto getVersion $(PORT)") field(PINI, "YES") - field(SCAN, "5 second") } -record(scalcout, "SCALC:OUT") { - field(DESC, "An scalcout record") +# Calc + +record(scalcout, "${P}2UCmd") { + field(DESC, "Twice the voltage command") field(A, "0") - field(CALC, "printf('1%i', A*A) + 'A'") + field(CALC, "printf('1%i', A*2)") field(DTYP, "stream") - field(OUT, "@simple.proto setSCalc $(PORT)") - field(FLNK, "SCALC:IN") - field(PINI, "YES") - field(SCAN, "5 second") + field(OUT, "@simple.proto setUCmd $(PORT)") + field(FLNK, "${P}URb") } # Regular Expressions -record(stringin, "REGEX_TITLE:IN") { - field(DESC, "A regex test") +record(stringin, "${P}VersionNum") { + field(DESC, "Just the version number") field(DTYP, "stream") - field(INP, "@simple.proto getRegexTitle $(PORT)") + field(INP, "@simple.proto getVersionNum $(PORT)") field(PINI, "YES") - field(SCAN, "5 second") } -record(stringin, "REGEX_SUB:IN") { - field(DESC, "A regex substitution test") +record(stringin, "${P}VersionCat") { + field(DESC, "Just the version numbers, concatenated") field(DTYP, "stream") - field(INP, "@simple.proto getRegexSub $(PORT)") + field(INP, "@simple.proto getVersionCat $(PORT)") field(PINI, "YES") - field(SCAN, "5 second") } diff --git a/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.proto b/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.proto index f7c28e6b..86b07964 100644 --- a/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.proto +++ b/ioc/tests/support/StreamDevice/simple/top/simpleApp/Db/simple.proto @@ -1,59 +1,52 @@ -Terminator = CR LF; +Terminator = LF; -# Fixed values +# Normal flow -getFloat { - out "FLOAT"; - in "%f"; +getURb { + out ":volt?"; + in "%f"; } -getFloatWithPrefix { - out "FLOAT_WITH_PREFIX"; - in "VALUE: %f"; -} +setUCmd { + out ":volt %f"; + in "OK"; -getEnum { - out "ENUM"; - in "%{ONE|TWO|THREE}"; + @init { getURb; } } -# Variable values - -setVarFloat { - out "SET_VARFLOAT %f"; +getUMes { + out "meas:volt?"; + in "%f"; } -getVarFloat { - out "GET_VARFLOAT"; - in "%f"; +# Checks parsing enums +getPowerRb { + out "outp:pon?"; + in "%{OFF|ON}"; } -# Calc +# Checks writing enums +setPowerCmd { + out "outp:pon %{OFF|ON}"; + in "OK"; -getSCalc { - out "GET_SCALC"; - in "%s"; + @init { getPowerRb; } } -setSCalc { - out "SET_SCALC %s"; - in "%s"; +# Checks parsing with prefix +getVersion { + out ":idn?"; + in "psu-simulator %s"; } -# Regular Expressions +# Checks regular expressions -getRegexTitle { - out "REGEX_TITLE"; - # Added `[\s\S]+$` at the end to silence warning of extra input - in "%.1/(.*)<\/title>[\s\S]+$/" +getVersionNum { + out ":idn?"; + in "%.1/^psu-simulator (\d+\.\d+\.\d+)$/" } -getRegexSub { - out "REGEX_SUB"; - # TODO: weirdness in StreamDevice, the `+.2` here means "replace a maximum of - # 2", but it also needs 2 regex sub-expression, hence the "(())", as if it - # were a normal regex converter ("%/regex/"). - # - # Also %s needed to be after instead of before. - in "%#+-10.2/((ab))/X/%s"; +getVersionCat { + out ":idn?"; + in "%#/^psu-simulator (\d+)\.(\d+)\.(\d+)$/\1\2\3/%s"; }