diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 16240653ae827112f531a2df361948472a9ffc46..e88c625fd80ec7de203f736942946347bcd450f6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,15 +1,14 @@
 default:
-  image: python:3.7 # minimum supported version
+  image: $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA # minimum supported version
   # Make sure each step is executed in a virtual environment with some basic dependencies present
   before_script:
     - python --version # For debugging
-    - python -m pip install --upgrade pip
-    - pip install --upgrade tox
   cache:
     paths:
       - .cache/pip
 
 stages:
+  - image
   - lint
   - test
   - package
@@ -20,6 +19,19 @@ stages:
 variables:
   PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
 
+build_test_image_python37:
+  stage: image
+  image: docker
+  services:
+    - name: docker:dind
+  variables:
+    DOCKER_TLS_CERTDIR: "/certs"
+  before_script:
+    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+  script:
+    - docker build -t $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA -f docker/Dockerfile.ci_python37 docker
+    - docker push $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA
+
 run_black:
   stage: lint
   script:
@@ -46,24 +58,37 @@ run_unit_tests_py37:
     - echo "run python3.7 unit tests /w coverage"
     - tox -e py37
 
-run_unit_tests_py38:
-  image: python:3.8
+.run_unit_tests_pyXX:
+  # installs the prerequisites explicitly, instead of piggy backing
+  # on the ci_python37 image. This allows us to use different base
+  # images with different python versions.
   stage: test
+  before_script:
+    - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev # Needed to install pytango
+    - python -m pip install --upgrade pip
+    - pip install --upgrade tox
+
+run_unit_tests_py38:
+  extends: .run_unit_tests_pyXX
+  image: python:3.8-buster
   script:
     - echo "run python3.8 unit tests /w coverage"
     - tox -e py38
 
 run_unit_tests_py39:
-  image: python:3.9
-  stage: test
+  extends: .run_unit_tests_pyXX
+  image: python:3.9-bullseye
   script:
     - echo "run python3.9 unit tests /w coverage"
     - tox -e py39
 
 run_unit_tests_py310:
-  image: python:3.10
-  stage: test
+  extends: .run_unit_tests_pyXX
+  image: python:3.10-bullseye
   script:
+    # Debian Bullseye ships with libboost-python linked to Python 3.9. Use the one from Debian Sid instead.
+    - echo 'deb http://deb.debian.org/debian sid main' >> /etc/apt/sources.list
+    - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python1.74-dev
     - echo "run python3.10 unit tests /w coverage"
     - tox -e py310
 
diff --git a/README.md b/README.md
index c6d9616b973d3f34fc5d8adf793d48a6d21331a4..32deb0e52f8e4f1770501e3bb6465af6cd68da12 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,32 @@ Client library for using Tango Station Control.
 
 Table of Contents:
 
