Skip to content
Work in progress. This library and its docs are new and still changing, and some parts are not yet tested on a real robot. Something wrong or unclear?Tell us (a GitHub issue).

Lesson 3: Buttons (Controls.java)

Goal: know which buttons your robot already has, and give the buttons that are left your own jobs in Controls.java.

You need: nothing at first: start in the simulator. Then your robot and gamepads 1 and 2.

The code: the lesson robot’s Controls.java (and the Actions.java it uses). Every method: the robot module, ControlMap. Before this: Lesson 2: Drive.

@TeleOp(name = "Lesson Bot TeleOp", group = "Examples")
@Disabled
public class Controls extends ControlMap<Robot> {
public static boolean oneDriver = false; // true: everything on gamepad 1
@Override
protected void buttons(Robot robot, Pad g1, Pad g2) {
g2.rb.press(Actions.scoreHigh(robot)).says("score high"); // lesson 5
g2.b.toggle(robot.claw).says("claw").rumble(); // 2 buzzes = closed, 1 = open
g2.lb.tap(Actions.grabAndLift(robot)).says("grab and lift") // a tap...
.heldFor(1.0, () -> robot.lift.goTo("down")).says("lift all the way down"); // ...or held 1 s
}
}

Its imports, at the top of the file:

import com.crossbotics.ftchelper.robot.ControlMap;
import com.crossbotics.ftchelper.robot.Pad;
import com.qualcomm.robotcore.eventloop.opmode.Disabled; // delete with @Disabled in your copy
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;

Your own Controls.java from the Quick start already has them. A name in red? Alt+Enter (Option+Enter on a Mac) on it adds the import.

ControlMap<Robot> is a whole TeleOp: driving, the match timer, every part updated each loop, and everything stopped at the end.

The loop: a robot program runs a loop: many times a second, it reads the gamepads, runs the button jobs and updates every part. Each time round is “a loop”. The library runs this loop for you. buttons(...) runs only once, at INIT: each line plugs a job into a button, and the loop runs the job later, when the button is pressed. (Know Scratch or Blocks? From Blocks to Java shows the same idea with blocks.) The <Robot> says which robot it runs: yours, from Robot.java. g1 is gamepad 1 (the driver), g2 is gamepad 2 (the operator).

  • @TeleOp(name = ...): the name on the Driver Station.
  • @Disabled: hides the example on a real robot. Delete it (and its import) in your copy, or the Driver Station will not list it.
  • @Override: buttons(...) fills in a method the library class leaves to you; the library calls it to set up the buttons.

Each part in Robot.java brings its standard buttons. You write nothing for them. The lesson robot gets this map (two drivers):

Input Gamepad 1 (driver) Gamepad 2 (operator)
Left stick drive (and strafe) lift by hand
Right stick X turn free
LT / RT slow / free intake out (hold) / intake in (hold)
A / X / Y free / free / field-centric on/off lift down / low / high
B free claw open / close
D-pad up / down free lift next height up / down
BACK pause / go on (held 1 s, field-centric only: reset “forward”) free

The lift’s buttons come from its named heights in Tunables.Lift (down = 0, low = 900, high = 2400): A is the lowest, Y the highest, X the second. Every part’s buttons are in robot.md’s standard map. Why: most robots use the same buttons for the same parts, so you only write what is different.

public static boolean oneDriver = false; // true: everything on gamepad 1
  • false: two drivers, as in the table above.
  • true: the operator’s buttons move to gamepad 1. Slow becomes the left stick click (on/off) and field-centric the right stick click. A part whose button is taken by the driving gets none, and INIT says so, for example Lift by hand: no button (G1 left stick Y is taken by library).
  • Write it exactly like that: public static boolean. If it is not static, INIT stops with a message.

Inside buttons(...), one line per job: the pad, the button, the kind of job.

() -> robot.claw.open() is a lambda: a small piece of code you hand over now, and the button runs later. robot.claw::open is a shorter way to write the same thing (a method reference). A Runnable, in the table below, is any such piece of code with no inputs.

Kind Example Does
press(Runnable) g2.dpadLeft.press(robot.claw::open) once per press
press(Action) g2.rb.press(Actions.scoreHigh(robot)) starts the action; press again to cancel it
hold(whileHeld) g1.rt.hold(robot.intake::in) every loop while held
hold(whileHeld, onRelease) g1.rt.hold(robot.intake::in, robot.intake::stop) every loop while held, then once when let go
release(Runnable) g1.a.release(robot.claw::close) once when let go
toggle(part) g1.x.toggle(robot.intake) switches a part on/off (an intake, a claw, a launcher)
toggle(onEnable, onDisable) g2.dpadRight.toggle(robot.claw::open, robot.claw::close) each press flips between the two
tap(a).doubleTap(b) g2.rb.tap(robot.intake::toggle).doubleTap(robot.intake::out) a tap runs a (after waiting 0.35 s for a second tap); a double-tap runs only b
tapOrDoubleTap(tap, doubleTap) g2.rt.tapOrDoubleTap(robot.intake::toggle, robot.intake::out) the older way: a tap runs the first at once; a second tap within 0.35 s runs the second too
slow() g1.lt.slow() one driver only: slow driving (see below)
tap, hold (with other gestures on the same button: starts after 0.3 s), doubleTap, tapThenHold, taps(n, ...), heldFor(sec, ...) g2.lb.tap(a).heldFor(1.0, b) several gestures on one button (section 5)
with(other, job) g2.lb.with(g2.y, job) a pair: hold LB, press Y
rumble() .toggle(robot.claw).rumble() a buzz when the job runs; on a toggle 2 = on, 1 = off
free() g2.dpadDown.free() no job, even if a part suggested one
says(words) .says("score high") the words on the button map

Notes:

  • Which double-tap? Use tap(...).doubleTap(...) when the tap must not run on a double-tap: the tap waits 0.35 s to be sure. Use tapOrDoubleTap(tap, doubleTap) when the tap should run at once and the two fit together: its first tap has already run tap, so tap = intake on/off, double-tap = push out. More: the button menu.
  • slow() works only with oneDriver = true (with two drivers, gamepad 1 LT is slow already, and INIT stops with a message). On a trigger, the further you pull, the slower, down to Tunables.Drive.slowSpeed. On a button, slow while it is held.
  • A trigger counts as “pressed” past half way.
  • Add .says("...") to every job. Why: the drivers read the map on the Driver Station, and “press” tells them nothing.

Sticks take use(...): the stick’s value every loop, from -1 to 1, up and right are +, with a small dead zone and a gentle curve. To move the lift by hand on the right stick instead of the left:

g2.rightStickY.use(robot.lift::manual).says("lift by hand");
g2.leftStickY.free();

A stick also takes free() and says(...).

Show a value on the Driver Station with show(words, value), a line of its own inside buttons(...) (like rumbleWhen). The library reads the value again every loop and shows it during the match:

show("Lift ticks", robot.lift::position); // the lift's encoder count (a robot with a lift)
show("Intake on", robot.intake::isRunning); // true or false

Why: you see what the robot “thinks” (a count, a yes/no) while you drive, without writing any telemetry code. The value must be quick to read: a method reference like these, or a small lambda.

A job must never wait. sleep(...) or a while loop in a job compiles, but it freezes the whole robot: the loop stops, so no driving, no other button, no part updates, until the job ends.

g2.dpadLeft.press(() -> { robot.intake.out(); sleep(500); robot.intake.stop(); }); // WRONG
g2.dpadLeft.press(Actions.spitOut(robot)).says("spit out"); // right: an Action waits safely

If a job takes too long, the Driver Station warns you: ... took 512 ms: the whole robot stopped that long. No sleep() or while loop in a button job: use an Action with waitSeconds (lesson 5). To wait, write an Action with waitSeconds(...) (lesson 5).

5. One job per button (or several gestures)

Section titled “5. One job per button (or several gestures)”

Each button has ONE job. Why: a button that does two things depends on which runs first, and the drivers cannot guess it.

The one exception is gestures: different ways of pressing the same button, each with its own job. The lesson robot’s LB: a tap grabs and lifts, holding it 1 s sends the lift all the way down.

g2.lb.tap(Actions.grabAndLift(robot)).says("grab and lift")
.heldFor(1.0, () -> robot.lift.goTo("down")).says("lift all the way down");

The library tells them apart for you: a tap is a press let go before the hold starts (0.3 s; with only heldFor, before its time), a double-tap is two taps less than 0.35 s apart. Every gesture, pairs of buttons and buzzes: the button menu.

The rules, checked at INIT:

  • A job of yours on a button a part suggested replaces the suggestion. (The part simply has no button there; use free() to clear one without a new job.)
  • Two jobs of yours on one button stop at INIT: Controls.java: G2 RB has two jobs. Give each button one job (put both commands in one lambda if you want both).
  • A job on a driving button (gamepad 1’s left stick, right stick X, LT with two drivers, Y on a mecanum with an IMU, BACK, START) stops at INIT: Controls.java: G1 LT is the library's "slow (squeeze: the further, the slower)" button. Pick another button.

