-
Notifications
You must be signed in to change notification settings - Fork 0
Guide to Java Level 4
High school FRC students transitioning from FTC, or students with Java basics who are new to FRC. This guide bridges what you already know into FRC's Command-Based framework.
Prerequisites: Java basics (Level 1) and ideally FTC experience (Level 2/2.5)
Time to Complete: 1-2 weeks before build season
┌─────────────────────────────────────────────────────────────┐
│ LEVEL 1: Java Foundations │
│ LEVEL 2: Java for FTC (if applicable) │
│ LEVEL 2.5: Advanced FTC Patterns (if applicable) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LEVEL 3: Java for FRC │
│ (You are here) │
│ Transition from FTC → FRC Command-Based │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LEVEL 4: Advanced FRC │
│ Commands Deep Dive • Triggers • State Machines │
└─────────────────────────────────────────────────────────────┘
In FTC, you wrote code like this:
// FTC: You check everything in a loop
while (opModeIsActive()) {
if (gamepad1.a) {
intake.setPower(1.0);
} else {
intake.setPower(0);
}
}In FRC Command-Based, you declare relationships once:
// FRC: Declare it once, framework handles the checking
operator.a().whileTrue(IntakeCommands.run(intake));That's it. No loop. The framework checks the button and runs the command for you.
This feels weird at first, but it's powerful:
- No more giant if/else chains
- No more forgetting to stop a motor
- Commands automatically handle cleanup
- Easy to compose complex behaviors
| FTC Concept | FRC Equivalent | Notes |
|---|---|---|
LinearOpMode |
Robot.java + RobotContainer.java
|
Split into framework and your code |
opModeIsActive() loop |
Command Scheduler | Framework runs the loop for you |
hardwareMap.get() |
Constructor injection | Motors created in subsystem constructors |
gamepad1.a |
controller.a() |
Returns a Trigger, not a boolean |
motor.setPower() |
Same, but inside subsystems | Hardware stays private to subsystem |
Subsystem class with update()
|
SubsystemBase with periodic()
|
Very similar pattern! |
| State machine enum | Same pattern works! | Commands can also manage state |
sleep() in auto |
Commands.waitSeconds() |
Non-blocking, composable |
| Time-based auto | Path following (Choreo) | Much more precise |
TeamCode/
└── org/firstinspires/ftc/teamcode/
├── TeleOp/
├── Autonomous/
└── Subsystems/
src/main/java/frc/robot/
├── Robot.java ← Framework entry point (don't touch much)
├── RobotContainer.java ← YOUR MAIN FILE: create subsystems, bind buttons
├── Constants.java ← All configuration values
├── subsystems/ ← One file per mechanism
│ ├── Intake.java
│ ├── Shooter.java
│ └── Drivetrain.java
└── commands/ ← Command factory classes
├── IntakeCommands.java
└── DriveCommands.java
RobotContainer.java — This is like your FTC OpMode, but split differently:
- Creates all subsystems (like your FTC
initsection) - Binds buttons to commands (like your FTC
loop, but declarative) - Sets up autonomous routines
Subsystem files — Very similar to your FTC subsystem classes:
- Own their hardware (motors, sensors)
- Provide methods for what they can do
- Have a
periodic()method (like yourupdate())
Constants.java — All your magic numbers in one place:
- Motor IDs
- Speeds and limits
- PID values
If you wrote good FTC subsystems, FRC subsystems will feel familiar:
public class Intake {
private DcMotor motor;
public Intake(HardwareMap hardwareMap) {
motor = hardwareMap.get(DcMotor.class, "intake");
}
public void run() {
motor.setPower(1.0);
}
public void stop() {
motor.setPower(0);
}
public void update() {
// Called every loop
}
}public class Intake extends SubsystemBase {
private final TalonFX motor;
public Intake() {
motor = new TalonFX(Constants.IntakeConstants.MOTOR_ID);
}
public void run() {
motor.set(1.0);
}
public void stop() {
motor.set(0);
}
@Override
public void periodic() {
// Called every loop — just like update()!
}
}Key differences:
-
extends SubsystemBase— Integrates with Command Scheduler - No
HardwareMap— Motor IDs come from Constants -
periodic()instead ofupdate()— Called automatically by framework -
TalonFXinstead ofDcMotor— Different hardware library (CTRE Phoenix)
In FTC, your OpMode directly controlled subsystems:
// FTC: Direct control in loop
if (gamepad1.a) {
intake.run();
} else {
intake.stop();
}In FRC, Commands are the actions:
// FRC: Command handles the action
Command runIntake = Commands.startEnd(
() -> intake.run(), // What to do when starting
() -> intake.stop(), // What to do when ending
intake // Which subsystem this uses
);Then you bind the command to a button:
// In RobotContainer
operator.a().whileTrue(runIntake);-
Automatic cleanup —
stop()is called when button released - No conflicts — Only one command can use a subsystem at a time
- Composable — Chain commands together easily
- Reusable — Same command for teleop and auto
Instead of creating commands inline, group them in factory classes:
public class IntakeCommands {
private IntakeCommands() {} // Prevent instantiation
public static Command run(Intake intake) {
return Commands.startEnd(
() -> intake.run(),
() -> intake.stop(),
intake
);
}
public static Command reverse(Intake intake) {
return Commands.startEnd(
() -> intake.reverse(),
() -> intake.stop(),
intake
);
}
}Usage:
operator.rightTrigger().whileTrue(IntakeCommands.run(intake));
operator.leftTrigger().whileTrue(IntakeCommands.reverse(intake));This is where everything comes together:
public class RobotContainer {
// ===== SUBSYSTEMS (create one of each) =====
private final Drivetrain drivetrain = new Drivetrain();
private final Intake intake = new Intake();
private final Shooter shooter = new Shooter();
// ===== CONTROLLERS =====
private final CommandXboxController driver = new CommandXboxController(0);
private final CommandXboxController operator = new CommandXboxController(1);
// ===== CONSTRUCTOR =====
public RobotContainer() {
configureBindings();
configureDefaultCommands();
}
private void configureBindings() {
// ----- DRIVER CONTROLS -----
// Left Stick: Drive
// Right Stick: Rotate
// Left Bumper: Slow mode
driver.leftBumper().whileTrue(
DriveCommands.slowMode(drivetrain, driver)
);
// ----- OPERATOR CONTROLS -----
// Right Trigger: Intake
// Left Trigger: Reverse intake
// A: Shoot
operator.rightTrigger().whileTrue(IntakeCommands.run(intake));
operator.leftTrigger().whileTrue(IntakeCommands.reverse(intake));
operator.a().onTrue(ShooterCommands.shoot(shooter, intake));
}
private void configureDefaultCommands() {
// Drivetrain always runs teleop drive unless something else needs it
drivetrain.setDefaultCommand(
DriveCommands.teleopDrive(drivetrain, driver)
);
}
public Command getAutonomousCommand() {
return AutoCommands.simpleAuto(drivetrain, intake, shooter);
}
}| FTC Location | FRC Location |
|---|---|
Hardware init in runOpMode()
|
Subsystem constructors |
Button checks in while loop |
configureBindings() method |
Default behavior in else
|
setDefaultCommand() |
| Auto selection | getAutonomousCommand() |
In FTC, buttons are booleans:
if (gamepad1.a) { } // true or falseIn FRC, buttons are Triggers that fire commands:
controller.a() // Returns a Trigger object
.onTrue(command) // Run once when pressed
.whileTrue(command) // Run while held
.onFalse(command) // Run once when released| Method | When Command Runs |
|---|---|
.onTrue(cmd) |
Once, when button first pressed |
.whileTrue(cmd) |
Continuously while held, stops when released |
.onFalse(cmd) |
Once, when button released |
.toggleOnTrue(cmd) |
Toggle on/off with each press |
Joysticks and triggers still give analog values:
// In a command or default command
double forward = -driver.getLeftY(); // -1 to 1
double strafe = -driver.getLeftX(); // -1 to 1
double rotate = -driver.getRightX(); // -1 to 1
// Note: Y axis is inverted (forward = negative), so we negate itIn FTC, you might have scattered numbers:
motor.setPower(0.8); // What is 0.8? Why 0.8?In FRC, everything goes in Constants:
// Constants.java
public final class Constants {
public static final class IntakeConstants {
public static final int MOTOR_ID = 1;
public static final double RUN_SPEED = 0.8;
public static final double REVERSE_SPEED = -0.5;
}
public static final class ShooterConstants {
public static final int LEFT_MOTOR_ID = 2;
public static final int RIGHT_MOTOR_ID = 3;
public static final double TARGET_RPM = 4500;
}
}Usage:
import static frc.robot.Constants.IntakeConstants.*;
public class Intake extends SubsystemBase {
private final TalonFX motor = new TalonFX(MOTOR_ID);
public void run() {
motor.set(RUN_SPEED); // Clear what this means!
}
}public class Intake { } // Subsystem: noun
public class IntakeCommands { } // Command factory: noun + Commands
public class RunIntake { } // Command class: verb + noun// Actions (verbs)
public void runIntake() { }
public void stopMotor() { }
// Queries (is/has/get)
public boolean hasPiece() { }
public boolean isAtTarget() { }
public double getSpeed() { }private TalonFX intakeMotor;
private boolean hasPiece;
private double targetSpeed;public static final double MAX_SPEED = 1.0;
public static final int MOTOR_ID = 5;If you learned state machines in Level 2.5, great news — they work the same way in FRC:
public class Intake extends SubsystemBase {
public enum IntakeState {
IDLE,
INTAKING,
HOLDING,
FEEDING
}
private IntakeState currentState = IntakeState.IDLE;
@Override
public void periodic() {
switch (currentState) {
case IDLE:
motor.set(0);
break;
case INTAKING:
motor.set(INTAKE_SPEED);
if (hasPiece()) {
currentState = IntakeState.HOLDING;
}
break;
// ... etc
}
}
public void requestIntake() {
if (currentState == IntakeState.IDLE) {
currentState = IntakeState.INTAKING;
}
}
}The pattern is identical — periodic() instead of update(), but same idea.
// BAD — Don't do this in FRC!
@Override
public void periodic() {
if (someButtonPressed) { // Where does this come from?
motor.set(1.0);
}
}// GOOD — Use commands and bindings
// In RobotContainer:
button.whileTrue(Commands.run(() -> intake.run(), intake));// BAD — Two Intake objects fighting over one motor!
public class ShooterCommands {
private static Intake intake = new Intake(); // NO!
}// GOOD — One instance in RobotContainer, passed to commands
public class ShooterCommands {
public static Command shoot(Shooter shooter, Intake intake) {
// Use the passed-in intake
}
}// BAD — Scheduler doesn't know this uses intake
Commands.run(() -> intake.run());
// GOOD — Scheduler can prevent conflicts
Commands.run(() -> intake.run(), intake);// BAD — Blocks the entire robot!
public void execute() {
motor.set(1.0);
Thread.sleep(1000); // NO!
motor.set(0);
}// GOOD — Use command composition
Commands.sequence(
Commands.runOnce(() -> motor.set(1.0)),
Commands.waitSeconds(1.0),
Commands.runOnce(() -> motor.set(0))
);@TeleOp(name = "Main TeleOp")
public class MainTeleOp extends LinearOpMode {
private Intake intake;
private Arm arm;
@Override
public void runOpMode() {
intake = new Intake(hardwareMap);
arm = new Arm(hardwareMap);
waitForStart();
while (opModeIsActive()) {
// Intake control
if (gamepad1.a) {
intake.run();
} else if (gamepad1.b) {
intake.reverse();
} else {
intake.stop();
}
// Arm control
if (gamepad1.dpad_up) {
arm.goToPosition(Arm.Position.HIGH);
}
if (gamepad1.dpad_down) {
arm.goToPosition(Arm.Position.LOW);
}
intake.update();
arm.update();
telemetry.addData("Intake", intake.getState());
telemetry.update();
}
}
}// RobotContainer.java
public class RobotContainer {
private final Intake intake = new Intake();
private final Arm arm = new Arm();
private final CommandXboxController operator = new CommandXboxController(0);
public RobotContainer() {
configureBindings();
}
private void configureBindings() {
// Intake control
operator.a().whileTrue(IntakeCommands.run(intake));
operator.b().whileTrue(IntakeCommands.reverse(intake));
// No "else" needed — commands handle cleanup!
// Arm control
operator.povUp().onTrue(ArmCommands.goToPosition(arm, Arm.Position.HIGH));
operator.povDown().onTrue(ArmCommands.goToPosition(arm, Arm.Position.LOW));
}
}
// IntakeCommands.java
public class IntakeCommands {
public static Command run(Intake intake) {
return Commands.startEnd(
() -> intake.run(),
() -> intake.stop(),
intake
);
}
public static Command reverse(Intake intake) {
return Commands.startEnd(
() -> intake.reverse(),
() -> intake.stop(),
intake
);
}
}Notice how the FRC version:
- No
whileloop — framework handles it - No
elsefor stopping —startEndhandles cleanup - No manual
update()calls —periodic()runs automatically - Declarations are separate from logic
Once you're comfortable with:
- Subsystems extending
SubsystemBase - Basic command factories
- Button bindings in RobotContainer
- Constants organization
Continue to Level 4: Advanced FRC Patterns:
- Command compositions (sequence, parallel, race)
- Complex triggers and bindings
- State machines in Command-Based
- Autonomous routines
public class MySubsystem extends SubsystemBase {
private final TalonFX motor;
public MySubsystem() {
motor = new TalonFX(Constants.MyConstants.MOTOR_ID);
}
public void doSomething() {
motor.set(Constants.MyConstants.SPEED);
}
public void stop() {
motor.set(0);
}
@Override
public void periodic() {
// Logging, state updates
}
}public class MyCommands {
private MyCommands() {}
public static Command doSomething(MySubsystem subsystem) {
return Commands.startEnd(
() -> subsystem.doSomething(),
() -> subsystem.stop(),
subsystem
);
}
}public class RobotContainer {
private final MySubsystem subsystem = new MySubsystem();
private final CommandXboxController controller = new CommandXboxController(0);
public RobotContainer() {
configureBindings();
}
private void configureBindings() {
controller.a().whileTrue(MyCommands.doSomething(subsystem));
}
}controller.a().onTrue(cmd); // Once on press
controller.a().whileTrue(cmd); // While held
controller.a().onFalse(cmd); // Once on release
controller.a().toggleOnTrue(cmd); // Toggle