diff --git a/tangostationcontrol/docs/source/calibration.rst b/tangostationcontrol/docs/source/calibration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b38b38d5c6fb5ccb3341d7ab60bfbc40ca9a7e42
--- /dev/null
+++ b/tangostationcontrol/docs/source/calibration.rst
@@ -0,0 +1,155 @@
+Calibration
+=============================
+
+The signal chain has to be tuned in order to precisely combine the sky signals received by the antennas.
+
+.. _casacore-measures:
+
+Emperedes & Geodetic calibration
+----------------------------------
+
+The ``TileBeam`` and ``DigitalBeam`` devices use `python-casacore <https://casacore.github.io/python-casacore/index.html>`_ to compute the direction of a given pointing with respect to our antennas and reference positions. Casacore in turn uses *measures* tables for the precise measurements of celestial positions, geodetical information, and time calibrations (f.e. leap seconds). These tables need to be installed and periodically updated to maintain the pointing accuracy:
+
+:measures_directory_R: Directory of the active set of measures tables. The directory name includes the timestamp denoting their age.
+
+  :type: ``str``
+
+:measures_directories_available_R: List of installed sets of measures tables.
+
+  :type: ``str[64]``
+
+:download_measures(): Download (but do not activate) the latest measures tables from ftp://ftp.astron.nl. Returns the directory name in which the measures were installed.
+
+  :returns: ``str``
+
+:use_measures(dir): Activate the measures tables in the provided directory. This necessitates turning off and restarting the TileBeam device, so the command will always appear to fail. Turn the device back and the selected measures tables will be active.
+
+  :returns: ``(does not return)``
+
+Instrumental calibration
+-----------------------------
+
+The antennas receive analog signals, which are delayed and attenuated (dampened) differently per antenna by the processing chain. These effects are caused most notably by:
+
+* The cables from antennas to RCUs,
+* The the RCU's processing chain,
+* The cables from RCU to RECV.
+
+We need to calibrate for the relative differences between the antennas, dominated by the cables from antennas to RCUs. To do so, we allow the station cabling to be configured, which provides us with a first-order model. From the configuration, the software computes default calibration values. The fine tuning of this calibration is to be overwritten by explicitly provided *calibration tables*, which are obtained through measurements. The calibration tables will be able to correct for any remaining differences in delay, phase, and power of the antennas.
+
+The instrument calibration depends on the frequency of the observed signal. We recognise several reference frequencies for which correction values are measured:
+
++--------------+----------------+------------------+-----------------------------+---------------------+
+| Antenna type | Frequency band | ``sdp.clock_RW`` | ``recv.RCU_band_select_RW`` | Reference frequency |
++==============+================+==================+=============================+=====================+
+| LBA          | 10 - 90 MHz    | (any)            | 1                           | 50 MHz              |
++--------------+----------------+------------------+-----------------------------+---------------------+
+| LBA          | 30 - 90 MHz    | (any)            | 2                           | 50 MHz              |
++--------------+----------------+------------------+-----------------------------+---------------------+
+| HBA          | 110 - 190 MHz  | 200_000_000      | 2                           | 150 MHz             |
++--------------+----------------+------------------+-----------------------------+---------------------+
+| HBA          | 170 - 230 MHz  | 160_000_000      | 1                           | 200 MHz             |
++--------------+----------------+------------------+-----------------------------+---------------------+
+| HBA          | 210 - 240 MHz  | 200_000_000      | 4                           | 250 MHz             |
++--------------+----------------+------------------+-----------------------------+---------------------+
+
+Instrument configuration
+`````````````````````````````
+
+The properties of the cables are modelled per cable type in the dictionary ``common.cables.cable_types``, and are named according to their length (f.e. "50m", "80m", etc). Which cables are used is configured in the properties of the ``AntennaField`` device:
+
+:Antenna_Cables: Names of the cable types used to connect each antenna (f.e. "50m", "80m", "120m"). Both polarisations of an antenna are assumed to be connected with the same type of cable.
+
+  :type: ``str[N_ant]``
+
+Information about the cabling is subsequently exposed through the following attributes:
+
+:Antenna_Cables_R: Which cable types are configured for each antenna (see the ``Antenna_Cables`` property).
+
+  :type: ``str[N_ant]``
+
+:Antenna_Cables_Delay_R: The delay introduced by the cabling for each antenna, in seconds.
+
+  :type: ``float[N_ant]``
+
+:Antenna_Cables_Loss_R: The attenuation introduced by the cabling for each antenna, in dB. The value returned depends on the current frequency that we observe from the antenna.
+
+  :type: ``float[N_ant]``
+
+Recalibrating the station
+`````````````````````````````
+
+The calibration values must be explicitly applied as part of initialising the RECV and SDP to process the antennas of an antenna field. These values are frequency dependent, so must be reapplied whenever the SDP clock changes, or whenever the RCU band selection does. The ``AntennaField`` device provides the following commands to apply the calibration:
+
+:calibrate_recv(): Compute and upload calibration settings to the RECV device(s) that process antennas of the antenna field.
+
+:calibrate_sdp(): Compute and upload calibration settings to the SDP device(s) that process antennas of the antenna field.
+
+Calibration explained
+`````````````````````````````
+
+We equalise the signals of the different antennas to compensate for the delay and attenuation effects, in two steps: coarse and fine. The following table describes what is corrected for where:
+
++------------+------------+---------------------------------------------+------------------------------+
+| Effect     | Granuality | Compensation                                | How                          |
++============+============+=============================================+==============================+
+| Delay      | Coarse     | ``sdp.FPGA_signal_input_samples_delay_RW``  | Delaying using a ring buffer |
++------------+------------+---------------------------------------------+------------------------------+
+| Delay      | Fine       | ``sdp.FPGA_subband_weights_RW``             | Phase shifts                 |
++------------+------------+---------------------------------------------+------------------------------+
+| Attenuation| Coarse     | ``recv.RCU_attenuator_dB_RW``               | Dampening whole dBs          |
++------------+------------+---------------------------------------------+------------------------------+
+| Attenuation| Fine       | ``sdp.FPGA_subband_weights_RW``             | Amplitude scaling            |
++------------+------------+---------------------------------------------+------------------------------+
+
+The *coarse delay compensation* is done in SDP, by delaying all inputs to line up with the latest arriving one. The FPGAs do this through a *sample shift*, in which the samples from each input is delayed a fixed number of samples. At the 200 MHz clock, samples are 5 ns. The sample shift aligns the inputs with a remaining difference of +/- 2.5 ns.
+
+This remainder is corrected for in the *fine delay compensation*, by shifting the phases of each input backwards. A phase shift is frequency dependent (``-2pi * frequency * delay``), and is thus applied at the higher frequency resolution after creating subbands. The ``FPGA_subband_weights_RW`` in SDP allows us to configure a complex correction factor for each subband from each input. A phase shift ``phi`` is converted into a complex factor through ``cos(phi) + i * sin(phi)``.
+
+.. note:: The delay compensation shifts all antenna signals by a fixed amount: the number of samples to delay to line up with the longest cable. Yet we mark those signals as "now" in SDP. This introduces a temporal shift of the order of 200ns. This is deemed acceptable, as after the station FFT (that creates the subbands), we have 5.12ms samples, which is an order of magnitude higher time scale.
+
+The *coarse loss compensation* is done in RECV on the RCU, which can attenuate each input an integer number of decibels. We attenuate each signal to line up with the weakest. The remaining attenuation is +/- 0.5 dB.
+
+The remainder is corrected for in the *fine loss compensation*, by applying an amplitude scaling factor (``10^(-dB/10)``) as part of the complex ``FPGA_subband_weights_RW`` (see above). This scaling factor is the same for all subbands.
+
+The ``AntennaField`` device provides the following attributes that provide the results of the above calculations per antenna:
+
+:Calibration_SDP_Signal_Input_Samples_Delay_R: The number of samples that each antenna must be delayed in SDP to line up.
+
+  :type: ``uint32[N_ant]``
+
+:Calibration_RCU_Attenuation_dB_R: The amount of dB that each antenna must be dampened on the RCU to line up.
+
+  :type: ``uint32[N_ant]``
+
+:Calibration_SDP_Subband_Weights_Default_R: The computed remaining delay & loss compensation, to be applied as subband weights in SDP. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling) that can be used to compute the complex correction factor for each subband.
+
+  :type: ``float[N_ant][3]``
+
+:Calibration_SDP_Subband_Weights_R: The remaining delay & loss compensation to be applied. Returns rows from the preconfigured calibration table if available, falling back to rows from the computed ``Calibration_SDP_Subband_Weights_Default_R`` table otherwise.
+
+  :type: ``float[N_ant][3]``
+
+The preconfigured calibration tables are described below.
+
+Calibration configuration
+`````````````````````````````
+
+The software models cannot account for all required corrections. We assume that the software correctly computes the coarse compensation, but that additional tuning of the fine compensations is needed. Actual measurements must be used to derive the precise correction factors for each antenna. The ``AntennaField`` device allows the following properties to be used to replace the computed ``Calibration_SDP_Subband_Weights_Default_R`` table for the fine correction factors:
+
+:Calibration_SDP_Subband_Weights_50MHz: The measured correction factors for each input, at the 50 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling).
+
+  :type: ``float[N_ant][3]``
+
+:Calibration_SDP_Subband_Weights_150MHz: The measured correction factors for each input, at the 150 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling).
+
+  :type: ``float[N_ant][3]``
+
+:Calibration_SDP_Subband_Weights_200MHz: The measured correction factors for each input, at the 200 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling).
+
+  :type: ``float[N_ant][3]``
+
+:Calibration_SDP_Subband_Weights_250MHz: The measured correction factors for each input, at the 250 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling).
+
+  :type: ``float[N_ant][3]``
+
diff --git a/tangostationcontrol/docs/source/devices/tilebeam-digitalbeam.rst b/tangostationcontrol/docs/source/devices/tilebeam-digitalbeam.rst
index c6701097b7976b55ce2d8fae9813bb842199b134..b1ed94a34593669fa352371dc586eb51529112be 100644
--- a/tangostationcontrol/docs/source/devices/tilebeam-digitalbeam.rst
+++ b/tangostationcontrol/docs/source/devices/tilebeam-digitalbeam.rst
@@ -84,26 +84,7 @@ The beam steering is responsible for pointing the beams at a target, by converti
 
   :returns: ``None``
 
-Celestial and geographical models
-"""""""""""""""""""""""""""""""""
-
-We use `python-casacore <https://casacore.github.io/python-casacore/index.html>`_ to compute the direction of a given pointing with respect to our antennas and reference positions. Casacore in turn uses *measures* tables for the precise measurements of celestial positions, geodetical information, and time calibrations (f.e. leap seconds). These tables need to be installed and periodically updated to maintain the pointing accuracy:
-
-:measures_directory_R: Directory of the active set of measures tables. The directory name includes the timestamp denoting their age.
-
-  :type: ``str``
-
-:measures_directories_available_R: List of installed sets of measures tables.
-
-  :type: ``str[64]``
-
-:download_measures(): Download (but do not activate) the latest measures tables from ftp://ftp.astron.nl. Returns the directory name in which the measures were installed.
-
-  :returns: ``str``
-
-:use_measures(dir): Activate the measures tables in the provided directory. This necessitates turning off and restarting the TileBeam device, so the command will always appear to fail. Turn the device back and the selected measures tables will be active.
-
-  :returns: ``(does not return)``
+The direction of each pointing is derived using *casacore*, which must be periodically calibrated, see also :ref:`casacore-measures`.
 
 Timing
 """""""""""""""""""""""""""""""""
diff --git a/tangostationcontrol/docs/source/index.rst b/tangostationcontrol/docs/source/index.rst
index 4c56206dfce3ab37b07f103a963d06846ccd8f12..55da77372087956bb057b6e340f9c40f72be5612 100644
--- a/tangostationcontrol/docs/source/index.rst
+++ b/tangostationcontrol/docs/source/index.rst
@@ -34,6 +34,7 @@ Even without having access to any LOFAR2.0 hardware, you can install the full st
    devices/configure
    configure_station
    signal_chain
+   calibration
    broken_hardware
    developer
    faq
diff --git a/tangostationcontrol/tangostationcontrol/clients/comms_client.py b/tangostationcontrol/tangostationcontrol/clients/comms_client.py
index d18c3e47e02bd113aa2954f406d1f863f383983f..cfa989439a2c12317a895f4f1a83618ca42c9bf9 100644
--- a/tangostationcontrol/tangostationcontrol/clients/comms_client.py
+++ b/tangostationcontrol/tangostationcontrol/clients/comms_client.py
@@ -15,12 +15,12 @@ class AbstractCommClient(ABC):
     def stop(self):
         """ Stop communication with the client. """
 
+    @abstractmethod
     def ping(self):
         """ Check whether the connection is still alive.
 
             Clients that override this method must raise an Exception if the
             connection died. """
-        pass
 
     @abstractmethod
     def setup_attribute(self, annotation, attribute):
diff --git a/tangostationcontrol/tangostationcontrol/common/cables.py b/tangostationcontrol/tangostationcontrol/common/cables.py
new file mode 100644
index 0000000000000000000000000000000000000000..b48123a26615a1c7d2ca2d97ffdea686968609f2
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/common/cables.py
@@ -0,0 +1,52 @@
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class CableType:
+    """ A cable used in LOFAR, with its properties. """
+    name: str
+    length: int
+    delay: float
+    loss: dict
+
+    def speed(self):
+        """ Return the speed of the signal in this cable, in m/s. """
+
+        return self.length / self.delay
+
+    def get_loss(self, antenna_type: str, rcu_band_select: int) -> float:
+        """ Get the appropiate loss value (in dB), for the given
+            antenna type and RCU band selection. """
+
+        if antenna_type == "LBA":
+            if rcu_band_select == 1:
+                return self.loss[50]
+            elif rcu_band_select == 2:
+                return self.loss[50]
+            else:
+                raise ValueError(f"Unsupported RCU band selection for LBA: {rcu_band_select}")
+        elif antenna_type == "HBA":
+            if rcu_band_select == 1:
+                return self.loss[200]
+            elif rcu_band_select == 2:
+                return self.loss[150]
+            elif rcu_band_select == 4:
+                return self.loss[250]
+            else:
+                raise ValueError(f"Unsupported RCU band selection for HBA: {rcu_band_select}")
+
+        raise ValueError(f"Unsupported antenna type: {antenna_type}")
+
+# Global list of all known cable types.
+#
+# NB: The LOFAR1 equivalents of these tables are:
+#          - MAC/Deployment/data/StaticMetaData/CableDelays/
+#          - MAC/Deployment/data/StaticMetaData/CableAttenuation.conf
+cable_types = {}
+cable_types[  "0m"] = CableType(name=  "0m", length=  0, delay=000.0000e-9, loss={50: 0.00, 150: 0.00, 200:  0.00, 250:  0.00})
+cable_types[ "50m"] = CableType(name= "50m", length= 50, delay=199.2573e-9, loss={50: 2.05, 150: 3.64, 200:  4.24, 250:  4.46})
+cable_types[ "80m"] = CableType(name= "80m", length= 80, delay=326.9640e-9, loss={50: 3.32, 150: 5.87, 200:  6.82, 250:  7.19})
+cable_types[ "85m"] = CableType(name= "85m", length= 85, delay=342.5133e-9, loss={50: 3.53, 150: 6.22, 200:  7.21, 250:  7.58})
+cable_types["115m"] = CableType(name="115m", length=115, delay=465.5254e-9, loss={50: 4.74, 150: 8.35, 200:  9.70, 250: 10.18})
+cable_types["120m"] = CableType(name="120m", length=120, delay=493.8617e-9, loss={50: 4.85, 150: 8.55, 200:  9.92, 250: 10.42}) # used on CS030
+cable_types["130m"] = CableType(name="130m", length=130, delay=530.6981e-9, loss={50: 5.40, 150: 9.52, 200: 11.06, 250: 11.61})
+
diff --git a/tangostationcontrol/tangostationcontrol/common/calibration.py b/tangostationcontrol/tangostationcontrol/common/calibration.py
new file mode 100644
index 0000000000000000000000000000000000000000..e08810bab69072db16f62c3cf9cf6a64b2862f98
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/common/calibration.py
@@ -0,0 +1,74 @@
+import numpy
+
+def delay_compensation(delays_seconds: numpy.ndarray, clock: int):
+    """ Return the delay compensation required to line up
+        signals that are delayed by "delays" seconds. The returned values
+        are the delay to apply, in samples (coarse) and remaining seconds
+        (fine), as a tuple (samples, remainder).
+
+        The coarse delay is to be applied in sdp.FPGA_signal_input_delays_RW,
+        the fine delay is to be incorporated into sdp.FPGA_subband_weights_RW.
+
+        Note that the remainder is -1/2 * sample <= remainder <= 1/2 * sample.
+
+        Applying this correction equalises the signal across all inputs to be delayed
+        max(round(delay_samples)) samples, instead of their value in delay_seconds.
+        So we do _not exactly_ delay all signals to match the longest.
+    """
+
+    # NB: signal_* are the amount of delay the signal obtained in our processing
+    #     chain, while input_* are the amount of (negative) delay to apply
+    #     to compensate.
+
+    # compute the coarse correction, in samples
+    signal_delays_samples = numpy.round(delays_seconds * clock).astype(numpy.uint32)
+
+    # correct for the coarse delay by delaying the other signals to line up
+    # we cannot configure a negative number of samples, so we must delay
+    # all of them sufficiently as well.
+    #
+    # This introduces a constant shift in timing for all samples,
+    # as we shift all of them to obtain a non-negative delay.
+    input_delays_samples = max(signal_delays_samples) - signal_delays_samples
+
+    # compute the remainder, in seconds
+    signal_delays_subsample_seconds = delays_seconds - signal_delays_samples / clock
+    input_delays_subsample_seconds = -signal_delays_subsample_seconds
+
+    return (input_delays_samples, input_delays_subsample_seconds)
+
+def dB_to_factor(dB: numpy.ndarray) -> numpy.ndarray:
+    """ Convert values in decibel (dB) into their equivalent scaling factors. """
+    return 10 ** (dB / 10)
+
+def loss_compensation(losses_dB: numpy.ndarray):
+    """ Return the attenuation required to line up
+        signals that are dampened by "lossed_dB" decibel.
+
+        Returned are the signal attenuations in whole dBs (coarse), and
+        the remaining scaling (as a factor), as a tuple (whole dBs, remainder).
+
+        The coarse attenuation is to be applied in recv.RCU_attenuation_dB_RW,
+        the fine scaling is to be incorporated into sdp.FPGA_subband_weights_RW.
+
+        Applying this correction equalises the signal across the inputs
+        to be dampened max(round(losses_dB)) instead of their value
+        in losses_dB. So we do _not_ fully dampen towards the weakest signal.
+    """
+
+    # NB: signal_* are the amount of loss the signal obtained in our processing
+    #     chain, while input_* are the amount of (dampening) attenuation to apply
+    #     to compensate.
+
+    # compute the coarse correction, in samples
+    signal_attenuation_integer_dB = numpy.round(losses_dB).astype(numpy.uint32)
+
+    # correct for the coarse loss by dampening the signals to line up.
+    input_attenuation_integer_dB = max(signal_attenuation_integer_dB) - signal_attenuation_integer_dB
+
+    # compute the remainder, as a scaling factor
+    signal_loss_remainder_dB = losses_dB - signal_attenuation_integer_dB
+    input_attenuation_remainder_dB = -signal_loss_remainder_dB
+    input_attenuation_remainder_factor = dB_to_factor(input_attenuation_remainder_dB)
+
+    return (input_attenuation_integer_dB, input_attenuation_remainder_factor)
diff --git a/tangostationcontrol/tangostationcontrol/common/constants.py b/tangostationcontrol/tangostationcontrol/common/constants.py
index 8a2dbb7bd75b281e411c3a73cc9686306f6d364b..a88e7151ae4fa767550167e156e956f3b71527db 100644
--- a/tangostationcontrol/tangostationcontrol/common/constants.py
+++ b/tangostationcontrol/tangostationcontrol/common/constants.py
@@ -94,3 +94,6 @@ MAX_ETH_FRAME_SIZE = 9000
 
 # The default polling period for polled attributes
 DEFAULT_POLLING_PERIOD = 1000
+
+# default numer tiles in a HBA for the non-international stations.
+DEFAULT_N_HBA_TILES = 48
diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
index b57d3b9622e74967a7b1adf3f3f3bf2ddfabd12a..8754eb83f7bfa9cb10d7a6d500fc29aea12882b2 100644
--- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
@@ -19,9 +19,12 @@ from tango.server import device_property, attribute, command
 from tangostationcontrol.common.type_checking import type_not_sequence
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions
-from tangostationcontrol.common.constants import N_elements, MAX_ANTENNA, N_pol, N_xyz, N_latlong, N_rcu, N_rcu_inp
+from tangostationcontrol.common.constants import N_elements, MAX_ANTENNA, N_pol, N_xyz, N_latlong, N_rcu, N_rcu_inp, N_pn, S_pn, N_subbands
+from tangostationcontrol.common.cables import cable_types
+from tangostationcontrol.common.calibration import delay_compensation, loss_compensation
 from tangostationcontrol.devices.lofar_device import lofar_device
 from tangostationcontrol.devices.device_decorators import fault_on_error
+from tangostationcontrol.devices.sdp.sdp import SDP
 from tangostationcontrol.beam.geo import ETRS_to_ITRF, ITRF_to_GEO, GEO_to_GEOHASH
 from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets
 
@@ -132,6 +135,37 @@ class AntennaField(lofar_device):
         default_value=numpy.array([False] * MAX_ANTENNA)
     )
 
+    Antenna_Cables = device_property(
+        doc=f"Which cables connect each antenna to the RCU. Both polarisations are assumed to be connected using the same type of cable. Needs to be any of ({', '.join(cable_types.keys())}).",
+        dtype='DevVarStringArray',
+        mandatory=False,
+        default_value = numpy.array(["0m"] * MAX_ANTENNA)
+    )
+
+    Calibration_SDP_Subband_Weights_50MHz = device_property(
+        doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 50 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.",
+        dtype='DevVarFloatArray',
+        mandatory=False
+    )
+
+    Calibration_SDP_Subband_Weights_150MHz = device_property(
+        doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 150 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.",
+        dtype='DevVarFloatArray',
+        mandatory=False
+    )
+
+    Calibration_SDP_Subband_Weights_200MHz = device_property(
+        doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 200 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.",
+        dtype='DevVarFloatArray',
+        mandatory=False
+    )
+
+    Calibration_SDP_Subband_Weights_250MHz = device_property(
+        doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 250 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.",
+        dtype='DevVarFloatArray',
+        mandatory=False
+    )
+
     # ----- Position information
 
     Antenna_Field_Reference_ITRF = device_property(
@@ -171,6 +205,7 @@ class AntennaField(lofar_device):
         mandatory=False,
         default_value=2015.5
     )
+
     HBAT_PQR_rotation_angles_deg = device_property(
         doc='Rotation of each tile in the PQ plane ("horizontal") in degrees.',
         dtype='DevVarFloatArray',
@@ -233,11 +268,39 @@ class AntennaField(lofar_device):
         default_value=[]
     )
 
+    # ----- Generic information
+
     Antenna_Type_R = attribute(doc='The type of antenna in this field (LBA or HBA).',
         dtype=str)
-
     Antenna_Names_R = attribute(access=AttrWriteType.READ,
                                 dtype=(str,), max_dim_x=MAX_ANTENNA)
+    Antenna_to_SDP_Mapping_R = attribute(doc='To which (fpga, input) pair each antenna is connected. -1=unconnected.',
+                                         dtype=((numpy.int32,),), max_dim_x=N_pol, max_dim_y=MAX_ANTENNA)
+
+    # ----- Cable information (between antenna and RCU)
+
+    Antenna_Cables_R = attribute(doc=f"Which cables connect each antenna to the RCU. Both polarisations are assumed to be connected using the same type of cable. Needs to be any of ({', '.join(cable_types.keys())}).",
+        dtype=(str,), max_dim_x=MAX_ANTENNA)
+    Antenna_Cables_Delay_R = attribute(doc=f"Delay caused by the cable between antenna and RCU, in seconds.",
+        dtype=(numpy.float64,), max_dim_x=MAX_ANTENNA, unit="s")
+    Antenna_Cables_Loss_R = attribute(doc=f"Loss caused by the cable between antenna and RCU, in dB.",
+        dtype=(numpy.float64,), max_dim_x=MAX_ANTENNA, unit="dB")
+
+    # ----- Calibration information
+
+    Calibration_SDP_Signal_Input_Samples_Delay_R = attribute(doc=f"Number of samples that each antenna signal should be delayed to line up. To be applied on sdp.FPGA_signal_input_samples_delay_RW.",
+        dtype=(numpy.uint32,), max_dim_x=MAX_ANTENNA, unit="samples")
+    Calibration_RCU_Attenuation_dB_R = attribute(doc=f"Amount of dB with which each antenna signal must be adjusted to line up. To be applied on recv.RCU_attenuator_dB_RW.",
+        dtype=(numpy.uint32,), max_dim_x=MAX_ANTENNA, unit="dB")
+    Calibration_SDP_Subband_Weights_Default_R = attribute(
+        doc=f"Computed calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.",
+        dtype=((numpy.float64,),), max_dim_y=MAX_ANTENNA, max_dim_x=3)
+    Calibration_SDP_Subband_Weights_R = attribute(
+        doc=f"Calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet. Returns the measured values from Calibration_SDP_Subband_Weights_XXXMHz if available, and values from Calibration_SDP_Subband_Weights_Default_R otherwise.",
+        dtype=((numpy.float64,),), max_dim_y=MAX_ANTENNA, max_dim_x=3)
+
+    # ----- Quality and usage information
+
     Antenna_Quality_R = attribute(doc='The quality of each antenna. 0=OK, 1=SUSPICIOUS, 2=BROKEN, 3=BEYOND_REPAIR.',
                                   dtype=(numpy.uint32,), max_dim_x=MAX_ANTENNA)
     Antenna_Use_R = attribute(
@@ -247,13 +310,10 @@ class AntennaField(lofar_device):
                                       dtype=(str,), max_dim_x=MAX_ANTENNA)
     Antenna_Use_str_R = attribute(doc='Whether each antenna should be used, as a string.',
                                   dtype=(str,), max_dim_x=MAX_ANTENNA)
-
     Antenna_Usage_Mask_R = attribute(doc='Whether each antenna will be used.',
-
                                      dtype=(bool,), max_dim_x=MAX_ANTENNA)
 
-    Antenna_to_SDP_Mapping_R = attribute(doc='To which (fpga, input) pair each antenna is connected. -1=unconnected.',
-                                         dtype=((numpy.int32,),), max_dim_x=N_pol, max_dim_y=MAX_ANTENNA)
+    # ----- Attributes mapped on RECV
 
     ANT_mask_RW = mapped_attribute("ANT_mask_RW", dtype=(bool,), max_dim_x=MAX_ANTENNA,
                                    access=AttrWriteType.READ_WRITE)
@@ -280,6 +340,8 @@ class AntennaField(lofar_device):
                                       max_dim_y=MAX_ANTENNA, access=AttrWriteType.READ_WRITE)
     RCU_band_select_RW = mapped_attribute("RCU_band_select_RW", dtype=(numpy.int64,), max_dim_x=MAX_ANTENNA,
                                           access=AttrWriteType.READ_WRITE)
+    RCU_attenuator_dB_RW = mapped_attribute("RCU_attenuator_dB_RW", dtype=(numpy.int64,), max_dim_x=MAX_ANTENNA,
+                                            access=AttrWriteType.READ_WRITE)
 
     # ----- Position information
 
@@ -322,6 +384,88 @@ class AntennaField(lofar_device):
     def read_Antenna_Names_R(self):
         return self.Antenna_Names
 
+    def read_Antenna_Cables_R(self):
+        return self.Antenna_Cables
+
+    def read_Antenna_Cables_Delay_R(self):
+        return numpy.array([cable_types[antenna].delay for antenna in self.Antenna_Cables])
+
+    def read_Antenna_Cables_Loss_R(self):
+        rcu_bands = self.read_attribute("RCU_band_select_RW")
+        return numpy.array([cable_types[antenna].get_loss(self.Antenna_Type, rcu_band) for antenna, rcu_band in zip(self.Antenna_Cables, rcu_bands)])
+
+    def read_Calibration_SDP_Signal_Input_Samples_Delay_R(self):
+        # Correct for signal delays in the cables
+        signal_delay_seconds = self.read_attribute("Antenna_Cables_Delay_R")
+
+        # compute the required compensation
+        clock = self.sdp_proxy.clock_RW
+        input_delay_samples, _ = delay_compensation(signal_delay_seconds, clock)
+
+        # return the delay to apply (in samples)
+        return input_delay_samples
+
+    def read_Calibration_SDP_Subband_Weights_Default_R(self):
+        # ----- Delay
+
+        # correct for signal delays in the cables
+        signal_delay_seconds = self.read_attribute("Antenna_Cables_Delay_R")
+
+        # compute the required compensation
+        clock = self.sdp_proxy.clock_RW
+        _, input_delay_subsample_seconds = delay_compensation(signal_delay_seconds, clock)
+
+        # ----- Phase offsets
+
+        # we don't have any
+        phase_offsets = numpy.zeros((self.read_attribute("nr_antennas_R"),),dtype=numpy.float64)
+
+        # ----- Amplitude
+
+        # correct for signal loss in the cables
+        signal_delay_loss = self.read_attribute("Antenna_Cables_Loss_R")
+
+        # return fine scaling to apply
+        _, input_attenuation_remaining_factor = loss_compensation(signal_delay_loss)
+
+        # Return as (delay, phase_offset, amplitude) triplet per antenna
+        return numpy.stack((input_delay_subsample_seconds, phase_offsets, input_attenuation_remaining_factor), axis=1)
+
+    def read_Calibration_SDP_Subband_Weights_R(self):
+        # default table to use if no calibration table is available
+        default_table = self.read_attribute("Calibration_SDP_Subband_Weights_Default_R")
+
+        # construct selector for the right calibration table
+        if self.Antenna_Type == "LBA":
+            rcu_band_to_caltable = {
+                1: self.Calibration_SDP_Subband_Weights_50MHz or default_table,
+                2: self.Calibration_SDP_Subband_Weights_50MHz or default_table,
+            }
+        else: # HBA
+            rcu_band_to_caltable = {
+                2: self.Calibration_SDP_Subband_Weights_150MHz or default_table,
+                1: self.Calibration_SDP_Subband_Weights_200MHz or default_table,
+                4: self.Calibration_SDP_Subband_Weights_250MHz or default_table,
+            }
+        
+        rcu_bands = self.read_attribute("RCU_band_select_RW")
+
+        # construct the subband weights based on the rcu_band of each antenna,
+        # combining the relevant tables.
+        subband_weights = numpy.zeros((self.read_attribute("nr_antennas_R"), 3), dtype=numpy.float64)
+        for antenna_nr, rcu_band in enumerate(rcu_bands):
+            subband_weights[antenna_nr, :] = rcu_band_to_caltable[rcu_band][antenna_nr, :]
+
+        return subband_weights
+
+    def read_Calibration_RCU_Attenuation_dB_R(self):
+        # Correct for signal loss in the cables
+        signal_delay_loss = self.read_attribute("Antenna_Cables_Loss_R")
+
+        # return coarse attenuation to apply
+        input_attenuation_integer_dB, _ = loss_compensation(signal_delay_loss)
+        return input_attenuation_integer_dB
+
     def read_Antenna_Use_R(self):
         return self.Antenna_Use
 
@@ -514,6 +658,82 @@ class AntennaField(lofar_device):
 
         self.sdp_proxy.antenna_type_RW = tuple(sdp_antenna_type)
 
+    @command()
+    def calibrate_recv(self):
+        """ Calibrate RECV for our antennas.
+
+            Run whenever the following changes:
+                sdp.clock_RW
+                antennafield.RCU_band_select_RW
+        """
+
+        # -----------------------------------------------------------
+        #   Set signal-input attenuations to compensate for
+        #   differences in cable length.
+        # -----------------------------------------------------------
+
+        rcu_attenuator_db = self.read_attribute("Calibration_RCU_Attenuation_dB_R")
+        self.proxy.write_attribute("RCU_attenuator_dB_RW", rcu_attenuator_db)
+
+    @command()
+    def calibrate_sdp(self):
+        """ Calibrate SDP for our antennas.
+
+            Run whenever the following changes:
+                sdp.clock_RW
+                antennafield.RCU_band_select_RW
+        """
+
+        # Mapping [antenna] -> [fpga][input]
+        antenna_to_sdp_mapping = self.read_attribute("Antenna_to_SDP_Mapping_R")
+
+        # -----------------------------------------------------------
+        #   Set coarse delay compensation by delaying the samples.
+        # -----------------------------------------------------------
+
+        # The delay to apply, in samples [antenna]
+        input_samples_delay  = self.read_attribute("Calibration_SDP_Signal_Input_Samples_Delay_R")
+
+        # read-modify-write on [fpga][(input, polarisation)]
+        fpga_signal_input_samples_delay = self.sdp_proxy.FPGA_signal_input_samples_delay_RW
+        for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping):
+            if input_nr == -1:
+                # skip unconnected antennas
+                continue
+
+            # set for X polarisation
+            fpga_signal_input_samples_delay[fpga_nr, input_nr * N_pol + 0] = input_samples_delay[antenna_nr]
+            # set for Y polarisation
+            fpga_signal_input_samples_delay[fpga_nr, input_nr * N_pol + 1] = input_samples_delay[antenna_nr]
+        self.sdp_proxy.FPGA_signal_input_samples_delay_RW = fpga_signal_input_samples_delay
+
+        # -----------------------------------------------------------
+        #   Compute calibration of subband weights for the remaining
+        #   delay and loss corrections.
+        # -----------------------------------------------------------
+
+        # obtain caltable
+        caltable = self.read_attribute("Calibration_SDP_Subband_Weights_R")
+
+        # obtain frequency information
+        clock = self.sdp_proxy.clock_RW
+        nyquist_zone = self.sdp_proxy.nyquist_zone_R # NB: for all inputs, unmapped, not just ours
+
+        # read-modify-write on [fpga][(input, polarisation)]
+        fpga_subband_weights = self.sdp_proxy.FPGA_subband_weights_RW.reshape(N_pn, S_pn, N_subbands)
+        for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping):
+            if input_nr == -1:
+                # skip unconnected antennas
+                continue
+
+            # compute weights
+            weights = SDP.subband_weights(caltable[antenna_nr][0], caltable[antenna_nr][1], caltable[antenna_nr][2], clock, nyquist_zone[fpga_nr, input_nr])
+
+            # set weights
+            fpga_subband_weights[fpga_nr, input_nr, :] = weights
+        self.sdp_proxy.FPGA_subband_weights_RW = fpga_subband_weights.reshape(N_pn, S_pn * N_subbands)
+
+
     @command(dtype_in=DevVarFloatArray, dtype_out=DevVarLongArray)
     def calculate_HBAT_bf_delay_steps(self, delays: numpy.ndarray):
         num_tiles = self.read_nr_antennas_R()
