-
Notifications
You must be signed in to change notification settings - Fork 300
Add commands v3 documentation pages #3339
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
Open
SamCarlberg
wants to merge
12
commits into
wpilibsuite:main
Choose a base branch
from
SamCarlberg:commandsv3
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
de18976
Add commands v3 documentation pages
SamCarlberg 737cbe9
Fix scopes ref and swap clause order
SamCarlberg 2c4a548
Update code examples
SamCarlberg 8844370
Move yield-in-loop warning and add note for Java-only
SamCarlberg 03dbcca
Update creating-commands
SamCarlberg bf68cb5
Define DSL
SamCarlberg ee0af83
Use "custom periodic functions" to define sideloads
SamCarlberg 8e20c27
Remove dangling text
SamCarlberg 3e3bc59
Update telemetry
SamCarlberg a5b6f4e
Remove extra backtick
SamCarlberg ee2ad9f
Cleanup migration code sample code
SamCarlberg cad04c5
Discourage one-shot commands for actuation
SamCarlberg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
321 changes: 321 additions & 0 deletions
321
source/docs/software/commandbased/commands-v3/creating-commands.rst
Large diffs are not rendered by default.
Oops, something went wrong.
48 changes: 48 additions & 0 deletions
48
source/docs/software/commandbased/commands-v3/how-it-works.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # How the Scheduler Works | ||
|
|
||
| The ``Scheduler`` is the heart of the commands library. It manages the lifecycle of commands, processes triggers, tracks scopes, and ensures that mechanisms are only used by one command at a time. The scheduler runs periodically, typically every 20 ms from ``robotPeriodic()``, and follows a specific sequence of phases. | ||
|
|
||
| The scheduler does not run commands on separate operating-system threads. Each command runs until it finishes or reaches a coroutine yield point. This keeps command behavior deterministic, but it also means command code must yield regularly so other commands, triggers, and periodic robot logic can run. | ||
|
|
||
| .. warning:: The commands framework is designed for single-threaded use. Commands should be scheduled and canceled from the same thread that runs the scheduler, and commands should not be run in virtual threads. Normal concurrency tools such as locks and atomics do not make coroutine commands safe to use from multiple threads. | ||
|
|
||
| ## Scheduler Phases | ||
|
|
||
| ### Phase 1: Cleanup | ||
|
|
||
| In the cleanup phase, the scheduler removes any trigger bindings or custom periodic functions that are no longer active. This happens when the :doc:`scopes` in which they were created (such as a command or an opmode) has finished or exited. | ||
|
|
||
| Cleanup is what prevents old bindings from continuing to affect the robot after the context that created them is gone. When a scoped trigger binding is removed, the command attached to that binding is canceled as well. | ||
|
|
||
| ### Phase 2: Sideloads | ||
|
|
||
| Sideloads are custom periodic functions that are registered with the scheduler but are not commands. These functions are run once every scheduler cycle. They are useful for tasks that need to happen regardless of which commands are running, such as updating telemetry or processing sensor data. | ||
|
|
||
| Because sideloads are not commands, they do not own mechanisms and should not be used as a back door for actuator control. Hardware-changing behavior belongs in commands so the requirement system can reason about conflicts. | ||
|
|
||
| ### Phase 3: Scheduling | ||
|
|
||
| The scheduling phase is where most of the decision making happens: | ||
|
|
||
| 1. **Poll Triggers**: The scheduler polls all active trigger bindings. Depending on the trigger state and the binding type (e.g., ``onTrue``, ``whileTrue``), commands may be added to the pending set or running commands may be canceled. | ||
| 2. **Schedule Default Commands**: For every mechanism that does not have a command currently requiring it, the scheduler adds its default command to the pending set. | ||
| 3. **Promote Scheduled Commands**: The scheduler looks at all commands in the pending set and decides which ones should start running. If a pending command requires a mechanism currently owned by a running command, the scheduler compares their priorities: | ||
|
|
||
| * If the pending command has **higher priority**, the running command is interrupted and the pending command starts. | ||
| * If they have the **same priority**, the pending command interrupts the running one; newly scheduled commands win ties. | ||
| * If the pending command has **lower priority**, it is discarded and does not start. | ||
|
|
||
| Commands in the pending set have been requested, but they have not necessarily started. A command can be scheduled by a trigger and still fail to run if it loses a priority conflict. This distinction is useful when reading telemetry: "scheduled" means "queued for consideration", while "mounted" means "started running". | ||
|
|
||
| ### Phase 4: Execution | ||
|
|
||
| In the final phase, the scheduler iterates through all running commands and gives each one a chance to execute. | ||
|
|
||
| 1. **Mounting**: Before a command runs, its coroutine is mounted. This sets up the execution context and unthaws the coroutine's stack and register data. | ||
| 2. **Running**: The command's logic executes until it either finishes or calls a yielding method (like ``coroutine.yield()`` or ``await()``). | ||
| 3. **Completion**: If the command finishes, it is removed from the running set and its requirements are released. | ||
| 4. **Yielding**: If the command yields, it remains in the running set and will resume from the yield point in the next scheduler cycle. | ||
|
|
||
| Commands are run in reverse order of their scheduling (from newest to oldest). This allows parent commands to resume in the same loop cycle that an awaited child command completes, minimizing latency in nested command structures. | ||
|
|
||
| If a command completes naturally, its requirements are released and a ``Completed`` event is emitted. If it is canceled because a conflicting command took over a mechanism, an ``Interrupted`` event is emitted before the ``Canceled`` event. If command code throws an exception, the scheduler emits ``CompletedWithError`` and the exception still propagates; event listeners can log the failure, but they cannot suppress it, and the robot program will crash. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # Commands v3 Programming | ||
|
|
||
| .. toctree:: | ||
| :maxdepth: 1 | ||
|
|
||
| creating-commands | ||
| how-it-works | ||
| lambda-functions | ||
| making-commands-run | ||
| mechanisms | ||
| migration-guide | ||
| scopes | ||
| state-machines | ||
| structuring-your-project | ||
| telemetry | ||
| triggers | ||
| troubleshooting | ||
|
|
||
| Command-based programming is a way of writing a program where actions can be defined and configured to execute in response to some event. We call these actions "commands" and the events "triggers". Commands may run other commands to perform more complex actions; these are called "compositions" and are a powerful tool for building sophisticated behavior from simple building blocks. Just like commands outside of compositions, commands inside of compositions can still be configured to run in response to a trigger, but can also be manually scheduled when direct control is desired. | ||
|
|
||
| Commands v3 command logic is written as ordinary Java code. If a command needs to do something repeatedly, it writes a loop. If it needs to wait for a sensor, it waits for the sensor. If it needs to run another command, it forks or awaits that command. This makes command code read much closer to the behavior you are trying to describe. | ||
|
SamCarlberg marked this conversation as resolved.
|
||
|
|
||
| Because multiple commands need to be able to run simultaneously, commands use a :term:`coroutine` to manage concurrency. Coroutines allow commands to say when they have reached a pause point in their work by calling ``Coroutine.yield()``. This pauses the command and lets the scheduler run another command until *it* reaches a pause point, and so on until every running command has had a chance to make progress. Most importantly, coroutines let us write command logic using standard Java with ``while`` loops, ``if`` statements, local variables, and helper methods, with the addition of ``coroutine.yield()`` in loops to allow other commands to run. | ||
|
|
||
| .. note:: Commands v3 relies on specific APIs in the Java language. It is only available for Java teams. Teams using C++ or Python can continue to use Commands v2. Future work may be done to bring v3 to C++ and Python using those language's specific coroutine APIs. | ||
|
|
||
| Core APIs | ||
| --------- | ||
|
|
||
| The commands library is built around three core concepts. Most robot code will use all three: | ||
|
|
||
| - **Coroutines** manage concurrency and allow commands to pause and resume. | ||
| - **Commands** define the actions to be performed. | ||
| - **Mechanisms** represent robot hardware and manage resource ownership. | ||
|
|
||
| Coroutines | ||
| ^^^^^^^^^^ | ||
|
|
||
| Coroutines are the engine of the commands framework. They allow you to write asynchronous code that looks like synchronous code. When a command is running, it has access to a ``Coroutine`` object that can pause execution, wait for time to pass, wait for a condition to become true, or run child commands. | ||
|
|
||
| The most important method on the ``Coroutine`` class is ``yield()``. This method pauses the current command and allows the scheduler to run other commands. When the scheduler returns to the paused command, it will resume from where it left off. | ||
|
|
||
| Other useful methods on the ``Coroutine`` class include: | ||
|
|
||
| - ``wait(Time duration)``: Pauses the command for a specific amount of time. | ||
| - ``waitUntil(BooleanSupplier condition)``: Pauses the command until a condition is met. | ||
| - ``await(Command command)``: Starts another command and pauses until it completes. | ||
| - ``park()``: Pauses the command indefinitely until it is canceled. | ||
|
|
||
| Coroutines are cooperative, not preemptive. A command only gives other commands time to run when it calls a yielding method such as ``yield()``, ``wait()``, ``waitUntil()``, ``await()``, or ``park()``. A long calculation, blocking I/O operation, or infinite loop without a yield will stall the scheduler just as surely as it would stall any other periodic robot code. | ||
|
|
||
| Commands | ||
| ^^^^^^^^ | ||
|
|
||
| A command is a named piece of robot behavior that can be scheduled now or configured to run later in response to a trigger. Most commands control one or more mechanisms, but a command may also require no hardware at all. For example, resetting odometry, setting a flag, printing a diagnostic message, or coordinating other commands can all be useful no-requirement commands. | ||
|
|
||
| All commands have three required attributes: | ||
|
|
||
| 1. **Requirements**. Commands must declare what mechanisms they control in order to avoid conflicting hardware requests. | ||
| 2. **Logic**. Commands must *do* something. | ||
| 3. **Name**. Names appear in telemetry and are crucial for debugging. | ||
|
|
||
| The recommended way to create commands is with the staged builders on ``Mechanism`` and ``Command``. The builders force each command to declare its requirements, provide logic, and end with a name, which catches incomplete command definitions at compile time instead of leaving unnamed or requirement-free commands hidden in a robot program. | ||
|
|
||
| Mechanisms | ||
| ^^^^^^^^^^ | ||
|
|
||
| A mechanism is a piece of robot hardware that can only be used by one command at a time. Examples include a drivetrain, an arm, an intake, an LED strip, or a vision processor whose active pipeline should not be changed by two commands at once. Mechanism classes are responsible for owning their hardware objects and providing command factory methods for the actions that are safe to perform on that hardware. | ||
|
|
||
| .. note:: Because the Java programming language does not have a concept of coroutines, the ``Coroutine`` class used in the commands library is a custom type created specifically for the library. It can only be used with commands and the command scheduler; it is *not* general-purpose. | ||
|
SamCarlberg marked this conversation as resolved.
|
||
|
|
||
| Commands prevent conflicting hardware requests from being made by using a requirements system. Every command requires some number of mechanisms, and only one running command may require a particular mechanism at a time. For example, if a running command requires an ``Arm`` mechanism, then no other commands may use the arm at the same time. If another command starts that needs the arm, then the existing command will be canceled to allow the new command to run. | ||
|
|
||
| We recommend users define commands using the builders provided by the ``Command`` and ``Mechanism`` classes. The builders keep command code dense, which typically helps readability, and allow user code to hide direct hardware access from other classes, which prevents another class from bypassing requirements and sending unsafe actuator commands directly. Users who prefer object-oriented programming may implement the ``Command`` interface directly, but such class-based commands need to handle requirements, names, priorities, and cancellation behavior with care. | ||
|
|
||
| ```java | ||
| public class Arm implements Mechanism { | ||
| // All hardware is private. | ||
| // Anything that wants to move the arm should be done through commands. | ||
| private final MotorController motor = ...; | ||
|
|
||
| /** | ||
| * Creates a command that moves the arm up. | ||
| */ | ||
| public Command up() { | ||
| // .run() will automatically require the arm mechanism | ||
| return run(coroutine -> { | ||
| // ... logic to move the arm up to a predetermined angle ... | ||
| }).named("Arm Up"); | ||
| } | ||
| } | ||
| ``` | ||
96 changes: 96 additions & 0 deletions
96
source/docs/software/commandbased/commands-v3/lambda-functions.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # Using Lambda Functions | ||
|
SamCarlberg marked this conversation as resolved.
|
||
|
|
||
| Lambda functions are a way of passing code to a function for *that* function to execute when it needs it. Java allows any object that could be an interface with a single method (a so-called "functional interface") to be written instead using a lambda function to improve readability and performance. | ||
|
|
||
| Commands v3 uses lambdas heavily because command builders need to be given behavior. A command factory does not usually run the command immediately; it packages up the lambda so the scheduler can run it later, when the command is scheduled. | ||
|
|
||
| The commonly used functional interfaces in v3 are: | ||
|
|
||
| - ``Consumer<Coroutine>`` - a function that accepts a ``Coroutine`` input with no outputs. Used when defining command bodies with the builder API. Every occurrence of ``run(coroutine -> ... )`` is a ``Consumer<Coroutine>``. | ||
| - ``Runnable`` - a function with no inputs and no outputs. Used when setting ``whenCanceled`` and for ``runRepeatedly(() -> ... )`` | ||
| - ``BooleanSupplier`` - a function with no inputs and returns a ``boolean`` value. Heavily used by :doc:`triggers` and for coroutine ``waitUntil(() -> ...)`` | ||
|
|
||
| ## Lambda Examples | ||
|
|
||
| Imagine you have a function that needs to have some dynamic behavior based on its input. You need to pass an object to it that it can call when it needs it. These are often referred to as *callbacks*. | ||
|
|
||
| ```java | ||
| /** | ||
| * Runs until a user-supplied callback tells us to stop. | ||
| */ | ||
| public void runUntil(BooleanSupplier stop) { | ||
| int counter = 0; | ||
| while (!stop.getAsBoolean()) { | ||
| counter += 1; | ||
| System.out.println("Still going at iteration " + i + "!"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 1. Object-Oriented Approach (pre-Java 8) | ||
|
|
||
| In a purely object-oriented programming style, anything passed to the ``stop`` would need to be an instance of a type that implements the ``BooleanSupplier`` interface. This was the case in Java before the release of Java 8 in 2013: | ||
|
|
||
| ```java | ||
| public class RandomCondition implements BooleanSupplier { | ||
| @Override | ||
| public boolean getAsBoolean() { | ||
| return Math.random() >= 0.5; | ||
| } | ||
| } | ||
|
|
||
| runUntil(new RandomCondition()); | ||
| ``` | ||
|
|
||
| ### 2. Anonymous Classes (pre-Java 8) | ||
|
|
||
| However, you didn't need to write an entire class for this. Java allows for *anonymous* classes, where you define the class where you need it instead of in its own file. This approach is a little easier than the first, since it means all the logic is defined exactly where it's used instead of in a separate file: | ||
|
|
||
| ```java | ||
| runUntil(new BooleanSupplier() { | ||
| @Override | ||
| public void getAsBoolean() { | ||
| return Math.random() >= 0.5; | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| ### 3. Lambda Functions (Java 8 and later) | ||
|
|
||
| The anonymous class approach still has some problems, though. There's a lot of unnecessary code that buries the code we actually care about - the actual logic of ``Math.random() >= 0.5``. Everything else - ``new BooleanSupplier()``, ``public void getAsBoolean()``, even the ``@Override`` annotation - is redundant because there's only thing that could *possibly* be implemented here. Lambda functions were added to make this process simpler. The anonymous class above can be rewritten as a lambda function instead and cut out all the redundant code: | ||
|
|
||
| ```java | ||
| runUntil(() -> { | ||
| return Math.random() >= 0.5; | ||
| }); | ||
| ``` | ||
|
|
||
| And because this function is so simple - only a single line - it can be simplified by removing the curly braces and ``return`` keyword, going from a total of 6 lines of code down to just 1: | ||
|
|
||
| ```java | ||
| runUntil(() -> Math.random() >= 0.5); | ||
| ``` | ||
|
|
||
| ## Lambda Function Structure | ||
|
|
||
| A lambda function, like a normal function, has three components: a list of parameters that it accepts (which may be empty), a return type (which may be ``void``), and a body to perform some work. A lambda function separates the parameter list from the body using an arrow ``->``, pointing from the *inputs* to the *outputs*; the return type doesn't need to be specified anywhere, because the Java compiler already knows what it has to return based on the signature of the function it's passed to or the type of the variable it's assigned to. | ||
|
|
||
| ```java | ||
| (param1, param2, ..., paramN) -> { | ||
| ... body ... | ||
| return <result>; | ||
| } | ||
| ``` | ||
|
|
||
| There are also several special cases for lambda functions to make them more concise: | ||
|
|
||
| 1. Lambda functions with exactly one input parameter don't need parentheses around the parameter list. | ||
| 2. Parameters to lambda functions don't need to have their types specified. This is why commands v3 code can use ``run(coroutine -> ...)`` instead of having to to specify ``run((Coroutine coroutine) -> ...)`` every time. | ||
| 3. Lambda functions with only one line of code can omit the curly braces and ``return`` keyword | ||
|
|
||
| ## How commands use lambda functions | ||
|
|
||
| Command builders are based around providing a lambda function for the logic that the command will run. ``Command.noRequirements()`` and ``Command.requiring(...).executing()`` both accept lambda functions for the command logic. These lambda functions accept a single ``Coroutine`` object and perform whatever command logic is needed. The optional ``whenCanceled()`` builder method also accepts a lambda function, but this one doesn't have any arguments. | ||
|
|
||
|
|
||
| The ``coroutine`` parameter is only valid while the command is running. Do not store it in a field or try to call it later from another thread or callback. If a command does not need the coroutine parameter, name it ``_`` to make that clear to readers and to the compiler. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.