From de189769bc828e107e7faef0993f9be431e3465a Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 2 Jul 2026 10:01:45 -0400 Subject: [PATCH 01/12] Add commands v3 documentation pages --- .../commands-v3/creating-commands.rst | 306 ++++++++++ .../commandbased/commands-v3/how-it-works.rst | 48 ++ .../commandbased/commands-v3/index.rst | 94 +++ .../commands-v3/lambda-functions.rst | 12 + .../commands-v3/making-commands-run.rst | 126 +++++ .../commandbased/commands-v3/mechanisms.rst | 92 +++ .../commands-v3/migration-guide.rst | 533 ++++++++++++++++++ .../commandbased/commands-v3/scopes.rst | 94 +++ .../commands-v3/state-machines.rst | 91 +++ .../commands-v3/structuring-your-project.rst | 49 ++ .../commandbased/commands-v3/telemetry.rst | 122 ++++ .../commandbased/commands-v3/triggers.rst | 142 +++++ .../commands-v3/troubleshooting.rst | 159 ++++++ source/docs/software/commandbased/index.rst | 1 + source/docs/software/frc-glossary.rst | 12 + ...{2026-game-data.rst => 2026-Game-Data.rst} | 0 source/docs/yearly-overview/index.rst | 2 +- source/redirects.txt | 1 - 18 files changed, 1882 insertions(+), 2 deletions(-) create mode 100644 source/docs/software/commandbased/commands-v3/creating-commands.rst create mode 100644 source/docs/software/commandbased/commands-v3/how-it-works.rst create mode 100644 source/docs/software/commandbased/commands-v3/index.rst create mode 100644 source/docs/software/commandbased/commands-v3/lambda-functions.rst create mode 100644 source/docs/software/commandbased/commands-v3/making-commands-run.rst create mode 100644 source/docs/software/commandbased/commands-v3/mechanisms.rst create mode 100644 source/docs/software/commandbased/commands-v3/migration-guide.rst create mode 100644 source/docs/software/commandbased/commands-v3/scopes.rst create mode 100644 source/docs/software/commandbased/commands-v3/state-machines.rst create mode 100644 source/docs/software/commandbased/commands-v3/structuring-your-project.rst create mode 100644 source/docs/software/commandbased/commands-v3/telemetry.rst create mode 100644 source/docs/software/commandbased/commands-v3/triggers.rst create mode 100644 source/docs/software/commandbased/commands-v3/troubleshooting.rst rename source/docs/yearly-overview/{2026-game-data.rst => 2026-Game-Data.rst} (100%) diff --git a/source/docs/software/commandbased/commands-v3/creating-commands.rst b/source/docs/software/commandbased/commands-v3/creating-commands.rst new file mode 100644 index 0000000000..c6f7dcf0aa --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/creating-commands.rst @@ -0,0 +1,306 @@ +# Creating Commands + +Commands can be created in one of three ways: + +1. Using a :term:`factory method` on a :term:`Mechanism` object +2. Using a static factory method from the ``Command`` interface +3. Creating a class that implements the ``Command`` interface. This approach is only recommended for very complex logic or for programmers who are uncomfortable with :term:`lambda functions` + +Most robot commands should be created by mechanism factory methods. That keeps the hardware, the helper methods that directly touch hardware, and the commands that expose safe behavior all in the same class. ``Command.noRequirements()`` is useful for commands that coordinate other commands or update software-only state. Implementing ``Command`` directly is an option when the builder style is a poor fit, but it is the easiest approach to get subtly wrong because the implementation must supply every part of the command contract itself - notably, passing all required mechanisms to the constructor. + +## Where to put Commands + +A key idea in the commands framework is that of requirements: every command requires some number of mechanisms, and each mechanism may only be required by a single running command at a time. This prevents conflicting or dangerous control requests being issued: for example, if an "arm up" command is started while an "arm down" command is running, the "arm down" command will stop - if these commands weren't using the requirements system, then the arm would be commanded to go both up *and* down simultaneously. + +The requirement system can only protect hardware that is controlled through commands. If other classes can reach into a mechanism and set motor outputs directly, those calls bypass the scheduler entirely. It is therefore *strongly* recommended to use the following system when writing code that controls physical hardware: + +1. Write a class that implements the ``Mechanism`` interface +2. Make all fields in the class ``private`` to prevent external access +3. Make all methods that use those fields to control hardware also ``private`` +4. Write public ``Command``-returning methods for all control of the mechanism + +Public sensor accessors and triggers are fine, and are often useful. Reading a sensor does not fight with a command that owns the mechanism, but directly commanding an actuator does. The goal is not to hide all information from the rest of the robot program; the goal is to make every hardware-changing action pass through the scheduler's ownership rules to ensure every mechanism is only trying to do one thing at a time. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Command; + import org.wpilib.command3.Mechanism; + import org.wpilib.hardware.motor.PWMSparkMax; + + public class ExampleArm implements Mechanism { + // This motor controller is declared private to guarantee that it can't be used + // dangerously, outside of the command requirements system + private final PWMSparkMax pivotMotor = new PWMSparkMax(1); + + // Triggers can be and are encouraged to be public. They can't control the mechanism, + // and make it easier to coordinate complex actions + public final Trigger isUp = new Trigger(() -> pivotMotor.getPosition() >= 90); + public final Trigger isDown = new Trigger(() -> pivotMotor.getPosition() <= 0); + + public ExampleArm() { + setDefaultCommand(stop()); + } + + // This method controls the motor directly. + // It's private for the same reason the field is - to prevent dangerous usage + private void stopMotor() { + pivotMotor.setVoltage(0); + } + + // This factory method returns a Command. + // It's public because all hardware control should go through commands + // instead of unsafe method calls. + public Command stop() { + // `run` and `runRepeatedly` will automatically require the mechanism + // so we don't need to manually spell it out every time + return runRepeatedly(this::stopMotor).named("Stop Arm"); + } + + public Command up() { + return run(coroutine -> { + pivotMotor.set(0.5); + coroutine.waitUntil(isUp); + pivotMotor.set(0); + }).named("Arm Up"); + } + + public Command down() { + return run(coroutine -> { + pivotMotor.set(-0.5); + coroutine.waitUntil(isDown); + pivotMotor.set(0); + }).named("Arm Down"); + } + } + ``` + + +## Looping Commands + +Most commands need to run for more than a single loop cycle. This is done by using a loop (like ``while``) and calling ``coroutine.yield()`` at the end of every loop to allow other commands to run. + +.. tab-set-code:: + + ```java + public Command driveForward() { + return run(coroutine -> { + while (distance < 10) { + drive.setSpeed(0.5); + coroutine.yield(); // Required to allow other commands to run! + } + drive.setSpeed(0); + }).named("Drive Forward"); + } + ``` + +If you have a command that only needs to run the same piece of code every loop cycle, you can use the ``runRepeatedly`` factory method on a mechanism. This method automatically handles the loop and the yield for you. + +.. tab-set-code:: + + ```java + public Command stop() { + return runRepeatedly(() -> motor.set(0)).named("Stop"); + } + ``` + +Use ``runRepeatedly`` for simple "do this every scheduler cycle" behavior, such as a default command that continuously applies joystick drive output or holds a motor at zero volts. Use ``run`` when the command has a beginning, a middle, and an end: start the motor, wait for a condition, then stop the motor. If a loop appears in a ``run`` command, that loop must include a call to a yielding method; otherwise, it's a greedy loop and will lock up the robot program. WPILib will report compilation errors any any non-yielding ``while`` loops in command code. + +### Waiting for Conditions + +The ``Coroutine`` class provides methods to pause a command until a condition is met. The most basic of these is ``waitUntil(BooleanSupplier)``, which pauses until the given condition returns ``true``. + +.. tab-set-code:: + + ```java + public Command waitForButton() { + return run(coroutine -> { + coroutine.waitUntil(driverController.a()); + System.out.println("Button A pressed!"); + }).named("Wait for Button"); + } + ``` + +#### Timeouts and WaitResult + +Sometimes, a condition might never be met (for example, if a sensor fails or a mechanism jams). To prevent your robot from getting stuck indefinitely, you can provide a timeout to ``waitUntil``. When a timeout is provided, ``waitUntil`` returns a ``WaitResult`` object that you can use to check whether the condition was met or if the command timed out. + +.. tab-set-code:: + + ```java + import static org.wpilib.units.Units.Seconds; + import org.wpilib.command3.Coroutine; + + public Command safeElevatorUp() { + return run(coroutine -> { + coroutine.fork(elevator.up()); + + // Wait for the elevator to reach the top, but only for 1.25 seconds at most + Coroutine.WaitResult result = coroutine.waitUntil(elevator::atTop, Seconds.of(1.25)); + + if (result.timedOut()) { + // The elevator took too long! It might be jammed. Bail early. + elevator.setJamAlert(); + return; + } + + // ... do more things, confident that the elevator is in place + }).named("Safe Elevator Up"); + } + ``` + +Timeouts are most useful around physical state changes: elevators reaching a height, arms hitting a limit, flywheels reaching speed, or drivetrains arriving at a pose. A timeout should normally lead to an explicit fallback such as stopping the mechanism, retrying a safer action, or exiting the larger routine early. It may be dangerous to ignore a timeout + +## One-Shot Commands + +A command that never yields is called a *one-shot* command. It will be mounted and run to completion before the scheduler will pick up the next command to execute. Long-running one-shot commands, such as ones that wait on data to be loaded from disk or run expensive vision or path planning algorithms, will stall the scheduler and the entire robot program. + +One-shot commands generally do one very simple thing and immediately exit without taking much time. Good examples of one-shot commands are zeroing a sensor or assigning a new value to a variable. + +.. tab-set-code:: + + ```java + Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); + Command.noRequirements(_ -> field = 0).named("Reset Field"); + ``` + +One-shot commands are not bad. They are the right tool for small pieces of immediate work. The important rule is that "does not yield" also means "does not share time". If the action might take a noticeable amount of time, write it as a yielding command or move the expensive work somewhere that will not block robot control. + +## Complex Command Logic + +For more complex logic, you can use the various methods on the ``Coroutine`` object to coordinate multiple actions. + +### Waiting + +You can pause a command for a certain amount of time or until a condition is met. + +.. tab-set-code:: + + ```java + public Command waitAndThen() { + return run(coroutine -> { + System.out.println("Starting..."); + + coroutine.wait(Seconds.of(2)); + System.out.println("2 seconds later!"); + + coroutine.waitUntil(trigger); + System.out.println("Triggered!"); + }).named("Wait Example"); + } + ``` + +The resolution of ``coroutine.wait()`` is the scheduler loop period. With a 20 ms robot loop, a wait for 1 ms and a wait for 19 ms both resume on a later scheduler cycle, not exactly at the requested timestamp. This is normally fine for robot actions, but it is worth remembering when writing tests or when building routines with very short delays. + +### Concurrent Execution (Forking) + +If you want to start multiple actions at once, you can use ``coroutine.fork()``. Forking schedules a child command and immediately returns to the parent command. The child then runs alongside the parent until it completes, is canceled, or the parent exits. + +.. tab-set-code:: + + ```java + public Command parallelActions() { + return run(coroutine -> { + coroutine.fork(arm.up()); + coroutine.fork(intake.spin()); + coroutine.await(drive.followPath("ScorePath")); + }).named("Parallel Actions"); + } + ``` + +Note that forking a command from within a command creates a parent-child relationship with the following properties: + +* If the parent command is canceled, all of its forked children are also canceled. +* If a child command is interrupted by an external command (one not part of the same composition), the entire composition - including the parent and all other forked children - will be canceled. +* If one forked child is interrupted by *another* child of the same parent (a "sibling"), only the interrupted child and its descendants are canceled. The parent and the other siblings continue to run. + +Parent-child relationships exist regardless of how the child command was scheduled. Forking a command using ``coroutine.fork()``, manually scheduling it via ``Scheduler.getDefault().schedule()``, or automatically scheduling it from a ``Trigger`` will all have the same parent-child relationship. + +Use ``coroutine.await()`` when the parent needs to wait for a child command before continuing. ``await`` schedules the child if needed, then yields until that child is no longer scheduled or running. This is the v3 equivalent of writing an ordered composition, but without forcing the parent command to own every mechanism used by every child for the entire duration. + +#### Interruption Example + +Consider an autonomous command that forks two sub-tasks: one to control the arm and one to control the intake. + +.. tab-set-code:: + + ```java + public Command autoScore(Robot robot) { + return run(coroutine -> { + // Fork two sibling commands + coroutine.fork(robot.arm.moveUp().named("Arm Task")); + coroutine.fork(robot.intake.spin().named("Intake Task")); + + coroutine.await(robot.drive.followPath("ScorePath")); + }).named("Auto Score"); + } + ``` + +If an external command (like a safety trigger) interrupts the **Arm Task**, the entire **Auto Score** composition (including the **Intake Task** and the path following) will be canceled. + +#### Sibling Interruption Example + +In this example, the parent command forks two siblings that both require the same mechanism. The second sibling will interrupt the first one, but the parent command will continue running. + +.. tab-set-code:: + + ```java + public Command siblingConflict(Robot robot) { + return run(coroutine -> { + // Both of these require robot.arm + coroutine.fork(robot.arm.moveUp().named("First Sibling")); + coroutine.fork(robot.arm.moveDown().named("Second Sibling")); + + // "Second Sibling" will interrupt "First Sibling". + // Because they are siblings, the parent command ("siblingConflict") + // and other forked siblings will NOT be canceled. + coroutine.park(); + }).named("Sibling Conflict"); + } + ``` + +### Forking vs. Default Commands + +It is important to understand the difference between forking a command and setting a mechanism's default command. + +**Forking** a command starts it immediately in the background. It will run alongside the command that forked it, and will not be rescheduled when it finishes or is interrupted. + +**Setting a default command** does not start the command immediately. Instead, it tells the scheduler to run that command whenever no other command is requiring the mechanism. If you want a default command to start immediately, fork or schedule it immediately after assigning it. + +.. tab-set-code:: + + ```java + // Forking: runs in the background immediately + coroutine.fork(arm.holdLastPosition()); + + // Move the elevator up; the arm continues to hold position because it was forked + coroutine.await(elevator.up()); + + // Move the arm down; if an outer scope set a default command for the elevator, + // it runs; otherwise, the elevator is uncommanded + coroutine.await(arm.down()); + + // After the arm is down, there's no command in this scope that controls it. + // If an outer scope set a default command, it runs; otherwise, the arm is uncommanded. + ``` + +.. tab-set-code:: + + ```java + // Setting a default command: it will only start if the arm is uncommanded + arm.setDefaultCommand(arm.holdLastPosition()); + + // If you want the new default command to start immediately (if the arm is idle), + // you can fork its getter. + coroutine.fork(arm.getDefaultCommand()); + + // Move the elevator up; the arm continues to hold position + coroutine.await(elevator.up()); + + // Move the arm down; if an outer scope set a default command for the elevator, + // it runs; otherwise, the elevator is uncommanded + coroutine.await(arm.down()); + + // Once arm.down() completes, the arm is uncommanded in this scope, + // so its default command (holdLastPosition) starts automatically. + ``` diff --git a/source/docs/software/commandbased/commands-v3/how-it-works.rst b/source/docs/software/commandbased/commands-v3/how-it-works.rst new file mode 100644 index 0000000000..57724f49ee --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/how-it-works.rst @@ -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 sideloaded 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 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. diff --git a/source/docs/software/commandbased/commands-v3/index.rst b/source/docs/software/commandbased/commands-v3/index.rst new file mode 100644 index 0000000000..2b7dbbdc41 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/index.rst @@ -0,0 +1,94 @@ +# 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. + +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. + +.. warning:: Calling ``coroutine.yield()`` is required to prevent commands from being greedy - if a command never yields, no other commands will be able to run and driver inputs will not be read until the command exits. WPILib will check ``while`` loops at compile-time to ensure that loops inside of command code will yield, and issue a compiler error if any greedy loops are found. + +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. + +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. + +.. tab-set-code:: + + ```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"); + } + } + ``` diff --git a/source/docs/software/commandbased/commands-v3/lambda-functions.rst b/source/docs/software/commandbased/commands-v3/lambda-functions.rst new file mode 100644 index 0000000000..c8489b6456 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/lambda-functions.rst @@ -0,0 +1,12 @@ +# Using Lambda Functions + +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. + +## 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. diff --git a/source/docs/software/commandbased/commands-v3/making-commands-run.rst b/source/docs/software/commandbased/commands-v3/making-commands-run.rst new file mode 100644 index 0000000000..0050c4e926 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/making-commands-run.rst @@ -0,0 +1,126 @@ +# Making Commands Run + +There are three ways to make a command run: using a ``Trigger`` to set up a command to automatically run when some event occurs in the future; running a command from inside another command via ``Coroutine.fork(Command)`` or ``Coroutine.await(Command)``; and configuring a :term:`Mechanism` with a default command to execute when it would otherwise be idle. + +Each approach describes a different kind of intent. Triggers say "when this signal changes, run this command." Forking says "this command should start now, and the parent should keep going." Awaiting says "this command should start now, and the parent should wait for it." Default commands say "when nobody else owns this mechanism, keep it in this safe or useful state." + +## Triggers + +Triggers allow you to set up automated behavior that runs in response to external events, such as a button press or a sensor reaching a threshold. They are the primary way to start commands outside of compositions. A trigger is checked when its event loop is polled; for the default event loop, this happens during ``Scheduler.run()``. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Command; + import org.wpilib.command3.Trigger; + import org.wpilib.hardware.discrete.DigitalInput; + + public class Robot extends OpModeRobot { + private final DigitalInput lowerLimitSwitch = new DigitalInput(1); + + // This trigger will be checked every time the scheduler runs. + public final Trigger atMinLimit = new Trigger(() -> lowerLimitSwitch.get()); + + public Robot() { + // Bind a command to execute whenever the minimum limit is reached. + atMinLimit.onTrue(Command.print("Min limit reached!").named("Limit Message")); + } + } + ``` + +For more detailed information on trigger types, combining triggers, and advanced behavior, see the :doc:`triggers` page. + +## Manually running a command + +While triggers are the most common way to start commands in response to external events, you often need to start a command directly from within another command or when an OpMode starts. This is done using the ``Coroutine.fork()`` or ``Coroutine.await()`` methods, or rarely a direct call to ``Scheduler.getDefault().schedule()``. The scheduler will ensure that the command does not outlive the :doc:`scope` that scheduled it, regardless of the method used. + +### Forking (Asynchronous) + +``Coroutine.fork(Command)`` starts a command and returns immediately. The forked command runs concurrently with the command that started it. If the parent command is canceled, all of its forked commands are also canceled. Forking a command that conflicts with a higher-priority running command will fail; the higher-priority command continues to run and the parent command immediately continues to the next statement. + +.. tab-set-code:: + + ```java + public Command exampleFork() { + return run(coroutine -> { + // Start this command in the background. + coroutine.fork(arm.up()); + + // This code runs immediately after forking, without waiting for the arm + System.out.println("Arm is moving up in the background..."); + + // The arm will continue to move to its "up" position while the intake is extending. + coroutine.await(intake.extend()); + }).named("Example Fork"); + } + ``` + +### Awaiting (Synchronous) + +``Coroutine.await(Command)`` starts a command and pauses the current command until the child command completes. Awaiting is useful for step-by-step routines because the code reads in the same order the robot should act. + +.. tab-set-code:: + + ```java + public Command exampleAwait() { + return run(coroutine -> { + // Start the command and wait for it to finish + coroutine.await(arm.up()); + + // This code only runs after the arm has finished moving up + System.out.println("Arm is now up!"); + }).named("Example Await"); + } + ``` + +## Scheduling Already Running Commands + +If a command is already running, attempting to schedule it again will have no effect. The command will simply continue to run from its current point of execution; it will **not** be restarted or interrupted. ``fork`` and ``await`` can be combined to start running a command in the background and then wait for it to complete at a later point (assuming that it hadn't finished by then - otherwise ``await`` would start it over again). + + +.. tab-set-code:: + + ```java + public Command duplicateScheduling() { + return run(coroutine -> { + Command armUp = arm.up(); + + // Start the arm moving up + coroutine.fork(armUp); + + // Attempting to schedule the same instance again while it's running does nothing. + // The arm continues its original 'up' movement uninterrupted. + coroutine.fork(armUp); + + // Perform another action while the arm is moving + coroutine.await(intake.spin()); + + // Similarly, awaiting a running command will wait for the existing instance to finish. + // However, if the armUp command has exited by the time spin() finished, + // this await call would actually start the armUp command from the beginning + coroutine.await(armUp); + }).named("Duplicate Scheduling"); + } + ``` + +This applies only to command _objects_. If two identical commands are scheduled - even if they both implement Java's ``equals()`` method - the scheduler still treats them as different commands, and the second command will interrupt and cancel the first: + +.. tab-set-code:: + + ```java + public Command createNewCommand() { return ... } + + Command command1 = createNewCommand(); + Command command2 = createNewCommand(); + + Scheduler.getDefault().schedule(command1); + + // command1 is interrupted by command2, even though they're identical + Scheduler.getDefault().schedule(command2); + ``` + +## Default Commands + +Every mechanism can have a **default command** that runs whenever no other command is requiring it. This is useful for ensuring that hardware is always in a safe state. A drivetrain default command might read joysticks, an elevator default command might hold position, and an intake default command might stop the motor. + +Default commands are scheduled during the scheduler's normal scheduling phase. Setting a default command changes what the scheduler will choose next time the mechanism is idle; it is not an immediate call to start the command's logic. See the :doc:`mechanisms` page for more details. diff --git a/source/docs/software/commandbased/commands-v3/mechanisms.rst b/source/docs/software/commandbased/commands-v3/mechanisms.rst new file mode 100644 index 0000000000..fd5b523cb8 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/mechanisms.rst @@ -0,0 +1,92 @@ +# Command Mechanisms + +Mechanisms are the building blocks of a robot program. They represent physical hardware components, such as a drivetrain, an elevator, or a claw. A mechanism can also represent non-actuator hardware that still needs coordinated ownership, such as an LED strip or a vision processor whose pipeline can be changed by commands. + +In the commands framework, mechanisms serve two primary purposes: + +1. **Hardware Abstraction**: They encapsulate the low-level hardware control (motor controllers, sensors, etc.) and provide a high-level API for commands to use. +2. **Resource Management**: They act as locks that commands must acquire to run. Only one command can own a mechanism at a time, which prevents conflicting hardware requests. + +The abstraction part and the ownership part are meant to work together. If a motor controller is public, code elsewhere can still set it directly and bypass the scheduler. If the motor controller is private and the mechanism exposes commands instead, the scheduler can see every action that controls that hardware and apply the normal conflict rules. + +## Defining a Mechanism + +To create a mechanism, write a class that implements the ``Mechanism`` interface. Hardware fields and low-level actuator helpers should usually be private. Public methods should either read state or return commands that perform safe actions. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Command; + import org.wpilib.command3.Mechanism; + import org.wpilib.command3.Trigger; + import org.wpilib.hardware.discrete.DigitalInput; + import org.wpilib.hardware.motor.PWMSparkMax; + + public class Intake implements Mechanism { + private final PWMSparkMax motor = new PWMSparkMax(1); + private final DigitalInput beamBreak = new DigitalInput(2); + + public final Trigger hasGamePiece = new Trigger(() -> !beamBreak.get()); + + public Intake() { + setDefaultCommand(stop()); + } + + private void setSpeed(double speed) { + motor.set(speed); + } + + public Command intake() { + return run(coroutine -> { + setSpeed(0.75); + coroutine.waitUntil(hasGamePiece); + setSpeed(0); + }).named("Intake"); + } + + public Command stop() { + return runRepeatedly(() -> setSpeed(0)).named("Stop Intake"); + } + } + ``` + +This style gives other code useful tools without giving it raw control. Other classes can bind to ``hasGamePiece`` or schedule ``intake()``, but they cannot accidentally leave the motor running outside the requirement system. + +## Mechanism Commands + +Mechanisms provide several factory methods to create commands that use them. Using these methods automatically adds the mechanism to the command's requirements. + +- ``run(Consumer body)``: Creates a command that executes the given body. +- ``runRepeatedly(Runnable body)``: Creates a command that executes the given body in an infinite loop, automatically yielding each cycle. +- ``idle()``: Creates a command that owns the mechanism, does nothing, and has the lowest priority. + +``run`` is the general-purpose builder. It is best for commands with staged logic, such as "start moving, wait until the top limit is reached, then stop". ``runRepeatedly`` is for commands that should execute the same short action every scheduler cycle, such as applying arcade drive output or continuously holding zero voltage. ``idle`` is useful when you intentionally want a mechanism to be owned but uncommanded until another command interrupts it. + +Commands created from these methods still need a name before they become ``Command`` objects. This is deliberate: command names appear in scheduler events, telemetry, and debugging output, so every command should have a meaningful one. + +## Default Commands + +Every mechanism can have a **default command**. This is the command that the scheduler will run whenever no other command is requiring the mechanism. Default commands are useful for ensuring that hardware is always in a safe or predictable state (e.g., stopping a motor or holding a position). + +The default command is initially an ``idle()`` command. That means a mechanism with no configured default command is owned by a lowest-priority command that does nothing. For many mechanisms, especially mechanisms affected by gravity or motors that should be explicitly stopped, a real default command is safer than leaving the mechanism uncommanded. + +Default commands also have priorities. A default command effectively sets the minimum priority needed to take over the mechanism, so defaults should usually have lower priority than ordinary user commands. A high-priority default command can accidentally prevent other low-priority behavior from ever starting. + +.. tab-set-code:: + + ```java + public class Elevator implements Mechanism { + public Elevator() { + // Set the default command to stay at the current position + setDefaultCommand(holdPosition()); + } + + public Command holdPosition() { + return runRepeatedly(() -> motor.setVoltage(feedforwardForCurrentHeight())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Hold Position"); + } + } + ``` + +Setting a default command does not immediately run it. The scheduler starts the default command during its normal scheduling phase when the mechanism is otherwise idle. If a command temporarily changes a default command from inside its own logic, the previous default command is restored when that command's scope exits. diff --git a/source/docs/software/commandbased/commands-v3/migration-guide.rst b/source/docs/software/commandbased/commands-v3/migration-guide.rst new file mode 100644 index 0000000000..ab93d2b22d --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/migration-guide.rst @@ -0,0 +1,533 @@ +# Migrating from Commands v2 + +This page is for teams that already know the commands v2 framework and want to understand how the same ideas appear in commands v3. The goal is not to mechanically translate every class name. The goal is to preserve the intent of the robot code while using the v3 tools that express that intent directly. + +Most of the familiar concepts still exist: commands, requirements, triggers, default commands, cancellation, and command composition. The major change is how command logic is written and how composed commands own mechanisms while they run. + +## The Big Shift + +In commands v2, command behavior is commonly split across lifecycle methods such as ``initialize()``, ``execute()``, ``isFinished()``, and ``end()``. In commands v3, command behavior is written as one :term:`coroutine`-backed function. Setup code goes before the loop, repeated work goes in the loop, finishing happens by returning from the function, and cleanup code runs at the end of the function just before it returns. + +v3 commands are primarily expected to be created using builder objects, similar to the v2 fluent API where methods are chained together to configure the command object. A key change in the fluent API is that command names are now **required**; it's impossible to create a command without a name. Names are used in the v3 telemetry data (see :doc:`telemetry`) and are crucial for debugging. + +The v3 command function is free-form and flexible and promotes standard Java language features instead of a custom DSL. If a command needs to run something repeatedly, use a standard Java ``while`` loop; if a command needs to do just one thing and exit, then just don't use ``yield``. + +.. tab-set-code:: + + ```java + public class MoveArmUp extends Command { + @Override + public void initialize() { + arm.setVoltage(4); + } + + @Override + public void execute() { + // Optional repeated work. + } + + @Override + public boolean isFinished() { + return arm.atTop(); + } + + @Override + public void end(boolean interrupted) { + arm.stop(); + } + } + ``` + + ```java + public Command up() { + return run(coroutine -> { + setVoltage(4); + coroutine.waitUntil(this::atTop); + stop(); + }).whenCanceled(this::stop) + .named("Arm Up"); + } + ``` + +The v3 version reads in the order the robot acts: start moving, wait until the arm is up, then stop. ``waitUntil()`` yields while it waits, so other commands and triggers continue to run. + +## Subsystems Become Mechanisms + +The v2 ``Subsystem`` corresponds to v3's ``Mechanism`` interface. Most of the ``Subsystem`` behaviors still apply to ``Mechanism``, such as providing command factories and acting as requirements for commands. + +v3 provides the following factory methods: + +- ``run(Consumer)`` - starts building a coroutine-based command. Add command logic goes in the lambda function passed to this method. Does not directly correspond with any v2 factory, but acts like ``runOnce`` if the command body never yields or awaits any child commands. +- ``runRepeatedly(Runnable)`` - starts building a command that executes the same function over and over until the end condition is reached. Corresponds with v2's ``run`` factory. +- ``idle()`` - creates a command that does nothing. Users can override this to do things like explicitly turning off motors. +- ``idleFor(Time)`` - creates an idle command from ``idle()`` and gives it a timeout. It can be convenient for simple timed sequences, + +However, there is no similar API to the subsystem-level ``periodic()`` function. If you need to run a periodic function outside of the commands framework, such as reading sensor inputs or updating telemetry, call that function directly in the relevant method (often ``robotPeriodic()`` in your main robot class). + +.. tab-set-code:: + + ```java + public class Elevator implements Mechanism { + private final MotorController motor = ...; + private final Encoder encoder = ...; + + private static final double MAX_HEIGHT = ...; + private double position; + + public Elevator() { + setDefaultCommand(holdPosition()); + } + + public void updateInputs() { + // Call this in robotPeriodic() + this.position = encoder.getDistance(); + } + + private void setVoltage(double volts) { + motor.setVoltage(volts); + } + + public boolean atTop() { + return position >= MAX_HEIGHT; + } + + public Command up() { + return run(coroutine -> { + setVoltage(6); + coroutine.waitUntil(this::atTop); + setVoltage(0); + }).whenCanceled(() -> setVoltage(0)) + .named("Elevator Up"); + } + + public Command holdPosition() { + return runRepeatedly(() -> setVoltage(feedforwardForCurrentHeight())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Hold Elevator"); + } + } + ``` + +Reading mechanism state can still be public. Direct actuator control should usually be private. That keeps hardware-changing actions inside commands, where the scheduler can enforce requirements. + +## Requirements Still Matter + +The requirements system is the same as v2: commands declare the mechanisms they control, and only one running command may require a mechanism at a time. If a new command is scheduled that conflicts with at least one running command, the scheduler compares priorities: + +1. If the new command is the same or higher priority as every command it conflicts with, it is scheduled and the running commands are canceled. +2. If the new command is lower priority than _any_ command it conflicts with, the new command is not canceled and all conflicting commands continue to run. + +Like v2, a command created with a mechanism's ``run(...)`` or ``runRepeatedly(...)`` helper automatically requires that mechanism. + +.. tab-set-code:: + + ```java + public Command intake() { + // Automatically requires this Intake mechanism. + return run(coroutine -> { + motor.set(0.8); + coroutine.waitUntil(hasGamePiece); + motor.set(0); + }).whenCanceled(() -> motor.set(0)) + .named("Intake"); + } + ``` + +Use ``Command.noRequirements(...)`` for commands that truly do not own hardware, or for parent commands that coordinate child commands without inheriting all of their requirements up front. Good use cases for no-requirement commands include sensor resets or debugging prints. + +## Default Commands + +Default commands still describe what a mechanism should do when no other command owns it. A drivetrain default command might read joysticks, an elevator default command might hold position, and an intake default command might stop the motor. + +The differences worth remembering are: + +- A default command must require exactly the mechanism it is assigned to. +- Setting a default command does not immediately run it; the scheduler starts it when the mechanism is otherwise idle. +- Default command settings are scoped. A default set inside an OpMode or command is reverted when that scope exits. +- Default commands should usually have lower priority than ordinary commands. Lower-priority commands cannot interrupt higher-priority commands, so the default command's priority is effectively the minimum priority that's usable for that mechanism. + +.. tab-set-code:: + + ```java + public class Drive implements Mechanism { + public Drive(CommandGamepad controller) { + setDefaultCommand( + runRepeatedly(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Teleop Drive")); + } + } + ``` + +## Command Logic And Finishing + +In v2, ``isFinished()`` decides when a command is done. In v3, ordinary control flow decides when a command is done. A command finishes naturally when its command body returns. + +For a command that runs until a condition is met, use ``waitUntil(...)``: + +.. tab-set-code:: + + ```java + public Command shootWhenReady() { + return run(coroutine -> { + spinUp(); + coroutine.waitUntil(this::atSpeed); + feedNote(); + }).whenCanceled(this::stop) + .named("Shoot When Ready"); + } + ``` + +For a command that updates every scheduler cycle, use a loop and yield: + +.. tab-set-code:: + + ```java + public Command driveDistance(double meters) { + return run(coroutine -> { + resetDistance(); + while (getDistance() < meters) { + setSpeed(0.5); + coroutine.yield(); + } + stop(); + }).whenCanceled(this::stop) + .named("Drive Distance"); + } + ``` + +The yield is not optional. Commands v3 uses cooperative scheduling: a command gives other commands time to run by calling ``yield()``, ``wait()``, ``waitUntil()``, ``await()``, ``awaitAll()``, ``awaitAny``, or ``park()``. + +## One-Shot Commands + +A v2 ``InstantCommand`` usually becomes a one-shot v3 command: a command that does a small amount of work and returns without yielding. + +.. tab-set-code:: + + ```java + public Command resetGyro() { + return Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); + } + ``` + +One-shot commands are appropriate for quick state changes: resetting a sensor, updating a flag, printing a diagnostic message, or clearing an alert. They are not appropriate for blocking I/O, expensive calculations, or anything that may take enough time to delay robot control. + +## Command Groups And Coroutine Composition + +Commands v3 has fluent command group builders like v2: + +- ``Command.sequence(...)`` and ``commandA.andThen(commandB)`` +- ``Command.parallel(...)`` and ``commandA.alongWith(commandB)`` +- ``Command.race(...)`` and ``commandA.raceWith(commandB)`` + +These are useful for straightforward compositions, and behave much like their v2 equivalents. + +v3's ``SequentialGroup`` and ``ParallelGroup`` retain the v1 and v2 behavior of owning all mechanisms used by all commands in the group, even when those inner commands are otherwise not running. This behavior is intentional, since it allows command groups to be interrupted at any point when a conflicting command is scheduled, but will result in uncommanded behavior for mechanisms owned by the group but not controlled by a running command within the group. + +Complex command sequences can be built using the v3 ``StateMachine`` API (see :doc:`state-machines`) + +There are some key improvements in ownership and interruption behavior in v3 to be aware of: + +1. The v3 scheduler is responsible for _every_ command. The v2 scheduler only handled the topmost level of commands, and compositions like ``SequentialCommandGroup`` effectively acted like mini-schedulers to run the commands inside the group. +2. v3 compositions do not have to have any requirements. Because the v3 scheduler tracks parent-child relationships, an interrupt to a child command will bubble up to its parent (and its parent, and so on). Parent commands effectively inherit all of a child's requirements *while the child is running*. +3. v3 child commands inherit the priority of their parent if it's higher than their own. See Priorities_ for details. + +The coroutine API is often a better migration target for complex routines because it lets mechanisms do other things when they're not actively in use by a child command. + +### Handling Fork Failures + +Forking a child command with a coroutine's ``fork``, ``await``, ``awaitAll``, or ``awaitAny`` method will fail if one or more of the forked commands shares a requirement with a running command with a higher priority. + +.. tab-set-code:: + + ```java + Scheduler.getDefault().schedule( + arm.run(...).withPriority(1000).named("Super High Priority Arm Command")); + + Command parent = Command.noRequirements(coroutine -> { + // This child command can't be forked because a higher-priority command already owns the arm + coroutine.fork( + arm.run(...).withPriority(0).named("Lower Priority Arm Command")); + }).named("Parent"); + + Scheduler.getDefault().schedule(parent); + ``` + +The v3 framework provides two ways of handling failures: by interrupting the command that called a forking method, or by returning a failure object that user code can handle. The v3 framework defaults to the interruption behavior, so if a child command that you assume will run is unable to be scheduled, the entire composition will stop and an ``Interrupted`` telemetry event will be issued by the scheduler, attributed to the command that prevented the child command from being scheduled. In this setup, there is no chance for user code to receive the failure event and retry or fall back to different behavior. + +The other option is to call ``setCancelOnForkFailure(false)`` on the coroutine object, telling it to return to user code instead of immediately interrupting the command. This setting only applies to the single coroutine, and is _not_ inherited by child commands; every command that wants this behavior needs to opt into it. + +.. tab-set-code:: + + ```java + Scheduler.getDefault().schedule( + arm.run(...).withPriority(1000).named("Super High Priority Arm Command")); + + Command parent = Command.noRequirements(coroutine -> { + // Lets us handle the failure, instead of the framework immediately canceling the command. + coroutine.setCancelOnForkFailure(false); + + ForkResult result = coroutine.fork( + arm.run(...).withPriority(0).named("Lower Priority Arm Command")); + + // Handle the result. Pattern-matching instanceof lets us easily access the failure data + if (result instanceof ForkResultFailure failure) { + for (SchedulerResult.Failure failure : failure.failed()) { + switch (failure) { + case LowerPriorityThanRunningCommand(Command failed, Command conflict) -> { + System.err.println("Could not fork " + failed + " because " + conflict + " is already running"); + } + case LowerPriorityThanQueuedCommand(Command failed, Command conflict) -> { + System.err.println("Could not fork " + failed + " because " + conflict + " is already queued"); + } + } + } + + // Plausibly continue with behavior that doesn't need the arm. + // If this _also_ fails to be scheduled, we'll just return immediately + coroutine.await(armlessBehavior()); + return; + } + + // The fork succeeded, so we can proceed with behavior that knows we own the arm. + // If this behavior can't be scheduled, then we just return immediately. + coroutine.await(armedBehavior()); + }).named("Parent"); + + Scheduler.getDefault().schedule(parent); + ``` + +### Sequential Work + +Use ``coroutine.await(...)`` to run a child command and wait until it finishes. + +.. tab-set-code:: + + ```java + public Command scoreSequence() { + return Command.noRequirements(coroutine -> { + coroutine.await(drive.driveToScoringLocation()); + coroutine.await(elevator.moveToScoringHeight()); + coroutine.await(gripper.release()); + }).named("Score Sequence"); + } + ``` + +The parent command above requires no mechanisms. The drivetrain, elevator, and gripper are only owned while their own commands are running, which allows other commands to control them when the scoring sequence doesn't actively control them. This can be a strength because it allows default commands to run, but it also allows other commands to run that would break the sequence (such as moving the drivebase out of the scoring location while the elevator is moving, possibly tipping the robot). Care should be taken to avoid running sequence-breaking commands, or set default commands within the command + + +### Parallel Work + +Use ``fork(...)`` to start child commands that should run in the background, or ``await``, ``awaitAll``, or ``awaitAny`` to fork and then wait for the child commands to finish. + +.. tab-set-code:: + + ```java + public Command prepareToScore() { + return Command.noRequirements(coroutine -> { + // Start the turret and shooter commands, and wait for both to finish. + coroutine.awaitAll(turret.aimAtGoal(), shooter.spinUp()); + + // Feed a ball into the shooter only after the turret and shooter are ready + coroutine.await(feeder.feed()); + }).named("Prepare To Score"); + } + ``` + +### Race Work + +Use ``awaitAny(...)`` when several child commands should start and the parent should continue after the first one finishes. The remaining commands are canceled. + +.. tab-set-code:: + + ```java + public Command intakeUntilPieceOrTimeout() { + return Command.noRequirements(coroutine -> { + coroutine.awaitAny( + intake.intake(), + Command.waitFor(Seconds.of(2)).named("Intake Timeout")); + + if (!intake.hasGamePiece()) { + intake.setNoPieceAlert(); + } + }).named("Intake Until Piece Or Timeout"); + } + ``` + +For simple race groups, ``Command.race(...)`` is also available. Use explicit coroutine logic when the next step depends on which condition won or when you need additional fallback behavior. + +## Proxy Commands And Smart Requirements + +In v2, teams often used proxy commands or schedule-command patterns to avoid a composition inheriting requirements too early. For example, a large autonomous command might not want to require the elevator for the full routine if the elevator is only used near the end. + +In v3, this pattern is built into coroutine composition. A parent command can require no mechanisms and ``await()`` child commands as needed. The child command owns its requirements while it runs, and releases them when it completes. Child commands can also share requirements with their parents; the scheduler automatically detects the parent-child relationship and won't interrupt the parent. In v2, sharing requirements between parent and child commands would result in the child interrupting its parent. + +.. tab-set-code:: + + ```java + public Command autonomousScore() { + return Command.noRequirements(coroutine -> { + // Owns only the drivetrain while this child runs. + coroutine.await(drive.followPath("ScorePath")); + + // Owns only the elevator while this child runs. + coroutine.await(elevator.moveToScoringHeight()); + + // Owns only the gripper while this child runs. + coroutine.await(gripper.release()); + }).named("Autonomous Score"); + } + ``` + +This is often the cleanest replacement for v2 proxy-heavy code. The requirements are local to the actions that actually use them, but the larger routine still cancels as a unit if one of its children is externally interrupted. + +## Triggers And Bindings + +Most trigger bindings carry over by name or by intent: ``onTrue``, ``onFalse``, ``whileTrue``, ``whileFalse``, and toggle bindings all exist in v3. The important new idea is :doc:`scopes`: a binding created in the robot constructor is global and will always be active; a binding created while an OpMode is running is only active while that OpMode is selected on the driverstation, and will be deleted when the OpMode changes; and a binding created inside a running command is removed when that command exits, and any command attached to that binding is canceled. + +.. tab-set-code:: + + ```java + public Command aimAndShootWhenReady() { + return Command.noRequirements(coroutine -> { + // This binding only exists while aimAndShootWhenReady is running. + shooter.atSpeed.onTrue(feeder.feedOnce()); + + // shooter.spinUp() only runs while aimAndShootWhenReady is running, + // and will be canceled when aimAndShootWhenReady exits + coroutine.fork(shooter.spinUp()); + + coroutine.await(turret.aimAtGoal()); + }).named("Aim And Shoot When Ready"); + } + ``` + +New in v3 are the ``retryWhileTrue`` and ``retryWhileFalse`` bindings. A retry binding restarts its command if the command finishes while the trigger signal is still active, unlike ``whileTrue`` or ``whileFalse`` which will not restart the the command if it finishes or is interrupted before the trigger condition changes. They act similar to a v2-style ``whileTrue(command.repeatedly())`` binding. + +## Cancellation And Interruption + +The same cancellation and interruption concepts carry over from v2 in v3: cancellation means the command was stopped before its natural completion; interruption is a particular kind of cancellation specifically caused by another command taking ownership of a required mechanism, rather than being canceled by a trigger binding or a manual call to the scheduler's ``cancel()`` method. + +Use ``whenCanceled(...)`` for cleanup that must happen when a command is canceled. Note that this runs regardless of _why_ the command was canceled. + +.. tab-set-code:: + + ```java + public Command runRollerUntilLoaded() { + return run(coroutine -> { + roller.set(0.6); + coroutine.waitUntil(hasGamePiece); + roller.set(0); + }).whenCanceled(() -> roller.set(0)) + .named("Run Roller Until Loaded"); + } + ``` + +Do not put long loops in cancellation cleanup. Cancellation cleanup should be short and single-shot: stop a motor, clear a flag, or close a resource. + +Scheduler telemetry reports these cases separately. A command that finishes normally emits ``Completed``. A command that is interrupted emits ``Interrupted`` followed by ``Canceled``. A command that throws emits ``CompletedWithError`` and the exception still propagates, bubbling up to the scheduler ``run()`` call and crashing the robot program; the WPILib framework will print the exception and its stacktrace to the driver station console for operators to see and debug the program. + +## Priorities + +v2 had a simple priority system: a command can either always be interrupted by a conflicting command (via ``kCancelSelf``, which was the default setting), or could always ignore a conflicting command (via ``kCancelIncoming``). In effect, a command would either have the absolute _minimum_ priority, always interruptable by other commands, or the absolute _maximum_ priority, never interruptable by other commands. + +Commands v3 an integer-based priority system, using the full range of integer values. The default priority is 0, but can be specified in the full 32-bit integer range of -2^31 through 2^31-1. If two commands conflict: + +- A higher-priority scheduled command interrupts the lower-priority running command. +- An equal-priority scheduled command interrupts the running command. +- A lower-priority scheduled command is discarded and does not start. + +Default commands should usually have priority below ordinary commands, and never above 0. If a default command has the same or higher priority as normal controls, it can block behavior that should be allowed to take over the mechanism. + +Commands in a v3 composition inherit the priority of their parent if it's higher than their own. This allows for child commands to be "promoted" and take ownership of mechanisms that are owned by otherwise higher-priority commands. + +## Common Migration Recipes + +### Default Drive Command + +.. tab-set-code:: + + ```java + public class Drive implements Mechanism { + public Command teleopDrive(CommandGamepad controller) { + return runRepeatedly(() -> + arcadeDrive(controller.getLeftY(), controller.getRightX())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Teleop Drive"); + } + } + ``` + +### Timed Command + +.. tab-set-code:: + + ```java + public Command outtakeFor(Time duration) { + return run(coroutine -> { + motor.set(-0.7); + coroutine.wait(duration); + motor.set(0); + }).whenCanceled(() -> motor.set(0)) + .named("Timed Outtake"); + } + ``` + +For a timeout around an existing command, use ``withTimeout(...)``: + +.. tab-set-code:: + + ```java + Command safeMoveToTop = elevator.up().withTimeout(Seconds.of(1.5)); + ``` + +### Conditional Wait With Fallback + +.. tab-set-code:: + + ```java + public Command safeMoveToTop() { + return run(coroutine -> { + motor.setVoltage(6); + var result = coroutine.waitUntil(this::atTop, Seconds.of(1.5)); + motor.setVoltage(0); + + if (result.timedOut()) { + setJamAlert(); + } else { + clearJamAlert(); + } + }).whenCanceled(() -> motor.setVoltage(0)) + .named("Safe Move To Top"); + } + ``` + +### Autonomous Routine + +.. tab-set-code:: + + ```java + public Command autoScoreAndLeave() { + return Command.noRequirements(coroutine -> { + coroutine.await(drive.followPath("ScorePath")); + + coroutine.fork(shooter.spinUp()); + coroutine.await(elevator.moveToScoringHeight()); + coroutine.await(gripper.release()); + + coroutine.await(drive.followPath("LeaveCommunity")); + }).named("Auto Score And Leave"); + } + ``` + +## What Not To Carry Over + +Avoid mechanically recreating v2 structure when migrating to v3: + +- Do not write command classes by default just to preserve lifecycle-method shape. Use mechanism factory methods unless a class genuinely makes the code clearer. +- Do not expose public motor controllers or actuator helpers and call them directly from unrelated code. Put hardware-changing behavior behind commands. +- Do not use global trigger bindings for behavior that is only valid in one OpMode or during one command. +- Do not treat ``fork()`` as fire-and-forget. Forked commands are children and are canceled when their parent exits. +- Do not use command groups everywhere by habit. For complex routines, coroutine logic is often clearer and can avoid owning mechanisms before they are needed. + +The best migration usually keeps the same robot behavior, but changes the shape of the code: mechanisms own hardware, command factories describe safe actions, and larger routines coordinate those actions with ``fork()``, ``await()``, ``awaitAll()``, and ``awaitAny()``. diff --git a/source/docs/software/commandbased/commands-v3/scopes.rst b/source/docs/software/commandbased/commands-v3/scopes.rst new file mode 100644 index 0000000000..162443582b --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/scopes.rst @@ -0,0 +1,94 @@ +# Scopes + +Scopes are how the commands library tracks the lifetime of commands, trigger bindings, default commands, and other scheduler resources. A scope represents a period during which certain actions are valid and safe. When a scope exits or becomes inactive, the scheduler automatically cleans up the resources associated with it. + +This automatic cleanup is a critical safety feature. It ensures that commands and bindings do not leak from one part of the robot program to another. A teleop-only binding should not keep scheduling commands in autonomous, and a trigger created by a command should not keep controlling hardware after the command that created it is gone. + +Scopes are used to manage several resources: + +* **Commands**: A command scheduled in a scope will be canceled when the scope exits. +* **Triggers** A trigger created in a scope will stop being polled when the scope exits. +* **Trigger Bindings**: Trigger bindings created in a scope will be deactivated when the scope exits, and any running commands bound to the trigger will be canceled. +* **Default Commands**: Setting a mechanism's default command in a scope will only be applied during that scope. + +When the library needs to create a scope, it chooses the narrowest one available: code running inside a command gets the command scope, code running inside an OpMode, but not inside a command, gets the OpMode scope, and code running outside a command and with no OpMode selected + +The framework has three hierarchical scopes, from narrowest to widest: + +## The Command Scope + +The command scope is tied to the lifetime of a specific running command. Any resources created *inside* a command's logic are automatically scoped to that command. + +* **Child Commands**: If a command schedules another command (a "child"), the child is automatically canceled if the parent command finishes or is canceled. +* **Trigger Bindings**: If you create a trigger binding (e.g., ``trigger.onTrue(anotherCommand)``) inside a command, that binding is only active while the parent command is running. +* **Default Commands**: If a command sets a mechanism's default command, the previous default command will be restored when the command exits. The parent command also inherits ownership of that mechanism, even when the default command isn't running. Normal interruption rules apply, so the parent will be interrupted if an external command of the same or higher priority is scheduled that requires the mechanism. + +Command scope is useful for temporary controls. For example, an aiming command can create a binding that fires only while the robot is actively aimed at the target. Once the aiming command completes or is canceled, the binding disappears and any command it started is canceled. + +.. tab-set-code:: + + ```java + public Command sweepAndScore(Robot robot) { + return Command.noRequirements(coroutine -> { + // This binding only exists while sweepAndScore is running + intakeTrigger.onTrue(robot.intake.intake()); + + coroutine.await(robot.drive.followPath("SweepPath")); + }).named("Sweep and Score"); + } + ``` + +## The OpMode Scope + +The OpMode scope is tied to the current robot mode (e.g., Autonomous, Teleop, Utility). Resources created while an OpMode is active are scoped to that mode. + +When the robot transitions to a different mode, all commands and bindings scoped to the previous OpMode are automatically canceled and removed. This prevents an autonomous command from continuing to run into teleop, for example. + +OpMode scope is where mode-specific setup belongs. Autonomous path commands, autonomous-only safety bindings, and autonomous default commands can be created in an autonomous OpMode without needing manual cleanup in teleop. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Command; + import org.wpilib.command3.Trigger; + import org.wpilib.command3.button.RobotModeTriggers; + + @Autonomous + public class SweepAuto implements OpMode { + public SweepAuto(Robot robot) { + // Start the intake stowed + robot.intake.setDefaultCommand(robot.intake.stow()); + + // Once the robot is enabled, start following a sweep path through the + // left trench, into the neutral zone, then back over the bump. + // When we return to the alliance zone, aim at the hub and start shooting. + RobotModeTriggers.enabled().onTrue(sweepAndScore(robot)); + } + } + ``` + +## The Global Scope + +The global scope is the widest scope and is active for the entire duration of the robot program. + +.. warning:: + + The global scope is used only when code is running outside of a command and when the robot program hasn't received an OpMode selection from the driverstation. WPILib only guarantees the latter condition in the main robot class constructor and field initialization, and any code called by it (often Mechanism class constructions). Any code called in robot mode methods such as ``robotPeriodic()`` may be in either the global or OpMode scope depending on when the robot connects to the driverstation and when an OpMode selection is made there. + +Global resources are never automatically cleaned up by the scheduler. Use the global scope for things that should always be available, such as default commands for mechanisms, driver controls, and basic safety bindings. Avoid putting mode-specific behavior in global scope unless it explicitly checks the current mode or enable state. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Command; + import org.wpilib.command3.Trigger; + import org.wpilib.framework.TimedRobot; + + public class Robot extends TimedRobot { + public Robot() { + // GLOBAL SCOPE: These are always active + arm.setDefaultCommand(arm.holdPosition()); + driverController.a().onTrue(arm.up()); + } + } + ``` diff --git a/source/docs/software/commandbased/commands-v3/state-machines.rst b/source/docs/software/commandbased/commands-v3/state-machines.rst new file mode 100644 index 0000000000..6e8d4ebbf0 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/state-machines.rst @@ -0,0 +1,91 @@ +# State Machines with Commands + +The `StateMachine `__ class provides a way to define complex behavior as a series of states and transitions. Each state in a state machine runs a single ``Command``, and transitions define when the state machine should move from one state to another. + +State machines are commands themselves, so they have a name and can be scheduled just like any other command. They can even be used as states in other state machines. However, state machines do **not** have any requirements of their own: the active state command owns the mechanisms it requires. This means a state machine can move between states that use different mechanisms without owning all of them for the entire lifetime of the machine, but also means that a state machine cannot be used as a default command. + +## Defining a State Machine + +To define a state machine, create an instance of ``StateMachine``, add all of its states, choose an initial state, and then add transitions. Defining states first makes global transitions easier to reason about because ``switchFromAny()`` without arguments only applies to states that already exist. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.StateMachine; + + StateMachine sm = new StateMachine("Example State Machine"); + + // 1. Define all states + var idleState = sm.addState(arm.idle()); + var upState = sm.addState(arm.up()); + var downState = sm.addState(arm.down()); + + // 2. Define transitions + idleState.switchTo(upState).when(driverController.y()); + idleState.switchTo(downState).when(driverController.a()); + + upState.switchTo(idleState).whenComplete(); + downState.switchTo(idleState).whenComplete(); + + // 3. Set the initial state + sm.setInitialState(idleState); + ``` + +.. note:: + Calling `setInitialState() `__ is **required** - otherwise the state machine wouldn't know where to start. If you forget to set an initial state, the WPILib compiler plugin will detect it and issue an error. + +## State Machine States + +Each state is a wrapper around a ``Command``. When the state machine enters a state, it schedules the associated command. When the state machine transitions away from a state, the command is canceled. + +If an external command is scheduled that conflicts with any mechanisms owned by the currently running state command, the state machine command will be interrupted. The parent-child relationships section in :doc:`creating-commands` goes into more detail on how interruptions work. + +If a state's command finishes and no completion transition is configured, the state machine exits. Use ``whenComplete()`` when a finished state should automatically move to another state. Use ``whenCompleteAnd(condition)`` when a finished state should choose a next state only if some condition is also true. + +You can also add enter and exit callbacks to states: + +.. tab-set-code:: + + ```java + upState.onEnter(() -> System.out.println("Entering UP state")); + upState.onExit(() -> System.out.println("Exiting UP state")); + ``` + +Enter callbacks run immediately after the state's command is scheduled. Exit callbacks run immediately before the state's command is canceled during a transition, or immediately after it completes naturally. If an enter callback schedules commands, those commands are scoped to the lifetime of the state machine, not to the lifetime of just that state. + +## Transitions + +Transitions define the movement between states. They are checked every loop cycle while the current state's command is running. + +- `switchTo(targetState).when(condition) `__: Transitions to the target state when the condition becomes true. +- `switchTo(targetState).whenComplete() `__: Transitions to the target state when the current state's command finishes. +- `exitStateMachine().when(condition) `__: Finishes the state machine when the condition becomes true. + +Conditional transitions are treated as rising-edge conditions to prevent a state from repeatedly transitioning to itself in the same scheduler cycle. If multiple transitions from the same state become true in the same loop, the first transition that was declared wins and the rest are ignored. Transitions created with ``when(condition)`` do not fire for one-shot states that complete without yielding; use ``whenComplete()`` or ``whenCompleteAnd(...)`` for those states. + +You can also define transitions for multiple states at once, which can help make your code more readable. + +.. tab-set-code:: + + ```java + sm.switchFromAny(upState, downState).to(idleState).when(driverController.b()); + ``` + +### Global Transitions with `switchFromAny()` + +If you call `switchFromAny() `__ without any arguments, it creates a transition that applies to **all** states in the state machine. This is useful for "global" transitions, such as returning to an initial or home state from anywhere in the state graph. + +.. tab-set-code:: + + ```java + // Any state will transition to idle if the X button is pressed + sm.switchFromAny().to(idleState).when(driverController.x()); + + // Any state will exit the state machine if a safety sensor is tripped + sm.switchFromAny().toExitStateMachine().when(safetySensor::get); + ``` + +.. warning:: + `switchFromAny()` with no arguments only applies to the states that have **already been defined** on the state machine at the time the method is called. Any states added with `addState() `__ *after* the call to `switchFromAny()` will not have this transition applied to them. + + For this reason, it is recommended to add all of your states first, and then define transitions after all states have been added. diff --git a/source/docs/software/commandbased/commands-v3/structuring-your-project.rst b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst new file mode 100644 index 0000000000..a3a54ca7eb --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst @@ -0,0 +1,49 @@ +# Structuring Your Project + +While WPILib and the commands v3 framework are flexible and don't force code to be written in a specific way, we recommend a standard structure to make code easier to understand (both for programmers and :term:`CSA` volunteers). + +1. Write a single ``Robot`` class that owns the mechanisms and driver controls. +2. Write separate classes that implement ``Mechanism`` to represent each mechanism on the robot. +3. Use OpMode classes to define OpMode-specific behaviors. +4. Any commands that require a single mechanism should be written as ``Command`` methods in that mechanism's class. +5. Any commands that require multiple mechanisms should be written as ``Command`` methods in a related helper class. + +## A Single Robot Class + +WPILib needs a single entry point to start your program. Use a ``Robot`` class that extends from ``OpModeRobot``. Any behavior that should always be active - regardless of OpMode - should be defined here, such as default commands or trigger bindings. + +Mechanisms should be declared as ``public final`` fields in the robot class and initialized in the field declaration or in the constructor. The former makes the code a little more concise, while the latter allows for flexibility if different mechanism implementations exist. + +.. remoteliteralinclude:: https://raw.githubusercontent.com/wpilibsuite/allwpilib/main/wpilibjExamples/src/main/java/org/wpilib/examples/rebuiltcmdv3/Robot.java + +## Mechanism Classes + +Each physical mechanism on a robot should have a corresponding class in the codebase. These are typically in the ``first.robot.mechanisms`` package. + +Mechanisms are often a combination of multiple actuators, such as a slapdown intake with one actuator or set of actuators to run intake rollers and a separate actuator that extends and retracts the intake. It's usually simpler for each independent actuator to have its own ``Mechanism`` class; if they're only used in the context of a larger mechanism, they can still be separate classes, but only used by a single encompassing mechanism. + +In this example of a slapdown intake, there could be three classes: + +1. ``IntakeRoller``, for controlling just the rollers +2. ``IntakeWrist``, for controlling the deployment of the intake +3. ``Intake``, which combines both the rollers and the wrist + +.. remoteliteralinclude:: https://raw.githubusercontent.com/wpilibsuite/allwpilib/main/wpilibjExamples/src/main/java/org/wpilib/examples/rebuiltcmdv3/mechanisms/Intake.java +.. remoteliteralinclude:: https://raw.githubusercontent.com/wpilibsuite/allwpilib/main/wpilibjExamples/src/main/java/org/wpilib/examples/rebuiltcmdv3/mechanisms/IntakeRoller.java +.. remoteliteralinclude:: https://raw.githubusercontent.com/wpilibsuite/allwpilib/main/wpilibjExamples/src/main/java/org/wpilib/examples/rebuiltcmdv3/mechanisms/IntakeWrist.java + +## OpMode Classes + +OpMode classes let you group mode-specific logic together in one place without cluttering the main robot class. Because the command scheduler automatically scopes everything you do in the OpMode class to that OpMode, you don't need to worry about logic specific to that OpMode leaking out and still running when modes change. Commands that are only used in a specific OpMode can be defined in that OpMode class, too. + +OpMode constructors are generally all that's needed. WPILib will automatically call them when that mode is selected on the driver station, passing in the main ``Robot`` object if the constructor accepts it. + +.. remoteliteralinclude:: https://raw.githubusercontent.com/wpilibsuite/allwpilib/main/wpilibjExamples/src/main/java/org/wpilib/examples/rebuiltcmdv3/opmodes/auto/SweepAuto.java + +## Mechanism-level Commands + + + +## Multi-Mechanism Commands + + diff --git a/source/docs/software/commandbased/commands-v3/telemetry.rst b/source/docs/software/commandbased/commands-v3/telemetry.rst new file mode 100644 index 0000000000..84b9bb7244 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/telemetry.rst @@ -0,0 +1,122 @@ +# Telemetry with Commands + +The commands library provides built-in support for telemetry and logging. This allows you to monitor the state of your commands and mechanisms in real time, which is invaluable for debugging unexpected cancellations, priority conflicts, stuck commands, and mode-transition cleanup. + +## Protobuf Serialization + +The ``Scheduler`` and its running commands can be serialized using Google Protocol Buffers (Protobuf). This is the primary way that command state is sent to external tools like AdvantageScope or the WPILib Dashboards. + +The ``Scheduler`` implements the ``ProtobufSerializable`` interface, which means it can be sent over NetworkTables and logged using the ``ProtobufLogEntry`` API. + +Scheduler state is a snapshot. It answers questions like "which commands are running right now?" and "which mechanisms do they require?" Scheduler events are a timeline. They answer questions like "why did this command stop?" and "what interrupted it?" In practice, teams often want both. + +## Scheduler Events + +The ``Scheduler`` emits events whenever something significant happens, such as a command being scheduled, mounted, yielding, completing, or being canceled. You can register an event listener to respond to these events via ``Scheduler.getDefault().addEventListener(Consumer)``. + +Event listeners run as part of scheduler processing, so they should be short and nonblocking. Logging a small message is fine. Doing expensive formatting, file loading, network calls, or long calculations in an event listener can delay the scheduler just like expensive command code can. + +### Event Types + +All events implement the ``SchedulerEvent`` interface and include a ``timestampMicros()`` (measured in microseconds since robot start). + +* **Scheduled**: A command was added to the scheduler's "pending" set. This happens when a trigger is activated, a default command is queued, or ``Scheduler.schedule()`` is called manually. +* **Mounted**: A running command's coroutine has been mounted and is about to execute until it yields or completes. This occurs every scheduler cycle for each running command. +* **Yielded**: A running command called ``coroutine.yield()`` (or another yielding method like ``wait()``). It will remain in the running set and resume in the next cycle. +* **Completed**: A command finished its execution naturally (the ``run()`` method returned). +* **CompletedWithError**: A command encountered an unhandled exception. The event includes the ``Throwable`` error that caused the failure. An event listener can log this event, but the error will still be thrown and cause the program to crash. +* **Interrupted**: A command was interrupted by another command. This event includes the ``interrupter`` command that caused the interruption. +* **Canceled**: A command was removed from the scheduler without finishing naturally. This happens due to an interruption, a manual ``Scheduler.cancel()`` call, or because its enclosing scope exited. + +### Event Ordering and Co-occurrence + +Events often occur in specific sequences or together within the same scheduler cycle: + +* **Interruption and Cancellation**: When a command is interrupted, an ``Interrupted`` event is emitted immediately before the ``Canceled`` event. +* **Initial Run**: When a command starts for the first time, you will see a ``Scheduled`` event, followed by a ``Mounted`` event when the command first gets CPU time. +* **Natural Completion**: A command that finishes naturally will emit a ``Completed`` event. It does **not** emit a ``Canceled`` event. +* **Errors**: If a command throws an exception, it emits a ``CompletedWithError`` event and is removed from the scheduler. It does **not** emit a ``Canceled`` event. + +Do not treat every ``Canceled`` event as a bug. Commands are canceled when they are interrupted by newer or higher-priority commands, when their enclosing scope exits, when a ``whileTrue`` binding goes false, or when user code cancels them manually. The surrounding events and the command requirements usually tell you which case happened. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.command3.SchedulerEvent; + + Scheduler.getDefault().addEventListener(event -> { + if (event instanceof SchedulerEvent.Scheduled e) { + System.out.println("Command " + e.command().name() + " was scheduled"); + } else if (event instanceof SchedulerEvent.Mounted e) { + // Mounted events occur every cycle - we might not want to log them all! + // System.out.println("Command " + e.command().name() + " mounted"); + } else if (event instanceof SchedulerEvent.Yielded e) { + // System.out.println("Command " + e.command().name() + " yielded"); + } else if (event instanceof SchedulerEvent.Completed e) { + System.out.println("Command " + e.command().name() + " completed naturally"); + } else if (event instanceof SchedulerEvent.CompletedWithError e) { + System.err.println("Command " + e.command().name() + " failed with error: " + e.error()); + } else if (event instanceof SchedulerEvent.Interrupted e) { + System.out.println("Command " + e.command().name() + " was interrupted by " + e.interrupter().name()); + } else if (event instanceof SchedulerEvent.Canceled e) { + System.out.println("Command " + e.command().name() + " was canceled"); + } + }); + ``` + +## Data Logging + +While printing to the console is useful for quick debugging, it is not recommended for persistent storage or deep analysis. For these cases, you should use the standard WPILib data logging APIs to save telemetry to a ``.wpilog`` file on the robot. + +### Logging Scheduler Events + +You can log individual scheduler events to a data log using a ``StringLogEntry``. This is particularly useful for tracking the exact sequence of command lifecycle events during a match. The event stream is often the fastest way to explain a command that "randomly stopped": look for an ``Interrupted`` event, then inspect the interrupter command and the mechanisms both commands required. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.command3.SchedulerEvent; + import org.wpilib.datalog.StringLogEntry; + import org.wpilib.system.DataLogManager; + + // Create a log entry for scheduler events + StringLogEntry eventLog = new StringLogEntry(DataLogManager.getLog(), "SchedulerEvents"); + + // Register a listener to log every event + Scheduler.getDefault().addEventListener(event -> { + // Log the string representation of the event + eventLog.append(event.toString()); + }); + ``` + +### Logging Scheduler State + +Because the ``Scheduler`` class implements ``ProtobufSerializable``, you can log the entire state of the scheduler, including running commands and their mechanisms, using a ``ProtobufLogEntry``. This allows tools like AdvantageScope to visualize the state of the command scheduler over time. + +Because the scheduler state changes every loop cycle, you should append the current state to the log in your ``robotPeriodic`` method. + +.. tab-set-code:: + + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.datalog.ProtobufLogEntry; + import org.wpilib.system.DataLogManager; + import org.wpilib.framework.TimedRobot; + + public class Robot extends TimedRobot { + // Create a log entry for the scheduler state + private final ProtobufLogEntry schedulerLog = + ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto); + + @Override + public void robotPeriodic() { + // Run the scheduler + Scheduler.getDefault().run(); + + // Log the current state of the scheduler + schedulerLog.append(Scheduler.getDefault()); + } + } + ``` diff --git a/source/docs/software/commandbased/commands-v3/triggers.rst b/source/docs/software/commandbased/commands-v3/triggers.rst new file mode 100644 index 0000000000..cedd807310 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/triggers.rst @@ -0,0 +1,142 @@ +# Command Triggers + +Triggers are the primary way to start commands in response to external events, such as a button press, a sensor value reaching a threshold, or a specific robot state. A trigger represents a true/false signal that is polled by an event loop. By default, that event loop is polled during ``Scheduler.run()``. + +Triggers cache their signal when they are polled. Calling ``getAsBoolean()`` reads the most recently polled value, not necessarily the live value of the underlying button or sensor at that exact instant. This is what lets triggers reliably detect rising and falling edges within a scheduler cycle. + +## Creating Triggers + +A ``Trigger`` is created by providing a ``BooleanSupplier`` (a function that returns ``true`` or ``false``) or by combining existing triggers. + +.. tab-set-code:: + + ```java + // A trigger for a gamepad button + Trigger button = xboxController.a(); + + // A trigger for a limit switch + Trigger limitSwitch = new Trigger(limitSwitch::get); + + // A trigger for a complex condition by combining two triggers + Trigger isReady = new Trigger(arm::isAtTarget).and(shooter::isAtSpeed); + ``` + +## Trigger Bindings + +Once you have a trigger, you can bind commands to it. There are several types of bindings that determine how the command responds to changes in the trigger's state. + +### State-Based Bindings + +Triggers implement the Java ``BooleanSupplier`` interface, making them compatible with any method that accepts a boolean condition, such as ``Coroutine.waitUntil``. The binding methods below schedule or cancel commands based on the trigger's cached signal and its previous cached signal. + +* ``onTrue(Command)``: Schedules the command when the trigger transitions from ``false`` to ``true`` (a *rising edge*). The command runs until it finishes or is interrupted, even if the trigger signal becomes ``false``. +* ``onFalse(Command)``: Schedules the command when the trigger transitions from ``true`` to ``false`` (a *falling edge*). The command runs until it finishes or is interrupted, even if the trigger signal becomes ``true``. +* ``whileTrue(Command)``: Schedules the command on a rising edge and cancels it on a falling edge. If the command stops while the trigger is still ``true``, it is **not** restarted. +* ``whileFalse(Command)``: Schedules the command on a falling edge and cancels it on a rising edge. If the command stops while the trigger is still ``false``, it is **not** restarted. + +Use ``onTrue`` and ``onFalse`` for commands that should start once and then manage their own lifetime. Use ``whileTrue`` and ``whileFalse`` for commands whose lifetime should be tied to the signal. For example, "move while the bumper is held" is a ``whileTrue`` binding, while "start an intake sequence when the bumper is pressed" is usually an ``onTrue`` binding. + +### Continuous/Retry Bindings + +* ``retryWhileTrue(Command)``: Like ``whileTrue``, but if the command finishes while the trigger is still ``true``, it is immediately restarted. +* ``retryWhileFalse(Command)``: Like ``whileFalse``, but restarts if the command finishes while the trigger is still ``false``. + +Retry bindings continuously attempt to schedule their command while the signal remains in the requested state. If the command ends naturally, it will be started again. If it was interrupted by another same-priority command that requires the same mechanism, the retry binding may immediately schedule it again and interrupt the would-be interrupter. Use retry bindings when repeated attempts are intentional, not as a default replacement for ``whileTrue``. + +### Toggle Bindings + +* ``toggleOnTrue(Command)``: Schedules the command on a ``false`` to ``true`` transition, and cancels it on the next ``false`` to ``true`` transition. +* ``toggleOnFalse(Command)``: Schedules the command on a ``true`` to ``false`` transition, and cancels it on the next ``true`` to ``false`` transition. + +Toggle bindings are best for operator controls where the driver explicitly switches a behavior on and off. They are usually a poor fit for safety behavior because the command's lifetime depends on remembering how many edges have occurred. + +### Multi-Press Bindings + +The ``multiPress(int, Time)`` binding allows commands to be bound when a trigger signal has had a minimum number of rising edges within a specific time period. + +For example, ``Trigger.multiPress(2, Seconds.of(1.5))`` will go high when there have been **at least** two rising edges within the last 1.5 seconds, and will go low when there are fewer. This can be used with ``onTrue`` to respond to a double-press. The multi-press trigger remains high as long as enough presses are still inside the time window; it is not only high on the final button press. + +## Combining Triggers + +Triggers can be combined using standard boolean operators to create more complex conditions. These operations create new trigger objects and do not modify the originals. + +* ``Trigger.and(BooleanSupplier)``: High when **both** signals are high. +* ``Trigger.or(BooleanSupplier)``: High when **either** signal is high. +* ``Trigger.negate()``: High when the original signal is low. + +.. tab-set-code:: + + ```java + Trigger bothButtons = buttonA.and(buttonB); + Trigger eitherButton = buttonA.or(buttonB); + Trigger notButton = buttonA.negate(); + ``` + +## Modifying Trigger Behavior + +You can also modify how a trigger responds to the underlying condition: + +* ``debounce(Time duration)``: Creates a trigger that only becomes ``true`` if the original condition is ``true`` for at least the specified duration. +* ``risingEdge()``: Creates a trigger that is only ``true`` for a single loop cycle when the original condition transitions from ``false`` to ``true``. +* ``fallingEdge()``: Creates a trigger that is only ``true`` for a single loop cycle when the original condition transitions from ``true`` to ``false``. + +Because ``risingEdge()`` and ``fallingEdge()`` are only high for one scheduler cycle, bind commands to them with ``onTrue``. A ``whileTrue`` binding on a one-cycle edge trigger will schedule the command and then cancel it on the next cycle. + +## Scopes + +Trigger bindings exist in *scopes*. When a scope exits, any trigger binding that was created in that scope will be removed and the commands attached to that binding will be canceled. This is a critical safety feature of the library. + +1. **Global Scope**: Bindings created in the ``Robot`` constructor or methods called by it. These are always active. +2. **OpMode Scope**: Bindings created while a specific OpMode is running. These are automatically removed when the OpMode ends. +3. **Command Scope**: Bindings created inside a running command. These are removed when the command finishes or is canceled. + +See :doc:`scopes` for more details. + +## Game Controller Triggers + +The ``org.wpilib.command3.button`` package provides specialized classes for creating triggers from game controllers, such as Xbox and PS5 (DualSense) controllers. These classes provide methods that return ``Trigger`` objects for every button, d-pad direction, and trigger on the controller. Prefer these named methods over raw button numbers when possible; the resulting robot code is easier to read and easier to audit during an event. + +For a full list of available controller classes and their methods, see the `org.wpilib.command3.button `__ Javadoc. + +### Advanced Controller Triggers + +You can also use axis values (like the analog sticks or analog triggers) to create triggers by using the ``axisGreaterThan``, ``axisLessThan``, or ``axisMagnitudeGreaterThan`` methods, or by providing a custom ``BooleanSupplier``. Axis-based triggers usually need a threshold and sometimes a debounce so small joystick noise does not repeatedly schedule and cancel commands. + +.. tab-set-code:: + + ```java + // Trigger when the left Y axis is pushed more than 50% forward + Trigger highThrottle = new Trigger(() -> driverController.getLeftY() > 0.5); + + highThrottle.onTrue(Command.print("High Throttle!")); + ``` + +.. tab-set-code:: + + ```java + import org.wpilib.framework.TimedRobot; + import org.wpilib.command3.button.RobotModeTriggers; + + public class Robot extends TimedRobot { + public Robot() { + // GLOBAL SCOPE: This binding is always active + driverController.a().onTrue(arm.up()); + } + + @Override + public void autonomousInit() { + // OPMODE SCOPE: This binding only exists during autonomous + RobotModeTriggers.autonomous().onTrue(drive.followPath("AutoPath")); + } + } + + // COMMAND SCOPE example + public Command sweepAndScore() { + return Command.noRequirements(coroutine -> { + // This binding only exists while the 'sweepAndScore' command is running + intakeTrigger.onTrue(intake.runOnce()); + + coroutine.await(drive.followPath("SweepPath")); + }).named("Sweep and Score"); + } + ``` diff --git a/source/docs/software/commandbased/commands-v3/troubleshooting.rst b/source/docs/software/commandbased/commands-v3/troubleshooting.rst new file mode 100644 index 0000000000..02453ae3b8 --- /dev/null +++ b/source/docs/software/commandbased/commands-v3/troubleshooting.rst @@ -0,0 +1,159 @@ +# Troubleshooting Commands + +## Common Errors + +### Greedy Loops (Compile-time) + +If you write a ``while`` loop in a command that is missing a ``coroutine.yield()`` call, the WPILib compiler plugin will issue an error. This is because a loop that never yields will starve the rest of the robot program, preventing other commands from running and sensor data from being updated. + +The fix is not "avoid loops"; loops are expected in commands v3. The fix is to make sure every periodic loop reaches a yielding method. ``yield()``, ``wait()``, ``waitUntil()``, ``await()``, and ``park()`` all give control back to the scheduler. + +**Example of an error:** + +```java +public Command greedyCommand() { + return run(coroutine -> { + while (true) { + // Error: missing call to coroutine.yield()! + doSomething(); + } + }); +} +``` + +**How to fix it:** Add a call to ``coroutine.yield()`` (or another yielding method like ``wait()`` or ``waitUntil()``) inside the loop. + +```java +public Command healthyCommand() { + return run(coroutine -> { + while (true) { + doSomething(); + coroutine.yield(); // Fixed! + } + }); +} +``` + +### Resource Conflicts + +If two commands require the same mechanism, the command with the higher priority will win. If they have the same priority, the newly scheduled command will interrupt the existing one. + +If you see a command being unexpectedly canceled, check if another command that requires the same mechanism is being scheduled at the same time. You can use the scheduler's :doc:`telemetry` to see which commands are running and which mechanisms they require. + +This is usually not a scheduler bug. It is the requirements system doing its job. Look for an ``Interrupted`` event in the scheduler event log; the event names both the command that was interrupted and the command that interrupted it. If the interrupter is a default command, check its priority. Default commands should normally be lower priority than ordinary commands so they do not block expected behavior. + +If a command directly manipulates another mechanism's private hardware instead of scheduling one of that mechanism's commands, the scheduler cannot see the conflict. Keep actuator fields private and expose command-returning factory methods so conflicts are visible. + +### Incomplete Builder Chains + +The command builder uses a staged approach to ensure that all required attributes (requirements, logic, and name) are provided. If you forget one of these, you will get a compile-time error because the resulting object will not be a ``Command``. + +**Example of an error:** + +```java +// Error: This returns a builder stage, not a Command! +Command cmd = arm.run(coroutine -> { ... }); +``` + +**How to fix it:** Ensure you call ``.named("...")`` at the end of your builder chain to produce a ``Command`` object. + +```java +// Fixed: .named() completes the builder and returns a Command +Command cmd = arm.run(coroutine -> { ... }).named("My Command"); +``` + +The staged builder is intentionally strict. A command without a name is hard to debug, and a command without declared requirements can bypass the ownership system. Treat the compile error as a sign that the command definition is incomplete, not as a place to add casts or change variable types until it compiles. + +### Command Does Not Restart + +Scheduling the same ``Command`` instance while it is already scheduled or running has no effect. The scheduler will not rewind the coroutine, rerun the command from the beginning, or create a second copy of the same command instance. + +```java +Command armUp = arm.up(); + +coroutine.fork(armUp); +coroutine.fork(armUp); // No effect; armUp is already scheduled or running. +``` + +**How to fix it:** If you need a fresh run later, call the mechanism factory method again to create a new command instance, or wait until the existing command has completed before awaiting it again. If the behavior should restart automatically while a trigger remains true, use ``retryWhileTrue`` or ``retryWhileFalse`` intentionally. + +### Trigger Binding Goes Away + +Trigger bindings are scoped to the place where they were created. A binding created inside a command is removed when that command exits. A binding created inside an OpMode is removed when that OpMode exits. + +This is usually exactly what you want, but it can be surprising if a binding is created inside a short-lived command: + +```java +public Command temporaryBinding() { + return Command.noRequirements(coroutine -> { + driverController.a().onTrue(arm.up()); + }).named("Temporary Binding"); +} +``` + +The command above finishes immediately, so the binding is cleaned up immediately. **How to fix it:** create long-lived controls in global or OpMode setup, or keep the command alive with ``coroutine.park()`` or another yielding wait if the binding is meant to exist only while that command is running. + +### State Machine Missing Initial State + +When defining :doc:`state-machines`, you must call `setInitialState() `__ before the state machine can be used as a command. Forgetting this call will result in a compile-time error. + +**Example of an error:** + +```java +public Command stateMachineExample() { + StateMachine sm = new StateMachine("Example"); + var stateA = sm.addState(arm.up()); + return sm; +} +``` + +**How to fix it:** Call ``setInitialState()`` with the state you want the machine to start in. + +```java +public Command stateMachineExample() { + StateMachine sm = new StateMachine("Example"); + var stateA = sm.addState(arm.up()); + sm.setInitialState(stateA); // Fixed! + return sm; +} +``` + +### State Machine Global Transitions + +If you use `switchFromAny() `__ without arguments to define a global transition, it only applies to states that were added to the state machine **before** the `switchFromAny()` call. + +**Example of an error:** + +```java +StateMachine sm = new StateMachine("My SM"); +var stateA = sm.addState(arm.up()); + +// This transition ONLY applies to stateA! +sm.switchFromAny().toExitStateMachine().when(driverController.b()); + +var stateB = sm.addState(arm.down()); +// stateB will NOT transition when the B button is pressed. +``` + +**How to fix it:** Define your global transitions **after** all states have been added to the state machine. + +```java +StateMachine sm = new StateMachine("My SM"); +var stateA = sm.addState(arm.up()); +var stateB = sm.addState(arm.down()); + +// Fixed: This transition now applies to both stateA and stateB +sm.switchFromAny().toExitStateMachine().when(driverController.b()); +``` + +### State Machine Transition Never Fires + +Transitions created with ``when(condition)`` are checked while the current state's command is running. If the state command is a one-shot command that completes immediately without yielding, there may be no loop cycle where the transition can be checked. + +**How to fix it:** For one-shot states, use ``whenComplete()`` or ``whenCompleteAnd(condition)``. Use ``when(condition)`` for states whose commands yield while they are active. + +### Coroutine Used Outside a Command + +The ``Coroutine`` object passed to command logic is only valid while that command is mounted and running. Storing it in a field and calling it later, or trying to use it from another callback or thread, will throw an ``IllegalStateException``. + +**How to fix it:** Keep coroutine calls inside the command body. If another part of the robot program needs to start behavior, expose a command factory method and schedule the returned command through a trigger or from another command. diff --git a/source/docs/software/commandbased/index.rst b/source/docs/software/commandbased/index.rst index 97aa5a5ea2..e50c032b27 100644 --- a/source/docs/software/commandbased/index.rst +++ b/source/docs/software/commandbased/index.rst @@ -8,3 +8,4 @@ For a collection of example projects using the command-based framework, see :ref :maxdepth: 1 commands-v2/index + commands-v3/index diff --git a/source/docs/software/frc-glossary.rst b/source/docs/software/frc-glossary.rst index c8f29876cc..6e02b1f949 100644 --- a/source/docs/software/frc-glossary.rst +++ b/source/docs/software/frc-glossary.rst @@ -46,6 +46,9 @@ Classical Mechanics The branch of physics which studies and describes the motion of relatively large, relatively slow objects. See [Classical Mechanics](https://en.wikipedia.org/wiki/Classical_mechanics) on Wikipedia for more info. + Coroutine + A function that can be paused to be resumed later. + COTS Commercial off the shelf - a standard (i.e. not custom order) part commonly available from a vendor to all teams for purchase. @@ -100,6 +103,9 @@ FLL FIRST Lego League - Introduces science, technology, engineering, and math (STEM) to children ages 4-16 through fun, exciting hands-on learning. + factory method + A software design pattern where a method is used to create and return an object, rather than calling a constructor directly. In the commands framework, factory methods on mechanisms are the preferred way to create commands. See [factory method](https://en.wikipedia.org/wiki/Factory_method_pattern) on Wikipedia for more info. + floating point A method for approximating real numbers in computer-based arithmetic, using a fixed precision integer scaled by an integer exponent. Typically computer systems support both "single" precision (32-bit storage) and "double" precision (64-bit storage) floating point values, as defined by IEEE 754. @@ -154,12 +160,18 @@ KOP chassis The KOP contains a drive base (chassis) distributed to every team (that did not opt out) as part of the :term:`KOP`. For the 2026 season, the KOP chassis is the [AM14U6](https://www.andymark.com/products/am14u6-6-wheel-drop-center-robot-drive-base-2025-frc-kit-of-parts-drive-base). + lambda functions + An anonymous function that can be passed as an argument to another function or stored in a variable. In Java, lambda functions are commonly used with the commands frameworks to define the logic that a command executes. See [lambda expressions](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html) on the Java documentation site for more info. + LED Light-Emitting Diode - a semiconductor device that emits light when current flows through it. Used on multiple robot parts to convey the status of the device. mass the amount of matter in a physical object. Objects with more mass will resist changes in motion more than objects with less mass. See [mass](https://en.wikipedia.org/wiki/Mass) on Wikipedia for more info. + Mechanism + A software representation of physical hardware on a robot, such as a drivetrain or an arm. Mechanisms in the commands v3 framework are used to manage resource ownership, ensuring that only one command can control a particular piece of hardware at a time. + moment of inertia The property of an object that describes both how much mass it has, and how that mass is distributed relative to a certain axis of rotation. Objects with higher moments of inertia resist changes in rotational motion more than objects with lower moments of inertia. Increasing the moment of inertia is accomplished by adding more mass, or moving the mass further away from the axis of rotation. See [moment of inertia](https://en.wikipedia.org/wiki/Moment_of_inertia) on Wikipedia for more info. diff --git a/source/docs/yearly-overview/2026-game-data.rst b/source/docs/yearly-overview/2026-Game-Data.rst similarity index 100% rename from source/docs/yearly-overview/2026-game-data.rst rename to source/docs/yearly-overview/2026-Game-Data.rst diff --git a/source/docs/yearly-overview/index.rst b/source/docs/yearly-overview/index.rst index 7d242aed53..93eb9096c6 100644 --- a/source/docs/yearly-overview/index.rst +++ b/source/docs/yearly-overview/index.rst @@ -6,5 +6,5 @@ known-issues yearly-changelog returning-quickstart - 2026-game-data + 2026-Game-Data removed-features diff --git a/source/redirects.txt b/source/redirects.txt index 7f4075a167..72f6ccecf8 100644 --- a/source/redirects.txt +++ b/source/redirects.txt @@ -309,7 +309,6 @@ "docs/software/wpilib-tools/choreo/index.rst" "docs/software/pathplanning/choreo/index.rst" "docs/networking/networking-introduction/om5p-ac-radio-modification.rst" "docs/zero-to-robot/step-3/radio-programming.rst" "docs/software/driverstation/programming-radios-for-fms-offseason.rst" "docs/zero-to-robot/step-3/radio-programming.rst" -"docs/yearly-overview/2026-Game-Data.rst" "docs/yearly-overview/2026-game-data.rst" "docs/software/hardware-apis/sensors/counters.rst" "docs/yearly-overview/removed-features.rst" "docs/software/hardware-apis/sensors/ultrasonics-software.rst" "docs/yearly-overview/removed-features.rst" "docs/hardware/sensors/ultrasonics-hardware.rst" "docs/yearly-overview/removed-features.rst" From 737cbe9197ee6299d0b8e8172109566115f46c53 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Sun, 2 Aug 2026 21:25:09 -0400 Subject: [PATCH 02/12] Fix scopes ref and swap clause order --- .../software/commandbased/commands-v3/making-commands-run.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/docs/software/commandbased/commands-v3/making-commands-run.rst b/source/docs/software/commandbased/commands-v3/making-commands-run.rst index 0050c4e926..159610e2b5 100644 --- a/source/docs/software/commandbased/commands-v3/making-commands-run.rst +++ b/source/docs/software/commandbased/commands-v3/making-commands-run.rst @@ -32,7 +32,7 @@ For more detailed information on trigger types, combining triggers, and advanced ## Manually running a command -While triggers are the most common way to start commands in response to external events, you often need to start a command directly from within another command or when an OpMode starts. This is done using the ``Coroutine.fork()`` or ``Coroutine.await()`` methods, or rarely a direct call to ``Scheduler.getDefault().schedule()``. The scheduler will ensure that the command does not outlive the :doc:`scope` that scheduled it, regardless of the method used. +While triggers are the most common way to start commands in response to external events, you often need to start a command directly from within another command or when an OpMode starts. This is done using the ``Coroutine.fork()`` or ``Coroutine.await()`` methods, or rarely a direct call to ``Scheduler.getDefault().schedule()``. Regardless of the method used, the scheduler will ensure that the command does not outlive the :doc:`scope ` that scheduled it. ### Forking (Asynchronous) From 2c4a548ca996f566ff96a0b67564ffb99506605e Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:18:34 -0400 Subject: [PATCH 03/12] Update code examples Removed a lot of unnecessary tab-set-code and replaced others with manual tab-sets with descriptive tab labels --- .../commands-v3/creating-commands.rst | 344 +++++---- .../commandbased/commands-v3/index.rst | 34 +- .../commands-v3/lambda-functions.rst | 84 +++ .../commands-v3/making-commands-run.rst | 135 ++-- .../commandbased/commands-v3/mechanisms.rst | 94 ++- .../commands-v3/migration-guide.rst | 672 ++++++++++++------ .../commandbased/commands-v3/scopes.rst | 80 +-- .../commands-v3/state-machines.rst | 66 +- .../commands-v3/structuring-your-project.rst | 79 ++ .../commandbased/commands-v3/telemetry.rst | 84 ++- .../commandbased/commands-v3/triggers.rst | 116 +-- 11 files changed, 1084 insertions(+), 704 deletions(-) diff --git a/source/docs/software/commandbased/commands-v3/creating-commands.rst b/source/docs/software/commandbased/commands-v3/creating-commands.rst index c6f7dcf0aa..8d19314d3e 100644 --- a/source/docs/software/commandbased/commands-v3/creating-commands.rst +++ b/source/docs/software/commandbased/commands-v3/creating-commands.rst @@ -21,88 +21,82 @@ The requirement system can only protect hardware that is controlled through comm Public sensor accessors and triggers are fine, and are often useful. Reading a sensor does not fight with a command that owns the mechanism, but directly commanding an actuator does. The goal is not to hide all information from the rest of the robot program; the goal is to make every hardware-changing action pass through the scheduler's ownership rules to ensure every mechanism is only trying to do one thing at a time. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Command; - import org.wpilib.command3.Mechanism; - import org.wpilib.hardware.motor.PWMSparkMax; - - public class ExampleArm implements Mechanism { - // This motor controller is declared private to guarantee that it can't be used - // dangerously, outside of the command requirements system - private final PWMSparkMax pivotMotor = new PWMSparkMax(1); - - // Triggers can be and are encouraged to be public. They can't control the mechanism, - // and make it easier to coordinate complex actions - public final Trigger isUp = new Trigger(() -> pivotMotor.getPosition() >= 90); - public final Trigger isDown = new Trigger(() -> pivotMotor.getPosition() <= 0); - - public ExampleArm() { - setDefaultCommand(stop()); - } +```java +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.hardware.motor.PWMSparkMax; + +public class ExampleArm implements Mechanism { + // This motor controller is declared private to guarantee that it can't be used + // dangerously, outside of the command requirements system + private final PWMSparkMax pivotMotor = new PWMSparkMax(1); + + // Triggers can be and are encouraged to be public. They can't control the mechanism, + // and make it easier to coordinate complex actions + public final Trigger isUp = new Trigger(() -> pivotMotor.getPosition() >= 90); + public final Trigger isDown = new Trigger(() -> pivotMotor.getPosition() <= 0); + + public ExampleArm() { + setDefaultCommand(stop()); + } - // This method controls the motor directly. - // It's private for the same reason the field is - to prevent dangerous usage - private void stopMotor() { - pivotMotor.setVoltage(0); - } + // This method controls the motor directly. + // It's private for the same reason the field is - to prevent dangerous usage + private void stopMotor() { + pivotMotor.setVoltage(0); + } - // This factory method returns a Command. - // It's public because all hardware control should go through commands - // instead of unsafe method calls. - public Command stop() { - // `run` and `runRepeatedly` will automatically require the mechanism - // so we don't need to manually spell it out every time - return runRepeatedly(this::stopMotor).named("Stop Arm"); - } + // This factory method returns a Command. + // It's public because all hardware control should go through commands + // instead of unsafe method calls. + public Command stop() { + // `run` and `runRepeatedly` will automatically require the mechanism + // so we don't need to manually spell it out every time + return runRepeatedly(this::stopMotor).named("Stop Arm"); + } - public Command up() { - return run(coroutine -> { - pivotMotor.set(0.5); - coroutine.waitUntil(isUp); - pivotMotor.set(0); - }).named("Arm Up"); - } + public Command up() { + return run(coroutine -> { + pivotMotor.set(0.5); + coroutine.waitUntil(isUp); + pivotMotor.set(0); + }).named("Arm Up"); + } - public Command down() { - return run(coroutine -> { - pivotMotor.set(-0.5); - coroutine.waitUntil(isDown); - pivotMotor.set(0); - }).named("Arm Down"); - } + public Command down() { + return run(coroutine -> { + pivotMotor.set(-0.5); + coroutine.waitUntil(isDown); + pivotMotor.set(0); + }).named("Arm Down"); } - ``` +} +``` ## Looping Commands Most commands need to run for more than a single loop cycle. This is done by using a loop (like ``while``) and calling ``coroutine.yield()`` at the end of every loop to allow other commands to run. -.. tab-set-code:: - - ```java - public Command driveForward() { - return run(coroutine -> { - while (distance < 10) { - drive.setSpeed(0.5); - coroutine.yield(); // Required to allow other commands to run! - } - drive.setSpeed(0); - }).named("Drive Forward"); - } - ``` +```java +public Command driveForward() { + return run(coroutine -> { + while (distance < 10) { + drive.setSpeed(0.5); + coroutine.yield(); // Required to allow other commands to run! + } + drive.setSpeed(0); + }).named("Drive Forward"); +} +``` If you have a command that only needs to run the same piece of code every loop cycle, you can use the ``runRepeatedly`` factory method on a mechanism. This method automatically handles the loop and the yield for you. -.. tab-set-code:: - - ```java - public Command stop() { - return runRepeatedly(() -> motor.set(0)).named("Stop"); - } - ``` +```java +public Command stop() { + return runRepeatedly(() -> motor.set(0)).named("Stop"); +} +``` Use ``runRepeatedly`` for simple "do this every scheduler cycle" behavior, such as a default command that continuously applies joystick drive output or holds a motor at zero volts. Use ``run`` when the command has a beginning, a middle, and an end: start the motor, wait for a condition, then stop the motor. If a loop appears in a ``run`` command, that loop must include a call to a yielding method; otherwise, it's a greedy loop and will lock up the robot program. WPILib will report compilation errors any any non-yielding ``while`` loops in command code. @@ -110,44 +104,40 @@ Use ``runRepeatedly`` for simple "do this every scheduler cycle" behavior, such The ``Coroutine`` class provides methods to pause a command until a condition is met. The most basic of these is ``waitUntil(BooleanSupplier)``, which pauses until the given condition returns ``true``. -.. tab-set-code:: - - ```java - public Command waitForButton() { - return run(coroutine -> { - coroutine.waitUntil(driverController.a()); - System.out.println("Button A pressed!"); - }).named("Wait for Button"); - } - ``` +```java +public Command waitForButton() { + return run(coroutine -> { + coroutine.waitUntil(driverController.a()); + System.out.println("Button A pressed!"); + }).named("Wait for Button"); +} +``` #### Timeouts and WaitResult Sometimes, a condition might never be met (for example, if a sensor fails or a mechanism jams). To prevent your robot from getting stuck indefinitely, you can provide a timeout to ``waitUntil``. When a timeout is provided, ``waitUntil`` returns a ``WaitResult`` object that you can use to check whether the condition was met or if the command timed out. -.. tab-set-code:: +```java +import static org.wpilib.units.Units.Seconds; +import org.wpilib.command3.Coroutine; - ```java - import static org.wpilib.units.Units.Seconds; - import org.wpilib.command3.Coroutine; +public Command safeElevatorUp() { + return run(coroutine -> { + coroutine.fork(elevator.up()); - public Command safeElevatorUp() { - return run(coroutine -> { - coroutine.fork(elevator.up()); + // Wait for the elevator to reach the top, but only for 1.25 seconds at most + Coroutine.WaitResult result = coroutine.waitUntil(elevator::atTop, Seconds.of(1.25)); - // Wait for the elevator to reach the top, but only for 1.25 seconds at most - Coroutine.WaitResult result = coroutine.waitUntil(elevator::atTop, Seconds.of(1.25)); + if (result.timedOut()) { + // The elevator took too long! It might be jammed. Bail early. + elevator.setJamAlert(); + return; + } - if (result.timedOut()) { - // The elevator took too long! It might be jammed. Bail early. - elevator.setJamAlert(); - return; - } - - // ... do more things, confident that the elevator is in place - }).named("Safe Elevator Up"); - } - ``` + // ... do more things, confident that the elevator is in place + }).named("Safe Elevator Up"); +} +``` Timeouts are most useful around physical state changes: elevators reaching a height, arms hitting a limit, flywheels reaching speed, or drivetrains arriving at a pose. A timeout should normally lead to an explicit fallback such as stopping the mechanism, retrying a safer action, or exiting the larger routine early. It may be dangerous to ignore a timeout @@ -157,12 +147,10 @@ A command that never yields is called a *one-shot* command. It will be mounted a One-shot commands generally do one very simple thing and immediately exit without taking much time. Good examples of one-shot commands are zeroing a sensor or assigning a new value to a variable. -.. tab-set-code:: - - ```java - Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); - Command.noRequirements(_ -> field = 0).named("Reset Field"); - ``` +```java +Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); +Command.noRequirements(_ -> field = 0).named("Reset Field"); +``` One-shot commands are not bad. They are the right tool for small pieces of immediate work. The important rule is that "does not yield" also means "does not share time". If the action might take a noticeable amount of time, write it as a yielding command or move the expensive work somewhere that will not block robot control. @@ -174,21 +162,19 @@ For more complex logic, you can use the various methods on the ``Coroutine`` obj You can pause a command for a certain amount of time or until a condition is met. -.. tab-set-code:: +```java +public Command waitAndThen() { + return run(coroutine -> { + System.out.println("Starting..."); - ```java - public Command waitAndThen() { - return run(coroutine -> { - System.out.println("Starting..."); + coroutine.wait(Seconds.of(2)); + System.out.println("2 seconds later!"); - coroutine.wait(Seconds.of(2)); - System.out.println("2 seconds later!"); - - coroutine.waitUntil(trigger); - System.out.println("Triggered!"); - }).named("Wait Example"); - } - ``` + coroutine.waitUntil(trigger); + System.out.println("Triggered!"); + }).named("Wait Example"); +} +``` The resolution of ``coroutine.wait()`` is the scheduler loop period. With a 20 ms robot loop, a wait for 1 ms and a wait for 19 ms both resume on a later scheduler cycle, not exactly at the requested timestamp. This is normally fine for robot actions, but it is worth remembering when writing tests or when building routines with very short delays. @@ -196,17 +182,15 @@ The resolution of ``coroutine.wait()`` is the scheduler loop period. With a 20 m If you want to start multiple actions at once, you can use ``coroutine.fork()``. Forking schedules a child command and immediately returns to the parent command. The child then runs alongside the parent until it completes, is canceled, or the parent exits. -.. tab-set-code:: - - ```java - public Command parallelActions() { - return run(coroutine -> { - coroutine.fork(arm.up()); - coroutine.fork(intake.spin()); - coroutine.await(drive.followPath("ScorePath")); - }).named("Parallel Actions"); - } - ``` +```java +public Command parallelActions() { + return run(coroutine -> { + coroutine.fork(arm.up()); + coroutine.fork(intake.spin()); + coroutine.await(drive.followPath("ScorePath")); + }).named("Parallel Actions"); +} +``` Note that forking a command from within a command creates a parent-child relationship with the following properties: @@ -222,19 +206,17 @@ Use ``coroutine.await()`` when the parent needs to wait for a child command befo Consider an autonomous command that forks two sub-tasks: one to control the arm and one to control the intake. -.. tab-set-code:: +```java +public Command autoScore(Robot robot) { + return run(coroutine -> { + // Fork two sibling commands + coroutine.fork(robot.arm.moveUp().named("Arm Task")); + coroutine.fork(robot.intake.spin().named("Intake Task")); - ```java - public Command autoScore(Robot robot) { - return run(coroutine -> { - // Fork two sibling commands - coroutine.fork(robot.arm.moveUp().named("Arm Task")); - coroutine.fork(robot.intake.spin().named("Intake Task")); - - coroutine.await(robot.drive.followPath("ScorePath")); - }).named("Auto Score"); - } - ``` + coroutine.await(robot.drive.followPath("ScorePath")); + }).named("Auto Score"); +} +``` If an external command (like a safety trigger) interrupts the **Arm Task**, the entire **Auto Score** composition (including the **Intake Task** and the path following) will be canceled. @@ -242,22 +224,20 @@ If an external command (like a safety trigger) interrupts the **Arm Task**, the In this example, the parent command forks two siblings that both require the same mechanism. The second sibling will interrupt the first one, but the parent command will continue running. -.. tab-set-code:: - - ```java - public Command siblingConflict(Robot robot) { - return run(coroutine -> { - // Both of these require robot.arm - coroutine.fork(robot.arm.moveUp().named("First Sibling")); - coroutine.fork(robot.arm.moveDown().named("Second Sibling")); - - // "Second Sibling" will interrupt "First Sibling". - // Because they are siblings, the parent command ("siblingConflict") - // and other forked siblings will NOT be canceled. - coroutine.park(); - }).named("Sibling Conflict"); - } - ``` +```java +public Command siblingConflict(Robot robot) { + return run(coroutine -> { + // Both of these require robot.arm + coroutine.fork(robot.arm.moveUp().named("First Sibling")); + coroutine.fork(robot.arm.moveDown().named("Second Sibling")); + + // "Second Sibling" will interrupt "First Sibling". + // Because they are siblings, the parent command ("siblingConflict") + // and other forked siblings will NOT be canceled. + coroutine.park(); + }).named("Sibling Conflict"); +} +``` ### Forking vs. Default Commands @@ -267,40 +247,36 @@ It is important to understand the difference between forking a command and setti **Setting a default command** does not start the command immediately. Instead, it tells the scheduler to run that command whenever no other command is requiring the mechanism. If you want a default command to start immediately, fork or schedule it immediately after assigning it. -.. tab-set-code:: - - ```java - // Forking: runs in the background immediately - coroutine.fork(arm.holdLastPosition()); - - // Move the elevator up; the arm continues to hold position because it was forked - coroutine.await(elevator.up()); +```java +// Forking: runs in the background immediately +coroutine.fork(arm.holdLastPosition()); - // Move the arm down; if an outer scope set a default command for the elevator, - // it runs; otherwise, the elevator is uncommanded - coroutine.await(arm.down()); +// Move the elevator up; the arm continues to hold position because it was forked +coroutine.await(elevator.up()); - // After the arm is down, there's no command in this scope that controls it. - // If an outer scope set a default command, it runs; otherwise, the arm is uncommanded. - ``` +// Move the arm down; if an outer scope set a default command for the elevator, +// it runs; otherwise, the elevator is uncommanded +coroutine.await(arm.down()); -.. tab-set-code:: +// After the arm is down, there's no command in this scope that controls it. +// If an outer scope set a default command, it runs; otherwise, the arm is uncommanded. +``` - ```java - // Setting a default command: it will only start if the arm is uncommanded - arm.setDefaultCommand(arm.holdLastPosition()); +```java +// Setting a default command: it will only start if the arm is uncommanded +arm.setDefaultCommand(arm.holdLastPosition()); - // If you want the new default command to start immediately (if the arm is idle), - // you can fork its getter. - coroutine.fork(arm.getDefaultCommand()); +// If you want the new default command to start immediately (if the arm is idle), +// you can fork its getter. +coroutine.fork(arm.getDefaultCommand()); - // Move the elevator up; the arm continues to hold position - coroutine.await(elevator.up()); +// Move the elevator up; the arm continues to hold position +coroutine.await(elevator.up()); - // Move the arm down; if an outer scope set a default command for the elevator, - // it runs; otherwise, the elevator is uncommanded - coroutine.await(arm.down()); +// Move the arm down; if an outer scope set a default command for the elevator, +// it runs; otherwise, the elevator is uncommanded +coroutine.await(arm.down()); - // Once arm.down() completes, the arm is uncommanded in this scope, - // so its default command (holdLastPosition) starts automatically. - ``` +// Once arm.down() completes, the arm is uncommanded in this scope, +// so its default command (holdLastPosition) starts automatically. +``` diff --git a/source/docs/software/commandbased/commands-v3/index.rst b/source/docs/software/commandbased/commands-v3/index.rst index 2b7dbbdc41..825cdb38cf 100644 --- a/source/docs/software/commandbased/commands-v3/index.rst +++ b/source/docs/software/commandbased/commands-v3/index.rst @@ -73,22 +73,20 @@ Commands prevent conflicting hardware requests from being made by using a requir 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. -.. tab-set-code:: - - ```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"); - } +```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"); } - ``` +} +``` diff --git a/source/docs/software/commandbased/commands-v3/lambda-functions.rst b/source/docs/software/commandbased/commands-v3/lambda-functions.rst index c8489b6456..875acc8b0a 100644 --- a/source/docs/software/commandbased/commands-v3/lambda-functions.rst +++ b/source/docs/software/commandbased/commands-v3/lambda-functions.rst @@ -4,6 +4,90 @@ Lambda functions are a way of passing code to a function for *that* function to 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`` - 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``. +- ``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 ; +} +``` + +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. diff --git a/source/docs/software/commandbased/commands-v3/making-commands-run.rst b/source/docs/software/commandbased/commands-v3/making-commands-run.rst index 159610e2b5..7136973dad 100644 --- a/source/docs/software/commandbased/commands-v3/making-commands-run.rst +++ b/source/docs/software/commandbased/commands-v3/making-commands-run.rst @@ -8,25 +8,23 @@ Each approach describes a different kind of intent. Triggers say "when this sign Triggers allow you to set up automated behavior that runs in response to external events, such as a button press or a sensor reaching a threshold. They are the primary way to start commands outside of compositions. A trigger is checked when its event loop is polled; for the default event loop, this happens during ``Scheduler.run()``. -.. tab-set-code:: +```java +import org.wpilib.command3.Command; +import org.wpilib.command3.Trigger; +import org.wpilib.hardware.discrete.DigitalInput; - ```java - import org.wpilib.command3.Command; - import org.wpilib.command3.Trigger; - import org.wpilib.hardware.discrete.DigitalInput; +public class Robot extends OpModeRobot { + private final DigitalInput lowerLimitSwitch = new DigitalInput(1); - public class Robot extends OpModeRobot { - private final DigitalInput lowerLimitSwitch = new DigitalInput(1); + // This trigger will be checked every time the scheduler runs. + public final Trigger atMinLimit = new Trigger(() -> lowerLimitSwitch.get()); - // This trigger will be checked every time the scheduler runs. - public final Trigger atMinLimit = new Trigger(() -> lowerLimitSwitch.get()); - - public Robot() { - // Bind a command to execute whenever the minimum limit is reached. - atMinLimit.onTrue(Command.print("Min limit reached!").named("Limit Message")); - } + public Robot() { + // Bind a command to execute whenever the minimum limit is reached. + atMinLimit.onTrue(Command.print("Min limit reached!").named("Limit Message")); } - ``` +} +``` For more detailed information on trigger types, combining triggers, and advanced behavior, see the :doc:`triggers` page. @@ -38,86 +36,77 @@ While triggers are the most common way to start commands in response to external ``Coroutine.fork(Command)`` starts a command and returns immediately. The forked command runs concurrently with the command that started it. If the parent command is canceled, all of its forked commands are also canceled. Forking a command that conflicts with a higher-priority running command will fail; the higher-priority command continues to run and the parent command immediately continues to the next statement. -.. tab-set-code:: - - ```java - public Command exampleFork() { - return run(coroutine -> { - // Start this command in the background. - coroutine.fork(arm.up()); +```java +public Command exampleFork() { + return run(coroutine -> { + // Start this command in the background. + coroutine.fork(arm.up()); - // This code runs immediately after forking, without waiting for the arm - System.out.println("Arm is moving up in the background..."); + // This code runs immediately after forking, without waiting for the arm + System.out.println("Arm is moving up in the background..."); - // The arm will continue to move to its "up" position while the intake is extending. - coroutine.await(intake.extend()); - }).named("Example Fork"); - } - ``` + // The arm will continue to move to its "up" position while the intake is extending. + coroutine.await(intake.extend()); + }).named("Example Fork"); +} +``` ### Awaiting (Synchronous) ``Coroutine.await(Command)`` starts a command and pauses the current command until the child command completes. Awaiting is useful for step-by-step routines because the code reads in the same order the robot should act. -.. tab-set-code:: +```java +public Command exampleAwait() { + return run(coroutine -> { + // Start the command and wait for it to finish + coroutine.await(arm.up()); - ```java - public Command exampleAwait() { - return run(coroutine -> { - // Start the command and wait for it to finish - coroutine.await(arm.up()); - - // This code only runs after the arm has finished moving up - System.out.println("Arm is now up!"); - }).named("Example Await"); - } - ``` + // This code only runs after the arm has finished moving up + System.out.println("Arm is now up!"); + }).named("Example Await"); +} +``` ## Scheduling Already Running Commands If a command is already running, attempting to schedule it again will have no effect. The command will simply continue to run from its current point of execution; it will **not** be restarted or interrupted. ``fork`` and ``await`` can be combined to start running a command in the background and then wait for it to complete at a later point (assuming that it hadn't finished by then - otherwise ``await`` would start it over again). +```java +public Command duplicateScheduling() { + return run(coroutine -> { + Command armUp = arm.up(); -.. tab-set-code:: + // Start the arm moving up + coroutine.fork(armUp); - ```java - public Command duplicateScheduling() { - return run(coroutine -> { - Command armUp = arm.up(); + // Attempting to schedule the same instance again while it's running does nothing. + // The arm continues its original 'up' movement uninterrupted. + coroutine.fork(armUp); - // Start the arm moving up - coroutine.fork(armUp); - - // Attempting to schedule the same instance again while it's running does nothing. - // The arm continues its original 'up' movement uninterrupted. - coroutine.fork(armUp); - - // Perform another action while the arm is moving - coroutine.await(intake.spin()); - - // Similarly, awaiting a running command will wait for the existing instance to finish. - // However, if the armUp command has exited by the time spin() finished, - // this await call would actually start the armUp command from the beginning - coroutine.await(armUp); - }).named("Duplicate Scheduling"); - } - ``` + // Perform another action while the arm is moving + coroutine.await(intake.spin()); -This applies only to command _objects_. If two identical commands are scheduled - even if they both implement Java's ``equals()`` method - the scheduler still treats them as different commands, and the second command will interrupt and cancel the first: + // Similarly, awaiting a running command will wait for the existing instance to finish. + // However, if the armUp command has exited by the time spin() finished, + // this await call would actually start the armUp command from the beginning + coroutine.await(armUp); + }).named("Duplicate Scheduling"); +} +``` -.. tab-set-code:: +This applies only to command *objects*. If two identical commands are scheduled - even if they both implement Java's ``equals()`` method - the scheduler still treats them as different commands, and the second command will interrupt and cancel the first: - ```java - public Command createNewCommand() { return ... } +```java +public Command createNewCommand() { return ... } - Command command1 = createNewCommand(); - Command command2 = createNewCommand(); +Command command1 = createNewCommand(); +Command command2 = createNewCommand(); - Scheduler.getDefault().schedule(command1); +Scheduler.getDefault().schedule(command1); - // command1 is interrupted by command2, even though they're identical - Scheduler.getDefault().schedule(command2); - ``` +// command1 is interrupted by command2, even though they're identical +Scheduler.getDefault().schedule(command2); +``` ## Default Commands diff --git a/source/docs/software/commandbased/commands-v3/mechanisms.rst b/source/docs/software/commandbased/commands-v3/mechanisms.rst index fd5b523cb8..cffa8ead21 100644 --- a/source/docs/software/commandbased/commands-v3/mechanisms.rst +++ b/source/docs/software/commandbased/commands-v3/mechanisms.rst @@ -13,42 +13,40 @@ The abstraction part and the ownership part are meant to work together. If a mot To create a mechanism, write a class that implements the ``Mechanism`` interface. Hardware fields and low-level actuator helpers should usually be private. Public methods should either read state or return commands that perform safe actions. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Command; - import org.wpilib.command3.Mechanism; - import org.wpilib.command3.Trigger; - import org.wpilib.hardware.discrete.DigitalInput; - import org.wpilib.hardware.motor.PWMSparkMax; - - public class Intake implements Mechanism { - private final PWMSparkMax motor = new PWMSparkMax(1); - private final DigitalInput beamBreak = new DigitalInput(2); - - public final Trigger hasGamePiece = new Trigger(() -> !beamBreak.get()); - - public Intake() { - setDefaultCommand(stop()); - } - - private void setSpeed(double speed) { - motor.set(speed); - } - - public Command intake() { - return run(coroutine -> { - setSpeed(0.75); - coroutine.waitUntil(hasGamePiece); - setSpeed(0); - }).named("Intake"); - } - - public Command stop() { - return runRepeatedly(() -> setSpeed(0)).named("Stop Intake"); - } +```java +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.command3.Trigger; +import org.wpilib.hardware.discrete.DigitalInput; +import org.wpilib.hardware.motor.PWMSparkMax; + +public class Intake implements Mechanism { + private final PWMSparkMax motor = new PWMSparkMax(1); + private final DigitalInput beamBreak = new DigitalInput(2); + + public final Trigger hasGamePiece = new Trigger(() -> !beamBreak.get()); + + public Intake() { + setDefaultCommand(stop()); } - ``` + + private void setSpeed(double speed) { + motor.set(speed); + } + + public Command intake() { + return run(coroutine -> { + setSpeed(0.75); + coroutine.waitUntil(hasGamePiece); + setSpeed(0); + }).named("Intake"); + } + + public Command stop() { + return runRepeatedly(() -> setSpeed(0)).named("Stop Intake"); + } +} +``` This style gives other code useful tools without giving it raw control. Other classes can bind to ``hasGamePiece`` or schedule ``intake()``, but they cannot accidentally leave the motor running outside the requirement system. @@ -72,21 +70,19 @@ The default command is initially an ``idle()`` command. That means a mechanism w Default commands also have priorities. A default command effectively sets the minimum priority needed to take over the mechanism, so defaults should usually have lower priority than ordinary user commands. A high-priority default command can accidentally prevent other low-priority behavior from ever starting. -.. tab-set-code:: - - ```java - public class Elevator implements Mechanism { - public Elevator() { - // Set the default command to stay at the current position - setDefaultCommand(holdPosition()); - } +```java +public class Elevator implements Mechanism { + public Elevator() { + // Set the default command to stay at the current position + setDefaultCommand(holdPosition()); + } - public Command holdPosition() { - return runRepeatedly(() -> motor.setVoltage(feedforwardForCurrentHeight())) - .withPriority(Command.LOWEST_PRIORITY + 1) - .named("Hold Position"); - } + public Command holdPosition() { + return runRepeatedly(() -> motor.setVoltage(feedforwardForCurrentHeight())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Hold Position"); } - ``` +} +``` Setting a default command does not immediately run it. The scheduler starts the default command during its normal scheduling phase when the mechanism is otherwise idle. If a command temporarily changes a default command from inside its own logic, the previous default command is restored when that command's scope exits. diff --git a/source/docs/software/commandbased/commands-v3/migration-guide.rst b/source/docs/software/commandbased/commands-v3/migration-guide.rst index ab93d2b22d..c8f7caf5ac 100644 --- a/source/docs/software/commandbased/commands-v3/migration-guide.rst +++ b/source/docs/software/commandbased/commands-v3/migration-guide.rst @@ -12,42 +12,68 @@ v3 commands are primarily expected to be created using builder objects, similar The v3 command function is free-form and flexible and promotes standard Java language features instead of a custom DSL. If a command needs to run something repeatedly, use a standard Java ``while`` loop; if a command needs to do just one thing and exit, then just don't use ``yield``. -.. tab-set-code:: +.. tab-set:: - ```java - public class MoveArmUp extends Command { - @Override - public void initialize() { - arm.setVoltage(4); - } + .. tab-item:: v2 Class-Based Command + :sync: v2-class-commands - @Override - public void execute() { - // Optional repeated work. - } + ```java + public class MoveArmUp extends Command { + private final Arm arm; + + public MoveArmUp(Arm arm) { + this.arm = arm; + addRequirements(arm); + } + + @Override + public void initialize() { + arm.setVoltage(4); + } + + @Override + public void execute() { + // Optional repeated work. + } + + @Override + public boolean isFinished() { + return arm.atTop(); + } - @Override - public boolean isFinished() { - return arm.atTop(); + @Override + public void end(boolean interrupted) { + arm.stop(); + } } + ``` + + .. tab-item:: v2 Fluent Command + :sync: v2 - @Override - public void end(boolean interrupted) { - arm.stop(); + ```java + public Command up() { + return startEnd( + () -> setVoltage(4), + this::stop + ).until(this::atTop) + .withName("Arm Up"); } - } - ``` + ``` - ```java - public Command up() { - return run(coroutine -> { - setVoltage(4); - coroutine.waitUntil(this::atTop); - stop(); - }).whenCanceled(this::stop) - .named("Arm Up"); - } - ``` + .. tab-item:: v3 Command + :sync: v3 + + ```java + public Command up() { + return run(coroutine -> { + setVoltage(4); + coroutine.waitUntil(this::atTop); + stop(); + }).whenCanceled(this::stop) + .named("Arm Up"); + } + ``` The v3 version reads in the order the robot acts: start moving, wait until the arm is up, then stop. ``waitUntil()`` yields while it waits, so other commands and triggers continue to run. @@ -64,76 +90,207 @@ v3 provides the following factory methods: However, there is no similar API to the subsystem-level ``periodic()`` function. If you need to run a periodic function outside of the commands framework, such as reading sensor inputs or updating telemetry, call that function directly in the relevant method (often ``robotPeriodic()`` in your main robot class). -.. tab-set-code:: +.. tab-set:: - ```java - public class Elevator implements Mechanism { - private final MotorController motor = ...; - private final Encoder encoder = ...; + .. tab-item:: v2 Subsystem + :sync: v2 - private static final double MAX_HEIGHT = ...; - private double position; + ```java + public class Elevator extends SubsystemBase { + private final MotorController motor = ...; + private final Encoder encoder = ...; - public Elevator() { - setDefaultCommand(holdPosition()); - } + private static final double MAX_HEIGHT = ...; + private double position; - public void updateInputs() { - // Call this in robotPeriodic() - this.position = encoder.getDistance(); - } + public Elevator() { + setDefaultCommand(holdPosition()); + } - private void setVoltage(double volts) { - motor.setVoltage(volts); - } + @Override + public void periodic() { + this.position = encoder.getDistance(); + } - public boolean atTop() { - return position >= MAX_HEIGHT; - } + private void setVoltage(double volts) { + motor.setVoltage(volts); + } - public Command up() { - return run(coroutine -> { - setVoltage(6); - coroutine.waitUntil(this::atTop); - setVoltage(0); - }).whenCanceled(() -> setVoltage(0)) - .named("Elevator Up"); + public boolean atTop() { + return position >= MAX_HEIGHT; + } + + public Command up() { + return startEnd( + () -> setVoltage(6), + () -> setVoltage(0) + ).until(this::atTop) + .withName("Elevator Up"); + } + + public Command holdPosition() { + return run(() -> setVoltage(feedforwardForCurrentHeight()) + .withName("Hold Elevator"); } + ``` - public Command holdPosition() { - return runRepeatedly(() -> setVoltage(feedforwardForCurrentHeight())) - .withPriority(Command.LOWEST_PRIORITY + 1) - .named("Hold Elevator"); + .. tab-item:: v3 Mechanism + :sync: v3 + + ```java + public class Elevator implements Mechanism { + private final MotorController motor = ...; + private final Encoder encoder = ...; + + private static final double MAX_HEIGHT = ...; + private double position; + + public Elevator() { + setDefaultCommand(holdPosition()); + } + + public void updateInputs() { + // Call this in robotPeriodic() + this.position = encoder.getDistance(); + } + + private void setVoltage(double volts) { + motor.setVoltage(volts); + } + + public boolean atTop() { + return position >= MAX_HEIGHT; + } + + public Command up() { + return run(coroutine -> { + setVoltage(6); + coroutine.waitUntil(this::atTop); + setVoltage(0); + }).whenCanceled(() -> setVoltage(0)) + .named("Elevator Up"); + } + + public Command holdPosition() { + return runRepeatedly(() -> setVoltage(feedforwardForCurrentHeight())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Hold Elevator"); + } } - } - ``` + ``` Reading mechanism state can still be public. Direct actuator control should usually be private. That keeps hardware-changing actions inside commands, where the scheduler can enforce requirements. +## Priorities + +v2 had a simple priority system: a command can either always be interrupted by a conflicting command (via ``kCancelSelf``, which was the default setting), or could always ignore a conflicting command (via ``kCancelIncoming``). In effect, a command would either have the absolute *minimum* priority, always interruptible by other commands, or the absolute *maximum* priority, never interruptible by other commands. + +Commands v3 an integer-based priority system, using the full range of integer values. The default priority is 0, but can be specified in the full 32-bit integer range of -2^31 through 2^31-1. If two commands conflict: + +- A higher-priority scheduled command interrupts the lower-priority running command. +- An equal-priority scheduled command interrupts the running command. +- A lower-priority scheduled command is discarded and does not start. + +Commands in a v3 composition inherit the priority of their parent if it's higher than their own. This allows for child commands to be "promoted" and take ownership of mechanisms that are owned by otherwise higher-priority commands. + +.. warning:: Default commands should usually have priority below ordinary commands, and never above 0. If a default command has the same or higher priority as normal controls, it can block behavior that should be allowed to take over the mechanism. + +.. tab-set:: + + .. tab-item:: v2 Priorities + :sync: v2 + + ```java + // kCancelIncoming means this command can never be interrupted. + // It only stops if it's deliberately canceled or ends on its own. + Command highest = + arm.run(...) + .withInterruptBehavior(kCancelIncoming) + .withName("Not Interruptible"); + + // The default v2 behavior means this command can always be interrupted. + Command normal = + arm.run(...) + .withName("Always Interruptible"); + ``` + + .. tab-item:: v3 Priorities + :sync: v3 + + ```java + // In v3, this command is only interruptible by other commands with the highest priority. + // There's no higher priority because this is 2^31-1, the largest value of an int + Command highest = + arm.run(...) + .withPriority(Command.HIGHEST_PRIORITY) + .named("Rarely Interruptible"); + + // The default priority is 0. This command is interruptible by commands with a priority ≥ 0 + Command normal = + arm.run(...) + .named("Usually Interruptible"); + + // This command is higher priority than "normal", but not as high as "highest". + // It can interrupt the "normal" command but not the "highest" command. + Command medium = + arm.run(...) + .withPriority(500) + .named("Medium Priority"); + + // This command has the absolute lowest priority. + // Any other command can interrupt it, and it can only interrupt other lowest-priority commands. + Command lowest = + arm.run(...) + .withPriority(Command.LOWEST_PRIORITY) + .named("Always Interruptible"); + + // The wrapper command has a priority = 1000, which the "lowest" command will inherit. + // This will let it interrupt both "normal" and "medium" when it otherwise wouldn't be able to. + Command wrapper = + Command.noRequirements(coroutine -> coroutine.await(lowest)) + .withPriority(1000) + .named("High Priority Wrapper"); + ``` + ## Requirements Still Matter The requirements system is the same as v2: commands declare the mechanisms they control, and only one running command may require a mechanism at a time. If a new command is scheduled that conflicts with at least one running command, the scheduler compares priorities: 1. If the new command is the same or higher priority as every command it conflicts with, it is scheduled and the running commands are canceled. -2. If the new command is lower priority than _any_ command it conflicts with, the new command is not canceled and all conflicting commands continue to run. +2. If the new command is lower priority than *any* command it conflicts with, the new command is not canceled and all conflicting commands continue to run. -Like v2, a command created with a mechanism's ``run(...)`` or ``runRepeatedly(...)`` helper automatically requires that mechanism. +Like v2, a command created with a mechanism's ``run()`` or ``runRepeatedly()`` helper automatically requires that mechanism. If multiple requirements are needed, use the ``Command.requiring()`` method and pass it all the required mechanisms. Use ``Command.noRequirements(...)`` for commands that truly do not own hardware, or for parent commands that coordinate child commands without inheriting all of their requirements up front. Good use cases for no-requirement commands include sensor resets or debugging prints. -.. tab-set-code:: +.. tab-set:: - ```java - public Command intake() { - // Automatically requires this Intake mechanism. - return run(coroutine -> { - motor.set(0.8); - coroutine.waitUntil(hasGamePiece); - motor.set(0); - }).whenCanceled(() -> motor.set(0)) - .named("Intake"); - } - ``` + .. tab-item:: v2 + :sync: v2 -Use ``Command.noRequirements(...)`` for commands that truly do not own hardware, or for parent commands that coordinate child commands without inheriting all of their requirements up front. Good use cases for no-requirement commands include sensor resets or debugging prints. + ```java + public Command intake() { + // Automatically requires this Intake subsystem. + return startEnd( + () -> motor.set(0.8), + () -> motor.set(0) + ).until(hasGamePiece) + .withName("Intake"); + } + ``` + + .. tab-item:: v3 + :sync: v3 + + ```java + public Command intake() { + // Automatically requires this Intake mechanism. + return run(coroutine -> { + motor.set(0.8); + coroutine.waitUntil(hasGamePiece); + motor.set(0); + }).whenCanceled(() -> motor.set(0)) + .named("Intake"); + } + ``` ## Default Commands @@ -146,71 +303,137 @@ The differences worth remembering are: - Default command settings are scoped. A default set inside an OpMode or command is reverted when that scope exits. - Default commands should usually have lower priority than ordinary commands. Lower-priority commands cannot interrupt higher-priority commands, so the default command's priority is effectively the minimum priority that's usable for that mechanism. -.. tab-set-code:: +.. tab-set:: - ```java - public class Drive implements Mechanism { - public Drive(CommandGamepad controller) { - setDefaultCommand( - runRepeatedly(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) - .withPriority(Command.LOWEST_PRIORITY + 1) - .named("Teleop Drive")); + .. tab-item:: v2 + :sync: v2 + + ```java + public class Drive extends SubsystemBase { + public Drive(CommandGamepad controller) { + setDefaultCommand( + run(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) + .withInterruptBehavior(kCancelSelf) + .withName("Teleop Drive")); + } } - } - ``` + ``` + + .. tab-item:: v3 + :sync: v3 + + ```java + public class Drive implements Mechanism { + public Drive(CommandGamepad controller) { + setDefaultCommand( + runRepeatedly(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) + .withPriority(Command.LOWEST_PRIORITY + 1) + .named("Teleop Drive")); + } + } + ``` ## Command Logic And Finishing -In v2, ``isFinished()`` decides when a command is done. In v3, ordinary control flow decides when a command is done. A command finishes naturally when its command body returns. +In v2, ``isFinished()`` decides when a command is done. In v3, ordinary control flow decides when a command is done. A command finishes naturally when its command body returns. If a v3 command needs to finish early, use a ``return`` statement. For a command that runs until a condition is met, use ``waitUntil(...)``: -.. tab-set-code:: +.. tab-set:: + + .. tab-item:: v2 + :sync: v2 + + ```java + public Command shootWhenReady() { + return runOnce(this::spinUp) + .andThen(idle().until(this::atSpeed)) + .andThen(runOnce(this::feedNote)) + .finallyDo(interrupted -> { + if (!interrupted) { + stop(); + } + }) + .withName("Shoot When Ready"); + } + ``` - ```java - public Command shootWhenReady() { - return run(coroutine -> { - spinUp(); - coroutine.waitUntil(this::atSpeed); - feedNote(); - }).whenCanceled(this::stop) - .named("Shoot When Ready"); - } - ``` + .. tab-item:: v3 + :sync: v3 + + ```java + public Command shootWhenReady() { + return run(coroutine -> { + spinUp(); + coroutine.waitUntil(this::atSpeed); + feedNote(); + }).whenCanceled(this::stop) + .named("Shoot When Ready"); + } + ``` For a command that updates every scheduler cycle, use a loop and yield: -.. tab-set-code:: +.. warning:: The yield is not optional. Commands v3 uses cooperative scheduling: a command gives other commands time to run by calling ``yield()``, ``wait()``, ``waitUntil()``, ``await()``, ``awaitAll()``, ``awaitAny``, or ``park()``. The WPILib compiler plugin detects ``while`` loops without a call to a yielding method and flags them with a compilation error. - ```java - public Command driveDistance(double meters) { - return run(coroutine -> { - resetDistance(); - while (getDistance() < meters) { - setSpeed(0.5); - coroutine.yield(); - } - stop(); - }).whenCanceled(this::stop) - .named("Drive Distance"); - } - ``` +.. tab-set:: + + .. tab-item:: v2 + :sync: v2 + + ```java + public Command driveDistance(double meters) { + return startRun( + this::resetDistance, + () -> setSpeed(0.5) + ).until(() -> getDistance() >= meters) + .finallyDo(this::stop) + .withName("Drive Distance"); + } + ``` -The yield is not optional. Commands v3 uses cooperative scheduling: a command gives other commands time to run by calling ``yield()``, ``wait()``, ``waitUntil()``, ``await()``, ``awaitAll()``, ``awaitAny``, or ``park()``. + .. tab-item:: v3 + :sync: v3 + + ```java + public Command driveDistance(double meters) { + return run(coroutine -> { + resetDistance(); + while (getDistance() < meters) { + setSpeed(0.5); + coroutine.yield(); + } + stop(); + }).whenCanceled(this::stop) + .named("Drive Distance"); + } + ``` ## One-Shot Commands A v2 ``InstantCommand`` usually becomes a one-shot v3 command: a command that does a small amount of work and returns without yielding. -.. tab-set-code:: +One-shot commands are appropriate for quick state changes: resetting a sensor, updating a flag, printing a diagnostic message, or clearing an alert. They are not appropriate for blocking I/O, expensive calculations, or anything that may take enough time to delay robot control. - ```java - public Command resetGyro() { - return Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); - } - ``` +.. tab-set:: -One-shot commands are appropriate for quick state changes: resetting a sensor, updating a flag, printing a diagnostic message, or clearing an alert. They are not appropriate for blocking I/O, expensive calculations, or anything that may take enough time to delay robot control. + .. tab-item:: v2 + :sync: v2 + + ```java + public Command resetGyro() { + return Commands.runOnce(gyro::reset).withName("Reset Gyro"); + } + ``` + + .. tab-item:: v3 + :sync: v3 + + ```java + public Command resetGyro() { + return Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); + } + ``` ## Command Groups And Coroutine Composition @@ -228,7 +451,7 @@ Complex command sequences can be built using the v3 ``StateMachine`` API (see :d There are some key improvements in ownership and interruption behavior in v3 to be aware of: -1. The v3 scheduler is responsible for _every_ command. The v2 scheduler only handled the topmost level of commands, and compositions like ``SequentialCommandGroup`` effectively acted like mini-schedulers to run the commands inside the group. +1. The v3 scheduler is responsible for *every* command. The v2 scheduler only handled the topmost level of commands, and compositions like ``SequentialCommandGroup`` effectively acted like mini-schedulers to run the commands inside the group. 2. v3 compositions do not have to have any requirements. Because the v3 scheduler tracks parent-child relationships, an interrupt to a child command will bubble up to its parent (and its parent, and so on). Parent commands effectively inherit all of a child's requirements *while the child is running*. 3. v3 child commands inherit the priority of their parent if it's higher than their own. See Priorities_ for details. @@ -236,66 +459,99 @@ The coroutine API is often a better migration target for complex routines becaus ### Handling Fork Failures -Forking a child command with a coroutine's ``fork``, ``await``, ``awaitAll``, or ``awaitAny`` method will fail if one or more of the forked commands shares a requirement with a running command with a higher priority. - -.. tab-set-code:: - - ```java - Scheduler.getDefault().schedule( - arm.run(...).withPriority(1000).named("Super High Priority Arm Command")); - - Command parent = Command.noRequirements(coroutine -> { - // This child command can't be forked because a higher-priority command already owns the arm - coroutine.fork( - arm.run(...).withPriority(0).named("Lower Priority Arm Command")); - }).named("Parent"); - - Scheduler.getDefault().schedule(parent); - ``` - -The v3 framework provides two ways of handling failures: by interrupting the command that called a forking method, or by returning a failure object that user code can handle. The v3 framework defaults to the interruption behavior, so if a child command that you assume will run is unable to be scheduled, the entire composition will stop and an ``Interrupted`` telemetry event will be issued by the scheduler, attributed to the command that prevented the child command from being scheduled. In this setup, there is no chance for user code to receive the failure event and retry or fall back to different behavior. - -The other option is to call ``setCancelOnForkFailure(false)`` on the coroutine object, telling it to return to user code instead of immediately interrupting the command. This setting only applies to the single coroutine, and is _not_ inherited by child commands; every command that wants this behavior needs to opt into it. - -.. tab-set-code:: - - ```java - Scheduler.getDefault().schedule( - arm.run(...).withPriority(1000).named("Super High Priority Arm Command")); - - Command parent = Command.noRequirements(coroutine -> { - // Lets us handle the failure, instead of the framework immediately canceling the command. - coroutine.setCancelOnForkFailure(false); - - ForkResult result = coroutine.fork( - arm.run(...).withPriority(0).named("Lower Priority Arm Command")); - - // Handle the result. Pattern-matching instanceof lets us easily access the failure data - if (result instanceof ForkResultFailure failure) { - for (SchedulerResult.Failure failure : failure.failed()) { - switch (failure) { - case LowerPriorityThanRunningCommand(Command failed, Command conflict) -> { - System.err.println("Could not fork " + failed + " because " + conflict + " is already running"); - } - case LowerPriorityThanQueuedCommand(Command failed, Command conflict) -> { - System.err.println("Could not fork " + failed + " because " + conflict + " is already queued"); - } +In v2, a proxy command in a command group may fail to be scheduled without any feedback to the group; the group would just continue as if the proxy command was successful. This can be problematic if a sequence of proxied commands relies on earlier commands succeeding; for example, a sequence like ``sequence(elevator.moveToScoringHeight().asProxy(), claw.open())`` is a fairly standard pattern that allows the elevator's default command - typically holding the last setpoint - to run after the elevator gets to the scoring position. However, if an elevator command with the ``kCancelIncoming`` interrupt behavior is already running when the sequence starts, the elevator *will not move at all* and the sequence skips straight to opening the claw - which may damage the claw or something else on the robot if the elevator isn't in position where it's safe to open the claw. + +There is no way in v2 to detect and recover from these types of failures. + +In v3, an inner command scheduled via with a coroutine's ``fork``, ``await``, ``awaitAll``, or ``awaitAny`` method will act like a v2 proxy command (and almost always as a *deferred proxy* because the inner command is typically created only when the composition is running). These coroutine methods return a result object that can be queried to see if the commands were successfully scheduled, as well as which commands succeeded and which ones failed. However, by default, the v3 framework will detect these types of failures and automatically interrupt the composition instead of allowing it to continue in a potentially unsafe way; this behavior can be turned off via ``coroutine.setCancelOnForkFailure(false)`` in the parent command. Note that manually handling the failure is delicate and may cause unsafe operation of the robot unless the failure is properly handled. + +.. tab-set:: + + .. tab-item:: v2 Proxy Commands + :sync: v2 + + ```java + Command homeElevator = + elevator.run(() -> ...) + .until(elevator::isHomed) + .withInterruptBehavior(kCancelIncoming) + .withName("Home Elevator"); + + Command moveToScoringHeight = + elevator.run(() -> ...) + .until(elevator::isAtScoringHeight) + .withName("Move Elevator to Scoring Height"); + + SequentialCommandGroup score = + sequence( + moveToScoringHeight.asProxy(), // skipped if "Home Elevator" is running! + claw.open().asProxy() + ).withName("Score Gamepiece"); + + homeElevator.schedule(); + score.schedule(); // skips moving the elevator and immediately opens the claw! + ``` + + .. tab-item:: v3 Failure Handing (Automatic Interruption) + :sync: v3 + + ```java + Command homeElevator = + elevator.run(coroutine -> ...) + .withPriority(1) + .named("Home Elevator"); + + Command moveToScoringHeight = + elevator.run(coroutine -> ...) + .named("Move Elevator to Scoring Height"); + + Command score = + Command.noRequirements(coroutine -> { + coroutine.await(moveToScoringHeight); + coroutine.await(claw.close()); + }).named("Score Gamepiece"); + + Scheduler.getDefault().schedule(homeElevator); + Scheduler.getDefault().schedule(score); + ``` + + .. tab-item:: v3 Failure Handling (Manual) + :sync: v3-manual-failure-handling + + ```java + Command homeElevator = + elevator.run(coroutine -> ...) + .withPriority(1) + .named("Home Elevator"); + + Command moveToScoringHeight = + elevator.run(coroutine -> ...) + .named("Move Elevator to Scoring Height"); + + Command score = + Command.noRequirements(coroutine -> { + // Disable automatic interruption on fork failures so we can handle + // the failures ourselves. + coroutine.setCancelOnForkFailure(false); + + var elevatorMove = coroutine.await(moveToScoringHeight); + if (elevatorMove.failed()) { + // The elevator is doing something with a higher priority right now. + // Bail so we don't damage the claw. + System.err.println("Can't move the elevator to score!"); + return; } - } - - // Plausibly continue with behavior that doesn't need the arm. - // If this _also_ fails to be scheduled, we'll just return immediately - coroutine.await(armlessBehavior()); - return; - } - // The fork succeeded, so we can proceed with behavior that knows we own the arm. - // If this behavior can't be scheduled, then we just return immediately. - coroutine.await(armedBehavior()); - }).named("Parent"); + // Because this is the last command in the composition, we don't have to + // handle a failure result - the composition will just exit - but if this + // were a larger composition then _every_ command that's forked or awaited + // will need error handling for safe operation. + coroutine.await(claw.close()); + }).named("Score Gamepiece"); - Scheduler.getDefault().schedule(parent); - ``` + Scheduler.getDefault().schedule(homeElevator); + Scheduler.getDefault().schedule(score); + ``` ### Sequential Work @@ -362,22 +618,38 @@ In v2, teams often used proxy commands or schedule-command patterns to avoid a c In v3, this pattern is built into coroutine composition. A parent command can require no mechanisms and ``await()`` child commands as needed. The child command owns its requirements while it runs, and releases them when it completes. Child commands can also share requirements with their parents; the scheduler automatically detects the parent-child relationship and won't interrupt the parent. In v2, sharing requirements between parent and child commands would result in the child interrupting its parent. -.. tab-set-code:: +.. tab-set:: - ```java - public Command autonomousScore() { - return Command.noRequirements(coroutine -> { - // Owns only the drivetrain while this child runs. - coroutine.await(drive.followPath("ScorePath")); + .. tab-item:: v2 + :sync: v2 - // Owns only the elevator while this child runs. - coroutine.await(elevator.moveToScoringHeight()); + ```java + public Command autonomousScore() { + return Commands.sequence( + drive.followPath("ScorePath").asProxy(), + elevator.moveToScoringHeight().asProxy(), + gripper.release().asProxy() + ).withName("Autonomous Score"); + } + ``` - // Owns only the gripper while this child runs. - coroutine.await(gripper.release()); - }).named("Autonomous Score"); - } - ``` + .. tab-item:: v3 + :sync: v3 + + ```java + public Command autonomousScore() { + return Command.noRequirements(coroutine -> { + // Owns only the drivetrain while this child runs. + coroutine.await(drive.followPath("ScorePath")); + + // Owns only the elevator while this child runs. + coroutine.await(elevator.moveToScoringHeight()); + + // Owns only the gripper while this child runs. + coroutine.await(gripper.release()); + }).named("Autonomous Score"); + } + ``` This is often the cleanest replacement for v2 proxy-heavy code. The requirements are local to the actions that actually use them, but the larger routine still cancels as a unit if one of its children is externally interrupted. @@ -408,7 +680,7 @@ New in v3 are the ``retryWhileTrue`` and ``retryWhileFalse`` bindings. A retry b The same cancellation and interruption concepts carry over from v2 in v3: cancellation means the command was stopped before its natural completion; interruption is a particular kind of cancellation specifically caused by another command taking ownership of a required mechanism, rather than being canceled by a trigger binding or a manual call to the scheduler's ``cancel()`` method. -Use ``whenCanceled(...)`` for cleanup that must happen when a command is canceled. Note that this runs regardless of _why_ the command was canceled. +Use ``whenCanceled(...)`` for cleanup that must happen when a command is canceled. Note that this runs regardless of *why* the command was canceled. .. tab-set-code:: @@ -427,20 +699,6 @@ Do not put long loops in cancellation cleanup. Cancellation cleanup should be sh Scheduler telemetry reports these cases separately. A command that finishes normally emits ``Completed``. A command that is interrupted emits ``Interrupted`` followed by ``Canceled``. A command that throws emits ``CompletedWithError`` and the exception still propagates, bubbling up to the scheduler ``run()`` call and crashing the robot program; the WPILib framework will print the exception and its stacktrace to the driver station console for operators to see and debug the program. -## Priorities - -v2 had a simple priority system: a command can either always be interrupted by a conflicting command (via ``kCancelSelf``, which was the default setting), or could always ignore a conflicting command (via ``kCancelIncoming``). In effect, a command would either have the absolute _minimum_ priority, always interruptable by other commands, or the absolute _maximum_ priority, never interruptable by other commands. - -Commands v3 an integer-based priority system, using the full range of integer values. The default priority is 0, but can be specified in the full 32-bit integer range of -2^31 through 2^31-1. If two commands conflict: - -- A higher-priority scheduled command interrupts the lower-priority running command. -- An equal-priority scheduled command interrupts the running command. -- A lower-priority scheduled command is discarded and does not start. - -Default commands should usually have priority below ordinary commands, and never above 0. If a default command has the same or higher priority as normal controls, it can block behavior that should be allowed to take over the mechanism. - -Commands in a v3 composition inherit the priority of their parent if it's higher than their own. This allows for child commands to be "promoted" and take ownership of mechanisms that are owned by otherwise higher-priority commands. - ## Common Migration Recipes ### Default Drive Command diff --git a/source/docs/software/commandbased/commands-v3/scopes.rst b/source/docs/software/commandbased/commands-v3/scopes.rst index 162443582b..2de14c987d 100644 --- a/source/docs/software/commandbased/commands-v3/scopes.rst +++ b/source/docs/software/commandbased/commands-v3/scopes.rst @@ -25,18 +25,16 @@ The command scope is tied to the lifetime of a specific running command. Any res Command scope is useful for temporary controls. For example, an aiming command can create a binding that fires only while the robot is actively aimed at the target. Once the aiming command completes or is canceled, the binding disappears and any command it started is canceled. -.. tab-set-code:: +```java +public Command sweepAndScore(Robot robot) { + return Command.noRequirements(coroutine -> { + // This binding only exists while sweepAndScore is running + intakeTrigger.onTrue(robot.intake.intake()); - ```java - public Command sweepAndScore(Robot robot) { - return Command.noRequirements(coroutine -> { - // This binding only exists while sweepAndScore is running - intakeTrigger.onTrue(robot.intake.intake()); - - coroutine.await(robot.drive.followPath("SweepPath")); - }).named("Sweep and Score"); - } - ``` + coroutine.await(robot.drive.followPath("SweepPath")); + }).named("Sweep and Score"); +} +``` ## The OpMode Scope @@ -46,26 +44,24 @@ When the robot transitions to a different mode, all commands and bindings scoped OpMode scope is where mode-specific setup belongs. Autonomous path commands, autonomous-only safety bindings, and autonomous default commands can be created in an autonomous OpMode without needing manual cleanup in teleop. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Command; - import org.wpilib.command3.Trigger; - import org.wpilib.command3.button.RobotModeTriggers; - - @Autonomous - public class SweepAuto implements OpMode { - public SweepAuto(Robot robot) { - // Start the intake stowed - robot.intake.setDefaultCommand(robot.intake.stow()); - - // Once the robot is enabled, start following a sweep path through the - // left trench, into the neutral zone, then back over the bump. - // When we return to the alliance zone, aim at the hub and start shooting. - RobotModeTriggers.enabled().onTrue(sweepAndScore(robot)); - } +```java +import org.wpilib.command3.Command; +import org.wpilib.command3.Trigger; +import org.wpilib.command3.button.RobotModeTriggers; + +@Autonomous +public class SweepAuto implements OpMode { + public SweepAuto(Robot robot) { + // Start the intake stowed + robot.intake.setDefaultCommand(robot.intake.stow()); + + // Once the robot is enabled, start following a sweep path through the + // left trench, into the neutral zone, then back over the bump. + // When we return to the alliance zone, aim at the hub and start shooting. + RobotModeTriggers.enabled().onTrue(sweepAndScore(robot)); } - ``` +} +``` ## The Global Scope @@ -77,18 +73,16 @@ The global scope is the widest scope and is active for the entire duration of th Global resources are never automatically cleaned up by the scheduler. Use the global scope for things that should always be available, such as default commands for mechanisms, driver controls, and basic safety bindings. Avoid putting mode-specific behavior in global scope unless it explicitly checks the current mode or enable state. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Command; - import org.wpilib.command3.Trigger; - import org.wpilib.framework.TimedRobot; +```java +import org.wpilib.command3.Command; +import org.wpilib.command3.Trigger; +import org.wpilib.framework.TimedRobot; - public class Robot extends TimedRobot { - public Robot() { - // GLOBAL SCOPE: These are always active - arm.setDefaultCommand(arm.holdPosition()); - driverController.a().onTrue(arm.up()); - } +public class Robot extends TimedRobot { + public Robot() { + // GLOBAL SCOPE: These are always active + arm.setDefaultCommand(arm.holdPosition()); + driverController.a().onTrue(arm.up()); } - ``` +} +``` diff --git a/source/docs/software/commandbased/commands-v3/state-machines.rst b/source/docs/software/commandbased/commands-v3/state-machines.rst index 6e8d4ebbf0..6cd02cb73f 100644 --- a/source/docs/software/commandbased/commands-v3/state-machines.rst +++ b/source/docs/software/commandbased/commands-v3/state-machines.rst @@ -8,28 +8,26 @@ State machines are commands themselves, so they have a name and can be scheduled To define a state machine, create an instance of ``StateMachine``, add all of its states, choose an initial state, and then add transitions. Defining states first makes global transitions easier to reason about because ``switchFromAny()`` without arguments only applies to states that already exist. -.. tab-set-code:: +```java +import org.wpilib.command3.StateMachine; - ```java - import org.wpilib.command3.StateMachine; +StateMachine sm = new StateMachine("Example State Machine"); - StateMachine sm = new StateMachine("Example State Machine"); +// 1. Define all states +var idleState = sm.addState(arm.idle()); +var upState = sm.addState(arm.up()); +var downState = sm.addState(arm.down()); - // 1. Define all states - var idleState = sm.addState(arm.idle()); - var upState = sm.addState(arm.up()); - var downState = sm.addState(arm.down()); +// 2. Define transitions +idleState.switchTo(upState).when(driverController.y()); +idleState.switchTo(downState).when(driverController.a()); - // 2. Define transitions - idleState.switchTo(upState).when(driverController.y()); - idleState.switchTo(downState).when(driverController.a()); +upState.switchTo(idleState).whenComplete(); +downState.switchTo(idleState).whenComplete(); - upState.switchTo(idleState).whenComplete(); - downState.switchTo(idleState).whenComplete(); - - // 3. Set the initial state - sm.setInitialState(idleState); - ``` +// 3. Set the initial state +sm.setInitialState(idleState); +``` .. note:: Calling `setInitialState() `__ is **required** - otherwise the state machine wouldn't know where to start. If you forget to set an initial state, the WPILib compiler plugin will detect it and issue an error. @@ -44,12 +42,10 @@ If a state's command finishes and no completion transition is configured, the st You can also add enter and exit callbacks to states: -.. tab-set-code:: - - ```java - upState.onEnter(() -> System.out.println("Entering UP state")); - upState.onExit(() -> System.out.println("Exiting UP state")); - ``` +```java +upState.onEnter(() -> System.out.println("Entering UP state")); +upState.onExit(() -> System.out.println("Exiting UP state")); +``` Enter callbacks run immediately after the state's command is scheduled. Exit callbacks run immediately before the state's command is canceled during a transition, or immediately after it completes naturally. If an enter callback schedules commands, those commands are scoped to the lifetime of the state machine, not to the lifetime of just that state. @@ -65,27 +61,23 @@ Conditional transitions are treated as rising-edge conditions to prevent a state You can also define transitions for multiple states at once, which can help make your code more readable. -.. tab-set-code:: - - ```java - sm.switchFromAny(upState, downState).to(idleState).when(driverController.b()); - ``` +```java +sm.switchFromAny(upState, downState).to(idleState).when(driverController.b()); +``` ### Global Transitions with `switchFromAny()` If you call `switchFromAny() `__ without any arguments, it creates a transition that applies to **all** states in the state machine. This is useful for "global" transitions, such as returning to an initial or home state from anywhere in the state graph. -.. tab-set-code:: - - ```java - // Any state will transition to idle if the X button is pressed - sm.switchFromAny().to(idleState).when(driverController.x()); +```java +// Any state will transition to idle if the X button is pressed +sm.switchFromAny().to(idleState).when(driverController.x()); - // Any state will exit the state machine if a safety sensor is tripped - sm.switchFromAny().toExitStateMachine().when(safetySensor::get); - ``` +// Any state will exit the state machine if a safety sensor is tripped +sm.switchFromAny().toExitStateMachine().when(safetySensor::get); +``` .. warning:: `switchFromAny()` with no arguments only applies to the states that have **already been defined** on the state machine at the time the method is called. Any states added with `addState() `__ *after* the call to `switchFromAny()` will not have this transition applied to them. - For this reason, it is recommended to add all of your states first, and then define transitions after all states have been added. +For this reason, it is recommended to add all of your states first, and then define transitions after all states have been added. diff --git a/source/docs/software/commandbased/commands-v3/structuring-your-project.rst b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst index a3a54ca7eb..45b629948e 100644 --- a/source/docs/software/commandbased/commands-v3/structuring-your-project.rst +++ b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst @@ -42,6 +42,85 @@ OpMode constructors are generally all that's needed. WPILib will automatically c ## Mechanism-level Commands +Commands that only interact with a single mechanism should be defined as methods in that mechanism's class using the ``run`` or ``runRepeatedly`` builder methods provided by the ``Mechanism`` interface. These builder methods make the created command automatically require the mechanism so you can't forget to set the requirement. + +.. warning:: Command methods are designed to create a ``Command`` object that will be run at a later point in the program. Only the code in the lambda function passed to ``run`` or ``runRepeatedly`` will execute when the command is running. All code + +```java +package first.robot.mechanisms; + +import static org.wpilib.units.Units.*; + +import module wpilib; +import module wpilib.command3; + +public class Arm implements Mechanism { + // ... hardware definitions, constructors, PID controllers and so on ... + + // Good example: + public Command stop() { + return runRepeatedly(() -> motor.stop()).named("Stop Arm"); + } + + public Command incorrectStop() { + // Incorrect: This will print to console when the command is created, not when it actually runs + System.out.println("Stopping the arm motor! (but not really)"); + + return runRepeatedly(() -> motor.stop()).named("Incorrect Stop Arm"); + } +} +``` + +Highly complicated commands with a lot of logic can harm readability of a mechanism class, and can optionally be implemented as standalone class-based commands in the same package as the mechanism. The mechanism class should still have a method to create and return these commands. + +As a rule of thumb, commands with more than 15-20 lines of code are good candidates for being moved to class-based commands. Bits of logic can be more easily split into smaller helper methods, but you will need to implement all of the required ``Command`` methods yourself. + +```java +package first.robot.mechanisms; + +import module wpilib.command3; +import first.robot.commands.drivetrain.VeryComplicatedCommand; + +public class Drivetrain implements Mechanism { + // ... hardware definitions, constructors, PID controllers and so on ... + + public Command veryComplicatedCommand() { + return new VeryComplicatedCommand(this); + } +} +``` + +```java +package first.robot.commands.drivetrain; + +import module java.base; +import module wpilib.command3; + +public class VeryComplicatedCommand implements Command { + private final Drivetrain drivetrain; + private final Set requirements; + + public FollowPathPartsCommand(Drivetrain drivetrain) { + this.drivetrain = drivetrain; + this.requirements = Set.of(drivetrain); + } + + @Override + public void run(Coroutine coroutine) { + // ... lots of complicated logic ... + } + + @Override + public Set requirements() { + return requirements; + } + + @Override + public String name() { + return "VeryComplicatedCommand"; + } +} +``` ## Multi-Mechanism Commands diff --git a/source/docs/software/commandbased/commands-v3/telemetry.rst b/source/docs/software/commandbased/commands-v3/telemetry.rst index 84b9bb7244..7074b5d239 100644 --- a/source/docs/software/commandbased/commands-v3/telemetry.rst +++ b/source/docs/software/commandbased/commands-v3/telemetry.rst @@ -39,31 +39,29 @@ Events often occur in specific sequences or together within the same scheduler c Do not treat every ``Canceled`` event as a bug. Commands are canceled when they are interrupted by newer or higher-priority commands, when their enclosing scope exits, when a ``whileTrue`` binding goes false, or when user code cancels them manually. The surrounding events and the command requirements usually tell you which case happened. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Scheduler; - import org.wpilib.command3.SchedulerEvent; - - Scheduler.getDefault().addEventListener(event -> { - if (event instanceof SchedulerEvent.Scheduled e) { - System.out.println("Command " + e.command().name() + " was scheduled"); - } else if (event instanceof SchedulerEvent.Mounted e) { - // Mounted events occur every cycle - we might not want to log them all! - // System.out.println("Command " + e.command().name() + " mounted"); - } else if (event instanceof SchedulerEvent.Yielded e) { - // System.out.println("Command " + e.command().name() + " yielded"); - } else if (event instanceof SchedulerEvent.Completed e) { - System.out.println("Command " + e.command().name() + " completed naturally"); - } else if (event instanceof SchedulerEvent.CompletedWithError e) { - System.err.println("Command " + e.command().name() + " failed with error: " + e.error()); - } else if (event instanceof SchedulerEvent.Interrupted e) { - System.out.println("Command " + e.command().name() + " was interrupted by " + e.interrupter().name()); - } else if (event instanceof SchedulerEvent.Canceled e) { - System.out.println("Command " + e.command().name() + " was canceled"); - } - }); - ``` +```java +import org.wpilib.command3.Scheduler; +import org.wpilib.command3.SchedulerEvent; + +Scheduler.getDefault().addEventListener(event -> { + if (event instanceof SchedulerEvent.Scheduled e) { + System.out.println("Command " + e.command().name() + " was scheduled"); + } else if (event instanceof SchedulerEvent.Mounted e) { + // Mounted events occur every cycle - we might not want to log them all! + // System.out.println("Command " + e.command().name() + " mounted"); + } else if (event instanceof SchedulerEvent.Yielded e) { + // System.out.println("Command " + e.command().name() + " yielded"); + } else if (event instanceof SchedulerEvent.Completed e) { + System.out.println("Command " + e.command().name() + " completed naturally"); + } else if (event instanceof SchedulerEvent.CompletedWithError e) { + System.err.println("Command " + e.command().name() + " failed with error: " + e.error()); + } else if (event instanceof SchedulerEvent.Interrupted e) { + System.out.println("Command " + e.command().name() + " was interrupted by " + e.interrupter().name()); + } else if (event instanceof SchedulerEvent.Canceled e) { + System.out.println("Command " + e.command().name() + " was canceled"); + } +}); +``` ## Data Logging @@ -97,26 +95,24 @@ Because the ``Scheduler`` class implements ``ProtobufSerializable``, you can log Because the scheduler state changes every loop cycle, you should append the current state to the log in your ``robotPeriodic`` method. -.. tab-set-code:: - - ```java - import org.wpilib.command3.Scheduler; - import org.wpilib.datalog.ProtobufLogEntry; - import org.wpilib.system.DataLogManager; - import org.wpilib.framework.TimedRobot; +```java +import org.wpilib.command3.Scheduler; +import org.wpilib.datalog.ProtobufLogEntry; +import org.wpilib.system.DataLogManager; +import org.wpilib.framework.TimedRobot; - public class Robot extends TimedRobot { - // Create a log entry for the scheduler state - private final ProtobufLogEntry schedulerLog = - ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto); +public class Robot extends TimedRobot { + // Create a log entry for the scheduler state + private final ProtobufLogEntry schedulerLog = + ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto); - @Override - public void robotPeriodic() { - // Run the scheduler - Scheduler.getDefault().run(); + @Override + public void robotPeriodic() { + // Run the scheduler + Scheduler.getDefault().run(); - // Log the current state of the scheduler - schedulerLog.append(Scheduler.getDefault()); - } + // Log the current state of the scheduler + schedulerLog.append(Scheduler.getDefault()); } - ``` +} +``` diff --git a/source/docs/software/commandbased/commands-v3/triggers.rst b/source/docs/software/commandbased/commands-v3/triggers.rst index cedd807310..bab0d4fba7 100644 --- a/source/docs/software/commandbased/commands-v3/triggers.rst +++ b/source/docs/software/commandbased/commands-v3/triggers.rst @@ -8,18 +8,16 @@ Triggers cache their signal when they are polled. Calling ``getAsBoolean()`` rea A ``Trigger`` is created by providing a ``BooleanSupplier`` (a function that returns ``true`` or ``false``) or by combining existing triggers. -.. tab-set-code:: +```java +// A trigger for a gamepad button +Trigger button = xboxController.a(); - ```java - // A trigger for a gamepad button - Trigger button = xboxController.a(); +// A trigger for a limit switch +Trigger limitSwitch = new Trigger(limitSwitch::get); - // A trigger for a limit switch - Trigger limitSwitch = new Trigger(limitSwitch::get); - - // A trigger for a complex condition by combining two triggers - Trigger isReady = new Trigger(arm::isAtTarget).and(shooter::isAtSpeed); - ``` +// A trigger for a complex condition by combining two triggers +Trigger isReady = new Trigger(arm::isAtTarget).and(shooter::isAtSpeed); +``` ## Trigger Bindings @@ -43,6 +41,32 @@ Use ``onTrue`` and ``onFalse`` for commands that should start once and then mana Retry bindings continuously attempt to schedule their command while the signal remains in the requested state. If the command ends naturally, it will be started again. If it was interrupted by another same-priority command that requires the same mechanism, the retry binding may immediately schedule it again and interrupt the would-be interrupter. Use retry bindings when repeated attempts are intentional, not as a default replacement for ``whileTrue``. +Retry bindings are particularly useful for suspend-like behavior when combined with command priorities. Multiple retry bindings for the same mechanism can be created at once; higher priority commands will take precedence over lower priority ones. This approach can be simpler than a :doc:`state machine ` when states don't interact with each other. + +For example, if a robot has an LED strip to indicate robot state and error conditions to the drivers or spectators, the commands for indicating more severe errors can have higher priorities than those for less severe errors. The triggers and bindings for coordinating the LED state can be configured in a single command: + +```java +Command ledControl = leds.run(coroutine -> { + // Highest severity error: continually set the comms warning while the DS is disconnected. + new Trigger(RobotState::isDSAttached).retryWhileFalse(leds.flashCommsWarning()); + + // Medium severity error: continually set the intake warning while the intake is broken. + // If interrupted by the comms warning, it will resume when DS connection is restored. + new Trigger(intake::isJammed).retryWhileTrue(leds.flashIntakeWarning()); + + // Lowest priority, not even an error: continually indicate the alliance color. + // NOTE: Because ledControl is the default command, we can't set a command-scoped + // default command; it'll override the external setting and immediately cancel ledControl. + new Trigger(() -> true).retryWhileTrue(leds.showAllianceColor()); + + // All the logic is in the triggers. Park the coroutine to keep the command + // alive, but there's nothing to do otherwise. + coroutine.park(); +}).named("LED Control"); + +leds.setDefaultCommand(ledControl); +``` + ### Toggle Bindings * ``toggleOnTrue(Command)``: Schedules the command on a ``false`` to ``true`` transition, and cancels it on the next ``false`` to ``true`` transition. @@ -64,13 +88,11 @@ Triggers can be combined using standard boolean operators to create more complex * ``Trigger.or(BooleanSupplier)``: High when **either** signal is high. * ``Trigger.negate()``: High when the original signal is low. -.. tab-set-code:: - - ```java - Trigger bothButtons = buttonA.and(buttonB); - Trigger eitherButton = buttonA.or(buttonB); - Trigger notButton = buttonA.negate(); - ``` +```java +Trigger bothButtons = buttonA.and(buttonB); +Trigger eitherButton = buttonA.or(buttonB); +Trigger notButton = buttonA.negate(); +``` ## Modifying Trigger Behavior @@ -102,41 +124,37 @@ For a full list of available controller classes and their methods, see the `org. You can also use axis values (like the analog sticks or analog triggers) to create triggers by using the ``axisGreaterThan``, ``axisLessThan``, or ``axisMagnitudeGreaterThan`` methods, or by providing a custom ``BooleanSupplier``. Axis-based triggers usually need a threshold and sometimes a debounce so small joystick noise does not repeatedly schedule and cancel commands. -.. tab-set-code:: - - ```java - // Trigger when the left Y axis is pushed more than 50% forward - Trigger highThrottle = new Trigger(() -> driverController.getLeftY() > 0.5); +```java +// Trigger when the left Y axis is pushed more than 50% forward +Trigger highThrottle = new Trigger(() -> driverController.getLeftY() > 0.5); - highThrottle.onTrue(Command.print("High Throttle!")); - ``` +highThrottle.onTrue(Command.print("High Throttle!")); +``` -.. tab-set-code:: +```java +import org.wpilib.framework.TimedRobot; +import org.wpilib.command3.button.RobotModeTriggers; - ```java - import org.wpilib.framework.TimedRobot; - import org.wpilib.command3.button.RobotModeTriggers; - - public class Robot extends TimedRobot { - public Robot() { - // GLOBAL SCOPE: This binding is always active - driverController.a().onTrue(arm.up()); - } - - @Override - public void autonomousInit() { - // OPMODE SCOPE: This binding only exists during autonomous - RobotModeTriggers.autonomous().onTrue(drive.followPath("AutoPath")); - } +public class Robot extends TimedRobot { + public Robot() { + // GLOBAL SCOPE: This binding is always active + driverController.a().onTrue(arm.up()); } - // COMMAND SCOPE example - public Command sweepAndScore() { - return Command.noRequirements(coroutine -> { - // This binding only exists while the 'sweepAndScore' command is running - intakeTrigger.onTrue(intake.runOnce()); - - coroutine.await(drive.followPath("SweepPath")); - }).named("Sweep and Score"); + @Override + public void autonomousInit() { + // OPMODE SCOPE: This binding only exists during autonomous + RobotModeTriggers.autonomous().onTrue(drive.followPath("AutoPath")); } - ``` +} + +// COMMAND SCOPE example +public Command sweepAndScore() { + return Command.noRequirements(coroutine -> { + // This binding only exists while the 'sweepAndScore' command is running + intakeTrigger.onTrue(intake.runOnce()); + + coroutine.await(drive.followPath("SweepPath")); + }).named("Sweep and Score"); +} +``` From 8844370f3fbb62be1d67de6d303d56eb9d610bdb Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:22:59 -0400 Subject: [PATCH 04/12] Move yield-in-loop warning and add note for Java-only --- .../software/commandbased/commands-v3/creating-commands.rst | 2 ++ source/docs/software/commandbased/commands-v3/index.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/source/docs/software/commandbased/commands-v3/creating-commands.rst b/source/docs/software/commandbased/commands-v3/creating-commands.rst index 8d19314d3e..d549acd1ca 100644 --- a/source/docs/software/commandbased/commands-v3/creating-commands.rst +++ b/source/docs/software/commandbased/commands-v3/creating-commands.rst @@ -78,6 +78,8 @@ public class ExampleArm implements Mechanism { Most commands need to run for more than a single loop cycle. This is done by using a loop (like ``while``) and calling ``coroutine.yield()`` at the end of every loop to allow other commands to run. +.. warning:: Calling ``coroutine.yield()`` is required to prevent commands from being greedy - if a command never yields, no other commands will be able to run and driver inputs will not be read until the command exits. WPILib will check ``while`` loops at compile-time to ensure that loops inside of command code will yield, and issue a compiler error if any greedy loops are found. + ```java public Command driveForward() { return run(coroutine -> { diff --git a/source/docs/software/commandbased/commands-v3/index.rst b/source/docs/software/commandbased/commands-v3/index.rst index 825cdb38cf..d393cb3c9f 100644 --- a/source/docs/software/commandbased/commands-v3/index.rst +++ b/source/docs/software/commandbased/commands-v3/index.rst @@ -22,7 +22,7 @@ Commands v3 command logic is written as ordinary Java code. If a command needs t 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. -.. warning:: Calling ``coroutine.yield()`` is required to prevent commands from being greedy - if a command never yields, no other commands will be able to run and driver inputs will not be read until the command exits. WPILib will check ``while`` loops at compile-time to ensure that loops inside of command code will yield, and issue a compiler error if any greedy loops are found. +.. 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 --------- From 03dbcca11d7612935fdabd2c044531f864748339 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:28:15 -0400 Subject: [PATCH 05/12] Update creating-commands --- .../software/commandbased/commands-v3/creating-commands.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/docs/software/commandbased/commands-v3/creating-commands.rst b/source/docs/software/commandbased/commands-v3/creating-commands.rst index d549acd1ca..16fc21af06 100644 --- a/source/docs/software/commandbased/commands-v3/creating-commands.rst +++ b/source/docs/software/commandbased/commands-v3/creating-commands.rst @@ -2,7 +2,7 @@ Commands can be created in one of three ways: -1. Using a :term:`factory method` on a :term:`Mechanism` object +1. Using a :term:`factory method` on a :term:`Mechanism` object. These factory methods make heavy use of :doc:`lambda functions ` 2. Using a static factory method from the ``Command`` interface 3. Creating a class that implements the ``Command`` interface. This approach is only recommended for very complex logic or for programmers who are uncomfortable with :term:`lambda functions` @@ -15,7 +15,7 @@ A key idea in the commands framework is that of requirements: every command requ The requirement system can only protect hardware that is controlled through commands. If other classes can reach into a mechanism and set motor outputs directly, those calls bypass the scheduler entirely. It is therefore *strongly* recommended to use the following system when writing code that controls physical hardware: 1. Write a class that implements the ``Mechanism`` interface -2. Make all fields in the class ``private`` to prevent external access +2. Most fields in the class should be ``private`` to prevent external access. :doc:`Trigger ` fields can be ``public`` so other mechanisms or external code can check on the state of the mechanism. Constants can also be public. 3. Make all methods that use those fields to control hardware also ``private`` 4. Write public ``Command``-returning methods for all control of the mechanism @@ -151,7 +151,7 @@ One-shot commands generally do one very simple thing and immediately exit withou ```java Command.noRequirements(_ -> gyro.reset()).named("Reset Gyro"); -Command.noRequirements(_ -> field = 0).named("Reset Field"); +Command.noRequirements(_ -> someVariable = 0).named("Reset Variable"); ``` One-shot commands are not bad. They are the right tool for small pieces of immediate work. The important rule is that "does not yield" also means "does not share time". If the action might take a noticeable amount of time, write it as a yielding command or move the expensive work somewhere that will not block robot control. From bf68cb58b92afac24ab900739b7c361092cf367f Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:29:28 -0400 Subject: [PATCH 06/12] Define DSL --- .../docs/software/commandbased/commands-v3/migration-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/docs/software/commandbased/commands-v3/migration-guide.rst b/source/docs/software/commandbased/commands-v3/migration-guide.rst index c8f7caf5ac..78eb4dc509 100644 --- a/source/docs/software/commandbased/commands-v3/migration-guide.rst +++ b/source/docs/software/commandbased/commands-v3/migration-guide.rst @@ -10,7 +10,7 @@ In commands v2, command behavior is commonly split across lifecycle methods such v3 commands are primarily expected to be created using builder objects, similar to the v2 fluent API where methods are chained together to configure the command object. A key change in the fluent API is that command names are now **required**; it's impossible to create a command without a name. Names are used in the v3 telemetry data (see :doc:`telemetry`) and are crucial for debugging. -The v3 command function is free-form and flexible and promotes standard Java language features instead of a custom DSL. If a command needs to run something repeatedly, use a standard Java ``while`` loop; if a command needs to do just one thing and exit, then just don't use ``yield``. +The v3 command function is free-form and flexible and promotes standard Java language features instead of a custom domain-specific language (DSL). If a command needs to run something repeatedly, use a standard Java ``while`` loop; if a command needs to do just one thing and exit, then just don't use ``yield``. .. tab-set:: From ee0af8353cd66364de5a61331bcf734fd3f2a1dc Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:31:02 -0400 Subject: [PATCH 07/12] Use "custom periodic functions" to define sideloads --- .../docs/software/commandbased/commands-v3/how-it-works.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/docs/software/commandbased/commands-v3/how-it-works.rst b/source/docs/software/commandbased/commands-v3/how-it-works.rst index 57724f49ee..0d4df043d9 100644 --- a/source/docs/software/commandbased/commands-v3/how-it-works.rst +++ b/source/docs/software/commandbased/commands-v3/how-it-works.rst @@ -10,13 +10,13 @@ The scheduler does not run commands on separate operating-system threads. Each c ### Phase 1: Cleanup -In the cleanup phase, the scheduler removes any trigger bindings or sideloaded 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. +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 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. +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. From 8e20c2797a459b6d4f80b73b5c268af5f26ea48b Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 22:51:23 -0400 Subject: [PATCH 08/12] Remove dangling text --- .../commandbased/commands-v3/structuring-your-project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/docs/software/commandbased/commands-v3/structuring-your-project.rst b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst index 45b629948e..98166dba25 100644 --- a/source/docs/software/commandbased/commands-v3/structuring-your-project.rst +++ b/source/docs/software/commandbased/commands-v3/structuring-your-project.rst @@ -44,7 +44,7 @@ OpMode constructors are generally all that's needed. WPILib will automatically c Commands that only interact with a single mechanism should be defined as methods in that mechanism's class using the ``run`` or ``runRepeatedly`` builder methods provided by the ``Mechanism`` interface. These builder methods make the created command automatically require the mechanism so you can't forget to set the requirement. -.. warning:: Command methods are designed to create a ``Command`` object that will be run at a later point in the program. Only the code in the lambda function passed to ``run`` or ``runRepeatedly`` will execute when the command is running. All code +.. warning:: Command methods are designed to create a ``Command`` object that will be run at a later point in the program. Only the code in the lambda function passed to ``run`` or ``runRepeatedly`` will execute when the command is running. ```java package first.robot.mechanisms; From 3e3bc5930ea642ed1defd71e241c17f59c37c436 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 5 Aug 2026 23:00:39 -0400 Subject: [PATCH 09/12] Update telemetry --- .../commandbased/commands-v3/telemetry.rst | 108 ++++++++++++------ 1 file changed, 70 insertions(+), 38 deletions(-) diff --git a/source/docs/software/commandbased/commands-v3/telemetry.rst b/source/docs/software/commandbased/commands-v3/telemetry.rst index 7074b5d239..cedb9680b9 100644 --- a/source/docs/software/commandbased/commands-v3/telemetry.rst +++ b/source/docs/software/commandbased/commands-v3/telemetry.rst @@ -6,7 +6,7 @@ The commands library provides built-in support for telemetry and logging. This a The ``Scheduler`` and its running commands can be serialized using Google Protocol Buffers (Protobuf). This is the primary way that command state is sent to external tools like AdvantageScope or the WPILib Dashboards. -The ``Scheduler`` implements the ``ProtobufSerializable`` interface, which means it can be sent over NetworkTables and logged using the ``ProtobufLogEntry`` API. +The ``Scheduler`` implements the ``ProtobufSerializable`` interface, which means it can be sent over NetworkTables and logged using the ``ProtobufLogEntry`` API or :doc:`Epilogue <../../telemetry/robot-telemetry-with-annotations>` if assigned to a field in a logged class. Scheduler state is a snapshot. It answers questions like "which commands are running right now?" and "which mechanisms do they require?" Scheduler events are a timeline. They answer questions like "why did this command stop?" and "what interrupted it?" In practice, teams often want both. @@ -71,48 +71,80 @@ While printing to the console is useful for quick debugging, it is not recommend You can log individual scheduler events to a data log using a ``StringLogEntry``. This is particularly useful for tracking the exact sequence of command lifecycle events during a match. The event stream is often the fastest way to explain a command that "randomly stopped": look for an ``Interrupted`` event, then inspect the interrupter command and the mechanisms both commands required. -.. tab-set-code:: +.. tab-set:: - ```java - import org.wpilib.command3.Scheduler; - import org.wpilib.command3.SchedulerEvent; - import org.wpilib.datalog.StringLogEntry; - import org.wpilib.system.DataLogManager; + .. tab-item:: Manual Data Logging + :sync: datalog - // Create a log entry for scheduler events - StringLogEntry eventLog = new StringLogEntry(DataLogManager.getLog(), "SchedulerEvents"); + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.command3.SchedulerEvent; + import org.wpilib.datalog.StringLogEntry; + import org.wpilib.system.DataLogManager; - // Register a listener to log every event - Scheduler.getDefault().addEventListener(event -> { - // Log the string representation of the event - eventLog.append(event.toString()); - }); - ``` + // Create a log entry for scheduler events + StringLogEntry eventLog = new StringLogEntry(DataLogManager.getLog(), "SchedulerEvents"); + + // Register a listener to log every event + Scheduler.getDefault().addEventListener(event -> { + // Log the string representation of the event + eventLog.append(event.toString()); + }); + ``` ### Logging Scheduler State -Because the ``Scheduler`` class implements ``ProtobufSerializable``, you can log the entire state of the scheduler, including running commands and their mechanisms, using a ``ProtobufLogEntry``. This allows tools like AdvantageScope to visualize the state of the command scheduler over time. +Because the ``Scheduler`` class implements ``ProtobufSerializable``, you can log the entire state of the scheduler, including running commands and their mechanisms, using a ``ProtobufLogEntry``. This allows tools like AdvantageScope to visualize the state of the command scheduler over time. Epilogue supports logging protobuf-serializable objects, so storing the scheduler object in a field in the ``Robot`` class is an easy way to get automatic logging. -Because the scheduler state changes every loop cycle, you should append the current state to the log in your ``robotPeriodic`` method. +Because the scheduler state changes every loop cycle, you should append the current state to the log or call ``Epilogue.update`` in ``robotPeriodic`` to ensure telemetry is always updated. -```java -import org.wpilib.command3.Scheduler; -import org.wpilib.datalog.ProtobufLogEntry; -import org.wpilib.system.DataLogManager; -import org.wpilib.framework.TimedRobot; - -public class Robot extends TimedRobot { - // Create a log entry for the scheduler state - private final ProtobufLogEntry schedulerLog = - ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto); - - @Override - public void robotPeriodic() { - // Run the scheduler - Scheduler.getDefault().run(); - - // Log the current state of the scheduler - schedulerLog.append(Scheduler.getDefault()); - } -} -``` +.. tab-set:: + + .. tab-item:: Epilogue + :sync: epilogue + + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.epilogue.Epilogue; + import org.wpilib.epilogue.Logged; + import org.wpilib.framework.TimedRobot; + + @Logged + public class Robot extends TimedRobot { + private final Scheduler scheduler = Scheduler.getDefault(); + + @Override + public void robotPeriodic() { + // Run the scheduler + scheduler.run(); + + // Update telemetry + Epilogue.update(this); + } + } + ``` + + .. tab-item:: Manual Data Logging + :sync: datalog + + ```java + import org.wpilib.command3.Scheduler; + import org.wpilib.datalog.ProtobufLogEntry; + import org.wpilib.system.DataLogManager; + import org.wpilib.framework.TimedRobot; + + public class Robot extends TimedRobot { + // Create a log entry for the scheduler state + private final ProtobufLogEntry schedulerLog = + ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto); + + @Override + public void robotPeriodic() { + // Run the scheduler + Scheduler.getDefault().run(); + + // Log the current state of the scheduler + schedulerLog.append(Scheduler.getDefault()); + } + } + ``` From a5b6f4e94ab34369953c0328e892f34fe0b59517 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 6 Aug 2026 07:56:49 -0400 Subject: [PATCH 10/12] Remove extra backtick Co-authored-by: Dan Katzuv <31829093+katzuv@users.noreply.github.com> --- source/docs/software/commandbased/commands-v3/how-it-works.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/docs/software/commandbased/commands-v3/how-it-works.rst b/source/docs/software/commandbased/commands-v3/how-it-works.rst index 0d4df043d9..e9ec491aa7 100644 --- a/source/docs/software/commandbased/commands-v3/how-it-works.rst +++ b/source/docs/software/commandbased/commands-v3/how-it-works.rst @@ -39,7 +39,7 @@ Commands in the pending set have been requested, but they have not necessarily s 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()```). +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. From ee2ad9f87d34c87428382bac9d649b843b862443 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 6 Aug 2026 11:34:07 -0400 Subject: [PATCH 11/12] Cleanup migration code sample code --- .../commands-v3/migration-guide.rst | 322 +++++++++++------- 1 file changed, 193 insertions(+), 129 deletions(-) diff --git a/source/docs/software/commandbased/commands-v3/migration-guide.rst b/source/docs/software/commandbased/commands-v3/migration-guide.rst index 78eb4dc509..95b57b52f1 100644 --- a/source/docs/software/commandbased/commands-v3/migration-guide.rst +++ b/source/docs/software/commandbased/commands-v3/migration-guide.rst @@ -70,8 +70,7 @@ The v3 command function is free-form and flexible and promotes standard Java lan setVoltage(4); coroutine.waitUntil(this::atTop); stop(); - }).whenCanceled(this::stop) - .named("Arm Up"); + }).named("Arm Up"); } ``` @@ -167,13 +166,12 @@ However, there is no similar API to the subsystem-level ``periodic()`` function. setVoltage(6); coroutine.waitUntil(this::atTop); setVoltage(0); - }).whenCanceled(() -> setVoltage(0)) - .named("Elevator Up"); + }).named("Elevator Up"); } public Command holdPosition() { return runRepeatedly(() -> setVoltage(feedforwardForCurrentHeight())) - .withPriority(Command.LOWEST_PRIORITY + 1) + .withPriority(Command.LOWEST_PRIORITY) .named("Hold Elevator"); } } @@ -287,8 +285,7 @@ Like v2, a command created with a mechanism's ``run()`` or ``runRepeatedly()`` h motor.set(0.8); coroutine.waitUntil(hasGamePiece); motor.set(0); - }).whenCanceled(() -> motor.set(0)) - .named("Intake"); + }).named("Intake"); } ``` @@ -327,7 +324,7 @@ The differences worth remembering are: public Drive(CommandGamepad controller) { setDefaultCommand( runRepeatedly(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) - .withPriority(Command.LOWEST_PRIORITY + 1) + .withPriority(Command.LOWEST_PRIORITY) .named("Teleop Drive")); } } @@ -367,8 +364,7 @@ For a command that runs until a condition is met, use ``waitUntil(...)``: spinUp(); coroutine.waitUntil(this::atSpeed); feedNote(); - }).whenCanceled(this::stop) - .named("Shoot When Ready"); + }).named("Shoot When Ready"); } ``` @@ -404,8 +400,7 @@ For a command that updates every scheduler cycle, use a loop and yield: coroutine.yield(); } stop(); - }).whenCanceled(this::stop) - .named("Drive Distance"); + }).named("Drive Distance"); } ``` @@ -557,17 +552,44 @@ In v3, an inner command scheduled via with a coroutine's ``fork``, ``await``, `` Use ``coroutine.await(...)`` to run a child command and wait until it finishes. -.. tab-set-code:: +.. tab-set:: - ```java - public Command scoreSequence() { - return Command.noRequirements(coroutine -> { - coroutine.await(drive.driveToScoringLocation()); - coroutine.await(elevator.moveToScoringHeight()); - coroutine.await(gripper.release()); - }).named("Score Sequence"); - } - ``` + .. tab-item:: v2 Sequence + :sync: v2 + + ```java + public Command scoreSequence() { + return Commands.sequence( + drive.driveToScoringLocation(), + elevator.moveToScoringHeight(), + gripper.release() + ).withName("Score Sequence"); + } + ``` + + .. tab-item:: v3 Sequence (builders) + + ```java + public Command scoreSequence() { + return drive.driveToScoringLocation() + .andThen(elevator.moveToScoringHeight()) + .andThen(gripper.release()) + .named("Score Sequence") + } + ``` + + .. tab-item:: v3 Sequence (coroutines) + :sync: v3 + + ```java + public Command scoreSequence() { + return Command.noRequirements(coroutine -> { + coroutine.await(drive.driveToScoringLocation()); + coroutine.await(elevator.moveToScoringHeight()); + coroutine.await(gripper.release()); + }).named("Score Sequence"); + } + ``` The parent command above requires no mechanisms. The drivetrain, elevator, and gripper are only owned while their own commands are running, which allows other commands to control them when the scoring sequence doesn't actively control them. This can be a strength because it allows default commands to run, but it also allows other commands to run that would break the sequence (such as moving the drivebase out of the scoring location while the elevator is moving, possibly tipping the robot). Care should be taken to avoid running sequence-breaking commands, or set default commands within the command @@ -576,39 +598,77 @@ The parent command above requires no mechanisms. The drivetrain, elevator, and g Use ``fork(...)`` to start child commands that should run in the background, or ``await``, ``awaitAll``, or ``awaitAny`` to fork and then wait for the child commands to finish. -.. tab-set-code:: +.. tab-set:: - ```java - public Command prepareToScore() { - return Command.noRequirements(coroutine -> { - // Start the turret and shooter commands, and wait for both to finish. - coroutine.awaitAll(turret.aimAtGoal(), shooter.spinUp()); + .. tab-item:: v2 Parallel Group + :sync: v2 - // Feed a ball into the shooter only after the turret and shooter are ready - coroutine.await(feeder.feed()); - }).named("Prepare To Score"); - } - ``` + ```java + public Command prepareToScore() { + return turret.aimAtGoal().alongWith(shooter.spinUp()).withName("Prepare To Score"); + } + ``` + + .. tab-item:: v3 Parallel Group (builders) + + + ```java + public Command prepareToScore() { + return turret.aimAtGoal().alongWith(shooter.spinUp()).named("Prepare To Score"); + } + ``` + + .. tab-item:: v3 Parallel (awaitAll) + :sync: v3 + + ```java + public Command prepareToScore() { + return Command.noRequirements(coroutine -> { + coroutine.awaitAll(turret.aimAtGoal(), shooter.spinUp()); + }).named("Prepare To Score"); + } + ``` ### Race Work Use ``awaitAny(...)`` when several child commands should start and the parent should continue after the first one finishes. The remaining commands are canceled. -.. tab-set-code:: +.. tab-set:: + + .. tab-item:: v2 Race Group + :sync: v2 + + ```java + Command intakeUntilPieceOrTimeout() { + return intake.intake() + .raceWith(Commands.waitTime(Seconds.of(2)) + .withName("Intake Until Piece Or Timeout"); + } + ``` - ```java - public Command intakeUntilPieceOrTimeout() { - return Command.noRequirements(coroutine -> { - coroutine.awaitAny( + .. tab-item:: v3 Race (builders) + + ```java + public Command intakeUntilPieceOrTimeout() { + return Command.race( intake.intake(), - Command.waitFor(Seconds.of(2)).named("Intake Timeout")); + Command.waitFor(Seconds.of(2)).named("Intake Timeout") + ).named("Intake Until Piece Or Timeout"); + } + ``` - if (!intake.hasGamePiece()) { - intake.setNoPieceAlert(); - } - }).named("Intake Until Piece Or Timeout"); - } - ``` + .. tab-item:: v3 Race (awaitAny) + :sync: v3 + + ```java + public Command intakeUntilPieceOrTimeout() { + return Command.noRequirements(coroutine -> { + coroutine.awaitAny( + intake.intake(), + Command.waitFor(Seconds.of(2)).named("Intake Timeout")); + }).named("Intake Until Piece Or Timeout"); + } + ``` For simple race groups, ``Command.race(...)`` is also available. Use explicit coroutine logic when the next step depends on which condition won or when you need additional fallback behavior. @@ -657,22 +717,20 @@ This is often the cleanest replacement for v2 proxy-heavy code. The requirements Most trigger bindings carry over by name or by intent: ``onTrue``, ``onFalse``, ``whileTrue``, ``whileFalse``, and toggle bindings all exist in v3. The important new idea is :doc:`scopes`: a binding created in the robot constructor is global and will always be active; a binding created while an OpMode is running is only active while that OpMode is selected on the driverstation, and will be deleted when the OpMode changes; and a binding created inside a running command is removed when that command exits, and any command attached to that binding is canceled. -.. tab-set-code:: - - ```java - public Command aimAndShootWhenReady() { - return Command.noRequirements(coroutine -> { - // This binding only exists while aimAndShootWhenReady is running. - shooter.atSpeed.onTrue(feeder.feedOnce()); +```java +public Command aimAndShootWhenReady() { + return Command.noRequirements(coroutine -> { + // This binding only exists while aimAndShootWhenReady is running. + shooter.atSpeed.onTrue(feeder.feedOnce()); - // shooter.spinUp() only runs while aimAndShootWhenReady is running, - // and will be canceled when aimAndShootWhenReady exits - coroutine.fork(shooter.spinUp()); + // shooter.spinUp() only runs while aimAndShootWhenReady is running, + // and will be canceled when aimAndShootWhenReady exits + coroutine.fork(shooter.spinUp()); - coroutine.await(turret.aimAtGoal()); - }).named("Aim And Shoot When Ready"); - } - ``` + coroutine.await(turret.aimAtGoal()); + }).named("Aim And Shoot When Ready"); +} +``` New in v3 are the ``retryWhileTrue`` and ``retryWhileFalse`` bindings. A retry binding restarts its command if the command finishes while the trigger signal is still active, unlike ``whileTrue`` or ``whileFalse`` which will not restart the the command if it finishes or is interrupted before the trigger condition changes. They act similar to a v2-style ``whileTrue(command.repeatedly())`` binding. @@ -682,18 +740,16 @@ The same cancellation and interruption concepts carry over from v2 in v3: cancel Use ``whenCanceled(...)`` for cleanup that must happen when a command is canceled. Note that this runs regardless of *why* the command was canceled. -.. tab-set-code:: - - ```java - public Command runRollerUntilLoaded() { - return run(coroutine -> { - roller.set(0.6); - coroutine.waitUntil(hasGamePiece); - roller.set(0); - }).whenCanceled(() -> roller.set(0)) - .named("Run Roller Until Loaded"); - } - ``` +```java +public Command runRollerUntilLoaded() { + return run(coroutine -> { + roller.set(0.6); + coroutine.waitUntil(hasGamePiece); + roller.set(0); + }).whenCanceled(() -> roller.set(0)) + .named("Run Roller Until Loaded"); +} +``` Do not put long loops in cancellation cleanup. Cancellation cleanup should be short and single-shot: stop a motor, clear a flag, or close a resource. @@ -703,80 +759,88 @@ Scheduler telemetry reports these cases separately. A command that finishes norm ### Default Drive Command -.. tab-set-code:: +.. tab-set:: - ```java - public class Drive implements Mechanism { - public Command teleopDrive(CommandGamepad controller) { - return runRepeatedly(() -> - arcadeDrive(controller.getLeftY(), controller.getRightX())) - .withPriority(Command.LOWEST_PRIORITY + 1) - .named("Teleop Drive"); + .. tab-item:: v2 + :sync: v2 + + ```java + public class Drive extends SubsystemBase { + public Command teleopDrive(CommandGamepad controller) { + return run(() -> arcadeDrive(controller.getLeftY(), controller.getRightX()) + .withName("Teleop Drive"); + } } - } - ``` + ``` -### Timed Command + .. tab-item:: v3 + :sync: v3 -.. tab-set-code:: - - ```java - public Command outtakeFor(Time duration) { - return run(coroutine -> { - motor.set(-0.7); - coroutine.wait(duration); - motor.set(0); - }).whenCanceled(() -> motor.set(0)) - .named("Timed Outtake"); - } - ``` + ```java + public class Drive implements Mechanism { + public Command teleopDrive(CommandGamepad controller) { + return runRepeatedly(() -> arcadeDrive(controller.getLeftY(), controller.getRightX())) + .withPriority(Command.LOWEST_PRIORITY) + .named("Teleop Drive"); + } + } + ``` -For a timeout around an existing command, use ``withTimeout(...)``: +### Timed Command -.. tab-set-code:: +.. tab-set:: - ```java - Command safeMoveToTop = elevator.up().withTimeout(Seconds.of(1.5)); - ``` + .. tab-item:: v2 + :sync: v2 -### Conditional Wait With Fallback + ```java + public Command outtakeFor(Time duration) { + return startEnd( + () -> motor.set(-0.7), + () -> motor.set(0) + ).withTimeout(duration) + .withName("Timed Outtake"); + } + ``` + + .. tab-item:: v3 + :sync: v3 -.. tab-set-code:: + ```java + public Command outtakeFor(Time duration) { + return run(coroutine -> { + motor.set(-0.7); + coroutine.wait(duration); + motor.set(0); + }).named("Timed Outtake"); + } + ``` - ```java - public Command safeMoveToTop() { - return run(coroutine -> { - motor.setVoltage(6); - var result = coroutine.waitUntil(this::atTop, Seconds.of(1.5)); - motor.setVoltage(0); +For a timeout around an existing command, use ``withTimeout(...)``: - if (result.timedOut()) { - setJamAlert(); - } else { - clearJamAlert(); - } - }).whenCanceled(() -> motor.setVoltage(0)) - .named("Safe Move To Top"); - } +```java +Command safeMoveToTop = elevator.up().withTimeout(Seconds.of(1.5)); ``` -### Autonomous Routine - -.. tab-set-code:: +### Conditional Wait With Fallback - ```java - public Command autoScoreAndLeave() { - return Command.noRequirements(coroutine -> { - coroutine.await(drive.followPath("ScorePath")); +The coroutine ``waitUntil`` method takes an optional timeout parameter so the wait doesn't last forever. For example, an elevator may be jammed if it doesn't reach a set position within the expected timeframe. - coroutine.fork(shooter.spinUp()); - coroutine.await(elevator.moveToScoringHeight()); - coroutine.await(gripper.release()); +```java +public Command safeMoveToTop() { + return run(coroutine -> { + motor.setVoltage(6); + WaitResult result = coroutine.waitUntil(this::atTop, Seconds.of(1.5)); + motor.setVoltage(0); - coroutine.await(drive.followPath("LeaveCommunity")); - }).named("Auto Score And Leave"); - } - ``` + if (result.timedOut()) { + setJamAlert(); + } else { + clearJamAlert(); + } + }).named("Safe Move To Top"); +} +``` ## What Not To Carry Over From cad04c5488042a85d3ba8c907afa04ae2999546b Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 6 Aug 2026 12:03:22 -0400 Subject: [PATCH 12/12] Discourage one-shot commands for actuation --- .../commands-v3/creating-commands.rst | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/source/docs/software/commandbased/commands-v3/creating-commands.rst b/source/docs/software/commandbased/commands-v3/creating-commands.rst index 16fc21af06..074e030fa1 100644 --- a/source/docs/software/commandbased/commands-v3/creating-commands.rst +++ b/source/docs/software/commandbased/commands-v3/creating-commands.rst @@ -156,6 +156,43 @@ Command.noRequirements(_ -> someVariable = 0).named("Reset Variable"); One-shot commands are not bad. They are the right tool for small pieces of immediate work. The important rule is that "does not yield" also means "does not share time". If the action might take a noticeable amount of time, write it as a yielding command or move the expensive work somewhere that will not block robot control. +.. warning:: One-shot commands should **never** control motors or actuators. + +### Not For Actuation + +1. One-shot commands don't appear in the default :doc:`telemetry `, making them harder to debug. +2. If a one-shot command is awaited by a parent command, it finishes immediately and the parent may assume that the controlled mechanism has reached the state or position set by the one-shot command. + +For example, if a robot has a pneumatic slap-down intake that's prone to jams if the rollers start spinning before it's fully extended, you may want a command sequence that deploys the intake (waiting for it to fully deploy), and only then start spinning the rollers. If the deploy command is a one-shot, the sequence will *immediately* start to spin the rollers instead of waiting like it's supposed to. + +```java +Command startIntaking() { + return Command.noRequirements(coroutine -> { + coroutine.await(intakeWrist.deployIntake()); + coroutine.await(intakeRollers.run()); + }).named("Start Intaking"); +} + +// Bad: returns immediately, and a sequence using it immediately moves to the next command +public Command deployIntake() { + return run(_ -> solenoid.set(FORWARD)).named("Bad Intake Deploy"); +} + +public Command deployIntake() { + return run(coroutine -> { + solenoid.set(FORWARD); + + // Good: wait until a sensor tells you the intake is down + coroutine.waitUntil(() -> wristEncoder.getPosition() <= WRIST_DEPLOYED_ANGLE); + + // Decent: If there's no sensor, wait for about as much time as it takes to deploy + // This can be inconsistent depending on available air pressure and load on the intake, + // so it's not as good as using sensor feedback + coroutine.wait(Seconds.of(0.35)); + }).nameD("Better Intake Deploy") +} +``` + ## Complex Command Logic For more complex logic, you can use the various methods on the ``Coroutine`` object to coordinate multiple actions.