diff --git a/.gitattributes b/.gitattributes
index 929b6bb4f5a33f37a97181698c5a0626021018f0..fd70048c7ad908b94cd835cee34f0f16eadd84a4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,3 +8,6 @@
 *.h5 binary
 *.jpg binary
 *.bin binary
+
+# casacore measures tables
+table.* binary
diff --git a/CDB/stations/DTS_ConfigDb.json b/CDB/stations/DTS_ConfigDb.json
index 741d9dc910e6ff35d04e993dab21dbd3eb08cc01..bd797a7eabcfff3050d9650f334edb11c3999376 100644
--- a/CDB/stations/DTS_ConfigDb.json
+++ b/CDB/stations/DTS_ConfigDb.json
@@ -64,6 +64,207 @@
                             ],
                             "OPC_Time_Out": [
                                 "5.0"
+                            ],
+                            "HBAT_reference_ETRS": [
+                                "3839371.416", "430339.901", "5057958.886",
+                                "3839368.919", "430335.979", "5057961.1",
+                                "3839365.645", "430339.299", "5057963.288",
+                                "3839368.142", "430343.221", "5057961.074",
+                                "3839374.094", "430299.513", "5057960.017",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0",
+                                "0", "0", "0"
+                            ],
+                            "HBAT_PQR_rotation_angle_deg": [
+                                "45.73",
+                                "45.73",
+                                "45.73",
+                                "45.73",
+                                "54.40",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0",
+                                "0"
+                            ],
+                            "HBAT_PQR_to_ETRS_rotation_matrix": [
+                               "-0.11660087", "-0.79095632", "0.60065992",
+                               " 0.99317077", "-0.09529842", "0.06730545",
+                               " 0.00400627", " 0.60440575", "0.79666658"
                             ]
                         }
                     }
diff --git a/CDB/stations/dummy_positions_ConfigDb.json b/CDB/stations/dummy_positions_ConfigDb.json
index 5f998a8102a8ceaad66b7a7a46ed293aa2223b67..1608917b748c4d7466f726153610be09981c7510 100644
--- a/CDB/stations/dummy_positions_ConfigDb.json
+++ b/CDB/stations/dummy_positions_ConfigDb.json
@@ -103,23 +103,108 @@
                                "3826577.066", "461022.948", "5064892.786",
                                "3826577.066", "461022.948", "5064892.786"
                             ],
-                            "HBAT_antenna_itrf_offsets": [
-                               "-1.847", "-1.180", " 1.493",
-                               "-1.581", " 0.003", " 1.186",
-                               "-1.315", " 1.185", " 0.880",
-                               "-1.049", " 2.367", " 0.573",
-                               "-0.882", "-1.575", " 0.804",
-                               "-0.616", "-0.393", " 0.498",
-                               "-0.350", " 0.789", " 0.191",
-                               "-0.083", " 1.971", "-0.116",
-                               " 0.083", "-1.971", " 0.116",
-                               " 0.350", "-0.789", "-0.191",
-                               " 0.616", " 0.393", "-0.498",
-                               " 0.882", " 1.575", "-0.804",
-                               " 1.049", "-2.367", "-0.573",
-                               " 1.315", "-1.185", "-0.880",
-                               " 1.581", "-0.003", "-1.186",
-                               " 1.847", " 1.180", "-1.493"
+                            "HBAT_PQR_rotation_angles_deg": [
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24",
+                               "24"
+                            ],
+                            "HBAT_PQR_to_ETRS_rotation_matrix": [
+                               "-0.1195951054", "-0.7919544517", "0.5987530018",
+                               " 0.9928227484", "-0.0954186800", "0.0720990002",
+                               " 0.0000330969", " 0.6030782884", "0.7976820024"
                             ]
                         }
                     }
diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat
new file mode 100644
index 0000000000000000000000000000000000000000..1f7869a8f1af1f6d4089128eeed130c78d5c6ae2
Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat differ
diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0 b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0
new file mode 100644
index 0000000000000000000000000000000000000000..5c14b2d3c72812c815005ec6fbd10110c7ceab70
Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0 differ
diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i
new file mode 100644
index 0000000000000000000000000000000000000000..d50b1bdd28ae76847df63f335bcbdd1bd1e7cc9f
Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i differ
diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info
new file mode 100644
index 0000000000000000000000000000000000000000..d1fa70b4e608c03fc4f6da5a213c36d568f9fbc4
Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info differ
diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock
new file mode 100644
index 0000000000000000000000000000000000000000..dab1a78a5dec98a99cad64cd140e1308abf761a8
Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock differ
diff --git a/tangostationcontrol/docs/source/devices/recv.rst b/tangostationcontrol/docs/source/devices/recv.rst
index 847f8bb51f7f52850e66b0d25c01269b9961e726..00bbd89ef2e748b120e34143c8e6cc4cb5763887 100644
--- a/tangostationcontrol/docs/source/devices/recv.rst
+++ b/tangostationcontrol/docs/source/devices/recv.rst
@@ -13,3 +13,28 @@ The ``recv == DeviceProxy("STAT/RECV/1")`` device controls the RCUs, the LBA ant
 
 Typically, ``N_RCUs == 32``, and ``N_antennas_per_RCU == 3``.
 
+Error information
+---------------------
+
+These attributes summarise the basic state of the device. Any elements which are not present in ``FPGA_mask_RW`` will be ignored and thus not report errors:
+
+:RCU_error_R: Whether the RCUs appear usable.
+
+  :type: ``bool[N_RCUs]``
+
+:ANT_error_R: Whether the antennas appear usable.
+
+  :type: ``bool[N_RCUs][N_antennas_per_RCU]``
+
+:RCU_IOUT_error_R: Whether there are alarms on any of the amplitudes in the measured currents.
+
+  :type: ``bool[N_RCUs]``
+
+:RCU_VOUT_error_R: Whether there are alarms on any of the voltages in the measured currents.
+
+  :type: ``bool[N_RCUs]``
+
+:RCU_TEMP_error_R: Whether there are alarms on any of the temperatures. NB: These values are also exposed for unused RCUs (the ``RCU_mask_RW`` is ignored).
+
+  :type: ``bool[N_RCUs]``
+
diff --git a/tangostationcontrol/docs/source/devices/sdp.rst b/tangostationcontrol/docs/source/devices/sdp.rst
index 2ca1ea6fa95e7295a21041d12242e15cec3b8001..6386dd851b178ce44b0b1b53968ba0f219fe5140 100644
--- a/tangostationcontrol/docs/source/devices/sdp.rst
+++ b/tangostationcontrol/docs/source/devices/sdp.rst
@@ -30,16 +30,29 @@ The following points are significant for the operations of this device:
 Data-quality information
 ---------------------------
 
-The following fields describe the data quality:
+The following fields describe the data quality (see also :doc:`../signal_chain`):
 
 :FPGA_signal_input_mean_R: Mean value of the last second of input (in ADC quantisation units). Should be close to 0.
 
   :type: ``double[N_fpgas][N_ants_per_fpga]``
 
-:FPGA_signal_input_rms_R: Root means square value of the last second of input (in ADC quantisation units). ``rms^2 = mean^2 + std^2``. Values above 2048 indicate strong RFI.
+:FPGA_signal_input_rms_R: Root means square value of the last second of input (in ADC quantisation units). ``rms^2 = mean^2 + std^2``. Values above 2048 indicate strong RFI. Values of 0 indicate a lack of signal input.
 
   :type: ``double[N_fpgas][N_ants_per_fpga]``
 
+Error information
+---------------------
+
+These attributes summarise the basic state of the device. Any elements which are not present in ``FPGA_mask_RW`` will be ignored and thus not report errors:
+
+:FPGA_error_R: Whether the FPGAs appear usable.
+
+  :type: ``bool[N_fpgas]``
+
+:FPGA_procesing_error_R: Whether the FPGAs are processing their input from the RCUs. NB: This will also raise an error if the Waveform Generator is enabled.
+
+  :type: ``bool[N_fpgas]``
+
 Version Information
 ---------------------
 
diff --git a/tangostationcontrol/docs/source/devices/using.rst b/tangostationcontrol/docs/source/devices/using.rst
index 7aef380071836e7cd6ece9d7202f04658e68a99a..825ee74d83b5a7ad8a8e18c8813ee1ace81f5459 100644
--- a/tangostationcontrol/docs/source/devices/using.rst
+++ b/tangostationcontrol/docs/source/devices/using.rst
@@ -59,13 +59,12 @@ typically involves the following sequence of commands::
   # turn the device off completely first.
   device.off()
 
-  # setup any connections and threads
-  device.initialise()
+  # turn on the device and fully reinitialise it
+  # alternatively, device.warm_boot() can be used,
+  # in which case no hardware is reinitialised.
+  device.boot()
 
-  # turn on the device
-  device.on()
-
-Of course, the device could go into ``FAULT`` again, even during the ``initialise()`` command, for example because the hardware it manages is unreachable. To debug the fault condition, check the :doc:`../interfaces/logs` of the device in question.
+Of course, the device could go into ``FAULT`` again, even during the ``boot()`` command, for example because the hardware it manages is unreachable. To debug the fault condition, check the :doc:`../interfaces/logs` of the device in question.
 
 Initialise hardware
 ````````````````````
@@ -74,6 +73,10 @@ Most devices provide the following commands, in order to configure the hardware
 
 :initialise(): Initialise the device (connect to the hardware). Moves from ``OFF`` to ``STANDBY``.
 
+:set_translator_defaults(): Select the hardware to configure and monitor.
+
+:prepare_hardware(): For devices that control hardware, this command prepares the hardware to accept commands (f.e. load firmware).
+
 :set_defaults(): Upload default attribute settings from the TangoDB to the hardware.
 
 :initialise_hardware(): For devices that control hardware, this command runs the hardware initialisation procedure.
@@ -145,4 +148,3 @@ For example, the ``RCU_mask_RW`` array is the RCU mask in the ``recv`` device. I
   # <--- only LED0 on RCU3 is now on
   # recv.RCU_LED0_R should show this,
   # if you have the RCU hardware installed.
-
diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt
index f8bad33a66897ce01bfb5084430a9e7af8ce621b..a93c35d3d5a643afaf0d78f53cdd2c36be65f8f2 100644
--- a/tangostationcontrol/requirements.txt
+++ b/tangostationcontrol/requirements.txt
@@ -12,3 +12,4 @@ psutil >= 5.8.0 # BSD
 docker >= 5.0.3 # Apache 2
 python-logstash-async >= 2.3.0 # MIT
 python-casacore >= 3.3.1 # GPL2
+etrs-itrs@git+https://github.com/brentjens/etrs-itrs # license pending
diff --git a/tangostationcontrol/tangostationcontrol/beam/delays.py b/tangostationcontrol/tangostationcontrol/beam/delays.py
index 771fad6c90a59ebe002867fedfc55446ead51000..ff81bd485fb66d8a043922a9a65443b3c080ff81 100644
--- a/tangostationcontrol/tangostationcontrol/beam/delays.py
+++ b/tangostationcontrol/tangostationcontrol/beam/delays.py
@@ -50,9 +50,21 @@ class delay_calculator:
 
         return numpy.dot(reference_direction_vector, relative_itrf) / speed_of_light
 
+    def is_valid_direction(self, direction):
+        try:
+            _ = self.measure.direction(*direction)
+        except RuntimeError as e:
+            return False
+
+        return True
+
     def convert(self, direction, antenna_itrf: list([float])):
-        # obtain the direction vector for a specific pointing
-        pointing = self.measure.direction(*direction)
+        try:
+            # obtain the direction vector for a specific pointing
+            pointing = self.measure.direction(*direction)
+        except RuntimeError as e:
+            raise ValueError(f"Could not convert direction {direction} into a pointing") from e
+
         reference_dir_vector = self.get_direction_vector(pointing)
 
         # # compute the delays for an antennas w.r.t. the reference position
