Joystick and Controller Input
A game controller is a collection of axes and buttons. WPILib maps every physical input to a method call your code polls every 20ms. This lesson covers reading that input correctly — including the deadband you must apply before commanding a motor, and the Y-axis inversion that trips up every first-year programmer without exception.
By the end of this lesson, you will:
- Construct an
XboxControllerand read both axes and buttons inteleopPeriodic() - Explain why a deadband is required and apply
MathUtil.applyDeadband()correctly - Fix the Y-axis inversion that makes "forward" produce a negative value
- Distinguish between button state (held) and button event (rising edge) and choose the right method for each use case
- Read trigger axes, bumpers, and the POV hat correctly
Controllers Are Just Axes and Buttons
An Xbox controller has 6 axes (left X/Y, right X/Y, left trigger, right trigger) and 10 buttons (A, B, X, Y, left bumper, right bumper, left stick click, right stick click, Start, Back). WPILib reads all of them through the Driver Station connection — your code polls the current state each 20ms cycle.
The XboxController class wraps raw axis and button indices into named methods. For non-Xbox hardware use PS4Controller, PS5Controller, or the generic Joystick class with raw axis numbers.
Controller Explorer
Click any input below to see the WPILib API call, return type, value range, and a hardware note for each input on the Xbox controller.
The Y-Axis Inversion Problem
Pushing the left stick forward returns a negative value. This is the HID standard — historically joystick Y-axes used screen coordinates where up is negative. WPILib inherits this and does not correct it.
If you use controller.getLeftY() directly to command a forward-positive motor, pushing forward drives the robot backward. Fix: negate the value before using it.
Deadband: The Required Safety Step
A joystick at rest is not perfectly at zero. Mechanical wear and electrical noise produce a small residual value — typically 0.02–0.08 — even when nobody is touching the stick. Without a deadband, that residual value commands the motor and the robot slowly drifts while the driver's hands are off the controls. A deadband zeroes out all values below a threshold.
Button Input: State vs. Event
Use button state when the behavior should run continuously while the button is held — run the intake while A is pressed, enable turbo mode while right bumper is held.
Use rising-edge detection for toggles or one-time actions. Without it, the toggle flips 50 times per second while the button is held.
getAButtonPressed() returns true only when the button is down this cycle and was up last cycle — that's a rising edge. WPILib tracks the previous state internally each cycle. You write no debouncing code yourself.
Driver vs. Operator: The Two-Controller Convention
Most competition robots use two controllers: port 0 for the driver (motion, navigation, defense) and port 1 for the operator (intake, shooter, arm, climber). This separation keeps the driver focused without accidentally triggering mechanisms, and lets the operator run complex sequences without touching drive controls.
During robot inspections I ask teams to show me their button mapping document. Teams that have a printed or posted controller layout can swap drivers without confusion and brief backup operators in under a minute. Teams that rely on "the operator just knows" lose time in the queue when a backup steps in. Document your mapping in Constants.java or a layout sheet. The drive team reads code through the physical controller, not the source file.
- Always negate the Y-axis before using it for forward motion.
-controller.getLeftY()makes forward stick produce positive output. This is the first thing to verify after first deploy: push stick forward, robot drives forward. - Always apply a deadband before commanding any axis-driven motor. A threshold of 0.05 is safe for most Xbox controllers. Use
MathUtil.applyDeadband()for smooth response, not a manualifcheck. - Use
getButtonPressed()for toggle actions,getButton()for held actions. A toggle driven bygetButton()will flip 50 times per second. - Triggers return a
double(0.0–1.0), not a boolean. Threshold them explicitly or use the analog value for proportional control. - POV hat returns -1 when unpressed. Always check for -1 before acting on the angle value.
- Document your button mapping. The drive team's mental model must match the code. Mismatches are discovered during a match, not before it.
Knowledge Check
if (controller.getBButton()) { armDeployed = !armDeployed; }. The operator presses and holds B for 0.5 seconds. What happens?MathUtil.applyDeadband(value, 0.05) produce better driver feel than (Math.abs(value) < 0.05) ? 0.0 : value?Full Controller Integration
- Add
XboxController driverController(port 0) andXboxController operatorController(port 1) to yourRobot.javafrom Lesson 1. Construct inrobotInit(). - In
teleopPeriodic(), read the left Y-axis with negation and a 0.05 deadband. Print the result to the console. Confirm pushing forward prints a positive value near 1.0. - Add A-button held-state control: print "INTAKE RUNNING" while A is held using
getAButton(). Verify it prints continuously while held. - Add a B-button toggle using
getBButtonPressed()and aboolean deployActivefield. Confirm it fires exactly once per physical press regardless of hold duration. - Read the POV hat — print the angle when a direction is pressed, skip when it returns -1. Test all four cardinal directions.
- Read the right trigger as an analog value. Print "SHOOTING AT X%" where X is the trigger depth times 100. Confirm partial presses produce intermediate values.
- Bonus: Create a
ControllerMapsection inConstants.javadocumenting every binding. Then look at a recent Team 2910 robot's button mapping documentation or code. How do they organize it?