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..b98336472165d390a050a63c95d8dfb91a67be5a --- /dev/null +++ b/SDP/SDP/SDP.py @@ -0,0 +1,319 @@ +# -*- 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:'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='DevString', + mandatory=True + ) + + OPC_Server_Port = device_property( + dtype='DevULong', + mandatory=True + ) + + OPC_Time_Out = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + + SDP_mask_RW = attribute( + dtype = ('DevBoolean',), + max_dim_x = 32, + access=AttrWriteType.READ_WRITE, + ) + + fpga_temp_R = attribute( + dtype = ('DevDouble',), + max_dim_x = 4, + ) + + + + # --------------- + # General methods + # --------------- + + def get_sdp_node(self, node): + try: + return self.sdp_node.get_child(["{}:{}".format(self.name_space_index, node)]) + except opcua.ua.uaerrors._auto.BadNoMatch: + self.error_stream("Could not find SDP 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.sdp_node = self.obj_node.get_child(["{}:SDP".format(self.name_space_index)]) + + self.debug_stream("Mapping OPC-UA MP/CP to attributes...") + + self.attribute_mapping["SDP_mask_RW"] = self.get_pcc_node("SDP_mask_RW") + self.attribute_mapping["fpga_temp_R"] = self.get_pcc_node("fpga_temp_R") + + 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 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._SDP_mask_RW = numpy.full(32, False) + self.attribute_mapping["SDP_mask_RW"] = {} + self._fpga_temp_R = numpy.full(4, False) + self.attribute_mapping["fpga_temp_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_SDP_mask_RW(self._SDP_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_SDP_mask_RW(self): + """Return the SDP_mask_RW attribute.""" + return self._SDP_mask_RW + + @only_when_on + @fault_on_error + def write_SDP_mask_RW(self, value): + """Set the SDP_mask_RW attribute.""" + self.attribute_mapping["SDP_mask_RW"].set_value(value.tolist()) + self._SDP_mask_RW = value + + @only_when_on + @fault_on_error + def read_fpga_temp_R(self): + """Return the fpga_temp_R attribute.""" + value = numpy.array(self.attribute_mapping["fpga_temp_R"].get_value()) + self._fpga_temp_R = numpy.array(numpy.split(value, indices_or_sections = 4)) + return self._fpga_temp_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..65c33ca7beb896c0f77e9acea2ad3dc311c33cd2 --- /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='jurges', + author_email='jurges at astron.nl', + license='APACHE', + long_description=long_description, + url='www.tango-controls.org', + platforms="Unix Like" + )