+- [Prerequisites](#prerequisites)
 - [Installation](#installation)
 - [Usage](#usage)
 - [Development](#development)
 - [Debug](#debug)
 - [Releasenotes](#releasenotes)
 
+##  Prerequisites
+
+This package uses [PyTango](https://pytango.readthedocs.io), which wraps the Tango C++ library. You will need to install the Tango C++ headers & library to allow `pip` to install PyTango as part of this package's requirements:
+
+Debian and Ubuntu provide these natively:
+
+```shell
+apt-get install libtango-dev
+```
+
+Under Arch, install the [tango-cpp AUR package](https://aur.archlinux.org/packages/tango-cpp). For other distros and installation methods, see [Tango's Linux installation manual](https://tango-controls.readthedocs.io/en/latest/installation/tango-on-linux.html).
+
+You will also need the Boost Python C++ headers & libraries to compile PyTango. Under Debian/Ubuntu, obtain these through:
+
+```shell
+apt-get install libboost-python-dev
+```
+
+
 ##  Installation
 
 Wheel distributions are available from the [gitlab package registry](https://git.astron.nl/lofar2.0/lofar-station-client/-/packages/),
@@ -86,6 +106,9 @@ tox -e debug tests.requests.test_prometheus
 
 ## Releasenotes
 
+- 0.9  - Added `devices.LofarDeviceProxy` that transparently exposes >2D attributes
+- 0.8  - Fixed XST packet parsing.
+- 0.7. - Partial rework of DTS outside code, still many possible improvements.
 - 0.6. - Correctly transpose XST blocks in `XSTCollector`.
 - 0.5. - Swapped [x][y] for [y][x] dimensionality in `get_attribute_history`
 - 0.4. - Added collectors including `XSTCollector`, `BSTCollector` and `SSTCollecotr`
diff --git a/VERSION b/VERSION
index 5a2a5806df6e909afe3609b5706cb1012913ca0e..ac39a106c48515b621e90c028ed94c6f71bc03fa 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.6
+0.9.0
diff --git a/docker/Dockerfile.ci_python37 b/docker/Dockerfile.ci_python37
new file mode 100644
index 0000000000000000000000000000000000000000..1f4362fcff091ce1ba35661777eeb3e78c2a8b98
--- /dev/null
+++ b/docker/Dockerfile.ci_python37
@@ -0,0 +1,9 @@
+FROM python:3.7-buster
+
+# Install PyTango dependencies
+RUN apt-get update -y
+RUN DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev
+
+# Make sure we have the latest tooling for our tests
+RUN python -m pip install --upgrade pip
+RUN pip install --upgrade tox
diff --git a/docs/source/source_documentation/lofar_station_client.rst b/docs/source/source_documentation/lofar_station_client.rst
index ff79cba7ecc0bf053163dcb02b5a10274120c3e8..aa18120d5728a2511280c0bdb0b4e6a1f4224820 100644
--- a/docs/source/source_documentation/lofar_station_client.rst
+++ b/docs/source/source_documentation/lofar_station_client.rst
@@ -1,6 +1,18 @@
 lofar\_station\_client package
 ==============================
 
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   lofar_station_client.dts
+   lofar_station_client.math
+   lofar_station_client.parsing
+   lofar_station_client.requests
+   lofar_station_client.statistics
+
 Module contents
 ---------------
 
diff --git a/lofar_station_client/RCUs.py b/lofar_station_client/RCUs.py
deleted file mode 100644
index 2637079d1228c6da62fe47f36d3904f47344e179..0000000000000000000000000000000000000000
--- a/lofar_station_client/RCUs.py
+++ /dev/null
@@ -1,6 +0,0 @@
-RCU2L = [0, 1, 2, 3, 4, 5]
-RCU2H = [8, 9]
-# RCU2H = [8]
-RCU2Hpwr = [8]  # Should be even numbers
-RCU2Hctl = [9]  # Should be even numbers
-# RCU2Hctl = [rcu + 1 for rcu in RCU2Hpwr]  # This is then odd numbers
diff --git a/lofar_station_client/devices.py b/lofar_station_client/devices.py
new file mode 100644
index 0000000000000000000000000000000000000000..aac73f1b8f89f2c859c5f5fd42f9450b8116ad1f
--- /dev/null
+++ b/lofar_station_client/devices.py
@@ -0,0 +1,77 @@
+""" Enhanced interfaces towards the station's Tango devices. """
+
+import ast
+from functools import lru_cache
+
+import numpy
+
+from tango import DeviceProxy
+from tango import ExtractAs
+
+
+class LofarDeviceProxy(DeviceProxy):
+    """A LOFAR-specific tango.DeviceProxy that provides
+    a richer experience."""
+
+    @lru_cache()
+    def get_attribute_config(self, name):
+        """Get cached attribute configurations, as they are not expected to change."""
+
+        return super().get_attribute_config(name)
+
+    @lru_cache()
+    def get_attribute_shape(self, name):
+        """Get the shape of the requested attribute, as a tuple."""
+
+        config = self.get_attribute_config(name)
+
+        if config.format and config.format[0] == "(":
+            # For >2D arrays, the "format" property describes actual
+            # the dimensions as a tuple (x, y, z, ...),
+            # so reshape the value accordingly.
+            shape = ast.literal_eval(config.format)
+        elif config.max_dim_y > 0:
+            # 2D array
+            shape = (config.max_dim_y, config.max_dim_x)
+        elif config.max_dim_x > 1:
+            # 1D array
+            shape = (config.max_dim_x,)
+        else:
+            # scalar
+            shape = ()
+
+        return shape
+
+    def read_attribute(self, name, extract_as=ExtractAs.Numpy):
+        """Read an attribute from the server."""
+
+        attr = super().read_attribute(name, extract_as)
+
+        # convert non-scalar values into their actual shape
+        shape = self.get_attribute_shape(name)
+        if shape != ():
+            attr.value = attr.value.reshape(shape)
+
+        return attr
+
+    def write_attribute(self, name, value):
+        """Write an attribute to the server."""
+
+        config = self.get_attribute_config(name)
+        shape = self.get_attribute_shape(name)
+
+        # 2D arrays also represent arrays of higher dimensionality. reshape them
+        # to fit their original Tango shape before writing.
+        if shape != ():
+            value = numpy.array(value)
+
+            if value.shape != shape:
+                raise ValueError(
+                    f"Invalid shape. Given: {value.shape} Expected: {shape}"
+                )
+
+            if len(shape) > 2:
+                # >2D arrays collapse into 2D
+                value = value.reshape((config.max_dim_y, config.max_dim_x))
+
+        return super().write_attribute(name, value)
diff --git a/lofar_station_client/dts/__init__.py b/lofar_station_client/dts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lofar_station_client/dts/bands.py b/lofar_station_client/dts/bands.py
new file mode 100644
index 0000000000000000000000000000000000000000..2979f56c54df711d2b088cac09b62d1e74bca41d
--- /dev/null
+++ b/lofar_station_client/dts/bands.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+
+"""Band methods and conversions to and from frequencies"""
+
+import copy
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Union
+from typing import Tuple
+
+import numpy as np
+from nptyping import NDArray
+from nptyping import String
+from nptyping import Float
+
+from lofar_station_client.dts import constants
+from lofar_station_client.dts import index
+
+
+def get_valid_band_names():
+    """Return the valid band names."""
+
+    return copy.deepcopy(constants.BANDLIMS)
+
+
+def get_band_name_and_subband_index_for_frequency(
+    freqs: Union[float, List[float]]
+) -> Tuple[NDArray[Any, String], NDArray[Any, Float]]:
+    """Get band name and subband index for one or more frequencies.
+
+    :param freqs: one or more frequencies in Hz
+    :return: list of band names (see get_valid_band_names)
+             sbi = one or more indices of the subbands for which the frequencies
+             should be returned
+    """
+    # make sure freqs is a list
+    if not isinstance(freqs, list):
+        freqs = [freqs]
+    # get band name and subband index
+    freqs = np.array(freqs)
+    band_names = []
+    for freq in freqs:
+        band_name = get_band_name_for_frequency(freq)
+        band_names.append(band_name)
+    band_names = np.array(band_names)
+    # get indices for band_name "LB"
+    sbis = np.round(freqs * constants.CST_N_SUB / constants.CST_FS)
+    # and update for the other bands:
+    for idx in np.where(np.array(band_names) == "HB1")[0]:
+        sbis[idx] = 2 * constants.CST_N_SUB - sbis[idx]
+    for idx in np.where(np.array(band_names) == "HB2")[0]:
+        sbis[idx] = sbis[idx] - 2 * constants.CST_N_SUB
+    return band_names, sbis
+
+
+def get_band_name_for_frequency(freq: float, bandlims: Dict[str, float] = None) -> str:
+    """Get band name for frequency
+
+    :param freq: one frequency in Hz
+    :param bandlims: dict with band names and their limits (default: LOFAR2 bands)
+    :return: name of analog band (see get_valid_band_names),
+             empty string ("") if unsuccessful
+    """
+    if bandlims is None:
+        bandlims = constants.BANDLIMS
+    for band_name in bandlims:
+        if np.min(bandlims[band_name]) <= freq <= np.max(bandlims[band_name]):
+            return band_name
+    return ""
+
+
+def get_frequency_for_band_name(band_name: str):
+    """Get all frequencies for given band name
+
+    :param band_name: name of analog band (see get_valid_band_names)
+    :return: frequencies in Hz for the selected band
+    """
+
+    return get_frequency_for_band_name_and_subband_index(band_name, sbi=np.arange(512))
+
+
+def get_frequency_for_band_name_and_subband_index(
+    band_name: str, sbi: Union[float, List[float], NDArray[Any, Float]]
+) -> Union[float, List[float], NDArray[Any, Float]]:
+    """Get frequency for band name and subband(s)
+
+    Transparenently returns the same type as supplied for sbi parameter.
+
+    :warning: Will return frequencies for non-existing subbands outside the
+              range of :py:attr:`~constants.CST_N_SUB`
+
+    :param band_name: Name of analog band (see get_valid_band_names)
+    :param sbi: One or more indices of the subbands for which the frequencies
+                should be returned
+    :return: frequencies in Hz for the selected band and subband
+    """
+
+    # check input
+    valid_band_names = get_valid_band_names()
+    if band_name not in valid_band_names and band_name not in constants.LB_ALIASES:
+        raise ValueError(
+            f"Unknown band_name '{band_name}'. Valid band_names are: "
+            f"{valid_band_names}"
+        )
+
+    # generate frequencies
+    freqs = sbi * constants.CST_FS / constants.CST_N_SUB
+    if band_name == "HB1":
+        return 200e6 - freqs
+    if band_name == "HB2":
+        return 200e6 + freqs
+    # else: # LB
+    return freqs
+
+
+# TODO(Bouwdewijn): dimensionality does not correspond to recv attributes
+def get_band_names(
+    rcu_band_select_r, rcu_pcb_version_r, lb_tags: List = None, hb_tags: List = None
+):
+    """Get the frequency band names per receiver, based on their band selection & RCU.
+
+    PCB name is used to determine the band (Low Band "LB" or High Band "HB",
+    None otherwise). The band selection is added as a postfix.
+    RCU PCB version can also be used, but then the default tags will not be sufficient
+
+    rcu_pcb_version_r can also hold IDs as returned by recv.RCU_PCB_ID_R
+    In that case, the lb_tags and hb_tags should be passed on by the user
+
+    :param rcu_band_select_r: As returned by recv.RCU_band_select_R
+    :param rcu_pcb_version_r: As returned by recv.RCU_PCB_version_R
+    :param lb_tags: Substrings in RCU_PCB_version_R fields to indicate Low Band
+                    Default: `["RCU2L"]`
+    :param hb_tags: Substrings in RCU_PCB_version_R fields to indicate High Band
+                    Default: `["RCU2H"]`
+    :return: list of strings indicating the band name per receiver.
+             Returns `None` if no band name could be determined
+
+    """
+
+    # if IDs are passed on, convert to list of strings
+    if isinstance(rcu_pcb_version_r, np.ndarray):
+        rcu_pcb_version_r = [str(x) for x in rcu_pcb_version_r]
+    #
+    if lb_tags is None:
+        lb_tags = ["RCU2L"]
+    if hb_tags is None:
+        hb_tags = ["RCU2H"]
+
+    # This statement can never work if rcu_band_select_r is one dimensional
+    n_signal_indices = rcu_band_select_r.shape[0] * rcu_band_select_r.shape[1]
+    band_name_per_signal_index = [None] * n_signal_indices
+
+    for rcu_index, _ in enumerate(rcu_band_select_r):
+        for rcu_input_index, _ in enumerate(rcu_band_select_r[rcu_index]):
+            signal_index = index.rcu_index_and_rcu_input_index_2_signal_index(
+                rcu_index, rcu_input_index
+            )
+
+            this_band = None
+            if _is_lowband(lb_tags, rcu_pcb_version_r[rcu_index]):
+                this_band = f"LB{rcu_band_select_r[rcu_index][rcu_input_index]:1}"
+            elif _is_highband(hb_tags, rcu_pcb_version_r[rcu_index]):
+                this_band = f"HB{rcu_band_select_r[rcu_index][rcu_input_index]:1}"
+
+            band_name_per_signal_index[signal_index] = this_band
+    return band_name_per_signal_index
+
+
+def _is_lowband(tags, indexes):
+    for tag in tags:
+        if tag not in indexes:
+            continue
+        return True
+    return False
+
+
+def _is_highband(tags, indexes):
+    for tag in tags:
+        if tag not in indexes:
+            continue
+        return True
+    return False
diff --git a/lofar_station_client/dts/common.py b/lofar_station_client/dts/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d2ecf62c87e345dd0238157a1ee5a13a66f99c2
--- /dev/null
+++ b/lofar_station_client/dts/common.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+
+"""Common methods shared across DTS not further classified"""
+
+
+def devices_list_2_dict(devices_list, device_keys=None):
+    """Convert a list of devices to a dictionary with known keys
+
+    Devices are selected based on substring presence in the devices name
+
+    :param devices_list: list of Tango devices
+    :param device_keys: list of keys, should be a substring of the device names
+    :return: dict of devices with known keys
+    """
+    if device_keys is None:
+        device_keys = [
+            "boot",
+            "unb2",
+            "recv",
+            "sdp",
+            "sst",
+            "bst",
+            "xst",
+            "digitalbeam",
+            "tilebeam",
+            "beamlet",
+            "apsct",
+            "apspu",
+        ]
+    devices_dict = {}
+    if isinstance(devices_list, dict):
+        # if by accident devices_list is already a dict,
+        # then simply return the dict
+        return devices_list
+    for device in devices_list:
+        for device_key in device_keys:
+            if device_key in device.name().lower():
+                devices_dict[device_key] = device
+    return devices_dict
diff --git a/lofar_station_client/dts/constants.py b/lofar_station_client/dts/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..51e298587fe34926b6c4d854582db525ed6d2e24
--- /dev/null
+++ b/lofar_station_client/dts/constants.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+"""Constants used by DTS"""
+
+# pylint: disable=consider-using-f-string
+
+RCU2L = [0, 1, 2, 3, 4, 5]
+RCU2H = [8, 9]
+RCU2HPWR = [8]  # Should be even numbers
+# TODO(Corne): Why is this not an even number if it should be?!
+RCU2HCTL = [9]  # Should be even numbers
+
+CST_N_SUB = 512  # number of subbands as output of subband generation in firmware
+CST_FS = 100e6  # sampling frequency in Hz
+BANDLIMS = {"LB": [0, 100e6], "HB1": [100e6, 200e6], "HB2": [200e6, 300e6]}
+LB_ALIASES = ["LB1", "LB2"]
+
+# define some constants for the setup:
+N_FPGA = 4  # number of fpgas
+
+APS_LOCATION_LABELS = ["Slot%02d" % nr for nr in range(32)]
+
+# positions of RCU boards. (0,0) is bottom, left
+APS_RCU_POS_X = (
+    [11, 10, 9, 8, 7, 6]
+    + [11, 10, 9, 8, 7] * 2
+    + [4, 3, 2, 1, 0] * 2
+    + [5, 4, 3, 2, 1, 0]
+)
+APS_RCU_POS_Y = [2] * 6 + [1] * 5 + [0] * 5 + [2] * 5 + [1] * 5 + [0] * 6
+# positions of the other modules in the APS
+APS_MOD_POS_X = [0, 1, 3, 4, 5]
+APS_MOD_POS_Y = [0] * 5
+
+# if the names are correctly provided by the RCU2s,
+# then update plot_subband_statistics() (see annotation there)
+# and remove these two lines:
+# TODO(Corne): Improve mechanism for defining / undefining these variables
+RCU2L_TAGS = ["8393812", "8416938", "8469028", "8386883", "8374523"]
+RCU2H_TAGS = ["8514859", "8507272"]
+
+PLOT_DIR = "inspection_plots"
+STATION_NAME = "DTS-Outside"
+
+RCU_INDICES = range(32)
+
+# RCU Mask:
+RCU2L_MASK = [rcu_nr in RCU2L for rcu_nr in range(32)]
+
+# LBA Masks
+LBA_MASK = [[True] * 3 if rcu_nr in RCU2L else [False] * 3 for rcu_nr in range(32)]
+
+# HBA masks
+HBA_MASK = [[True] * 3 if rcui in RCU2H else [False] * 3 for rcui in RCU_INDICES]
+HBA_PWR_MASK = [[True] * 3 if rcui in RCU2HPWR else [False] * 3 for rcui in RCU_INDICES]
+HBA_CTL_MASK = [[True] * 3 if rcui in RCU2HCTL else [False] * 3 for rcui in RCU_INDICES]
diff --git a/lofar_station_client/dts/index.py b/lofar_station_client/dts/index.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca81f4692672092c0de6e91f880cadd06c5dce9a
--- /dev/null
+++ b/lofar_station_client/dts/index.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+
+"""Conversions between signal and node indexes"""
+
+import datetime
+from typing import Tuple
+
+from deprecate import deprecated
+import numpy as np
+
+
+def signal_index_2_processing_node_index(signal_index) -> int:
+    """Convert signal index to processing node index.
+
+    Can be used to determine on which processing node the signal is processed.
+    """
+    return (signal_index % 192) // 12
+
+
+def signal_index_2_processing_node_input_index(signal_index) -> int:
+    """Convert signal index to processing node input index.
+
+    Can be used to determine wich input to the processing node is used for the signal.
+    """
+    return (signal_index % 192) % 12
+
+
+def signal_index_2_uniboard_index(signal_index) -> int:
+    """Convert signal index to UniBoard index.
+
+    Can be used to determine on which UniBoard the signal is processed.
+    """
+    return signal_index // 48
+
+
+def signal_index_2_rcu_index(signal_index) -> int:
+    """Convert signal index to RCU index.
+
+    Can be used to determine which RCU is used to provide the signal.
+    """
+    return (signal_index // 6) * 2 + signal_index % 2
+
+
+def signal_index_2_rcu_input_index(signal_index) -> int:
+    """Convert signal index to RCU input index.
+
+    Can be used to determine which input to the RCU is used for the signal.
+    """
+    return (signal_index // 2) % 3
+
+
+def signal_index_2_aps_index(signal_index) -> int:
+    """Convert signal index to APS index.
+
+    Can be used to determine in which Antenna Processing Subrack this signal is
+    processed.
+    """
+    return signal_index // 96
+
+
+def signal_input_2_rcu_index_and_rcu_input_index(signal_index) -> Tuple[int, int]:
+    """Given the gsi, get the RCU index and the RCU input index in one call."""
+
+    return (
+        signal_index_2_rcu_index(signal_index),
+        signal_index_2_rcu_input_index(signal_index),
+    )
+
+
+# the inverse:
+def rcu_index_2_signal_index(rcu_index):
+    """Convert RCU index to signal indices."""
+    return rcu_index_and_rcu_input_index_2_signal_index(rcu_index)
+
+
+def rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index=None):
+    """Convert RCU index and RCU input index to signal index."""
+
+    if rcu_input_index is None:
+        rcu_input_index = np.array([0, 1, 2])
+
+    return rcu_index + rcu_input_index * 2 + (rcu_index // 2) * 4
+
+
+@deprecated(target=signal_index_2_rcu_input_index, deprecated_in="0.2", remove_in="0.5")
+def gsi_2_rcu_input_index(gsi):
+    """Replaced by signal_index_2_rcu_input_index
+
+    :deprecated: Please use signal_index_2_rcu_input_index.
+    """
+
+    return signal_index_2_rcu_input_index(gsi)
+
+
+@deprecated(target=signal_index_2_rcu_index, deprecated_in="0.2", remove_in="0.5")
+def gsi_2_rcu_index(gsi):
+    """Replaced by signal_index_2_rcu_index
+
+    `lofar_station_control`
+
+    :deprecated: Please use signal_index_2_rcu_index.
+    """
+
+    return signal_index_2_rcu_index(gsi)
+
+
+@deprecated(
+    target=signal_input_2_rcu_index_and_rcu_input_index,
+    deprecated_in="0.2",
+    remove_in="0.5",
+)
+def gsi_2_rcu_index_and_rcu_input_index(gsi):
+    """Replaced by signal_input_2_rcu_index_and_rcu_input_index
+
+    :deprecated: Please use signal_input_2_rcu_index_and_rcu_input_index.
+    """
+
+    return signal_input_2_rcu_index_and_rcu_input_index(gsi)
+
+
+@deprecated(target=rcu_index_2_signal_index, deprecated_in="0.2", remove_in="0.5")
+def rcu_index_2_gsi(rcu_index):
+    """Replaced by rcu_index_2_signal_index
+
+    :deprecated: Please use rcu_index_2_signal_index.
+    """
+    return rcu_index_2_signal_index(rcu_index)
+
+
+@deprecated(
+    target=rcu_index_and_rcu_input_index_2_signal_index,
+    deprecated_in="0.2",
+    remove_in="0.5",
+)
+def rcu_index_and_rcu_input_index_2_gsi(rcu_index, rcu_input_index=None):
+    """Replaced by rcu_index_and_rcu_input_index_2_signal_index
+
+    :deprecated: Please use rcu_index_and_rcu_input_index_2_signal_index.
+    """
+    return rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index)
+
+
+def get_timestamp(format_specifier=None):
+    """Get the timestamp in standard format
+
+    :param format_specifier: format specifier. iso format (default)
+                             "filename": filename without spaces and special
+                             characters, format: yyyymmddThhmmss
+
+    :return: Current timestamp in requested format
+    """
+    timestamp = datetime.datetime.isoformat(datetime.datetime.now())
+    if format_specifier == "filename":
+        timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
+    return timestamp
diff --git a/lofar_station_client/dts/outside.py b/lofar_station_client/dts/outside.py
new file mode 100644
index 0000000000000000000000000000000000000000..fee31ac7902b701c0dce9448afc585eee0c18c7b
--- /dev/null
+++ b/lofar_station_client/dts/outside.py
@@ -0,0 +1,720 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+"""DTS outside functionality such as restarting and configuring
+
+Functions to:
+- restart the Dwingeloo Test Station (Outside)
+- configure the Dwingeloo Test Station (Outside) to a defined default
+
+Boudewijn Hut
+"""
+
+import time
+import datetime
+import os
+import logging
+
+import numpy as np
+
+from lofar_station_client.dts import common
+from lofar_station_client.dts import constants
+from lofar_station_client.dts import plot
+
+folders = [constants.PLOT_DIR]
+for folder in folders:
+    if not os.path.exists(folder):
+        os.makedirs(folder)
+
+logger = logging.getLogger()
+
+
+def init_and_set_to_default_config(devices, skip_boot=False, silent=False):
+    """Restart system and set to default configuration
+
+    :param devices: struct of software devices of m&c of the station name
+
+    :return: True if an error ocurred, False otherwise
+    """
+
+    original_log_level = logger.getEffectiveLevel()
+    if silent:
+        logger.setLevel(logging.WARNING)
+
+    found_error = False
+    # initialisation
+    if not skip_boot:
+        logger.info("Start initialisation")
+        found_error = found_error | initialise(devices)
+    else:
+        logger.info("SKIPPING INITIALISATION (skip_init was set to True)")
+    logger.info("Check initialisation")
+    found_error = found_error | check_initialise(devices)
+    # configuring
+    logger.info("Start setting to default configuration")
+    found_error = found_error | set_to_default_configuration(devices)
+    logger.info("Check setting to default configuration")
+    found_error = found_error | check_set_to_default_configuration(
+        devices, readonly=False
+    )  # readonly=False, only at first time after init
+    # plot statistics data
+    logger.info("Plot statistics data")
+    plot.plot_statistics(devices)
+
+    if silent and original_log_level != logging.NOTSET:
+        logger.setLevel(original_log_level)
+
+    return found_error
+
+
+def initialise(devices, timeout=90):
+    """Initialise the system
+
+    :param timeout: Timeout to flag an error in seconds
+    :return: true if error, false otherwise
+    """
+
+    devices = common.devices_list_2_dict(devices)
+    boot = devices["boot"]
+    recv = devices["recv"]
+    sst = devices["sst"]
+    unb2 = devices["unb2"]
+
+    # initialise boot device
+    boot.put_property({"DeviceProxy_Time_Out": 60})
+    boot.off()
+    boot.initialise()
+    boot.on()
+
+    # increase timeouts for reboot
+    logger.debug("Time out settings prior to increase for reboot:")
+    logger.debug(recv.get_timeout_millis())
+    logger.debug(unb2.get_timeout_millis())
+    logger.debug(sst.get_timeout_millis())
+
+    recv.set_timeout_millis(30000)
+    unb2.set_timeout_millis(60000)
+    sst.set_timeout_millis(20000)
+
+    unb2.put_property({"UNB2_On_Off_timeout": 20})
+    recv.put_property({"RCU_On_Off_timeout": 60})
+
+    # reboot system
+    boot.reboot()
+    logger.info("Rebooting now")
+    time_start = time.time()
+    time_dt = time.time() - time_start
+
+    # indicate progress
+    while boot.booting_R and (time_dt < timeout):
+        if time_dt % 5 < 1:  # print every 5 seconds
+            logger.info("Initialisation at %f%%: %s", boot.progress_R, boot.status_R)
+        time.sleep(1)
+        time_dt = time.time() - time_start
+    logger.info("Initialisation took %d seconds", time_dt)
+    # check for errors
+    found_error = False
+    if boot.booting_R:
+        found_error = True
+        logger.warning(
+            "Warning! Initialisation still ongoing after timeout (%d sec)", timeout
+        )
+    if boot.uninitialised_devices_R:
+        found_error = True
+        logger.error("Warning! Did not initialise %s.", boot.uninitialised_devices_R)
+    return found_error
+
+
+def check_initialise(devices):
+    """Check the initialisation of the system by an independent monitoring point.
+
+    Note that this method should be used after calling initialise()
+
+    All checks that are directly related to the boot device are already
+    in the method initialise()
+    """
+
+    found_error = False
+    found_error = found_error | check_firmware_loaded(devices)
+    found_error = found_error | check_clock_locked(devices)
+    return found_error
+
+
+def check_firmware_loaded(devices):
+    """Verify that the firmware has loaded in the fpgas
+
+    :return: True if not loaded, False otherwise
+    """
+    devices = common.devices_list_2_dict(devices)
+    sdp = devices["sdp"]
+    res = sdp.FPGA_boot_image_R
+    logger.debug("")
+    logger.debug("Checking sdp.FPGA_boot_image_R:")
+    logger.debug("Reply: %s", res)
+    if not np.array_equal(res[0 : constants.N_FPGA], np.ones(constants.N_FPGA)):
+        logger.debug("Firmware is not loaded!")
+        return True
+    logger.debug("Ok - Firmware loaded")
+    return False
+
+
+def check_clock_locked(devices):
+    """Verify that the APSCT clock is locked
+
+    :return: True if not loaded, False otherwise
+    """
+
+    devices = common.devices_list_2_dict(devices)
+    apsct = devices["apsct"]
+    res = apsct.APSCT_PLL_200MHz_error_R
+
+    logger.debug("")
+    logger.debug("Checking apsct.APSCT_PLL_200MHz_error_R:")
+    logger.debug("Reply: %s", res)
+    if res:
+        logger.debug("APSCT clock not locked!")
+        return True
+    logger.debug("Ok - APSCT clock locked")
+    return False
+
+
+def set_to_default_configuration(devices):
+    """Set the station to its default configuration
+
+    :return: True if error found, False otherwise
+    """
+
+    found_error = False
+    logger.info("Start configuring Low Receivers")
+    found_error = found_error | set_rcul_to_default_config(devices)
+    logger.info("Start configuring High Receivers")
+    found_error = found_error | set_rcuh_to_default_config(devices)
+    logger.info("Start configuring Station Digital Processor")
+    found_error = found_error | set_sdp_to_default_config(devices)
+    logger.info("Start configuring Subband Statistics")
+    found_error = found_error | set_sdp_sst_to_default_config(devices)
+    logger.info("Start configuring Crosslet Statistics")
+    found_error = found_error | set_sdp_xst_to_default_config(devices)
+    logger.info("Start configuring Beamlet Statistics")
+    found_error = found_error | set_sdp_bst_to_default_config(devices)
+    return found_error
+
+
+def set_rcul_to_default_config(devices):
+    """Set RCU Low to its default configuration"""
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+
+    # Set RCUs Lows in default modes
+
+    recv.set_defaults()
+
+    # Set attenuator, antenna power etc.
+    recv.RCU_band_select_RW = set_rcu_ant_masked(
+        recv, constants.LBA_MASK, recv.RCU_band_select_R, [[1] * 3] * 32
+    )  # 1 = 30-80 MHz
+    recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked(
+        recv, constants.LBA_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32
+    )  # Off
+    recv.RCU_attenuator_dB_RW = set_rcu_ant_masked(
+        recv, constants.LBA_MASK, recv.RCU_attenuator_dB_R, [[0] * 3] * 32
+    )  # 0dB attenuator
+    wait_rcu_receiver_busy(recv)
+
+    #
+    found_error = False
+    return found_error
+
+
+def set_rcuh_to_default_config(devices):
+    """Set RCU High to its default configuration"""
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+
+    # Setup HBA RCUs and HBA Tiles
+
+    # Set RCU in correct mode
+    rcu_modes = recv.RCU_band_select_R
+    rcu_modes[8] = [2, 2, 2]  # 2 = 110-180 MHz, 1 = 170-230 MHz, 4 = 210-270 MHz ??
+    rcu_modes[9] = [2, 2, 2]
+    recv.RCU_band_select_RW = set_rcu_ant_masked(
+        recv, constants.HBA_MASK, recv.RCU_band_select_R, rcu_modes
+    )
+    wait_rcu_receiver_busy(recv)
+
+    # Switch on the Antenna
+    recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked(
+        recv, constants.HBA_PWR_MASK, recv.RCU_PWR_ANT_on_R, [[False] * 3] * 32
+    )  # Switch off
+    wait_rcu_receiver_busy(recv)
+    recv.HBAT_PWR_LNA_on_RW = set_rcu_hba_mask(
+        recv, constants.HBA_CTL_MASK, recv.HBAT_PWR_LNA_on_R, [[True] * 32] * 96
+    )  # LNA default on
+    recv.HBAT_BF_delay_steps_RW = set_rcu_hba_mask(
+        recv, constants.HBA_CTL_MASK, recv.HBAT_BF_delay_steps_R, [[0] * 32] * 96
+    )  # Default
+    recv.HBAT_PWR_on_RW = set_rcu_hba_mask(
+        recv, constants.HBA_CTL_MASK, recv.HBAT_PWR_on_R, [[True] * 32] * 96
+    )  # Default
+    wait_rcu_receiver_busy(recv)
+    recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked(
+        recv, constants.HBA_PWR_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32
+    )  # Switch on
+    wait_rcu_receiver_busy(recv)
+    # default in the tile: after power-on, zero delay
+    # delays should be set by the tile beam device
+    # recv.HBAT_BF_delay_steps_RW=[[1]*32]*32
+    # wait_receiver_busy(recv)
+    # by default: equal delay settings for all elements --> Pointing to zenith
+
+    # TODO(Boudewijn): read values and only update the ones that need to be changed.
+    # Could be a function that manages the masks.
+    logging.info(recv.RCU_PWR_ANT_on_RW)
+    logging.info("")
+    logging.info("done")
+
+    found_error = False
+    return found_error
+
+
+####
+# RCU specific functions and variables for DTS-Outside
+####
+
+# General RCU functions
+
+
+def wait_rcu_receiver_busy(recv):
+    """Wait for the Receiver Translators busy monitoring point to return False"""
+
+    while recv.RECVTR_translator_busy_R:
+        time.sleep(0.1)
+
+
+def set_rcu_ant_masked(recv, mask, old_value, new_value):
+    """Set the antenna mask for the Receiver Translator"""
+
+    recv.ANT_mask_RW = mask
+
+    for idx, mask_elem in enumerate(mask):
+        # TODO(Boudewijn): Check that range(3) still is correct dimensionality
+        for rcu_input_index in range(3):
+            if mask_elem[rcu_input_index]:
+                old_value[idx][rcu_input_index] = new_value[idx][rcu_input_index]
+    return old_value
+
+
+def set_rcu_hba_mask(recv, mask, old_value, new_value):
+    """Set the hba mask for the Receiver Translator
+
+    :param mask: Takes flattened one dimensional array of values
+    :param old_value: Takes flattened one dimensional array of values
+    :param new_value: Takes flattened one dimensional array of values
+    """
+
+    recv.ANT_mask_RW = mask
+    for idx, mask_elem in enumerate(mask):
+        # TODO(Boudewijn): Check that range(3) still is correct dimensionality
+        for i in range(3):
+            if mask_elem[i]:
+                old_value[idx * 3 + i] = new_value[idx * 3 + i]
+            else:
+                old_value[idx * 3 + i] = old_value[idx * 3 + i]
+    return old_value
+
+
+def set_sdp_to_default_config(devices):
+    """Set RCU High to its default configuration
+
+    TODO(Corne): Update this docstring to reflect actual method functionality
+    """
+
+    devices = common.devices_list_2_dict(devices)
+    sdp = devices["sdp"]
+    #
+    # Set SDP in default modes
+    #
+
+    logging.info("Start configuring SDP to default")
+    sdp.set_defaults()
+    # should be part of sdp.set_defaults:
+    next_ring = [False] * 16
+    next_ring[3] = True
+    sdp.FPGA_ring_use_cable_to_previous_rn_RW = [True] + [False] * 15
+    sdp.FPGA_ring_use_cable_to_next_rn_RW = next_ring
+    sdp.FPGA_ring_nof_nodes_RW = [constants.N_FPGA] * 16
+    sdp.FPGA_ring_node_offset_RW = [0] * 16
+
+    #
+    found_error = False
+    return found_error
+
+
+def set_sdp_sst_to_default_config(devices):
+    """Set SSTs to default"""
+
+    devices = common.devices_list_2_dict(devices)
+    sst = devices["sst"]
+
+    sst.set_defaults()
+    # prepare for subband stati
+    sst.FPGA_sst_offload_weighted_subbands_RW = [
+        True
+    ] * 16  # should be in set_defaults()
+
+    #
+    found_error = False
+    return found_error
+
+
+def set_sdp_xst_to_default_config(devices):
+    """Set XSTs to default"""
+
+    devices = common.devices_list_2_dict(devices)
+    xst = devices["xst"]
+
+    # prepare for correlations
+    int_time = 1  # used for 'medium' measurement using statistics writer
+    subband_step_size = 7  # 0 is no stepping
+    n_xst_subbands = 7
+    subband_select = [subband_step_size, 0, 1, 2, 3, 4, 5, 6]
+
+    xst.set_defaults()
+    # should be part of set_defaults()
+    xst.FPGA_xst_processing_enable_RW = [False] * 16
+    # this (line above) should be first, then configure and enable
+    xst.fpga_xst_ring_nof_transport_hops_RW = [3] * 16  # [((n_fgpa/2)+1)]*16
+
+    crosslets = [0] * 16
+    for fpga_nr in range(constants.N_FPGA):
+        crosslets[fpga_nr] = n_xst_subbands
+    xst.FPGA_xst_offload_nof_crosslets_RW = crosslets
+
+    xst.FPGA_xst_subband_select_RW = [subband_select] * 16
+    xst.FPGA_xst_integration_interval_RW = [int_time] * 16
+
+    xst.FPGA_xst_processing_enable_RW = [True] * 16
+    #
+    found_error = False
+    return found_error
+
+
+def set_sdp_bst_to_default_config(devices):
+    """Set XSTs to default"""
+
+    devices = common.devices_list_2_dict(devices)
+    beamlet = devices["beamlet"]
+
+    # prepare for beamformer
+    beamlet.set_defaults()
+
+    beamlet.FPGA_bf_ring_nof_transport_hops_RW = [[1]] * 3 + [[0]] * 13
+    # line above should be part of set_defaults(), specifically for DTS-Outside
+
+    beamletoutput_mask = [False] * 16
+    beamletoutput_mask[3] = True
+    # beamlet.FPGA_beamlet_output_enable_RW=beamletoutput_mask
+    beamlet.FPGA_beamlet_output_hdr_eth_destination_mac_RW = ["40:a6:b7:2d:4f:68"] * 16
+    beamlet.FPGA_beamlet_output_hdr_ip_destination_address_RW = ["192.168.0.249"] * 16
+
+    # define weights
+    # dim: FPGA, input, subband
+    weights_xx = beamlet.FPGA_bf_weights_xx_R.reshape([16, 6, 488])
+    weights_yy = beamlet.FPGA_bf_weights_yy_R.reshape([16, 6, 488])
+    # make a beam for the HBA
+    weights_xx[:][:] = 0
+    weights_yy[:][:] = 0
+    for signal_index in [0, 1]:  # range(8*3,9*3+3):
+        unb2i = signal_index // 12
+        signal_index_2 = signal_index // 2
+        signal_index_2 = signal_index_2 % 6
+        if signal_index % 2 == 0:
+            weights_xx[unb2i, signal_index_2] = 16384
+        else:
+            weights_yy[unb2i, signal_index_2] = 16384
+
+    beamlet.FPGA_bf_weights_xx_RW = weights_xx.reshape([16, 6 * 488])
+    beamlet.FPGA_bf_weights_yy_RW = weights_yy.reshape([16, 6 * 488])
+
+    blt = beamlet.FPGA_beamlet_subband_select_R.reshape([16, 12, 488])
+    blt[:] = np.array(range(10, 10 + 488))[np.newaxis, np.newaxis, :]
+    beamlet.FPGA_beamlet_subband_select_RW = blt.reshape([16, 12 * 488])
+
+    #
+    found_error = False
+    return found_error
+
+
+def enable_outputs(
+    devices, enable_sst=True, enable_xst=True, enable_bst=True, enable_beam_output=True
+):
+    """Enable the station outputs after everything is set."""
+
+    devices = common.devices_list_2_dict(devices)
+    if enable_beam_output:
+        sdp = devices["sdp"]
+        sdp.FPGA_processing_enable_RW = [True] * 16
+    if enable_sst:
+        sst = devices["sst"]
+        sst.FPGA_sst_offload_enable_RW = [True] * 16
+    if enable_xst:
+        xst = devices["xst"]
+        xst.FPGA_xst_offload_enable_R = [True] * 16
+    if enable_bst:
+        beamlet = devices["beamlet"]
+        beamlet.FPGA_beamlet_output_enable_RW = [i == 3 for i in range(16)]
+
+    #
+    found_error = False
+    return found_error
+
+
+def check_set_to_default_configuration(devices, readonly=True):
+    """Check the default configuration of the system to be set correctly."""
+
+    found_error = False
+    found_error = found_error | check_default_configuration_low_receivers(devices)
+    found_error = found_error | check_default_configuration_hbat(
+        devices, readonly=readonly
+    )
+    found_error = found_error | check_rcu_fpga_interface(devices, si_to_check=range(18))
+    found_error = found_error | check_set_sdp_xst_to_default_config(
+        devices, lane_mask=range(0, 6)
+    )
+
+    plot.print_fpga_input_statistics(devices)
+
+    return found_error
+
+
+def check_default_configuration_low_receivers(devices, mask=None):
+    """Check that the LNAs are on"""
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    found_error = False
+
+    if mask is None:
+        mask = constants.LBA_MASK
+
+    res = recv.RCU_PWR_ANT_on_R
+    if len(mask) > 0:
+        res = res[mask]
+    logging.debug("")
+    logging.debug("Checking recv.RCU_PWR_ANT_on_R:")
+    logging.debug("Reply%s: %s}", " (masked)" if len(mask) > 0 else "", res)
+    if not res.all():
+        logging.info("One or more LBAs are powered off!")
+        found_error = True
+    else:
+        logging.debug("Ok - Low Receivers operational")
+    return found_error
+
+
+def check_default_configuration_hbat(devices, readonly=True):
+    """Check that the HBA Tiles are on"""
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    hbat_si = [rcu * 3 + r_input for rcu in constants.RCU2HPWR for r_input in range(3)]
+    if not readonly:
+        recv.RECVTR_monitor_rate_RW = 1  # for this checking
+        time.sleep(3)
+
+    found_error = False
+    found_error = found_error | check_rcu_vin(devices, si_mask=hbat_si)
+    found_error = found_error | check_hbat_vout(devices, si_mask=hbat_si)
+    found_error = found_error | check_hbat_iout(devices, si_mask=hbat_si)
+
+    if not readonly:
+        recv.RECVTR_monitor_rate_RW = 10
+
+    # TODO(Boudewijn): - what is checked for here?
+    logging.info(recv.HBAT_PWR_LNA_on_R[8:10])
+    logging.info(recv.ANT_mask_RW[8:10])
+
+    return found_error
+
+
+def check_rcu_vin(devices, si_mask=None, valid_range=None):
+    """Verify HBA Tile is powered by comparing V_in against signal inputs thresholds.
+
+    If no si_mask is given, all returned values are compared.
+    """
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    if si_mask is None:
+        si_mask = []
+    if valid_range is None:
+        valid_range = [30, 50]
+
+    res = recv.RCU_PWR_ANT_VIN_R
+    if len(si_mask) > 0:
+        res = [res[si // 3][si % 3] for si in si_mask]
+    logger.debug("")
+    logger.debug("Checking recv.RCU_PWR_ANT_VIN_R:")
+    logger.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res)
+    if not (
+        np.all(valid_range[0] < np.array(res))
+        and np.all(np.array(res) < valid_range[1])
+    ):
+        logging.warning("No power at input RCU2!")
+        return True
+
+    logging.debug("Ok - Power at input RCU2")
+    return False
+
+
+def check_hbat_vout(devices, si_mask=None, valid_range=None):
+    """Verify HBA Tile is powered by comparing V_out against signal input thresholds
+
+    If no si_mask is given, all returned values are compared.
+    """
+
+    # Vout should be 48 +- 1 V
+    devices = common.devices_list_2_dict(devices)
+
+    recv = devices["recv"]
+    if si_mask is None:
+        si_mask = []
+    if valid_range is None:
+        valid_range = [30, 50]
+
+    res = recv.RCU_PWR_ANT_VOUT_R
+    if len(si_mask) > 0:
+        res = [res[si // 3][si % 3] for si in si_mask]
+
+    logging.debug("")
+    logging.debug("Checking recv.RCU_PWR_ANT_VOUT_R:")
+    logging.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res)
+
+    if not (
+        np.all(valid_range[0] < np.array(res))
+        and np.all(np.array(res) < valid_range[1])
+    ):
+        logging.warning("Not all HBA Tiles powered on!")
+        return True
+
+    logging.debug("Ok - HBA Tiles powered on")
+    return False
+
+
+def check_hbat_iout(devices, si_mask=None, valid_range=None):
+    """Verify HBA Tile is powered by comparing I_out against signal input thresholds
+
+    If no si_mask is given, all returned values are compared.
+    """
+
+    # Iout = 0.7 +- 0.1 Amp
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    if si_mask is None:
+        si_mask = []
+    if valid_range is None:
+        valid_range = [0.6, 0.8]
+
+    res = recv.RCU_PWR_ANT_IOUT_R
+    if len(si_mask) > 0:
+        res = [res[si // 3][si % 3] for si in si_mask]
+    logging.debug("")
+    logging.debug("Checking recv.RCU_PWR_ANT_IOUT_R:")
+    logging.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res)
+    if not (
+        np.all(valid_range[0] < np.array(res))
+        and np.all(np.array(res) < valid_range[1])
+    ):
+        logging.warning("Not all HBA Tiles powered on (draw current)!")
+        return True
+
+    logging.debug("Ok - HBA Tiles powered on (draw current)")
+    return False
+
+
+def check_rcu_fpga_interface(devices, si_to_check=range(18)):
+    """Function to check the RCU - FPGA interface
+
+    if error occurs, try to reboot by resetting Uniboard (switch)
+    if only one works than the APSCT board is not making contact with the backplane.
+    (Can take up to 3 times to get it right..)
+    23/6/2022T15:42 errorcode 0x4h: xxxxx on all of them.
+    Error descriptions can be found here:
+    https://support.astron.nl/jira/browse/L2SDP-774
+    """
+    devices = common.devices_list_2_dict(devices)
+    sdp = devices["sdp"]
+    found_error = False
+    # TODO(Boudewijn): the number of calls can be reduced
+    for signal_index in si_to_check:
+        lock = sdp.FPGA_jesd204b_csr_dev_syncn_R[signal_index // 12][signal_index % 12]
+        if lock != 1:
+            found_error = True
+            print(f"Transceiver SI: {signal_index} is not locked {lock}")
+        err0 = sdp.FPGA_jesd204b_rx_err0_R[signal_index // 12][signal_index % 12]
+        if err0 != 0:
+            found_error = True
+            print(f"SI {signal_index} has JESD err0: 0x{err0:x}h")
+        err1 = sdp.FPGA_jesd204b_rx_err1_R[signal_index // 12][signal_index % 12]
+        if (err1 & 0x02FF) != 0:
+            found_error = True
+            print(f"SI {signal_index} has JESD err1: 0x{err1:x}h")
+    return found_error
+
+
+def check_set_sdp_xst_to_default_config(
+    devices, lane_mask=None, time_1=None, time_dt=2.5
+):
+    """Check sdp/xst configuration by checking the returned timestamps for:
+
+        1. all being the same
+        2. not epoch zero (at 1 Jan 1970 00:00:00)
+        3. within 5 seconds from the local clock timestamp (now +/- 2.5 sec)
+
+    :param time_1: number of seconds since the epoch to compare against
+                   Default: None --> now()
+    :param time_dt: delta in seconds around time_1, in which the xst timestamps
+                    should fall.
+    """
+
+    devices = common.devices_list_2_dict(devices)
+    xst = devices["xst"]
+    if lane_mask is None:
+        lane_mask = []
+    if time_1 is None:
+        time_1 = datetime.datetime.now().timestamp()
+    res = xst.xst_timestamp_R
+    if len(lane_mask) > 0:  # mask lanes
+        res = [res[lane] for lane in lane_mask]
+    logging.debug("")
+    logging.debug("Checking xst.xst_timestamp_R:")
+    logging.debug("Reply%s: %s", " (masked)" if len(lane_mask) > 0 else "", res)
+    logging.debug("Converted:")
+    logging.debug(
+        "\n".join([f"{time.asctime(time.gmtime(xst_time))}" for xst_time in res])
+    )
+    if not (res == res[0]).all():
+        logging.warning("Not all xst timestamps are equal!")
+        return True
+    if res[0] == 0:
+        logging.warning("One or more xst timestamps are zero!")
+        return True
+    if abs(res[0] - time_1) > time_dt:
+        logging.warning(
+            "One or more xst timestamps are outside the specified time window!"
+        )
+        return True
+    logging.debug("Ok - xst configured correctly")
+    return False
diff --git a/lofar_station_client/dts/plot.py b/lofar_station_client/dts/plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f28156d5f0de4b72abeaf1fd72468ffe6d998d5
--- /dev/null
+++ b/lofar_station_client/dts/plot.py
@@ -0,0 +1,617 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+
+"""Plotting functionality for DTS"""
+
+# pylint: disable=R0914,C0209,R0915,R0913
+
+import logging
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+from lofar_station_client.dts import bands
+from lofar_station_client.dts import constants
+from lofar_station_client.dts import common
+from lofar_station_client.dts import index as plib
+
+logger = logging.getLogger()
+
+
+def print_fpga_input_statistics(devices):
+    """Print FPGA input statistics"""
+    devices = common.devices_list_2_dict(devices)
+    sdp = devices["sdp"]
+
+    # get data from device
+    dc_offset = sdp.FPGA_signal_input_mean_R
+    signal_input_rms = sdp.FPGA_signal_input_rms_R
+    lock_to_adc = sdp.FPGA_jesd204b_csr_dev_syncn_R
+    err0 = sdp.FPGA_jesd204b_rx_err0_R
+    err1 = sdp.FPGA_jesd204b_rx_err1_R
+    count = sdp.FPGA_jesd204b_csr_rbd_count_R
+    signal_index = np.reshape(np.arange(count.shape[0] * count.shape[1]), count.shape)
+    # get some info on the hardware connections
+    pn_index = plib.signal_index_2_processing_node_index(signal_index)
+    pn_input_index = plib.signal_index_2_processing_node_input_index(signal_index)
+    rcu_index = plib.signal_index_2_rcu_index(signal_index)
+    rcu_input_index = plib.signal_index_2_rcu_input_index(signal_index)
+
+    # print
+    print("+------------+-------------+----------------------------------------------+")
+    print("|   Input    | Processed by|             Statistics of input              |")
+    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
+    print("|RCU|RCU| SI | Node | Node | DC      | RMS     | Lock| Err0 | Err1 | Count|")
+    print("|idx|inp|    | Index| Input| Offset  |   (LSB) |     | (0x) | (0x) | (0x) |")
+    print("|   |idx|    |      | Index|   (LSB) |         |     |      |      |      |")
+    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
+    pfmt = (
+        "| %02d| %1d |%3d |  %02d  |  %2d  | %7.2f | %7.2f | %3s | %04x | %04x | %04x |"
+    )
+    for rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt in zip(
+        rcu_index.flatten(),
+        rcu_input_index.flatten(),
+        signal_index.flatten(),
+        pn_index.flatten(),
+        pn_input_index.flatten(),
+        dc_offset.flatten(),
+        signal_input_rms.flatten(),
+        lock_to_adc.flatten(),
+        err0.flatten(),
+        err1.flatten(),
+        count.flatten(),
+    ):
+        print(
+            pfmt % (rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt)
+        )
+    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
+
+
+def report_setup_configuration(devices, save_fig=True):
+    """Read name and version data from the setup and print that as a report"""
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    apsct = devices["apsct"]
+    apspu = devices["apspu"]
+    unb2 = devices["unb2"]
+    sdp = devices["sdp"]
+    # Receiver / High Band Antenna Tile
+
+    # APS Hardware
+    # APS / Receiver Unit2 side
+    rcu2_pcb_ids = recv.RCU_PCB_ID_R.tolist()
+    rcu2_pcb_nrs = list(recv.RCU_PCB_number_R)
+    rcu2_pcb_vrs = list(recv.RCU_PCB_version_R)
+
+    # APS / UniBoard2 side
+    # TODO(Boudewijn): FIXME - shouldn't this be a list with multiple entries?
+    # See also LOFAR2-10774 (APSCT)
+    apsct_pcb_ids = [apsct.APSCT_PCB_ID_R]
+    apsct_pcb_nrs = [apsct.APSCT_PCB_number_R]
+    apsct_pcb_vrs = [apsct.APSCT_PCB_version_R]
+    # TODO(Boudewijn): FIXME - shoudln't this be a list with multiple entries?
+    # See also LOFAR2-11434 (APSPU)
+    apspu_pcb_ids = [apspu.APSPU_PCB_ID_R]
+    apspu_pcb_nrs = [apspu.APSPU_PCB_number_R]
+    apspu_pcb_vrs = [apspu.APSPU_PCB_version_R]
+    unb2_pcb_ids = unb2.UNB2_PCB_ID_R.tolist()
+    unb2_pcb_nrs = list(unb2.UNB2_PCB_number_R)
+    unb2_pcb_vrs = list(sdp.FPGA_hardware_version_R)
+    # TODO(Boudewijn): FIXME:
+    # FPGA_hardware_version_R returns hardware version info per FPGA. Hardware is shared
+    # per 4 fpgas, so why repeating values here?
+    unb2_pcb_vrs = [unb2_pcb_vrs[idx] for idx in [0, 4, 8, 12]]
+
+    # TODO(Boudewijn): FIXME:
+    # fixing list lengths for a large station with multiple APS
+    # this makes sure that the list indexing below works
+    n_aps = 4
+
+    apsct_pcb_ids = apsct_pcb_ids + [0] * (n_aps - len(apsct_pcb_ids))
+    apsct_pcb_nrs = apsct_pcb_nrs + [""] * (n_aps - len(apsct_pcb_nrs))
+    apsct_pcb_vrs = apsct_pcb_vrs + [""] * (n_aps - len(apsct_pcb_vrs))
+
+    apspu_pcb_ids = apspu_pcb_ids + [0] * (n_aps * 2 - len(apspu_pcb_ids))
+    apspu_pcb_nrs = apspu_pcb_nrs + [""] * (n_aps * 2 - len(apspu_pcb_nrs))
+    apspu_pcb_vrs = apspu_pcb_vrs + [""] * (n_aps * 2 - len(apspu_pcb_vrs))
+
+    unb2_pcb_ids = unb2_pcb_ids + [0] * (n_aps * 2 - len(unb2_pcb_ids))
+    unb2_pcb_nrs = unb2_pcb_nrs + [""] * (n_aps * 2 - len(unb2_pcb_nrs))
+    unb2_pcb_vrs = unb2_pcb_vrs + [""] * (n_aps * 2 - len(unb2_pcb_vrs))
+
+    logging.debug("APSPU:")
+    logging.debug("apsct_pcb_ids: %s", apsct_pcb_ids)
+    logging.debug("apsct_pcb_nrs: %s", apsct_pcb_nrs)
+    logging.debug("apsct_pcb_vrs: %s", apsct_pcb_vrs)
+    logging.debug("APSPU:")
+    logging.debug("apspu_pcb_ids: %s", apspu_pcb_ids)
+    logging.debug("apspu_pcb_nrs: %s", apspu_pcb_nrs)
+    logging.debug("apspu_pcb_vrs: %s", apspu_pcb_vrs)
+    logging.debug("UNB2:")
+    logging.debug("unb2_pcb_ids: %s", unb2_pcb_ids)
+    logging.debug("unb2_pcb_nrs: %s", unb2_pcb_nrs)
+    logging.debug("unb2_pcb_vrs: %s", unb2_pcb_vrs)
+
+    aps_pcb_ids = [
+        apspu_pcb_ids[1],
+        unb2_pcb_ids[0],
+        apsct_pcb_ids[0],
+        apspu_pcb_ids[0],
+        unb2_pcb_ids[1],
+    ]
+    aps_pcb_nrs = [
+        apspu_pcb_nrs[1],
+        unb2_pcb_nrs[0],
+        apsct_pcb_nrs[0],
+        apspu_pcb_nrs[0],
+        unb2_pcb_nrs[1],
+    ]
+    aps_pcb_vers = [
+        apsct_pcb_vrs[1],
+        unb2_pcb_vrs[0],
+        apsct_pcb_vrs[0],
+        apsct_pcb_vrs[0],
+        unb2_pcb_vrs[1],
+    ]
+
+    if not save_fig:
+        fig_filenames = []
+    else:
+        fig_filenames = [
+            f'{constants.PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.png',
+            f'{constants.PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.pdf',
+        ]
+    plot_aps_configuration(
+        rcu2_pcb_ids,
+        rcu2_pcb_nrs,
+        rcu2_pcb_vrs,
+        aps_pcb_ids,
+        aps_pcb_nrs,
+        aps_pcb_vers,
+        filenames=fig_filenames,
+    )
+
+    # Services
+    # Firmware images
+    firmware_versions = list(sdp.FPGA_firmware_version_R)
+    # make sure there are versions for at least n_aps subracks
+    firmware_versions = firmware_versions + [""] * (n_aps * 8 - len(firmware_versions))
+    aps_services = {}
+    for apsi, fpgai in zip(sorted(list(range(n_aps)) * 8), range(n_aps * 8)):
+        aps_services["aps%i-fpga%02d" % (apsi, fpgai)] = firmware_versions[fpgai]
+        aps_services[f"aps{apsi}-unb2tr" % apsi] = ""  # TODO(Boudewijn):
+        aps_services[f"aps{apsi}-recvtr" % apsi] = ""  # TODO(Boudewijn):
+        aps_services[f"aps{apsi}-apscttr" % apsi] = ""  # TODO(Boudewijn):
+        aps_services[f"aps{apsi}-apsputr" % apsi] = ""  # TODO(Boudewijn):
+    # Translators
+    sdptr_software_versions = [sdp.TR_software_version_R]
+    sdptr_software_versions = sdptr_software_versions + [""] * (
+        2 - len(sdptr_software_versions)
+    )
+    station_services = {}
+    station_services["sdptr0"] = sdptr_software_versions[0]
+    station_services["sdptr1"] = sdptr_software_versions[1]
+    station_services["ccdtr"] = ""  # TODO(Boudewijn): currently missing
+    # station control tango devices
+    for dev in devices:
+        station_services[devices[dev].name().lower()] = devices[dev].version_R
+    if not save_fig:
+        figs = []
+    else:
+        figs = [
+            f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}"
+            "_station_services.png",
+            f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}"
+            "_station_services.pdf",
+        ]
+    plot_services_configuration(aps_services, station_services, filenames=figs)
+
+
+def plot_aps_configuration(
+    rcu_pcb_identifiers,
+    rcu_pcb_numbers,
+    rcu_pcb_versions,
+    aps_pcb_ids,
+    aps_pcb_nrs,
+    aps_pcb_vers,
+    filenames=None,
+):
+    """Plot the configuration of the Antenna Processing Subrack, both sides."""
+
+    fig, axs = plt.subplots(1, 2, figsize=(20, 8))
+
+    if filenames is None:
+        filenames = [
+            f"{constants.PLOT_DIR}/aps_config.png",
+            f"{constants.PLOT_DIR}/aps_config.pdf",
+        ]
+
+    plot_aps_rcu2_configuration(
+        axs[0], rcu_pcb_identifiers, rcu_pcb_numbers, rcu_pcb_versions
+    )
+    plot_aps_unb2_configuration(axs[1], aps_pcb_ids, aps_pcb_nrs, aps_pcb_vers)
+    for filename in filenames:
+        fig.savefig(filename)
+    fig.show()
+
+
+def plot_aps_rcu2_configuration(
+    plot_ax,
+    rcu_pcb_identifiers,
+    rcu_pcb_numbers,
+    rcu_pcb_versions,
+    rcu_locations=constants.APS_LOCATION_LABELS,
+    rcu_pos_x=None,
+    rcu_pos_y=None,
+):
+    """Plot the configuration of the Antenna Processing Subrack, RCU2 side."""
+
+    if rcu_pos_x is None:
+        rcu_pos_x = constants.APS_RCU_POS_X
+    if rcu_pos_y is None:
+        rcu_pos_y = constants.APS_RCU_POS_Y
+    plot_ax.plot(0, 0)
+    plot_dx = plot_dy = 0.9
+    plot_ax.set_xlim(-1 + plot_dx, np.max(rcu_pos_x) + 1)
+    plot_ax.set_ylim(-1 + plot_dy, np.max(rcu_pos_y) + 1)
+
+    for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip(
+        rcu_pos_x,
+        rcu_pos_y,
+        rcu_locations,
+        rcu_pcb_identifiers,
+        rcu_pcb_numbers,
+        rcu_pcb_versions,
+    ):
+        plot_ax.plot(
+            [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x],
+            [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y],
+            color="black",
+            alpha=0.2,
+        )
+        plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center")
+        plot_ax.text(
+            plot_x + plot_dx / 2,
+            plot_y + plot_dy / 2,
+            "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr),
+            va="center",
+            ha="center",
+            rotation=90,
+        )
+        if pcb_id == 0 and pcb_v == "" and pcb_nr == "":
+            plot_ax.plot(
+                [plot_x, plot_x + plot_dx],
+                [plot_y, plot_y + plot_dy],
+                color="black",
+                alpha=0.2,
+            )
+            plot_ax.plot(
+                [plot_x + plot_dx, plot_x],
+                [plot_y, plot_y + plot_dy],
+                color="black",
+                alpha=0.2,
+            )
+    plot_ax.set_title("Configuration of RCU2s")
+    plot_ax.set_xticks([])
+    plot_ax.set_yticks([])
+
+
+def plot_aps_unb2_configuration(
+    plot_ax,
+    aps_pcb_identifiers,
+    aps_pcb_numbers,
+    aps_pcb_versions,
+    mod_locations=constants.APS_LOCATION_LABELS,
+    mod_pos_x=None,
+    mod_pos_y=None,
+):
+    """Plot the configuration of the Antenna Processing Subrack, UNB2 side."""
+
+    if mod_pos_x is None:
+        mod_pos_x = constants.APS_MOD_POS_X
+    if mod_pos_y is None:
+        mod_pos_y = constants.APS_MOD_POS_Y
+    plot_ax.plot(0, 0)
+    plot_dx = 0.9
+    plot_dy = 3.9
+    plot_ax.set_xlim(-1 + plot_dx, np.max(mod_pos_x) + 2)
+    plot_ax.set_ylim(-1 + plot_dx, np.max(mod_pos_y) + 4)
+
+    for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip(
+        mod_pos_x,
+        mod_pos_y,
+        mod_locations,
+        aps_pcb_identifiers,
+        aps_pcb_numbers,
+        aps_pcb_versions,
+    ):
+        plot_ax.plot(
+            [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x],
+            [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y],
+            color="black",
+            alpha=0.2,
+        )
+        plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center")
+        plot_ax.text(
+            plot_x + plot_dx / 2,
+            plot_y + plot_dy / 2,
+            "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr),
+            va="center",
+            ha="center",
+            rotation=90,
+        )
+        if pcb_id == 0 and pcb_v == "" and pcb_nr == "":
+            plot_ax.plot(
+                [plot_x, plot_x + plot_dx],
+                [plot_y, plot_y + plot_dy],
+                color="black",
+                alpha=0.2,
+            )
+            plot_ax.plot(
+                [plot_x + plot_dx, plot_x],
+                [plot_y, plot_y + plot_dy],
+                color="black",
+                alpha=0.2,
+            )
+    plot_ax.set_title("Configuration of UniBoard2 side")
+    plot_ax.set_xticks([])
+    plot_ax.set_yticks([])
+
+
+def plot_services_configuration(
+    aps_services,
+    station_services,
+    filenames=None,
+):
+    """Plot the service version information in the setup."""
+
+    if filenames is None:
+        filenames = [
+            f"{constants.PLOT_DIR}/station_services.png",
+            f"{constants.PLOT_DIR}/station_services.pdf",
+        ]
+
+    fig, plot_ax = plt.subplots(1, 1, figsize=(16, 15))
+
+    aps_service_dx = 0.9
+    aps_service_dy = 0.9
+    first_aps_service = sorted(aps_services)[0][5:]
+    plot_y = plot_y0 = len(aps_services) / 2 + 2
+    for aps_service in sorted(aps_services):
+        plot_x = 0 if (aps_service[3] in "01") else 1
+        plot_y = (
+            plot_y0
+            if (aps_service[5:] == first_aps_service and aps_service[3] in "02")
+            else plot_y - 1
+        )
+        plot_y = (
+            plot_y - 1
+            if (aps_service[5:] == first_aps_service and aps_service[3] in "13")
+            else plot_y
+        )
+        plot_ax.plot(
+            [plot_x, plot_x + aps_service_dx, plot_x + aps_service_dx, plot_x, plot_x],
+            [plot_y, plot_y, plot_y + aps_service_dy, plot_y + aps_service_dy, plot_y],
+            color="black",
+            alpha=0.2,
+        )
+        plot_ax.text(
+            plot_x,
+            plot_y + aps_service_dy / 2,
+            aps_service,
+            va="center",
+            ha="left",
+            color="black",
+        )
+        plot_ax.text(
+            plot_x + aps_service_dx,
+            plot_y + aps_service_dy / 2,
+            aps_services[aps_service],
+            va="center",
+            ha="right",
+            color="blue",
+        )
+        if aps_services[aps_service] == "":
+            plot_ax.plot(
+                [plot_x, plot_x + aps_service_dx],
+                [plot_y, plot_y + aps_service_dy],
+                color="black",
+                alpha=0.2,
+            )
+            plot_ax.plot(
+                [plot_x + aps_service_dx, plot_x],
+                [plot_y, plot_y + aps_service_dy],
+                color="black",
+                alpha=0.2,
+            )
+
+    stat_service_dx = 1.9
+    stat_service_dy = 0.9
+    plot_x = 0
+    plot_y = 0
+    for stat_service in sorted(station_services):
+        plot_ax.plot(
+            [
+                plot_x,
+                plot_x + stat_service_dx,
+                plot_x + stat_service_dx,
+                plot_x,
+                plot_x,
+            ],
+            [
+                plot_y,
+                plot_y,
+                plot_y + stat_service_dy,
+                plot_y + stat_service_dy,
+                plot_y,
+            ],
+            color="black",
+            alpha=0.2,
+        )
+        plot_ax.text(
+            plot_x,
+            plot_y + stat_service_dy / 2,
+            stat_service,
+            va="center",
+            ha="left",
+            color="black",
+        )
+        plot_ax.text(
+            plot_x + stat_service_dx,
+            plot_y + stat_service_dy / 2,
+            station_services[stat_service],
+            va="center",
+            ha="right",
+            color="blue",
+        )
+        if station_services[stat_service] == "":
+            plot_ax.plot(
+                [plot_x, plot_x + stat_service_dx],
+                [plot_y, plot_y + stat_service_dy],
+                color="black",
+                alpha=0.2,
+            )
+            plot_ax.plot(
+                [plot_x + stat_service_dx, plot_x],
+                [plot_y, plot_y + stat_service_dy],
+                color="black",
+                alpha=0.2,
+            )
+        plot_y -= 1
+    plot_ax.set_title("Services in setup")
+    plot_ax.set_xticks([])
+    plot_ax.set_yticks([])
+
+    for filename in filenames:
+        fig.savefig(filename)
+    fig.show()
+
+
+def plot_statistics(devices, save_fig=True):
+    """Plot the statistics: SST, XST and BST"""
+
+    logging.info("Plot subband statistics")
+    if not save_fig:
+        figs = []
+    else:
+        figs = [
+            f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}"
+            "_subband_statistics.png",
+            f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}"
+            "_subband_statistics.pdf",
+        ]
+    plot_subband_statistics(devices, filenames=figs)
+
+    # TODO(Boudewijn):
+    # if not silent:
+    #     plib.log("Plot crosslet statistics")
+    # TODO(Boudewijn):
+    # if not silent:
+    #     plib.log("Plot beamlet statistics")
+
+
+def plot_subband_statistics(
+    devices,
+    xlim=None,
+    ylim=None,
+    filenames=None,
+):
+    """Make the subband statistics plot"""
+
+    if filenames is None:
+        filenames = [
+            f"{constants.PLOT_DIR}/subband_statistics.png",
+            f"{constants.PLOT_DIR}/subband_statistics.pdf",
+        ]
+
+    if xlim is None:
+        xlim = [-5, 305]
+    if ylim is None:
+        ylim = [-105, -30]
+    sst_data, band_names, frequency_axes = _get_subband_statistics(devices)
+
+    fig, _ = plt.subplots(1, 1, figsize=(18, 6))
+    for signal_index in range(len(sst_data)):
+        if band_names[signal_index] is None:
+            continue
+        freq = frequency_axes[band_names[signal_index]]
+        # plot data in dB full scale
+        plot_data = 10 * np.log10(sst_data[signal_index, :] + 1) - 128 - 6 * 4
+        # since we're lacking antenna names,
+        # show the RCU input info as label
+        rcu_index, rcu_input_index = plib.signal_input_2_rcu_index_and_rcu_input_index(
+            signal_index
+        )
+        label = "RCU%02d, Inp%d" % (rcu_index, rcu_input_index)
+        plt.plot(freq / 1e6, plot_data, label=label)
+    plt.grid()
+    plt.legend(bbox_to_anchor=(1, 1), loc="upper left")
+    plt.xlim(xlim)
+    plt.ylim(ylim)
+    plt.xlabel("Frequency (MHz)")
+    plt.ylabel("Power (dB full scale)")
+    plt.suptitle("Subband Statistics for all fully-connected receivers")
+    plt.title(f"{constants.STATION_NAME}; {plib.get_timestamp()}")
+
+    for filename in filenames:
+        fig.savefig(filename)
+    fig.show()
+
+
+def _get_subband_statistics(devices):
+    """Get subband statistics data from the setup, including proper axes and labels"""
+
+    devices = common.devices_list_2_dict(devices)
+    recv = devices["recv"]
+    sst = devices["sst"]
+
+    # get band names for the current configuration
+    cur_band_names = bands.get_band_names(
+        recv.RCU_band_select_R,
+        # TODO(Boudewijn): when correct LCNs are provided
+        # in "RCU_PCB_number_R", uncomment the
+        # following line:
+        # recv.RCU_PCB_number_R,
+        # and remove the following line:
+        recv.RCU_PCB_ID_R,
+        lb_tags=constants.RCU2L_TAGS,
+        hb_tags=constants.RCU2H_TAGS,
+    )
+
+    # replace the aliases of LB1 and LB2 into LB: (same frequency axis)
+    for alias in constants.LB_ALIASES:
+        while alias in cur_band_names:
+            cur_band_names[cur_band_names.index(alias)] = "LB"
+    # get frequency axes
+
+    # TODO(Boudewijn): get frequency axes correct (indexing is incorrect/inconsequent)
+    key1 = "HB2"
+    key2 = "HB1"
+    tmp_key = "magic_band"
+    while key1 in cur_band_names:
+        cur_band_names[cur_band_names.index(key1)] = tmp_key
+    while key2 in cur_band_names:
+        cur_band_names[cur_band_names.index(key1)] = key1
+    while tmp_key in cur_band_names:
+        cur_band_names[cur_band_names.index(tmp_key)] = key2
+
+    frequency_axes = {}
+    for band_name in bands.get_valid_band_names():
+        if band_name in cur_band_names:
+            frequency_axes[band_name] = bands.get_frequency_for_band_name(band_name)
+    # get sst data
+    sst_data = sst.sst_r
+    # make sure header data is of correct size
+    cur_band_names = cur_band_names + [None] * (len(sst_data) - len(cur_band_names))
+    return sst_data, cur_band_names, frequency_axes
diff --git a/lofar_station_client/dts_outside.py b/lofar_station_client/dts_outside.py
deleted file mode 100644
index 1fd015f5eba03d3d9331622fbf2185ae57d4152c..0000000000000000000000000000000000000000
--- a/lofar_station_client/dts_outside.py
+++ /dev/null
@@ -1,1420 +0,0 @@
-# dts_outside.py: Module with functions to restart dts outside and
-# put it in a default configuration
-#
-# -*- coding: utf-8 -*-
-#
-# 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.
-
-
-"""  module dts_outside
-
-This module contains functions to:
-- restart the Dwingeloo Test Station (Outside)
-- configure the Dwingeloo Test Station (Outside) to a defined default
-
-Boudewijn Hut
-"""
-
-import sys
-import time
-import datetime
-import os
-import numpy as np
-import matplotlib.pyplot as plt
-import processing_lib as plib
-import RCUs
-
-
-# define some constants for the setup:
-N_FPGA = 4  # number of fpgas
-
-APS_LOCATION_LABELS = [
-    "Slot%02d" % nr for nr in range(32)  # pylint: disable=consider-using-f-string
-]
-# positions of RCU boards. (0,0) is bottom, left
-APS_RCU_POS_X = (
-    [11, 10, 9, 8, 7, 6]
-    + [11, 10, 9, 8, 7] * 2
-    + [4, 3, 2, 1, 0] * 2
-    + [5, 4, 3, 2, 1, 0]
-)
-APS_RCU_POS_Y = [2] * 6 + [1] * 5 + [0] * 5 + [2] * 5 + [1] * 5 + [0] * 6
-# positions of the other modules in the APS
-APS_MOD_POS_X = [0, 1, 3, 4, 5]
-APS_MOD_POS_Y = [0] * 5
-
-# if the names are correctly provided by the RCU2s,
-# then update plot_subband_statistics() (see annotation there)
-# and remove these two lines:
-RCU2L_TAGS = ["8393812", "8416938", "8469028", "8386883", "8374523"]
-RCU2H_TAGS = ["8514859", "8507272"]
-
-PLOT_DIR = "inspection_plots"
-STATION_NAME = "DTS-Outside"
-
-# RCU Masks:
-# LBA masks
-RCU2L_MASK = [rcu_nr in RCUs.RCU2L for rcu_nr in range(32)]
-LBA_MASK = [[True] * 3 if rcu_nr in RCUs.RCU2L else [False] * 3 for rcu_nr in range(32)]
-# HBA masks
-# signal inputs connected to HBAT power:
-si_hbat_pwr = [signal_index * 3 + x for signal_index in RCUs.RCU2Hpwr for x in range(3)]
-# signal inputs. HBAT control connect to first element:
-si_nbat_ctl = [signal_index * 3 + x for signal_index in RCUs.RCU2Hctl for x in range(3)]
-#
-rcu_indices = range(32)
-hba_rcu_mask_pwr = [rcui in RCUs.RCU2Hpwr for rcui in rcu_indices]
-hba_rcu_mask_ctrl = [rcui in RCUs.RCU2Hctl for rcui in rcu_indices]
-hba_PwrMask = [
-    [True] * 3 if rcui in RCUs.RCU2Hpwr else [False] * 3 for rcui in rcu_indices
-]
-hba_CtlMask = [
-    [True] * 3 if rcui in RCUs.RCU2Hctl else [False] * 3 for rcui in rcu_indices
-]
-hba_mask = [[True] * 3 if rcui in RCUs.RCU2H else [False] * 3 for rcui in rcu_indices]
-
-folders = [PLOT_DIR]
-for folder in folders:
-    if not os.path.exists(folder):
-        os.makedirs(folder)
-
-
-def init_and_set_to_default_config(devices, skip_boot=False, silent=False):
-    """
-    Restart system and set to default configuration
-
-    Input arguments:
-    devices = struct of software devices of m&c of the station
-              name: object
-
-    Output arguments:
-    found_error = True if an error ocurred, False otherwise
-    """
-    found_error = False
-    # initialisation
-    if not skip_boot:
-        if not silent:
-            plib.log("Start initialisation")
-        found_error = found_error | initialise(devices)
-    else:
-        plib.log("SKIPPING INITIALISATION (skip_init was set to True)")
-    if not silent:
-        plib.log("Check initialisation")
-    found_error = found_error | check_initialise(devices)
-    # configuring
-    if not silent:
-        plib.log("Start setting to default configuration")
-    found_error = found_error | set_to_default_configuration(devices, silent=silent)
-    if not silent:
-        plib.log("Check setting to default configuration")
-    found_error = found_error | check_set_to_default_configuration(
-        devices, readonly=False
-    )  # readonly=False, only at first time after init
-    # plot statistics data
-    if not silent:
-        plib.log("Plot statistics data")
-    plot_statistics(devices)
-    return found_error
-
-
-def initialise(devices, timeout=90, debug_flag=False):
-    """
-    Initialise the system
-
-    Input arguments:
-    timeout = timeout to flag an error in seconds
-
-    Output arguments:
-    found_error
-    """
-    devices = devices_list_2_dict(devices)
-    boot = devices["boot"]
-    recv = devices["recv"]
-    sst = devices["sst"]
-    unb2 = devices["unb2"]
-    # initialise boot device
-    boot.put_property({"DeviceProxy_Time_Out": 60})
-    boot.off()
-    boot.initialise()
-    boot.on()
-    # reboot system
-    # increase timeouts for that
-    if debug_flag:
-        print("Time out settings prior to increase for reboot:")
-        print(recv.get_timeout_millis())
-        print(unb2.get_timeout_millis())
-        print(sst.get_timeout_millis())
-    recv.set_timeout_millis(30000)
-    unb2.set_timeout_millis(60000)
-    sst.set_timeout_millis(20000)
-    unb2.put_property({"UNB2_On_Off_timeout": 20})
-    recv.put_property({"RCU_On_Off_timeout": 60})
-    # and start reboot process
-    boot.reboot()
-    print("Rebooting now")
-    time_start = time.time()
-    time_dt = time.time() - time_start
-    # indicate progress
-    while boot.booting_R and (time_dt < timeout):
-        if time_dt % 5 < 1:  # print every 5 seconds
-            print(f"Initialisation at {boot.progress_R}%: {boot.status_R}")
-        time.sleep(1)
-        time_dt = time.time() - time_start
-    print(f"Initialisation took {time_dt} seconds")
-    # check for errors
-    found_error = False
-    if boot.booting_R:
-        found_error = True
-        print(f"Warning! Initialisation still ongoing after timeout ({timeout} sec)")
-    if boot.uninitialised_devices_R:
-        found_error = True
-        print(f"Warning! Did not initialise {boot.uninitialised_devices_R}.")
-    return found_error
-
-
-def check_initialise(devices):
-    """
-    Check the initialisation of the system by an independent monitoring
-    point.
-
-    Note that this method should be used after calling initialise()
-
-    All checks that are directly related to the boot device are already
-    in the method initialise()
-    """
-    found_error = False
-    found_error = found_error | check_firmware_loaded(devices)
-    found_error = found_error | check_clock_locked(devices)
-    return found_error
-
-
-def check_firmware_loaded(devices, debug_flag=True):
-    """
-    Verify that the firmware has loaded in the fpgas
-    Returns True if not loaded, False otherwise
-    """
-    devices = devices_list_2_dict(devices)
-    sdp = devices["sdp"]
-    res = sdp.FPGA_boot_image_R
-    if debug_flag:
-        print("")
-        print("Checking sdp.FPGA_boot_image_R:")
-        print(f"Reply: {res}")
-    if not np.array_equal(res[0:N_FPGA], np.ones(N_FPGA)):
-        print("Firmware is not loaded!")
-        return True
-    if debug_flag:
-        print("Ok - Firmware loaded")
-    return False
-
-
-def check_clock_locked(devices, debug_flag=True):
-    """
-    Verify that the APSCT clock is locked
-    Returns True if not loaded, False otherwise
-    """
-    devices = devices_list_2_dict(devices)
-    apsct = devices["apsct"]
-    res = apsct.APSCT_PLL_200MHz_error_R
-    if debug_flag:
-        print("")
-        print("Checking apsct.APSCT_PLL_200MHz_error_R:")
-        print(f"Reply: {res}")
-    if res:
-        print("APSCT clock not locked!")
-        return True
-    if debug_flag:
-        print("Ok - APSCT clock locked")
-    return False
-
-
-def set_to_default_configuration(devices, silent=True):
-    """
-    Set the station to its default configuration
-    Returns True if error found, False otherwise
-    """
-    found_error = False
-    if not silent:
-        plib.log("Start configuring Low Receivers")
-    found_error = found_error | set_rcul_to_default_config(devices)
-    if not silent:
-        plib.log("Start configuring High Receivers")
-    found_error = found_error | set_rcuh_to_default_config(devices)
-    if not silent:
-        plib.log("Start configuring Station Digital Processor")
-    found_error = found_error | set_sdp_to_default_config(devices)
-    if not silent:
-        plib.log("Start configuring Subband Statistics")
-    found_error = found_error | set_sdp_sst_to_default_config(devices)
-    if not silent:
-        plib.log("Start configuring Crosslet Statistics")
-    found_error = found_error | set_sdp_xst_to_default_config(devices)
-    if not silent:
-        plib.log("Start configuring Beamlet Statistics")
-    found_error = found_error | set_sdp_bst_to_default_config(devices)
-    return found_error
-
-
-def set_rcul_to_default_config(devices):
-    """
-    Set RCU Low to its default configuration
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    #
-    # Set RCUs Lows in default modes
-    #
-
-    recv.set_defaults()
-
-    # Set attenuator, antenna power etc.
-    recv.RCU_band_select_RW = set_ant_masked(
-        recv, LBA_MASK, recv.RCU_band_select_R, [[1] * 3] * 32
-    )  # 1 = 30-80 MHz
-    recv.RCU_PWR_ANT_on_RW = set_ant_masked(
-        recv, LBA_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32
-    )  # Off
-    recv.RCU_attenuator_dB_RW = set_ant_masked(
-        recv, LBA_MASK, recv.RCU_attenuator_dB_R, [[0] * 3] * 32
-    )  # 0dB attenuator
-    wait_receiver_busy(recv)
-
-    #
-    found_error = False
-    return found_error
-
-
-def set_rcuh_to_default_config(devices):
-    """
-    Set RCU High to its default configuration
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    #
-    # Setup HBA RCUs and HBA Tiles
-    #
-
-    # Set RCU in correct mode
-    rcu_modes = recv.RCU_band_select_R
-    rcu_modes[8] = [2, 2, 2]  # 2 = 110-180 MHz, 1 = 170-230 MHz, 4 = 210-270 MHz ??
-    rcu_modes[9] = [2, 2, 2]
-    recv.RCU_band_select_RW = set_ant_masked(
-        recv, hba_mask, recv.RCU_band_select_R, rcu_modes
-    )
-    wait_receiver_busy(recv)
-
-    # Switch on the Antenna
-    recv.RCU_PWR_ANT_on_RW = set_ant_masked(
-        recv, hba_PwrMask, recv.RCU_PWR_ANT_on_R, [[False] * 3] * 32
-    )  # Switch off
-    wait_receiver_busy(recv)
-    recv.HBAT_PWR_LNA_on_RW = set_hba_mask(
-        recv, hba_CtlMask, recv.HBAT_PWR_LNA_on_R, [[True] * 32] * 96
-    )  # LNA default on
-    recv.HBAT_BF_delay_steps_RW = set_hba_mask(
-        recv, hba_CtlMask, recv.HBAT_BF_delay_steps_R, [[0] * 32] * 96
-    )  # Default
-    recv.HBAT_PWR_on_RW = set_hba_mask(
-        recv, hba_CtlMask, recv.HBAT_PWR_on_R, [[True] * 32] * 96
-    )  # Default
-    wait_receiver_busy(recv)
-    recv.RCU_PWR_ANT_on_RW = set_ant_masked(
-        recv, hba_PwrMask, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32
-    )  # Switch on
-    wait_receiver_busy(recv)
-    # default in the tile: after power-on, zero delay
-    # delays should be set by the tile beam device
-    # recv.HBAT_BF_delay_steps_RW=[[1]*32]*32
-    # wait_receiver_busy(recv)
-    # by default: equal delay settings for all elements --> Pointing to zenith
-
-    # TODO: read values and only update the ones that need to be changed.
-    # Could be a function that manages the masks.
-    print(recv.RCU_PWR_ANT_on_RW)
-    print()
-    print("done")
-
-    #
-    found_error = False
-    return found_error
-
-
-####
-# RCU specific functions and variables for DTS-Outside
-####
-
-# General RCU functions
-
-
-def wait_receiver_busy(recv):
-    """
-    Wait for the Receiver Translators busy monitoring point to return False
-    """
-    time.sleep(0.5)
-    while recv.RECVTR_translator_busy_R:
-        time.sleep(0.1)
-
-
-def set_ant_masked(recv, mask, old_value, new_value):
-    """
-    Set the antenna mask for the Receiver Translator
-    """
-    recv.ANT_mask_RW = mask
-
-    for imask, maski in enumerate(mask):
-        for rcu_input_index in range(3):
-            if maski[rcu_input_index]:
-                old_value[imask][rcu_input_index] = new_value[imask][rcu_input_index]
-    # this used to be the following one liner, but that line was too long.
-    # Here is it, but cut in pieces:
-    # old_value[imask] =
-    # [new_value[imask][j] if maski[j] else old_value[imask][j] for j in range(3)]
-    return old_value
-
-
-def set_hba_mask(recv, mask, old_value, new_value):
-    """
-    Set the hba mask for the Receiver Translator
-    """
-    # TODO: Is this function equal to set_ant_masked()? Can this definition be removed?
-    recv.ANT_mask_RW = mask
-    for imask, maski in enumerate(mask):
-        for i in range(3):
-            if maski[i]:
-                old_value[imask * 3 + i] = new_value[imask * 3 + i]
-            else:
-                old_value[imask * 3 + i] = old_value[imask * 3 + i]
-    return old_value
-
-
-####
-# Till here RCU specific functions and variables for DTS-Outside
-####
-
-
-def set_sdp_to_default_config(devices):
-    """
-    Set RCU High to its default configuration
-    """
-    devices = devices_list_2_dict(devices)
-    sdp = devices["sdp"]
-    #
-    # Set SDP in default modes
-    #
-
-    plib.log("Start configuring SDP to default")
-    sdp.set_defaults()
-    # should be part of sdp.set_defaults:
-    next_ring = [False] * 16
-    next_ring[3] = True
-    sdp.FPGA_ring_use_cable_to_previous_rn_RW = [True] + [False] * 15
-    sdp.FPGA_ring_use_cable_to_next_rn_RW = next_ring
-    sdp.FPGA_ring_nof_nodes_RW = [N_FPGA] * 16
-    sdp.FPGA_ring_node_offset_RW = [0] * 16
-
-    #
-    found_error = False
-    return found_error
-
-
-def set_sdp_sst_to_default_config(devices):
-    """
-    Set SSTs to default
-    """
-    devices = devices_list_2_dict(devices)
-    sst = devices["sst"]
-
-    sst.set_defaults()
-    # prepare for subband stati
-    sst.FPGA_sst_offload_weighted_subbands_RW = [
-        True
-    ] * 16  # should be in set_defaults()
-
-    #
-    found_error = False
-    return found_error
-
-
-def set_sdp_xst_to_default_config(devices):
-    """
-    Set XSTs to default
-    """
-    devices = devices_list_2_dict(devices)
-    xst = devices["xst"]
-
-    # prepare for correlations
-    int_time = 1  # used for 'medium' measurement using statistics writer
-    subband_step_size = 7  # 0 is no stepping
-    n_xst_subbands = 7
-    subband_select = [subband_step_size, 0, 1, 2, 3, 4, 5, 6]
-
-    xst.set_defaults()
-    # should be part of set_defaults()
-    xst.FPGA_xst_processing_enable_RW = [False] * 16
-    # this (line above) should be first, then configure and enable
-    xst.fpga_xst_ring_nof_transport_hops_RW = [3] * 16  # [((n_fgpa/2)+1)]*16
-
-    crosslets = [0] * 16
-    for fpga_nr in range(N_FPGA):
-        crosslets[fpga_nr] = n_xst_subbands
-    xst.FPGA_xst_offload_nof_crosslets_RW = crosslets
-
-    xst.FPGA_xst_subband_select_RW = [subband_select] * 16
-    xst.FPGA_xst_integration_interval_RW = [int_time] * 16
-
-    xst.FPGA_xst_processing_enable_RW = [True] * 16
-    #
-    found_error = False
-    return found_error
-
-
-def set_sdp_bst_to_default_config(devices):
-    """
-    Set XSTs to default
-    """
-    devices = devices_list_2_dict(devices)
-    beamlet = devices["beamlet"]
-
-    # prepare for beamformer
-    beamlet.set_defaults()
-
-    beamlet.FPGA_bf_ring_nof_transport_hops_RW = [[1]] * 3 + [[0]] * 13
-    # line above should be part of set_defaults(), specifically for DTS-Outside
-
-    beamletoutput_mask = [False] * 16
-    beamletoutput_mask[3] = True
-    # beamlet.FPGA_beamlet_output_enable_RW=beamletoutput_mask
-    beamlet.FPGA_beamlet_output_hdr_eth_destination_mac_RW = ["40:a6:b7:2d:4f:68"] * 16
-    beamlet.FPGA_beamlet_output_hdr_ip_destination_address_RW = ["192.168.0.249"] * 16
-
-    # define weights
-    # dim: FPGA, input, subband
-    weights_xx = beamlet.FPGA_bf_weights_xx_R.reshape([16, 6, 488])
-    weights_yy = beamlet.FPGA_bf_weights_yy_R.reshape([16, 6, 488])
-    # make a beam for the HBA
-    weights_xx[:][:] = 0
-    weights_yy[:][:] = 0
-    for signal_index in [0, 1]:  # range(8*3,9*3+3):
-        unb2i = signal_index // 12
-        signal_index_2 = signal_index // 2
-        signal_index_2 = signal_index_2 % 6
-        if signal_index % 2 == 0:
-            weights_xx[unb2i, signal_index_2] = 16384
-        else:
-            weights_yy[unb2i, signal_index_2] = 16384
-
-    beamlet.FPGA_bf_weights_xx_RW = weights_xx.reshape([16, 6 * 488])
-    beamlet.FPGA_bf_weights_yy_RW = weights_yy.reshape([16, 6 * 488])
-
-    blt = beamlet.FPGA_beamlet_subband_select_R.reshape([16, 12, 488])
-    blt[:] = np.array(range(10, 10 + 488))[np.newaxis, np.newaxis, :]
-    beamlet.FPGA_beamlet_subband_select_RW = blt.reshape([16, 12 * 488])
-
-    #
-    found_error = False
-    return found_error
-
-
-def enable_outputs(
-    devices, enable_sst=True, enable_xst=True, enable_bst=True, enable_beam_output=True
-):
-    """
-    Enable the station outputs after everything is set.
-    """
-    devices = devices_list_2_dict(devices)
-    if enable_beam_output:
-        sdp = devices["sdp"]
-        sdp.FPGA_processing_enable_RW = [True] * 16
-    if enable_sst:
-        sst = devices["sst"]
-        sst.FPGA_sst_offload_enable_RW = [True] * 16
-    if enable_xst:
-        xst = devices["xst"]
-        xst.FPGA_xst_offload_enable_R = [True] * 16
-    if enable_bst:
-        beamlet = devices["beamlet"]
-        beamlet.FPGA_beamlet_output_enable_RW = [i == 3 for i in range(16)]
-
-    #
-    found_error = False
-    return found_error
-
-
-def check_set_to_default_configuration(devices, readonly=True):
-    """
-    Check the default configuration of the system to be set correctly.
-    """
-    found_error = False
-    found_error = found_error | check_default_configuration_low_receivers(devices)
-    found_error = found_error | check_default_configuration_hbat(
-        devices, readonly=readonly
-    )
-    found_error = found_error | check_rcu_fpga_interface(devices, si_to_check=range(18))
-    found_error = found_error | check_set_sdp_xst_to_default_config(
-        devices, lane_mask=range(0, 6)
-    )
-
-    print_fpga_input_statistics(devices)
-
-    return found_error
-
-
-def check_default_configuration_low_receivers(devices, mask=None, debug_flag=True):
-    """
-    Check that the LNAs are on
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    found_error = False
-
-    if mask is None:
-        mask = LBA_MASK
-
-    res = recv.RCU_PWR_ANT_on_R
-    if len(mask) > 0:
-        res = res[mask]
-    if debug_flag:
-        print("")
-        print("Checking recv.RCU_PWR_ANT_on_R:")
-        print(f"Reply{' (masked)' if len(mask) > 0 else ''}: {res}")
-    if not (res).all():
-        print("One or more LBAs are powered off!")
-        found_error = True
-    else:
-        if debug_flag:
-            print("Ok - Low Receivers operational")
-    return found_error
-
-
-def check_default_configuration_hbat(devices, readonly=True):
-    """
-    Check that the HBA Tiles are on
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    HBATsi = [rcu * 3 + r_input for rcu in RCUs.RCU2Hpwr for r_input in range(3)]
-    if not readonly:
-        recv.RECVTR_monitor_rate_RW = 1  # for this checking
-        time.sleep(3)
-
-    found_error = False
-    found_error = found_error | check_rcu_vin(devices, si_mask=HBATsi)
-    found_error = found_error | check_hbat_vout(devices, si_mask=HBATsi)
-    found_error = found_error | check_hbat_iout(devices, si_mask=HBATsi)
-
-    if not readonly:
-        recv.RECVTR_monitor_rate_RW = 10
-
-    # TODO - what is checked for here?
-    print(recv.HBAT_PWR_LNA_on_R[8:10])
-    print(recv.ANT_mask_RW[8:10])
-
-    return found_error
-
-
-def check_rcu_vin(devices, si_mask=None, valid_range=None, debug_flag=True):
-    """
-    Verify that the HBA Tile is powered on by comparing the V_in monitoring
-    point on the RCU against the thresholds for the given signal inputs.
-
-    If no si_mask is given, all returned values are compared.
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    if si_mask is None:
-        si_mask = []
-    if valid_range is None:
-        valid_range = [30, 50]
-
-    res = recv.RCU_PWR_ANT_VIN_R
-    if len(si_mask) > 0:
-        res = [res[si // 3][si % 3] for si in si_mask]
-    if debug_flag:
-        print("")
-        print("Checking recv.RCU_PWR_ANT_VIN_R:")
-        print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}")
-    if not (
-        np.all(valid_range[0] < np.array(res))
-        and np.all(np.array(res) < valid_range[1])
-    ):
-        print("No power at input RCU2!")
-        return True
-    if debug_flag:
-        print("Ok - Power at input RCU2")
-    return False
-
-
-def check_hbat_vout(devices, si_mask=None, valid_range=None, debug_flag=True):
-    """
-    Verify that the HBA Tile is powered on by comparing the V_out monitoring
-    point on the RCU against the thresholds for the given signal inputs.
-
-    If no si_mask is given, all returned values are compared.
-    """
-    # Vout should be 48 +- 1 V
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    if si_mask is None:
-        si_mask = []
-    if valid_range is None:
-        valid_range = [30, 50]
-
-    res = recv.RCU_PWR_ANT_VOUT_R
-    if len(si_mask) > 0:
-        res = [res[si // 3][si % 3] for si in si_mask]
-    if debug_flag:
-        print("")
-        print("Checking recv.RCU_PWR_ANT_VOUT_R:")
-        print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}")
-    if not (
-        np.all(valid_range[0] < np.array(res))
-        and np.all(np.array(res) < valid_range[1])
-    ):
-        print("Not all HBA Tiles powered on!")
-        return True
-    if debug_flag:
-        print("Ok - HBA Tiles powered on")
-    return False
-
-
-def check_hbat_iout(devices, si_mask=None, valid_range=None, debug_flag=True):
-    """
-    Verify that the HBA Tile is powered on by comparing the I_out monitoring
-    point on the RCU against the thresholds for the given signal inputs.
-
-    If no si_mask is given, all returned values are compared.
-    """
-    # Iout = 0.7 +- 0.1 Amp
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    if si_mask is None:
-        si_mask = []
-    if valid_range is None:
-        valid_range = [0.6, 0.8]
-
-    res = recv.RCU_PWR_ANT_IOUT_R
-    if len(si_mask) > 0:
-        res = [res[si // 3][si % 3] for si in si_mask]
-    if debug_flag:
-        print("")
-        print("Checking recv.RCU_PWR_ANT_IOUT_R:")
-        print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}")
-    if not (
-        np.all(valid_range[0] < np.array(res))
-        and np.all(np.array(res) < valid_range[1])
-    ):
-        print("Not all HBA Tiles powered on (draw current)!")
-        return True
-    if debug_flag:
-        print("Ok - HBA Tiles powered on (draw current)")
-    return False
-
-
-def check_rcu_fpga_interface(devices, si_to_check=range(18)):
-    """
-    Function to check the RCU - FPGA interface
-
-    # if error occurs, try to reboot by resetting Uniboard (switch)
-    # if only one works than the APSCT board is not making contact with the backplane.
-    # (Can take up to 3 times to get it right..)
-    # 23/6/2022T15:42 errorcode 0x4h: xxxxx on all of them.
-    # Error descriptions can be found here:
-    # https://support.astron.nl/jira/browse/L2SDP-774
-    """
-    devices = devices_list_2_dict(devices)
-    sdp = devices["sdp"]
-    found_error = False
-    # TODO: the number of calls can be reduced
-    for signal_index in si_to_check:
-        lock = sdp.FPGA_jesd204b_csr_dev_syncn_R[signal_index // 12][signal_index % 12]
-        if lock != 1:
-            found_error = True
-            print(f"Transceiver SI: {signal_index} is not locked {lock}")
-        err0 = sdp.FPGA_jesd204b_rx_err0_R[signal_index // 12][signal_index % 12]
-        if err0 != 0:
-            found_error = True
-            print(f"SI {signal_index} has JESD err0: 0x{err0:x}h")
-        err1 = sdp.FPGA_jesd204b_rx_err1_R[signal_index // 12][signal_index % 12]
-        if (err1 & 0x02FF) != 0:
-            found_error = True
-            print(f"SI {signal_index} has JESD err1: 0x{err1:x}h")
-    return found_error
-
-
-def check_set_sdp_xst_to_default_config(
-    devices, lane_mask=None, time_1=None, time_dt=2.5, debug_flag=True
-):
-    """
-    Check sdp/xst configuration by checking the returned timestamps for:
-    1. all being the same
-    2. not epoch zero (at 1 Jan 1970 00:00:00)
-    3. within 5 seconds from the local clock timestamp (now +/- 2.5 sec)
-
-    Input arguments:
-    time_1 = number of seconds since the epoch to compare against
-             Default: None --> now()
-    time_dt = delta in seconds around time_1, in which the xst timestamps should fall.
-             Default: 2.5 --> p/m 2.5 sec
-    """
-    devices = devices_list_2_dict(devices)
-    xst = devices["xst"]
-    if lane_mask is None:
-        lane_mask = []
-    if time_1 is None:
-        time_1 = datetime.datetime.now().timestamp()
-    res = xst.xst_timestamp_R
-    if len(lane_mask) > 0:  # mask lanes
-        res = [res[lane] for lane in lane_mask]
-    if debug_flag:
-        print("")
-        print("Checking xst.xst_timestamp_R:")
-        print(f"Reply{' (masked)' if len(lane_mask) > 0 else ''}: {res}")
-        print("Converted:")
-        print("\n".join([f"{time.asctime(time.gmtime(xst_time))}" for xst_time in res]))
-    if not (res == res[0]).all():
-        print("Not all xst timestamps are equal!")
-        return True
-    if res[0] == 0:
-        print("One or more xst timestamps are zero!")
-        return True
-    if abs(res[0] - time_1) > time_dt:
-        print("One or more xst timestamps are outside the specified time window!")
-        return True
-    if debug_flag:
-        print("Ok - xst configured correctly")
-    return False
-
-
-def print_fpga_input_statistics(devices):
-    """
-    Print FPGA input statistics
-    """
-    devices = devices_list_2_dict(devices)
-    sdp = devices["sdp"]
-
-    # get data from device
-    dc_offset = sdp.FPGA_signal_input_mean_R
-    signal_input_rms = sdp.FPGA_signal_input_rms_R
-    lock_to_adc = sdp.FPGA_jesd204b_csr_dev_syncn_R
-    err0 = sdp.FPGA_jesd204b_rx_err0_R
-    err1 = sdp.FPGA_jesd204b_rx_err1_R
-    count = sdp.FPGA_jesd204b_csr_rbd_count_R
-    signal_index = np.reshape(np.arange(count.shape[0] * count.shape[1]), count.shape)
-    # get some info on the hardware connections
-    pn_index = plib.signal_index_2_processing_node_index(signal_index)
-    pn_input_index = plib.signal_index_2_processing_node_input_index(signal_index)
-    rcu_index = plib.signal_index_2_rcu_index(signal_index)
-    rcu_input_index = plib.signal_index_2_rcu_input_index(signal_index)
-    # print
-    print("+------------+-------------+----------------------------------------------+")
-    print("|   Input    | Processed by|             Statistics of input              |")
-    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
-    print("|RCU|RCU| SI | Node | Node | DC      | RMS     | Lock| Err0 | Err1 | Count|")
-    print("|idx|inp|    | Index| Input| Offset  |   (LSB) |     | (0x) | (0x) | (0x) |")
-    print("|   |idx|    |      | Index|   (LSB) |         |     |      |      |      |")
-    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
-    pfmt = "| %02d| %1d |%3d |  %02d  |  %2d  | %7.2f | %7.2f | %3s | %04x | %04x | %04x |"
-    for rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt in zip(
-        rcu_index.flatten(),
-        rcu_input_index.flatten(),
-        signal_index.flatten(),
-        pn_index.flatten(),
-        pn_input_index.flatten(),
-        dc_offset.flatten(),
-        signal_input_rms.flatten(),
-        lock_to_adc.flatten(),
-        err0.flatten(),
-        err1.flatten(),
-        count.flatten(),
-    ):
-        print(
-            pfmt % (rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt)
-        )
-    print("+---+---+----+------+------+---------+---------+-----+------+------+------+")
-
-
-def report_setup_configuration(devices, save_fig=True, debug_flag=False):
-    """
-    Read name and version data from the setup and print that as a report
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    apsct = devices["apsct"]
-    apspu = devices["apspu"]
-    unb2 = devices["unb2"]
-    sdp = devices["sdp"]
-    # Receiver / High Band Antenna Tile
-
-    # APS Hardware
-    # APS / Receiver Unit2 side
-    rcu2_pcb_ids = recv.RCU_PCB_ID_R.tolist()
-    rcu2_pcb_nrs = list(recv.RCU_PCB_number_R)
-    rcu2_pcb_vrs = list(recv.RCU_PCB_version_R)
-
-    # APS / UniBoard2 side
-    # TODO, FIXME - shouldn't this be a list with multiple entries?
-    # See also LOFAR2-10774 (APSCT)
-    apsct_pcb_ids = [apsct.APSCT_PCB_ID_R]
-    apsct_pcb_nrs = [apsct.APSCT_PCB_number_R]
-    apsct_pcb_vrs = [apsct.APSCT_PCB_version_R]
-    # TODO, FIXME - shoudln't this be a list with multiple entries?
-    # See also LOFAR2-11434 (APSPU)
-    apspu_pcb_ids = [apspu.APSPU_PCB_ID_R]
-    apspu_pcb_nrs = [apspu.APSPU_PCB_number_R]
-    apspu_pcb_vrs = [apspu.APSPU_PCB_version_R]
-    unb2_pcb_ids = unb2.UNB2_PCB_ID_R.tolist()
-    unb2_pcb_nrs = list(unb2.UNB2_PCB_number_R)
-    unb2_pcb_vrs = list(sdp.FPGA_hardware_version_R)
-    # TODO, FIXME:
-    # FPGA_hardware_version_R returns hardware version info per FPGA. Hardware is shared
-    # per 4 fpgas, so why repeating values here?
-    unb2_pcb_vrs = [unb2_pcb_vrs[idx] for idx in [0, 4, 8, 12]]
-
-    # TODO, FIXME:
-    # fixing list lengths for a large station with multiple APS
-    # this makes sure that the list indexing below works
-    n_aps = 4
-    apsct_pcb_ids = apsct_pcb_ids + [0] * (n_aps - len(apsct_pcb_ids))
-    apsct_pcb_nrs = apsct_pcb_nrs + [""] * (n_aps - len(apsct_pcb_nrs))
-    apsct_pcb_vrs = apsct_pcb_vrs + [""] * (n_aps - len(apsct_pcb_vrs))
-    apspu_pcb_ids = apspu_pcb_ids + [0] * (n_aps * 2 - len(apspu_pcb_ids))
-    apspu_pcb_nrs = apspu_pcb_nrs + [""] * (n_aps * 2 - len(apspu_pcb_nrs))
-    apspu_pcb_vrs = apspu_pcb_vrs + [""] * (n_aps * 2 - len(apspu_pcb_vrs))
-    unb2_pcb_ids = unb2_pcb_ids + [0] * (n_aps * 2 - len(unb2_pcb_ids))
-    unb2_pcb_nrs = unb2_pcb_nrs + [""] * (n_aps * 2 - len(unb2_pcb_nrs))
-    unb2_pcb_vrs = unb2_pcb_vrs + [""] * (n_aps * 2 - len(unb2_pcb_vrs))
-
-    if debug_flag:
-        print("APSPU:")
-        print(f"apsct_pcb_ids: {apsct_pcb_ids}")
-        print(f"apsct_pcb_nrs: {apsct_pcb_nrs}")
-        print(f"apsct_pcb_vrs: {apsct_pcb_vrs}")
-        print("APSPU:")
-        print(f"apspu_pcb_ids: {apspu_pcb_ids}")
-        print(f"apspu_pcb_nrs: {apspu_pcb_nrs}")
-        print(f"apspu_pcb_vrs: {apspu_pcb_vrs}")
-        print("UNB2:")
-        print(f"unb2_pcb_ids: {unb2_pcb_ids}")
-        print(f"unb2_pcb_nrs: {unb2_pcb_nrs}")
-        print(f"unb2_pcb_vrs: {unb2_pcb_vrs}")
-
-    aps_pcb_ids = [
-        apspu_pcb_ids[1],
-        unb2_pcb_ids[0],
-        apsct_pcb_ids[0],
-        apspu_pcb_ids[0],
-        unb2_pcb_ids[1],
-    ]
-    aps_pcb_nrs = [
-        apspu_pcb_nrs[1],
-        unb2_pcb_nrs[0],
-        apsct_pcb_nrs[0],
-        apspu_pcb_nrs[0],
-        unb2_pcb_nrs[1],
-    ]
-    aps_pcb_vers = [
-        apsct_pcb_vrs[1],
-        unb2_pcb_vrs[0],
-        apsct_pcb_vrs[0],
-        apsct_pcb_vrs[0],
-        unb2_pcb_vrs[1],
-    ]
-
-    if not save_fig:
-        fig_filenames = []
-    else:
-        fig_filenames = [
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.png',
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.pdf',
-        ]
-    plot_aps_configuration(
-        rcu2_pcb_ids,
-        rcu2_pcb_nrs,
-        rcu2_pcb_vrs,
-        aps_pcb_ids,
-        aps_pcb_nrs,
-        aps_pcb_vers,
-        filenames=fig_filenames,
-    )
-
-    # Services
-    # Firmware images
-    firmware_versions = list(sdp.FPGA_firmware_version_R)
-    # make sure there are versions for at least n_aps subracks
-    firmware_versions = firmware_versions + [""] * (n_aps * 8 - len(firmware_versions))
-    aps_services = {}
-    for apsi, fpgai in zip(sorted([i for i in range(n_aps)] * 8), range(n_aps * 8)):
-        aps_services["aps%i-fpga%02d" % (apsi, fpgai)] = firmware_versions[fpgai]
-        aps_services["aps%i-unb2tr" % apsi] = ""  # TODO
-        aps_services["aps%i-recvtr" % apsi] = ""  # TODO
-        aps_services["aps%i-apscttr" % apsi] = ""  # TODO
-        aps_services["aps%i-apsputr" % apsi] = ""  # TODO
-    # Translators
-    sdptr_software_versions = [sdp.TR_software_version_R]
-    sdptr_software_versions = sdptr_software_versions + [""] * (
-        2 - len(sdptr_software_versions)
-    )
-    station_services = {}
-    station_services["sdptr0"] = sdptr_software_versions[0]
-    station_services["sdptr1"] = sdptr_software_versions[1]
-    station_services["ccdtr"] = ""  # TODO, currently missing
-    # station control tango devices
-    for dev in devices:
-        station_services[devices[dev].name().lower()] = devices[dev].version_R
-    if not save_fig:
-        figs = []
-    else:
-        figs = [
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_station_services.png',
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_station_services.pdf',
-        ]
-    plot_services_configuration(aps_services, station_services, filenames=figs)
-
-
-def plot_aps_configuration(
-    rcu_pcb_identifiers,
-    rcu_pcb_numbers,
-    rcu_pcb_versions,
-    aps_pcb_ids,
-    aps_pcb_nrs,
-    aps_pcb_vers,
-    filenames=[f"{PLOT_DIR}/aps_config.png", f"{PLOT_DIR}/aps_config.pdf"],
-):
-    """
-    Plot the configuration of the Antenna Processing Subrack, both sides.
-    """
-
-    fig, axs = plt.subplots(1, 2, figsize=(20, 8))
-
-    plot_aps_rcu2_configuration(
-        axs[0], rcu_pcb_identifiers, rcu_pcb_numbers, rcu_pcb_versions
-    )
-    plot_aps_unb2_configuration(axs[1], aps_pcb_ids, aps_pcb_nrs, aps_pcb_vers)
-    for filename in filenames:
-        fig.savefig(filename)
-    fig.show()
-
-
-def plot_aps_rcu2_configuration(
-    plot_ax,
-    rcu_pcb_identifiers,
-    rcu_pcb_numbers,
-    rcu_pcb_versions,
-    rcu_locations=APS_LOCATION_LABELS,
-    rcu_pos_x=None,
-    rcu_pos_y=None,
-):
-    """
-    Plot the configuration of the Antenna Processing Subrack, RCU2 side.
-    """
-    if rcu_pos_x is None:
-        rcu_pos_x = APS_RCU_POS_X
-    if rcu_pos_y is None:
-        rcu_pos_y = APS_RCU_POS_Y
-    plot_ax.plot(0, 0)
-    plot_dx = plot_dy = 0.9
-    plot_ax.set_xlim(-1 + plot_dx, np.max(rcu_pos_x) + 1)
-    plot_ax.set_ylim(-1 + plot_dy, np.max(rcu_pos_y) + 1)
-    for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip(
-        rcu_pos_x,
-        rcu_pos_y,
-        rcu_locations,
-        rcu_pcb_identifiers,
-        rcu_pcb_numbers,
-        rcu_pcb_versions,
-    ):
-        plot_ax.plot(
-            [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x],
-            [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y],
-            color="black",
-            alpha=0.2,
-        )
-        plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center")
-        plot_ax.text(
-            plot_x + plot_dx / 2,
-            plot_y + plot_dy / 2,
-            "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr),
-            va="center",
-            ha="center",
-            rotation=90,
-        )
-        if pcb_id == 0 and pcb_v == "" and pcb_nr == "":
-            plot_ax.plot(
-                [plot_x, plot_x + plot_dx],
-                [plot_y, plot_y + plot_dy],
-                color="black",
-                alpha=0.2,
-            )
-            plot_ax.plot(
-                [plot_x + plot_dx, plot_x],
-                [plot_y, plot_y + plot_dy],
-                color="black",
-                alpha=0.2,
-            )
-    plot_ax.set_title("Configuration of RCU2s")
-    plot_ax.set_xticks([])
-    plot_ax.set_yticks([])
-
-
-def plot_aps_unb2_configuration(
-    plot_ax,
-    aps_pcb_identifiers,
-    aps_pcb_numbers,
-    aps_pcb_versions,
-    mod_locations=APS_LOCATION_LABELS,
-    mod_pos_x=None,
-    mod_pos_y=None,
-):
-    """
-    Plot the configuration of the Antenna Processing Subrack, UNB2 side.
-    """
-    if mod_pos_x is None:
-        mod_pos_x = APS_MOD_POS_X
-    if mod_pos_y is None:
-        mod_pos_y = APS_MOD_POS_Y
-    plot_ax.plot(0, 0)
-    plot_dx = 0.9
-    plot_dy = 3.9
-    plot_ax.set_xlim(-1 + plot_dx, np.max(mod_pos_x) + 2)
-    plot_ax.set_ylim(-1 + plot_dx, np.max(mod_pos_y) + 4)
-    for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip(
-        mod_pos_x,
-        mod_pos_y,
-        mod_locations,
-        aps_pcb_identifiers,
-        aps_pcb_numbers,
-        aps_pcb_versions,
-    ):
-        plot_ax.plot(
-            [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x],
-            [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y],
-            color="black",
-            alpha=0.2,
-        )
-        plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center")
-        plot_ax.text(
-            plot_x + plot_dx / 2,
-            plot_y + plot_dy / 2,
-            "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr),
-            va="center",
-            ha="center",
-            rotation=90,
-        )
-        if pcb_id == 0 and pcb_v == "" and pcb_nr == "":
-            plot_ax.plot(
-                [plot_x, plot_x + plot_dx],
-                [plot_y, plot_y + plot_dy],
-                color="black",
-                alpha=0.2,
-            )
-            plot_ax.plot(
-                [plot_x + plot_dx, plot_x],
-                [plot_y, plot_y + plot_dy],
-                color="black",
-                alpha=0.2,
-            )
-    plot_ax.set_title("Configuration of UniBoard2 side")
-    plot_ax.set_xticks([])
-    plot_ax.set_yticks([])
-
-
-def plot_services_configuration(
-    aps_services,
-    station_services,
-    filenames=[f"{PLOT_DIR}/station_services.png", f"{PLOT_DIR}/station_services.pdf"],
-):
-    """
-    Plot the service version information in the setup.
-    """
-
-    fig, plot_ax = plt.subplots(1, 1, figsize=(16, 15))
-
-    aps_service_dx = 0.9
-    aps_service_dy = 0.9
-    first_aps_service = sorted(aps_services)[0][5:]
-    plot_y = plot_y0 = len(aps_services) / 2 + 2
-    for aps_service in sorted(aps_services):
-        plot_x = 0 if (aps_service[3] in "01") else 1
-        plot_y = (
-            plot_y0
-            if (aps_service[5:] == first_aps_service and aps_service[3] in "02")
-            else plot_y - 1
-        )
-        plot_y = (
-            plot_y - 1
-            if (aps_service[5:] == first_aps_service and aps_service[3] in "13")
-            else plot_y
-        )
-        plot_ax.plot(
-            [plot_x, plot_x + aps_service_dx, plot_x + aps_service_dx, plot_x, plot_x],
-            [plot_y, plot_y, plot_y + aps_service_dy, plot_y + aps_service_dy, plot_y],
-            color="black",
-            alpha=0.2,
-        )
-        plot_ax.text(
-            plot_x,
-            plot_y + aps_service_dy / 2,
-            aps_service,
-            va="center",
-            ha="left",
-            color="black",
-        )
-        plot_ax.text(
-            plot_x + aps_service_dx,
-            plot_y + aps_service_dy / 2,
-            aps_services[aps_service],
-            va="center",
-            ha="right",
-            color="blue",
-        )
-        if aps_services[aps_service] == "":
-            plot_ax.plot(
-                [plot_x, plot_x + aps_service_dx],
-                [plot_y, plot_y + aps_service_dy],
-                color="black",
-                alpha=0.2,
-            )
-            plot_ax.plot(
-                [plot_x + aps_service_dx, plot_x],
-                [plot_y, plot_y + aps_service_dy],
-                color="black",
-                alpha=0.2,
-            )
-
-    stat_service_dx = 1.9
-    stat_service_dy = 0.9
-    plot_x = 0
-    plot_y = 0
-    for stat_service in sorted(station_services):
-        plot_ax.plot(
-            [
-                plot_x,
-                plot_x + stat_service_dx,
-                plot_x + stat_service_dx,
-                plot_x,
-                plot_x,
-            ],
-            [
-                plot_y,
-                plot_y,
-                plot_y + stat_service_dy,
-                plot_y + stat_service_dy,
-                plot_y,
-            ],
-            color="black",
-            alpha=0.2,
-        )
-        plot_ax.text(
-            plot_x,
-            plot_y + stat_service_dy / 2,
-            stat_service,
-            va="center",
-            ha="left",
-            color="black",
-        )
-        plot_ax.text(
-            plot_x + stat_service_dx,
-            plot_y + stat_service_dy / 2,
-            station_services[stat_service],
-            va="center",
-            ha="right",
-            color="blue",
-        )
-        if station_services[stat_service] == "":
-            plot_ax.plot(
-                [plot_x, plot_x + stat_service_dx],
-                [plot_y, plot_y + stat_service_dy],
-                color="black",
-                alpha=0.2,
-            )
-            plot_ax.plot(
-                [plot_x + stat_service_dx, plot_x],
-                [plot_y, plot_y + stat_service_dy],
-                color="black",
-                alpha=0.2,
-            )
-        plot_y -= 1
-    plot_ax.set_title("Services in setup")
-    plot_ax.set_xticks([])
-    plot_ax.set_yticks([])
-
-    for filename in filenames:
-        fig.savefig(filename)
-    fig.show()
-
-
-def plot_statistics(devices, save_fig=True, silent=False):
-    """
-    Plot the statistics: SST, XST and BST
-    """
-    if not silent:
-        plib.log("Plot subband statistics")
-    if not save_fig:
-        figs = []
-    else:
-        figs = [
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_subband_statistics.png',
-            f'{PLOT_DIR}/{plib.get_timestamp("filename")}_subband_statistics.pdf',
-        ]
-    plot_subband_statistics(devices, filenames=figs)
-    # TODO:
-    # if not silent:
-    #     plib.log("Plot crosslet statistics")
-    # TODO:
-    # if not silent:
-    #     plib.log("Plot beamlet statistics")
-
-
-def plot_subband_statistics(
-    devices,
-    xlim=None,
-    ylim=[-105, -30],
-    filenames=[
-        f"{PLOT_DIR}/subband_statistics.png",
-        f"{PLOT_DIR}/subband_statistics.pdf",
-    ],
-):
-    """
-    Make the subband statistics plot
-    """
-    if xlim is None:
-        xlim = [-5, 305]
-    if ylim is None:
-        ylim = [-105, -30]
-    sst_data, band_names, frequency_axes = get_subband_statistics(devices)
-
-    fig, axs = plt.subplots(1, 1, figsize=(18, 6))
-    for signal_index in range(len(sst_data)):
-        if band_names[signal_index] is None:
-            continue
-        freq = frequency_axes[band_names[signal_index]]
-        # plot data in dB full scale
-        plot_data = 10 * np.log10(sst_data[signal_index, :] + 1) - 128 - 6 * 4
-        # since we're lacking antenna names,
-        # show the RCU input info as label
-        rcu_index, rcu_input_index = plib.signal_input_2_rcu_index_and_rcu_input_index(
-            signal_index
-        )
-        label = "RCU%02d, Inp%d" % (rcu_index, rcu_input_index)
-        plt.plot(freq / 1e6, plot_data, label=label)
-    plt.grid()
-    plt.legend(bbox_to_anchor=(1, 1), loc="upper left")
-    plt.xlim(xlim)
-    plt.ylim(ylim)
-    plt.xlabel("Frequency (MHz)")
-    plt.ylabel("Power (dB full scale)")
-    plt.suptitle("Subband Statistics for all fully-connected receivers")
-    plt.title(f"{STATION_NAME}; {plib.get_timestamp()}")
-
-    for filename in filenames:
-        fig.savefig(filename)
-    fig.show()
-
-
-def get_subband_statistics(devices):
-    """
-    Get subband statistics data from the setup, including proper axes and labels
-    """
-    devices = devices_list_2_dict(devices)
-    recv = devices["recv"]
-    sst = devices["sst"]
-
-    # get band names for the current configuration
-    cur_band_names = plib.get_band_names(
-        recv.RCU_band_select_R,
-        # TODO - when correct LCNs are provided
-        # in "RCU_PCB_number_R", uncomment the
-        # following line:
-        # recv.RCU_PCB_number_R,
-        # and remove the following line:
-        recv.RCU_PCB_ID_R,
-        lb_tags=RCU2L_TAGS,
-        hb_tags=RCU2H_TAGS,
-    )
-
-    # replace the aliases of LB1 and LB2 into LB: (same frequency axis)
-    for alias in plib.LB_ALIASES:
-        while alias in cur_band_names:
-            cur_band_names[cur_band_names.index(alias)] = "LB"
-    # get frequency axes
-
-    # TODO - get frequency axes correct (indexing is incorrect/inconsequent)
-    key1 = "HB2"
-    key2 = "HB1"
-    tmp_key = "magic_band"
-    while key1 in cur_band_names:
-        cur_band_names[cur_band_names.index(key1)] = tmp_key
-    while key2 in cur_band_names:
-        cur_band_names[cur_band_names.index(key1)] = key1
-    while tmp_key in cur_band_names:
-        cur_band_names[cur_band_names.index(tmp_key)] = key2
-
-    frequency_axes = {}
-    for band_name in plib.get_valid_band_names():
-        if band_name in cur_band_names:
-            frequency_axes[band_name] = plib.get_frequency_for_band_name(band_name)
-    # get sst data
-    sst_data = sst.sst_r
-    # make sure header data is of correct size
-    cur_band_names = cur_band_names + [None] * (len(sst_data) - len(cur_band_names))
-    return sst_data, cur_band_names, frequency_axes
-
-
-def devices_list_2_dict(devices_list, device_keys=None):
-    """
-    Convert a list of devices to a dictionary with known keys for internal
-    use.
-
-    Devices are selected based on substring presence in the devices name
-
-    Input arguments:
-    devices_list = list of Tango devices
-    device_keys  = list of keys, should be a substring of the device names
-
-    Output arguments:
-    devices_dict = dict of devices with known keys
-    """
-    if device_keys is None:
-        device_keys = [
-            "boot",
-            "unb2",
-            "recv",
-            "sdp",
-            "sst",
-            "bst",
-            "xst",
-            "digitalbeam",
-            "tilebeam",
-            "beamlet",
-            "apsct",
-            "apspu",
-        ]
-    devices_dict = {}
-    if isinstance(devices_list, dict):
-        # if by accident devices_list is already a dict,
-        # then simply return the dict
-        return devices_list
-    for device in devices_list:
-        for device_key in device_keys:
-            if device_key in device.name().lower():
-                devices_dict[device_key] = device
-    return devices_dict
-
-
-def main():
-    """Main"""
-    return False
-
-
-if __name__ == "__main__":
-    sys.exit(main())
diff --git a/lofar_station_client/processing_lib.py b/lofar_station_client/processing_lib.py
deleted file mode 100644
index 285654b9d10ef1bc025293f3a7ee2ea05cced9c5..0000000000000000000000000000000000000000
--- a/lofar_station_client/processing_lib.py
+++ /dev/null
@@ -1,467 +0,0 @@
-# processing_lib.py: Module with generic functions for processing
-# LOFAR2 station data
-#
-# -*- coding: utf-8 -*-
-#
-# 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.
-
-
-"""  module processing_lib
-
-This module contains generic functions for processing LOFAR2 station data.
-
-"""
-
-import sys
-
-# import h5py
-# from os import listdir
-# from os.path import isfile, join
-import datetime
-import numpy as np
-
-# from mpl_toolkits.mplot3d import axes3d
-# import matplotlib.pyplot as plt
-
-
-CST_N_SUB = 512  # number of subbands as output of subband generation in firmware
-CST_FS = 100e6  # sampling frequency in Hz
-BANDLIMS = {"LB": [0, 100e6], "HB1": [100e6, 200e6], "HB2": [200e6, 300e6]}
-LB_ALIASES = ["LB1", "LB2"]
-
-
-def signal_index_2_processing_node_index(signal_index):
-    """
-    Convert signal index to processing node index.
-    Can be used to determine on which processing node the signal is processed.
-    """
-    return (signal_index % 192) // 12
-
-
-def signal_index_2_processing_node_input_index(signal_index):
-    """
-    Convert signal index to processing node input index.
-    Can be used to determine wich input to the processing node is used for the signal.
-    """
-    return (signal_index % 192) % 12
-
-
-def signal_index_2_uniboard_index(signal_index):
-    """
-    Convert signal index to UniBoard index.
-    Can be used to determine on which UniBoard the signal is processed.
-    """
-    return signal_index // 48
-
-
-def signal_index_2_rcu_index(signal_index):
-    """
-    Convert signal index to RCU index.
-    Can be used to determine which RCU is used to provide the signal.
-    """
-    return (signal_index // 6) * 2 + signal_index % 2
-
-
-def signal_index_2_rcu_input_index(signal_index):
-    """
-    Convert signal index to RCU input index.
-    Can be used to determine which input to the RCU is used for the signal.
-    """
-    return (signal_index // 2) % 3
-
-
-def signal_index_2_aps_index(signal_index):
-    """
-    Convert signal index to APS index.
-    Can be used to determine in which Antenna Processing Subrack this signal is
-    processed.
-    """
-    return signal_index // 96
-
-
-def signal_input_2_rcu_index_and_rcu_input_index(signal_index):
-    """
-    Given the gsi, get the RCU index and the RCU input index in one call.
-    """
-    return (
-        signal_index_2_rcu_index(signal_index),
-        signal_index_2_rcu_input_index(signal_index),
-    )
-
-
-# the inverse:
-def rcu_index_2_signal_index(rcu_index):
-    """
-    Convert RCU index to signal indices.
-    """
-    return rcu_index_and_rcu_input_index_2_signal_index(rcu_index)
-
-
-def rcu_index_and_rcu_input_index_2_signal_index(
-    rcu_index, rcu_input_index=np.array([0, 1, 2])
-):
-    """
-    Convert RCU index and RCU input index to signal index.
-    """
-    return rcu_index + rcu_input_index * 2 + (rcu_index // 2) * 4
-
-
-# backward compatible methods:
-def gsi_2_rcu_input_index(gsi):
-    """Deprecated. Please use signal_index_2_rcu_input_index."""
-    print("Warning! Do not use gsi_2_rcu_input_index().")
-    print("Use signal_index_2_rcu_input_index() instead")
-    return signal_index_2_rcu_input_index(gsi)
-
-
-def gsi_2_rcu_index(gsi):
-    """Deprecated. Please use signal_index_2_rcu_index."""
-    print("Warning! Do not use gsi_2_rcu_index().")
-    print("Use signal_index_2_rcu_index() instead")
-    return signal_index_2_rcu_index(gsi)
-
-
-def gsi_2_rcu_index_and_rcu_input_index(gsi):
-    """Deprecated. Please use signal_input_2_rcu_index_and_rcu_input_index."""
-    print("Warning! Do not use gsi_2_rcu_index_and_rcu_input_index().")
-    print("Use signal_input_2_rcu_index_and_rcu_input_index() instead")
-    return signal_input_2_rcu_index_and_rcu_input_index(gsi)
-
-
-def rcu_index_2_gsi(rcu_index):
-    """Deprecated. Please use rcu_index_2_signal_index."""
-    print("Warning! Do not use rcu_index_2_gsi().")
-    print("Use rcu_index_2_signal_index() instead")
-    return rcu_index_2_signal_index(rcu_index)
-
-
-def rcu_index_and_rcu_input_index_2_gsi(rcu_index, rcu_input_index=np.array([0, 1, 2])):
-    """Deprecated. Please use rcu_index_and_rcu_input_index_2_signal_index."""
-    print("Warning! Do not use rcu_index_and_rcu_input_index_2_gsi().")
-    print("Use rcu_index_and_rcu_input_index_2_signal_index() instead")
-    return rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index)
-
-
-def test_rcu_index_2_signal_index():
-    """
-    Test code for rcu_index_2_signal_index
-    """
-    test_input = []
-    test_output = []
-    # test 1
-    test_input.append(np.array([0]))
-    test_output.append(np.array([0, 2, 4]))
-    # test 2
-    test_input.append(np.array([1]))
-    test_output.append(np.array([1, 3, 5]))
-    for input_rcu_index, expected_output in zip(test_input, test_output):
-        actual_output = rcu_index_2_signal_index(input_rcu_index)
-        if not np.all(expected_output == actual_output):
-            print(expected_output)
-            print(actual_output)
-            raise Exception("Tests fails!")
-
-
-def test_rcu_index_and_rcu_input_index_2_signal_index():
-    """
-    Test code for rcu_index_and_rcu_input_index_2_signal_index
-    """
-    test_input = []
-    test_output = []
-    # test 1
-    test_input.append([np.array([0]), np.array([0])])
-    test_output.append(np.array([0]))
-    # test 2
-    test_input.append([np.array([0]), np.array([0, 1, 2])])
-    test_output.append(np.array([0, 2, 4]))
-    for inputs, expected_output in zip(test_input, test_output):
-        input_rcu_index = inputs[0]
-        input_rcu_input_index = inputs[1]
-        actual_output = rcu_index_and_rcu_input_index_2_signal_index(
-            input_rcu_index, input_rcu_input_index
-        )
-        if not np.all(expected_output == actual_output):
-            print(expected_output)
-            print(actual_output)
-            raise Exception("Tests fails!")
-
-
-def test_signal_input_2_rcu_index_and_rcu_input_index():
-    """
-    Test code for signal_input_2_rcu_index_and_rcu_input_index
-    """
-    test_input = []
-    test_output = []
-    # test 1
-    test_input.append(np.array([0]))
-    test_output.append((np.array([0]), np.array([0])))
-    # test 2
-    test_input.append(np.array([1]))
-    test_output.append((np.array([1]), np.array([0])))
-    for input_signal_index, expected_output in zip(test_input, test_output):
-        actual_output = signal_input_2_rcu_index_and_rcu_input_index(input_signal_index)
-        print(actual_output)
-        print(expected_output)
-        if not np.all(expected_output == actual_output):
-            print(expected_output)
-            print(actual_output)
-            raise Exception("Tests fails!")
-
-
-def get_timestamp(format_specifier=None):
-    """
-    Get the timestamp in standard format
-
-    Input argument:
-    format_specifier = format specifier.
-                       iso format (default)
-                       "filename": filename without spaces and special characters,
-                                   format: yyyymmddThhmmss
-
-    Output argument:
-    timestamp = timestamp in correct format
-    """
-    timestamp = datetime.datetime.isoformat(datetime.datetime.now())
-    if format_specifier == "filename":
-        timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
-    return timestamp
-
-
-def log(string):
-    """
-    Standard printing of a log line, including a timestamp.
-    """
-    print(f"{get_timestamp()} : {string}")
-
-
-def get_valid_band_names():
-    """
-    Return the valid band names.
-    """
-    band_names = []
-    for band_name in BANDLIMS:
-        band_names.append(band_name)
-    return band_names
-
-
-def get_band_name_and_subband_index_for_frequency(freqs):
-    """
-    Get band name and subband index for one or more frequencies.
-
-    Input arguments:
-    freqs = one or more frequencies in Hz
-
-    Output arguments:
-    band_names = list of band names (see get_valid_band_names)
-    sbi        = one or more indices of the subbands for which the frequencies
-                 should be returned
-    """
-    # make sure freqs is a list
-    if not isinstance(freqs, list):
-        freqs = [freqs]
-    # get band name and subband index
-    freqs = np.array(freqs)
-    band_names = []
-    for freq in freqs:
-        band_name = get_band_name_for_frequency(freq)
-        band_names.append(band_name)
-    # get indices for band_name "LB"
-    sbis = np.round(freqs * CST_N_SUB / CST_FS)
-    # and update for the other bands:
-    for idx in np.where(np.array(band_names) == "HB1")[0]:
-        sbis[idx] = 2 * CST_N_SUB - sbis[idx]
-    for idx in np.where(np.array(band_names) == "HB2")[0]:
-        sbis[idx] = sbis[idx] - 2 * CST_N_SUB
-    return band_names, sbis
-
-
-def get_band_name_for_frequency(freq, bandlims=None):
-    """
-    Get band name for frequency
-
-    Input arguments:
-    freq = one frequency in Hz, float
-    bandlims = dict with band names and their limits (default: LOFAR2 bands)
-
-    Output arguments:
-    band_name = name of analog band (see get_valid_band_names),
-                empty string ("") if unsuccessful
-    """
-    if bandlims is None:
-        bandlims = BANDLIMS
-    for band_name in bandlims:
-        if np.min(bandlims[band_name]) <= freq <= np.max(bandlims[band_name]):
-            return band_name
-    return ""
-
-
-def get_frequency_for_band_name(band_name):
-    """
-    Get all frequencies for given band name
-
-    Input arguments:
-    band_name = name of analog band (see get_valid_band_names)
-
-    Output arguments:
-    freqs = frequencies in Hz for the selected band
-    """
-    return get_frequency_for_band_name_and_subband_index(band_name, sbi=np.arange(512))
-
-
-def get_frequency_for_band_name_and_subband_index(band_name, sbi):
-    """
-    Get frequency for band name and subband(s)
-
-    Input arguments:
-    band_name = name of analog band (see get_valid_band_names)
-    sbi       = one or more indices of the subbands for which the frequencies
-                should be returned
-
-    Output arguments:
-    freqs = frequencies in Hz for the selected band and subband
-    """
-    # check input
-    valid_band_names = get_valid_band_names()
-    if band_name not in valid_band_names and band_name not in LB_ALIASES:
-        raise Exception(
-            f"Unknown band_name '{band_name}'. Valid band_names are: {valid_band_names}"
-        )
-    # generate frequencies
-    freqs = sbi * CST_FS / CST_N_SUB
-    if band_name == "HB1":
-        return 200e6 - freqs
-    if band_name == "HB2":
-        return 200e6 + freqs
-    # else: # LB
-    return freqs
-
-
-def get_band_names(rcu_band_select_r, rcu_pcb_version_r, lb_tags=None, hb_tags=None):
-    """
-    Get the frequency band names per receiver, based on their band selection
-    and RCU PCB name.
-
-    PCB name is used to determine the band (Low Band "LB" or High Band "HB",
-    None otherwise). The band selection is added as a postfix.
-    RCU PCB version can also be used, but then the default tags will not be sufficient
-
-    Input Arguments:
-    rcu_band_select_r = as returned by recv.RCU_band_select_R
-    rcu_pcb_version_r = as returned by recv.RCU_PCB_version_R
-    lb_tags           = substrings in RCU_PCB_version_R fields to indicate Low Band
-                        Default: RCU2L
-    hb_tags           = substrings in RCU_PCB_version_R fields to indicate High Band
-                        Default: RCU2H
-
-    Note:
-    rcu_pcb_version_r can also hold IDs as returned by recv.RCU_PCB_ID_R
-    In that case, the lb_tags and hb_tags should be passed on by the user
-
-    Output Arguments:
-    band_name = list of strings indicating the band name per receiver.
-                containts None if no band name could be determined
-
-    """
-    # if IDs are passed on, convert to list of strings
-    if isinstance(rcu_pcb_version_r, np.ndarray):
-        rcu_pcb_version_r = [str(x) for x in rcu_pcb_version_r]
-    #
-    if lb_tags is None:
-        lb_tags = ["RCU2L"]
-    if hb_tags is None:
-        hb_tags = ["RCU2H"]
-
-    n_signal_indices = rcu_band_select_r.shape[0] * rcu_band_select_r.shape[1]
-    band_name_per_signal_index = [None] * n_signal_indices
-
-    for rcu_index in range(len(rcu_band_select_r)):
-        for rcu_input_index in range(len(rcu_band_select_r[rcu_index])):
-            signal_index = rcu_index_and_rcu_input_index_2_signal_index(
-                rcu_index, rcu_input_index
-            )
-            this_band = None
-            for tag in lb_tags:
-                if tag in rcu_pcb_version_r[rcu_index]:
-                    this_band = f"LB{rcu_band_select_r[rcu_index][rcu_input_index]:1}"
-                    break
-            if this_band is None:  # continue with high band
-                for tag in hb_tags:
-                    if tag in rcu_pcb_version_r[rcu_index]:
-                        this_band = (
-                            f"HB{rcu_band_select_r[rcu_index][rcu_input_index]:1}"
-                        )
-                        break
-            band_name_per_signal_index[signal_index] = this_band
-    return band_name_per_signal_index
-
-
-# some basic test methods, for internal use:
-def test_get_band_name_and_subband_index_for_frequency():
-    """
-    Test code for get_band_name_and_subband_index_for_frequency
-    """
-    input_frequencies = [138.1e6, 137.9e6, 0.1, 0, 195312.5, 234e6]
-    expected_band_names = ["HB1", "HB1", "LB", "LB", "LB", "HB2"]
-    expected_subband_index = np.array([317.0, 318.0, 0.0, 0.0, 1.0, 174.0])
-    (
-        actual_band_names,
-        actual_subband_index,
-    ) = get_band_name_and_subband_index_for_frequency(input_frequencies)
-    if expected_band_names != actual_band_names:
-        print(expected_band_names)
-        print(actual_band_names)
-        raise Exception("Tests fails!")
-    if not (expected_subband_index == actual_subband_index).all():
-        print(expected_subband_index)
-        print(actual_subband_index)
-        raise Exception("Tests fails!")
-
-
-def test_get_frequency_for_band_name_and_subband_index():
-    """
-    Test code for get_frequency_for_band_name_and_subband_index
-    """
-    for input_band_name, input_subband_index, expected_frequencies in zip(
-        ["HB1", "HB1", "LB", "LB", "LB", "HB2", "LB1"],
-        [195.0, 194.0, 0.0, 0.0, 1.0, 174.0, 1.0],
-        [161914062.5, 162109375.0, 0.0, 0.0, 195312.5, 233984375, 195312.5],
-    ):
-        actual_frequencies = get_frequency_for_band_name_and_subband_index(
-            input_band_name, input_subband_index
-        )
-        if not expected_frequencies == actual_frequencies:
-            print(expected_frequencies)
-            print(actual_frequencies)
-            raise Exception("Tests fails!")
-
-
-def test():
-    """
-    Method to call the defined test methods.
-    """
-    test_rcu_index_2_signal_index()
-    test_rcu_index_and_rcu_input_index_2_signal_index()
-    test_signal_input_2_rcu_index_and_rcu_input_index()
-    test_get_band_name_and_subband_index_for_frequency()
-    test_get_frequency_for_band_name_and_subband_index()
-
-
-def main():
-    """
-    Main function
-    """
-    return False
-
-
-if __name__ == "__main__":
-    sys.exit(main())
diff --git a/lofar_station_client/requests/prometheus.py b/lofar_station_client/requests/prometheus.py
index 0c61ace42a391e9895164e545e661f675aa61169..801cad2f8f14f189ecfafa1611280b3e3a6764df 100644
--- a/lofar_station_client/requests/prometheus.py
+++ b/lofar_station_client/requests/prometheus.py
@@ -42,6 +42,7 @@ class PrometheusRequests:
     DEFAULT_HOST = "http://prometheus:9090"
     DEFAULT_DURATION = 3600  # in seconds
     DEFAULT_STEP_SIZE = 60  # in seconds
+    DEFAULT_TIMEOUT = 60.0  # in seconds
 
     @staticmethod
     def get_attribute_history(
@@ -98,7 +99,8 @@ class PrometheusRequests:
 
         # Perform the actual get request
         http_result = requests.get(
-            f"{host}{path}{query}&start={start}&end={end}&step={step}"
+            f"{host}{path}{query}&start={start}&end={end}&step={step}",
+            timeout=PrometheusRequests.DEFAULT_TIMEOUT,
         )
 
         # Check status code for any errors, only OK 200 accepted
diff --git a/requirements.txt b/requirements.txt
index 2c7f9268c3114449507a67ed7db4d9206b86a7ef..5e2e3478a9c1d974fc92a40993834cc03b7fd5c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,8 @@
-# TODO(Corne): Uncomment below when L2SS-832 is fixed
-# tangostationcontrol@git+https://git.astron.nl/lofar2.0/tango.git#egg=tangostationcontrol&subdirectory=tangostationcontrol
-
+# Order does matter
 requests>=2.0 # Apache 2
-numpy>=1.21.0 # BSD
\ No newline at end of file
+numpy>=1.21.0 # BSD
+nptyping>=2.3.0 # MIT
+matplotlib>=3.5.0 # PSF
+pyDeprecate>=0.3.0 # MIT
+# commented out until https://gitlab.com/tango-controls/pytango/-/issues/468 is resolved
+# PyTango>=9.3.5 # GNU LGPL v3
diff --git a/test-requirements.txt b/test-requirements.txt
index 80999ddacb2ce4ee1e95e89428be850bd76bd63d..12f8572083f614e07782b856455a54982f3eff6e 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,7 +1,8 @@
-black
-build
-flake8
-pylint
+black>=22.0.0 # MIT
+build>=0.8.0 # MIT
+flake8>=5.0.0 # MIT
+pylint>=2.15.0 # GPLv2
+autopep8>=1.7.0 # MIT
 coverage!=4.4,>=4.0 # Apache-2.0
 stestr>=2.0.0 # Apache-2.0
 testtools>=2.2.0 # MIT
diff --git a/tests/dts/__init__.py b/tests/dts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/dts/test_bands.py b/tests/dts/test_bands.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed5b8ce14712c93db00e047c63a018a403e98f73
--- /dev/null
+++ b/tests/dts/test_bands.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+import numpy as np
+
+from lofar_station_client.dts import bands
+
+from tests import base
+
+
+class DTSBandsTest(base.TestCase):
+    def test_get_band_name_and_subband_index_for_frequency_single(self):
+        """Test automatic single value to list conversion in sbi_for_freq"""
+
+        result = bands.get_band_name_and_subband_index_for_frequency(138.1e6)
+
+        self.assertEqual(result[0], "HB1")
+        self.assertEqual(result[1], 317.0)
+
+        # Access ndarray element directly
+        self.assertEqual(result[0][0], "HB1")
+        self.assertEqual(result[1][0], 317.0)
+
+    def test_get_band_name_for_frequency(self):
+        result = bands.get_band_name_for_frequency(138.1e6)
+        self.assertEqual("HB1", result)
+
+    def test_get_band_name_for_frequency_not_exist(self):
+        result = bands.get_band_name_for_frequency(998.1e6)
+        self.assertEqual("", result)
+
+    def test_get_band_name_for_frequency_custom_bandlims(self):
+        t_bandlims = {"LB": [0, 900e6], "HB1": [900e6, 1000e6], "HB2": [1000e6, 1300e6]}
+        result = bands.get_band_name_for_frequency(998.1e6, t_bandlims)
+        self.assertEqual("HB1", result)
+
+    def test_get_band_name_and_subband_index_for_frequency(self):
+        """Test code for get_band_name_and_subband_index_for_frequency"""
+
+        input_frequencies = [138.1e6, 137.9e6, 0.1, 0, 195312.5, 234e6]
+        expected_band_names = ["HB1", "HB1", "LB", "LB", "LB", "HB2"]
+        expected_subband_index = np.array([317.0, 318.0, 0.0, 0.0, 1.0, 174.0])
+        (
+            actual_band_names,
+            actual_subband_index,
+        ) = bands.get_band_name_and_subband_index_for_frequency(input_frequencies)
+
+        np.testing.assert_array_equal(expected_band_names, actual_band_names)
+        np.testing.assert_array_equal(expected_subband_index, actual_subband_index)
+
+    def test_get_frequency_for_band_name_and_subband_index(self):
+        """Test code for get_frequency_for_band_name_and_subband_index"""
+
+        for input_band_name, input_subband_index, expected_frequencies in zip(
+            ["HB1", "HB1", "LB", "LB", "LB", "HB2", "LB1"],
+            [195, 194, 0, 0, 1, 174, 1],
+            [161914062.5, 162109375.0, 0.0, 0.0, 195312.5, 233984375, 195312.5],
+        ):
+            actual_frequencies = bands.get_frequency_for_band_name_and_subband_index(
+                input_band_name, input_subband_index
+            )
+
+            self.assertEqual(expected_frequencies, actual_frequencies)
+
+    def test_get_frequency_for_band_name_and_subband_index_except(self):
+        """Test retrieving frequency for non-existing band raises exception"""
+
+        self.assertRaises(
+            ValueError,
+            bands.get_frequency_for_band_name_and_subband_index,
+            "not-exists",
+            0,
+        )
+
+    def test_get_frequency_for_band_name_hb1(self):
+        """HB1 band should return frequencies in descending order"""
+        frequencies = bands.get_frequency_for_band_name("HB1")
+
+        previous_freq = frequencies[0]
+        for x in frequencies[1:]:
+            self.assertLess(x, previous_freq)
+
+    def test_get_frequency_for_band_name_hb2(self):
+        """HB2 band should return frequencies in ascending order"""
+        frequencies = bands.get_frequency_for_band_name("HB2")
+
+        previous_freq = frequencies[0]
+        for x in frequencies[1:]:
+            self.assertGreater(x, previous_freq)
+
+    def test_get_frequency_for_band_name_lb(self):
+        """All bands except HB1 should return frequencies in ascending order"""
+        frequencies = bands.get_frequency_for_band_name("LB1")
+
+        previous_freq = frequencies[0]
+        for x in frequencies[1:]:
+            self.assertGreater(x, previous_freq)
+
+    def test_get_band_names(self):
+        pass
diff --git a/tests/dts/test_index.py b/tests/dts/test_index.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c07caf8a46a7bc882e66af39dca4be8cdd06102
--- /dev/null
+++ b/tests/dts/test_index.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+import numpy as np
+
+from lofar_station_client.dts import index
+
+from tests import base
+
+
+class DTSIndexTest(base.TestCase):
+    @staticmethod
+    def test_rcu_index_2_signal_index():
+        """Test code for rcu_index_2_signal_index"""
+
+        test_input = []
+        test_output = []
+
+        # test 1
+        test_input.append(np.array([0]))
+        test_output.append(np.array([0, 2, 4]))
+
+        # test 2
+        test_input.append(np.array([1]))
+        test_output.append(np.array([1, 3, 5]))
+
+        for input_rcu_index, expected_output in zip(test_input, test_output):
+            actual_output = index.rcu_index_2_signal_index(input_rcu_index)
+            np.testing.assert_array_equal(expected_output, actual_output)
+
+    @staticmethod
+    def test_rcu_index_and_rcu_input_index_2_signal_index():
+        """Test code for rcu_index_and_rcu_input_index_2_signal_index"""
+
+        test_input = []
+        test_output = []
+
+        # test 1
+        test_input.append([np.array([0]), np.array([0])])
+        test_output.append(np.array([0]))
+
+        # test 2
+        test_input.append([np.array([0]), np.array([0, 1, 2])])
+        test_output.append(np.array([0, 2, 4]))
+
+        for inputs, expected_output in zip(test_input, test_output):
+            input_rcu_index = inputs[0]
+            input_rcu_input_index = inputs[1]
+            actual_output = index.rcu_index_and_rcu_input_index_2_signal_index(
+                input_rcu_index, input_rcu_input_index
+            )
+            np.testing.assert_array_equal(expected_output, actual_output)
+
+    @staticmethod
+    def test_signal_input_2_rcu_index_and_rcu_input_index():
+        """Test code for signal_input_2_rcu_index_and_rcu_input_index"""
+
+        test_input = []
+        test_output = []
+
+        # test 1
+        test_input.append(np.array([0]))
+        test_output.append((np.array([0]), np.array([0])))
+
+        # test 2
+        test_input.append(np.array([1]))
+        test_output.append((np.array([1]), np.array([0])))
+
+        for input_signal_index, expected_output in zip(test_input, test_output):
+            actual_output = index.signal_input_2_rcu_index_and_rcu_input_index(
+                input_signal_index
+            )
+            np.testing.assert_array_equal(expected_output, actual_output)
diff --git a/tests/dts/test_outside.py b/tests/dts/test_outside.py
new file mode 100644
index 0000000000000000000000000000000000000000..05cae1a19a1327fb56f122591679091afa5d5ecb
--- /dev/null
+++ b/tests/dts/test_outside.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+
+import numpy as np
+
+import copy
+
+from unittest import mock
+
+from lofar_station_client.dts import outside
+
+from tests import base
+
+
+class DTSOutsideTest(base.TestCase):
+    def test_set_rcu_ant_masked_update_all(self):
+        """Test masked mixing of new and old values, update all"""
+
+        t_recv = mock.Mock()
+
+        mask = [[True] * 3] * 12
+        old = [[0] * 3] * 12
+        new = [[1] * 3] * 12
+
+        result = outside.set_rcu_ant_masked(t_recv, mask, copy.deepcopy(old), new)
+        np.testing.assert_array_equal(result, new)
+
+    def test_set_rcu_ant_masked_update_none(self):
+        """Test masked mixing of new and old values, update none"""
+
+        t_recv = mock.Mock()
+
+        mask = [[False] * 3] * 12
+        old = [[0] * 3] * 12
+        new = [[1] * 3] * 12
+
+        result = outside.set_rcu_ant_masked(t_recv, mask, copy.deepcopy(old), new)
+        np.testing.assert_array_equal(result, old)
+
+    def test_set_rcu_hba_mask_update_all(self):
+        """Test masked mixing of new and old values, update all"""
+        t_recv = mock.Mock()
+
+        mask = [[True] * 3] * 12
+        old = np.array([[0] * 3] * 12).flatten()
+        new = np.array([[1] * 3] * 12).flatten()
+
+        result = outside.set_rcu_hba_mask(t_recv, mask, copy.deepcopy(old), new)
+        np.testing.assert_array_equal(result, new)
+
+    def test_set_rcu_hba_mask_update_none(self):
+        """Test masked mixing of new and old values, update none"""
+        t_recv = mock.Mock()
+
+        mask = [[False] * 3] * 12
+        old = np.array([[0] * 3] * 12).flatten()
+        new = np.array([[1] * 3] * 12).flatten()
+
+        result = outside.set_rcu_hba_mask(t_recv, mask, copy.deepcopy(old), new)
+        np.testing.assert_array_equal(result, old)
diff --git a/tests/statistics/test_collector.py b/tests/statistics/test_collector.py
index 8ca45f9cc8583387318dc950a5df319f63640717..bc5a147b11a0835eeff7eda2a0dd87eefa455843 100644
--- a/tests/statistics/test_collector.py
+++ b/tests/statistics/test_collector.py
@@ -218,7 +218,12 @@ class TestXSTCollector(base.TestCase):
                         f"but was written to the XST matrix.",
                     )
 
-        self.assertEqual(correct_nr_values, actual_nr_values, "Mismatch between number of values in the packet and in the resulting matrix")
+        self.assertEqual(
+            correct_nr_values,
+            actual_nr_values,
+            "Mismatch between number of values in the packet and in the resulting "
+            "matrix",
+        )
 
     def test_multiple_subbands(self):
         collector = XSTCollector()
diff --git a/tests/test_devices.py b/tests/test_devices.py
new file mode 100644
index 0000000000000000000000000000000000000000..26701397a49ef7c5d38f07142a6366fe37c6f707
--- /dev/null
+++ b/tests/test_devices.py
@@ -0,0 +1,136 @@
+import numpy
+
+from tests import base
+
+from lofar_station_client.devices import LofarDeviceProxy
+
+from tango.test_context import MultiDeviceTestContext
+from tango.server import Device, attribute, AttrWriteType
+
+
+class MyDevice(Device):
+    A = attribute(
+        dtype=((bool,),),
+        max_dim_x=4,
+        max_dim_y=6,
+        format="(2,3,4)",
+        access=AttrWriteType.READ_WRITE,
+    )
+
+    scalar = attribute(dtype=bool, access=AttrWriteType.READ_WRITE)
+
+    spectrum = attribute(dtype=(bool,), max_dim_x=2, access=AttrWriteType.READ_WRITE)
+
+    image = attribute(
+        dtype=((bool,),), max_dim_x=2, max_dim_y=3, access=AttrWriteType.READ_WRITE
+    )
+
+    def init_device(self):
+        self.value_A = numpy.zeros((6, 4), dtype=bool)
+        self.value_scalar = False
+        self.value_spectrum = [False, False]
+        self.value_image = [[False, False]] * 3
+
+    def read_scalar(self):
+        return self.value_scalar
+
+    def write_scalar(self, value):
+        self.value_scalar = value
+
+    def read_spectrum(self):
+        return self.value_spectrum
+
+    def write_spectrum(self, value):
+        self.value_spectrum = value
+
+    def read_image(self):
+        return self.value_image
+
+    def write_image(self, value):
+        self.value_image = value
+
+    def read_A(self):
+        return self.value_A
+
+    def write_A(self, value):
+        self.value_A = value
+
+
+class LofarDeviceProxyTest(base.TestCase):
+    TEST_DEVICE_INFO = [
+        {
+            "class": MyDevice,
+            "devices": [{"name": "STAT/MyDevice/1", "properties": {}, "memorized": {}}],
+        }
+    ]
+
+    @classmethod
+    def setUpClass(cls):
+        # setting up the TestContext takes ~1 second, so do it only once
+        cls.context = MultiDeviceTestContext(
+            cls.TEST_DEVICE_INFO,
+            process=True,
+        )
+
+        cls.context.start()
+        cls.proxy = LofarDeviceProxy(cls.context.get_device_access("STAT/MyDevice/1"))
+
+    @classmethod
+    def tearDownClass(cls):
+        # In Python3.8+, we can use addClassCleanup instead
+        cls.context.stop()
+
+    def test_read_scalar(self):
+        value = self.proxy.scalar
+
+        self.assertEqual(bool, type(value))
+
+    def test_write_scalar(self):
+        self.proxy.scalar = True
+
+    def test_read_spectrum(self):
+        value = self.proxy.spectrum
+
+        self.assertEqual((2,), value.shape)
+        self.assertEqual(numpy.bool_, type(value[0]))
+
+    def test_write_spectrum(self):
+        self.proxy.spectrum = [True, False]
+
+    def test_read_image(self):
+        value = self.proxy.image
+
+        self.assertEqual((3, 2), value.shape)
+        self.assertEqual(numpy.bool_, type(value[0, 0]))
+
+    def test_write_image(self):
+        self.proxy.image = [[True, False]] * 3
+
+    def test_write_3D_attribute_lists(self):
+        self.proxy.A = [
+            [True, True, True, True],
+            [True, True, True, True],
+            [True, True, True, True],
+        ], [
+            [False, False, False, False],
+            [False, False, False, False],
+            [False, False, False, False],
+        ]
+
+    def test_write_3D_attribute_numpy(self):
+        self.proxy.A = numpy.zeros((2, 3, 4), dtype=bool)
+
+    def test_write_3D_attribute_numpy_with_too_few_dimensions(self):
+        # write a 2D shape
+        with self.assertRaises(ValueError):
+            self.proxy.A = numpy.zeros((2, 12), dtype=bool)
+
+    def test_write_3D_attribute_numpy_with_too_many_dimension(self):
+        # write a 4D shape
+        with self.assertRaises(ValueError):
+            self.proxy.A = numpy.zeros((2, 3, 2, 2), dtype=bool)
+
+    def test_read_3D_attribute(self):
+        value = self.proxy.A
+
+        self.assertEqual((2, 3, 4), value.shape)
diff --git a/tox.ini b/tox.ini
index 6610b9523416f658c7a4d60c2ab37bfd22be470c..a358069440d42dfbfe9790a122a5cca328be7acf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 # Generative environment list to test all supported Python versions
-envlist = py3{7,8,9,10},black,pep8,pylint
+envlist = black,pep8,pylint,py3{7,8,9,10},docs
 minversion = 3.18.0
 # Source distributions are explicitly build using tox -e build
 skipsdist = True
@@ -14,6 +14,10 @@ setenv =
 deps =
     -r{toxinidir}/requirements.txt
     -r{toxinidir}/test-requirements.txt
+commands_pre =
+    # required until https://gitlab.com/tango-controls/pytango/-/issues/468 is resolved
+    pip install 'numpy>=1.21.0'
+    pip install --no-cache 'PyTango>=9.3.5'
 commands =
     {envpython} --version
     stestr run {posargs}