diff --git a/tangostationcontrol/tangostationcontrol/beam/geo.py b/tangostationcontrol/tangostationcontrol/beam/geo.py
new file mode 100644
index 0000000000000000000000000000000000000000..033a6c4e4293573d05cb3fa09bcab5071c88a97b
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/beam/geo.py
@@ -0,0 +1,33 @@
+import etrsitrs
+import numpy
+
+"""
+   LOFAR station positions are measured in ETRS89, which are the coordinates of the position as it would be in 1989.
+   
+   These coordinates are carthesian (X, Y, Z), with (0, 0, 0) being the center of the Earth.
+
+   The ETRS89 positions differ from the current due to tectonic movements. Periodically, these differences are modelled
+   as position offsets and velocities. For example, ITRF2005 represents these offsets updated to 2005. We can
+   furthermore extrapolate these models to later dates, such as 2015.5.
+
+   By periodically extrapolating to later dates, or by using later models, we can obtain more precise positions of our
+   antennas without having to remeasure them.
+
+   The ETRSitrs package does all the transformation calculations for us.
+"""
+
+def ETRS_to_ITRF(ETRS_coordinates: numpy.array, ITRF_reference_frame: str = "ITRF2005", ITRF_reference_epoch: float = 2015.5) -> numpy.array:
+    """ Convert an array of coordinate triples from ETRS to ITRF, in the given reference frame and epoch. """
+
+    # fetch converter
+    ETRS_to_ITRF_fn = etrsitrs.convert_fn("ETRF2000", ITRF_reference_frame, ITRF_reference_epoch)
+
+    if ETRS_coordinates.ndim == 1:
+       # convert a single coordinate triple
+       ITRF_coordinates = ETRS_to_ITRF_fn(ETRS_coordinates)
+    else:
+       # convert each coordinate triple
+       ITRF_coordinates = numpy.apply_along_axis(ETRS_to_ITRF_fn, 1, ETRS_coordinates)
+
+    # return computed ITRF coordinates
+    return ITRF_coordinates
diff --git a/tangostationcontrol/tangostationcontrol/beam/hba_tile.py b/tangostationcontrol/tangostationcontrol/beam/hba_tile.py
new file mode 100644
index 0000000000000000000000000000000000000000..cede7be17c1c12773931e19ec81fd7d1a666b72b
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/beam/hba_tile.py
@@ -0,0 +1,68 @@
+import numpy
+from math import sin, cos
+
+class HBATAntennaOffsets(object):
+    """
+        This class helps calculate the absolute offsets of the antennas within a tile,
+        based on their relative orientation to ETRS. 
+    
+        These offsets are Known in LOFAR1 as "iHBADeltas".
+
+        Within LOFAR, we use a relative "PQR" coordinate system for each station:
+       
+        * The origin is the same as the ETRS reference position of the station,
+        * "Q" points to true North as seen from CS002LBA, the center of LOFAR,
+        * "P" points (roughly) east,
+        * "R" points (roughly) up,
+        * All antennas are positioned in the same plane (R=0 with an error < 3cm).
+
+        With the PQR->ETRS rotation matrix (or "ROTATION_MATRIX" in LOFAR1),
+        we can thus convert from PQR to (relative) ETRS coordinates.
+
+        Below, the rotation of the HBA tiles is provided with respect to this PQR frame.
+        Along with the PQR->ETRS rotation matrix, this allows us to calculate the
+    `   relative positions of each antenna element within a tile in ETRS space.
+
+        The relative ITRF positions are subsequently equal to the relative ETRS positions.
+        
+        For reference, see:
+            https://git.astron.nl/RD/lofar-referentie-vlak/-/blob/master/data/dts/dts.ipynb
+            https://git.astron.nl/ro/lofar/-/blob/master/MAC/Deployment/data/Coordinates/calc_hba_deltas.py
+            https://github.com/brentjens/lofar-antenna-positions/blob/master/lofarantpos/db.py#L208
+        """
+
+    """ Model of the HBAT1 tile, as offsets of each antenna with respect to the reference center, in metres. """
+    HBAT1_BASE_ANTENNA_OFFSETS = numpy.array(
+        [[-1.5, +1.5, 0.0], [-0.5, +1.5, 0.0], [+0.5, +1.5, 0.0], [+1.5, +1.5, 0.0],
+         [-1.5, +0.5, 0.0], [-0.5, +0.5, 0.0], [+0.5, +0.5, 0.0], [+1.5, +0.5, 0.0],
+         [-1.5, -0.5, 0.0], [-0.5, -0.5, 0.0], [+0.5, -0.5, 0.0], [+1.5, -0.5, 0.0],
+         [-1.5, -1.5, 0.0], [-0.5, -1.5, 0.0], [+0.5, -1.5, 0.0], [+1.5, -1.5, 0.0]]) * 1.25
+
+    @staticmethod
+    def rotation_matrix(rad: float) -> numpy.array:
+        """ Return a rotation matrix for coordinates for a given number of radians. """
+
+        rotation_matrix = numpy.array(
+            [[ cos(rad), sin(rad), 0],
+             [-sin(rad), cos(rad), 0],
+             [        0,        0, 1]])
+
+        return rotation_matrix
+
+    @staticmethod
+    def ITRF_offsets(base_antenna_offsets: numpy.array, PQR_rotation: float, PQR_to_ETRS_rotation_matrix: numpy.array) -> numpy.array:
+        """ Return the antenna offsets in ITRF, given:
+
+            :param: base_antenna_offsets:        antenna offsets within an unrotated tile (16x3).
+            :param: PQR_rotation:                rotation of the tile(s) in PQR space (radians).
+            :param: PQR_to_ETRS_rotation_matrix: rotation matrix for PQR -> ETRS conversion (3x3).
+        """
+
+        # Offsets in PQR are derived by rotating the base tile by the specified number of radians
+        PQR_offsets = numpy.inner(base_antenna_offsets, HBATAntennaOffsets.rotation_matrix(PQR_rotation))
+
+        # The PQR->ETRS mapping is a rotation as well
+        ETRS_offsets = numpy.inner(PQR_offsets, PQR_to_ETRS_rotation_matrix)
+
+        # The ITRF offsets are the same as the ETRS offsets
+        return ETRS_offsets
diff --git a/tangostationcontrol/tangostationcontrol/beam/test_delays.py b/tangostationcontrol/tangostationcontrol/beam/test_delays.py
deleted file mode 100644
index 1668345163c4f3bff244ede6ff63cf2c2786f9b1..0000000000000000000000000000000000000000
--- a/tangostationcontrol/tangostationcontrol/beam/test_delays.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from delays import *
-
-if __name__ == '__main__':
-    # # create a frame tied to the reference position
-    reference_itrf = [3826577.066, 461022.948, 5064892.786] # CS002LBA, in ITRF2005 epoch 2012.5
-    d = delay_calculator(reference_itrf)
-
-    # # set the timestamp to solve for
-    timestamp = datetime.datetime(2021,1,1,0,0,5)
-    d.set_measure_time(timestamp)
-
-    # compute the delays for an antennas w.r.t. the reference position
-    antenna_itrf = [[3826923.546, 460915.441, 5064643.489]] # CS001LBA, in ITRF2005 epoch 2012.5
-
-    # # obtain the direction vector for a specific pointing
-    direction = "J2000","0deg","0deg"
-
-    # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions.
-    delays = d.convert(direction, antenna_itrf)
-
-    # print the delays
-    # pprint.pprint(delays)
-
-
-    #test changing the time
-
-    print(f"Changing timestamp test\nBase parametres: Direction: {direction}, position: {antenna_itrf}")
-    for i in range(10):
-        # # set the timestamp to solve for
-        timestamp = datetime.datetime(2021,1,1,0,i,5)
-        d.set_measure_time(timestamp)
-
-        delays = d.convert(direction, antenna_itrf)
-
-        # print the delays
-        print(f"Timestamp: {timestamp}:   {delays}")
-
-
-    # reset time
-    timestamp = datetime.datetime(2021, 1, 1, 0, 0, 5)
-    d.set_measure_time(timestamp)
-
-
-    #test changing the antenna position
-    print(f"Changing Antenna position test.\nBase parametres: Time: {timestamp} Direction: {direction}")
-    for i in range(10):
-        antenna_itrf = [[3826577.066 + i, 461022.948, 5064892.786]]  # CS002LBA, in ITRF2005 epoch 2012.5
-
-        delays = d.convert(direction, antenna_itrf)
-
-        # print the delays
-        print(f"Antenna position: {antenna_itrf}:   {delays}")
-
-    # test changing the direction
-
-    antenna_itrf = [[3826923.546, 460915.441, 5064643.489]]  # CS001LBA, in ITRF2005 epoch 2012.5
-    print(f"Changing direction test.\nBase parametres: Time: {timestamp} , position: {antenna_itrf}")
-
-    for i in range(10):
-        direction = "J2000", f"{i}deg", "0deg"
-
-        delays = d.convert(direction, antenna_itrf)
-
-        # print the delays
-        print(f"Direction: {direction}:  {delays}")
diff --git a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
index b8e829ede0b5857cc5cc6c4da5d6052118cf57b7..7f9c8d07fb3e5ceca540c2a03294dc9a92450d66 100644
--- a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
+++ b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
@@ -1,8 +1,8 @@
 from tango.server import attribute
-from tango import AttrWriteType, DevState
+from tango import AttrWriteType
 import numpy
 
-from tangostationcontrol.devices.device_decorators import only_in_states, fault_on_error
+from tangostationcontrol.devices.device_decorators import fault_on_error
 import logging
 
 logger = logging.getLogger()
@@ -72,7 +72,6 @@ class attribute_wrapper(attribute):
         if access == AttrWriteType.READ_WRITE:
             """ if the attribute is of READ_WRITE type, assign the write function to it"""
 
-            @only_in_states([DevState.STANDBY, DevState.ON, DevState.ALARM], log=False)
             @fault_on_error()
             def write_func_wrapper(device, value):
                 """
@@ -86,7 +85,6 @@ class attribute_wrapper(attribute):
 
         """ Assign the read function to the attribute"""
 
-        @only_in_states([DevState.STANDBY, DevState.ON, DevState.ALARM], log=False)
         @fault_on_error()
         def read_func_wrapper(device):
             """
@@ -97,9 +95,13 @@ class attribute_wrapper(attribute):
 
         self.fget = read_func_wrapper
 
-        super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, **kwargs)
-
-        return
+        # "fisallowed" is called to ask us whether an attribute can be accessed. If not, the attribute won't be accessed,
+        # and the cache not updated. This forces Tango to also force a read the moment an attribute does become accessible.
+        # The provided function will be used with the call signature "(device: Device, req_type: AttReqType) -> bool".
+        #
+        # NOTE: fisallowed=<callable> does not work: https://gitlab.com/tango-controls/pytango/-/issues/435
+        # So we have to use fisallowed=<str> here, which causes the function device.<str> to be called.
+        super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_wrapper_allowed", **kwargs)
 
     def initial_value(self):
         """
diff --git a/tangostationcontrol/tangostationcontrol/clients/statistics_client.py b/tangostationcontrol/tangostationcontrol/clients/statistics_client.py
index 16d46bc71bbeba33c66aa8aa590f301cd0de0fa8..08d2889d0b61c5bd9c9ce5468e35ac6bf82fdffb 100644
--- a/tangostationcontrol/tangostationcontrol/clients/statistics_client.py
+++ b/tangostationcontrol/tangostationcontrol/clients/statistics_client.py
@@ -97,7 +97,14 @@ class StatisticsClient(AsyncCommClient):
         # redirect to right object. this works as long as the parameter names are unique among them.
         if annotation["type"] == "statistics":
             def read_function():
-                return self.collector.parameters[parameter]
+                if annotation.get("reshape", False):
+                    # force array into the shape of the attribute
+                    if attribute.dim_y > 1:
+                        return self.collector.parameters[parameter].reshape(attribute.dim_y, attribute.dim_x)
+                    else:
+                        return self.collector.parameters[parameter].reshape(attribute.dim_x)
+                else:
+                    return self.collector.parameters[parameter]
         elif annotation["type"] == "udp":
             def read_function():
                 return self.udp.parameters[parameter]
diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py
index 20b161e97b16f99d352340246770d5db9f99f3e3..6a3ef5ff8a609eecbc86242d745c7fcb97f61b1a 100644
--- a/tangostationcontrol/tangostationcontrol/devices/apsct.py
+++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py
@@ -79,12 +79,12 @@ class APSCT(opcua_device):
 
     def read_APSCT_error_R(self):
         return ((self.proxy.APSCTTR_I2C_error_R > 0)
-               | self.alarm_val("APSCT_PCB_ID_R")
-               | ~self.proxy.APSCT_INPUT_10MHz_good_R
-               | (~self.proxy.APSCT_INPUT_PPS_good_R & ~self.proxy.ASPCT_PPS_ignore_R)
-               | (~self.proxy.APSCT_PLL_160MHz_locked_R & ~self.proxy.APSCT_PLL_200MHz_locked_R)
-               | (self.proxy.APSCT_PLL_200MHz_locked_R & self.proxy.APSCT_PLL_200MHz_error_R)
-               | (self.proxy.APSCT_PLL_160MHz_locked_R & self.proxy.APSCT_PLL_160MHz_error_R)
+               or self.alarm_val("APSCT_PCB_ID_R")
+               or (not self.proxy.APSCT_INPUT_10MHz_good_R)
+               or (not self.proxy.APSCT_INPUT_PPS_good_R and not self.proxy.APSCT_PPS_ignore_R)
+               or (not self.proxy.APSCT_PLL_160MHz_locked_R and not self.proxy.APSCT_PLL_200MHz_locked_R)
+               or (self.proxy.APSCT_PLL_200MHz_locked_R and self.proxy.APSCT_PLL_200MHz_error_R)
+               or (self.proxy.APSCT_PLL_160MHz_locked_R and self.proxy.APSCT_PLL_160MHz_error_R)
                )
 
     APSCT_TEMP_error_R            = attribute(dtype=bool)
@@ -95,13 +95,13 @@ class APSCT(opcua_device):
 
     def read_APSCT_VOUT_error_R(self):
         return ( self.alarm_val("APSCT_PWR_PPSDIST_3V3_R")
-               | self.alarm_val("APSCT_PWR_CLKDIST1_3V3_R")
-               | self.alarm_val("APSCT_PWR_CLKDIST2_3V3_R")
-               | self.alarm_val("APSCT_PWR_CTRL_3V3_R")
-               | self.alarm_val("APSCT_PWR_INPUT_3V3_R")
-               | (self.proxy.APSCT_PWR_PLL_160MHz_on_R & self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R"))
-               | (self.proxy.APSCT_PWR_PLL_200MHz_on_R & self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R"))
-               | ~self.proxy.APSCT_PWR_on_R
+               or self.alarm_val("APSCT_PWR_CLKDIST1_3V3_R")
+               or self.alarm_val("APSCT_PWR_CLKDIST2_3V3_R")
+               or self.alarm_val("APSCT_PWR_CTRL_3V3_R")
+               or self.alarm_val("APSCT_PWR_INPUT_3V3_R")
+               or (self.proxy.APSCT_PWR_PLL_160MHz_on_R and self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R"))
+               or (self.proxy.APSCT_PWR_PLL_200MHz_on_R and self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R"))
+               or (not self.proxy.APSCT_PWR_on_R)
                )
 
     # --------
diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py
index d014a0eae151d89e77c71a9f0048dfb5e01bcd6b..6be213696c16efc69ca8f20319efc03cfc0f4fe7 100644
--- a/tangostationcontrol/tangostationcontrol/devices/apspu.py
+++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py
@@ -67,10 +67,10 @@ class APSPU(opcua_device):
 
     def read_APSPU_error_R(self):
         return ((self.proxy.APSPUTR_I2C_error_R > 0)
-               | self.alarm_val("APSPU_PCB_ID_R")
-               | self.alarm_val("APSPU_FAN1_RPM_R")
-               | self.alarm_val("APSPU_FAN2_RPM_R") 
-               | self.alarm_val("APSPU_FAN3_RPM_R"))
+               or self.alarm_val("APSPU_PCB_ID_R")
+               or self.alarm_val("APSPU_FAN1_RPM_R")
+               or self.alarm_val("APSPU_FAN2_RPM_R") 
+               or self.alarm_val("APSPU_FAN3_RPM_R"))
 
     APSPU_IOUT_error_R          = attribute(dtype=bool)
     APSPU_TEMP_error_R          = attribute(dtype=bool)
@@ -78,20 +78,20 @@ class APSPU(opcua_device):
 
     def read_APSPU_IOUT_error_R(self):
         return ( self.alarm_val("APSPU_LBA_IOUT_R")
-               | self.alarm_val("APSPU_RCU2A_IOUT_R")
-               | self.alarm_val("APSPU_RCU2D_IOUT_R")
+               or self.alarm_val("APSPU_RCU2A_IOUT_R")
+               or self.alarm_val("APSPU_RCU2D_IOUT_R")
                )
 
     def read_APSPU_TEMP_error_R(self):
         return ( self.alarm_val("APSPU_LBA_TEMP_R")
-               | self.alarm_val("APSPU_RCU2A_TEMP_R")
-               | self.alarm_val("APSPU_RCU2D_TEMP_R")
+               or self.alarm_val("APSPU_RCU2A_TEMP_R")
+               or self.alarm_val("APSPU_RCU2D_TEMP_R")
                )
 
     def read_APSPU_VOUT_error_R(self):
         return ( self.alarm_val("APSPU_LBA_VOUT_R")
-               | self.alarm_val("APSPU_RCU2A_VOUT_R")
-               | self.alarm_val("APSPU_RCU2D_VOUT_R")
+               or self.alarm_val("APSPU_RCU2A_VOUT_R")
+               or self.alarm_val("APSPU_RCU2D_VOUT_R")
                )
 
     # --------
diff --git a/tangostationcontrol/tangostationcontrol/devices/beam.py b/tangostationcontrol/tangostationcontrol/devices/beam.py
index ac6accea05a7131ff58f0c48daf880267fdb098d..1f24e804abd1469c11caf86506ac95fecb88a0ac 100644
--- a/tangostationcontrol/tangostationcontrol/devices/beam.py
+++ b/tangostationcontrol/tangostationcontrol/devices/beam.py
@@ -11,8 +11,9 @@ import numpy
 import datetime
 from json import loads
 
-from tango.server import attribute, command
-from tango import AttrWriteType, DebugIt, DevState, DeviceProxy, DevVarStringArray, DevVarDoubleArray, DevString
+from tango.server import attribute, command, device_property
+from tango import AttrWriteType, DebugIt, DevState, DeviceProxy, DevVarStringArray, DevVarDoubleArray, DevString, DevSource
+from threading import Thread, Lock, Condition
 
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
@@ -25,30 +26,56 @@ from tangostationcontrol.devices.device_decorators import *
 import logging
 logger = logging.getLogger()
 
-__all__ = ["Beam", "main"]
 
 
+__all__ = ["Beam", "main", "BeamTracker"]
+
 @device_logging_to_python()
 class Beam(lofar_device):
 
-    _hbat_pointing_direction = numpy.zeros((96,3), dtype=numpy.str)
-    _hbat_pointing_timestamp = numpy.zeros(96, dtype=numpy.double)
-
     # -----------------
     # Device Properties
     # -----------------
 
+    HBAT_beam_tracking_interval = device_property(
+        dtype='DevFloat',
+        doc='HBAT beam weights updating interval time [seconds]',
+        mandatory=False,
+        default_value = 10.0
+    )
+
+    HBAT_beam_tracking_preparation_period = device_property(
+        dtype='DevFloat',
+        doc='Preparation time [seconds] needed before starting update operation',
+        mandatory=False,
+        default_value = 0.25
+    )
+
     # ----------
     # Attributes
     # ----------
 
     HBAT_pointing_direction_R = attribute(access=AttrWriteType.READ,
         dtype=((numpy.str,),), max_dim_x=3, max_dim_y=96,
-        fget=lambda self: self._hbat_pointing_direction)
+        fget=lambda self: self._hbat_pointing_direction_r)
+    
+    HBAT_pointing_direction_RW = attribute(access=AttrWriteType.READ_WRITE,
+        dtype=((numpy.str,),), max_dim_x=3, max_dim_y=96,
+        fget=lambda self: self._hbat_pointing_direction_rw)
 
     HBAT_pointing_timestamp_R = attribute(access=AttrWriteType.READ,
         dtype=(numpy.double,), max_dim_x=96,
-        fget=lambda self: self._hbat_pointing_timestamp)
+        fget=lambda self: self._hbat_pointing_timestamp_r)
+
+    HBAT_tracking_enabled_R = attribute(access=AttrWriteType.READ,
+        doc="Whether the HBAT tile beam is updated periodically",
+        dtype=numpy.bool,
+        fget=lambda self: self.HBAT_beam_tracker.is_alive())
+
+    HBAT_tracking_enabled_RW = attribute(access=AttrWriteType.READ_WRITE,
+        doc="Whether the HBAT tile beam should be updated periodically",
+        dtype=numpy.bool,
+        fget=lambda self: self._hbat_tracking_enabled_rw)
 
     # Directory where the casacore measures that we use, reside. We configure ~/.casarc to
     # use the symlink /opt/IERS/current, which we switch to the actual set of files to use.
@@ -64,24 +91,72 @@ class Beam(lofar_device):
     def configure_for_initialise(self):
         super().configure_for_initialise()
 
+        # Initialise pointing array data and attribute
+        self._hbat_pointing_timestamp_r     = numpy.zeros(96, dtype=numpy.double)
+        self._hbat_pointing_direction_r     = numpy.zeros((96,3), dtype="<U32")
+        self._hbat_pointing_direction_rw    = numpy.array([["AZELGEO","0deg","90deg"]] * 96, dtype="<U32")
+
+        # Initialise tracking control
+        self._hbat_tracking_enabled_rw      = True
+
         # Set a reference of RECV device
         self.recv_proxy = DeviceProxy("STAT/RECV/1")
+        self.recv_proxy.set_source(DevSource.DEV)
 
         # Retrieve positions from RECV device
         HBAT_reference_itrf = self.recv_proxy.HBAT_reference_itrf_R
-        HBAT_antenna_itrf_offsets = self.recv_proxy.HBAT_antenna_itrf_offsets_R
+        HBAT_antenna_itrf_offsets = self.recv_proxy.HBAT_antenna_itrf_offsets_R.reshape(96,16,3)
 
         # a delay calculator for each tile
         self.HBAT_delay_calculators = [delay_calculator(reference_itrf) for reference_itrf in HBAT_reference_itrf]
 
         # absolute positions of each antenna element
-        self.HBAT_antenna_positions = [reference_itrf + HBAT_antenna_itrf_offsets for reference_itrf in HBAT_reference_itrf]
+        self.HBAT_antenna_positions = [HBAT_reference_itrf[tile] + HBAT_antenna_itrf_offsets[tile] for tile in range(96)]
+
+        # Create a thread object to update HBAT beam weights
+        self.HBAT_beam_tracker = BeamTracker(self)
+
+    @log_exceptions()
+    def configure_for_on(self):
+        super().configure_for_on()
+
+        # Start beam tracking thread
+        if self._hbat_tracking_enabled_rw:
+            self.HBAT_beam_tracker.start()
+    
+    @log_exceptions()
+    def configure_for_off(self):
+        # Stop thread object
+        self.HBAT_beam_tracker.stop()
+
+        super().configure_for_off()
 
     # --------
     # internal functions
     # --------
 
-    def _HBAT_delays(self, pointing_direction: numpy.array, timestamp: datetime.datetime = None):
+    def write_HBAT_pointing_direction_RW(self, value):
+        """ Setter method for attribute HBAT_pointing_direction_RW """
+        # verify whether values are valid
+        for tile in range(96):
+            if not self.HBAT_delay_calculators[tile].is_valid_direction(value[tile]):
+                raise ValueError(f"Invalid direction: {value[tile]}")
+
+        self._hbat_pointing_direction_rw = value
+
+        # force update across tiles if pointing changes
+        self.HBAT_beam_tracker.force_update()
+        logger.info("Pointing direction update requested")
+
+    def write_HBAT_tracking_enabled_RW(self, value):
+        self._hbat_tracking_enabled_rw = value
+
+        if value:
+            self.HBAT_beam_tracker.start()
+        else:
+            self.HBAT_beam_tracker.stop()
+
+    def _HBAT_delays(self, pointing_direction: numpy.array, timestamp: datetime.datetime = datetime.datetime.now()):
         """
         Calculate the delays (in seconds) based on the pointing list and the timestamp
         """
@@ -120,17 +195,18 @@ class Beam(lofar_device):
         delays = delays.flatten()
         HBAT_bf_delay_steps = self.recv_proxy.calculate_HBAT_bf_delay_steps(delays)
         HBAT_bf_delay_steps = numpy.array(HBAT_bf_delay_steps, dtype=numpy.int64).reshape(96,32)
-
+    
         # Write weights to RECV
         self.recv_proxy.HBAT_BF_delay_steps_RW = HBAT_bf_delay_steps
 
         # Record where we now point to, now that we've updated the weights.
         # Only the entries within the mask have been updated
-        mask = self.recv_proxy.Ant_mask_RW.flatten()
+        mask = self.recv_proxy.ANT_mask_RW.flatten()
         for rcu in range(96):
             if mask[rcu]:
-                self._hbat_pointing_direction[rcu] = pointing_direction[rcu]
-                self._hbat_pointing_timestamp[rcu] = timestamp.timestamp()
+                self._hbat_pointing_direction_r[rcu]    = pointing_direction[rcu]
+                self._hbat_pointing_timestamp_r[rcu]    = timestamp.timestamp()
+        logger.info("Pointing direction updated")
 
     # --------
     # Commands
@@ -190,6 +266,7 @@ class Beam(lofar_device):
     
     @command(dtype_in=DevVarStringArray)
     @DebugIt()
+    @log_exceptions()
     @only_in_states([DevState.ON])
     def HBAT_set_pointing(self, pointing_direction: list, timestamp: datetime.datetime = None):
         """
@@ -236,3 +313,104 @@ class Beam(lofar_device):
 def main(**kwargs):
     """Main function of the ObservationControl module."""
     return entry(Beam, **kwargs)
+
+# ----------
+# Beam Tracker
+# ---------- 
+class BeamTracker():
+    
+    DISCONNECT_TIMEOUT = 3.0
+
+    """ Object that encapsulates a Thread, resposible for beam tracking operations """
+    def __init__(self, device: lofar_device):
+        self.thread = None
+        self.device = device
+
+        # Condition to trigger a forced update or early abort
+        self.update_lock = Lock()
+        self.update_condition = Condition(self.update_lock)
+
+        # Whether the pointing has to be forced updated
+        self.stale_pointing = True
+    
+    def start(self):
+        """ Starts the Beam Tracking thread """
+        if self.thread:
+            # already started
+            return
+
+        self.done = False
+        self.thread = Thread(target=self._update_HBAT_pointing_direction, name=f"BeamTracker of {self.device.get_name()}")
+        self.thread.start()
+
+        logger.info("BeamTracking thread started")
+    
+    def is_alive(self):
+        """ Returns True just before the Thread run() method starts until just after the Thread run() method terminates. """
+        return self.thread and self.thread.is_alive()
+
+    def force_update(self):
+        """ Force the pointing to be updated. """
+
+        self.stale_pointing = True
+        self.unlock_thread()
+
+    def unlock_thread(self):
+        # inform the thread to stop waiting
+        with self.update_lock:
+            self.update_condition.notify()
+    
+    def stop(self):
+        """ Stops the Beam Tracking loop """
+
+        if not self.thread:
+            return
+        
+        logger.info("BeamTracking thread stopping")
+
+        self.done = True
+        self.force_update()
+
+        # wait for thread to finish
+        self.thread.join(self.DISCONNECT_TIMEOUT)
+
+        if self.is_alive():
+            logger.error("BeamTracking Thread did not properly terminate")
+
+        self.thread = None
+        
+        logger.info("BeamTracking thread stopped")
+    
+    def _get_sleep_time(self):
+        """ Computes the sleep time (in seconds) that needs to be waited for the next beam tracking update """  
+        now = datetime.datetime.now().timestamp()
+
+        # Computes the left seconds before the next update 
+        next_update_in = self.device.HBAT_beam_tracking_interval - (now % self.device.HBAT_beam_tracking_interval)
+
+        # Computes the needed sleep time before the next update
+        sleep_time = next_update_in - self.device.HBAT_beam_tracking_preparation_period
+        # If sleep time is negative, add the tracking interval for the next update
+        if sleep_time < 0:
+            return sleep_time + self.device.HBAT_beam_tracking_interval
+        else:
+            return sleep_time
+
+    # @fault_on_error routes errors here. we forward them to our device
+    def Fault(self, msg):
+        self.device.Fault(msg)
+   
+    @log_exceptions()
+    @fault_on_error()
+    def _update_HBAT_pointing_direction(self):
+        """ Updates the beam weights using a fixed interval of time """
+
+        # Check if  flag beamtracking is true
+        with self.update_lock:
+            while not self.done:
+                self.stale_pointing = False
+                self.device._HBAT_set_pointing(self.device._hbat_pointing_direction_rw, datetime.datetime.now())
+
+                # sleep until the next update, or when interrupted (this releases the lock, allowing for notification)
+                # note that we need wait_for as conditions can be triggered multiple times in succession
+                self.update_condition.wait_for(lambda: self.done or self.stale_pointing, self._get_sleep_time())
diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py
index 7dc95e7480231237a6c45ba349797a01e2c10056..0bee84c2edacafae8e387a5c2309efe2f35bcd5e 100644
--- a/tangostationcontrol/tangostationcontrol/devices/boot.py
+++ b/tangostationcontrol/tangostationcontrol/devices/boot.py
@@ -260,10 +260,16 @@ class Boot(lofar_device):
         super().init_device()
 
         # always turn on automatically, so the user doesn't have to boot the boot device
-        # note: we overloaded our own boot() to boot the station, so explicitly call our own initialisation
         self.Initialise()
         self.On()
 
+    # Override to avoid having this called during Initialise, which we call
+    # from init_device. The original clear_poll_cache requires a functioning
+    # self.proxy, which is not yet the case during init_device. The proxy
+    # needs a running server_loop, which is started after init_device.
+    def clear_poll_cache(self):
+        pass
+
     @log_exceptions()
     def configure_for_off(self):
         """ user code here. is called when the state is set to OFF """
diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
index a53fdfcfd8b61167789a4ed2a05728379d026e61..6943f86d4bde44010662cbcea45cb350ca271cea 100644
--- a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
@@ -62,7 +62,10 @@ class lofar_device(Device, metaclass=DeviceMeta):
 
     version_R = attribute(dtype=str, access=AttrWriteType.READ, fget=lambda self: get_version())
 
-    # list of property names too be set first by set_defaults
+    # list of translator property names to be set by set_translator_defaults
+    translator_default_settings = []
+
+    # list of hardware property names to be set first by set_defaults
     first_default_settings = []
 
     @classmethod
@@ -75,6 +78,31 @@ class lofar_device(Device, metaclass=DeviceMeta):
 
         self.value_dict = {i: i.initial_value() for i in self.attr_list()}
 
+    def is_attribute_wrapper_allowed(self, req_type):
+        """ Returns whether an attribute wrapped by the attribute_wrapper be accessed. """
+
+        return self.get_state() in [DevState.STANDBY, DevState.ON, DevState.ALARM]
+
+    def clear_poll_cache(self):
+        """ Remove all attributes from the poll cache, to remove stale entries
+            once we know the values can be read. """
+
+        # we use proxy.attribute_query_list(), as proxy.get_attribute_list() will throw if we
+        # call it too soon when starting everything (database, device) from scratch.
+        attr_names = [config.name for config in self.proxy.attribute_list_query()]
+
+        for attr_name in attr_names:
+            if self.proxy.is_attribute_polled(attr_name):
+                # save poll period
+                poll_period = self.proxy.get_attribute_poll_period(attr_name)
+
+                try:
+                    # stop polling to remove cache entry
+                    self.proxy.stop_poll_attribute(attr_name)
+                finally:
+                    # start polling again
+                    self.proxy.poll_attribute(attr_name, poll_period)
+
     @log_exceptions()
     def init_device(self):
         """ Instantiates the device in the OFF state. """
@@ -131,10 +159,12 @@ class lofar_device(Device, metaclass=DeviceMeta):
 
         # reload our class & device properties from the Tango database
         self.get_device_properties()
+        self.properties_changed()
 
         self.configure_for_initialise()
 
-        self.properties_changed()
+        # any values read so far are stale. clear the polling cache.
+        self.clear_poll_cache()
 
         self.set_state(DevState.STANDBY)
         self.set_status("Device is in the STANDBY state.")
@@ -224,6 +254,27 @@ class lofar_device(Device, metaclass=DeviceMeta):
         """Method always executed before any TANGO command is executed."""
         pass
 
+    def _set_defaults(self, attribute_names: list):
+        """ Set hardware points to their default value.
+
+            attribute_names: The names of the attributes to set to their default value.
+
+            A hardware point XXX is set to the value of the object member named XXX_default, if it exists.
+            XXX_default can be f.e. a constant, or a device_property.
+        """
+
+        # set them all
+        for name in attribute_names:
+            try:
+                default_value = getattr(self, f"{name}_default")
+
+                # set the attribute to the configured default
+                logger.debug(f"Setting attribute {name} to {default_value}")
+                self.proxy.write_attribute(name, default_value)
+            except Exception as e:
+                # log which attribute we're addressing
+                raise Exception(f"Cannot assign default to attribute {name}") from e
+
     def properties_changed(self):
         pass
 
