diff --git a/src/hal/actor/commands/auto_pilot.py b/src/hal/actor/commands/auto_pilot.py index 115d18f..765ef21 100644 --- a/src/hal/actor/commands/auto_pilot.py +++ b/src/hal/actor/commands/auto_pilot.py @@ -14,6 +14,7 @@ import click +from hal.macros.auto_pilot import AutoPilotMacro from hal.macros.expose import ExposeMacro from . import hal_command_parser @@ -65,6 +66,18 @@ default=None, help="Preload the next design this many seconds before the exposure completes.", ) +@click.option( + "--add-hartmann", + is_flag=True, + default=False, + help="Take a Hartmann during the next goto-field (will not repeat Hartmanns).", +) +@click.option( + "--remove-hartmann", + is_flag=True, + default=False, + help="Removes a previously scheduled Hartmann.", +) async def auto_pilot( command: HALCommandType, stop: bool = False, @@ -74,6 +87,8 @@ async def auto_pilot( resume: bool = False, count: int = 1, preload_ahead: float | None = None, + add_hartmann: bool = False, + remove_hartmann: bool = False, ): """Starts the auto-pilot mode.""" @@ -83,12 +98,20 @@ async def auto_pilot( assert isinstance(expose_macro, ExposeMacro) macro = command.actor.helpers.macros["auto_pilot"] + assert isinstance(macro, AutoPilotMacro) if (stop or modify or pause or resume) and not macro.running: return command.fail( "I'm afraid I cannot do that Dave. The auto pilot mode is not running." ) + if macro.running and (add_hartmann or remove_hartmann): + macro.hartmann = False if remove_hartmann else True + if macro.hartmann: + return command.finish("Scheduled a Hartmann for the next goto-field.") + else: + return command.finish("Removed any previously scheduled Hartmanns.") + if pause and resume: return command.fail("--pause and --resume are incompatible Dave.") @@ -131,6 +154,8 @@ async def auto_pilot( ) macro.reset(command, count=count, preload_ahead_time=preload_ahead) + if add_hartmann: + macro.hartmann = True result: bool = True while True: diff --git a/src/hal/etc/schema.json b/src/hal/etc/schema.json index c0d9e60..ffc5b0b 100644 --- a/src/hal/etc/schema.json +++ b/src/hal/etc/schema.json @@ -79,6 +79,7 @@ }, "expose_is_paused": { "type": "boolean" }, "auto_pilot_message": { "type": "string" }, + "auto_pilot_hartmann": { "type": "boolean" }, "additionalProperties": false } } diff --git a/src/hal/helpers/jaeger.py b/src/hal/helpers/jaeger.py index b1d275f..39c6354 100644 --- a/src/hal/helpers/jaeger.py +++ b/src/hal/helpers/jaeger.py @@ -117,7 +117,7 @@ def get_goto_field_stages(self): else: stages = goto_auto_mode_stages["new_field_stages"][observatory] - return stages + return list(stages) class JaegerHelper(HALHelper): diff --git a/src/hal/macros/auto_pilot.py b/src/hal/macros/auto_pilot.py index 127e83b..400b959 100644 --- a/src/hal/macros/auto_pilot.py +++ b/src/hal/macros/auto_pilot.py @@ -92,8 +92,24 @@ def __init__(self): self.system_state = SystemState(self) + self._hartmann: bool = False self._preload_task: asyncio.Task | None = None + @property + def hartmann(self): + """Returns whether a Hartmann is scheduled.""" + + return self._hartmann + + @hartmann.setter + def hartmann(self, value: bool): + """Sets whether a Hartmann is to be scheduled.""" + + self._hartmann = value + + if self.command: + self.command.debug(auto_pilot_hartmann=value) + async def prepare(self): """Prepares the auto pilot.""" @@ -184,9 +200,18 @@ async def goto_field(self): self._auto_pilot_message("Skipping goto-field") return + if self.hartmann and "hartmann" not in stages: + stages.append("hartmann") + self._auto_pilot_message("Running goto-field") self.helpers.macros["goto_field"].reset(self.command, stages) - if not await self.helpers.macros["goto_field"].run(): + + result = await self.helpers.macros["goto_field"].run() + + if self.hartmann and "hartmann" in stages: + self.hartmann = False + + if not result: raise MacroError("Goto-field failed during auto pilot mode.") async def expose(self): diff --git a/tests/actor/test_command_auto_pilot.py b/tests/actor/test_command_auto_pilot.py new file mode 100644 index 0000000..d998478 --- /dev/null +++ b/tests/actor/test_command_auto_pilot.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# @Author: José Sánchez-Gallego (gallegoj@uw.edu) +# @Date: 2024-05-30 +# @Filename: test_command_auto_pilot.py +# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from hal.helpers.jaeger import Configuration +from hal.macros.auto_pilot import AutoPilotMacro + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from hal.actor import HALActor + + +def _auto_pilot_run_once(macro: AutoPilotMacro): + async def run_once(): + result = await AutoPilotMacro.run(macro) + macro.cancelled = True + return result + + return run_once + + +@pytest.fixture() +def mock_auto_pilot( + actor: HALActor, + mocker: MockerFixture, + monkeypatch: pytest.MonkeyPatch, +): + macro = actor.helpers.macros["auto_pilot"] + assert isinstance(macro, AutoPilotMacro) + + mocker.patch.object(actor.helpers.apogee, "get_exposure_state", return_value="idle") + mocker.patch.object(actor.helpers.boss, "get_exposure_state", return_value="idle") + monkeypatch.setattr(macro.system_state, "exposure_time_remaining", 0.0) + + monkeypatch.setattr( + actor.helpers.jaeger, + "configuration", + Configuration( + actor=actor, + design_id=1, + configuration_id=1, + loaded=True, + ), + ) + + goto_field_macro = actor.helpers.macros["goto_field"] + mocker.patch.object(goto_field_macro, "run") + + mocker.patch.object(actor.helpers.cherno, "is_guiding", return_value=True) + mocker.patch.object(actor.helpers.cherno, "guiding_at_rms", return_value=True) + + expose_macro = actor.helpers.macros["expose"] + mocker.patch.object(expose_macro, "run") + + mocker.patch.object(macro, "run", side_effect=_auto_pilot_run_once(macro)) + + +async def test_command_auto_pilot(actor: HALActor, mock_auto_pilot): + cmd = await actor.invoke_mock_command("auto-pilot") + await cmd + + assert cmd.status.did_succeed