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

Add coroutine command support #12

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e4d360c
Add coroutine command support
TheTripleV Apr 22, 2022
8488fbc
Allow setting runs_when_disabled for coroutines
TheTripleV Apr 22, 2022
5de62e0
black
TheTripleV Apr 22, 2022
3ee269a
formatting
TheTripleV Apr 22, 2022
d285fb0
underscore c++ trigger
TheTripleV Apr 23, 2022
70f5715
fix bitwise operators for and/or/not
TheTripleV Apr 23, 2022
9122d07
add trigger constructor and debounce
TheTripleV Apr 23, 2022
7174dfb
add missing trigger api
TheTripleV Apr 23, 2022
decd650
add missing button api
TheTripleV Apr 23, 2022
1c7e4d9
remove old c++ wrapping
TheTripleV Apr 23, 2022
ed5b106
add __iter__ docstring
TheTripleV Apr 23, 2022
aafde42
better networkbutton error message
TheTripleV Apr 23, 2022
0bb3251
add docstrings
TheTripleV Apr 23, 2022
8f91b2b
change commandify implementation to function
TheTripleV Apr 23, 2022
71322a3
black
TheTripleV Apr 23, 2022
bac4080
fix test
TheTripleV Apr 24, 2022
7815111
Update commands2/button/networkbutton.py
TheTripleV Apr 24, 2022
021ce43
Update commands2/coroutinecommand.py
TheTripleV Apr 24, 2022
401aa5f
ci and nt args
TheTripleV Apr 28, 2022
04d90d7
Merge branch 'hotchocolate' of https://github.com/TheTripleV/robotpy-…
TheTripleV Apr 28, 2022
ffbeeca
better error messages
TheTripleV Apr 28, 2022
f2cae93
3.7 support
TheTripleV Apr 29, 2022
6068723
skip test
TheTripleV Apr 29, 2022
d8078e7
more 3.7
TheTripleV Apr 29, 2022
c977711
fix whileHeld instant command
TheTripleV Apr 30, 2022
4028320
black
TheTripleV Apr 30, 2022
243072c
most tests
TheTripleV May 3, 2022
748c84c
merge b3
TheTripleV Nov 23, 2022
a2da722
merge b4
TheTripleV Nov 23, 2022
cb5af47
get it testable
TheTripleV Nov 24, 2022
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
9 changes: 7 additions & 2 deletions commands2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from . import _init_impl
from .trigger import Trigger
from .coroutinecommand import CoroutineCommand, commandify

from .version import version as __version__

Expand Down Expand Up @@ -40,7 +42,7 @@
TrapezoidProfileCommandRadians,
TrapezoidProfileSubsystem,
TrapezoidProfileSubsystemRadians,
Trigger,
# Trigger,
WaitCommand,
WaitUntilCommand,
# button,
Expand Down Expand Up @@ -84,9 +86,12 @@
"TrapezoidProfileCommandRadians",
"TrapezoidProfileSubsystem",
"TrapezoidProfileSubsystemRadians",
"Trigger",
# "Trigger",
"WaitCommand",
"WaitUntilCommand",
# "button",
"requirementsDisjoint",
"commandify",
"CoroutineCommand",
"Trigger",
]
6 changes: 4 additions & 2 deletions commands2/button/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# autogenerated by 'robotpy-build create-imports commands2.button commands2._impl.button'
from .._impl.button import Button, JoystickButton, NetworkButton, POVButton
from .button import Button
from .joystickbutton import JoystickButton
from .networkbutton import NetworkButton
from .povbutton import POVButton

__all__ = ["Button", "JoystickButton", "NetworkButton", "POVButton"]
6 changes: 6 additions & 0 deletions commands2/button/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..trigger import Trigger


class Button(Trigger):
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
whenPressed = Trigger.whenActive
whenReleased = Trigger.whenInactive
8 changes: 8 additions & 0 deletions commands2/button/joystickbutton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from wpilib import Joystick

from .button import Button


class JoystickButton(Button):
def __init__(self, joystick: Joystick, button: int) -> None:
super().__init__(lambda: joystick.getRawButton(button))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what error message you get when something that isn't an integer is passed in...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It constructs fine but fails the first time .get is called:

py", line 21, in <lambda>
    super().__init__(lambda: joystick.getRawButton(button))