@@ -240,7 +291,7 @@ class lofar_device(Device, metaclass=DeviceMeta):
             The points are set in the following order:
                 1) The python class property 'first_default_settings' is read, as an array of strings denoting property names. Each property
                    is set in that order.
-                2) Any remaining default properties are set.
+                2) Any remaining default properties are set, except the translators (those in 'translator_default_settings').
         """
 
         # collect all attributes for which defaults are provided
@@ -248,22 +299,33 @@ class lofar_device(Device, metaclass=DeviceMeta):
                                     # collect all attribute members
                                     if isinstance(getattr(self, name), Attribute)
                                     # with a default set
-                                    and hasattr(self, f"{name}_default")]
+                                    and hasattr(self, f"{name}_default")
+                                    and name not in self.translator_default_settings]
 
         # determine the order: first do the ones mentioned in default_settings_order
         attributes_to_set = self.first_default_settings + [name for name in attributes_with_defaults if name not in self.first_default_settings]
 
-        # set them all
-        for name in attributes_to_set:
-            try:
-                default_value = getattr(self, f"{name}_default")
+        # set them
+        self._set_defaults(attributes_to_set)
 
-                # set the attribute to the configured default
-                logger.debug(f"Setting attribute {name} to {default_value}")
-                self.proxy.write_attribute(name, default_value)
-            except Exception as e:
-                # log which attribute we're addressing
-                raise Exception(f"Cannot assign default to attribute {name}") from e
+
+    @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON])
+    @fault_on_error()
+    @command()
+    def set_translator_defaults(self):
+        """ Initialise the translator translators to their configured settings. """
+
+        # This is just the command version of _set_translator_defaults().
+        self._set_translator_defaults()
+
+    @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON])
+    @fault_on_error()
+    @command()
+    def prepare_hardware(self):
+        """ Load firmware required before configuring anything. """
+
+        # This is just the command version of _prepare_hardware().
+        self._prepare_hardware()
 
     @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON])
     @fault_on_error()
@@ -278,7 +340,12 @@ class lofar_device(Device, metaclass=DeviceMeta):
         # setup connections
         self.Initialise()
 
+        self.set_translator_defaults()
+
         if initialise_hardware:
+            # prepare hardware to accept settings
+            self.prepare_hardware()
+
             # initialise settings
             self.set_defaults()
 
@@ -288,7 +355,6 @@ class lofar_device(Device, metaclass=DeviceMeta):
         # make device available
         self.On()
 
-
     @only_in_states([DevState.OFF])
     @command()
     def boot(self):
@@ -299,6 +365,15 @@ class lofar_device(Device, metaclass=DeviceMeta):
     def warm_boot(self):
         self._boot(initialise_hardware=False)
 
+    def _set_translator_defaults(self):
+        """ Initialise any translators to their default settings. """
+
+        self._set_defaults(self.translator_default_settings)
+
+    def _prepare_hardware(self):
+        """ Override this method to load any firmware before configuring the hardware. """
+        pass
+
     def _initialise_hardware(self):
         """ Override this method to initialise any hardware after configuring it. """
         pass
@@ -309,13 +384,21 @@ class lofar_device(Device, metaclass=DeviceMeta):
 
             Raises an Exception if it has not after the timeout.
 
+            value:       The value that needs to be matched, or a function
+                         that needs to evaluate to True given the attribute.
             timeout:     time until an Exception is raised, in seconds.
             pollperiod:  how often to check the attribute, in seconds.
         """
+        if type(value) == type(lambda x: True):
+            # evaluate function
+            is_correct = value
+        else:
+            # compare to value
+            is_correct = lambda x: x == value
 
         # Poll every half a second
         for _ in range(math.ceil(timeout/pollperiod)):
-            if getattr(self.proxy, attr_name) != value:
+            if is_correct(getattr(self.proxy, attr_name)):
                 return
 
             time.sleep(pollperiod)
@@ -335,7 +418,7 @@ class lofar_device(Device, metaclass=DeviceMeta):
         # fetch attribute value as an array
         value = self.proxy.read_attribute(attr_name).value
         if is_scalar:
-            value = numpy.array(value)
+            value = numpy.array(value) # this stays a scalar in numpy
 
         # construct alarm state, in the same shape as the attribute
         alarm_state = numpy.zeros(value.shape, dtype=bool)
@@ -346,9 +429,9 @@ class lofar_device(Device, metaclass=DeviceMeta):
         if alarms.min_alarm != 'Not specified':
             alarm_state |= value <= value.dtype.type(alarms.min_alarm)
 
-        # return alarm state, as the same type as the attribute
+        # return alarm state, with the same dimensions as the attribute
         if is_scalar:
-            return alarm_state[0]
+            return alarm_state.item()
         else:
             return alarm_state
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py
index f2acb0794d294eece593ad247b5b15b19c0ee4ad..78dbece6bd2be4d56bd877e77b2ba2067c0570c3 100644
--- a/tangostationcontrol/tangostationcontrol/devices/recv.py
+++ b/tangostationcontrol/tangostationcontrol/devices/recv.py
@@ -17,9 +17,13 @@ from tango import DebugIt
 from tango.server import command
 from tango.server import device_property, attribute
 from tango import AttrWriteType, DevState, DevVarFloatArray
+
 import numpy
+from math import pi
 
 # Additional import
+from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets
+from tangostationcontrol.beam.geo import ETRS_to_ITRF
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper
@@ -37,6 +41,9 @@ class RECV(opcua_device):
     # -----------------
     # Device Properties
     # -----------------
+
+    # ----- Default settings
+
     ANT_mask_RW_default = device_property(
         dtype='DevVarBooleanArray',
         mandatory=False,
@@ -67,6 +74,13 @@ class RECV(opcua_device):
         default_value=1
     )
 
+    translator_default_settings = [
+        'ANT_mask_RW',
+        'RCU_mask_RW'
+    ]
+
+    # ----- Calibration values
+    
     HBAT_bf_delay_step_delays = device_property(
         dtype="DevVarFloatArray",
         mandatory=False,
@@ -80,40 +94,64 @@ class RECV(opcua_device):
             14.9781E-9, 15.5063E-9
         ],dtype=numpy.float64)
     )
-    
-    HBAT_reference_itrf = device_property(
+
+    HBAT_signal_input_delays = device_property(
         dtype='DevVarFloatArray',
-        mandatory=False
+        mandatory=False,
+        default_value = numpy.zeros((96,32), dtype=numpy.float64)
     )
 
-    HBAT_antenna_itrf_offsets = device_property(
+    HBAT_base_antenna_offsets = device_property(
+        doc="Offsets of the antennas in a HBAT, with respect to its reference center (16x3).",
         dtype='DevVarFloatArray',
-        mandatory=False
+        mandatory=False,
+        default_value = HBATAntennaOffsets.HBAT1_BASE_ANTENNA_OFFSETS.flatten()
     )
 
-    HBAT_signal_input_delays = device_property(
+    # ----- Position information
+
+    HBAT_reference_ITRF = device_property(
+        doc="ITRF position (XYZ) of each HBAT (leave empty to auto-derive from ETRS)",
         dtype='DevVarFloatArray',
-        mandatory=False,
-        default_value = numpy.zeros((96,32), dtype=numpy.float64)
+        mandatory=False
+    )
+ 
+    HBAT_reference_ETRS = device_property(
+        doc="ETRS position (XYZ) of each HBAT",
+        dtype='DevVarFloatArray',
+        mandatory=False
     )
 
     ITRF_Reference_Frame = device_property(
+        doc="Reference frame in which the ITRF coordinates are provided, or are to be computed from ETRS89",
         dtype='DevString',
         mandatory=False,
         default_value = "ITRF2005"
     )
 
     ITRF_Reference_Epoch = device_property(
+        doc="Reference epoch in which the ITRF coordinates are provided, or are to be extrapolated from ETRS89",
         dtype='DevFloat',
         mandatory=False,
         default_value = 2015.5
     )
 
-    first_default_settings = [
-        # set the masks first, as those filter any subsequent settings
-        'ANT_mask_RW',
-        'RCU_mask_RW'
-    ]
+    HBAT_PQR_rotation_angles_deg = device_property(
+        doc='Rotation of each tile in the PQ plane ("horizontal") in degrees.',
+        dtype='DevVarFloatArray',
+        mandatory=False,
+        default_value = [0.0] * 96
+    )
+
+    HBAT_PQR_to_ETRS_rotation_matrix = device_property(
+        doc="Field-specific rotation matrix to convert PQR offsets to ETRS/ITRF offsets.",
+        dtype='DevVarFloatArray',
+        mandatory=False,
+        default_value = numpy.array([ # PQR->ETRS rotation matrix for the core stations
+                            [-0.1195951054, -0.7919544517, 0.5987530018],
+                            [ 0.9928227484, -0.0954186800, 0.0720990002],
+                            [ 0.0000330969,  0.6030782884, 0.7976820024]]).flatten()
+    )
 
     # ----------
     # Attributes
@@ -162,13 +200,38 @@ class RECV(opcua_device):
     RECVTR_monitor_rate_RW       = attribute_wrapper(comms_annotation=["RECVTR_monitor_rate_RW"    ],datatype=numpy.int64  , access=AttrWriteType.READ_WRITE)
     RECVTR_translator_busy_R     = attribute_wrapper(comms_annotation=["RECVTR_translator_busy_R"  ],datatype=numpy.bool_  )
 
-    HBAT_antenna_itrf_offsets_R = attribute(access=AttrWriteType.READ,
-        dtype=((numpy.float,),), max_dim_x=3, max_dim_y=16,
-        fget=lambda self: numpy.array(self.HBAT_antenna_itrf_offsets).reshape(16,3))
+    # ----- Position information
+
+    HBAT_antenna_ITRF_offsets_R = attribute(access=AttrWriteType.READ,
+        doc='Offsets of the antennas within a tile, in ITRF ("iHBADeltas"). True shape: 96x16x3.',
+        dtype=((numpy.float,),), max_dim_x=48, max_dim_y=96)
+
+    HBAT_reference_ITRF_R = attribute(access=AttrWriteType.READ,
+        doc='Absolute reference position of each tile, in ITRF',
+        dtype=((numpy.float,),), max_dim_x=3, max_dim_y=96)
+
+    def read_HBAT_antenna_ITRF_offsets_R(self):
+        base_antenna_offsets        = numpy.array(self.HBAT_base_antenna_offsets).reshape(16,3)
+        PQR_to_ETRS_rotation_matrix = numpy.array(self.HBAT_PQR_to_ETRS_rotation_matrix).reshape(3,3)
 
-    HBAT_reference_itrf_R = attribute(access=AttrWriteType.READ,
-        dtype=((numpy.float,),), max_dim_x=3, max_dim_y=96,
-        fget=lambda self: numpy.array(self.HBAT_reference_itrf).reshape(96,3))
+        # each tile has its own rotation angle, resulting in different offsets per tile
+        all_offsets = numpy.array(
+                   [HBATAntennaOffsets.ITRF_offsets(
+                       base_antenna_offsets,
+                       self.HBAT_PQR_rotation_angles_deg[tile] * pi / 180,
+                       PQR_to_ETRS_rotation_matrix)
+                    for tile in range(96)])
+
+        return all_offsets.reshape(96,48)
+
+    def read_HBAT_reference_ITRF_R(self):
+        # provide ITRF coordinates if they were configured
+        if self.HBAT_reference_ITRF:
+            return numpy.array(self.HBAT_reference_ITRF).reshape(96,3)
+
+        # calculate them from ETRS coordinates if not, using the configured ITRF reference
+        ETRS_coordinates = numpy.array(self.HBAT_reference_ETRS).reshape(96,3)
+        return ETRS_to_ITRF(ETRS_coordinates, self.ITRF_Reference_Frame, self.ITRF_Reference_Epoch)
 
     # ----------
     # Summarising Attributes
@@ -197,9 +260,9 @@ class RECV(opcua_device):
     RECV_VOUT_error_R          = attribute(dtype=(bool,), max_dim_x=32)
 
     def read_RECV_IOUT_error_R(self):
-        return self.proxy.ANT_mask_RW & (
+        return (self.proxy.ANT_mask_RW & (
                  self.alarm_val("RCU_PWR_ANT_IOUT_R")
-               ).any(axis=1)
+               )).any(axis=1)
 
     def read_RECV_TEMP_error_R(self):
         # Don't apply the mask here --- we always want to know if things get too hot!
@@ -211,13 +274,13 @@ class RECV(opcua_device):
         return (self.proxy.ANT_mask_RW & (
                  self.alarm_val("RCU_PWR_ANT_VIN_R")
                | self.alarm_val("RCU_PWR_ANT_VOUT_R")
-               ).any(axis=1) | (self.proxy.RCU_mask_RW & (
+               )).any(axis=1) | (self.proxy.RCU_mask_RW & (
                  self.alarm_val("RCU_PWR_1V8_R")
                | self.alarm_val("RCU_PWR_2V5_R")
                | self.alarm_val("RCU_PWR_3V3_R")
                | ~self.proxy.RCU_PWR_DIGITAL_on_R
                | ~self.proxy.RCU_PWR_good_R
-               )))
+               ))
 
     # --------
     # overloaded functions
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
index 93e9077f07580f79a3f06e89d81a4e5fe42ae7c8..a483aec43eacf571a6344ee1b1823cf62af96a92 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
@@ -89,8 +89,7 @@ class SDP(opcua_device):
         default_value=[[8192] * 12 * 512] * 16
     )
 
-    first_default_settings = [
-        # set the masks first, as those filter any subsequent settings
+    translator_default_settings = [
         'TR_fpga_mask_RW'
     ]
 
@@ -109,8 +108,8 @@ class SDP(opcua_device):
     FPGA_beamlet_output_scale_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_R"], datatype=numpy.uint32, dims=(16,))
     FPGA_beamlet_output_scale_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
     FPGA_firmware_version_R = attribute_wrapper(comms_annotation=["FPGA_firmware_version_R"], datatype=numpy.str, dims=(16,))
-    FPGA_reboot_R = attribute_wrapper(comms_annotation=["FPGA_reboot_R"], datatype=numpy.uint32, dims=(16,), doc="Active FPGA image (0=factory, 1=user)")
-    FPGA_reboot_RW = attribute_wrapper(comms_annotation=["FPGA_reboot_R"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
+    FPGA_boot_image_R = attribute_wrapper(comms_annotation=["FPGA_boot_image_R"], datatype=numpy.uint32, dims=(16,), doc="Active FPGA image (0=factory, 1=user)")
+    FPGA_boot_image_RW = attribute_wrapper(comms_annotation=["FPGA_boot_image_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
     FPGA_global_node_index_R = attribute_wrapper(comms_annotation=["FPGA_global_node_index_R"], datatype=numpy.uint32, dims=(16,))
     FPGA_hardware_version_R = attribute_wrapper(comms_annotation=["FPGA_hardware_version_R"], datatype=numpy.str, dims=(16,))
     FPGA_processing_enable_R = attribute_wrapper(comms_annotation=["FPGA_processing_enable_R"], datatype=numpy.bool_, dims=(16,))
@@ -130,8 +129,6 @@ class SDP(opcua_device):
     FPGA_subband_weights_R = attribute_wrapper(comms_annotation=["FPGA_subband_weights_R"], datatype=numpy.uint32, dims=(12 * 512, 16))
     FPGA_subband_weights_RW = attribute_wrapper(comms_annotation=["FPGA_subband_weights_RW"], datatype=numpy.uint32, dims=(12 * 512, 16), access=AttrWriteType.READ_WRITE)
     FPGA_temp_R = attribute_wrapper(comms_annotation=["FPGA_temp_R"], datatype=numpy.float_, dims=(16,))
-    FPGA_weights_R = attribute_wrapper(comms_annotation=["FPGA_weights_R"], datatype=numpy.int16, dims=(12 * 488 * 2, 16))
-    FPGA_weights_RW = attribute_wrapper(comms_annotation=["FPGA_weights_RW"], datatype=numpy.int16, dims=(12 * 488 * 2, 16), access=AttrWriteType.READ_WRITE)
     FPGA_wg_amplitude_R = attribute_wrapper(comms_annotation=["FPGA_wg_amplitude_R"], datatype=numpy.float_, dims=(12, 16))
     FPGA_wg_amplitude_RW = attribute_wrapper(comms_annotation=["FPGA_wg_amplitude_RW"], datatype=numpy.float_, dims=(12, 16), access=AttrWriteType.READ_WRITE)
     FPGA_wg_enable_R = attribute_wrapper(comms_annotation=["FPGA_wg_enable_R"], datatype=numpy.bool_, dims=(12, 16))
@@ -212,25 +209,39 @@ class SDP(opcua_device):
     # ----------
     FPGA_error_R                 = attribute(dtype=(bool,), max_dim_x=16)
     FPGA_processing_error_R      = attribute(dtype=(bool,), max_dim_x=16)
+    FPGA_input_error_R           = attribute(dtype=(bool,), max_dim_x=16)
 
     def read_FPGA_error_R(self):
         return self.proxy.TR_fpga_mask_RW & (
                  self.proxy.TR_fpga_communication_error_R
-               | self.proxy.FPGA_firmware_version_R != ""
+               | (self.proxy.FPGA_firmware_version_R != "")
                | (self.proxy.FPGA_jesd204b_csr_dev_syncn_R == 0).any(axis=1)
                )
 
     def read_FPGA_processing_error_R(self):
         return self.proxy.TR_fpga_mask_RW & (
                  ~self.proxy.FPGA_processing_enable_R
-               | (self.proxy.FPGA_reboot_R == 0)
-               | ~self.proxy.FPGA_wg_enable_R.any(axis=1)
+               | (self.proxy.FPGA_boot_image_R == 0)
+               )
+
+    def read_FPGA_input_error_R(self):
+        return self.proxy.TR_fpga_mask_RW & (
+                 self.proxy.FPGA_wg_enable_R.any(axis=1)
+               | (self.proxy.FPGA_signal_input_rms_R == 0).any(axis=1)
                )
 
     # --------
     # overloaded functions
     # --------
 
+    def _prepare_hardware(self):
+        # FPGAs need the correct firmware loaded 
+        self.FPGA_boot_image_RW = [1] * self.N_pn
+
+        # wait for the firmware to be loaded (ignoring masked out elements)
+        mask = self.proxy.TR_fpga_mask_RW
+        self.wait_attribute("FPGA_boot_image_R", lambda attr: ((attr == 1) | ~mask).all(), 10)
+
     # --------
     # Commands
     # --------
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
index 0e96366bcaa2758921ef4431a82e01ac653d19ad..338c5506458f0f63bf65fd33dd34f4d3e6c4122e 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
@@ -13,7 +13,7 @@
 
 # PyTango imports
 from tango.server import device_property
-from tango import DeviceProxy
+from tango import DeviceProxy, DevSource
 
 # Additional import
 import asyncio
@@ -126,6 +126,7 @@ class Statistics(opcua_device):
 
         # proxy the SDP device in case we need the FPGA mask
         self.sdp_proxy = DeviceProxy("STAT/SDP/1")
+        self.sdp_proxy.set_source(DevSource.DEV)
 
     async def _connect_statistics(self):
         # map an access helper class
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py
index 5c00c90b1dff70e5aa281ee84093454c44b4ef96..8c88a3a638eafdd152e0ee39be5db3dfc8e1063f 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py
@@ -2,6 +2,7 @@ from queue import Queue
 from threading import Thread
 import logging
 import numpy
+import datetime
 
 from .statistics_packet import SSTPacket, XSTPacket
 from tangostationcontrol.common.baselines import nr_baselines, baseline_index, baseline_from_index
@@ -99,7 +100,21 @@ class SSTCollector(StatisticsCollector):
         self.parameters["subbands_calibrated"][input_index]   = fields.subband_calibrated_flag
 
 class XSTCollector(StatisticsCollector):
-    """ Class to process XST statistics packets. """
+    """ Class to process XST statistics packets.
+    
+        XSTs are received for up to MAX_PARALLEL_SUBBANDS simultaneously, and only the values of the last
+        MAX_PARALLEL_SUBBANDS are kept. Raw data are collected for each subband in parameters["xst_blocks"],
+        and overwritten if newer (younger) data is received for the same subband. As such, the data represent
+        a rolling view on the XSTs.
+
+        The xst_values() function is a user-friendly way to read the xst_blocks.
+
+        The hardware can be configured to emit different and/or fewer subbands, causing some of the XSTs
+        to become stale. It is therefor advised to inspect parameters["xst_timestamps"] as well.
+    """
+
+    # Maximum number of subbands for which we collect XSTs simultaneously
+    MAX_PARALLEL_SUBBANDS = 8
 
     # Maximum number of antenna inputs we support (used to determine array sizes)
     MAX_INPUTS = 192
@@ -130,16 +145,33 @@ class XSTCollector(StatisticsCollector):
             "nof_payload_errors":    numpy.zeros((self.MAX_FPGAS,), dtype=numpy.uint64),
 
             # Last value array we've constructed out of the packets
-            "xst_blocks":            numpy.zeros((self.MAX_BLOCKS, self.BLOCK_LENGTH * self.BLOCK_LENGTH * self.VALUES_PER_COMPLEX), dtype=numpy.int64),
+            "xst_blocks":            numpy.zeros((self.MAX_PARALLEL_SUBBANDS, self.MAX_BLOCKS, self.BLOCK_LENGTH * self.BLOCK_LENGTH * self.VALUES_PER_COMPLEX), dtype=numpy.int64),
             # Whether the values are actually conjugated and transposed
-            "xst_conjugated":        numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.bool_),
-            "xst_timestamps":        numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.float64),
-            "xst_subbands":          numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.uint16),
-            "integration_intervals": numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.float32),
+            "xst_conjugated":        numpy.zeros((self.MAX_PARALLEL_SUBBANDS, self.MAX_BLOCKS,), dtype=numpy.bool_),
+            # When the youngest data for each subband was received
+            "xst_timestamps":        numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.float64),
+            "xst_subbands":          numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.uint16),
+            "xst_integration_intervals": numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.float32),
         })
 
         return defaults
 
+    def select_subband_slot(self, subband):
+        """ Return which subband slot (0..MAX_PARALLEL_SUBBANDS) to use when confronted with a new subband.
+            Keep recording the same subband if we're already tracking it, but allocate or replace a slot if not. """
+
+        indices = numpy.where(self.parameters["xst_subbands"] == subband)[0]
+
+        if len(indices) > 0:
+            # subband already being recorded, use same spot
+            return indices[0]
+        else:
+            # a new subband, kick out the oldest
+            oldest_timestamp = self.parameters["xst_timestamps"].min()
+
+            # prefer the first one in case of multiple minima
+            return numpy.where(self.parameters["xst_timestamps"] == oldest_timestamp)[0][0]
+
     def parse_packet(self, packet):
         fields = XSTPacket(packet)
 
@@ -172,6 +204,19 @@ class XSTCollector(StatisticsCollector):
         else:
             conjugated = False
 
+        # we keep track of multiple subbands. select slot for this one
+        subband_slot = self.select_subband_slot(fields.subband_index)
+
+        assert 0 <= subband_slot < self.MAX_PARALLEL_SUBBANDS, f"Selected slot {subband_slot}, but only have room for {self.MAX_PARALLEL_SUBBANDS}. Existing slots are {self.parameters['xst_subbands']}, processing subband {fields.subband_index}."
+
+        # log if we're replacing a subband we were once recording
+        previous_subband_in_slot   = self.parameters["xst_subbands"][subband_slot]
+        if previous_subband_in_slot != fields.subband_index:
+            previous_subband_timestamp = datetime.datetime.fromtimestamp(self.parameters["xst_timestamps"][subband_slot])
+
+            if previous_subband_timestamp.timestamp() > 0:
+                logger.info(f"Stopped recording XSTs for subband {previous_subband_in_slot}. Last data for this subband was received at {previous_subband_timestamp}.")
+
         # the payload contains complex values for the block of baselines of size BLOCK_LENGTH x BLOCK_LENGTH
         # starting at baseline first_baseline.
         #
@@ -185,36 +230,40 @@ class XSTCollector(StatisticsCollector):
         # process the packet
         self.parameters["nof_valid_payloads"][fields.gn_index] += numpy.uint64(1)
 
-        self.parameters["xst_blocks"][block_index][:fields.nof_statistics_per_packet] = fields.payload
-        self.parameters["xst_timestamps"][block_index]        = numpy.float64(fields.timestamp().timestamp())
-        self.parameters["xst_conjugated"][block_index]        = conjugated
-        self.parameters["xst_subbands"][block_index]          = numpy.uint16(fields.subband_index)
-        self.parameters["integration_intervals"][block_index] = fields.integration_interval()
+        self.parameters["xst_blocks"][subband_slot, block_index, :fields.nof_statistics_per_packet] = fields.payload
+        self.parameters["xst_timestamps"][subband_slot]              = numpy.float64(fields.timestamp().timestamp())
+        self.parameters["xst_conjugated"][subband_slot, block_index] = conjugated
+        self.parameters["xst_subbands"][subband_slot]                = numpy.uint16(fields.subband_index)
+        self.parameters["xst_integration_intervals"][subband_slot]   = fields.integration_interval()
 
-    def xst_values(self):
-        """ xst_blocks, but as a matrix[MAX_INPUTS][MAX_INPUTS] of complex values. """
+    def xst_values(self, subband_indices=range(MAX_PARALLEL_SUBBANDS)):
+        """ xst_blocks, but as a matrix[len(subband_indices)][MAX_INPUTS][MAX_INPUTS] of complex values.
+        
+            The subband indices must be in [0..MAX_PARALLEL_SUBBANDS). By default, all recorded XSTs are returned.
+        """
 
-        matrix = numpy.zeros((self.MAX_INPUTS, self.MAX_INPUTS), dtype=numpy.complex64)
+        matrix = numpy.zeros((len(subband_indices), self.MAX_INPUTS, self.MAX_INPUTS), dtype=numpy.complex64)
         xst_blocks = self.parameters["xst_blocks"]
         xst_conjugated = self.parameters["xst_conjugated"]
 
-        for block_index in range(self.MAX_BLOCKS):
-            # convert real/imag int to complex float values. this works as real/imag come in pairs
-            block = xst_blocks[block_index].astype(numpy.float32).view(numpy.complex64)
+        for matrix_idx, subband_index in enumerate(subband_indices):
+            for block_index in range(self.MAX_BLOCKS):
+                # convert real/imag int to complex float values. this works as real/imag come in pairs
+                block = xst_blocks[subband_index][block_index].astype(numpy.float32).view(numpy.complex64)
 
-            if xst_conjugated[block_index]:
-                # block is conjugated and transposed. process.
-                block = block.conjugate().transpose()
+                if xst_conjugated[subband_index][block_index]:
+                    # block is conjugated and transposed. process.
+                    block = block.conjugate().transpose()
 
-            # reshape into [a][b]
-            block = block.reshape(self.BLOCK_LENGTH, self.BLOCK_LENGTH)
+                # reshape into [a][b]
+                block = block.reshape(self.BLOCK_LENGTH, self.BLOCK_LENGTH)
 
-            # compute destination in matrix
-            first_baseline = baseline_from_index(block_index)
-            first_baseline = (first_baseline[0] * self.BLOCK_LENGTH, first_baseline[1] * self.BLOCK_LENGTH)
+                # compute destination in matrix
+                first_baseline = baseline_from_index(block_index)
+                first_baseline = (first_baseline[0] * self.BLOCK_LENGTH, first_baseline[1] * self.BLOCK_LENGTH)
 
-            # copy block into matrix
-            matrix[first_baseline[0]:first_baseline[0]+self.BLOCK_LENGTH, first_baseline[1]:first_baseline[1]+self.BLOCK_LENGTH] = block
+                # copy block into matrix
+                matrix[matrix_idx][first_baseline[0]:first_baseline[0]+self.BLOCK_LENGTH, first_baseline[1]:first_baseline[1]+self.BLOCK_LENGTH] = block
 
         return matrix
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
index 89da8dddcf19dc9195db6bc6c88c61475c61c2f6..8be9cdb483ef4f21818791009a19e8fbc2b91cb9 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
@@ -116,33 +116,86 @@ class XST(Statistics):
     # number of packets with invalid payloads
     nof_payload_errors_R    = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "nof_payload_errors"}, dims=(XSTCollector.MAX_FPGAS,), datatype=numpy.uint64)
     # latest XSTs
-    xst_blocks_R            = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_blocks"}, dims=(XSTCollector.BLOCK_LENGTH * XSTCollector.BLOCK_LENGTH * XSTCollector.VALUES_PER_COMPLEX, XSTCollector.MAX_BLOCKS), datatype=numpy.int64)
+    xst_blocks_R            = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_blocks", "reshape": True}, dims=(XSTCollector.MAX_BLOCKS * XSTCollector.BLOCK_LENGTH * XSTCollector.BLOCK_LENGTH * XSTCollector.VALUES_PER_COMPLEX, XSTCollector.MAX_PARALLEL_SUBBANDS), datatype=numpy.int64)
     # whether the values in the block are conjugated and transposed
-    xst_conjugated_R        = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_conjugated"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.bool_)
-    # reported timestamp for each row in the latest XSTs
-    xst_timestamp_R         = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_timestamps"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.uint64)
+    xst_conjugated_R        = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_conjugated", "reshape": True}, dims=(XSTCollector.MAX_BLOCKS, XSTCollector.MAX_PARALLEL_SUBBANDS), datatype=numpy.bool_)
+    # reported timestamp for each subband in the latest XSTs
+    xst_timestamp_R         = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_timestamps"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.uint64)
     # which subband the XSTs describe
-    xst_subbands_R          = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_subbands"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.uint16)
-    # integration interval for each row in the latest XSTs
-    integration_interval_R  = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "integration_intervals"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.float32)
+    xst_subbands_R          = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_subbands"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.uint16)
+    # integration interval for each subband in the latest XSTs
+    xst_integration_interval_R  = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_integration_intervals"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.float32)
 
-    # xst_R, but as a matrix of input x input
-    xst_real_R              = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),))
-    xst_imag_R              = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),))
-    xst_power_R             = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),))
-    xst_phase_R             = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),))
+    # xst_R, but as a matrix of subband x (input x input)
+    xst_real_R              = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),))
+    xst_imag_R              = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),))
+    xst_power_R             = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),))
+    xst_phase_R             = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),))
 
     def read_xst_real_R(self):
-        return numpy.real(self.statistics_client.collector.xst_values())
+        return numpy.real(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS)
 
     def read_xst_imag_R(self):
