From 687b1042d06dfe70bdf9bf5b920211bd3f996bc4 Mon Sep 17 00:00:00 2001 From: Igor Suarez-Sola Date: Thu, 5 Sep 2024 12:59:25 -0700 Subject: [PATCH] Implement dome parking / DM-45609 - Add the `park_dome` method in `MTCS`: - Ensures the dome is enabled before initiating the park sequence. - Flush events before issuing the remote dome park command. - Commands the dome to park and waits for the dome's azimuth motion state to reach `PARKED`. - Skips parking if the dome is already in the `PARKED` state. - Added the `wait_for_dome_state` method to periodically check the dome's `azMotion` event until it reaches the `PARKED` state with `inPosition=True`, or until the `park_dome_timeout` (initially set to 10 minutes/600 seconds) is reached. - Unit test: - Added a new unit test `test_park_dome` - Updated the `MTCSAsyncMock` class to simulate dome parking behavior by mocking the `evt_azMotion` event to simulate the dome reaching the `PARKED` state with `inPosition=True`. --- .../ts/observatory/control/maintel/mtcs.py | 95 ++++++++++++++++++- .../control/mock/mtcs_async_mock.py | 26 +++++ tests/maintel/test_mtcs.py | 18 ++++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/python/lsst/ts/observatory/control/maintel/mtcs.py b/python/lsst/ts/observatory/control/maintel/mtcs.py index 90b71733..1d44c4b1 100644 --- a/python/lsst/ts/observatory/control/maintel/mtcs.py +++ b/python/lsst/ts/observatory/control/maintel/mtcs.py @@ -31,8 +31,8 @@ import numpy as np from astropy.coordinates import Angle from lsst.ts import salobj, utils -from lsst.ts.idl.enums import MTM1M3, MTM2, MTPtg, MTRotator from lsst.ts.utils import angle_diff +from lsst.ts.xml.enums import MTM1M3, MTM2, MTDome, MTPtg, MTRotator try: from lsst.ts.xml.tables.m1m3 import FATable @@ -141,6 +141,10 @@ def __init__( self.dome_flat_el = self.dome_park_el self.dome_slew_tolerance = Angle(1.5 * u.deg) + # TODO (DM-45609): This is an initial guess for the time it takes the + # dome to park. It might need updating. + self.park_dome_timeout = 600 + self._dome_az_in_position: typing.Union[None, asyncio.Event] = None self._dome_el_in_positio: typing.Union[None, asyncio.Event] = None @@ -508,9 +512,9 @@ async def wait_for_dome_inposition( Parameters ---------- timeout: `float` - How to to wait for mount to be in position (in seconds). + How to wait for mount to be in position (in seconds). wait_settle: `bool` - After receiving the in position command add an addional settle + After receiving the in position command, add an additional settle wait? (default: True) Returns @@ -579,6 +583,91 @@ async def dome_el_in_position(self) -> str: await self._dome_el_in_position.wait() return "Dome elevation in position." + async def wait_for_dome_state( + self, + expected_states: set[MTDome.MotionState], + bad_states: set[MTDome.MotionState], + timeout: float, + check_in_position: bool = False, + ) -> None: + """Wait for a specific dome state. + + Parameters + ---------- + expected_states : set[MTDome.MotionState] + Valid states to transition into while un-parking. + bad_states : set[MTDome.MotionState] + States that are not allowed while un-parking and should raise an + error. + timeout : float + Maximum time to wait for the correct state. + + Raises + ------ + RuntimeError + If a bad state is encountered or the expected state is not reached + in time. + """ + + def dome_ready(az_motion: salobj.type_hints.BaseMsgType) -> bool: + return ( + az_motion.state in expected_states and az_motion.inPosition + if check_in_position + else az_motion.state in expected_states + ) + + az_motion = await self.rem.mtdome.evt_azMotion.aget(timeout=timeout) + + while not dome_ready(az_motion): + az_motion = await self.rem.mtdome.evt_azMotion.next( + Flush=False, timeout=timeout + ) + + if az_motion.state in bad_states: + raise RuntimeError( + f"Dome transitioned to an invalid state: {MTDome.MotionState(az_motion.state).name}" + ) + + self.log.debug( + f"Dome state: {MTDome.MotionState(az_motion.state).name}, inPosition: {az_motion.inPosition}" + ) + + async def park_dome(self) -> None: + """Park the dome by moving it to the park azimuth.""" + self.log.info("Parking dome") + + await self.assert_all_enabled( + message="All components need to be enabled for parking the Dome." + ) + + # check first if Dome is already in PARKED state + az_motion = await self.rem.mtdome.evt_azMotion.aget(timeout=self.fast_timeout) + + if az_motion.state == MTDome.MotionState.PARKED: + self.log.info("Dome is already in PARKED state.") + else: + self.rem.mtdome.evt_azMotion.flush() + + await self.rem.mtdome.cmd_park.start(timeout=self.long_timeout) + + # Define expected and bad states for parking + expected_states = {MTDome.MotionState.PARKED} + bad_states = { + MTDome.MotionState.ERROR, + MTDome.MotionState.UNDETERMINED, + MTDome.MotionState.DISABLED, + MTDome.MotionState.DISABLING, + } + + self.log.info("Waiting for dome to reach the PARKED state.") + + await self.wait_for_dome_state( + expected_states, + bad_states, + timeout=self.park_dome_timeout, + check_in_position=True, + ) + def set_azel_slew_checks(self, wait_dome: bool) -> typing.Any: """Handle azEl slew to wait or not for the dome. diff --git a/python/lsst/ts/observatory/control/mock/mtcs_async_mock.py b/python/lsst/ts/observatory/control/mock/mtcs_async_mock.py index f9655dea..4c3f7dc4 100644 --- a/python/lsst/ts/observatory/control/mock/mtcs_async_mock.py +++ b/python/lsst/ts/observatory/control/mock/mtcs_async_mock.py @@ -29,6 +29,7 @@ from lsst.ts.idl.enums import MTM1M3 from lsst.ts.observatory.control.maintel.mtcs import MTCS, MTCSUsages from lsst.ts.observatory.control.mock import RemoteGroupAsyncMock +from lsst.ts.xml.enums import MTDome class MTCSAsyncMock(RemoteGroupAsyncMock): @@ -119,6 +120,11 @@ async def setup_types(self) -> None: positionCommanded=0.0, ) + # MTDome Motion PARKED state. State is set by the mocked park cmd. + self._mtdome_evt_azMotion_state = types.SimpleNamespace( + state=MTDome.MotionState.UNDETERMINED, inPosition=False + ) + # MTM1M3 data self._mtm1m3_evt_detailed_state = types.SimpleNamespace( detailedState=idl.enums.MTM1M3.DetailedState.PARKED @@ -229,6 +235,9 @@ async def setup_mtdome(self) -> None: mtdome_mocks = { "tel_azimuth.next.side_effect": self.mtdome_tel_azimuth_next, "tel_lightWindScreen.next.side_effect": self.mtdome_tel_light_wind_screen_next, + "cmd_park.start.side_effect": self.mtdome_cmd_park, + "evt_azMotion.aget.side_effect": self.mtdome_evt_az_motion_state_next, + "evt_azMotion.next.side_effect": self.mtdome_evt_az_motion_state_next, } self.mtcs.rem.mtdome.configure_mock(**mtdome_mocks) @@ -461,6 +470,23 @@ async def mtdome_tel_light_wind_screen_next( ) -> types.SimpleNamespace: return self._mtdome_tel_light_wind_screen + async def mtdome_cmd_park(self, timeout: float) -> None: + asyncio.create_task(self._mtdome_park()) + + async def _mtdome_park(self) -> None: + # Mock implementation of cmd_park + await asyncio.sleep(self.heartbeat_time) + self.log.info("Dome park command executed") + self._mtdome_evt_azMotion_state = types.SimpleNamespace( + state=MTDome.MotionState.PARKED, inPosition=True + ) + + async def mtdome_evt_az_motion_state_next( + self, *args: typing.Any, **kwargs: typing.Any + ) -> types.SimpleNamespace: + await asyncio.sleep(self.heartbeat_time * 3) + return self._mtdome_evt_azMotion_state + async def mtm1m3_evt_detailed_state( self, *args: typing.Any, **kwargs: typing.Any ) -> types.SimpleNamespace: diff --git a/tests/maintel/test_mtcs.py b/tests/maintel/test_mtcs.py index 704f2007..0c3ed42f 100644 --- a/tests/maintel/test_mtcs.py +++ b/tests/maintel/test_mtcs.py @@ -32,6 +32,7 @@ from lsst.ts.idl.enums import MTM1M3, MTM2 from lsst.ts.observatory.control.mock.mtcs_async_mock import MTCSAsyncMock from lsst.ts.observatory.control.utils import RotType +from lsst.ts.xml.enums import MTDome class TestMTCS(MTCSAsyncMock): @@ -764,6 +765,23 @@ async def test_offset_xy_absorb(self) -> None: num=0, ) + async def test_park_dome(self) -> None: + await self.mtcs.enable() + await self.mtcs.assert_all_enabled() + + # Call the park_dome method + await self.mtcs.park_dome() + + az_motion = await self.mtcs.rem.mtdome.evt_azMotion.aget( + timeout=self.mtcs.park_dome_timeout + ) + + # Check the state of the azMotion event + assert ( + az_motion.state == MTDome.MotionState.PARKED + ), "Dome did not reach the PARKED state." + assert az_motion.inPosition, "Dome is not in position." + async def test_slew_dome_to(self) -> None: az = 90.0