Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying initial condition of Trigger bindings #78

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions commands2/button/trigger.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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 _():
Expand All @@ -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 _():
Expand All @@ -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 _():
Expand All @@ -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`.
Expand All @@ -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 _():
Expand All @@ -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`.
Expand All @@ -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 _():
Expand All @@ -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 _():
Expand All @@ -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 _():
Expand Down
72 changes: 72 additions & 0 deletions tests/test_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down