-        return numpy.imag(self.statistics_client.collector.xst_values())
+        return numpy.imag(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS)
 
     def read_xst_power_R(self):
-        return numpy.abs(self.statistics_client.collector.xst_values())
+        return numpy.abs(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS)
 
     def read_xst_phase_R(self):
-        return numpy.angle(self.statistics_client.collector.xst_values())
+        return numpy.angle(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS)
+
+    # xst_R, but as a matrix of input x input, for each specific subband index
+    xst_0_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(0))
+    xst_0_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(0))
+    xst_0_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(0))
+    xst_0_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(0))
+
+    xst_1_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(1))
+    xst_1_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(1))
+    xst_1_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(1))
+    xst_1_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(1))
+
+    xst_2_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(2))
+    xst_2_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(2))
+    xst_2_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(2))
+    xst_2_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(2))
+
+    xst_3_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(3))
+    xst_3_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(3))
+    xst_3_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(3))
+    xst_3_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(3))
+
+    xst_4_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(4))
+    xst_4_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(4))
+    xst_4_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(4))
+    xst_4_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(4))
+
+    xst_5_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(5))
+    xst_5_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(5))
+    xst_5_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(5))
+    xst_5_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(5))
+
+    xst_6_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(6))
+    xst_6_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(6))
+    xst_6_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(6))
+    xst_6_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(6))
+
+    xst_7_real_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(7))
+    xst_7_imag_R            = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(7))
+    xst_7_power_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(7))
+    xst_7_phase_R           = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(7))
+
+    def read_xst_N_real_R(self, subband_idx):
+        return numpy.real(self.statistics_client.collector.xst_values([subband_idx])[0])
+
+    def read_xst_N_imag_R(self, subband_idx):
+        return numpy.imag(self.statistics_client.collector.xst_values([subband_idx])[0])
+
+    def read_xst_N_power_R(self, subband_idx):
+        return numpy.abs(self.statistics_client.collector.xst_values([subband_idx])[0])
+
+    def read_xst_N_phase_R(self, subband_idx):
+        return numpy.angle(self.statistics_client.collector.xst_values([subband_idx])[0])
 
     # ----------
     # Summarising Attributes
diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py
index a49b120bc6f3502b7d836b86ca68e8ec0b2df9b8..0b2d5c48256405a83b6de67e6e39103f76d044bd 100644
--- a/tangostationcontrol/tangostationcontrol/devices/unb2.py
+++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py
@@ -59,8 +59,7 @@ class UNB2(opcua_device):
         default_value=[True] * 2
     )
 
-    first_default_settings = [
-        # set the masks first, as those filter any subsequent settings
+    translator_default_settings = [
         'UNB2_mask_RW'
     ]
 
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
index db9f51eb7b82d081c5b83ebe04dc998a8e5f1506..7a2609e811432a8c772a239186e16f2f442ba7a3 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
@@ -61,9 +61,30 @@ class AbstractTestBases:
                 self.self.assertListEqual([], self.proxy.opcua_missing_attributes_R)
 
         def test_device_on(self):
-            """Test if we can transition to on"""
+            """Test if we can transition off -> standby -> on"""
 
             self.proxy.initialise()
             self.proxy.on()
 
             self.assertEqual(DevState.ON, self.proxy.state())
+
+        def test_device_warm_boot(self):
+            """Test if we can transition off -> on using a warm boot"""
+
+            self.proxy.warm_boot()
+
+            self.assertEqual(DevState.ON, self.proxy.state())
+
+        def test_device_read_all_attributes(self):
+            """Test if we can read all of the exposed attributes in the ON state.
+            
+               This test covers the reading logic of all attributes. """
+
+            self.proxy.initialise()
+            self.proxy.on()
+
+            for attribute_name in self.proxy.get_attribute_list():
+                try:
+                    _ = self.proxy.read_attribute(attribute_name).value
+                except Exception as e:
+                    raise AssertionError(f"Could not read attribute {attribute_name} from device {self.name}") from e
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py
index 21c93eab4f434350fa091523a421837513aadf9c..381678118b123793767aff753e4461b7916fe5bd 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py
@@ -12,7 +12,6 @@ import numpy
 import datetime
 import json
 
-from tango import DevState
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 
 from .base import AbstractTestBases
@@ -36,11 +35,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
         # setup RECV
         recv_proxy = TestDeviceProxy("STAT/RECV/1")
         recv_proxy.off()
-        recv_proxy.initialise()
-        self.assertEqual(DevState.STANDBY, recv_proxy.state())
+        recv_proxy.warm_boot()
         recv_proxy.set_defaults()
-        recv_proxy.on()
-        self.assertEqual(DevState.ON, recv_proxy.state())
         return recv_proxy
     
     def test_HBAT_delays_dims(self):
@@ -48,12 +44,7 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
         self.setup_recv_proxy()
 
         # setup BEAM
-        self.proxy.init()
-        self.proxy.Initialise()
-        self.assertEqual(DevState.STANDBY, self.proxy.state())
-        self.proxy.set_defaults()
-        self.proxy.on()
-        self.assertEqual(DevState.ON, self.proxy.state())
+        self.proxy.warm_boot()
 
         # verify HBAT_delays method returns the correct dimensions
         HBAT_delays = self.proxy.HBAT_delays(self.pointing_direction)
@@ -64,12 +55,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
         recv_proxy = self.setup_recv_proxy()
 
         # setup BEAM
-        self.proxy.init()
-        self.proxy.Initialise()
-        self.assertEqual(DevState.STANDBY, self.proxy.state())
-        self.proxy.set_defaults()
-        self.proxy.on()
-        self.assertEqual(DevState.ON, self.proxy.state())
+        self.proxy.warm_boot()
+        self.proxy.HBAT_tracking_enabled_RW = False
 
         # Verify attribute is present (all zeros if never used before)
         HBAT_delays_r1 = numpy.array(recv_proxy.read_attribute('HBAT_BF_delay_steps_RW').value)
@@ -89,8 +76,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
         # setup RECV as well
         recv_proxy = self.setup_recv_proxy()
 
-        self.proxy.initialise()
-        self.proxy.on()
+        self.proxy.warm_boot()
+        self.proxy.HBAT_tracking_enabled_RW = False
 
         # Point to Zenith
         self.proxy.HBAT_set_pointing(numpy.array([["AZELGEO","0deg","90deg"]] * 96).flatten())
@@ -101,12 +88,39 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
 
         numpy.testing.assert_equal(calculated_HBAT_delay_steps, expected_HBAT_delay_steps)
 
+    def test_pointing_across_horizon(self):
+        # setup RECV as well
+        recv_proxy = self.setup_recv_proxy()
+
+        self.proxy.warm_boot()
+        self.proxy.HBAT_tracking_enabled_RW = False
+
+        # point at north on the horizon
+        self.proxy.HBAT_set_pointing(["AZELGEO","0deg","0deg"] * 96)
+
+        # obtain delays of the X polarisation of all the elements of the first tile
+        north_beam_delay_steps = recv_proxy.HBAT_BF_delay_steps_RW[0].reshape(2,4,4)[0]
+
+        # delays must differ under rotation, or our test will give a false positive
+        self.assertNotEqual(north_beam_delay_steps.tolist(), numpy.rot90(north_beam_delay_steps).tolist())
+
+        for angle in (90,180,270):
+            # point at angle degrees (90=E, 180=S, 270=W)
+            self.proxy.HBAT_set_pointing(["AZELGEO",f"{angle}deg","0deg"] * 96)
+
+            # obtain delays of the X polarisation of all the elements of the first tile
+            angled_beam_delay_steps = recv_proxy.HBAT_BF_delay_steps_RW[0].reshape(2,4,4)[0]
+
+            expected_delay_steps = numpy.rot90(north_beam_delay_steps, k=-(angle/90))
+
+            self.assertListEqual(expected_delay_steps.tolist(), angled_beam_delay_steps.tolist(), msg=f"angle={angle}")
+
     def test_delays_same_as_LOFAR_ref_pointing(self):
         # setup RECV as well
         recv_proxy = self.setup_recv_proxy()
 
-        self.proxy.initialise()
-        self.proxy.on()
+        self.proxy.warm_boot()
+        self.proxy.HBAT_tracking_enabled_RW = False
 
         # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000)
         pointings = numpy.array([["J2000", "0.929342rad", "0.952579rad"]] * 96).flatten()
@@ -131,3 +145,20 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase):
         expected_HBAT_delay_steps = numpy.array([24, 25, 27, 29, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7] * 2, dtype=numpy.int64)
         numpy.testing.assert_equal(calculated_HBAT_delay_steps[0], expected_HBAT_delay_steps)
         numpy.testing.assert_equal(calculated_HBAT_delay_steps[48], expected_HBAT_delay_steps)
+
+    def test_beam_tracking(self):
+        # setup RECV as well
+        recv_proxy = self.setup_recv_proxy()
+
+        self.proxy.warm_boot()
+
+        # check if we're really tracking
+        self.assertTrue(self.proxy.HBAT_tracking_enabled_R)
+
+        # point somewhere
+        new_pointings = [("J2000",f"{tile}deg","0deg") for tile in range(96)]
+        self.proxy.HBAT_pointing_direction_RW = new_pointings
+
+        # check pointing
+        self.assertListEqual(new_pointings, list(self.proxy.HBAT_pointing_direction_R))
+
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
index 3a075e697c473c9800257b4796f21f7b79e5e430..570d8ce6cee1bc2f1ab833c9d93acdd11cc270f7 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
@@ -19,7 +19,6 @@ class TestDeviceSDP(AbstractTestBases.TestDeviceBase):
     def test_device_sdp_read_attribute(self):
         """Test if we can read an attribute obtained over OPC-UA"""
 
-        self.proxy.initialise()
-        self.proxy.on()
+        self.proxy.warm_boot()
 
         self.assertListEqual([True]*16, list(self.proxy.TR_fpga_communication_error_R))
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py
index 60675e121364b52fb692b2f8461001bfdc78b50a..2398206083533a5fcc40051b6a80e42923a38347 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py
@@ -12,6 +12,7 @@ import sys
 import time
 
 from tango._tango import DevState
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 
 from .base import AbstractTestBases
 
@@ -22,14 +23,24 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase):
         """Intentionally recreate the device object in each test"""
         super().setUp("STAT/SST/1")
 
+    def test_device_read_all_attributes(self):
+        # We need to connect to SDP first to read some of our attributes
+        self.sdp_proxy = self.setup_sdp()
+
+        super().test_device_read_all_attributes()
+    
+    def setup_sdp(self):
+        # setup SDP, on which this device depends
+        sdp_proxy = TestDeviceProxy("STAT/SDP/1")
+        sdp_proxy.off()
+        sdp_proxy.warm_boot()
+        sdp_proxy.set_defaults()
+        return sdp_proxy
+
     def test_device_sst_send_udp(self):
         port_property = {"Statistics_Client_TCP_Port": "4998"}
         self.proxy.put_property(port_property)
-        self.proxy.initialise()
-
-        self.assertEqual(DevState.STANDBY, self.proxy.state())
-
-        self.proxy.on()
+        self.proxy.warm_boot()
 
         self.assertEqual(DevState.ON, self.proxy.state())
 
@@ -44,13 +55,7 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase):
     def test_device_sst_connect_tcp_receive(self):
         port_property = {"Statistics_Client_TCP_Port": "5101"}
         self.proxy.put_property(port_property)
-        self.proxy.initialise()
-
-        self.assertEqual(DevState.STANDBY, self.proxy.state())
-
-        self.proxy.on()
-
-        self.assertEqual(DevState.ON, self.proxy.state())
+        self.proxy.warm_boot()
 
         time.sleep(2)
 
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py
new file mode 100644
index 0000000000000000000000000000000000000000..071108ee57e025c30fc423d7ce2312b66d5012a7
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py
@@ -0,0 +1,32 @@
+
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+from .base import AbstractTestBases
+
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
+
+class TestDeviceSST(AbstractTestBases.TestDeviceBase):
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+        super().setUp("STAT/XST/1")
+
+    def test_device_read_all_attributes(self):
+        # We need to connect to SDP first to read some of our attributes
+        self.sdp_proxy = self.setup_sdp()
+
+        super().test_device_read_all_attributes()
+    
+    def setup_sdp(self):
+        # setup SDP, on which this device depends
+        sdp_proxy = TestDeviceProxy("STAT/SDP/1")
+        sdp_proxy.off()
+        sdp_proxy.warm_boot()
+        sdp_proxy.set_defaults()
+        return sdp_proxy
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py b/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py
index 25c92411ecaababad20007868cfd19bdc3e9e18a..93f84dc98a789c2e0fa5451529728dbed2e73f79 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py
@@ -1,7 +1,7 @@
 import logging
 import time
 
-from tango import DeviceProxy
+from tango import DeviceProxy, DevSource
 
 logger = logging.getLogger()
 
@@ -10,8 +10,18 @@ class TestDeviceProxy(DeviceProxy):
 
     def __init__(self, *args, **kwargs):
         super(TestDeviceProxy, self).__init__(*args, **kwargs)
+
+        # extend timeout for running commands
         self.set_timeout_millis(10000)
 
+        # When requesting attribute values and states,
+        # always get values from the device, never the polling
+        # cache. This makes sure updates are immediately reflected
+        # in this proxy, which is essential for fast-paced tests.
+        #
+        # See also https://www.tango-controls.org/community/forum/c/development/python/attribute-direct-reading-from-device-when-polling-is-turned-on/
+        self.set_source(DevSource.DEV)
+
     @staticmethod
     def test_device_turn_off(endpoint):
         d = TestDeviceProxy(endpoint)
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py b/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5a935eb537a9031ee67b2252ba1b1cfd1eb6687
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+import time
+from tango import DevState
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
+
+from tangostationcontrol.integration_test import base
+
+class TestProxyAttributeAccess(base.IntegrationTestCase):
+    """ Test whether DeviceProxy's can always access attributes immediately after turning them on. """
+
+    # We use RECV as our victim. Any device would do.
+    device_name = "STAT/RECV/1"
+
+    # an attribute to access
+    attribute_name = "ANT_mask_RW"
+    
+    def setUp(self):
+        self.proxy = TestDeviceProxy(self.device_name)
+        self.proxy.off()
+
+    def poll_attribute(self):
+        # make sure the attribute is polled, so we force proxies to access the poll first
+        if self.proxy.is_attribute_polled(self.attribute_name):
+            self.proxy.stop_poll_attribute(self.attribute_name)
+        self.proxy.poll_attribute(self.attribute_name, 1000)
+
+    def dont_poll_attribute(self):
+        # make sure the attribute is NOT polled, so we force proxies to access the device
+        if self.proxy.is_attribute_polled(self.attribute_name):
+            self.proxy.stop_poll_attribute(self.attribute_name)
+
+    def read_attribute(self):
+        # turn on the device
+        self.proxy.initialise()
+        self.assertEqual(DevState.STANDBY, self.proxy.state())
+
+        # read the attribute -- shouldn't throw
+        _ = self.proxy.read_attribute(self.attribute_name)
+    
+    def test_fast_setup_polled_attribute(self):
+        """ Setup a device as fast as possible and access its attributes immediately. """
+
+        self.poll_attribute()
+        self.read_attribute()
+
+    
+    def test_slow_setup_polled_attribute(self):
+        """ Have the device be off for a while, allowing Tango to poll. Then,
+            Setup a device as fast as possible and access its attributes immediately. """
+
+        self.poll_attribute()
+        time.sleep(3) # allow Tango to poll the attribute in OFF state
+        self.read_attribute()
+
+
+    def test_fast_setup_nonpolled_attribute(self):
+        """ Setup a device as fast as possible and access its attributes immediately. """
+        self.dont_poll_attribute()
+        self.read_attribute()
+
+
+    def test_slow_setup_nonpolled_attribute(self):
+        """ Have the device be off for a while, allowing Tango to poll. Then,
+            Setup a device as fast as possible and access its attributes immediately. """
+        self.dont_poll_attribute()
+        time.sleep(3) # allow Tango to poll the attribute in OFF state
+        self.read_attribute()
+
diff --git a/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py b/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py
index 30710a871e157909aa9e4d42169ac686fc23e889..d2bdbad1c02eae894ec100b63477a92ce1e84994 100644
--- a/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py
+++ b/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py
@@ -7,6 +7,7 @@ import h5py
 
 import numpy
 import logging