@@ -572,7 +792,8 @@ class AntennaToRecvMapper(object):
             "HBAT_PWR_on_RW": value_map_ant_32_bool,
             "RCU_PWR_ANT_on_R": value_map_ant_bool,
             "RCU_PWR_ANT_on_RW": value_map_ant_bool,
-            "RCU_band_select_RW": numpy.zeros(number_of_antennas, dtype=numpy.int64)
+            "RCU_band_select_RW": numpy.zeros(number_of_antennas, dtype=numpy.int64),
+            "RCU_attenuator_dB_RW": numpy.zeros(number_of_antennas, dtype=numpy.int64),
         }
         self._masked_value_mapping_write = {
             "ANT_mask_RW": AntennaToRecvMapper._VALUE_MAP_NONE_96,
@@ -582,18 +803,21 @@ class AntennaToRecvMapper(object):
             "HBAT_PWR_on_RW": AntennaToRecvMapper._VALUE_MAP_NONE_96_32,
             "RCU_PWR_ANT_on_RW": AntennaToRecvMapper._VALUE_MAP_NONE_96,
             "RCU_band_select_RW": AntennaToRecvMapper._VALUE_MAP_NONE_96,
+            "RCU_attenuator_dB_RW": AntennaToRecvMapper._VALUE_MAP_NONE_96,
         }
         self._reshape_attributes_in = {
             "HBAT_BF_delay_steps_RW": (MAX_ANTENNA, N_rcu),
             "RCU_PWR_ANT_on_R": (MAX_ANTENNA,),
             "RCU_PWR_ANT_on_RW": (MAX_ANTENNA,),
             "RCU_band_select_RW": (MAX_ANTENNA,),
+            "RCU_attenuator_dB_RW": (MAX_ANTENNA,),
         }
         self._reshape_attributes_out = {
             "HBAT_BF_delay_steps_RW": (MAX_ANTENNA, N_rcu),
             "RCU_PWR_ANT_on_R": (N_rcu, N_rcu_inp),
             "RCU_PWR_ANT_on_RW": (N_rcu, N_rcu_inp),
             "RCU_band_select_RW": (N_rcu, N_rcu_inp),
+            "RCU_attenuator_dB_RW": (N_rcu, N_rcu_inp),
         }
 
     def map_read(self, mapped_attribute: str, recv_results: List[any]) -> List[any]:
diff --git a/tangostationcontrol/tangostationcontrol/devices/beam_device.py b/tangostationcontrol/tangostationcontrol/devices/beam_device.py
index dc2ee888d21726ea3fdefc99b471e0d076e8ab67..41e0b7f70cc1432e345f9420a058f5bafe0ab275 100644
--- a/tangostationcontrol/tangostationcontrol/devices/beam_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/beam_device.py
@@ -468,10 +468,10 @@ class BeamTracker():
                 else:
                     next_update_in = now + datetime.timedelta(seconds=now.timestamp() % self.interval)
 
+                    # 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())
+
                 # update pointing at requested time
                 self.stale_pointing = False
                 self.update_pointing_callback(next_update_in)
