diff --git a/PCC/LICENSE.txt b/PCC/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae533fce6dc75595f91290511273c7ff62312f76 --- /dev/null +++ b/PCC/LICENSE.txt @@ -0,0 +1,202 @@ + 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 2020 Stichting Nederlandse Wetenschappelijk Onderzoek Instituten, +ASTRON Netherlands Institute for Radio Astronomy + + 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/PCC/MANIFEST.in b/PCC/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..251eebae288846436b6dc7b6022214cda5af98af --- /dev/null +++ b/PCC/MANIFEST.in @@ -0,0 +1,9 @@ +recursive-include PCC *.py +recursive-include test *.py +include *.rst +include PCC.xmi +include *.txt +graft docs + +global-exclude *.pyc +global-exclude *.pyo diff --git a/PCC/NOTICE b/PCC/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..9c7867598e17de5d69b8c26656caa8316cd0a30f --- /dev/null +++ b/PCC/NOTICE @@ -0,0 +1,8 @@ +Citation Notice version 1.0 +This Citation Notice is part of the LOFAR software suite. +Parties that use ASTRON Software resulting in papers and/or publications are requested to +refer to the DOI(s) that correspond(s) to the version(s) of the ASTRON Software used: +<List of DOIs> +Parties that use ASTRON Software for purposes that do not result in publications (e.g. +commercial parties) are asked to inform ASTRON about their use of ASTRON Software, by +sending an email to including the DOIs mentioned above in the message. \ No newline at end of file diff --git a/PCC/PCC/PCC.py b/PCC/PCC/PCC.py new file mode 100644 index 0000000000000000000000000000000000000000..ce4de28c8c0f5f0e45dfe006ecc46e86f5d4a49b --- /dev/null +++ b/PCC/PCC/PCC.py @@ -0,0 +1,886 @@ +# -*- 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_state_R = attribute( + dtype = (numpy.str), + ) + + 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, + ) + + uC_ID_R = attribute( + dtype=(numpy.int64,), + max_dim_x=32, + ) + + 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_state_R"] = self.get_pcc_node("RCU_state_R") + + 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["uC_ID_R"] = self.get_pcc_node("uC_ID _R") + + 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_state_R = "" + self.attribute_mapping["RCU_state_R"] = {} + + 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._uC_ID_R = numpy.full(32, 0) + self.attribute_mapping["uC_ID_R"] = {} + + 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_state_R(self): + """Return the RCU_state_R attribute.""" + self._RCU_state_R = self.attribute_mapping["RCU_state_R"].get_value() + return self._RCU_state_R + + @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_uC_ID_R(self): + """Return the uC_ID_R attribute.""" + self._uC_ID_R = numpy.array(self.attribute_mapping["uC_ID_R"].get_value()) + return self._uC_ID_R + + @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() diff --git a/PCC/PCC/__init__.py b/PCC/PCC/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f46dac89c5c6fb96b76b166b6d8c2be540b19e0d --- /dev/null +++ b/PCC/PCC/__init__.py @@ -0,0 +1,19 @@ +# -*- 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 + +""" + +from . import release +from .PCC import PCC, main + +__version__ = release.version +__version_info__ = release.version_info +__author__ = release.author diff --git a/PCC/PCC/__main__.py b/PCC/PCC/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..52b1fda83b2b1f2f8d9dbae05f114527555136ce --- /dev/null +++ b/PCC/PCC/__main__.py @@ -0,0 +1,11 @@ +# -*- 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. + +from PCC import main +main() diff --git a/PCC/PCC/opcua_connection.py b/PCC/PCC/opcua_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..cfcfb74ab00416b79bfc2ccc8fbf263372c9f80c --- /dev/null +++ b/PCC/PCC/opcua_connection.py @@ -0,0 +1,84 @@ +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/PCC/release.py b/PCC/PCC/release.py new file mode 100644 index 0000000000000000000000000000000000000000..5c73788786197b9f66c7b94dfb27cb747be6776d --- /dev/null +++ b/PCC/PCC/release.py @@ -0,0 +1,20 @@ +# -*- 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. + +"""Release information for Python Package""" + +name = """tangods-PCC""" +version = "1.0.0" +version_info = version.split(".") +description = """""" +author = "Thomas Juerges" +author_email = "jurges at astron.nl" +license = """APACHE""" +url = """https://git.astron.nl/lofar2.0/tango.git""" +copyright = """""" diff --git a/PCC/PCC/wrappers.py b/PCC/PCC/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..9dbc45a68dc850b36bd30a0a5b8664d104b58e30 --- /dev/null +++ b/PCC/PCC/wrappers.py @@ -0,0 +1,53 @@ +from tango import DevState, Except +from functools import wraps +import traceback + +__all__ = ["only_in_states", "only_when_on", "fault_on_error"] + +def only_in_states(allowed_states): + """ + Wrapper to call and return the wrapped function if the device is + in one of the given states. Otherwise a PyTango exception is thrown. + """ + def wrapper(func): + @wraps(func) + def state_check_wrapper(self, *args, **kwargs): + if self.get_state() in allowed_states: + return func(self, *args, **kwargs) + + self.warn_stream("Illegal command: Function %s can only be called in states %s. Current state: %s" % (func.__name__, allowed_states, self.get_state())) + Except.throw_exception("IllegalCommand", "Function can only be called in states %s. Current state: %s" % (allowed_states, self.get_state()), func.__name__) + + return state_check_wrapper + + return wrapper + +def only_when_on(func): + """ + Wrapper to call and return the wrapped function if the device is + in the ON state. Otherwise None is returned and nothing + will be called. + """ + @wraps(func) + def when_on_wrapper(self, *args, **kwargs): + if self.get_state() == DevState.ON: + return func(self, *args, **kwargs) + + return None + + return when_on_wrapper + +def fault_on_error(func): + """ + Wrapper to catch exceptions. Sets the device in a FAULT state if any occurs. + """ + @wraps(func) + def error_wrapper(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except Exception as e: + self.error_stream("Function failed. Trace: %s", traceback.format_exc()) + self.Fault() + return None + + return error_wrapper diff --git a/PCC/README.rst b/PCC/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..9d42e957e28bd5e452349ec56c00dab61a4def46 --- /dev/null +++ b/PCC/README.rst @@ -0,0 +1,25 @@ +## PCC Device Server for LOFAR2.0 + + +## Requirement + +- PyTango >= 8.1.6 +- devicetest (for using tests) +- sphinx (for building sphinx documentation) + +## Installation + +Run python setup.py install + +If you want to build sphinx documentation, +run python setup.py build_sphinx + +If you want to pass the tests, +run python setup.py test + +## Usage + +Now you can start your device server in any +Terminal or console by calling it : + +PCC instance_name diff --git a/PCC/requirements.txt b/PCC/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3b3b3b08bb560cfbe4fed2e7c7a0241f02f0af24 --- /dev/null +++ b/PCC/requirements.txt @@ -0,0 +1 @@ +opcua >= 0.98.9 diff --git a/PCC/setup.py b/PCC/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..0d197078aa73c4adfd461e86f94bdc5b90554629 --- /dev/null +++ b/PCC/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- 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. + +import os +import sys +from setuptools import setup + +setup_dir = os.path.dirname(os.path.abspath(__file__)) + +# make sure we use latest info from local code +sys.path.insert(0, setup_dir) + +readme_filename = os.path.join(setup_dir, 'README.rst') +with open(readme_filename) as file: + long_description = file.read() + +release_filename = os.path.join(setup_dir, 'PCC', 'release.py') +exec(open(release_filename).read()) + +pack = ['PCC'] + +setup(name=name, + version=version, + description='', + packages=pack, + include_package_data=True, + test_suite="test", + entry_points={'console_scripts':['PCC = PCC:main']}, + author='Thomas Juerges', + author_email='jurges at astron.nl', + license='APACHE', + long_description=long_description, + url='https://git.astron.nl/lofar2.0/tango.git', + platforms="Unix Like" + ) diff --git a/PCC/test/PCC_test.py b/PCC/test/PCC_test.py new file mode 100644 index 0000000000000000000000000000000000000000..37b80b7e27125b85765da7f7b29e261f32eef1f6 --- /dev/null +++ b/PCC/test/PCC_test.py @@ -0,0 +1,89 @@ +######################################################################################### +# -*- 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. +######################################################################################### +"""Contain the tests for the RCU-SCC Device Server for LOFAR2.0.""" + +# Path +import sys +import os +path = os.path.join(os.path.dirname(__file__), os.pardir) +sys.path.insert(0, os.path.abspath(path)) + +# Imports +import pytest +from mock import MagicMock + +from PyTango import DevState + +# PROTECTED REGION ID(PCC.test_additional_imports) ENABLED START # +# PROTECTED REGION END # // PCC.test_additional_imports + + +# Device test case +@pytest.mark.usefixtures("tango_context", "initialize_device") +# PROTECTED REGION ID(PCC.test_PCC_decorators) ENABLED START # +# PROTECTED REGION END # // PCC.test_PCC_decorators +class TestPCC(object): + """Test case for packet generation.""" + + properties = { + 'OPC_Server_Name': '', + 'OPC_Server_Port': '', + 'OPC_Time_Out': '', + } + + @classmethod + def mocking(cls): + """Mock external libraries.""" + # Example : Mock numpy + # cls.numpy = PCC.numpy = MagicMock() + # PROTECTED REGION ID(PCC.test_mocking) ENABLED START # + # PROTECTED REGION END # // PCC.test_mocking + + def test_properties(self, tango_context): + # Test the properties + # PROTECTED REGION ID(PCC.test_properties) ENABLED START # + # PROTECTED REGION END # // PCC.test_properties + pass + + # PROTECTED REGION ID(PCC.test_State_decorators) ENABLED START # + # PROTECTED REGION END # // PCC.test_State_decorators + def test_State(self, tango_context): + """Test for State""" + # PROTECTED REGION ID(PCC.test_State) ENABLED START # + assert tango_context.device.State() == DevState.UNKNOWN + # PROTECTED REGION END # // PCC.test_State + + # PROTECTED REGION ID(PCC.test_Status_decorators) ENABLED START # + # PROTECTED REGION END # // PCC.test_Status_decorators + def test_Status(self, tango_context): + """Test for Status""" + # PROTECTED REGION ID(PCC.test_Status) ENABLED START # + assert tango_context.device.Status() == "The device is in UNKNOWN state." + # PROTECTED REGION END # // PCC.test_Status + + + # PROTECTED REGION ID(PCC.test_time_offset_rw_decorators) ENABLED START # + # PROTECTED REGION END # // PCC.test_time_offset_rw_decorators + def test_time_offset_rw(self, tango_context): + """Test for time_offset_rw""" + # PROTECTED REGION ID(PCC.test_time_offset_rw) ENABLED START # + assert tango_context.device.time_offset_rw == 0 + # PROTECTED REGION END # // PCC.test_time_offset_rw + + # PROTECTED REGION ID(PCC.test_time_offset_r_decorators) ENABLED START # + # PROTECTED REGION END # // PCC.test_time_offset_r_decorators + def test_time_offset_r(self, tango_context): + """Test for time_offset_r""" + # PROTECTED REGION ID(PCC.test_time_offset_r) ENABLED START # + assert tango_context.device.time_offset_r == 0 + # PROTECTED REGION END # // PCC.test_time_offset_r + + diff --git a/PCC/test/__init__.py b/PCC/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/PCC/test/test-PCC.py b/PCC/test/test-PCC.py new file mode 100644 index 0000000000000000000000000000000000000000..d151a5d31203690883b24de7916470d8acc8a653 --- /dev/null +++ b/PCC/test/test-PCC.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python3 + +import opcua +from time import sleep + +port = 4840 +host = "10.87.2.8" + +client = opcua.Client("opc.tcp://{}:{}".format(host, port)) +client.connect() +obj = client.get_objects_node() +name_space_index = 2 +time_offset = obj.get_child("{}:time_offset".format(name_space_index)) +time_offset_R = time_offset.get_child("{}:time_offset_R".format(name_space_index)) +time_offset_RW = time_offset.get_child("{}:time_offset_RW".format(name_space_index)) +old_time_offset = time_offset_R.get_value() +target_time_offset = 1 +new_time_offset = old_time_offset + target_time_offset +time_offset_RW.set_value(new_time_offset) +sleep(1.0) +latest_time_offset = time_offset_R.get_value() +difference_time_offset = latest_time_offset - old_time_offset +if difference_time_offset != target_time_offset: + print("ERROR: Setting and reading back time_offset. old_time_offset = %d, new_time_offset = %d, latest_time_offset = %d, target_time_offset = %d, difference_time_offset = %d." % (old_time_offset, new_time_offset, latest_time_offset, target_time_offset, difference_time_offset)) +else: + print("SUCCESS: Setting and reading back time_offset.") +time_offset_RW.set_value(old_time_offset) +client.disconnect() diff --git a/SDP/LICENSE.txt b/SDP/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae533fce6dc75595f91290511273c7ff62312f76 --- /dev/null +++ b/SDP/LICENSE.txt @@ -0,0 +1,202 @@ + 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 2020 Stichting Nederlandse Wetenschappelijk Onderzoek Instituten, +ASTRON Netherlands Institute for Radio Astronomy + + 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/MANIFEST.in b/SDP/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..d9d9aaba41c43f633c9a02635e72b4eb1b791f50 --- /dev/null +++ b/SDP/MANIFEST.in @@ -0,0 +1,9 @@ +recursive-include RCUSCC *.py +recursive-include test *.py +include *.rst +include RCUSCC.xmi +include *.txt +graft docs + +global-exclude *.pyc +global-exclude *.pyo diff --git a/SDP/NOTICE b/SDP/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..9c7867598e17de5d69b8c26656caa8316cd0a30f --- /dev/null +++ b/SDP/NOTICE @@ -0,0 +1,8 @@ +Citation Notice version 1.0 +This Citation Notice is part of the LOFAR software suite. +Parties that use ASTRON Software resulting in papers and/or publications are requested to +refer to the DOI(s) that correspond(s) to the version(s) of the ASTRON Software used: +<List of DOIs> +Parties that use ASTRON Software for purposes that do not result in publications (e.g. +commercial parties) are asked to inform ASTRON about their use of ASTRON Software, by +sending an email to including the DOIs mentioned above in the message. \ No newline at end of file diff --git a/SDP/README.rst b/SDP/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..aafea1e3022ae9546fd62e1a2b1474dfc1bbef6a --- /dev/null +++ b/SDP/README.rst @@ -0,0 +1,25 @@ +## SDP Device Server for LOFAR2.0 + + +## Requirement + +- PyTango >= 8.1.6 +- devicetest (for using tests) +- sphinx (for building sphinx documentation) + +## Installation + +Run python setup.py install + +If you want to build sphinx documentation, +run python setup.py build_sphinx + +If you want to pass the tests, +run python setup.py test + +## Usage + +Now you can start your device server in any +Terminal or console by calling it : + +SDP instance_name diff --git a/SDP/SDP/SDP.py b/SDP/SDP/SDP.py new file mode 100644 index 0000000000000000000000000000000000000000..ee98bab36fa7cef38131f8c607bc46cbfeb652ac --- /dev/null +++ b/SDP/SDP/SDP.py @@ -0,0 +1,488 @@ +# -*- 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() diff --git a/SDP/SDP/__init__.py b/SDP/SDP/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cf68b03729a1a4d562556c9765b9e7389fd49b18 --- /dev/null +++ b/SDP/SDP/__init__.py @@ -0,0 +1,19 @@ +# -*- 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 + +""" + +from . import release +from .SDP import SDP, main + +__version__ = release.version +__version_info__ = release.version_info +__author__ = release.author diff --git a/SDP/SDP/__main__.py b/SDP/SDP/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..5ef0710c551b94138cfbe4b1c762af830dae9a62 --- /dev/null +++ b/SDP/SDP/__main__.py @@ -0,0 +1,11 @@ +# -*- 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. + +from SDP import main +main() diff --git a/SDP/SDP/opcua_connection.py b/SDP/SDP/opcua_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..cfcfb74ab00416b79bfc2ccc8fbf263372c9f80c --- /dev/null +++ b/SDP/SDP/opcua_connection.py @@ -0,0 +1,84 @@ +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/SDP/SDP/release.py b/SDP/SDP/release.py new file mode 100644 index 0000000000000000000000000000000000000000..74a9dd436a73d6acd8d9c7918c63dfc95b49ca09 --- /dev/null +++ b/SDP/SDP/release.py @@ -0,0 +1,20 @@ +# -*- 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. + +"""Release information for Python Package""" + +name = """tangods-sdp""" +version = "1.0.0" +version_info = version.split(".") +description = """""" +author = "Thomas Juerges" +author_email = "jurges at astron.nl" +license = """APACHE""" +url = """https://git.astron.nl/lofar2.0/tango.git""" +copyright = """""" diff --git a/SDP/SDP/wrappers.py b/SDP/SDP/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..9dbc45a68dc850b36bd30a0a5b8664d104b58e30 --- /dev/null +++ b/SDP/SDP/wrappers.py @@ -0,0 +1,53 @@ +from tango import DevState, Except +from functools import wraps +import traceback + +__all__ = ["only_in_states", "only_when_on", "fault_on_error"] + +def only_in_states(allowed_states): + """ + Wrapper to call and return the wrapped function if the device is + in one of the given states. Otherwise a PyTango exception is thrown. + """ + def wrapper(func): + @wraps(func) + def state_check_wrapper(self, *args, **kwargs): + if self.get_state() in allowed_states: + return func(self, *args, **kwargs) + + self.warn_stream("Illegal command: Function %s can only be called in states %s. Current state: %s" % (func.__name__, allowed_states, self.get_state())) + Except.throw_exception("IllegalCommand", "Function can only be called in states %s. Current state: %s" % (allowed_states, self.get_state()), func.__name__) + + return state_check_wrapper + + return wrapper + +def only_when_on(func): + """ + Wrapper to call and return the wrapped function if the device is + in the ON state. Otherwise None is returned and nothing + will be called. + """ + @wraps(func) + def when_on_wrapper(self, *args, **kwargs): + if self.get_state() == DevState.ON: + return func(self, *args, **kwargs) + + return None + + return when_on_wrapper + +def fault_on_error(func): + """ + Wrapper to catch exceptions. Sets the device in a FAULT state if any occurs. + """ + @wraps(func) + def error_wrapper(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except Exception as e: + self.error_stream("Function failed. Trace: %s", traceback.format_exc()) + self.Fault() + return None + + return error_wrapper diff --git a/SDP/requirements.txt b/SDP/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a0195dd42b98b0f3194e55e91cded17608ed6ee3 --- /dev/null +++ b/SDP/requirements.txt @@ -0,0 +1,2 @@ +opcua >= 0.98.9 +numpy diff --git a/SDP/setup.py b/SDP/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..8def2ec90e9cf446fc680fe7740f2c18aaa19975 --- /dev/null +++ b/SDP/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- 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. + +import os +import sys +from setuptools import setup + +setup_dir = os.path.dirname(os.path.abspath(__file__)) + +# make sure we use latest info from local code +sys.path.insert(0, setup_dir) + +readme_filename = os.path.join(setup_dir, 'README.rst') +with open(readme_filename) as file: + long_description = file.read() + +release_filename = os.path.join(setup_dir, 'SDP', 'release.py') +exec(open(release_filename).read()) + +pack = ['SDP'] + +setup(name=name, + version=version, + description='', + packages=pack, + include_package_data=True, + test_suite="test", + entry_points={'console_scripts':['SDP = SDP:main']}, + author='Thomas Juerges', + author_email='jurges at astron.nl', + license='APACHE', + long_description=long_description, + url='https://git.astron.nl/lofar2.0/tango.git', + platforms="Unix Like" + ) diff --git a/docker-compose/Makefile b/docker-compose/Makefile index 6c2e767c8dfcac11cd1fb1d54f035e7b73b1f48a..65de8951a33821b2f06c3b530b11b2f3c1fbc355 100644 --- a/docker-compose/Makefile +++ b/docker-compose/Makefile @@ -92,6 +92,8 @@ pull: ## pull the images from the Docker hub $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) pull build: ## rebuild images + # docker-compose does not support build dependencies, so manage those here + $(DOCKER_COMPOSE_ARGS) docker-compose -f lofar-device-base.yml build $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build up: minimal ## start the base TANGO system and prepare all services @@ -122,6 +124,9 @@ attach: ## attach a service to an existing Tango network status: ## show the container status $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps +images: ## show the container images + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) images + clean: down ## clear all TANGO database entries docker volume rm $(BASEDIR)_tangodb diff --git a/docker-compose/device-pcc.yml b/docker-compose/device-pcc.yml new file mode 100644 index 0000000000000000000000000000000000000000..96eb6901583216b69035b1d2e906a2c1744e6644 --- /dev/null +++ b/docker-compose/device-pcc.yml @@ -0,0 +1,31 @@ +# +# Docker compose file that launches an interactive iTango session. +# +# Connect to the interactive session with 'docker attach itango'. +# Disconnect with the Docker deattach sequence: <CTRL>+<P> <CTRL>+<Q> +# +# Defines: +# - itango: iTango interactive session +# +# Requires: +# - lofar-device-base.yml +# +version: '2' + +services: + device-pcc: + image: lofar-device-base + container_name: ${CONTAINER_NAME_PREFIX}device-pcc + network_mode: ${NETWORK_MODE} + volumes: + - ${TANGO_LOFAR_CONTAINER_MOUNT} + environment: + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - python3 -u ${TANGO_LOFAR_CONTAINER_DIR}/PCC/PCC LTS -v + restart: on-failure diff --git a/docker-compose/device-sdp.yml b/docker-compose/device-sdp.yml new file mode 100644 index 0000000000000000000000000000000000000000..8fbc3cbc536360658446d2f21c57231d29b8d6ca --- /dev/null +++ b/docker-compose/device-sdp.yml @@ -0,0 +1,31 @@ +# +# Docker compose file that launches an interactive iTango session. +# +# Connect to the interactive session with 'docker attach itango'. +# Disconnect with the Docker deattach sequence: <CTRL>+<P> <CTRL>+<Q> +# +# Defines: +# - itango: iTango interactive session +# +# Requires: +# - lofar-device-base.yml +# +version: '2' + +services: + device-sdp: + image: lofar-device-base + container_name: ${CONTAINER_NAME_PREFIX}device-sdp + network_mode: ${NETWORK_MODE} + volumes: + - ${TANGO_LOFAR_CONTAINER_MOUNT} + environment: + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - python3 -u ${TANGO_LOFAR_CONTAINER_DIR}/SDP/SDP LTS -v + restart: on-failure diff --git a/docker-compose/itango.yml b/docker-compose/itango.yml index ef825b5c20526f2db03e8d03f26bbdf5c145ee5c..d131d405371bc6ff7e59892587b2bd2aba9d1fd6 100644 --- a/docker-compose/itango.yml +++ b/docker-compose/itango.yml @@ -37,3 +37,4 @@ services: - --strict - -- - /venv/bin/itango3 + restart: on-failure diff --git a/docker-compose/jupyter.yml b/docker-compose/jupyter.yml index 7f0011446935a0c49cef406f5addb870d3fad515..f1970916f7e9facc25908c08c63f0938be0c4eb7 100644 --- a/docker-compose/jupyter.yml +++ b/docker-compose/jupyter.yml @@ -18,6 +18,7 @@ services: volumes: - ${TANGO_SKA_CONTAINER_MOUNT} - ${TANGO_LOFAR_CONTAINER_MOUNT} + - ${TANGO_LOFAR_LOCAL_DIR}/jupyter-notebooks:/jupyter-notebooks:rw - ${HOME}:/hosthome environment: - TANGO_HOST=${TANGO_HOST} @@ -25,7 +26,7 @@ services: - DISPLAY=${DISPLAY} ports: - "8888:8888" - working_dir: ${TANGO_LOFAR_CONTAINER_DIR}/jupyter-notebooks + working_dir: /jupyter-notebooks entrypoint: - /usr/local/bin/wait-for-it.sh - ${TANGO_HOST} @@ -33,3 +34,4 @@ services: - --strict - -- - /usr/bin/tini -- jupyter notebook --port=8888 --no-browser --ip=0.0.0.0 --allow-root --NotebookApp.token= --NotebookApp.password= + restart: on-failure diff --git a/docker-compose/lofar-device-base.yml b/docker-compose/lofar-device-base.yml new file mode 100644 index 0000000000000000000000000000000000000000..3d40f0f63554222907e6094bc61f1342cc8fabfa --- /dev/null +++ b/docker-compose/lofar-device-base.yml @@ -0,0 +1,21 @@ +# +# Docker compose file that forms the basis for LOFAR tango devices +# +# This is an abstract image that does not need to be spinned up, but +# might be out of consistency with other images. +# +# Defines: +# - device-base: Base configuration for devices. +# +# Requires: +# - tango.yml +# +version: '2' + +services: + lofar-device-base: + image: lofar-device-base + build: + context: lofar-device-base + container_name: ${CONTAINER_NAME_PREFIX}lofar-device-base + network_mode: ${NETWORK_MODE} diff --git a/docker-compose/lofar-device-base/Dockerfile b/docker-compose/lofar-device-base/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..cd6b59511f7e0a3e515ab282775edeadec6b9aaf --- /dev/null +++ b/docker-compose/lofar-device-base/Dockerfile @@ -0,0 +1,4 @@ +FROM nexus.engageska-portugal.pt/ska-docker/tango-itango:latest + +COPY lofar-requirements.txt /lofar-requirements.txt +RUN pip3 install -r /lofar-requirements.txt diff --git a/docker-compose/lofar-device-base/lofar-requirements.txt b/docker-compose/lofar-device-base/lofar-requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..90d21efe0e5ac0601204d4f05ce7efcd16dca2de --- /dev/null +++ b/docker-compose/lofar-device-base/lofar-requirements.txt @@ -0,0 +1,2 @@ +opcua >= 0.98.9 +astropy diff --git a/docker-compose/pypcc-sim.yml b/docker-compose/pypcc-sim.yml index bb6d519f2e98a3beb8bc34711d44f16f8aad29ce..d1c47c0465affd6b7bcb2de27dfa340c48e681ce 100644 --- a/docker-compose/pypcc-sim.yml +++ b/docker-compose/pypcc-sim.yml @@ -16,3 +16,4 @@ services: - ${HOME}:/hosthome ports: - "4842:4842" + restart: on-failure diff --git a/jupyter-notebooks/Start All Devices.ipynb b/jupyter-notebooks/Start All Devices.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..beb52a381c89a4cda30b08374d36c337def29eae --- /dev/null +++ b/jupyter-notebooks/Start All Devices.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "id": "social-massachusetts", + "metadata": {}, + "outputs": [], + "source": [ + "def force_start(device):\n", + " if device.state() == DevState.FAULT:\n", + " device.Off()\n", + " if device.state() == DevState.OFF:\n", + " device.initialise()\n", + " if device.state() == DevState.INIT:\n", + " device.Standby()\n", + " if device.state() == DevState.STANDBY:\n", + " device.On()\n", + " \n", + " return device.state()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "defined-apache", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Device PCC(lts/pcc/1) is now in state FAULT\n", + "Device SDP(lts/sdp/1) is now in state ON\n" + ] + } + ], + "source": [ + "for d in devices:\n", + " print(\"Device %s is now in state %s\" % (d, force_start(d)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "superior-wheel", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "StationControl", + "language": "python", + "name": "stationcontrol" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}