TypeError: getRawButton(): incompatible function arguments. The following argument types are supported:
    1. (self: wpilib.interfaces._interfaces.GenericHID, button: int) -> bool

Invoked with: <XboxController 0>, 'qwe'

which isn't an ideal error message. I'll check the type in the constructor.

32 changes: 32 additions & 0 deletions commands2/button/networkbutton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from networktables import NetworkTable, NetworkTables, NetworkTableEntry

from typing import Union, overload

from .button import Button


class NetworkButton(Button):
@overload
def __init__(self, entry: NetworkTableEntry) -> None:
...

@overload
def __init__(self, table: Union[NetworkTable, str], field: str) -> None:
...

def __init__(self, *args, **kwargs) -> None:
num_args = len(args) + len(kwargs)
if num_args == 1:
entry: NetworkTableEntry = kwargs.get("entry", args[0])
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
super().__init__(
lambda: NetworkTables.isConnected and entry.getBoolean(False)
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
)
else:
table = kwargs.get("table", args[0])
field = kwargs.get("field", args[-1])
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(table, str):
table = NetworkTables.getTable(table)

entry = table.getEntry(field)
self.__init__(entry)
8 changes: 8 additions & 0 deletions commands2/button/povbutton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from wpilib import Joystick

from .button import Button


class POVButton(Button):
def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None:
super().__init__(lambda: joystick.getPOV(povNumber) == angle)
96 changes: 96 additions & 0 deletions commands2/coroutinecommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from functools import wraps
from typing import Any, Callable, Generator, List, Union, Optional
from ._impl import CommandBase, Subsystem
import inspect
from typing_extensions import TypeGuard
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Figure out the minimum version of typing_extensions to depend on


Coroutine = Generator[None, None, None]
CoroutineFunction = Callable[[], Generator[None, None, None]]
Coroutineable = Union[Callable[[], None], CoroutineFunction]


def is_coroutine(func: Any) -> TypeGuard[Coroutine]:
return inspect.isgenerator(func)


def is_coroutine_function(func: Any) -> TypeGuard[CoroutineFunction]:
return inspect.isgeneratorfunction(func)


def is_coroutineable(func: Any) -> TypeGuard[Coroutineable]:
return is_coroutine_function(func) or callable(func)


def ensure_generator_function(func: Coroutineable) -> Callable[..., Coroutine]:
if is_coroutine_function(func):
return func

@wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
yield

return wrapper


class CoroutineCommand(CommandBase):
coroutine: Optional[Coroutine]
coroutine_function: Optional[Coroutineable]
is_finished: bool

def __init__(
self,
coroutine: Union[Coroutine, Coroutineable],
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> None:
self.coroutine = None
self.coroutine_function = None
self.runsWhenDisabled = lambda: runs_when_disabled

if is_coroutine(coroutine):
self.coroutine = coroutine
elif is_coroutine_function(coroutine):
self.coroutine_function = coroutine
else:
raise TypeError("The coroutine must be a coroutine or a coroutine function")

if requirements is not None:
self.addRequirements(requirements)

self.is_finished = False

def initialize(self) -> None:
if self.coroutine_function:
self.coroutine = ensure_generator_function(self.coroutine_function)()
elif self.coroutine and self.is_finished:
RuntimeError("Generator objects cannot be reused.")
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved

self.is_finished = False

def execute(self):
try:
if not self.is_finished:
if not self.coroutine:
raise TypeError("This command was not properly initialized")
next(self.coroutine)
except StopIteration:
self.is_finished = True

def isFinished(self):
return self.is_finished


class commandify:
def __init__(self, requirements: Optional[List[Subsystem]] = None) -> None:
self.requirements = requirements

def __call__(self, func: Coroutineable):
@wraps(func)
def arg_accepter(*args, **kwargs) -> CoroutineCommand:
return CoroutineCommand(
lambda: ensure_generator_function(func)(*args, **kwargs),
self.requirements,
)

return arg_accepter
7 changes: 7 additions & 0 deletions commands2/src/Command.cpp.inl
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,12 @@ cls_Command
"\n"
":returns: the command with the timeout added\n"
DECORATOR_NOTE)
.def("__iter__",
[](std::shared_ptr<Command> self) {
return py::make_iterator(CommandIterator(self), CommandIteratorSentinel());
},
py::keep_alive<0, 1>(),
"qweqwe\n"
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
)
;

