-
Notifications
You must be signed in to change notification settings - Fork 1
RLD 6: State Management
So far, we've explored the basics of writing TimedRobot code, interacting with robot hardware using vendor libraries, and retrieving user input data from controllers.
With these tools we can already control a plethora of robot systems.
#include <frc/PS4Controller.h>
#include <ctre/phoenix6/TalonFX.hpp>
frc::PS4Controller ps4{0};
ctre::phoenix6::hardware::TalonFX arm_motor{3, "rio"};
void Robot::TeleopPeriodic()
{
using namespace ctre::phoenix6;
if(ps4.GetTriangleButton())
{
controls::PositionDutyCycle request{30_deg};
arm.SetControl(request);
}
else
{
controls::PositionDutyCycle request{0_deg};
arm.SetControl(request);
}
}
Assuming the arm's motor controller is properly configured and tuned, the following will raise the arm to 30 degrees when pressing the triangle button, and lower it back down when releasing.
TimedRobot excels at providing a no-frills, simple way to directly control systems. However, you may start to notice limitations with something called State Management.
Let's go back to that arm example from earlier. Our 2023 robot, Blackjack, actually used a double-jointed arm to score game pieces on poles, but it required a lot more logic to control.
For example, a number of conditions needed to be met before the arm was allowed to extend.
- The arm should be holding a game piece
- The robot should be in position (or nearing position) to score
- The operator should be holding a trigger, indicating it is safe for the arm to extend
With your knowledge of logic statements, you probably recognize the opportunity to apply if
statements, something like so
if (gamePieceHeld() && robotInPosition() && operator.GetR2Button())
{
arm.extend();
}
else
{
arm.retract();
}
But now let's imagine, what happens if the robot gets pushed slightly out of position while the arm is extended. Should the arm immediately retract? No, because depending on the situation, that could actually damage the arm.
So, we want a way of checking if the robot has previously "met tolerance" for being in position to score, regardless of whether the robot is still in that position. Enter, a state variable.
static bool robot_previously_in_position = false;
if (robotInPosition())
{
robot_previously_in_position = true;
}
if (gamePieceHeld() && robot_previously_in_position && operator.GetR2Button())
{
arm.extend();
}
else
{
arm.retract();
}
A state variable is simply a way of recording the current or past state of the robot or its subsystems.
For example, a state variable might indicate whether the robot has completed an action, which can be used to indicate that the next action should be run.
static bool arm_not_yet_extended = false;
if (game_piece_held && robot_previously_in_position && operator.GetR2Button())
{
if (arm_not_yet_extended)
{
arm.extend();
arm_not_yet_extended = arm.inTolerance(); // Returns true when arm has reached "extended" position
}
else
{
scoreGamePiece();
game_piece_held = false;
}
}
else ...
However, state variables can quickly become a nightmare when trying to represent the complex states of a robot.
Our 2023 code was a mess, precisely because there were so many checks and states to keep track of.
Some of you might recognize the need for a more verbose variable type. Enums can be a perfect way to represent the multiple states of a robot or a subsystem!
Let's pull out a new example from our 2024 robot, Toothless. When shooting a note into the speaker, the bot must wait for a number of "checks" to be passed, including that
- the robot is holding a note
- the robot should be facing towards the speaker (+- 3 degrees)
- the pivot should be aiming at the proper point (+- 0.5 degrees)
- the shooter system should be spun up to the proper speed (+- 50 rpm)
For the sake of example, let's say all of these events can only happen once the previous has finished (in reality these would happen simultaneously).
Let's take a look at a way of managing all these states with an enum
enum class STATE
{
WAITING_FOR_NOTE,
NOTE_HELD,
FACING_SPEAKER,
PIVOT_AIMED,
SHOOTER_SPUNUP;
}
STATE current_state{}; // This variable will be updated as the robot progresses through its states.
switch (current_state)
{
case (STATE::WAITING_FOR_NOTE):
intake.run();
if (intake.noteDetected())
current_state = STATE::NOTE_HELD;
break;
case (STATE::NOTE_HELD):
drivetrain.faceSpeaker();
if (drivetrain.errorToSpeaker() < 3_deg))
current_state = STATE::FACING_SPEAKER;
break;
case (STATE::FACING_SPEAKER):
pivot.aim();
if (pivot.aimingError() < 0.5_deg))
current_state = STATE::PIVOT_AIMED;
break;
case (STATE::PIVOT_AIMED):
speaker.spinup();
if (speaker.spunUp())
current_state = STATE::SHOOTER_SPUNUP;
break;
case (STATE::SHOOTER_SPUNUP):
intake.shoot();
if (intake.shotTimerElapsed())
current_state = STATE::WAITING_FOR_NOTE;
break;
}
The "best practice" now used by the majority of top teams is tiered state management. This is an example of very commonly mentioned programming concepts called encapsulation and abstraction, which basically seeks to minimize the complexity of code by ensuring each system manages all that it can by itself. In our case, we do this by having each system manage its own state, and then having an overarching "robot state" that informs each system on what to do.
This is best seen with more examples, so here's a few subsystems on our 2024 robot and what states they would have
Intake
- OFF
- INTAKING
- INDEXING
- HOLDING NOTE
- EJECTING NOTE
- SHOOTING NOTE
Shooter
- OFF
- IDLE
- SPUNUP
Pivot
- INTAKING
- AIM_SPEAKER
- AMP
- TRAP
Climbers
- ZEROING
- IDLE_POS
- UP_POS
Each subsystem would manage its own behavior, based on its state
Then the overall robot states might look like
- AWAITING_NOTE
- NOTE_HELD
- SPEAKER_SHOOTING
- AMPING
- TRAPPING
And then a class, typically called a State Machine, would handle the transitions between these states and based on feedback from each system
There are many ways to actually going about implementing state management, but we'll cover the team's preferred practice in the next section.