Drive module
Package: com.crossbotics.ftchelper.drive
Learn it hands-on: 04 Mecanum, 05 Tank, 09 Autonomous by hand, 10 Autonomous with mechanisms.
Drives a mecanum chassis (4 mecanum wheels, so it can also slide sideways) or a standard chassis (also called tank or differential drive: normal wheels, a left side and a right side, 2 or 4 motors), in TeleOp and in autonomous. With the robot in five files you describe the drive in one line and these classes work underneath. Use this page to drive by hand in your own OpMode, or to look up a setting. Which way is plus and minus: Signs and units. Read it first.
| Your robot | TeleOp class | Autonomous class | Config |
|---|---|---|---|
| Mecanum, 4 motors | MecanumDrive |
AutoDrive |
DriveConfig.mecanum() |
| Standard, 4 motors | TankDrive |
AutoDrive |
DriveConfig.tank() |
| Standard, 2 motors | TankDrive |
AutoDrive |
DriveConfig.tankTwoMotor() |
| Class | Use it to |
|---|---|
DriveConfig |
choose the drive type, name the motors, set their directions and a power limit |
MecanumDrive |
drive a mecanum robot in TeleOp (robot-centric or field-centric) |
TankDrive |
drive a standard (tank) robot in TeleOp (arcade or tank style) |
DriverControls |
standard TeleOp driving from one gamepad (mecanum or tank, slow trigger, field-centric switch) |
DriverHeading |
which way the robot faces compared with the driver; continues from autonomous |
ReverseDrive |
a mode where the back of the robot acts as the front |
AutoDriveConfig |
tuning numbers for autonomous moves |
AutoDrive |
autonomous: drive a distance, turn an angle, turn to a heading |
MecanumMixer |
the mecanum wheel-power math (no hardware) |
DifferentialMixer |
the standard-drive left/right math (no hardware) |
MotionMath |
the math that slows a move down near its end (the “taper”) |
StallDetector |
tell when a move is stuck |
Also in this page: Check motor directions and Tune AutoDrive.
DriveConfig
Section titled “DriveConfig”A small settings object. Start from the factory method for your drivetrain (a method that makes one with the usual settings), then change what is different on your robot.
DriveConfig driveConfig = DriveConfig.mecanum(); // or DriveConfig.tank(), DriveConfig.tankTwoMotor()driveConfig.frontLeft = "fl"; // only if your names are differentdriveConfig.powerLimit = 0.6; // no wheel ever gets more than 0.6 power| Factory | Type | Motor names |
|---|---|---|
DriveConfig.mecanum() (same as new DriveConfig()) |
MECANUM |
front_left, front_right, back_left, back_right |
DriveConfig.tank() |
TANK |
front_left, front_right, back_left, back_right |
DriveConfig.tankTwoMotor() |
TANK |
left_drive, right_drive (the FTC sample names); back names are null |
| Field | Default | Meaning |
|---|---|---|
type |
MECANUM |
DriveType.MECANUM or DriveType.TANK |
frontLeft, frontRight, backLeft, backRight |
"front_left" … "back_right" |
configuration names of the motors. For a 2-motor tank, use frontLeft/frontRight and set both back names to null. |
frontLeftReversed … backRightReversed |
left = true, right = false |
which motors are reversed |
powerLimit |
1.0 |
the most power any wheel ever gets. Use 0.4 on a new robot. |
brake |
true |
wheels hold still at power 0 (false = coast: they roll freely) |
| Method | What it does |
|---|---|
hasBackMotors() |
true if both back motor names are set |
validate() |
checks the settings make sense (the drive classes call it for you). A mecanum drive needs 4 names; back names must be both set or both null. |
Use the SAME DriveConfig in TeleOp and autonomous. A good way: one class in TeamCode with a
static DriveConfig make() method that both use.
MecanumDrive
Section titled “MecanumDrive”MecanumDrive drive = new MecanumDrive(hardwareMap, driveConfig);waitForStart();while (opModeIsActive()) { // forward, strafe right, turn clockwise. Minus: the SDK gives stick UP as negative. drive.drive(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x);}drive.stop(); // wheels off when the OpMode endsThe constructor gets the four motors, sets direction and brake, uses plain power (RUN_WITHOUT_ENCODER), and sets power to 0. A wrong motor name gives a clear error.
| Method | What it does |
|---|---|
drive(forward, strafeRight, turnClockwise) |
robot-centric drive. Each value -1 to 1. |
drive(forward, strafeRight, turnClockwise, speedScale) |
same, times a speed multiplier 0 to 1 (for a slow mode) |
driveFieldCentric(fieldForward, fieldRight, turnClockwise, headingCcwDeg) |
field-centric drive (see below) |
driveFieldCentric(..., headingCcwDeg, speedScale) |
same, with a speed multiplier |
stop() |
all wheels to 0 |
setPowers(double[4]) |
set each wheel yourself (order FL, FR, BL, BR). For testing. The power limit still applies. |
setBrake(boolean) |
true = hold at 0 power, false = coast |
getMotors() |
the four DcMotorEx motors (FL, FR, BL, BR) |
getConfig() |
the DriveConfig |
If the numbers add up to more than 1 on a wheel (for example full forward + full strafe), all four wheels are scaled down together. The robot keeps the direction you asked for.
Robot-centric or field-centric?
Section titled “Robot-centric or field-centric?”- Robot-centric: stick up = robot drives where IT faces. Simple. If the robot faces you, stick up drives it toward you.
- Field-centric: stick up = robot drives AWAY FROM YOU, whichever way it faces. Easier for new drivers. Needs the IMU (the turn sensor inside the Control Hub, see ImuSetup).
double heading = ImuSetup.headingDeg(imu); // read every loop: which way the robot faces nowdrive.driveFieldCentric(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x, heading);The heading must be 0 when the robot faces away from the driver. In a match, use
DriverHeading instead of the plain IMU heading: it continues from where
autonomous ended, so “forward” is right from the first second of TeleOp.
DriverControls
Section titled “DriverControls”Standard TeleOp driving from one gamepad, for mecanum AND tank robots, in one line per loop.
EasyTeleOp uses it for you.
DriverHeading heading = new DriverHeading(imu); // or null: no field-centricDriverControls driving = DriverControls.create(hardwareMap, new RobotSettings().drive(), heading);// every loop:driving.update(gamepad1);// at the end (in finally):driving.stop();| Control | Does |
|---|---|
| left stick | drive (mecanum: any direction; tank: forward/back) |
| right stick X | turn |
| left trigger | slow: down to slowSpeed at a full squeeze |
| Method | What it does |
|---|---|
DriverControls.create(hardwareMap, config, heading) |
makes a MecanumDrive or a TankDrive, from config.type |
update(gamepad) |
drives from the sticks. Every loop. |
update(gamepad, slow) |
the same, with slow mode from your own code instead of the left trigger |
update(gamepad, speed) |
the same, at a speed your own code works out (0 to 1, e.g. a slow trigger that is not LT) |
drive(forward, strafeRight, turnClockwise, speed) |
drive with your own numbers (robot-centric), for custom controls |
tank(left, right, speed) |
two-stick tank style: one value per side. Works on tank and mecanum robots. |
hold() |
wheels still this loop (e.g. while a menu uses the gamepad) |
stop() |
all wheels to 0 |
slowSpeed(s) |
speed at full slow trigger, default 0.3 |
sticks(deadband, exponent) |
stick feel, default 0.05 and 2 (StickShaping) |
setFieldCentric(on), toggleFieldCentric(), isFieldCentric() |
field-centric switch. Starts off. Stays off on a tank robot or without a heading. |
canFieldCentric() |
true with mecanum wheels and a heading |
isSlow(), status() |
for telemetry: status() is e.g. “field SLOW” |
DriverHeading
Section titled “DriverHeading”Which way the robot faces compared with the driver: 0 = straight away from the driver, left = +.
Why: TeleOp sets up the IMU again in INIT, and that makes “0” the way the robot faces at that moment, which is wherever autonomous left it. Drivers may not touch the robot between autonomous and TeleOp. Without help, field-centric “forward” would be wrong until the driver turns the robot and presses a reset button.
// TeleOp INIT, after the IMU is set up:DriverHeading heading = new DriverHeading(imu); // reads autonomous's saved headingcontrols.bindPress(Pad.G1, Btn.BACK, heading::reset); // manual reset: face away firstwaitForStart();heading.forgetSaved(); // this run uses it now
// every loop:drive.driveFieldCentric(forward, strafe, turn, heading.degrees(), speed);| Method | What it does |
|---|---|
new DriverHeading(imu) |
call in TeleOp INIT, after the IMU is set up. Reads the heading autonomous saved (only if less than 5 minutes old). Otherwise 0 = the way the robot faces now. |
forgetSaved() |
call right after START: clears the saved heading, so a later TeleOp run does not use it. (Not in INIT, so INIT, STOP, INIT again still works.) EasyTeleOp does this for you. |
degrees() |
the heading compared with the driver, -180 to 180. Call every loop. |
reset() |
“the robot now faces away from me”: now = 0. For a button. |
isFromAuto() |
true while it still counts from autonomous (no reset since) |
Where the saved heading comes from:
- EasyAuto saves it for you at the end. If the robot does not start facing straight away from
its driver, override
startHeadingDeg()with the start angle (left = +), written for BLUE: it is mirrored for RED, like the turns (unlessmirrorForRed()returns false). - Your own autonomous: at the end,
PoseStore.save(new FieldPose(0, 0, heading)), with the heading measured from “away from the driver” (auto.headingDeg()if the robot started facing away).
TankDrive
Section titled “TankDrive”For a standard (tank / differential) drive with 2 or 4 motors. It cannot move sideways.
TankDrive drive = new TankDrive(hardwareMap, DriveConfig.tankTwoMotor());waitForStart();while (opModeIsActive()) { // left stick = forward/back (minus: stick UP is negative), right stick = turn drive.arcade(-gamepad1.left_stick_y, gamepad1.right_stick_x);}drive.stop(); // wheels off when the OpMode endsThe constructor gets the motors, sets direction and brake, uses plain power, and sets power to 0.
It refuses a MECANUM config (and MecanumDrive refuses a TANK one), so you cannot mix them up.
| Method | What it does |
|---|---|
arcade(forward, turnClockwise) |
arcade style: one stick for forward, one for turning. Each -1 to 1. |
arcade(forward, turnClockwise, speedScale) |
same, with a speed multiplier 0 to 1 |
tank(left, right) |
tank style: one stick for each side, e.g. tank(-gamepad1.left_stick_y, -gamepad1.right_stick_y) |
tank(left, right, speedScale) |
same, with a speed multiplier |
stop() |
both sides to 0 |
setPowers({left, right}) |
set each side yourself. The power limit still applies. |
setBrake(boolean) |
true = hold at 0 power, false = coast |
getMotors() |
{left, right} for 2 motors, {FL, FR, BL, BR} for 4 |
getConfig() |
the DriveConfig |
Arcade or tank style? Arcade is easier for most new drivers. Tank style gives very direct
control of each side. ExampleTankTeleOp lets the driver switch with X.
ReverseDrive
Section titled “ReverseDrive”When ON, forward and strafe flip, so the back of the robot acts as the front. Turning does not flip.
ReverseDrive reverse = new ReverseDrive();// toggle() flips the mode and returns it; confirm (a ToggleRumble) buzzes 2x = ON, 1x = OFFcontrols.bindPress(Pad.G1, Btn.L_STICK_BTN, () -> confirm.play(reverse.toggle()));// every loop:drive.drive(reverse.apply(forward), reverse.apply(strafe), turn);| Method | What it does |
|---|---|
toggle() |
flip the mode. Returns the new state (true = reversed). |
set(boolean) |
set the mode |
isReversed() |
true when reversed |
apply(value) |
use on forward and strafe only. Returns the value, flipped if reversed. |
AutoDriveConfig
Section titled “AutoDriveConfig”Tuning numbers for AutoDrive. The defaults are last season’s robot. Tune them on this robot
(see Tune AutoDrive).
| Field | Default | Meaning |
|---|---|---|
calibrationConfirmed |
false |
AutoDrive refuses to run until you set this to true. A reminder to tune first. |
ticksPerRev |
537.7 | encoder counts per motor turn. goBILDA 312 RPM = 537.7, 435 RPM = 384.5. Check your motor’s page. |
wheelDiameterIn |
4.098 | wheel size, inches. Start with the real size (96 mm = 3.78 in), then tune. |
gearRatio |
1.0 | motor turns per wheel turn |
strafeCorrection |
1.15 | mecanum wheels slip sideways. Sideways wheel travel is divided by this to get the real distance, and sideways power is raised by it so diagonals stay straight. |
turnKp |
0.012 | turn power per degree of error |
turnKd |
0.003 | slows the turn near the end to stop overshoot |
turnToleranceDeg |
1.0 | a turn is done inside this error… |
turnSettleSec |
0.15 | …for this long |
turnMinPower |
0.0 | smallest turn power outside the tolerance. Try 0.05-0.1 if turns stop short. |
moveMinPower |
0.30 | slowest speed at the end of a move |
taperStartFraction |
0.10 | start slowing for the last 10% of a move |
moveTurnScale |
0.60 | moveWithTurn: heading correction is multiplied by this |
moveMaxTurnPower |
0.35 | moveWithTurn: most turn power while moving |
actionTimeoutSec |
10.0 | any move or turn gives up after this long |
stallExitEnabled |
true |
stop a move early if the robot is stuck |
stallProgressIn |
0.25 | stuck = less than this many inches of progress… |
stallTimeSec |
0.4 | …within this many seconds |
ticksPerInch() gives the encoder counts per inch, worked out from the first four numbers.
AutoDrive
Section titled “AutoDrive”Autonomous moves for mecanum and standard (tank) robots. It measures distance with the wheel
encoders (sensors that count how far each motor turned) and turns with the IMU.
The DriveConfig type tells it which drive you have. Each method waits until the move ends,
so use them only in a LinearOpMode autonomous. No wheel ever gets more power than
DriveConfig.powerLimit, whatever maxPower you pass.
AutoDrive auto = new AutoDrive(this, driveConfig, autoConfig, imu); // this = your OpModewaitForStart();// Each line waits until that move is done, then the next one starts. 0.6 = top power.auto.move(24, 0, 0.6); // 24 in forwardauto.move(12, 90, 0.5); // 12 in rightauto.turn(90, 0.5); // turn 90 degrees leftauto.turnTo(0, 0.5); // face the start directionThe constructor
Section titled “The constructor”new AutoDrive(opMode, driveConfig, autoConfig, imu)
opMode: yourLinearOpMode, usuallythis.driveConfig: the sameDriveConfigTeleOp uses.autoConfig: tuning. Throws an error ifcalibrationConfirmedis false.imu: fromImuSetup.get(...).
It resets the drive encoders, uses RUN_USING_ENCODER and brake, and sets power to 0.
Methods
Section titled “Methods”| Method | What it does |
|---|---|
move(distanceIn, directionDeg, maxPower) |
drive straight, no turning. directionDeg is robot-relative: 0 forward, 90 right, 180 back, -90 left. A negative distance goes the other way. |
moveWithTurn(distanceIn, fieldDirectionDeg, targetHeadingDeg, maxPower, maxTurnPower) |
drive in a FIELD direction (fixed even while turning) and turn to a heading at the same time. If the move finishes and the heading is not there yet, it finishes the turn inside the same time limit. After a stall it does not turn. |
turn(degrees, maxPower) |
turn in place by an amount. Positive = left. |
turnTo(headingDeg, maxPower) |
turn in place to face a heading (IMU degrees, 0 = start direction) |
headingDeg() |
the IMU heading |
resetHeading() |
make the current heading 0 |
getWheelTicks() |
encoder counts (FL, FR, BL, BR) |
stop() |
stop all wheels |
setLoopHook(code) |
run your code once per loop during every move, e.g. auto.setLoopHook(() -> lift.moveTo(target, pid)); Keep it quick. |
setStopCondition(condition) |
end a move early (result STOPPED) when this becomes true. AutoRoutine uses it for park time. |
There is also a constructor for simulations and tests, with no robot: you give it pretend motors, a heading, “is the OpMode running” and “idle”. The library’s own tests drive a pretend robot this way.
On a standard (tank) drive
Section titled “On a standard (tank) drive”A standard drive cannot move sideways, so:
moveworks only with direction0(forward) or180(back). Any other direction throws an error that says “turn first, then move”.moveWithTurnthrows an error. UseturnTo(...)and thenmove(...).turnandturnTowork the same as on mecanum.strafeCorrectionis not used.
auto.move(24, 0, 0.6); // forwardauto.turnTo(90, 0.5); // face leftauto.move(12, 0, 0.6); // forward again (in the new direction)Results
Section titled “Results”Every move returns how it ended, an AutoDrive.Result:
| Result | Meaning |
|---|---|
DONE |
reached the target |
STALLED |
stopped early: the robot was stuck (pushing a wall or robot) |
TIMED_OUT |
took longer than actionTimeoutSec |
STOPPED |
the OpMode was stopped |
You can ignore it, or react:
// STALLED = stuck: pushing on would waste time and strain the motorsif (auto.move(30, 0, 0.7) == AutoDrive.Result.STALLED) { auto.move(4, 180, 0.4); // back off a little}How a move works (cruise and taper)
Section titled “How a move works (cruise and taper)”The distance is measured from the four wheel encoders: forward travel plus sideways travel
(divided by strafeCorrection), so straight, sideways and diagonal moves all go the real distance.
The robot drives at maxPower until the last 10% (taperStartFraction) of the distance, then
slows in a straight line down to moveMinPower. This makes stops more accurate. The motors
always stop at the end, whatever happens (even if your code throws an error).
Check motor directions
Section titled “Check motor directions”Do this first on every new drivetrain. Put the robot on a block, wheels off the ground.
- Copy
examples/.../MotorDirectionCheck.javainto TeamCode, set itsconfig()to yourDriveConfig, and remove@Disabled. - Run it. Hold X, Y, A, B: each spins one wheel (front-left, front-right, back-left, back-right; on a 2-motor tank only X = left and Y = right).
- Every wheel must roll so the robot would go FORWARD.
- If one rolls backward, flip its
...Reversedvalue in yourDriveConfig. - Then on the floor with
powerLimit = 0.4: stick up = forward, right stick right = turn clockwise. Mecanum only: left stick right = strafe right. If strafing goes diagonal, a motor name is on the wrong wheel.
Tune AutoDrive
Section titled “Tune AutoDrive”Do these in order. Write each result in your team’s log.
- IMU: check the heading sign (see hardware.md).
- Ticks per inch: set
ticksPerRevfrom the motor’s product page andwheelDiameterInto the real wheel size. Runauto.move(48, 0, 0.5). Measure the real distance. New diameter = old diameter x (real distance / 48). Repeat until within half an inch. - Strafe (mecanum only): run
auto.move(48, 90, 0.5). NewstrafeCorrection= old x (48 / real distance). - Turns: run
auto.turn(90, 0.5)andauto.turn(-90, 0.5). If it overshoots, raiseturnKdor lowerturnKp. If it stops short, raiseturnKp(small steps, like 0.002) or setturnMinPower. - Set
calibrationConfirmed = true.
MecanumMixer
Section titled “MecanumMixer”The math under MecanumDrive. It uses no hardware, so you can test it on a laptop. You only need it to build your own drive.
| Method | What it does |
|---|---|
mix(forward, strafeRight, turnClockwise, limit) |
four wheel powers (FL, FR, BL, BR), scaled together so none is over limit. All zero if an input is NaN. |
fieldToRobot(fieldForward, fieldRight, headingCcwDeg) |
changes a field-relative command into {forward, strafeRight} for the robot |
fieldDirection(fieldClockwiseDeg, headingCcwDeg) |
a field direction as {forward, strafeRight} for the robot, with a length of 1 |
FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT |
the index of each wheel (0-3) |
DifferentialMixer
Section titled “DifferentialMixer”The math under TankDrive. No hardware.
| Method | What it does |
|---|---|
arcade(forward, turnClockwise, limit) |
{left, right} = {forward + turn, forward - turn}, scaled together so neither is over limit. Both zero if an input is NaN. |
tank(left, right, limit) |
{left, right}, each cut to -1..1 and multiplied by limit |
LEFT, RIGHT |
the index of each side (0, 1) |
MotionMath
Section titled “MotionMath”| Method | What it does |
|---|---|
taperSpeed(remainingFraction, minSpeed, maxSpeed, taperStart) |
the speed for this part of a move. maxSpeed until taperStart is left, then a straight line down to minSpeed. Example: (0.05, 0.3, 0.8, 0.1) gives 0.55. |
StallDetector
Section titled “StallDetector”Tells when something with a target is stuck. AutoDrive uses it; you can use it for a lift too.
StallDetector stall = new StallDetector(0.25, 0.4); // 0.25 of progress within 0.4 s// every loop: how far is still left, and the time now (getRuntime() = seconds since INIT)if (stall.update(distanceLeft, getRuntime())) { ... stuck ... }| Method | What it does |
|---|---|
new StallDetector(minProgress, windowSec) |
stuck = less than minProgress closer within windowSec |
update(remaining, timeSec) |
true if stuck. Never true when remaining is already below minProgress. |
reset() |
start over (before a new move) |
It checks progress over the whole time window, not each loop, so it works the same at any loop speed.