-
-                # 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/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py
index 3c1b81ae137fdf93646b11b8b0fc061c475503b2..e53fcbd4035b433019aacc45197f94bb1ef1f293 100644
--- a/tangostationcontrol/tangostationcontrol/devices/recv.py
+++ b/tangostationcontrol/tangostationcontrol/devices/recv.py
@@ -75,7 +75,7 @@ class RECV(opcua_device):
     RCU_band_select_RW_default = device_property(
         dtype='DevVarLong64Array',
         mandatory=False,
-        default_value=[0] * N_rcu * N_rcu_inp
+        default_value=[1] * N_rcu * N_rcu_inp
     )
 
     RCU_PWR_ANT_on_RW_default = device_property(
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
index 5fd05975cc88c0828e3166ef65fd589174a15896..68cb16d1aea06aa45e427acedbdb1d57694cf798 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
@@ -14,9 +14,10 @@ from tango import AttrWriteType, DevVarFloatArray, DevVarULongArray, DeviceProxy
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import log_exceptions
-from tangostationcontrol.common.constants import N_pn, A_pn, N_pol, N_beamlets_ctrl, N_beamsets_ctrl, P_sum, N_subband_res, N_subbands, DEFAULT_SUBBAND
+from tangostationcontrol.common.constants import N_pn, A_pn, N_pol, N_beamlets_ctrl, N_beamsets_ctrl, P_sum, DEFAULT_SUBBAND
 from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper
 from tangostationcontrol.devices.opcua_device import opcua_device
+from tangostationcontrol.devices.sdp.common import phases_to_weights, subband_frequencies
 
 import numpy
 from functools import lru_cache
@@ -282,43 +283,6 @@ class Beamlet(opcua_device):
 
     BF_UNIT_WEIGHT = 2**14
 
-    @staticmethod
-    def _phases_to_bf_weights(phases: numpy.ndarray):
-        """ Convert differences in phase (in radians) into FPGA weights (complex numbers packed into uint32). """
-
-        # flatten array and restore its shape later, which makes running over all elements a lot easier
-        orig_shape = phases.shape
-        phases = phases.flatten()
-
-        # Convert array values in complex numbers
-        real = Beamlet.BF_UNIT_WEIGHT * numpy.cos(phases)
-        imag = Beamlet.BF_UNIT_WEIGHT * numpy.sin(phases)
-
-        # Interleave into (real, imag) pairs, and store as int16
-        # see also https://stackoverflow.com/questions/5347065/interweaving-two-numpy-arrays/5347492
-        # Round to nearest integer instead of rounding down
-        real_imag = numpy.empty(phases.size * 2, dtype=numpy.int16)
-        real_imag[0::2] = numpy.round(real)
-        real_imag[1::2] = numpy.round(imag)
-
-        # Cast each (real, imag) pair into an uint32, which brings the array size
-        # back to the original.
-        bf_weights = real_imag.view(numpy.uint32)
-
-        return bf_weights.reshape(orig_shape)
-
-    @staticmethod
-    def _subband_frequencies(subbands: numpy.ndarray, clock: int, nyquist_zones: numpy.ndarray) -> numpy.ndarray:
-        """ Obtain the frequencies of each subband, given a clock and an antenna type. """
-
-        subband_width = clock / N_subband_res
-        base_subbands = nyquist_zones * N_subbands
-
-        # broadcast clock across frequencies
-        frequencies = (subbands + base_subbands) * subband_width
-
-        return frequencies
-
     @lru_cache() # this function requires large hardware reads for values that don't change often
     def _beamlet_frequencies(self):
         """ Obtain the frequencies (in Hz) of each subband that is selected for each input and beamlet.
@@ -334,7 +298,7 @@ class Beamlet(opcua_device):
         nyquist_zones    = numpy.repeat(nyquist_zones, N_beamlets_ctrl, axis=1)
 
         # compute the frequency of each beamlet for each input
-        return self._subband_frequencies(beamlet_subbands, self.sdp_proxy.clock_RW, nyquist_zones)
+        return subband_frequencies(beamlet_subbands, self.sdp_proxy.clock_RW, nyquist_zones)
 
     @staticmethod
     def _calculate_bf_weights(delays: numpy.ndarray, beamlet_frequencies: numpy.ndarray):
@@ -345,11 +309,11 @@ class Beamlet(opcua_device):
 
         # compute the phases
 
-        # applying a delay means rotating *backwards*
+        # correcting for a delay means rotating *backwards*
         beamlet_phases = (-2.0 * numpy.pi) * beamlet_frequencies * delays
 
         # convert to weights
-        bf_weights = Beamlet._phases_to_bf_weights(beamlet_phases)
+        bf_weights = phases_to_weights(beamlet_phases, Beamlet.BF_UNIT_WEIGHT)
 
         return bf_weights
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/common.py b/tangostationcontrol/tangostationcontrol/devices/sdp/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..891dbb648b1417f5f8504e878658fbe6773bf66b
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/common.py
@@ -0,0 +1,65 @@
+from tangostationcontrol.common.constants import N_subbands, N_subband_res, VALUES_PER_COMPLEX
+
+import numpy
+from ctypes import c_short
+
+def subband_frequencies(subbands: numpy.ndarray, clock: int, nyquist_zones: numpy.ndarray) -> numpy.ndarray:
+    """ Obtain the frequencies of multiple subbands, given a clock and a nyquist zone for each subband. """
+
+    subband_width = clock / N_subband_res
+    base_subbands = nyquist_zones * N_subbands
+
+    # broadcast clock across frequencies
+    frequencies = (subbands + base_subbands) * subband_width
+
+    return frequencies
+
+def subband_frequency(subband: int, clock: float, nyquist_zone: int) -> int:
+    """ Obtain the frequencies of a subband, given a clock and a nyquist zone. """
+
+    # just use the interface for multiple subbands to avoid code duplication
+    return subband_frequencies(numpy.array(subband), clock, numpy.array(nyquist_zone)).item()
+
+def phases_to_weights(phases: numpy.ndarray, unit_weight: float, amplitudes: numpy.ndarray = None) -> numpy.ndarray:
+    """ Convert phases (in radians) into FPGA weights (complex numbers packed into uint32). """
+       
+    # The FPGA accepts weights as a 16-bit (imag,real) complex pair packed into an uint32.
+
+    # flatten array and restore its shape later, which makes running over all elements a lot easier
+    orig_shape = phases.shape
+    phases = phases.flatten()
+
+    # Convert array values in complex numbers
+    real = unit_weight * numpy.cos(phases)
+    imag = unit_weight * numpy.sin(phases)
+
+    if amplitudes is not None:
+        real *= amplitudes
+        imag *= amplitudes
+
+    # Interleave into (real, imag) pairs, and store as int16
+    # see also https://stackoverflow.com/questions/5347065/interweaving-two-numpy-arrays/5347492
+    # Round to nearest integer instead of rounding down
+    real_imag = numpy.empty(phases.size * VALUES_PER_COMPLEX, dtype=numpy.int16)
+    real_imag[0::2] = numpy.round(real)
+    real_imag[1::2] = numpy.round(imag)
+
+    # Cast each (real, imag) pair into an uint32, which brings the array size
+    # back to the original.
+    weights = real_imag.view(numpy.uint32)
+
+    return weights.reshape(orig_shape)
+
+def weight_to_complex(weight: numpy.uint32, unit: int) -> complex:
+    """Unpack an FPGA weight (uint32) into a complex number.
+
+       unit: the weight value representing a weight of 1.0."""
+
+    # A weight is a (real, imag) pair, stored as int16 packed into an uint32 value
+
+    # obtain the upper 16 bits, but make sure we interpret it as c_short to get the correct sign
+    imag = c_short(weight >> 16).value
+    # isolate the lower 16 bits, and interpret it as c_short to get the correct sign
+    real = c_short(weight & 0xFFFF).value
+
+    return (real + 1j * imag) / unit
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
index ded37fe8b296445aa3a7a0a82a44b1ce41a5742a..677b82e0f3fd9e241ab12c32433ad7d45a44dcde 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
@@ -18,6 +18,7 @@ from tango import AttrWriteType
 # Additional import
 from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper
 from tangostationcontrol.devices.opcua_device import opcua_device
+from tangostationcontrol.devices.sdp.common import phases_to_weights, subband_frequency
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.common.constants import S_pn, N_pn, CLK_200_MHZ, CLK_160_MHZ, N_subband_res, N_subbands, DEFAULT_SUBBAND, N_beamsets_ctrl, DEFAULT_POLLING_PERIOD
@@ -318,6 +319,28 @@ class SDP(opcua_device):
     # Commands
     # --------
 
+    # --------
+    # Support functions
+    # --------
+
+    # A weight representing no scaling in FPGA_subband_weights_R(W)
+    SUBBAND_UNIT_WEIGHT = 2**13
+
+    @staticmethod
+    def subband_weights(delay_seconds: float, phase_offset: float, amplitude_scaling: float, clock: int, nyquist_zone: int) -> numpy.ndarray:
+        """ Compute a FPGA_subband_weights_RW row for all subbands for a single input. """
+
+        subband_phases = numpy.zeros((N_subbands,), dtype=numpy.float64)
+
+        for subband_nr in range(N_subbands):
+            frequency = subband_frequency(subband_nr, clock, nyquist_zone)
+
+            # correct for the delay by rotating the signal *back*
+            subband_phases[subband_nr] = (-2.0 * numpy.pi) * frequency * delay_seconds + phase_offset
+
+        # convert phases to their complex equivalent, as FPGA weights (cint16 packed into uint32)
+        return phases_to_weights(subband_phases, SDP.SUBBAND_UNIT_WEIGHT, numpy.array([amplitude_scaling] * N_subbands))
+
 # ----------
 # Run server
 # ----------
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_tcp_replicator.py b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_tcp_replicator.py
index faab4d9c3e9cd0a8a25d5c3e8900c481b5e8bbdf..c63a8ec5d41a0f325cab3db5548a6d018c99cb1f 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_tcp_replicator.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_tcp_replicator.py
@@ -15,6 +15,7 @@ import sys
 import timeout_decorator
 
 from tangostationcontrol.clients.tcp_replicator import TCPReplicator
+from tangostationcontrol.common.constants import MAX_ETH_FRAME_SIZE
 
 from tangostationcontrol.integration_test import base
 
@@ -78,7 +79,7 @@ class TestTCPReplicator(base.IntegrationTestCase):
 
         replicator.join()
 
-        self.assertEqual(b'', s.recv(9000))
+        self.assertEqual(b'', s.recv(MAX_ETH_FRAME_SIZE))
 
     @timeout_decorator.timeout(15)
     def test_start_connect_receive(self):
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
index a38fa52aa1814501d2eaa429e258cf6d960c05d8..7a61a457e941da98c3695e91573a5a4424102db3 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
@@ -33,10 +33,20 @@ class AbstractTestBases:
             # make sure the device starts in Off
             self.proxy.Off()
 
+            # make a backup of the properties, in case they're changed
+            # NB: "or {}" is needed to deal with devices that have no properties.
+            self.original_properties = self.proxy.get_property(self.proxy.get_property_list("*") or {}) or {}
+
             self.addCleanup(TestDeviceProxy.test_device_turn_off, self.name)
+            self.addCleanup(self.restore_properties)
 
             super().setUp()
 
+        def restore_properties(self):
+            """Restore the properties as they were before the test."""
+
+            self.proxy.put_property(self.original_properties)
+
         def test_device_fetch_state(self):
             """Test if we can successfully fetch state"""
 
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py
index 24e2c1191425cf80389490eeb34fcc2ec45414e5..14002bca5b81ccfec845bf3291b986c6c8fd34da 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py
@@ -12,8 +12,10 @@ import numpy
 
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
+from tangostationcontrol.devices.sdp.common import weight_to_complex
+from tangostationcontrol.devices.sdp.sdp import SDP
 from .base import AbstractTestBases
-
+from tangostationcontrol.common.constants import N_elements, MAX_ANTENNA, N_pol, N_rcu, N_rcu_inp, DEFAULT_N_HBA_TILES, CLK_200_MHZ, N_pn, S_pn, N_subbands
 
 class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
@@ -21,20 +23,24 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         super().setUp("STAT/AntennaField/1")
         self.proxy.put_property({
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [1, 1, 1, 0] + [-1] * 92
+            "Power_to_RECV_mapping": [1, 1, 1, 0] + [-1] * ((DEFAULT_N_HBA_TILES * 2) - 4)
         })
         self.recv_proxy = self.setup_recv_proxy()
         self.sdp_proxy = self.setup_sdp_proxy()
 
-        self.addCleanup(self.restore_antennafield)
         self.addCleanup(self.shutdown_recv)
         self.addCleanup(self.shutdown_sdp)
 
+        # configure the frequencies, which allows access
+        # to the calibration attributes and commands
+        self.sdp_proxy.clock_RW = CLK_200_MHZ
+        self.recv_proxy.RCU_band_select_RW = [[1] * N_rcu_inp] * N_rcu
+
     def restore_antennafield(self):
         self.proxy.put_property({
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
-            "Control_to_RECV_mapping":  [-1, -1] * 48
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
+            "Control_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES
         })
 
     @staticmethod
@@ -71,20 +77,20 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         antennafield_proxy = self.proxy
         numpy.testing.assert_equal(
-            numpy.array([True] * 96), self.recv_proxy.ANT_mask_RW
+            numpy.array([True] * MAX_ANTENNA), self.recv_proxy.ANT_mask_RW
         )
 
-        antenna_qualities = numpy.array([AntennaQuality.OK] * 96)
-        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * 95)
+        antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
+        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * (MAX_ANTENNA - 1))
         antenna_properties = {
             'Antenna_Quality': antenna_qualities, 'Antenna_Use': antenna_use
         }
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
             # Two inputs of recv device connected, only defined for 48 inputs
             # each pair is one input
-            "Control_to_RECV_mapping":  [1, 0 , 1, 1] + [-1, -1] * 46
+            "Control_to_RECV_mapping":  [1, 0 , 1, 1] + [-1, -1] * (DEFAULT_N_HBA_TILES - 2)
         }
         antennafield_proxy.off()
         antennafield_proxy.put_property(antenna_properties)
@@ -93,19 +99,19 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         # Verify all antennas are indicated to work
         numpy.testing.assert_equal(
-            numpy.array([True] * 96), antennafield_proxy.Antenna_Usage_Mask_R
+            numpy.array([True] * MAX_ANTENNA), antennafield_proxy.Antenna_Usage_Mask_R
         )
 
         # Verify only connected inputs + Antenna_Usage_Mask_R are true
         # As well as dimensions of ANT_mask_RW must match control mapping
         numpy.testing.assert_equal(
-            numpy.array([True] * 2 + [False] * 46),
+            numpy.array([True] * 2 + [False] * (DEFAULT_N_HBA_TILES - 2)),
             antennafield_proxy.ANT_mask_RW
         )
 
         # Verify recv proxy values unaffected as default for ANT_mask_RW is true
         numpy.testing.assert_equal(
-            numpy.array([True] * 2 + [True] * 94),
+            numpy.array([True] * 2 + [True] * (MAX_ANTENNA - 2)),
             self.recv_proxy.ANT_mask_RW
         )
 
@@ -115,8 +121,8 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         antennafield_proxy = self.proxy
 
         # Broken antennas except second
-        antenna_qualities = numpy.array([AntennaQuality.BROKEN] + [AntennaQuality.OK] + [AntennaQuality.BROKEN] * 94)
-        antenna_use = numpy.array([AntennaUse.AUTO] * 96)
+        antenna_qualities = numpy.array([AntennaQuality.BROKEN] + [AntennaQuality.OK] + [AntennaQuality.BROKEN] * (MAX_ANTENNA - 2))
+        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
         antenna_properties = {
             'Antenna_Quality': antenna_qualities, 'Antenna_Use': antenna_use
         }
@@ -124,10 +130,10 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         # Configure control mapping to control all 96 inputs of recv device
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
             "Control_to_RECV_mapping":
                 # [1, 0,  1, 1,  1, 2,  1, x  ...  1, 95]
-                numpy.array([[1, x] for x in range(0, 96)]).flatten()
+                numpy.array([[1, x] for x in range(0, MAX_ANTENNA)]).flatten()
         }
 
         # Cycle device and set properties
@@ -138,18 +144,18 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         # Antenna_Usage_Mask_R should be false except one
         numpy.testing.assert_equal(
-            numpy.array([False] + [True] + [False] * 94),
+            numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)),
             antennafield_proxy.Antenna_Usage_Mask_R
         )
         # device.boot() writes Antenna_Usage_Mask_R to ANT_mask_RW
         numpy.testing.assert_equal(
-            numpy.array([False] + [True] + [False] * 94),
+            numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)),
             antennafield_proxy.ANT_mask_RW
         )
         # ANT_mask_RW on antennafield writes to configured recv devices for all
         # mapped inputs
         numpy.testing.assert_equal(
-            numpy.array([False] + [True] + [False] * 94),
+            numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)),
             self.recv_proxy.ANT_mask_RW
         )
 
@@ -159,8 +165,8 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         # Connect recv/1 device but no control inputs
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
-            "Control_to_RECV_mapping": [-1, -1] * 48
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
+            "Control_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES
         }
 
         # Cycle device an put properties
@@ -170,11 +176,11 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         antennafield_proxy.boot()
 
         # Set HBAT_PWR_on_RW to false on recv device and read results
-        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * 32] * 96)
+        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA)
         current_values = self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
 
         # write true through antennafield
-        antennafield_proxy.write_attribute("HBAT_PWR_on_RW", [[True] * 32] * 48)
+        antennafield_proxy.write_attribute("HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * DEFAULT_N_HBA_TILES)
         # Test that original recv values for HBAT_PWR_on_RW match current
         numpy.testing.assert_equal(
             current_values,
@@ -189,9 +195,9 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
             # Each pair is one mapping so 2 inputs are connected
-            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * 46
+            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * (DEFAULT_N_HBA_TILES - 2)
         }
 
         antennafield_proxy = self.proxy
@@ -199,20 +205,20 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         antennafield_proxy.put_property(mapping_properties)
         antennafield_proxy.boot()
 
-        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * 32] * 96)
+        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA)
 
         try:
             antennafield_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[True] * 32] * 48
+                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * DEFAULT_N_HBA_TILES
             )
             numpy.testing.assert_equal(
-                numpy.array([[True] * 32] * 2 + [[False] * 32] * 94),
+                numpy.array([[True] * N_elements * N_pol] * 2 + [[False] * N_elements * N_pol] * (MAX_ANTENNA - 2)),
                 self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
             )
         finally:
             # Always restore recv again
             self.recv_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[False] * 32] * 96
+                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
             )
 
         # Verify device did not enter FAULT state
@@ -223,10 +229,10 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
             "Control_to_RECV_mapping":
                 # [1, 0, 1, 1, 1, 2, 1, x ... 1, 95]
-                numpy.array([[1, x] for x in range(0, 96)]).flatten()
+                numpy.array([[1, x] for x in range(0, MAX_ANTENNA)]).flatten()
         }
 
         antennafield_proxy = self.proxy
@@ -234,20 +240,20 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         antennafield_proxy.put_property(mapping_properties)
         antennafield_proxy.boot()
 
-        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * 32] * 96)
+        self.recv_proxy.write_attribute("HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA)
 
         try:
             antennafield_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[True] * 32] * 96
+                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * MAX_ANTENNA
             )
             numpy.testing.assert_equal(
-                numpy.array([[True] * 32] * 96),
+                numpy.array([[True] * N_elements * N_pol] * MAX_ANTENNA),
                 self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
             )
         finally:
             # Always restore recv again
             self.recv_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[False] * 32] * 96
+                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
             )
 
         # Verify device did not enter FAULT state
@@ -258,10 +264,10 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
 
         mapping_properties = {
             "RECV_devices": ["STAT/RECV/1"],
-            "Power_to_RECV_mapping": [-1, -1] * 48,
+            "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
             "Control_to_RECV_mapping":
                 # [1, 0, 1, 1, 1, 2, 1, x ... 1, 95]
-                numpy.array([[1, x] for x in range(0, 96)]).flatten()
+                numpy.array([[1, x] for x in range(0, MAX_ANTENNA)]).flatten()
         }
 
         antennafield_proxy = self.proxy
@@ -269,21 +275,89 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
         antennafield_proxy.put_property(mapping_properties)
         antennafield_proxy.boot()
 
-        self.recv_proxy.write_attribute("RCU_band_select_RW", [[False] * 3] * 32)
+        self.recv_proxy.write_attribute("RCU_band_select_RW", [[False] * N_rcu_inp] * N_rcu)
 
         try:
             antennafield_proxy.write_attribute(
-                "RCU_band_select_RW", [True] * 96
+                "RCU_band_select_RW", [True] * MAX_ANTENNA
             )
             numpy.testing.assert_equal(
-                numpy.array([[True] * 3] * 32),
+                numpy.array([[True] * N_rcu_inp] * N_rcu),
                 self.recv_proxy.read_attribute("RCU_band_select_RW").value
             )
         finally:
             # Always restore recv again
             self.recv_proxy.write_attribute(
-                "RCU_band_select_RW", [[False] * 3] * 32
+                "RCU_band_select_RW", [[False] * N_rcu_inp] * N_rcu
             )
 
         # Verify device did not enter FAULT state
         self.assertEqual(DevState.ON, antennafield_proxy.state())
+
+    def test_calibrate_recv(self):
+        calibration_properties = {
+            "Antenna_Type": ["LBA"],
+            "Antenna_Cables": ["50m","80m"] * (DEFAULT_N_HBA_TILES // 2),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(calibration_properties)
+        antennafield_proxy.boot()
+
+        # calibrate
+        antennafield_proxy.calibrate_recv()
+
+        # check the results
+        rcu_attenuator_db = antennafield_proxy.RCU_attenuator_dB_RW
+
+        # values should be the same for the same cable length
+        self.assertEqual(1, len(set(rcu_attenuator_db[0::2])), msg=f"rcu_attenuator_db={rcu_attenuator_db}")
+        self.assertEqual(1, len(set(rcu_attenuator_db[1::2])), msg=f"rcu_attenuator_db={rcu_attenuator_db}")
+        # value should be larger for the shorter cable, as those signals need damping
+        self.assertGreater(rcu_attenuator_db[0], rcu_attenuator_db[1])
+        # longest cable should require no damping
+        self.assertEqual(0, rcu_attenuator_db[1])
+
+    def test_calibrate_sdp(self):
+        calibration_properties = {
+            "Antenna_Type": ["LBA"],
+            "Antenna_Cables": ["50m","80m"] * (DEFAULT_N_HBA_TILES // 2),
+            "Antenna_to_SDP_Mapping":  [0, 1, 0, 0] + [-1, -1] * (DEFAULT_N_HBA_TILES - 2),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(calibration_properties)
+        antennafield_proxy.boot()
+
+        # calibrate
+        antennafield_proxy.calibrate_sdp()
+
+        # check the results
+        # antenna #0 is on FPGA 0, input 2 and 3,
+        # antenna #1 is on FPGA 0, input 0 and 1
+        signal_input_samples_delay = self.sdp_proxy.FPGA_signal_input_samples_delay_RW
+
+        # delays should be equal for both polarisations
+        self.assertEqual(signal_input_samples_delay[0,0], signal_input_samples_delay[0,1])
+        self.assertEqual(signal_input_samples_delay[0,2], signal_input_samples_delay[0,3])
+
+        # antenna #0 is shorter, so should have a greater delay
+        self.assertGreater(signal_input_samples_delay[0,2], signal_input_samples_delay[0,0], msg=f"{signal_input_samples_delay}")
+        # antenna #1 is longest, so should have delay 0
+        self.assertEqual(0, signal_input_samples_delay[0,0])
+
+        # the subband weights depend on the frequency of the subband,
+        # and on the exact delay and loss differences between the cables.
+        # rather than repeating the computations from the code,
+        # we implement this as a regression test.
+        subband_weights = self.sdp_proxy.FPGA_subband_weights_RW.reshape(N_pn, S_pn, N_subbands)
+
+        def to_complex(weight):
+            return weight_to_complex(weight, SDP.SUBBAND_UNIT_WEIGHT)
+
+        self.assertAlmostEqual(0.929 + 0j,     to_complex(subband_weights[0, 0,   0]), places=3)
+        self.assertAlmostEqual(0.309 + 0.876j, to_complex(subband_weights[0, 0, 511]), places=3)
+        self.assertAlmostEqual(0.989 + 0j,     to_complex(subband_weights[0, 1,   0]), places=3)
+        self.assertAlmostEqual(0.883 - 0.444j, to_complex(subband_weights[0, 1, 511]), places=3)
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
index 6aa67918d4b76867109234478c281ad68789da08..d16cbb9abbdd75f443ac9e89367b00cf2e7ae082 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
@@ -10,6 +10,7 @@
 from .base import AbstractTestBases
 
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
+from tangostationcontrol.common.constants import N_beamlets_ctrl, S_pn, CLK_200_MHZ, CLK_160_MHZ, MAX_INPUTS, N_pn
 
 from tango import DevState
 
@@ -30,7 +31,7 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
 
         super().test_device_read_all_attributes()
 
-    def setup_sdp(self, antenna_type="HBA", clock=200_000_000):
+    def setup_sdp(self, antenna_type="HBA", clock=CLK_200_MHZ):
         # setup SDP, on which this device depends
         sdp_proxy = TestDeviceProxy("STAT/SDP/1")
         sdp_proxy.off()
@@ -38,7 +39,7 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
         sdp_proxy.set_defaults()
 
         # setup the frequencies as expected in the test
-        sdp_proxy.antenna_type_RW = [[antenna_type] * 12] * 16
+        sdp_proxy.antenna_type_RW = [[antenna_type] * S_pn] * N_pn
         sdp_proxy.clock_RW = clock
 
         return sdp_proxy
@@ -53,14 +54,14 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
         # The subband frequency of HBA subband 0 is 200 MHz,
         # so its period is 5 ns. A delay of 2.5e-9 seconds is thus half a period,
         # and result in a 180 degree phase offset.
-        delays = numpy.array([[2.5e-9] * 192] * 488)
+        delays = numpy.array([[2.5e-9] * MAX_INPUTS] * N_beamlets_ctrl)
 
         calculated_bf_weights = self.proxy.calculate_bf_weights(delays.flatten())
 
         # With a unit weight of 2**14, we thus expect beamformer weights of -2**14 + 0j,
         # which is 49152 when read as an uint32.
         self.assertEqual(-2**14, c_short(49152).value) # check our calculations
-        expected_bf_weights = numpy.array([49152] * 192 * 488, dtype=numpy.uint32)
+        expected_bf_weights = numpy.array([49152] * MAX_INPUTS * N_beamlets_ctrl, dtype=numpy.uint32)
 
         numpy.testing.assert_almost_equal(expected_bf_weights, calculated_bf_weights)
     
@@ -72,17 +73,17 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
         self.proxy.off()
         self.proxy.initialise()
         self.assertEqual(DevState.STANDBY, self.proxy.state())
-        self.proxy.subband_select_RW = [10] * 488
+        self.proxy.subband_select_RW = [10] * N_beamlets_ctrl
         self.proxy.on()
         self.assertEqual(DevState.ON, self.proxy.state())
 
         # The subband frequency of HBA subband 10 is 201953125 Hz
         # so its period is 4.95 ns ca, half period is 2.4758e-9
-        delays = numpy.array([[2.4758e-9] * 192] * 488)
+        delays = numpy.array([[2.4758e-9] * MAX_INPUTS] * N_beamlets_ctrl)
         calculated_bf_weights_subband_10 = self.proxy.calculate_bf_weights(delays.flatten())
         
         self.assertEqual(-2**14, c_short(49152).value) # check our calculations
-        expected_bf_weights_10 = numpy.array([49152] * 192 * 488, dtype=numpy.uint32)
+        expected_bf_weights_10 = numpy.array([49152] * MAX_INPUTS * N_beamlets_ctrl, dtype=numpy.uint32)
         numpy.testing.assert_almost_equal(expected_bf_weights_10, calculated_bf_weights_subband_10)
 
     def test_sdp_clock_change(self):
@@ -90,21 +91,21 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
         sdp_proxy = self.setup_sdp()
 
         self.proxy.initialise()
-        self.proxy.subband_select_RW = numpy.array(list(range(317)) + [316] + list(range(318,488)), dtype=numpy.uint32)
+        self.proxy.subband_select_RW = numpy.array(list(range(317)) + [316] + list(range(318,N_beamlets_ctrl)), dtype=numpy.uint32)
         self.proxy.on()
 
         # any non-zero delay should result in different weights for different clocks
-        delays = numpy.array([[2.5e-9] * 192] * 488)
+        delays = numpy.array([[2.5e-9] * MAX_INPUTS] * N_beamlets_ctrl)
 
-        sdp_proxy.clock_RW = 200 * 1000000
+        sdp_proxy.clock_RW = CLK_200_MHZ
         time.sleep(1) # wait for beamlet device to process change event
         calculated_bf_weights_200 = self.proxy.calculate_bf_weights(delays.flatten())
 
-        sdp_proxy.clock_RW = 160 * 1000000
+        sdp_proxy.clock_RW = CLK_160_MHZ
         time.sleep(1) # wait for beamlet device to process change event
         calculated_bf_weights_160 = self.proxy.calculate_bf_weights(delays.flatten())
 
-        sdp_proxy.clock_RW = 200 * 1000000
+        sdp_proxy.clock_RW = CLK_200_MHZ
         time.sleep(1) # wait for beamlet device to process change event
         calculated_bf_weights_200_v2 = self.proxy.calculate_bf_weights(delays.flatten())
 
@@ -115,12 +116,12 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
         # change subbands
         self.proxy.off()
         self.proxy.initialise()
-        self.proxy.subband_select_RW = [317] * 488
+        self.proxy.subband_select_RW = [317] * N_beamlets_ctrl
         self.proxy.on()
         calculated_bf_weights_200_v3 = self.proxy.calculate_bf_weights(delays.flatten())
         self.assertTrue((calculated_bf_weights_200_v2 != calculated_bf_weights_200_v3).all())
 
-        sdp_proxy.clock_RW = 160 * 1000000
+        sdp_proxy.clock_RW = CLK_160_MHZ
         time.sleep(1) # wait for beamlet device to process change event
         calculated_bf_weights_160_v2 = self.proxy.calculate_bf_weights(delays.flatten())
         self.assertTrue((calculated_bf_weights_160 != calculated_bf_weights_160_v2).all())
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_boot.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_boot.py
index e9fb0f16e30cedd18a2dc0dc4df2f09fcb0ab213..7bd56cba750ac29bce04b45f01872b15671ff2a3 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_boot.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_boot.py
@@ -13,6 +13,7 @@ from tango import DevState
 
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.integration_test import base
+from tangostationcontrol.common.constants import DEFAULT_POLLING_PERIOD
 
 
 class TestDeviceBoot(base.IntegrationTestCase):
@@ -29,7 +30,7 @@ class TestDeviceBoot(base.IntegrationTestCase):
         """Test if we can reinitialise the station"""
 
         # This attribute needs to be polled for the TemperatureManager test to succesfully initialise
-        TestDeviceProxy("STAT/RECV/1").poll_attribute("HBAT_LED_on_RW", 1000)
+        TestDeviceProxy("STAT/RECV/1").poll_attribute("HBAT_LED_on_RW", DEFAULT_POLLING_PERIOD)
 
         self.proxy.reboot()
 
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
index 9a7d22306e6aa842e8976a37c7a1d2b8e1923c1e..c83722787db23f90b91c90306ee474a1784d3b75 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
@@ -10,6 +10,7 @@
 
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
+from tangostationcontrol.common.constants import MAX_ANTENNA, N_beamlets_ctrl, N_pn, CLK_200_MHZ, CLK_160_MHZ, DEFAULT_N_HBA_TILES
 
 from .base import AbstractTestBases
 
@@ -18,9 +19,9 @@ import time
 
 class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
 
-    antenna_qualities_ok = numpy.array([AntennaQuality.OK] * 96)
-    antenna_qualities_only_second = numpy.array([AntennaQuality.BROKEN] + [AntennaQuality.OK] + [AntennaQuality.BROKEN] * 94)
-    antenna_use_ok = numpy.array([AntennaUse.AUTO] * 96)
+    antenna_qualities_ok = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
+    antenna_qualities_only_second = numpy.array([AntennaQuality.BROKEN] + [AntennaQuality.OK] + [AntennaQuality.BROKEN] * (MAX_ANTENNA - 2))
+    antenna_use_ok = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
 
     antennafield_iden = "STAT/AntennaField/1"
     beamlet_iden = "STAT/Beamlet/1"
@@ -67,7 +68,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
 
     def setup_antennafield_proxy(self, antenna_qualities, antenna_use):
         # setup AntennaField
-        NR_TILES = 48
+        NR_TILES = DEFAULT_N_HBA_TILES
         antennafield_proxy = TestDeviceProxy(self.antennafield_iden)
         control_mapping = [[1,i] for i in range(NR_TILES)]
         antennafield_proxy.put_property({
@@ -94,7 +95,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.beamlet_proxy.on()
 
         # Set first (default) clock configuration
-        self.sdp_proxy.clock_RW = 200 * 1000000
+        self.sdp_proxy.clock_RW = CLK_200_MHZ
         time.sleep(1)
 
         self.proxy.initialise()
@@ -102,7 +103,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.on()
 
         # Point to Zenith
-        self.proxy.set_pointing(numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten())
+        self.proxy.set_pointing(numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten())
 
         # beam weights should now be non-zero, we don't actually check their values for correctness
         FPGA_bf_weights_xx_yy_clock200 = self.beamlet_proxy.FPGA_bf_weights_xx_yy_RW.flatten()
@@ -112,7 +113,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.beamlet_proxy.on()
 
         # Change clock configuration
-        self.sdp_proxy.clock_RW = 160 * 1000000
+        self.sdp_proxy.clock_RW = CLK_160_MHZ
         time.sleep(1)
 
         FPGA_bf_weights_xx_yy_clock160 = self.beamlet_proxy.FPGA_bf_weights_xx_yy_RW.flatten()
@@ -130,7 +131,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok)
 
         self.beamlet_proxy = self.initialise_beamlet_proxy()
-        self.beamlet_proxy.subband_select_RW = numpy.array(list(range(317)) + [316] + list(range(318,488)), dtype=numpy.uint32)
+        self.beamlet_proxy.subband_select_RW = numpy.array(list(range(317)) + [316] + list(range(318,N_beamlets_ctrl)), dtype=numpy.uint32)
         self.beamlet_proxy.on()
 
         self.proxy.initialise()
@@ -138,13 +139,13 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.on()
 
         # Point to Zenith
-        self.proxy.set_pointing(numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten())
+        self.proxy.set_pointing(numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten())
         # Store values with first subband configuration
         FPGA_bf_weights_xx_yy_subband_v1 = self.beamlet_proxy.FPGA_bf_weights_xx_yy_RW.flatten()
 
         # Restart beamlet proxy
         self.beamlet_proxy = self.initialise_beamlet_proxy()
-        self.beamlet_proxy.subband_select_RW = [317] * 488
+        self.beamlet_proxy.subband_select_RW = [317] * N_beamlets_ctrl
         self.beamlet_proxy.on()
 
         # Store values with second subband configuration
@@ -167,14 +168,14 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.Tracking_enabled_RW = False
         self.proxy.on()
 
-        all_zeros = numpy.array([[0] * 5856] * 16)
+        all_zeros = numpy.array([[0] * 5856] * N_pn)
         self.beamlet_proxy.FPGA_bf_weights_xx_yy_RW = all_zeros
 
         # Enable all inputs
-        self.proxy.input_select_RW = numpy.array([[True] * 488] * 96)
+        self.proxy.input_select_RW = numpy.array([[True] * N_beamlets_ctrl] * MAX_ANTENNA)
 
         self.proxy.set_pointing(
-            numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten()
+            numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten()
         )
 
         # Verify all zeros are replaced with other values for all inputs
@@ -197,14 +198,14 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.Tracking_enabled_RW = False
         self.proxy.on()
 
-        non_zeros = numpy.array([[16] * 5856] * 16)
+        non_zeros = numpy.array([[N_pn] * 5856] * N_pn)
         self.beamlet_proxy.FPGA_bf_weights_xx_yy_RW = non_zeros
 
         # Disable all inputs
-        self.proxy.input_select_RW = numpy.array([[False] * 488] * 96)
+        self.proxy.input_select_RW = numpy.array([[False] * N_beamlets_ctrl] * MAX_ANTENNA)
 
         self.proxy.set_pointing(
-            numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten()
+            numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten()
         )
 
         # Verify all zeros are replaced with other values for all inputs
@@ -220,12 +221,12 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         )
 
         antennafield_proxy = self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok)
-        numpy.testing.assert_equal(numpy.array([True] * 96), antennafield_proxy.Antenna_Usage_Mask_R)
+        numpy.testing.assert_equal(numpy.array([True] * MAX_ANTENNA), antennafield_proxy.Antenna_Usage_Mask_R)
         self.setUp()
         self.proxy.warm_boot()
-        expected_input_select = numpy.array([[True] * 488 ] * 48 + [[False] * 488] * 48)    # first 48 rows are True
+        expected_input_select = numpy.array([[True] * N_beamlets_ctrl ] * DEFAULT_N_HBA_TILES + [[False] * N_beamlets_ctrl] * DEFAULT_N_HBA_TILES)    # first 48 rows are True
         numpy.testing.assert_equal(expected_input_select, self.proxy.input_select_RW)
-        expected_antenna_select = numpy.array([[True] * 488 ] * 48)
+        expected_antenna_select = numpy.array([[True] * N_beamlets_ctrl ] * DEFAULT_N_HBA_TILES)
         numpy.testing.assert_equal(expected_antenna_select, self.proxy.antenna_select_RW)
 
     def test_input_select_with_only_second_antenna_ok(self):
@@ -236,10 +237,10 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
         )
 
         antennafield_proxy = self.setup_antennafield_proxy(self.antenna_qualities_only_second, self.antenna_use_ok)
-        numpy.testing.assert_equal(numpy.array([False] + [True] + [False] * 94), antennafield_proxy.Antenna_Usage_Mask_R)
+        numpy.testing.assert_equal(numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)), antennafield_proxy.Antenna_Usage_Mask_R)
         self.setUp()
         self.proxy.warm_boot()
-        expected_input_select = numpy.array([[False] * 488 ] + [[True] * 488] + [[False] * 488] * 46 + [[False] * 488] * 48)    # first 48 rows are True
+        expected_input_select = numpy.array([[False] * N_beamlets_ctrl ] + [[True] * N_beamlets_ctrl] + [[False] * N_beamlets_ctrl] * (DEFAULT_N_HBA_TILES - 2) + [[False] * N_beamlets_ctrl] * DEFAULT_N_HBA_TILES)    # first 48 rows are True
         numpy.testing.assert_equal(expected_input_select, self.proxy.input_select_RW)
-        expected_antenna_select = numpy.array([[False] * 488 ] + [[True] * 488] + [[False] * 488] * 46)
+        expected_antenna_select = numpy.array([[False] * N_beamlets_ctrl ] + [[True] * N_beamlets_ctrl] + [[False] * N_beamlets_ctrl] * (DEFAULT_N_HBA_TILES - 2))
         numpy.testing.assert_equal(expected_antenna_select, self.proxy.antenna_select_RW)
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py
index 46c77246eb5fd876079998c341bc7b4693d48ec3..32b4f5841ffbf76e10a85745e1c7ae518c798acc 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py
@@ -15,13 +15,11 @@ from tango import DevState, DevFailed
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.test.devices.test_observation_base import TestObservationBase
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
+from tangostationcontrol.common.constants import N_beamlets_ctrl,  MAX_ANTENNA, DEFAULT_N_HBA_TILES
 from .base import AbstractTestBases
 
 class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
 
-    NUM_TILES = 48
-    NUM_BEAMLETS_CTRL = 488
-    NUM_INPUTS = 96
     ANTENNA_TO_SDP_MAPPING = [
       "0", "0", "0", "1", "0", "2", "0", "3", "0", "4", "0", "5",
       "1", "0", "1", "1", "1", "2", "1", "3", "1", "4", "1", "5",
@@ -61,9 +59,9 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
     def setup_antennafield_proxy(self):
         # setup AntennaField
         antennafield_proxy = TestDeviceProxy("STAT/AntennaField/1")
-        control_mapping = [[1,i] for i in range(self.NUM_TILES)]
-        antenna_qualities = numpy.array([AntennaQuality.OK] * 96)
-        antenna_use = numpy.array([AntennaUse.AUTO] * 96)
+        control_mapping = [[1,i] for i in range(DEFAULT_N_HBA_TILES)]
+        antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
+        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
         antennafield_proxy.put_property({"RECV_devices": ["STAT/RECV/1"],
                                  "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
                                  "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING,
@@ -94,7 +92,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
         tilebeam_proxy.off()
         tilebeam_proxy.warm_boot()
         tilebeam_proxy.set_defaults()
-        return tilebeam_proxy     
+        return tilebeam_proxy
 
     def test_init_valid(self):
         """Initialize an observation with valid JSON"""
@@ -158,10 +156,10 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
         """Test that attributes antenna_mask and filter are correctly applied"""
         self.setup_recv_proxy()
         antennafield_proxy = self.setup_antennafield_proxy()
-        antennafield_proxy.ANT_mask_RW = [True] * 48 # set all masks to True
-        self.assertListEqual(antennafield_proxy.ANT_mask_RW.tolist(), [True] * 48)
-        antennafield_proxy.RCU_band_select_RW = [0] * 48
-        self.assertListEqual(antennafield_proxy.RCU_band_select_RW.tolist(), [0] * 48)
+        antennafield_proxy.ANT_mask_RW = [True] * DEFAULT_N_HBA_TILES # set all masks to True
+        self.assertListEqual(antennafield_proxy.ANT_mask_RW.tolist(), [True] * DEFAULT_N_HBA_TILES)
+        antennafield_proxy.RCU_band_select_RW = [0] * DEFAULT_N_HBA_TILES
+        self.assertListEqual(antennafield_proxy.RCU_band_select_RW.tolist(), [0] * DEFAULT_N_HBA_TILES)
         self.proxy.off()
         self.proxy.observation_settings_RW = self.VALID_JSON
         self.proxy.Initialise()
@@ -174,46 +172,46 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
     def test_apply_subbands(self):
         """Test that attribute sap subbands is correctly applied"""
         beamlet_proxy = self.setup_beamlet_proxy()
-        subband_select = [0] * self.NUM_BEAMLETS_CTRL
+        subband_select = [0] * N_beamlets_ctrl
         beamlet_proxy.subband_select_RW = subband_select
         self.assertListEqual(beamlet_proxy.subband_select_RW.tolist(), subband_select)
         self.proxy.off()
         self.proxy.observation_settings_RW = self.VALID_JSON
         self.proxy.Initialise()
         self.proxy.On()
-        expected_subbands = [10,20,30] + [0] * (self.NUM_BEAMLETS_CTRL-3)
+        expected_subbands = [10,20,30] + [0] * (N_beamlets_ctrl-3)
         self.assertListEqual(beamlet_proxy.subband_select_RW.tolist(), expected_subbands)
 
     def test_apply_pointing(self):
         """Test that attribute sap pointing is correctly applied"""
         digitalbeam_proxy = self.setup_digitalbeam_proxy()
-        default_pointing = [("AZELGEO","0deg","90deg")]*488
+        default_pointing = [("AZELGEO","0deg","90deg")]*N_beamlets_ctrl
         digitalbeam_proxy.Pointing_direction_RW = default_pointing
         self.assertListEqual(list(digitalbeam_proxy.Pointing_direction_RW), default_pointing)
         self.proxy.off()
         self.proxy.observation_settings_RW = self.VALID_JSON
         self.proxy.Initialise()
         self.proxy.On()
-        expected_pointing = [("J2000","1.5deg","0deg")] + [("AZELGEO","0deg","90deg")] * 487
+        expected_pointing = [("J2000","1.5deg","0deg")] + [("AZELGEO","0deg","90deg")] * (N_beamlets_ctrl - 1)
         self.assertListEqual(list(digitalbeam_proxy.Pointing_direction_RW), expected_pointing)
 
     def test_apply_antenna_select(self):
         """Test that antenna selection is correctly applied"""
         digitalbeam_proxy = self.setup_digitalbeam_proxy()
-        default_selection = [[False] * self.NUM_BEAMLETS_CTRL] * self.NUM_INPUTS
+        default_selection = [[False] * N_beamlets_ctrl] * MAX_ANTENNA
         digitalbeam_proxy.antenna_select_RW = default_selection
         self.assertListEqual(digitalbeam_proxy.antenna_select_RW.tolist()[9], default_selection[9])
         self.proxy.off()
         self.proxy.observation_settings_RW = self.VALID_JSON
         self.proxy.Initialise()
         self.proxy.On()
-        self.assertListEqual(digitalbeam_proxy.antenna_select_RW.tolist()[9], [True] * self.NUM_BEAMLETS_CTRL)
-        self.assertListEqual(digitalbeam_proxy.antenna_select_RW.tolist()[10], [False] * self.NUM_BEAMLETS_CTRL)
+        self.assertListEqual(digitalbeam_proxy.antenna_select_RW.tolist()[9], [True] * N_beamlets_ctrl)
+        self.assertListEqual(digitalbeam_proxy.antenna_select_RW.tolist()[10], [False] * N_beamlets_ctrl)
 
     def test_apply_tilebeam(self):
         """Test that attribute tilebeam is correctly applied"""
         tilebeam_proxy = self.setup_tilebeam_proxy()
-        pointing_direction = [("J2000","0deg","0deg")] * self.NUM_TILES
+        pointing_direction = [("J2000","0deg","0deg")] * DEFAULT_N_HBA_TILES
         tilebeam_proxy.Pointing_direction_RW = pointing_direction
         self.assertListEqual(list(tilebeam_proxy.Pointing_direction_RW[0]), ["J2000","0deg","0deg"])
         self.proxy.off()
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
index ac325e0cbcc9f5dd663ce21c104461516819a9cf..1e6c7d72a41b0c10ff66088c664048366a652647 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
@@ -19,11 +19,10 @@ from tangostationcontrol.test.devices.test_observation_base import TestObservati
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from .base import AbstractTestBases
 
+from tangostationcontrol.common.constants import DEFAULT_N_HBA_TILES
+
 class TestObservationControlDevice(AbstractTestBases.TestDeviceBase):
 
-    NUM_TILES = 48
-    NUM_BEAMLETS_CTRL = 488
-    NUM_INPUTS = 96
     ANTENNA_TO_SDP_MAPPING = [
       "0", "0", "0", "1", "0", "2", "0", "3", "0", "4", "0", "5",
       "1", "0", "1", "1", "1", "2", "1", "3", "1", "4", "1", "5",
@@ -63,7 +62,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase):
     def setup_antennafield_proxy(self):
         # setup AntennaField
         antennafield_proxy = TestDeviceProxy("STAT/AntennaField/1")
-        control_mapping = [[1,i] for i in range(self.NUM_TILES)]
+        control_mapping = [[1,i] for i in range(DEFAULT_N_HBA_TILES)]
         antennafield_proxy.put_property({"RECV_devices": ["STAT/RECV/1"],
                                  "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(),
                                  "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING})
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 570d8ce6cee1bc2f1ab833c9d93acdd11cc270f7..12b1d031b83b1aca9c4e6fc5dd163fe5cab9613f 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
@@ -8,7 +8,7 @@
 # See LICENSE.txt for more info.
 
 from .base import AbstractTestBases
-
+from tangostationcontrol.common.constants import N_pn
 
 class TestDeviceSDP(AbstractTestBases.TestDeviceBase):
 
@@ -21,4 +21,4 @@ class TestDeviceSDP(AbstractTestBases.TestDeviceBase):
 
         self.proxy.warm_boot()
 
-        self.assertListEqual([True]*16, list(self.proxy.TR_fpga_communication_error_R))
+        self.assertListEqual([True]*N_pn, list(self.proxy.TR_fpga_communication_error_R))
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py
index 44dcfdcf1e02afb14ac80de0d79416a46144fc8e..27fb3f8103c6b8f1f26a6cef5cca254023471ba0 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py
@@ -11,6 +11,8 @@ from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tango._tango import DevState
 from tango import DeviceProxy
 
+from tangostationcontrol.common.constants import DEFAULT_POLLING_PERIOD, N_elements, N_pol, N_rcu, N_rcu_inp
+
 import time
 
 import logging
@@ -33,7 +35,7 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase):
         recv_proxy.initialise()
         recv_proxy.on()
 
-        recv_proxy.poll_attribute("HBAT_LED_on_RW", 1000)
+        recv_proxy.poll_attribute("HBAT_LED_on_RW", DEFAULT_POLLING_PERIOD)
         self.assertTrue(recv_proxy.is_attribute_polled(f"HBAT_LED_on_RW"))
         return recv_proxy
 
@@ -69,12 +71,12 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase):
         self.assertEqual(self.proxy.get_property('Shutdown_Device_List')['Shutdown_Device_List'][0], "STAT/SDP/1")
 
         # Here we trigger our own change event by just using an RW attribute
-        self.recv_proxy.HBAT_LED_on_RW = [[False] * 32] * 96
+        self.recv_proxy.HBAT_LED_on_RW = [[False] * N_elements * N_pol] * N_rcu * N_rcu_inp
         time.sleep(2)
 
         self.assertFalse(self.proxy.is_alarming_R)
 
-        self.recv_proxy.HBAT_LED_on_RW = [[True] * 32] * 96
+        self.recv_proxy.HBAT_LED_on_RW = [[True] * N_elements * N_pol] * N_rcu * N_rcu_inp
         time.sleep(2)
 
         # the TEMP_MANAGER_is_alarming_R should now be True, since it should have detected the temperature alarm.
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
index c474e6628d695d4fd29eba7e0fed04e296477a8a..e88a5acfc881856d1248cd376ec9e9c810e16562 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
@@ -14,6 +14,7 @@ import json
 
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
+from tangostationcontrol.common.constants import DEFAULT_N_HBA_TILES, MAX_ANTENNA, N_elements, N_pol
 
 from .base import AbstractTestBases
 
@@ -26,10 +27,8 @@ class NumpyEncoder(json.JSONEncoder):
 
 
 class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
-    # The AntennaField is setup with self.NR_TILES tiles in the test configuration
-    NR_TILES = 48
 
-    POINTING_DIRECTION = numpy.array([["J2000","0deg","0deg"]] * NR_TILES).flatten()
+    POINTING_DIRECTION = numpy.array([["J2000","0deg","0deg"]] * DEFAULT_N_HBA_TILES).flatten()
 
     def setUp(self):
         super().setUp("STAT/TileBeam/1")
@@ -45,9 +44,9 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
     def setup_antennafield_proxy(self):
         # setup AntennaField
         antennafield_proxy = TestDeviceProxy("STAT/AntennaField/1")
-        control_mapping = [[1,i] for i in range(self.NR_TILES)]
-        antenna_qualities = numpy.array([AntennaQuality.OK] * 96)
-        antenna_use = numpy.array([AntennaUse.AUTO] * 96)
+        control_mapping = [[1,i] for i in range(DEFAULT_N_HBA_TILES)]
+        antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
+        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
         antennafield_proxy.put_property({"RECV_devices": ["STAT/RECV/1"],
                                  "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
                                  'Antenna_Quality': antenna_qualities, 'Antenna_Use': antenna_use})
@@ -55,7 +54,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         antennafield_proxy.boot()
 
         # check if AntennaField really exposes the expected number of tiles
-        self.assertEqual(self.NR_TILES, antennafield_proxy.nr_antennas_R)
+        self.assertEqual(DEFAULT_N_HBA_TILES, antennafield_proxy.nr_antennas_R)
         return antennafield_proxy
 
     def test_delays_dims(self):
@@ -68,7 +67,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
 
         # verify delays method returns the correct dimensions
         delays = self.proxy.delays(self.POINTING_DIRECTION)
-        self.assertEqual(self.NR_TILES*16, len(delays))
+        self.assertEqual(DEFAULT_N_HBA_TILES * N_elements, len(delays))
 
     def test_set_pointing(self):
         """Verify if set pointing procedure is correctly executed"""
@@ -102,11 +101,11 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.Tracking_enabled_RW = False
 
         # Point to Zenith
-        self.proxy.set_pointing(numpy.array([["AZELGEO","0deg","90deg"]] * self.NR_TILES).flatten())
+        self.proxy.set_pointing(numpy.array([["AZELGEO","0deg","90deg"]] * DEFAULT_N_HBA_TILES).flatten())
 
         calculated_HBAT_delay_steps = numpy.array(antennafield_proxy.read_attribute('HBAT_BF_delay_steps_RW').value)
 
-        expected_HBAT_delay_steps = numpy.array([[15] * 32] * self.NR_TILES, dtype=numpy.int64)
+        expected_HBAT_delay_steps = numpy.array([[15] * N_elements * N_pol] * DEFAULT_N_HBA_TILES, dtype=numpy.int64)
 
         numpy.testing.assert_equal(calculated_HBAT_delay_steps, expected_HBAT_delay_steps)
 
@@ -119,7 +118,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.Tracking_enabled_RW = False
 
         # point at north on the horizon
-        self.proxy.set_pointing(["AZELGEO","0deg","0deg"] * self.NR_TILES)
+        self.proxy.set_pointing(["AZELGEO","0deg","0deg"] * DEFAULT_N_HBA_TILES)
 
         # obtain delays of the X polarisation of all the elements of the first tile
         north_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_RW[0].reshape(4,4,2)[:,:,0]
@@ -129,7 +128,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
 
         for angle in (90,180,270):
             # point at angle degrees (90=E, 180=S, 270=W)
-            self.proxy.set_pointing(["AZELGEO",f"{angle}deg","0deg"] * self.NR_TILES)
+            self.proxy.set_pointing(["AZELGEO",f"{angle}deg","0deg"] * DEFAULT_N_HBA_TILES)
 
             # obtain delays of the X polarisation of all the elements of the first tile
             angled_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_RW[0].reshape(4,4,2)[:,:,0]
@@ -147,7 +146,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         self.proxy.Tracking_enabled_RW = False
 
         # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000)
-        pointings = numpy.array([["J2000", "0.929342rad", "0.952579rad"]] * self.NR_TILES).flatten()
+        pointings = numpy.array([["J2000", "0.929342rad", "0.952579rad"]] * DEFAULT_N_HBA_TILES).flatten()
         # Need to set the time to '2022-01-18 11:19:35'
         timestamp = datetime.datetime(2022, 1, 18, 11, 19, 35).timestamp()
 
@@ -159,7 +158,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         json_string = json.dumps(parameters, cls=NumpyEncoder)
         self.proxy.set_pointing_for_specific_time(json_string)
 
-        calculated_HBAT_delay_steps = numpy.array(antennafield_proxy.read_attribute('HBAT_BF_delay_steps_RW').value) # dims (self.NR_TILES, 32)
+        calculated_HBAT_delay_steps = numpy.array(antennafield_proxy.read_attribute('HBAT_BF_delay_steps_RW').value) # dims (DEFAULT_N_HBA_TILES, 32)
 
         # Check all delay steps are zero with small margin
         # [24, 25, 27, 28, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7] These are the real values from LOFAR.
@@ -168,7 +167,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         # they can be explained.
         expected_HBAT_delay_steps = numpy.repeat(numpy.array([24, 25, 27, 29, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7], dtype=numpy.int64), 2)
         numpy.testing.assert_equal(calculated_HBAT_delay_steps[0], expected_HBAT_delay_steps)
-        numpy.testing.assert_equal(calculated_HBAT_delay_steps[self.NR_TILES - 1], expected_HBAT_delay_steps)
+        numpy.testing.assert_equal(calculated_HBAT_delay_steps[DEFAULT_N_HBA_TILES - 1], expected_HBAT_delay_steps)
 
     def test_tilebeam_tracking(self):
         self.setup_recv_proxy()
@@ -179,7 +178,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase):
         self.assertTrue(self.proxy.Tracking_enabled_R)
 
         # point somewhere
-        new_pointings = [("J2000",f"{tile}deg","0deg") for tile in range(self.NR_TILES)]
+        new_pointings = [("J2000",f"{tile}deg","0deg") for tile in range(DEFAULT_N_HBA_TILES)]
         self.proxy.Pointing_direction_RW = new_pointings
 
         # check pointing
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_lofar_device.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_lofar_device.py
index e1d3256b937f9d859b108ef76db3cf8cc9f861a8..96a7d0e4d7582fb3d639ae2c436c4a5cdefb19d3 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_lofar_device.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_lofar_device.py
@@ -9,8 +9,10 @@
 
 import time
 from tango import DevState
-from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 
+from tangostationcontrol.common.constants import DEFAULT_POLLING_PERIOD
+
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.integration_test import base
 
 
@@ -31,7 +33,7 @@ class TestProxyAttributeAccess(base.IntegrationTestCase):
         # 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)
+        self.proxy.poll_attribute(self.ATTRIBUTE_NAME, DEFAULT_POLLING_PERIOD)
 
     def dont_poll_attribute(self):
         # make sure the attribute is NOT polled, so we force proxies to access the device
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
index 0a779f980eabd5cc08d95f0521e48acceefee2f5..9c99a5ac776a61334bbade193c12c425e0d92d33 100644
--- a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
+++ b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
@@ -7,6 +7,8 @@ import numpy
 import numpy.testing
 import casacore
 
+from tangostationcontrol.common.constants import MAX_ANTENNA, N_beamlets_ctrl
+
 from tangostationcontrol.beam.delays import Delays
 from tangostationcontrol.test import base
 
@@ -179,8 +181,8 @@ class TestDelays(base.TestCase):
         timestamp = datetime.datetime(2022, 3, 1, 0, 0, 0) # timestamp does not actually matter, but casacore doesn't know that.
         d.set_measure_time(timestamp)
 
-        positions = numpy.array([[1,2,3]] * 96)
-        directions = numpy.array([["J2000", "0deg", "0deg"]] * 488)
+        positions = numpy.array([[1,2,3]] * MAX_ANTENNA)
+        directions = numpy.array([["J2000", "0deg", "0deg"]] * N_beamlets_ctrl)
 
         count = 10
         before = time.monotonic_ns()
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py b/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py
index 25eb5d1dfffa2fca8748d74020893edfb17c2037..0701dee8262042bca4039d66939bd2246bb76ed9 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py
+++ b/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py
@@ -8,6 +8,7 @@
 # See LICENSE.txt for more info.
 
 from tangostationcontrol.common import baselines
+from tangostationcontrol.common.constants import MAX_INPUTS
 
 from tangostationcontrol.test import base
 
@@ -24,7 +25,7 @@ class TestBaselines(base.TestCase):
     def test_baseline_indices(self):
         """ Test whether baseline_from_index and baseline_index line up. """
 
-        for major in range(192):
+        for major in range(MAX_INPUTS):
             for minor in range(major + 1):
                 idx = baselines.baseline_index(major, minor)
                 self.assertEqual((major, minor), baselines.baseline_from_index(idx), msg=f'baseline_index({major},{minor}) resulted in {idx}, and should match baseline_from_index({idx})')
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_cables.py b/tangostationcontrol/tangostationcontrol/test/common/test_cables.py
new file mode 100644
index 0000000000000000000000000000000000000000..04fd3653a10229e8ab2cf89810516a535a23dab8
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/common/test_cables.py
@@ -0,0 +1,45 @@
+# -*- 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.common import cables
+
+from tangostationcontrol.test import base
+
+class TestCables(base.TestCase):
+    def test_cable_names(self):
+        """ Test whether cable names match their key in the cable_types dictionary. """
+
+        for name, cable in cables.cable_types.items():
+            self.assertEqual(name, cable.name)
+
+    def test_cable_speeds(self):
+        """ Test whether cables are transporting signals at 80% - 100% the speed of light,
+            which is a property of all our cables. """
+
+        speed_of_light = 299_792_458
+
+        for cable in cables.cable_types.values():
+            if cable.length > 0:
+                self.assertLess(80, cable.speed() / speed_of_light * 100, msg=f"Cable {cable.name}")
+                self.assertGreater(100, cable.speed() / speed_of_light * 100, msg=f"Cable {cable.name}")
+
+    def test_cable_loss_increases_with_frequency(self):
+        """ Test whether cable losses increase with frequency for each cable. """
+
+        for cable in cables.cable_types.values():
+            if cable.length == 0:
+                self.assertEqual(0.0, cable.loss[50])
+                self.assertEqual(0.0, cable.loss[150])
+                self.assertEqual(0.0, cable.loss[200])
+                self.assertEqual(0.0, cable.loss[250])
+            else:
+                self.assertLess(cable.loss[50], cable.loss[150], msg=f"Cable {cable.name}")
+                self.assertLess(cable.loss[150], cable.loss[200], msg=f"Cable {cable.name}")
+                self.assertLess(cable.loss[200], cable.loss[250], msg=f"Cable {cable.name}")
+
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py
new file mode 100644
index 0000000000000000000000000000000000000000..632a388f3c1daf1d6820e941e64f6d1788eb4c5a
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py
@@ -0,0 +1,116 @@
+# -*- 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.common.calibration import delay_compensation, loss_compensation, dB_to_factor
+
+from tangostationcontrol.test import base
+
+import numpy
+
+class TestCalibration(base.TestCase):
+    def test_dB_to_factor(self):
+        # Throw some known values at it
+        self.assertAlmostEqual(1.0, dB_to_factor(0.0), places=7)
+        self.assertAlmostEqual(2.0, dB_to_factor(3.0), places=2)
+        self.assertAlmostEqual(10.0, dB_to_factor(10.0), places=7)
+
+class TestLossCompensation(base.TestCase):
+    def test_integer_losses_no_remainder(self):
+        losses = [1.0, 2.0, 3.0, 4.0]
+
+        attenuation_integer_dB, remainder_factor = loss_compensation(numpy.array(losses))
+
+        # verify that there is no remainder
+        self.assertTrue(numpy.all(remainder_factor == 1.0), msg=f"attenuation_integer_dB = {attenuation_integer_dB}, remainder_factor = {remainder_factor}")
+
+    def test_loss_compensation_lines_up(self):
+        """ Test whether signals line up after the computed delay compensation. """
+
+        losses = [1.0, 2.0, 3.0, 4.0]
+
+        attenuation_integer_dB, _ = loss_compensation(numpy.array(losses))
+
+        # sample_shift and delay_samples together should line everything up
+        effective_attenuation = losses + attenuation_integer_dB
+
+        # all values must be lined up equally
+        self.assertEqual(1, len(set(effective_attenuation)), msg=f"effective_attenuation = {effective_attenuation}, attenuation_integer_dB = {attenuation_integer_dB}, losses = {losses}")
+
+    def test_loss_compensation_remainder(self):
+        """ Test correctness of the loss compensation remainders. """
+
+        # losses in dB we want to compensate for. they all round to the same integer value
+        losses = [0.75, 1.0, 1.25]
+
+        attenuation_integer_dB, remainder_factor = loss_compensation(numpy.array(losses))
+
+        # should not result in any sample shifts
+        self.assertEqual(0, attenuation_integer_dB[0])
+        self.assertEqual(0, attenuation_integer_dB[1])
+        self.assertEqual(0, attenuation_integer_dB[2])
+
+        # remainder should correspond with differences.
+        # NB: these are the factors to apply to line up the signals.
+        self.assertAlmostEqual(dB_to_factor(+0.25), remainder_factor[0])
+        self.assertAlmostEqual(dB_to_factor( 0.0), remainder_factor[1])
+        self.assertAlmostEqual(dB_to_factor(-0.25), remainder_factor[2])
+
+
+class TestDelayCompensation(base.TestCase):
+    def _compute_delay_compensation(self, delays_samples: list):
+        # convert to seconds (200 MHz clock => 5 ns samples) 
+        clock = 200_000_000
+        delays_seconds = numpy.array(delays_samples) / clock
+
+        # compute delay compensation
+        return delay_compensation(delays_seconds, clock)
+
+    def test_whole_sample_shifts_no_remainder(self):
+        """ Test whether delay compensation indeed has no remainder if we shift whole samples. """
+
+        # delay to compensate for, in samples
+        delay_samples = [1, 2, 3, 4]
+
+        _, remainder_seconds = self._compute_delay_compensation(delay_samples)
+
+        # verify that there is no remainder
+        self.assertTrue(numpy.all(remainder_seconds == 0.0), msg=f"{remainder_seconds}")
+
+    def test_sample_shifts_line_up(self):
+        """ Test whether signals line up after the computed delay compensation. """
+
+        # delay to compensate for, in samples
+        delay_samples = [1, 2, 3, 4]
+
+        sample_shift, _ = self._compute_delay_compensation(delay_samples)
+
+        # sample_shift and delay_samples together should line everything up
+        effective_signal_delay = delay_samples + sample_shift
+
+        # all values must be lined up equally
+        self.assertEqual(1, len(set(effective_signal_delay)), msg=f"effective_signal_delay = {effective_signal_delay}, sample_shift = {sample_shift}, delay_samples = {delay_samples}")
+
+    def test_delay_compensation_remainder(self):
+        """ Test correctness of the delay compensation remainders. """
+
+        # delays in samples we want to compensate for. they all round to the same sample
+        delay_samples = [0.75, 1.0, 1.25]
+
+        sample_shift, remainder_seconds = self._compute_delay_compensation(delay_samples)
+
+        # should not result in any sample shifts
+        self.assertEqual(0, sample_shift[0])
+        self.assertEqual(0, sample_shift[1])
+        self.assertEqual(0, sample_shift[2])
+
+        # remainder should correspond with differences.
+        # NB: these are the remainders to apply to line up the signals.
+        self.assertAlmostEqual(+0.25, remainder_seconds[0] / 5e-9)
+        self.assertAlmostEqual( 0.00, remainder_seconds[1] / 5e-9)
+        self.assertAlmostEqual(-0.25, remainder_seconds[2] / 5e-9)
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py
index 73a9f071a61f3af0a7afedb75b9a59591cc3a469..0b4796e39692e8f987cb06d0237a5f6602941fca 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py
@@ -20,6 +20,7 @@ from tangostationcontrol.devices import antennafield
 from tangostationcontrol.devices.antennafield import AntennaToRecvMapper, AntennaQuality, AntennaUse
 from tangostationcontrol.test import base
 from tangostationcontrol.test.devices import device_base
+from tangostationcontrol.common.constants import MAX_ANTENNA, N_rcu, DEFAULT_N_HBA_TILES
 
 logger = logging.getLogger()
 
@@ -27,50 +28,50 @@ logger = logging.getLogger()
 class TestAntennaToRecvMapper(base.TestCase):
 
     # A mapping where Antennas are all not mapped to power RCUs
-    POWER_NOT_CONNECTED = [[-1, -1]] * 48
+    POWER_NOT_CONNECTED = [[-1, -1]] * DEFAULT_N_HBA_TILES
     # A mapping where Antennas are all not mapped to control RCUs
-    CONTROL_NOT_CONNECTED = [[-1, -1]] * 48
+    CONTROL_NOT_CONNECTED = [[-1, -1]] * DEFAULT_N_HBA_TILES
     # A mapping where first two Antennas are mapped on the first Receiver.
     # The first Antenna control line on RCU 1 and the second Antenna control line on RCU 0.
-    CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1 = [[1, 1], [1, 0]] + [[-1, -1]] * 46
+    CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1 = [[1, 1], [1, 0]] + [[-1, -1]] * (DEFAULT_N_HBA_TILES - 2)
 
     def test_ant_read_mask_r_no_mapping(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[False] * 96, [False] * 96, [False] * 96]
-        expected = [False] * 48
+        receiver_values = [[False] * MAX_ANTENNA, [False] * MAX_ANTENNA, [False] * MAX_ANTENNA]
+        expected = [False] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("ANT_mask_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_ant_read_mask_r_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[False, True, False] + [False, False, False] * 31, [False] * 96, [False] * 96]
-        expected = [True, False] + [False] * 46
+        receiver_values = [[False, True, False] + [False, False, False] * (N_rcu - 1), [False] * MAX_ANTENNA, [False] * MAX_ANTENNA]
+        expected = [True, False] + [False] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("ANT_mask_RW", receiver_values)
 
         numpy.testing.assert_equal(expected, actual)
 
     def test_rcu_band_select_no_mapping(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
-        receiver_values = [[0] * 96, [0] * 96, [0] * 96]
-        expected = [0] * 48
+        receiver_values = [[0] * MAX_ANTENNA, [0] * MAX_ANTENNA, [0] * MAX_ANTENNA]
+        expected = [0] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("RCU_band_select_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_bf_read_delay_steps_r_no_mapping(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[0] * 32] * 96, [[0] * 32] * 96, [[0] * 32] * 96]
-        expected = [[0] * 32] * 48
+        receiver_values = [[[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA]
+        expected = [[0] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_BF_delay_steps_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_bf_read_delay_steps_r_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[2] * 32, [1] * 32] + [[0] * 32] * 94, [[0] * 32] * 96, [[0] * 32] * 96]
-        expected = [[1] * 32, [2] * 32] + [[0] * 32] * 46
+        receiver_values = [[[2] * N_rcu, [1] * N_rcu] + [[0] * N_rcu] * (MAX_ANTENNA - 2), [[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA]
+        expected = [[1] * N_rcu, [2] * N_rcu] + [[0] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_BF_delay_steps_R", receiver_values)
 
         numpy.testing.assert_equal(expected, actual)
@@ -78,16 +79,16 @@ class TestAntennaToRecvMapper(base.TestCase):
     def test_bf_read_delay_steps_rw_no_mapping(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[0] * 32] * 96, [[0] * 32] * 96, [[0] * 32] * 96]
-        expected = [[0] * 32] * 48
+        receiver_values = [[[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA]
+        expected = [[0] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_BF_delay_steps_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_bf_read_delay_steps_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[2] * 32, [1] * 32] + [[0] * 32] * 94, [[0] * 32] * 96, [[0] * 32] * 96]
-        expected = [[1] * 32, [2] * 32] + [[0] * 32] * 46
+        receiver_values = [[[2] * N_rcu, [1] * N_rcu] + [[0] * N_rcu] * (MAX_ANTENNA - 2), [[0] * N_rcu] * MAX_ANTENNA, [[0] * N_rcu] * MAX_ANTENNA]
+        expected = [[1] * N_rcu, [2] * N_rcu] + [[0] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_BF_delay_steps_RW", receiver_values)
 
         numpy.testing.assert_equal(expected, actual)
@@ -95,102 +96,102 @@ class TestAntennaToRecvMapper(base.TestCase):
     def test_map_read_led_on_r_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_LED_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_led_on_r_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_LED_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_led_on_rw_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_LED_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_led_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_LED_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_lna_on_r_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_PWR_LNA_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_lna_on_r_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_PWR_LNA_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_lna_on_rw_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_PWR_LNA_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_lna_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_PWR_LNA_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_on_r_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_PWR_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_on_r_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_PWR_on_R", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_on_rw_unmapped(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False] * 32] * 96, [[False] * 32] * 96, [[False] * 32] * 96]
-        expected = [[False] * 32] * 48
+        receiver_values = [[[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
+        expected = [[False] * N_rcu] * DEFAULT_N_HBA_TILES
         actual = mapper.map_read("HBAT_PWR_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_read_pwr_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 3)
 
-        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * 32] * 94, [[False] * 32] * 96, [[False] * 32] * 96]
+        receiver_values = [[[False, True] * 16, [True, False] * 16] + [[False] * N_rcu] * (MAX_ANTENNA - 2), [[False] * N_rcu] * MAX_ANTENNA, [[False] * N_rcu] * MAX_ANTENNA]
 
-        expected = [[True, False] * 16, [False, True] * 16] + [[False] * 32] * 46
+        expected = [[True, False] * 16, [False, True] * 16] + [[False] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
         actual = mapper.map_read("HBAT_PWR_on_RW", receiver_values)
         numpy.testing.assert_equal(expected, actual)
 
@@ -201,8 +202,8 @@ class TestAntennaToRecvMapper(base.TestCase):
 
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [None] * 48
-        expected = [[None] * 96]
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[None] * MAX_ANTENNA]
         actual = mapper.map_write("ANT_mask_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
@@ -211,159 +212,159 @@ class TestAntennaToRecvMapper(base.TestCase):
 
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [None] * 48
-        expected = [[None] * 96] * 2
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[None] * MAX_ANTENNA] * 2
         actual = mapper.map_write("ANT_mask_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_ant_mask_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [True, False] + [None] * 46
-        expected = [[False, True] + [None] * 94]
+        set_values = [True, False] + [None] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[False, True] + [None] * (MAX_ANTENNA - 2)]
         actual = mapper.map_write("ANT_mask_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_pwr_ant_on_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [None] * 48
-        expected = [[[None, None, None]] * 32]
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[[None, None, None]] * N_rcu]
         actual = mapper.map_write("RCU_PWR_ANT_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_pwr_ant_on_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [None] * 48
-        expected = [[[None, None, None]] * 32] * 2
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[[None, None, None]] * N_rcu] * 2
         actual = mapper.map_write("RCU_PWR_ANT_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_pwr_ant_on_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [1, 0] + [None] * 46
-        expected = [[[0, 1, None]] + [[None, None, None]] * 31]
+        set_values = [1, 0] + [None] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[0, 1, None]] + [[None, None, None]] * (N_rcu - 1)]
         actual = mapper.map_write("RCU_PWR_ANT_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_band_select_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [None] * 48
-        expected = [[[None, None, None]] * 32]
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[[None, None, None]] * N_rcu]
         actual = mapper.map_write("RCU_band_select_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_band_select_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [None] * 48
-        expected = [[[None, None, None]] * 32] * 2
+        set_values = [None] * DEFAULT_N_HBA_TILES
+        expected = [[[None, None, None]] * N_rcu] * 2
         actual = mapper.map_write("RCU_band_select_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_rcu_band_select_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [1, 0] + [None] * 46
-        expected = [[[0, 1, None]] + [[None, None, None]] * 31]
+        set_values = [1, 0] + [None] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[0, 1, None]] + [[None, None, None]] * (N_rcu - 1)]
         actual = mapper.map_write("RCU_band_select_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_bf_delay_steps_rw_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[1] * 32] * 48
-        expected = [[[None] * 32] * 96]
+        set_values = [[1] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_BF_delay_steps_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_bf_delay_steps_rw_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [[1] * 32] * 48
-        expected = [[[None] * 32] * 96, [[None] * 32] * 96]
+        set_values = [[1] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA, [[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_BF_delay_steps_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_bf_delay_steps_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[1] * 32, [2] * 32] + [[None] * 32] * 46
-        expected = [[[2] * 32, [1] * 32] + [[None] * 32] * 94]
+        set_values = [[1] * N_rcu, [2] * N_rcu] + [[None] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[2] * N_rcu, [1] * N_rcu] + [[None] * N_rcu] * (MAX_ANTENNA - 2)]
         actual = mapper.map_write("HBAT_BF_delay_steps_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_led_on_rw_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_LED_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_led_on_rw_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96, [[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA, [[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_LED_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_led_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * 32] * 46
-        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * 32] * 94]
+        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * N_rcu] * (MAX_ANTENNA - 2)]
         actual = mapper.map_write("HBAT_LED_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_pwr_lna_on_rw_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_PWR_LNA_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_pwr_lna_on_rw_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96, [[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA, [[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_PWR_LNA_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_pwr_lna_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * 32] * 46
-        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * 32] * 94]
+        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * N_rcu] * (MAX_ANTENNA - 2)]
         actual = mapper.map_write("HBAT_PWR_LNA_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_pwr_on_rw_no_mapping_and_one_receiver(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 1)
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_PWR_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_lna_on_rw_no_mapping_and_two_receivers(self):
         mapper = AntennaToRecvMapper(self.CONTROL_NOT_CONNECTED, self.POWER_NOT_CONNECTED, 2)
 
-        set_values = [[None] * 32] * 48
-        expected = [[[None] * 32] * 96, [[None] * 32] * 96]
+        set_values = [[None] * N_rcu] * DEFAULT_N_HBA_TILES
+        expected = [[[None] * N_rcu] * MAX_ANTENNA, [[None] * N_rcu] * MAX_ANTENNA]
         actual = mapper.map_write("HBAT_PWR_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
     def test_map_write_pwr_on_rw_hba_0_and_1_on_rcu_1_and_0_of_recv_1(self):
         mapper = AntennaToRecvMapper(self.CONTROL_HBA_0_AND_1_ON_RCU_1_AND_0_OF_RECV_1, self.POWER_NOT_CONNECTED, 1)
 
-        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * 32] * 46
-        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * 32] * 94]
+        set_values = [[False, True] * 16, [True, False] * 16] + [[None] * N_rcu] * (DEFAULT_N_HBA_TILES - 2)
+        expected = [[[True, False] * 16, [False, True] * 16] + [[None] * N_rcu] * (MAX_ANTENNA - 2)]
         actual = mapper.map_write("HBAT_PWR_on_RW", set_values)
         numpy.testing.assert_equal(expected, actual)
 
@@ -395,31 +396,31 @@ class TestAntennafieldDevice(device_base.DeviceTestCase):
 
     def test_read_Antenna_Quality(self):
         """ Verify if Antenna_Quality_R is correctly retrieved """
-        antenna_qualities = numpy.array([AntennaQuality.OK] * 96)
+        antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
         with DeviceTestContext(antennafield.AntennaField, properties=self.AT_PROPERTIES, process=True) as proxy:
             numpy.testing.assert_equal(antenna_qualities, proxy.Antenna_Quality_R)
 
     def test_read_Antenna_Use(self):
         """ Verify if Antenna_Use_R is correctly retrieved """
-        antenna_use = numpy.array([AntennaUse.AUTO] * 96)
+        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
         with DeviceTestContext(antennafield.AntennaField, properties=self.AT_PROPERTIES, process=True) as proxy:
             numpy.testing.assert_equal(antenna_use, proxy.Antenna_Use_R)
 
     def test_read_Antenna_Usage_Mask(self):
         """ Verify if Antenna_Usage_Mask_R is correctly retrieved """
-        antenna_qualities = numpy.array([AntennaQuality.OK] * 96)
-        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * 95)
+        antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA)
+        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * (MAX_ANTENNA - 1))
         antenna_properties = {'Antenna_Quality': antenna_qualities, 'Antenna_Use': antenna_use}
         with DeviceTestContext(antennafield.AntennaField, properties={**self.AT_PROPERTIES, **antenna_properties}, process=True) as proxy:
-            numpy.testing.assert_equal(numpy.array([True] * 96), proxy.Antenna_Usage_Mask_R)
+            numpy.testing.assert_equal(numpy.array([True] * MAX_ANTENNA), proxy.Antenna_Usage_Mask_R)
 
     def test_read_Antenna_Usage_Mask_only_one_functioning_antenna(self):
         """ Verify if Antenna_Usage_Mask_R (only first antenna is OK) is correctly retrieved """
-        antenna_qualities = numpy.array([AntennaQuality.OK] + [AntennaQuality.BROKEN] * 95)
-        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * 95)
+        antenna_qualities = numpy.array([AntennaQuality.OK] + [AntennaQuality.BROKEN] * (MAX_ANTENNA - 1))
+        antenna_use = numpy.array([AntennaUse.ON] + [AntennaUse.AUTO] * (MAX_ANTENNA - 1))
         antenna_properties = {'Antenna_Quality': antenna_qualities, 'Antenna_Use': antenna_use}
         with DeviceTestContext(antennafield.AntennaField, properties={**self.AT_PROPERTIES, **antenna_properties}, process=True) as proxy:
-            numpy.testing.assert_equal(numpy.array([True] + [False] * 95), proxy.Antenna_Usage_Mask_R)
+            numpy.testing.assert_equal(numpy.array([True] + [False] * (MAX_ANTENNA - 1)), proxy.Antenna_Usage_Mask_R)
 
     def test_read_Antenna_Names(self):
         """ Verify if Antenna_Names_R is correctly retrieved """
@@ -438,7 +439,7 @@ class TestAntennafieldDevice(device_base.DeviceTestCase):
             'RECV_devices': ['stat/RECV/1'],
         }
 
-        data = numpy.array([[False] * 32] * 96)
+        data = numpy.array([[False] * N_rcu] * MAX_ANTENNA)
 
         m_proxy.return_value = mock.Mock(
             read_attribute=mock.Mock(
@@ -452,7 +453,7 @@ class TestAntennafieldDevice(device_base.DeviceTestCase):
         ) as proxy:
             proxy.boot()
 
-            proxy.write_attribute("HBAT_PWR_on_RW", numpy.array([[False] * 32] * 48))
+            proxy.write_attribute("HBAT_PWR_on_RW", numpy.array([[False] * N_rcu] * DEFAULT_N_HBA_TILES))
 
             numpy.testing.assert_equal(
                 m_proxy.return_value.write_attribute.call_args[0][1],
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py
index ff43c4ada4f8f73257f32b13c3e1261afd13aa47..1fa9c8b096a409d642e303e4cff0dca6ca6a54dd 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py
@@ -7,38 +7,20 @@
 # Distributed under the terms of the APACHE license.
 # See LICENSE.txt for more info.
 
+from tangostationcontrol.common.constants import CLK_200_MHZ
 from tangostationcontrol.devices.sdp.beamlet import Beamlet
+from tangostationcontrol.devices.sdp.common import weight_to_complex
 
 import numpy
 import numpy.testing
-from ctypes import c_short
 
 from tangostationcontrol.test import base
 
 # unpack into 16-bit complex
 def to_complex(uint32):
-    imag = c_short(uint32 >> 16).value
-    real = c_short(uint32 & 0xFFFF).value
-    unit = Beamlet.BF_UNIT_WEIGHT
-
-    return (real + 1j * imag) / unit
+    return weight_to_complex(uint32, Beamlet.BF_UNIT_WEIGHT)
 
 class TestBeamletDevice(base.TestCase):
-    def test_phases_to_bf_weights(self):
-        # offer nice 0, 90, 180, 270, 360 degrees
-        phases = numpy.array([0.0, numpy.pi / 2, numpy.pi, numpy.pi * 1.5, numpy.pi * 2])
-        unit   = Beamlet.BF_UNIT_WEIGHT
-
-        bf_weights = Beamlet._phases_to_bf_weights(phases)
-
-        # check whether the complex representation is also along the right axes and
-        # has the right amplitude
-        self.assertEqual(to_complex(bf_weights[0]),  1 + 0j, msg=f"bf_weights = {bf_weights}")
-        self.assertEqual(to_complex(bf_weights[1]),  0 + 1j, msg=f"bf_weights = {bf_weights}")
-        self.assertEqual(to_complex(bf_weights[2]), -1 + 0j, msg=f"bf_weights = {bf_weights}")
-        self.assertEqual(to_complex(bf_weights[3]),  0 - 1j, msg=f"bf_weights = {bf_weights}")
-        self.assertEqual(to_complex(bf_weights[4]),  1 + 0j, msg=f"bf_weights = {bf_weights}")
-
     def test_calculate_bf_weights_small_numbers(self):
         # 2 beamlets, 3 antennas. The antennas are 1 second apart.
         delays = numpy.array([
@@ -79,7 +61,7 @@ class TestBeamletDevice(base.TestCase):
         ])
 
         beamlet_frequencies = numpy.array([
-            [200e6, 200e6, 200e6, 200e6, 200e6]
+            [CLK_200_MHZ, CLK_200_MHZ, CLK_200_MHZ, CLK_200_MHZ, CLK_200_MHZ]
         ])
 
         bf_weights = Beamlet._calculate_bf_weights(delays, beamlet_frequencies)
@@ -91,42 +73,3 @@ class TestBeamletDevice(base.TestCase):
         self.assertEqual(to_complex(bf_weights[0][2]), -1 + 0j, msg=f"bf_weights = {bf_weights}")
         self.assertEqual(to_complex(bf_weights[0][3]),  0 + 1j, msg=f"bf_weights = {bf_weights}")
         self.assertEqual(to_complex(bf_weights[0][4]),  1 + 0j, msg=f"bf_weights = {bf_weights}")
-
-    def test_subband_frequencies(self):
-        subbands = numpy.array([
-          [0, 1, 102],
-        ])
-
-        nyquist_zones_0 = numpy.zeros(subbands.shape)
-        nyquist_zones_1 = numpy.ones(subbands.shape)
-        nyquist_zones_2 = numpy.ones(subbands.shape) * 2
-
-        # for reference values, see https://proxy.lofar.eu/rtsm/tests/
-
-        lba_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, nyquist_zones_0)
-        self.assertAlmostEqual(lba_frequencies[0][0],  0.0000000e6)
-        self.assertAlmostEqual(lba_frequencies[0][1],  0.1562500e6)
-        self.assertAlmostEqual(lba_frequencies[0][2], 15.9375000e6)
-
-        lba_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_0)
-        self.assertAlmostEqual(lba_frequencies[0][0],  0.0000000e6)
-        self.assertAlmostEqual(lba_frequencies[0][1],  0.1953125e6)
-        self.assertAlmostEqual(lba_frequencies[0][2], 19.9218750e6)
-
-        # Nyquist zone 1 is not used in 160 MHz
-
-        hba_low_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_1)
-        self.assertAlmostEqual(hba_low_frequencies[0][0], 100.0000000e6)
-        self.assertAlmostEqual(hba_low_frequencies[0][1], 100.1953125e6)
-        self.assertAlmostEqual(hba_low_frequencies[0][2], 119.9218750e6)
-
-        hba_high_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, nyquist_zones_2)
-        self.assertAlmostEqual(hba_high_frequencies[0][0], 160.0000000e6)
-        self.assertAlmostEqual(hba_high_frequencies[0][1], 160.1562500e6)
-        self.assertAlmostEqual(hba_high_frequencies[0][2], 175.9375000e6)
-
-        hba_high_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_2)
-        self.assertAlmostEqual(hba_high_frequencies[0][0], 200.0000000e6)
-        self.assertAlmostEqual(hba_high_frequencies[0][1], 200.1953125e6)
-        self.assertAlmostEqual(hba_high_frequencies[0][2], 219.9218750e6)
-
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py
index 0d19f7cab3b6284caed0e6a6e63f701957f5c582..d57f912218550b7e73181a332caa61b94e6c3914 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py
@@ -15,6 +15,7 @@ import numpy
 
 # Internal regular imports
 from tangostationcontrol.devices.sdp import digitalbeam
+from tangostationcontrol.common.constants import MAX_ANTENNA, N_beamlets_ctrl, N_xyz, N_pn
 
 # Builtin test libraries
 from unittest import mock
@@ -40,20 +41,20 @@ class TestDigitalBeamDevice(device_base.DeviceTestCase):
     def test_apply_weights_disabled(self, m_proxy, m_compute, m_wait):
         """Verify won't overwrite digitalbeam data if no input_selected"""
 
-        input_data = numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten()
-        current_data = numpy.array([[16384] * 5856] * 16)
+        input_data = numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten()
+        current_data = numpy.array([[16384] * 5856] * N_pn)
 
         m_proxy.return_value = mock.Mock(
             read_attribute=mock.Mock(
                 return_value=mock.Mock(value=copy.copy(current_data))
             ),
-            Antenna_Usage_Mask_R=numpy.array([0] * 96),
+            Antenna_Usage_Mask_R=numpy.array([0] * MAX_ANTENNA),
             Antenna_Field_Reference_ITRF_R=mock.MagicMock(),
-            HBAT_reference_ITRF_R=numpy.array([[0] * 3] * 96)
+            HBAT_reference_ITRF_R=numpy.array([[0] * N_xyz] * MAX_ANTENNA)
         )
 
         new_data = numpy.array(
-            [[16384] * 2928 + [0] * 2928] * 16
+            [[16384] * 2928 + [0] * 2928] * N_pn
         )
         m_compute.return_value = copy.copy(new_data)
 
@@ -62,7 +63,7 @@ class TestDigitalBeamDevice(device_base.DeviceTestCase):
         ) as proxy:
             proxy.initialise()
             proxy.Tracking_enabled_RW = False
