From 45d0012b5e0b8c87673ed902f29f6a79a01141c5 Mon Sep 17 00:00:00 2001
From: Jan David Mol <mol@astron.nl>
Date: Thu, 3 Mar 2022 14:15:05 +0100
Subject: [PATCH] L2SS-433: Add beamlet device, which takes over responsibility
 of all beamlet and bf_weight attributes from SDP

---
 .gitlab-ci.yml                                |  24 ++++
 CDB/LOFAR_ConfigDb.json                       |  28 ++++
 CDB/stations/DTS_ConfigDb.json                |  55 +++++++
 CDB/stations/LTS_ConfigDb.json                |  55 +++++++
 CDB/stations/simulators_ConfigDb.json         |  55 +++++++
 docker-compose/device-beamlet.yml             |  47 ++++++
 .../startup/01-devices.py                     |   3 +-
 sbin/run_integration_test.sh                  |   6 +-
 .../docs/source/devices/beamlet.rst           |   8 ++
 tangostationcontrol/docs/source/index.rst     |   1 +
 tangostationcontrol/setup.cfg                 |   1 +
 .../tangostationcontrol/devices/README.md     |   3 +-
 .../tangostationcontrol/devices/boot.py       |   5 +-
 .../devices/sdp/beamlet.py                    | 136 ++++++++++++++++++
 .../tangostationcontrol/devices/sdp/sdp.py    |  46 ------
 .../default/devices/test_device_beamlet.py    |  32 +++++
 16 files changed, 452 insertions(+), 53 deletions(-)
 create mode 100644 docker-compose/device-beamlet.yml
 create mode 100644 tangostationcontrol/docs/source/devices/beamlet.rst
 create mode 100644 tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
 create mode 100644 tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 24be19a0d..4b4dd9211 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -97,6 +97,8 @@ docker_build_image_all:
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh unb2-sim latest
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-apsct latest
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-apspu latest
+    - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-beam latest
+    - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-beamlet latest
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-boot latest
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-docker latest
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-observation_control latest
@@ -250,6 +252,28 @@ docker_build_image_device_apspu:
   script:
 #    Do not remove 'bash' or statement will be ignored by primitive docker shell
     - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-apspu $tag