Want two things on one button? Put them in one lambda: g2.dpadLeft.press(() -> { robot.claw.open(); robot.intake.stop(); }).says("drop").

  • At INIT the Driver Station shows the whole map, one line per input, with the part that gave it, for example G2 A: down [Lift] and G2 RB: score high, then the free buttons: G2 free: .... Read it before every match.
  • During the match, tap GUIDE: the map again, without the free buttons. The robot still drives. Tap GUIDE again to close it. (Holding GUIDE 1 s opens the tuning menu, PRACTICE profile only.)
  • In TeleOp’s INIT (before START), gamepad 1 X and B choose the alliance: X = BLUE, B = RED. After an autonomous, its alliance is already set.

A tap of gamepad 1 BACK pauses the robot: every part stops on brake, the wheels hold still, actions wait, and no button or stick job runs. The Driver Station says PAUSED. Tap BACK again and every part goes on with what it was doing (a lift goes on to its height, the intake runs again). Why: a safe stop for a jam or a dropped piece, without ending the program. (With field-centric on, hold BACK 1 s instead to reset “forward”, lesson 2; in robot-centric driving a held BACK does nothing.)

8. The lesson robot’s two actions, and a buzz

Section titled “8. The lesson robot’s two actions, and a buzz”

The lesson robot uses gamepad 2’s bumpers, which no part suggested:

  • RB, “score high”: lift up, open the claw at the top, lift down.
  • LB, “grab and lift” (a tap): close the claw, then lift the piece a little. Held 1 s: the lift goes all the way down.

It also replaces the claw’s own B button with the same toggle plus .rumble(): gamepad 2 buzzes twice when the claw closes and once when it opens. Why: the operator feels it without looking away from the robot.

An action runs a little every loop, so the robot keeps driving. Press the same button again to cancel it. If the operator moves one of its parts by hand (say, the lift stick), the action stops: the driver always wins. Lesson 5 shows how to write them.

From the ftc-helper-lib folder (skip the first line if you did it in lesson 2):

Terminal window
./gradlew :sim:fromRobot -PteamCode=none -ProbotClass=com.crossbotics.ftchelper.examples.lessonbot.Robot -Pout=build/lessonbot.properties
./gradlew :sim:run -PteamCode=none -Probot=build/lessonbot.properties

Open http://localhost:8780, pick “Lesson Bot TeleOp” and INIT: read the map. START. Use the on-screen gamepad 2, or the keyboard (set gamepad 2’s list to “Keyboard” first; A/B/X/Y are K L J I, LB/RB are Q E, LT/RT are Z C, GUIDE is M, BACK is Backspace). Press RB and watch the lift rise. Press RB again while it moves: it cancels. Tap GUIDE. Then add one job of your own to Controls.java, and one on a button that already has one, to see the INIT message.

Your own robot (from the ftc-helper-lib folder):

Terminal window
./gradlew :sim:fromRobot -PteamCode=../TeamCode
./gradlew :sim:run -PteamCode=../TeamCode

Pick your own TeleOp, INIT, and read your map. Add a job and a show(...) line, build again (stop the simulator with Ctrl+C, then run both lines again), and check the button and the new line on the Driver Station. (Simulator guide.)

  1. INIT: read the map. Is every job on the button you planned? Any notes about parts with no button?
  2. START. Press each of your buttons once. Does each do what its words say?
  3. Tap GUIDE: the map shows. Tap again: it closes.
  4. Start an action, then press its button again: it stops, and the lift holds where it is.
  5. Tap BACK: everything stops. Tap again: everything goes on.
Problem Fix
INIT stops: “has two jobs” Two of your lines use one button. Pick another, or put both in one lambda.
INIT stops: “is the library’s … button” That button drives the robot. Pick a free one (INIT lists them).
INIT stops: “slow() is for one driver” With two drivers, G1 LT is slow already. Delete the line.
A part’s button does nothing Your job replaced its suggestion, or INIT says it has no button (one driver).
No button does anything The robot is paused: tap BACK.
A button held at START does nothing Let it go and press again: a button held through START does not count.
The help says “press” Add .says("...") to that job.
Warning: ... took N ms: the whole robot stopped that long A job has sleep(...) or a while loop. Use an Action with waitSeconds (lesson 5).

How buttons work inside (presses, holds, toggles, rumble), built by hand: Tutorial 03: Buttons.

Lesson 4: An intake, a lift and a claw