diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json index 4ce19112ec551a878b3e87fa1e7a4e3298769340..3b0456e8e74057faeaa57af08931640bf7c4e557 100644 --- a/CDB/LOFAR_ConfigDb.json +++ b/CDB/LOFAR_ConfigDb.json @@ -11,6 +11,9 @@ "Station_Number": [ "999" ], + "Suppress_State_Transition_Failures": [ + "True" + ], "Power_Children": [ "STAT/EC/1", "STAT/PSOC/1", @@ -19,7 +22,6 @@ "STAT/Configuration/1", "STAT/ObservationControl/1", "STAT/TemperatureManager/1", - "STAT/Docker/1", "STAT/Calibration/1", "STAT/Boot/1" @@ -32,7 +34,6 @@ "STAT/PCON/1", "STAT/ObservationControl/1", "STAT/TemperatureManager/1", - "STAT/Docker/1", "STAT/Calibration/1", "STAT/Configuration/1", "STAT/Boot/1", @@ -55,14 +56,11 @@ "STAT": { "CCD": { "STAT/CCD/1": { - } - } - } - }, - "Docker": { - "STAT": { - "Docker": { - "STAT/Docker/1": { + "properties": { + "CCD_On_Off_timeout": [ + "1" + ] + } } } } @@ -71,6 +69,11 @@ "STAT": { "Calibration": { "STAT/Calibration/1": { + "properties": { + "Station_Name": [ + "DevStation" + ] + } } } } @@ -206,8 +209,7 @@ "False" ], "Power_Children": [ - "STAT/APS/H0", - "STAT/APSPU/1" + "STAT/APS/H0" ] } } @@ -356,6 +358,7 @@ "STAT/APS/H0": { "properties": { "Power_Children": [ + "STAT/APSPU/1" ], "Control_Children": [ "STAT/APSCT/1", @@ -372,6 +375,11 @@ "STAT": { "APSCT": { "STAT/APSCT/1": { + "properties": { + "APSCT_On_Off_timeout": [ + "1" + ] + } } } } @@ -396,6 +404,14 @@ "STAT": { "RECVH": { "STAT/RECVH/1": { + "properties": { + "RCU_On_Off_timeout": [ + "1" + ], + "RCU_DTH_On_Off_timeout": [ + "1" + ] + } } } } @@ -404,6 +420,14 @@ "STAT": { "RECVL": { "STAT/RECVL/1": { + "properties": { + "RCU_On_Off_timeout": [ + "1" + ], + "RCU_DTH_On_Off_timeout": [ + "1" + ] + } } } } @@ -547,6 +571,11 @@ "STAT": { "UNB2": { "STAT/UNB2/1": { + "properties": { + "UNB2_On_Off_timeout": [ + "1" + ] + } } } } diff --git a/CDB/stations/cs001.json b/CDB/stations/cs001.json index 569bb889b82b49d09fc461ad8292722f05088864..af1c95884cd07f5d01c38018f3572dc0940ff9ba 100644 --- a/CDB/stations/cs001.json +++ b/CDB/stations/cs001.json @@ -46,6 +46,19 @@ } } }, + "Calibration": { + "STAT": { + "Calibration": { + "STAT/Calibration/1": { + "properties": { + "Station_Name": [ + "CS001" + ] + } + } + } + } + }, "PCON": { "STAT": { "PCON": { @@ -114,19 +127,6 @@ } } }, - "CCD": { - "STAT": { - "CCD": { - "STAT/CCD/1": { - "properties": { - "OPC_Server_Name": [ - "ccd-sim" - ] - } - } - } - } - }, "EC": { "STAT": { "EC": { diff --git a/CDB/stations/dummy_positions_ConfigDb.json b/CDB/stations/dummy_positions_ConfigDb.json index 020ebe51e6854b5150fd548afb4460d3784858ba..ee9fc852b9c772b65408bff8719e282cd663fd26 100644 --- a/CDB/stations/dummy_positions_ConfigDb.json +++ b/CDB/stations/dummy_positions_ConfigDb.json @@ -129,6 +129,16 @@ "6", "0", "6", "1", "6", "2", "6", "3", "6", "4", "6", "5", "7", "0", "7", "1", "7", "2", "7", "3", "7", "4", "7", "5" ], + "Antenna_Names": [ + "H0", "H1", "H2", "H3", "H4", "H5", + "H6", "H7", "H8", "H9", "H10", "H11", + "H12", "H13", "H14", "H15", "H16", "H17", + "H18", "H19", "H20", "H21", "H22", "H23", + "H24", "H25", "H26", "H27", "H28", "H29", + "H30", "H31", "H32", "H33", "H34", "H35", + "H36", "H37", "H38", "H39", "H40", "H41", + "H42", "H43", "H44", "H45", "H46", "H47" + ], "Antenna_Field_Reference_ETRS": [ "3826896.631", "460979.131", "5064657.943" ], diff --git a/README.md b/README.md index 9c5a78f4e06b29b666feba16371d8fb0f4f104a7..0423d581a9dfcf2d2418bf7cdf834a53bae4800a 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ Next change the version in the following places: # Release Notes +* 0.20.0 Complete implementation of station-state transitions in StationManager device. + Unified power management under power_hardware_on/off(), dropping prepare_hardware(), + disable_hardware(). + Replaced device.warm_boot() by device.boot(). * 0.19.0 Ensure requirements.txt are installed when using pip install * 0.18.3 Many configuration fixes in tango device configs, Fixed APS & EC device port mapping, fixed variable initialization in several devices, Fixed XST device going into diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh index ce817b134f80b71aab5f6e5722d6622a3990c287..5864730e09b8aa7626746867c8dfb0781759a804 100755 --- a/sbin/run_integration_test.sh +++ b/sbin/run_integration_test.sh @@ -89,7 +89,7 @@ echo '/usr/local/bin/wait-for-it.sh ${TANGO_HOST} --strict --timeout=300 -- true # Devices list is used to explitly word split when supplied to commands, must # disable shellcheck SC2086 for each case. -DEVICES=(device-station-manager device-boot device-apsct device-ccd device-ec device-apspu device-sdpfirmware device-sdp device-recvh device-recvl device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration device-calibration) +DEVICES=(device-station-manager device-boot device-aps device-apsct device-ccd device-ec device-apspu device-sdpfirmware device-sdp device-recvh device-recvl device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration device-calibration) SIMULATORS=(sdptr-sim recvh-sim recvl-sim unb2-sim apsct-sim apspu-sim ccd-sim ec-sim) diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index 1cf0537c34385c81dd797cc51be2a589e17118e9..5a03fb737b381f0b07b830ba4bb6e87342a7a914 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.19.0 +0.20.0 diff --git a/tangostationcontrol/integration_test/configuration/test_device_configuration.py b/tangostationcontrol/integration_test/configuration/test_device_configuration.py index 37334af2441cac1c472cccba1600c6e3f7306dd4..fbc2d492919bd2164b92e5329018053b9ccf543c 100644 --- a/tangostationcontrol/integration_test/configuration/test_device_configuration.py +++ b/tangostationcontrol/integration_test/configuration/test_device_configuration.py @@ -55,7 +55,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): if not INITIAL_CONFIGURATION: if self.proxy.state() == DevState.OFF: - self.proxy.warm_boot() + self.proxy.boot() INITIAL_CONFIGURATION = self.proxy.station_configuration_RW @@ -73,7 +73,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): global INITIAL_CONFIGURATION if self.proxy.state() == DevState.OFF: - self.proxy.warm_boot() + self.proxy.boot() self.proxy.station_configuration_RW = INITIAL_CONFIGURATION @@ -83,7 +83,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): """Upload station configuration with original default values""" if self.proxy.state() == DevState.OFF: - self.proxy.warm_boot() + self.proxy.boot() f = pkg_resources.resource_stream( __name__, f"configDB/{self.DB_DEFAULT_CONFIG_FILE}" @@ -99,7 +99,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): """Base method shared across read configuration tests""" self.assertEqual(DevState.OFF, self.proxy.state()) - self.proxy.warm_boot() + self.proxy.boot() self.assertEqual(DevState.ON, self.proxy.state()) station_configuration = self.proxy.station_configuration_RW @@ -214,7 +214,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): control_child_property = self.CONTROL_CHILD_PROPERTY.casefold() self.assertEqual(DevState.OFF, self.proxy.state()) - self.proxy.warm_boot() + self.proxy.boot() self.assertEqual(DevState.ON, self.proxy.state()) # So far configuration Database is set from files in the DB_FILE_LIST @@ -262,7 +262,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): def test_backup_station_configuration(self): """Test whether the station control configuration is correctly saved in the correspondent attribute as a backup""" - self.proxy.warm_boot() + self.proxy.boot() default_dbdata = json.loads(self.proxy.station_configuration_RW) # Load a full new configuration, and consequently erase the previous one self.proxy.station_configuration_RW = self.TEST_CONFIGURATION @@ -275,7 +275,7 @@ class TestDeviceConfiguration(AbstractTestBases.TestDeviceBase): def test_restore_station_configuration(self): """Test whether the backup station control configuration can be correctly restored after a loading/update operation""" - self.proxy.warm_boot() + self.proxy.boot() default_dbdata = json.loads(self.proxy.station_configuration_RW) backup_dbdata = json.loads(self.proxy.backup_station_configuration_R) # Load a full new configuration, and consequently erase the previous one diff --git a/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/integration_test/default/devices/base.py index 0cbc5dbf6678e5c634301eb0615c757e528481fd..377b22df6d6210f030090362c26a3e8ff9d2911d 100644 --- a/tangostationcontrol/integration_test/default/devices/base.py +++ b/tangostationcontrol/integration_test/default/devices/base.py @@ -74,10 +74,10 @@ class AbstractTestBases: self.assertEqual(DevState.ON, self.proxy.state()) - def test_device_warm_boot(self): + def test_device_boot(self): """Test if we can transition off -> on using a warm boot""" - self.proxy.warm_boot() + self.proxy.boot() self.assertEqual(DevState.ON, self.proxy.state()) diff --git a/tangostationcontrol/integration_test/default/devices/interfaces/test_power_hierarchy.py b/tangostationcontrol/integration_test/default/devices/interfaces/test_power_hierarchy.py index 8e6cbd04dac53c8842f3ec2597cd89cbb87ed2fa..9b6433683c8199aed97d106bca086f035e0ddec8 100644 --- a/tangostationcontrol/integration_test/default/devices/interfaces/test_power_hierarchy.py +++ b/tangostationcontrol/integration_test/default/devices/interfaces/test_power_hierarchy.py @@ -4,13 +4,17 @@ """ Power Hierarchy module integration test """ -from tango import DevState, DeviceProxy, DevFailed +from tango import DevState, DeviceProxy from tangostationcontrol.devices.interfaces.power_hierarchy import PowerHierarchy from integration_test import base from integration_test.device_proxy import TestDeviceProxy +import logging + +logger = logging.getLogger() + class TestPowerHierarchy(base.IntegrationTestCase): """Integration Test class for PowerHierarchy methods""" @@ -35,9 +39,37 @@ class TestPowerHierarchy(base.IntegrationTestCase): super().setUp() self.setup_all_devices() + def _unacceptable_exceptions(self): + """Return the set of exceptions raised by the last state transition + in the StationManager, that we do not accept. + + We must accept exceptions since we do not emulate interaction with + actual hardware. In this function, we make sure to just ignore those + which we know will be raised even in a sunny-day scenario.""" + + result = [] + + for ex_str in self.stationmanager_proxy.last_requested_transition_exceptions_R: + # Skip "vetted" exceptions that involve switching power + # on actual hardware, which obviously won't power on in + # our simulators. + if "Failed to execute command_inout on device" in ex_str: + if "command power_hardware_on" in ex_str: + continue + if "command power_hardware_off" in ex_str: + continue + + # Anything left is not acceptable + result.append(ex_str) + + return result + def setup_stationmanager_proxy(self): """Initialise StationManager device""" stationmanager_proxy = TestDeviceProxy(self.stationmanager_name) + # extend timeout for running commands, as state transitions can take a long time + stationmanager_proxy.set_timeout_millis(60000) + stationmanager_proxy.off() stationmanager_proxy.init() self.assertEqual(stationmanager_proxy.state(), DevState.ON) @@ -104,16 +136,28 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.assertEqual(self.ccd_proxy.state(), DevState.OFF) # Switch from OFF to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, "OFF -> HIBERNATE" + ) self.assertEqual(self.psoc_proxy.state(), DevState.ON) self.assertEqual(self.pcon_proxy.state(), DevState.ON) self.assertEqual(self.ccd_proxy.state(), DevState.ON) + logger.info( + f"Exceptions suppressed in test_off_to_hibernate: {self.stationmanager_proxy.last_requested_transition_exceptions_R}" + ) + + self.assertListEqual([], self._unacceptable_exceptions()) + def test_hibernate_to_standby(self): """ Test whether Tango devices are correctly triggered in the HIBERNATE to STANDBY transition """ # Switch from OFF to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, "OFF -> HIBERNATE" + ) self.assertEqual(self.apspu_proxy.state(), DevState.OFF) self.assertEqual(self.apsct_proxy.state(), DevState.OFF) self.assertEqual(self.unb2_proxy.state(), DevState.OFF) @@ -122,6 +166,10 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF) # Switch from HIBERNATE to STANDBY self.stationmanager_proxy.station_standby() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, + "HIBERNATE -> STANDBY", + ) self.assertEqual(self.apspu_proxy.state(), DevState.ON) self.assertEqual(self.apsct_proxy.state(), DevState.ON) self.assertEqual(self.unb2_proxy.state(), DevState.ON) @@ -135,6 +183,12 @@ class TestPowerHierarchy(base.IntegrationTestCase): firmware_images, ) + logger.info( + f"Exceptions suppressed: {self.stationmanager_proxy.last_requested_transition_exceptions_R}" + ) + + self.assertListEqual([], self._unacceptable_exceptions()) + def test_standby_to_on(self): """ Test whether Tango devices are correctly triggered in the STANDBY to ON transition @@ -147,9 +201,18 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.assertEqual(self.sdp_proxy.state(), DevState.OFF) self.assertEqual(self.antennafield_proxy.state(), DevState.OFF) self.stationmanager_proxy.station_on() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, "STANDBY -> ON" + ) self.assertEqual(self.sdp_proxy.state(), DevState.ON) self.assertEqual(self.antennafield_proxy.state(), DevState.ON) + logger.info( + f"Exceptions suppressed: {self.stationmanager_proxy.last_requested_transition_exceptions_R}" + ) + + self.assertListEqual([], self._unacceptable_exceptions()) + def test_on_to_standby(self): """ Test whether Tango devices are correctly triggered in the ON to STANDBY transition @@ -162,9 +225,18 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.stationmanager_proxy.station_on() # Reverse to STANDBY self.stationmanager_proxy.station_standby() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, "ON -> STANDBY" + ) self.assertEqual(self.sdp_proxy.state(), DevState.OFF) self.assertEqual(self.antennafield_proxy.state(), DevState.OFF) + logger.info( + f"Exceptions suppressed: {self.stationmanager_proxy.last_requested_transition_exceptions_R}" + ) + + self.assertListEqual([], self._unacceptable_exceptions()) + def test_standby_to_hibernate(self): """ Test whether Tango devices are correctly triggered in the STANDBY to HIBERNATE transition @@ -175,6 +247,10 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.stationmanager_proxy.station_standby() # Reverse to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual( + self.stationmanager_proxy.last_requested_transition_R, + "STANDBY -> HIBERNATE", + ) self.assertEqual(self.apspu_proxy.state(), DevState.OFF) self.assertEqual(self.apsct_proxy.state(), DevState.OFF) self.assertEqual(self.unb2_proxy.state(), DevState.OFF) @@ -182,21 +258,8 @@ class TestPowerHierarchy(base.IntegrationTestCase): self.assertEqual(self.recvl_proxy.state(), DevState.OFF) self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF) - def test_ensure_power(self): - """ - Test Tango devices ensure_power functionalities within state transitions - """ - stationmanager_ph = PowerHierarchy() - stationmanager_ph.init(self.stationmanager_name) - - # Boot CCD device - self.ccd_proxy.initialise() - self.ccd_proxy.set_translator_defaults() - self.ccd_proxy.set_defaults() - self.ccd_proxy.on() - self.assertEqual(self.ccd_proxy.state(), DevState.ON) + logger.info( + f"Exceptions suppressed: {self.stationmanager_proxy.last_requested_transition_exceptions_R}" + ) - # Power monitoring attribute is False in development environment - self.assertEqual(self.ccd_proxy.CCD_PWR_on_R, False) - with self.assertRaises(DevFailed): - stationmanager_ph.ensure_power(self.ccd_proxy) + self.assertListEqual([], self._unacceptable_exceptions()) diff --git a/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py b/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py index 85c81dc5d468f8643029b5070037bb828366edab..1dd6c9e23a1d1bf62f26443f516909d7932ba734 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py @@ -68,7 +68,7 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECVH/1") recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -76,14 +76,14 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): # setup SDPFirmware sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() return sdpfirmware_proxy def setup_sdp_proxy(self): # setup SDP sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() return sdp_proxy def setup_stationmanager_proxy(self): @@ -120,7 +120,8 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): antennafield_proxy.off() antennafield_proxy.put_property(antenna_properties) antennafield_proxy.put_property(mapping_properties) - antennafield_proxy.boot() # initialises hardware values as well + antennafield_proxy.boot() + antennafield_proxy.power_hardware_on() # Verify all antennas are indicated to work numpy.testing.assert_equal( @@ -171,14 +172,15 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): antennafield_proxy.off() antennafield_proxy.put_property(antenna_properties) antennafield_proxy.put_property(mapping_properties) - antennafield_proxy.boot() # initialises hardware values as well + antennafield_proxy.boot() + antennafield_proxy.power_hardware_on() # Antenna_Usage_Mask_R should be false except one numpy.testing.assert_equal( numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)), antennafield_proxy.Antenna_Usage_Mask_R, ) - # device.boot() writes Antenna_Usage_Mask_R to ANT_mask_RW + # device.power_hardware_on() writes Antenna_Usage_Mask_R to ANT_mask_RW numpy.testing.assert_equal( numpy.array([False] + [True] + [False] * (MAX_ANTENNA - 2)), antennafield_proxy.ANT_mask_RW, diff --git a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py index 02814dcbe852546bb608db71b67fc32c0c6b4d4a..1eecddd87c21c609726862679ee8f7cc1593a73e 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py @@ -34,7 +34,7 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() # setup the frequencies as expected in the test diff --git a/tangostationcontrol/integration_test/default/devices/test_device_bst.py b/tangostationcontrol/integration_test/default/devices/test_device_bst.py index c4a08d430c2ab0aca92ed55efc591a768e112135..e39bb9b2aee0409a566bc1f29b89be84e0bd3630 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_bst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_bst.py @@ -20,6 +20,6 @@ class TestDeviceBST(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + 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 725d50a60f5e8ddc7868e0b85c4ce38c0ca5b6fd..cdd9650060ae1015a1b503282f88f04e9988fcb8 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_calibration.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_calibration.py @@ -121,7 +121,7 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECVH/1") recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -129,21 +129,21 @@ class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): # setup SDP sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() return sdpfirmware_proxy def setup_sdp_proxy(self): # setup SDP sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() return sdp_proxy def setup_proxy(self, dev: str): # setup SDP proxy = TestDeviceProxy(dev) proxy.off() - proxy.warm_boot() + proxy.boot() self.addCleanup(self.shutdown(dev)) return proxy diff --git a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py index 76cfd79e1daa85569ae0c9abd522ab1f46dc80ca..42974a60e8198ec4bf558b8b497f753862532479 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py @@ -56,7 +56,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): def setup_recv_proxy(self): recv_proxy = TestDeviceProxy(self.recv_iden) recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -69,7 +69,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): def setup_beamlet_proxy(self): beamlet_proxy = TestDeviceProxy(self.beamlet_iden) beamlet_proxy.off() - beamlet_proxy.warm_boot() + beamlet_proxy.boot() beamlet_proxy.set_defaults() return beamlet_proxy @@ -77,7 +77,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): # setup SDPFirmware sdpfirmware_proxy = TestDeviceProxy(self.sdpfirmware_iden) sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() sdpfirmware_proxy.set_defaults() return sdpfirmware_proxy @@ -85,7 +85,7 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy(self.sdp_iden) sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() return sdp_proxy diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/integration_test/default/devices/test_device_observation.py index bb26162533c2e86b48a9c58a1b10e6f3ac75ce14..dcd4b53662728021a11b2e55a07cdff794877554 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -139,7 +139,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECVH/1") recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -147,14 +147,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # setup SDPFirmware sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() return sdpfirmware_proxy def setup_sdp_proxy(self): # setup SDP sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() return sdp_proxy def setup_antennafield_proxy(self): @@ -188,7 +188,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # setup Digitalbeam beamlet_proxy = TestDeviceProxy("STAT/Beamlet/HBA") beamlet_proxy.off() - beamlet_proxy.warm_boot() + beamlet_proxy.boot() beamlet_proxy.set_defaults() return beamlet_proxy @@ -196,7 +196,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # setup Digitalbeam digitalbeam_proxy = TestDeviceProxy("STAT/DigitalBeam/HBA") digitalbeam_proxy.off() - digitalbeam_proxy.warm_boot() + digitalbeam_proxy.boot() digitalbeam_proxy.set_defaults() return digitalbeam_proxy @@ -204,7 +204,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): # Setup Tilebeam tilebeam_proxy = TestDeviceProxy("STAT/TileBeam/HBA") tilebeam_proxy.off() - tilebeam_proxy.warm_boot() + tilebeam_proxy.boot() tilebeam_proxy.set_defaults() return tilebeam_proxy 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 1f27234f55f3f9e6b7cb8ef95d9b1d9c9367f542..7457d826656e963c27d380adfcf41112d70237cd 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py @@ -130,7 +130,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECVH/1") recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -138,14 +138,14 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # setup SDPFirmware sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() return sdpfirmware_proxy def setup_sdp_proxy(self): # setup SDP sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() return sdp_proxy def setup_antennafield_proxy(self): @@ -160,7 +160,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): } ) antennafield_proxy.off() - antennafield_proxy.warm_boot() + antennafield_proxy.boot() antennafield_proxy.set_defaults() return antennafield_proxy @@ -168,7 +168,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # setup Digitalbeam beamlet_proxy = TestDeviceProxy("STAT/Beamlet/HBA") beamlet_proxy.off() - beamlet_proxy.warm_boot() + beamlet_proxy.boot() beamlet_proxy.set_defaults() return beamlet_proxy @@ -176,7 +176,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # setup Digitalbeam digitalbeam_proxy = TestDeviceProxy("STAT/DigitalBeam/HBA") digitalbeam_proxy.off() - digitalbeam_proxy.warm_boot() + digitalbeam_proxy.boot() digitalbeam_proxy.set_defaults() return digitalbeam_proxy @@ -184,7 +184,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # Setup Tilebeam tilebeam_proxy = TestDeviceProxy("STAT/TileBeam/HBA") tilebeam_proxy.off() - tilebeam_proxy.warm_boot() + tilebeam_proxy.boot() tilebeam_proxy.set_defaults() return tilebeam_proxy diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py b/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py index dfaf9030187c2afaf32bada38902a416c06cbad3..56487cdd10e2d70954eff0fec9205fe9c50ccaf7 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py @@ -14,7 +14,7 @@ class TestDeviceSDPFirmware(AbstractTestBases.TestDeviceBase): def test_device_sdpfirmware_read_attribute(self): """Test if we can read an attribute obtained over OPC-UA""" - self.proxy.warm_boot() + self.proxy.boot() self.assertListEqual( [True] * N_pn, list(self.proxy.TR_fpga_communication_error_R) diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sst.py b/tangostationcontrol/integration_test/default/devices/test_device_sst.py index 1f0781110daeed79efe82c09dace80825f9ef0a2..7ca8847ce505faa504672a16d2f258742e76b060 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_sst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_sst.py @@ -27,7 +27,7 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): # setup SDP Firmware sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() sdpfirmware_proxy.set_defaults() return sdpfirmware_proxy @@ -35,14 +35,14 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() return sdp_proxy def test_device_sst_send_udp(self): port_property = {"Statistics_Client_TCP_Port": "4998"} self.proxy.put_property(port_property) - self.proxy.warm_boot() + self.proxy.boot() self.assertEqual(DevState.ON, self.proxy.state()) @@ -57,7 +57,7 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): def test_device_sst_connect_tcp_receive(self): port_property = {"Statistics_Client_TCP_Port": "5101"} self.proxy.put_property(port_property) - self.proxy.warm_boot() + self.proxy.boot() time.sleep(2) 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 32bf3719c1e8f5964d177608a01dde8f18070dcd..4c6a33f2546ddc93fbc72b900c6d8f9ee20ee256 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py @@ -53,7 +53,7 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): # setup SDPFirmware sdpfirmware_proxy = TestDeviceProxy(self.sdp_firmware_name) sdpfirmware_proxy.off() - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() sdpfirmware_proxy.set_defaults() return sdpfirmware_proxy @@ -61,7 +61,7 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy(self.sdp_name) sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() return sdp_proxy @@ -86,10 +86,10 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): # is fine for dev in devices: if dev.state() == DevState.OFF: - dev.warm_boot() + dev.boot() elif dev.state() == DevState.FAULT: dev.off() - dev.warm_boot() + dev.boot() self.assertEqual( self.proxy.get_property(self.shutdown_list_attribute)[ diff --git a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py index 45a9f8e1ca11f5beb7c278a2d9b5fc281ebc07e5..528715dbda6544e364b3e0bb4933c6adb5d2f5b1 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py @@ -39,7 +39,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): """Setup RECV""" recv_proxy = TestDeviceProxy("STAT/RECVH/1") recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy @@ -78,7 +78,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): self.setup_antennafield_proxy() # setup BEAM - self.proxy.warm_boot() + self.proxy.boot() # verify delays method returns the correct dimensions delays = self.proxy.delays(self.POINTING_DIRECTION) @@ -90,7 +90,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): antennafield_proxy = self.setup_antennafield_proxy() # setup BEAM - self.proxy.warm_boot() + self.proxy.boot() self.proxy.Tracking_enabled_RW = False # Verify attribute is present (all zeros if never used before) @@ -119,7 +119,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): # setup AntennaField as well antennafield_proxy = self.setup_antennafield_proxy() - self.proxy.warm_boot() + self.proxy.boot() self.proxy.Tracking_enabled_RW = False # Point to Zenith @@ -145,7 +145,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): # setup AntennaField as well antennafield_proxy = self.setup_antennafield_proxy() - self.proxy.warm_boot() + self.proxy.boot() self.proxy.Tracking_enabled_RW = False # point at north on the horizon @@ -187,7 +187,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): # setup AntennaField as well antennafield_proxy = self.setup_antennafield_proxy() - self.proxy.warm_boot() + self.proxy.boot() self.proxy.Tracking_enabled_RW = False # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000) @@ -230,7 +230,7 @@ class TestDeviceTileBeam(AbstractTestBases.TestDeviceBase): self.setup_stationmanager_proxy() self.setup_recv_proxy() self.setup_antennafield_proxy() - self.proxy.warm_boot() + self.proxy.boot() # check if we're really tracking self.assertTrue(self.proxy.Tracking_enabled_R) diff --git a/tangostationcontrol/integration_test/default/devices/test_device_xst.py b/tangostationcontrol/integration_test/default/devices/test_device_xst.py index 806f3148d4f297d3ed35e142941d1fc9fcc5515f..9a8a0bb92b678dcf7016927c631835c988144964 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_xst.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_xst.py @@ -20,6 +20,6 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() return sdp_proxy diff --git a/tangostationcontrol/integration_test/default/devices/test_observation.py b/tangostationcontrol/integration_test/default/devices/test_observation.py index e262c512dc262ea7d22fc03c3df7b43d8df0579a..05f64a2a3cd9e6db1b081fb72a7e9fae3844d2c5 100644 --- a/tangostationcontrol/integration_test/default/devices/test_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_observation.py @@ -20,7 +20,7 @@ class TestObservation(base.IntegrationTestCase): self.setup_stationmanager_proxy() self.observation_control_proxy = TestDeviceProxy("STAT/ObservationControl/1") self.observation_control_proxy.off() - self.observation_control_proxy.warm_boot() + self.observation_control_proxy.boot() # make sure any devices we depend on are also started for device in [ @@ -33,7 +33,7 @@ class TestObservation(base.IntegrationTestCase): ]: proxy = TestDeviceProxy(device) proxy.off() - proxy.warm_boot() + proxy.boot() def setup_stationmanager_proxy(self): """Setup StationManager""" diff --git a/tangostationcontrol/integration_test/default/prometheus/test_tango_prometheus_client.py b/tangostationcontrol/integration_test/default/prometheus/test_tango_prometheus_client.py index 533366f5d7f68295973c4ba00138fd690de0ec54..3a8a042544a05ec63b86bab133ae8a8812a4aed3 100644 --- a/tangostationcontrol/integration_test/default/prometheus/test_tango_prometheus_client.py +++ b/tangostationcontrol/integration_test/default/prometheus/test_tango_prometheus_client.py @@ -52,7 +52,7 @@ class TestPrometheusClient(BaseIntegrationTestCase): # setup RECV recv_proxy = TestDeviceProxy(device_name) recv_proxy.off() - recv_proxy.warm_boot() + recv_proxy.boot() recv_proxy.set_defaults() return recv_proxy diff --git a/tangostationcontrol/integration_test/digitalbeam_performance/test_digitalbeam_performance.py b/tangostationcontrol/integration_test/digitalbeam_performance/test_digitalbeam_performance.py index 0b48e86d404d08c58777b004b2f83acadac0c408..f52746567f7f402fb5bc312d96fec6337b326c2f 100644 --- a/tangostationcontrol/integration_test/digitalbeam_performance/test_digitalbeam_performance.py +++ b/tangostationcontrol/integration_test/digitalbeam_performance/test_digitalbeam_performance.py @@ -49,7 +49,7 @@ class TestDigitalbeamPerformance(base.IntegrationTestCase): for proxy in sdpfirmware_proxies + sdp_proxies + recv_proxies + beamlet_proxies: proxy.off() self.assertTrue(proxy.state() is DevState.OFF) - proxy.warm_boot() + proxy.boot() proxy.set_defaults() self.assertTrue(proxy.state() is DevState.ON) @@ -77,7 +77,7 @@ class TestDigitalbeamPerformance(base.IntegrationTestCase): for proxy in beam_proxies: proxy.off() self.assertTrue(proxy.state() is DevState.OFF) - proxy.warm_boot() + proxy.boot() proxy.set_defaults() proxy.Tracking_enabled_RW = tracking self.assertTrue(proxy.state() is DevState.ON) diff --git a/tangostationcontrol/integration_test/tilebeam_performance/test_tilebeam_performance.py b/tangostationcontrol/integration_test/tilebeam_performance/test_tilebeam_performance.py index 8eda5ab52609657f36a19bedc221f5927fa80eef..60a1984e1c4110cba1b0e65d6aa39eb85bb54619 100644 --- a/tangostationcontrol/integration_test/tilebeam_performance/test_tilebeam_performance.py +++ b/tangostationcontrol/integration_test/tilebeam_performance/test_tilebeam_performance.py @@ -43,14 +43,14 @@ class TestTilebeamPerformance(base.IntegrationTestCase): sdpfirmware_proxy = TestDeviceProxy("STAT/SDPFirmware/HBA") sdpfirmware_proxy.off() self.assertTrue(sdpfirmware_proxy.state() is DevState.OFF) - sdpfirmware_proxy.warm_boot() + sdpfirmware_proxy.boot() sdpfirmware_proxy.set_defaults() self.assertTrue(sdpfirmware_proxy.state() is DevState.ON) sdp_proxy = TestDeviceProxy("STAT/SDP/HBA") sdp_proxy.off() self.assertTrue(sdp_proxy.state() is DevState.OFF) - sdp_proxy.warm_boot() + sdp_proxy.boot() sdp_proxy.set_defaults() self.assertTrue(sdp_proxy.state() is DevState.ON) @@ -64,7 +64,7 @@ class TestTilebeamPerformance(base.IntegrationTestCase): for proxy in recv_proxies: proxy.off() self.assertTrue(proxy.state() is DevState.OFF) - proxy.warm_boot() + proxy.boot() proxy.set_defaults() self.assertTrue(proxy.state() is DevState.ON) @@ -114,7 +114,7 @@ class TestTilebeamPerformance(base.IntegrationTestCase): for proxy in beam_proxies: proxy.off() self.assertTrue(proxy.state() is DevState.OFF) - proxy.warm_boot() + proxy.boot() proxy.set_defaults() proxy.Tracking_enabled_RW = False self.assertTrue(proxy.state() is DevState.ON) diff --git a/tangostationcontrol/tangostationcontrol/common/states.py b/tangostationcontrol/tangostationcontrol/common/states.py index d8d4a9b4a1f40e932d76ecd7e097fd6a376c8bdb..6ab0c5a581978e00e1ae09bcb4c3d3d08c219879 100644 --- a/tangostationcontrol/tangostationcontrol/common/states.py +++ b/tangostationcontrol/tangostationcontrol/common/states.py @@ -1,7 +1,17 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tango import DevState +from enum import Enum +from typing import Dict, Optional +from functools import wraps + +from tango import DevState, DeviceProxy + +from .type_checking import device_class_matches + +# ----------------------- +# Device states +# ----------------------- # The Device states in which we consider our device operational, # and thus allow interaction. @@ -19,3 +29,76 @@ POWER_ON_COMMAND_STATES = DEFAULT_COMMAND_STATES + [DevState.OFF, DevState.INIT] # States in which the hardware can be powered off POWER_OFF_COMMAND_STATES = OPERATIONAL_STATES + [DevState.STANDBY, DevState.DISABLE] + + +# ----------------------- +# Station states +# ----------------------- + + +class StationState(Enum): + """Station states enumeration""" + + OFF = "OFF" + HIBERNATE = "HIBERNATE" + STANDBY = "STANDBY" + ON = "ON" + + +# Contains which transitions are allowed for a given states +ALLOWED_STATION_STATE_TRANSITIONS = { + StationState.OFF: [StationState.HIBERNATE], + StationState.HIBERNATE: [StationState.OFF, StationState.STANDBY], + StationState.STANDBY: [StationState.HIBERNATE, StationState.ON], + StationState.ON: [StationState.STANDBY], +} + + +DEVICES_ON_IN_STATION_STATE: Dict[str, Optional[StationState]] = { + """In which StationState each device class should be switched ON.""" + "StationManager": StationState.HIBERNATE, + "CCD": StationState.HIBERNATE, + "PCON": StationState.HIBERNATE, + "PSOC": StationState.HIBERNATE, + "TemperatureManager": StationState.HIBERNATE, + "Docker": StationState.HIBERNATE, + "Configuration": StationState.HIBERNATE, + "SDPFirmware": StationState.STANDBY, + "APS": StationState.STANDBY, + "APSPU": StationState.STANDBY, + "APSCT": StationState.STANDBY, + "UNB2": StationState.STANDBY, + "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, + # Unmentioned devices will go ON in this state + "_default": StationState.ON, +} + + +def run_if_device_on_in_station_state( + target_state: StationState, + state_table: Dict[str, StationState] = DEVICES_ON_IN_STATION_STATE, +): + """Decorator for a function func(device). It only executes the decorated function if the device should be turned ON in the target_state. Returns None otherwise.""" + + def inner(func): + @wraps(func) + def wrapper(device: DeviceProxy, *args, **kwargs): + def should_execute(): + for pattern, station_state in state_table.items(): + if not device_class_matches(device, pattern): + continue + + return target_state == station_state + + # no matches, use default (if any) + return target_state == state_table.get("_default") + + # execute function if we should in the configured state + return func(device, *args, **kwargs) if should_execute() else None + + return wrapper + + return inner diff --git a/tangostationcontrol/tangostationcontrol/common/type_checking.py b/tangostationcontrol/tangostationcontrol/common/type_checking.py index d70ee6b6c750aa570b7b45d0eb2d56f59036e3eb..9d4496743412bce99b9554060c4859f1f8d11012 100644 --- a/tangostationcontrol/tangostationcontrol/common/type_checking.py +++ b/tangostationcontrol/tangostationcontrol/common/type_checking.py @@ -1,7 +1,10 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 +from typing import Union + from tango.utils import is_seq +from tango import DeviceProxy def sequence_not_str(obj): @@ -12,3 +15,25 @@ def sequence_not_str(obj): def type_not_sequence(obj): """True for types that are not sequences""" return not is_seq(obj) and isinstance(obj, type) + + +def device_class_matches( + device: DeviceProxy, class_name: Union[str, list[str]] +) -> bool: + """Returns whether the clas of the device represented by `device` + matches `class_name`, which is either a string or a list of strings.""" + + # NB: We'd like to use device.info().dev_class but that requires + # a connection to the device instead of just to the database. + # If the pattern does not match, the device might not be needed, + # so we should not assume the device is actually up. + # + # In production, they always should be up, but it grealy helps + # development and debugging if we can ignore devices that are + # in the hierarchy but not needed for the requested state + # transition. + device_class_name = device.dev_name().split("/")[1].casefold() + if isinstance(class_name, str): + return device_class_name == class_name.casefold() + else: + return device_class_name in (name.casefold() for name in class_name) diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index 61329acaea2d85be8cdc153cc744e254e61c3f48..e408613cb53a39f6b1c6b62ba10e8bbca29b348c 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -999,15 +999,25 @@ class AntennaField(LOFARDevice): self.__setup_recv_mapper() self.__setup_sdp_mapper() - @log_exceptions() - def _prepare_hardware(self): + def _power_hardware_on(self): # Configure the devices that process our antennas self.configure_recv() self.configure_sdp() + def _power_hardware_off(self): + # Save actual mask values + ANT_mask_RW = self.read_attribute("ANT_mask_RW") + + # Enable controlling all antennas + self.proxy.write_attribute("ANT_mask_RW", [True] * len(ANT_mask_RW)) + + # Turn off power to all antennas + self.proxy.write_attribute("RCU_PWR_ANT_on_RW", [True] * len(ANT_mask_RW)) + # -------- # Commands # -------- + @command() @only_in_states(DEFAULT_COMMAND_STATES) @DebugIt() @@ -1016,8 +1026,6 @@ class AntennaField(LOFARDevice): """Configure RECV to process our antennas.""" # Disable controlling the tiles that fall outside the mask - # WARN: Needed in configure_for_initialise but Tango does not allow to write - # attributes in INIT state self.proxy.write_attribute( "ANT_mask_RW", self.read_attribute("Antenna_Usage_Mask_R") ) diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py index 0c37b0f077661c99907c4d123862111cb8019590..a0274ea5bdcb52194c40b07608662b239d698ec9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apsct.py +++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py @@ -191,8 +191,12 @@ class APSCT(OPCUADevice): # overloaded functions # -------- - def _prepare_hardware(self): - """Initialise the APSCT hardware.""" + def _read_hardware_powered_R(self): + """Read attribute which monitors the power""" + return self.read_attribute("APSCT_PWR_on_R") + + def _power_hardware_on(self): + """Turns on the 200MHz clock.""" # Cycle clock self.APSCT_off() @@ -204,6 +208,8 @@ class APSCT(OPCUADevice): "APSCTTR_translator_busy_R", False, self.APSCT_On_Off_timeout ) + self.wait_attribute("APSCT_PWR_on_R", True, self.APSCT_On_Off_timeout) + if not self.read_attribute("APSCT_PLL_200MHz_locked_R"): if self.read_attribute("APSCTTR_I2C_error_R"): raise Exception( @@ -216,8 +222,8 @@ class APSCT(OPCUADevice): + "The subrack probably do not receive clock input or the CLK PCB is broken?" ) - def _disable_hardware(self): - """Disable the APSCT hardware.""" + def _power_hardware_off(self): + """Turns off the clock.""" # Turn off the APSCT self.APSCT_off() @@ -225,19 +231,7 @@ class APSCT(OPCUADevice): "APSCTTR_translator_busy_R", False, self.APSCT_On_Off_timeout ) - def _read_hardware_powered_R(self): - """Read attribute which monitors the power""" - return self.read_attribute("APSCT_PWR_on_R") - - def _power_hardware_on(self): - """Power hardware on and check the correctness of the operation""" - # TODO() power on operations - self.wait_attribute("APSCT_PWR_on_R", True, timeout=5) - - def _power_hardware_off(self): - """Power hardware off and check the correctness of the operation""" - # TODO() power off operations - self.wait_attribute("APSCT_PWR_on_R", False, timeout=5) + self.wait_attribute("APSCT_PWR_on_R", False, self.APSCT_On_Off_timeout) # -------- # Commands diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py index 7691077de046530b7cd8bb1fea0b66c6217677e2..590fd63b88c9663c863b92644f8fe35e15a9d242 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apspu.py +++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py @@ -144,10 +144,6 @@ class APSPU(OPCUADevice): # overloaded functions # -------- - def _disable_hardware(self): - """Disable the APSPU hardware.""" - super()._disable_hardware() - # -------- # Commands # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py index d942e27dabbce020533b50875c776128f6df6101..2f86b812b1b36b0acf3721c6a10ee7e7904b9158 100644 --- a/tangostationcontrol/tangostationcontrol/devices/boot.py +++ b/tangostationcontrol/tangostationcontrol/devices/boot.py @@ -207,10 +207,9 @@ class DevicesInitialiser(object): proxy = self.devices[device_name] self.set_status(f"[restarting {device_name}] Booting device.") + proxy.boot() if self.initialise_hardware: - proxy.boot() - else: - proxy.warm_boot() + proxy.power_hardware_on() if proxy.state() not in OPERATIONAL_STATES: raise InitialisationException( @@ -247,7 +246,7 @@ class Boot(LOFARDevice): dtype="DevVarStringArray", mandatory=False, default_value=[ - "STAT/Docker/1", + # "STAT/Docker/1", # TODO(JDM): disabled as it doesn't work in gitlab CI/CD dind. # Docker controls the device containers, so it goes before anything else "STAT/Configuration/1", # Configuration device loads and update station configuration diff --git a/tangostationcontrol/tangostationcontrol/devices/calibration.py b/tangostationcontrol/tangostationcontrol/devices/calibration.py index bbfac52d214ec052eb26277eb40fe1dfe234bd63..d4cfe7fcd7867bdddb7687fdce348808904e1df7 100644 --- a/tangostationcontrol/tangostationcontrol/devices/calibration.py +++ b/tangostationcontrol/tangostationcontrol/devices/calibration.py @@ -41,7 +41,6 @@ class Calibration(LOFARDevice): self.sdpfirmware_proxies: dict = {} self.sdp_proxies: dict = {} self.ant_proxies: dict = {} - self.station_name: str = None # Super must be called after variable assignment due to executing init_device! super().__init__(cl, name) @@ -98,6 +97,17 @@ class Calibration(LOFARDevice): self.calibrate_sdp(k) self.calibrate_recv(k) + # TODO(JDM): While we could read this from our control parent (StationManager), + # doing so creates a deadlock when StationManager wants to initialise this + # device. At which point this device cannot reach StationManager to read the + # station name. + Station_Name = device_property( + doc="Name of the station, f.e. CS001", + dtype="DevString", + mandatory=False, + default_value="DevStation", + ) + Calibration_Table_Base_URL = device_property( doc="Base URL of the calibration tables", dtype="DevString", @@ -190,11 +200,8 @@ class Calibration(LOFARDevice): def configure_for_initialise(self): super().configure_for_initialise() - self.station_name = self.control.read_parent_attribute( - self.control.parent(), "station_name_R" - ) self._calibration_manager = CalibrationManager( - self.Calibration_Table_Base_URL, self.station_name + self.Calibration_Table_Base_URL, self.Station_Name ) db = Database() diff --git a/tangostationcontrol/tangostationcontrol/devices/ccd.py b/tangostationcontrol/tangostationcontrol/devices/ccd.py index 4170f52a22b28efef0535d55d9dfafabf5dc0c7c..88c587451ea3e29eda5b66d1750500d20a839d05 100644 --- a/tangostationcontrol/tangostationcontrol/devices/ccd.py +++ b/tangostationcontrol/tangostationcontrol/devices/ccd.py @@ -201,14 +201,18 @@ class CCD(OPCUADevice): return self.read_attribute("CCD_PWR_on_R") def _power_hardware_on(self): - """Power hardware on and check the correctness of the operation""" - # TODO() power on operations - self.wait_attribute("CCD_PWR_on_R", True, timeout=5) + """Forward the clock signal.""" + + # CCD is powered on by default when it + # starts up. + pass def _power_hardware_off(self): - """Power hardware off and check the correctness of the operation""" - # TODO() power off operations - self.wait_attribute("CCD_PWR_on_R", False, timeout=5) + """Stop forwarding the clock signal.""" + + # CCD does not have to be powered off + # during shutdown sequence. + pass # -------- # Commands diff --git a/tangostationcontrol/tangostationcontrol/devices/device_decorators.py b/tangostationcontrol/tangostationcontrol/devices/device_decorators.py index c70fe7cede03deafa5642cf6a24c4480f5a70116..406e4b3ddac5fb09cbc6912987df385e2a20fd17 100644 --- a/tangostationcontrol/tangostationcontrol/devices/device_decorators.py +++ b/tangostationcontrol/tangostationcontrol/devices/device_decorators.py @@ -8,12 +8,14 @@ import logging import time from functools import wraps -from tango import DevState, DevFailed +from tango import DevState, DevFailed, DeviceProxy + +from tangostationcontrol.common.lofar_logging import exception_to_str logger = logging.getLogger() __all__ = [ - "hierarchy_device_connected", + "suppress_exceptions", "only_in_states", "only_when_on", "fault_on_error", @@ -21,22 +23,40 @@ __all__ = [ ] -def hierarchy_device_connected(): - """ - Wrapper to handle a DevFailed Exception with a HierarchyDevice operation - """ +def suppress_exceptions(suppress: bool = True): + """Catch all Exceptions and collect them in a list `exceptions` attached + to the decorated function. If suppress = True, do not reraise them. + + Each element in the `exceptions` list is a (device, exception) tuple, + where "device" is the parameter to func(device) on which func + threw the exception. + + Never supresses programming/deployment errors (SyntaxError, etc).""" def inner(func): @wraps(func) - def error_wrapper(self, *args, **kwargs): + def wrapper(device: DeviceProxy): try: - return func(self, *args, **kwargs) - except DevFailed as _e: - reason = _e.args[-1].desc.replace("\n", " ") - logger.error("Error in state transition: %s", reason) + return func(device) + except (SyntaxError, NameError, ImportError) as e: + # These exceptions we never suppress raise + except Exception as e: + logger.exception( + f"Collected exception during execution of {func.__name__}({device}): {exception_to_str(e)}" + ) - return error_wrapper + # collect it + wrapper.exceptions.append((device, e)) + + # reraise if requested + if not suppress: + raise + + # collection of exceptions raised by this function + wrapper.exceptions = [] + + return wrapper return inner diff --git a/tangostationcontrol/tangostationcontrol/devices/ec.py b/tangostationcontrol/tangostationcontrol/devices/ec.py index 165eaf745c233416ff9f54d13aa6ce162ecfe388..ecf59482efea4626dbc1708ac4ebd65f8175b305 100644 --- a/tangostationcontrol/tangostationcontrol/devices/ec.py +++ b/tangostationcontrol/tangostationcontrol/devices/ec.py @@ -111,6 +111,7 @@ class EC(OPCUADevice): # -------- # overloaded functions # -------- + def _read_hardware_powered_R(self): # the device and translator are one, so if # the translator is reachable, the hardware diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py index 01a343525fb5427dda178f47f5216cfd99d8ef3b..7601487e5477f7ece2aaeffe38cd4a56dfc0e4b6 100644 --- a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py @@ -147,7 +147,7 @@ class AbstractHierarchy(ABC): device = device.casefold() if not self._proxies.get(device): - self._proxies[device] = create_device_proxy(device, 10000) + self._proxies[device] = create_device_proxy(device, 30000) return self._proxies[device] @@ -430,3 +430,23 @@ class AbstractHierarchy(ABC): return None return _parent.state() + + def walk_down(self, func, depth: int = -1): + """Execute the given function on every node in the tree downwards from the root, depth first.""" + + def _walk(children, func): + for child in children.values(): + func(child["proxy"]) + _walk(child["children"], func) + + _walk(self.children(depth), func) + + def walk_up(self, func, depth: int = -1): + """Execute the given function on every node in the tree upwards from the leaves, depth first.""" + + def _walk(children, func): + for child in reversed(children.values()): + _walk(child["children"], func) + func(child["proxy"]) + + _walk(self.children(depth), func) diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py index 36f2cad95907072ce003d2e38b902aebb24b3a63..a197c966dd2a8b0d8b8cd1d5a78c8be81629a34d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py @@ -29,8 +29,6 @@ from tangostationcontrol.common.lofar_logging import log_exceptions from tangostationcontrol.common.states import ( DEFAULT_COMMAND_STATES, INITIALISED_STATES, - POWER_ON_COMMAND_STATES, - POWER_OFF_COMMAND_STATES, ) from tangostationcontrol.common.proxy import create_device_proxy from tangostationcontrol.common.type_checking import sequence_not_str @@ -66,9 +64,9 @@ class LOFARDevice(Device): OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, STANDBY -> ON: Triggered by user. Device reports to be functional, - STANDBY -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the disable_hardware() command, - ON -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the disable_hardware() command, - ALARM -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the disable_hardware() command, + STANDBY -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the power_hardware_off() command, + ON -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the power_hardware_off() command, + ALARM -> DISABLE: Triggered by user. Device has shut down its hardware. Triggered by the power_hardware_off() command, ON -> ALARM: Triggered by tango. Device has attribute(s) with value(s) exceeding their alarm treshold, * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, @@ -431,8 +429,9 @@ class LOFARDevice(Device): def _read_hardware_powered_R(self): """Overloadable function called in read attribute hardware_powered_R""" - logger.warning("Attribute hardware_powered_R not implemented on this device.") - return False + + # If device backs no hardware, assume it's powered + return True def _set_defaults_for(self, attribute_names: list): """Set hardware points to their default value for attribute_names. @@ -515,28 +514,24 @@ class LOFARDevice(Device): """ self._set_defaults() - @only_in_states(DEFAULT_COMMAND_STATES) - @fault_on_error() - @command() - def set_translator_defaults(self): - """Initialise the translator translators to their configured settings.""" + def _set_translator_defaults(self): + """Initialise any translators to their default settings.""" - # This is just the command version of _set_translator_defaults(). - self._set_translator_defaults() + self._set_defaults_for(self.TRANSLATOR_DEFAULT_SETTINGS) @command() @DebugIt() - def prepare_hardware(self): - """Load firmware required before configuring anything.""" + def power_hardware_on(self): + """Power the hardware related to the device.""" # This is just the command version of _prepare_hardware(). - self._prepare_hardware() + self._power_hardware_on() @only_in_states(INITIALISED_STATES) @fault_on_error() @command() @DebugIt() - def disable_hardware(self): + def power_hardware_off(self): """Disable the hardware related to the device.""" if self.get_state() == DevState.DISABLE: @@ -546,58 +541,28 @@ class LOFARDevice(Device): ) return - self._disable_hardware() + self._power_hardware_off() # Set state to DISABLE self.set_state(DevState.DISABLE) self.set_status("Device is in the DISABLE state.") - @only_in_states(POWER_ON_COMMAND_STATES) - @command() - @DebugIt() - def power_hardware_on(self): - """Power on the hardware related to the device.""" - - if self.read_attribute("hardware_powered_R"): - # Already powered on - logger.warning( - "Requested to power on the hardware, but have already powered it on." - ) - return - - self._power_hardware_on() - - logger.info("Device hardware is now powered on.") - - @only_in_states(POWER_OFF_COMMAND_STATES) + @only_in_states(DEFAULT_COMMAND_STATES) + @fault_on_error() @command() - @DebugIt() - def power_hardware_off(self): - """Power off the hardware related to the device.""" - - if not self.read_attribute("hardware_powered_R"): - # Already powered off - logger.warning( - "Requested to power off the hardware, but have already powered it off." - ) - return - - self._power_hardware_off() + def set_translator_defaults(self): + """Initialise the translator translators to their configured settings.""" - logger.info("Device hardware is now powered off.") + # This is just the command version of _set_translator_defaults(). + self._set_translator_defaults() - def _boot(self, initialise_hardware=True): + def _boot(self): # setup connections self._Initialise() + # initialise settings self.set_translator_defaults() - - if initialise_hardware: - # prepare hardware to accept settings - self._prepare_hardware() - - # initialise settings - self._set_defaults() + self.set_defaults() # make device available self._On() @@ -605,35 +570,17 @@ class LOFARDevice(Device): @only_in_states([DevState.OFF]) @command() def boot(self): - self._boot(initialise_hardware=True) - - @only_in_states([DevState.OFF]) - @command() - def warm_boot(self): - self._boot(initialise_hardware=False) - - def _set_translator_defaults(self): - """Initialise any translators to their default settings.""" - - self._set_defaults_for(self.TRANSLATOR_DEFAULT_SETTINGS) + self._boot() @only_in_states(DEFAULT_COMMAND_STATES) @fault_on_error() - def _prepare_hardware(self): + def _power_hardware_on(self): """Override this method to load any firmware or power cycle components before configuring the hardware.""" pass - def _disable_hardware(self): - """Override this method to disable any hardware related to the device.""" - pass - - def _power_hardware_on(self): - """Override this method to power on the hardware related to the device""" - pass - def _power_hardware_off(self): - """Override this method to power off the hardware related to the device""" + """Override this method to disable any hardware related to the device.""" pass def _read_attribute(self, attr_name): diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py index b711c14c63a35eaca6979fc50470279a1b04cea3..ccf8892e730447c1d38dfa66bf39b7df89903c49 100644 --- a/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py @@ -3,18 +3,20 @@ """Power Hierarchy for PyTango devices""" -from typing import Dict -from typing import Optional +from typing import Dict, Optional import logging -from tango import DeviceProxy, DevState, DevFailed +from tango import DeviceProxy, DevState -from tangostationcontrol.common.proxy import create_device_proxy +from tangostationcontrol.common.states import ( + StationState, + run_if_device_on_in_station_state, +) +from tangostationcontrol.common.type_checking import device_class_matches from tangostationcontrol.devices.interfaces.hierarchy_device import ( AbstractHierarchyDevice, - HierarchyMatchingFilter, ) -from tangostationcontrol.devices.device_decorators import hierarchy_device_connected +from tangostationcontrol.devices.device_decorators import suppress_exceptions logger = logging.getLogger() @@ -29,243 +31,201 @@ class PowerHierarchy(AbstractHierarchyDevice): def init( self, device_name: str, + continue_on_failure: bool = False, proxies: Optional[Dict[str, DeviceProxy]] = None, ): super().init(device_name, self.POWER_CHILD_PROPERTY, proxies) - def ensure_power( - self, - device: DeviceProxy, - timeout_millis: int = 10000, - ): - """Check if a device is correctly power connected - - :param device: DeviceProxy to be checked - :param timeout_millis: Tango device timeout to be increased from its default 3000 ms - """ - attr_name = "hardware_powered_R" - - # Try to power on again if attribute indicates power off - try: - pwr_attr = device.read_attribute(attr_name) - if pwr_attr.value is False: - logger.warning( - "Device %s has not powered correctly. Attribute %s has value %s", - device.name(), - pwr_attr.name, - pwr_attr.value, - ) - - # Wait for power - device.set_timeout_millis(timeout_millis) - device.power_hardware_on() - except DevFailed as exc: - error_string = ( - f"Device power attribute {device.name()}/{pwr_attr.name} " - f"is not responding" - ) - logger.exception(error_string) - raise DevFailed(error_string) from exc - except Exception as exc: - logger.exception(str(exc)) - raise Exception from exc - - # Final check - if pwr_attr.value is False: - error_string = ( - f"Something went wrong. " - f"Device {device.name()} has not powered correctly." - ) - logger.exception(error_string) - raise Exception(error_string) + self.continue_on_failure = continue_on_failure def _boot_device(self, device: DeviceProxy): """Default sequence of device booting operations""" - # Device must be in OFF state - if device.state() != DevState.OFF: - logger.warning( - "Device %s must be in OFF state. Current state is %s", - device.name(), - device.state(), - ) + if device.state() == DevState.ON: + logger.info(f"Booting {device}: Succesful: It's already ON?") + return + + logger.info(f"Booting {device}: off()") device.off() + logger.info(f"Booting {device}: initialise()") device.initialise() + logger.info(f"Booting {device}: set_translator_defaults()") device.set_translator_defaults() + logger.info(f"Booting {device}: set_defaults()") device.set_defaults() + logger.info(f"Booting {device}: on()") device.on() - logger.info( - "Device %s has been succesfully booted into %s state", - device.name(), - device.state(), - ) + logger.info(f"Booting {device}: Succesful: state={device.state()}") + + def _shutdown_device(self, device: DeviceProxy): + """Default sequence of dervice turning-off operations""" + if device.state() == DevState.OFF: + logger.info(f"Shutting down {device}: Succesful: It's already OFF?") + return + + logger.info(f"Shutting down {device}: off()") + device.off() + logger.info(f"Shutting down {device}: Succesful: state={device.state()}") - @hierarchy_device_connected() def off_to_hibernate(self): """Manage the device operations involved in the OFF -> HIBERNATE state transition. Only minimal hardware is powered.""" - # Boot (each) PSOC - for psoc_dev_name in self.children_names( - "*/psoc/*", HierarchyMatchingFilter.Regex - ): - psoc_proxy = self.child(psoc_dev_name) - self._boot_device(psoc_proxy) - logger.info("Device %s is in %s state", psoc_dev_name, psoc_proxy.state()) - # Turn on power to CCD device which is child of PSOC - assert psoc_proxy.state() == DevState.ON - psoc_proxy.socket_on("socket_1") # connected to CCD - # Boot the CCD (To be removed: must be booted from control sequence) - for ccd_dev_name in self.children_names( - "*/ccd/*", HierarchyMatchingFilter.Regex - ): - ccd_proxy = self.child(ccd_dev_name) - self._boot_device(ccd_proxy) - logger.info("Device %s is in %s state", ccd_dev_name, ccd_proxy.state()) - # Check if CCD is correctly powered on - try: - self.ensure_power(self.child(ccd_dev_name)) - except Exception: - # Do not raise on Development phase - pass - # Boot the PCON (To be removed: must be booted from control sequence) - for pcon_dev_name in self.children_names( - "*/pcon/*", HierarchyMatchingFilter.Regex - ): - pcon_proxy = self.child(pcon_dev_name) - self._boot_device(pcon_proxy) - logger.info("Device %s is in %s state", pcon_dev_name, pcon_proxy.state()) - - @hierarchy_device_connected() + + @suppress_exceptions(self.continue_on_failure) + @run_if_device_on_in_station_state(StationState.HIBERNATE) + def boot_to_hibernate(device: DeviceProxy): + # TODO(JDM): wait for CCDTR to be powered on before booting it? + self._boot_device(device) + + # PSOC: Power on CCD + if device_class_matches(device, "PSOC"): + logger.info(f"Powering on {device}: CCD") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: CCD") + + # CCD: Power on clock + if device_class_matches(device, "CCD"): + logger.info(f"Powering on {device}: Clock") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: Clock") + + self._hierarchy.walk_down(boot_to_hibernate, -1) + + # Return the suppressed exceptions + return boot_to_hibernate.exceptions + def hibernate_to_standby(self): """Manage the device operations involved in the HIBERNATE -> STANDBY state transition. Powers hardware except antennas and firmware. """ - # Retrieve PCON Device(s) - for pcon_dev_name in self.children_names( - "*/pcon/*", HierarchyMatchingFilter.Regex - ): - pcon_proxy = self.child(pcon_dev_name) - # PCON must be in ON state since the previous transition (OFF -> HIBERNATE) - assert pcon_proxy.state() == DevState.ON - # Turn ON power (48V) for each APS - # Turn ON APSPU - for apspu_dev_name in self.children_names( - "*/apspu/*", HierarchyMatchingFilter.Regex - ): - apspu_proxy = self.child(apspu_dev_name) - self._boot_device(apspu_proxy) - logger.info( - "Device %s is in %s state", - apspu_dev_name, - apspu_proxy.state(), - ) - # Turn ON children of APSPU (APSCT, UNB2, RCUs) - apspu_children = self.children(-1)[pcon_dev_name]["children"][ - apspu_dev_name - ]["children"] - for name, device in apspu_children.items(): - # boot all children - self._boot_device(device["proxy"]) - logger.info( - "Device %s is in %s state", - name, - device["proxy"].state(), - ) - # Check if APSCT is correctly powered on - if "apsct" in name.casefold(): - try: - self.ensure_power(device["proxy"]) - except Exception: - # Do not raise on Development phase - pass - # Check if UNB2 is correctly powered on - elif "unb2" in name.casefold(): - try: - self.ensure_power(device["proxy"]) - except Exception: - # Do not raise on Development phase - pass - # Retrieve PSOC Device(s) - for psoc_dev_name in self.children_names( - "*/psoc/*", HierarchyMatchingFilter.Regex - ): - psoc_proxy = self.child(psoc_dev_name) - # Turn ON power to each SDP Firmware - assert psoc_proxy.state() == DevState.ON - psoc_proxy.socket_on("socket_2") # connected to SDP - for firmware_dev_name in self.children_names( - "*/sdpfirmware/*", HierarchyMatchingFilter.Regex - ): - sdpfirmware_proxy = self.child(firmware_dev_name) - self._boot_device(sdpfirmware_proxy) - # Flash factory image - sdpfirmware_proxy.use_factory_image() - - @hierarchy_device_connected() + + @suppress_exceptions(self.continue_on_failure) + @run_if_device_on_in_station_state(StationState.STANDBY) + def boot_to_standby(device: DeviceProxy): + self._boot_device(device) + + # UNB2: Power on the Uniboards + if device_class_matches(device, "UNB2"): + logger.info(f"Powering on {device}: Uniboards") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: Uniboards") + + self._hierarchy.walk_down(boot_to_standby, -1) + + # Return the suppressed exceptions + return boot_to_standby.exceptions + def standby_to_hibernate(self): """Manage the device operations involved in the STANDBY -> HIBERNATE state transition.""" - # PSOC: Turn OFF power to each SDP Firmware - for firmware_dev_name in self.children_names( - "*/sdpfirmware/*", HierarchyMatchingFilter.Regex - ): - # TODO(Stefano): implement a better parent search when L2SS-1151 will be completed - firmware_ph = PowerHierarchy() - firmware_ph.init(firmware_dev_name) - sdpfirmware_proxy = self.child(firmware_dev_name) - sdpfirmware_proxy.off() - logger.info( - "Device %s in %s state", firmware_dev_name, sdpfirmware_proxy.state() - ) - if firmware_ph.parent_state(firmware_ph.parent()) == DevState.ON: - _psoc_proxy = create_device_proxy(firmware_ph.parent()) - _psoc_proxy.socket_off("socket_2") # connected to SDP Firmware - # PCON: Turn OFF power to each APS and RCUs - for apspu_dev_name in self.children_names( - "*/apspu/*", HierarchyMatchingFilter.Regex - ): - apspu_ph = PowerHierarchy() - apspu_ph.init(apspu_dev_name) - for name, device in apspu_ph.children().items(): - device["proxy"].off() - logger.info("Device %s in %s state", name, device["proxy"].state()) - self.child(apspu_dev_name).off() - - @hierarchy_device_connected() + + @suppress_exceptions(self.continue_on_failure) + @run_if_device_on_in_station_state(StationState.STANDBY) + def power_off_from_standby(device: DeviceProxy): + self._shutdown_device(device) + + # UNB2: Power off the Uniboards + if device_class_matches(device, "UNB2"): + logger.info(f"Powering off {device}: Uniboards") + device.power_hardware_off() + logger.info(f"Powering off {device}: Succesful: Uniboards") + + self._hierarchy.walk_up(power_off_from_standby, -1) + + # Return the suppressed exceptions + return power_off_from_standby.exceptions + def standby_to_on(self): """Manage the device operations involved in the STANDBY -> ON state transition. Powers power-hungry devices (SDP, antennas). """ - # Initialises SDP and powers RCUs. - for sdp_dev_name in self.children_names( - "*/sdp/*", HierarchyMatchingFilter.Regex - ): - sdp_proxy = self.child(sdp_dev_name) - self._boot_device(sdp_proxy) - logger.info("Device %s is in %s state", sdp_dev_name, sdp_proxy.state()) - # Power Antennas ON - for antennafield_dev_name in self.children_names( - "*/antennafield/*", HierarchyMatchingFilter.Regex - ): - antennafield_proxy = self.child(antennafield_dev_name) - antennafield_proxy.set_timeout_millis(10000) - self._boot_device(antennafield_proxy) - logger.info( - "Device %s is in %s state", - antennafield_dev_name, - antennafield_proxy.state(), - ) - - @hierarchy_device_connected() + + # first, power on additional (power-hungry) hardware + @suppress_exceptions(self.continue_on_failure) + def power_on(device: DeviceProxy): + # APSCT: Select 200 MHz clock + if device_class_matches(device, "APSCT"): + logger.info(f"Powering on {device}: 200MHz clock") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: 200MHz clock") + + # RECV: Power on RCUs + if device_class_matches(device, ["RECVH", "RECVL"]): + logger.info(f"Powering on {device}: RCUs") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: RCUs") + + # SDPFirmware: Flash user image + if device_class_matches(device, "SDPFirmware"): + logger.info(f"Powering on {device}: User image") + device.power_hardware_on() + logger.info(f"Powering on {device}: Succesful: User image") + + self._hierarchy.walk_down(power_on, -1) + + # now transition to on + @suppress_exceptions(self.continue_on_failure) + @run_if_device_on_in_station_state(StationState.ON) + def boot_to_on(device: DeviceProxy): + self._boot_device(device) + + self._hierarchy.walk_down(boot_to_on, -1) + + # power on antennas (now that AntennaField is booted) + @suppress_exceptions(self.continue_on_failure) + def power_antennas_on(device: DeviceProxy): + # AntennaField: Power on used antennas + if device_class_matches(device, "AntennaField"): + logger.info(f"Powering on {device}: Antennas") + device.power_hardware_on() + # TODO(JDM): Report which antennas + logger.info(f"Powering on {device}: Succesful: Antennas") + + self._hierarchy.walk_down(power_antennas_on, -1) + + # Return the suppressed exceptions + return ( + power_on.exceptions + boot_to_on.exceptions + power_antennas_on.exceptions + ) + def on_to_standby(self): """Manage the device operations involved in the ON -> STANDBY state transition.""" - # Antennafield: Power OFF Antennas - for antennafield_dev_name in self.children_names( - "*/antennafield/*", HierarchyMatchingFilter.Regex - ): - antennafield_proxy = self.child(antennafield_dev_name) - antennafield_proxy.off() - # Power OFF SDP - for sdp_dev_name in self.children_names( - "*/sdp/*", HierarchyMatchingFilter.Regex - ): - sdp_proxy = self.child(sdp_dev_name) - sdp_proxy.off() + + # turn off software devices + @suppress_exceptions(self.continue_on_failure) + @run_if_device_on_in_station_state(StationState.ON) + def power_off_from_on(device: DeviceProxy): + # AntennaField: Power off all antennas + if device_class_matches(device, "AntennaField"): + logger.info(f"Powering off {device}: Antennas") + device.power_hardware_off() + # TODO(JDM): Report which antennas + logger.info(f"Powering off {device}: Succesful: Antennas") + + self._shutdown_device(device) + + self._hierarchy.walk_up(power_off_from_on, -1) + + # now turn off power to power-hungry hardware + @suppress_exceptions(self.continue_on_failure) + def power_off(device: DeviceProxy): + # APSCT: Turn off clock + if device_class_matches(device, "APSCT"): + logger.info(f"Powering off {device}: Clock") + device.power_hardware_off() + logger.info(f"Powering off {device}: Succesful: Clock") + + # RECV: Power off RCUs + if device_class_matches(device, ["RECVH", "RECVL"]): + logger.info(f"Powering off {device}: RCUs") + device.power_hardware_off() + logger.info(f"Powering off {device}: Succesful: RCUs") + + # SDPFirmware: Flash factory image + if device_class_matches(device, "SDPFirmware"): + logger.info(f"Powering off {device}: Factory image") + device.power_hardware_off() + logger.info(f"Powering off {device}: Succesful: Factory image") + + self._hierarchy.walk_up(power_off, -1) + + # Return the suppressed exceptions + return power_off_from_on.exceptions + power_off.exceptions diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/recv_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/recv_device.py index c1a44436c043a00cbcd3ad6d5f679907a51b4945..d7d04f454209ea09e29c6823e99293c4ec2a3019 100644 --- a/tangostationcontrol/tangostationcontrol/devices/interfaces/recv_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/recv_device.py @@ -383,8 +383,8 @@ class RECVDevice(OPCUADevice): # overloaded functions # -------- - def _prepare_hardware(self): - """Initialise the RCU hardware.""" + def _power_hardware_on(self): + """Power the RCUs.""" # Cycle RCUs self.RCU_off() @@ -392,8 +392,8 @@ class RECVDevice(OPCUADevice): self.RCU_on() self.wait_attribute("RECVTR_translator_busy_R", False, self.RCU_On_Off_timeout) - def _disable_hardware(self): - """Disable the RECV hardware.""" + def _power_hardware_off(self): + """Turns off the RCUs.""" # Save actual mask values RCU_mask = self.proxy.RCU_mask_RW diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py index 08eabc9817b35013817062731808e7a622ac4a22..1104c68406d7497177e69f4e4b44c97660aa7d38 100644 --- a/tangostationcontrol/tangostationcontrol/devices/psoc.py +++ b/tangostationcontrol/tangostationcontrol/devices/psoc.py @@ -19,6 +19,8 @@ from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) +from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES +from tangostationcontrol.devices.device_decorators import only_in_states from tangostationcontrol.devices.interfaces.snmp_device import SNMPDevice from tangostationcontrol.clients.snmp.attribute_classes import PSOC_sim @@ -128,6 +130,16 @@ class PSOC(SNMPDevice): # write the correct value attr.write_function([socket_set]) + def _power_hardware_on(self): + self.power_ccd_on() + + def _power_hardware_off(self): + self.power_ccd_off() + + # -------- + # Commands + # -------- + @command(dtype_in=str) def socket_on(self, socket_name): """Turn socket on""" @@ -159,6 +171,18 @@ class PSOC(SNMPDevice): # for whatever reason, the uptime is given in hundredts of a second return str(timedelta(seconds=uptime_attr.read_function())) + @command() + @only_in_states(DEFAULT_COMMAND_STATES) + def power_ccd_on(self): + """Turn on 230V to the CCD""" + self.socket_on("socket_1") + + @command() + @only_in_states(DEFAULT_COMMAND_STATES) + def power_ccd_off(self): + """Turn off 230V to the CCD""" + self.socket_off("socket_1") + # ---------- # Run server diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py b/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py index 9bd1aef198e8d765aae0b858df7a6a49342005c9..2d86dbc55c11c46fd7dd5da020cdc2303c52709a 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py @@ -8,19 +8,16 @@ import logging import numpy from attribute_wrapper.attribute_wrapper import AttributeWrapper -from tango import AttrWriteType, DebugIt +from tango import AttrWriteType # PyTango imports -from tango.server import device_property, attribute, command +from tango.server import device_property, attribute # Additional import from tangostationcontrol.common.constants import N_pn from tangostationcontrol.common.entrypoint import entry -from tangostationcontrol.common.lofar_logging import log_exceptions from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice -from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.types import DeviceTypes logger = logging.getLogger() @@ -100,7 +97,8 @@ class SDPFirmware(OPCUADevice): # -------- # overloaded functions # -------- - def _prepare_hardware(self): + + def _power_hardware_on(self): """Boot the SDP Firmware user image""" # FPGAs that are actually reachable and we care about wait_for = ~( @@ -115,7 +113,7 @@ class SDPFirmware(OPCUADevice): "FPGA_boot_image_R", lambda attr: ((attr == 1) | ~wait_for).all(), 60 ) - def _disable_hardware(self): + def _power_hardware_off(self): """Use the SDP Firmware factory image""" # Save actual mask values TR_fpga_mask = self.proxy.TR_fpga_mask_RW @@ -129,21 +127,6 @@ class SDPFirmware(OPCUADevice): # -------- # Commands # -------- - @command() - @only_in_states(DEFAULT_COMMAND_STATES) - @DebugIt() - @log_exceptions() - def use_user_image(self): - """Boot the SDP Firmware user image""" - self._prepare_hardware() - - @command() - @only_in_states(DEFAULT_COMMAND_STATES) - @DebugIt() - @log_exceptions() - def use_factory_image(self): - """Use the SDP Firmware factory image""" - self._disable_hardware() # ---------- diff --git a/tangostationcontrol/tangostationcontrol/devices/station_manager.py b/tangostationcontrol/tangostationcontrol/devices/station_manager.py index 0d1c8dfb8f6fb93c69c736885fe9da1a8e80955c..0ebcc01c9bbf9f76cddf9a5b10e6bed4d559d6f3 100644 --- a/tangostationcontrol/tangostationcontrol/devices/station_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/station_manager.py @@ -5,7 +5,6 @@ """ -from enum import Enum import logging # pytango imports @@ -15,7 +14,12 @@ from tango import DebugIt, DevState, DevFailed, Except # Additional import from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python +from tangostationcontrol.common.lofar_logging import exception_to_str from tangostationcontrol.common.lofar_logging import log_exceptions +from tangostationcontrol.common.states import ( + StationState, + ALLOWED_STATION_STATE_TRANSITIONS, +) from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice from tangostationcontrol.devices.interfaces.power_hierarchy import PowerHierarchy @@ -25,50 +29,60 @@ logger = logging.getLogger() __all__ = ["StationManager", "main"] -class StationState(Enum): - """Station states enumeration""" - - OFF = "OFF" - HIBERNATE = "HIBERNATE" - STANDBY = "STANDBY" - ON = "ON" - - @device_logging_to_python() class StationManager(LOFARDevice): """StationManager Device Server for LOFAR2.0""" - # Contains which transitions are allowed for a given states - ALLOWED_TRANSITIONS = { - StationState.OFF: [StationState.HIBERNATE], - StationState.HIBERNATE: [StationState.OFF, StationState.STANDBY], - StationState.STANDBY: [StationState.HIBERNATE, StationState.ON], - StationState.ON: [StationState.STANDBY], - } - # ----------------- # Device Properties # ----------------- Station_Name = device_property( - dtype="DevString", mandatory=False, default_value="DevStation" + doc="Name of the station, f.e. CS001", + dtype="DevString", + mandatory=False, + default_value="DevStation", ) Station_Number = device_property( - dtype="DevLong64", mandatory=False, default_value=999 + doc="Number of the station, f.e. CS001 has number 1", + dtype="DevLong64", + mandatory=False, + default_value=999, + ) + Suppress_State_Transition_Failures = device_property( + doc="Allow state transitions to continue even if a device fails to initialise.", + dtype="DevBoolean", + mandatory=False, + default_value=False, ) + # ---------- # Attributes # ---------- - station_name_R = attribute(dtype=str, fisallowed="is_attribute_access_allowed") - def read_station_name_R(self): + @attribute(dtype=str, fisallowed="is_attribute_access_allowed") + def station_name_R(self): return self.Station_Name - station_state_R = attribute(dtype=str, fisallowed="is_attribute_access_allowed") - - def read_station_state_R(self): + @attribute(dtype=str, fisallowed="is_attribute_access_allowed") + def station_state_R(self): return self.station_state.name + @attribute(dtype=str, fisallowed="is_attribute_access_allowed") + def last_requested_transition_R(self): + return self.last_requested_transition or "" + + @attribute(dtype=(str,), max_dim_x=1024, fisallowed="is_attribute_access_allowed") + def last_requested_transition_exceptions_R(self): + return [ + f"{device}: {exception_to_str(ex)}" + for (device, ex) in self.last_requested_transition_exceptions + ][:1024] + + @attribute(dtype=bool, fisallowed="is_attribute_access_allowed") + def last_requested_transition_ok_R(self): + return self.last_requested_transition_exceptions == [] + # -------- # overloaded functions # -------- @@ -76,6 +90,8 @@ class StationManager(LOFARDevice): def __init__(self, cl, name): self.station_state = StationState.OFF self.stationmanager_ph = None + self.last_requested_transition = None + self.last_requested_transition_exceptions = [] # Super must be called after variable assignment due to executing init_device! super().__init__(cl, name) @@ -101,11 +117,13 @@ class StationManager(LOFARDevice): """Create and initialise the PowerHierarchy to manage the power sequence""" # create a power hierarchy device instance self.stationmanager_ph = PowerHierarchy() - self.stationmanager_ph.init(self.get_name()) + self.stationmanager_ph.init( + self.get_name(), continue_on_failure=self.Suppress_State_Transition_Failures + ) def _is_transition_allowed(self, to_state) -> bool: # get allowed transitions for the current state - allowed_transitions = self.ALLOWED_TRANSITIONS[self.station_state] + allowed_transitions = ALLOWED_STATION_STATE_TRANSITIONS[self.station_state] # check if the station is already in state it wants to go to if to_state == self.station_state: @@ -128,68 +146,27 @@ class StationManager(LOFARDevice): # the requested state transition is allowed return True - def _off_to_hibernate(self): - """Manage the device operations involved in the OFF -> HIBERNATE state transition. - Only minimal hardware is powered. - """ + def _transition(self, transition_desc: str, transition_func): + """Transition to a station state using `transition_func`.""" + # StationManager device must be in ON state if self.proxy.state() != DevState.ON: raise Exception( f"Station Manager must be in ON state. Current state is {self.proxy.state()}" ) # Power Sequence OFF -> HIBERNATE - self.stationmanager_ph.off_to_hibernate() - logger.info( - "Station %s has correctly completed the OFF->HIBERNATE Power Sequence", - self.proxy.station_name_R, - ) - - def _hibernate_to_off(self): - """Manage the device operations involved in the HIBERNATE -> OFF state transition. - Transition is currently not allowed. - """ - # Not allowed in PowerSequence but it may be allowed for the other sequences - # TODO: functionality - return - - def _hibernate_to_standby(self): - """Manage the device operations involved in the HIBERNATE -> STANDBY state transition. - Called on demand. Powers hardware except antennas and firmware. - """ - # Power Sequence HIBERNATE -> STANDBY - self.stationmanager_ph.hibernate_to_standby() - logger.info( - "Station %s has correctly completed the HIBERNATE->STANDBY Power Sequence", - self.proxy.station_name_R, - ) - - def _standby_to_hibernate(self): - """Manage the device operations involved in the STANDBY -> HIBERNATE state transition.""" - # Power Sequence STANDBY -> HIBERNATE - self.stationmanager_ph.standby_to_hibernate() - logger.info( - "Station %s has correctly completed the STANDBY->HIBERNATE Power Sequence", - self.proxy.station_name_R, - ) - - def _standby_to_on(self): - """Manage the device operations involved in the STANDBY -> ON state transition. - Called on demand. Powers power-hungry devices (SDP firmware, antennas, RCUs). - """ - # Power Sequence STANDBY -> ON - self.stationmanager_ph.standby_to_on() - logger.info( - "Station %s has correctly completed the STANDBY->ON Power Sequence", - self.proxy.station_name_R, - ) + try: + self.last_requested_transition = transition_desc + self.last_requested_transition_exceptions = transition_func() + except Exception as ex: + # unsuppressed exception + self.last_requested_transition_exceptions = [(None, ex)] + raise - def _on_to_standby(self): - """Manage the device operations involved in the ON -> STANDBY state transition.""" - # Power Sequence ON -> STANDBY - self.stationmanager_ph.on_to_standby() logger.info( - "Station %s has correctly completed the STANDBY->HIBERNATE Power Sequence", + "Station %s has correctly completed the %s Power Sequence", self.proxy.station_name_R, + transition_desc, ) # -------- @@ -208,8 +185,9 @@ class StationManager(LOFARDevice): raise Exception(f"Station did not transition to {StationState.OFF.name}") # call the correct state transition function - else: - self._hibernate_to_off() + + # not implemented + pass # update the station_state variable when successful self.station_state = StationState.OFF @@ -230,9 +208,13 @@ class StationManager(LOFARDevice): # call the correct state transition function try: if self.station_state == StationState.OFF: - self._off_to_hibernate() + self._transition( + "OFF -> HIBERNATE", self.stationmanager_ph.off_to_hibernate + ) elif self.station_state == StationState.STANDBY: - self._standby_to_hibernate() + self._transition( + "STANDBY -> HIBERNATE", self.stationmanager_ph.standby_to_hibernate + ) except DevFailed as exc: error_string = f"Station {self.proxy.station_name_R} \ did not transition to {StationState.HIBERNATE.name} state. \ @@ -259,9 +241,11 @@ class StationManager(LOFARDevice): # call the correct state transition function try: if self.station_state == StationState.HIBERNATE: - self._hibernate_to_standby() + self._transition( + "HIBERNATE -> STANDBY", self.stationmanager_ph.hibernate_to_standby + ) elif self.station_state == StationState.ON: - self._on_to_standby() + self._transition("ON -> STANDBY", self.stationmanager_ph.on_to_standby) except DevFailed as exc: error_string = f"Station {self.proxy.station_name_R} \ did not transition to {StationState.STANDBY.name} state. \ @@ -285,7 +269,7 @@ class StationManager(LOFARDevice): # call the correct state transition function try: - self._standby_to_on() + self._transition("STANDBY -> ON", self.stationmanager_ph.standby_to_on) except DevFailed as exc: error_string = f"Station {self.proxy.station_name_R} \ did not transition to {StationState.ON.name} state. \ diff --git a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py index bd006b767c49f9327327194cc0f207a4f9d78a4e..62de714a909f6dad62ff9d08da951ded8d4814d9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py @@ -195,7 +195,7 @@ class TemperatureManager(LOFARDevice): for dev_name in self.Shutdown_Device_List: try: proxy = create_device_proxy(dev_name) - proxy.disable_hardware() + proxy.power_hardware_off() except Exception as e: logger.warning( f"Automatic hardware shutdown of device {dev_name} has failed: {e.args[0]}" diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py index 7e225969b620afe92e118efbdbf809065ebf81aa..44ef128b278bf0336b7d82631e4f756250a49521 100644 --- a/tangostationcontrol/tangostationcontrol/devices/unb2.py +++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py @@ -406,8 +406,13 @@ class UNB2(OPCUADevice): # overloaded functions # -------- - def _prepare_hardware(self): - """Initialise the UNB2 hardware.""" + def _read_hardware_powered_R(self): + """Read attribute which monitors the power""" + # Return True if all uniboards are powered + return all(self.read_attribute("UNB2_PWR_on_R")) + + def _power_hardware_on(self): + """Power the Uniboards.""" # Cycle UNB2s self.UNB2_off() @@ -415,8 +420,10 @@ class UNB2(OPCUADevice): self.UNB2_on() self.wait_attribute("UNB2TR_translator_busy_R", False, self.UNB2_On_Off_timeout) - def _disable_hardware(self): - """Disable the UNB2 hardware.""" + self.wait_attribute("hardware_powered_R", True, self.UNB2_On_Off_timeout) + + def _power_hardware_off(self): + """Disable the Uniboards.""" # Save actual mask values UNB2_mask = self.proxy.UNB2_mask_RW @@ -428,22 +435,6 @@ class UNB2(OPCUADevice): # Restore the mask self.UNB2_mask_RW = UNB2_mask - def _read_hardware_powered_R(self): - """Read attribute which monitors the power""" - # Return True if all uniboards are powered - for pwr_val in self.read_attribute("UNB2_PWR_on_R"): - if not pwr_val: - return False - return True - - def _power_hardware_on(self): - """Power hardware on and check the correctness of the operation""" - # TODO() power on operations - - def _power_hardware_off(self): - """Power hardware off and check the correctness of the operation""" - # TODO() power off operations - # -------- # Commands # -------- diff --git a/tangostationcontrol/test/devices/interfaces/test_hierarchy.py b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py index 1564eb1f820ae01ee2569ac953e6a6963aa08d7e..68f3ee04070f54798bcc13a25ebf92abe50bdd1b 100644 --- a/tangostationcontrol/test/devices/interfaces/test_hierarchy.py +++ b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py @@ -6,9 +6,9 @@ import logging from typing import Callable from typing import Dict from typing import List -from unittest.mock import Mock +from unittest.mock import Mock, patch -from tango import DevState +from tango import DevState, DeviceProxy from tangostationcontrol.devices.interfaces import hierarchy @@ -424,3 +424,70 @@ class TestAbstractHierarchy(device_base.DeviceTestCase): self.device_proxy_mock[ "object" ].return_value.get_property.side_effect = self.TEST_GET_PROPERTY_CALLS + + class FakeDeviceProxy: + """A stateful fake to return the right values for the right device regardless of calling order.""" + + def __init__(self, name, timeout): + self.name = name + + def dev_name(self): + return self.name + + def get_property(self, prop_name): + children = { + "1": ["1.1", "1.2"], + "2": ["2.1", "2.2"], + "2.1": ["2.1.1"], + } + return { + TestAbstractHierarchy.TEST_PROPERTY_NAME: children.get(self.name, []) + } + + @patch.object(hierarchy, "create_device_proxy", wraps=FakeDeviceProxy) + def test_walk_down(self, m_create_device_proxy): + """Test whether walking down the hierarchy tree (root -> leaves) works.""" + + test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, ["1", "2", "3"], None + ) + + def walker(device: DeviceProxy): + walk_order.append(device.dev_name()) + + # walk one level + walk_order = [] + test_hierarchy.walk_down(walker, depth=1) + self.assertListEqual(["1", "2", "3"], walk_order) + + # walk whole tree + walk_order = [] + test_hierarchy.walk_down(walker, depth=-1) + self.assertListEqual( + ["1", "1.1", "1.2", "2", "2.1", "2.1.1", "2.2", "3"], + walk_order, + ) + + @patch.object(hierarchy, "create_device_proxy", wraps=FakeDeviceProxy) + def test_walk_up(self, m_create_device_proxy): + """Test whether walking up the hierarchy (leaves -> root) tree works.""" + + test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, ["1", "2", "3"], None + ) + + def walker(device: DeviceProxy): + walk_order.append(device.dev_name()) + + # walk one level + walk_order = [] + test_hierarchy.walk_up(walker, depth=1) + self.assertListEqual(["3", "2", "1"], walk_order) + + # walk whole tree + walk_order = [] + test_hierarchy.walk_up(walker, depth=-1) + self.assertListEqual( + ["3", "2.2", "2.1.1", "2.1", "2", "1.2", "1.1", "1"], + walk_order, + ) diff --git a/tangostationcontrol/test/devices/interfaces/test_lofar_device.py b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py index 9fdcc2fb6b2830cdb17161ab160e77bbf4fd282e..1c5b21d2d9eb43d1e68134619d86064467ea95db 100644 --- a/tangostationcontrol/test/devices/interfaces/test_lofar_device.py +++ b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py @@ -55,18 +55,18 @@ class TestLofarDevice(device_base.DeviceTestCase): self.assertEqual(DevState.STANDBY, proxy.state()) proxy.on() self.assertEqual(DevState.ON, proxy.state()) - proxy.disable_hardware() + proxy.power_hardware_off() self.assertEqual(DevState.DISABLE, proxy.state()) def test_disable_state_transitions(self): with DeviceTestContext(self.test_device, process=False, timeout=10) as proxy: proxy.off() with self.assertRaises(DevFailed): - proxy.disable_hardware() - proxy.warm_boot() + proxy.power_hardware_off() + proxy.boot() proxy.Fault() with self.assertRaises(DevFailed): - proxy.disable_hardware() + proxy.power_hardware_off() def test_atomic_read_modify_write(self): """Test atomic read modify write for attribute"""