Unit 7 · Lesson 8

Robot-Oriented Controls

This is the moment the swerve drive moves. Six lessons of hardware configuration, kinematics, odometry, and subsystem architecture all converge in one DriveCommand that reads two joystick axes and a rotation input, builds a ChassisSpeeds, and calls drive(). We start robot-oriented — no gyro, no field transforms — so you can verify every prior layer is working before adding the complexity of field-orientation in Lesson 9.

By the end of this lesson, you will:

  • Write a complete DriveCommand that drives robot-oriented using a CommandXboxController
  • Apply MathUtil.applyDeadband() correctly to both translation axes and the rotation axis
  • Scale joystick input by max speed constants to produce meters/second and radians/second
  • Set DriveCommand as the drivetrain's default command in RobotContainer
  • Use the robot-oriented drive as a systematic verification layer before adding field-oriented control
  • Explain why robot-oriented control is the essential first test after building the swerve stack

Why Start Robot-Oriented?

Field-oriented control (Lesson 9) adds a gyro-based rotation transform on top of robot-oriented control. If field-oriented is wrong, it could be the gyro, it could be the sign convention, it could be the rotation math — or it could be something in the underlying drive pipeline. By verifying robot-oriented first, you establish that kinematics, module hardware, and unit conversions are all correct before adding the gyro layer.

The test sequence is deliberate:

  1. Robot-oriented drive (this lesson): Push forward → robot moves forward. Push right → robot strafes right. Twist → robot spins. If any of these is wrong, it's a hardware or configuration issue.
  2. Field-oriented drive (Lesson 9): Push forward → robot moves toward the far wall regardless of heading. If robot-oriented works but field-oriented doesn't, it's the gyro or the rotation transform.
🔍 LRI Perspective: "I watch them drive on blocks before I watch them drive on carpet"

On every swerve robot's first enable, I stand close and watch the module reactions to joystick input before I let the robot drive on the floor. If all four wheels point the correct direction and spin the correct speed when I push the stick forward — the system is working. If one module points backward, it's a drive inversion. If one module doesn't move at all, it's a CAN fault. Robot-oriented drive on blocks is the safest, fastest system-level verification available. Don't skip it to rush to field-oriented.

The Joystick → ChassisSpeeds Pipeline

The full translation from raw joystick values to hardware motor commands. Press the direction buttons below to see how each input propagates.

Joystick → ChassisSpeeds → drive() — press directions to simulate
Left Stick (translation)
Arrow keys — simulate input
Raw left Y:0.00
Raw left X:0.00
After deadband:0.00 / 0.00
vx (m/s):0.00
vy (m/s):0.00
ω (rad/s):0.00
← press direction buttons to see how joystick values map to ChassisSpeeds

The DriveCommand Code

The deadband and the sign conventions below are the two places most teams make first-time mistakes. Click each token.

❌ Common mistake: no deadband
// Drifts when joystick is released
double vx = -m_controller.getLeftY()
  * Constants.Swerve.kMaxSpeed;
✅ With deadband applied
// Stops cleanly when released
double vx = -MathUtil.applyDeadband(
  m_controller.getLeftY(), 0.05)
  * Constants.Swerve.kMaxSpeed;
DriveCommand.java — robot-oriented swerve drive command
public class DriveCommand extends CommandBase {

  private final SwerveDrivetrain m_drive;
  private final CommandXboxController m_controller;

  public DriveCommand(SwerveDrivetrain drive,
                     CommandXboxController controller) {
    m_drive = drive;
    m_controller = controller;
    addRequirements(drive);
  }

  @Override
  public void execute() {

    // Read and deadband joystick axes
    double rawVx = -m_controller.getLeftY(); // negated: push fwd = positive
    double rawVy = -m_controller.getLeftX(); // negated: push left = positive
    double rawOmega = -m_controller.getRightX();// negated: push left = CCW +

    double vx = MathUtil.applyDeadband(rawVx, 0.05)
      * Constants.Swerve.kMaxSpeedMetersPerSecond;
    double vy = MathUtil.applyDeadband(rawVy, 0.05)
      * Constants.Swerve.kMaxSpeedMetersPerSecond;
    double omega = MathUtil.applyDeadband(rawOmega, 0.05)
      * Constants.Swerve.kMaxAngularRadPerSecond;

    // Build ChassisSpeeds — robot-oriented (no gyro)
    m_drive.drive(new ChassisSpeeds(vx, vy, omega));
  }

  @Override
  public void end(boolean interrupted) {
    m_drive.stop();
  }

  @Override
  public boolean isFinished() { return false; }
}
← click a highlighted token

Wiring in RobotContainer

RobotContainer.java — wiring DriveCommand as default
public class RobotContainer {

  private final SwerveDrivetrain m_drive = new SwerveDrivetrain();
  private final CommandXboxController m_driver = new CommandXboxController(0);

  public RobotContainer() {
    configureDefaultCommands();
    configureButtonBindings();
  }

  private void configureDefaultCommands() {
    m_drive.setDefaultCommand(
      new DriveCommand(m_drive, m_driver));
  }

  private void configureButtonBindings() {
    // Zero gyro heading on Start
    m_driver.start().onTrue(
      Commands.runOnce(m_drive::zeroHeading, m_drive)
      .withName("ZeroHeading"));

    // Lock wheels on X button — hold for defense
    m_driver.x().whileTrue(
      Commands.run(m_drive::lockWheels, m_drive)
      .withName("LockWheels"));
  }
}
← click a highlighted token
💡 Deadband: 0.05 is the starting point, not the final answer

