diff --git a/.gitignore b/.gitignore index 4ccabd3716b8e8e40abe61bf82c11916c4dbfbc7..8ee8430be40555ed5cf527dfb54c8e515d0ae80c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ **/vscode-server.tar .orig +bin/jumppad + tangostationcontrol/build tangostationcontrol/cover tangostationcontrol/dist diff --git a/CDB/integrations/multiobs_ConfigDb.json b/CDB/integrations/multiobs_ConfigDb.json deleted file mode 100644 index ed52f2202654d17367fb9b379b148a7d4aeb6cec..0000000000000000000000000000000000000000 --- a/CDB/integrations/multiobs_ConfigDb.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "servers": { - "ObservationControl": { - "STAT": { - "Observation": { - "STAT/Observation/1": {}, - "STAT/Observation/2": {}, - "STAT/Observation/3": {} - } - } - } - } -} diff --git a/CDB/stations/testenv_cs001.json b/CDB/stations/testenv_cs001.json index a6286bd311fe9f121cd75347e7156d48b352e428..da4b205c396a99402dde96c116180209efdcf227 100644 --- a/CDB/stations/testenv_cs001.json +++ b/CDB/stations/testenv_cs001.json @@ -1115,26 +1115,6 @@ } } } - }, - "ObservationControl": { - "STAT": { - "Observation": { - "STAT/Observation/1": { - } - }, - "ObservationControl": { - "STAT/ObservationControl/1": { - "properties": { - "Power_Children": [ - "STAT/Observation/1" - ], - "Control_Children": [ - "STAT/Observation/1" - ] - } - } - } - } } } } diff --git a/CDB/stations/testenv_cs001.json.orig b/CDB/stations/testenv_cs001.json.orig deleted file mode 100644 index db183dbea108cf16d064e71e25b6616c4bfed753..0000000000000000000000000000000000000000 --- a/CDB/stations/testenv_cs001.json.orig +++ /dev/null @@ -1,1186 +0,0 @@ -{ - "servers": { - "APSCT": { - "STAT": { - "APSCT": { - "STAT/APSCT/L0": { - "properties": { - "OPC_Server_Name": [ - "apsct-sim" - ], - "OPC_Server_Port": [ - "4843" - ], - "OPC_Time_Out": [ - "5.0" - ], - "APSCT_On_Off_timeout": [ - "1" - ] - } - }, - "STAT/APSCT/L1": { - "properties": { - "OPC_Server_Name": [ - "apsct-sim" - ], - "OPC_Server_Port": [ - "4843" - ], - "OPC_Time_Out": [ - "5.0" - ], - "APSCT_On_Off_timeout": [ - "1" - ] - } - }, - "STAT/APSCT/H0": { - "properties": { - "OPC_Server_Name": [ - "apsct-sim" - ], - "OPC_Server_Port": [ - "4843" - ], - "OPC_Time_Out": [ - "5.0" - ], - "APSCT_On_Off_timeout": [ - "1" - ] - } - } - } - } - }, - "CCD": { - "STAT": { - "CCD": { - "STAT/CCD/1": { - "properties": { - "OPC_Server_Name": [ - "ccd-sim" - ], - "OPC_Server_Port": [ - "4843" - ], - "OPC_Time_Out": [ - "5.0" - ], - "CCD_On_Off_timeout": [ - "1" - ] - } - } - } - } - }, - "EC": { - "STAT": { - "EC": { - "STAT/EC/1": { - "properties": { - "OPC_Server_Name": [ - "ec-sim.service.consul" - ], - "OPC_Server_Port": [ - "4850" - ], - "OPC_Time_Out": [ - "5.0" - ], - "OPC_Node_Path_Prefix": [ - "3:ServerInterfaces", - "4:Environmental_Control" - ], - "OPC_namespace": [ - "http://Environmental_Control" - ] - } - } - } - } - }, - "APSPU": { - "STAT": { - "APSPU": { - "STAT/APSPU/L0": { - "properties": { - "OPC_Server_Name": [ - "apspu-sim" - ], - "OPC_Server_Port": [ - "4842" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - }, - "STAT/APSPU/L1": { - "properties": { - "OPC_Server_Name": [ - "apspu-sim" - ], - "OPC_Server_Port": [ - "4842" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - }, - "STAT/APSPU/H0": { - "properties": { - "OPC_Server_Name": [ - "apspu-sim" - ], - "OPC_Server_Port": [ - "4842" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - } - } - } - }, - "Beamlet": { - "STAT": { - "Beamlet": { - "STAT/Beamlet/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/Beamlet/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/Beamlet/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - } - } - } - }, - "DigitalBeam": { - "STAT": { - "DigitalBeam": { -<<<<<<< HEAD - "STAT/DigitalBeam/LBA": { - "properties": { - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] -======= - "STAT": { - "DigitalBeam": { - "STAT/DigitalBeam/LBA": { - "properties": { - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - }, - "STAT/DigitalBeam/HBA0": { - "properties": { - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - }, - "STAT/DigitalBeam/HBA1": { - "properties": { - "AntennaField_Device": [ - "STAT/AFH/HBA1" - ], - "Beamlet_Device": [ - "STAT/Beamlet/HBA1" - ], - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - } - } ->>>>>>> master - } - }, - "STAT/DigitalBeam/HBA0": { - "properties": { - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - }, - "STAT/DigitalBeam/HBA1": { - "properties": { - "AntennaField_Device": [ - "STAT/AntennaField/HBA1" - ], - "Beamlet_Device": [ - "STAT/Beamlet/HBA1" - ], - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - } - } - } - }, - "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", - "RECVH, RECV_TEMP_error_R", - "RECVL, RECV_TEMP_error_R" - ], - "Shutdown_Device_List": [ - "STAT/SDPFirmware/LBA", - "STAT/SDPFirmware/HBA0", - "STAT/SDPFirmware/HBA1", - "STAT/SDP/LBA", - "STAT/SDP/HBA0", - "STAT/SDP/HBA1", - "STAT/UNB2/L0", - "STAT/UNB2/L1", - "STAT/UNB2/H0", - "STAT/RECVH/H0", - "STAT/RECVL/L0", - "STAT/RECVL/L1", - "STAT/APSCT/L0", - "STAT/APSCT/L1", - "STAT/APSCT/H0", - "STAT/CCD/1", - "STAT/APSPU/L0", - "STAT/APSPU/L1", - "STAT/APSPU/H0" - ] - } - } - } - } - }, - "PCON": { - "STAT": { - "PCON": { - "STAT/PCON/1": { - "properties": { - "SNMP_use_simulators": [ - "True" - ] - } - } - } - } - }, - "PSOC": { - "STAT": { - "PSOC": { - "STAT/PSOC/1": { - "properties": { - "SNMP_use_simulators": [ - "True" - ] - } - } - } - } - }, - "RECVH": { - "STAT": { - "RECVH": { - "STAT/RECVH/H0": { - "properties": { - "OPC_Server_Name": [ - "recvh-sim" - ], - "OPC_Server_Port": [ - "4844" - ], - "OPC_Time_Out": [ - "5.0" - ], - "RCU_On_Off_timeout": [ - "1" - ], - "RCU_DTH_On_Off_timeout": [ - "1" - ] - } - } - } - } - }, - "RECVL": { - "STAT": { - "RECVL": { - "STAT/RECVL/L0": { - "properties": { - "OPC_Server_Name": [ - "recvl-sim" - ], - "OPC_Server_Port": [ - "4845" - ], - "OPC_Time_Out": [ - "5.0" - ], - "RCU_On_Off_timeout": [ - "1" - ], - "RCU_DTH_On_Off_timeout": [ - "1" - ] - } - }, - "STAT/RECVL/L1": { - "properties": { - "OPC_Server_Name": [ - "recvl-sim" - ], - "OPC_Server_Port": [ - "4845" - ], - "OPC_Time_Out": [ - "5.0" - ], - "RCU_On_Off_timeout": [ - "1" - ], - "RCU_DTH_On_Off_timeout": [ - "1" - ] - } - } - } - } - }, - "SDPFirmware": { - "STAT": { - "SDPFirmware": { - "STAT/SDPFirmware/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "Firmware_Boot_timeout": [ - "1.0" - ] - } - }, - "STAT/SDPFirmware/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "Firmware_Boot_timeout": [ - "1.0" - ] - } - }, - "STAT/SDPFirmware/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "Firmware_Boot_timeout": [ - "1.0" - ] - } - } - } - } - }, - "SDP": { - "STAT": { - "SDP": { - "STAT/SDP/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - }, - "STAT/SDP/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - }, - "STAT/SDP/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ] - } - } - } - } - }, - "BST": { - "STAT": { - "BST": { - "STAT/BST/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_bst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_bst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/BST/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_bst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_bst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/BST/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_bst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_bst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - } - } - } - }, - "SST": { - "STAT": { - "SST": { - "STAT/SST/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_sst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_sst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/SST/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_sst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_sst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/SST/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_sst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_sst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - } - } - } - }, - "XST": { - "STAT": { - "XST": { - "STAT/XST/LBA": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_xst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_xst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/XST/HBA0": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_xst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_xst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - }, - "STAT/XST/HBA1": { - "properties": { - "OPC_Server_Name": [ - "sdptr-sim" - ], - "OPC_Server_Port": [ - "4840" - ], - "OPC_Time_Out": [ - "5.0" - ], - "FPGA_xst_offload_hdr_eth_destination_mac_RW_default": [ - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB", - "01:23:45:67:89:AB" - ], - "FPGA_xst_offload_hdr_ip_destination_address_RW_default": [ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1" - ] - } - } - } - } - }, - "UNB2": { - "STAT": { - "UNB2": { - "STAT/UNB2/L0": { - "properties": { - "OPC_Server_Name": [ - "unb2-sim" - ], - "OPC_Server_Port": [ - "4841" - ], - "OPC_Time_Out": [ - "5.0" - ], - "UNB2_On_Off_timeout": [ - "1" - ] - } - }, - "STAT/UNB2/L1": { - "properties": { - "OPC_Server_Name": [ - "unb2-sim" - ], - "OPC_Server_Port": [ - "4841" - ], - "OPC_Time_Out": [ - "5.0" - ], - "UNB2_On_Off_timeout": [ - "1" - ] - } - }, - "STAT/UNB2/H0": { - "properties": { - "OPC_Server_Name": [ - "unb2-sim" - ], - "OPC_Server_Port": [ - "4841" - ], - "OPC_Time_Out": [ - "5.0" - ], - "UNB2_On_Off_timeout": [ - "1" - ] - } - } - } - } - }, - "TileBeam": { - "STAT": { - "TileBeam": { - "STAT/TileBeam/HBA0": { - "properties": { - "Tracking_enabled_RW_default": [ - "True" - ] - } - }, - "STAT/TileBeam/HBA1": { - "properties": { - "Tracking_enabled_RW_default": [ - "True" - ] - } - } - } - } - }, - "StationManager": { - "STAT": { - "StationManager": { - "STAT/StationManager/1": { - "properties": { - "Suppress_State_Transition_Failures": [ - "True" - ] - } - } - } - } - }, - "ObservationControl": { - "STAT": { - "Observation": { - "STAT/Observation/1": { - } - }, - "ObservationControl": { - "STAT/ObservationControl/1": { - "properties": { - "Power_Children": [ - "STAT/Observation/1" - ], - "Control_Children": [ - "STAT/Observation/1" - ] - } - } - } - } - } - } -} diff --git a/README.md b/README.md index 1dcb85bc833d894f5d33dea39ffb30be39fdacee..59bc8e7de288a778e68a24aa6ce56709bc79f274 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ You will also need: * bash * dig (dnsutils package on ubuntu/debian) * wget +* hexdump (bsdextrautils package on ubuntu/debian) ## Start dev environment @@ -70,25 +71,6 @@ to start the dev environment including tango. Nomad is now available at http://localhost:4646/ -## Start dev environment - -For local development a dev environment is needed. To setup this environment run - -``` -./sbin/prepare_dev_env.sh -``` - -This will install `jumppad`, if not present yet as well as creating the docker volume needed to simulate the station -nomad cluster. - -Afterwards run - -``` -jumppad up infra/dev -``` - -to start the dev environment including tango. - ## Bootstrap The bootstrap procedure is needed only once. First we build all docker @@ -157,10 +139,12 @@ Next change the version in the following places: # Release Notes +* 0.24.0 Allow multiple antenna fields to be used in single observation, + This renames the `Observation` device to `ObservationField`. * 0.23.0 Migrate execution environment to nomad * 0.22.0 Split `Antennafield` in `AFL` and `AFH` devices in order to separate Low-Band and High-Band functionalities - Removed `Antenna_Type_R` attribute from antennafield devices + Removed `Antenna_Type_R` attribute from antennafield devices * 0.21.4 Replace `ACC-MIB.mib` with `SP2-MIB.mib` source file in PCON device * 0.21.3 Added DigitalBeam.Antenna_Usage_Mask_R to expose antennas used in beamforming * 0.21.2 Removed deprecated "Boot" device (use StationManager now) diff --git a/docker-compose/tango-prometheus-exporter/lofar2-fast-policy.json b/docker-compose/tango-prometheus-exporter/lofar2-fast-policy.json index c181931ebdb89dfbbf34c5f9576587a85818df7b..afd380aba7cbb15ec1cddcf51f8993c1aa7c3020 100644 --- a/docker-compose/tango-prometheus-exporter/lofar2-fast-policy.json +++ b/docker-compose/tango-prometheus-exporter/lofar2-fast-policy.json @@ -30,7 +30,7 @@ }, "stat/docker/1": { }, - "stat/observation/*":{ + "stat/observationfield/*":{ }, "stat/observationcontrol/1":{ }, diff --git a/docker-compose/tango-prometheus-exporter/lofar2-policy.json b/docker-compose/tango-prometheus-exporter/lofar2-policy.json index 895f6e307dddc2de5e9df3653816695fbe0b9627..ee30f469d34ce5ba7591bde2307da174b3a6050a 100644 --- a/docker-compose/tango-prometheus-exporter/lofar2-policy.json +++ b/docker-compose/tango-prometheus-exporter/lofar2-policy.json @@ -67,7 +67,7 @@ }, "stat/docker/1": { }, - "stat/observation/*":{ + "stat/observationfield/*":{ "exclude": [ "saps_pointing_R", "saps_subbands_R" diff --git a/docker-compose/tango-prometheus-exporter/lofar2-slow-policy.json b/docker-compose/tango-prometheus-exporter/lofar2-slow-policy.json index c9aee832684042b4d73350d036cec68ecd58cf8d..56bda80f5d98a26fd746319b44a6fbc1cf67dc76 100644 --- a/docker-compose/tango-prometheus-exporter/lofar2-slow-policy.json +++ b/docker-compose/tango-prometheus-exporter/lofar2-slow-policy.json @@ -41,7 +41,7 @@ }, "stat/docker/1": { }, - "stat/observation/*":{ + "stat/observationfield/*":{ "include": [ "saps_pointing_R", "saps_subbands_R" diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index ca222b7cf394c7e48e13308f41fb4c49478f6e12..2094a100ca8bd90c2861a5da1807755ff3608831 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.23.0 +0.24.0 diff --git a/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json b/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json index e37e343c4a267ac5d88f52f23c0aacf97cd78094..3a9275db45119dc9cdbbef7106d2a57b78285c3e 100644 --- a/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json +++ b/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json @@ -24,9 +24,9 @@ } } }, - "Observation": { + "ObservationField": { "STAT": { - "Observation": { + "ObservationField": { } } }, @@ -37,10 +37,10 @@ } } }, - "AntennaField": { + "AFH": { "STAT": { - "AntennaField": { - "STAT/AntennaField/HBA": { + "AFH": { + "STAT/AFH/HBA": { "properties": { "Control_Children": ["STAT/RECVH/1"] } diff --git a/tangostationcontrol/integration_test/configuration/configDB/dummy_positions_ConfigDb.json b/tangostationcontrol/integration_test/configuration/configDB/dummy_positions_ConfigDb.json index 7758a30d8ad45a0b67effe7b38f5a4812025929c..17a3aa6c27d1ac132f7d2e9a6ea8f30b4dfa1c45 100644 --- a/tangostationcontrol/integration_test/configuration/configDB/dummy_positions_ConfigDb.json +++ b/tangostationcontrol/integration_test/configuration/configDB/dummy_positions_ConfigDb.json @@ -1,9 +1,9 @@ { "servers": { - "AntennaField": { + "AFH": { "STAT": { - "AntennaField": { - "STAT/AntennaField/HBA": { + "AFH": { + "STAT/AFH/HBA": { "properties": { "Control_to_RECV_mapping": [ "1", "0", diff --git a/tangostationcontrol/integration_test/configuration/configDB/test_environment_ConfigDb.json b/tangostationcontrol/integration_test/configuration/configDB/test_environment_ConfigDb.json index 17ce2a7f58083db9dc31b5b39b34c81bda50cd56..3e574fddfb0f2aeda2b9ee23ccc7816f4d56b275 100644 --- a/tangostationcontrol/integration_test/configuration/configDB/test_environment_ConfigDb.json +++ b/tangostationcontrol/integration_test/configuration/configDB/test_environment_ConfigDb.json @@ -10,13 +10,6 @@ } } } - }, - "ObservationControl": { - "STAT": { - "Observation": { - "STAT/Observation/1": {} - } - } } } } diff --git a/tangostationcontrol/integration_test/configuration/test_device_configuration.py b/tangostationcontrol/integration_test/configuration/test_device_configuration.py index 78c3044b9f3f14f7034089a6e500eafca302f87f..0f998b8f74d23ec97b8b803291f7c249914e21e8 100644 --- a/tangostationcontrol/integration_test/configuration/test_device_configuration.py +++ b/tangostationcontrol/integration_test/configuration/test_device_configuration.py @@ -22,10 +22,10 @@ INITIAL_CONFIGURATION = None class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): TEST_CONFIGURATION = """{ "servers": { - "AntennaField": { + "AFH": { "STAT": { - "AntennaField": { - "STAT/AntennaField/HBA": { + "AFH": { + "STAT/AFH/HBA": { "properties": { "Control_Children": [ "STAT/MOCKRCU/1" ] } @@ -35,8 +35,8 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): }, "ObservationControl": { "STAT": { - "Observation": { - "STAT/Observation/11": {} + "ObservationControl": { + "STAT/ObservationControl/11": {} } } } @@ -148,20 +148,19 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): msg=f"{dbdata}", ) # configuration device self.assertFalse( - "stat/observation/11" - in dbdata["servers"]["observationcontrol"]["stat"]["observation"], + "stat/observationcontrol/11" + in dbdata["servers"]["observationcontrol"]["stat"][ + "observationcontrol" + ], msg=f"{dbdata}", ) # observation device self.assertTrue( - "stat/antennafield/hba" - in dbdata["servers"]["antennafield"]["stat"]["antennafield"], + "stat/afh/hba" in dbdata["servers"]["afh"]["stat"]["afh"], msg=f"{dbdata}", ) # antennafield device antennafield_properties = CaseInsensitiveDict( - dbdata["servers"]["antennafield"]["stat"]["antennafield"][ - "stat/antennafield/hba" - ]["properties"] + dbdata["servers"]["afh"]["stat"]["afh"]["stat/afh/hba"]["properties"] ) self.assertIn( @@ -183,9 +182,9 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): ) # Test whether new device has been added self.assertTrue( - "stat/observation/11" + "stat/observationcontrol/11" in updated_dbdata["servers"]["observationcontrol"]["stat"][ - "observation" + "observationcontrol" ], msg=f"{updated_dbdata}", ) # observation device @@ -195,15 +194,14 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): ) # recvh device # Test whether old attribute has been updated self.assertTrue( - "stat/antennafield/hba" - in updated_dbdata["servers"]["antennafield"]["stat"]["antennafield"], + "stat/afh/hba" in updated_dbdata["servers"]["afh"]["stat"]["afh"], msg=f"{updated_dbdata}", ) antennafield_properties = CaseInsensitiveDict( - updated_dbdata["servers"]["antennafield"]["stat"]["antennafield"][ - "stat/antennafield/hba" - ]["properties"] + updated_dbdata["servers"]["afh"]["stat"]["afh"]["stat/afh/hba"][ + "properties" + ] ) self.assertIn( @@ -241,8 +239,10 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): ) # Test whether new device has been added self.assertTrue( - "stat/observation/11" - in updated_dbdata["servers"]["observationcontrol"]["stat"]["observation"], + "stat/observationcontrol/11" + in updated_dbdata["servers"]["observationcontrol"]["stat"][ + "observationcontrol" + ], msg=f"{updated_dbdata}", ) # observation device # Test whether old device has NOT been deleted @@ -252,14 +252,13 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): ) # recvh device # Test whether old attribute has been updated self.assertTrue( - "stat/antennafield/hba" - in updated_dbdata["servers"]["antennafield"]["stat"]["antennafield"], + "stat/afh/hba" in updated_dbdata["servers"]["afh"]["stat"]["afh"], msg=f"{updated_dbdata}", ) antennafield_properties = CaseInsensitiveDict( - updated_dbdata["servers"]["antennafield"]["stat"]["antennafield"][ - "stat/antennafield/hba" - ]["properties"] + updated_dbdata["servers"]["afh"]["stat"]["afh"]["stat/afh/hba"][ + "properties" + ] ) self.assertIn( diff --git a/tangostationcontrol/integration_test/default/common/test_configuration.py b/tangostationcontrol/integration_test/default/common/test_configuration.py index c2b5b3ed70af955f54cdbf66ebdda37d16a6467a..6b6a8e2f1159c923c0c7eb9eb8b03c6e0a6e6a1c 100644 --- a/tangostationcontrol/integration_test/default/common/test_configuration.py +++ b/tangostationcontrol/integration_test/default/common/test_configuration.py @@ -16,10 +16,10 @@ class TestStationConfiguration(BaseIntegrationTestCase): TEST_CONFIGURATION = """{ "servers": { - "AntennaField": { + "AFH": { "STAT": { - "AntennaField": { - "STAT/AntennaField/1": { + "AFH": { + "STAT/AFH/1": { "properties": { "Control_Children": [ "STAT/MOCKRCU/1" ] } @@ -154,9 +154,9 @@ class TestStationConfiguration(BaseIntegrationTestCase): self.assertTrue(self.check_configuration_validity(self.TEST_CONFIGURATION)) # Insert invalid field json_test_configuration = json.loads(self.TEST_CONFIGURATION) - invalid_configuration = json_test_configuration["servers"]["AntennaField"][ - "STAT" - ]["AntennaField"]["STAT/AntennaField/1"]["new_field"] = "test" + invalid_configuration = json_test_configuration["servers"]["AFH"]["STAT"][ + "AFH" + ]["STAT/AFH/1"]["new_field"] = "test" self.assertFalse( self.check_configuration_validity(json.dumps(invalid_configuration)) ) diff --git a/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py b/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py index b65e62870dcf2eba5977395b95f0dd80ccae59e0..b49b1902ad71cc3456cbd64def017ffad45fc801 100644 --- a/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py +++ b/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py @@ -4,7 +4,6 @@ import time import numpy -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases from tango import DevState @@ -28,7 +27,9 @@ from tangostationcontrol.devices.base_device_classes.antennafield_device import class TestHBADevice(AbstractTestBases.TestDeviceBase): def setUp(self): - self.stationmanager_proxy = self.setup_stationmanager_proxy() + self.stationmanager_proxy = self.setup_proxy("STAT/StationManager/1") + + # Setup will dump current properties and restore them for us super().setUp("STAT/AFH/HBA0") # Typical tests emulate 'CS001_TILES' number of antennas in @@ -48,66 +49,15 @@ class TestHBADevice(AbstractTestBases.TestDeviceBase): ).flatten(), } ) - self.recv_proxy = self.setup_recv_proxy() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() - - self.addCleanup(self.shutdown_recv) - self.addCleanup(self.shutdown_sdp) + self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True) + self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0") + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0") # configure the frequencies, which allows access # to the calibration attributes and commands self.sdpfirmware_proxy.clock_RW = CLK_160_MHZ self.recv_proxy.RCU_band_select_RW = [[1] * N_rcu_inp] * N_rcu - def restore_antennafield(self): - self.proxy.put_property( - { - "Power_to_RECV_mapping": [-1, -1] * CS001_TILES, - "Control_to_RECV_mapping": [-1, -1] * CS001_TILES, - } - ) - - @staticmethod - def shutdown_recv(): - recv_proxy = TestDeviceProxy("STAT/RECVH/H0") - recv_proxy.off() - - @staticmethod - def shutdown_sdp(): - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - - def setup_recv_proxy(self): - # setup RECV - recv_proxy = TestDeviceProxy("STAT/RECVH/H0") - recv_proxy.off() - recv_proxy.boot() - recv_proxy.set_defaults() - return recv_proxy - - def setup_sdpfirmware_proxy(self): - # setup SDPFirmware - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - sdp_proxy.boot() - return sdp_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_ANT_mask_RW_configured_after_Antenna_Usage_Mask(self): """Verify if ANT_mask_RW values are correctly configured from Antenna_Usage_Mask values""" diff --git a/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/integration_test/default/devices/base.py index 8b3d44d12d35ca34286a907c5b6b2c8e1c8b13b4..d8afd1004b5f1daf8073fd1598b836d47ad0ff5c 100644 --- a/tangostationcontrol/integration_test/default/devices/base.py +++ b/tangostationcontrol/integration_test/default/devices/base.py @@ -1,5 +1,6 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 +from typing import Callable from tango._tango import DevState, AttrWriteType @@ -29,19 +30,56 @@ class AbstractTestBases: # make a backup of the properties, in case they're changed # NB: "or {}" is needed to deal with devices that have no properties. - self.original_properties = ( - self.proxy.get_property(self.proxy.get_property_list("*") or {}) or {} - ) + self.original_properties = self.current_properties() self.addCleanup(TestDeviceProxy.test_device_turn_off, self.name) self.addCleanup(self.restore_properties) super().setUp() - def restore_properties(self): + def current_properties(self): + """Return the current properties of self.proxy""" + return ( + self.proxy.get_property(self.proxy.get_property_list("*") or {}) or {} + ) + + def setup_proxy( + self, + name: str, + defaults: bool = False, + restore_properties: bool = False, + cb: Callable[[TestDeviceProxy], None] = None, + ): + """Setup A TestDeviceProxy and handle cleanup""" + proxy = TestDeviceProxy(name) + + if restore_properties: + self.addCleanup(proxy.test_device_turn_off, name) + self.addCleanup(self.restore_properties, self.current_properties()) + + if cb: + cb(proxy) + + proxy.off() + proxy.boot() + + if defaults: + proxy.set_defaults() + + self.assertEqual(proxy.state(), DevState.ON) + + if not restore_properties: + self.addCleanup(proxy.test_device_turn_off, name) + + return proxy + + def restore_properties(self, properties=None): """Restore the properties as they were before the test.""" - self.proxy.put_property(self.original_properties) + if not properties: + properties = self.original_properties + + self.proxy.put_property(properties) def test_device_fetch_state(self): """Test if we can successfully fetch state""" diff --git a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py index ecfd45b55ed04e4033c6d3ea3202220f67299bd7..b07ea727cb8c60aabae2a3772cca3a109d0742de 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py @@ -7,7 +7,6 @@ from ctypes import c_short import numpy import numpy.testing -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases from tango import DevState @@ -26,42 +25,15 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase): """Intentionally recreate the device object in each test""" super().setUp("STAT/Beamlet/HBA0") - def test_device_read_all_attributes(self): - # We need to connect to SDP first to read some of our attributes - self.sdp_proxy = self.setup_sdp() - self.sdpfirmware_proxy = self.setup_sdpfirmware() + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0", defaults=True) + self.sdp_proxy.nyquist_zone_RW = [[2] * S_pn] * N_pn - super().test_device_read_all_attributes() - - def setup_sdp(self, clock=CLK_200_MHZ): - # setup SDP, on which this device depends - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - sdp_proxy.boot() - sdp_proxy.set_defaults() - - # setup the frequencies as expected in the test - sdp_proxy.nyquist_zone_RW = [[2] * S_pn] * N_pn - - return sdp_proxy - - def setup_sdpfirmware(self, clock=CLK_200_MHZ): - # setup SDP, on which this device depends - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - sdpfirmware_proxy.set_defaults() - - # setup the frequencies as expected in the test - sdpfirmware_proxy.clock_RW = clock - - return sdpfirmware_proxy + self.sdpfirmware_proxy = self.setup_proxy( + "STAT/SDPFirmware/HBA0", defaults=True + ) + self.sdpfirmware_proxy.clock_RW = CLK_200_MHZ def test_pointing_to_zenith(self): - # Setup configuration - sdp_proxy = self.setup_sdp() - sdpfirmware_proxy = self.setup_sdpfirmware() - self.proxy.initialise() self.proxy.on() @@ -82,10 +54,6 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase): numpy.testing.assert_almost_equal(expected_bf_weights, calculated_bf_weights) def test_subband_select_change(self): - # Setup configuration - sdp_proxy = self.setup_sdp() - sdpfirmware_proxy = self.setup_sdpfirmware() - # Change subband self.proxy.off() self.proxy.initialise() @@ -110,10 +78,7 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase): ) def test_sdp_clock_change(self): - # Setup configuration - sdp_proxy = self.setup_sdp() - sdpfirmware_proxy = self.setup_sdpfirmware() - + sdpfirmware_proxy = self.sdpfirmware_proxy self.proxy.initialise() self.proxy.subband_select_RW = numpy.array( list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)), diff --git a/tangostationcontrol/integration_test/default/devices/test_device_bst.py b/tangostationcontrol/integration_test/default/devices/test_device_bst.py index ccab06aa504b6f77fb2e3e22e304a4416747f7e3..5b19450db75fe59312c4701dd573d6c5631b9f28 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_bst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_bst.py @@ -1,7 +1,6 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases @@ -12,14 +11,7 @@ class TestDeviceBST(AbstractTestBases.TestDeviceBase): def test_device_read_all_attributes(self): # We need to connect to SDP first to read some of our attributes - self.sdp_proxy = self.setup_sdp() + self.setup_proxy("STAT/sdpfirmware/HBA0", defaults=True) + self.setup_proxy("STAT/SDP/HBA0", defaults=True) super().test_device_read_all_attributes() - - def setup_sdp(self): - # setup SDP, on which this device depends - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - sdp_proxy.boot() - sdp_proxy.set_defaults() - return sdp_proxy diff --git a/tangostationcontrol/integration_test/default/devices/test_device_calibration.py b/tangostationcontrol/integration_test/default/devices/test_device_calibration.py index 9629ece0342cf802723f5d7f9c8146aae738003d..0b3c7e93cd0e03dcb8a97e454da15ecf19206745 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_calibration.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_calibration.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import numpy -from tango import DevState from tangostationcontrol.common.constants import ( N_rcu, @@ -11,7 +10,6 @@ from tangostationcontrol.common.constants import ( CLK_200_MHZ, ) -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases @@ -68,32 +66,30 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): ] def setUp(self): - self.stationmanager_proxy = self.setup_stationmanager_proxy() - super().setUp("STAT/Calibration/1") - - self.recv_proxy = self.setup_recv_proxy() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() + self.stationmanager_proxy = self.setup_proxy("STAT/StationManager/1") - self.antennafield_proxy = self.setup_proxy("STAT/AFH/HBA0") + super().setUp("STAT/Calibration/1") - # make sure we restore any properties we modify - self.original_antennafield_properties = self.antennafield_proxy.get_property( - self.antennafield_proxy.get_property_list("*") - ) - self.addCleanup(self.restore_antennafield) - - self.antennafield_proxy.put_property( - { - "Power_to_RECV_mapping": [1, 1, 1, 0] - + [-1] * ((DEFAULT_N_HBA_TILES * 2) - 4), - "Antenna_Sets": ["ALL"], - "Antenna_Set_Masks": ["1" * DEFAULT_N_HBA_TILES], - "Frequency_Band_RW_default": ["HBA_110_190"] - * (DEFAULT_N_HBA_TILES * 2), - } + self.recv_proxy = self.setup_proxy("STAT/RECVH/H0") + self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0") + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0") + + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", + restore_properties=True, + cb=lambda x: { + x.put_property( + { + "Power_to_RECV_mapping": [1, 1, 1, 0] + + [-1] * ((DEFAULT_N_HBA_TILES * 2) - 4), + "Antenna_Sets": ["ALL"], + "Antenna_Set_Masks": ["1" * DEFAULT_N_HBA_TILES], + "Frequency_Band_RW_default": ["HBA_110_190"] + * (DEFAULT_N_HBA_TILES * 2), + } + ) + }, ) - self.antennafield_proxy.boot() # configure the frequencies, which allows access # to the calibration attributes and commands @@ -103,45 +99,6 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): def restore_antennafield(self): self.proxy.put_property(self.original_antennafield_properties) - @staticmethod - def shutdown(device: str): - def off(): - proxy = TestDeviceProxy(device) - proxy.off() - - return off - - def setup_recv_proxy(self): - # setup RECV - recv_proxy = self.setup_proxy("STAT/RECVH/H0") - recv_proxy.boot() - return recv_proxy - - def setup_sdpfirmware_proxy(self): - # setup SDP - sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.boot() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP - sdp_proxy = self.setup_proxy("STAT/SDP/HBA0") - sdp_proxy.boot() - return sdp_proxy - - def setup_proxy(self, dev: str): - proxy = TestDeviceProxy(dev) - proxy.off() - self.addCleanup(self.shutdown(dev)) - return proxy - - def setup_stationmanager_proxy(self): - """Setup StationManager""" - stationmanager_proxy = self.setup_proxy("STAT/StationManager/1") - stationmanager_proxy.boot() - self.assertEqual(stationmanager_proxy.state(), DevState.ON) - return stationmanager_proxy - def test_calibrate_recv(self): calibration_properties = { "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), @@ -155,9 +112,10 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): "Frequency_Band_RW_default": ["HBA_110_190"] * (DEFAULT_N_HBA_TILES * 2), } - self.antennafield_proxy = self.setup_proxy("STAT/AFH/HBA0") - self.antennafield_proxy.put_property(calibration_properties) - self.antennafield_proxy.boot() + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", + cb=lambda x: {x.put_property(calibration_properties)}, + ) self.proxy.boot() @@ -215,9 +173,10 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): "Frequency_Band_RW_default": ["HBA_110_190"] * (DEFAULT_N_HBA_TILES * 2), } - self.antennafield_proxy = self.setup_proxy("STAT/AFH/HBA0") - self.antennafield_proxy.put_property(calibration_properties) - self.antennafield_proxy.boot() + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", + cb=lambda x: {x.put_property(calibration_properties)}, + ) self.proxy.boot() diff --git a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py index 428b151885c789d7a9def82b4a1671c2021a7f9c..ac26002f382fba3114cbf9ae850a122a1d0b9f92 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py @@ -47,84 +47,46 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): self.proxy.put_property({"Beamlet_Select": [True] * N_beamlets_ctrl}) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.recv_iden) - - self.recv_proxy = self.setup_recv_proxy() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() + self.recv_proxy = self.setup_proxy(self.recv_iden, defaults=True) + self.sdpfirmware_proxy = self.setup_proxy(self.sdpfirmware_iden, defaults=True) + self.sdp_proxy = self.setup_proxy(self.sdp_iden) self.beamlet_proxy = self.initialise_beamlet_proxy() - def setup_recv_proxy(self): - recv_proxy = TestDeviceProxy(self.recv_iden) - recv_proxy.off() - recv_proxy.boot() - recv_proxy.set_defaults() - return recv_proxy - - def initialise_beamlet_proxy(self): - beamlet_proxy = TestDeviceProxy(self.beamlet_iden) - beamlet_proxy.off() - beamlet_proxy.initialise() - return beamlet_proxy - - def setup_beamlet_proxy(self): - beamlet_proxy = TestDeviceProxy(self.beamlet_iden) - beamlet_proxy.off() - beamlet_proxy.boot() - beamlet_proxy.set_defaults() - return beamlet_proxy - - def setup_sdpfirmware_proxy(self): - # setup SDPFirmware - sdpfirmware_proxy = TestDeviceProxy(self.sdpfirmware_iden) - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - sdpfirmware_proxy.set_defaults() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP, on which this device depends - sdp_proxy = TestDeviceProxy(self.sdp_iden) - sdp_proxy.off() - sdp_proxy.boot() - sdp_proxy.set_defaults() - return sdp_proxy - - def setup_antennafield_proxy(self, antenna_qualities, antenna_use): - # setup AntennaField NR_TILES = CS001_TILES - antennafield_proxy = TestDeviceProxy(self.antennafield_iden) control_mapping = [[1, i] for i in range(NR_TILES)] sdp_mapping = [[i // 6, i % 6] for i in range(NR_TILES)] - antennafield_proxy.put_property( - { - "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), - "Antenna_to_SDP_Mapping": numpy.array(sdp_mapping).flatten(), - "Antenna_Quality": antenna_qualities, - "Antenna_Use": antenna_use, - "Antenna_Cables": ["50m", "80m"] * (CS001_TILES // 2), - "Antenna_Sets": ["FIRST", "ALL"], - "Antenna_Set_Masks": [ - "1" + ("0" * (NR_TILES - 1)), - "1" * NR_TILES, - ], - } + self.antennafield_proxy = self.setup_proxy( + self.antennafield_iden, + cb=lambda x: { + x.put_property( + { + "Control_to_RECV_mapping": numpy.array( + control_mapping + ).flatten(), + "Antenna_to_SDP_Mapping": numpy.array(sdp_mapping).flatten(), + "Antenna_Quality": self.antenna_qualities_ok, + "Antenna_Use": self.antenna_use_ok, + "Antenna_Cables": ["50m", "80m"] * (CS001_TILES // 2), + "Antenna_Sets": ["FIRST", "ALL"], + "Antenna_Set_Masks": [ + "1" + ("0" * (NR_TILES - 1)), + "1" * NR_TILES, + ], + "Antenna_Type": "HBA", + } + ) + }, ) - antennafield_proxy.off() - antennafield_proxy.boot() - return antennafield_proxy - def test_pointing_to_zenith_clock_change(self): self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.sdp_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.antennafield_iden) - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) + def initialise_beamlet_proxy(self): + beamlet_proxy = TestDeviceProxy(self.beamlet_iden) + beamlet_proxy.off() + beamlet_proxy.initialise() + return beamlet_proxy - self.beamlet_proxy = self.initialise_beamlet_proxy() + def test_pointing_to_zenith_clock_change(self): self.beamlet_proxy.on() # Set first (default) clock configuration @@ -160,15 +122,6 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): ) def test_pointing_to_zenith_subband_change(self): - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.sdp_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.antennafield_iden) - - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) - - self.beamlet_proxy = self.initialise_beamlet_proxy() self.beamlet_proxy.subband_select_RW = numpy.array( list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)), dtype=numpy.uint32, @@ -207,16 +160,6 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): def test_set_pointing_masked_enable(self): """Verify that only selected inputs are written""" - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.sdp_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.antennafield_iden) - - self.setup_sdpfirmware_proxy() - self.setup_sdp_proxy() - self.antennafield_proxy = self.setup_antennafield_proxy( - self.antenna_qualities_ok, self.antenna_use_ok - ) - self.proxy.initialise() self.proxy.Tracking_enabled_RW = False self.proxy.on() @@ -261,14 +204,6 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): def test_beam_tracking_90_percent_interval(self): """Verify that the beam tracking operates within 95% of interval""" - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.sdp_iden) - self.addCleanup(TestDeviceProxy.test_device_turn_off, self.antennafield_iden) - - self.setup_sdpfirmware_proxy() - self.setup_sdp_proxy() - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) - self.proxy.initialise() self.proxy.Tracking_enabled_RW = True self.proxy.on() diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py b/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py index 2e65a37b4f54ff82c23cf678ed34e342cdd4d59f..772ab495c57464cefed3b46b63ceb972ce5bde9e 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py @@ -2,17 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 import json +import logging from datetime import datetime from datetime import timedelta import numpy - -from integration_test.device_proxy import TestDeviceProxy -from integration_test.default.devices.base import AbstractTestBases from tango import DevFailed from tango import DevState + from tangostationcontrol.common.constants import CS001_TILES -from tangostationcontrol.test.devices.test_observation_base import TestObservationBase +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_hba_immediate, +) + +from integration_test.default.devices.base import AbstractTestBases + +logger = logging.getLogger() class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): @@ -67,81 +72,44 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): "5", ] + VALID_JSON = get_observation_settings_hba_immediate().to_json() + EXPECTED_OBS_ID = json.loads(VALID_JSON)["antenna_fields"][0]["observation_id"] + def setUp(self): super().setUp("STAT/ObservationControl/1") - self.VALID_JSON = TestObservationBase.VALID_JSON - self.recv_proxy = self.setup_recv_proxy() - self.antennafield_proxy = self.setup_antennafield_proxy() - self.beamlet_proxy = self.setup_beamlet_proxy() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() - self.digitalbeam_proxy = self.setup_digitalbeam_proxy() - self.tilebeam_proxy = self.setup_tilebeam_proxy() - - def setup_recv_proxy(self): - # setup RECV - recv_proxy = TestDeviceProxy("STAT/RECVH/H0") - recv_proxy.off() - recv_proxy.boot() - recv_proxy.set_defaults() - return recv_proxy - - def setup_sdpfirmware_proxy(self): - # setup SDPFirmware - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - sdp_proxy.boot() - return sdp_proxy - - def setup_antennafield_proxy(self): - # setup AntennaField - antennafield_proxy = TestDeviceProxy("STAT/AFH/HBA0") + self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True) + self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0") + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0") + control_mapping = [[1, i] for i in range(CS001_TILES)] - antennafield_proxy.put_property( - { - "Antenna_Set": "ALL", - "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(), - "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, - } + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", + defaults=True, + restore_properties=True, + cb=lambda x: { + x.put_property( + { + "Antenna_Set": "ALL", + "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(), + "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, + } + ) + }, ) - antennafield_proxy.off() - antennafield_proxy.boot() - antennafield_proxy.set_defaults() - return antennafield_proxy - - def setup_beamlet_proxy(self): - # setup Digitalbeam - beamlet_proxy = TestDeviceProxy("STAT/Beamlet/HBA0") - beamlet_proxy.off() - beamlet_proxy.boot() - beamlet_proxy.set_defaults() - return beamlet_proxy - - def setup_digitalbeam_proxy(self): - # setup Digitalbeam - digitalbeam_proxy = TestDeviceProxy("STAT/DigitalBeam/HBA0") - digitalbeam_proxy.off() - digitalbeam_proxy.boot() - digitalbeam_proxy.set_defaults() - return digitalbeam_proxy - - def setup_tilebeam_proxy(self): - # Setup Tilebeam - tilebeam_proxy = TestDeviceProxy("STAT/TileBeam/HBA0") - tilebeam_proxy.off() - tilebeam_proxy.boot() - tilebeam_proxy.set_defaults() - return tilebeam_proxy + + self.beamlet_proxy = self.setup_proxy("STAT/Beamlet/HBA0", defaults=True) + self.digitalbeam_proxy = self.setup_proxy( + "STAT/DigitalBeam/HBA0", defaults=True + ) + self.tilebeam_proxy = self.setup_proxy("STAT/TileBeam/HBA0", defaults=True) def on_device_assert(self, proxy): - """Transition the device to ON and assert intermediate states""" + """Transition the device to ON and assert intermediate states + + This will repeatedly call ``stop_all_observations_now`` in turn calling + ``_destroy_all_observation_field_devices`` cleaning the Database from exported + devices + """ proxy.Off() self.assertEqual(DevState.OFF, proxy.state()) @@ -159,33 +127,9 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): self.on_device_assert(self.proxy) self.assertFalse(self.proxy.is_any_observation_running()) - self.assertFalse(self.proxy.is_observation_running(12345)) + self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) self.assertFalse(self.proxy.is_observation_running(54321)) - def test_check_and_convert_parameters_invalid_id(self): - """Test invalid parameter detection""" - - parameters = json.loads(self.VALID_JSON) - parameters["observation_id"] = -1 - - self.on_device_assert(self.proxy) - self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters)) - - def test_check_and_convert_parameters_invalid_time(self): - """Test invalid parameter detection""" - - parameters = json.loads(self.VALID_JSON) - parameters["stop_time"] = (datetime.now() - timedelta(seconds=1)).isoformat() - - self.on_device_assert(self.proxy) - self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters)) - - def test_check_and_convert_parameters_invalid_empty(self): - """Test empty parameter detection""" - - self.on_device_assert(self.proxy) - self.assertRaises(DevFailed, self.proxy.add_observation, "{}") - def test_add_observation_now(self): """Test starting an observation now""" @@ -194,9 +138,9 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): self.proxy.add_observation(self.VALID_JSON) self.assertTrue(self.proxy.is_any_observation_running()) - self.assertTrue(self.proxy.is_observation_running(12345)) + self.assertTrue(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) - self.proxy.stop_observation_now(12345) + self.proxy.stop_observation_now(self.EXPECTED_OBS_ID) def test_add_observation_future(self): """Test starting an observation in the future""" @@ -204,22 +148,28 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): self.on_device_assert(self.proxy) parameters = json.loads(self.VALID_JSON) - parameters["start_time"] = (datetime.now() + timedelta(days=1)).isoformat() - parameters["stop_time"] = (datetime.now() + timedelta(days=2)).isoformat() + for antenna_field in parameters["antenna_fields"]: + antenna_field["start_time"] = ( + datetime.now() + timedelta(days=1) + ).isoformat() + antenna_field["stop_time"] = ( + datetime.now() + timedelta(days=2) + ).isoformat() + self.proxy.add_observation(json.dumps(parameters)) - self.assertIn(12345, self.proxy.observations_R) - self.assertNotIn(12345, self.proxy.running_observations_R) + self.assertIn(self.EXPECTED_OBS_ID, self.proxy.observations_R) + self.assertNotIn(self.EXPECTED_OBS_ID, self.proxy.running_observations_R) self.assertFalse(self.proxy.is_any_observation_running()) - self.assertFalse(self.proxy.is_observation_running(12345)) + self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) - self.proxy.stop_observation_now(12345) + self.proxy.stop_observation_now(self.EXPECTED_OBS_ID) def test_add_observation_multiple(self): """Test starting multiple observations""" second_observation_json = json.loads(self.VALID_JSON) - second_observation_json["observation_id"] = 54321 + second_observation_json["antenna_fields"][0]["observation_id"] = 54321 self.on_device_assert(self.proxy) @@ -227,10 +177,10 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): self.proxy.add_observation(json.dumps(second_observation_json)) self.assertTrue(self.proxy.is_any_observation_running()) - self.assertTrue(self.proxy.is_observation_running(12345)) + self.assertTrue(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) self.assertTrue(self.proxy.is_observation_running(54321)) - self.proxy.stop_observation_now(12345) + self.proxy.stop_observation_now(self.EXPECTED_OBS_ID) self.proxy.stop_observation_now(54321) def test_stop_observation_invalid_id(self): @@ -265,31 +215,60 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # uses ID 12345 self.proxy.add_observation(self.VALID_JSON) - self.proxy.stop_observation_now(12345) + self.proxy.stop_observation_now(self.EXPECTED_OBS_ID) # Test false - self.assertFalse(self.proxy.is_observation_running(12345)) + self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) def test_start_multi_stop_all_observation(self): """Test starting and stopping multiple observations""" second_observation_json = json.loads(self.VALID_JSON) - second_observation_json["observation_id"] = 54321 + second_observation_json["antenna_fields"][0]["observation_id"] = 54321 self.on_device_assert(self.proxy) - # uses ID 12345 + # uses ID 5 self.proxy.add_observation(self.VALID_JSON) self.proxy.add_observation(json.dumps(second_observation_json)) self.proxy.stop_all_observations_now() # Test false - self.assertFalse(self.proxy.is_observation_running(12345)) + self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID)) self.assertFalse(self.proxy.is_observation_running(54321)) + def test_check_and_convert_parameters_invalid_id(self): + """Test invalid parameter detection""" + + parameters = json.loads(self.VALID_JSON) + for station in parameters["antenna_fields"]: + station["observation_id"] = -1 + + self.on_device_assert(self.proxy) + self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters)) + + def test_check_and_convert_parameters_invalid_empty(self): + """Test empty parameter detection""" + + self.on_device_assert(self.proxy) + self.assertRaises(DevFailed, self.proxy.add_observation, "{}") + def test_check_and_convert_parameters_invalid_antenna_set(self): """Test invalid antenna set name""" parameters = json.loads(self.VALID_JSON) - parameters["antenna_set"] = "ZZZ" + for station in parameters["antenna_fields"]: + station["antenna_set"] = "ZZZ" + self.on_device_assert(self.proxy) + self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters)) + + def test_check_and_convert_parameters_invalid_time(self): + """Test invalid parameter detection""" + + parameters = json.loads(self.VALID_JSON) + for antenna_field in parameters["antenna_fields"]: + antenna_field["stop_time"] = ( + datetime.now() - timedelta(seconds=1) + ).isoformat() + self.on_device_assert(self.proxy) self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters)) diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py similarity index 73% rename from tangostationcontrol/integration_test/default/devices/test_device_observation.py rename to tangostationcontrol/integration_test/default/devices/test_device_observation_field.py index 8dd06ebca2a9fa72feac5160b359e4ff53216146..61f6e7f367189d6a3bbb6931bdc6536963488f49 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py @@ -1,6 +1,7 @@ # Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 +import json from datetime import datetime from json import loads @@ -9,7 +10,8 @@ import numpy from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases -from tango import DevState, DevFailed +from tango import DevState +from tango import DevFailed from tangostationcontrol.common.constants import ( N_beamlets_ctrl, N_elements, @@ -21,10 +23,13 @@ from tangostationcontrol.devices.base_device_classes.antennafield_device import AntennaQuality, AntennaUse, ) -from tangostationcontrol.test.devices.test_observation_base import TestObservationBase + +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_hba_immediate, +) -class TestDeviceObservation(AbstractTestBases.TestDeviceBase): +class TestDeviceObservationField(AbstractTestBases.TestDeviceBase): ANTENNA_TO_SDP_MAPPING = [ "0", "0", @@ -81,51 +86,46 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_device_write_all_attributes(self): pass + @classmethod + def setUpClass(cls): + obs_control = TestDeviceProxy("STAT/ObservationControl/1") + obs_control.create_test_device() + + @classmethod + def tearDownClass(cls): + obs_control = TestDeviceProxy("STAT/ObservationControl/1") + obs_control.destroy_test_device() + def setUp(self): - super().setUp("STAT/Observation/1") - self.VALID_JSON = TestObservationBase.VALID_JSON - self.recv_proxy = self.setup_recv_proxy() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() - self.antennafield_proxy = self.setup_antennafield_proxy() - self.beamlet_proxy = self.setup_beamlet_proxy() - self.digitalbeam_proxy = self.setup_digitalbeam_proxy() - self.tilebeam_proxy = self.setup_tilebeam_proxy() - - def setup_recv_proxy(self): - # setup RECV - recv_proxy = TestDeviceProxy("STAT/RECVH/H0") - recv_proxy.off() - recv_proxy.boot() - recv_proxy.set_defaults() - return recv_proxy - - def setup_sdpfirmware_proxy(self): - # setup SDPFirmware - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP - sdp_proxy = TestDeviceProxy("STAT/SDP/HBA0") - sdp_proxy.off() - sdp_proxy.boot() - return sdp_proxy - - def setup_antennafield_proxy(self): - # setup AntennaField - antennafield_proxy = TestDeviceProxy("STAT/AFH/HBA0") + super().setUp("STAT/ObservationField/1") + self.VALID_JSON = json.dumps( + json.loads(get_observation_settings_hba_immediate().to_json())[ + "antenna_fields" + ][0] + ) + self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True) + self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0") + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0") + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", cb=self.antennafield_configure + ) + self.beamlet_proxy = self.setup_proxy("STAT/Beamlet/HBA0", defaults=True) + self.digitalbeam_proxy = self.setup_proxy( + "STAT/DigitalBeam/HBA0", defaults=True + ) + self.tilebeam_proxy = self.setup_proxy("STAT/TileBeam/HBA0", defaults=True) + + @staticmethod + def antennafield_configure(proxy: TestDeviceProxy): power_mapping = [[1, i * 2 + 0] for i in range(CS001_TILES)] control_mapping = [[1, i * 2 + 1] for i in range(CS001_TILES)] antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA) antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA) - antennafield_proxy.put_property( + proxy.put_property( { "Power_to_RECV_mapping": numpy.array(power_mapping).flatten(), "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), - "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, + "Antenna_to_SDP_Mapping": TestDeviceObservationField.ANTENNA_TO_SDP_MAPPING, "Antenna_Quality": antenna_qualities, "Antenna_Use": antenna_use, "Antenna_Sets": ["INNER", "OUTER", "SPARSE_EVEN", "SPARSE_ODD", "ALL"], @@ -141,47 +141,12 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): ], } ) - antennafield_proxy.off() - antennafield_proxy.boot() - return antennafield_proxy - - def setup_beamlet_proxy(self): - # setup Digitalbeam - beamlet_proxy = TestDeviceProxy("STAT/Beamlet/HBA0") - beamlet_proxy.off() - beamlet_proxy.boot() - beamlet_proxy.set_defaults() - return beamlet_proxy - - def setup_digitalbeam_proxy(self): - # setup Digitalbeam - digitalbeam_proxy = TestDeviceProxy("STAT/DigitalBeam/HBA0") - digitalbeam_proxy.off() - digitalbeam_proxy.boot() - digitalbeam_proxy.set_defaults() - return digitalbeam_proxy - - def setup_tilebeam_proxy(self): - # Setup Tilebeam - tilebeam_proxy = TestDeviceProxy("STAT/TileBeam/HBA0") - tilebeam_proxy.off() - tilebeam_proxy.boot() - tilebeam_proxy.set_defaults() - return tilebeam_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_init_valid(self): """Initialize an observation with valid JSON""" self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.assertEqual(DevState.STANDBY, self.proxy.state()) @@ -204,7 +169,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # Cannot write invalid settings with self.assertRaises(DevFailed): - self.proxy.observation_settings_RW = "{}" + self.proxy.observation_field_settings_RW = "{}" self.assertEqual(DevState.OFF, self.proxy.state()) @@ -212,11 +177,11 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): """Test that changing observation settings is disallowed once init""" self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() with self.assertRaises(DevFailed): - self.proxy.write_attribute("observation_settings_RW", self.VALID_JSON) + self.proxy.write_attribute("observation_field_settings_RW", self.VALID_JSON) def test_attribute_match(self): """Test that JSON data is exposed to attributes""" @@ -250,7 +215,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): dab_filter = data["HBA"]["DAB_filter"] self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() @@ -271,16 +236,16 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_apply_antennafield_settings(self): """Test that attribute filter is correctly applied""" - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - antennafield_proxy = self.setup_antennafield_proxy() + self.setup_proxy("STAT/StationManager/1") + + antennafield_proxy = self.antennafield_proxy antennafield_proxy.RCU_band_select_RW = [[0, 0]] * CS001_TILES self.assertListEqual( antennafield_proxy.RCU_band_select_RW.tolist(), [[0, 0]] * CS001_TILES, ) self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() expected_bands = [ @@ -292,12 +257,12 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_apply_subbands(self): """Test that attribute sap subbands is correctly applied""" - beamlet_proxy = self.setup_beamlet_proxy() + beamlet_proxy = self.beamlet_proxy subband_select = [0] * N_beamlets_ctrl beamlet_proxy.subband_select_RW = subband_select self.assertListEqual(beamlet_proxy.subband_select_RW.tolist(), subband_select) self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() expected_subbands = [10, 20, 30] + [0] * (N_beamlets_ctrl - 3) @@ -307,14 +272,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_apply_pointing(self): """Test that attribute sap pointing is correctly applied""" - digitalbeam_proxy = self.setup_digitalbeam_proxy() + digitalbeam_proxy = self.digitalbeam_proxy default_pointing = [("AZELGEO", "0rad", "1.570796rad")] * N_beamlets_ctrl digitalbeam_proxy.Pointing_direction_RW = default_pointing self.assertListEqual( list(digitalbeam_proxy.Pointing_direction_RW), default_pointing ) self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() @@ -338,14 +303,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_apply_tilebeam(self): # failing """Test that attribute tilebeam is correctly applied""" - tilebeam_proxy = self.setup_tilebeam_proxy() + tilebeam_proxy = self.tilebeam_proxy pointing_direction = [("J2000", "0rad", "0rad")] * CS001_TILES tilebeam_proxy.Pointing_direction_RW = pointing_direction self.assertListEqual( list(tilebeam_proxy.Pointing_direction_RW[0]), ["J2000", "0rad", "0rad"] ) self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() @@ -360,7 +325,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def test_apply_element_selection(self): # failing """Test that attribute element_selection is correctly applied""" - antennafield_proxy = self.setup_antennafield_proxy() + antennafield_proxy = self.antennafield_proxy antennafield_proxy.HBAT_PWR_on_RW = numpy.ones( (CS001_TILES, N_elements * N_pol), dtype=bool ) @@ -369,7 +334,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): ) self.proxy.off() - self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.observation_field_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sdp.py b/tangostationcontrol/integration_test/default/devices/test_device_sdp.py index ce74408401c5ba8ff048e007648a103bce4bd72d..6df13f529dcbb3b32633f047746027c63e415233 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_sdp.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_sdp.py @@ -8,3 +8,5 @@ class TestDeviceSDP(AbstractTestBases.TestDeviceBase): def setUp(self): """Intentionally recreate the device object in each test""" super().setUp("STAT/SDP/HBA0") + + self.setup_proxy("STAT/SDPFirmware/HBA0") diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sst.py b/tangostationcontrol/integration_test/default/devices/test_device_sst.py index 521562abed9480634d25c27ae329b5a49eedcd01..4975fd253f4a1ca4bbdbb4662e18418860c9afcf 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_sst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_sst.py @@ -5,7 +5,6 @@ import socket import sys import time -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases from tango import DevState @@ -16,15 +15,9 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): """Intentionally recreate the device object in each test""" super().setUp("STAT/SST/HBA0") - self.sdpfirmware_proxy = self.setup_sdpfirmware() - - def setup_sdpfirmware(self): - # setup SDP Firmware - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - sdpfirmware_proxy.set_defaults() - return sdpfirmware_proxy + self.sdpfirmware_proxy = self.setup_proxy( + "STAT/SDPFirmware/HBA0", defaults=True + ) def test_device_sst_send_udp(self): port_property = {"Statistics_Client_TCP_Port": "4998"} diff --git a/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py b/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py index fa561eb2ddd0d27c10d4d274d678244a1a8d9aa9..f9dbfefece2a1db754f0870376988a4e62b16db1 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py @@ -17,7 +17,6 @@ from tangostationcontrol.common.constants import ( from tangostationcontrol.devices.recv.recvh import RECVH -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases logger = logging.getLogger() @@ -37,8 +36,8 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): self.recvh_names = devices self.recv_proxies = self.setup_recvh_proxies() - self.sdpfirmware_proxy = self.setup_sdpfirmware_proxy() - self.sdp_proxy = self.setup_sdp_proxy() + self.sdpfirmware_proxy = self.setup_proxy(self.sdp_firmware_name, defaults=True) + self.sdp_proxy = self.setup_proxy(self.sdp_name, defaults=True) super().setUp(self.temperature_manager_name) self.addCleanup(self.restore_polling) @@ -51,33 +50,13 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): proxies = [] for recvh_name in self.recvh_names: - recv_proxy = TestDeviceProxy(recvh_name) + recv_proxy = self.setup_proxy(recvh_name) proxies.append(recv_proxy) - recv_proxy.off() - recv_proxy.initialise() - recv_proxy.on() - recv_proxy.poll_attribute(self.hbat_led_attribute, DEFAULT_POLLING_PERIOD) self.assertTrue(recv_proxy.is_attribute_polled(self.hbat_led_attribute)) return proxies - def setup_sdpfirmware_proxy(self): - # setup SDPFirmware - sdpfirmware_proxy = TestDeviceProxy(self.sdp_firmware_name) - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - sdpfirmware_proxy.set_defaults() - return sdpfirmware_proxy - - def setup_sdp_proxy(self): - # setup SDP, on which this device depends - sdp_proxy = TestDeviceProxy(self.sdp_name) - sdp_proxy.off() - sdp_proxy.boot() - sdp_proxy.set_defaults() - return sdp_proxy - def test_alarm(self): # Exclude other devices which raise a TimeoutError, # since they wait for the attribute *_translator_busy_R to become False diff --git a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py index 728d2aabc3361b7dacf72b1320639b1e38e30bbd..e8f2bcbedcb5d8a1e211521627600dee0fe807b9 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py @@ -9,7 +9,6 @@ import numpy from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases -from tango import DevState from tangostationcontrol.common.constants import ( CS001_TILES, MAX_ANTENNA, @@ -38,48 +37,34 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): """Setup Tilebeam""" super().setUp("STAT/TileBeam/HBA0") - def setup_recv_proxy(self): - """Setup RECV""" - recv_proxy = TestDeviceProxy("STAT/RECVH/H0") - recv_proxy.off() - recv_proxy.boot() - recv_proxy.set_defaults() - return recv_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 setup_antennafield_proxy(self): + self.station_manager_proxy = self.setup_proxy("STAT/StationManager/1") + self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True) + self.sdpfirmware_proxy = self.setup_proxy( + "STAT/SDPFirmware/HBA0", defaults=True + ) + self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0", defaults=True) + self.antennafield_proxy = self.setup_proxy( + "STAT/AFH/HBA0", cb=self.setup_antennafield_property + ) + + def setup_antennafield_property(self, proxy: TestDeviceProxy): """Setup AntennaField""" - antennafield_proxy = TestDeviceProxy("STAT/AFH/HBA0") control_mapping = [[1, i] for i in range(CS001_TILES)] antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA) antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA) - antennafield_proxy.put_property( + proxy.put_property( { "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_Quality": antenna_qualities, "Antenna_Use": antenna_use, } ) - antennafield_proxy.off() - antennafield_proxy.boot() # check if AntennaField really exposes the expected number of tiles - self.assertEqual(CS001_TILES, antennafield_proxy.nr_antennas_R) - return antennafield_proxy + self.assertEqual(CS001_TILES, proxy.nr_antennas_R) def test_delays_dims(self): """Verify delays are retrieved with correct dimensions""" - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - self.setup_antennafield_proxy() - # setup BEAM self.proxy.boot() @@ -89,16 +74,13 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): def test_set_pointing(self): """Verify if set pointing procedure is correctly executed""" - self.setup_stationmanager_proxy() - antennafield_proxy = self.setup_antennafield_proxy() - # setup BEAM self.proxy.boot() self.proxy.Tracking_enabled_RW = False # Verify attribute is present (all zeros if never used before) delays_r1 = numpy.array( - antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value + self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value ) self.assertIsNotNone(delays_r1) @@ -108,7 +90,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): # Verify writing operation does not lead to errors self.proxy.set_pointing(self.POINTING_DIRECTION) # write values to RECV delays_r2 = numpy.array( - antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value + self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value ) self.assertIsNotNone(delays_r2) @@ -117,11 +99,6 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): # self.assertFalse((delays_r1==delays_r2).all()) def test_pointing_to_zenith(self): - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - # setup AntennaField as well - antennafield_proxy = self.setup_antennafield_proxy() - self.proxy.boot() self.proxy.Tracking_enabled_RW = False @@ -131,7 +108,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): ) calculated_HBAT_delay_steps = numpy.array( - antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value + self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value ) expected_HBAT_delay_steps = numpy.array( @@ -143,10 +120,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): ) def test_pointing_across_horizon(self): - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - # setup AntennaField as well - antennafield_proxy = self.setup_antennafield_proxy() + antennafield_proxy = self.antennafield_proxy self.proxy.boot() self.proxy.Tracking_enabled_RW = False @@ -185,11 +159,6 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): ) def test_delays_same_as_LOFAR_ref_pointing(self): - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - # setup AntennaField as well - antennafield_proxy = self.setup_antennafield_proxy() - self.proxy.boot() self.proxy.Tracking_enabled_RW = False @@ -206,7 +175,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): self.proxy.set_pointing_for_specific_time(json_string) calculated_HBAT_delay_steps = numpy.array( - antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value + self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_RW").value ) # dims (CS001_TILES, 32) # Check all delay steps are zero with small margin @@ -230,9 +199,6 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): ) def test_tilebeam_tracking(self): - self.setup_stationmanager_proxy() - self.setup_recv_proxy() - self.setup_antennafield_proxy() self.proxy.boot() # check if we're really tracking diff --git a/tangostationcontrol/integration_test/default/devices/test_device_xst.py b/tangostationcontrol/integration_test/default/devices/test_device_xst.py index 7f0218b0d3ac379d4cde1de957efa9754374a476..2acca91246d88ce7465e732d559ca33eec4ad25e 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_xst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_xst.py @@ -1,7 +1,6 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from integration_test.device_proxy import TestDeviceProxy from integration_test.default.devices.base import AbstractTestBases @@ -10,12 +9,6 @@ class TestDeviceXST(AbstractTestBases.TestDeviceBase): """Intentionally recreate the device object in each test""" super().setUp("STAT/XST/HBA0") - self.sdpfirmware_proxy = self.setup_sdpfirmware() - - def setup_sdpfirmware(self): - # setup SDP Firmware - sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA0") - sdpfirmware_proxy.off() - sdpfirmware_proxy.boot() - sdpfirmware_proxy.set_defaults() - return sdpfirmware_proxy + self.sdpfirmware_proxy = self.setup_proxy( + "STAT/SDPFirmware/HBA0", defaults=True + ) diff --git a/tangostationcontrol/integration_test/default/devices/test_observation.py b/tangostationcontrol/integration_test/default/devices/test_observation_client.py similarity index 74% rename from tangostationcontrol/integration_test/default/devices/test_observation.py rename to tangostationcontrol/integration_test/default/devices/test_observation_client.py index 6233bbd5bc8907106990ca3ea06e038823841eb5..3b46b8319110f5acc9404af44e3f5e1765e21ad1 100644 --- a/tangostationcontrol/integration_test/default/devices/test_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_observation_client.py @@ -9,11 +9,12 @@ from lofar_station_client.observation.station_observation import ( ) from integration_test import base from integration_test.device_proxy import TestDeviceProxy +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_hba_immediate, +) from tango import DevState -from tangostationcontrol.test.devices.test_observation_base import TestObservationBase - class TestObservation(base.IntegrationTestCase): def setUp(self): @@ -21,10 +22,15 @@ class TestObservation(base.IntegrationTestCase): self.observation_control_proxy = TestDeviceProxy("STAT/ObservationControl/1") self.observation_control_proxy.off() self.observation_control_proxy.boot() + self.addCleanup( + self.observation_control_proxy.test_device_turn_off, + self.observation_control_proxy, + ) # make sure any devices we depend on are also started for device in [ "STAT/RECVH/H0", + "STAT/SDPFirmware/HBA0", "STAT/SDP/HBA0", "STAT/Beamlet/HBA0", "STAT/DigitalBeam/HBA0", @@ -34,22 +40,25 @@ class TestObservation(base.IntegrationTestCase): proxy = TestDeviceProxy(device) proxy.off() proxy.boot() + self.addCleanup(proxy.test_device_turn_off, proxy) def setup_stationmanager_proxy(self): """Setup StationManager""" stationmanager_proxy = TestDeviceProxy("STAT/StationManager/1") stationmanager_proxy.off() stationmanager_proxy.boot() + self.addCleanup(stationmanager_proxy.test_device_turn_off, stationmanager_proxy) self.assertEqual(stationmanager_proxy.state(), DevState.ON) return stationmanager_proxy def test_observation(self): """Test of the observation_wrapper class basic functionality""" - # convert the JSON specificiation to a dict for this class - specification_dict = loads(TestObservationBase.VALID_JSON) + # convert the JSON specification to a dict for this class + specification_dict = loads(get_observation_settings_hba_immediate().to_json()) - # create an observation class using the dict and as host just get it using a util function + # create an observation class using the dict and as host just get it using a + # util function observation = StationObservation( specification=specification_dict, host=environ["TANGO_HOST"] ) @@ -60,7 +69,7 @@ class TestObservation(base.IntegrationTestCase): # Assert the proxy is on station = observation.observation - proxy = station.observation_proxy + proxy = station.observation_field_proxies[0] self.assertTrue(proxy.state() == DevState.ON) # Assert the observation has stopped after aborting diff --git a/tangostationcontrol/integration_test/device_proxy.py b/tangostationcontrol/integration_test/device_proxy.py index d61e4f00a6a3afee2f8c2925dc61baa6deabedd0..87557e2194547434e1fb3307cd5ef1798b0ce607 100644 --- a/tangostationcontrol/integration_test/device_proxy.py +++ b/tangostationcontrol/integration_test/device_proxy.py @@ -25,7 +25,7 @@ class TestDeviceProxy(DeviceProxy): self.set_source(DevSource.DEV) @staticmethod - def test_device_turn_off(endpoint): + def test_device_turn_off(endpoint: str, sleep: float = 1): d = TestDeviceProxy(endpoint) try: d.Off() @@ -33,5 +33,5 @@ class TestDeviceProxy(DeviceProxy): """Failing to turn Off devices should not raise errors here""" logger.error(f"Failed to turn device off in teardown {e}") - """Wait for 1 second to prevent propagating reconnection errors""" - time.sleep(1) + """Wait to prevent propagating reconnection errors""" + time.sleep(sleep) diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt index a875eb4b0bfff61e9d732e0c0f7048f84730b9e5..58c9d0693759fb5e84d69e2e4028051488627317 100644 --- a/tangostationcontrol/requirements.txt +++ b/tangostationcontrol/requirements.txt @@ -2,7 +2,7 @@ # order of appearance. Changing the order has an impact on the overall # integration process, which may cause wedges in the gate later. -lofar-station-client@git+https://git.astron.nl/lofar2.0/lofar-station-client # Apache 2 +lofar-station-client@git+https://git.astron.nl/lofar2.0/lofar-station-client # Apache 2 PyTango>=9.4.2 # LGPL v3 numpy>=1.21.6 # BSD3 asyncua >= 0.9.90 # LGPLv3 diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg index 90a78e2f90be128adf4a0e6fc8e15878e9217e06..f9594fe96de49f5420aad7bf8ba5850ff30f1edb 100644 --- a/tangostationcontrol/setup.cfg +++ b/tangostationcontrol/setup.cfg @@ -33,14 +33,14 @@ where = . console_scripts = l2ss-ds = tangostationcontrol.device_server:main l2ss-analyse-dsconfig-hierarchies = tangostationcontrol.toolkit.analyse_dsconfig_hierarchies:main + l2ss-version = tangostationcontrol:print_version + l2ss-health = tangostationcontrol.common.health:main # The following entry points should eventually be removed / replaced l2ss-hardware-device-template = tangostationcontrol.examples.HW_device_template:main l2ss-ini-device = tangostationcontrol.examples.load_from_disk.ini_device:main l2ss-parse-statistics-packet = tangostationcontrol.statistics.packet:main l2ss-random-data = tangostationcontrol.test.devices.random_data:main - l2ss-version = tangostationcontrol:print_version - l2ss-health = tangostationcontrol.common.health:main [options.package_data] * = *.json, *.mib diff --git a/tangostationcontrol/tangostationcontrol/common/__init__.py b/tangostationcontrol/tangostationcontrol/common/__init__.py index bbdd80eaa2ac676116d0ae5a10856f970e3378b0..c92b615444d854a6e87370b16cf733a5859a07e7 100644 --- a/tangostationcontrol/tangostationcontrol/common/__init__.py +++ b/tangostationcontrol/tangostationcontrol/common/__init__.py @@ -1,6 +1,2 @@ # Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 - -from .observation_controller import ObservationController - -__all__ = ["ObservationController"] diff --git a/tangostationcontrol/tangostationcontrol/common/antennas.py b/tangostationcontrol/tangostationcontrol/common/antennas.py index a06e58984a085fa775154e75fd8cc530789c5046..309934820b94a9b575822ec062e094c771515fdd 100644 --- a/tangostationcontrol/tangostationcontrol/common/antennas.py +++ b/tangostationcontrol/tangostationcontrol/common/antennas.py @@ -1,5 +1,6 @@ # Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 + import numpy from tango import Util @@ -52,6 +53,10 @@ def device_member_to_full_device_name(device_member: str) -> str: return f"{util.get_ds_inst_name()}/AFL/{device_member}" if CaseInsensitiveString("HBA") in CaseInsensitiveString(device_member): return f"{util.get_ds_inst_name()}/AFH/{device_member}" + if CaseInsensitiveString("HBA0") in CaseInsensitiveString(device_member): + return f"{util.get_ds_inst_name()}/AFH/{device_member}" + if CaseInsensitiveString("HBA1") in CaseInsensitiveString(device_member): + return f"{util.get_ds_inst_name()}/AFH/{device_member}" raise ValueError(f"Invalid value for antennafield parameter: {device_member}") @@ -68,4 +73,8 @@ def device_member_to_antenna_type(device_member: str) -> str: return "LBA" if CaseInsensitiveString("HBA") in CaseInsensitiveString(device_member): return "HBA" + if CaseInsensitiveString("HBA0") in CaseInsensitiveString(device_member): + return "HBA" + if CaseInsensitiveString("HBA1") in CaseInsensitiveString(device_member): + return "HBA" raise ValueError(f"Invalid value for antennafield: {device_member}") diff --git a/tangostationcontrol/tangostationcontrol/common/case_insensitive_string.py b/tangostationcontrol/tangostationcontrol/common/case_insensitive_string.py index 507bb69700501a0808a6a5eb95237ec168e85599..35e510e917f8638d1a98f51a1ccedc6bb2f67f15 100644 --- a/tangostationcontrol/tangostationcontrol/common/case_insensitive_string.py +++ b/tangostationcontrol/tangostationcontrol/common/case_insensitive_string.py @@ -14,6 +14,11 @@ class CaseInsensitiveString(str): def __hash__(self): return hash(self.__str__()) + def __contains__(self, key): + if isinstance(key, str): + return key.casefold() in str(self) + return key in str(self) + def __str__(self) -> str: return self.casefold().__str__() diff --git a/tangostationcontrol/tangostationcontrol/common/configuration.py b/tangostationcontrol/tangostationcontrol/common/configuration.py index 09be2024ee77f3a7ba60b7df25f2537aecaf6d24..b2fd8549bff314821d41109b31d336ec2eb3d043 100644 --- a/tangostationcontrol/tangostationcontrol/common/configuration.py +++ b/tangostationcontrol/tangostationcontrol/common/configuration.py @@ -49,7 +49,6 @@ class StationConfiguration: @classmethod def get_validator(cls): """Retrieve the JSON validator from Schemas container""" - name = cls.__name__ return Draft7Validator( REGISTRY["station-configuration"].contents, format_checker=FormatChecker(), diff --git a/tangostationcontrol/tangostationcontrol/common/observation_controller.py b/tangostationcontrol/tangostationcontrol/common/observation_controller.py deleted file mode 100644 index 360e353515b25fa4b1f88cbeb97da696f7ce52c5..0000000000000000000000000000000000000000 --- a/tangostationcontrol/tangostationcontrol/common/observation_controller.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 - -import logging -import time -from datetime import datetime - -from tango import DevFailed, DevState, Except, Util, EventType, DeviceProxy -from tangostationcontrol.common.lofar_logging import log_exceptions -from tangostationcontrol.common.proxy import create_device_proxy -from tangostationcontrol.configuration import ObservationSettings - -logger = logging.getLogger() - - -class Observation(object): - @property - def proxy(self) -> DeviceProxy: - return self._device_proxy - - @property - def observation_id(self) -> int: - return self._parameters.observation_id - - @property - def class_name(self) -> str: - from tangostationcontrol.devices.observation import Observation - - return Observation.__name__ - - @property - def device_name(self) -> str: - return f"{self._tango_domain}/{self.class_name}/{self.observation_id}" - - # Name for the Observation.observation_running subscription - @property - def attribute_name(self) -> str: - return f"{self.device_name}/alive_R" - - def __init__(self, tango_domain, parameters: ObservationSettings): - self._device_proxy: DeviceProxy | None = None - self._event_id: int | None = None - self._parameters: ObservationSettings = parameters - self._tango_domain: str = tango_domain - - # The pyTango.Util class is a singleton and every DS can only - # have one instance of it. - self._tango_util: Util = Util.instance() - - def create_observation_device(self): - """Instatiate an Observation Device""" - logger.info("Create device: %s", self.device_name) - try: - # Create the Observation device and instantiate it. - self._tango_util.create_device(self.class_name, f"{self.device_name}") - except DevFailed as ex: - logger.exception(ex) - if ( - ex.args[0].desc - == f"The device {self.device_name.lower()} is already defined in the database" - ): - # and self.is_observation_running(self.observation_id) is False: - self._tango_util.delete_device(self.class_name, self.device_name) - error_string = f"Cannot create the Observation device {self.device_name} \ - because it is already present in the Database but it is not running. \ - Try to re-run the start_observation command" - logger.exception(error_string) - Except.re_throw_exception(ex, "DevFailed", error_string, __name__) - else: - error_string = f"Cannot create the Observation device instance \ - {self.device_name} for ID={self.observation_id}. \ - This means that the observation did not start." - logger.exception(error_string) - Except.re_throw_exception(ex, "DevFailed", error_string, __name__) - - def destroy_observation_device(self): - try: - self._tango_util.delete_device(self.class_name, self.device_name) - except DevFailed: - logger.exception( - f"Could not delete device {self.device_name} of class {self.class_name} from Tango DB." - ) - - def initialise_observation(self): - # Instantiate a dynamic Tango Device "Observation". - self._device_proxy = create_device_proxy(self.device_name) - - # Initialise generic properties - self.proxy.put_property({"Control_Children": [], "Power_Children": []}) - - # Configure the dynamic device its attribute for the observation - # parameters. - self.proxy.observation_settings_RW = self._parameters.to_json() - - # Take the Observation device through the motions. Pass the - # entire JSON set of parameters so that it can pull from it what it - # needs. - self.proxy.Initialise() - - def start(self): - self.proxy.On() - - def is_running(self): - return self.proxy and self.proxy.state() == DevState.ON - - def subscribe(self, cb): - # Turn on the polling for the attribute. - # Note that this is not automatically done despite the attribute - # having the right polling values set in the ctor. - self.proxy.poll_attribute(self.attribute_name.split("/")[-1], 1000) - - # Right. Now subscribe to periodic events. - self._event_id = self._device_proxy.subscribe_event( - self.attribute_name.split("/")[-1], EventType.PERIODIC_EVENT, cb - ) - logger.info( - "Successfully started an observation with ID=%s.", self.observation_id - ) - - def stop(self): - # Check if the device has not terminated itself in the meanwhile. - try: - self.proxy.ping() - except DevFailed: - logger.error( - f"Observation device for ID={self.observation_id} unexpectedly disappeared." - ) - else: - # Unsubscribe from the subscribed event. - self.proxy.unsubscribe_event(self._event_id) - - # Tell the Observation device to stop the running - # observation. This is a synchronous call and the clean-up - # does not take long. - self.proxy.Off() - - # Finally remove the device object from the Tango DB. - self.destroy_observation_device() - - -class ObservationController(dict[str, Observation]): - """A dictionary of observations. Actively manages the observation state transtions - (start, stop).""" - - def __init__(self, tango_domain: str): - self._tango_util = Util.instance() - self._tango_domain = tango_domain - - @property - def running_observations(self) -> list[str]: - return [obs_id for obs_id, obs in self.items() if obs.is_running()] - - @log_exceptions() - def observation_callback(self, event): - """ - This callback checks and manages the state transitions - for each observation. - - It starts observations at their specified start_time, - and stops & removes them at their specified stop_time. - """ - if event.err: - # Something is fishy with this event. - logger.warning( - "The Observation device %s sent an event but the event \ - signals an error. It is advised to check the logs for any indication \ - that something went wrong in that device. Event data=%s", - event.device, - event, - ) - return - - # update the state of this observation, if needed - self._update_observation_state(event.device) - - def _update_observation_state(self, device: DeviceProxy): - """Start or stop the observation managed by the given Observation - device.""" - - # Get the Observation ID from the sending device. - obs_id = device.observation_id_R - - # Get the start/stop times from the sending device - obs_start_time = device.start_time_R - obs_stop_time = device.stop_time_R - - # Get how much earlier we have to start - obs_lead_time = device.lead_time_R - - # Obtain the current time ONCE to avoid race conditions - now = time.time() - - # Manage state transitions - if now > obs_stop_time: - # Stop observation - self.stop_observation_now(obs_id) - elif ( - now >= obs_start_time - obs_lead_time and device.state() == DevState.STANDBY - ): - # Start observation - self.start_observation(obs_id) - - def add_observation(self, settings: ObservationSettings): - """Create a new Observation Device and start an observation""" - # Check further properties that cannot be validated through a JSON schema - if settings.stop_time <= datetime.now(): - raise ValueError( - f"Cannot start observation {settings.observation_id} because it is already past its stop time {settings.stop_time}" - ) - - obs = Observation(self._tango_domain, settings) - obs.create_observation_device() - - try: - obs.initialise_observation() - except DevFailed as ex: - # Remove the device again. - obs.destroy_observation_device() - - raise Exception( - "Failed to initialise observation {settings.observation_id}" - ) from ex - - # Register this observation now that is has been succesfully created - self[obs.observation_id] = obs - - # Keep pinging it to manage its state transitions - try: - obs.subscribe(self.observation_callback) - except DevFailed as ex: - # Remove the device again. - obs.destroy_observation_device() - - raise Exception( - "Cannot subscribe to attribute. Cancelled observation {settings.observation_id}." - ) from ex - - # Make sure the current state is accurate - self._update_observation_state(obs.proxy) - - def start_observation(self, obs_id): - """Start the observation with the given ID""" - try: - observation = self[obs_id] - except KeyError as _exc: - raise Exception(f"Unknown observation: {obs_id}") - - observation.start() - - def stop_observation_now(self, obs_id): - """Stop the observation with the given ID""" - try: - observation = self.pop(obs_id) - except KeyError as _exc: - raise Exception(f"Unknown observation: {obs_id}") - - observation.stop() - - def stop_all_observations_now(self): - """Stop all observations (running or to be run)""" - for obs_id in list(self): # draw a copy as we modify the list - self.stop_observation_now(obs_id) diff --git a/tangostationcontrol/tangostationcontrol/common/states.py b/tangostationcontrol/tangostationcontrol/common/states.py index 6ab0c5a581978e00e1ae09bcb4c3d3d08c219879..e5664f35234cbe659f2c86d5eb36ba8618e0fb47 100644 --- a/tangostationcontrol/tangostationcontrol/common/states.py +++ b/tangostationcontrol/tangostationcontrol/common/states.py @@ -71,7 +71,7 @@ DEVICES_ON_IN_STATION_STATE: Dict[str, Optional[StationState]] = { "RECVH": StationState.STANDBY, "RECVL": StationState.STANDBY, # TODO(JDM) Lingering observations (and debug observation 0) should not be booted as we do not persist their settings - "Observation": None, + "ObservationField": None, # Unmentioned devices will go ON in this state "_default": StationState.ON, } diff --git a/tangostationcontrol/tangostationcontrol/configuration/__init__.py b/tangostationcontrol/tangostationcontrol/configuration/__init__.py index d5b9dc9bd9cc8bd7d1e442f79c5e00832c7b2b5b..b567fc35750358442fa60da4e845980744541f72 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/__init__.py +++ b/tangostationcontrol/tangostationcontrol/configuration/__init__.py @@ -5,7 +5,16 @@ from ._schemas import REGISTRY from .dithering import Dithering from .hba import HBA from .observation_settings import ObservationSettings +from .observation_field_settings import ObservationFieldSettings from .pointing import Pointing from .sap import Sap -__all__ = ["ObservationSettings", "Pointing", "Sap", "HBA", "Dithering", "REGISTRY"] +__all__ = [ + "ObservationSettings", + "ObservationFieldSettings", + "Pointing", + "Sap", + "HBA", + "Dithering", + "REGISTRY", +] diff --git a/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py b/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py index 026be5be9c924a59591fe72717d8e0ec7e37e917..92daf15a46e8bc444263c2380936c7f072ab56b3 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py +++ b/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py @@ -11,13 +11,23 @@ def _from_json_hook_t(primary: Type): Pointing, Sap, ObservationSettings, + ObservationFieldSettings, HBA, Dithering, ) def actual_hook(json_dct): + """Validate json_dct for each schema layer up to the primary type""" primary_ex = None - for t in [Pointing, Sap, HBA, Dithering, ObservationSettings]: + # Order is critical, must match inheritance, deepest layers first + for t in [ + Pointing, + Sap, + HBA, + Dithering, + ObservationFieldSettings, + ObservationSettings, + ]: try: t.get_validator().validate(json_dct) except ValidationError as ex: diff --git a/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py b/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py index ea8769d550f9659e1b5ce516c14339574686a84d..a8e94dc8a140765fe32b7463d6f6e8a5c7e64ea8 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py +++ b/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py @@ -29,17 +29,21 @@ jsonschema.validators.Draft7Validator.TYPE_CHECKER = ( class _ConfigurationBase(ABC): @staticmethod - def _class_to_url(cls_name): + def _class_to_url(cls_name: str) -> str: + """Class name to json schema file conversion name""" cls_name = cls_name.replace("HBA", "hba") cls_name = cls_name.replace("LBA", "lba") return re.sub(r"(?<!^)(?=[A-Z])", "-", cls_name).lower() @classmethod - def get_validator(cls): - """Retrieve the JSON validator from Schemas container""" - name = cls.__name__ + def get_validator(cls) -> Draft7Validator: + """Retrieve schema validation file and return Draft7Validator instance + + Schema file name is derived from class name see :py:func:`~._class_to_url` for + conversion + """ return Draft7Validator( - REGISTRY[_ConfigurationBase._class_to_url(name)].contents, + REGISTRY[_ConfigurationBase._class_to_url(cls.__name__)].contents, format_checker=FormatChecker(), registry=REGISTRY, ) @@ -62,7 +66,7 @@ class _ConfigurationBase(ABC): def __contains__(self, item): return hasattr(self, item) and getattr(self, item) is not None - def to_json(self): + def to_json(self) -> str: return self.__str__() @staticmethod @@ -75,6 +79,7 @@ class _ConfigurationBase(ABC): s = json.loads(data, object_hook=_from_json_hook_t(cls)) if not isinstance(s, cls): raise ValidationError( - f"Unexpected type: expected <{cls.__class__.__name__}>, got <{type(s).__name__}>" + f"Unexpected type: expected <{cls.__class__.__name__}>, got " + f"<{type(s).__name__}>" ) return s diff --git a/tangostationcontrol/tangostationcontrol/configuration/dithering.py b/tangostationcontrol/tangostationcontrol/configuration/dithering.py index 750a6e463358e34b56151baf001e46ce24638530..fa02bde00ec5541b87bc9a2911a74f1be0d81ba3 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/dithering.py +++ b/tangostationcontrol/tangostationcontrol/configuration/dithering.py @@ -11,7 +11,7 @@ class Dithering(_ConfigurationBase): self, enabled: bool, power: float | None, - frequency: float | None, + frequency: int | None, ): self.enabled = enabled self.power = power diff --git a/tangostationcontrol/tangostationcontrol/configuration/observation_field_settings.py b/tangostationcontrol/tangostationcontrol/configuration/observation_field_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..d862468ac193d8184bbdf12b1b29b7e6a10e86c7 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/configuration/observation_field_settings.py @@ -0,0 +1,86 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from datetime import datetime +from typing import Sequence + +from tangostationcontrol.configuration.configuration_base import _ConfigurationBase +from tangostationcontrol.configuration.dithering import Dithering +from tangostationcontrol.configuration.hba import HBA +from tangostationcontrol.configuration.sap import Sap + + +class ObservationFieldSettings(_ConfigurationBase): + def __init__( + self, + observation_id: int, + start_time: str | datetime | None, + stop_time: str | datetime, + antenna_field: str, + antenna_set: str, + filter: str, + SAPs: Sequence[Sap], + HBA: HBA | None = None, + first_beamlet: int = 0, + lead_time: float | None = None, + dithering: Dithering | None = None, + ): + self.observation_id = observation_id + self.start_time = self._parse_and_convert_datetime(start_time) + self.stop_time = self._parse_and_convert_datetime(stop_time) + self.antenna_field = antenna_field + self.antenna_set = antenna_set + self.filter = filter + self.SAPs = SAPs + self.HBA = HBA + self.first_beamlet = first_beamlet + self.lead_time = lead_time + self.dithering = dithering + + @staticmethod + def _parse_and_convert_datetime(time: str | datetime | None): + """Transparently convert datetime to string in isoformat""" + if time and not isinstance(time, datetime): + try: + datetime.fromisoformat(time) + except ValueError as ex: + raise ex + if time and isinstance(time, datetime): + return time.isoformat() + + return time + + def __iter__(self): + yield "observation_id", self.observation_id + if self.start_time: + yield "start_time", self.start_time + yield from { + "stop_time": self.stop_time, + "antenna_field": self.antenna_field, + "antenna_set": self.antenna_set, + "filter": self.filter, + "SAPs": [dict(s) for s in self.SAPs], + }.items() + if self.HBA: + yield "HBA", dict(self.HBA) + yield "first_beamlet", self.first_beamlet + if self.lead_time is not None: + yield "lead_time", self.lead_time + if self.dithering is not None: + yield "dithering", dict(self.dithering) + + @staticmethod + def to_object(json_dct) -> "ObservationFieldSettings": + return ObservationFieldSettings( + json_dct["observation_id"], + json_dct["start_time"] if "start_time" in json_dct else None, + json_dct["stop_time"], + json_dct["antenna_field"], + json_dct["antenna_set"], + json_dct["filter"], + json_dct["SAPs"], + json_dct.get("HBA"), + json_dct.get("first_beamlet", 0), + json_dct.get("lead_time"), + json_dct.get("dithering"), + ) diff --git a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py index 40d64922187fb1bcec7cb7c30578e0922d1ce1ae..dff442d7cd63b057f59f71604c2f28328d949ce2 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py +++ b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py @@ -1,75 +1,25 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from datetime import datetime from typing import Sequence from tangostationcontrol.configuration.configuration_base import _ConfigurationBase -from tangostationcontrol.configuration.dithering import Dithering -from tangostationcontrol.configuration.hba import HBA -from tangostationcontrol.configuration.sap import Sap +from tangostationcontrol.configuration.observation_field_settings import ( + ObservationFieldSettings, +) class ObservationSettings(_ConfigurationBase): def __init__( - self, - observation_id: int, - start_time: datetime | None, - stop_time: datetime, - antenna_field: str, - antenna_set: str, - filter: str, - SAPs: Sequence[Sap], - HBA: HBA | None = None, - first_beamlet: int = 0, - lead_time: float | None = None, - dithering: Dithering | None = None, + self, station: str, antenna_fields: Sequence[ObservationFieldSettings] ): - self.observation_id = observation_id - self.start_time = start_time - self.stop_time = stop_time - self.antenna_field = antenna_field - self.antenna_set = antenna_set - self.filter = filter - self.SAPs = SAPs - self.HBA = HBA - self.first_beamlet = first_beamlet - self.lead_time = lead_time - self.dithering = dithering + self.station = station + self.antenna_fields = antenna_fields def __iter__(self): - yield "observation_id", self.observation_id - if self.start_time: - yield "start_time", self.start_time.isoformat() - yield from { - "stop_time": self.stop_time.isoformat(), - "antenna_field": self.antenna_field, - "antenna_set": self.antenna_set, - "filter": self.filter, - "SAPs": [dict(s) for s in self.SAPs], - }.items() - if self.HBA: - yield "HBA", dict(self.HBA) - yield "first_beamlet", self.first_beamlet - if self.lead_time is not None: - yield "lead_time", self.lead_time - if self.dithering is not None: - yield "dithering", dict(self.dithering) + yield "station", self.station + yield "antenna_fields", [dict(s) for s in self.antenna_fields] @staticmethod def to_object(json_dct) -> "ObservationSettings": - return ObservationSettings( - json_dct["observation_id"], - datetime.fromisoformat(json_dct["start_time"]) - if "start_time" in json_dct - else None, - datetime.fromisoformat(json_dct["stop_time"]), - json_dct["antenna_field"], - json_dct["antenna_set"], - json_dct["filter"], - json_dct["SAPs"], - json_dct.get("HBA"), - json_dct.get("first_beamlet", 0), - json_dct.get("lead_time"), - json_dct.get("dithering"), - ) + return ObservationSettings(json_dct["station"], json_dct["antenna_fields"]) diff --git a/tangostationcontrol/tangostationcontrol/configuration/schemas/dithering.json b/tangostationcontrol/tangostationcontrol/configuration/schemas/dithering.json index 05c327b6eb2bcf92af96b457fef97f6024605987..7197cea261a21e69d097b168d3dade4463c085d8 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/schemas/dithering.json +++ b/tangostationcontrol/tangostationcontrol/configuration/schemas/dithering.json @@ -19,7 +19,7 @@ "default": -4.0 }, "frequency": { - "type": "number", + "type": "integer", "description": "Frequency of dithering signal, in Hz", "default": 102000000 } diff --git a/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-field-settings.json b/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-field-settings.json new file mode 100644 index 0000000000000000000000000000000000000000..98d90868c0f863a5b4f355211d3eb7f12e531ea4 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-field-settings.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "observation-field-settings", + "type": "object", + "required": [ + "observation_id", + "stop_time", + "antenna_field", + "antenna_set", + "filter", + "SAPs" + ], + "properties": { + "observation_id": { + "type": "number", + "minimum": 1 + }, + "start_time": { + "type": "string", + "format": "date-time", + "default": "1970-01-01T00:00:00" + }, + "stop_time": { + "type": "string", + "format": "date-time" + }, + "lead_time": { + "type": "number", + "description": "Number of seconds to start before the provided start time, to account for initialising the on-line signal chain, and for possibly negative geometrical delay compensation.", + "default": 2.0, + "minimum": 0 + }, + "antenna_field": { + "default": "HBA", + "description": "Antenna field to use", + "type": "string", + "enum": [ + "LBA", + "HBA", + "HBA0", + "HBA1" + ] + }, + "antenna_set": { + "default": "ALL", + "description": "Fields & antennas to use", + "type": "string", + "enum": [ + "ALL", + "INNER", + "OUTER", + "SPARSE_EVEN", + "SPARSE_ODD" + ] + }, + "dithering": { + "$ref": "dithering" + }, + "filter": { + "type": "string", + "enum": [ + "LBA_10_90", + "LBA_10_70", + "LBA_30_90", + "LBA_30_70", + "HBA_170_230", + "HBA_110_190", + "HBA_210_250" + ] + }, + "SAPs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "sap" + } + }, + "first_beamlet": { + "type": "number", + "default": 0, + "minimum": 0 + }, + "HBA": { + "$ref": "hba" + } + } +} diff --git a/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-settings.json b/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-settings.json index c23561dc71f93d298a9c03cccfe7bbf388e655f3..a7d6ce1057a7dfbb9767a9ee8c2e55659df8ca84 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-settings.json +++ b/tangostationcontrol/tangostationcontrol/configuration/schemas/observation-settings.json @@ -3,85 +3,20 @@ "$id": "observation-settings", "type": "object", "required": [ - "observation_id", - "stop_time", - "antenna_field", - "antenna_set", - "filter", - "SAPs" + "station", + "antenna_fields" ], "properties": { - "observation_id": { - "type": "number", - "minimum": 1 - }, - "start_time": { - "type": "string", - "format": "date-time", - "default": "1970-01-01T00:00:00" - }, - "stop_time": { + "station": { "type": "string", - "format": "date-time" - }, - "lead_time": { - "type": "number", - "description": "Number of seconds to start before the provided start time, to account for initialising the on-line signal chain, and for possibly negative geometrical delay compensation.", - "default": 2.0, - "minimum": 0 + "default": "CS001" }, - "antenna_field": { - "default": "HBA", - "description": "Antenna field to use", - "type": "string", - "enum": [ - "LBA", - "HBA", - "HBA0", - "HBA1" - ] - }, - "antenna_set": { - "default": "ALL", - "description": "Fields & antennas to use", - "type": "string", - "enum": [ - "ALL", - "INNER", - "OUTER", - "SPARSE_EVEN", - "SPARSE_ODD" - ] - }, - "dithering": { - "$ref": "dithering" - }, - "filter": { - "type": "string", - "enum": [ - "LBA_10_90", - "LBA_10_70", - "LBA_30_90", - "LBA_30_70", - "HBA_170_230", - "HBA_110_190", - "HBA_210_250" - ] - }, - "SAPs": { + "antenna_fields": { "type": "array", "minItems": 1, "items": { - "$ref": "sap" + "$ref": "observation-field-settings" } - }, - "first_beamlet": { - "type": "number", - "default": 0, - "minimum": 0 - }, - "HBA": { - "$ref": "hba" } } } diff --git a/tangostationcontrol/tangostationcontrol/devices/__init__.py b/tangostationcontrol/tangostationcontrol/devices/__init__.py index b6038faf0137d4b710dbe92be7f0a953755d1a0f..acbde7b69d7a2d2bdbb00b6fc504b69dfa749d59 100644 --- a/tangostationcontrol/tangostationcontrol/devices/__init__.py +++ b/tangostationcontrol/tangostationcontrol/devices/__init__.py @@ -11,7 +11,7 @@ from .ccd import CCD from .configuration import Configuration from .docker import Docker from .ec import EC -from .observation import Observation +from .observation_field import ObservationField from .observation_control import ObservationControl from .pcon import PCON from .psoc import PSOC @@ -41,7 +41,7 @@ __all__ = [ "Configuration", "Docker", "EC", - "Observation", + "ObservationField", "ObservationControl", "PCON", "PSOC", diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_control.py b/tangostationcontrol/tangostationcontrol/devices/observation_control.py index ef6eb5fdd377454dbf8795c3eed605e4203104f0..714b4507e7fdd04221372cb487d269d0c4ce5561 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation_control.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_control.py @@ -12,15 +12,17 @@ from tango import ( DevString, ) from tango.server import Device, command, attribute -from tangostationcontrol.common import ObservationController +from tangostationcontrol.observation.observation_controller import ObservationController from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) from tangostationcontrol.configuration import ObservationSettings from tangostationcontrol.devices.base_device_classes.lofar_device import LOFARDevice +from tangostationcontrol.devices.observation_field import ObservationField from tangostationcontrol.common.device_decorators import only_when_on + logger = logging.getLogger() __all__ = ["ObservationControl"] @@ -29,31 +31,33 @@ __all__ = ["ObservationControl"] @device_logging_to_python() class ObservationControl(LOFARDevice): """Observation Control Device Server for LOFAR2.0 - The ObservationControl Tango device controls the instantiation of a Tango Dynamic Device - from the Observation class. - ObservationControl then keeps a record of the Observation devices and if they are still alive. + The ObservationControl Tango device controls the instantiation of a Tango Dynamic + Device from the ObservationField class. + ObservationControl then keeps a record of the ObservationField devices and if they + are still alive. At the end of an observation ObservationControl checks if the respective - Observation device has stopped its execution and releases it. If the Observation device - has not stopped its execution yet, it is attempted to forcefully stop the execution - of the Observation device. + ObservationField devices have stopped its execution and releases it. If the + ObservationField devices have not stopped its execution yet, it is attempted to + forcefully stop the execution of them. - The Observation devices are responsible for the "real" execution of an observation. - They get references to the hardware devices that are needed to set values in the - relevant Control Points. The Observation device performs only a check if enough parameters - are available to perform the set-up. + The ObservationField devices are responsible for the "real" execution of an + observation. They get references to the hardware devices that are needed to set + values in the relevant Control Points. The ObservationField device performs only a + check if enough parameters are available to perform the set-up. Essentially this is what happens: Somebody calls ObservationControl.add_observation(parameters). Then ObservationControl will perform: - - Creates a new instance of an Observation device in the Tango DB + - Creates a new instances of an ObservationField devices in the Tango DB + (one per antenna field) - Call Initialise(parameters) - Wait for initialise to return - Check status() - If status() is NOT STANDBY, abort with an exception - Wait for start_time - lead_time - Call On() - - Subscribe to the Observation.running MP's periodic event + - Subscribe to the ObservationField.alive_R MP's periodic event - Register the observation in the dict self.running_observations[ID] - The Observation updates the MP every second with the current time - The callback gets called periodically. @@ -65,19 +69,6 @@ class ObservationControl(LOFARDevice): - Call off() - Remove the device from the Tango DB which will make the device disappear - This should in broad strokes pretty much cover any type of observation. - - ObservationControl can expose this interface: - - Functions - - Normal lifecycle funcs: initialise, on, off - - add_observation(parameters) - - start_observation_now(ID) - - stop_observation_now(ID) - - stop_all_observations_now() - - running_observations() -> dict - - is_observation_running(obs_id) -> bool - MPs - string version """ @@ -101,6 +92,15 @@ class ObservationControl(LOFARDevice): def running_observations_R(self): return self._observation_controller.running_observations + @attribute( + doc="List of active antenna fields.", + dtype=(str,), + max_dim_x=1000, + fisallowed="is_attribute_access_allowed", + ) + def active_antenna_fields_R(self): + return self._observation_controller.active_antenna_fields + def __init__(self, cl, name): # Super must be called after variable assignment due to executing init_device! super().__init__(cl, name) @@ -113,10 +113,13 @@ class ObservationControl(LOFARDevice): self.myTangoDomain ) + self._stop_all_observations_now() + # Core functions @log_exceptions() @DebugIt() def init_device(self): + logger.debug("[ObservationControl] init device") Device.init_device(self) self.set_state(DevState.OFF) @@ -130,7 +133,7 @@ class ObservationControl(LOFARDevice): def configure_for_off(self): super().configure_for_off() - self.stop_all_observations_now() + self._stop_all_observations_now() # API @command(dtype_in=DevString) @@ -138,7 +141,10 @@ class ObservationControl(LOFARDevice): @log_exceptions() def start_observation(self, parameters: DevString = None): """Deprecated. For backward compatibility with old lofar-station-clients.""" - + logger.warning( + "Deprecated start_observation command used, please use add_observation " + "or start_observation_now instead" + ) self.add_observation(parameters) @command(dtype_in=numpy.int64) @@ -147,6 +153,10 @@ class ObservationControl(LOFARDevice): def stop_observation(self, obs_id: numpy.int64): """Deprecated. For backward compatibility with old lofar-station-clients.""" + logger.warning( + "Deprecated stop_observation command used, please use stop_observation_now " + "instead" + ) self.stop_observation_now(obs_id) @command(dtype_in=DevString) @@ -176,12 +186,16 @@ class ObservationControl(LOFARDevice): self._observation_controller.stop_observation_now(obs_id) + def _stop_all_observations_now(self): + logger.info("Force stopping all observations!") + self._observation_controller.stop_all_observations_now() + @command() @only_when_on() @log_exceptions() def stop_all_observations_now(self): """Force all observations to stop now.""" - self._observation_controller.stop_all_observations_now() + self._stop_all_observations_now() @command(dtype_in=numpy.int64, dtype_out=DevBoolean) @only_when_on() @@ -194,3 +208,23 @@ class ObservationControl(LOFARDevice): @log_exceptions() def is_any_observation_running(self) -> DevBoolean: return len(self._observation_controller.running_observations) > 0 + + @command(dtype_out=DevBoolean) + @only_when_on() + @log_exceptions() + def is_antenna_field_active(self, antenna_field: str) -> DevBoolean: + return antenna_field in self._observation_controller.active_antenna_fields + + TEST_OBS_FIELD_NAME = "STAT/ObservationField/1" + + @command() + def create_test_device(self): + Util.instance().create_device( + ObservationField.__name__, self.TEST_OBS_FIELD_NAME + ) + + @command() + def destroy_test_device(self): + Util.instance().delete_device( + ObservationField.__name__, self.TEST_OBS_FIELD_NAME + ) diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation_field.py similarity index 78% rename from tangostationcontrol/tangostationcontrol/devices/observation.py rename to tangostationcontrol/tangostationcontrol/devices/observation_field.py index 9ccd2a821b2d0c3a8b01554ece37c9394e3aecdc..da4bc6c028d255a8f379c8b758609b44ecd8c93d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_field.py @@ -1,5 +1,7 @@ -# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from datetime import datetime import logging from itertools import chain @@ -24,26 +26,29 @@ from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.common.lofar_logging import log_exceptions from tangostationcontrol.common.proxy import create_device_proxy from tangostationcontrol.common.antennas import device_member_to_full_device_name -from tangostationcontrol.configuration import ObservationSettings +from tangostationcontrol.configuration import ObservationFieldSettings from tangostationcontrol.devices.base_device_classes.lofar_device import LOFARDevice from tangostationcontrol.common.device_decorators import fault_on_error from tangostationcontrol.common.device_decorators import only_in_states + logger = logging.getLogger() -__all__ = ["Observation"] +__all__ = ["ObservationField"] @device_logging_to_python() -class Observation(LOFARDevice): - """Observation Device for LOFAR2.0 +class ObservationField(LOFARDevice): + """ObservationField Device for LOFAR2.0 + This Tango device is responsible for the set-up of hardware for a - specific observation. It will, if necessary keep tabs on HW MPs to signal - issues that are not caught by MPs being outside their nominal range. + specific observation and antenna field pair. It will, if necessary keep tabs on + HW MPs to signal issues that are not caught by MPs being outside their nominal + range. The lifecycle of instances of this device is controlled by ObservationControl. - Settings are written to observation_settings_RW, which can only be done + Settings are written to observation_field_settings_RW, which can only be done in the OFF state. """ @@ -63,7 +68,15 @@ class Observation(LOFARDevice): dtype=numpy.int64, ) def observation_id_R(self): - return self._observation_settings.observation_id + return self._observation_field_settings.observation_id + + @attribute( + doc="Which antenna field this ObservationField configures", + dtype=str, + fisallowed="is_attribute_access_allowed", + ) + def antenna_field_R(self): + return self._observation_field_settings.antenna_field @attribute( doc="Observation start time (seconds since 1970), or 0 if immediate.", @@ -72,8 +85,10 @@ class Observation(LOFARDevice): ) def start_time_R(self): return ( - self._observation_settings.start_time.timestamp() - if self._observation_settings.start_time + datetime.fromisoformat( + self._observation_field_settings.start_time + ).timestamp() + if self._observation_field_settings.start_time else 0.0 ) @@ -83,31 +98,26 @@ class Observation(LOFARDevice): dtype=numpy.float64, ) def stop_time_R(self): - return self._observation_settings.stop_time.timestamp() + return datetime.fromisoformat( + self._observation_field_settings.stop_time + ).timestamp() @attribute( - doc="Seconds to be on sky before the observation start time, to fill buffers and allow for negative geometric delay compensation downstream.", + doc="Seconds to be on sky before the observation start time, to fill buffers " + "and allow for negative geometric delay compensation downstream.", unit="s", dtype=numpy.float64, ) def lead_time_R(self): - return self._observation_settings.lead_time or 0.0 + return self._observation_field_settings.lead_time or 0.0 @attribute( - doc="Which antenna field this Observation configures", - dtype=str, - fisallowed="is_attribute_access_allowed", - ) - def antenna_field_R(self): - return self._observation_settings.antenna_field - - @attribute( - doc="Which antenna set this Observation configures", + doc="Which antenna set this ObservationField configures", dtype=str, fisallowed="is_attribute_access_allowed", ) def antenna_set_R(self): - return self._observation_settings.antenna_set + return self._observation_field_settings.antenna_set @attribute( doc="Whether to add dithering to the signal to increase its linearity", @@ -116,7 +126,7 @@ class Observation(LOFARDevice): ) def dithering_enabled_R(self): try: - return self._observation_settings.dithering.enabled or False + return self._observation_field_settings.dithering.enabled or False except AttributeError: return False @@ -128,7 +138,7 @@ class Observation(LOFARDevice): ) def dithering_power_R(self): try: - return self._observation_settings.dithering.power or -4.0 + return self._observation_field_settings.dithering.power or -4.0 except AttributeError: return -4.0 @@ -140,7 +150,7 @@ class Observation(LOFARDevice): ) def dithering_frequency_R(self): try: - return self._observation_settings.dithering.frequency or 102_000_000 + return self._observation_field_settings.dithering.frequency or 102_000_000 except AttributeError: return 102_000_000 @@ -150,7 +160,7 @@ class Observation(LOFARDevice): fisallowed="is_attribute_access_allowed", ) def filter_R(self): - return self._observation_settings.filter + return self._observation_field_settings.filter @attribute( doc="Which subbands to beamform for each beamlet.", @@ -160,7 +170,9 @@ class Observation(LOFARDevice): ) def saps_subband_R(self): return numpy.array( - list(chain(*[sap.subbands for sap in self._observation_settings.SAPs])), + list( + chain(*[sap.subbands for sap in self._observation_field_settings.SAPs]) + ), dtype=numpy.uint32, ) @@ -172,7 +184,7 @@ class Observation(LOFARDevice): ) def saps_pointing_R(self): saps_pointing = [] - for sap in self._observation_settings.SAPs: + for sap in self._observation_field_settings.SAPs: for _ in sap.subbands: saps_pointing.append( ( @@ -184,12 +196,13 @@ class Observation(LOFARDevice): return saps_pointing @attribute( - doc="Beamlet index of the FPGA output, at which to start mapping the beamlets of this observation.", + doc="Beamlet index of the FPGA output, at which to start mapping the beamlets " + "of this observation.", dtype=numpy.uint64, fisallowed="is_attribute_access_allowed", ) def first_beamlet_R(self): - return self._observation_settings.first_beamlet + return self._observation_field_settings.first_beamlet @attribute( doc="Which pointing to beamform all HBA tiles to (if any).", @@ -198,12 +211,12 @@ class Observation(LOFARDevice): ) def HBA_tile_beam_R(self): try: - if self._observation_settings.HBA.tile_beam is None: + if self._observation_field_settings.HBA.tile_beam is None: return None except AttributeError: return None - pointing_direction = self._observation_settings.HBA.tile_beam + pointing_direction = self._observation_field_settings.HBA.tile_beam return [ str(pointing_direction.direction_type), f"{pointing_direction.angle1}rad", @@ -217,7 +230,7 @@ class Observation(LOFARDevice): ) def HBA_DAB_filter_R(self): try: - return self._observation_settings.HBA.DAB_filter or False + return self._observation_field_settings.HBA.DAB_filter or False except AttributeError: return False @@ -228,45 +241,48 @@ class Observation(LOFARDevice): ) def HBA_element_selection_R(self): try: - return self._observation_settings.HBA.element_selection or "ALL" + return self._observation_field_settings.HBA.element_selection or "ALL" except AttributeError: return "ALL" - observation_settings_RW = attribute(dtype=str, access=AttrWriteType.READ_WRITE) + observation_field_settings_RW = attribute( + dtype=str, access=AttrWriteType.READ_WRITE + ) def __init__(self, cl, name): self.antennafield_proxy: Optional[DeviceProxy] = None self.beamlet_proxy: Optional[DeviceProxy] = None self.digitalbeam_proxy: Optional[DeviceProxy] = None self.tilebeam_proxy: Optional[DeviceProxy] = None - self._observation_settings: Optional[ObservationSettings] = None + self._observation_field_settings: Optional[ObservationFieldSettings] = None # Super must be called after variable assignment due to executing init_device! super().__init__(cl, name) def init_device(self): """Setup some class member variables for observation state""" + logger.debug("[ObservationField] init device") super().init_device() def configure_for_initialise(self): """Load the JSON from the attribute and configure member variables""" - if self._observation_settings is None: + if self._observation_field_settings is None: raise RuntimeError("Device can not be initialized without configuration") super().configure_for_initialise() logger.info( - f"Initialising observation with ID={self._observation_settings.observation_id} " - f"with settings: {self._observation_settings.to_json()}" + f"Initialising observation with ID={self._observation_field_settings.observation_id} " + f"with settings: {self._observation_field_settings.to_json()}" ) self._prepare_observation() logger.info( - f"The observation with ID={self._observation_settings.observation_id} " - f"is initialised to run between {self._observation_settings.start_time} " - f"and {self._observation_settings.stop_time}." + f"The observation with ID={self._observation_field_settings.observation_id} " + f"is initialised to run between {self._observation_field_settings.start_time} " + f"and {self._observation_field_settings.stop_time}." ) def configure_for_on(self): @@ -277,7 +293,7 @@ class Observation(LOFARDevice): self._start_observation() logger.info( - f"Started the observation with ID={self._observation_settings.observation_id}." + f"Started the observation with ID={self._observation_field_settings.observation_id}." ) def configure_for_off(self): @@ -290,8 +306,8 @@ class Observation(LOFARDevice): logger.info( "Stopped the observation with ID=%s.", { - self._observation_settings.observation_id - if self._observation_settings + self._observation_field_settings.observation_id + if self._observation_field_settings else None }, ) @@ -309,7 +325,7 @@ class Observation(LOFARDevice): # certain aspects that only an Observation device can know. util = Util.instance() - antennafield = self._observation_settings.antenna_field + antennafield = self._observation_field_settings.antenna_field self.antennafield_proxy = create_device_proxy( device_member_to_full_device_name(antennafield) ) @@ -332,7 +348,7 @@ class Observation(LOFARDevice): @log_exceptions() def _start_observation(self): - """Configure the station for this observation.""" + """Configure the station for this observation and antenna field.""" # Apply ObservationID self.antennafield_proxy.FPGA_sdp_info_observation_id_RW = ( @@ -403,28 +419,30 @@ class Observation(LOFARDevice): @fault_on_error() @log_exceptions() - def read_observation_settings_RW(self): + def read_observation_field_settings_RW(self): """Return current observation_parameters string""" return ( None - if self._observation_settings is None - else self._observation_settings.to_json() + if self._observation_field_settings is None + else self._observation_field_settings.to_json() ) @only_in_states([DevState.OFF]) @fault_on_error() @log_exceptions() - def write_observation_settings_RW(self, parameters: str): + def write_observation_field_settings_RW(self, parameters: str): """No validation on configuring parameters as task of control device""" try: - self._observation_settings = ObservationSettings.from_json(parameters) - except ValidationError: - self._observation_settings = None - raise + self._observation_field_settings = ObservationFieldSettings.from_json( + parameters + ) + except ValidationError as ex: + self._observation_field_settings = None + raise ex def _is_HBA(self): """Return whether this observation should control a TileBeam device.""" - return self._observation_settings.antenna_field.startswith("HBA") + return self._observation_field_settings.antenna_field.startswith("HBA") def _apply_antennafield_settings(self, filter_name: str): """Retrieve the RCU band from filter name, returning the correct format for diff --git a/tangostationcontrol/tangostationcontrol/observation/__init__.py b/tangostationcontrol/tangostationcontrol/observation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..28c62b201ab99e836031972c5c0a1cf14e22ebde --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/observation/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from tangostationcontrol.observation.observation_controller import ObservationController + +__all__ = ["ObservationController"] diff --git a/tangostationcontrol/tangostationcontrol/observation/observation.py b/tangostationcontrol/tangostationcontrol/observation/observation.py new file mode 100644 index 0000000000000000000000000000000000000000..8e64f6bb80e43625f3efce127b02101452b6a4c7 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/observation/observation.py @@ -0,0 +1,215 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import logging +from typing import List +import time + +from tango import DeviceProxy, Except, EventData +from tango import DevFailed +from tango import DevState +from tangostationcontrol.configuration import ObservationSettings +from tangostationcontrol.configuration import ObservationFieldSettings +from tangostationcontrol.common.lofar_logging import log_exceptions +from tangostationcontrol.observation.observation_field import ObservationField + +logger = logging.getLogger() + + +class Observation(object): + """Manage one or more ObservationField devices depending on observation""" + + def is_partially_running(self) -> bool: + """ + + :raises DevFailed: when calling :py:func:`is_running` failed + """ + return any([field.is_running for field in self._observation_fields]) + + def is_running(self) -> bool: + """ + + :raises DevFailed: when calling :py:fund:`is_running` failed + """ + return all([field.is_running() for field in self._observation_fields]) + + @property + def observation_id(self) -> int: + return self._parameters.antenna_fields[0].observation_id + + @property + def antenna_fields(self) -> list[str]: + return [field.antenna_field for field in self._parameters.antenna_fields] + + def __init__(self, tango_domain, parameters: ObservationSettings): + self._tango_domain: str = tango_domain + self._parameters: ObservationSettings = parameters + + for antenna_field in self._parameters.antenna_fields: + if not antenna_field.observation_id == self.observation_id: + raise RuntimeError( + "Observation configured for different observation IDs across " + "antenna fields" + ) + + self._observation_fields: List[ObservationField] = [] + + @log_exceptions() + def observation_callback(self, event: EventData): + """ + This callback checks and manages the state transitions + for each observation. + + It starts observations at their specified start_time, + and stops & removes them at their specified stop_time. + + :raises DevFailed: if calling :py:func:`~._update_observation_state` fails + """ + if event.err: + # Something is fishy with this event. + logger.warning( + "The ObservationField device %s sent an event but the event \ + signals an error. It is advised to check the logs for any indication \ + that something went wrong in that device. Event data=%s", + event.device, + event, + ) + return + + # update the state of this observation, if needed + self._update_observation_state(event.device) + + def _update_observation_state(self, device: DeviceProxy): + """Start / stop the observation managed by the given ObservationField device. + + :raises DevFailed: if either calling :py:func:`~._stop_antenna_field`, + :py:func:`~._start_antenna_field` or reading attribute fails + """ + + # Get the antenna field name + antenna_field = device.antenna_field_R + + # Get the start/stop times from the sending device + obs_start_time = device.start_time_R + obs_stop_time = device.stop_time_R + + # Get how much earlier we have to start + obs_lead_time = device.lead_time_R + + # Obtain the current time ONCE to avoid race conditions + now = time.time() + + # Manage state transitions + if now > obs_stop_time: + # Stop observation + logger.info("Time: %f now surpassed %f stopping", now, obs_stop_time) + self._stop_antenna_field(antenna_field) + elif ( + now >= obs_start_time - obs_lead_time and device.state() == DevState.STANDBY + ): + logger.info( + "Time: %f now surpassed %f starting", + now, + obs_start_time - obs_lead_time, + ) + # Start observation + self._start_antenna_field(antenna_field) + + def create_devices(self): + """Call create_observation_field_device per ObservationField + + :raises DevFailed: Raised when calling :py:func:`~._create_device` fails + """ + for observation_field in self._parameters.antenna_fields: + self._create_device(observation_field) + + def destroy_devices(self): + """Destroy each of the registered ObservationField device""" + + for observation_field in self._observation_fields: + observation_field.destroy_observation_field_device() + + def _create_device(self, parameters: ObservationFieldSettings): + """Create singular ObservationField device with ObservationFieldSettings + + :raises DevFailed: Raised when :py:func:`create_observation_field_device` fails + """ + f = ObservationField(self._tango_domain, parameters) + f.create_observation_field_device() + self._observation_fields.append(f) + + def initialise_observation(self): + """Call initialise_observation per ObservationField + + :raises DevFailed: Raised when :py:func:`initialise_observation_field` fails + """ + for observation_field in self._observation_fields: + try: + observation_field.initialise_observation_field() + except DevFailed as ex: + error_string = ( + f"Failed to initialise observation: " + f"{observation_field.observation_id} due to error for antenna " + f"field: {observation_field.antenna_field}" + ) + Except.re_throw_exception(ex, "DevFailed", error_string, __name__) + + def update(self): + """Call callback per ObservationField device proxy + + :raises DevFailed: If calling :py:func:`~._update_observation_state` fails + """ + + for observation_field in self._observation_fields: + self._update_observation_state(observation_field.proxy) + + def create_subscriptions(self): + """Register event subscription for callback per ObservationField device proxy + + :raises DevFailed: Raised when :py:func:`subscribe` fails + """ + + for observation_field in self._observation_fields: + try: + observation_field.subscribe(self.observation_callback) + except DevFailed as ex: + error_string = ( + "Cannot subscribe to attribute. Cancelled observation: " + f"{observation_field.observation_id} for antenna field: " + f"{observation_field.antenna_field}" + ) + Except.re_throw_exception(ex, "DevFailed", error_string, __name__) + + def _start_antenna_field(self, antenna_field: str): + """ + + raises DevFailed: when executing start for the specified observation field fails + """ + for observation_field in self._observation_fields: + if observation_field.antenna_field == antenna_field: + observation_field.start() + + def _stop_antenna_field(self, antenna_field: str): + """ + + raises DevFailed: when executing stop for the specified observation field fails + """ + for observation_field in self._observation_fields: + if observation_field.antenna_field == antenna_field: + observation_field.stop() + + def start(self): + """Call start on each ObservationField + + raises DevFailed: when executing start for a specific observation field fails + """ + for observation_field in self._observation_fields: + observation_field.start() + + def stop(self): + """Call stop on each ObservationField + + raises DevFailed: when executing stop for a specific observation field fails + """ + for observation_field in self._observation_fields: + observation_field.stop() diff --git a/tangostationcontrol/tangostationcontrol/observation/observation_controller.py b/tangostationcontrol/tangostationcontrol/observation/observation_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..25bbb7fd7e8d714eb63bd52c856b48e34e3817a8 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/observation/observation_controller.py @@ -0,0 +1,133 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from datetime import datetime +import logging + +from tango import DevFailed, Util, Database +from tangostationcontrol.configuration import ObservationSettings +from tangostationcontrol.observation.observation import Observation +from tangostationcontrol.devices.observation_field import ObservationField + +logger = logging.getLogger() + + +class ObservationController(dict[int, Observation]): + """A dictionary of observations. Actively manages the observation state transitions + (start, stop).""" + + def __init__(self, tango_domain: str): + super().__init__() + self._tango_util = Util.instance() + self._tango_domain = tango_domain + + @property + def running_observations(self) -> list[int]: + return [ + obs_id for obs_id, observation in self.items() if observation.is_running() + ] + + @property + def active_antenna_fields(self) -> list[str]: + active_fields = [] + running_field_sets = [ + observation.antenna_fields + for obs_id, observation in self.items() + if observation.is_running() + ] + for running_field_set in running_field_sets: + active_fields.extend(running_field_set) + return active_fields + + def add_observation(self, settings: ObservationSettings): + """Create an Observation which will start 1 or more ObservationField devices + + :raises RuntimeError: if either settings contains multiple observation ids or if + the creation and configuration of devices failed + """ + + # Check further properties that cannot be validated through a JSON schema + # TODO(Corne): Discuss do we want this? + for observation_field_settings in settings.antenna_fields: + if ( + datetime.fromisoformat(observation_field_settings.stop_time) + <= datetime.now() + ): + raise ValueError( + "Cannot start observation " + f"{observation_field_settings.observation_id} because antenna " + f"field {observation_field_settings.antenna_field} is already " + f"past its stop time {observation_field_settings.stop_time}" + ) + + obs = Observation(self._tango_domain, settings) + + try: + obs.create_devices() + obs.initialise_observation() + # Register this observation now that it has been successfully created + self[obs.observation_id] = obs + # Keep pinging to manage its state transitions + obs.create_subscriptions() + # Make sure the current state is accurate + obs.update() + except DevFailed as ex: + # Remove the devices again if creation, proxies or subscriptions fail. + obs.destroy_devices() + + raise RuntimeError( + f"Observation: {settings.antenna_fields[0].observation_id} failed, " + "destroying devices..." + ) from ex + + def start_observation(self, obs_id: int): + """Start the observation with the given ID + + :raises DevFailed: if executing :py:func:`start` fails + :raises KeyError: if observation id is unknown + """ + try: + observation = self[obs_id] + except KeyError as _exc: + raise KeyError(f"Unknown observation: {obs_id}") + + observation.start() + + def stop_observation_now(self, obs_id: int): + """Stop the observation with the given ID + + :raises DevFailed: if executing :py:func:`stop` fails + :raises KeyError: if observation id is unknown + """ + try: + observation = self.pop(obs_id) + except KeyError as _exc: + raise KeyError(f"Unknown observation: {obs_id}") + + observation.stop() + + def stop_all_observations_now(self): + """Stop all observations (running or to be run)""" + for obs_id in list(self): # draw a copy as we modify the list + try: + self.stop_observation_now(obs_id) + except DevFailed as ex: + logger.exception(ex) + + self._destroy_all_observation_field_devices() + + @staticmethod + def _destroy_all_observation_field_devices(): + """Prevent any lingering observation field devices, remove all from database""" + db = Database() + devices = db.get_device_exported_for_class(ObservationField.__name__) + for device in devices: + # if CaseInsensitiveString(device) == CaseInsensitiveString( + # "Stat/ObservationField/1" + # ): + # continue + try: + db.delete_device(device) + logger.warning("Destroyed lingering device: %s", device) + except Exception as ex: + logger.exception(ex) diff --git a/tangostationcontrol/tangostationcontrol/observation/observation_field.py b/tangostationcontrol/tangostationcontrol/observation/observation_field.py new file mode 100644 index 0000000000000000000000000000000000000000..a294b347c188121a9d6fc8b96c2045ce24c95731 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/observation/observation_field.py @@ -0,0 +1,159 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from tango import DevFailed, DevState, Except, Util, EventType, DeviceProxy +from tangostationcontrol.common.proxy import create_device_proxy +from tangostationcontrol.configuration.observation_field_settings import ( + ObservationFieldSettings, +) + +logger = logging.getLogger() + + +class ObservationField(object): + """Manage specific antenna field for a particular observation id""" + + @property + def proxy(self) -> DeviceProxy: + return self._device_proxy + + @property + def observation_id(self) -> int: + return self._parameters.observation_id + + @property + def antenna_field(self) -> str: + return self._parameters.antenna_field + + @property + def class_name(self) -> str: + from tangostationcontrol.devices.observation_field import ObservationField + + return ObservationField.__name__ + + @property + def device_name(self) -> str: + """Device names property creating unique device name""" + return ( + f"{self._tango_domain}/{self.class_name}/{self.observation_id}-" + f"{self.antenna_field}" + ) + + @property + def attribute_name(self) -> str: + """Name for the ObservationField.observation_running subscription""" + return f"{self.device_name}/alive_R" + + def __init__(self, tango_domain, parameters: ObservationFieldSettings): + self._device_proxy: DeviceProxy | None = None + self._event_id: int | None = None + self._parameters: ObservationFieldSettings = parameters + self._tango_domain: str = tango_domain + + # The pyTango.Util class is a singleton and every DS can only + # have one instance of it. + self._tango_util: Util = Util.instance() + + def create_observation_field_device(self): + """Instantiate an Observation Device + + :raises DevFailed: when calling :py:func:`create_device` fails + """ + logger.info("Create device: %s", self.device_name) + try: + # Create the Observation device and instantiate it. + self._tango_util.create_device(self.class_name, f"{self.device_name}") + except DevFailed as ex: + logger.exception(ex) + error_string = ( + f"Cannot create the ObservationField device instance " + f"{self.device_name} for ID={self.observation_id} and " + f"field={self.antenna_field} " + ) + Except.re_throw_exception(ex, "DevFailed", error_string, __name__) + + def destroy_observation_field_device(self): + try: + self._tango_util.delete_device(self.class_name, self.device_name) + except DevFailed: + logger.exception( + f"Could not delete device {self.device_name} of class " + f"{self.class_name} from Tango DB." + ) + + def initialise_observation_field(self): + """ + + raises DevFailed: when an operation on the device proxy fails + """ + # Instantiate a dynamic Tango Device "Observation". + self._device_proxy = create_device_proxy(self.device_name) + + # Initialise generic properties + self.proxy.put_property({"Control_Children": [], "Power_Children": []}) + + # Configure the dynamic device its attribute for the observation + # parameters. + self.proxy.observation_field_settings_RW = self._parameters.to_json() + + # Take the Observation device through the motions. Pass the + # entire JSON set of parameters so that it can pull from it what it + # needs. + self.proxy.Initialise() + + def start(self): + """ + + raises DevFailed: when executing the :py:func:`On` command fails + """ + self.proxy.On() + + def is_running(self) -> bool: + """ + + :raises DevFailed: when proxy state could not be retrieved + """ + return self.proxy and self.proxy.state() == DevState.ON + + def subscribe(self, cb): + """ + + raises DevFailed: when an operation on the device proxy fails + """ + # Turn on the polling for the attribute. + # Note that this is not automatically done despite the attribute + # having the right polling values set in the ctor. + self.proxy.poll_attribute(self.attribute_name.split("/")[-1], 1000) + + # Right. Now subscribe to periodic events. + self._event_id = self._device_proxy.subscribe_event( + self.attribute_name.split("/")[-1], EventType.PERIODIC_EVENT, cb + ) + logger.info( + "Successfully started an observation field with ID=%s for antenna field=%s", + self.observation_id, + self.antenna_field, + ) + + def stop(self): + # Check if the device has not terminated itself in the meanwhile. + try: + self.proxy.ping() + except DevFailed: + logger.error( + f"Observation field device for ID={self.observation_id} and antenna" + f"field={self.antenna_field} unexpectedly disappeared." + ) + else: + # Unsubscribe from the subscribed event. + self.proxy.unsubscribe_event(self._event_id) + + # Tell the ObservationField device to stop the running + # observation for the given antenna field. This is a synchronous call and + # the clean-up does not take long. + self.proxy.Off() + + # Finally remove the device object from the Tango DB. + self.destroy_observation_field_device() diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py deleted file mode 100644 index 37e863b4e4f1e11e027082872c0d9b97d56a6d8c..0000000000000000000000000000000000000000 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 - - -class TestObservationBase: - VALID_JSON = """ - { - "observation_id": 12345, - "stop_time": "2106-02-07T00:00:00", - "antenna_field": "HBA0", - "antenna_set": "ALL", - "filter": "HBA_110_190", - "SAPs": [{ - "subbands": [10, 20, 30], - "pointing": { - "angle1": 0.0261799, "angle2": 0, "direction_type": "J2000" - } - }], - "dithering": { - "enabled": true, - "power": -10.0, - "frequency": 123000000 - }, - "HBA": { - "tile_beam": - { "angle1": 0.0261799, "angle2": 0, "direction_type": "J2000" }, - "DAB_filter": true, - "element_selection": "GENERIC_201512" - }, - "first_beamlet": 0 - } - """ diff --git a/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py b/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..ab8a8c626cda8c8f734d39727be2c95a4b9b94c3 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py @@ -0,0 +1,75 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +import copy + +from tangostationcontrol.configuration import ( + Dithering, + ObservationFieldSettings, + ObservationSettings, + Pointing, + Sap, + HBA, +) + +SETTINGS_TWO_FIELDS = ObservationSettings( + "cs001", + [ + ObservationFieldSettings( + 5, + "2022-10-26T11:35:54.704150", + "2022-10-27T11:35:54.704150", + "HBA0", + "ALL", + "filter settings", + [ + Sap([3, 2], Pointing(1.2, 2.1, "LMN")), + Sap([1], Pointing(3.3, 4.4, "MOON")), + ], + ), + ObservationFieldSettings( + 5, + "2022-10-26T11:35:54.704150", + "2022-10-27T11:35:54.704150", + "LBA", + "ALL", + "filter settings", + [ + Sap([3, 2], Pointing(1.2, 2.1, "LMN")), + Sap([1], Pointing(3.3, 4.4, "MOON")), + ], + ), + ], +) + +SETTINGS_HBA_IMMEDIATE = ObservationSettings( + "cs001", + [ + ObservationFieldSettings( + observation_id=12345, + start_time=None, + stop_time="2106-02-07T00:00:00", + antenna_field="HBA0", + antenna_set="ALL", + filter="HBA_110_190", + SAPs=[ + Sap([10, 20, 30], Pointing(0.0261799, 0, "J2000")), + ], + HBA=HBA( + tile_beam=Pointing(0.0261799, 0, "J2000"), + DAB_filter=True, + element_selection="GENERIC_201512", + ), + dithering=Dithering(enabled=True, power=-10, frequency=123000000), + ), + ], +) + + +def get_observation_settings_two_fields() -> ObservationSettings: + """Get an observation with two antenna fields""" + return copy.deepcopy(SETTINGS_TWO_FIELDS) + + +def get_observation_settings_hba_immediate() -> ObservationSettings: + """Get an observation with one antenna field and no start time""" + return copy.deepcopy(SETTINGS_HBA_IMMEDIATE) diff --git a/tangostationcontrol/test/common/test_case_insensitive_string.py b/tangostationcontrol/test/common/test_case_insensitive_string.py new file mode 100644 index 0000000000000000000000000000000000000000..ee9ce71941ec7b8e9d0ebf7ccb5b0f2616e65828 --- /dev/null +++ b/tangostationcontrol/test/common/test_case_insensitive_string.py @@ -0,0 +1,22 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +from tangostationcontrol.common.case_insensitive_dict import CaseInsensitiveString + +from test import base + + +class TestCaseInsensitiveString(base.TestCase): + def test_a_in_b(self): + """Get and set an item with different casing""" + + self.assertTrue(CaseInsensitiveString("hba0") in CaseInsensitiveString("HBA0")) + + def test_b_in_a(self): + """Get and set an item with different casing""" + + self.assertTrue(CaseInsensitiveString("HBA0") in CaseInsensitiveString("hba0")) + + def test_a_not_in_b(self): + """Get and set an item with different casing""" + + self.assertFalse(CaseInsensitiveString("hba0") in CaseInsensitiveString("LBA0")) diff --git a/tangostationcontrol/test/common/test_observation_controller.py b/tangostationcontrol/test/common/test_observation_controller.py deleted file mode 100644 index a9d31c01c77e80dd712f843bdd436918e12fa090..0000000000000000000000000000000000000000 --- a/tangostationcontrol/test/common/test_observation_controller.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 - -import importlib -import sys -from datetime import datetime -from unittest import mock -from unittest.mock import Mock - -from tango import DevState - -from tangostationcontrol.common import ObservationController -from tangostationcontrol.common.observation_controller import Observation -from tangostationcontrol.common.proxy import create_device_proxy -from tangostationcontrol.configuration import ObservationSettings, Pointing, Sap - -from test import base - - -class MockObservation(object): - def __init__(self, running): - self._running = running - - def is_running(self): - return self._running - - -@mock.patch("tango.Util.instance") -class TestObservationController(base.TestCase): - """Test Observation Controller main operations""" - - def test_observations_running(self, _): - sut = ObservationController("DMR") - self.assertListEqual([], sut.running_observations) - sut[1] = MockObservation(True) - self.assertListEqual([1], sut.running_observations) - - def test_observations_not_running(self, _): - sut = ObservationController("DMR") - self.assertListEqual([], sut.running_observations) - sut[2] = MockObservation(False) - self.assertListEqual([], sut.running_observations) - - def test_stop_all_observations_now_no_running(self, _): - sut = ObservationController("DMR") - sut.stop_all_observations_now() - - -@mock.patch("tango.Util.instance") -class TestObservation(base.TestCase): - SETTINGS = ObservationSettings( - 5, - datetime.fromisoformat("2022-10-26T11:35:54.704150"), - datetime.fromisoformat("2022-10-27T11:35:54.704150"), - "HBA", - "ALL", - "filter settings", - [Sap([3, 2], Pointing(1.2, 2.1, "LMN")), Sap([1], Pointing(3.3, 4.4, "MOON"))], - ) - - def test_properties(self, _): - sut = Observation("DMR", TestObservation.SETTINGS) - self.assertEqual(5, sut.observation_id) - self.assertEqual("Observation", sut.class_name) - self.assertEqual("DMR/Observation/5", sut.device_name) - self.assertEqual("DMR/Observation/5/alive_R", sut.attribute_name) - - def test_create_observation_device(self, tu_mock): - sut = Observation("DMR", TestObservation.SETTINGS) - sut.create_observation_device() - - @mock.patch("tango.DeviceProxy") - def test_initialise_observation(self, dp_mock, tu_mock): - importlib.reload(sys.modules[create_device_proxy.__module__]) - sut = Observation("DMR", TestObservation.SETTINGS) - sut.initialise_observation() - - self.assertEqual( - dp_mock.return_value.observation_settings_RW, - TestObservation.SETTINGS.to_json(), - ) - dp_mock.return_value.Initialise.assert_called() - - @mock.patch("tango.DeviceProxy") - def test_start(self, dp_mock, tu_mock): - importlib.reload(sys.modules[create_device_proxy.__module__]) - sut = Observation("DMR", TestObservation.SETTINGS) - sut.initialise_observation() - sut.start() - - dp_mock.return_value.On.assert_called() - - def test_subscribe(self, _): - def dummy(): - pass - - sut = Observation("DMR", TestObservation.SETTINGS) - dp_mock = Mock() - sut._device_proxy = dp_mock - sut.subscribe(dummy) - - dp_mock.poll_attribute.assert_called() - dp_mock.subscribe_event.assert_called() - - def test_stop(self, tu_mock): - importlib.reload(sys.modules[Observation.__module__]) - sut = Observation("DMR", TestObservation.SETTINGS) - - dp_mock = Mock() - dp_mock.state.return_value = DevState.OFF - - sut._device_proxy = dp_mock - - sut.stop() - - dp_mock.ping.assert_called() - dp_mock.unsubscribe_event.assert_called() - dp_mock.Off.assert_called() - - tu_mock.return_value.delete_device.assert_called() diff --git a/tangostationcontrol/test/configuration/test_observation_field_settings.py b/tangostationcontrol/test/configuration/test_observation_field_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..90b8aae33e9acbd09e51916fc13545b4076597d5 --- /dev/null +++ b/tangostationcontrol/test/configuration/test_observation_field_settings.py @@ -0,0 +1,144 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import json + +from jsonschema.exceptions import ValidationError + +from tangostationcontrol.configuration import Pointing, ObservationFieldSettings, Sap + +from test import base + + +class TestObservationFieldSettings(base.TestCase): + def test_from_json(self): + sut = ObservationFieldSettings.from_json( + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' + '"antenna_field": "HBA", ' + '"antenna_set": "ALL", "filter": "HBA_110_190",' + '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}]}' + ) + + self.assertEqual(sut.observation_id, 3) + self.assertEqual(sut.stop_time, "2012-04-23T18:25:43") + self.assertEqual(sut.antenna_set, "ALL") + self.assertEqual(sut.filter, "HBA_110_190") + self.assertEqual(len(sut.SAPs), 1) + + sut = ObservationFieldSettings.from_json( + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' + '"antenna_field": "HBA", ' + '"antenna_set": "ALL", "filter": "HBA_110_190",' + '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' + '"HBA": { "tile_beam": {"angle1":2.2, "angle2": 3.1, "direction_type":"MOON"} } }' + ) + + self.assertEqual(sut.HBA.tile_beam.angle1, 2.2) + self.assertEqual(sut.HBA.tile_beam.angle2, 3.1) + self.assertEqual(sut.HBA.tile_beam.direction_type, "MOON") + + sut = ObservationFieldSettings.from_json( + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' + '"antenna_field": "HBA", ' + '"antenna_set": "ALL", "filter": "HBA_110_190",' + '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' + '"HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' + ) + + self.assertEqual(sut.first_beamlet, 2) + + def test_from_json_type_missmatch(self): + for json_str in [ + # observation_id + '{"observation_id": "3", "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', + # stop_time + '{"observation_id": 3, "stop_time": "test", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', + # antenna_set + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": 4, "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', + # filter + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": 1,"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', + # SAPs + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": {"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}', + # first_beamlet + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": "2"}', + ]: + with self.assertRaises((ValidationError, ValueError), msg=f"{json_str}"): + ObservationFieldSettings.from_json(json_str) + + def test_from_json_missing_fields(self): + complete_json = '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' + + for field in ( + "observation_id", + "stop_time", + "antenna_field", + "antenna_set", + "filter", + "SAPs", + ): + # construct a JSON string with the provided field missing + json_dict = json.loads(complete_json) + del json_dict[field] + json_str = json.dumps(json_dict) + + # trigger validation error + with self.assertRaises( + (ValidationError, ValueError), msg=f"Omitted field {field}: {json_str}" + ): + ObservationFieldSettings.from_json(json_str) + + def test_to_json(self): + sut = ObservationFieldSettings( + 5, + "2022-10-26T11:35:54.704150", + "2022-10-27T11:35:54.704150", + "HBA", + "ALL", + "filter settings", + [ + Sap([3, 2], Pointing(1.2, 2.1, "LMN")), + Sap([1], Pointing(3.3, 4.4, "MOON")), + ], + ) + self.assertEqual( + sut.to_json(), + '{"observation_id": 5, "start_time": "2022-10-26T11:35:54.704150", ' + '"stop_time": "2022-10-27T11:35:54.704150", ' + '"antenna_field": "HBA", ' + '"antenna_set": "ALL", "filter": "filter settings", "SAPs": ' + '[{"subbands": [3, 2], "pointing": {"angle1": 1.2, "angle2": 2.1, ' + '"direction_type": "LMN"}}, {"subbands": [1], "pointing": {"angle1": 3.3, ' + '"angle2": 4.4, "direction_type": "MOON"}}], "first_beamlet": 0}', + ) + + def test_to_json_datetime_named_args(self): + sut = ObservationFieldSettings( + observation_id=5, + start_time=None, + stop_time="2106-02-07T00:00:00", + antenna_field="HBA", + antenna_set="ALL", + filter="filter settings", + SAPs=[ + Sap([3, 2], Pointing(1.2, 2.1, "LMN")), + Sap([1], Pointing(3.3, 4.4, "MOON")), + ], + ) + + self.assertEqual( + sut.to_json(), + '{"observation_id": 5, "stop_time": "2106-02-07T00:00:00", ' + '"antenna_field": "HBA", "antenna_set": "ALL", "filter": ' + '"filter settings", "SAPs": [{"subbands": [3, 2], "pointing": ' + '{"angle1": 1.2, "angle2": 2.1, "direction_type": "LMN"}}, ' + '{"subbands": [1], "pointing": {"angle1": 3.3, "angle2": 4.4, ' + '"direction_type": "MOON"}}], "first_beamlet": 0}', + ) + + def test_throw_wrong_instance(self): + for json_str in [ + '{"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}', + '{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}', + ]: + with self.assertRaises(ValidationError): + ObservationFieldSettings.from_json(json_str) diff --git a/tangostationcontrol/test/configuration/test_observation_settings.py b/tangostationcontrol/test/configuration/test_observation_settings.py index 6220062ee59e905d6ba0db31a93e7fb526234de0..e727d149fbffe3232e7a124a28ee55a149e34221 100644 --- a/tangostationcontrol/test/configuration/test_observation_settings.py +++ b/tangostationcontrol/test/configuration/test_observation_settings.py @@ -1,114 +1,72 @@ -# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 - -import json -from datetime import datetime +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 from jsonschema.exceptions import ValidationError -from tangostationcontrol.configuration import Pointing, ObservationSettings, Sap +from tangostationcontrol.configuration import ObservationSettings +from tangostationcontrol.configuration import Sap +from tangostationcontrol.configuration import Pointing +from tangostationcontrol.configuration import ObservationFieldSettings + from test import base class TestObservationSettings(base.TestCase): def test_from_json(self): sut = ObservationSettings.from_json( - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "HBA_110_190",' - '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}]}' - ) - - self.assertEqual(sut.observation_id, 3) - self.assertEqual(sut.stop_time, datetime.fromisoformat("2012-04-23T18:25:43")) - self.assertEqual(sut.antenna_set, "ALL") - self.assertEqual(sut.filter, "HBA_110_190") - self.assertEqual(len(sut.SAPs), 1) - - sut = ObservationSettings.from_json( - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "HBA_110_190",' - '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' - '"HBA": { "tile_beam": {"angle1":2.2, "angle2": 3.1, "direction_type":"MOON"} } }' - ) - - self.assertEqual(sut.HBA.tile_beam.angle1, 2.2) - self.assertEqual(sut.HBA.tile_beam.angle2, 3.1) - self.assertEqual(sut.HBA.tile_beam.direction_type, "MOON") - - sut = ObservationSettings.from_json( - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "HBA_110_190",' - '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' - '"HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' + """ + { + "station": "cs0001", + "antenna_fields": [{ + "observation_id": 3, + "stop_time": "2012-04-23T18:25:43", + "antenna_field": "HBA", + "antenna_set": "ALL", + "filter": "HBA_110_190", + "SAPs": [{ + "subbands": [3, 2, 1], + "pointing": { + "angle1":1.2, "angle2": 2.1, "direction_type":"LMN" + } + }] + }] + }""" ) - self.assertEqual(sut.first_beamlet, 2) - - def test_from_json_type_missmatch(self): - for json_str in [ - # observation_id - '{"observation_id": "3", "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', - # stop_time - '{"observation_id": 3, "stop_time": "test", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', - # antenna_set - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": 4, "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', - # filter - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": 1,"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', - # SAPs - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": {"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}', - # first_beamlet - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": "2"}', - ]: - with self.assertRaises((ValidationError, ValueError), msg=f"{json_str}"): - ObservationSettings.from_json(json_str) - - def test_from_json_missing_fields(self): - complete_json = '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' - - for field in ( - "observation_id", - "stop_time", - "antenna_field", - "antenna_set", - "filter", - "SAPs", - ): - # construct a JSON string with the provided field missing - json_dict = json.loads(complete_json) - del json_dict[field] - json_str = json.dumps(json_dict) - - # trigger validation error - with self.assertRaises( - (ValidationError, ValueError), msg=f"Omitted field {field}: {json_str}" - ): - ObservationSettings.from_json(json_str) + self.assertEqual(sut.station, "cs0001") + self.assertEqual(sut.antenna_fields[0].observation_id, 3) + self.assertEqual(sut.antenna_fields[0].stop_time, "2012-04-23T18:25:43") + self.assertEqual(sut.antenna_fields[0].antenna_set, "ALL") + self.assertEqual(sut.antenna_fields[0].filter, "HBA_110_190") + self.assertEqual(len(sut.antenna_fields[0].SAPs), 1) def test_to_json(self): sut = ObservationSettings( - 5, - datetime.fromisoformat("2022-10-26T11:35:54.704150"), - datetime.fromisoformat("2022-10-27T11:35:54.704150"), - "HBA", - "ALL", - "filter settings", - [ - Sap([3, 2], Pointing(1.2, 2.1, "LMN")), - Sap([1], Pointing(3.3, 4.4, "MOON")), + station="cs001", + antenna_fields=[ + ObservationFieldSettings( + 5, + "2022-10-26T11:35:54.704150", + "2022-10-27T11:35:54.704150", + "HBA", + "ALL", + "filter settings", + [ + Sap([3, 2], Pointing(1.2, 2.1, "LMN")), + Sap([1], Pointing(3.3, 4.4, "MOON")), + ], + ) ], ) self.assertEqual( sut.to_json(), - '{"observation_id": 5, "start_time": "2022-10-26T11:35:54.704150", ' - '"stop_time": "2022-10-27T11:35:54.704150", ' - '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "filter settings", "SAPs": ' + '{"station": "cs001", "antenna_fields": [{"observation_id": 5, "start_time": ' + '"2022-10-26T11:35:54.704150", "stop_time": "2022-10-27T11:35:54.704150", ' + '"antenna_field": "HBA", "antenna_set": "ALL", ' + '"filter": "filter settings", "SAPs": ' '[{"subbands": [3, 2], "pointing": {"angle1": 1.2, "angle2": 2.1, ' '"direction_type": "LMN"}}, {"subbands": [1], "pointing": {"angle1": 3.3, ' - '"angle2": 4.4, "direction_type": "MOON"}}], "first_beamlet": 0}', + '"angle2": 4.4, "direction_type": "MOON"}}], "first_beamlet": 0}]}', ) def test_throw_wrong_instance(self): diff --git a/tangostationcontrol/test/devices/test_observation_control_device.py b/tangostationcontrol/test/devices/test_observation_control_device.py index 7b5c70be28c9bf76920a2e32dabbb081d2b03e40..535ebe62dd706b92d60216c82dbebfb6727f16d7 100644 --- a/tangostationcontrol/test/devices/test_observation_control_device.py +++ b/tangostationcontrol/test/devices/test_observation_control_device.py @@ -1,15 +1,11 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test.devices import test_observation_base - from test import base -class TestObservationControlDevice( - base.TestCase, test_observation_base.TestObservationBase -): +class TestObservationControlDevice(base.TestCase): def setUp(self): super(TestObservationControlDevice, self).setUp() - # Moved to Integration Test + # TODO(Corne): Either make this do something or remove this file, this makes 0 sense diff --git a/tangostationcontrol/test/devices/test_observation_device.py b/tangostationcontrol/test/devices/test_observation_device.py index 78392c400fb406fc0d1cdd569eb7c09b97698766..c70e7f8fc78f3da4fce0c011ab93eb4f433e05fb 100644 --- a/tangostationcontrol/test/devices/test_observation_device.py +++ b/tangostationcontrol/test/devices/test_observation_device.py @@ -1,14 +1,12 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test.devices import test_observation_base - from test.devices import device_base -class TestObservationDevice( - device_base.DeviceTestCase, test_observation_base.TestObservationBase -): +class TestObservationDevice(device_base.DeviceTestCase): def setUp(self): # DeviceTestCase setUp patches lofar_device DeviceProxy super(TestObservationDevice, self).setUp() + + # TODO(Corne): Either make this do something or remove this file, this makes 0 sense diff --git a/tangostationcontrol/tangostationcontrol/test/devices/__init__.py b/tangostationcontrol/test/observation/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/devices/__init__.py rename to tangostationcontrol/test/observation/__init__.py diff --git a/tangostationcontrol/test/observation/test_observation.py b/tangostationcontrol/test/observation/test_observation.py new file mode 100644 index 0000000000000000000000000000000000000000..fae464f015e8cd2fc20e907b26e69e09817870b1 --- /dev/null +++ b/tangostationcontrol/test/observation/test_observation.py @@ -0,0 +1,150 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import copy +import datetime +from unittest import mock + +from tango import DevFailed, DevState +from tangostationcontrol.observation.observation import Observation +from tangostationcontrol.observation import observation_field +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_two_fields, +) + +from test import base + + +@mock.patch("tango.Util.instance") +class TestObservation(base.TestCase): + def test_properties(self, _): + sut = Observation("DMR", get_observation_settings_two_fields()) + self.assertEqual(5, sut.observation_id) + self.assertEqual(["HBA0", "LBA"], sut.antenna_fields) + + @staticmethod + def mocked_observation_field_base(m_obs_field) -> Observation: + """Base function for tests using mocked ObservationField instances""" + + obs_settings = get_observation_settings_two_fields() + sut = Observation("DMR", obs_settings) + + for antenna_field in obs_settings.antenna_fields: + sut._observation_fields.append( + copy.deepcopy(m_obs_field(tango_domain="DMR", parameters=antenna_field)) + ) + + return sut + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_is_running_none(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + for obs_field in sut._observation_fields: + obs_field.is_running.return_value = False + + self.assertFalse(sut.is_running()) + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_is_running(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + for obs_field in sut._observation_fields: + obs_field.is_running.return_value = True + + self.assertTrue(sut.is_running()) + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_is_partially_running(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + for obs_field in sut._observation_fields: + obs_field.is_running.return_value = False + + sut._observation_fields[0].is_running = True + + self.assertTrue(sut.is_partially_running()) + + def test_create_devices(self, tu_mock): + sut = Observation("DMR", get_observation_settings_two_fields()) + sut.create_devices() + + self.assertEqual( + len(get_observation_settings_two_fields().antenna_fields), + len(sut._observation_fields), + ) + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_destroy_devices(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + sut.destroy_devices() + + for obs_field in sut._observation_fields: + obs_field.destroy_observation_field_device.assert_called_once() + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_initialise_observation(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + sut.initialise_observation() + + for obs_field in sut._observation_fields: + obs_field.initialise_observation_field.assert_called_once() + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_start(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + sut.start() + + for obs_field in sut._observation_fields: + obs_field.start.assert_called_once() + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_create_subscriptions(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + sut.create_subscriptions() + + for obs_field in sut._observation_fields: + obs_field.subscribe.assert_called_once_with(sut.observation_callback) + + def test_update_observation_state(self, tu_mock): + sut = Observation("DMR", get_observation_settings_two_fields()) + + sut._stop_antenna_field = mock.Mock() + sut._start_antenna_field = mock.Mock() + + m_device_proxy = mock.Mock( + antenna_field_R="HBA", + start_time_R=datetime.datetime.now().timestamp(), + stop_time_R=( + datetime.datetime.now() + datetime.timedelta(hours=1) + ).timestamp(), + lead_time_R=5, + state=mock.Mock(return_value=DevState.STANDBY), + ) + + sut._update_observation_state(m_device_proxy) + sut._start_antenna_field.assert_called_once_with(m_device_proxy.antenna_field_R) + + m_device_proxy.stop_time_R = datetime.datetime.now().timestamp() + sut._update_observation_state(m_device_proxy) + sut._stop_antenna_field.assert_called_once_with(m_device_proxy.antenna_field_R) + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_create_subscriptions_failed(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + sut._observation_fields[0].subscribe.side_effect = [DevFailed] + + self.assertRaises(DevFailed, sut.create_subscriptions) + + @mock.patch.object(observation_field, "ObservationField", autospec=True) + def test_stop(self, m_obs_field, tu_mock): + sut = self.mocked_observation_field_base(m_obs_field) + + sut.stop() + + for obs_field in sut._observation_fields: + obs_field.stop.assert_called_once() diff --git a/tangostationcontrol/test/observation/test_observation_controller.py b/tangostationcontrol/test/observation/test_observation_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..0f2ef4dcd8e802691b1ef954f276733b3949daf5 --- /dev/null +++ b/tangostationcontrol/test/observation/test_observation_controller.py @@ -0,0 +1,205 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import copy +from datetime import datetime, timedelta +from typing import Dict +from typing import List +from unittest import mock + +from tango import DevFailed +from tangostationcontrol.observation import observation_controller as obs_module +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_two_fields, +) + +from test import base + + +@mock.patch("tango.Util.instance") +class TestObservationController(base.TestCase): + """Test Observation Controller main operations""" + + def generate_observation( + self, running: bool, antenna_fields: list[str] = None + ) -> obs_module.Observation: + if not antenna_fields: + antenna_fields = ["HBA"] + + m_obs_field = self.m_observation(tango_domain="DMR", parameters={}) + m_obs_field.is_running.return_value = running + m_obs_field.antenna_fields = antenna_fields + return copy.deepcopy(m_obs_field) + + def setUp(self): + super().setUp() + proxy_patcher = mock.patch.object(obs_module, "Observation", autospec=True) + self.m_observation = proxy_patcher.start() + self.addCleanup(proxy_patcher.stop) + + def observation_setup( + self, + observations: Dict[int, obs_module.Observation], + ) -> obs_module.ObservationController: + sut = obs_module.ObservationController("DMR") + self.assertListEqual([], sut.running_observations) + for obs_id, observation in observations.items(): + sut[obs_id] = observation + return sut + + def observations_running( + self, + observations: Dict[int, obs_module.Observation], + expected_result: List[int], + ): + sut = self.observation_setup(observations) + self.assertListEqual(expected_result, sut.running_observations) + + def test_observations_running(self, _m_tango_util): + self.observations_running( + observations={1: self.generate_observation(True)}, expected_result=[1] + ) + + def test_observations_not_running(self, _m_tango_util): + self.observations_running({2: self.generate_observation(False)}, []) + + def test_observations_running_mix(self, _m_tango_util): + self.observations_running( + { + 1: self.generate_observation(True), + 2: self.generate_observation(False), + 3: self.generate_observation(True), + 5: self.generate_observation(False), + }, + [1, 3], + ) + + def observations_antenna_fields( + self, + observations: Dict[int, obs_module.Observation], + expected_result: List[str], + ): + sut = self.observation_setup(observations) + self.assertListEqual(expected_result, sut.active_antenna_fields) + + def test_active_field_single(self, _m_tango_util): + self.observations_antenna_fields( + {1: self.generate_observation(True, ["HBA"])}, ["HBA"] + ) + + def test_active_field_mix_multi(self, _m_tango_util): + self.observations_antenna_fields( + { + 1: self.generate_observation(False, ["HBA"]), + 2: self.generate_observation(True, ["HBA0"]), + 3: self.generate_observation(True, ["LBA"]), + 5: self.generate_observation(False, ["LBA"]), + }, + ["HBA0", "LBA"], + ) + + def test_add_observation(self, _m_tango_util): + settings = get_observation_settings_two_fields() + for antenna_field in settings.antenna_fields: + antenna_field.stop_time = (datetime.now() + timedelta(days=1)).isoformat() + + sut = obs_module.ObservationController("DMR") + + self.m_observation.return_value.observation_id = "5" + + sut.add_observation(settings) + + self.m_observation.assert_called_once_with("DMR", settings) + self.m_observation.return_value.create_devices.assert_called_once() + self.m_observation.return_value.initialise_observation.assert_called_once() + self.m_observation.return_value.create_subscriptions.assert_called_once() + self.m_observation.return_value.update.assert_called_once() + + self.assertEqual( + sut[self.m_observation.return_value.observation_id], + self.m_observation.return_value, + ) + + def test_add_observation_failed(self, _m_tango_util): + settings = get_observation_settings_two_fields() + for antenna_field in settings.antenna_fields: + antenna_field.stop_time = (datetime.now() + timedelta(days=1)).isoformat() + + sut = obs_module.ObservationController("DMR") + + self.m_observation.return_value.observation_id = "5" + self.m_observation.return_value.create_devices.side_effect = [DevFailed] + + self.assertRaises(RuntimeError, sut.add_observation, settings) + + self.m_observation.return_value.destroy_devices.assert_called_once() + + def test_start_observation(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + + sut["5"] = mock.Mock() + + sut.start_observation("5") + + sut["5"].start.assert_called_once() + + def test_start_observation_key_error(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + + self.assertRaises(KeyError, sut.start_observation, "12554812435") + + def test_stop_observation(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + + m_observation = mock.Mock() + sut["5"] = m_observation + + sut.stop_observation_now("5") + + m_observation.stop.assert_called_once() + + def test_stop_observation_key_error(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + + self.assertRaises(KeyError, sut.stop_observation_now, "12554812435") + + def test_stop_all_observations_now_no_running(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + sut._destroy_all_observation_field_devices = mock.Mock() + + sut.stop_all_observations_now() + + sut._destroy_all_observation_field_devices.assert_called_once() + + def test_stop_all_observations_now_running(self, _m_tango_util): + sut = obs_module.ObservationController("DMR") + sut._destroy_all_observation_field_devices = mock.Mock() + sut.stop_observation_now = mock.Mock() + + sut["5"] = mock.Mock() + sut["9"] = mock.Mock() + + sut.stop_all_observations_now() + + sut._destroy_all_observation_field_devices.assert_called_once() + self.assertEqual((("5",),), sut.stop_observation_now.call_args_list[0]) + self.assertEqual((("9",),), sut.stop_observation_now.call_args_list[1]) + + @mock.patch.object(obs_module, "Database") + def test_destroy_all_observation_field_devices_errors( + self, m_database, _m_tango_util + ): + """Test that all exported devices for the class are destroyed""" + devices = [mock.Mock(), mock.Mock()] + m_database.return_value.get_device_exported_for_class.return_value = devices + + m_database.return_value.delete_device.side_effect = [Exception] + + obs_module.ObservationController._destroy_all_observation_field_devices() + + self.assertEqual( + ((devices[0],),), m_database.return_value.delete_device.call_args_list[0] + ) + self.assertEqual( + ((devices[1],),), m_database.return_value.delete_device.call_args_list[1] + ) diff --git a/tangostationcontrol/test/observation/test_observation_field.py b/tangostationcontrol/test/observation/test_observation_field.py new file mode 100644 index 0000000000000000000000000000000000000000..29509ebe7b7cca5982df0e6a47b16ed4927ec3e1 --- /dev/null +++ b/tangostationcontrol/test/observation/test_observation_field.py @@ -0,0 +1,111 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import sys +from unittest import mock +from unittest.mock import Mock + +from tango import DevState, DevFailed + +from tangostationcontrol.observation import observation_field +from tangostationcontrol.common.proxy import create_device_proxy +from tangostationcontrol.test.dummy_observation_settings import ( + get_observation_settings_two_fields, +) + +from test import base + + +@mock.patch("tango.Util.instance") +class TestObservationField(base.TestCase): + def test_properties(self, _): + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + self.assertEqual(5, sut.observation_id) + self.assertEqual("HBA0", sut.antenna_field) + self.assertEqual("ObservationField", sut.class_name) + self.assertEqual("DMR/ObservationField/5-HBA0", sut.device_name) + self.assertEqual("DMR/ObservationField/5-HBA0/alive_R", sut.attribute_name) + + def test_create_observation_device(self, tu_mock): + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + sut.create_observation_field_device() + + tu_mock.return_value.create_device.assert_called_with( + sut.class_name, sut.device_name + ) + + def test_create_observation_device_fail(self, tu_mock): + """Test creation failed""" + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + + with mock.patch.object(observation_field, "logger") as m_logger: + tu_mock.return_value.create_device.side_effect = [DevFailed] + + self.assertRaises(DevFailed, sut.create_observation_field_device) + + m_logger.exception.assert_called() + + @mock.patch("tango.DeviceProxy") + def test_initialise_observation_field(self, dp_mock, tu_mock): + importlib.reload(sys.modules[create_device_proxy.__module__]) + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + sut.initialise_observation_field() + + self.assertEqual( + dp_mock.return_value.observation_field_settings_RW, + get_observation_settings_two_fields().antenna_fields[0].to_json(), + ) + dp_mock.return_value.Initialise.assert_called() + + @mock.patch("tango.DeviceProxy") + def test_start(self, dp_mock, tu_mock): + importlib.reload(sys.modules[create_device_proxy.__module__]) + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + sut.initialise_observation_field() + sut.start() + + dp_mock.return_value.On.assert_called() + + def test_subscribe(self, _): + def dummy(): + pass + + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + dp_mock = Mock() + sut._device_proxy = dp_mock + sut.subscribe(dummy) + + dp_mock.poll_attribute.assert_called() + dp_mock.subscribe_event.assert_called() + + def test_stop(self, tu_mock): + importlib.reload(sys.modules[observation_field.ObservationField.__module__]) + sut = observation_field.ObservationField( + "DMR", get_observation_settings_two_fields().antenna_fields[0] + ) + + dp_mock = Mock() + dp_mock.state.return_value = DevState.OFF + + sut._device_proxy = dp_mock + + sut.stop() + + dp_mock.ping.assert_called() + dp_mock.unsubscribe_event.assert_called() + dp_mock.Off.assert_called() + + tu_mock.return_value.delete_device.assert_called() diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini index 2d716d729d35128024837720c32f8f99f509e3e4..3e4f13eb407b5df5a4724ea4c7fa27cc83ace5db 100644 --- a/tangostationcontrol/tox.ini +++ b/tangostationcontrol/tox.ini @@ -13,13 +13,11 @@ wheel_build_env = .pkg install_command = {envbindir}/pip3 install {opts} {packages} passenv = HOME setenv = - VIRTUAL_ENV={envdir} PYTHONWARNINGS=default::DeprecationWarning ; Share the same envdir with as many jobs as possible due to extensive time it ; takes to compile the pytango wheel, in addition to its large install size. ; should the environment change (such as the Python version) the environment ; will automatically be recreated. -envdir = {toxworkdir}/testenv deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt