Our team uses Java to program our robots. Many of the coding skills can be learned from our AP Computer Science course.
Conceptual overview of the "what" and "why" of Command pattern
The Command pattern is a fundamental design principle FTCLib uses, and understanding it will significantly enhance your coding experience. This page delves into the "what" and "why" behind this pattern, equipping you to write efficient and maintainable robot code.
https://docs.ftclib.org/ftclib/
The Command pattern boasts a rich history dating back to the late 1970s and early 1980s. The concept emerged within the Smalltalk and Xerox PARC communities, documented in publications like Anneliese Schürr's 1992 paper "Separation of Concerns and the Command Pattern." Initially focused on graphical user interfaces (GUIs), the pattern's flexibility was adopted in various software development domains.
Its popularity soared with the rise of design patterns in the 1990s, solidified by Erich Gamma et al.'s influential book "Design Patterns: Elements of Reusable Object-Oriented Software." This widespread recognition cemented the Command pattern as a valuable tool for promoting loose coupling, reusability, and maintainability in object-oriented programming.
The Command pattern originates from software design principles and offers a structured way to manage robot actions. Instead of writing long, blocking code sections, you create independent classes called Commands. Each Command encapsulates your robot's specific task, like driving forward, turning, or activating a mechanism.
Traditional robot programming often involves lengthy code blocks that execute sequentially, potentially hindering responsiveness and multitasking. If your robot is stuck inside of a loop that gradually moves a motor, it's not doing other things. It's blocked until you move on.
Non-blocking commands eliminate the need to do one thing at a time by having repeatedly called execute
functions that run until their isFinished
condition is reached. This allows the robot to perform multiple actions simultaneously. This means smoother operation and the ability to react to real-time events effectively.
Commands are designed for reusability. You can create a generic RotateCommand
and adapt it for different distances or speeds across various parts of your code. This reduces code duplication and simplifies maintenance.
FTCLib encourages modularity with separate Command files. This translates to clear ownership and promotes a collaborative development environment. Team members can work on their specific Commands without worrying about conflicts in common code sections. Version control systems like Git become even more valuable as team members push their Command commits independently.
FTCLib does much more than implement the Command pattern. We've got ourselves an extensive toolbox to elevate our robot programming:
Finer Motor Control:
Motor Wrappers: Simplify motor control with pre-built classes handling direction, speed, and encoder feedback.
PID Control Helpers: Fine-tune motor movements with built-in PID controllers for precise control.
Navigation Expertise:
Odometry Support: Implement robust navigation using odometry wheels. FTCLib calculates robot pose from encoder readings, simplifying movement and autonomous routines.
Path Following: Define and execute pre-defined paths for complex maneuvers and automated navigation.
Visionary Solutions:
EasyOpenCV Integration: Seamlessly incorporate computer vision tasks. Detect objects, track targets, and analyze the environment using EasyOpenCV's intuitive tools.
Camera Support: Access and manage multiple cameras on your robot, expanding your visual perception capabilities.
Sensor Fusion Power:
Sensor Wrappers: Interact easily with various sensors like gyros, accelerometers, and ultrasonic sensors. Access their data effortlessly for informed decision-making.
Data Fusion: Combine data from multiple sensors to better understand your robot's environment, enabling intelligent responses.
Additional Gems:
Telemetry: Log and display robot data on the Driver Station for debugging, monitoring, and optimization.
Logging: Record robot behavior for analysis and performance improvements.
Scheduling: Manage task execution precisely with the built-in scheduler, ensuring coordinated robot actions.
The composition of hardware components and their basic helper functions
Subsystems encapsulate hardware components like motors, servos, and sensors and their basic power on/off functionality.
Hardware and Setup:
The Arm
subsystem in our example demonstrates essential aspects of subsystem creation:
Hardware References: Declares member variables like motor
, wristServo
, etc., linking them to hardware names from a centralized place for these "magic strings," such as HardwareNames
.
Motor Configuration: Sets up the motor
for precise position control using MotorEx
, defining parameters like PID coefficients and tolerances.
Servo Initialization: Retrieves servo objects from the hardwareMap
and sets their initial positions.
Helpful Methods:
Subsystems often include convenience methods to control hardware directly. The Arm
class provides examples:
travelMode()
: Sets the arm for driving by closing the claw, adjusting the wrist, and storing the new wrist position.
wristUp()
/wristDown()
: Increment/decrement the wrist servo position within set limits.
openClaw()
/closeClaw()
: Manipulate the claw servo based on an isOpen
boolean flag.
toggleOpen()
/toggleRoll()
: Implement button-pressable actions using conditional logic.
State Management:
Subsystems can maintain internal state using variables like isOpen
for the claw. The toString()
method provides a human-readable snapshot of the subsystem's current state (servo positions in this case).
Where do state variables go?
There are state variables like wristAng
and isOpen
in this subsystem. Does every subsystem get unique state variables? Should we consolidate them into the MyRobot
file? These are organizational guidelines that are flexible and will need to be discussed so all programmers know how things ought to be organized.
Beyond Commands:
Remember, not every action needs a dedicated Command. Servos like the wrist can be adjusted directly using methods like wristUp()
. They're instantaneous and aren't a threat to block off other commands. So here's how we can bind these to a button (more details in Running Your Code).
Next Steps:
While this explanation covers the basics of subsystems, exploring the provided Robot
OpMode will reveal how these subsystems are integrated and controlled within your robot's overall behavior. Remember, effectively utilizing subsystems is vital in writing well-structured and maintainable robot code in FTCLib.
A function-by-function discussion of a Command's essential components
Commands are the heart of FTCLib, promoting clean and organized robot code. Before we get too technical, let's dissect a basic Command example, exploring its critical sections.
When crafting a Command, the constructor serves as its architect, defining essential elements before the Command's execution:
1. Establishing Requirements:
Timeout: Specifies the maximum duration for the Command's execution, ensuring it doesn't run indefinitely. This is a default feature but can be eliminated for commands that run by default (like the DriveCommand
)
Subsystem Dependencies: Identifies which robot subsystems (e.g., drive, arm, elevator) are crucial for the Command's operation. This declaration is accomplished through the addRequirements(subsystem)
call. That subsystem will cancel any other commands when this one is run.
Handy References: Creates shortcuts to frequently used robot components, streamlining code and enhancing readability. Connection to our robot object allows us to check on its state variables.
2. Pre-Execution Setup:
The constructor executes during robot setup, preparing the Command before its actual launch. This separation from the initialize()
method, which runs immediately before execution, allows for careful configuration and resource management.
The initialize()
method acts as the Command's launchpad, performing critical setup tasks right before execution:
1. Setting the Stage:
Timer Activation: Initializes the timeout timer using timer.start()
to enforce the Command's duration.
Target Calculation:
FTCLib: Calculates the encoder target position for precise motor control. See examples here: https://docs.ftclib.org/ftclib/features/hardware/motors
RoadRunner: Constructs an Action object using actionBuilder
for complex trajectories.
2. Key Considerations:
Once and Done: initialize()
executes only once before the Command's execution loop begins.
Preparation Focus: Its primary responsibility lies in configuring initial values, targets, and action plans, setting the stage for successful execution.
Essential Reminder:
Although initialize()
lays the groundwork, the execute()
method, covered next, drives the Command's actions and interacts with the robot's physical systems. This non-blocking and concurrent nature can be a new concept for high school programmers.
The execute()
method is the Command's heart and soul, translating plans into action.
1. The Execution Loop:
Imagine execute()
as a conductor, continuously calling the shots throughout the Command's lifespan. It doesn't have a set duration; it will keep getting called until its isFinished()
method signals completion. However, it's crucial to avoid blocking calls like while
loops or sleep()
that would halt the entire robot program.
2. Concurrent Commands: Sharing the Stage Gracefully:
Remember our analogy of multiple Commands as athletes? They might perform simultaneously, especially if they don't require the same resources. execute()
needs to consider this possibility, ensuring smooth cooperation and avoiding conflicts.
3. Key Principles:
Non-Blocking: Steer clear of blocking calls that would freeze the robot program. Instead, favor short, focused actions within execute()
and rely on isFinished()
for termination.
Concurrent-Friendly: Design your execute()
logic to function correctly even when other Commands are running concurrently, as long as they don't interfere with the required resources.
4. Understanding the Challenge:
This non-blocking and concurrent nature can be a new concept for high school programmers. Here's an analogy to help:
Imagine juggling. Each Command is a ball you keep in the air. You can't hold onto any ball for too long (blocking call), or you'll drop the others (other Commands needing resources). But by quickly tossing and catching each ball (short actions), you can keep them all going simultaneously (concurrently).
Remember:
execute()
is the action stage, but design it wisely to avoid blocking and ensure smooth cooperation with other concurrent Commands. It's like the insides of the while loops we're avoiding because they're blocking (and the condition of this avoided while loops is the isFinished
)
Embrace non-blocking techniques and keep your execute()
methods focused and efficient for optimal robot performance.
The final act of our Command play unfolds in these two methods:
Think of isFinished()
as the stage manager, waiting for the right moment to signal the end of the Command's performance. Similar to the condition in a while
loop, it continuously evaluates whether the Command has achieved its goal. If isFinished()
returns true
, the curtain falls, and the Command gracefully exits.
Key Considerations:
Exit Criteria: Define clear conditions for completion within isFinished()
. Common criteria include:
Time elapsing (using the timer started in initialize()
)
Sensor readings reaching desired values
External signals (e.g., button press)
Concurrent Awareness: Remember other Commands might be waiting in the wings. Ensure your isFinished()
logic doesn't hold onto resources unnecessarily, preventing other Commands from running.
The end()
method serves as the post-performance cleanup crew, ensuring everything is left tidy after the Command finishes, whether naturally or interrupted by another Command.
Key Considerations:
Power Down: Stop motors, reset servos, and release any acquired resources.
State Management: If using an ENUM to track subsystem state, ensure it reflects the actual final state, especially if interrupted. Update the ENUM variable accordingly in end()
.
Cleanup Thoroughness: Leave the robot and subsystems ready for the next Command without lingering effects.
Where our commands meet our subsystems
From the little Android driver station, here's where we select our autonomous or teleop opModes. Our autonomous entry point can have some options to indicate where the robot is starting on the field and any decisions about scoring that need to be made.
The teleop kick-off is much simpler:
These two opModes go to the same place but get there using different constructors. The robot knows whether to run autonomous or teleop based on which constructor is being called.
This is where Commands get lined up for autonomous mode or bound to controller inputs in teleop. MyRobot
is a soulless filename; typically, we'll rename this file each year after our robot.
One for teleop, requiring only the opMode
object.
Another for autonomous, taking additional parameters so we know our starting position and what script to run (isRed
, isLeft
, goForBoard
)
Once our drive train drive
is registered, we give it a default command, DriveCommand
defaultCommand
is a special command assigned to a subsystem that continuously runs unless interrupted by another higher-priority command.
The DriveCommand
will constantly watch joystick inputs as well as apply dead zones and field-orientated drive
If you press a button that triggers another command for the drive system (e.g., rotating), that command will interrupt the DriveCommand
temporarily until it finishes.
Once the other command finishes, the DriveCommand
automatically resumes as the defaultCommand
, listening to joystick inputs again.
The MyRobot
code demonstrates different ways to connect gamepad buttons with robot actions using FTCLib. Here's a breakdown of the standard methods and how they translate to gameplay scenarios:
1. whenPressed
:
Definition: Executes a command once when the button is initially pressed.
Example: Pressing the A button on Gamepad 1 toggles the driving mode.
FTC Gameplay:
Use this for actions that only need to happen once per button press, like activating a boost ability or triggering a quick arm movement.
2. whenReleased
:
Definition: Executes a command once when the button is released.
Example: Releasing the B button on Gamepad 2 closes the arm claw.
FTC Gameplay:
This is useful for actions that occur when you stop pressing the button, like stopping arm movement or ending a special ability.
3. whileHeld
:
Definition: Continuously executes a command while the button is held down.
Example: Holding the d-pad on Gamepad 2 raises or lowers the arm's wrist.
4. toggleWhenActive
:
Definition: Starts/stops a command each time the button is pressed, regardless of previous state.
Example: Not used in the provided code, but it could be used for toggling a light or other on/off functionality.
FTC Gameplay:
This is helpful for quickly activating and deactivating abilities without keeping the button held down.
Here's an example of how to link a button with a command using whileHeld
:
This code creates a button listener for the up d-pad on Gamepad 2. When held down, it continuously executes a command that raises the arm's wrist.
initAuto
In MyRobot
's initAuto
function, the autonomous script takes shape. But how do you structure your commands? Let's delve into the options:
1. Sequential Commands:
Commands execute one after another, completing before the next starts.
Think of it as a step-by-step recipe: scan for the prop, turn towards it, open the claw, move closer, etc.
Benefits:
Easy to understand and troubleshoot.
Ensures each action finishes cleanly before proceeding.
Drawbacks:
Might be slower if some actions could happen simultaneously.
Less flexibility for complex maneuvers involving interactions or timing dependencies.
2. Concurrent Commands:
Multiple commands run simultaneously, sharing resources and potentially interacting dynamically.
Imagine multitasking: turning while moving closer or raising the wrist while opening the claw.
Benefits:
Can be more efficient if actions don't rely on each other's completion.
Offers greater flexibility for intricate robot behaviors.
Drawbacks:
Requires careful planning to avoid conflicts and ensure smooth coordination.
Debugging can be more challenging.
Writing the Script:
initAuto
provides flexibility:
Within initAuto
: You can build your entire autonomous sequence directly in our robot file using a new SequentialCommandGroup()
with nested commands. This approach keeps everything centralized and concise.
Separate File: Files are kept shorter if we keep the autonomous procedure in its own SequentialCommandGroup file. This means a programmer tasked with this job could push changes to our GitHub repo with less concern for conflicts. 🤝
The files that drive FTCLib
Before looking under the hood of our code, programmers should study the Command pattern's goals of organized, non-blocking code. We've done that ✅. You don't need to go past this point. We hardly ever need to touch these files. Programmers can contribute to a team by knowing how Command
and Subsystem
work together and how to run their code. Proceeding on means you want the next level. You're comfortable and open-minded about how this all works.
The secret to understanding how we turned FTC's code into a Command pattern is how the following two files connect. They sit in our utils folder, working their magic--we may never need to touch them. Let's start with Robot.java
.
This file acts as an invisible layer, providing essential functionalities:
Shared State: The isDisabled
static variable is a central flag accessible throughout your code, indicating robot state (enabled/disabled). The disable()
and enable()
methods allow external control over the robot's state during transitions between autonomous and teleop periods.
Command Control Center: Methods like reset()
, run()
, schedule()
, and register()
grant access to the CommandScheduler
, the mastermind behind command execution and resource management. Think of it as the conductor of your robot's actions.
isDisabled
: Expanding State VariablesRemember, the way the isDisabled
variable controls state is not how we need to track our robot's current status. It doesn't need a static variable; we'll have easy access to the instance of our robot. We're not restricted to booleans. We'll use custom state variables of all sorts--here's an example that uses enums to represent various robot conditions:
Here's the most straightforward connection point between Command and FTC. This file is our translator.
CommandOpMode
runs LinearOpMode
's critical method runOpMode()
method. That's it 🤯. Sure, we can also post telemetry
updates, but the real essential bit is just that stupid run()
call. That keeps our CommandScheduler working while satisfying the requirements for FTC gameplay.
The reset()
method within both Robot
and CommandOpMode
plays a crucial role in managing Commands during various FTC gameplay scenarios. Here are some practical examples of when you might utilize it:
1. Autonomous Period Transitions:
Switching Strategies: During autonomous, imagine your robot first performs a pre-determined path using a pre-scheduled set of Commands. If external sensors detect an unexpected obstacle, you can call reset()
in Robot
or CommandOpMode
to clear all running Commands and initiate a new set of Commands for obstacle avoidance or adaptation.
Recovering from Errors: If a sensor reading is deemed unreliable or a Command encounters an error, resetting can clear the current action and allow you to initiate a new Command for recovery or safe shutdown.
2. Teleop Period:
Button-Triggered Resets: Consider a button press on your gamepad that triggers reset()
in CommandOpMode
. This could be used to:
Cancel an ongoing movement Command if the driver wants to stop abruptly.
Reset arm or claw positions to known starting points for precise manipulation.
Restart a specific Command sequence for a repeated action.
3. Testing and Debugging:
Isolating Command Behavior: During testing, you might use reset()
after each test run to isolate specific Commands and verify their functionality without interference from previous actions.
Reproducing Issues: If your robot exhibits unexpected behavior, strategically placing reset()
calls can help reproduce the issue for easier debugging and troubleshooting.