Robot module (the robot in five files)
Package: com.crossbotics.ftchelper.robot
Learn it hands-on: Lesson 1: The robot in five files. Worked
examples: examples/.../gobildabot
(goBILDA’s starter bot), flywheelbot
and turretbot.
Quick answers with code to copy: the cookbook (driving, buttons, mechanisms, actions, autonomous).
A team’s whole robot program in five files a student can read in one sitting. This is the
recommended way to use the library, and the one the main-track lessons teach: start with lesson 1
(above), or the Quick start for a driving robot in 30 minutes. Each mechanism
is a module: one line in Robot.java that brings its own numbers, buttons and autonomous steps.
Each file answers one question, so you always know where to look:
| File | Answers | Changes when |
|---|---|---|
Robot.java (RobotDefinition) |
What is on the robot, and its name in the configuration? | a part is added, moved or renamed |
Tunables.java |
What are the numbers? | tuning (speeds, positions, PID, auto distances) |
Controls.java (ControlMap) |
Which button does what? (it is also the TeleOp) | the drivers change their minds |
Actions.java (Action) |
Which steps use several parts together? | a new combo move |
autos/ (Auto) |
What does the robot do on its own? | new or changed autonomous programs |
A new robot: ./gradlew :sim:newRobot -Pconfig=... -PteamCode=... writes the five files from the
robot configuration (below). A mechanism added later is one line in Robot.java:
its numbers, its standard buttons and its simulator lines come with it.
flowchart LR
R["Robot.java<br/>drive + modules"] --> C["Controls.java<br/>(TeleOp)"]
R --> A["autos/*.java"]
T["Tunables.java<br/>numbers"] --> R
X["Actions.java"] --> C
X --> A
R --> S["simulator robot file<br/>(:sim:fromRobot)"]
| Class | Use it to |
|---|---|
RobotDefinition |
describe the robot in Robot.java: drive(...) and one add(...) per mechanism |
Drive |
the drivetrain: mecanum or tank, motor names, motor, wheel, IMU |
Odometry |
where the robot is on the field: a goBILDA Pinpoint, a SparkFun OTOS, or dead wheels |
Intake |
a roller on a motor and/or CR servos: in, out, on at the start |
Lift |
a slide on a motor with named heights and a bottom switch |
Arm |
an arm on a motor with named angles in degrees |
Claw |
a claw servo: open, closed |
Wrist |
a wrist servo with named positions |
Positioner |
any servo with named positions (deploy, fold, gate) |
Turret |
a turret inside its limits, by hand or following the camera’s goal |
Launcher |
a flywheel and its Indexer: spin up, fire, speed from distance |
Indexer |
feeds one piece at a time (servo, CR servo or motor) |
Camera |
a Limelight or webcam; the alliance’s goal |
Action |
steps across several parts that never block (never make the loop wait) |
Mode |
a rule a module follows every loop while the mode is on |
ControlMap, Pad |
Controls.java: the TeleOp and the button map |
Auto, Steps |
one autonomous as a list of steps |
Toggleable |
anything one button switches on and off |
Motors |
look up the encoder counts and speed of common motors, by catalog id (a part’s name in the parts catalog, e.g. "gobilda-5203-312") |
Shape, Parts |
how each part looks and where it sits, for the simulator; wheels, rollers and servos by catalog id |
Module, MotorPositions, ServoPositions, Devices |
what the modules share (you rarely use these by name) |
RobotDefinition
Section titled “RobotDefinition”public class Robot extends RobotDefinition { public Intake intake; public Lift lift; public Claw claw;
@Override protected void define() { drive(Drive.mecanum("front_left", "front_right", "back_left", "back_right") .motor("gobilda-5203-312").wheelMm(96) .imu("imu", LogoFacingDirection.UP, UsbFacingDirection.FORWARD)); intake = add(Intake.motor("intake")); lift = add(Lift.slide("lift").bottomSwitch("lift_bottom")); claw = add(Claw.servo("claw")); atStart(() -> Actions.deployIntake(this)); // optional: an action right after START }}define() only describes the robot. It runs in INIT, and also without a robot (to write the
simulator file). The OpMode classes make the hardware (setup) and update every
part each loop. Nothing moves in INIT.
In define() |
|
|---|---|
drive(Drive d) |
the drivetrain (required) |
odometry(Odometry o) |
optional: what tells the robot where it is on the field (Odometry) |
follower(PathFollower.Maker m) |
optional: a path follower, e.g. follower(Pedro.follower(...)) (PathFollower); needs an odometry(...) line |
T add(T module) |
a mechanism; hands it back, so you can keep it in a field (lift = add(...)) |
atStart(Supplier<Action>) |
an action that runs once right after START, in autonomous and TeleOp |
tunables(Class) |
the Tunables class if it is not Tunables in the same folder |
chassisMm(l, w, h), heightMm(h), fixed(Shape...) |
for the simulator: the chassis box, the whole robot’s height, parts the code does not move (Shapes) |
| For your code | |
|---|---|
run(Action), cancel(Action), cancelAll() |
start or stop actions |
alliance() |
this match’s alliance (from autonomous’s menu; TeleOp remembers it) |
notes() |
INIT notes: missing Tunables sections or fields (the library’s defaults are used) |
modules(), drive(), all() |
the parts, the drivetrain, the update group |
pose() |
where the robot is now, from the odometry: a FieldPose (x in right, y in away from your wall, heading 0 = away, counterclockwise +); a copy; null with no odometry line |
odometry(), odometrySpec(), odometryChecked() |
the running PoseTracker (null before setup), the odometry line as written, and Tunables.Odometry.checked |
follower(), hasFollower(), makeOdometry(hardwareMap) |
the running path follower (null before setup), whether there is a follower line, and the odometry alone for a tool (Pedro’s tuners) |
robotConfig() |
the drive settings for EasyTeleOp/EasyAuto |
simLines() |
the simulator robot file’s lines from the code |
setup(hardwareMap) |
makes every module’s hardware (the OpMode classes call it in INIT) |
tunablesClass() |
the Tunables class found (null if none) |
static changedTunables() |
values tuned on the robot that differ from Tunables.java, as lines to copy back |
static sectionName(device) |
the Tunables section a device reads: “intake_deploy” -> IntakeDeploy |
Checked at INIT: a device name used twice, two parts reading the same Tunables section, an indexer added twice. Each stops with the name.
Tunables.java
Section titled “Tunables.java”public class Tunables { public static class Drive { public static double maxPower = 1.0, practiceMaxPower = 0.6, slowSpeed = 0.3; ... } public static class Lift { public static double min = 0, max = 2600; // the safe range (not tunable live) public static double kP = 0.004, kI = 0, kD = 0.0002, kF = 0.05; public static double down = 0, low = 900, high = 2400; // named heights: any names } public static class Claw { public static double open = 0.20, closed = 0.75; }}-
One section per part, named after its configuration name in CamelCase:
intake_deployreadsIntakeDeploy. Pick another with.tunables("Name")on the module. -
Sections for your own code (no part reads them, like
Tunables.Autowith the autonomous’s distances): everypublic staticnumber that is notfinalis in the tuning menu too (“Auto/parkInches”). The menu’s value is written into the field, so your code readsTunables.Auto.parkInchesas usual. Tune it in the TeleOp, save it (A), run the autonomous: it uses the saved value from its INIT, with no rebuild. Then copy the good value intoTunables.java(INIT lists it): a value saved only on the hub is lost with a new Control Hub. True/false settings and tables stay as written. -
The code is the record. The numbers you tune most become
Tunables (“Lift/kP”): the tuning menu (PRACTICE profile, after START hold GUIDE 1 s) changes them and saves them on the Control Hub. INIT then shows “Tuned on this robot (copy into Tunables.java)” with the lines to copy back. -
Missing numbers are not errors, with one exception: the library’s default is used and INIT says which. The exception: a Lift or Arm must write
minandmax(a safety limit never comes from a library default), andmaxmust be more thanmin. Otherwise INIT stops:Tunables.Lift has no max: write max (the top of the safe range, in counts: Motor Finder, B). A safety limit never comes from a library default.(no section at all:Tunables.java has no Lift section: ...; an Arm’s are indeg;minpoints to Motor Finder X). Withmaxnot abovemin:Tunables.Lift: max (...) must be more than min (...).minandmaxmust bepublic staticnumbers (public static double max = 2900;); notpublic, or not a number, and INIT stops withTunables.Lift.max must be a public static number (like "public static double max = 1000;"): .... -
A name that looks like a setting stops INIT. Every number that is not a known setting is a named position. So these field names stop INIT:
- Always: a known setting with other capitals or an
_added:Max,MAXorMINformax/min,kp,KPork_PforkP. (All capitals is a common habit: it stops too.) - Only when that setting itself is not written: for names of 5 letters or more, up to two
letters off (
maxPowrormaxPwrformaxPower). SoslowZone2next to a writtenslowZoneis a position, butslowZonwith noslowZonestops INIT.
The message:
Tunables.Lift.Max looks like "max" (capitals and spelling count). Fix the name in Tunables.java; a position must not look like a setting.Before, it quietly became a position calledMax, andmaxstayed the library default. The same check runs for Claw, Wrist and Positioner (their setting:travelSec). - Always: a known setting with other capitals or an
-
Safety: a tuned position stays inside
min..max; limits are read once and cannot be tuned. -
public staticfields: FTC Dashboard can show them too. -
The menu’s names (“Lift/kP”) are shared by the whole robot app: keep one Robot class per app.
| In the tuning menu, used at once | In the tuning menu, used from the next INIT | Code only (read at INIT) | |
|---|---|---|---|
| Drive | every number | calibrated |
|
| Intake | inPower, outPower |
||
| Lift, Arm | kP, kI, kD, kF, every named position |
min, max, maxPower, slowZone, slowZonePower, tolerance |
|
| Claw, Wrist, Positioner | every named position | travelSec |
|
| Turret | kP, kI, kD |
min, max, maxPower, tolerance, slowZone, slowZonePower, cameraOffset |
|
| Launcher | speedRpm, stepRpm |
readyBandRpm, idleRpm, shotIntervalSec, speedByDistance, kP..kF |
|
| Indexer | every number | ||
| Camera | pipeline, autoLighting, lightingSettleMs, lightingPictures, lightingMinPictures, lightingAfterStartMs; webcam exposureMs, gain |
||
| Odometry | checked, linearScalar, angularScalar (OTOS) |
||
Your own sections (Auto…) |
every number (a TeleOp reads the field each time) | an autonomous reads them when it builds its steps (INIT) | final numbers, true/false, tables |
drive(Drive.mecanum("front_left", "front_right", "back_left", "back_right") .motor("gobilda-5203-312").wheelMm(96).imu("imu", LogoFacingDirection.UP, UsbFacingDirection.FORWARD));drive(Drive.tank("left_drive", "right_drive").reversed("right_drive"));| Method | |
|---|---|
mecanum(fl, fr, bl, br), tank(left, right), tank(fl, fr, bl, br) |
the type and motor names |
motor(id) |
the drive motor’s catalog id (default gobilda-5203-312; the other parts use part(id)) |
wheelMm(mm), gearRatio(r) |
wheel diameter (default 96), motor turns per wheel turn |
wheel(id) |
the wheel by its catalog id, e.g. "gobilda-hogback-96": its diameter comes from the catalog |
wheelsAtMm(track, wheelBase) |
for the simulator: wheel centres side to side and front to back, mm |
reversed(names...) |
exactly these motors run reversed (default: the left side) |
imu(name, logo, usb), noImu() |
the IMU and how the hub is mounted (default “imu”, UP, FORWARD) |
Tunables.Drive: maxPower, practiceMaxPower (PRACTICE profile), slowSpeed (the slow trigger),
strafeCorrection, turnKp, turnKd, moveMinPower, and calibrated (autonomous refuses to run
until it is true: tune with the Drive Tuner first, lesson 6).
The Drive Tuner and the Shot Tuner take the whole robot: @Override protected RobotDefinition robot() { return new Robot(); };
every finder shows Tunables. lines to copy (finders).
Odometry
Section titled “Odometry”odometry(Odometry.pinpoint("pinpoint").offsetsMm(-84, -168)); // in define(), after drive(...)| Method | |
|---|---|
pinpoint(name) |
a goBILDA Pinpoint: offsetsMm(xPodLeft, yPodForward), podsSwingarm(), ticksPerMm(n), reversed("x", "y") |
otos(name) |
a SparkFun OTOS: mountIn(forward, left, headingDeg) |
threeWheels(left, right, perp) |
three dead wheels on encoder ports: trackWidthIn(in) (required), perpForwardIn(in) |
twoWheels(parallel, perp) |
two dead wheels and the drive’s IMU: parallelLeftIn(in), perpForwardIn(in) |
Tunables.Odometry: checked (set true after the Odometry Tuner; until then driveTo steps are
skipped on a real robot), and for an OTOS linearScalar and angularScalar. With it: robot.pose()
every loop, driveTo steps in autonomous, and TeleOp starting where autonomous ended. The
odometry is updated first each loop, before every part. Everything, with sketches of the offsets:
odometry module; hands-on: lesson 7.
PathFollower
Section titled “PathFollower”The small plug a path-following add-on fills in, so the core library never needs that other library.
The pedro module is one: follower(Pedro.follower(PedroConstants.foresight)) in define(),
then follow(label, path) steps in autonomous.
| Method | What it does |
|---|---|
follow(path, mirrorX) |
starts a path (mirrored for RED when routes are mirrored) |
isBusy(), cancel(), remainingIn() |
still going; stop now; inches left (for the “stuck” check) |
isReady() |
false while its pose source is not ready (the step then ends NO_ODOMETRY) |
check(path) |
at INIT: throws if it cannot follow this kind of path |
It is updated every loop right after the odometry, and drives the wheels only while a path runs.
Intake
Section titled “Intake”intake = add(Intake.motor("intake"));intake = add(Intake.motor("intake").onAtStart());intake = add(Intake.crServos("left_intake", "right_intake").reversed("right_intake"));intake = add(Intake.motor("intake").withCrServos("left_wheel", "right_wheel").reversed("right_wheel"));| Method | |
|---|---|
in(), out(), stop(), toggle() |
run in / out until stopped; on/off |
reverse(), endReverse() |
push out while a button is held, then back to what it was doing |
isRunning(), isPushingOut() |
running at all; pushing out now |
onAtStart() |
runs in from START until stopped |
holds(n) |
for the simulator: how many pieces it carries (default: a guess of 3) |
Tunables.Intake: inPower (1.0), outPower (0.7). Buttons: hold operator RT in, LT out; with
onAtStart(): RT switches it on/off, hold LT pushes out.
MotorPositions is what Lift and Arm share: goTo(name) (capitals do not matter; a typo
shows a warning and does nothing), stepUp(), stepDown(), manual(power) (0 holds where it was
let go), hold(), stop(), isAtTarget(), isAt(name), position(), positions(), part()
(the PositionMotor inside), and .reversed() when positive power moves
it down. Every number but min and max has a default, so a part works before it is tuned; min
and max must be in Tunables.java (from the Motor Finder), or INIT stops.
lift = add(Lift.slide("lift").bottomSwitch("lift_bottom"));Tunables.Lift (encoder counts): min, max, kP, kI, kD, kF (the steady push that holds it
up), maxPower, slowZone, slowZonePower, tolerance; every other number is a named height.
Buttons (operator): A lowest, Y highest (2 or more heights), X second (3 or more), D-pad up/down next height, left stick by hand.
With a bottom switch, the lift only goes down (slowly) until the switch has been pressed once, and the
switch sets 0. Without one, 0 is wherever the lift is at INIT (the encoder starts from there):
press INIT with the lift all the way down, also for TeleOp after an autonomous that left it up, or
max will be too high by that much. INIT reminds you.
arm = add(Arm.motor("arm", "gobilda-5203-117").gearRatio(2));The same as Lift, in degrees (gearRatio: motor turns per arm turn; bottomSwitch(name) like the
lift’s). 0 degrees is where the arm is
when the program starts. Button: operator right stick Y by hand.
ServoPositions is what Claw, Wrist and Positioner share: goTo(name), isDone() (it should
have arrived: a guess from travelSec), isAt(name), positions(), part() (the
PositionServo inside), reversed(). In Tunables every number is a
named position (0 to 1) except travelSec. A servo does not move until the first command after START.
claw = add(Claw.servo("claw")); // Tunables.Claw: open, closed (both needed)open(), close(), toggle(), isClosed(). Button: operator B opens/closes.
wrist = add(Wrist.servo("wrist")); // Tunables.Wrist: any positions, e.g. down, level, upnext(), previous() (by value). Buttons: operator D-pad left/right.
Positioner
Section titled “Positioner”intakeDeploy = add(Positioner.servo("intake_deploy")); // Tunables.IntakeDeploy: stowed, deployedgoTo(name), next(). No suggested buttons: use it from Controls.java or an action. For the simulator,
it swings an arm shape: .looks(Shape.arm()...angle("stowed", 90).angle("deployed", 20)), and can push a
passive part over (Shape.dropsWhen, Shapes).
Turret
Section titled “Turret”turret = add(Turret.motor("turret", "gobilda-5203-117").gearRatio(5).follows(camera));| Method | |
|---|---|
followMode() |
the mode that follows the camera’s goal (on from START with follows) |
aimAt(deg) |
turn to an angle (stops following) |
manual(power) |
by hand; following pauses while the stick moves |
isAimed(), isOutOfRange(), angle(), part() |
|
cameraOnBody() |
the camera is on the robot, not on the turret |
Tunables.Turret: min, max (degrees, counterclockwise +, 0 = forward: it never turns past them; a
goal outside waits at the limit and the Driver Station says “goal out of turret range”), kP, kI,
kD, maxPower, tolerance, slowZone, slowZonePower, cameraOffset. Buttons: operator right
stick X by hand, BACK following on/off.
Launcher
Section titled “Launcher”launcher = add(Launcher.flywheel("launcher", "gobilda-5203-6000").feeder(Indexer.servo("indexer")));launcher = add(Launcher.flywheel("launcher", "gobilda-5203-6000").feeder(Indexer.servo("indexer")) .speedFromDistance(camera).requireReady("turret aimed", () -> turret.isAimed()));| Method | |
|---|---|
spinUp(), idle(), toggle() |
to the shot speed; back to idleRpm |
fire(), fire(n) |
spins up if needed, feeds as soon as it is at speed (gives up after 3 s, saying why) |
fireWhileHeld(), ceaseFire() |
for a held button: one shot, then the next as soon as it is fed; on release no more (a tap still fires its one shot) |
adjust(steps) |
speed +/- steps x stepRpm (not saved) |
speedMode() |
the mode that sets the speed from the camera’s distance |
isReady(), isShooting(), targetRpm(), part() |
|
requireReady(what, check) |
another check before each feed (e.g. the turret is aimed) |
spinAtStart() |
spins up at START (autonomous and TeleOp), ready for the first shot |
shotAngle(deg) |
for the simulator: which way a piece leaves, degrees up from the wheel shape’s forward (the robot’s forward for a wheel mounted level; 135 = up and out of the back) |
Tunables.Launcher: speedRpm, stepRpm, readyBandRpm, idleRpm, shotIntervalSec,
speedByDistance ({{inches, rpm}, ...}, blended between rows; with no goal in view the last speed
stays), and kP, kI, kD, kF (the hub’s speed control; only used when kF is there: the
Flywheel Finder finds them). Buttons (operator): LB spin up/down, RB fire one (hold: keep firing, fireWhileHeld() / ceaseFire() on release),
D-pad up/down speed (when free and no speed-from-distance).
Indexer
Section titled “Indexer”Indexer.servo(name) (Tunables: rest, push, pushSec, returnSec), Indexer.crServo(name) and
Indexer.motor(name) (power, feedSec per piece), .reversed(). Usually given to a launcher with
.feeder(...); added on its own its button is operator RB (feed one). A servo indexer goes to rest
at START. Commands: feedOne(), isBusy(), isJammed(), clearJam().
Camera
Section titled “Camera”camera = add(Camera.limelight("limelight").goal("BLUE SCORING", "RED SCORING"));camera = add(Camera.webcam("Webcam 1"));goal() (the alliance’s goal, or null), target(name), target(id), source() (the
TargetSource inside). Tunables (Limelight): pipeline. It starts in INIT.
Venue lighting (vision.md): the camera tries a few settings in INIT and keeps the one that sees the most AprilTags.
camera = add(Camera.limelight("limelight").autoPipelines(0, 1, 2).goal("BLUE SCORING", "RED SCORING"));camera = add(Camera.webcam("Webcam 1").autoExposure()); // auto, 3, 6, 12 ms; or autoExposure(6, 3, 12)At INIT the Driver Station shows limelight lighting: pipeline 2 (9 of 10 pictures saw tags) (a
warning while no tag is seen), and the two lines that skip the test at this venue next time:
public static double pipeline = 2;public static boolean autoLighting = false;Put them in Tunables.Limelight (change the pipeline line; add autoLighting if it is not there).
A webcam shows public static double exposureMs = 6; instead: a new line in its section.
lighting() gives the test itself. At START the test stops and keeps the best setting so far, so
have a tag in view at INIT. A camera missing from the robot configuration is a warning at INIT, not
a stop: the OpMode runs and the camera sees nothing.
Its optional Tunables (leave them out for the defaults):
| Number | Default | What it does |
|---|---|---|
autoLighting |
true | false = skip the test: use pipeline (Limelight) or exposureMs and gain (webcam) |
lightingSettleMs |
300 | wait after a switch before counting pictures (ms) |
lightingPictures |
10 | pictures counted per setting |
lightingMinPictures |
3 | pictures with tags a setting needs to count as good |
lightingAfterStartMs |
0 | after START, how long to keep trying (ms); 0 = stop at START with the best so far. No targets while it tries |
exposureMs (webcam) |
-1 | a fixed manual exposure (ms) when the test is off; -1 = the camera’s own |
gain (webcam) |
-1 | the gain for manual exposures; -1 = the camera’s highest |
These are read once, in INIT. Changed one with the tuning menu? Copy the value back into
Tunables.java, or the robot uses the old one at the next INIT.
Action
Section titled “Action”public static Action scoreHigh(Robot r) { return Action.named("Score high") .then(() -> r.lift.goTo("high")) .waitUntil("lift at the top", r.lift::isAtTarget, 2.0) .then(r.claw::open) .waitSeconds(0.3) .then(() -> r.lift.goTo("down"));}// Controls.java: g2.y.press(Actions.scoreHigh(robot)); an autonomous: s.action(Actions.scoreHigh(robot));| Method | |
|---|---|
named(name) |
a new action |
then(Runnable...) |
do these at once (several parts together), then go on |
waitSeconds(s) |
wait (the robot keeps running) |
waitUntil(what, condition, timeoutSec) |
wait until true; if it times out the action STOPS and says why |
waitUntil(condition, timeoutSec) |
the same, with “the condition” as its words |
name(), isRunning(), status(), result() |
“Score high: step 2 of 5 (lift at the top)”; “done”, “cancelled”, “stopped: Lift moved by hand” |
- Never blocks: an action does a little every loop.
- Parts are claimed: an action owns the parts it commands while it runs. Another action that commands one of them stops it (“taken by …”); so does the driver moving it by hand (a button or a stick away from the middle): the driver always wins.
- Press again to cancel (a button bound with
press(action)). A stopped action (cancelled, taken over, timed out) stops what it started, safely: a lift or arm holds its height, a turret its angle, shots still waiting are dropped, an intake stops. An action that finishes keeps its last commands. A failed action in an autonomous says why in the report.
A Mode is a rule a module follows every loop while it is on: Turret.followMode(),
Launcher.speedMode(). on(), off(), toggle(), isOn(); a button switches it with
g2.back.toggle(robot.turret.followMode()). Modes are for “always” rules, actions for steps that end.
ControlMap
Section titled “ControlMap”@TeleOp(name = "TeleOp")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.y.press(Actions.scoreHigh(robot)).says("score high"); }}Controls.java is the TeleOp (EasyTeleOp underneath). Every module on the robot already
has its standard buttons; write only what differs. A Pad has every input by name:
a b x y lb rb lt rt back start dpadUp dpadDown dpadLeft dpadRight leftStickButton rightStickButton
(buttons; a trigger counts as pressed past half way) and leftStickX leftStickY rightStickX rightStickY
(sticks, up and right +).
| On a button | |
|---|---|
press(Runnable) |
once per press |
press(Action) |
starts it; press again cancels |
release(Runnable) |
once when let go |
hold(whileHeld), hold(whileHeld, onRelease) |
every loop while held (and once on release) |
toggle(Toggleable) |
switches a part on/off (an intake, a launcher, a claw, a mode: a Toggleable) |
toggle(onEnable, onDisable) |
each press flips |
tapOrDoubleTap(tap, doubleTap) |
the “no waiting” double-tap: a tap runs tap at once; a second tap within 0.35 s (Pad.DOUBLE_TAP_SEC) runs doubleTap after it (the first tap has already run tap: e.g. tap = intake on/off, double-tap = push out). For a tap that must not run on a double-tap, use tap(...).doubleTap(...) (the tap waits 0.35 s) |
tap(job), doubleTap(job), taps(n, job), heldFor(sec, job) |
gestures, several on one button: once per quick tap; on the second quick tap; on the n-th (2 to 5); once after held sec s. Each also takes an Action |
hold(...) with other gestures |
every loop while held, from 0.3 s on (Tunables.Buttons.holdSec), so a quick tap stays a tap |
tapThenHold(whileHeld), tapThenHold(whileHeld, onRelease) |
a quick tap, then press again and hold: every loop while held |
with(other, job) |
a pair: hold this button (the shift key, no job of its own), press other (same gamepad): job once. other keeps its own job alone |
rumble(), rumble(n) |
feedback: buzzes when the job (or the gesture just before) runs; a toggle buzzes 2 = on, 1 = off |
slow() |
one driver only: on a trigger, drives slower the further it is pulled (down to slowSpeed); on a button, slow while held |
free() |
no job: the button stays free, even if a module suggested one for it |
says(words) |
the words on the help page |
on a pad: g2.rumbleWhen(test), rumbleWhen(test, n) |
feedback from the robot: a buzz each time test becomes true |
on a stick: use(DoubleConsumer) |
its value every loop (small dead zone, gentle curve) |
on a stick: free(), says(words) |
as on a button |
in buttons(...): show(label, value) |
your own value on the Driver Station, read again every loop, e.g. show("Lift ticks", robot.lift::position); or show("Ready", () -> robot.launcher.isReady());. Not shown while the tuning menu or the button map is open. value must not wait; an error in it shows as error: ... instead of stopping |
While the robot is paused (BACK), no button or stick job runs, with one exception: a hold’s stop job
(the second job of hold(job, stopJob)) still runs when the button is let go, or when the pause starts
during a gesture’s hold, so the part stays stopped. A gesture’s job runs at once unless the
button has a longer gesture that starts the same way; then it waits until it knows (let go, or
Tunables.Buttons.tapGapSec, 0.35 s). Both times are in Tunables.Buttons (optional: leave it out for the defaults) and the tuning menu; copy
a tuned value back into Tunables.java.
Every option as a menu: the button menu.
In Controls.java you can also override useStandardButtons() (return false: only your own jobs, no
module suggestions); robot() and oneDriver() read the robot and the one-driver switch.
The standard map (two drivers; with oneDriver = true the operator’s buttons move to gamepad 1):
| Input | Gamepad 1 (driver) | Gamepad 2 (operator) |
|---|---|---|
| Left stick | drive (and strafe) | lift by hand (Y) |
| Right stick | turn (X) | arm by hand (Y), turret by hand (X) |
| LT / RT | slow (the further, the slower) / free | intake out (hold) / intake in (hold, or on/off) |
| LB / RB | free | launcher spin up/down / fire one (hold: keep firing) |
| A / X / Y / B | free / free / field-centric on/off (mecanum) / free | lift lowest / second / highest / claw |
| D-pad up/down | free | lift next height, or launcher speed |
| D-pad left/right | free | wrist |
| BACK | tap: pause everything (stopped on brake), tap again: go on; hold 1 s: “forward” = away from you now (with odometry: its heading becomes 0, x and y stay) | turret following on/off (with a turret; else free) |
| START + BACK | debug lines | |
| GUIDE | tap: the button map, without the free buttons (during the match too); hold 1 s: the tuning menu (PRACTICE) |
One driver: the left stick click switches slow on/off and the right stick click switches
field-centric. To put slow on a trigger instead, give it .slow(): g1.lt.slow(); slows the robot
the further LT is pulled (down to slowSpeed at a full squeeze, like the two-driver LT). On a button,
.slow() is slow while held. Write the switch exactly as public static boolean oneDriver = true;
(INIT stops with a message if it is not static).
Checked at INIT: a module whose button is taken gets none (INIT notes it). Two jobs on one button in Controls.java, or a job on a driving button, stop with the button’s name. INIT shows the whole map and the free buttons.
@Autonomous(name = "Score and Park", preselectTeleOp = "TeleOp")public class ScoreAndPark extends Auto<Robot> { @Override protected void steps(Steps s, Robot robot) { s.start("Back wall, 2nd tile from the left, facing the field", 0) .menuYesNo("Score", true) .run("Hold the piece", robot.claw::close) .move("Leave the wall", 4, 0, 0.4) .onlyIf("Score") .run("Lift up", () -> robot.lift.goTo("high")) .move("Drive to the goal", 24, 0, 0.6) .waitUntil("Lift at the top", robot.lift::isAtTarget, 2.0) .run("Drop", robot.claw::open) .end() .parkWithSeconds(4) .move("Park", 20, 90, 0.7); }}What Steps can do:
| Step | |
|---|---|
start(where, headingDeg) |
where to place the robot (shown in INIT) and which way it faces |
start(where, xIn, yIn, headingDeg) |
the same, plus the spot in inches. The simulator puts the robot there in INIT (see below) |
menuYesNo(label, yes), menuChoice(label, options...) |
a choice for the drivers in INIT |
onlyIf(label) / onlyIf(label, option) … end() |
the steps in between run only for that choice |
isYes(label), choice(label) |
read a choice |
move(label, inches, directionDeg, power) |
drive (0 forward, 90 right) |
turn, turnTo |
turn by an angle / to a heading |
driveTo(label, xIn, yIn, headingDeg, power[, toleranceIn]) |
drive to a field point and heading with the odometry, correcting all the way (mecanum; needs Tunables.Odometry.checked on a robot) (driveTo) |
run(label, command) |
start something and go on at once |
waitSeconds, waitUntil |
wait (the robot keeps running) |
action(action[, timeout]) |
run an Action and wait until it is done |
alongside(action) |
run an action during the next steps |
step(label, AutoStep) |
your own step |
parkWithSeconds(s) |
the steps after it get the last s seconds (see Keep time to park) |
The spot in start(where, xIn, yIn, headingDeg) is seen from your wall and written for BLUE:
- x = inches right of the centre line; y = inches from the centre toward the far wall (negative = your half).
- RED works like the routes: mirrored, or, on a turned field like BIOBUZZ with
mirrorForRed()false, the same numbers seen from the red wall. - Both alliances at one wall, like DECODE: override
alliancesOnOneWall()to return true. - With odometry, the first step (“Start”) sets the pose to the spot (mirrored for RED the same way), and at the end the whole pose goes to TeleOp. Without a spot the pose starts at (0, 0).
The full guide, with every step and a tested example: The autonomous builder.
Keep time to park
Section titled “Keep time to park”Parking points come at the very end of autonomous, so the steps that park must never run out of time.
parkWithSeconds(4) in the list splits the steps in two:
- The steps before it (main steps) run while more than 4 s of the 30 s are left. When the 4 s mark comes, the step that is running stops at once (a move stops, a wait stops waiting), and the main steps not started yet are skipped.
- The steps after it (park steps) always run, until autonomous ends.
The report at the end (Driver Station, and TeleOp’s INIT) says which main steps were “cut short (time
for park)” or “skipped (time for park)”. Choose the seconds from a timed run: how long the park steps
take, plus about a second. Without parkWithSeconds, every step is a main step and nothing keeps time
for the end. It is EasyAuto’s thenPark() and
parkReserveSec() in one call.
The steps are read twice: in INIT (for the menu and the start position; nothing runs) and after
START. Everything else is EasyAuto: alliance and delay menu, mirroring for RED,
the timer, stopping everything, the report. When its steps are done it ends at once (even with time
left), so the Driver Station moves to its TeleOp; TeleOp’s INIT shows the report and the alliance (or
lets the drivers choose it: X = BLUE, B = RED). Until Tunables.Drive.calibrated is true it says
“AUTONOMOUS IS OFF” and does nothing (in the simulator it runs anyway, with a warning).
Shapes and parts
Section titled “Shapes and parts”Robot.java can also say what each part IS (its catalog id) and how it LOOKS (a Shape). Then
./gradlew :sim:fromRobot writes the simulator robot file with no TODO shape guesses: the 3D view draws the robot,
the size rules are checked on it, and each shape moves with its part, so the simulator shows whether the
controls do what they should (the intake drops, the rollers spin, a piece is fired). Worked example:
flywheelbot/Robot.java.
Name the part, give only what is not known. A part in the parts catalog brings its numbers: a motor its speed and encoder counts, a servo its speed and range, a wheel or roller its diameter (and width, when the seller gives it). Places are always the robot’s own.
drive(Drive.tank("left_drive", "right_drive").motor("gobilda-5203-312").wheel("gobilda-hogback-96") .wheelsAtMm(380, 290).reversed("right_drive"));chassisMm(400, 400, 100); // the chassis box, mmheightMm(300); // the whole robot
intakeDeploy = add(Positioner.servo("intake_deploy").part("gobilda-2000-torque") .looks(Shape.arm().name("kicker").atMm(150, 90, 100).sizeMm(60, 20, 15) .angle("stowed", 90).angle("deployed", 20)));intake = add(Intake.motor("intake").part("gobilda-5203-1150").holds(3) .looks(Shape.arm().name("intake_arm").atMm(150, 0, 100).sizeMm(110, 200, 20) .range(-15, 90).startAt(90).dropsWhen(intakeDeploy, "deployed", -15), Shape.roller("gobilda-gecko-48-rex").on("intake_arm").atMm(110, 0, 0).lengthMm(300)));launcher = add(Launcher.flywheel("launcher", "gobilda-5203-6000").feeder(Indexer.servo("indexer").part("gobilda-2000-speed")) .looks(Shape.flywheel("gobilda-hogback-96").atMm(-40, 0, 170)).shotAngle(50));fixed(Shape.box("tower").atMm(-90, 0, 40).sizeMm(50, 180, 260));| On a part | |
|---|---|
part(id) |
the main motor’s or servo’s catalog id (a typo stops with the list of ids). A launcher’s motor is in Launcher.flywheel(name, id), the drive’s in Drive.motor(id). On a CR servo the id is only a note (the simulator turns every CR servo at one speed) |
looks(Shape...) |
how it looks. Intake, Positioner, Launcher and Indexer take shapes; the others are drawn in the robot file (simulator guide) |
Shape |
What it is | Moved by |
|---|---|---|
roller(ids...) |
a cylinder; several ids (90 mm and 60 mm rollers taking turns) draw one as big as the biggest. In the simulator every roller of an Intake takes pieces in, and any roller drags the pieces it touches | spins with its part’s motor or CR servo |
flywheel(id), flywheel() |
a launcher wheel (no id: give diameterMm and lengthMm) |
spins with the launcher’s motor |
arm() |
swings about a hinge: atMm is the hinge, sizeMm(length, width, thickness) reaches forward at 0 degrees |
a Positioner’s servo (angle(position, deg) twice), or nothing (passive) |
slide() |
moves in a straight line along its axis (default up): sizeMm is one stage |
nothing (passive, travelMm) |
box(name) |
does not move: atMm is the middle of its bottom |
nothing |
| Setting | |
|---|---|
atMm(forward, left, up) / atIn |
from the middle of the chassis on the tiles, or from the hinge (or middle) of the shape it rides on |
sizeMm(forward, left, up) / sizeIn |
its box (not needed for a catalog roller or flywheel) |
lengthMm(mm), diameterMm(mm) |
a roller’s length along its axle (needed when the catalog has no width); a part not in the catalog |
axis(dir) |
up, down, forward, back, left, right: a roller’s axle (up = lying flat), an arm’s hinge |
on(part), on("name") |
rides on another part’s first shape, or on a shape by its name |
name(n) |
its name in the robot file (default: the part’s name, then name_2…) |
angle(position, deg) |
an arm on a servo: its angle at a named position; give two (0 = level forward, +90 up) |
passive(), range(min, max), startAt(deg), travelMm(mm) |
moved by gravity or springs, not the code: drawn at its start, the size rules try its whole range (its hard stops) |
dropsWhen(servoPart, position, toDeg) |
a passive arm that a servo pushes over: once the servo is three quarters of the way to the position, it falls to toDeg and stays (a folded intake) |
note(text) |
where a number came from (“est.”, “measured 2026-10-20”), written after the line |
color(c) |
its colour in the 3D view, e.g. "#2fa84f" |
tiltDeg(deg) |
mounted at an angle on its parent, about the left-right line (front up +): a spinner kept level on an arm that is down at -40 degrees has 40 |
soft() |
a soft roller (surgical tubing, flaps): it bends against the field, so in the simulator only game pieces touch it. It still counts for the size rules |
reversed() |
a roller that turns the other way from its motor (geared) |
Mistakes stop with the part’s name: a typo in an id (with the known ids), a roller with no length, an
arm with fewer than two angles or an unknown position name (the names in angle(...) must be positions in
that part’s Tunables section), shapes on a part that cannot take them, a setting that does not fit the
shape (angle on a passive arm, range on a servo arm, travelMm on anything but a passive slide,
lengthMm on a box or arm), riding on a part that has no shape, an intake whose looks have no roller,
one Shape given to two parts, two parts with one name in the robot file, and a shotAngle outside
0 to 180.
Lines without a shape stay as before: guesses marked TODO.
Motors
Section titled “Motors”Motors.get("gobilda-5203-312") gives countsPerRev and freeRpm; Motors.all() lists them. The ids
are the simulator’s parts catalog (a test keeps the two the same).
Module
Section titled “Module”Module is what every mechanism shares: name() (its Tunables section), deviceName(),
tunables(section). Devices is where modules find their hardware: Devices.of(hardwareMap)
on the robot; tests give pretend devices. A new kind of module is a change to the library (its parts are
package-private, see AGENTS.md); in team code, use the plain parts (mechanism) in
your own class and call it from Controls.java and your autos.
Pause (gamepad 1 BACK in a Controls.java TeleOp, or robot.pauseAll() / resume() /
togglePause(), isPaused()): every part stops at once with its motors on brake, the wheels hold still, actions wait
(their time limits too), and the Driver Station says PAUSED. Going on, each part does what it did before
(an intake runs again, a lift goes on to its height, a launcher spins up again). STOP at the end of a
program (TeleOp or autonomous) brakes everything the same way.
stop() on any module switches it off now (a launcher fully off, until spinUp()/fire()), and
counts as the driver’s command: an action using that part stops. To let a launcher coast while keeping
it ready, use idle().
Making the files
Section titled “Making the files”./gradlew :sim:newRobot -Pconfig=path/to/config.xml -PteamCode=../TeamCodeWrites Robot.java, Tunables.java, Controls.java, Actions.java and autos/LeaveStart.java from
the robot configuration (the XML file the Driver Station keeps on the Control Hub). Each device gets
its module from its name and type (“lift” is a Lift, a servo called “claw” a Claw); a goBILDA Pinpoint
or SparkFun OTOS becomes the odometry(...) line (offsets 0 and a TODO for the Odometry Tuner) with a
Tunables.Odometry section; every guess it is not sure of is a TODO in Robot.java. It never overwrites a file.
./gradlew :sim:fromRobot -PteamCode=../TeamCodeWrites (or refreshes) simulator/robot.properties from Robot.java and Tunables.java: every device
name, the drive, each module’s lines (from its part and shapes when Robot.java gives them,
Shapes). Lines it writes end with # from Robot.java; your own lines
(measured, from photos or CAD) are kept. It stops if the file describes a device differently from
the code. See the simulator guide.