+docker_build_image_device_beam:
+  extends: .base_docker_images_except
+  only:
+    refs:
+      - merge_requests
+    changes:
+      - docker-compose/device-beam.yml
+      - docker-compose/lofar-device-base/*
+  script:
+#    Do not remove 'bash' or statement will be ignored by primitive docker shell
+    - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-beam $tag
+docker_build_image_device_beamlet:
+  extends: .base_docker_images_except
+  only:
+    refs:
+      - merge_requests
+    changes:
+      - docker-compose/device-beamlet.yml
+      - docker-compose/lofar-device-base/*
+  script:
+#    Do not remove 'bash' or statement will be ignored by primitive docker shell
+    - bash $CI_PROJECT_DIR/sbin/tag_and_push_docker_image.sh device-beamlet $tag
 docker_build_image_device_boot:
   extends: .base_docker_images_except
   only:
diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json
index c7573e87d..621c84589 100644
--- a/CDB/LOFAR_ConfigDb.json
+++ b/CDB/LOFAR_ConfigDb.json
@@ -21,6 +21,34 @@
                 }
             }
         },
+        "Beamlet": {
+            "STAT": {
+                "Beamlet": {
+                    "STAT/Beamlet/1": {
+                        "properties": {
+                            "FPGA_beamlet_output_hdr_udp_destination_port_RW_default": [
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001",
+                                "10001"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "boot": {
             "STAT": {
                 "Boot": {
diff --git a/CDB/stations/DTS_ConfigDb.json b/CDB/stations/DTS_ConfigDb.json
index bd797a7ea..7017e28e7 100644
--- a/CDB/stations/DTS_ConfigDb.json
+++ b/CDB/stations/DTS_ConfigDb.json
@@ -51,6 +51,61 @@
                 }
             }
         },
+        "Beamlet": {
+            "STAT": {
+                "Beamlet": {
+                    "STAT/Beamlet/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "10.99.0.252"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1",
+                                "0c:c4:7a:c0:30:f1"
+                            ],
+                            "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250",
+                                "10.99.250.250"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "RECV": {
             "STAT": {
                 "RECV": {
diff --git a/CDB/stations/LTS_ConfigDb.json b/CDB/stations/LTS_ConfigDb.json
index faa429379..63b16a78b 100644
--- a/CDB/stations/LTS_ConfigDb.json
+++ b/CDB/stations/LTS_ConfigDb.json
@@ -13,6 +13,61 @@
                 }
             }
         },
+        "Beamlet": {
+            "STAT": {
+                "Beamlet": {
+                    "STAT/Beamlet/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "dop369.astron.nl"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00",
+                                "00:07:43:06:c7:00"
+                            ],
+                            "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254",
+                                "192.168.0.254"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "RECV": {
             "STAT": {
                 "RECV": {
diff --git a/CDB/stations/simulators_ConfigDb.json b/CDB/stations/simulators_ConfigDb.json
index c9c1b8135..1418898b7 100644
--- a/CDB/stations/simulators_ConfigDb.json
+++ b/CDB/stations/simulators_ConfigDb.json
@@ -51,6 +51,61 @@
                 }
             }
         },
+        "Beamlet": {
+            "STAT": {
+                "Beamlet": {
+                    "STAT/Beamlet/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "RECV": {
             "STAT": {
                 "RECV": {
diff --git a/docker-compose/device-beamlet.yml b/docker-compose/device-beamlet.yml
new file mode 100644
index 000000000..d54366ccf
--- /dev/null
+++ b/docker-compose/device-beamlet.yml
@@ -0,0 +1,47 @@
+#
+# Docker compose file that launches an interactive iTango session.
+#
+# Connect to the interactive session with 'docker attach itango'.
+# Disconnect with the Docker deattach sequence: <CTRL>+<P> <CTRL>+<Q>
+#
+# Defines:
+#   - itango: iTango interactive session
+#
+# Requires:
+#   - lofar-device-base.yml
+#
+version: '2'
+
+services:
+  device-beamlet:
+    image: device-beamlet
+    # build explicitly, as docker-compose does not understand a local image
+    # being shared among services.
+    build:
+        context: ..
+        dockerfile: docker-compose/lofar-device-base/Dockerfile
+        args:
+            SOURCE_IMAGE: ${LOCAL_DOCKER_REGISTRY_HOST}/${LOCAL_DOCKER_REGISTRY_USER}/tango-itango:${TANGO_ITANGO_VERSION}
+    container_name: ${CONTAINER_NAME_PREFIX}device-beamlet
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "100m"
+        max-file: "10"
+    networks:
+      - control
+    ports:
+      - "5712:5712" # unique port for this DS
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    volumes:
+        - ..:/opt/lofar/tango:rw
+    environment:
+      - TANGO_HOST=${TANGO_HOST}
+    working_dir: /opt/lofar/tango
+    entrypoint:
+      - bin/start-ds.sh
+      # configure CORBA to _listen_ on 0:port, but tell others we're _reachable_ through ${HOSTNAME}:port, since CORBA
+      # can't know about our Docker port forwarding
+      - l2ss-beamlet Beamlet STAT -v -ORBendPoint giop:tcp:0:5712 -ORBendPointPublish giop:tcp:${HOSTNAME}:5712
+    restart: unless-stopped
diff --git a/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py b/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py
index 03e7b75b4..394979833 100644
--- a/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py
+++ b/docker-compose/jupyter/ipython-profiles/stationcontrol-jupyter/startup/01-devices.py
@@ -8,7 +8,8 @@ xst = DeviceProxy("STAT/XST/1")
 unb2 = DeviceProxy("STAT/UNB2/1")
 boot = DeviceProxy("STAT/Boot/1")
 beam = DeviceProxy("STAT/Beam/1")
+beamlet = DeviceProxy("STAT/Beamlet/1")
 docker = DeviceProxy("STAT/Docker/1")
 
 # Put them in a list in case one wants to iterate
-devices = [apsct, apspu, recv, sdp, sst, xst, unb2, boot, beam, docker]
+devices = [apsct, apspu, recv, sdp, sst, xst, unb2, boot, beam, beamlet, docker]
diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh
index c6078a890..17fa54803 100755
--- a/sbin/run_integration_test.sh
+++ b/sbin/run_integration_test.sh
@@ -14,13 +14,13 @@ cd "$LOFAR20_DIR/docker-compose" || exit 1
 # Build only the required images, please do not build everything that makes CI
 # take really long to finish, especially grafana / jupyter / prometheus.
 # jupyter is physically large > 2.5gb and overlayfs is really slow.
-make build device-sdp device-recv device-sst device-unb2 device-xst
+make build device-sdp device-recv device-sst device-unb2 device-xst device-beamlet device-beam
 make build sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim
 make build databaseds dsconfig elk integration-test
 make build archiver-timescale hdbppts-cm hdbppts-es
 
 # Start and stop sequence
-make stop device-boot device-docker device-apsct device-apspu device-sdp device-recv device-sst device-unb2 device-xst device-beam sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim hdbppts-es hdbppts-cm archiver-timescale
+make stop device-boot device-docker device-apsct device-apspu device-sdp device-recv device-sst device-unb2 device-xst device-beamlet device-beam sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim hdbppts-es hdbppts-cm archiver-timescale
 make start databaseds dsconfig elk
 
 # Give dsconfig and databaseds time to start
@@ -38,7 +38,7 @@ make start sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim
 # Give the simulators time to start
 sleep 5
 
-make start device-boot device-apsct device-apspu device-sdp device-recv device-sst device-unb2 device-xst device-beam
+make start device-boot device-apsct device-apspu device-sdp device-recv device-sst device-unb2 device-xst device-beam device-beamlet
 # Archive devices -> starting order is important
 make start archiver-timescale hdbppts-cm hdbppts-es
 
diff --git a/tangostationcontrol/docs/source/devices/beamlet.rst b/tangostationcontrol/docs/source/devices/beamlet.rst
new file mode 100644
index 000000000..39e2a5887
--- /dev/null
+++ b/tangostationcontrol/docs/source/devices/beamlet.rst
@@ -0,0 +1,8 @@
+Beamlet
+====================
+
+The ``beamlet == DeviceProxy("STAT/Beamlet/1")`` device controls the creation and emission of beamlets. Each beamlet is a signal stream characterised by:
+
+- The set of antennas to use as input,
+- The pointing towards which to beamform these antennas,
+- A single subband (frequency) selected from the PPF.
diff --git a/tangostationcontrol/docs/source/index.rst b/tangostationcontrol/docs/source/index.rst
index 25c71f9f7..f72e468dd 100644
--- a/tangostationcontrol/docs/source/index.rst
+++ b/tangostationcontrol/docs/source/index.rst
@@ -20,6 +20,7 @@ Even without having access to any LOFAR2.0 hardware, you can install the full st
    interfaces/overview
    devices/using
    devices/beam
+   devices/beamlet
    devices/boot
    devices/docker
    devices/recv
diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg
index 99960c1ba..44e21e97a 100644
--- a/tangostationcontrol/setup.cfg
+++ b/tangostationcontrol/setup.cfg
@@ -36,6 +36,7 @@ console_scripts =
     l2ss-apsct = tangostationcontrol.devices.apsct:main
     l2ss-apspu = tangostationcontrol.devices.apspu:main
     l2ss-beam = tangostationcontrol.devices.beam:main
+    l2ss-beamlet = tangostationcontrol.devices.sdp.beamlet:main
     l2ss-boot = tangostationcontrol.devices.boot:main
     l2ss-docker-device = tangostationcontrol.devices.docker_device:main
     l2ss-observation = tangostationcontrol.devices.observation:main
diff --git a/tangostationcontrol/tangostationcontrol/devices/README.md b/tangostationcontrol/tangostationcontrol/devices/README.md
index f546fb339..08eaf1f67 100644
--- a/tangostationcontrol/tangostationcontrol/devices/README.md
+++ b/tangostationcontrol/tangostationcontrol/devices/README.md
@@ -11,8 +11,9 @@ If a new device is added, it will (likely) need to be referenced in several plac
 - Adjust `tangostationcontrol/tangostationcontrol/devices/boot.py` to add the device to the station initialisation sequence,
 - Add to `docker-compose/` to create a YaML file to start the device in a docker container. NOTE: it needs a unique 57xx port assigned,
 - Adjust `tangostationcontrol/setup.cfg` to add an entry point for the device in the package installation,
-- Add to `tangostationcontrol/tangostationcontrol/integration_test/devices/` to add an integration test,
+- Add to `tangostationcontrol/tangostationcontrol/integration_test/default/devices/` to add an integration test,
 - Adjust `sbin/run_integration_test.sh` to have the device started when running the integration tests,
+- Adjust `.gitlab-ci.yml` to add the device to the `docker_build_image_all` step and to create a `docker_build_image_device_XXX` step, 
 - Add to `docs/source/devices/` to mention the device in the end-user documentation.
 - Adjust `docs/source/index.rst` to include the newly created file in `docs/source/devices/`.
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py
index 0bee84c2e..3ae76d7f8 100644
--- a/tangostationcontrol/tangostationcontrol/devices/boot.py
+++ b/tangostationcontrol/tangostationcontrol/devices/boot.py
@@ -237,10 +237,11 @@ class Boot(lofar_device):
                        "STAT/APSCT/1",
                        "STAT/RECV/1",   # RCUs are input for SDP, so initialise them first
                        "STAT/UNB2/1",   # Uniboards host SDP, so initialise them first
-                       "STAT/SDP/1",    # SDP controls the mask for SST/XST/BST, so initialise it first
+                       "STAT/SDP/1",    # SDP controls the mask for SST/XST/BST/Beamlet, so initialise it first
                        "STAT/SST/1",
                        "STAT/XST/1",
-                       "STAT/Beam/1",   # Accesses RECV and SDP
+                       "STAT/Beamlet/1",
+                       "STAT/Beam/1",   # Accesses RECV and Beamlet
                       ],
     )
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
new file mode 100644
index 000000000..36ac75f4e
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+""" Beamlet Device Server for LOFAR2.0
+
+"""
+
+# PyTango imports
+from tango.server import device_property
+from tango import AttrWriteType
+# Additional import
+
+from tangostationcontrol.common.entrypoint import entry
+from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper
+from tangostationcontrol.devices.opcua_device import opcua_device
+from tangostationcontrol.devices.sdp.sdp import SDP
+
+import numpy
+
+__all__ = ["Beamlet", "main"]
+
+
+class Beamlet(opcua_device):
+
+    # -----------------
+    # Device Properties
+    # -----------------
+
+    FPGA_beamlet_output_hdr_eth_destination_mac_RW_default = device_property(
+        dtype='DevVarStringArray',
+        mandatory=True
+    )
+
+    FPGA_beamlet_output_hdr_ip_destination_address_RW_default = device_property(
+        dtype='DevVarStringArray',
+        mandatory=True
+    )
+
+    FPGA_beamlet_output_hdr_udp_destination_port_RW_default = device_property(
+        dtype='DevVarUShortArray',
+        mandatory=True
+    )
+
+    FPGA_beamlet_output_enable_RW_default = device_property(
+        dtype='DevVarBooleanArray',
+        mandatory=False,
+        default_value=[False] * 16
+    )
+
+    FPGA_beamlet_output_scale_RW_default = device_property(
+        dtype='DevVarDoubleArray',
+        mandatory=False,
+        default_value=[1.0] * 16
+    )
+
+    first_default_settings = [
+        'FPGA_beamlet_output_hdr_eth_destination_mac_RW',
+        'FPGA_beamlet_output_hdr_ip_destination_address_RW',
+        'FPGA_beamlet_output_hdr_udp_destination_port_RW',
+
+        'FPGA_beamlet_output_enable_RW'
+    ]
+
+    # ----------
+    # Attributes
+    # ----------
+
+    FPGA_beamlet_output_enable_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_enable_R"], datatype=numpy.bool_, dims=(16,))
+    FPGA_beamlet_output_enable_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    FPGA_beamlet_output_hdr_eth_destination_mac_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_eth_destination_mac_R"], datatype=numpy.str, dims=(16,))
+    FPGA_beamlet_output_hdr_eth_destination_mac_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_eth_destination_mac_RW"], datatype=numpy.str, dims=(16,), access=AttrWriteType.READ_WRITE)
+    FPGA_beamlet_output_hdr_ip_destination_address_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_ip_destination_address_R"], datatype=numpy.str, dims=(16,))
+    FPGA_beamlet_output_hdr_ip_destination_address_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_ip_destination_address_RW"], datatype=numpy.str, dims=(16,), access=AttrWriteType.READ_WRITE)
+    FPGA_beamlet_output_hdr_udp_destination_port_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_udp_destination_port_R"], datatype=numpy.uint16, dims=(16,))
+    FPGA_beamlet_output_hdr_udp_destination_port_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_udp_destination_port_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
+    FPGA_beamlet_output_scale_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_R"], datatype=numpy.double, dims=(16,))
+    FPGA_beamlet_output_scale_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_RW"], datatype=numpy.double, dims=(16,), access=AttrWriteType.READ_WRITE)
+
+    # List of OPC-UA CP for BF beamlets
+    S_pn = SDP.S_pn
+    N_pn = SDP.N_pn
+    A_pn = 6
+    N_pol = 2
+    N_beamlets_ctrl = 488
+    N_pol_bf = 2
+
+    # cint16[N_pn][A_pn][N_pol][N_beamlets_ctrl]
+    # Co-polarization BF weights. The N_pol = 2 parameter index is:
+    # 0 for antenna polarization X in beamlet polarization X,
+    # 1 for antenna polarization Y in beamlet polarization Y.
+    FPGA_bf_weights_xx_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_yy_R"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_xx_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_yy_RW"], datatype=numpy.int16, dims=(A_pn * N_pol *  N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+
+    # cint16[N_pn][A_pn][N_pol][N_beamlets_ctrl]
+    # Cross-polarization BF weights. The N_pol = 2 parameter index is (note that index pol in range 0:N_pol-1 is the antenna polarization, so index !pol is the beamlet polarization):
+    # 0 for antenna polarization X in beamlet polarization Y,
+    # 1 for antenna polarization Y in beamlet polarization X.
+    FPGA_bf_weights_xy_yx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_yx_R"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_xy_yx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_yx_RW"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+
+    # cint16[N_pn][N_pol_bf][A_pn][N_pol][N_beamlets_ctrl]
+    # Full Jones matrix of BF weights.
+    FPGA_bf_weights_xx_xy_yx_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_xy_yx_yy_R"], datatype=numpy.int16, dims=(N_pol_bf * A_pn * N_pol * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_xx_xy_yx_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_xy_yx_yy_RW"], datatype=numpy.int16, dims=(N_pol_bf * A_pn * N_pol * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+
+    # cint16[N_pn][A_pn][N_beamlets_ctrl]
+    # BF weights for separate access to respectively w_xx, w_xy, w_yx, and w_yy.
+    FPGA_bf_weights_xx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_xx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+    FPGA_bf_weights_xy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_xy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+    FPGA_bf_weights_yx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yx_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_yx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yx_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+    FPGA_bf_weights_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yy_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
+    FPGA_bf_weights_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yy_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
+
+    # ----------
+    # Summarising Attributes
+    # ----------
+
+    # --------
+    # Overloaded functions
+    # --------
+
+    # --------
+    # Commands
+    # --------
+
+# ----------
+# Run server
+# ----------
+def main(**kwargs):
+    """Main function of the SST Device module."""
+    return entry(Beamlet, **kwargs)
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
index a483aec43..9c5d157a0 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
@@ -97,16 +97,6 @@ class SDP(opcua_device):
     # Attributes
     # ----------
 
-    FPGA_beamlet_output_enable_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_enable_R"], datatype=numpy.bool_, dims=(16,))
-    FPGA_beamlet_output_enable_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    FPGA_beamlet_output_hdr_eth_destination_mac_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_eth_destination_mac_R"], datatype=numpy.str, dims=(16,))
-    FPGA_beamlet_output_hdr_eth_destination_mac_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_eth_destination_mac_RW"], datatype=numpy.str, dims=(16,), access=AttrWriteType.READ_WRITE)
-    FPGA_beamlet_output_hdr_ip_destination_address_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_ip_destination_address_R"], datatype=numpy.str, dims=(16,))
-    FPGA_beamlet_output_hdr_ip_destination_address_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_ip_destination_address_RW"], datatype=numpy.str, dims=(16,), access=AttrWriteType.READ_WRITE)
-    FPGA_beamlet_output_hdr_udp_destination_port_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_udp_destination_port_R"], datatype=numpy.uint16, dims=(16,))
-    FPGA_beamlet_output_hdr_udp_destination_port_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_hdr_udp_destination_port_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
-    FPGA_beamlet_output_scale_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_R"], datatype=numpy.uint32, dims=(16,))
-    FPGA_beamlet_output_scale_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
     FPGA_firmware_version_R = attribute_wrapper(comms_annotation=["FPGA_firmware_version_R"], datatype=numpy.str, dims=(16,))
     FPGA_boot_image_R = attribute_wrapper(comms_annotation=["FPGA_boot_image_R"], datatype=numpy.uint32, dims=(16,), doc="Active FPGA image (0=factory, 1=user)")
     FPGA_boot_image_RW = attribute_wrapper(comms_annotation=["FPGA_boot_image_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
@@ -151,10 +141,6 @@ class SDP(opcua_device):
     # TODO: needs to not be statically declared as this can change depending on the station and configuration
     S_pn = 12 # Number of ADC signal inputs per Processing Node (PN) FPGA.
     N_pn = 16 # Number of FPGAs per antenna band that is controlled via the SC - SDP interface.
-    A_pn = 6
-    N_pol = 2
-    N_beamlets_ctrl = 488
-    N_pol_bf = 2
 
     # OPC-UA MP only points for AIT
     FPGA_signal_input_mean_R = attribute_wrapper(comms_annotation=["FPGA_signal_input_mean_R"], datatype=numpy.double , dims=(S_pn, N_pn))
@@ -172,38 +158,6 @@ class SDP(opcua_device):
     FPGA_signal_input_samples_delay_R = attribute_wrapper(comms_annotation=["FPGA_signal_input_samples_delay_R"], datatype=numpy.uint32, dims=(S_pn, N_pn))
     FPGA_signal_input_samples_delay_RW = attribute_wrapper(comms_annotation=["FPGA_signal_input_samples_delay_RW"], datatype=numpy.uint32, dims=(S_pn, N_pn), access=AttrWriteType.READ_WRITE)
 
-    # List of OPC-UA CP for BF beamlets
-
-    # cint16[N_pn][A_pn][N_pol][N_beamlets_ctrl]
-    # Co-polarization BF weights. The N_pol = 2 parameter index is:
-    # 0 for antenna polarization X in beamlet polarization X,
-    # 1 for antenna polarization Y in beamlet polarization Y.
-    FPGA_bf_weights_xx_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_yy_R"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_xx_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_yy_RW"], datatype=numpy.int16, dims=(A_pn * N_pol *  N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-
-    # cint16[N_pn][A_pn][N_pol][N_beamlets_ctrl]
-    # Cross-polarization BF weights. The N_pol = 2 parameter index is (note that index pol in range 0:N_pol-1 is the antenna polarization, so index !pol is the beamlet polarization):
-    # 0 for antenna polarization X in beamlet polarization Y,
-    # 1 for antenna polarization Y in beamlet polarization X.
-    FPGA_bf_weights_xy_yx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_yx_R"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_xy_yx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_yx_RW"], datatype=numpy.int16, dims=(A_pn * N_pol * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-
-    # cint16[N_pn][N_pol_bf][A_pn][N_pol][N_beamlets_ctrl]
-    # Full Jones matrix of BF weights.
-    FPGA_bf_weights_xx_xy_yx_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_xy_yx_yy_R"], datatype=numpy.int16, dims=(N_pol_bf * A_pn * N_pol * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_xx_xy_yx_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_xy_yx_yy_RW"], datatype=numpy.int16, dims=(N_pol_bf * A_pn * N_pol * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-
-    # cint16[N_pn][A_pn][N_beamlets_ctrl]
-    # BF weights for separate access to respectively w_xx, w_xy, w_yx, and w_yy.
-    FPGA_bf_weights_xx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_xx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xx_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-    FPGA_bf_weights_xy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_xy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_xy_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-    FPGA_bf_weights_yx_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yx_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_yx_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yx_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-    FPGA_bf_weights_yy_R = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yy_R"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn))
-    FPGA_bf_weights_yy_RW = attribute_wrapper(comms_annotation=["FPGA_bf_weights_yy_RW"], datatype=numpy.int16, dims=(A_pn * N_beamlets_ctrl, N_pn), access=AttrWriteType.READ_WRITE)
-
     # ----------
     # Summarising Attributes
     # ----------
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
new file mode 100644
index 000000000..dcabcafc1
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
@@ -0,0 +1,32 @@
+
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+from .base import AbstractTestBases
+
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
+
+class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase):
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+        super().setUp("STAT/Beamlet/1")
+
+    def test_device_read_all_attributes(self):
+        # We need to connect to SDP first to read some of our attributes
+        self.sdp_proxy = self.setup_sdp()
+
+        super().test_device_read_all_attributes()
+    
+    def setup_sdp(self):
+        # setup SDP, on which this device depends
+        sdp_proxy = TestDeviceProxy("STAT/SDP/1")
+        sdp_proxy.off()
+        sdp_proxy.warm_boot()
+        sdp_proxy.set_defaults()
+        return sdp_proxy
-- 
GitLab