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