From a0daceeb65fcbf0d412c8413726953c9e36a19b1 Mon Sep 17 00:00:00 2001
From: Hannes Feldt <feldt@astron.nl>
Date: Mon, 17 Apr 2023 09:29:53 +0000
Subject: [PATCH] L2SS-1083 As a maintainer, I want to apply available
 HDF5-formatted calibration tables

---
 CDB/LOFAR_ConfigDb.json                       | 734 +++++++++---------
 Makefile                                      |  11 +
 docker-compose/.env                           |   3 +
 docker-compose/Makefile                       |  41 +-
 docker-compose/calibration-tables/Dockerfile  |   2 -
 docker-compose/calibration-tables/README.md   |   3 -
 docker-compose/device-antennafield.yml        |   3 -
 docker-compose/device-calibration.yml         |  58 ++
 docker-compose/lofar-device-base/Dockerfile   |   1 -
 docker-compose/object-storage.yml             |  48 ++
 .../CalTable-DevStation-HBA-150MHz.h5         | Bin 872448 -> 874584 bytes
 .../CalTable-DevStation-HBA-200MHz.h5         | Bin 872448 -> 874584 bytes
 .../CalTable-DevStation-HBA-250MHz.h5         | Bin 872448 -> 874584 bytes
 .../CalTable-DevStation-LBA-50MHz.h5          | Bin 1740824 -> 1740888 bytes
 sbin/run_integration_test.sh                  |   9 +-
 sbin/tag_and_push_docker_image.sh             |   2 +-
 tangostationcontrol/requirements.txt          |   1 +
 tangostationcontrol/setup.cfg                 |   1 +
 .../tangostationcontrol/common/__init__.py    |   8 +-
 .../tangostationcontrol/common/calibration.py | 254 ++++--
 .../common/calibration_table.py               |  24 -
 .../tangostationcontrol/devices/__init__.py   |   4 +-
 .../devices/antennafield.py                   | 396 +---------
 .../tangostationcontrol/devices/boot.py       |  28 +-
 .../devices/calibration.py                    | 212 +++++
 .../devices/test_device_antennafield.py       | 166 +---
 .../devices/test_device_calibration.py        | 229 ++++++
 .../devices/test_device_digitalbeam.py        | 107 ++-
 .../devices/test_device_observation.py        |   7 +-
 .../test/common/test_calibration.py           | 118 ++-
 30 files changed, 1459 insertions(+), 1011 deletions(-)
 create mode 100644 Makefile
 delete mode 100644 docker-compose/calibration-tables/Dockerfile
 delete mode 100644 docker-compose/calibration-tables/README.md
 create mode 100644 docker-compose/device-calibration.yml
 create mode 100644 docker-compose/object-storage.yml
 rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-150MHz.h5 (94%)
 rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-200MHz.h5 (92%)
 rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-250MHz.h5 (94%)
 rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-LBA-50MHz.h5 (94%)
 delete mode 100644 tangostationcontrol/tangostationcontrol/common/calibration_table.py
 create mode 100644 tangostationcontrol/tangostationcontrol/devices/calibration.py
 create mode 100644 tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py

diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json
index 8aa444d30..73fb4d573 100644
--- a/CDB/LOFAR_ConfigDb.json
+++ b/CDB/LOFAR_ConfigDb.json
@@ -1,392 +1,426 @@
 {
-    "servers": {
+  "servers": {
+    "StationManager": {
+      "STAT": {
         "StationManager": {
-            "STAT": {
-                "StationManager": {
-                    "STAT/StationManager/1": {
-                        "properties": {
-                            "Station_Name": [
-                                "DevStation"
-                            ],
-                            "Station_Number":[
-                                "999"
-                            ]
-                        }
-                    }
-                }
+          "STAT/StationManager/1": {
+            "properties": {
+              "Station_Name": [
+                "DevStation"
+              ],
+              "Station_Number": [
+                "999"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "Docker": {
+      "STAT": {
         "Docker": {
-            "STAT": {
-                "Docker": {
-                    "STAT/Docker/1": {}
-                }
-            }
-        },
+          "STAT/Docker/1": {}
+        }
+      }
+    },
+    "Calibration": {
+      "STAT": {
+        "Calibration": {
+          "STAT/Calibration/1": {}
+        }
+      }
+    },
+    "Configuration": {
+      "STAT": {
         "Configuration": {
-            "STAT": {
-                "Configuration": {
-                    "STAT/Configuration/1": {}
-                }
-            }
-        },
+          "STAT/Configuration/1": {}
+        }
+      }
+    },
+    "Observation": {
+      "STAT": {
         "Observation": {
-            "STAT": {
-                "Observation": {
-                }
-            }
-        },
+        }
+      }
+    },
+    "ObservationControl": {
+      "STAT": {
         "ObservationControl": {
-            "STAT": {
-                "ObservationControl": {
-                    "STAT/ObservationControl/1": {}
-                }
-            }
-        },
+          "STAT/ObservationControl/1": {}
+        }
+      }
+    },
+    "AntennaField": {
+      "STAT": {
         "AntennaField": {
-            "STAT": {
-                "AntennaField": {
-                    "STAT/AntennaField/HBA": {
-                        "properties": {
-                            "RECV_devices": ["STAT/RECV/1"]
-                        }
-                    }
-                }
+          "STAT/AntennaField/HBA": {
+            "properties": {
+              "RECV_devices": [
+                "STAT/RECV/1"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "PSOC": {
+      "STAT": {
         "PSOC": {
-            "STAT": {
-                "PSOC": {
-                    "STAT/PSOC/1": {
-                        "properties": {
-                            "SNMP_host": ["127.0.0.1"],
-                            "SNMP_community": ["public"],
-                            "SNMP_mib_dir": ["devices/mibs/PowerNet-MIB.mib"],
-                            "SNMP_timeout": ["10.0"],
-                            "SNMP_version": ["1"],
-                            "PSOC_sockets": [
-                                "socket_1",
-                                "socket_2",
-                                "socket_3",
-                                "socket_4",
-                                "socket_5",
-                                "socket_6",
-                                "socket_7",
-                                "socket_8"
-                            ]
-                        }
-                    }
-                }
+          "STAT/PSOC/1": {
+            "properties": {
+              "SNMP_host": [
+                "127.0.0.1"
+              ],
+              "SNMP_community": [
+                "public"
+              ],
+              "SNMP_mib_dir": [
+                "devices/mibs/PowerNet-MIB.mib"
+              ],
+              "SNMP_timeout": [
+                "10.0"
+              ],
+              "SNMP_version": [
+                "1"
+              ],
+              "PSOC_sockets": [
+                "socket_1",
+                "socket_2",
+                "socket_3",
+                "socket_4",
+                "socket_5",
+                "socket_6",
+                "socket_7",
+                "socket_8"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "PCON": {
+      "STAT": {
         "PCON": {
-            "STAT": {
-                "PCON": {
-                    "STAT/PCON/1": {
-                        "properties": {
-                            "SNMP_host": ["127.0.0.1"],
-                            "SNMP_community": ["public"],
-                            "SNMP_mib_dir": ["devices/mibs/ACC-MIB.mib"],
-                            "SNMP_timeout": ["10.0"],
-                            "SNMP_version": ["1"]
-                        }
-                    }
-                }
+          "STAT/PCON/1": {
+            "properties": {
+              "SNMP_host": [
+                "127.0.0.1"
+              ],
+              "SNMP_community": [
+                "public"
+              ],
+              "SNMP_mib_dir": [
+                "devices/mibs/ACC-MIB.mib"
+              ],
+              "SNMP_timeout": [
+                "10.0"
+              ],
+              "SNMP_version": [
+                "1"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "TemperatureManager": {
+      "STAT": {
         "TemperatureManager": {
-            "STAT": {
-                "TemperatureManager": {
-                    "STAT/TemperatureManager/1": {
-                        "properties": {
-                            "Alarm_Error_List": [
-                                "APSCT, APSCT_TEMP_error_R",
-                                "APSPU, APSPU_TEMP_error_R",
-                                "UNB2, UNB2_TEMP_error_R",
-                                "RECV, RECV_TEMP_error_R"
-                            ],
-                            "Shutdown_Device_List":[
-                                "STAT/SDP/1", "STAT/UNB2/1", "STAT/RECV/1", "STAT/APSCT/1", "STAT/CCD/1", "STAT/APSPU/1"
-                            ]
-                        }
-                    }
-                }
+          "STAT/TemperatureManager/1": {
+            "properties": {
+              "Alarm_Error_List": [
+                "APSCT, APSCT_TEMP_error_R",
+                "APSPU, APSPU_TEMP_error_R",
+                "UNB2, UNB2_TEMP_error_R",
+                "RECV, RECV_TEMP_error_R"
+              ],
+              "Shutdown_Device_List": [
+                "STAT/SDP/1",
+                "STAT/UNB2/1",
+                "STAT/RECV/1",
+                "STAT/APSCT/1",
+                "STAT/CCD/1",
+                "STAT/APSPU/1"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "TileBeam": {
+      "STAT": {
         "TileBeam": {
-            "STAT": {
-                "TileBeam": {
-                    "STAT/TileBeam/HBA": {
-                    }
-                }
-            }
-        },
+          "STAT/TileBeam/HBA": {
+          }
+        }
+      }
+    },
+    "Beamlet": {
+      "STAT": {
         "Beamlet": {
-            "STAT": {
-                "Beamlet": {
-                    "STAT/Beamlet/1": {
-                        "properties": {
-                            "FPGA_beamlet_output_hdr_eth_source_mac_RW_default": [
-                                "00:22:86:08:00:00",
-                                "00:22:86:08:00:01",
-                                "00:22:86:08:00:02",
-                                "00:22:86:08:00:03",
-                                "00:22:86:08:01:00",
-                                "00:22:86:08:01:01",
-                                "00:22:86:08:01:02",
-                                "00:22:86:08:01:03",
-                                "00:22:86:08:02:00",
-                                "00:22:86:08:02:01",
-                                "00:22:86:08:02:02",
-                                "00:22:86:08:02:03",
-                                "00:22:86:08:03:00",
-                                "00:22:86:08:03:01",
-                                "00:22:86:08:03:02",
-                                "00:22:86:08:03:03"
-                            ],
-                            "FPGA_beamlet_output_hdr_ip_source_address_RW_default": [
-                                "192.168.0.1",
-                                "192.168.0.2",
-                                "192.168.0.3",
-                                "192.168.0.4",
-                                "192.168.1.1",
-                                "192.168.1.2",
-                                "192.168.1.3",
-                                "192.168.1.4",
-                                "192.168.2.1",
-                                "192.168.2.2",
-                                "192.168.2.3",
-                                "192.168.2.4",
-                                "192.168.3.1",
-                                "192.168.3.2",
-                                "192.168.3.3",
-                                "192.168.3.4"
-                            ],
-                            "FPGA_beamlet_output_hdr_udp_source_port_RW_default": [
-                                "53248",
-                                "53249",
-                                "53250",
-                                "53251",
-                                "53252",
-                                "53253",
-                                "53254",
-                                "53255",
-                                "53256",
-                                "53257",
-                                "53258",
-                                "53259",
-                                "53260",
-                                "53261",
-                                "53262",
-                                "53263"
-                            ],
-                            "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"
-                            ]
-                        }
-                    }
-                }
+          "STAT/Beamlet/1": {
+            "properties": {
+              "FPGA_beamlet_output_hdr_eth_source_mac_RW_default": [
+                "00:22:86:08:00:00",
+                "00:22:86:08:00:01",
+                "00:22:86:08:00:02",
+                "00:22:86:08:00:03",
+                "00:22:86:08:01:00",
+                "00:22:86:08:01:01",
+                "00:22:86:08:01:02",
+                "00:22:86:08:01:03",
+                "00:22:86:08:02:00",
+                "00:22:86:08:02:01",
+                "00:22:86:08:02:02",
+                "00:22:86:08:02:03",
+                "00:22:86:08:03:00",
+                "00:22:86:08:03:01",
+                "00:22:86:08:03:02",
+                "00:22:86:08:03:03"
+              ],
+              "FPGA_beamlet_output_hdr_ip_source_address_RW_default": [
+                "192.168.0.1",
+                "192.168.0.2",
+                "192.168.0.3",
+                "192.168.0.4",
+                "192.168.1.1",
+                "192.168.1.2",
+                "192.168.1.3",
+                "192.168.1.4",
+                "192.168.2.1",
+                "192.168.2.2",
+                "192.168.2.3",
+                "192.168.2.4",
+                "192.168.3.1",
+                "192.168.3.2",
+                "192.168.3.3",
+                "192.168.3.4"
+              ],
+              "FPGA_beamlet_output_hdr_udp_source_port_RW_default": [
+                "53248",
+                "53249",
+                "53250",
+                "53251",
+                "53252",
+                "53253",
+                "53254",
+                "53255",
+                "53256",
+                "53257",
+                "53258",
+                "53259",
+                "53260",
+                "53261",
+                "53262",
+                "53263"
+              ],
+              "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"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "DigitalBeam": {
+      "STAT": {
         "DigitalBeam": {
-            "STAT": {
-                "DigitalBeam": {
-                    "STAT/DigitalBeam/HBA": {
-                        "properties": {
-                            "Beam_tracking_interval": [
-                                "1.0"
-                            ],
-                            "Beam_tracking_preparation_period": [
-                                "0.5"
-                            ]
-                        }
-                    }
-                }
+          "STAT/DigitalBeam/HBA": {
+            "properties": {
+              "Beam_tracking_interval": [
+                "1.0"
+              ],
+              "Beam_tracking_preparation_period": [
+                "0.5"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "Boot": {
+      "STAT": {
         "Boot": {
-            "STAT": {
-                "Boot": {
-                    "STAT/Boot/1": {}
-                }
-            }
-        },
+          "STAT/Boot/1": {}
+        }
+      }
+    },
+    "APSCT": {
+      "STAT": {
         "APSCT": {
-            "STAT": {
-                "APSCT": {
-                    "STAT/APSCT/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/APSCT/1": {
+            "properties": {
             }
-        },
+          }
+        }
+      }
+    },
+    "CCD": {
+      "STAT": {
         "CCD": {
-            "STAT": {
-                "CCD": {
-                    "STAT/CCD/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/CCD/1": {
+            "properties": {
             }
-        },
+          }
+        }
+      }
+    },
+    "APSPU": {
+      "STAT": {
         "APSPU": {
-            "STAT": {
-                "APSPU": {
-                    "STAT/APSPU/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/APSPU/1": {
+            "properties": {
             }
-        },
+          }
+        }
+      }
+    },
+    "RECV": {
+      "STAT": {
         "RECV": {
-            "STAT": {
-                "RECV": {
-                    "STAT/RECV/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/RECV/1": {
+            "properties": {
             }
-        },
+          }
+        }
+      }
+    },
+    "SDP": {
+      "STAT": {
         "SDP": {
-            "STAT": {
-                "SDP": {
-                    "STAT/SDP/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/SDP/1": {
+            "properties": {
             }
-        },
+          }
+        }
+      }
+    },
+    "BST": {
+      "STAT": {
         "BST": {
-            "STAT": {
-                "BST": {
-                    "STAT/BST/1": {
-                        "properties": {
-                            "Statistics_Client_UDP_Port": [
-                                "5003"
-                            ],
-                            "Statistics_Client_TCP_Port": [
-                                "5103"
-                            ],
-                            "FPGA_bst_offload_hdr_udp_destination_port_RW_default": [
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003",
-                                "5003"
-                            ]
-                        }
-                    }
-                }
+          "STAT/BST/1": {
+            "properties": {
+              "Statistics_Client_UDP_Port": [
+                "5003"
+              ],
+              "Statistics_Client_TCP_Port": [
+                "5103"
+              ],
+              "FPGA_bst_offload_hdr_udp_destination_port_RW_default": [
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003",
+                "5003"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "SST": {
+      "STAT": {
         "SST": {
-            "STAT": {
-                "SST": {
-                    "STAT/SST/1": {
-                        "properties": {
-                            "Statistics_Client_UDP_Port": [
-                                "5001"
-                            ],
-                            "Statistics_Client_TCP_Port": [
-                                "5101"
-                            ],
-                            "FPGA_sst_offload_hdr_udp_destination_port_RW_default": [
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001",
-                                "5001"
-                            ]
-                       }
-                    }
-                }
+          "STAT/SST/1": {
+            "properties": {
+              "Statistics_Client_UDP_Port": [
+                "5001"
+              ],
+              "Statistics_Client_TCP_Port": [
+                "5101"
+              ],
+              "FPGA_sst_offload_hdr_udp_destination_port_RW_default": [
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001",
+                "5001"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "XST": {
+      "STAT": {
         "XST": {
-            "STAT": {
-                "XST": {
-                    "STAT/XST/1": {
-                        "properties": {
-                            "Statistics_Client_UDP_Port": [
-                                "5002"
-                            ],
-                            "Statistics_Client_TCP_Port": [
-                                "5102"
-                            ],
-                            "FPGA_xst_offload_hdr_udp_destination_port_RW_default": [
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002",
-                                "5002"
-                            ]
-                        }
-                    }
-                }
+          "STAT/XST/1": {
+            "properties": {
+              "Statistics_Client_UDP_Port": [
+                "5002"
+              ],
+              "Statistics_Client_TCP_Port": [
+                "5102"
+              ],
+              "FPGA_xst_offload_hdr_udp_destination_port_RW_default": [
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002",
+                "5002"
+              ]
             }
-        },
+          }
+        }
+      }
+    },
+    "UNB2": {
+      "STAT": {
         "UNB2": {
-            "STAT": {
-                "UNB2": {
-                    "STAT/UNB2/1": {
-                        "properties": {
-                        }
-                    }
-                }
+          "STAT/UNB2/1": {
+            "properties": {
             }
+          }
         }
+      }
     }
+  }
 }
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..992ce93ad
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+# forward everything to docker-compose subdirectory
+$(firstword $(MAKECMDGOALS)):
+	$(MAKE) -C docker-compose $(MAKECMDGOALS)
+
+docker-compose:
+	$(MAKE) -C $@ $(MAKECMDGOALS)
+
+.PHONY: docker-compose tox
+
+%::
+	cd .
diff --git a/docker-compose/.env b/docker-compose/.env
index 7de2eff68..f410ee249 100644
--- a/docker-compose/.env
+++ b/docker-compose/.env
@@ -16,4 +16,7 @@ TANGO_STARTER_VERSION=2021-05-28
 MYSQL_ROOT_PASSWORD=secret
 MYSQL_PASSWORD=tango
 
+MINIO_ROOT_USER=minioadmin
+MINIO_ROOT_PASSWORD=minioadmin
+
 TEST_MODULE=default
diff --git a/docker-compose/Makefile b/docker-compose/Makefile
index 34decf3d5..8a691278e 100644
--- a/docker-compose/Makefile
+++ b/docker-compose/Makefile
@@ -5,6 +5,11 @@
 MAKEPATH := $(abspath $(lastword $(MAKEFILE_LIST)))
 BASEDIR := $(notdir $(patsubst %/,%,$(dir $(MAKEPATH))))
 
+ifeq (, $(shell which docker-compose))
+    DOCKER_COMPOSE = docker compose
+endif
+DOCKER_COMPOSE ?= docker-compose
+
 DOCKER_COMPOSE_ENV_FILE := $(abspath .env)
 COMPOSE_FILES := $(wildcard *.yml)
 COMPOSE_FILE_ARGS := --env-file $(DOCKER_COMPOSE_ENV_FILE) $(foreach yml,$(COMPOSE_FILES),-f $(yml))
@@ -164,33 +169,33 @@ DOCKER_COMPOSE_ARGS := DISPLAY=$(DISPLAY) \
 .DEFAULT_GOAL := help
 
 pull: ## pull the images from the Docker hub
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) pull
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) pull
 
 base: context ## Build base lofar device image
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --progress=plain lofar-device-base
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --progress=plain lofar-device-base
 
 base-nocache: context ## Rebuild base lofar device image
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain lofar-device-base
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain lofar-device-base
 
 build: base ## build images
 	# docker-compose does not support build dependencies, so manage those here
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --parallel --progress=plain $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --parallel --progress=plain $(SERVICE)
 
 build-nocache: base-nocache ## rebuild images from scratch
 	# docker-compose does not support build dependencies, so manage those here
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain $(SERVICE)
 
 up: base minimal  ## start the base TANGO system and prepare requested services
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) up --no-start --no-recreate $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) up --no-start --no-recreate $(SERVICE)
 
 run: base minimal  ## run a service using arguments and delete it afterwards
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) run -T --no-deps --rm $(SERVICE) $(SERVICE_ARGS)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) run -T --no-deps --rm $(SERVICE) $(SERVICE_ARGS)
 
 integration: minimal  ## run a service using arguments and delete it afterwards
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) run -T --no-deps --rm integration-test $(INTEGRATION_ARGS)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) run -T --no-deps --rm integration-test $(INTEGRATION_ARGS)
 
 down:  ## stop all services and tear down the system
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) down
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) down
 ifneq ($(NETWORK_MODE),host)
 	docker network inspect $(NETWORK_MODE) &> /dev/null && ([ $$? -eq 0 ] && docker network rm $(NETWORK_MODE)) || true
 	docker network inspect 9000-$(NETWORK_MODE) &> /dev/null && ([ $$? -eq 0 ] && docker network rm 9000-$(NETWORK_MODE)) || true
@@ -202,7 +207,7 @@ ifneq ($(NETWORK_MODE),host)
 	docker network inspect 9000-$(NETWORK_MODE) &> /dev/null || ([ $$? -ne 0 ] && docker network create 9000-$(NETWORK_MODE) -o com.docker.network.driver.mtu=9000)
 endif
 
-	$(DOCKER_COMPOSE_ARGS) docker-compose -f tango.yml -f networks.yml up --no-recreate -d
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) -f tango.yml -f networks.yml up --no-recreate -d
 
 context: ## Move the necessary files to create minimal docker context
 	@mkdir -p tmp
@@ -216,24 +221,24 @@ bootstrap: pull build # first start, initialise from scratch
 
 start: up ## start a service (usage: make start <servicename>)
 	if [ $(UNAME_S) = Linux ]; then touch ~/.Xauthority; chmod a+r ~/.Xauthority; fi
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) start $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) start $(SERVICE)
 
 stop:  ## stop a service (usage: make stop <servicename>)
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) stop $(SERVICE)
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) rm -f $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) stop $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) rm -f $(SERVICE)
 
 restart: ## restart a service (usage: make restart <servicename>)
 	make stop $(SERVICE) # cannot use dependencies, as that would allow start and stop to run in parallel..
 	make start $(SERVICE)
 
 attach:  ## attach a service to an existing Tango network
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(ATTACH_COMPOSE_FILE_ARGS) up --no-recreate -d $(SERVICE)
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(ATTACH_COMPOSE_FILE_ARGS) up --no-recreate -d $(SERVICE)
 
 TIME := 0
 await:  ## Await every container with total max timeout of 300, do not reset timeout
 	time=$(TIME); \
 	for i in $(SERVICE); do \
-		current_service=$$($(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps -q $${i}); \
+		current_service=$$($(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) ps -q $${i}); \
 		if [ -z "$${current_service}" ]; then \
 			continue; \
 		fi; \
@@ -254,14 +259,14 @@ await:  ## Await every container with total max timeout of 300, do not reset tim
 	done
 
 status:  ## show the container status
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) ps
 
 images:  ## show the container images
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) images
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) images
 
 clean: down  ## clear all TANGO database entries, and all containers
 	docker volume rm $(BASEDIR)_tangodb
-	$(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) rm -f
+	$(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) rm -f
 
 help:   ## show this help.
 	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
diff --git a/docker-compose/calibration-tables/Dockerfile b/docker-compose/calibration-tables/Dockerfile
deleted file mode 100644
index e7ce3d858..000000000
--- a/docker-compose/calibration-tables/Dockerfile
+++ /dev/null
@@ -1,2 +0,0 @@
-FROM nginx
-COPY caltables /usr/share/nginx/html
diff --git a/docker-compose/calibration-tables/README.md b/docker-compose/calibration-tables/README.md
deleted file mode 100644
index 10261e80c..000000000
--- a/docker-compose/calibration-tables/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Calibration Tables
-
-Service to serve HDF5 calibration table files over HTTP. Used by antennafield device.
diff --git a/docker-compose/device-antennafield.yml b/docker-compose/device-antennafield.yml
index 69ccb78c0..5c38de7f3 100644
--- a/docker-compose/device-antennafield.yml
+++ b/docker-compose/device-antennafield.yml
@@ -34,9 +34,6 @@ services:
       - "host.docker.internal:host-gateway"
     volumes:
       - ..:/opt/lofar/tango:rw
-      - type: bind
-        source: ./calibration-tables/caltables
-        target: /opt/calibration-tables
     environment:
       - TANGO_HOST=${TANGO_HOST}
       - TANGO_ZMQ_EVENT_PORT=5815
diff --git a/docker-compose/device-calibration.yml b/docker-compose/device-calibration.yml
new file mode 100644
index 000000000..2be7c73bd
--- /dev/null
+++ b/docker-compose/device-calibration.yml
@@ -0,0 +1,58 @@
+# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+#
+# 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.1'
+
+services:
+  device-calibration:
+    image: lofar-device-base
+    container_name: device-calibration
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "100m"
+        max-file: "10"
+    networks:
+      - control
+    ports:
+      - "5724:5724" # unique port for this DS
+      - "5824:5824" # ZeroMQ event port
+      - "5924:5924" # ZeroMQ heartbeat port
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    volumes:
+      - ..:/opt/lofar/tango:rw
+    environment:
+      - TANGO_HOST=${TANGO_HOST}
+      - TANGO_ZMQ_EVENT_PORT=5824
+      - TANGO_ZMQ_HEARTBEAT_PORT=5924
+      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
+      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
+    healthcheck:
+      test: l2ss-health STAT/Calibration/1
+      interval: 1m
+      timeout: 30s
+      retries: 3
+      start_period: 30s
+    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-calibration Calibration STAT -v -ORBendPoint giop:tcp:0:5724 -ORBendPointPublish giop:tcp:${HOSTNAME}:5724
+    restart: unless-stopped
+    stop_signal: SIGINT # request a graceful shutdown of Tango
+    stop_grace_period: 2s
+    depends_on:
+      - object-storage
diff --git a/docker-compose/lofar-device-base/Dockerfile b/docker-compose/lofar-device-base/Dockerfile
index 42254ec6f..7b49a6cfc 100644
--- a/docker-compose/lofar-device-base/Dockerfile
+++ b/docker-compose/lofar-device-base/Dockerfile
@@ -27,4 +27,3 @@ COPY lofar-device-base/casarc /home/tango/.casarc
 
 ENV TANGO_LOG_PATH=/var/log/tango
 RUN sudo mkdir -p /var/log/tango && sudo chmod a+rwx /var/log/tango
-RUN sudo mkdir -p /opt/calibration-tables && sudo chmod a+rwx /opt/calibration-tables
diff --git a/docker-compose/object-storage.yml b/docker-compose/object-storage.yml
new file mode 100644
index 000000000..5ba417b41
--- /dev/null
+++ b/docker-compose/object-storage.yml
@@ -0,0 +1,48 @@
+# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+#
+# Docker compose file that launches Minio as a S3 compatible object storage. A UI is available via http
+#
+# Connect by surfing to http://localhost:9001/
+# View logs through 'docker logs -f -t object-storage'
+
+version: '2.1'
+
+services:
+  object-storage:
+    image: minio/minio
+    container_name: object-storage
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "100m"
+        max-file: "10"
+    networks:
+      - control
+    volumes:
+      - object-storage:/data:rw
+    environment:
+      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
+      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
+    ports:
+      - "9000:9000"
+      - "9001:9001"
+    command: server --console-address ":9001" /data
+    restart: unless-stopped
+
+  init-object-storage:
+    image: minio/mc
+    networks:
+      - control
+    depends_on:
+      - object-storage
+    volumes:
+      - ..:/opt/lofar/tango:rw
+    entrypoint: ''
+    command: >
+      sh -c "mc alias set object-storage http://object-storage:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
+             mc mb --with-versioning object-storage/caltables
+             mc cp --recursive /opt/lofar/tango/docker-compose/object-storage/caltables/ object-storage/caltables/"
+
+volumes:
+  object-storage:
diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-150MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-150MHz.h5
similarity index 94%
rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-150MHz.h5
rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-150MHz.h5
index f2beb4b078b240d5dc6d52804999707d98b5524e..0cb1af7b0cc5b0517a00de2b41a294643138aed7 100644
GIT binary patch
delta 6758
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

delta 6586
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

diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-200MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-200MHz.h5
similarity index 92%
rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-200MHz.h5
rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-200MHz.h5
index d560a6f71dd2c935bd993ad947503e864e129bc6..8af326794446c05ce57a1b548146346dc3b8fae3 100644
GIT binary patch
delta 6761
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

delta 6583
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

diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-250MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-250MHz.h5
similarity index 94%
rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-250MHz.h5
rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-250MHz.h5
index 868cf35e8677d1070f048e4b6150d1a41291033e..af5980a9fbabf0f5aa9ef3cd55e89fea3cbeea2f 100644
GIT binary patch
delta 6739
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
E0Nd5lu>b%7

delta 6429
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

diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-LBA-50MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-LBA-50MHz.h5
similarity index 94%
rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-LBA-50MHz.h5
rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-LBA-50MHz.h5
index 6637b2019a02e3d02335b5393b908bc20146fa44..baef10d555c1b0101146cca088bcac9c37365d67 100644
GIT binary patch
delta 13082
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

delta 12777
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

diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh
index 2a814ffaa..9267f4692 100755
--- a/sbin/run_integration_test.sh
+++ b/sbin/run_integration_test.sh
@@ -1,6 +1,8 @@
 #!/bin/bash -e
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+#
+# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
+#
 
 # Usage function explains how parameters are parsed
 function usage {
@@ -74,7 +76,7 @@ echo '/usr/local/bin/wait-for-it.sh ${TANGO_HOST} --strict --timeout=300 -- true
 
 # Devices list is used to explitly word split when supplied to commands, must
 # disable shellcheck SC2086 for each case.
-DEVICES=(device-station-manager device-boot device-apsct device-ccd device-apspu device-sdp device-recv device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration)
+DEVICES=(device-station-manager device-boot device-apsct device-ccd device-apspu device-sdp device-recv device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration device-calibration)
 
 SIMULATORS=(sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim ccd-sim)
 
@@ -87,11 +89,12 @@ make build logstash integration-test http-json-schemas
 
 # Start and stop sequence
 make stop http-json-schemas
+make stop object-storage init-object-storage
 make stop "${DEVICES[@]}" "${SIMULATORS[@]}"
 make stop device-docker # this one does not test well in docker-in-docker
 make stop logstash
 
-make start logstash http-json-schemas
+make start logstash http-json-schemas object-storage init-object-storage
 
 # Update the dsconfig
 # Do not remove `bash`, otherwise statement ignored by gitlab ci shell!
diff --git a/sbin/tag_and_push_docker_image.sh b/sbin/tag_and_push_docker_image.sh
index 4ae48590b..7d398b075 100755
--- a/sbin/tag_and_push_docker_image.sh
+++ b/sbin/tag_and_push_docker_image.sh
@@ -1,5 +1,5 @@
 #!/bin/bash -e
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
 function usage {
diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt
index 7c6af9b85..86f4505eb 100644
--- a/tangostationcontrol/requirements.txt
+++ b/tangostationcontrol/requirements.txt
@@ -17,3 +17,4 @@ python-casacore >= 3.3.1 # LGPLv3
 etrs-itrs@git+https://github.com/brentjens/etrs-itrs # Apache 2
 lofarantpos >= 0.5.0 # Apache 2
 python-geohash >= 0.8.5 # Apache 2 / MIT
+minio >= 7.1.14 # Apache 2
diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg
index 5ccd4a8ac..2b87dbffc 100644
--- a/tangostationcontrol/setup.cfg
+++ b/tangostationcontrol/setup.cfg
@@ -54,6 +54,7 @@ console_scripts =
     l2ss-xst = tangostationcontrol.devices.sdp.xst:main
     l2ss-temperaturemanager = tangostationcontrol.devices.temperature_manager:main
     l2ss-configuration = tangostationcontrol.devices.configuration:main
+    l2ss-calibration = tangostationcontrol.devices.calibration:main
 
 # The following entry points should eventually be removed / replaced
     l2ss-hardware-device-template = tangostationcontrol.examples.HW_device_template:main
diff --git a/tangostationcontrol/tangostationcontrol/common/__init__.py b/tangostationcontrol/tangostationcontrol/common/__init__.py
index a212ea51d..bbdd80eaa 100644
--- a/tangostationcontrol/tangostationcontrol/common/__init__.py
+++ b/tangostationcontrol/tangostationcontrol/common/__init__.py
@@ -1,8 +1,6 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
 from .observation_controller import ObservationController
 
-__all__ = [
-    "ObservationController",
-]
+__all__ = ["ObservationController"]
diff --git a/tangostationcontrol/tangostationcontrol/common/calibration.py b/tangostationcontrol/tangostationcontrol/common/calibration.py
index 970781c70..e949179e7 100644
--- a/tangostationcontrol/tangostationcontrol/common/calibration.py
+++ b/tangostationcontrol/tangostationcontrol/common/calibration.py
@@ -1,17 +1,159 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+import logging
+import os
+import tempfile
+from typing import Dict
+from urllib.parse import urlparse
 
 import numpy
-from lofar_station_client.file_access import read_hdf5
+from lofar_station_client.file_access import member, attribute, read_hdf5
+from minio import Minio
+from tango import DeviceProxy
 
-from tangostationcontrol.common.calibration_table import (
-    CalibrationTable as Hdf5CalibrationTable,
-)
 from tangostationcontrol.common.constants import (
-    N_pol,
     N_subbands,
+    N_pol,
+    N_pn,
+    S_pn,
 )
 
+logger = logging.getLogger()
+
+
+class CalibrationData:
+    x: numpy.ndarray = member()
+    y: numpy.ndarray = member()
+
+
+class CalibrationTable:
+    observation_station: str = attribute()
+    observation_station_version: str = attribute()
+    observation_mode: str = attribute()
+    observation_source: str = attribute()
+    observation_date: str = attribute()
+    calibration_version: int = attribute()
+    calibration_name: str = attribute()
+    calibration_date: str = attribute()
+    antennas: Dict[str, CalibrationData] = member()
+
+
+class CalibrationManager:
+    @property
+    def url(self):
+        return self._url
+
+    @url.setter
+    def url(self, new_url):
+        self._url = new_url
+
+    def __init__(self, url: str, station_name: str):
+        self._url = url
+        self._station_name = station_name
+        self._tmp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
+        self.bucket_name = "caltabules"
+        self.prefix = self._station_name
+        self._init_minio()
+        self.sync_calibration_tables()
+
+    def _init_minio(self):
+        result = urlparse(self._url)
+        bucket_name, prefix, *_ = result.path[1:].split("/", 1) + [""]
+        if len(prefix) > 0:
+            self.prefix = "/".join([prefix.rstrip("/"), self._station_name])
+        if len(bucket_name) > 0:
+            self.bucket_name = bucket_name
+
+        self._storage = Minio(
+            result.netloc,
+            access_key=os.getenv("MINIO_ROOT_USER"),
+            secret_key=os.getenv("MINIO_ROOT_PASSWORD"),
+            secure=result.scheme == "https",
+        )
+
+    def sync_calibration_tables(self):
+        logger.debug(
+            f"Sync calibration tables from bucket {self.bucket_name} "
+            f"with prefix {self.prefix}/"
+        )
+        objects = self._storage.list_objects(self.bucket_name, prefix=f"{self.prefix}/")
+        for obj in objects:
+            filename = os.path.basename(obj.object_name)
+            self._storage.fget_object(
+                self.bucket_name,
+                obj.object_name,
+                os.path.join(self._tmp_dir.name, filename),
+            )
+
+    @staticmethod
+    def _band_to_reference_frequency(is_hba, rcu_band):
+        if is_hba:
+            match rcu_band:
+                case 1:
+                    return "200"
+                case 2:
+                    return "150"
+                case 4:
+                    return "250"
+        return "50"
+
+    def calibrate_subband_weights(self, antenna_field: DeviceProxy, sdp: DeviceProxy):
+        # -----------------------------------------------------------
+        #   Compute calibration of subband weights for the remaining
+        #   delay and loss corrections.
+        # -----------------------------------------------------------
+
+        # Mapping [antenna] -> [fpga][input]
+        antenna_to_sdp_mapping = antenna_field.Antenna_to_SDP_Mapping_R
+
+        # read-modify-write on [fpga][(input, polarisation)]
+        fpga_subband_weights = sdp.FPGA_subband_weights_RW.reshape(
+            (N_pn, S_pn, N_subbands)
+        )
+
+        antennafield_name = antenna_field.name().split("/")[2]
+        rcu_bands = antenna_field.RCU_band_select_RW
+        antenna_names = antenna_field.Antenna_Names_R
+
+        is_hba = antenna_field.Antenna_Type_R != "LBA"
+
+        for antenna_nr, rcu_band in enumerate(rcu_bands):
+            fpga_nr, input_nr = antenna_to_sdp_mapping[antenna_nr]
+
+            if input_nr == -1:
+                # skip unconnected antennas
+                continue
+
+            calibration_filename = os.path.join(
+                self._tmp_dir.name,
+                f"CalTable-{self._station_name}-{antennafield_name}"
+                f"-{self._band_to_reference_frequency(is_hba, rcu_band)}MHz.h5",
+            )
+            with read_hdf5(calibration_filename, CalibrationTable) as table:
+                # Retrieve data and convert them in the correct Tango attr shape
+                if (
+                    table.observation_station.casefold()
+                    != self._station_name.casefold()
+                ):
+                    logger.error(
+                        f"Expected calibration table for {self._station_name}, "
+                        f"but got {table.observation_station}"
+                    )
+
+                # set weights
+                fpga_subband_weights[fpga_nr, input_nr * N_pol + 0] = table.antennas[
+                    antenna_names[antenna_nr]
+                ].x
+
+                fpga_subband_weights[fpga_nr, input_nr * N_pol + 1] = table.antennas[
+                    antenna_names[antenna_nr]
+                ].y
+
+        # TODO(L2SS-1312): This should use atomic_read_modify_write
+        sdp.FPGA_subband_weights_RW = fpga_subband_weights.reshape(
+            N_pn, S_pn * N_subbands
+        )
+
 
 def delay_compensation(delays_seconds: numpy.ndarray, clock: int):
     """Return the delay compensation required to line up
@@ -48,7 +190,7 @@ def delay_compensation(delays_seconds: numpy.ndarray, clock: int):
     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)
+    return input_delays_samples, input_delays_subsample_seconds
 
 
 def dB_to_factor(dB: numpy.ndarray) -> numpy.ndarray:
@@ -56,6 +198,63 @@ def dB_to_factor(dB: numpy.ndarray) -> numpy.ndarray:
     return 10 ** (dB / 10)
 
 
+# SDP input delay calibration
+def calibrate_input_samples_delay(antenna_field: DeviceProxy, sdp: DeviceProxy):
+    # Mapping [antenna] -> [fpga][input]
+    antenna_to_sdp_mapping = antenna_field.Antenna_to_SDP_Mapping_R
+
+    # -----------------------------------------------------------
+    #   Set coarse delay compensation by delaying the samples.
+    # -----------------------------------------------------------
+
+    # The delay to apply, in samples [antenna]
+    # Correct for signal delays in the cables
+    signal_delay_seconds = antenna_field.Antenna_Cables_Delay_R
+
+    # compute the required compensation
+    clock = sdp.clock_RW
+    input_samples_delay, _ = delay_compensation(signal_delay_seconds, clock)
+
+    # read-modify-write on [fpga][(input, polarisation)]
+    fpga_signal_input_samples_delay = sdp.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]
+
+    # TODO(L2SS-1312): This should use atomic_read_modify_write
+    sdp.FPGA_signal_input_samples_delay_RW = fpga_signal_input_samples_delay
+
+
+# RCU calibration
+
+
+def calibrate_RCU_attenuator_dB(antenna_field: DeviceProxy):
+    # -----------------------------------------------------------
+    #   Set signal-input attenuation to compensate for
+    #   differences in cable length.
+    # -----------------------------------------------------------
+
+    # Correct for signal loss in the cables
+    signal_delay_loss = (
+        antenna_field.Antenna_Cables_Loss_R - antenna_field.Field_Attenuation_R
+    )
+
+    # return coarse attenuation to apply
+    rcu_attenuator_db, _ = loss_compensation(signal_delay_loss)
+    antenna_field.RCU_attenuator_dB_RW = rcu_attenuator_db
+
+
 def loss_compensation(losses_dB: numpy.ndarray):
     """Return the attenuation required to line up
     signals that are dampened by "lossed_dB" decibel.
@@ -89,42 +288,3 @@ def loss_compensation(losses_dB: numpy.ndarray):
     input_attenuation_remainder_factor = dB_to_factor(input_attenuation_remainder_dB)
 
     return (input_attenuation_integer_dB, input_attenuation_remainder_factor)
-
-
-class CalibrationTable:
-    """A class to represent calibration tables, and to retrieve calibration data"""
-
-    def __init__(self, filename: str):
-        # open the file and fill the calibration with actual data
-        with read_hdf5(filename, Hdf5CalibrationTable) as table:
-            self.table = table
-
-    @staticmethod
-    def complex_to_float(caltable: numpy.ndarray) -> numpy.ndarray:
-        """Numpy array conversion from
-        (antenna_list, N_pol, N_subbands),dtype=complex128 to
-        (antenna_list, N_pol, N_subbands, VALUES_PER_COMPLEX),dtype=float64
-        """
-        caltable_float = caltable.view(dtype=numpy.float64)
-        return caltable_float.reshape(caltable.shape + (2,))
-
-    def get_antenna_data(self, antenna_list: list) -> numpy.ndarray:
-        """
-        Returns a multi-dimensional array of the calibration data.
-        The shape is (antenna_list, N_pol, N_subbands), type is numpy.complex128
-        """
-        data = []
-
-        for antenna in antenna_list:
-            if antenna in self.table._data_reader.data.keys():
-                # load the calibration for this antenna
-                x_data = self.table._data_reader.data[antenna]["x"]
-                y_data = self.table._data_reader.data[antenna]["y"]
-                x = numpy.array(x_data, dtype=numpy.complex128)
-                y = numpy.array(y_data, dtype=numpy.complex128)
-                pol = numpy.array([x, y])
-                data.append(pol)
-            else:
-                # append data with default 1.0 value
-                data.append([[1.0] * N_subbands] * N_pol)
-        return numpy.array(data, dtype=numpy.complex128)
diff --git a/tangostationcontrol/tangostationcontrol/common/calibration_table.py b/tangostationcontrol/tangostationcontrol/common/calibration_table.py
deleted file mode 100644
index 19d4f809c..000000000
--- a/tangostationcontrol/tangostationcontrol/common/calibration_table.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#  Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-#  SPDX-License-Identifier: Apache-2.0
-
-from typing import Dict
-
-from lofar_station_client.file_access import member, attribute
-from numpy import ndarray
-
-
-class CalibrationData:
-    x: ndarray = member()
-    y: ndarray = member()
-
-
-class CalibrationTable:
-    observation_station: str = attribute()
-    observation_station_version: str = attribute()
-    observation_mode: str = attribute()
-    observation_source: str = attribute()
-    observation_date: str = attribute()
-    calibration_version: int = attribute()
-    calibration_name: str = attribute()
-    calibration_date: str = attribute()
-    antennas: Dict[str, CalibrationData] = member()
diff --git a/tangostationcontrol/tangostationcontrol/devices/__init__.py b/tangostationcontrol/tangostationcontrol/devices/__init__.py
index 68ddd5cdc..c92b61544 100644
--- a/tangostationcontrol/tangostationcontrol/devices/__init__.py
+++ b/tangostationcontrol/tangostationcontrol/devices/__init__.py
@@ -1,2 +1,2 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
index d2a2baa77..0a2213a68 100644
--- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
@@ -1,4 +1,4 @@
-#  Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
 #  SPDX-License-Identifier: Apache-2.0
 
 """ AntennaField Device Server for LOFAR2.0
@@ -28,8 +28,6 @@ from tangostationcontrol.beam.geo import GEO_to_GEOHASH
 from tangostationcontrol.beam.geo import ITRF_to_GEO
 from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets
 from tangostationcontrol.common.cables import cable_types
-from tangostationcontrol.common.calibration import delay_compensation
-from tangostationcontrol.common.calibration import loss_compensation
 from tangostationcontrol.common.constants import (
     N_elements,
     MAX_ANTENNA,
@@ -39,14 +37,10 @@ from tangostationcontrol.common.constants import (
     N_rcu,
     N_rcu_inp,
     N_pn,
-    S_pn,
     A_pn,
-    N_subbands,
-    VALUES_PER_COMPLEX,
 )
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.frequency_bands import bands, Band
-from tangostationcontrol.common.calibration import CalibrationTable
 from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
     log_exceptions,
@@ -55,13 +49,8 @@ from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.common.type_checking import type_not_sequence
 from tangostationcontrol.devices.device_decorators import fault_on_error, only_in_states
 from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
-from tangostationcontrol.devices.sdp.common import (
-    real_imag_to_weights,
-)
-from tangostationcontrol.devices.sdp.sdp import SDP
 
 logger = logging.getLogger()
-CALIBRATION_ROOT_DIR = "/opt/calibration-tables"
 
 __all__ = ["AntennaField", "AntennaToRecvMapper", "AntennaToSdpMapper", "main"]
 
@@ -299,7 +288,7 @@ class AntennaField(LOFARDevice):
     )
 
     SDP_device = device_property(
-        dtype=str,
+        dtype="DevString",
         doc="Which SDP device is processing this AntennaField.",
         mandatory=False,
         default_value="STAT/SDP/1",
@@ -373,6 +362,26 @@ class AntennaField(LOFARDevice):
         max_dim_y=MAX_ANTENNA,
     )
 
+    @attribute(
+        access=AttrWriteType.READ,
+        dtype=str,
+    )
+    def SDP_device_R(self):
+        return self.SDP_device
+
+    @attribute(access=AttrWriteType.READ, dtype="DevFloat")
+    def Field_Attenuation_R(self):
+        return self.Field_Attenuation
+
+    @attribute(
+        access=AttrWriteType.READ,
+        dtype=((numpy.int32,),),
+        max_dim_x=N_pol,
+        max_dim_y=MAX_ANTENNA,
+    )
+    def Control_to_RECV_mapping_R(self):
+        return numpy.array(self.Control_to_RECV_mapping).reshape(-1, 2)
+
     Frequency_Band_RW = attribute(
         doc="The selected frequency band of each antenna.",
         dtype=(str,),
@@ -402,65 +411,6 @@ class AntennaField(LOFARDevice):
         unit="dB",
     )
 
-    # ----- Calibration information
-
-    Calibration_SDP_Signal_Input_Samples_Delay_R = attribute(
-        doc="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="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_Fine_Calibration_Default_R = attribute(
-        doc="Computed calibration values for the fine calibration of each "
-        "antenna. Each antenna is represented by a (delay, phase_offset, "
-        "amplitude_scaling) triplet.",
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA * N_pol,
-        max_dim_x=3,
-    )
-    Calibration_SDP_Subband_Weights_R = attribute(
-        doc="Calibration values for the rows in sdp.FPGA_subband_weights_RW "
-        "relevant for our antennas. Each subband of each polarisation of "
-        "each antenna is represented by a real_imag number (real, imag). "
-        "Returns the measured values from "
-        "Calibration_SDP_Subband_Weights_XXXMHz if available, and values "
-        "computed from Calibration_SDP_Fine_Calibration_Default_R otherwise.",
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA * N_pol,
-        max_dim_x=N_subbands * VALUES_PER_COMPLEX,
-    )
-
-    # calibration loading
-
-    Calibration_SDP_Subband_Weights_50MHz_R = attribute(
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA,
-        max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX),
-    )
-    Calibration_SDP_Subband_Weights_150MHz_R = attribute(
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA,
-        max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX),
-    )
-    Calibration_SDP_Subband_Weights_200MHz_R = attribute(
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA,
-        max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX),
-    )
-    Calibration_SDP_Subband_Weights_250MHz_R = attribute(
-        dtype=((numpy.float64,),),
-        max_dim_y=MAX_ANTENNA,
-        max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX),
-    )
-
     # ----- Quality and usage information
 
     Antenna_Quality_R = attribute(
@@ -804,7 +754,7 @@ class AntennaField(LOFARDevice):
             ):  # NB: "value[0] in bands" holds at this point
                 raise ValueError(
                     f"All frequency bands must use the same clock. \
-                        These do not: {val} and {value[0,0]}."
+                        These do not: {val} and {value[0, 0]}."
                 )
 
         # apply settings on RECV
@@ -837,63 +787,6 @@ class AntennaField(LOFARDevice):
             sdp_nyquist_zone, self.sdp_proxy, "nyquist_zone_RW", None, numpy.uint32
         )
 
-        # frequencies changed, so we need to recalibrate
-        self.calibrate_recv()
-        self.calibrate_sdp()
-
-    def _create_calibration_table_filename(self, reference_frequency: str) -> str:
-        """Create a valid calibration table filename given a mode"""
-        antennafield_name = self.get_name().split("/")[2]
-        return (
-            f"{CALIBRATION_ROOT_DIR}/"
-            + f"CalTable-{self.station}-{antennafield_name}-{reference_frequency}MHz.h5"
-        )
-
-    def _get_calibration_table(self, reference_frequency: str) -> numpy.ndarray:
-        """Returns a calibration table of shape
-        (MAX_ANTENNA, N_pol, N_subbands, VALUES_PER_COMPLEX)"""
-        filename = self._create_calibration_table_filename(reference_frequency)
-        # Initialise table from relative HDF5 file
-        try:
-            calibration_data = CalibrationTable(filename)
-            # Retrieve data and convert them in the correct Tango attr shape
-            caltable = CalibrationTable.complex_to_float(
-                calibration_data.get_antenna_data(self.Antenna_Names)
-            )
-        except FileNotFoundError:
-            caltable = numpy.full(
-                (
-                    self.read_attribute("nr_antennas_R"),
-                    N_pol * N_subbands * VALUES_PER_COMPLEX,
-                ),
-                1.0,
-            )
-        return caltable
-
-    def read_Calibration_SDP_Subband_Weights_50MHz_R(self):
-        subband_weights = self._get_calibration_table(reference_frequency="50")
-        return numpy.reshape(
-            subband_weights, (self.read_attribute("nr_antennas_R"), -1)
-        )
-
-    def read_Calibration_SDP_Subband_Weights_150MHz_R(self):
-        subband_weights = self._get_calibration_table(reference_frequency="150")
-        return numpy.reshape(
-            subband_weights, (self.read_attribute("nr_antennas_R"), -1)
-        )
-
-    def read_Calibration_SDP_Subband_Weights_200MHz_R(self):
-        subband_weights = self._get_calibration_table(reference_frequency="200")
-        return numpy.reshape(
-            subband_weights, (self.read_attribute("nr_antennas_R"), -1)
-        )
-
-    def read_Calibration_SDP_Subband_Weights_250MHz_R(self):
-        subband_weights = self._get_calibration_table(reference_frequency="250")
-        return numpy.reshape(
-            subband_weights, (self.read_attribute("nr_antennas_R"), -1)
-        )
-
     def read_Antenna_Cables_R(self):
         return self.Antenna_Cables
 
@@ -921,150 +814,6 @@ class AntennaField(LOFARDevice):
             ]
         )
 
-    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_Fine_Calibration_Default_R(self):
-        def repeat_per_pol(arr):
-            # repeat values twice, and restore the shape (with the inner dimension
-            # being twice the size now)
-            return numpy.dstack((arr, arr)).reshape(
-                arr.shape[0] * N_pol, *arr.shape[1:]
-            )
-
-        # ----- Delay
-
-        # correct for signal delays in the cables (equal for both polarisations)
-        signal_delay_seconds = repeat_per_pol(
-            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 = repeat_per_pol(
-            numpy.zeros((self.read_attribute("nr_antennas_R"),), dtype=numpy.float64)
-        )
-
-        # ----- Amplitude
-
-        # correct for signal loss in the cables
-        signal_delay_loss = repeat_per_pol(
-            self.read_attribute("Antenna_Cables_Loss_R") - self.Field_Attenuation
-        )
-
-        # return fine scaling to apply
-        _, input_attenuation_remaining_factor = loss_compensation(signal_delay_loss)
-
-        # Return as (delay, phase_offset, amplitude) triplet per polarisation
-        return numpy.stack(
-            (
-                input_delay_subsample_seconds,
-                phase_offsets,
-                input_attenuation_remaining_factor,
-            ),
-            axis=1,
-        )
-
-    def _rcu_band_to_calibration_table(self) -> dict:
-        """
-        Returns the SDP subband weights to apply per RCU band.
-        """
-        nr_antennas = self.read_attribute("nr_antennas_R")
-
-        # construct selector for the right calibration table
-        if self.Antenna_Type == "LBA":
-            rcu_band_to_caltable = {
-                1: self.read_attribute(
-                    "Calibration_SDP_Subband_Weights_50MHz_R"
-                ).tolist(),
-                2: self.read_attribute(
-                    "Calibration_SDP_Subband_Weights_50MHz_R"
-                ).tolist(),
-            }
-        else:  # HBA
-            rcu_band_to_caltable = {
-                2: self.read_attribute(
-                    "Calibration_SDP_Subband_Weights_150MHz_R"
-                ).tolist(),
-                1: self.read_attribute(
-                    "Calibration_SDP_Subband_Weights_200MHz_R"
-                ).tolist(),
-                4: self.read_attribute(
-                    "Calibration_SDP_Subband_Weights_250MHz_R"
-                ).tolist(),
-            }
-
-        # reshape them into their actual form
-        for band, caltable in rcu_band_to_caltable.items():
-            rcu_band_to_caltable[band] = numpy.array(caltable).reshape(
-                nr_antennas, N_pol, N_subbands, VALUES_PER_COMPLEX
-            )
-
-        return rcu_band_to_caltable
-
-    def read_Calibration_SDP_Subband_Weights_R(self):
-        # obtain the calibration tables and the RCU bands they depend on
-        rcu_bands = self.read_attribute("RCU_band_select_RW")
-        rcu_band_to_caltable = self._rcu_band_to_calibration_table()
-
-        # antenna mapping onto RECV
-        control_to_recv_mapping = numpy.array(self.Control_to_RECV_mapping).reshape(
-            -1, 2
-        )
-        recvs = control_to_recv_mapping[:, 0]  # first column is RECV device number
-
-        # antenna mapping onto SDP
-        antenna_to_sdp_mapping = self.read_attribute("Antenna_to_SDP_Mapping_R")
-
-        # construct the subband weights based on the rcu_band of each antenna,
-        # combining the relevant tables.
-        nr_antennas = self.read_attribute("nr_antennas_R")
-        subband_weights = numpy.zeros(
-            (nr_antennas, N_pol, N_subbands, VALUES_PER_COMPLEX), dtype=numpy.float64
-        )
-        for antenna_nr, rcu_band in enumerate(rcu_bands):
-            # Skip antennas not connected to RECV. These do not have a valid RCU band
-            # selected.
-            if recvs[antenna_nr] == 0:
-                continue
-
-            # Skip antennas not connected to SDP. They must retain a weight of 0.
-            if antenna_to_sdp_mapping[antenna_nr, 1] == -1:
-                continue
-
-            subband_weights[antenna_nr, :, :, :] = rcu_band_to_caltable[rcu_band][
-                antenna_nr, :, :, :
-            ]
-
-        return subband_weights.reshape(
-            nr_antennas * N_pol, N_subbands * VALUES_PER_COMPLEX
-        )
-
-    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") - self.Field_Attenuation
-        )
-
-        # 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
 
@@ -1322,105 +1071,6 @@ class AntennaField(LOFARDevice):
             antenna_type, self.sdp_proxy, "antenna_type_RW", None, str
         )
 
-    @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 attenuation 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 = numpy.full((N_pn, A_pn * N_pol), None)
-
-        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.atomic_read_modify_write_attribute(
-            fpga_signal_input_samples_delay,
-            self.sdp_proxy,
-            "FPGA_signal_input_samples_delay_RW",
-            None,
-            numpy.uint32,
-        )
-
-        # -----------------------------------------------------------
-        #   Compute calibration of subband weights for the remaining
-        #   delay and loss corrections.
-        # -----------------------------------------------------------
-
-        # obtain caltable
-        caltable = self.read_attribute("Calibration_SDP_Subband_Weights_R")
-
-        # read-modify-write on [fpga][(input, polarisation)]
-        fpga_subband_weights = numpy.full((N_pn, S_pn, N_subbands), None)
-
-        for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping):
-            if input_nr == -1:
-                # skip unconnected antennas
-                continue
-
-            # set weights
-            fpga_subband_weights[
-                fpga_nr, input_nr * N_pol + 0, :
-            ] = real_imag_to_weights(
-                caltable[antenna_nr * N_pol + 0, :], SDP.SUBBAND_UNIT_WEIGHT
-            )
-            fpga_subband_weights[
-                fpga_nr, input_nr * N_pol + 1, :
-            ] = real_imag_to_weights(
-                caltable[antenna_nr * N_pol + 1, :], SDP.SUBBAND_UNIT_WEIGHT
-            )
-
-        self.atomic_read_modify_write_attribute(
-            fpga_subband_weights.reshape(N_pn, S_pn * N_subbands),
-            self.sdp_proxy,
-            "FPGA_subband_weights_RW",
-            None,
-            numpy.uint32,
-        )
-
     @command(dtype_in=DevVarFloatArray, dtype_out=DevVarLongArray)
     def calculate_HBAT_bf_delay_steps(self, delays: numpy.ndarray):
         num_tiles = self.read_nr_antennas_R()
diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py
index 6bc05990b..5e4d79d4f 100644
--- a/tangostationcontrol/tangostationcontrol/devices/boot.py
+++ b/tangostationcontrol/tangostationcontrol/devices/boot.py
@@ -1,5 +1,5 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
 """ Boot Device Server for LOFAR2.0
 
@@ -17,6 +17,7 @@ from tango import AttrWriteType, DeviceProxy, DevState, DevSource
 from tango import DebugIt
 from tango.server import command
 from tango.server import device_property, attribute
+
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
@@ -148,13 +149,16 @@ class DevicesInitialiser(object):
         # reset initialisation parameters
         self.progress = 0
 
+        logger.debug(self.devices.keys())
         # restart devices in order
         for num_restarted_devices, device in enumerate(self.devices.keys(), 1):
             # allow resuming by skipping already initialised devices
             if self.device_initialised[device]:
+                logger.debug(f"{device} already initialised")
                 continue
 
             if self.is_available(device):
+                logger.debug(f"{device} available")
                 if (
                     self.reboot
                     or self.devices[device].state() not in OPERATIONAL_STATES
@@ -246,16 +250,22 @@ class Boot(LOFARDevice):
         dtype="DevVarStringArray",
         mandatory=False,
         default_value=[
-            "STAT/Docker/1",  # Docker controls the device containers, so it goes before anything else
-            "STAT/Configuration/1",  # Configuration device loads and update station configuration
-            "STAT/PSOC/1",  # PSOC boot early to detect power delivery failure as fast as possible
-            "STAT/PCON/1",  # PCON boot early because it is responsible for power delivery.
-            "STAT/APSPU/1",  # APS Power Units control other hardware we want to initialise
+            "STAT/Docker/1",
+            # Docker controls the device containers, so it goes before anything else
+            "STAT/Configuration/1",
+            # Configuration device loads and update station configuration
+            "STAT/PSOC/1",
+            # PSOC boot early to detect power delivery failure as fast as possible
+            "STAT/PCON/1",
+            # PCON boot early because it is responsible for power delivery.
+            "STAT/APSPU/1",
+            # APS Power Units control other hardware we want to initialise
             "STAT/APSCT/1",
             "STAT/CCD/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/Beamlet, so initialise it first
+            "STAT/SDP/1",
+            # SDP controls the mask for SST/XST/BST/Beamlet, so initialise it first
             "STAT/BST/1",
             "STAT/SST/1",
             "STAT/XST/1",
@@ -267,6 +277,8 @@ class Boot(LOFARDevice):
             "STAT/DigitalBeam/HBA",  # Accessed SDP and Beamlet
             # "STAT/DigitalBeam/LBA",   # Accessed SDP and Beamlet
             "STAT/TemperatureManager/1",
+            "STAT/Calibration/1",
+            # Calibration device loads and update station calibration
         ],
     )
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/calibration.py b/tangostationcontrol/tangostationcontrol/devices/calibration.py
new file mode 100644
index 000000000..d5894f836
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/calibration.py
@@ -0,0 +1,212 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+import logging
+
+from tango import DeviceProxy, EventType, Database, AttributeProxy
+from tango.server import device_property, command, attribute
+
+from tangostationcontrol.common.calibration import (
+    CalibrationManager,
+    calibrate_RCU_attenuator_dB,
+    calibrate_input_samples_delay,
+)
+from tangostationcontrol.common.entrypoint import entry
+from tangostationcontrol.common.lofar_logging import (
+    device_logging_to_python,
+    log_exceptions,
+)
+from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
+from tangostationcontrol.devices.antennafield import AntennaField
+from tangostationcontrol.devices.device_decorators import only_in_states
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
+from tangostationcontrol.devices.sdp.sdp import SDP
+
+logger = logging.getLogger()
+__all__ = ["Calibration", "main"]
+
+
+@device_logging_to_python()
+class Calibration(LOFARDevice):
+    """Manages the calibration of antenna field, SDP and RECV devices."""
+
+    def __init__(self, cl, name):
+        super().__init__(cl, name)
+        self._calibration_manager: CalibrationManager = None
+        self.event_subscriptions: list = []
+        self.sdp_proxies: dict = {}
+        self.ant_proxies: dict = {}
+        self.station_name: AttributeProxy = None
+
+    @log_exceptions()
+    def _frequency_band_changed_event(self, event):
+        """Trigger on external changes in frequency settings."""
+
+        if event.err:
+            # little we can do here. note that errors are also
+            # thrown if the device we subscribed to is offline
+            return
+
+        logger.info(
+            f"Received attribute change event from {event.device}: "
+            f"{event.attr_value.name} := {event.attr_value.value}"
+        )
+
+        if self.dev_state() not in DEFAULT_COMMAND_STATES:
+            logger.warning("Device not active. Ignore freq band changed event")
+            return
+
+        # frequencies changed, so we need to recalibrate
+        self.calibrate_recv(event.device)
+        self.calibrate_sdp(event.device)
+
+    @log_exceptions()
+    def _clock_changed_event(self, event):
+        """Trigger on external changes in frequency settings."""
+        if event.err:
+            # little we can do here. note that errors are also
+            # thrown if the device we subscribed to is offline
+            return
+
+        logger.info(
+            f"Received attribute change event from {event.device}: "
+            f"{event.attr_value.name} := {event.attr_value.value}"
+        )
+
+        if self.dev_state() not in DEFAULT_COMMAND_STATES:
+            logger.warning("Device not active. Ignore clock changed event")
+            return
+
+        for k, ant in self.ant_proxies.items():
+            if ant.SDP_device_R.casefold() == str(event.device).casefold():
+                logger.info(f"Re-calibrate antenna field {k}")
+                self.calibrate_sdp(k)
+                self.calibrate_recv(k)
+
+    Calibration_Table_Base_URL = device_property(
+        doc="Base URL of the calibration tables",
+        dtype="DevString",
+        mandatory=False,
+        update_db=True,
+        default_value="http://object-storage:9000/caltables",
+    )
+
+    @attribute(dtype=(str,), max_dim_x=20)
+    def AntennaFields_Monitored_R(self):
+        return list(self.ant_proxies.keys())
+
+    @attribute(dtype=(str,), max_dim_x=20)
+    def SDPs_Monitored_R(self):
+        return list(self.sdp_proxies.keys())
+
+    @command
+    def download_calibration_tables(self):
+        self._calibration_manager.sync_calibration_tables()
+        for ant in self.ant_proxies.keys():
+            self.calibrate_recv(ant)
+            self.calibrate_sdp(ant)
+
+    @command(dtype_in=str)
+    @only_in_states(DEFAULT_COMMAND_STATES)
+    def calibrate_recv(self, device: str):
+        """Calibrate RECV for our antennas.
+
+        Run whenever the following changes:
+            sdp.clock_RW
+            antennafield.RCU_band_select_RW
+        """
+
+        # -----------------------------------------------------------
+        #   Set signal-input attenuation to compensate for
+        #   differences in cable length.
+        # -----------------------------------------------------------
+
+        calibrate_RCU_attenuator_dB(self.ant_proxies[device])
+
+    @command(dtype_in=str)
+    @only_in_states(DEFAULT_COMMAND_STATES)
+    def calibrate_sdp(self, device: str):
+        """Calibrate SDP for our antennas.
+
+        Run whenever the following changes:
+            sdp.clock_RW
+            antennafield.RCU_band_select_RW
+        """
+
+        ant_proxy = self.ant_proxies[device]
+        sdp_device = str(ant_proxy.SDP_device_R)
+        sdp_proxy = self.sdp_proxies[sdp_device]
+
+        calibrate_input_samples_delay(ant_proxy, sdp_proxy)
+
+        self._calibration_manager.calibrate_subband_weights(ant_proxy, sdp_proxy)
+
+    # --------
+    # Overloaded functions
+    # --------
+
+    def configure_for_initialise(self):
+        super().configure_for_initialise()
+
+        self.station_name = AttributeProxy("STAT/StationManager/1/station_name_R")
+        self._calibration_manager = CalibrationManager(
+            self.Calibration_Table_Base_URL, self.station_name.read().value
+        )
+
+        db = Database()
+        devices = db.get_device_exported_for_class(AntennaField.__name__)
+        for d in devices:
+            logger.debug("found antenna field device " + str(d))
+        self.ant_proxies = {d: DeviceProxy(d) for d in devices}
+
+        devices = db.get_device_exported_for_class(SDP.__name__)
+        for d in devices:
+            logger.debug("found SDP device " + str(d))
+        self.sdp_proxies = {d: DeviceProxy(d) for d in devices}
+
+        # subscribe to events to notice setting changes in SDP that determine the
+        # input frequency
+        for prx in self.ant_proxies.values():
+            self.event_subscriptions.append(
+                (
+                    prx,
+                    prx.subscribe_event(
+                        "Frequency_Band_RW",
+                        EventType.CHANGE_EVENT,
+                        self._frequency_band_changed_event,
+                        stateless=True,
+                    ),
+                )
+            )
+        for prx in self.sdp_proxies.values():
+            self.event_subscriptions.append(
+                (
+                    prx,
+                    prx.subscribe_event(
+                        "clock_RW",
+                        EventType.CHANGE_EVENT,
+                        self._clock_changed_event,
+                        stateless=True,
+                    ),
+                )
+            )
+
+    def configure_for_off(self):
+        super().configure_for_off()
+
+        # unsubscribe from all events
+        subscriptions = self.event_subscriptions
+        self.event_subscriptions = []
+        for prx, s in subscriptions:
+            prx.unsubscribe_event(s)
+
+
+# ----------
+# Run server
+# ----------
+def main(**kwargs):
+    """Main function of the Calibration module."""
+    return entry(Calibration, **kwargs)
+
+
+if __name__ == "__main__":
+    main()
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 fafcc3308..d9f95b6a2 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py
@@ -1,9 +1,11 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C)  2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
-import numpy
 import time
+
+import numpy
 from tango import DevState
+
 from tangostationcontrol.common.constants import (
     N_elements,
     MAX_ANTENNA,
@@ -14,68 +16,14 @@ from tangostationcontrol.common.constants import (
     CLK_200_MHZ,
     N_pn,
     S_pn,
-    N_subbands,
-    VALUES_PER_COMPLEX,
 )
 from tangostationcontrol.common.frequency_bands import bands
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
-
 from .base import AbstractTestBases
 
 
 class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
-    HBA_ANTENNA_NAMES = [
-        "H0",
-        "H1",
-        "H2",
-        "H3",
-        "H4",
-        "H5",
-        "H6",
-        "H7",
-        "H8",
-        "H9",
-        "H10",
-        "H11",
-        "H12",
-        "H13",
-        "H14",
-        "H15",
-        "H16",
-        "H17",
-        "H18",
-        "H19",
-        "H20",
-        "H21",
-        "H22",
-        "H23",
-        "H24",
-        "H25",
-        "H26",
-        "H27",
-        "H28",
-        "H29",
-        "H30",
-        "H31",
-        "H32",
-        "H33",
-        "H34",
-        "H35",
-        "H36",
-        "H37",
-        "H38",
-        "H39",
-        "H40",
-        "H41",
-        "H42",
-        "H43",
-        "H44",
-        "H45",
-        "H46",
-        "H47",
-    ]
-
     def setUp(self):
         self.stationmanager_proxy = self.setup_stationmanager_proxy()
         super().setUp("STAT/AntennaField/HBA")
@@ -452,107 +400,3 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase):
                     antennafield_proxy.read_attribute("Frequency_Band_RW").value[:2],
                     err_msg=f"{band.name}",
                 )
-
-    def test_calibrate_recv(self):
-        calibration_properties = {
-            "Antenna_Type": ["LBA"],
-            "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2),
-            "Control_to_RECV_mapping":
-            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47]
-            numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(),
-        }
-
-        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": ["HBA"],
-            "Antenna_Names": self.HBA_ANTENNA_NAMES,
-            "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2),
-            "Antenna_to_SDP_Mapping": [0, 1, 0, 0]
-            + [-1, -1] * (DEFAULT_N_HBA_TILES - 2),
-            "Control_to_RECV_mapping":
-            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47]
-            numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(),
-        }
-
-        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])
-
-    def test_calibration_table(self):
-        """Test whether calibration table are correctly retrieved and reshaped"""
-        calibration_properties = {
-            "Antenna_Type": ["HBA"],
-            "Antenna_Names": self.HBA_ANTENNA_NAMES,
-            "Control_to_RECV_mapping": [1, 1, 1, 0]
-            + [-1, -1] * (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.warm_boot()
-
-        calibration_table = antennafield_proxy.Calibration_SDP_Subband_Weights_250MHz_R
-
-        # test whether the shape is correct
-        shape = calibration_table.shape
-        self.assertEqual(
-            shape,
-            (antennafield_proxy.nr_antennas_R, N_pol * N_subbands * VALUES_PER_COMPLEX),
-            f"Wrong shape, got {shape}",
-        )
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py
new file mode 100644
index 000000000..875775647
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py
@@ -0,0 +1,229 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+import numpy
+from tango import DevState
+
+from tangostationcontrol.common.constants import (
+    N_rcu,
+    N_rcu_inp,
+    DEFAULT_N_HBA_TILES,
+    CLK_200_MHZ,
+)
+from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
+from .base import AbstractTestBases
+
+
+class TestCalibrationDevice(AbstractTestBases.TestDeviceBase):
+    HBA_ANTENNA_NAMES = [
+        "H0",
+        "H1",
+        "H2",
+        "H3",
+        "H4",
+        "H5",
+        "H6",
+        "H7",
+        "H8",
+        "H9",
+        "H10",
+        "H11",
+        "H12",
+        "H13",
+        "H14",
+        "H15",
+        "H16",
+        "H17",
+        "H18",
+        "H19",
+        "H20",
+        "H21",
+        "H22",
+        "H23",
+        "H24",
+        "H25",
+        "H26",
+        "H27",
+        "H28",
+        "H29",
+        "H30",
+        "H31",
+        "H32",
+        "H33",
+        "H34",
+        "H35",
+        "H36",
+        "H37",
+        "H38",
+        "H39",
+        "H40",
+        "H41",
+        "H42",
+        "H43",
+        "H44",
+        "H45",
+        "H46",
+        "H47",
+    ]
+
+    def setUp(self):
+        self.stationmanager_proxy = self.setup_stationmanager_proxy()
+        super().setUp("STAT/Calibration/1")
+        self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA")
+        self.antennafield_proxy.put_property(
+            {
+                "RECV_devices": ["STAT/RECV/1"],
+                "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.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] * DEFAULT_N_HBA_TILES,
+                "Control_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES,
+            }
+        )
+
+    @staticmethod
+    def shutdown_recv():
+        recv_proxy = TestDeviceProxy("STAT/RECV/1")
+        recv_proxy.off()
+
+    @staticmethod
+    def shutdown_sdp():
+        sdp_proxy = TestDeviceProxy("STAT/SDP/1")
+        sdp_proxy.off()
+
+    @staticmethod
+    def shutdown(device: str):
+        def off():
+            proxy = TestDeviceProxy(device)
+            proxy.off()
+
+        return off
+
+    def setup_recv_proxy(self):
+        # setup RECV
+        recv_proxy = TestDeviceProxy("STAT/RECV/1")
+        recv_proxy.off()
+        recv_proxy.warm_boot()
+        recv_proxy.set_defaults()
+        return recv_proxy
+
+    def setup_sdp_proxy(self):
+        # setup SDP
+        sdp_proxy = TestDeviceProxy("STAT/SDP/1")
+        sdp_proxy.off()
+        sdp_proxy.warm_boot()
+        return sdp_proxy
+
+    def setup_proxy(self, dev: str):
+        # setup SDP
+        proxy = TestDeviceProxy(dev)
+        proxy.off()
+        proxy.warm_boot()
+        self.addCleanup(self.shutdown(dev))
+        return proxy
+
+    def setup_stationmanager_proxy(self):
+        """Setup StationManager"""
+        stationmanager_proxy = TestDeviceProxy("STAT/StationManager/1")
+        stationmanager_proxy.off()
+        stationmanager_proxy.boot()
+        self.assertEqual(stationmanager_proxy.state(), DevState.ON)
+        return stationmanager_proxy
+
+    def test_calibrate_recv(self):
+        calibration_properties = {
+            "Antenna_Type": ["LBA"],
+            "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2),
+            "Control_to_RECV_mapping":
+            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47]
+            numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(),
+        }
+
+        self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA")
+        self.antennafield_proxy.off()
+        self.antennafield_proxy.put_property(calibration_properties)
+        self.antennafield_proxy.boot()
+
+        self.proxy.boot()
+
+        # calibrate
+        self.proxy.calibrate_recv("STAT/AntennaField/HBA")
+
+        # check the results
+        rcu_attenuator_db = self.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": ["HBA"],
+            "Antenna_Names": self.HBA_ANTENNA_NAMES,
+            "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2),
+            "Antenna_to_SDP_Mapping": [0, 1, 0, 0]
+            + [-1, -1] * (DEFAULT_N_HBA_TILES - 2),
+            "Control_to_RECV_mapping":
+            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47]
+            numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(),
+        }
+
+        self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA")
+        self.antennafield_proxy.off()
+        self.antennafield_proxy.put_property(calibration_properties)
+        self.antennafield_proxy.boot()
+
+        self.proxy.boot()
+
+        # calibrate
+        self.proxy.calibrate_sdp("STAT/AntennaField/HBA")
+
+        # 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])
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 ed7fd6982..8225f89c2 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
@@ -1,8 +1,9 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
-import time
 import logging
+import time
+
 import numpy
 import timeout_decorator
 
@@ -16,7 +17,6 @@ from tangostationcontrol.common.constants import (
 )
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
-
 from .base import AbstractTestBases
 
 logger = logging.getLogger()
@@ -88,6 +88,105 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase):
                 "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
                 "Antenna_Quality": antenna_qualities,
                 "Antenna_Use": antenna_use,
+                "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2),
+                "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",
+                    "2",
+                    "0",
+                    "2",
+                    "1",
+                    "2",
+                    "2",
+                    "2",
+                    "3",
+                    "2",
+                    "4",
+                    "2",
+                    "5",
+                    "3",
+                    "0",
+                    "3",
+                    "1",
+                    "3",
+                    "2",
+                    "3",
+                    "3",
+                    "3",
+                    "4",
+                    "3",
+                    "5",
+                    "4",
+                    "0",
+                    "4",
+                    "1",
+                    "4",
+                    "2",
+                    "4",
+                    "3",
+                    "4",
+                    "4",
+                    "4",
+                    "5",
+                    "5",
+                    "0",
+                    "5",
+                    "1",
+                    "5",
+                    "2",
+                    "5",
+                    "3",
+                    "5",
+                    "4",
+                    "5",
+                    "5",
+                    "6",
+                    "0",
+                    "6",
+                    "1",
+                    "6",
+                    "2",
+                    "6",
+                    "3",
+                    "6",
+                    "4",
+                    "6",
+                    "5",
+                    "7",
+                    "0",
+                    "7",
+                    "1",
+                    "7",
+                    "2",
+                    "7",
+                    "3",
+                    "7",
+                    "4",
+                    "7",
+                    "5",
+                ],
             }
         )
         antennafield_proxy.off()
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 00257ead7..d16455a35 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py
@@ -1,11 +1,12 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
 from datetime import datetime
 from json import loads
 
 import numpy
 from tango import DevState, DevFailed
+
 from tangostationcontrol.common.constants import (
     N_beamlets_ctrl,
     MAX_ANTENNA,
@@ -14,7 +15,6 @@ from tangostationcontrol.common.constants import (
 from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.test.devices.test_observation_base import TestObservationBase
-
 from .base import AbstractTestBases
 
 
@@ -152,6 +152,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase):
         antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
         antennafield_proxy.put_property(
             {
+                "Antenna_Type": ["LBA"],
                 "RECV_devices": ["STAT/RECV/1"],
                 "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
                 "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING,
diff --git a/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/test/common/test_calibration.py
index 44b87271f..9a71bdd13 100644
--- a/tangostationcontrol/test/common/test_calibration.py
+++ b/tangostationcontrol/test/common/test_calibration.py
@@ -1,17 +1,129 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+import os
+from os import path
+from unittest.mock import patch, Mock, call, PropertyMock
 
 import numpy
+from numpy.testing import assert_array_equal
 
 from tangostationcontrol.common.calibration import (
     delay_compensation,
     loss_compensation,
     dB_to_factor,
+    CalibrationManager,
+    CalibrationTable,
 )
-
+from tangostationcontrol.common.constants import S_pn, N_subbands, N_pn
+from tangostationcontrol.devices.sdp.sdp import SDP
 from test import base
 
 
+class MockMinio:
+    def __init__(self, **kwargs):
+        self.args = kwargs
+
+
+@patch("tangostationcontrol.common.calibration.Minio")
+@patch.dict(
+    os.environ,
+    {"MINIO_ROOT_USER": "my_user", "MINIO_ROOT_PASSWORD": "my_passwd"},
+    clear=True,
+)
+class TestCalibrationManager(base.TestCase):
+    def test_sync_calibration_tables(self, minio):
+        minio.return_value.list_objects.return_value = [
+            Mock(object_name="/unittest-station/file1.h5"),
+            Mock(object_name="/unittest-station/file2.h5"),
+            Mock(object_name="/unittest-station/file3.h5"),
+        ]
+        sut = CalibrationManager(
+            "http://server:1234/test_bucket/test_prefix", "unittest-station"
+        )
+        minio.has_call_with(
+            "server:1234", access_key="my_user", secret_key="my_passwd", secure=False
+        )
+        minio.return_value.list_objects.has_call_with(
+            "test_bucket", prefix="test_prefix/unittest-station/"
+        )
+        minio.return_value.fget_object.assert_has_calls(
+            [
+                call(
+                    "test_bucket",
+                    "/unittest-station/file1.h5",
+                    path.join(sut._tmp_dir.name, "file1.h5"),
+                ),
+                call(
+                    "test_bucket",
+                    "/unittest-station/file2.h5",
+                    path.join(sut._tmp_dir.name, "file2.h5"),
+                ),
+                call(
+                    "test_bucket",
+                    "/unittest-station/file3.h5",
+                    path.join(sut._tmp_dir.name, "file3.h5"),
+                ),
+            ]
+        )
+
+    @patch("tangostationcontrol.common.calibration.read_hdf5")
+    def test_calibrate_subband_weights(self, hdf_reader, _):
+        antenna_field_mock = Mock(
+            Antenna_to_SDP_Mapping_R=numpy.array(
+                [[1, 1], [1, 2]], dtype=numpy.int32
+            ).reshape(-1, 2),
+            Antenna_Names_R=[f"T{n + 1}" for n in range(2)],
+            Antenna_Type_R="HBA",
+            RCU_band_select_RW=numpy.array([1, 2]),
+            **{"name.return_value": "Stat/AntennaField/TEST"},
+        )
+        subband_weights = numpy.array(
+            [[SDP.SUBBAND_UNIT_WEIGHT] * S_pn * N_subbands] * N_pn
+        )
+
+        def subband_weights_side_effect(new_value=None):
+            nonlocal subband_weights
+            if new_value is not None:
+                subband_weights = new_value
+            return subband_weights
+
+        sdp_mock = Mock()
+        subband_property_mock = PropertyMock(side_effect=subband_weights_side_effect)
+        type(sdp_mock).FPGA_subband_weights_RW = subband_property_mock
+        caltable_mock = Mock(
+            observation_station="unittest-station",
+            antennas={
+                "T1": Mock(x=numpy.arange(0, 512), y=numpy.arange(512, 1024)),
+                "T2": Mock(x=numpy.arange(1024, 1536), y=numpy.arange(1536, 2048)),
+            },
+        )
+        hdf_reader.return_value.__enter__.return_value = caltable_mock
+
+        sut = CalibrationManager("http://server:1234", "unittest-station")
+        sut.calibrate_subband_weights(antenna_field_mock, sdp_mock)
+
+        hdf_reader.assert_has_calls(
+            [
+                call(
+                    f"{sut._tmp_dir.name}/CalTable-unittest-station-TEST-200MHz.h5",
+                    CalibrationTable,
+                ),
+                call().__enter__(),
+                call().__exit__(None, None, None),
+                call(
+                    f"{sut._tmp_dir.name}/CalTable-unittest-station-TEST-150MHz.h5",
+                    CalibrationTable,
+                ),
+                call().__enter__(),
+                call().__exit__(None, None, None),
+            ]
+        )
+        assert_array_equal(subband_weights[1, 1024:1536], numpy.arange(0, 512))
+        assert_array_equal(subband_weights[1, 1536:2048], numpy.arange(512, 1024))
+        assert_array_equal(subband_weights[1, 2048:2560], numpy.arange(1024, 1536))
+        assert_array_equal(subband_weights[1, 2560:3072], numpy.arange(1536, 2048))
+
+
 class TestCalibration(base.TestCase):
     def test_dB_to_factor(self):
         # Throw some known values at it
-- 
GitLab