-            proxy.input_select_RW = numpy.array([[False] * 488] * 96)
+            proxy.input_select_RW = numpy.array([[False] * N_beamlets_ctrl] * MAX_ANTENNA)
 
             proxy.set_pointing(input_data)
 
@@ -78,20 +79,20 @@ class TestDigitalBeamDevice(device_base.DeviceTestCase):
     def test_apply_weights_enabled(self, m_proxy, m_compute, m_wait):
         """Verify can overwrite digitalbeam data if input_selected"""
 
-        input_data = numpy.array([["AZELGEO", "0deg", "90deg"]] * 488).flatten()
-        current_data = numpy.array([[16384] * 5856] * 16)
+        input_data = numpy.array([["AZELGEO", "0deg", "90deg"]] * N_beamlets_ctrl).flatten()
+        current_data = numpy.array([[16384] * 5856] * N_pn)
 
         m_proxy.return_value = mock.Mock(
             read_attribute=mock.Mock(
                 return_value=mock.Mock(value=current_data)
             ),
-            Antenna_Usage_Mask_R=numpy.array([0] * 96),
+            Antenna_Usage_Mask_R=numpy.array([0] * MAX_ANTENNA),
             Antenna_Field_Reference_ITRF_R=mock.MagicMock(),
-            HBAT_reference_ITRF_R=numpy.array([[0] * 3] * 96)
+            HBAT_reference_ITRF_R=numpy.array([[0] * N_xyz] * MAX_ANTENNA)
         )
 
         new_data = numpy.array(
-            [[16384] * 2928 + [0] * 2928] * 16
+            [[16384] * 2928 + [0] * 2928] * N_pn
         )
         m_compute.return_value = copy.copy(new_data)
 