18 changes: 17 additions & 1 deletion commands2/src/helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,20 @@ std::vector<frc2::Subsystem*> pyargs2SubsystemList(py::args subs) {
subsystems.emplace_back(py::cast<frc2::Subsystem*>(sub));
}
return subsystems;
}
}

CommandIterator::CommandIterator(std::shared_ptr<frc2::Command> cmd) : cmd(cmd) {}
std::shared_ptr<frc2::Command> CommandIterator::operator*() const { return cmd; }
CommandIterator& CommandIterator::operator++() {
if (!called_initialize) {
cmd->Initialize();
called_initialize = true;
return *this;
}
cmd->Execute();
return *this;
}

bool operator==(const CommandIterator& it, const CommandIteratorSentinel&) {
return it.called_initialize && it.cmd->IsFinished();
}
15 changes: 14 additions & 1 deletion commands2/src/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,17 @@ std::shared_ptr<T> convertToSharedPtrHack(T *orig) {

py::object pyo = py::cast(orig);
return py::cast<std::shared_ptr<T>>(pyo);
}
}

class CommandIterator {
public:
std::shared_ptr<frc2::Command> cmd;
bool called_initialize = false;
explicit CommandIterator(std::shared_ptr<frc2::Command> cmd);
std::shared_ptr<frc2::Command> operator*() const;
CommandIterator& operator++();
};

class CommandIteratorSentinel {};

bool operator==(const CommandIterator& it, const CommandIteratorSentinel&);
149 changes: 149 additions & 0 deletions commands2/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from typing import Callable, Optional, overload, List, Union

from ._impl import Command, Subsystem
from ._impl import Trigger as _Trigger
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved

from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine


class Trigger:
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
"""
A button that can be pressed or released.
"""

def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None:
self._trigger = _Trigger(is_active)

def __bool__(self) -> bool:
return bool(self._trigger)

def get(self) -> bool:
return bool(self)

def __call__(self) -> bool:
return bool(self)

def __and__(self, other: "Trigger") -> "Trigger":
return Trigger(lambda: self() and other())

def __or__(self, other: "Trigger") -> "Trigger":
return Trigger(lambda: self() or other())

def __not__(self) -> "Trigger":
return Trigger(lambda: not self())

@overload
def whenActive(self, command: Command, /, interruptible: bool = True) -> None:
...

@overload
def whenActive(
self,
coroutine: Union[Coroutine, Coroutineable],
/,
*,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> None:
...

@overload
def whenActive(
self,
coroutine: None,
/,
*,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> Callable[[Coroutineable], None]:
...

def whenActive(
self,
command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]],
/,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> Union[None, Callable[[Coroutineable], None]]:
TheTripleV marked this conversation as resolved.
Show resolved Hide resolved
if command_or_coroutine is None:

def wrapper(coroutine: Coroutineable) -> None:
self.whenActive(
coroutine,
interruptible=interruptible,
requirements=requirements,
runs_when_disabled=runs_when_disabled,
)

return wrapper

if isinstance(command_or_coroutine, Command):
self._trigger.whenActive(command_or_coroutine, interruptible)
return

self._trigger.whenActive(
CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled),
interruptible,
)
return

@overload
def whenInactive(self, command: Command, /, interruptible: bool = True) -> None:
...

@overload
def whenInactive(
self,
coroutine: Union[Coroutine, Coroutineable],
/,
*,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> None:
...

@overload
def whenInactive(
self,
coroutine: None,
/,
*,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> Callable[[Coroutineable], None]:
...

def whenInactive(
self,
command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]],
/,
interruptible: bool = True,
requirements: Optional[List[Subsystem]] = None,
runs_when_disabled: bool = False,
) -> Union[None, Callable[[Coroutineable], None]]:
if command_or_coroutine is None:

def wrapper(coroutine: Coroutineable) -> None:
self.whenInactive(
coroutine,
interruptible=interruptible,
requirements=requirements,
runs_when_disabled=runs_when_disabled,
)

return wrapper

if isinstance(command_or_coroutine, Command):
self._trigger.whenInactive(command_or_coroutine, interruptible)
return

self._trigger.whenInactive(
CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled),
interruptible,
)
return
Loading