+from abc import ABC, abstractmethod
 
 # import statistics classes with workaround
 import sys
@@ -17,9 +18,10 @@ import tangostationcontrol.devices.sdp.statistics_collector as statistics_collec
 
 logger = logging.getLogger("statistics_writer")
 
-__all__ = ["hdf5_writer"]
+__all__ = ["hdf5_writer", "parallel_xst_hdf5_writer", "xst_hdf5_writer", "sst_hdf5_writer"]
 
-class hdf5_writer:
+
+class hdf5_writer(ABC):
 
     SST_MODE = "SST"
     XST_MODE = "XST"
@@ -39,18 +41,22 @@ class hdf5_writer:
         self.statistics_header = None
 
         # file handing
-        self.file_location = file_location
+        self.file_location = file_location or '.'
         self.decimation_factor = decimation_factor
         self.new_file_time_interval = timedelta(seconds=new_file_time_interval)
         self.last_file_time = datetime.min.replace(tzinfo=pytz.UTC)
         self.file = None
 
         # parameters that are configured depending on the mode the statistics writer is in (SST,XST,BST)
-        self.decoder = None
-        self.collector = None
-        self.store_function = None
         self.mode = statistics_mode.upper()
-        self.config_mode()
+
+    @abstractmethod
+    def decoder(self):
+        pass
+
+    @abstractmethod
+    def new_collector(self):
+        pass
 
     def next_packet(self, packet):
         """
@@ -123,7 +129,7 @@ class hdf5_writer:
             self.start_new_hdf5(timestamp)
 
         # create a new and empty current_matrix
-        self.current_matrix = self.collector()
+        self.current_matrix = self.new_collector()
         self.statistics_header = None
 
     def write_matrix(self):
@@ -136,7 +142,7 @@ class hdf5_writer:
         current_group = self.file.create_group("{}_{}".format(self.mode, self.current_timestamp.isoformat(timespec="milliseconds")))
 
         # store the statistics values for the current group
-        self.store_function(current_group)
+        self.write_values_matrix(current_group)
 
         # might be optional, but they're easy to add.
         current_group.create_dataset(name="nof_payload_errors", data=self.current_matrix.parameters["nof_payload_errors"])
@@ -145,6 +151,10 @@ class hdf5_writer:
         # get the statistics header
         header = self.statistics_header
 
+        if not header:
+            # edge case: no valid packet received at all
+            return
+
         # can't store datetime objects, convert to string instead
         header["timestamp"] = header["timestamp"].isoformat(timespec="milliseconds")
 
@@ -156,17 +166,13 @@ class hdf5_writer:
             else:
                 current_group.attrs[k] = v
 
-    def write_sst_matrix(self, current_group):
-        # store the SST values
-        current_group.create_dataset(name="values", data=self.current_matrix.parameters["sst_values"].astype(numpy.float32), compression="gzip")
-
-    def write_xst_matrix(self, current_group):
-        # requires a function call to transform the xst_blocks in to the right structure
-        current_group.create_dataset(name="values", data=self.current_matrix.xst_values().astype(numpy.cfloat), compression="gzip")
-
-    def write_bst_matrix(self, current_group):
-        raise NotImplementedError("BST values not implemented")
+    @abstractmethod
+    def write_values_matrix(self, current_group):
+        pass
 
+    def next_filename(self, timestamp, suffix=".h5"):
+        time_str = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S"))
+        return f"{self.file_location}/{self.mode}_{time_str}{suffix}"
 
     def process_packet(self, packet):
         """
@@ -186,44 +192,17 @@ class hdf5_writer:
             except Exception as e:
                 logger.exception(f"Error while attempting to close hdf5 file to disk. file {self.file} likely empty, please verify integrity.")
 
-        current_time = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S"))
-        logger.info(f"creating new file: {self.file_location}/{self.mode}_{current_time}.h5")
+        filename = self.next_filename(timestamp)
+        logger.info(f"creating new file: {filename}")
 
         try:
-            self.file = h5py.File(f"{self.file_location}/{self.mode}_{current_time}.h5", 'w')
+            self.file = h5py.File(filename, 'w')
         except Exception as e:
             logger.exception(f"Error while creating new file")
             raise e
 
         self.last_file_time = timestamp
 
-    def config_mode(self):
-        logger.debug(f"attempting to configure {self.mode} mode")
-
-        """
-        Configures the object for the correct statistics type to be used.
-        decoder:            the class to decode a single packet
-        collector:          the class to collect statistics packets
-        store_function:     the function to write the mode specific data to file
-        """
-
-        if self.mode == self.SST_MODE:
-            self.decoder = SSTPacket
-            self.collector = statistics_collector.SSTCollector
-            self.store_function = self.write_sst_matrix
-
-        elif self.mode == self.XST_MODE:
-            self.decoder = XSTPacket
-            self.collector = statistics_collector.XSTCollector
-            self.store_function = self.write_xst_matrix
-
-        elif self.mode == self.BST_MODE:
-            self.store_function = self.write_bst_matrix
-            raise NotImplementedError("BST collector has not yet been implemented")
-
-        else:
-            raise ValueError("invalid statistics mode specified '{}', please use 'SST', 'XST' or 'BST' ".format(self.mode))
-
     def close_writer(self):
         """
         Function that can be used to stop the writer without data loss.
@@ -240,3 +219,79 @@ class hdf5_writer:
                     self.file.close()
                     logger.debug(f"{filename} closed")
                     logger.debug(f"Received a total of {self.statistics_counter} statistics while running. With {int(self.statistics_counter/self.decimation_factor)} written to disk ")
+
+
+class sst_hdf5_writer(hdf5_writer):
+    def __init__(self, new_file_time_interval, file_location, decimation_factor):
+        super().__init__(new_file_time_interval, file_location, hdf5_writer.SST_MODE, decimation_factor)
+
+    def decoder(self, packet):
+        return SSTPacket(packet)
+
+    def new_collector(self):
+        return statistics_collector.SSTCollector()
+
+    def write_values_matrix(self, current_group):
+        # store the SST values
+        current_group.create_dataset(name="values", data=self.current_matrix.parameters["sst_values"].astype(numpy.float32), compression="gzip")
+
+
+class xst_hdf5_writer(hdf5_writer):
+    def __init__(self, new_file_time_interval, file_location, decimation_factor, subband_index):
+        super().__init__(new_file_time_interval, file_location, hdf5_writer.XST_MODE, decimation_factor)
+        self.subband_index = subband_index
+
+    def decoder(self, packet):
+        return XSTPacket(packet)
+
+    def new_collector(self):
+        return statistics_collector.XSTCollector()
+
+    def next_filename(self, timestamp):
+        time_str = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S"))
+        return f"{self.file_location}/{self.mode}_SB{self.subband_index}_{time_str}.h5"
+
+    def write_values_matrix(self, current_group):
+        # requires a function call to transform the xst_blocks in to the right structure
+        current_group.create_dataset(name="values", data=self.current_matrix.xst_values([self.subband_index])[0].astype(numpy.cfloat), compression="gzip")
+
+
+class parallel_xst_hdf5_writer:
+    """ Writes multiple subbands in parallel. Each subband gets written to its own HDF5 file(s). """
+
+    def __init__(self, new_file_time_interval, file_location, decimation_factor):
+        # maintain a dedicated hdf5_writer per subband
+        self.writers = {}
+
+        # function to create a new writer, to avoid having to store
+        # all the init parameters just for this purpose.
+        #
+        def new_writer(subband):
+            # Since we use a dedicated writer per subband, the data will end
+            # up at subband_index == 0 in each of them.
+            return xst_hdf5_writer(
+                       new_file_time_interval,
+                       file_location,
+                       decimation_factor,
+                       0)
+
+        self.new_writer = new_writer
+
+    def next_packet(self, packet):
+        # decode to get subband of this packet
+        fields = XSTPacket(packet)
+        subband = fields.subband_index
+
+        # make sure there is a writer for it
+        if subband not in self.writers:
+            self.writers[subband] = self.new_writer(subband)
+
+        # demux packet to the correct writer
+        self.writers[subband].next_packet(packet)
+
+    def close_writer(self):
+        for writer in self.writers.values():
+            writer.close_writer()
+
+        self.writers = {}
+
diff --git a/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py b/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py
index 1a1ecb671159e1b3ca143ecbf860000d6cdbe0c5..52747fff71d436d62bcbe40844aa0a3a45a2ab25 100644
--- a/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py
+++ b/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py
@@ -3,7 +3,7 @@ import time
 import sys
 
 from tangostationcontrol.statistics_writer.receiver import tcp_receiver, file_receiver
-from tangostationcontrol.statistics_writer.hdf5_writer import hdf5_writer
+from tangostationcontrol.statistics_writer.hdf5_writer import sst_hdf5_writer, parallel_xst_hdf5_writer
 
 import logging
 logging.basicConfig(level=logging.INFO, format = '%(asctime)s:%(levelname)s: %(message)s')
@@ -13,13 +13,13 @@ def main():
     parser = argparse.ArgumentParser(
         description='Converts a stream of statistics packets into HDF5 files.')
     parser.add_argument(
-        '-a', '--host', type=str, required=True, help='the host to connect to')
+        '-a', '--host', type=str, required=False, help='the host to connect to')
     parser.add_argument(
         '-p', '--port', type=int, default=0,
         help='the port to connect to, or 0 to use default port for the '
              'selected mode (default: %(default)s)')
     parser.add_argument(
-        '-f', '--file', type=str, required=True, help='the file to read from')
+        '-f', '--file', type=str, required=False, help='the file to read from')
     parser.add_argument(
         '-m', '--mode', type=str, choices=['SST', 'XST', 'BST'], default='SST',
         help='sets the statistics type to be decoded options (default: '
@@ -57,6 +57,9 @@ def main():
     debug = args.debug
     reconnect = args.reconnect
 
+    if not filename and not host:
+        raise ValueError("Supply either a filename (--file) or a hostname (--host)")
+
     if decimation < 1:
         raise ValueError("Please use an integer --Decimation value 1 or higher to only store one every n statistics' ")
 
@@ -78,7 +81,16 @@ def main():
         sys.exit(1)
 
     # create the writer
-    writer = hdf5_writer(new_file_time_interval=interval, file_location=output_dir, statistics_mode=mode, decimation_factor=decimation)
+    if mode == "XST":
+        writer = parallel_xst_hdf5_writer(new_file_time_interval=interval, file_location=output_dir, decimation_factor=decimation)
+    elif mode == "SST":
+        writer = sst_hdf5_writer(new_file_time_interval=interval, file_location=output_dir, decimation_factor=decimation)
+    elif mode == "BST":
+        logger.fatal(f"BST mode not supported")
+        sys.exit(1)
+    else:
+        logger.fatal(f"Invalid mode: {mode}")
+        sys.exit(1)
 
     # start looping
     try:
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/__init__.py b/tangostationcontrol/tangostationcontrol/test/beam/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
new file mode 100644
index 0000000000000000000000000000000000000000..624491c454f6d1b6795295b66eee072f45774928
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
@@ -0,0 +1,103 @@
+import datetime
+
+from tangostationcontrol.beam.delays import *
+from tangostationcontrol.test import base
+
+
+
+
+
+class TestDelays(base.TestCase):
+    def test_init(self):
+        """
+        Fail condition is simply the object creation failing
+        """
+
+        reference_itrf = [3826577.066, 461022.948, 5064892.786]  # CS002LBA, in ITRF2005 epoch 2012.5
+        d = delay_calculator(reference_itrf)
+
+        self.assertIsNotNone(d)
+
+    def test_sun(self):
+        # # create a frame tied to the reference position
+        reference_itrf = [3826577.066, 461022.948, 5064892.786]
+        d = delay_calculator(reference_itrf)
+
+        for i in range(24):
+
+            # set the time to the day of the winter solstice 2021 (21 december 16:58) as this is the time with the least change in sunlight
+            timestamp = datetime.datetime(2021, 12, 21, i, 58, 0)
+            d.set_measure_time(timestamp)
+
+
+            # point to the sun
+            direction = "SUN", "0deg", "0deg"
+
+            # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions.
+            pointing = d.measure.direction(*direction)
+            direction = d.get_direction_vector(pointing)
+
+            """
+            direction[2] is the z-coordinate of ITRF, which points to the north pole. 
+            This direction is constant when pointing to the sun, as the earth rotates around its axis, 
+            but changes slowly due to the earths rotation around the sun.
+            The summer and winter solstices are when these values are at their peaks and the changes are the smallest.
+            This test takes the value at the winter solstice and checks whether the measured values are near enough to that. 
+            """
+
+            # Measured manually at the winter solstice. Using datetime.datetime(2021, 12, 21, 16, 58, 0)
+            z_at_solstice = -0.3977784695213487
+            z_direction = direction[2]
+
+            self.assertAlmostEqual(z_at_solstice, z_direction, 4)
+
+    def test_identical_location(self):
+        # # create a frame tied to the reference position
+        reference_itrf = [3826577.066, 461022.948, 5064892.786]  # CS002LBA, in ITRF2005 epoch 2012.5
+        d = delay_calculator(reference_itrf)
+
+        # set the antenna position identical to the reference position
+        antenna_itrf = [[reference_itrf[0], reference_itrf[1], reference_itrf[2]]]  # CS001LBA, in ITRF2005 epoch 2012.5
+
+        # # set the timestamp to solve for
+        timestamp = datetime.datetime(2000, 1, 1, 0, 0, 0)
+        d.set_measure_time(timestamp)
+
+        # compute the delays for an antennas w.r.t. the reference position
+
+        # # obtain the direction vector for a specific pointing
+        direction = "J2000", "0deg", "0deg"
+
+        # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions.
+        delays = d.convert(direction, antenna_itrf)
+
+        self.assertListEqual(delays, [0.0], msg=f"delays = {delays}")
+
+    def test_light_second_delay(self):
+        """
+        This test measures the delay between 2 positions 1 light second apart.
+        """
+
+        # # create a frame tied to the reference position
+        reference_itrf = [3826577.066, 461022.948, 5064892.786]  # CS002LBA, in ITRF2005 epoch 2012.5
+        d = delay_calculator(reference_itrf)
+
+        # set the antenna position identical to the reference position
+        speed_of_light = 299792458.0
+        antenna_itrf = [[reference_itrf[0], reference_itrf[1] - speed_of_light, reference_itrf[2]]]  # CS001LBA, in ITRF2005 epoch 2012.5
+
+        # # set the timestamp to solve for
+        timestamp = datetime.datetime(2000, 1, 1, 0, 0, 0)
+        d.set_measure_time(timestamp)
+
+        # compute the delays for an antennas w.r.t. the reference position
+
+        # # obtain the direction vector for a specific pointing
+        direction = "J2000", "0deg", "0deg"
+
+        # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions.
+        delays = d.convert(direction, antenna_itrf)
+
+
+        self.assertTrue(0.98 <= delays[0] <= 1.02, f"delays[0] = {delays[0]}")
+
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py b/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py
new file mode 100644
index 0000000000000000000000000000000000000000..858b3f32e954d19271f8a0dc6fc3cba7b92f47e2
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+from tangostationcontrol.beam.geo import ETRS_to_ITRF
+
+from tangostationcontrol.test import base
+
+import numpy.testing
+
+class TestETRS_to_ITRF(base.TestCase):
+    def test_convert_single_coordinate(self):
+        """ Convert a single coordinate. """
+        ETRS_coords = numpy.array([1.0, 1.0, 1.0])
+        ITRF_coords = ETRS_to_ITRF(ETRS_coords, "ITRF2005", 2015.5)
+
+        self.assertEqual(ETRS_coords.shape, ITRF_coords.shape)
+
+    def test_convert_array(self):
+        """ Convert an array of coordinates. """
+        ETRS_coords = numpy.array([ [1.0, 1.0, 1.0], [2.0, 2.0, 2.0] ])
+        ITRF_coords = ETRS_to_ITRF(ETRS_coords, "ITRF2005", 2015.5)
+
+        self.assertEqual(ETRS_coords.shape, ITRF_coords.shape)
+
+    def test_verify_CS001_LBA(self):
+        """ Verify if the calculated CS001LBA phase center matches those calculated in LOFAR1. """
+
+        # See CLBA in MAC/Deployment/data/Coordinates/ETRF_FILES/CS001/CS001-antenna-positions-ETRS.csv 
+        CS001_LBA_ETRS = [3826923.942, 460915.117, 5064643.229]
+
+        # Convert to ITRF
+        CS001_LBA_ITRF = ETRS_to_ITRF(numpy.array(CS001_LBA_ETRS), "ITRF2005", 2015.5)
+
+        # verify against LOFAR1 (MAC/Deployment/data/StaticMetaData/AntennaFields/CS001-AntennaField.conf)
+        LOFAR1_CS001_LBA_ITRF = [3826923.50275, 460915.488115, 5064643.517]
+
+        numpy.testing.assert_almost_equal(CS001_LBA_ITRF, LOFAR1_CS001_LBA_ITRF, decimal=1.5)
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py b/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py
new file mode 100644
index 0000000000000000000000000000000000000000..d698264f845cde35c5af63612e040832120e2455
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets
+
+from tangostationcontrol.test import base
+
+from math import pi
+import numpy.testing
+
+class TestHBATAntennaOffsets(base.TestCase):
+    def test_verify_CS001_HBA0(self):
+        """ Verify if the calculated HBAT Antenna Offsets match those calculated in LOFAR1. """
+
+        CS001_HBA0_rotation_angle_deg = 24
+        CS001_PQR_to_ETRS_rotation_matrix = numpy.array([
+                            [-0.1195951054, -0.7919544517, 0.5987530018],
+                            [ 0.9928227484, -0.0954186800, 0.0720990002],
+                            [ 0.0000330969,  0.6030782884, 0.7976820024]])
+
+        # recalculate the ITRF offsets
+        ITRF_offsets = HBATAntennaOffsets.ITRF_offsets(
+            HBATAntennaOffsets.HBAT1_BASE_ANTENNA_OFFSETS,
+            CS001_HBA0_rotation_angle_deg * pi / 180,
+            CS001_PQR_to_ETRS_rotation_matrix)
+
+        # verify against LOFAR1 (MAC/Deployment/data/StaticMetaData/iHBADeltas/CS001-iHBADeltas.conf)
+        LOFAR1_CS001_HBA0_ITRF_offsets = numpy.array([
+                               [-1.847, -1.180,  1.493],
+                               [-1.581,  0.003,  1.186],
+                               [-1.315,  1.185,  0.880],
+                               [-1.049,  2.367,  0.573],
+                               [-0.882, -1.575,  0.804],
+                               [-0.616, -0.393,  0.498],
+                               [-0.350,  0.789,  0.191],
+                               [-0.083,  1.971, -0.116],
+                               [ 0.083, -1.971,  0.116],
+                               [ 0.350, -0.789, -0.191],
+                               [ 0.616,  0.393, -0.498],
+                               [ 0.882,  1.575, -0.804],
+                               [ 1.049, -2.367, -0.573],
+                               [ 1.315, -1.185, -0.880],
+                               [ 1.581, -0.003, -1.186],
+                               [ 1.847,  1.180, -1.493]])
+
+        numpy.testing.assert_almost_equal(ITRF_offsets, LOFAR1_CS001_HBA0_ITRF_offsets, decimal=3)
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
index 651fbabe8a47061811fcfa67f68978ff527cec72..bffc23aaa24b7ed0269dffa4fec3afb5363b7ba9 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
+++ b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
@@ -7,7 +7,7 @@
 """
 
 # External imports