@@ -100,7 +101,7 @@ class TestDigitalBeamDevice(device_base.DeviceTestCase):
         ) as proxy:
             proxy.initialise()
             proxy.Tracking_enabled_RW = False
-            proxy.input_select_RW = numpy.array([[True] * 488] * 96)
+            proxy.input_select_RW = numpy.array([[True] * N_beamlets_ctrl] * MAX_ANTENNA)
 
             proxy.set_pointing(input_data)
 
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py
index a0f85653cd55aa2b6a60dd61c4b7194eea237f9c..1ce23d972729bbac403313482db8f65e527cada9 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py
@@ -10,6 +10,7 @@
 from tango.test_context import DeviceTestContext
 
 from tangostationcontrol.devices import recv
+from tangostationcontrol.common.constants import N_rcu, N_rcu_inp, N_elements
 
 import numpy
 
@@ -28,7 +29,7 @@ class TestRecvDevice(device_base.DeviceTestCase):
     def test_calculate_HBAT_bf_delay_steps(self):
         """Verify HBAT beamforming calculations are correctly executed"""
         with DeviceTestContext(recv.RECV, properties=self.RECV_PROPERTIES, process=True) as proxy:
-            delays = numpy.random.rand(96,16).flatten()
+            delays = numpy.random.rand(N_rcu * N_rcu_inp,N_elements).flatten()
             HBAT_bf_delay_steps = proxy.calculate_HBAT_bf_delay_steps(delays)
             self.assertEqual(3072, len(HBAT_bf_delay_steps))                             # 96x32=3072
 
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py b/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py
new file mode 100644
index 0000000000000000000000000000000000000000..84655351499ef7b117fedf28e5f119b4c0905523
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py
@@ -0,0 +1,107 @@
+# -*- 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.common.constants import CLK_200_MHZ, CLK_160_MHZ
+from tangostationcontrol.devices.sdp.common import phases_to_weights, subband_frequencies, subband_frequency, weight_to_complex
+
+import numpy
+
+from tangostationcontrol.test import base
+
+class TestSDPCommon(base.TestCase):
+    def test_subband_frequencies(self):
+        subbands = numpy.array([
+          [0, 1, 102],
+        ])
+
+        nyquist_zones_0 = numpy.zeros(subbands.shape)
+        nyquist_zones_1 = numpy.ones(subbands.shape)
+        nyquist_zones_2 = numpy.ones(subbands.shape) * 2
+
+        # for reference values, see https://proxy.lofar.eu/rtsm/tests/
+
+        lba_frequencies = subband_frequencies(subbands, CLK_160_MHZ, nyquist_zones_0)
+        self.assertAlmostEqual(lba_frequencies[0][0],  0.0000000e6)
+        self.assertAlmostEqual(lba_frequencies[0][1],  0.1562500e6)
+        self.assertAlmostEqual(lba_frequencies[0][2], 15.9375000e6)
+
+        lba_frequencies = subband_frequencies(subbands, CLK_200_MHZ, nyquist_zones_0)
+        self.assertAlmostEqual(lba_frequencies[0][0],  0.0000000e6)
+        self.assertAlmostEqual(lba_frequencies[0][1],  0.1953125e6)
+        self.assertAlmostEqual(lba_frequencies[0][2], 19.9218750e6)
+
+        # Nyquist zone 1 is not used in 160 MHz
+
+        hba_low_frequencies = subband_frequencies(subbands, CLK_200_MHZ, nyquist_zones_1)
+        self.assertAlmostEqual(hba_low_frequencies[0][0], 100.0000000e6)
+        self.assertAlmostEqual(hba_low_frequencies[0][1], 100.1953125e6)
+        self.assertAlmostEqual(hba_low_frequencies[0][2], 119.9218750e6)
+
+        hba_high_frequencies = subband_frequencies(subbands, CLK_160_MHZ, nyquist_zones_2)
+        self.assertAlmostEqual(hba_high_frequencies[0][0], 160.0000000e6)
+        self.assertAlmostEqual(hba_high_frequencies[0][1], 160.1562500e6)
+        self.assertAlmostEqual(hba_high_frequencies[0][2], 175.9375000e6)
+
+        hba_high_frequencies = subband_frequencies(subbands, CLK_200_MHZ, nyquist_zones_2)
+        self.assertAlmostEqual(hba_high_frequencies[0][0], 200.0000000e6)
+        self.assertAlmostEqual(hba_high_frequencies[0][1], 200.1953125e6)
+        self.assertAlmostEqual(hba_high_frequencies[0][2], 219.9218750e6)
+
+    def test_subband_frequency(self):
+        # most coverage is in test_subband_frequencies, we only need to test
+        # whether the right parameter types are accepted.
+        hba_low_frequency = subband_frequency(102, CLK_200_MHZ, 1)
+        self.assertAlmostEqual(hba_low_frequency, 119.9218750e6)
+
+    def test_phases_to_weights(self):
+        # offer nice 0, 90, 180, 270, 360 degrees
+        phases = numpy.array([0.0, numpy.pi / 2, numpy.pi, numpy.pi * 1.5, numpy.pi * 2])
+        unit = 2**14
+
+        sdp_weights = phases_to_weights(phases, unit)
+
+        # check whether the complex representation is also along the right axes and
+        # has the right amplitude
+        self.assertEqual(weight_to_complex(sdp_weights[0], unit),  1 + 0j, msg=f"sdp_weights = {sdp_weights}")
+        self.assertEqual(weight_to_complex(sdp_weights[1], unit),  0 + 1j, msg=f"sdp_weights = {sdp_weights}")
+        self.assertEqual(weight_to_complex(sdp_weights[2], unit), -1 + 0j, msg=f"sdp_weights = {sdp_weights}")
+        self.assertEqual(weight_to_complex(sdp_weights[3], unit),  0 - 1j, msg=f"sdp_weights = {sdp_weights}")
+        self.assertEqual(weight_to_complex(sdp_weights[4], unit),  1 + 0j, msg=f"sdp_weights = {sdp_weights}")
+
+    def test_phases_to_weights_with_amplitude(self):
+        # offer nice 0, 90, 180, 270, 360 degrees
+        phases = numpy.array([0.0, numpy.pi / 2, numpy.pi, numpy.pi * 1.5, numpy.pi * 2])
+        amplitudes = numpy.array([0.1, 0.2, 0.3, 0.4, 0.5])
+        unit = 2**14
+
+        sdp_weights = phases_to_weights(phases, unit, amplitudes)
+
+        # check whether the complex representation is also along the right axes and
+        # has the right amplitude
+        self.assertAlmostEqual(weight_to_complex(sdp_weights[0], unit),  0.1 + 0j, places=3, msg=f"sdp_weights = {sdp_weights}")
+        self.assertAlmostEqual(weight_to_complex(sdp_weights[1], unit),  0 + 0.2j, places=3, msg=f"sdp_weights = {sdp_weights}")
+        self.assertAlmostEqual(weight_to_complex(sdp_weights[2], unit), -0.3 + 0j, places=3, msg=f"sdp_weights = {sdp_weights}")
+        self.assertAlmostEqual(weight_to_complex(sdp_weights[3], unit),  0 - 0.4j, places=3, msg=f"sdp_weights = {sdp_weights}")
+        self.assertAlmostEqual(weight_to_complex(sdp_weights[4], unit),  0.5 + 0j, places=3, msg=f"sdp_weights = {sdp_weights}")
+
+    def test_weight_to_complex(self):
+        unit = 8192
+
+        # some trivial values
+        self.assertEqual(1, weight_to_complex(unit, unit))
+        self.assertEqual(1j, weight_to_complex(unit << 16, unit))
+
+        # some constructed values
+        def complex_to_weight(c: complex) -> numpy.uint32:
+            return numpy.array([c.real * unit, c.imag * unit],dtype=numpy.int16).view(numpy.uint32).item()
+
+        self.assertEqual(-1, weight_to_complex(complex_to_weight(-1), unit))
+        self.assertEqual(-1j, weight_to_complex(complex_to_weight(-1j), unit))
+        self.assertEqual(2 - 3j, weight_to_complex(complex_to_weight(2 - 3j), unit))
+        self.assertEqual(0.5 - 0.25j, weight_to_complex(complex_to_weight(0.5 - 0.25j), unit))
diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini
index 332d3777fd58491bfe96c270568a12e8d10f5f77..d528ccc75b0280f93740f5859987c225166f2c33 100644
--- a/tangostationcontrol/tox.ini
+++ b/tangostationcontrol/tox.ini
@@ -118,4 +118,4 @@ commands =
 [flake8]
 filename = *.py,.stestr.conf,.txt
 ignore = B014, B019, W291, W293, W391, E111, E114, E121, E122, E123, E124, E126, E127, E128, E131, E201, E201, E202, E203, E221, E222, E225, E226, E231, E241, E251, E252, E261, E262, E265, E271, E301, E302, E303, E305, E306, E401, E402, E501, E502, E701, E712, E721, E731, F403, F523, F541, F841, H301, H306, H401, H403, H404, H405, W503
-exclude=.tox,.egg-info,libhdbpp-python, SNMP_mib_loading
+exclude=.tox,build,.egg-info,libhdbpp-python, SNMP_mib_loading