From c65a905563f8b0ddb7633093eafffecd8c0ba255 Mon Sep 17 00:00:00 2001 From: Joseph Eng Date: Thu, 28 Nov 2024 10:01:01 -0800 Subject: [PATCH] Allow specifying initial condition of Trigger bindings --- commands2/button/trigger.py | 85 +++++++++++++++++++++++++++++++------ tests/test_trigger.py | 72 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 14 deletions(-) diff --git a/commands2/button/trigger.py b/commands2/button/trigger.py index 1612706..adbde47 100644 --- a/commands2/button/trigger.py +++ b/commands2/button/trigger.py @@ -1,4 +1,5 @@ # validated: 2024-04-02 DS 0b1345946950 button/Trigger.java +from enum import Enum from types import SimpleNamespace from typing import Callable, overload @@ -11,6 +12,37 @@ from ..util import format_args_kwargs +class InitialState(Enum): + """ + Enum specifying the initial state to use for a binding. This impacts whether or not the binding will be triggered immediately. + """ + + kFalse = 0 + """ + Indicates the binding should use false as the initial value. This causes a rising edge at the + start if and only if the condition starts true. + """ + + kTrue = 1 + """ + Indicates the binding should use true as the initial value. This causes a falling edge at the + start if and only if the condition starts false. + """ + + kCondition = 2 + """ + Indicates the binding should use the trigger's condition as the initial value. This never causes an edge at the + start. + """ + + kNegCondition = 3 + """ + Indicates the binding should use the negated trigger's condition as the initial value. This always causes an edge + at the start. Rising or falling depends on if the condition starts true or false, + respectively. + """ + + class Trigger: """ This class provides an easy way to link commands to conditions. @@ -84,15 +116,34 @@ def init_condition(condition: Callable[[], bool]): """ ) - def onTrue(self, command: Command) -> Self: + def _get_initial_state(self, initial_state: InitialState) -> bool: + """ + Gets the initial state for a binding based on an initial state policy. + + :param initialState: Initial state policy. + :returns: The initial state to use. + """ + # match-case statement is Python 3.10+ + if initial_state is InitialState.kFalse: + return False + if initial_state is InitialState.kTrue: + return True + if initial_state is InitialState.kCondition: + return self._condition() + if initial_state is InitialState.kNegCondition: + return not self._condition() + return False + + def onTrue(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Starts the given command whenever the condition changes from `False` to `True`. :param command: the command to start + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -103,15 +154,16 @@ def _(): return self - def onFalse(self, command: Command) -> Self: + def onFalse(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Starts the given command whenever the condition changes from `True` to `False`. :param command: the command to start + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -122,15 +174,16 @@ def _(): return self - def onChange(self, command: Command) -> Self: + def onChange(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Starts the command when the condition changes. :param command: the command t start + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -143,7 +196,7 @@ def _(): return self - def whileTrue(self, command: Command) -> Self: + def whileTrue(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Starts the given command when the condition changes to `True` and cancels it when the condition changes to `False`. @@ -152,10 +205,11 @@ def whileTrue(self, command: Command) -> Self: should restart, see :class:`commands2.RepeatCommand`. :param command: the command to start + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -168,7 +222,7 @@ def _(): return self - def whileFalse(self, command: Command) -> Self: + def whileFalse(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Starts the given command when the condition changes to `False` and cancels it when the condition changes to `True`. @@ -177,10 +231,11 @@ def whileFalse(self, command: Command) -> Self: should restart, see :class:`commands2.RepeatCommand`. :param command: the command to start + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -193,15 +248,16 @@ def _(): return self - def toggleOnTrue(self, command: Command) -> Self: + def toggleOnTrue(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Toggles a command when the condition changes from `False` to `True`. :param command: the command to toggle + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): @@ -215,15 +271,16 @@ def _(): return self - def toggleOnFalse(self, command: Command) -> Self: + def toggleOnFalse(self, command: Command, initial_state: InitialState = InitialState.kCondition) -> Self: """ Toggles a command when the condition changes from `True` to `False`. :param command: the command to toggle + :param initial_state: the initial state to use :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) + state = SimpleNamespace(pressed_last=self._get_initial_state(initial_state)) @self._loop.bind def _(): diff --git a/tests/test_trigger.py b/tests/test_trigger.py index 79ef9f4..2d2dd79 100644 --- a/tests/test_trigger.py +++ b/tests/test_trigger.py @@ -8,6 +8,18 @@ from .util import * +initialStates: list[tuple[commands2.button.InitialState, bool, map[str, bool]]] = [ + (commands2.button.InitialState.kFalse, True, { "onTrue": True, "onFalse": False }), + (commands2.button.InitialState.kFalse, False, { "onTrue": False, "onFalse": False }), + (commands2.button.InitialState.kTrue, True, { "onTrue": False, "onFalse": False }), + (commands2.button.InitialState.kTrue, False, { "onTrue": False, "onFalse": True }), + (commands2.button.InitialState.kCondition, True, { "onTrue": False, "onFalse": False }), + (commands2.button.InitialState.kCondition, False, { "onTrue": False, "onFalse": False }), + (commands2.button.InitialState.kNegCondition, True, { "onTrue": True, "onFalse": False }), + (commands2.button.InitialState.kNegCondition, False, { "onTrue": False, "onFalse": True }), +] + + def test_onTrue(scheduler: commands2.CommandScheduler): finished = OOBoolean(False) command1 = commands2.WaitUntilCommand(finished) @@ -25,6 +37,21 @@ def test_onTrue(scheduler: commands2.CommandScheduler): assert not command1.isScheduled() +@pytest.mark.parametrize("initialState,pressed,results", initialStates) +def test_onTrueInitialState(scheduler: commands2.CommandScheduler, initialState: commands2.button.InitialState, pressed: bool, results: map[str, bool]): + command1 = commands2.cmd.idle() + button = InternalButton() + shouldBeScheduled = results["onTrue"] + + button.setPressed(pressed) + button.onTrue(command1, initialState) + + assert not command1.isScheduled() + + scheduler.run() + assert command1.isScheduled() + + def test_onFalse(scheduler: commands2.CommandScheduler): finished = OOBoolean(False) command1 = commands2.WaitUntilCommand(finished) @@ -42,6 +69,21 @@ def test_onFalse(scheduler: commands2.CommandScheduler): assert not command1.isScheduled() +@pytest.mark.parametrize("initialState,pressed,results", initialStates) +def test_onFalseInitialState(scheduler: commands2.CommandScheduler, initialState: commands2.button.InitialState, pressed: bool, results: map[str, bool]): + command1 = commands2.cmd.idle() + button = InternalButton() + shouldBeScheduled = results["onFalse"] + + button.setPressed(pressed) + button.onFalse(command1, initialState) + + assert not command1.isScheduled() + + scheduler.run() + assert command1.isScheduled() + + def test_onChange(scheduler: commands2.CommandScheduler): finished = OOBoolean(False) command1 = commands2.WaitUntilCommand(finished) @@ -59,6 +101,21 @@ def test_onChange(scheduler: commands2.CommandScheduler): assert not command1.isScheduled() +@pytest.mark.parametrize("initialState,pressed,results", initialStates) +def test_onChangeInitialState(scheduler: commands2.CommandScheduler, initialState: commands2.button.InitialState, pressed: bool, results: map[str, bool]): + command1 = commands2.cmd.idle() + button = InternalButton() + shouldBeScheduled = results["onTrue"] || results["onFalse"] + + button.setPressed(pressed) + button.onChange(command1, initialState) + + assert not command1.isScheduled() + + scheduler.run() + assert command1.isScheduled() + + def test_whileTrueRepeatedly(scheduler: commands2.CommandScheduler): inits = OOInteger(0) counter = OOInteger(0) @@ -161,6 +218,21 @@ def test_toggleOnTrue(scheduler: commands2.CommandScheduler): assert endCounter == 1 +@pytest.mark.parametrize("initialState,pressed,results", initialStates) +def test_toggleOnTrueInitialState(scheduler: commands2.CommandScheduler, initialState: commands2.button.InitialState, pressed: bool, results: map[str, bool]): + command1 = commands2.cmd.idle() + button = InternalButton() + shouldBeScheduled = results["onTrue"] + + button.setPressed(pressed) + button.toggleOnTrue(command1, initialState) + + assert not command1.isScheduled() + + scheduler.run() + assert command1.isScheduled() + + def test_cancelWhenActive(scheduler: commands2.CommandScheduler): startCounter = OOInteger(0) endCounter = OOInteger(0)