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..7dce046 --- /dev/null +++ b/dripline/extensions/muxer_service.py @@ -0,0 +1,155 @@ +''' +A class to interface with the multiplexer aka muxer instrument +''' + +from dripline.core import ThrowReply, Entity, calibrate +from dripline.implementations import EthernetSCPIService, FormatEntity + +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') + + + +__all__.append('MuxerRelay') +class MuxerRelay(FormatEntity): + ''' + Entity to communicate with relay cards in muxer, + ''' + 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 '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) + 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)" +