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
+}