From b83642d99119d9a7d2e15f35240cafcecfb13dbb Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Tue, 27 May 2025 18:21:07 +0200 Subject: [PATCH 1/6] added a FormatEntity that supports asteval, in addition define an command entity that we can use e.g. to degas our pressure gauges --- dripline/extensions/__init__.py | 1 + dripline/extensions/asteval_endpoint.py | 75 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 dripline/extensions/asteval_endpoint.py diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index b62df1e..dd061d2 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -7,3 +7,4 @@ # Modules in this directory from .add_auth_spec import * +from .asteval_endpoint import * diff --git a/dripline/extensions/asteval_endpoint.py b/dripline/extensions/asteval_endpoint.py new file mode 100644 index 0000000..bd44fd5 --- /dev/null +++ b/dripline/extensions/asteval_endpoint.py @@ -0,0 +1,75 @@ +import asteval # used for FormatEntity +import re # used for FormatEntity + +from dripline.core import calibrate, ThrowReply +from dripline.implementations import FormatEntity +from dripline.core import Entity + +import logging +logger = logging.getLogger(__name__) + +__all__ = [] + + + +__all__.append('FormatEntityAsteval') +class FormatEntityAsteval(FormatEntity): + ''' + Utility Entity allowing arbitrary set and query syntax and formatting for more complicated usage cases + No assumption about SCPI communication syntax. + ''' + + def __init__(self, + asteval_get_string="def f(response): return response", + **kwargs): + ''' + Args: + get_str (str): sent verbatim in the event of on_get; if None, getting of endpoint is disabled + get_reply_float (bool): apply special formatting to get return + set_str (str): sent as set_str.format(value) in the event of on_set; if None, setting of endpoint is disabled + set_value_lowercase (bool): default option to map all string set value to .lower() + **WARNING**: never set to False if using a set_value_map dict + set_value_map (str||dict): inverse of calibration to map raw set value to value sent; either a dictionary or an asteval-interpretable string + extract_raw_regex (str): regular expression search pattern applied to get return. Must be constructed with an extraction group keyed with the name "value_raw" (ie r'(?P)' ) + ''' + super().__init__(**kwargs) + self.asteval_get_string = asteval_get_string # has to contain a definition "def f(response): ... return value" + logger.debug(f'asteval_get_string: {repr(self.asteval_get_string)}') + self.evaluator(asteval_get_string) + + @calibrate() + def on_get(self): + if self._get_str is None: + raise ThrowReply('message_error_invalid_method', f"endpoint '{self.name}' does not support get") + result = self.service.send_to_device([self._get_str]) + logger.debug(f'result is: {result}') + if self._extract_raw_regex is not None: + first_result = result + matches = re.search(self._extract_raw_regex, first_result) + if matches is None: + logger.error('matching returned none') + # exceptions.DriplineValueError + raise ThrowReply('resource_error', 'device returned unparsable result, [{}] has no match to input regex [{}]'.format(first_result, self._extract_raw_regex)) + logger.debug(f"matches are: {matches.groupdict()}") + result = matches.groupdict()['value_raw'] + + result = result.replace('\x00', '') + + processed_result = self.evaluator(f"f('{result}')") + logger.debug(f"processed_result: {repr(processed_result)}") + return processed_result + + + +__all__.append('CmdEntity') +class CmdEntity(Entity): + def __init__(self, cmd_str=None, **kwargs): + Entity.__init__(self, **kwargs) + logger.debug(f"I get cmd_str: {cmd_str}, which is of type {type(cmd_str)}.") + self.cmd_str = cmd_str + + def cmd(self): + logger.debug("Command function was successfully called") + if self.cmd_str is None: + raise ThrowReply('service_error', f"endpoint '{self.name}' does not support cmd") + return self.service.send_to_device([self.cmd_str]) From d8b8a415694269487cdc9e21e39de78d043b080a Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Sun, 20 Jul 2025 10:24:46 +0200 Subject: [PATCH 2/6] extending documentation of functions and classes, reusing code that is already defined in FormatEntity in init and on_get function. --- dripline/extensions/asteval_endpoint.py | 30 +++++++++++-------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/dripline/extensions/asteval_endpoint.py b/dripline/extensions/asteval_endpoint.py index bd44fd5..21eca9b 100644 --- a/dripline/extensions/asteval_endpoint.py +++ b/dripline/extensions/asteval_endpoint.py @@ -31,30 +31,17 @@ def __init__(self, **WARNING**: never set to False if using a set_value_map dict set_value_map (str||dict): inverse of calibration to map raw set value to value sent; either a dictionary or an asteval-interpretable string extract_raw_regex (str): regular expression search pattern applied to get return. Must be constructed with an extraction group keyed with the name "value_raw" (ie r'(?P)' ) + asteval_get_string (str): function definition to format response. Default: "def f(response): return response" ''' - super().__init__(**kwargs) + FormatEntity.__init__(self, **kwargs) self.asteval_get_string = asteval_get_string # has to contain a definition "def f(response): ... return value" logger.debug(f'asteval_get_string: {repr(self.asteval_get_string)}') self.evaluator(asteval_get_string) @calibrate() def on_get(self): - if self._get_str is None: - raise ThrowReply('message_error_invalid_method', f"endpoint '{self.name}' does not support get") - result = self.service.send_to_device([self._get_str]) - logger.debug(f'result is: {result}') - if self._extract_raw_regex is not None: - first_result = result - matches = re.search(self._extract_raw_regex, first_result) - if matches is None: - logger.error('matching returned none') - # exceptions.DriplineValueError - raise ThrowReply('resource_error', 'device returned unparsable result, [{}] has no match to input regex [{}]'.format(first_result, self._extract_raw_regex)) - logger.debug(f"matches are: {matches.groupdict()}") - result = matches.groupdict()['value_raw'] - - result = result.replace('\x00', '') - + result =FormatEntiry.on_get(self) + #result = result.replace('\x00', '') processed_result = self.evaluator(f"f('{result}')") logger.debug(f"processed_result: {repr(processed_result)}") return processed_result @@ -63,7 +50,16 @@ def on_get(self): __all__.append('CmdEntity') class CmdEntity(Entity): + ''' + SCPI Entity to execute a command, instead of a get or set. + The command is given via "cmd_str" and takes no additional arguments. + This can e.g. be used to auto-calibrate, set to zero or similar device commands. + ''' def __init__(self, cmd_str=None, **kwargs): + ''' + Args: + cmd_str (str): sent verbatim in the event of cmd(). + ''' Entity.__init__(self, **kwargs) logger.debug(f"I get cmd_str: {cmd_str}, which is of type {type(cmd_str)}.") self.cmd_str = cmd_str From 8d1f7b26d583028c06cfc1afd25a911b28eb4213 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 28 Jul 2025 11:55:07 +0200 Subject: [PATCH 3/6] Fixing the dripline version in the docker compose file --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 27455bb..4a0d367 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ ARG img_user=ghcr.io/driplineorg ARG img_repo=dripline-python -#ARG img_tag=develop-dev -ARG img_tag=receiver-test +ARG img_tag=v5.0.0-dev FROM ${img_user}/${img_repo}:${img_tag} From 6ecb50a0f5892c0d915434d3c3ddf18bddc155cf Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 28 Jul 2025 11:55:28 +0200 Subject: [PATCH 4/6] fixing typo in name of super class --- dripline/extensions/asteval_endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/asteval_endpoint.py b/dripline/extensions/asteval_endpoint.py index 21eca9b..c795980 100644 --- a/dripline/extensions/asteval_endpoint.py +++ b/dripline/extensions/asteval_endpoint.py @@ -40,7 +40,7 @@ def __init__(self, @calibrate() def on_get(self): - result =FormatEntiry.on_get(self) + result =FormatEntity.on_get(self) #result = result.replace('\x00', '') processed_result = self.evaluator(f"f('{result}')") logger.debug(f"processed_result: {repr(processed_result)}") From e2e23c10012d03a7a8ebf7ffe3d3f6186072a692 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 28 Jul 2025 16:19:29 +0200 Subject: [PATCH 5/6] we have to format the raw value, since its reported as a dict, we have to apply the asteval function to the dict value not the dict itself. --- dripline/extensions/asteval_endpoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/asteval_endpoint.py b/dripline/extensions/asteval_endpoint.py index c795980..7b81a7c 100644 --- a/dripline/extensions/asteval_endpoint.py +++ b/dripline/extensions/asteval_endpoint.py @@ -41,8 +41,8 @@ def __init__(self, @calibrate() def on_get(self): result =FormatEntity.on_get(self) - #result = result.replace('\x00', '') - processed_result = self.evaluator(f"f('{result}')") + raw = result["value_raw"] + processed_result = self.evaluator(f"f('{raw}')") logger.debug(f"processed_result: {repr(processed_result)}") return processed_result From 656d2c58cb5cd749cf9c6c98ce4c1b3fd00c252d Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Tue, 29 Jul 2025 11:26:28 +0200 Subject: [PATCH 6/6] separating cmd and asteval endpoints into different files, changing FormatEntityAsteval into AstevalFormatEntity, checking of input at constructor, improving doc strings --- dripline/extensions/__init__.py | 1 + dripline/extensions/asteval_endpoint.py | 47 +++++-------------------- dripline/extensions/cmd_endpoint.py | 31 ++++++++++++++++ 3 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 dripline/extensions/cmd_endpoint.py diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index 9bafdc6..f016cc8 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -8,6 +8,7 @@ # Modules in this directory from .add_auth_spec import * +from .cmd_endpoint import * from .asteval_endpoint import * from .thermo_fisher_endpoint import * from .ethernet_thermo_fisher_service import * diff --git a/dripline/extensions/asteval_endpoint.py b/dripline/extensions/asteval_endpoint.py index 7b81a7c..93df946 100644 --- a/dripline/extensions/asteval_endpoint.py +++ b/dripline/extensions/asteval_endpoint.py @@ -3,7 +3,6 @@ from dripline.core import calibrate, ThrowReply from dripline.implementations import FormatEntity -from dripline.core import Entity import logging logger = logging.getLogger(__name__) @@ -12,31 +11,25 @@ -__all__.append('FormatEntityAsteval') -class FormatEntityAsteval(FormatEntity): +__all__.append('AstevalFormatEntity') +class AstevalFormatEntity(FormatEntity): ''' Utility Entity allowing arbitrary set and query syntax and formatting for more complicated usage cases No assumption about SCPI communication syntax. ''' def __init__(self, - asteval_get_string="def f(response): return response", + asteval_format_response_string="def f(response): return response", **kwargs): ''' Args: - get_str (str): sent verbatim in the event of on_get; if None, getting of endpoint is disabled - get_reply_float (bool): apply special formatting to get return - set_str (str): sent as set_str.format(value) in the event of on_set; if None, setting of endpoint is disabled - set_value_lowercase (bool): default option to map all string set value to .lower() - **WARNING**: never set to False if using a set_value_map dict - set_value_map (str||dict): inverse of calibration to map raw set value to value sent; either a dictionary or an asteval-interpretable string - extract_raw_regex (str): regular expression search pattern applied to get return. Must be constructed with an extraction group keyed with the name "value_raw" (ie r'(?P)' ) - asteval_get_string (str): function definition to format response. Default: "def f(response): return response" + asteval_format_response_string (str): function definition to format response. Default: "def f(response): return response" + *kwargs -> FormatEntity ''' FormatEntity.__init__(self, **kwargs) - self.asteval_get_string = asteval_get_string # has to contain a definition "def f(response): ... return value" - logger.debug(f'asteval_get_string: {repr(self.asteval_get_string)}') - self.evaluator(asteval_get_string) + self.asteval_format_response_string = asteval_format_response_string # has to contain a definition "def f(response): ... return value" + logger.debug(f'asteval_format_response_string: {repr(self.asteval_format_response_string)}') + self.evaluator(asteval_format_response_string) @calibrate() def on_get(self): @@ -45,27 +38,3 @@ def on_get(self): processed_result = self.evaluator(f"f('{raw}')") logger.debug(f"processed_result: {repr(processed_result)}") return processed_result - - - -__all__.append('CmdEntity') -class CmdEntity(Entity): - ''' - SCPI Entity to execute a command, instead of a get or set. - The command is given via "cmd_str" and takes no additional arguments. - This can e.g. be used to auto-calibrate, set to zero or similar device commands. - ''' - def __init__(self, cmd_str=None, **kwargs): - ''' - Args: - cmd_str (str): sent verbatim in the event of cmd(). - ''' - Entity.__init__(self, **kwargs) - logger.debug(f"I get cmd_str: {cmd_str}, which is of type {type(cmd_str)}.") - self.cmd_str = cmd_str - - def cmd(self): - logger.debug("Command function was successfully called") - if self.cmd_str is None: - raise ThrowReply('service_error', f"endpoint '{self.name}' does not support cmd") - return self.service.send_to_device([self.cmd_str]) diff --git a/dripline/extensions/cmd_endpoint.py b/dripline/extensions/cmd_endpoint.py new file mode 100644 index 0000000..7524dda --- /dev/null +++ b/dripline/extensions/cmd_endpoint.py @@ -0,0 +1,31 @@ +import re # used for FormatEntity + +from dripline.core import calibrate, ThrowReply +from dripline.core import Entity + +import logging +logger = logging.getLogger(__name__) + +__all__ = [] + +__all__.append('CmdEntity') +class CmdEntity(Entity): + ''' + SCPI Entity to execute a command, instead of a get or set. + The command is given via "cmd_str" and takes no additional arguments. + This can e.g. be used to auto-calibrate, set to zero or similar device commands. + ''' + def __init__(self, cmd_str=None, **kwargs): + ''' + Args: + cmd_str (str): sent verbatim in the event of cmd(). + ''' + Entity.__init__(self, **kwargs) + logger.debug(f"I get cmd_str: {cmd_str}, which is of type {type(cmd_str)}.") + if cmd_str is None: + raise ValueError("cmd_str is required for CmdEntity") + self.cmd_str = cmd_str + + def cmd(self): + logger.debug("Command function was successfully called") + return self.service.send_to_device([self.cmd_str])