diff --git a/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/command3/button/CommandXboxController.java b/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/command3/button/CommandXboxController.java index 0e6fa243..ac47ce79 100644 --- a/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/command3/button/CommandXboxController.java +++ b/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/command3/button/CommandXboxController.java @@ -18,6 +18,10 @@ public double getLeftY() { return 0; } + public double getRightX() { + return 0; + } + public Trigger a() { return new Trigger(() -> false); } diff --git a/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/driverstation/XboxController.java b/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/driverstation/XboxController.java index ba490e5e..0bb5312b 100644 --- a/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/driverstation/XboxController.java +++ b/examples/stage1/stage1b/snippets/src/main/java/org/wpilib/driverstation/XboxController.java @@ -15,4 +15,8 @@ public XboxController(int id) {} public boolean getAButton() { return false; } + + public boolean getLeftBumper() { + return false; + } } diff --git a/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheError.java b/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheError.java new file mode 100644 index 00000000..ce1b5663 --- /dev/null +++ b/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheError.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026 FRCSoftware + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package sources; + +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.command3.Trigger; +import org.wpilib.driverstation.XboxController; + +class SpotTheError { + private final XboxController xbox = new XboxController(0); + private final Intake intake = new Intake(); + + void triggerBindings() { + // [triggerCapturedValueBug] + boolean aButton = xbox.getAButton(); + Trigger aButtonTrigger = new Trigger(() -> aButton); + aButtonTrigger.whileTrue(intake.runAtThrottle(0.5)); + // [/triggerCapturedValueBug] + + // [intakeOnTrueBug] + new Trigger(() -> xbox.getLeftBumper()).onTrue(intake.runAtThrottle(0.5)); + // [/intakeOnTrueBug] + + // [intakeWhileTrue] + new Trigger(() -> xbox.getLeftBumper()).whileTrue(intake.runAtThrottle(0.5)); + // [/intakeWhileTrue] + } + + void triggerBindingsFixed() { + // [triggerCapturedValueFix] + Trigger aButtonTrigger = new Trigger(() -> xbox.getAButton()); + aButtonTrigger.whileTrue(intake.runAtThrottle(0.5)); + // [/triggerCapturedValueFix] + } + + // [intakeClass] + class Intake implements Mechanism { + private final ExampleMotor motor = new ExampleMotor(); + + Command runAtThrottle(double throttle) { + return run(coroutine -> { + while (true) { + motor.setThrottle(throttle); + coroutine.yield(); + } + }) + .named("Intake"); + } + } + // [/intakeClass] + + void intakeClassWithDefaultCmd() { + // [intakeClassWithDefaultCmd] + class Intake implements Mechanism { + private final ExampleMotor motor = new ExampleMotor(); + + public Intake() { + setDefaultCommand(runAtThrottle(0)); + } + + Command runAtThrottle(double throttle) { + return run(coroutine -> { + while (true) { + motor.setThrottle(throttle); + coroutine.yield(); + } + }) + .named("Intake"); + } + } + // [/intakeClassWithDefaultCmd] + } +} diff --git a/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheErrorPt2.java b/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheErrorPt2.java new file mode 100644 index 00000000..7ed0dca9 --- /dev/null +++ b/examples/stage1/stage1b/snippets/src/main/java/sources/SpotTheErrorPt2.java @@ -0,0 +1,123 @@ +/* + * Copyright 2026 FRCSoftware + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package sources; + +import java.util.function.DoubleSupplier; +import org.wpilib.command3.Command; +import org.wpilib.command3.Mechanism; +import org.wpilib.command3.Scheduler; +import org.wpilib.command3.button.CommandXboxController; +import org.wpilib.drive.DifferentialDrive; +import org.wpilib.framework.OpModeRobot; +import org.wpilib.hardware.imu.OnboardIMU; + +public class SpotTheErrorPt2 { + private final DifferentialDrive differentialDrive = null; + private final OnboardIMU imu = null; + + class ArcadeDriveBug implements Mechanism { + // [arcadeDriveBug] + Command arcadeDrive(DoubleSupplier forwardThrottle, DoubleSupplier rotationThrottle) { + double forward = forwardThrottle.getAsDouble(); + double rotation = rotationThrottle.getAsDouble(); + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive(forward, rotation); + coroutine.yield(); + } + }) + .named("Drive"); + } + // [/arcadeDriveBug] + } + + class ArcadeDriveFix implements Mechanism { + // [arcadeDriveFix] + Command arcadeDrive(DoubleSupplier forwardThrottle, DoubleSupplier rotationThrottle) { + return run(coroutine -> { + while (true) { + differentialDrive.arcadeDrive( + forwardThrottle.getAsDouble(), rotationThrottle.getAsDouble()); + coroutine.yield(); + } + }) + .named("Drive"); + } + // [/arcadeDriveFix] + } + + // [robotPeriodicBug] + public class Robot extends OpModeRobot { + private final CommandXboxController xbox = new CommandXboxController(0); + + public Robot() { + xbox.a().whileTrue(printHelloWorld()); + } + + @Override + public void robotPeriodic() {} + + private Command printHelloWorld() { + return Command.noRequirements(coroutine -> { + while (true) { + System.out.println("Hello World!"); + coroutine.yield(); + } + }) + .named("Hello World!"); + } + } + // [/robotPeriodicBug] + + class RobotFixed extends OpModeRobot { + private final Drivetrain drivetrain = new Drivetrain(); + + // [robotPeriodicFix] + @Override + public void robotPeriodic() { + Scheduler.getDefault().run(); + } + // [/robotPeriodicFix] + } + + class RotateInPlaceBug implements Mechanism { + // [rotateInPlaceBug] + Command rotateInPlace(double angleDegrees) { + double targetAngle = imu.getRotation2d().getDegrees() + angleDegrees; + return run(coroutine -> { + while (imu.getRotation2d().getDegrees() < targetAngle) { + differentialDrive.arcadeDrive(0.0, 0.2); + coroutine.yield(); + } + }) + .named("RotateInPlace"); + } + // [/rotateInPlaceBug] + } + + class RotateInPlaceFix implements Mechanism { + // [rotateInPlaceFix] + Command rotateInPlace(double angleDegrees) { + return run(coroutine -> { + double targetAngle = imu.getRotation2d().getDegrees() + angleDegrees; + while (imu.getRotation2d().getDegrees() < targetAngle) { + differentialDrive.arcadeDrive(0.0, 0.2); + coroutine.yield(); + } + }) + .named("RotateInPlace"); + } + // [/rotateInPlaceFix] + } + + class Drivetrain implements Mechanism { + void periodic() {} + + Command arcadeDrive(DoubleSupplier forwardThrottle, DoubleSupplier rotationThrottle) { + return null; + } + } +} diff --git a/src/config/sidebarConfig.ts b/src/config/sidebarConfig.ts index aff81701..c6569f82 100644 --- a/src/config/sidebarConfig.ts +++ b/src/config/sidebarConfig.ts @@ -137,6 +137,10 @@ export const sidebarSections: Record = { label: 'Commands & Mechanisms, Pt. 2', slug: 'learning-course/stage1/stage1b/commands-and-mechanisms-pt2', }, + { + label: 'Bonus: Spot the Error', + slug: 'learning-course/stage1/stage1b/spot-the-error', + }, { label: 'Exercise - Kitbot Rewrite, Pt. 1', slug: 'learning-course/stage1/stage1b/command-based-kitbot', @@ -149,6 +153,10 @@ export const sidebarSections: Record = { label: 'Exercise - Kitbot Rewrite, Pt. 2', slug: 'learning-course/stage1/stage1b/command-based-kitbot-pt2', }, + { + label: 'Bonus: Spot the Error, Pt 2', + slug: 'learning-course/stage1/stage1b/spot-the-error-pt2', + }, ], }, ], diff --git a/src/content/docs/learning-course/stage1/stage1b/command-based-kitbot-pt2.mdx b/src/content/docs/learning-course/stage1/stage1b/command-based-kitbot-pt2.mdx index 00eb2a17..05ee8b1d 100644 --- a/src/content/docs/learning-course/stage1/stage1b/command-based-kitbot-pt2.mdx +++ b/src/content/docs/learning-course/stage1/stage1b/command-based-kitbot-pt2.mdx @@ -2,7 +2,7 @@ title: Exercise - Kitbot Rewrite, Pt. 2 description: Rewriting the kitbot code in stage 1A to command-based prev: learning-course/stage1/stage1b/suppliers-in-command-based -next: false +next: learning-course/stage1/stage1b/spot-the-error-pt2 codeRegionSources: default: stage1/stage1b/snippets/src/main/java/sources/CommandBasedKitbotPt2.java --- diff --git a/src/content/docs/learning-course/stage1/stage1b/commands-and-mechanisms-pt2.mdx b/src/content/docs/learning-course/stage1/stage1b/commands-and-mechanisms-pt2.mdx index 2164d60e..9d88b62f 100644 --- a/src/content/docs/learning-course/stage1/stage1b/commands-and-mechanisms-pt2.mdx +++ b/src/content/docs/learning-course/stage1/stage1b/commands-and-mechanisms-pt2.mdx @@ -2,7 +2,7 @@ title: Commands and Mechanisms, Part 2 description: Intermediate-level uses of commands and mechanisms prev: learning-course/stage1/stage1b/triggers -next: learning-course/stage1/stage1b/command-based-kitbot +next: learning-course/stage1/stage1b/spot-the-error codeRegionSources: default: stage1/stage1b/snippets/src/main/java/sources/CommandsAndMechsPt2.java --- diff --git a/src/content/docs/learning-course/stage1/stage1b/spot-the-error-pt2.mdx b/src/content/docs/learning-course/stage1/stage1b/spot-the-error-pt2.mdx new file mode 100644 index 00000000..dcea6c47 --- /dev/null +++ b/src/content/docs/learning-course/stage1/stage1b/spot-the-error-pt2.mdx @@ -0,0 +1,72 @@ +--- +title: Spot the Error, Part 2 +description: A review exercise for stage 1B +prev: learning-course/stage1/stage1b/command-based-kitbot-pt2 +next: false +codeRegionSources: + default: stage1/stage1b/snippets/src/main/java/sources/SpotTheErrorPt2.java +--- + +### Exercise 1 + +"Hello World!" doesn't show up in the console while the a button is pressed. +Can you spot the error? + +```java #robotPeriodicBug + +``` + +
+ Reveal + The scheduler never runs, so scheduled commands never execute. + Call `Scheduler.getDefault().run()` inside `robotPeriodic()`: + + ```java #robotPeriodicFix + + ``` + +
+ +### Exercise 2 + +This command is supposed to drive the robot using the joystick's forward and rotation axes. + +```java #arcadeDriveBug + +``` + +Can you spot the error? + +
+ Reveal + The supplier values are read once, before the command starts running. + The robot drives at the same speed and rotation forever, even as the joystick moves. + Read the values from the suppliers inside the loop instead: + + ```java #arcadeDriveFix + + ``` + +
+ +### Exercise 3 + +This command is supposed to rotate the robot 90 degrees clockwise from its starting position. + +```java #rotateInPlaceBug + +``` + +Can you spot the error? + +
+ Reveal + The target angle is read once, before the command starts running. + If the robot's heading changes before the command runs, it will rotate to the wrong angle. + Read the target angle from the IMU when the command starts running instead: + + ```java #rotateInPlaceFix + + ``` + +
diff --git a/src/content/docs/learning-course/stage1/stage1b/spot-the-error.mdx b/src/content/docs/learning-course/stage1/stage1b/spot-the-error.mdx new file mode 100644 index 00000000..cfd55aa2 --- /dev/null +++ b/src/content/docs/learning-course/stage1/stage1b/spot-the-error.mdx @@ -0,0 +1,139 @@ +--- +title: Spot the Error +description: A review exercise for stage 1B +prev: learning-course/stage1/stage1b/commands-and-mechanisms-pt2 +next: learning-course/stage1/stage1b/command-based-kitbot +codeRegionSources: + default: stage1/stage1b/snippets/src/main/java/sources/SpotTheError.java +--- + +import Aside from '@components/Aside.astro'; + +### The Purpose of this Section + +By looking through buggy code snippets that use command-based programming, +you can hopefully get a better intuition on how to spot common bugs and write better code. +If you are confident in your skills, you can skip ahead to the stage 1B exercise, but +the authors still highly recommend it. + + + +### Exercise 1 + +This code is supposed to spin the intake while the left bumper is held. +The intake should stop when the bumper is released. +It also compiles fine. + +```java #intakeOnTrueBug + +``` + +Can you spot the error? + +
+ Reveal + The `onTrue()` binding schedules the command once, when the bumper is first pressed. + The command keeps running even after the bumper is released. + Use `whileTrue()` instead, which runs the command for as long as the trigger is active: + + ```java #intakeWhileTrue + + ``` + +
+ +### Exercise 2 + +The `aButtonTrigger` is supposed to run a command while the A button is held. +The code compiles fine. + +```java #triggerCapturedValueBug + +``` + +Can you spot the error? + +
+ Reveal + The button is read once, before the trigger is created. + The trigger always checks the same stored value, so it either runs forever or never runs at all. + Read the button inside the trigger's lambda instead: + + ```java #triggerCapturedValueFix + + ``` + +
+ +### Exercise 3 + +{/* rli:ignore */} + +```java +class Intake implements Mechanism { + private final ExampleMotor motor = new ExampleMotor(); + + Command runAtThrottle(double throttle) { + return Command.noRequirements(coroutine -> { + while (false) { // Compile-time error: Unreachable statement + motor.setThrottle(throttle); + coroutine.yield(); + } + }) + .named("Intake"); + } +} +``` + +Can you fix the compile-time error and the logic error? + +
+ Reveal + The goal is for the commands to set the motor's throttle forever, so + `while (true)` should be used instead of `while (false)`. A + `while (false)` statement will never run! + + Furthermore, the `intake()` Command needs to require the `Intake` mechanism. + So, `run(coroutine -> {})` should be used in place of `Command.noRequirements(coroutine -> {})`. + + ```java #intakeClass + + ``` + +
+ +### Exercise 4 + +You've fixed the intake (reveal the answer to exercise 3 to see the full class), +and have the following code to run it while the A button is held: + +```java #intakeWhileTrue + +``` + +You hold down the A button, and the intake starts spinning; but releasing it doesn't +stop the intake. +Can you spot the error? + +
+ Hint + Setting the Intake's default command might help you. But why? +
+
+ Reveal + When you call `motor.setThrottle(double)`, the motor will start spinning forever, + even if you aren't calling `setThrottle` anymore. Even though releasing the A button + will cancel the `runAtThrottle` command, it won't stop the motor. + + To fix this, we set a default command. The default command for a mechanism will always + run when no other commands are requiring that mechanism. In this case, "no other + command to the intake" = "we want to stop the intake", so the default command is + setting the throttle to 0. + ```java #intakeClassWithDefaultCmd + + ``` + +
diff --git a/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx b/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx index 7ceb62d0..18b35719 100644 --- a/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx +++ b/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx @@ -19,11 +19,13 @@ By the end of Stage 1B, you'll rewrite your Stage 1A kitbot code into a command- This stage will cover the following topics: -- [The Concepts of Command-Based Programming](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-overview/) -- [The Body of a Command](https://frcsoftware.org/learning-course/stage1/stage1b/the-command-body/) -- [Commands & Mechanisms, Part 1](https://frcsoftware.org/learning-course/stage1/stage1b/commands-and-mechanisms/) -- [Triggers and Scheduling](https://frcsoftware.org/learning-course/stage1/stage1b/triggers/) -- [Commands and Mechanisms, Part 2](https://frcsoftware.org/learning-course/stage1/stage1b/commands-and-mechanisms-pt2/) -- [Exercise: Kitbot Rewrite, Pt 1](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-kitbot/) -- [Suppliers in Command-Based](https://frcsoftware.org/learning-course/stage1/stage1b/suppliers-in-command-based/) -- [Exercise: Kitbot Rewrite, Pt 2](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-kitbot-pt2/) +- [The Concepts of Command-Based Programming](/learning-course/stage1/stage1b/command-based-overview/) +- [The Body of a Command](/learning-course/stage1/stage1b/the-command-body/) +- [Commands & Mechanisms, Part 1](/learning-course/stage1/stage1b/commands-and-mechanisms/) +- [Triggers and Scheduling](/learning-course/stage1/stage1b/triggers/) +- [Commands and Mechanisms, Part 2](/learning-course/stage1/stage1b/commands-and-mechanisms-pt2/) +- [Bonus: Spot the Error](/learning-course/stage1/stage1b/spot-the-error/) +- [Exercise: Kitbot Rewrite, Pt 1](/learning-course/stage1/stage1b/command-based-kitbot/) +- [Suppliers in Command-Based](/learning-course/stage1/stage1b/suppliers-in-command-based/) +- [Exercise: Kitbot Rewrite, Pt 2](/learning-course/stage1/stage1b/command-based-kitbot-pt2/) +- [Bonus: Spot the Error, Pt 2](/learning-course/stage1/stage1b/spot-the-error-pt2/)