diff --git a/CDB/windows_ConfigDb.json b/CDB/windows_ConfigDb.json new file mode 100644 index 0000000000000000000000000000000000000000..47c23ed110ba476e508bebe1ccf7bca1c387bd8d --- /dev/null +++ b/CDB/windows_ConfigDb.json @@ -0,0 +1,56 @@ +{ + "servers": { + "PCC": { + "1": { + "PCC": { + "LTS/PCC/1": { + "properties": { + "OPC_Server_Name": [ + "host.docker.internal" + ] + } + } + } + } + }, + "SDP": { + "1": { + "SDP": { + "LTS/SDP/1": { + "properties": { + "OPC_Server_Name": [ + "host.docker.internal" + ] + } + } + } + } + }, + "example_device": { + "1": { + "example_device": { + "LTS/example_device/1": { + "attribute_properties": { + "Ant_mask_RW": { + "archive_period": [ + "600000" + ] + } + }, + "properties": { + "OPC_Server_Name": [ + "host.docker.internal" + ], + "OPC_Server_Port": [ + "4842" + ], + "OPC_Time_Out": [ + "5.0" + ] + } + } + } + } + } + } +} diff --git a/PCC/PCC/PCC.py b/PCC/PCC/PCC.py index 1b8fca093bbd64af1c1ddd5a657857ae26da3136..660d12e712c3281a9b4d080ee9e2b1de6c545f16 100644 --- a/PCC/PCC/PCC.py +++ b/PCC/PCC/PCC.py @@ -1,853 +1,158 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the PCC project -# -# -# -# Distributed under the terms of the APACHE license. -# See LICENSE.txt for more info. - -""" PCC Device Server for LOFAR2.0 - -""" - -# PyTango imports -from tango import DebugIt -from tango.server import run -from tango.server import Device -from tango.server import attribute, command -from tango.server import device_property -from tango import AttrQuality, DispLevel, DevState -from tango import AttrWriteType, PipeWriteType -# Additional import -import sys -import opcua -import numpy - -from wrappers import only_in_states, only_when_on, fault_on_error -from opcua_connection import OPCUAConnection - -__all__ = ["PCC", "main"] - -class PCC(Device): - """ - - **Properties:** - - - Device Property - OPC_Server_Name - - Type:'DevString' - OPC_Server_Port - - Type:'DevULong' - OPC_Time_Out - - Type:'DevDouble' - - States are as follows: - INIT = Device is initialising. - STANDBY = Device is initialised, but pends external configuration and an explicit turning on, - ON = Device is fully configured, functional, controls the hardware, and is possibly actively running, - FAULT = Device detected an unrecoverable error, and is thus malfunctional, - OFF = Device is turned off, drops connection to the hardware, - - The following state transitions are implemented: - boot -> OFF: Triggered by tango. Device will be instantiated, - OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), - INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, - STANDBY -> ON: Triggered by user. Device reports to be functional, - * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, - * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, - * -> OFF: Triggered by user. Device is turned off. Triggered by the Off() command, - FAULT -> INIT: Triggered by user. Device is reinitialised to recover from an error, - - The user triggers their transitions by the commands reflecting the target state (Initialise(), On(), Fault()). - """ - client = 0 - name_space_index = 0 - obj = 0 - - # ----------------- - # Device Properties - # ----------------- - OPC_Server_Name = device_property( - dtype=numpy.str, - mandatory=True - ) - - OPC_Server_Port = device_property( - dtype=numpy.uint64, - mandatory=True - ) - - OPC_Time_Out = device_property( - dtype=numpy.float_, - mandatory=True - ) - - # ---------- - # Attributes - # ---------- - - RCU_mask_RW = attribute( - dtype=(numpy.bool_,), - max_dim_x=32, - access=AttrWriteType.READ_WRITE, - ) - - Ant_mask_RW = attribute( - dtype=((numpy.bool_,),), - max_dim_x=32, max_dim_y=3, - access=AttrWriteType.READ_WRITE, - ) - - RCU_attenuator_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_attenuator_RW = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - access=AttrWriteType.READ_WRITE, - ) - - RCU_band_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_band_RW = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - access=AttrWriteType.READ_WRITE, - ) - - RCU_temperature_R = attribute( - dtype=(numpy.float_,), - max_dim_x=32, - ) - - RCU_Pwr_dig_R = attribute( - dtype=(numpy.int64,), - max_dim_x=32, - ) - - RCU_LED0_R = attribute( - dtype=(numpy.int64,), - max_dim_x=32, - ) - - RCU_LED0_RW = attribute( - dtype=(numpy.int64,), - max_dim_x=32, - access=AttrWriteType.READ_WRITE, - ) - - RCU_ADC_lock_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_ADC_SYNC_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_ADC_JESD_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_ADC_CML_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_OUT1_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_OUT2_R = attribute( - dtype=((numpy.int64,),), - max_dim_x=32, max_dim_y=3, - ) - - RCU_ID_R = attribute( - dtype=(numpy.int64,), - max_dim_x=32, - ) - - RCU_version_R = attribute( - dtype=(numpy.str,), - max_dim_x=32, - ) - - HBA_element_beamformer_delays_R = attribute( - dtype=((numpy.int64,),), - max_dim_x = 96, max_dim_y = 32, - ) - - HBA_element_beamformer_delays_RW = attribute( - dtype=((numpy.int64,),), - max_dim_x = 96, max_dim_y = 32, - access=AttrWriteType.READ_WRITE, - ) - - HBA_element_pwr_R = attribute( - dtype=((numpy.int64,),), - max_dim_x = 96, max_dim_y = 32, - ) - - HBA_element_pwr_RW = attribute( - dtype=((numpy.int64,),), - max_dim_x = 96, max_dim_y = 32, - access=AttrWriteType.READ_WRITE, - ) - - RCU_monitor_rate_RW = attribute( - dtype=numpy.float_, - access=AttrWriteType.READ_WRITE, - ) - - # --------------- - # General methods - # --------------- - def get_pcc_node(self, node): - try: - return self.pcc_node.get_child(["{}:{}".format(self.name_space_index, node)]) - except opcua.ua.uaerrors._auto.BadNoMatch: - self.error_stream("Could not find PCC node %s", node) - - # Contract with hardware is broken --- cannot recover - raise - - def _map_attributes(self): - try: - self.name_space_index = self.client.get_namespace_index("http://lofar.eu") - except Exception as e: - self.warn_stream("Cannot determine the OPC-UA name space index. Will try and use the default = 2.") - self.name_space_index = 2 - - self.obj_node = self.client.get_objects_node() - self.pcc_node = self.obj_node.get_child(["{}:PCC".format(self.name_space_index)]) - - self.debug_stream("Mapping OPC-UA MP/CP to attributes...") - - self.attribute_mapping["RCU_mask_RW"] = self.get_pcc_node("RCU_mask_RW") - - self.attribute_mapping["Ant_mask_RW"] = self.get_pcc_node("Ant_mask_RW") - - self.attribute_mapping["RCU_attenuator_R"] = self.get_pcc_node("RCU_attenuator_R") - - self.attribute_mapping["RCU_attenuator_RW"] = self.get_pcc_node("RCU_attenuator_RW") - - self.attribute_mapping["RCU_band_R"] = self.get_pcc_node("RCU_band_R") - - self.attribute_mapping["RCU_band_RW"] = self.get_pcc_node("RCU_band_RW") - - self.attribute_mapping["RCU_temperature_R"] = self.get_pcc_node("RCU_temperature_R") - - self.attribute_mapping["RCU_Pwr_dig_R"] = self.get_pcc_node("RCU_Pwr_dig_R") - - self.attribute_mapping["RCU_LED0_R"] = self.get_pcc_node("RCU_LED0_R") - - self.attribute_mapping["RCU_LED0_RW"] = self.get_pcc_node("RCU_LED0_RW") - - self.attribute_mapping["RCU_ADC_lock_R"] = self.get_pcc_node("RCU_ADC_lock_R") - - self.attribute_mapping["RCU_ADC_SYNC_R"] = self.get_pcc_node("RCU_ADC_SYNC_R") - - self.attribute_mapping["RCU_ADC_CML_R"] = self.get_pcc_node("RCU_ADC_CML_R") - - self.attribute_mapping["RCU_ADC_JESD_R"] = self.get_pcc_node("RCU_ADC_JESD_R") - - self.attribute_mapping["RCU_OUT1_R"] = self.get_pcc_node("RCU_OUT1_R") - - self.attribute_mapping["RCU_OUT2_R"] = self.get_pcc_node("RCU_OUT2_R") - - self.attribute_mapping["RCU_ID_R"] = self.get_pcc_node("RCU_ID_R") - - self.attribute_mapping["RCU_version_R"] = self.get_pcc_node("RCU_version_R") - - self.attribute_mapping["HBA_element_beamformer_delays_R"] = self.get_pcc_node("HBA_element_beamformer_delays_R") - - self.attribute_mapping["HBA_element_beamformer_delays_RW"] = self.get_pcc_node("HBA_element_beamformer_delays_RW") - - self.attribute_mapping["HBA_element_pwr_R"] = self.get_pcc_node("HBA_element_pwr_R") - - self.attribute_mapping["HBA_element_pwr_RW"] = self.get_pcc_node("HBA_element_pwr_RW") - - self.attribute_mapping["RCU_monitor_rate_RW"] = self.get_pcc_node("RCU_monitor_rate_RW") - - self.function_mapping["RCU_off"] = self.get_pcc_node("RCU_off") - - self.function_mapping["RCU_on"] = self.get_pcc_node("RCU_on") - - self.function_mapping["ADC_on"] = self.get_pcc_node("ADC_on") - - self.function_mapping["RCU_update"] = self.get_pcc_node("RCU_update") - - self.function_mapping["CLK_off"] = self.get_pcc_node("CLK_off") - - self.function_mapping["CLK_on"] = self.get_pcc_node("CLK_on") - - self.function_mapping["CLK_PLL_setup"] = self.get_pcc_node("CLK_PLL_setup") - - - self.debug_stream("Mapping OPC-UA MP/CP to attributes done.") - - def init_device(self): - """ Instantiates the device in the OFF state. """ - - # NOTE: Will delete_device first, if necessary - Device.init_device(self) - - self.set_state(DevState.OFF) - - def initialise(self): - """Initialises the attributes and properties of the PCC.""" - - self.set_state(DevState.INIT) - - # Init the dict that contains attribute to OPC-UA MP/CP mappings. - self.attribute_mapping = {} - - # Set default values in the RW/R attributes and add them to - # the mapping. - self._RCU_mask_RW = numpy.full(32, False) - self.attribute_mapping["RCU_mask_RW"] = {} - - self._Ant_mask_RW = numpy.full((3, 32), False) - self.attribute_mapping["Ant_mask_RW"] = {} - - self._RCU_attenuator_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_attenuator_R"] = {} - - self._RCU_attenuator_RW = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_attenuator_RW"] = {} - - self._RCU_band_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_band_R"] = {} - - self._RCU_band_RW = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_band_RW"] = {} - - self._RCU_temperature_R = numpy.full((3, 32), 0.0) - self.attribute_mapping["RCU_temperature_R"] = {} - - self._RCU_Pwr_dig_R = numpy.full(32, 0) - self.attribute_mapping["RCU_Pwr_dig_R"] = {} - - self._RCU_LED0_R = numpy.full(32, 0) - self.attribute_mapping["RCU_LED0_R"] = {} - - self._RCU_LED0_RW = numpy.full(32, 0) - self.attribute_mapping["RCU_LED0_RW"] = {} - - self._RCU_ADC_lock_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_ADC_lock_R"] = {} - - self._RCU_ADC_SYNC_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_ADC_SYNC_R"] = {} - - self._RCU_ADC_JESD_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_ADC_JESD_R"] = {} - - self._RCU_ADC_CML_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_ADC_CML_R"] = {} - - self._RCU_OUT1_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_OUT1_R"] = {} - - self._RCU_OUT2_R = numpy.full((3, 32), 0) - self.attribute_mapping["RCU_OUT2_R"] = {} - - self._RCU_ID_R = numpy.full(32, 0) - self.attribute_mapping["RCU_ID_R"] = {} - - self._RCU_version_R = numpy.full(32, "1234567890") - self.attribute_mapping["RCU_version_R"] = {} - - self._HBA_element_beamformer_delays_R = numpy.full((96, 32), 0) - self.attribute_mapping["HBA_element_beamformer_delays_R"] = {} - - self._HBA_element_beamformer_delays_RW = numpy.full((96, 32), 0) - self.attribute_mapping["HBA_element_beamformer_delays_RW"] = {} - - self._HBA_element_pwr_R = numpy.full((96, 32), 0) - self.attribute_mapping["HBA_element_pwr_R"] = {} - - self._HBA_element_pwr_RW = numpy.full((96, 32), 0) - self.attribute_mapping["HBA_element_pwr_RW"] = {} - - self._RCU_monitor_rate_RW = 30.0 - self.attribute_mapping["RCU_monitor_rate_RW"] = {} - - # Init the dict that contains function to OPC-UA function mappings. - self.function_mapping = {} - self.function_mapping["RCU_on"] = {} - self.function_mapping["RCU_off"] = {} - self.function_mapping["ADC_on"] = {} - self.function_mapping["RCU_update"] = {} - self.function_mapping["CLK_on"] = {} - self.function_mapping["CLK_off"] = {} - self.function_mapping["CLK_PLL_setup"] = {} - - self.client = opcua.Client("opc.tcp://{}:{}/".format(self.OPC_Server_Name, self.OPC_Server_Port), self.OPC_Time_Out) # timeout in seconds - - # Connect to OPC-UA -- will set ON state on success in case of a reconnect - self.opcua_connection = OPCUAConnection(self.client, self.Standby, self.Fault, self) - - # Explicitly connect - if not self.opcua_connection.connect(): - # hardware or infra is down -- needs fixing first - self.Fault() - return - - # Retrieve and map server attributes - try: - self._map_attributes() - except Exception as e: - self.error_stream("Could not map server interface: %s", e) - self.Fault() - return - - # Start keep-alive - self.opcua_connection.start() - - # Set the masks. - # - # Attention! - # Set the masks only after the OPCUA connection has been - # established! The setting of the masks needs to go through - # to the server. - # - # TODO - # Read default masks from config DB - self.write_RCU_mask_RW(self._RCU_mask_RW) - self.write_Ant_mask_RW(self._Ant_mask_RW) - - # Everything went ok -- go standby. - self.set_state(DevState.STANDBY) - - - def always_executed_hook(self): - """Method always executed before any TANGO command is executed.""" - pass - - def delete_device(self): - """Hook to delete resources allocated in init_device. - - This method allows for any memory or other resources allocated in the - init_device method to be released. This method is called by the device - destructor and by the device Init command (a Tango built-in). - """ - self.debug_stream("Shutting down...") - - self.Off() - self.debug_stream("Shut down. Good bye.") - - # ------------------ - # Attributes methods - # ------------------ - @only_when_on - @fault_on_error - def read_RCU_mask_R(self): - """Return the RCU_mask_R attribute.""" - self._RCU_mask_R = numpy.array(self.attribute_mapping["RCU_mask_R"].get_value()) - return self._RCU_mask_R - - @only_when_on - @fault_on_error - def read_RCU_mask_RW(self): - """Return the RCU_mask_RW attribute.""" - return self._RCU_mask_RW - - @only_when_on - @fault_on_error - def write_RCU_mask_RW(self, value): - """Set the RCU_mask_RW attribute.""" - self.attribute_mapping["RCU_mask_RW"].set_value(value.tolist()) - self._RCU_mask_RW = value - - @only_when_on - @fault_on_error - def read_Ant_mask_R(self): - """Return the Ant_mask_R attribute.""" - value = numpy.array(self.attribute_mapping["Ant_mask_R"].get_value()) - self._Ant_mask_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._Ant_mask_R - - @only_when_on - @fault_on_error - def read_Ant_mask_RW(self): - """Return the Ant_mask_RW attribute.""" - return self._Ant_mask_RW - - @only_when_on - @fault_on_error - def write_Ant_mask_RW(self, value): - """Set the Ant_mask_RW attribute.""" - v = numpy.concatenate(value) - self.attribute_mapping["Ant_mask_RW"].set_value(v.tolist()) - self._Ant_mask_RW = value - - @only_when_on - @fault_on_error - def read_RCU_attenuator_R(self): - """Return the RCU_attenuator_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_attenuator_R"].get_value()) - self._RCU_attenuator_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_attenuator_R - - @only_when_on - @fault_on_error - def read_RCU_attenuator_RW(self): - """Return the RCU_attenuator_RW attribute.""" - return self._RCU_attenuator_RW - - @only_when_on - @fault_on_error - def write_RCU_attenuator_RW(self, value): - """Set the RCU_attenuator_RW attribute.""" - v = numpy.concatenate(value) - self.attribute_mapping["RCU_attenuator_RW"].set_value(v.tolist()) - self._RCU_attenuator_RW = value - - @only_when_on - @fault_on_error - def read_RCU_band_R(self): - """Return the RCU_band_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_band_R"].get_value()) - self._RCU_band_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_band_R - - @only_when_on - @fault_on_error - def read_RCU_band_RW(self): - """Return the RCU_band_RW attribute.""" - return self._RCU_band_RW - - @only_when_on - @fault_on_error - def write_RCU_band_RW(self, value): - """Set the RCU_band_RW attribute.""" - v = numpy.concatenate(value) - self.attribute_mapping["RCU_band_RW"].set_value(v.tolist()) - self._RCU_band_RW = value - - @only_when_on - @fault_on_error - def read_RCU_temperature_R(self): - """Return the RCU_temperature_R attribute.""" - self._RCU_temperature_R = numpy.array(self.attribute_mapping["RCU_temperature_R"].get_value()) - return self._RCU_temperature_R - - @only_when_on - @fault_on_error - def read_RCU_Pwr_dig_R(self): - """Return the RCU_Pwr_dig_R attribute.""" - self._RCU_Pwr_dig_R = numpy.array(self.attribute_mapping["RCU_Pwr_dig_R"].get_value()) - return self._RCU_Pwr_dig_R - - @only_when_on - @fault_on_error - def read_RCU_LED0_R(self): - """Return the RCU_LED0_R attribute.""" - self._RCU_LED0_R = numpy.array(self.attribute_mapping["RCU_LED0_R"].get_value()) - return self._RCU_LED0_R - - @only_when_on - @fault_on_error - def read_RCU_LED0_RW(self): - """Return the RCU_LED0_RW attribute.""" - return self._RCU_LED0_RW - - @only_when_on - @fault_on_error - def write_RCU_LED0_RW(self, value): - """Set the RCU_LED0_RW attribute.""" - self.attribute_mapping["RCU_LED0_RW"].set_value(value.tolist()) - self._RCU_LED0_RW = value - - @only_when_on - @fault_on_error - def read_RCU_ADC_lock_R(self): - """Return the RCU_ADC_lock_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_ADC_lock_R"].get_value()) - self._RCU_ADC_lock_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_ADC_lock_R - - @only_when_on - @fault_on_error - def read_RCU_ADC_SYNC_R(self): - """Return the RCU_ADC_SYNC_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_ADC_SYNC_R"].get_value()) - self._RCU_ADC_SYNC_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_ADC_SYNC_R - - @only_when_on - @fault_on_error - def read_RCU_ADC_JESD_R(self): - """Return the RCU_ADC_JESD_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_ADC_JESD_R"].get_value()) - self._RCU_ADC_JESD_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_ADC_JESD_R - - @only_when_on - @fault_on_error - def read_RCU_ADC_CML_R(self): - """Return the RCU_ADC_CML_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_ADC_CML_R"].get_value()) - self._RCU_ADC_CML_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_ADC_CML_R - - @only_when_on - @fault_on_error - def read_RCU_OUT1_R(self): - """Return the RCU_OUT1_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_OUT1_R"].get_value()) - self._RCU_OUT1_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_OUT1_R - - @only_when_on - @fault_on_error - def read_RCU_OUT2_R(self): - """Return the RCU_OUT2_R attribute.""" - value = numpy.array(self.attribute_mapping["RCU_OUT2_R"].get_value()) - self._RCU_OUT2_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._RCU_OUT2_R - - @only_when_on - @fault_on_error - def read_RCU_ID_R(self): - """Return the RCU_ID_R attribute.""" - self._RCU_ID_R = numpy.array(self.attribute_mapping["RCU_ID_R"].get_value()) - return self._RCU_ID_R - - @only_when_on - @fault_on_error - def read_RCU_version_R(self): - """Return the RCU_version_R attribute.""" - value = self.attribute_mapping["RCU_version_R"].get_value() - self._RCU_version_R = numpy.array(value) - return self._RCU_version_R - - @only_when_on - @fault_on_error - def read_HBA_element_beamformer_delays_R(self): - """Return the HBA_element_beamformer_delays_R attribute.""" - value = numpy.array(self.attribute_mapping["HBA_element_beamformer_delays_R"].get_value()) - self._HBA_element_beamformer_delays_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._HBA_element_beamformer_delays_R - - @only_when_on - @fault_on_error - def read_HBA_element_beamformer_delays_RW(self): - """Return the HBA_element_beamformer_delays_RW attribute.""" - return self._HBA_element_beamformer_delays_RW - - @only_when_on - @fault_on_error - def write_HBA_element_beamformer_delays_RW(self, value): - """Set the HBA_element_beamformer_delays_RW attribute.""" - self.attribute_mapping["HBA_element_beamformer_delays_RW"].set_value(value.flatten().tolist()) - self._HBA_element_beamformer_delays_RW = value - - @only_when_on - @fault_on_error - def read_HBA_element_pwr_R(self): - """Return the HBA_element_pwr_R attribute.""" - value = numpy.array(self.attribute_mapping["HBA_element_pwr_R"].get_value()) - self._HBA_element_pwr_R = numpy.array(numpy.split(value, indices_or_sections = 32)) - return self._HBA_element_pwr_R - - @only_when_on - @fault_on_error - def read_HBA_element_pwr_RW(self): - """Return the HBA_element_pwr_RW attribute.""" - return self._HBA_element_pwr_RW - - @only_when_on - @fault_on_error - def write_HBA_element_pwr_RW(self, value): - """Set the HBA_element_pwr_RW attribute.""" - self.attribute_mapping["HBA_element_pwr_RW"].set_value(value.flatten().tolist()) - self._HBA_element_pwr_RW = value - - @only_when_on - @fault_on_error - def read_RCU_monitor_rate_RW(self): - """Return the RCU_monitor_rate_RW attribute.""" - return self._RCU_monitor_rate_RW - - @only_when_on - @fault_on_error - def write_RCU_monitor_rate_RW(self, value): - """Set the RCU_monitor_rate_RW attribute.""" - self.attribute_mapping["RCU_monitor_rate_RW"].set_value(value) - self._RCU_monitor_rate_RW = value - - - # -------- - # Commands - # -------- - - @command() - @only_in_states([DevState.FAULT, DevState.OFF]) - @DebugIt() - def Initialise(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - - self.initialise() - - @only_in_states([DevState.INIT]) - def Standby(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - - self.set_state(DevState.STANDBY) - - @command() - @only_in_states([DevState.STANDBY]) - @DebugIt() - def On(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - - self.set_state(DevState.ON) - - @command() - @DebugIt() - def Off(self): - """ - Command to ask for shutdown of this device. - - :return:None - """ - if self.get_state() == DevState.OFF: - # Already off. Don't complain. - return - - # Turn off - self.set_state(DevState.OFF) - - # Stop keep-alive - self.opcua_connection.stop() - - # Turn off again, in case of race conditions through reconnecting - self.set_state(DevState.OFF) - - @command() - @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) - @DebugIt() - def Fault(self): - """ - FAULT state is used to indicate our connection with the OPC-UA server is down. - - This device will try to reconnect once, and transition to the ON state on success. - - If reconnecting fails, the user needs to call Initialise() to retry to restart this device. - - :return:None - """ - self.set_state(DevState.FAULT) - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def RCU_off(self): - """ - - :return:None - """ - self.function_mapping["RCU_off"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def RCU_on(self): - """ - - :return:None - """ - self.function_mapping["RCU_on"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def ADC_on(self): - """ - - :return:None - """ - self.function_mapping["ADC_on"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def RCU_update(self): - """ - - :return:None - """ - self.function_mapping["RCU_update"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def CLK_off(self): - """ - - :return:None - """ - self.function_mapping["CLK_off"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def CLK_on(self): - """ - - :return:None - """ - self.function_mapping["CLK_on"]() - - @command() - @DebugIt() - @only_when_on - @fault_on_error - def CLK_PLL_setup(self): - """ - - :return:None - """ - self.function_mapping["CLK_PLL_setup"]() - - -# ---------- -# Run server -# ---------- -def main(args=None, **kwargs): - """Main function of the PCC module.""" - return run((PCC,), args=args, **kwargs) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" PCC Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.opcua_connection import OPCUAConnection +from attribute_wrapper import * +from hardware_device import * + + +__all__ = ["PCC", "main"] + +class PCC(hardware_device): + """ + + **Properties:** + + - Device Property + OPC_Server_Name + - Type:'DevString' + OPC_Server_Port + - Type:'DevULong' + OPC_Time_Out + - Type:'DevDouble' + """ + + # ----------------- + # Device Properties + # ----------------- + + OPC_Server_Name = device_property( + dtype='DevString', + mandatory=True + ) + + OPC_Server_Port = device_property( + dtype='DevULong', + mandatory=True + ) + + OPC_Time_Out = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + RCU_mask_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_mask_RW"], datatype=numpy.bool_, dims=(32,), access=AttrWriteType.READ_WRITE) + Ant_mask_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:Ant_mask_RW"], datatype=numpy.bool_, dims=(3, 32), access=AttrWriteType.READ_WRITE) + RCU_attenuator_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_attenuator_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_attenuator_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_attenuator_RW"], datatype=numpy.int64, dims=(3, 32), access=AttrWriteType.READ_WRITE) + RCU_band_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_band_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_band_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_band_RW"], datatype=numpy.int64, dims=(3, 32), access=AttrWriteType.READ_WRITE) + RCU_temperature_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_temperature_R"], datatype=numpy.float64, dims=(32,)) + RCU_Pwr_dig_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_Pwr_dig_R"], datatype=numpy.int64, dims=(32,)) + RCU_LED0_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_LED0_R"], datatype=numpy.int64, dims=(32,)) + RCU_LED0_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_LED0_RW"], datatype=numpy.int64, dims=(32,), access=AttrWriteType.READ_WRITE) + RCU_ADC_lock_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_ADC_lock_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_ADC_SYNC_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_ADC_SYNC_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_ADC_JESD_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_ADC_JESD_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_ADC_CML_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_ADC_CML_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_OUT1_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_OUT1_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_OUT2_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_OUT2_R"], datatype=numpy.int64, dims=(3, 32)) + RCU_ID_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_ID_R"], datatype=numpy.int64, dims=(32,)) + RCU_version_R = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_version_R"], datatype=numpy.str_, dims=(32,)) + + HBA_element_beamformer_delays_R = attribute_wrapper(comms_annotation=["2:PCC", "2:HBA_element_beamformer_delays_R"], datatype=numpy.int64, dims=(32,96)) + HBA_element_beamformer_delays_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:HBA_element_beamformer_delays_RW"], datatype=numpy.int64, dims=(32,96), access=AttrWriteType.READ_WRITE) + HBA_element_pwr_R = attribute_wrapper(comms_annotation=["2:PCC", "2:HBA_element_pwr_R"], datatype=numpy.int64, dims=(32,96)) + HBA_element_pwr_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:HBA_element_pwr_RW"], datatype=numpy.int64, dims=(32,96), access=AttrWriteType.READ_WRITE) + + RCU_monitor_rate_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:RCU_monitor_rate_RW"], datatype=numpy.float64, access=AttrWriteType.READ_WRITE) + + def setup_value_dict(self): + self.value_dict = {str(i): i.initial_value() for i in self.attr_list()} + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # overloaded functions + # -------- + def fault(self): + """ user code here. is called when the state is set to FAULT """ + pass + + def off(self): + """ user code here. is called when the state is set to OFF """ + + # Stop keep-alive + self.opcua_connection.stop() + + def on(self): + """ user code here. is called when the state is set to ON """ + + pass + + def standby(self): + """ user code here. is called when the state is set to STANDBY """ + pass + + def initialise(self): + """ user code here. is called when the state is set to STANDBY """ + """Initialises the attributes and properties of the PCC.""" + + #set up the OPC ua client + self.OPCua_client = OPCUAConnection("opc.tcp://{}:{}/".format(self.OPC_Server_Name, self.OPC_Server_Port), self.OPC_Time_Out, self.Standby, self.Fault, self) + + # will contain all the values for this object + self.setup_value_dict() + + # map an access helper class + for i in self.attr_list(): + i.set_comm_client(self.OPCua_client) + + self.OPCua_client.start() + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the PCC module.""" + return run((PCC,), args=args, **kwargs) + + +if __name__ == '__main__': + main() + diff --git a/PCC/PCC/README.md b/PCC/PCC/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d7926f28a9a0660f25eb7824ed38af0fd41be25c --- /dev/null +++ b/PCC/PCC/README.md @@ -0,0 +1,20 @@ +# Device wrapper + +This code provides an attribute_wrapper class in place of attributes for tango devices. the attribute wrappers contain additional code +that moves a lot of the complexity and redundant code to the background. + +The only things required on the users part are to declare the attributes using the attribute_wrapper (see `example/example_device`), +declare what client the attribute has to use in the initialisation and provide support for the used clients. +To see how to add support for new clients, see `clients/README.md` + + +Requires numpy +```pip install numpy``` + +Requires opcua +```pip install opcua``` + +Requires pytango +```pip install pytango``` + + diff --git a/PCC/PCC/attribute_wrapper.py b/PCC/PCC/attribute_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..ff1937c17dbb377dc6a0f9737d6b64379294d5b8 --- /dev/null +++ b/PCC/PCC/attribute_wrapper.py @@ -0,0 +1,153 @@ +from tango.server import attribute, command +from tango import DebugIt +from tango import AttrWriteType + +import numpy + +from wrappers import only_in_states, only_when_on, fault_on_error + + +def swap_dims_tuple(dims): + """ + arrays are inconsistent between tango and numpy. + This function exists to swap around the tuple containing the dimension data + """ + if len(dims) == 2: + return tuple((dims[1], dims[0])) + else: + return dims + + + +class attribute_wrapper(attribute): + """ + Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes + """ + + def __init__(self, comms_annotation=None, datatype=None, dims=1, **kwargs): + """ + wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract + managing the communications interface. + """ + + # ensure the type is a numpy array + if "numpy" not in str(datatype) and type(datatype) != str: + raise TypeError("Attribute needs to be a Tango-supported numpy or str type, but has type \"%s\"" % (datatype,)) + + + + self.comms_annotation = comms_annotation # store data that can be used by the comms interface. not used by the wrapper itself + self.numpy_type = datatype # tango changes our attribute to their representation (E.g numpy.int64 becomes "DevLong64") + + wrap_RW = kwargs.get("access", AttrWriteType.READ) + + self.init_value = kwargs.get("init_value", None) # if not None, gets used as default value + + max_dim_y = 0 + + # tango doesn't recognise numpy.str_, for consistencies sake we convert it here and hide this from the top level + # NOTE: discuss, idk if this is an important detail somewhere else + if datatype is numpy.str_: + datatype = str + + # check if not scalar + if isinstance(dims, tuple): + + # fill the array with initial values + # self.value = numpy.zeros(swap_dims_tuple(dims), dtype=datatype) + # self.value = numpy.full(swap_dims_tuple(dims), datatype(0), dtype=data_type) + + # get first dimension + max_dim_x = dims[0] + + # single dimension/spectrum requires the datatype to be wrapped in a tuple + datatype = (datatype,) + + if len(dims) == 2: + # get second dimension + max_dim_y = dims[1] + # wrap the datatype tuple in another tuple for 2d arrays/images + datatype = (datatype,) + else: + # scalar, just set the single dimension + max_dim_x = 1 + + + if wrap_RW == AttrWriteType.READ_WRITE: + """ if the attribute is of READ_WRITE type, assign the RW and write function to it""" + + @only_when_on + @fault_on_error + def read_RW(device): + # print("read_RW {}, {}x{}, {}, {}".format(me.name, me.dim_x, me.dim_y, me.attr_type, me.value)) + """ + read_RW returns the value that was last written to the attribute + """ + try: + return device.value_dict[str(self)] + except: + print() + + @only_when_on + @fault_on_error + def write_RW(device, value): + """ + _write_RW writes a value to this attribute + """ + self.write_function(value) + device.value_dict[str(self)] = value + + self.fget = read_RW + self.fset = write_RW + + + else: + """ if the attribute is of READ type, assign the read function to it""" + + @only_when_on + @fault_on_error + def read_R(device): + """ + _read_R reads the attribute value, stores it and returns it" + """ + device.value_dict[str(self)] = self.read_function() + return device.value_dict[str(self)] + + self.fget = read_R + + super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, **kwargs) + + return + + def initial_value(self): + """ + returns a numpy array filled with zeroes fit to the size of the attribute. Or if init_value is not the default None, return that value + """ + if self.init_value is not None: + return self.init_value + + if self.dim_y > 1: + dims = (self.dim_x, self.dim_y) + else: + dims = (self.dim_x,) + + # x and y are swapped for numpy and Tango. to maintain tango conventions, x and y are swapped for numpy + value = numpy.zeros(swap_dims_tuple(dims), dtype=self.numpy_type) + return value + + def set_comm_client(self, client): + """ + takes a communications client as input arguments This client should be of a class containing a "get_mapping" function + and return a read and write function that the wrapper will use to get/set data. + """ + try: + self.read_function, self.write_function = client.setup_attribute(self.comms_annotation, self) + except: + def pass_func(value=None): + pass + print("setting comm_client failed. using pass function instead") + + self.read_function = pass_func + self.write_function = pass_func + + diff --git a/PCC/PCC/clients/README.md b/PCC/PCC/clients/README.md new file mode 100644 index 0000000000000000000000000000000000000000..34d2709a0c70ab960b4ac23cb19668fab689bc7c --- /dev/null +++ b/PCC/PCC/clients/README.md @@ -0,0 +1,4 @@ +this folder contains all the comms_client implementations for organisation + +### How to add a new client +soon™ \ No newline at end of file diff --git a/PCC/PCC/clients/opcua_connection.py b/PCC/PCC/clients/opcua_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..35709f0b71cbde79db5cfe2daa5b992229989046 --- /dev/null +++ b/PCC/PCC/clients/opcua_connection.py @@ -0,0 +1,215 @@ +from comms_client import * + + +__all__ = ["OPCUAConnection"] + +OPCua_to_numpy_dict = { + "VariantType.Boolean": numpy.bool_, + "VariantType.SByte": numpy.int8, + "VariantType.Byte": numpy.uint8, + "VariantType.Int16": numpy.int16, + "VariantType.UInt16": numpy.uint16, + "VariantType.Int32": numpy.int32, + "VariantType.UInt32": numpy.uint32, + "VariantType.Int64": numpy.int64, + "VariantType.UInt64": numpy.uint64, + "VariantType.DateTime": numpy.datetime_data, # is this the right type, does it even matter? + "VariantType.Float": numpy.float32, + "VariantType.Double": numpy.double, + "VariantType.String": numpy.str, + "VariantType.ByteString": numpy.uint8 # sequence of bytes, not a string +} + +numpy_to_OPCua_dict = { + "<class 'numpy.bool_'>": opcua.ua.VariantType.Boolean, + "<class 'numpy.int8'>": opcua.ua.VariantType.SByte, + "<class 'numpy.uint8'>": opcua.ua.VariantType.Byte, + "<class 'numpy.int16'>": opcua.ua.VariantType.Int16, + "<class 'numpy.uint16'>": opcua.ua.VariantType.UInt16, + "<class 'numpy.int32'>": opcua.ua.VariantType.Int32, + "<class 'numpy.uint32'>": opcua.ua.VariantType.UInt32, + "<class 'numpy.int64'>": opcua.ua.VariantType.Int64, + "<class 'numpy.uint64'>": opcua.ua.VariantType.UInt64, + "<class 'numpy.datetime_data'>": opcua.ua.VariantType.DateTime, # is this the right type, does it even matter? + "<class 'numpy.float32'>": opcua.ua.VariantType.Float, + "<class 'numpy.float64'>": opcua.ua.VariantType.Double, + "<class 'numpy.double'>": opcua.ua.VariantType.Double, + "<class 'numpy.str_'>": opcua.ua.VariantType.String, + "<class 'numpy.str'>": opcua.ua.VariantType.String, + "str": opcua.ua.VariantType.String +} + +# <class 'numpy.bool_'> + +class OPCUAConnection(CommClient): + """ + Connects to OPC-UA in the foreground or background, and sends HELLO + messages to keep a check on the connection. On connection failure, reconnects once. + """ + + def start(self): + super().start() + + def __init__(self, address, timeout, on_func, fault_func, streams, try_interval=2): + """ + Create the OPC ua client and connect() to it and get the object node + """ + super().__init__(on_func, fault_func, streams, try_interval) + + self.client = Client(address, timeout) + + # Explicitly connect + if not self.connect(): + # hardware or infra is down -- needs fixing first + fault_func() + return + + self.streams.debug_stream("Demo ||\t connection established") + + # determine namespace used + try: + self.name_space_index = self.client.get_namespace_index("http://lofar.eu") + except Exception as e: + self.streams.warn_stream("Cannot determine the OPC-UA name space index. Will try and use the default = 2.") + self.name_space_index = 2 + + self.obj = self.client.get_objects_node() + + def _servername(self): + return self.client.server_url.geturl() + + def connect(self): + """ + Try to connect to the client + """ + + try: + self.streams.debug_stream("Connecting to server %s", self._servername()) + self.client.connect() + self.connected = True + self.streams.debug_stream("Connected to server. Initialising.") + return True + except socket.error as e: + self.streams.error_stream("Could not connect to server %s: %s", self._servername(), e) + return False + + def disconnect(self): + """ + disconnect from the client + """ + self.connected = False # always force a reconnect, regardless of a successful disconnect + + try: + self.client.disconnect() + except Exception as e: + self.streams.error_stream("Disconnect from OPC-UA server %s failed: %s", self._servername(), e) + + def ping(self): + """ + ping the client to make sure the connection with the client is still functional. + """ + try: + self.client.send_hello() + except Exception as e: + raise Exception("Lost connection to server %s: %s", self._servername(), e) + + def _setup_annotation(self, annotation): + """ + This class's Implementation of the get_mapping function. returns the read and write functions + """ + + if isinstance(annotation, dict): + # check if required path inarg is present + if annotation.get('path') is None: + AssertionError("OPC-ua mapping requires a path argument in the annotation") + + path = annotation.get("path") # required + ua_Type = annotation.get("ua_type") # optional, if excluded must be a build in python type + elif isinstance(annotation, list): + path = annotation + else: + TypeError("OPC-ua mapping requires either a list or dict with the path") + return + + #TODO exceptions + + try: + node = self.obj.get_child(path) + except Exception as e: + self.streams.error_stream("Could not get node: %s on server %s: %s", path, self._servername(), e) + raise Exception("Could not get node: %s on server %s: %s", path, self._servername(), e) + + return node + + def setup_value_conversion(self, attribute): + """ + gives the client access to the attribute_wrapper object in order to access all data it could potentially need. + the OPC ua read/write functions require the dimensionality and the type to be known + """ + + dim_x = attribute.dim_x + dim_y = attribute.dim_y + ua_type = numpy_to_OPCua_dict[str(attribute.numpy_type)] # convert the numpy type to a corresponding UA type + + return dim_x, dim_y, ua_type + + def setup_attribute(self, annotation, attribute): + """ + MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions + """ + + # process the annotation + node = self._setup_annotation(annotation) + + # get all the necessary data to set up the read/write functions from the attribute_wrapper + dim_x, dim_y, ua_type = self.setup_value_conversion(attribute) + + # configure and return the read/write functions + prot_attr = ProtocolAttribute(node, dim_x, dim_y, ua_type) + + node_name = str(node.get_browse_name())[len("QualifiedName(2:"):] + self.streams.debug_stream("connected OPC ua node {} of type {} to attribute with dimensions: {} x {} ".format(str(node_name)[:len(node_name)-1], str(ua_type)[len("VariantType."):], dim_x, dim_y)) + + # return the read/write functions + return prot_attr.read_function, prot_attr.write_function + + + + +class ProtocolAttribute: + """ + This class provides a small wrapper for the OPC ua read/write functions in order to better organise the code + """ + + def __init__(self, node, dim_x, dim_y, ua_type): + self.node = node + self.dim_y = dim_y + self.dim_x = dim_x + self.ua_type = ua_type + + def read_function(self): + """ + Read_R function + """ + value = numpy.array(self.node.get_value()) + + if self.dim_y != 0: + value = numpy.array(numpy.split(value, indices_or_sections=self.dim_y)) + else: + value = numpy.array(value) + return value + + def write_function(self, value): + """ + write_RW function + """ + # set_data_value(opcua.ua.uatypes.Variant(value = value.tolist(), varianttype=opcua.ua.VariantType.Int32)) + + if self.dim_y != 0: + v = numpy.concatenate(value) + self.node.set_data_value(opcua.ua.uatypes.Variant(value=v.tolist(), varianttype=self.ua_type)) + + elif self.dim_x != 1: + self.node.set_data_value(opcua.ua.uatypes.Variant(value=value.tolist(), varianttype=self.ua_type)) + else: + self.node.set_data_value(opcua.ua.uatypes.Variant(value=value, varianttype=self.ua_type)) diff --git a/PCC/PCC/clients/test_client.py b/PCC/PCC/clients/test_client.py new file mode 100644 index 0000000000000000000000000000000000000000..37ab2d32b6ede43477327204969d5778e01bc59d --- /dev/null +++ b/PCC/PCC/clients/test_client.py @@ -0,0 +1,110 @@ +from comms_client import * + + +__all__ = ["example_client"] + + +# <class 'numpy.bool_'> + +class example_client(CommClient): + """ + this class provides an example implementation of a comms_client. + Durirng initialisation it creates a correctly shaped zero filled value. on read that value is returned and on write its modified. + """ + + def start(self): + super().start() + + def __init__(self, standby_func, fault_func, streams, try_interval=2): + """ + initialises the class and tries to connect to the client. + """ + super().__init__(standby_func, fault_func, streams, try_interval) + + # Explicitly connect + if not self.connect(): + # hardware or infra is down -- needs fixing first + fault_func() + return + + def connect(self): + """ + this function provides a location for the code neccecary to connect to the client + """ + + self.streams.debug_stream("the example client doesn't actually connect to anything silly") + + self.connected = True # set connected to true + return True # if succesfull, return true. otherwise return false + + def disconnect(self): + self.connected = False # always force a reconnect, regardless of a successful disconnect + self.streams.debug_stream("disconnected from the 'client' ") + + def _setup_annotation(self, annotation): + """ + this function gives the client access to the comm client annotation data given to the attribute wrapper. + The annotation data can be used to provide whatever extra data is necessary in order to find/access the monitor/control point. + + the annotation can be in whatever format may be required. it is up to the user to handle its content + example annotation may include: + - a file path and file line/location + - COM object path + """ + + # as this is an example, just print the annotation + self.streams.debug_stream("annotation: {}".format(annotation)) + + def _setup_value_conversion(self, attribute): + """ + gives the client access to the attribute_wrapper object in order to access all + necessary data such as dimensionality and data type + """ + + if attribute.dim_y > 1: + dims = (attribute.dim_y, attribute.dim_x) + else: + dims = (attribute.dim_x,) + + dtype = attribute.numpy_type + + return dims, dtype + + + def _setup_mapping(self, dims, dtype): + """ + takes all gathered data to configure and return the correct read and write functions + """ + + value = numpy.zeros(dims, dtype) + + def read_function(): + self.streams.debug_stream("from read_function, reading {} array of type {}".format(dims, dtype)) + return value + + def write_function(write_value): + self.streams.debug_stream("from write_function, writing {} array of type {}".format(dims, dtype)) + value = write_value + + self.streams.debug_stream("created and bound example_client read/write functions to attribute_wrapper object") + return read_function, write_function + + + def setup_attribute(self, annotation=None, attribute=None): + """ + MANDATORY function: is used by the attribute wrapper to get read/write functions. + must return the read and write functions + """ + + # process the comms_annotation + self._setup_annotation(annotation) + + # get all the necessary data to set up the read/write functions from the attribute_wrapper + dims, dtype = self._setup_value_conversion(attribute) + + # configure and return the read/write functions + read_function, write_function = self._setup_mapping(dims, dtype) + + # return the read/write functions + return read_function, write_function + diff --git a/PCC/PCC/comms_client.py b/PCC/PCC/comms_client.py new file mode 100644 index 0000000000000000000000000000000000000000..544df539d972cff9da37ac6a2dce00db27d76796 --- /dev/null +++ b/PCC/PCC/comms_client.py @@ -0,0 +1,114 @@ +from threading import Thread +import socket +import time +import numpy + +import opcua +from opcua import Client + +from tango import DevState + + +class CommClient(Thread): + """ + The ProtocolHandler class is the generic interface class between the tango attribute_wrapper and the outside world + """ + + def __init__(self, standby_func, fault_func, streams, try_interval=2): + """ + + """ + self.standby_func = standby_func + 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.standby_func() + + self.stopping = False + while not self.stopping: + # keep trying to connect + print("connected check") + if not self.connected: + print("not connected, try to connect") + if self.connect(): + print("connected now, call on_func") + self.standby_func() + 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): + pass + + 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. + + 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. + ''' + AssertionError("the setup_attribute must be implemented and provide return a valid read/write function for the attribute") + diff --git a/PCC/PCC/example/README.md b/PCC/PCC/example/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d84d08646c3b88417269d864d2de4882e17337d0 --- /dev/null +++ b/PCC/PCC/example/README.md @@ -0,0 +1,6 @@ +####This example provides a more minimal tango device. + +The tango device, named `example_device` provides a number of attribute wrappers named after their type +and dimensionality (E.g `float32_image_R`). + +These attributes make use of the `example_client`, a very minimal implementation of a comms_client. \ No newline at end of file diff --git a/PCC/PCC/example/example_device.py b/PCC/PCC/example/example_device.py new file mode 100644 index 0000000000000000000000000000000000000000..35dba23e45425e5f4c6c30ef55ca99678a3a06ce --- /dev/null +++ b/PCC/PCC/example/example_device.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" PCC Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.example_client import example_client +from attribute_wrapper import * + +__all__ = ["example_device", "main"] + + +class example_device(Device): + client = 0 + name_space_index = 0 + obj = 0 + + # ----------------- + # Device Properties + # ----------------- + + OPC_Server_Name = device_property( + dtype='DevString', + mandatory=True + ) + + OPC_Server_Port = device_property( + dtype='DevULong', + mandatory=True + ) + + OPC_Time_Out = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + bool_scalar_R = attribute_wrapper(comms_annotation="numpy.bool_ type read scalar", datatype=numpy.bool_) + bool_scalar_RW = attribute_wrapper(comms_annotation="numpy.bool_ type read/write scalar", datatype=numpy.bool_, access=AttrWriteType.READ_WRITE) + + int64_spectrum_R = attribute_wrapper(comms_annotation="numpy.int64 type read spectrum (len = 8)", datatype=numpy.int64, dims=(8,)) + str_spectrum_RW = attribute_wrapper(comms_annotation="numpy.str type read/write spectrum (len = 8)", datatype=numpy.str_, dims=(8,), access=AttrWriteType.READ_WRITE) + + double_image_R = attribute_wrapper(comms_annotation="numpy.double type read image (dims = 2x8)", datatype=numpy.double, dims=(2, 8)) + double_image_RW = attribute_wrapper(comms_annotation="numpy.double type read/write image (dims = 8x2)", datatype=numpy.double, dims=(8, 2), access=AttrWriteType.READ_WRITE) + + int32_scalar_R = attribute_wrapper(comms_annotation="numpy.int32 type read scalar", datatype=numpy.int32) + uint16_spectrum_RW = attribute_wrapper(comms_annotation="numpy.uint16 type read/write spectrum (len = 8)", datatype=numpy.uint16, dims=(8,), access=AttrWriteType.READ_WRITE) + float32_image_R = attribute_wrapper(comms_annotation="numpy.float32 type read image (dims = 8x2)", datatype=numpy.float32, dims=(8, 2)) + uint8_image_RW = attribute_wrapper(comms_annotation="numpy.uint8 type read/write image (dims = 2x8)", datatype=numpy.uint8, dims=(2, 8), access=AttrWriteType.READ_WRITE) + + attr_list = [ bool_scalar_R, bool_scalar_RW, int64_spectrum_R, str_spectrum_RW, double_image_R, double_image_RW, int32_scalar_R, + uint16_spectrum_RW, float32_image_R, uint8_image_RW] + + def setup_value_dict(self): + self.value_dict = {} + for i in self.attr_list: + self.value_dict[str(i)] = i.initial_value() + + def init_device(self): + """ Instantiates the device in the OFF state. """ + + # NOTE: Will delete_device first, if necessary + Device.init_device(self) + + self.set_state(DevState.OFF) + + def initialise(self): + """Initialises the attributes and properties of the PCC.""" + + self.set_state(DevState.INIT) + + + #set up the OPC ua client + self.example_client = example_client(self.Standby, self.Fault, self) + + + # NOTE: MANDATORY will contain all attribute values for this tango device instance + self.setup_value_dict() + + # map an access helper class + for i in self.attr_list: + i.set_comm_client(self.example_client) + + + self.example_client.start() + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # Commands + # -------- + + @command() + @only_in_states([DevState.FAULT, DevState.OFF]) + @DebugIt() + def Initialise(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.initialise() + + @only_in_states([DevState.INIT]) + def Standby(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + print("setting standby state") + self.set_state(DevState.STANDBY) + + @command() + @only_in_states([DevState.STANDBY]) + @DebugIt() + def On(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.set_state(DevState.ON) + + @command() + @DebugIt() + def Off(self): + """ + Command to ask for shutdown of this device. + + :return:None + """ + if self.get_state() == DevState.OFF: + # Already off. Don't complain. + return + + # Turn off + self.set_state(DevState.OFF) + + # Stop keep-alive + self.opcua_connection.stop() + + # Turn off again, in case of race conditions through reconnecting + self.set_state(DevState.OFF) + + @command() + @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) + @DebugIt() + def Fault(self): + """ + FAULT state is used to indicate our connection with the OPC-UA server is down. + + This device will try to reconnect once, and transition to the ON state on success. + + If reconnecting fails, the user needs to call Initialise() to retry to restart this device. + + :return:None + """ + self.set_state(DevState.FAULT) + + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the example module.""" + return run((example_device,), args=args, **kwargs) + + +if __name__ == '__main__': + main() diff --git a/PCC/PCC/example_device.py b/PCC/PCC/example_device.py new file mode 100644 index 0000000000000000000000000000000000000000..6e1842f8d04b8ccaaae3b5c8ec5c9d87925a62e0 --- /dev/null +++ b/PCC/PCC/example_device.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" PCC Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.example_client import example_client +from attribute_wrapper import * + +__all__ = ["example_device", "main"] + + +class example_device(Device): + client = 0 + name_space_index = 0 + obj = 0 + + # ----------------- + # Device Properties + # ----------------- + + OPC_Server_Name = device_property( + dtype='DevString', + mandatory=True + ) + + OPC_Server_Port = device_property( + dtype='DevULong', + mandatory=True + ) + + OPC_Time_Out = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + bool_scalar_R = attribute_wrapper(comms_annotation="numpy.bool_ type read scalar", datatype=numpy.bool_) + bool_scalar_RW = attribute_wrapper(comms_annotation="numpy.bool_ type read/write scalar", datatype=numpy.bool_, access=AttrWriteType.READ_WRITE) + + int64_spectrum_R = attribute_wrapper(comms_annotation="numpy.int64 type read spectrum (len = 8)", datatype=numpy.int64, dims=(8,)) + str_spectrum_RW = attribute_wrapper(comms_annotation="numpy.str type read/write spectrum (len = 8)", datatype=numpy.str_, dims=(8,), access=AttrWriteType.READ_WRITE) + + double_image_R = attribute_wrapper(comms_annotation="numpy.double type read image (dims = 2x8)", datatype=numpy.double, dims=(2, 8)) + double_image_RW = attribute_wrapper(comms_annotation="numpy.double type read/write image (dims = 8x2)", datatype=numpy.double, dims=(8, 2), access=AttrWriteType.READ_WRITE) + + int32_scalar_R = attribute_wrapper(comms_annotation="numpy.int32 type read scalar", datatype=numpy.int32) + uint16_spectrum_RW = attribute_wrapper(comms_annotation="numpy.uint16 type read/write spectrum (len = 8)", datatype=numpy.uint16, dims=(8,), access=AttrWriteType.READ_WRITE) + float32_image_R = attribute_wrapper(comms_annotation="numpy.float32 type read image (dims = 8x2)", datatype=numpy.float32, dims=(8, 2)) + uint8_image_RW = attribute_wrapper(comms_annotation="numpy.uint8 type read/write image (dims = 2x8)", datatype=numpy.uint8, dims=(2, 8), access=AttrWriteType.READ_WRITE) + + attr_list = [ bool_scalar_R, bool_scalar_RW, int64_spectrum_R, str_spectrum_RW, double_image_R, double_image_RW, int32_scalar_R, + uint16_spectrum_RW, float32_image_R, uint8_image_RW] + + def setup_value_dict(self): + self.value_dict = {} + for i in self.attr_list: + self.value_dict[str(i)] = i.initial_value() + + def init_device(self): + """ Instantiates the device in the OFF state. """ + + # NOTE: Will delete_device first, if necessary + Device.init_device(self) + + self.set_state(DevState.OFF) + + def initialise(self): + """Initialises the attributes and properties of the PCC.""" + + self.set_state(DevState.INIT) + + + #set up the OPC ua client + self.example_client = example_client(self.Standby, self.Fault, self) + + + # NOTE: MANDATORY will contain all attribute values for this tango device instance + self.setup_value_dict() + + # map an access helper class + for i in self.attr_list: + i.set_comm_client(self.example_client) + + + self.example_client.start() + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # Commands + # -------- + + @command() + @only_in_states([DevState.FAULT, DevState.OFF]) + @DebugIt() + def Initialise(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.initialise() + + @only_in_states([DevState.INIT]) + def Standby(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + print("setting standby state") + self.set_state(DevState.STANDBY) + + @command() + @only_in_states([DevState.STANDBY]) + @DebugIt() + def On(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.set_state(DevState.ON) + + @command() + @DebugIt() + def Off(self): + """ + Command to ask for shutdown of this device. + + :return:None + """ + if self.get_state() == DevState.OFF: + # Already off. Don't complain. + return + + # Turn off + self.set_state(DevState.OFF) + + # Stop keep-alive + self.opcua_connection.stop() + + # Turn off again, in case of race conditions through reconnecting + self.set_state(DevState.OFF) + + @command() + @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) + @DebugIt() + def Fault(self): + """ + FAULT state is used to indicate our connection with the OPC-UA server is down. + + This device will try to reconnect once, and transition to the ON state on success. + + If reconnecting fails, the user needs to call Initialise() to retry to restart this device. + + :return:None + """ + self.set_state(DevState.FAULT) + + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the PCC module.""" + return run((example_device,), args=args, **kwargs) + + +if __name__ == '__main__': + main() diff --git a/PCC/PCC/hardware_device.py b/PCC/PCC/hardware_device.py new file mode 100644 index 0000000000000000000000000000000000000000..6b665055cf35387ea3be4eebfb12f8928cfc6e4e --- /dev/null +++ b/PCC/PCC/hardware_device.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" PCC Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.opcua_connection import OPCUAConnection +from attribute_wrapper import * + + +__all__ = ["hardware_device"] + +class hardware_device(Device): + """ + + **Properties:** + + States are as follows: + INIT = Device is initialising. + STANDBY = Device is initialised, but pends external configuration and an explicit turning on, + ON = Device is fully configured, functional, controls the hardware, and is possibly actively running, + FAULT = Device detected an unrecoverable error, and is thus malfunctional, + OFF = Device is turned off, drops connection to the hardware, + + The following state transitions are implemented: + boot -> OFF: Triggered by tango. Device will be instantiated, + OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), + INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, + STANDBY -> ON: Triggered by user. Device reports to be functional, + * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, + * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, + * -> OFF: Triggered by user. Device is turned off. Triggered by the Off() command, + FAULT -> INIT: Triggered by user. Device is reinitialised to recover from an error, + + The user triggers their transitions by the commands reflecting the target state (Initialise(), On(), Fault()). + """ + + @classmethod + def attr_list(cls): + """ Return a list of all the attribute_wrapper members of this class. """ + return [v for k, v in cls.__dict__.items() if type(v) == attribute_wrapper] + + def init_device(self): + """ Instantiates the device in the OFF state. """ + + # NOTE: Will delete_device first, if necessary + Device.init_device(self) + + self.set_state(DevState.OFF) + + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # Commands + # -------- + + @command() + @only_in_states([DevState.FAULT, DevState.OFF]) + @DebugIt() + def Initialise(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + self.set_state(DevState.INIT) + self.initialise() + + # if self.get_state() == DevState.STANDBY: + # # Already STANDBY. Don't complain. + # return + # self.set_state(DevState.STANDBY) + + @only_in_states([DevState.INIT]) + def Standby(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.standby() + self.set_state(DevState.STANDBY) + + @command() + @only_in_states([DevState.STANDBY]) + @DebugIt() + def On(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + self.on() + self.set_state(DevState.ON) + + @command() + @DebugIt() + def Off(self): + """ + Command to ask for shutdown of this device. + + :return:None + """ + if self.get_state() == DevState.OFF: + # Already off. Don't complain. + return + + # Turn off + self.set_state(DevState.OFF) + + self.off() + + # Turn off again, in case of race conditions through reconnecting + self.set_state(DevState.OFF) + + @command() + @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) + @DebugIt() + def Fault(self): + """ + FAULT state is used to indicate our connection with the OPC-UA server is down. + + This device will try to reconnect once, and transition to the ON state on success. + + If reconnecting fails, the user needs to call Initialise() to retry to restart this device. + + :return:None + """ + self.fault() + self.set_state(DevState.FAULT) + + + # functions that can be overloaded + def fault(self): + pass + def off(self): + pass + def on(self): + pass + def standby(self): + pass + def initialise(self): + pass + diff --git a/PCC/PCC/opcua_connection.py b/PCC/PCC/opcua_connection.py deleted file mode 100644 index cfcfb74ab00416b79bfc2ccc8fbf263372c9f80c..0000000000000000000000000000000000000000 --- a/PCC/PCC/opcua_connection.py +++ /dev/null @@ -1,84 +0,0 @@ -from threading import Thread -import socket -import time - -__all__ = ["OPCUAConnection"] - -class OPCUAConnection(Thread): - """ - Connects to OPC-UA in the foreground or background, and sends HELLO - messages to keep a check on the connection. On connection failure, reconnects once. - """ - - def __init__(self, client, on_func, fault_func, streams, try_interval=2): - super().__init__(daemon=True) - - self.client = client - self.on_func = on_func - self.fault_func = fault_func - self.try_interval = try_interval - self.streams = streams - self.stopping = False - self.connected = False - - def _servername(self): - return self.client.server_url.geturl() - - def connect(self): - try: - self.streams.debug_stream("Connecting to server %s", self._servername()) - self.client.connect() - self.connected = True - self.streams.debug_stream("Connected to server. Initialising.") - return True - except socket.error as e: - self.streams.error_stream("Could not connect to server %s: %s", self._servername(), e) - return False - - def disconnect(self): - self.connected = False # always force a reconnect, regardless of a successful disconnect - - try: - self.client.disconnect() - except Exception as e: - self.streams.error_stream("Disconnect from OPC-UA server %s failed: %s", self._servername(), e) - - def run(self): - while not self.stopping: - # keep trying to connect - if not self.connected: - if self.connect(): - self.on_func() - 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.client.send_hello() - time.sleep(self.try_interval) - except Exception as e: - self.streams.error_stream("Lost connection to server %s: %s", self._servername(), 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 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() diff --git a/PCC/test/test-PCC.py b/PCC/test/test-PCC.py old mode 100755 new mode 100644 diff --git a/SDP/SDP/LICENSE.txt b/SDP/SDP/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..583bd5a9884fcc0c6e3e6908de9a930203b19d24 --- /dev/null +++ b/SDP/SDP/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/SDP/SDP/README.md b/SDP/SDP/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d7926f28a9a0660f25eb7824ed38af0fd41be25c --- /dev/null +++ b/SDP/SDP/README.md @@ -0,0 +1,20 @@ +# Device wrapper + +This code provides an attribute_wrapper class in place of attributes for tango devices. the attribute wrappers contain additional code +that moves a lot of the complexity and redundant code to the background. + +The only things required on the users part are to declare the attributes using the attribute_wrapper (see `example/example_device`), +declare what client the attribute has to use in the initialisation and provide support for the used clients. +To see how to add support for new clients, see `clients/README.md` + + +Requires numpy +```pip install numpy``` + +Requires opcua +```pip install opcua``` + +Requires pytango +```pip install pytango``` + + diff --git a/SDP/SDP/SDP.py b/SDP/SDP/SDP.py index ee98bab36fa7cef38131f8c607bc46cbfeb652ac..0faa38a1ed1cc53061ca6c4773ee49924adfd584 100644 --- a/SDP/SDP/SDP.py +++ b/SDP/SDP/SDP.py @@ -1,488 +1,146 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the SDP project -# -# -# -# Distributed under the terms of the APACHE license. -# See LICENSE.txt for more info. - -""" SDP Device Server for LOFAR2.0 - -""" - -# PyTango imports -from tango import DebugIt -from tango.server import run -from tango.server import Device -from tango.server import attribute, command -from tango.server import device_property -from tango import AttrQuality, DispLevel, DevState -from tango import AttrWriteType, PipeWriteType -# Additional import -import sys -import opcua -import numpy - -from wrappers import only_in_states, only_when_on, fault_on_error -from opcua_connection import OPCUAConnection - -__all__ = ["SDP", "main"] - -class SDP(Device): - """ - - **Properties:** - - - Device Property - OPC_Server_Name - - Type:numpy.str - OPC_Server_Port - - Type:'DevULong' - OPC_Time_Out - - Type:numpy.float_ - - States are as follows: - INIT = Device is initialising. - STANDBY = Device is initialised, but pends external configuration and an explicit turning on, - ON = Device is fully configured, functional, controls the hardware, and is possibly actively running, - FAULT = Device detected an unrecoverable error, and is thus malfunctional, - OFF = Device is turned off, drops connection to the hardware, - - The following state transitions are implemented: - boot -> OFF: Triggered by tango. Device will be instantiated, - OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), - INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, - STANDBY -> ON: Triggered by user. Device reports to be functional, - * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, - * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, - * -> OFF: Triggered by user. Device is turned off. Triggered by the Off() command, - FAULT -> INIT: Triggered by user. Device is reinitialised to recover from an error, - - The user triggers their transitions by the commands reflecting the target state (Initialise(), On(), Fault()). - """ - device = "SDP" - client = 0 - name_space_index = 0 - obj = 0 - - # ----------------- - # Device Properties - # ----------------- - - OPC_Server_Name = device_property( - dtype=numpy.str, - mandatory=True - ) - - OPC_Server_Port = device_property( - dtype=numpy.uint64, - mandatory=True - ) - - OPC_Time_Out = device_property( - dtype=numpy.float_, - mandatory=True - ) - - # ---------- - # Attributes - # ---------- - fpga_mask_RW = attribute( - dtype = (numpy.bool_,), - max_dim_x = 16, - access=AttrWriteType.READ_WRITE, - ) - - fpga_scrap_R = attribute( - dtype = (numpy.int32,), - max_dim_x = 2048, - ) - - fpga_scrap_RW = attribute( - dtype = (numpy.int32,), - max_dim_x = 2048, - access=AttrWriteType.READ_WRITE, - ) - - fpga_status_R = attribute( - dtype = (numpy.bool_,), - max_dim_x = 16, - ) - - fpga_temp_R = attribute( - dtype = (numpy.float_,), - max_dim_x = 16, - ) - - fpga_version_R = attribute( - dtype = (numpy.str,), - max_dim_x = 16, - ) - - fpga_weights_R = attribute( - dtype = ((numpy.int16,),), - max_dim_x = 12 * 488 * 2, max_dim_y = 16, - ) - - fpga_weights_RW = attribute( - dtype = ((numpy.int16,),), - max_dim_x = 12 * 488 * 2, max_dim_y = 16, - access=AttrWriteType.READ_WRITE, - ) - - tr_busy_R = attribute( - dtype = (numpy.bool_), - ) - - tr_reload_RW = attribute( - dtype = (numpy.bool_), - access=AttrWriteType.READ_WRITE, - ) - - tr_tod_R = attribute( - dtype = (numpy.uint64), - ) - - tr_uptime_R = attribute( - dtype = (numpy.uint64,), - ) - - - # --------------- - # General methods - # --------------- - def get_node(self, node): - try: - return self.lofar_device_node.get_child(["{}:{}".format(self.name_space_index, node)]) - except opcua.ua.uaerrors._auto.BadNoMatch: - self.error_stream("Could not find LOFAR device %s node %s", self.device, node) - - # Contract with hardware is broken --- cannot recover - raise - - def _map_attributes(self): - try: - self.name_space_index = self.client.get_namespace_index("http://lofar.eu") - except Exception as e: - self.name_space_index = 1 - self.warn_stream("Cannot determine the OPC-UA name space index. Will try and use the default = %d." % (self.name_space_index)) - - self.obj_node = self.client.get_objects_node() - # TODO - # The server does not implement the correct namespace yet. - # Instead it is directly using the Objects node. - #self.lofar_device_node = self.obj_node.get_child(["{}:SDP".format(self.name_space_index)]) - self.lofar_device_node = self.obj_node - - self.info_stream("Mapping OPC-UA MP/CP to attributes...") - - self.attribute_mapping["fpga_mask_RW"] = self.get_node("fpga_mask_RW") - self.attribute_mapping["fpga_scrap_R"] = self.get_node("fpga_scrap_R") - self.attribute_mapping["fpga_scrap_RW"] = self.get_node("fpga_scrap_RW") - self.attribute_mapping["fpga_status_R"] = self.get_node("fpga_status_R") - self.attribute_mapping["fpga_temp_R"] = self.get_node("fpga_temp_R") - self.attribute_mapping["fpga_version_R"] = self.get_node("fpga_version_R") - self.attribute_mapping["fpga_weights_R"] = self.get_node("fpga_weights_R") - self.attribute_mapping["fpga_weights_RW"] = self.get_node("fpga_weights_RW") - self.attribute_mapping["tr_busy_R"] = self.get_node("tr_busy_R") - self.attribute_mapping["tr_reload_RW"] = self.get_node("tr_reload_W") - self.attribute_mapping["tr_tod_R"] = self.get_node("tr_tod_R") - self.attribute_mapping["tr_uptime_R"] = self.get_node("tr_uptime_R") - - self.info_stream("Mapping OPC-UA MP/CP to attributes done.") - - def init_device(self): - """ Instantiates the device in the OFF state. """ - - # NOTE: Will delete_device first, if necessary - Device.init_device(self) - - self.set_state(DevState.OFF) - - def initialise(self): - """Initialises the attributes and properties of the SDP.""" - - self.set_state(DevState.INIT) - - # Init the dict that contains attribute to OPC-UA MP/CP mappings. - self.attribute_mapping = {} - - # Set default values in the RW/R attributes and add them to - # the mapping. - self._fpga_mask_RW = numpy.full(16, False) - self.attribute_mapping["fpga_mask_RW"] = {} - self._fpga_scrap_R = numpy.full(2048, False) - self.attribute_mapping["fpga_scrap_R"] = {} - self._fpga_scrap_RW = numpy.full(2048, False) - self.attribute_mapping["fpga_scrap_RW"] = {} - self._fpga_status_R = numpy.full(16, False) - self.attribute_mapping["fpga_status_R"] = {} - self._fpga_temp_R = numpy.full(16, 0.0) - self.attribute_mapping["fpga_temp_R"] = {} - self._fpga_version_R = numpy.full(16, "NO_VERSION_INFO_YET") - self.attribute_mapping["fpga_version_R"] = {} - self._fpga_weights_R = numpy.full((16, 2 * 488 * 12), 0) - self.attribute_mapping["fpga_weights_R"] = {} - self._fpga_weights_RW = numpy.full((16, 2 * 488 * 12), 0) - self.attribute_mapping["fpga_weights_RW"] = {} - self._tr_busy_R = False - self.attribute_mapping["tr_busy_R"] = {} - self._tr_reload_RW = False - self.attribute_mapping["tr_reload_RW"] = {} - self._tr_tod_R = 0 - self.attribute_mapping["tr_tod_R"] = {} - self._tr_uptime_R = 0 - self.attribute_mapping["tr_uptime_R"] = {} - - # Init the dict that contains function to OPC-UA function mappings. - self.function_mapping = {} - - self.client = opcua.Client("opc.tcp://{}:{}/".format(self.OPC_Server_Name, self.OPC_Server_Port), self.OPC_Time_Out) # timeout in seconds - - # Connect to OPC-UA -- will set ON state on success in case of a reconnect - self.opcua_connection = OPCUAConnection(self.client, self.Standby, self.Fault, self) - - # Explicitly connect - if not self.opcua_connection.connect(): - # hardware or infra is down -- needs fixing first - self.Fault() - return - - # Retrieve and map server attributes - try: - self._map_attributes() - except Exception as e: - self.error_stream("Could not map server interface: %s", e) - self.Fault() - return - - # Start keep-alive - self.opcua_connection.start() - - # Set the masks. - # - # Attention! - # Set the masks only after the OPCUA connection has been - # established! The setting of the masks needs to go through - # to the server. - # - # TODO - # Read default masks from config DB - #self.write_fpga_mask_RW(self._fpga_mask_R) - - # Everything went ok -- go standby. - self.set_state(DevState.STANDBY) - - - def always_executed_hook(self): - """Method always executed before any TANGO command is executed.""" - pass - - @DebugIt() - def delete_device(self): - """Hook to delete resources allocated in init_device. - - This method allows for any memory or other resources allocated in the - init_device method to be released. This method is called by the device - destructor and by the device Init command (a Tango built-in). - """ - self.Off() - - - # ------------------ - # Attributes methods - # ------------------ - @only_when_on - @fault_on_error - def read_fpga_mask_RW(self): - """Return the fpga_mask_RW attribute.""" - return self._fpga_mask_RW - - @only_when_on - @fault_on_error - def write_fpga_mask_RW(self, value): - """Return the fpga_mask_RW attribute.""" - self.attribute_mapping["fpga_mask_RW"].set_value(value.tolist()) - self._fpga_mask_RW = value - return - - @only_when_on - @fault_on_error - def read_fpga_scrap_R(self): - """Return the fpga_scrap_R attribute.""" - self._fpga_scrap_R = numpy.array(self.attribute_mapping["fpga_scrap_R"].get_value(), dtype = numpy.int32) - return self._fpga_scrap_R - - @only_when_on - @fault_on_error - def read_fpga_scrap_RW(self): - """Return the fpga_scrap_RW attribute.""" - return self._fpga_scrap_RW - - @only_when_on - @fault_on_error - def write_fpga_scrap_RW(self, value): - """Return the fpga_scrap_RW attribute.""" - self.attribute_mapping["fpga_scrap_RW"].set_data_value(opcua.ua.uatypes.Variant(value = value.tolist(), varianttype=opcua.ua.VariantType.Int32)) - _fpga_scrap_RW = value - - @only_when_on - @fault_on_error - def read_fpga_status_R(self): - """Return the fpga_status_R attribute.""" - self._fpga_status_R = numpy.array(self.attribute_mapping["fpga_status_R"].get_value()) - return self._fpga_status_R - - @only_when_on - @fault_on_error - def read_fpga_temp_R(self): - """Return the fpga_temp_R attribute.""" - self._fpga_temp_R = numpy.array(self.attribute_mapping["fpga_temp_R"].get_value()) - return self._fpga_temp_R - - @only_when_on - @fault_on_error - def read_fpga_version_R(self): - """Return the fpga_version_R attribute.""" - self._fpga_version_R = numpy.array(self.attribute_mapping["fpga_version_R"].get_value()) - return self._fpga_version_R - - @only_when_on - @fault_on_error - def read_fpga_weights_R(self): - """Return the fpga_weights_R attribute.""" - value = numpy.array(numpy.split(numpy.array(self.attribute_mapping["fpga_weights_R"].get_value(), dtype = numpy.int16), indices_or_sections = 16)) - self._fpga_weights_R = value - return self._fpga_weights_R - - @only_when_on - @fault_on_error - def read_fpga_weights_RW(self): - """Return the fpga_weights_RW attribute.""" - return self._fpga_weights_RW - - @only_when_on - @fault_on_error - def write_fpga_weights_RW(self, value): - """Return the fpga_weights_RW attribute.""" - self.attribute_mapping["fpga_weights_RW"].set_data_value(opcua.ua.uatypes.Variant(value = value.flatten().tolist(), varianttype=opcua.ua.VariantType.Int16)) - self._fpga_weights_RW = value - - @only_when_on - @fault_on_error - def read_tr_busy_R(self): - """Return the tr_busy_R attribute.""" - self._tr_busy_R = self.attribute_mapping["tr_busy_R"].get_value() - return self._tr_busy_R - - @only_when_on - @fault_on_error - def read_tr_reload_RW(self): - """Return the tr_reload_RW attribute.""" - self._tr_reload_RW = self.attribute_mapping["tr_reload_RW"].get_value() - return self._tr_reload_RW - - @only_when_on - @fault_on_error - def write_tr_reload_RW(self, value): - """Return the tr_reload_RW attribute.""" - self.attribute_mapping["tr_reload_RW"].set_value(value) - self._tr_reload_RW = value - - @only_when_on - @fault_on_error - def read_tr_tod_R(self): - """Return the _tr_tod_R attribute.""" - self._tr_tod_R = self.attribute_mapping["tr_tod_R"].get_value() - return self._tr_tod_R - - @only_when_on - @fault_on_error - def read_tr_uptime_R(self): - """Return the _tr_uptime_R attribute.""" - self._tr_uptime_R = self.attribute_mapping["tr_uptime_R"].get_value() - return self._tr_uptime_R - - - # -------- - # Commands - # -------- - @command() - @only_in_states([DevState.FAULT, DevState.OFF]) - @DebugIt() - def Initialise(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - self.initialise() - - @only_in_states([DevState.INIT]) - def Standby(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - self.set_state(DevState.STANDBY) - - @command() - @only_in_states([DevState.STANDBY]) - @DebugIt() - def On(self): - """ - Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. - - :return:None - """ - self.set_state(DevState.ON) - - @command() - @DebugIt() - def Off(self): - """ - Command to ask for shutdown of this device. - - :return:None - """ - if self.get_state() == DevState.OFF: - # Already off. Don't complain. - return - - # Turn off - self.set_state(DevState.OFF) - - # Stop keep-alive - self.opcua_connection.stop() - - # Turn off again, in case of race conditions through reconnecting - self.set_state(DevState.OFF) - - @command() - @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) - @DebugIt() - def Fault(self): - """ - FAULT state is used to indicate our connection with the OPC-UA server is down. - - This device will try to reconnect once, and transition to the ON state on success. - - If reconnecting fails, the user needs to call Initialise() to retry to restart this device. - - :return:None - """ - self.set_state(DevState.FAULT) - - -# ---------- -# Run server -# ---------- -def main(args=None, **kwargs): - """Main function of the SDP module.""" - return run((SDP,), args=args, **kwargs) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +# +# This file is part of the SDP project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" SDP Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.opcua_connection import OPCUAConnection +from attribute_wrapper import * +from hardware_device import * + + +__all__ = ["SDP", "main"] + +class SDP(hardware_device): + """ + + **Properties:** + + - Device Property + OPC_Server_Name + - Type:'DevString' + OPC_Server_Port + - Type:'DevULong' + OPC_Time_Out + - Type:'DevDouble' + """ + + # ----------------- + # Device Properties + # ----------------- + + OPC_Server_Name = device_property( + dtype='DevString', + mandatory=True + ) + + OPC_Server_Port = device_property( + dtype='DevULong', + mandatory=True + ) + + OPC_Time_Out = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + fpga_mask_RW = attribute_wrapper(comms_annotation=["1:fpga_mask_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE) + fpga_scrap_R = attribute_wrapper(comms_annotation=["1:fpga_scrap_R"], datatype=numpy.int32, dims=(2048,)) + fpga_scrap_RW = attribute_wrapper(comms_annotation=["1:fpga_scrap_RW"], datatype=numpy.int32, dims=(2048,), access=AttrWriteType.READ_WRITE) + fpga_status_R = attribute_wrapper(comms_annotation=["1:fpga_status_R"], datatype=numpy.bool_, dims=(16,)) + fpga_temp_R = attribute_wrapper(comms_annotation=["1:fpga_temp_R"], datatype=numpy.float_, dims=(16,)) + fpga_version_R = attribute_wrapper(comms_annotation=["1:fpga_version_R"], datatype=numpy.str_, dims=(16,)) + fpga_weights_R = attribute_wrapper(comms_annotation=["1:fpga_weights_R"], datatype=numpy.int16, dims=(16, 12 * 488 * 2)) + fpga_weights_RW = attribute_wrapper(comms_annotation=["1:fpga_weights_RW"], datatype=numpy.int16, dims=(16, 12 * 488 * 2), access=AttrWriteType.READ_WRITE) + tr_busy_R = attribute_wrapper(comms_annotation=["1:tr_busy_R"], datatype=numpy.bool_) + # NOTE: typo in node name is 'tr_reload_W' should be 'tr_reload_RW' + tr_reload_RW = attribute_wrapper(comms_annotation=["1:tr_reload_W"], datatype=numpy.bool_, access=AttrWriteType.READ_WRITE) + tr_tod_R = attribute_wrapper(comms_annotation=["1:tr_tod_R"], datatype=numpy.uint64) + tr_uptime_R = attribute_wrapper(comms_annotation=["1:tr_uptime_R"], datatype=numpy.uint64) + + def setup_value_dict(self): + self.value_dict = {str(i): i.initial_value() for i in self.attr_list()} + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # overloaded functions + # -------- + def fault(self): + """ user code here. is called when the state is set to FAULT """ + pass + + def off(self): + """ user code here. is called when the state is set to OFF """ + + # Stop keep-alive + self.opcua_connection.stop() + + def on(self): + """ user code here. is called when the state is set to ON """ + + pass + + def standby(self): + """ user code here. is called when the state is set to STANDBY """ + pass + + def initialise(self): + """ user code here. is called when the sate is set to INIT """ + """Initialises the attributes and properties of the PCC.""" + + # set up the OPC ua client + self.OPCua_client = OPCUAConnection("opc.tcp://{}:{}/".format(self.OPC_Server_Name, self.OPC_Server_Port), self.OPC_Time_Out, self.Standby, self.Fault, self) + + # will contain all the values for this object + self.setup_value_dict() + + # map an access helper class + for i in self.attr_list(): + i.set_comm_client(self.OPCua_client) + + self.OPCua_client.start() + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the SDP module.""" + return run((SDP,), args=args, **kwargs) + + +if __name__ == '__main__': + main() + diff --git a/SDP/SDP/attribute_wrapper.py b/SDP/SDP/attribute_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..ff1937c17dbb377dc6a0f9737d6b64379294d5b8 --- /dev/null +++ b/SDP/SDP/attribute_wrapper.py @@ -0,0 +1,153 @@ +from tango.server import attribute, command +from tango import DebugIt +from tango import AttrWriteType + +import numpy + +from wrappers import only_in_states, only_when_on, fault_on_error + + +def swap_dims_tuple(dims): + """ + arrays are inconsistent between tango and numpy. + This function exists to swap around the tuple containing the dimension data + """ + if len(dims) == 2: + return tuple((dims[1], dims[0])) + else: + return dims + + + +class attribute_wrapper(attribute): + """ + Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes + """ + + def __init__(self, comms_annotation=None, datatype=None, dims=1, **kwargs): + """ + wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract + managing the communications interface. + """ + + # ensure the type is a numpy array + if "numpy" not in str(datatype) and type(datatype) != str: + raise TypeError("Attribute needs to be a Tango-supported numpy or str type, but has type \"%s\"" % (datatype,)) + + + + self.comms_annotation = comms_annotation # store data that can be used by the comms interface. not used by the wrapper itself + self.numpy_type = datatype # tango changes our attribute to their representation (E.g numpy.int64 becomes "DevLong64") + + wrap_RW = kwargs.get("access", AttrWriteType.READ) + + self.init_value = kwargs.get("init_value", None) # if not None, gets used as default value + + max_dim_y = 0 + + # tango doesn't recognise numpy.str_, for consistencies sake we convert it here and hide this from the top level + # NOTE: discuss, idk if this is an important detail somewhere else + if datatype is numpy.str_: + datatype = str + + # check if not scalar + if isinstance(dims, tuple): + + # fill the array with initial values + # self.value = numpy.zeros(swap_dims_tuple(dims), dtype=datatype) + # self.value = numpy.full(swap_dims_tuple(dims), datatype(0), dtype=data_type) + + # get first dimension + max_dim_x = dims[0] + + # single dimension/spectrum requires the datatype to be wrapped in a tuple + datatype = (datatype,) + + if len(dims) == 2: + # get second dimension + max_dim_y = dims[1] + # wrap the datatype tuple in another tuple for 2d arrays/images + datatype = (datatype,) + else: + # scalar, just set the single dimension + max_dim_x = 1 + + + if wrap_RW == AttrWriteType.READ_WRITE: + """ if the attribute is of READ_WRITE type, assign the RW and write function to it""" + + @only_when_on + @fault_on_error + def read_RW(device): + # print("read_RW {}, {}x{}, {}, {}".format(me.name, me.dim_x, me.dim_y, me.attr_type, me.value)) + """ + read_RW returns the value that was last written to the attribute + """ + try: + return device.value_dict[str(self)] + except: + print() + + @only_when_on + @fault_on_error + def write_RW(device, value): + """ + _write_RW writes a value to this attribute + """ + self.write_function(value) + device.value_dict[str(self)] = value + + self.fget = read_RW + self.fset = write_RW + + + else: + """ if the attribute is of READ type, assign the read function to it""" + + @only_when_on + @fault_on_error + def read_R(device): + """ + _read_R reads the attribute value, stores it and returns it" + """ + device.value_dict[str(self)] = self.read_function() + return device.value_dict[str(self)] + + self.fget = read_R + + super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, **kwargs) + + return + + def initial_value(self): + """ + returns a numpy array filled with zeroes fit to the size of the attribute. Or if init_value is not the default None, return that value + """ + if self.init_value is not None: + return self.init_value + + if self.dim_y > 1: + dims = (self.dim_x, self.dim_y) + else: + dims = (self.dim_x,) + + # x and y are swapped for numpy and Tango. to maintain tango conventions, x and y are swapped for numpy + value = numpy.zeros(swap_dims_tuple(dims), dtype=self.numpy_type) + return value + + def set_comm_client(self, client): + """ + takes a communications client as input arguments This client should be of a class containing a "get_mapping" function + and return a read and write function that the wrapper will use to get/set data. + """ + try: + self.read_function, self.write_function = client.setup_attribute(self.comms_annotation, self) + except: + def pass_func(value=None): + pass + print("setting comm_client failed. using pass function instead") + + self.read_function = pass_func + self.write_function = pass_func + + diff --git a/SDP/SDP/clients/opcua_connection.py b/SDP/SDP/clients/opcua_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..35709f0b71cbde79db5cfe2daa5b992229989046 --- /dev/null +++ b/SDP/SDP/clients/opcua_connection.py @@ -0,0 +1,215 @@ +from comms_client import * + + +__all__ = ["OPCUAConnection"] + +OPCua_to_numpy_dict = { + "VariantType.Boolean": numpy.bool_, + "VariantType.SByte": numpy.int8, + "VariantType.Byte": numpy.uint8, + "VariantType.Int16": numpy.int16, + "VariantType.UInt16": numpy.uint16, + "VariantType.Int32": numpy.int32, + "VariantType.UInt32": numpy.uint32, + "VariantType.Int64": numpy.int64, + "VariantType.UInt64": numpy.uint64, + "VariantType.DateTime": numpy.datetime_data, # is this the right type, does it even matter? + "VariantType.Float": numpy.float32, + "VariantType.Double": numpy.double, + "VariantType.String": numpy.str, + "VariantType.ByteString": numpy.uint8 # sequence of bytes, not a string +} + +numpy_to_OPCua_dict = { + "<class 'numpy.bool_'>": opcua.ua.VariantType.Boolean, + "<class 'numpy.int8'>": opcua.ua.VariantType.SByte, + "<class 'numpy.uint8'>": opcua.ua.VariantType.Byte, + "<class 'numpy.int16'>": opcua.ua.VariantType.Int16, + "<class 'numpy.uint16'>": opcua.ua.VariantType.UInt16, + "<class 'numpy.int32'>": opcua.ua.VariantType.Int32, + "<class 'numpy.uint32'>": opcua.ua.VariantType.UInt32, + "<class 'numpy.int64'>": opcua.ua.VariantType.Int64, + "<class 'numpy.uint64'>": opcua.ua.VariantType.UInt64, + "<class 'numpy.datetime_data'>": opcua.ua.VariantType.DateTime, # is this the right type, does it even matter? + "<class 'numpy.float32'>": opcua.ua.VariantType.Float, + "<class 'numpy.float64'>": opcua.ua.VariantType.Double, + "<class 'numpy.double'>": opcua.ua.VariantType.Double, + "<class 'numpy.str_'>": opcua.ua.VariantType.String, + "<class 'numpy.str'>": opcua.ua.VariantType.String, + "str": opcua.ua.VariantType.String +} + +# <class 'numpy.bool_'> + +class OPCUAConnection(CommClient): + """ + Connects to OPC-UA in the foreground or background, and sends HELLO + messages to keep a check on the connection. On connection failure, reconnects once. + """ + + def start(self): + super().start() + + def __init__(self, address, timeout, on_func, fault_func, streams, try_interval=2): + """ + Create the OPC ua client and connect() to it and get the object node + """ + super().__init__(on_func, fault_func, streams, try_interval) + + self.client = Client(address, timeout) + + # Explicitly connect + if not self.connect(): + # hardware or infra is down -- needs fixing first + fault_func() + return + + self.streams.debug_stream("Demo ||\t connection established") + + # determine namespace used + try: + self.name_space_index = self.client.get_namespace_index("http://lofar.eu") + except Exception as e: + self.streams.warn_stream("Cannot determine the OPC-UA name space index. Will try and use the default = 2.") + self.name_space_index = 2 + + self.obj = self.client.get_objects_node() + + def _servername(self): + return self.client.server_url.geturl() + + def connect(self): + """ + Try to connect to the client + """ + + try: + self.streams.debug_stream("Connecting to server %s", self._servername()) + self.client.connect() + self.connected = True + self.streams.debug_stream("Connected to server. Initialising.") + return True + except socket.error as e: + self.streams.error_stream("Could not connect to server %s: %s", self._servername(), e) + return False + + def disconnect(self): + """ + disconnect from the client + """ + self.connected = False # always force a reconnect, regardless of a successful disconnect + + try: + self.client.disconnect() + except Exception as e: + self.streams.error_stream("Disconnect from OPC-UA server %s failed: %s", self._servername(), e) + + def ping(self): + """ + ping the client to make sure the connection with the client is still functional. + """ + try: + self.client.send_hello() + except Exception as e: + raise Exception("Lost connection to server %s: %s", self._servername(), e) + + def _setup_annotation(self, annotation): + """ + This class's Implementation of the get_mapping function. returns the read and write functions + """ + + if isinstance(annotation, dict): + # check if required path inarg is present + if annotation.get('path') is None: + AssertionError("OPC-ua mapping requires a path argument in the annotation") + + path = annotation.get("path") # required + ua_Type = annotation.get("ua_type") # optional, if excluded must be a build in python type + elif isinstance(annotation, list): + path = annotation + else: + TypeError("OPC-ua mapping requires either a list or dict with the path") + return + + #TODO exceptions + + try: + node = self.obj.get_child(path) + except Exception as e: + self.streams.error_stream("Could not get node: %s on server %s: %s", path, self._servername(), e) + raise Exception("Could not get node: %s on server %s: %s", path, self._servername(), e) + + return node + + def setup_value_conversion(self, attribute): + """ + gives the client access to the attribute_wrapper object in order to access all data it could potentially need. + the OPC ua read/write functions require the dimensionality and the type to be known + """ + + dim_x = attribute.dim_x + dim_y = attribute.dim_y + ua_type = numpy_to_OPCua_dict[str(attribute.numpy_type)] # convert the numpy type to a corresponding UA type + + return dim_x, dim_y, ua_type + + def setup_attribute(self, annotation, attribute): + """ + MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions + """ + + # process the annotation + node = self._setup_annotation(annotation) + + # get all the necessary data to set up the read/write functions from the attribute_wrapper + dim_x, dim_y, ua_type = self.setup_value_conversion(attribute) + + # configure and return the read/write functions + prot_attr = ProtocolAttribute(node, dim_x, dim_y, ua_type) + + node_name = str(node.get_browse_name())[len("QualifiedName(2:"):] + self.streams.debug_stream("connected OPC ua node {} of type {} to attribute with dimensions: {} x {} ".format(str(node_name)[:len(node_name)-1], str(ua_type)[len("VariantType."):], dim_x, dim_y)) + + # return the read/write functions + return prot_attr.read_function, prot_attr.write_function + + + + +class ProtocolAttribute: + """ + This class provides a small wrapper for the OPC ua read/write functions in order to better organise the code + """ + + def __init__(self, node, dim_x, dim_y, ua_type): + self.node = node + self.dim_y = dim_y + self.dim_x = dim_x + self.ua_type = ua_type + + def read_function(self): + """ + Read_R function + """ + value = numpy.array(self.node.get_value()) + + if self.dim_y != 0: + value = numpy.array(numpy.split(value, indices_or_sections=self.dim_y)) + else: + value = numpy.array(value) + return value + + def write_function(self, value): + """ + write_RW function + """ + # set_data_value(opcua.ua.uatypes.Variant(value = value.tolist(), varianttype=opcua.ua.VariantType.Int32)) + + if self.dim_y != 0: + v = numpy.concatenate(value) + self.node.set_data_value(opcua.ua.uatypes.Variant(value=v.tolist(), varianttype=self.ua_type)) + + elif self.dim_x != 1: + self.node.set_data_value(opcua.ua.uatypes.Variant(value=value.tolist(), varianttype=self.ua_type)) + else: + self.node.set_data_value(opcua.ua.uatypes.Variant(value=value, varianttype=self.ua_type)) diff --git a/SDP/SDP/comms_client.py b/SDP/SDP/comms_client.py new file mode 100644 index 0000000000000000000000000000000000000000..544df539d972cff9da37ac6a2dce00db27d76796 --- /dev/null +++ b/SDP/SDP/comms_client.py @@ -0,0 +1,114 @@ +from threading import Thread +import socket +import time +import numpy + +import opcua +from opcua import Client + +from tango import DevState + + +class CommClient(Thread): + """ + The ProtocolHandler class is the generic interface class between the tango attribute_wrapper and the outside world + """ + + def __init__(self, standby_func, fault_func, streams, try_interval=2): + """ + + """ + self.standby_func = standby_func + 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.standby_func() + + self.stopping = False + while not self.stopping: + # keep trying to connect + print("connected check") + if not self.connected: + print("not connected, try to connect") + if self.connect(): + print("connected now, call on_func") + self.standby_func() + 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): + pass + + 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. + + 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. + ''' + AssertionError("the setup_attribute must be implemented and provide return a valid read/write function for the attribute") + diff --git a/SDP/SDP/hardware_device.py b/SDP/SDP/hardware_device.py new file mode 100644 index 0000000000000000000000000000000000000000..6b665055cf35387ea3be4eebfb12f8928cfc6e4e --- /dev/null +++ b/SDP/SDP/hardware_device.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" PCC Device Server for LOFAR2.0 + +""" + +# PyTango imports +from tango import DebugIt +from tango.server import run +from tango.server import Device +from tango.server import device_property +from tango import DevState +# Additional import + +from clients.opcua_connection import OPCUAConnection +from attribute_wrapper import * + + +__all__ = ["hardware_device"] + +class hardware_device(Device): + """ + + **Properties:** + + States are as follows: + INIT = Device is initialising. + STANDBY = Device is initialised, but pends external configuration and an explicit turning on, + ON = Device is fully configured, functional, controls the hardware, and is possibly actively running, + FAULT = Device detected an unrecoverable error, and is thus malfunctional, + OFF = Device is turned off, drops connection to the hardware, + + The following state transitions are implemented: + boot -> OFF: Triggered by tango. Device will be instantiated, + OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), + INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, + STANDBY -> ON: Triggered by user. Device reports to be functional, + * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, + * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, + * -> OFF: Triggered by user. Device is turned off. Triggered by the Off() command, + FAULT -> INIT: Triggered by user. Device is reinitialised to recover from an error, + + The user triggers their transitions by the commands reflecting the target state (Initialise(), On(), Fault()). + """ + + @classmethod + def attr_list(cls): + """ Return a list of all the attribute_wrapper members of this class. """ + return [v for k, v in cls.__dict__.items() if type(v) == attribute_wrapper] + + def init_device(self): + """ Instantiates the device in the OFF state. """ + + # NOTE: Will delete_device first, if necessary + Device.init_device(self) + + self.set_state(DevState.OFF) + + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # Commands + # -------- + + @command() + @only_in_states([DevState.FAULT, DevState.OFF]) + @DebugIt() + def Initialise(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + self.set_state(DevState.INIT) + self.initialise() + + # if self.get_state() == DevState.STANDBY: + # # Already STANDBY. Don't complain. + # return + # self.set_state(DevState.STANDBY) + + @only_in_states([DevState.INIT]) + def Standby(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.standby() + self.set_state(DevState.STANDBY) + + @command() + @only_in_states([DevState.STANDBY]) + @DebugIt() + def On(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + self.on() + self.set_state(DevState.ON) + + @command() + @DebugIt() + def Off(self): + """ + Command to ask for shutdown of this device. + + :return:None + """ + if self.get_state() == DevState.OFF: + # Already off. Don't complain. + return + + # Turn off + self.set_state(DevState.OFF) + + self.off() + + # Turn off again, in case of race conditions through reconnecting + self.set_state(DevState.OFF) + + @command() + @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) + @DebugIt() + def Fault(self): + """ + FAULT state is used to indicate our connection with the OPC-UA server is down. + + This device will try to reconnect once, and transition to the ON state on success. + + If reconnecting fails, the user needs to call Initialise() to retry to restart this device. + + :return:None + """ + self.fault() + self.set_state(DevState.FAULT) + + + # functions that can be overloaded + def fault(self): + pass + def off(self): + pass + def on(self): + pass + def standby(self): + pass + def initialise(self): + pass + diff --git a/SDP/SDP/main.py b/SDP/SDP/main.py new file mode 100644 index 0000000000000000000000000000000000000000..6fae6c9e440b093c6d607cb53eb5509ec5b3fbe8 --- /dev/null +++ b/SDP/SDP/main.py @@ -0,0 +1,61 @@ +from tango.server import run +from tango.server import Device +from tango.server import attribute, command +from tango import AttrWriteType + +import devices.PCC +devices.PCC.main() + +# from attribute_wrapper import * +# +# import numpy +# # +# # class TangoDevice(Device): +# # def init_device(self): +# # Device.init_device(self) +# # self._my_attribute = 0.0 +# # +# # @attribute(label="stuff", unit="stuff_unit", dtype="DevDouble", doc="blah blah blah") +# # def my_attribute(self): +# # """some stuff""" +# # return 123456.123456 +# # +# # @my_attribute.write +# # def my_attribute(self, set_point): +# # self._my_attribute = set_point +# +# +# test_obj1 = attribute_wrapper(dtype="DevLong64", label="test1", max_dim_x=32, max_dim_y=3, access=AttrWriteType.READ_WRITE) +# print(test_obj1._read_RW()) +# test_obj1._write_RW(numpy.full((32, 3), 1)) +# +# test_obj2 = attribute_wrapper(dtype="DevDouble", label="test2", max_dim_x=32, max_dim_y=1, access=AttrWriteType.READ_WRITE) +# print(test_obj2._read_R()) +# test_obj2._write_RW(numpy.full((32, 1), 1.0)) +# +# test_obj3 = attribute_wrapper(dtype="DevString", label="test3", max_dim_x=1, max_dim_y=1, access=AttrWriteType.READ_WRITE) +# print(test_obj3._read_RW()) +# test_obj3._write_RW(numpy.full((1, 1), "test")) +# +# +# print(test_obj1._read_RW()) +# print(test_obj2._read_RW()) +# print(test_obj3._read_RW()) +# +# +# # def decorator(func): +# # def wrapper(arg1, arg2): +# # print("the decorator caught: ", arg1, arg2) +# # func(arg1, arg2) +# # return wrapper +# # +# # @decorator +# # def test(a, b): +# # print("") +# +# device_list = [] +# +# device_list.append(attribute_wrapper()) +# +# +# diff --git a/SDP/SDP/opcua_connection.py b/SDP/SDP/opcua_connection.py deleted file mode 100644 index cfcfb74ab00416b79bfc2ccc8fbf263372c9f80c..0000000000000000000000000000000000000000 --- a/SDP/SDP/opcua_connection.py +++ /dev/null @@ -1,84 +0,0 @@ -from threading import Thread -import socket -import time - -__all__ = ["OPCUAConnection"] - -class OPCUAConnection(Thread): - """ - Connects to OPC-UA in the foreground or background, and sends HELLO - messages to keep a check on the connection. On connection failure, reconnects once. - """ - - def __init__(self, client, on_func, fault_func, streams, try_interval=2): - super().__init__(daemon=True) - - self.client = client - self.on_func = on_func - self.fault_func = fault_func - self.try_interval = try_interval - self.streams = streams - self.stopping = False - self.connected = False - - def _servername(self): - return self.client.server_url.geturl() - - def connect(self): - try: - self.streams.debug_stream("Connecting to server %s", self._servername()) - self.client.connect() - self.connected = True - self.streams.debug_stream("Connected to server. Initialising.") - return True - except socket.error as e: - self.streams.error_stream("Could not connect to server %s: %s", self._servername(), e) - return False - - def disconnect(self): - self.connected = False # always force a reconnect, regardless of a successful disconnect - - try: - self.client.disconnect() - except Exception as e: - self.streams.error_stream("Disconnect from OPC-UA server %s failed: %s", self._servername(), e) - - def run(self): - while not self.stopping: - # keep trying to connect - if not self.connected: - if self.connect(): - self.on_func() - 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.client.send_hello() - time.sleep(self.try_interval) - except Exception as e: - self.streams.error_stream("Lost connection to server %s: %s", self._servername(), 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 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()