-
Notifications
You must be signed in to change notification settings - Fork 18
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me so far, seems like a nice capability. Definitely would want docs/tests (most of the docs could just be a page created from what you have in this PR).
|
||
class JoystickButton(Button): | ||
def __init__(self, joystick: Joystick, button: int) -> None: | ||
super().__init__(lambda: joystick.getRawButton(button)) |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
Co-authored-by: David Vo <[email protected]>
Co-authored-by: David Vo <[email protected]>
from typing import Any, Callable, Generator, List, Union, Optional, overload | ||
from ._impl import CommandBase, Subsystem | ||
import inspect | ||
from typing_extensions import TypeGuard |
There was a problem hiding this comment.
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
Todo: Add support for the new event loop stuff. This can look pretty similar to the existing stuff, where lambdas are avoided in preference of decorators. The Java example BooleanEvent atTargetVelocity =
new BooleanEvent(m_loop, m_controller::atSetpoint)
// debounce for more stability
.debounce(0.2);
// if we're at the target velocity, kick the ball into the shooter wheel
atTargetVelocity.ifHigh(() -> m_kicker.set(0.7)); is clunky in Python because builder syntax isn't supported and the condition must be a one-line lambda or a function (same issue that this whole pr is about). The first statement would have to be written as atTargetVelocity = BooleanEvent(m_loop, m_controller.atSetpoint).debounce(0.2) or atTargetVelocity = (
BooleanEvent(m_loop,m_controller.atSetpoint)
.debounce(0.2)
) The event could be defined with multiple decorators @event.debounce(0.2)
@event(m_loop)
def atVelocityTarget():
return m_controller.atSetpoint() or by punting all builders into the constructor (this is how the existing syntax does it and is way better for intellisense) @event(m_loop, debounce=0.2)
def atVelocityTarget():
return m_controller.atSetpoint() This event can then be used as a decorator to define a callback as @atVelocityTarget.isHigh()
def _():
m_kicker.set(0.7) or in the case of rising/falling @atTargetVelocity.rising().isHigh()
def _():
m_kicker.set(0.7) Food for thought: @atVelocityTarget
def _():
m_kicker.set(0.7)
@atTargetVelocity.rising()
def _():
m_kicker.set(0.7) Food for thought, the sequel: I think the solution might be to leave these as builders that are forced to be one lined as @event(m_loop).debounce(0.2)
def atVelocityTarget():
return m_controller.atSetpoint() |
Still todo
runsWhenDisabled
in the constructor.andThen
, etc.) andSubsystem.setDefaultCommand
? These currently must be manually wrapped inCoroutineCommand
sOverview
Add support for coroutines into commands2. These coroutines are compatible with commands. Coroutines can be converted to Commands and Commands can be converted to coroutines.
Note: "Coroutine" in this PR and impl act as coroutines for the purposes of FRC but are actually generators. This PR does not add support for any async/await. Coroutines have been implemented as generators so they can run synchronously with robot code and can be managed by the CommandScheduler loop (instead of asyncio).
Usage
Buttons/Triggers
Buttons and Triggers currently accept
Command
s and functions (Callable[[], None]
). This has been expanded to accept generators and generator functions.Buttons have been moved to a Python implementation and
Trigger
has been partially moved (the exposedTrigger
is Python that encapsulates the c++Trigger
) to facilitate this. This is because most ofTrigger
's methods have to have multiple return types to support both decorator and non-decorator forms.Generator
Warning: While this is supported, it is not recommended. Python does not allow for generators to be copied. The workaround,
tee
can potentially use infinite memory. Because of this, coroutines bound this way can only be used once. The above snippet would only work the first time the button is pressed.Generator Function
This is the recommended way to pass in generators in the non-decorator style. Using a function with no params (
Callable[[], Generator[None, None, None]
) that builds the generator. The internally usedCoroutineCommand
saves this function and rebuilds the generator each time.initialize()
is called.Buttons/Trigger as decorators
All
Callables
accepted by buttons and triggers can also be passed in by using it as a decorator. This style is how flask, fastapi, and Swift (Trailing Closure Syntax) do things.Functions
Generator Functions
All 4 example arguments as above work but only the "Ex 1" form is shown.
Simple Example in 2022 Robot Code
Complex Example in 2022 Robot Code
Composing Coroutines
In the current command framework, commands are composed using extra classes (
ParallelCommandGroup
,SequentialCommandGroup
, etc.) or using helper functions (.andThen(...)
, etc.).Coroutines aren't natively supported by these constructors (yet?) and must be wrapped in
CoroutineCommand
to enable this.But, coroutines can be composed within themselves using
yield from
.Furthermore, since coroutines are generators with no input, they can be used as iterables.
is equivalent to
for the purposes of this framework.
Very Complex Example in 2022 Robot Code (Untested)
So, to run coroutines in parallel and move on when any one command finishes:
Converting Coroutines to Commands
commandify
The
@commandify
decorator lets commands be written as coroutines and be exposed as commands currently are.The signature of MyCommand is now
(i, j) -> CoroutineCommand
Requirements can be passed into
commandify
as@commandify(requirements = [robot.climber])
Note that the access time of requirements has changed.
In the new style (above), requirements are saved when the file is parsed.
In the old style, requirements are saved when the command is constructed:
Because of this, commands with requirements created using
commandify
must be defined in a scope where the robot object has already been created (in the subsystem object for example).CoroutineCommand
The
CoroutineCommand
class's constructor can be used to convert coroutines to commands. This way, coroutine functions defined in code stay as coroutine functions can be converted to commands as needed.Converting Commands to Coroutines
All
Command
and it's subclasses are now iterable. Callingcommand_obj.__iter__()
returns an iterable that will only exhaust when the command'sisFinished
returnsTrue
.A coroutine is created with
command_obj.__iter__()
.A coroutine function is created with
command_obj.__iter__
.Using
Command
s in coroutinesManual calls
Command
's initialize, execute, and isFinished can be manually called.Yield from
Via the
__iter__
added toCommand
,Command
s can be yielded from in coroutines.