-from tango import DevState
+from tango import DevState, DevFailed
 
 # Internal imports
 from tangostationcontrol.test.clients.test_client import test_client
@@ -609,3 +609,29 @@ class TestAttributeTypes(base.TestCase):
             self.readback_test(
                 attribute_type_test['image'], attribute_type_test['type'],
                 'image')
+
+class TestAttributeAccess(base.TestCase):
+    def setUp(self):
+        # Avoid the device trying to access itself as a client
+        self.deviceproxy_patch = mock.patch.object(tangostationcontrol.devices.lofar_device,'DeviceProxy')
+        self.deviceproxy_patch.start()
+        self.addCleanup(self.deviceproxy_patch.stop)
+
+    class float32_scalar_device(lofar_device):
+        scalar_R = attribute_wrapper(comms_annotation="float32_scalar_R", datatype=numpy.float32)
+        scalar_RW = attribute_wrapper(comms_annotation="float32_scalar_RW", datatype=numpy.float32, access=AttrWriteType.READ_WRITE)
+
+        def configure_for_initialise(self):
+            dev_init(self)
+            self.set_state(DevState.STANDBY)
+
+    def test_cannot_access_before_initialise(self):
+        with DeviceTestContext(self.float32_scalar_device, process=True) as proxy:
+            with self.assertRaises(DevFailed):
+                _ = proxy.scalar_R
+
+    def test_can_access_after_initialise(self):
+        with DeviceTestContext(self.float32_scalar_device, process=True) as proxy:
+            proxy.initialise()
+
+            _ = proxy.scalar_R
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py
index c071dddfe8570f987079807fc76c7ef530132069..48ce76bf15d0c445b58320d223285153b7d57259 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py
@@ -31,11 +31,13 @@ class TestBeamDevice(base.TestCase):
     def test_get_pointing_directions(self):
         """Verify can read pointings attribute and length matches without err"""
         with DeviceTestContext(beam.Beam, process=True, timeout=10) as proxy:
+            proxy.initialise()
             self.assertEqual(96, len(proxy.read_attribute(
                 "HBAT_pointing_direction_R").value))
 
     def test_get_pointing_timestamps(self):
         """Verify can read timestamps attribute and length matches without err"""
         with DeviceTestContext(beam.Beam, process=True, timeout=10) as proxy:
+            proxy.initialise()
             self.assertEqual(96, len(proxy.read_attribute(
                 "HBAT_pointing_timestamp_R").value))
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py b/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py
index 4b58141c06d9b09d68dba295b007b41081ca3618..7113ee837631789e99218f3356a602637ccac116 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py
@@ -3,6 +3,45 @@ from tangostationcontrol.devices.sdp.statistics_packet import XSTPacket
 
 from tangostationcontrol.test import base
 
+class TestSelectSubbandSlot(base.TestCase):
+    def test_first_entry(self):
+        collector = XSTCollector()
+
+        # on start, any subband should map on the first entry
+        self.assertEqual(0, collector.select_subband_slot(102))
+
+    def test_subsequent_entries(self):
+        collector = XSTCollector()
+
+        # assign some subbands
+        collector.parameters["xst_subbands"][0] = 102
+        collector.parameters["xst_subbands"][2] = 103
+        collector.parameters["xst_subbands"][3] = 104
+
+        # give them non-zero timestamps to make them newer than the other entries
+        collector.parameters["xst_timestamps"][0] = 1
+        collector.parameters["xst_timestamps"][2] = 1
+        collector.parameters["xst_timestamps"][3] = 1
+
+        # these should be reported back when looking them up again
+        self.assertEqual(0, collector.select_subband_slot(102))
+        self.assertEqual(2, collector.select_subband_slot(103))
+        self.assertEqual(3, collector.select_subband_slot(104))
+
+        # a place for another subband should be the lowest
+        self.assertEqual(1, collector.select_subband_slot(101))
+
+    def test_spilling(self):
+        collector = XSTCollector()
+
+        # assign all subbands, in decreasing age
+        for n in range(XSTCollector.MAX_PARALLEL_SUBBANDS):
+            collector.parameters["xst_subbands"][n] = 100 + n
+            collector.parameters["xst_timestamps"][n] = 100 - n
+
+        # check where a new subband replaces the oldest
+        self.assertEqual(XSTCollector.MAX_PARALLEL_SUBBANDS - 1, collector.select_subband_slot(200))
+
 class TestXSTCollector(base.TestCase):
     def test_valid_packet(self):
         collector = XSTCollector()
@@ -17,6 +56,9 @@ class TestXSTCollector(base.TestCase):
         # baseline indeed should be (12,0)
         self.assertEqual((12,0), fields.first_baseline)
 
+        # subband should indeed be 102
+        self.assertEqual(102,    fields.subband_index)
+
         # this should not throw
         collector.process_packet(packet)
 
@@ -27,8 +69,10 @@ class TestXSTCollector(base.TestCase):
         self.assertEqual(1, collector.parameters["nof_valid_payloads"][fpga_index])
         self.assertEqual(0, collector.parameters["nof_payload_errors"][fpga_index])
 
+        self.assertListEqual([102,0,0,0,0,0,0,0], list(collector.parameters["xst_subbands"]))
+
         # check whether the data ended up in the right block, and the rest is still zero
-        xst_values = collector.xst_values()
+        xst_values = collector.xst_values()[0]
 
         for baseline_a in range(collector.MAX_INPUTS):
             for baseline_b in range(collector.MAX_INPUTS):
@@ -67,7 +111,7 @@ class TestXSTCollector(base.TestCase):
         self.assertEqual(0, collector.parameters["nof_invalid_packets"])
 
         # check whether the data ended up in the right block, and the rest is still zero
-        xst_values = collector.xst_values()
+        xst_values = collector.xst_values()[0]
 
         for baseline_a in range(collector.MAX_INPUTS):
             for baseline_b in range(collector.MAX_INPUTS):
@@ -84,6 +128,48 @@ class TestXSTCollector(base.TestCase):
                 else:
                     self.assertEqual(0+0j, xst_values[baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] was not in packet, but was written to the XST matrix.')
 
+    def test_multiple_subbands(self):
+        collector = XSTCollector()
+
+        # a valid packet as obtained from SDP, with 64-bit BE 1+1j as payload at (12,0)
+        packet_subband_102 =  b'X\x05\x00\x00\x00\x00\x00\x00\x10\x08\x00\x02\xfa\xef\x00f\x0c\x00\x0c\x08\x01 \x14\x00\x00\x01!\xd9&z\x1b\xb3' + 288 * b'\x00\x00\x00\x00\x00\x00\x00\x01'
+        packet_subband_103 =  b'X\x05\x00\x00\x00\x00\x00\x00\x10\x08\x00\x02\xfa\xef\x00g\x0c\x00\x0c\x08\x01 \x14\x00\x00\x01!\xd9&z\x1b\xb3' + 288 * b'\x00\x00\x00\x00\x00\x00\x00\x02'
+
+        # make sure the subband_indices are indeed what we claim they are
+        fields = XSTPacket(packet_subband_102)
+        self.assertEqual(102,    fields.subband_index)
+
+        fields = XSTPacket(packet_subband_103)
+        self.assertEqual(103,    fields.subband_index)
+
+        # process our packets
+        collector.process_packet(packet_subband_102)
+        collector.process_packet(packet_subband_103)
+
+        # counters should now be updated
+        self.assertListEqual([102,103,0,0,0,0,0,0], list(collector.parameters["xst_subbands"]))
+
+        # check whether the data ended up in the right block, and the rest is still zero
+        xst_values = collector.xst_values()
+
+        for subband_idx in range(collector.MAX_PARALLEL_SUBBANDS):
+            for baseline_a in range(collector.MAX_INPUTS):
+                for baseline_b in range(collector.MAX_INPUTS):
+                    if baseline_b > baseline_a:
+                        # only scan top-left triangle
+                        continue
+
+                    baseline_a_was_in_packet = (fields.first_baseline[0] <= baseline_a < fields.first_baseline[0] + fields.nof_signal_inputs)
+                    baseline_b_was_in_packet = (fields.first_baseline[1] <= baseline_b < fields.first_baseline[1] + fields.nof_signal_inputs)
+
+                    if baseline_a_was_in_packet and baseline_b_was_in_packet and subband_idx == 0:
+                        self.assertEqual(1+1j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] did not end up in XST matrix.')
+                    elif baseline_a_was_in_packet and baseline_b_was_in_packet and subband_idx == 1:
+                        self.assertEqual(2+2j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] did not end up in XST matrix.')
+                    else:
+                        self.assertEqual(0+0j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] was not in packet, but was written to the XST matrix.')
+
+
     def test_invalid_packet(self):
         collector = XSTCollector()
 
diff --git a/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py b/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b10230c8fca8c0773d085c6e42719b74dd7b2c3
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+from tangostationcontrol.test import base
+from tangostationcontrol.statistics_writer import statistics_writer
+import sys
+from os.path import dirname
+from tempfile import TemporaryDirectory
+from unittest import mock
+
+class TestStatisticsWriter(base.TestCase):
+    def test_sst(self):
+        with TemporaryDirectory() as tmpdir:
+            new_sys_argv = [sys.argv[0], "--mode", "SST", "--file", dirname(__file__) + "/SDP_SST_statistics_packets.bin", "--output_dir", tmpdir]
+            with mock.patch.object(statistics_writer.sys, 'argv', new_sys_argv):
+                with self.assertRaises(SystemExit):
+                    statistics_writer.main()
+
+    def test_xst(self):
+        with TemporaryDirectory() as tmpdir:
+            new_sys_argv = [sys.argv[0], "--mode", "XST", "--file", dirname(__file__) + "/SDP_XST_statistics_packets.bin", "--output_dir", tmpdir]
+            with mock.patch.object(statistics_writer.sys, 'argv', new_sys_argv):
+                with self.assertRaises(SystemExit):
+                    statistics_writer.main()