A deadband of 0.05 means any joystick axis value between −0.05 and +0.05 is treated as zero. This is the typical drift threshold for a well-maintained Xbox controller. Brand new controllers drift less; worn controllers drift more. Test your specific controllers with SmartDashboard.putNumber("LeftY", m_driver.getLeftY()) in periodic(). Set the deadband to slightly above the highest idle drift value you observe. Never set it above 0.10 — that's 10% of the stick's range where the robot won't respond at all, which degrades fine control at low speeds.

⚠️ Sign conventions: the three negations that trip everyone

WPILib's joystick Y axis returns negative when pushed forward (up on screen = negative by hardware convention). WPILib's ChassisSpeeds.vx is positive forward. So you negate getLeftY(). Similarly, getLeftX() returns positive when pushed right, but ChassisSpeeds.vy is positive to the left — so you negate getLeftX(). For rotation: getRightX() returns positive when pushed right, but WPILib counter-clockwise-positive means right-stick-right should be clockwise (negative). So you negate getRightX(). All three negations are present in the code above — remove any one and the corresponding axis reverses.

🔌 System Check — First Drive Verification Protocol

With the robot on blocks, run through every input axis independently:

  • Forward/back (left stick Y): Push forward → all four wheels point forward and spin. Pull back → all four wheels point forward and spin backward. If any wheel points backward when you push forward, that module's drive motor inversion is wrong.
  • Left/right strafe (left stick X): Push right → all four wheels point 90° right and spin. If the robot steers to 90° but spins backward, negate the vy input instead of the drive motor. If the robot steers to −90° instead, negate the vy sign.
  • Rotation (right stick X): Push right → robot should spin clockwise from above. Modules form the X-tangent pattern. If it spins counter-clockwise, negate omega.
  • Release joystick: All modules should hold their last position with near-zero speed. If any module drifts, the deadband is too small or the module's near-zero guard threshold needs increasing.
  • On carpet, slow speed first: Before driving at competition speed, drive at 10% max speed (add a 0.1 scale factor temporarily) to verify directional correctness without risk of damage. Only after confirming all axes correct should you remove the scale factor.

Knowledge Check

1. When you release the joystick, the robot continues to slowly creep forward. You're using a 0.02 deadband. What is the most likely cause, and what should you do?

  • A The brake mode configuration is wrong — change steering to coast mode.
  • B The controller's left Y axis drifts slightly above 0.02 when released. Measure the actual idle drift with SmartDashboard, then set the deadband to slightly above that value — likely 0.05. Controller drift is hardware variation, not a code bug.
  • C The end() method is not stopping the motors when the command ends.
  • D The ChassisSpeeds constructor always adds a minimum forward velocity.

2. You command the robot to strafe right by pushing the left stick to the right. Instead, the robot strafes left. What single change fixes this?

  • A Swap the CAN IDs of the front-left and front-right modules.
  • B Remove the negation from getLeftX(): change double rawVy = -m_controller.getLeftX() to double rawVy = m_controller.getLeftX(). Strafing in the wrong direction means the vy sign convention is inverted.
  • C Invert the drive motors on both left-side modules.
  • D Swap the left and right stick X assignments in the command.

3. isFinished() returns false in the DriveCommand. Why is this necessary, and what would happen if it returned true?

  • A Returning true is fine — the scheduler would just reschedule the default command immediately.
  • B If isFinished() returned true, the scheduler would call end(true) after the first execute() cycle, which calls stop(). The command would end, then immediately reschedule as the default command, run one execute(), stop again, and repeat — creating a 50 Hz stop/start oscillation. Drive commands must return false to run continuously.
  • C The command is the default command, so isFinished() is never called by the scheduler for default commands.
  • D Returning true would cause the command to be interrupted by the next incoming command immediately.
💪 Practice Prompt

First Drive — Robot-Oriented

  1. Create DriveCommand.java exactly as shown. Set it as the default command for SwerveDrivetrain in RobotContainer. Deploy and enable in TeleOp. With the robot on blocks, verify each axis independently: forward/back, strafe left/right, rotate. Fix any inverted axis before taking the robot off the blocks.
  2. Open Shuffleboard during drive testing. Watch the four module steer angle channels. During a pure strafe right input, confirm all four modules show approximately 270° (or −90°). During a pure rotation input, confirm the X-tangent pattern (approximately ±45°, ±135°). If any module shows a different angle, that module's CANcoder offset or wiring has a problem.
  3. Test the deadband: with the robot on the floor, release the joystick completely. Confirm the robot stops cleanly. Then add SmartDashboard.putNumber("RawLeftY", m_driver.getLeftY()) to the command's execute() and watch it with no input. Adjust the deadband threshold to be comfortably above the idle noise floor you observe.
  4. Bind the gyro zero button (Start) and test it. Push the robot to a 45° heading, press Start, confirm Drive/HeadingDeg returns to 0. This button is critical for field-oriented control — it must work perfectly before Lesson 9.
  5. Stretch goal: Add a slow-mode multiplier: when the left bumper is held, multiply vx, vy, and omega by 0.25 before building the ChassisSpeeds. Bind this using .whileTrue() on the left bumper. Verify the robot drives at 25% speed while LB is held and returns to full speed when released. This is an immediately useful competition feature — precision alignment is the most common use case for slow mode on swerve.