From 3097b601b68493173c79f5457c84ca4a86c52460 Mon Sep 17 00:00:00 2001 From: "Walter C. Pettus" Date: Wed, 5 Feb 2025 17:20:16 -0800 Subject: [PATCH 01/43] initial muxer implementation --- dripline/extensions/__init__.py | 1 + dripline/extensions/muxer_service.py | 87 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 dripline/extensions/muxer_service.py diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index b62df1e..b61a31e 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 .muxer_service import * diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py new file mode 100644 index 0000000..a0e433b --- /dev/null +++ b/dripline/extensions/muxer_service.py @@ -0,0 +1,87 @@ +''' +A class to interface with the multiplexer aka muxer instrument +''' + +from dripline.core import ThrowReply, Entity, calibrate +from dripline.implementations import EthernetSCPIService + +import logging +logger = logging.getLogger(__name__) + +__all__ = [] +__all__.append('MuxerService') + +class MuxerService(EthernetSCPIService): + ''' + Provider to interface with muxer + ''' + + def __init__(self, scan_interval=0,**kwargs): + ''' + scan_interval (int): time between scans in seconds + ''' + EthernetSCPIService.__init__(self,**kwargs) + if scan_interval <= 0: + raise ThrowReply('service_error_invalid_value', 'scan interval must be > 0') + self.scan_interval = scan_interval + self.configure_scan() + + def configure_scan(self, *args, **kwargs): + ''' + loops over the provider's internal list of endpoints and attempts to configure each, then configures and begins scan + ''' + self.send_to_device(['ABOR;*CLS;*OPC?']) + + ch_scan_list = list() + for childname, child in self.sync_children.items(): + + if not isinstance(child, MuxerGetEntity): + continue + error_data = self.send_to_device([child.conf_str+';*OPC?','SYST:ERR?']) + if error_data != '1;+0,"No error"': + logger.critical('Error detected; cannot configure muxer') + raise ThrowReply('resource_error', + f'{error_data} when attempting to configure endpoint <{childname}>') + + ch_scan_list.append(str(child.ch_number)) + child.log_interval = self.scan_interval + + scan_list_cmd = 'ROUT:SCAN (@{})'.format(','.join(ch_scan_list)) + self.send_to_device([scan_list_cmd+';*OPC?',\ + 'TRIG:SOUR TIM;*OPC?',\ + 'TRIG:COUN INF;*OPC?',\ + 'TRIG:TIM {};*OPC?'.format(self.scan_interval),\ + 'INIT;*ESE?']) + + +__all__.append('MuxerGetEntity') +class MuxerGetEntity(Entity): + ''' + Entity for communication with muxer endpoints. No set functionality. + ''' + + def __init__(self, + ch_number, + conf_str=None, + **kwargs): + ''' + ch_number (int): channel number for endpoint + conf_str (str): used by MuxerService to configure endpoint scan + ''' + Entity.__init__(self, **kwargs) + if conf_str is None: + raise ThrowReply('service_error_invalid_value', + f' required for MuxerGetEntity {self.name}') + self.get_str = "DATA:LAST? (@{})".format(ch_number) + self.ch_number = ch_number + self.conf_str = conf_str.format(ch_number) + + @calibrate() + def on_get(self): + result = self.service.send_to_device([self.get_str.format(self.ch_number)]) + logger.debug('very raw is: {}'.format(result)) + return result.split()[0] + + def on_set(self, value): + raise ThrowReply('message_error_invalid_method', + f'endpoint {self.name} does not support set') From 5a36087e8c0cc64cc00c4c8cc1632e11445fdd62 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 19 Mar 2025 15:29:53 -0700 Subject: [PATCH 02/43] two files to connect muxer to dripline service --- muxer-test.yaml | 31 +++++++++++++++++++++++ muxer.yaml | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 muxer-test.yaml create mode 100644 muxer.yaml diff --git a/muxer-test.yaml b/muxer-test.yaml new file mode 100644 index 0000000..ed8c3a2 --- /dev/null +++ b/muxer-test.yaml @@ -0,0 +1,31 @@ +version: "3" +services: + + # The broker for the mesh + rabbit-broker: + image: rabbitmq:3-management + ports: + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=dripline + - RABBITMQ_DEFAULT_PASS=dripline + healthcheck: + test: ["CMD-SHELL", "curl -u dripline:dripline http://rabbit-broker:15672/api/overview &> /dev/null || exit 1"] + + muxer-service: + image: ghcr.io/project8/dragonfly:muxer_test + depends_on: + rabbit-broker: + condition: service_healthy + volumes: + - ./muxer.yaml:/root/muxer.yaml + environment: + - DRIPLINE_USER=dripline + - DRIPLINE_PASSWORD=dripline + command: + - dl-serve + - -c + - /root/muxer.yaml + - -vv + - -b + - rabbit-broker diff --git a/muxer.yaml b/muxer.yaml new file mode 100644 index 0000000..dc22721 --- /dev/null +++ b/muxer.yaml @@ -0,0 +1,65 @@ +name: muxer +module: MuxerService +socket_info: ('glenlivet.p8', 5024) +cmd_at_reconnect: + - + - "" + - "SYST:ERR?" + - "TRIG:DEL:AUTO?" +command_terminator: "\r\n" +response_terminator: "\r\n34980A> " +reply_echo_cmd: True +scan_interval: 30 +endpoints: +##################### Cable B #################### + # PT 100 1/12 + - name: pt100_1_12 + module: MuxerGetEntity + ch_number: 1011 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' + # PT 100 2/12 + - name: pt100_2_12 + module: MuxerGetEntity + ch_number: 1012 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' +##################### Cable C #################### + # PT 100 3/12 + - name: pt100_3_12 + module: MuxerGetEntity + ch_number: 1004 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' + # PT 100 4/12 + - name: pt100_4_12 + module: MuxerGetEntity + ch_number: 1005 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' + # PT 100 5/12 + - name: pt100_5_12 + module: MuxerGetEntity + ch_number: 1006 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' + # PT 100 6/12 + - name: pt100_6_12 + module: MuxerGetEntity + ch_number: 1007 + conf_str: 'CONF:FRES AUTO,DEF,(@{})' + calibration: 'pt100_calibration({})' + # PT 100 7/12 +## - name: pt_100_7_12 + ## modeule: MuxerGetEntity + ## ch_number: 1013 + ## conf_str: 'CONF:FRES AUTO,DEF,(@{})' + ## calibration: 'pt100_calibration({})' + + # this is not set up but wanted to keep the syntax available as an example + - name: hall_probe_field + module: MuxerGetEntity + ch_number: 1029 + conf_str: 'CONF:VOLT:DC 10,(@{})' + calibration: "(0.9991/0.847)*(1000*{}+0.007)" + From 37d26bd34f4eedfdb276eb4ecbce0923f4dcad06 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 2 Apr 2025 15:22:15 -0700 Subject: [PATCH 03/43] added RelayService by copying old spime_endpoints.py --- dripline/extensions/muxer_service.py | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index a0e433b..26acaef 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -85,3 +85,60 @@ def on_get(self): def on_set(self, value): raise ThrowReply('message_error_invalid_method', f'endpoint {self.name} does not support set') +class MuxerRelay(Entity): + ''' + Entity to communicate with relay cards in muxer, + ''' + def __init__(self, + ch_number, + relay_type=None, + **kwargs): + ''' + # default get/set strings + if 'get_str' not in kwargs: + if relay_type=='relay' or relay_type=='polarity': + kwargs.update( {'get_str':':ROUTE:OPEN? (@{})'.format(ch_number)} ) + elif relay_type=='switch': + kwargs.update( {'get_str':':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + if 'set_str' not in kwargs: + kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) + # Default kwargs for get_on_set and set_value_lowercase + if 'get_on_set' not in kwargs: + kwargs.update( {'get_on_set':True} ) + if 'set_value_lowercase' not in kwargs: + kwargs.update( {'set_value_lowercase':True} ) + # Default set_value_map and calibration for known relay types (relay, polarity, switch) + if relay_type == 'relay': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', + 0: 'CLOSE', + 'on': 'OPEN', + 'off': 'CLOSE', + 'enable': 'OPEN', + 'disable': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'enabled', + '0': 'disabled'} } ) + elif relay_type == 'polarity': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', + 0: 'CLOSE', + 'positive': 'OPEN', + 'negative': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'positive', + '0': 'negative'} } ) + elif relay_type == 'switch': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {0: 'OPEN', + 1: 'CLOSE', + 'off': 'OPEN', + 'on': 'CLOSE', + 'disable': 'OPEN', + 'enable': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'0': 'disabled', + '1': 'enabled'} } ) + elif relay_type is not None: + raise ThrowReply("message_error_invalid_method", + f"endpoint {self.name} expect 'relay'or 'polarity'") From 23e118e15a431b5d4a08f65d9e70ff710c4876e9 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 30 Apr 2025 15:36:22 -0700 Subject: [PATCH 04/43] closed the comment that was commenting out all of RelayService --- dripline/extensions/muxer_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 26acaef..4a467a0 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -94,6 +94,9 @@ def __init__(self, relay_type=None, **kwargs): ''' + ch_number (int): channel number for endpoint + relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) + ''' # default get/set strings if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': From d0eb97ccf67dcc7c79c31e5a92e571618827256e Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 30 Apr 2025 18:01:48 -0700 Subject: [PATCH 05/43] commenting out the error lines --- dripline/extensions/muxer_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 4a467a0..15acb5b 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -93,10 +93,10 @@ def __init__(self, ch_number, relay_type=None, **kwargs): - ''' - ch_number (int): channel number for endpoint - relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) - ''' +# ''' +# ch_number (int): channel number for endpoint + # relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) + # ''' # default get/set strings if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': From 32b43e2b3b139a43c2230af9e2a4a89270a0270f Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Mon, 5 May 2025 13:57:43 -0700 Subject: [PATCH 06/43] added an 'append.MuxerRelay' above the relay class and fixed a comment indent --- dripline/extensions/muxer_service.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 15acb5b..05002a3 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -85,6 +85,7 @@ def on_get(self): def on_set(self, value): raise ThrowReply('message_error_invalid_method', f'endpoint {self.name} does not support set') +__all__.append('MuxerRelay') class MuxerRelay(Entity): ''' Entity to communicate with relay cards in muxer, @@ -93,10 +94,10 @@ def __init__(self, ch_number, relay_type=None, **kwargs): -# ''' -# ch_number (int): channel number for endpoint - # relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) - # ''' + ''' + ch_number (int): channel number for endpoint + relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) + ''' # default get/set strings if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': From 52e578c2580d0b9fa5b0dff4733dacdbee9da219 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Mon, 5 May 2025 15:09:27 -0700 Subject: [PATCH 07/43] added an 'Enitity.__init__(self,**kwargs) and hopefully fixed the indentation around line 102 --- dripline/extensions/muxer_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 05002a3..0cccc15 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -94,10 +94,11 @@ def __init__(self, ch_number, relay_type=None, **kwargs): - ''' + ''' ch_number (int): channel number for endpoint relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) - ''' + ''' + Entity.__init__(self, **kwargs) # default get/set strings if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': From b30672a029f853d651e3aabd7f19b807706d39b3 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Mon, 5 May 2025 15:31:36 -0700 Subject: [PATCH 08/43] changed the tabs around using 'expand -t 4' --- dripline/extensions/muxer_service.py | 76 ++++++++++++++-------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 0cccc15..13050c6 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -91,59 +91,59 @@ class MuxerRelay(Entity): Entity to communicate with relay cards in muxer, ''' def __init__(self, - ch_number, - relay_type=None, - **kwargs): - ''' - ch_number (int): channel number for endpoint + ch_number, + relay_type=None, + **kwargs): + ''' + ch_number (int): channel number for endpoint relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) - ''' - Entity.__init__(self, **kwargs) - # default get/set strings - if 'get_str' not in kwargs: - if relay_type=='relay' or relay_type=='polarity': - kwargs.update( {'get_str':':ROUTE:OPEN? (@{})'.format(ch_number)} ) - elif relay_type=='switch': - kwargs.update( {'get_str':':ROUTE:CLOSE? (@{})'.format(ch_number)} ) - if 'set_str' not in kwargs: - kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) - # Default kwargs for get_on_set and set_value_lowercase - if 'get_on_set' not in kwargs: - kwargs.update( {'get_on_set':True} ) - if 'set_value_lowercase' not in kwargs: - kwargs.update( {'set_value_lowercase':True} ) - # Default set_value_map and calibration for known relay types (relay, polarity, switch) - if relay_type == 'relay': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', + ''' + Entity.__init__(self, **kwargs) + # default get/set strings + if 'get_str' not in kwargs: + if relay_type=='relay' or relay_type=='polarity': + kwargs.update( {'get_str':':ROUTE:OPEN? (@{})'.format(ch_number)} ) + elif relay_type=='switch': + kwargs.update( {'get_str':':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + if 'set_str' not in kwargs: + kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) + # Default kwargs for get_on_set and set_value_lowercase + if 'get_on_set' not in kwargs: + kwargs.update( {'get_on_set':True} ) + if 'set_value_lowercase' not in kwargs: + kwargs.update( {'set_value_lowercase':True} ) + # Default set_value_map and calibration for known relay types (relay, polarity, switch) + if relay_type == 'relay': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', 0: 'CLOSE', 'on': 'OPEN', 'off': 'CLOSE', 'enable': 'OPEN', 'disable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'enabled', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'enabled', '0': 'disabled'} } ) - elif relay_type == 'polarity': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', + elif relay_type == 'polarity': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', 0: 'CLOSE', 'positive': 'OPEN', 'negative': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'positive', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'positive', '0': 'negative'} } ) - elif relay_type == 'switch': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {0: 'OPEN', + elif relay_type == 'switch': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {0: 'OPEN', 1: 'CLOSE', 'off': 'OPEN', 'on': 'CLOSE', 'disable': 'OPEN', 'enable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'0': 'disabled', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'0': 'disabled', '1': 'enabled'} } ) - elif relay_type is not None: - raise ThrowReply("message_error_invalid_method", + elif relay_type is not None: + raise ThrowReply("message_error_invalid_method", f"endpoint {self.name} expect 'relay'or 'polarity'") From 1f9d28281d781d299b347bffc7f298bb4ed90830 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Tue, 6 May 2025 11:30:50 -0700 Subject: [PATCH 09/43] changed 'Entity.__init__(self,**kwargs)' to be at the end of Relay --- dripline/extensions/muxer_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 13050c6..7fccd2d 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -98,7 +98,7 @@ def __init__(self, ch_number (int): channel number for endpoint relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) ''' - Entity.__init__(self, **kwargs) + # default get/set strings if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': @@ -146,4 +146,5 @@ def __init__(self, '1': 'enabled'} } ) elif relay_type is not None: raise ThrowReply("message_error_invalid_method", - f"endpoint {self.name} expect 'relay'or 'polarity'") + f"endpoint {self.name} expect 'relay'or 'polarity'") + Entity.__init__(self, **kwargs) From 4722d3bf6ff3d78691f58f5197781029b7dddf1f Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Tue, 6 May 2025 15:13:04 -0700 Subject: [PATCH 10/43] trying to address the get_str eror using pop --- dripline/extensions/muxer_service.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 7fccd2d..9aef636 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -147,4 +147,11 @@ def __init__(self, elif relay_type is not None: raise ThrowReply("message_error_invalid_method", f"endpoint {self.name} expect 'relay'or 'polarity'") + # Remove invalid args before calling Entity + get_str = kwargs.pop('get_str', None) + set_str = kwargs.pop('set_str', None) + Entity.__init__(self, **kwargs) + + self.get_str = get_str + self.set_str = set_str #another addition to fix the get_str error From d21d26b9cde720384c0d83ad08f4e21a216c9b4e Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Tue, 6 May 2025 15:29:08 -0700 Subject: [PATCH 11/43] added self. in front of any kwargs, hoping I didnt break spacing --- dripline/extensions/muxer_service.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 9aef636..1084f99 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -110,39 +110,39 @@ def __init__(self, # Default kwargs for get_on_set and set_value_lowercase if 'get_on_set' not in kwargs: kwargs.update( {'get_on_set':True} ) - if 'set_value_lowercase' not in kwargs: - kwargs.update( {'set_value_lowercase':True} ) + if self.set_value_lowercase not in kwargs: + kwargs.update( {self.set_value_lowercase :True} ) # Default set_value_map and calibration for known relay types (relay, polarity, switch) if relay_type == 'relay': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', + if self.set_value_map not in kwargs: + kwargs.update( { self.set_value_map : {1: 'OPEN', 0: 'CLOSE', 'on': 'OPEN', 'off': 'CLOSE', 'enable': 'OPEN', 'disable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'enabled', + if self.calibration not in kwargs: + kwargs.update( { self.calibration : {'1': 'enabled', '0': 'disabled'} } ) elif relay_type == 'polarity': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', + if self.set_value_map not in kwargs: + kwargs.update( { self.set_value_map : {1: 'OPEN', 0: 'CLOSE', 'positive': 'OPEN', 'negative': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'positive', + if self.calibration not in kwargs: + kwargs.update( { self.calibration : {'1': 'positive', '0': 'negative'} } ) elif relay_type == 'switch': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {0: 'OPEN', + if self.set_value_map not in kwargs: + kwargs.update( { self.set_value_map : {0: 'OPEN', 1: 'CLOSE', 'off': 'OPEN', 'on': 'CLOSE', 'disable': 'OPEN', 'enable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'0': 'disabled', + if self.calibration not in kwargs: + kwargs.update( { self.calibration : {'0': 'disabled', '1': 'enabled'} } ) elif relay_type is not None: raise ThrowReply("message_error_invalid_method", From cf1000bd05c8c3b51a00f42873f6ef055aecdc12 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Tue, 6 May 2025 15:37:15 -0700 Subject: [PATCH 12/43] did away with adding self. to all objects, ended up doing the kwargs.pop to all of them, which is also what I see in object.py (havent done it to calibration yet tho) --- dripline/extensions/muxer_service.py | 32 ++++++++++++++++------------ 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 1084f99..369ea6c 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -110,39 +110,39 @@ def __init__(self, # Default kwargs for get_on_set and set_value_lowercase if 'get_on_set' not in kwargs: kwargs.update( {'get_on_set':True} ) - if self.set_value_lowercase not in kwargs: - kwargs.update( {self.set_value_lowercase :True} ) + if 'set_value_lowercase' not in kwargs: + kwargs.update( {'set_value_lowercase' :True} ) # Default set_value_map and calibration for known relay types (relay, polarity, switch) if relay_type == 'relay': - if self.set_value_map not in kwargs: - kwargs.update( { self.set_value_map : {1: 'OPEN', + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', 0: 'CLOSE', 'on': 'OPEN', 'off': 'CLOSE', 'enable': 'OPEN', 'disable': 'CLOSE'} } ) - if self.calibration not in kwargs: - kwargs.update( { self.calibration : {'1': 'enabled', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'enabled', '0': 'disabled'} } ) elif relay_type == 'polarity': - if self.set_value_map not in kwargs: - kwargs.update( { self.set_value_map : {1: 'OPEN', + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', 0: 'CLOSE', 'positive': 'OPEN', 'negative': 'CLOSE'} } ) - if self.calibration not in kwargs: - kwargs.update( { self.calibration : {'1': 'positive', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'positive', '0': 'negative'} } ) elif relay_type == 'switch': - if self.set_value_map not in kwargs: - kwargs.update( { self.set_value_map : {0: 'OPEN', + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {0: 'OPEN', 1: 'CLOSE', 'off': 'OPEN', 'on': 'CLOSE', 'disable': 'OPEN', 'enable': 'CLOSE'} } ) - if self.calibration not in kwargs: - kwargs.update( { self.calibration : {'0': 'disabled', + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'0': 'disabled', '1': 'enabled'} } ) elif relay_type is not None: raise ThrowReply("message_error_invalid_method", @@ -150,8 +150,12 @@ def __init__(self, # Remove invalid args before calling Entity get_str = kwargs.pop('get_str', None) set_str = kwargs.pop('set_str', None) + set_value_map = kwargs.pop('set_value_map', None) + set_value_lowercase = kwargs.pop('set_value_lowercase', None) Entity.__init__(self, **kwargs) self.get_str = get_str self.set_str = set_str #another addition to fix the get_str error + self.set_value_lowercase=set_value_lowercase + self.set_value_map= set_value_map From 8ae9e0b95265737854cbce4953ba6a7094e431b3 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 10:35:01 -0700 Subject: [PATCH 13/43] changing the input to MuxerRelay from Entity to FormatEntity --- dripline/extensions/muxer_service.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 369ea6c..510bc40 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -86,7 +86,7 @@ def on_set(self, value): raise ThrowReply('message_error_invalid_method', f'endpoint {self.name} does not support set') __all__.append('MuxerRelay') -class MuxerRelay(Entity): +class MuxerRelay(FormatEntity): ''' Entity to communicate with relay cards in muxer, ''' @@ -148,14 +148,15 @@ def __init__(self, raise ThrowReply("message_error_invalid_method", f"endpoint {self.name} expect 'relay'or 'polarity'") # Remove invalid args before calling Entity - get_str = kwargs.pop('get_str', None) - set_str = kwargs.pop('set_str', None) - set_value_map = kwargs.pop('set_value_map', None) - set_value_lowercase = kwargs.pop('set_value_lowercase', None) +# commenting all of this out to see if changing to FormatEntity will work + # get_str = kwargs.pop('get_str', None) + # set_str = kwargs.pop('set_str', None) + # set_value_map = kwargs.pop('set_value_map', None) + # set_value_lowercase = kwargs.pop('set_value_lowercase', None) Entity.__init__(self, **kwargs) - self.get_str = get_str - self.set_str = set_str #another addition to fix the get_str error - self.set_value_lowercase=set_value_lowercase - self.set_value_map= set_value_map + # self.get_str = get_str + # self.set_str = set_str #another addition to fix the get_str error + # self.set_value_lowercase=set_value_lowercase + # self.set_value_map= set_value_map From 3985355ba4092b99fdde53fdc96be7cabfa5fd5d Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 10:54:09 -0700 Subject: [PATCH 14/43] importing FormatEntity from implementations --- dripline/extensions/muxer_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 510bc40..9416bf9 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -3,7 +3,7 @@ ''' from dripline.core import ThrowReply, Entity, calibrate -from dripline.implementations import EthernetSCPIService +from dripline.implementations import EthernetSCPIService, FormatEntity import logging logger = logging.getLogger(__name__) From 854ae0b7377d4bde48d9e3948c4b458c0a01f733 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 10:59:26 -0700 Subject: [PATCH 15/43] removed ' ' from get_str to see if that is the issue --- dripline/extensions/muxer_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 9416bf9..182a0df 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -100,11 +100,11 @@ def __init__(self, ''' # default get/set strings - if 'get_str' not in kwargs: + if get_str not in kwargs: if relay_type=='relay' or relay_type=='polarity': - kwargs.update( {'get_str':':ROUTE:OPEN? (@{})'.format(ch_number)} ) + kwargs.update( {get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) elif relay_type=='switch': - kwargs.update( {'get_str':':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + kwargs.update( {get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) if 'set_str' not in kwargs: kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) # Default kwargs for get_on_set and set_value_lowercase From 62e45249cd40e420090e96670e4f972b67eb85ec Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 14:00:28 -0700 Subject: [PATCH 16/43] putting self. in front get_str --- dripline/extensions/muxer_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 182a0df..0d26294 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -100,11 +100,11 @@ def __init__(self, ''' # default get/set strings - if get_str not in kwargs: + if self.get_str not in kwargs: if relay_type=='relay' or relay_type=='polarity': - kwargs.update( {get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) + kwargs.update( {self.get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) elif relay_type=='switch': - kwargs.update( {get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + kwargs.update( {self.get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) if 'set_str' not in kwargs: kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) # Default kwargs for get_on_set and set_value_lowercase From 97cfa8b56473d1eec85cfa51063215406663e2ad Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 14:02:48 -0700 Subject: [PATCH 17/43] adding the kwargs.pop thing again to get_str --- dripline/extensions/muxer_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 0d26294..b0021e8 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -149,14 +149,14 @@ def __init__(self, f"endpoint {self.name} expect 'relay'or 'polarity'") # Remove invalid args before calling Entity # commenting all of this out to see if changing to FormatEntity will work - # get_str = kwargs.pop('get_str', None) + get_str = kwargs.pop('get_str', None) # set_str = kwargs.pop('set_str', None) # set_value_map = kwargs.pop('set_value_map', None) # set_value_lowercase = kwargs.pop('set_value_lowercase', None) Entity.__init__(self, **kwargs) - # self.get_str = get_str + self.get_str = get_str # self.set_str = set_str #another addition to fix the get_str error # self.set_value_lowercase=set_value_lowercase # self.set_value_map= set_value_map From c01ccbefd7e46d276a1b777b1921f40579cfdc9d Mon Sep 17 00:00:00 2001 From: "Walter C. Pettus" Date: Wed, 7 May 2025 17:08:32 -0400 Subject: [PATCH 18/43] Fixing super __init__ for Relay --- dripline/extensions/muxer_service.py | 135 +++++++++++++-------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index b0021e8..c97daa0 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -85,78 +85,71 @@ def on_get(self): def on_set(self, value): raise ThrowReply('message_error_invalid_method', f'endpoint {self.name} does not support set') + + + __all__.append('MuxerRelay') class MuxerRelay(FormatEntity): - ''' - Entity to communicate with relay cards in muxer, - ''' - def __init__(self, - ch_number, - relay_type=None, - **kwargs): + ''' + Entity to communicate with relay cards in muxer, ''' - ch_number (int): channel number for endpoint + def __init__(self, + ch_number, + relay_type=None, + **kwargs): + ''' + ch_number (int): channel number for endpoint relay_type (None,'relay','polarity','switch'): automatically configure set_value_map and calibration dictionaries (overwriteable) - ''' - - # default get/set strings - if self.get_str not in kwargs: - if relay_type=='relay' or relay_type=='polarity': - kwargs.update( {self.get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) - elif relay_type=='switch': - kwargs.update( {self.get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) - if 'set_str' not in kwargs: - kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) - # Default kwargs for get_on_set and set_value_lowercase - if 'get_on_set' not in kwargs: - kwargs.update( {'get_on_set':True} ) - if 'set_value_lowercase' not in kwargs: - kwargs.update( {'set_value_lowercase' :True} ) - # Default set_value_map and calibration for known relay types (relay, polarity, switch) - if relay_type == 'relay': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', - 0: 'CLOSE', - 'on': 'OPEN', - 'off': 'CLOSE', - 'enable': 'OPEN', - 'disable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'enabled', - '0': 'disabled'} } ) - elif relay_type == 'polarity': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {1: 'OPEN', - 0: 'CLOSE', - 'positive': 'OPEN', - 'negative': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'1': 'positive', - '0': 'negative'} } ) - elif relay_type == 'switch': - if 'set_value_map' not in kwargs: - kwargs.update( { 'set_value_map' : {0: 'OPEN', - 1: 'CLOSE', - 'off': 'OPEN', - 'on': 'CLOSE', - 'disable': 'OPEN', - 'enable': 'CLOSE'} } ) - if 'calibration' not in kwargs: - kwargs.update( { 'calibration' : {'0': 'disabled', - '1': 'enabled'} } ) - elif relay_type is not None: - raise ThrowReply("message_error_invalid_method", - f"endpoint {self.name} expect 'relay'or 'polarity'") - # Remove invalid args before calling Entity -# commenting all of this out to see if changing to FormatEntity will work - get_str = kwargs.pop('get_str', None) - # set_str = kwargs.pop('set_str', None) - # set_value_map = kwargs.pop('set_value_map', None) - # set_value_lowercase = kwargs.pop('set_value_lowercase', None) - - Entity.__init__(self, **kwargs) - - self.get_str = get_str - # self.set_str = set_str #another addition to fix the get_str error - # self.set_value_lowercase=set_value_lowercase - # self.set_value_map= set_value_map + ''' + + # default get/set strings + if get_str not in kwargs: + if relay_type=='relay' or relay_type=='polarity': + kwargs.update( {get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) + elif relay_type=='switch': + kwargs.update( {get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + if 'set_str' not in kwargs: + kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) + # Default kwargs for get_on_set and set_value_lowercase + if 'get_on_set' not in kwargs: + kwargs.update( {'get_on_set':True} ) + if 'set_value_lowercase' not in kwargs: + kwargs.update( {'set_value_lowercase' :True} ) + # Default set_value_map and calibration for known relay types (relay, polarity, switch) + if relay_type == 'relay': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', + 0: 'CLOSE', + 'on': 'OPEN', + 'off': 'CLOSE', + 'enable': 'OPEN', + 'disable': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'enabled', + '0': 'disabled'} } ) + elif relay_type == 'polarity': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {1: 'OPEN', + 0: 'CLOSE', + 'positive': 'OPEN', + 'negative': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'1': 'positive', + '0': 'negative'} } ) + elif relay_type == 'switch': + if 'set_value_map' not in kwargs: + kwargs.update( { 'set_value_map' : {0: 'OPEN', + 1: 'CLOSE', + 'off': 'OPEN', + 'on': 'CLOSE', + 'disable': 'OPEN', + 'enable': 'CLOSE'} } ) + if 'calibration' not in kwargs: + kwargs.update( { 'calibration' : {'0': 'disabled', + '1': 'enabled'} } ) + elif relay_type is not None: + raise ThrowReply("message_error_invalid_method", + f"endpoint {self.name} expect 'relay'or 'polarity'") + + FormatEntity.__init__(self, **kwargs) + From da797444e608f34d318f11679f0b154ae7a69f2e Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 14:36:46 -0700 Subject: [PATCH 19/43] indentation issue at line 147 w calibration --- dripline/extensions/muxer_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index c97daa0..2e0f90d 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -144,7 +144,7 @@ def __init__(self, 'on': 'CLOSE', 'disable': 'OPEN', 'enable': 'CLOSE'} } ) - if 'calibration' not in kwargs: + if 'calibration' not in kwargs: kwargs.update( { 'calibration' : {'0': 'disabled', '1': 'enabled'} } ) elif relay_type is not None: From 21518e0fc722f84a5d166cf9962f6a36afa72633 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 7 May 2025 14:39:18 -0700 Subject: [PATCH 20/43] putting get_str back into a string --- dripline/extensions/muxer_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/muxer_service.py b/dripline/extensions/muxer_service.py index 2e0f90d..7dce046 100644 --- a/dripline/extensions/muxer_service.py +++ b/dripline/extensions/muxer_service.py @@ -103,11 +103,11 @@ def __init__(self, ''' # default get/set strings - if get_str not in kwargs: + if 'get_str' not in kwargs: if relay_type=='relay' or relay_type=='polarity': - kwargs.update( {get_str:':ROUTE:OPEN? (@{})'.format(ch_number)} ) + kwargs.update( {'get_str':':ROUTE:OPEN? (@{})'.format(ch_number)} ) elif relay_type=='switch': - kwargs.update( {get_str:':ROUTE:CLOSE? (@{})'.format(ch_number)} ) + kwargs.update( {'get_str':':ROUTE:CLOSE? (@{})'.format(ch_number)} ) if 'set_str' not in kwargs: kwargs.update( {'set_str':':ROUTE:{{}} (@{});{}'.format(ch_number,kwargs['get_str'])} ) # Default kwargs for get_on_set and set_value_lowercase From 18f2eed942ff12caf12b4b0843377b7ecad08ee9 Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 16 Jul 2025 13:28:07 -0700 Subject: [PATCH 21/43] Pid loop from phase 2 changed certain syntax, mostly updated throw reply, but also provider to service --- dripline/extensions/cca_pid_loop.py | 199 ++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 dripline/extensions/cca_pid_loop.py diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py new file mode 100644 index 0000000..eab617b --- /dev/null +++ b/dripline/extensions/cca_pid_loop.py @@ -0,0 +1,199 @@ +''' +Implementation of a PID control loop +''' + +from __future__ import print_function +__all__ = [] + +import time +import datetime + +from dripline.core import AlertConsumer, constants, exceptions, fancy_doc, ThrowReply + +import logging +logger = logging.getLogger(__name__) + +__all__.append('PidController') +@fancy_doc +class PidController(AlertConsumer): + ''' + Implementation of a PID control loop with constant offset. That is, the PID equation + is used to compute the **change** to the value of some channel and not the value + itself. In the case of temperature control, this makes sense if the loop is working + against some fixed load (such as a cryocooler). + + The input sensor can be anything which broadcasts regular values on the alerts + exchange (using the standard sensor_value. routing key format). Usually + this would be a temperature sensor, but it could be anything. Similarly, the + output is anything that can be set to a float value, though a current output + is probably most common. After setting the new value of current, this value is checked + to be within a range around the desired value. + + **NOTE** + The "exchange" and "keys" arguments list below come from the Service class but + are not valid for this class. Any value provided will be ignored + ''' + + def __init__(self, + input_channel, + output_channel, + check_channel, + status_channel, + payload_field='value_cal', + tolerance = 0.01, + target_value=110, + proportional=0.0, integral=0.0, differential=0.0, + maximum_out=1.0, minimum_out=1.0, delta_out_min= 0.001, + enable_offset_term=True, + minimum_elapsed_time=0, + **kwargs + ): + ''' + input_channel (str): name of the logging sensor to use as input to PID (this will override any provided values for keys) + output_channel (str): name of the endpoint to be set() based on PID + check_channel (str): name of the endpoint to be checked() after a set() + status_channel (str): name of the endpoint which controls the status of the heater (enabled/disabled output) + payload_field (str): name of the field in the payload when the sensor logs (default is 'value_cal' and 'value_raw' is the only other expected value) + target_value (float): numerical value to which the loop will try to lock the input_channel + proportional (float): coefficient for the P term in the PID equation + integral (float): coefficient for the I term in the PID equation + differential (float): coefficient for the D term in the PID equation + maximum_out (float): max value to which the output_channel may be set; if the PID equation gives a larger value this value is used instead + delta_out_min (float): minimum value by which to change the output_channel; if the PID equation gives a smaller change, the value is left unchanged (no set is attempted) + tolerance (float): acceptable difference between the set and get values (default: 0.01) + minimum_elapsed_time (float): minimum time interval to perform PID calculation over + ''' + kwargs.update({'keys':['sensor_value.'+input_channel]}) + AlertConsumer.__init__(self, **kwargs) + + self._set_channel = output_channel + self._check_channel = check_channel + self._status_channel = status_channel + self.payload_field = payload_field + self.tolerance = tolerance + + self._last_data = {'value':None, 'time':datetime.datetime.utcnow()} + self.target_value = target_value + + self.Kproportional = proportional + self.Kintegral = integral + self.Kdifferential = differential + + self._integral= 0 + + self.max_current = maximum_out + self.min_current = minimum_out + self.min_current_change = delta_out_min + + self.enable_offset_term = enable_offset_term + self.minimum_elapsed_time = minimum_elapsed_time + + self.__validate_status() + self._old_current = self.__get_current() + logger.info('starting current is: {}'.format(self._old_current)) + + def __get_current(self): + #value = self.provider.get(self._check_channel)[self.payload_field] + alue = self.service.get(self._check_channel)[self.payload_field] + logger.info('current get is {}'.format(value)) + + try: + value = float(value) + except (TypeError, ValueError): + raise ThrowReply('DriplineValueError','value get ({}) is not floatable'.format(value)) + return value + + def __validate_status(self): + # value = self.provider.get(self._status_channel)[self.payload_field] + value = self.service.get(self._status_channel)[self.payload_field] + if value == 'enabled': + logger.debug("{} returns {}".format(self._status_channel,value)) + else: + logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) + raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + + def this_consume(self, message, method): + logger.info('consuming message') + this_value = message.payload[self.payload_field] + if this_value is None: + logger.info('value is None') + return + + this_time = datetime.datetime.strptime(message['timestamp'], constants.TIME_FORMAT) + if (this_time - self._last_data['time']).total_seconds() < self.minimum_elapsed_time: + # handle self._force_reprocess from @target_value.setter + if not self._force_reprocess: + logger.info("not enough time has elasped: {}[{}]".format((this_time - self._last_data['time']).total_seconds(),self.minimum_elapsed_time)) + return + logger.info("Forcing process due to changed target_value") + self._force_reprocess = False + + self.process_new_value(timestamp=this_time, value=float(this_value)) + + @property + def target_value(self): + return self._target_value + @target_value.setter + def target_value(self, value): + self._target_value = value + self._integral = 0 + self._force_reprocess = True + + def set_current(self, value): + logger.info('going to set new current to: {}'.format(value)) + #reply = self.provider.set(self._set_channel, value) + reply = self.service.set(self._set_channel, value) + logger.info('set response was: {}'.format(reply)) + + def process_new_value(self, value, timestamp): + + delta = self.target_value - value + logger.info('value is <{}>; delta is <{}>'.format(value, delta)) + + self._integral += delta * (timestamp - self._last_data['time']).total_seconds() + if (timestamp - self._last_data['time']).total_seconds() < 2*self.minimum_elapsed_time: + try: + derivative = (self._last_data['value'] - value) / (timestamp - self._last_data['time']).total_seconds() + except TypeError: + derivative = 0 + else: + logger.warning("invalid time for calculating derivative") + derivative = 0. + self._last_data = {'value': value, 'time': timestamp} + + logger.info("proportional <{}>; integral <{}>; differential <{}>".format\ + (self.Kproportional*delta, self.Kintegral*self._integral, self.Kdifferential*derivative)) + change_to_current = (self.Kproportional * delta + + self.Kintegral * self._integral + + self.Kdifferential * derivative + ) + new_current = (self._old_current or 0)*self.enable_offset_term + change_to_current + + if abs(change_to_current) < self.min_current_change: + logger.info("current change less than min delta") + logger.info("old[new] are: {}[{}]".format(self._old_current,new_current)) + return + logger.info('computed new current to be: {}'.format(new_current)) + if new_current > self.max_current: + logger.info("new current above max") + new_current = self.max_current + if new_current < self.min_current: + logger.info("new current below min") + new_current = self.min_current + if new_current < 0.: + logger.info("new current < 0") + new_current = 0. + + self.set_current(new_current) + logger.debug("allow settling time and checking the current value") + # FIXME: remove sleep when set_and_check handled properly + time.sleep(1) + current_get = self.__get_current() + if abs(current_get-new_current) < self.tolerance: + logger.debug("current set is equal to current get") + else: + self.__validate_status() + raise ThrowReply('DriplineValueError',"set value ({}) is not equal to checked value ({})".format(new_current,current_get)) + + logger.info("current set is: {}".format(new_current)) + self._old_current = new_current From 18dccc0927d1155df37bcc8312d844807beea1ed Mon Sep 17 00:00:00 2001 From: Meg Wynne Date: Wed, 16 Jul 2025 14:27:24 -0700 Subject: [PATCH 22/43] added ServiceAttributeEntity, which is the module in the config file for endpoints, copied from phase 2 w same changes --- dripline/extensions/cca_pid_loop.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index eab617b..79bca37 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -197,3 +197,27 @@ def process_new_value(self, value, timestamp): logger.info("current set is: {}".format(new_current)) self._old_current = new_current + +__all__.append('ServiceAttributeEntity') +#changed things like self.provider to self.service, idk if this is the move tho +class ServiceAttributeEntity(Entity): + ''' + Entity allowing communication with spime property. + ''' + + def __init__(self, + attribute_name, + disable_set=False, + **kwargs): + Entity.__init__(self, **kwargs) + self._attribute_name = attribute_name + self._disable_set = disable_set + + @calibrate() + def on_get(self): + return getattr(self.service, self._attribute_name) + + def on_set(self, value): + if self._disable_set: + raise ThrowReply('DriplineMethodNotSupportedError','setting not available for {}'.format(self.name)) + setattr(self.service, self._attribute_name, value) From 67c54f78459fac6c63b53d7ce33dc7cc0c347930 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:47:12 -0700 Subject: [PATCH 23/43] changing this_time def to match Mainz and removing constants --- dripline/extensions/cca_pid_loop.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 79bca37..b392adc 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime -from dripline.core import AlertConsumer, constants, exceptions, fancy_doc, ThrowReply +from dripline.core import AlertConsumer, exceptions, fancy_doc, ThrowReply import logging logger = logging.getLogger(__name__) @@ -118,8 +118,9 @@ def this_consume(self, message, method): if this_value is None: logger.info('value is None') return - - this_time = datetime.datetime.strptime(message['timestamp'], constants.TIME_FORMAT) + + this_time = datetime.datetime.strptime(message.timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') + #this_time = datetime.datetime.strptime(message['timestamp'], constants.TIME_FORMAT) if (this_time - self._last_data['time']).total_seconds() < self.minimum_elapsed_time: # handle self._force_reprocess from @target_value.setter if not self._force_reprocess: From d9ec4f36724c089bb043267d13a372e85ff2f1e8 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:52:22 -0700 Subject: [PATCH 24/43] removed exceptions from import --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index b392adc..f8de082 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime -from dripline.core import AlertConsumer, exceptions, fancy_doc, ThrowReply +from dripline.core import AlertConsumer,fancy_doc, ThrowReply import logging logger = logging.getLogger(__name__) From 9fafe4c3cee3e8c96c552c14d3a781fd9a142db6 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:57:32 -0700 Subject: [PATCH 25/43] remove fancy docs --- dripline/extensions/cca_pid_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index f8de082..adf690d 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,13 +8,13 @@ import time import datetime -from dripline.core import AlertConsumer,fancy_doc, ThrowReply +from dripline.core import AlertConsumer, ThrowReply import logging logger = logging.getLogger(__name__) __all__.append('PidController') -@fancy_doc + class PidController(AlertConsumer): ''' Implementation of a PID control loop with constant offset. That is, the PID equation From 2aedcb2e1cca6ecea68074e9dfd5644a54bc0e19 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:25:09 -0700 Subject: [PATCH 26/43] added the same imports as muxer_service.py --- dripline/extensions/cca_pid_loop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index adf690d..bfd2a30 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,8 @@ import time import datetime -from dripline.core import AlertConsumer, ThrowReply +from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer, +from dripline.implementations import EthernetSCPIService, FormatEntity import logging logger = logging.getLogger(__name__) From c95e9c90c208afddb7aa1a350a133a4d0bd6511a Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:29:36 -0700 Subject: [PATCH 27/43] had a trailing comma rip --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index bfd2a30..4ecca43 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime -from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer, +from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer from dripline.implementations import EthernetSCPIService, FormatEntity import logging From 41ea9a2de8619dd15550c0c399145b7a7a3a6af8 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:38:48 -0700 Subject: [PATCH 28/43] changed def of __validate_status() to be like Mainz --- dripline/extensions/cca_pid_loop.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 4ecca43..2c40328 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -106,12 +106,27 @@ def __get_current(self): def __validate_status(self): # value = self.provider.get(self._status_channel)[self.payload_field] - value = self.service.get(self._status_channel)[self.payload_field] - if value == 'enabled': + # value = self.service.get(self._status_channel)[self.payload_field] + # if value == 'enabled': + # logger.debug("{} returns {}".format(self._status_channel,value)) + # else: + # logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) + # raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + connection={ + "broker": "rabbit-broker", + "auth-file": "/root/authentications.json" + } + + con = Interface(connection) + + value = con.get(self._status_channel).payload["value_raw"].as_string() + + logger.info("{} returns {}".format(self._status_channel,value)) + if value == "ON": logger.debug("{} returns {}".format(self._status_channel,value)) else: logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + raise ThrowReply("{} returns {}".format(self._status_channel,value)) def this_consume(self, message, method): logger.info('consuming message') From 35869cc124a77404dafe1a9c484169871c0dbf9d Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:40:28 -0700 Subject: [PATCH 29/43] import interface from dripline.core --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 2c40328..8045987 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime -from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer +from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer, Interface from dripline.implementations import EthernetSCPIService, FormatEntity import logging From da00834db401f84314f1d604c76cd44fa3acb608 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:51:31 -0700 Subject: [PATCH 30/43] debugging __validate_status --- dripline/extensions/cca_pid_loop.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 8045987..39030b2 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -105,13 +105,6 @@ def __get_current(self): return value def __validate_status(self): - # value = self.provider.get(self._status_channel)[self.payload_field] - # value = self.service.get(self._status_channel)[self.payload_field] - # if value == 'enabled': - # logger.debug("{} returns {}".format(self._status_channel,value)) - # else: - # logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - # raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) connection={ "broker": "rabbit-broker", "auth-file": "/root/authentications.json" @@ -120,13 +113,21 @@ def __validate_status(self): con = Interface(connection) value = con.get(self._status_channel).payload["value_raw"].as_string() - - logger.info("{} returns {}".format(self._status_channel,value)) - if value == "ON": + # value = self.provider.get(self._status_channel)[self.payload_field] + # value = self.service.get(self._status_channel)[self.payload_field] + if value == 'enabled': logger.debug("{} returns {}".format(self._status_channel,value)) else: logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - raise ThrowReply("{} returns {}".format(self._status_channel,value)) + raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + + + # logger.info("{} returns {}".format(self._status_channel,value)) + # if value == "ON": + # logger.debug("{} returns {}".format(self._status_channel,value)) + # else: + # logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) + # raise ThrowReply("{} returns {}".format(self._status_channel,value)) def this_consume(self, message, method): logger.info('consuming message') From a8b5e8d91d97eb71da297999cd0842540a9f9d39 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:57:56 -0700 Subject: [PATCH 31/43] change validate status back to normal --- dripline/extensions/cca_pid_loop.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 39030b2..c41939e 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -105,16 +105,17 @@ def __get_current(self): return value def __validate_status(self): - connection={ - "broker": "rabbit-broker", - "auth-file": "/root/authentications.json" - } + + # connection={ + # "broker": "rabbit-broker", + # "auth-file": "/root/authentications.json" + # } - con = Interface(connection) + # con = Interface(connection) - value = con.get(self._status_channel).payload["value_raw"].as_string() + #value = con.get(self._status_channel).payload["value_raw"].as_string() # value = self.provider.get(self._status_channel)[self.payload_field] - # value = self.service.get(self._status_channel)[self.payload_field] + value = self.service.get(self._status_channel)[self.payload_field] if value == 'enabled': logger.debug("{} returns {}".format(self._status_channel,value)) else: From 00ea3ac67ca65df07c0c7e75aa26b79635e2aaa9 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:59:13 -0700 Subject: [PATCH 32/43] commenting out validate_status as a whole --- dripline/extensions/cca_pid_loop.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index c41939e..a21c82e 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -104,7 +104,7 @@ def __get_current(self): raise ThrowReply('DriplineValueError','value get ({}) is not floatable'.format(value)) return value - def __validate_status(self): + #def __validate_status(self): # connection={ # "broker": "rabbit-broker", @@ -115,12 +115,12 @@ def __validate_status(self): #value = con.get(self._status_channel).payload["value_raw"].as_string() # value = self.provider.get(self._status_channel)[self.payload_field] - value = self.service.get(self._status_channel)[self.payload_field] - if value == 'enabled': - logger.debug("{} returns {}".format(self._status_channel,value)) - else: - logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + #value = self.service.get(self._status_channel)[self.payload_field] + #if value == 'enabled': + #logger.debug("{} returns {}".format(self._status_channel,value)) + #else: + #logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) + #raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) # logger.info("{} returns {}".format(self._status_channel,value)) From 7bfbb0d4d4b6843822ddd8944a290d3cb8700425 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:01:35 -0700 Subject: [PATCH 33/43] commenting out all oisntances of valid status --- dripline/extensions/cca_pid_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index a21c82e..3bd20ef 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -89,7 +89,7 @@ def __init__(self, self.enable_offset_term = enable_offset_term self.minimum_elapsed_time = minimum_elapsed_time - self.__validate_status() + #self.__validate_status() self._old_current = self.__get_current() logger.info('starting current is: {}'.format(self._old_current)) @@ -211,7 +211,7 @@ def process_new_value(self, value, timestamp): if abs(current_get-new_current) < self.tolerance: logger.debug("current set is equal to current get") else: - self.__validate_status() + #self.__validate_status() raise ThrowReply('DriplineValueError',"set value ({}) is not equal to checked value ({})".format(new_current,current_get)) logger.info("current set is: {}".format(new_current)) From 617824587e56c845ef4a417a1a8e93866f05d74a Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:08:47 -0700 Subject: [PATCH 34/43] missing v on value --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 3bd20ef..b045bd7 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -95,7 +95,7 @@ def __init__(self, def __get_current(self): #value = self.provider.get(self._check_channel)[self.payload_field] - alue = self.service.get(self._check_channel)[self.payload_field] + value = self.service.get(self._check_channel)[self.payload_field] logger.info('current get is {}'.format(value)) try: From 675078b41213bb8f027c0c50249eee244fd97892 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:13:41 -0700 Subject: [PATCH 35/43] change .service back to .provider --- dripline/extensions/cca_pid_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index b045bd7..a43f739 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -94,8 +94,8 @@ def __init__(self, logger.info('starting current is: {}'.format(self._old_current)) def __get_current(self): - #value = self.provider.get(self._check_channel)[self.payload_field] - value = self.service.get(self._check_channel)[self.payload_field] + value = self.provider.get(self._check_channel)[self.payload_field] + #value = self.service.get(self._check_channel)[self.payload_field] logger.info('current get is {}'.format(value)) try: From e09b18998f45c7b7edfb6b4b63103fed45b30d5e Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:01:38 -0700 Subject: [PATCH 36/43] copying the Mainz method and seeing what sticks --- dripline/extensions/cca_pid_loop.py | 589 ++++++++++++++++++++++++---- 1 file changed, 514 insertions(+), 75 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index a43f739..518464f 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -7,15 +7,15 @@ import time import datetime - -from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer, Interface -from dripline.implementations import EthernetSCPIService, FormatEntity +from simple_pid import PID +from dripline.core import AlertConsumer +from dripline.core import Interface +from dripline.core import ThrowReply import logging logger = logging.getLogger(__name__) __all__.append('PidController') - class PidController(AlertConsumer): ''' Implementation of a PID control loop with constant offset. That is, the PID equation @@ -42,11 +42,17 @@ def __init__(self, status_channel, payload_field='value_cal', tolerance = 0.01, - target_value=110, + target_value=0, proportional=0.0, integral=0.0, differential=0.0, - maximum_out=1.0, minimum_out=1.0, delta_out_min= 0.001, + maximum_out=15.0, minimum_out=0.0, delta_out_min= 0.001, enable_offset_term=True, minimum_elapsed_time=0, + pid_auto_mode=1, # for simple-pid + pid_PonM = False, + p_term = 0, + i_term = 0, + d_term = 0, + change_to_current = 0, **kwargs ): ''' @@ -64,7 +70,7 @@ def __init__(self, tolerance (float): acceptable difference between the set and get values (default: 0.01) minimum_elapsed_time (float): minimum time interval to perform PID calculation over ''' - kwargs.update({'keys':['sensor_value.'+input_channel]}) + kwargs.update({'alert_keys':['sensor_value.'+input_channel]}) AlertConsumer.__init__(self, **kwargs) self._set_channel = output_channel @@ -73,72 +79,273 @@ def __init__(self, self.payload_field = payload_field self.tolerance = tolerance - self._last_data = {'value':None, 'time':datetime.datetime.utcnow()} + self._last_data = {'delta':None, 'time':datetime.datetime.utcnow(), 'lastInput':0} self.target_value = target_value self.Kproportional = proportional self.Kintegral = integral self.Kdifferential = differential - - self._integral= 0 + + self.p_term = p_term + self.i_term = i_term + self.d_term = d_term + self.change_to_current = change_to_current + # save initial values + self._old_Kp = self.Kproportional + self._old_Ki = self.Kintegral + self._old_Kd = self.Kdifferential + + # self._integral= 0 self.max_current = maximum_out self.min_current = minimum_out self.min_current_change = delta_out_min - self.enable_offset_term = enable_offset_term + # self.enable_offset_term = enable_offset_term self.minimum_elapsed_time = minimum_elapsed_time - #self.__validate_status() + self.pid_auto_mode = pid_auto_mode # for simple-pid + if self.pid_auto_mode == 1: + self._old_pid_auto_mode = 1 + elif pid_auto_mode == 0: + self._old_pid_auto_mode = 0 + else: + raise ValueError(f'pid_auto_mode is neither 0 nor 1: {pid_auto_mode}') + + # create instance of simple-pid + self.first_run = True + self.pid = PID(self.Kproportional, + self.Kintegral, + self.Kdifferential, + setpoint = self.target_value) + self.pid.output_limits = (-0.5,0.5) + self.pid.proportional_on_measurement = False + # self.pid.proportional_on_measurement = pid_PonM + + "If set to None, the PID will compute a new output value every time it is called." + self.pid.sample_time = None + + # self.pid.auto_mode = self.pid_auto_mode + + if self.pid_auto_mode == 1: + # turn on PID and start at the existing current + self.pid.set_auto_mode(True, last_output = 0) + elif self.pid_auto_mode == 0: + # don't turn on PID + self.pid.auto_mode = False + else: + logger.critical('self.auto_mode value {} is neither true nor false'.format(self.pid_auto_mode)) + self.__validate_status() self._old_current = self.__get_current() logger.info('starting current is: {}'.format(self._old_current)) def __get_current(self): - value = self.provider.get(self._check_channel)[self.payload_field] - #value = self.service.get(self._check_channel)[self.payload_field] - logger.info('current get is {}'.format(value)) + connection={ + "broker": "rabbit-broker", + "auth-file": "/root/authentications.json" + } + + con = Interface(connection) + value = con.get(self._check_channel).payload[self.payload_field].as_string() + + logger.info('old current is {}'.format(value)) + try: value = float(value) - except (TypeError, ValueError): - raise ThrowReply('DriplineValueError','value get ({}) is not floatable'.format(value)) + + except Exception as err: ##TODO correct exceptions + raise ThrowReply('value get ({}) is not floatable'.format(value)) return value - #def __validate_status(self): - - # connection={ - # "broker": "rabbit-broker", - # "auth-file": "/root/authentications.json" - # } - - # con = Interface(connection) - - #value = con.get(self._status_channel).payload["value_raw"].as_string() - # value = self.provider.get(self._status_channel)[self.payload_field] - #value = self.service.get(self._status_channel)[self.payload_field] - #if value == 'enabled': - #logger.debug("{} returns {}".format(self._status_channel,value)) - #else: - #logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - #raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) - - - # logger.info("{} returns {}".format(self._status_channel,value)) - # if value == "ON": - # logger.debug("{} returns {}".format(self._status_channel,value)) - # else: - # logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - # raise ThrowReply("{} returns {}".format(self._status_channel,value)) - def this_consume(self, message, method): - logger.info('consuming message') - this_value = message.payload[self.payload_field] + # dripline utilities + + @property + def target_value(self): + return self._target_value + @target_value.setter + def target_value(self, value): + self._target_value = value + self._integral = 0 + self._force_reprocess = True + + @property + def pid_auto_mode(self): + return self._pid_auto_mode + @pid_auto_mode.setter + def pid_auto_mode(self, value): + self._pid_auto_mode = value + #self._force_reprocess = True + + @property + def Kproportional(self): + return self._Kproportional + @Kproportional.setter + def Kproportional(self, value): + self._Kproportional = value + + @property + def Kintegral(self): + return self._Kintegral + @Kintegral.setter + def Kintegral(self, value): + self._Kintegral = value + + @property + def Kdifferential(self): + return self._Kdifferential + @Kdifferential.setter + def Kdifferential(self, value): + self._Kdifferential = value + + @property + def p_term(self): + return self._p_term + @p_term.setter + def p_term(self, value): + self._p_term = value + + @property + def i_term(self): + return self._i_term + @i_term.setter + def i_term(self, value): + self._i_term = value + + @property + def d_term(self): + return self._d_term + @d_term.setter + def d_term(self, value): + self._d_term = value + + @property + def change_to_current(self): + return self._change_to_current + @change_to_current.setter + def change_to_current(self, value): + self._change_to_current = value + + def set_current(self, value): + logger.info('going to set new current to: {}'.format(value)) + reply = self.service.set(self._set_channel, value) + logger.info('set response was: {}'.format(reply)) + + # These functions broadcast their values to the dripline exchange in + # a way that allows those values to go into the database without locking + # up the pid_loop thread by trying to send and recieve the same message + # at the same time. + def send_p_term_to_db(self, value): + logger.info('going to send new p_term to DB: {}'.format(value)) + values = {'value_raw': value, 'value_cal': value} + #reply = self.store_value(alert=values, severity='sensor_value.p_term') + logger.info('set response was: {}'.format(reply)) + + def send_i_term_to_db(self, value): + logger.info('going to send new i_term to DB: {}'.format(value)) + values = {'value_raw': value, 'value_cal': value} + #reply = self.store_value(alert=values, severity='sensor_value.i_term') + logger.info('set response was: {}'.format(reply)) + + def send_d_term_to_db(self, value): + logger.info('going to send new d_term to DB: {}'.format(value)) + values = {'value_raw': value, 'value_cal': value} + #reply = self.store_value(alert=values, severity='sensor_value.d_term') + logger.info('set response was: {}'.format(reply)) + + def send_change_to_current_to_db(self, value): + logger.info('going to send new change_to_current to DB: {}'.format(value)) + values = {'value_raw': value, 'value_cal': value} + #reply = self.store_value(alert=values, severity='sensor_value.change_to_current') + logger.info('set response was: {}'.format(reply)) + + def send_pid_auto_mode_to_db(self, value): + logger.info('going to send new pid_auto_mode to DB: {}'.format(value)) + values = {'value_raw': value, 'value_cal': value} + #reply = self.store_value(alert=values, severity='sensor_value.pid_auto_mode') + logger.info('set response was: {}'.format(reply)) + + def current_from_TC(self, TC): + ''' + Calcuates the estimated steady-state current for a given TC target value + + Based on data from 2023-02-18T01:01:02Z to 2023-02-18T21:57:52Z + and TC = 50 point from 2023-03-03T2200Z + See AGBoeser\Project8\Phase_IV\Lab\Slow_Controls\dataDownloads\2023-02-18T0100.csv + ''' + if TC > 100: + # power law approximate fit above TC = 100 + # good to ~ 10 %, maybe? + A = 500 + power = 0.4 + minus = 750 + adj_current = 0.087098992 + est_current = ((TC+minus)/A)**(1/power) + adj_current + elif TC > 50: + # linear interpolation between 50 at 2.662 A and 100 at 3.68 A + m = 49.11591356 + b = -80.7956778 + est_current = (TC-b)/m + elif TC > 24.65: + # linear interpolation between 24.65 at zero current and 50 at 2.662 A + # this part is unreliable + m = 9.522915101 + b = 24.65 + est_current = (TC-b)/m + else: + logger.info('TC reading should not be <= 24.65; estimating zero current') + est_current = 0 + logger.info(f'current_from_TC({TC}) returning est_current = {est_current} A') + return est_current + + + # Respond to a new message on the exchange from the sensor endpoint + # Because this file is a Gogol, the appearance of the message + # is the trigger for computing a new PID output value + + + def __validate_status(self): + connection={ + "broker": "rabbit-broker", + "auth-file": "/root/authentications.json" + } + + con = Interface(connection) + + value = con.get(self._status_channel).payload["value_raw"].as_string() + + logger.info("{} returns {}".format(self._status_channel,value)) + if value == "ON": + logger.debug("{} returns {}".format(self._status_channel,value)) + else: + logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) + # raise ThrowReply("{} returns {}".format(self._status_channel,value)) + + def on_alert_message(self, message): + logger.info('consuming message {}'.format(message)) + this_value = message.payload[self.payload_field].as_double() if this_value is None: logger.info('value is None') return - + + + + # if this is the first run after starting the service + # and the sensor reading (this_value) is more than 1 [TC unit] different from the target_value + # change the target value to the present sensor reading + # and update the PID setpoint to that value as well + if self.first_run == True: + logger.info('This is the first run of this_consume') + if abs(self.target_value - this_value) > 1: + # change target to present value + self.target_value = this_value + # send setpoint to simple-pid + self.pid.setpoint = self.target_value + + + this_time = datetime.datetime.strptime(message.timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') - #this_time = datetime.datetime.strptime(message['timestamp'], constants.TIME_FORMAT) if (this_time - self._last_data['time']).total_seconds() < self.minimum_elapsed_time: # handle self._force_reprocess from @target_value.setter if not self._force_reprocess: @@ -146,6 +353,27 @@ def this_consume(self, message, method): return logger.info("Forcing process due to changed target_value") self._force_reprocess = False +# update all simple-pid params + #self.pid.setpoint = self.target_value + logger.info('Setting PID tunings to ({self.Kproportional}, {self.Kintegral}, {self.Kdifferential})') + self.pid.tunings = (self.Kproportional, self.Kintegral, self.Kdifferential) + logger.info('simple-pid tunings are {self.pid.tunings}') + if (self.pid_auto_mode == 1) and (self._old_pid_auto_mode == 0): + if abs(self.target_value - this_value) > 1: + # change target to present value + self.target_value = this_value + # send setpoint to simple-pid + self.pid.setpoint = self.target_value + self.pid.set_auto_mode(True, last_output = 0) + if self.pid_auto_mode == 0: + self.pid.auto_mode = False + #self.pid.proportional_on_measurement = self.pid_PonM + logger.info(f'Variables outside simple-pid object: self._old_pid_auto_mode {self._old_pid_auto_mode}; self.pid_auto_mode {self.pid_auto_mode}') + logger.info('Updated simple-pid params: setpoint = {}, tune = {}, pid.auto_mode = {}'.format(self.pid.setpoint, self.pid.tunings, self.pid.auto_mode)) + # mark that the next invocation will not be the first run + self.first_run = False + + # start process of computing and setting a new output self.process_new_value(timestamp=this_time, value=float(this_value)) @@ -159,11 +387,245 @@ def target_value(self, value): self._force_reprocess = True def set_current(self, value): + connection={ + "broker": "rabbit-broker", + "auth-file": "/root/authentications.json" + } + + con = Interface(connection) logger.info('going to set new current to: {}'.format(value)) - #reply = self.provider.set(self._set_channel, value) - reply = self.service.set(self._set_channel, value) + reply = con.set(self._set_channel, value) logger.info('set response was: {}'.format(reply)) + # Use the new sensor reading to adjust the output value + + # Use the new sensor reading to adjust the output value + def process_new_value(self, value, timestamp): + + # send auto mode state to database + self.send_pid_auto_mode_to_db(self.pid_auto_mode) + + # send setpoint to simple-pid + self.pid.setpoint = self.target_value + logger.info(f'simple-pid thinks setpoint is {self.pid.setpoint}; requested was {self.target_value}') + + # compute change in time + dt = (timestamp - self._last_data['time']).total_seconds() + + if dt > 6: + dt = 6.0 + + # check that simple-pid has the right tuning values + self.pid.tunings = (self.Kproportional, self.Kintegral, self.Kdifferential) + # assert self.pid.tunings == (self.Kproportional, self.Kintegral, self.Kdifferential) + if self.pid.tunings[0] != self.Kproportional: + logger.info(f'simple-pid\'s P value ({self.pid.tunings[0]}) does not match requested P ({self.Kproportional})') + if self.pid.tunings[1] != self.Kintegral: + logger.info(f'simple-pid\'s I value ({self.pid.tunings[0]}) does not match requested I ({self.Kintegral})') + if self.pid.tunings[2] != self.Kdifferential: + logger.info(f'simple-pid\'s D value ({self.pid.tunings[0]}) does not match requested D ({self.Kdifferential})') + + # Compute new current using simple-pid + # This will go through checks before being set later on + logger.info('Computing new output value with simple-pid') + original_change_to_current = self.pid(float(value)) + #logger.info(f'simple-pid initially requested a new total current of {new_current}') + self.p_term, self.i_term, self.d_term = self.pid.components + logger.info('The computed constituents are: p_term = {}, i_term = {}, d_term = {}'.format(self.p_term, self.i_term, self.d_term)) + logger.info(f'Relative to the previous current, simple-pid has requested a change of {original_change_to_current}') + + # compute the difference between the sensor reading and the target + delta = (value - self.target_value) # this used to be an absolute value + abs_delta = abs(delta) # TODO: is this redundant? + + # gain-schedule the d_term when close to the setpoint to minimize noise + try: + last_abs_delta = abs(self._last_data['delta']) + except: + last_abs_delta = 0 + # adjusted so that d_term is cut to 1% of the normal value + # within a range equal to the noise band we observe when stable at a setpoint + self.d_term = self.d_term * (1 - (1-0.01) * (1 / (1 + ((delta/0.05)**6)))) + logger.info(f'd_term has been cut to {self.d_term}') + + # send initial values to database for slowplot + self.send_p_term_to_db(self.p_term) + self.send_i_term_to_db(self.i_term) + self.send_d_term_to_db(self.d_term) + logger.info('Logged requested p, i, and d_terms') + + # just to be sure + self.change_to_current = self.p_term + self.i_term + self.d_term + + # reduce the commanded change to B*change_to_current + # in a narrow region with a half width of 2*B_width + # around the setpoint + self.B = 0.02 + self.B_width = 10 + self.B_exp = 2 + if dt > 0: + if (((abs_delta - last_abs_delta )**2 )**0.5 / dt ) > 1: + # if the slope of the error (either towards or away from the setpoint) + # is larger than 1 [TC unit]/[s], apply braking + braking = 1 - (1-self.B) * (1 / (1 + ((delta/self.B_width)**self.B_exp))) + self.change_to_current = braking * self.change_to_current + logger.info(f'Braking reduced output change to {self.change_to_current}') + + # check if change is bigger than minimum change + # if not, don't set a new current + # this is the first place the function can return, + # so need to save values + if abs(self.change_to_current) < self.min_current_change: + logger.info("current change less than min delta") + self.change_to_current = 0 + logger.info("old[change] are: {}[{}]".format(self._old_current, self.change_to_current)) + self.send_change_to_current_to_db(0) + self._old_pid_auto_mode = self.pid_auto_mode + self._old_current = self._old_current + self.change_to_current + self._old_Kp = self.Kproportional + self._old_Ki = self.Kintegral + self._old_Kd = self.Kdifferential + self._last_data['time'] = timestamp + self._last_data['delta'] = delta + logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) + return + + + # To minimize over/undershoot, use a lookup function to get the + # the estimated steady-state current (ESSC) at the target TC value. + # When the actual current crosses the ESSC, hold at the ESSC + # until the error (TC vs SP) is less than some percentage + # TODO: apply some multiple of the essc until the measured value gets within some outer tolerance, then jump to the essc + # That should probably depend on the size of the change in SP + logger.info(f'Computing est_current with TC = {float(self.target_value)}') + est_current = self.current_from_TC(float(self.target_value)) + next_new_current = (self._old_current or 0) + self.change_to_current + + # negative if approaching SP from above + # positive if approaching SP from below + # dError = delta - self._last_data['delta'] + # negative if approaching SP; positive if moving away + # abs_dError = abs(dError) + # if abs_dError < 0: + # logger.info(f'The absolute value of the error is shrinking; setting `approaching_target = True`') + # approaching_target = True + # else: + # logger.info(f'The absolute value of the error is growing; setting `approaching_target = False`') + # approaching_target = False + + if self.target_value > 0: + rel_error = abs(delta)/self.target_value + elif self.target_value == 0: + rel_error = abs(delta) + else: + raise ValueError + + if self.target_value > 0: + if (rel_error >= 0.05) and (rel_error <= 0.2): + logger.info(f'The relative error is between 5% and 20%; setting `error_in_band = True`') + error_in_band = True + else: + logger.info(f'The relative error is outside 5% to 20%; setting `error_in_band = False`') + error_in_band = False + elif self.target_value == 0: + error_in_band = False + else: + raise ValueError + + # if (approaching_target == True) and (self.target_value >= 50) and (error_in_band == False): + if (self.target_value >= 50) and (error_in_band == True): + # if measured value is moving towards SP, and + # the SP is within the range of the estimation function, and + # the error is more than the given percentage + # then replace the PID current request with the ESSC + + # when old_current is 10 and est_current is 11, change is 1 + # when old_current is 10 and est_current is 9, change is -1 + self.change_to_current = (est_current - self._old_current) + logger.info(f'TC {value}; Error {delta}; SP {self.target_value}; rel. error {rel_error}; Replacing original change_to_current with {self.change_to_current} A from lookup of ESSC ({est_current} A)') + + else: + logger.info(f'Conditions for lookup of ESSC not met; continuing with PID change to current of {self.change_to_current}') + + + # Limit output ramp rate to X A/min + ramp_rate_limit_A_per_min = 2/60 #2/60 + if (abs(self.change_to_current / dt)) > ramp_rate_limit_A_per_min: + sign = 1 + if self.change_to_current < 0: + sign = -1 + self.change_to_current = ramp_rate_limit_A_per_min * dt * sign + logger.info(f'Ramp rate limit reduced output change to {self.change_to_current}') + + # Convert adjusted change_to_current back into new total current + new_current = (self._old_current or 0) + self.change_to_current + + # test if current within allowed parameters + logger.info('computed new current to be: {}'.format(new_current)) + if new_current > self.max_current: + logger.info("new current above max") + # clamp new_current to max + new_current = self.max_current + if new_current < self.min_current: + logger.info("new current below min") + # clamp new_current to min + new_current = self.min_current + if new_current < 0.: + logger.info("new current < 0") + # clamp new_current to 0 + new_current = 0. + + # re-compute the resulting change_to_current + self.change_to_current = new_current - self._old_current + # log the final current change + self.send_change_to_current_to_db(self.change_to_current) + # send the final, total new_current to the power supply + if self.pid_auto_mode == 1: + self.set_current(new_current) + else: + logger.info(f'Not setting current because pid_auto_mode != True') + # save existing current so the storage with `self._old_current = new_current` + # down below has the corect value for the next iteration + new_current = self._old_current + + + # confirm the power supply is actually putting out the requested value + logger.debug("allow settling time and checking the current value") + current_get = self.__get_current() + if abs(current_get-new_current) < self.tolerance: + logger.info("current set is equal to current get") + else: + # actual current is not within the acceptable range around the requested value + self.__validate_status() + # Record values and throw an exception + # this is the second place the function can return, + # so need to save values + self._old_pid_auto_mode = self.pid_auto_mode + self._old_current = new_current + self._old_Kp = self.Kproportional + self._old_Ki = self.Kintegral + self._old_Kd = self.Kdifferential + self._last_data['time'] = timestamp + self._last_data['delta'] = delta + logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) +# raise exceptions.DriplineValueError("set value ({}) is not equal to checked value ({})".format(new_current,current_get)) + + # save the old values for the next loop + # this is the final place the function can return, + # so need to save values + logger.info("current set is: {}".format(new_current)) + self._old_pid_auto_mode = self.pid_auto_mode + self._old_current = new_current + self._old_Kp = self.Kproportional + self._old_Ki = self.Kintegral + self._old_Kd = self.Kdifferential + self._last_data['time'] = timestamp + self._last_data['delta'] = delta + logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) + + +''' +#OldVersion def process_new_value(self, value, timestamp): delta = self.target_value - value @@ -211,32 +673,9 @@ def process_new_value(self, value, timestamp): if abs(current_get-new_current) < self.tolerance: logger.debug("current set is equal to current get") else: - #self.__validate_status() - raise ThrowReply('DriplineValueError',"set value ({}) is not equal to checked value ({})".format(new_current,current_get)) + self.__validate_status() + raise ThrowReply("set value ({}) is not equal to checked value ({})".format(new_current,current_get)) logger.info("current set is: {}".format(new_current)) self._old_current = new_current - -__all__.append('ServiceAttributeEntity') -#changed things like self.provider to self.service, idk if this is the move tho -class ServiceAttributeEntity(Entity): - ''' - Entity allowing communication with spime property. - ''' - - def __init__(self, - attribute_name, - disable_set=False, - **kwargs): - Entity.__init__(self, **kwargs) - self._attribute_name = attribute_name - self._disable_set = disable_set - - @calibrate() - def on_get(self): - return getattr(self.service, self._attribute_name) - - def on_set(self, value): - if self._disable_set: - raise ThrowReply('DriplineMethodNotSupportedError','setting not available for {}'.format(self.name)) - setattr(self.service, self._attribute_name, value) +''' From fa887f2db1dbf30bf109243aa257c2bb2514df58 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:03:27 -0700 Subject: [PATCH 37/43] import Simple_PID --- dripline/extensions/cca_pid_loop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 518464f..d72b2da 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -7,6 +7,7 @@ import time import datetime +import simple_PID from simple_pid import PID from dripline.core import AlertConsumer from dripline.core import Interface From 8308572d0169cc600d2235c872a421b66c79e0c6 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:11:00 -0700 Subject: [PATCH 38/43] I found this in a folder called simple_pid on my computer we need simple_pid for the mainz pid loop so maybe this is a Hail Mary --- dripline/extensions/PID.py | 147 +++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 dripline/extensions/PID.py diff --git a/dripline/extensions/PID.py b/dripline/extensions/PID.py new file mode 100644 index 0000000..a94d931 --- /dev/null +++ b/dripline/extensions/PID.py @@ -0,0 +1,147 @@ +mport time +import warnings + + +def _clamp(value, limits): + lower, upper = limits + if value is None: + return None + elif upper is not None and value > upper: + return upper + elif lower is not None and value < lower: + return lower + return value + + +try: + # get monotonic time to ensure that time deltas are always positive + _current_time = time.monotonic +except AttributeError: + # time.monotonic() not available (using python < 3.3), fallback to time.time() + _current_time = time.time + warnings.warn('time.monotonic() not available, using time.time() as fallback. Consider using Python 3.3 or newer to get monotonic time measurements.') + + +class PID(object): + """ + A simple PID controller. No fuss. + """ + def __init__(self, Kp=1.0, Ki=0.0, Kd=0.0, setpoint=0, sample_time=0.01, output_limits=(None, None), auto_mode=True, proportional_on_measurement=False): + """ + :param Kp: The value for the proportional gain Kp + :param Ki: The value for the integral gain Ki + :param Kd: The value for the derivative gain Kd + :param setpoint: The initial setpoint that the PID will try to achieve + :param sample_time: The time in seconds which the controller should wait before generating a new output value. The PID works best when it is constantly called (eg. during a loop), but with a sample time set so that the time difference between each update is (close to) constant. If set to None, the PID will compute a new output value every time it is called. + :param output_limits: The initial output limits to use, given as an iterable with 2 elements, for example: (lower, upper). The output will never go below the lower limit or above the upper limit. Either of the limits can also be set to None to have no limit in that direction. Setting output limits also avoids integral windup, since the integral term will never be allowed to grow outside of the limits. + :param auto_mode: Whether the controller should be enabled (in auto mode) or not (in manual mode) + :param proportional_on_measurement: Whether the proportional term should be calculated on the input directly rather than on the error (which is the traditional way). Using proportional-on-measurement avoids overshoot for some types of systems. + """ + self.Kp, self.Ki, self.Kd = Kp, Ki, Kd + self.setpoint = setpoint + self.sample_time = sample_time + + self._min_output, self._max_output = output_limits + self._auto_mode = auto_mode + self.proportional_on_measurement = proportional_on_measurement + + self._error_sum = 0 + + self._last_time = _current_time() + self._last_output = None + self._proportional = 0 + self._last_input = None + + def __call__(self, input_): + """ + Call the PID controller with *input_* and calculate and return a control output if sample_time seconds has passed + since the last update. If no new output is calculated, return the previous output instead (or None if no value + has been calculated yet). + """ + if not self.auto_mode: + return self._last_output + + now = _current_time() + dt = now - self._last_time + + if self.sample_time is not None and dt < self.sample_time and self._last_output is not None: + # only update every sample_time seconds + return self._last_output + + # compute error terms + error = self.setpoint - input_ + self._error_sum += self.Ki * error * dt + d_input = input_ - (self._last_input if self._last_input is not None else input_) + + # compute the proportional term + if not self.proportional_on_measurement: + # regular proportional-on-error, simply set the proportional term + self._proportional = self.Kp * error + else: + # add the proportional error on measurement to error_sum + self._error_sum -= self.Kp * d_input + self._proportional = 0 + + # clamp error sum to avoid integral windup (and proportional, if proportional-on-measurement is used) + self._error_sum = _clamp(self._error_sum, self.output_limits) + + # compute final output + output = self._proportional + self._error_sum - self.Kd * d_input + output = _clamp(output, self.output_limits) + + # keep track of state + self._last_output = output + self._last_input = input_ + self._last_time = now + + return output + + @property + def tunings(self): + """The tunings used by the controller as a tuple: (Kp, Ki, Kd)""" + return self.Kp, self.Ki, self.Kd + + @tunings.setter + def tunings(self, tunings): + """Setter for the PID tunings""" + self.Kp, self.Ki, self.Kd = tunings + + @property + def auto_mode(self): + """Whether the controller is currently enabled (in auto mode) or not""" + return self._auto_mode + + @auto_mode.setter + def auto_mode(self, enabled): + """Enable or disable the PID controller""" + if enabled and not self._auto_mode: + # switching from manual mode to auto, reset + self._last_output = None + self._last_input = None + self._error_sum = 0 + self._error_sum = _clamp(self._error_sum, self.output_limits) + + self._auto_mode = enabled + + @property + def output_limits(self): + """The current output limits as a 2-tuple: (lower, upper). See also the *output_limts* parameter in :meth:`PID.__init__`.""" + return (self._min_output, self._max_output) + + @output_limits.setter + def output_limits(self, limits): + """Setter for the output limits""" + if limits is None: + self._min_output, self._max_output = None, None + return + + min_output, max_output = limits + + if None not in limits and max_output < min_output: + raise ValueError('lower limit must be less than upper limit') + + self._min_output = min_output + self._max_output = max_output + + self._error_sum = _clamp(self._error_sum, self.output_limits) + self._last_output = _clamp(self._last_output, self.output_limits) From df7880fa109cf9e58fca363f578ccb41a7fa6da3 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:11:39 -0700 Subject: [PATCH 39/43] getting PID.py to come from this repo hopefully --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index d72b2da..2ee2133 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime import simple_PID -from simple_pid import PID +import PID from dripline.core import AlertConsumer from dripline.core import Interface from dripline.core import ThrowReply From 52cb318e407e0f87513cb059ca75dcc04101fe3d Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:12:50 -0700 Subject: [PATCH 40/43] missing an I in import --- dripline/extensions/PID.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/PID.py b/dripline/extensions/PID.py index a94d931..bf032a9 100644 --- a/dripline/extensions/PID.py +++ b/dripline/extensions/PID.py @@ -1,4 +1,4 @@ -mport time +import time import warnings From 3dcf0dd9634df875fb4fe0ef15c5f1150f4779d4 Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:15:14 -0700 Subject: [PATCH 41/43] commenting out simple_pid --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index 2ee2133..d87354d 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -7,7 +7,7 @@ import time import datetime -import simple_PID +#import simple_PID import PID from dripline.core import AlertConsumer from dripline.core import Interface From 37fdbb11eae4e1bad6b40404f97b4c81bdd60a8d Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:17:11 -0700 Subject: [PATCH 42/43] still trying to import this PID --- dripline/extensions/cca_pid_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index d87354d..e80795f 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -8,7 +8,7 @@ import time import datetime #import simple_PID -import PID +from dripline.extensions import PID from dripline.core import AlertConsumer from dripline.core import Interface from dripline.core import ThrowReply From 072b599cc507cded87d86e764943bc5a256de2ec Mon Sep 17 00:00:00 2001 From: miniwynne <137235445+miniwynne@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:58:41 -0700 Subject: [PATCH 43/43] back to doing phase 2 way --- dripline/extensions/cca_pid_loop.py | 567 +++------------------------- 1 file changed, 57 insertions(+), 510 deletions(-) diff --git a/dripline/extensions/cca_pid_loop.py b/dripline/extensions/cca_pid_loop.py index e80795f..bad1275 100644 --- a/dripline/extensions/cca_pid_loop.py +++ b/dripline/extensions/cca_pid_loop.py @@ -7,16 +7,15 @@ import time import datetime -#import simple_PID -from dripline.extensions import PID -from dripline.core import AlertConsumer -from dripline.core import Interface -from dripline.core import ThrowReply + +from dripline.core import ThrowReply, Entity, calibrate, AlertConsumer, Interface +from dripline.implementations import EthernetSCPIService, FormatEntity import logging logger = logging.getLogger(__name__) __all__.append('PidController') + class PidController(AlertConsumer): ''' Implementation of a PID control loop with constant offset. That is, the PID equation @@ -43,17 +42,11 @@ def __init__(self, status_channel, payload_field='value_cal', tolerance = 0.01, - target_value=0, + target_value=110, proportional=0.0, integral=0.0, differential=0.0, - maximum_out=15.0, minimum_out=0.0, delta_out_min= 0.001, + maximum_out=1.0, minimum_out=1.0, delta_out_min= 0.001, enable_offset_term=True, minimum_elapsed_time=0, - pid_auto_mode=1, # for simple-pid - pid_PonM = False, - p_term = 0, - i_term = 0, - d_term = 0, - change_to_current = 0, **kwargs ): ''' @@ -71,7 +64,7 @@ def __init__(self, tolerance (float): acceptable difference between the set and get values (default: 0.01) minimum_elapsed_time (float): minimum time interval to perform PID calculation over ''' - kwargs.update({'alert_keys':['sensor_value.'+input_channel]}) + kwargs.update({'keys':['sensor_value.'+input_channel]}) AlertConsumer.__init__(self, **kwargs) self._set_channel = output_channel @@ -80,273 +73,59 @@ def __init__(self, self.payload_field = payload_field self.tolerance = tolerance - self._last_data = {'delta':None, 'time':datetime.datetime.utcnow(), 'lastInput':0} + self._last_data = {'value':None, 'time':datetime.datetime.utcnow()} self.target_value = target_value self.Kproportional = proportional self.Kintegral = integral self.Kdifferential = differential - - self.p_term = p_term - self.i_term = i_term - self.d_term = d_term - self.change_to_current = change_to_current - # save initial values - self._old_Kp = self.Kproportional - self._old_Ki = self.Kintegral - self._old_Kd = self.Kdifferential - - # self._integral= 0 + + self._integral= 0 self.max_current = maximum_out self.min_current = minimum_out self.min_current_change = delta_out_min - # self.enable_offset_term = enable_offset_term + self.enable_offset_term = enable_offset_term self.minimum_elapsed_time = minimum_elapsed_time - self.pid_auto_mode = pid_auto_mode # for simple-pid - if self.pid_auto_mode == 1: - self._old_pid_auto_mode = 1 - elif pid_auto_mode == 0: - self._old_pid_auto_mode = 0 - else: - raise ValueError(f'pid_auto_mode is neither 0 nor 1: {pid_auto_mode}') - - # create instance of simple-pid - self.first_run = True - self.pid = PID(self.Kproportional, - self.Kintegral, - self.Kdifferential, - setpoint = self.target_value) - self.pid.output_limits = (-0.5,0.5) - self.pid.proportional_on_measurement = False - # self.pid.proportional_on_measurement = pid_PonM - - "If set to None, the PID will compute a new output value every time it is called." - self.pid.sample_time = None - - # self.pid.auto_mode = self.pid_auto_mode - - if self.pid_auto_mode == 1: - # turn on PID and start at the existing current - self.pid.set_auto_mode(True, last_output = 0) - elif self.pid_auto_mode == 0: - # don't turn on PID - self.pid.auto_mode = False - else: - logger.critical('self.auto_mode value {} is neither true nor false'.format(self.pid_auto_mode)) self.__validate_status() self._old_current = self.__get_current() logger.info('starting current is: {}'.format(self._old_current)) def __get_current(self): - connection={ - "broker": "rabbit-broker", - "auth-file": "/root/authentications.json" - } - - con = Interface(connection) - value = con.get(self._check_channel).payload[self.payload_field].as_string() - - logger.info('old current is {}'.format(value)) - + value = self.provider.get(self._check_channel)[self.payload_field] + #value = self.service.get(self._check_channel)[self.payload_field] + logger.info('current get is {}'.format(value)) try: value = float(value) - - except Exception as err: ##TODO correct exceptions - raise ThrowReply('value get ({}) is not floatable'.format(value)) + except (TypeError, ValueError): + raise ThrowReply('DriplineValueError','value get ({}) is not floatable'.format(value)) return value - - # dripline utilities - - @property - def target_value(self): - return self._target_value - @target_value.setter - def target_value(self, value): - self._target_value = value - self._integral = 0 - self._force_reprocess = True - - @property - def pid_auto_mode(self): - return self._pid_auto_mode - @pid_auto_mode.setter - def pid_auto_mode(self, value): - self._pid_auto_mode = value - #self._force_reprocess = True - - @property - def Kproportional(self): - return self._Kproportional - @Kproportional.setter - def Kproportional(self, value): - self._Kproportional = value - - @property - def Kintegral(self): - return self._Kintegral - @Kintegral.setter - def Kintegral(self, value): - self._Kintegral = value - - @property - def Kdifferential(self): - return self._Kdifferential - @Kdifferential.setter - def Kdifferential(self, value): - self._Kdifferential = value - - @property - def p_term(self): - return self._p_term - @p_term.setter - def p_term(self, value): - self._p_term = value - - @property - def i_term(self): - return self._i_term - @i_term.setter - def i_term(self, value): - self._i_term = value - - @property - def d_term(self): - return self._d_term - @d_term.setter - def d_term(self, value): - self._d_term = value - - @property - def change_to_current(self): - return self._change_to_current - @change_to_current.setter - def change_to_current(self, value): - self._change_to_current = value - - def set_current(self, value): - logger.info('going to set new current to: {}'.format(value)) - reply = self.service.set(self._set_channel, value) - logger.info('set response was: {}'.format(reply)) - - # These functions broadcast their values to the dripline exchange in - # a way that allows those values to go into the database without locking - # up the pid_loop thread by trying to send and recieve the same message - # at the same time. - def send_p_term_to_db(self, value): - logger.info('going to send new p_term to DB: {}'.format(value)) - values = {'value_raw': value, 'value_cal': value} - #reply = self.store_value(alert=values, severity='sensor_value.p_term') - logger.info('set response was: {}'.format(reply)) - - def send_i_term_to_db(self, value): - logger.info('going to send new i_term to DB: {}'.format(value)) - values = {'value_raw': value, 'value_cal': value} - #reply = self.store_value(alert=values, severity='sensor_value.i_term') - logger.info('set response was: {}'.format(reply)) - - def send_d_term_to_db(self, value): - logger.info('going to send new d_term to DB: {}'.format(value)) - values = {'value_raw': value, 'value_cal': value} - #reply = self.store_value(alert=values, severity='sensor_value.d_term') - logger.info('set response was: {}'.format(reply)) - - def send_change_to_current_to_db(self, value): - logger.info('going to send new change_to_current to DB: {}'.format(value)) - values = {'value_raw': value, 'value_cal': value} - #reply = self.store_value(alert=values, severity='sensor_value.change_to_current') - logger.info('set response was: {}'.format(reply)) - - def send_pid_auto_mode_to_db(self, value): - logger.info('going to send new pid_auto_mode to DB: {}'.format(value)) - values = {'value_raw': value, 'value_cal': value} - #reply = self.store_value(alert=values, severity='sensor_value.pid_auto_mode') - logger.info('set response was: {}'.format(reply)) - - def current_from_TC(self, TC): - ''' - Calcuates the estimated steady-state current for a given TC target value - - Based on data from 2023-02-18T01:01:02Z to 2023-02-18T21:57:52Z - and TC = 50 point from 2023-03-03T2200Z - See AGBoeser\Project8\Phase_IV\Lab\Slow_Controls\dataDownloads\2023-02-18T0100.csv - ''' - if TC > 100: - # power law approximate fit above TC = 100 - # good to ~ 10 %, maybe? - A = 500 - power = 0.4 - minus = 750 - adj_current = 0.087098992 - est_current = ((TC+minus)/A)**(1/power) + adj_current - elif TC > 50: - # linear interpolation between 50 at 2.662 A and 100 at 3.68 A - m = 49.11591356 - b = -80.7956778 - est_current = (TC-b)/m - elif TC > 24.65: - # linear interpolation between 24.65 at zero current and 50 at 2.662 A - # this part is unreliable - m = 9.522915101 - b = 24.65 - est_current = (TC-b)/m - else: - logger.info('TC reading should not be <= 24.65; estimating zero current') - est_current = 0 - logger.info(f'current_from_TC({TC}) returning est_current = {est_current} A') - return est_current - - - # Respond to a new message on the exchange from the sensor endpoint - # Because this file is a Gogol, the appearance of the message - # is the trigger for computing a new PID output value - - def __validate_status(self): - connection={ - "broker": "rabbit-broker", - "auth-file": "/root/authentications.json" - } - - con = Interface(connection) - - value = con.get(self._status_channel).payload["value_raw"].as_string() - - logger.info("{} returns {}".format(self._status_channel,value)) - if value == "ON": + + value = self.provider.get(self._status_channel)[self.payload_field] + + if value == 'enabled': logger.debug("{} returns {}".format(self._status_channel,value)) else: logger.critical("Invalid status of {} for PID control by {}".format(self._status_channel,self.name)) - # raise ThrowReply("{} returns {}".format(self._status_channel,value)) + raise ThrowReply('DriplineHardwareError',"{} returns {}".format(self._status_channel,value)) + - def on_alert_message(self, message): - logger.info('consuming message {}'.format(message)) - this_value = message.payload[self.payload_field].as_double() + + + def this_consume(self, message, method): + logger.info('consuming message') + this_value = message.payload[self.payload_field] if this_value is None: logger.info('value is None') return - - - - # if this is the first run after starting the service - # and the sensor reading (this_value) is more than 1 [TC unit] different from the target_value - # change the target value to the present sensor reading - # and update the PID setpoint to that value as well - if self.first_run == True: - logger.info('This is the first run of this_consume') - if abs(self.target_value - this_value) > 1: - # change target to present value - self.target_value = this_value - # send setpoint to simple-pid - self.pid.setpoint = self.target_value - - - + this_time = datetime.datetime.strptime(message.timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') + #this_time = datetime.datetime.strptime(message['timestamp'], constants.TIME_FORMAT) if (this_time - self._last_data['time']).total_seconds() < self.minimum_elapsed_time: # handle self._force_reprocess from @target_value.setter if not self._force_reprocess: @@ -354,27 +133,6 @@ def on_alert_message(self, message): return logger.info("Forcing process due to changed target_value") self._force_reprocess = False -# update all simple-pid params - #self.pid.setpoint = self.target_value - logger.info('Setting PID tunings to ({self.Kproportional}, {self.Kintegral}, {self.Kdifferential})') - self.pid.tunings = (self.Kproportional, self.Kintegral, self.Kdifferential) - logger.info('simple-pid tunings are {self.pid.tunings}') - if (self.pid_auto_mode == 1) and (self._old_pid_auto_mode == 0): - if abs(self.target_value - this_value) > 1: - # change target to present value - self.target_value = this_value - # send setpoint to simple-pid - self.pid.setpoint = self.target_value - self.pid.set_auto_mode(True, last_output = 0) - if self.pid_auto_mode == 0: - self.pid.auto_mode = False - #self.pid.proportional_on_measurement = self.pid_PonM - logger.info(f'Variables outside simple-pid object: self._old_pid_auto_mode {self._old_pid_auto_mode}; self.pid_auto_mode {self.pid_auto_mode}') - logger.info('Updated simple-pid params: setpoint = {}, tune = {}, pid.auto_mode = {}'.format(self.pid.setpoint, self.pid.tunings, self.pid.auto_mode)) - # mark that the next invocation will not be the first run - self.first_run = False - - # start process of computing and setting a new output self.process_new_value(timestamp=this_time, value=float(this_value)) @@ -388,245 +146,11 @@ def target_value(self, value): self._force_reprocess = True def set_current(self, value): - connection={ - "broker": "rabbit-broker", - "auth-file": "/root/authentications.json" - } - - con = Interface(connection) logger.info('going to set new current to: {}'.format(value)) - reply = con.set(self._set_channel, value) + #reply = self.provider.set(self._set_channel, value) + reply = self.service.set(self._set_channel, value) logger.info('set response was: {}'.format(reply)) - # Use the new sensor reading to adjust the output value - - # Use the new sensor reading to adjust the output value - def process_new_value(self, value, timestamp): - - # send auto mode state to database - self.send_pid_auto_mode_to_db(self.pid_auto_mode) - - # send setpoint to simple-pid - self.pid.setpoint = self.target_value - logger.info(f'simple-pid thinks setpoint is {self.pid.setpoint}; requested was {self.target_value}') - - # compute change in time - dt = (timestamp - self._last_data['time']).total_seconds() - - if dt > 6: - dt = 6.0 - - # check that simple-pid has the right tuning values - self.pid.tunings = (self.Kproportional, self.Kintegral, self.Kdifferential) - # assert self.pid.tunings == (self.Kproportional, self.Kintegral, self.Kdifferential) - if self.pid.tunings[0] != self.Kproportional: - logger.info(f'simple-pid\'s P value ({self.pid.tunings[0]}) does not match requested P ({self.Kproportional})') - if self.pid.tunings[1] != self.Kintegral: - logger.info(f'simple-pid\'s I value ({self.pid.tunings[0]}) does not match requested I ({self.Kintegral})') - if self.pid.tunings[2] != self.Kdifferential: - logger.info(f'simple-pid\'s D value ({self.pid.tunings[0]}) does not match requested D ({self.Kdifferential})') - - # Compute new current using simple-pid - # This will go through checks before being set later on - logger.info('Computing new output value with simple-pid') - original_change_to_current = self.pid(float(value)) - #logger.info(f'simple-pid initially requested a new total current of {new_current}') - self.p_term, self.i_term, self.d_term = self.pid.components - logger.info('The computed constituents are: p_term = {}, i_term = {}, d_term = {}'.format(self.p_term, self.i_term, self.d_term)) - logger.info(f'Relative to the previous current, simple-pid has requested a change of {original_change_to_current}') - - # compute the difference between the sensor reading and the target - delta = (value - self.target_value) # this used to be an absolute value - abs_delta = abs(delta) # TODO: is this redundant? - - # gain-schedule the d_term when close to the setpoint to minimize noise - try: - last_abs_delta = abs(self._last_data['delta']) - except: - last_abs_delta = 0 - # adjusted so that d_term is cut to 1% of the normal value - # within a range equal to the noise band we observe when stable at a setpoint - self.d_term = self.d_term * (1 - (1-0.01) * (1 / (1 + ((delta/0.05)**6)))) - logger.info(f'd_term has been cut to {self.d_term}') - - # send initial values to database for slowplot - self.send_p_term_to_db(self.p_term) - self.send_i_term_to_db(self.i_term) - self.send_d_term_to_db(self.d_term) - logger.info('Logged requested p, i, and d_terms') - - # just to be sure - self.change_to_current = self.p_term + self.i_term + self.d_term - - # reduce the commanded change to B*change_to_current - # in a narrow region with a half width of 2*B_width - # around the setpoint - self.B = 0.02 - self.B_width = 10 - self.B_exp = 2 - if dt > 0: - if (((abs_delta - last_abs_delta )**2 )**0.5 / dt ) > 1: - # if the slope of the error (either towards or away from the setpoint) - # is larger than 1 [TC unit]/[s], apply braking - braking = 1 - (1-self.B) * (1 / (1 + ((delta/self.B_width)**self.B_exp))) - self.change_to_current = braking * self.change_to_current - logger.info(f'Braking reduced output change to {self.change_to_current}') - - # check if change is bigger than minimum change - # if not, don't set a new current - # this is the first place the function can return, - # so need to save values - if abs(self.change_to_current) < self.min_current_change: - logger.info("current change less than min delta") - self.change_to_current = 0 - logger.info("old[change] are: {}[{}]".format(self._old_current, self.change_to_current)) - self.send_change_to_current_to_db(0) - self._old_pid_auto_mode = self.pid_auto_mode - self._old_current = self._old_current + self.change_to_current - self._old_Kp = self.Kproportional - self._old_Ki = self.Kintegral - self._old_Kd = self.Kdifferential - self._last_data['time'] = timestamp - self._last_data['delta'] = delta - logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) - return - - - # To minimize over/undershoot, use a lookup function to get the - # the estimated steady-state current (ESSC) at the target TC value. - # When the actual current crosses the ESSC, hold at the ESSC - # until the error (TC vs SP) is less than some percentage - # TODO: apply some multiple of the essc until the measured value gets within some outer tolerance, then jump to the essc - # That should probably depend on the size of the change in SP - logger.info(f'Computing est_current with TC = {float(self.target_value)}') - est_current = self.current_from_TC(float(self.target_value)) - next_new_current = (self._old_current or 0) + self.change_to_current - - # negative if approaching SP from above - # positive if approaching SP from below - # dError = delta - self._last_data['delta'] - # negative if approaching SP; positive if moving away - # abs_dError = abs(dError) - # if abs_dError < 0: - # logger.info(f'The absolute value of the error is shrinking; setting `approaching_target = True`') - # approaching_target = True - # else: - # logger.info(f'The absolute value of the error is growing; setting `approaching_target = False`') - # approaching_target = False - - if self.target_value > 0: - rel_error = abs(delta)/self.target_value - elif self.target_value == 0: - rel_error = abs(delta) - else: - raise ValueError - - if self.target_value > 0: - if (rel_error >= 0.05) and (rel_error <= 0.2): - logger.info(f'The relative error is between 5% and 20%; setting `error_in_band = True`') - error_in_band = True - else: - logger.info(f'The relative error is outside 5% to 20%; setting `error_in_band = False`') - error_in_band = False - elif self.target_value == 0: - error_in_band = False - else: - raise ValueError - - # if (approaching_target == True) and (self.target_value >= 50) and (error_in_band == False): - if (self.target_value >= 50) and (error_in_band == True): - # if measured value is moving towards SP, and - # the SP is within the range of the estimation function, and - # the error is more than the given percentage - # then replace the PID current request with the ESSC - - # when old_current is 10 and est_current is 11, change is 1 - # when old_current is 10 and est_current is 9, change is -1 - self.change_to_current = (est_current - self._old_current) - logger.info(f'TC {value}; Error {delta}; SP {self.target_value}; rel. error {rel_error}; Replacing original change_to_current with {self.change_to_current} A from lookup of ESSC ({est_current} A)') - - else: - logger.info(f'Conditions for lookup of ESSC not met; continuing with PID change to current of {self.change_to_current}') - - - # Limit output ramp rate to X A/min - ramp_rate_limit_A_per_min = 2/60 #2/60 - if (abs(self.change_to_current / dt)) > ramp_rate_limit_A_per_min: - sign = 1 - if self.change_to_current < 0: - sign = -1 - self.change_to_current = ramp_rate_limit_A_per_min * dt * sign - logger.info(f'Ramp rate limit reduced output change to {self.change_to_current}') - - # Convert adjusted change_to_current back into new total current - new_current = (self._old_current or 0) + self.change_to_current - - # test if current within allowed parameters - logger.info('computed new current to be: {}'.format(new_current)) - if new_current > self.max_current: - logger.info("new current above max") - # clamp new_current to max - new_current = self.max_current - if new_current < self.min_current: - logger.info("new current below min") - # clamp new_current to min - new_current = self.min_current - if new_current < 0.: - logger.info("new current < 0") - # clamp new_current to 0 - new_current = 0. - - # re-compute the resulting change_to_current - self.change_to_current = new_current - self._old_current - # log the final current change - self.send_change_to_current_to_db(self.change_to_current) - # send the final, total new_current to the power supply - if self.pid_auto_mode == 1: - self.set_current(new_current) - else: - logger.info(f'Not setting current because pid_auto_mode != True') - # save existing current so the storage with `self._old_current = new_current` - # down below has the corect value for the next iteration - new_current = self._old_current - - - # confirm the power supply is actually putting out the requested value - logger.debug("allow settling time and checking the current value") - current_get = self.__get_current() - if abs(current_get-new_current) < self.tolerance: - logger.info("current set is equal to current get") - else: - # actual current is not within the acceptable range around the requested value - self.__validate_status() - # Record values and throw an exception - # this is the second place the function can return, - # so need to save values - self._old_pid_auto_mode = self.pid_auto_mode - self._old_current = new_current - self._old_Kp = self.Kproportional - self._old_Ki = self.Kintegral - self._old_Kd = self.Kdifferential - self._last_data['time'] = timestamp - self._last_data['delta'] = delta - logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) -# raise exceptions.DriplineValueError("set value ({}) is not equal to checked value ({})".format(new_current,current_get)) - - # save the old values for the next loop - # this is the final place the function can return, - # so need to save values - logger.info("current set is: {}".format(new_current)) - self._old_pid_auto_mode = self.pid_auto_mode - self._old_current = new_current - self._old_Kp = self.Kproportional - self._old_Ki = self.Kintegral - self._old_Kd = self.Kdifferential - self._last_data['time'] = timestamp - self._last_data['delta'] = delta - logger.info("Saved values for next run: self._old_current {}, self._old_Kp {}, self._old_Ki {}, self._old_Kd {}, self._last_data['time'] {}".format(self._old_current, self._old_Kp, self._old_Ki, self._old_Kd, self._last_data['time'] )) - - -''' -#OldVersion def process_new_value(self, value, timestamp): delta = self.target_value - value @@ -674,9 +198,32 @@ def process_new_value(self, value, timestamp): if abs(current_get-new_current) < self.tolerance: logger.debug("current set is equal to current get") else: - self.__validate_status() - raise ThrowReply("set value ({}) is not equal to checked value ({})".format(new_current,current_get)) + #self.__validate_status() + raise ThrowReply('DriplineValueError',"set value ({}) is not equal to checked value ({})".format(new_current,current_get)) logger.info("current set is: {}".format(new_current)) self._old_current = new_current -''' + +__all__.append('ServiceAttributeEntity') +#changed things like self.provider to self.service, idk if this is the move tho +class ServiceAttributeEntity(Entity): + ''' + Entity allowing communication with spime property. + ''' + + def __init__(self, + attribute_name, + disable_set=False, + **kwargs): + Entity.__init__(self, **kwargs) + self._attribute_name = attribute_name + self._disable_set = disable_set + + @calibrate() + def on_get(self): + return getattr(self.service, self._attribute_name) + + def on_set(self, value): + if self._disable_set: + raise ThrowReply('DriplineMethodNotSupportedError','setting not available for {}'.format(self.name)) + setattr(self.service, self._attribute_name, value)