from threading import Thread import time class CommClient(Thread): """ The ProtocolHandler class is the generic interface class between the tango attribute_wrapper and the outside world """ def __init__(self, fault_func, streams, try_interval=2): """ """ self.fault_func = fault_func self.try_interval = try_interval self.streams = streams self.stopping = False self.connected = False super().__init__(daemon=True) def connect(self): """ Function used to connect to the client. """ self.connected = True return True def disconnect(self): """ Function used to connect to the client. """ self.connected = False def run(self): # Explicitly connect if not self.connect(): # hardware or infra is down -- needs fixing first self.fault_func() return self.stopping = False while not self.stopping: # keep trying to connect if not self.connected: if self.connect(): pass else: # we retry only once, to catch exotic network issues. if the infra or hardware is down, # our device cannot help, and must be reinitialised after the infra or hardware is fixed. self.fault_func() return # keep checking if the connection is still alive try: while not self.stopping: self.ping() time.sleep(self.try_interval) except Exception as e: self.streams.error_stream("Fault condition in communication detected.", e) # technically, we may not have dropped the connection, but encounter a different error. so explicitly disconnect. self.disconnect() # signal that we're disconnected self.fault_func() def ping(self): return def stop(self): """ Stop connecting & disconnect. Can take a few seconds for the timeouts to hit. """ if not self.ident: # have not yet been started, so nothing to do return self.stopping = True self.join() self.disconnect() def setup_attribute(self, annotation, attribute): """ This function is responsible for providing the attribute_wrapper with a read/write function How this is done is implementation specific. The setup-attribute has access to the comms_annotation provided to the attribute wrapper to pass along to the comms client as well as a reference to the attribute itself. It should do this by first calling: _setup_annotation and setup_value_conversion to get all data necceacry to configure the read/write functions. It should then return the read and write functions to the attribute. MANDATORY: annotation_outputs = _setup_annotation(annotation) attribute_outputs = _setup_annotation(attribute) (note: outputs are up to the user) REQUIRED: provide read and write functions to return, there are no restrictions on how these should be provided, except that the read function takes a single input value and the write function returns a single value MANDATORY: return read_function, write_function Examples: - File system: get_mapping returns functions that read/write a fixed number of bytes at a fixed location in a file. (SEEK) - OPC-UA: traverse the OPC-UA tree until the node is found. Then return the read/write functions for that node which automatically convert values between Python and OPC-UA. """ raise NotImplementedError("the setup_attribute must be implemented and provide return a valid read/write function for the attribute") def _setup_annotation(self, annotation): """ This function is responsible for handling the annotation data provided by the attribute to configure the read/write function the client must provide. This function should be called by setup_attribute """ raise NotImplementedError("the _setup_annotation must be implemented, content and outputs are up to the user") def setup_value_conversion(self, attribute): """ this function is responsible for setting up the value conversion between the client and the attribute. This function should be called by setup_attribute """ raise NotImplementedError("the setup_value_conversion must be implemented, content and outputs are up to the user")