Drivetrains
Mecanum and tank drivetrains, driving with the sticks, robot-centric and field-centric.
Two kinds
Drivetrains come in two kinds, which take different commands and suit different controls:
OmniDrivetrainmoves in any direction while turning: mecanum, X and swerve drives. Path following and field-centric driving need one.MecanumDriveis one.TankDrivetraindrives forward and back and turns, with the wheels of each side turning together: tank and six-wheel drives. It can’t move sideways.
Both are parts of the robot, with the actions of any part: driving to a target takes the drivetrain over, and the sticks stop it when pushed far (see below).
Mecanum drive
class MyRobot(host: RobotHost) : Robot(host) {
val drive = MecanumDrive(this) // motors leftFront, rightFront, leftBack, rightBack
}
The four motors are named leftFront, rightFront, leftBack and rightBack in the robot configuration, and are
the drive’s parts: drive.leftFront is a motor with its own
settings. The left motors run reversed by default, as they are mounted mirrored to the right ones.
Checking the wheels: drive.checkWheels() turns the wheels one at a time for a second each, and the Driver Station
shows which. Each should turn so it would push the robot forward. One that turns backward needs its direction flipped,
like drive.leftFront.direction = F; a different wheel turning than the one shown means the motors are plugged into
other ports than configured. Put it on a button: controller1.back.onPress { robot.drive.checkWheels() }.
| Setting | Is |
|---|---|
drive.maxPower |
scales every power down: 0.3 for the first tries (1.0) |
drive.nominalVoltage |
scales powers by this over the battery’s voltage, so the robot drives the same on a full battery as on a tired one: with 12 here, a power of 0.5 is 0.46 on 13 V. Path following stays tuned as the battery runs down. Full power stays full power. |
drive.strafeScale |
multiplies the sideways power, as mecanum wheels slip more sideways: raise it until a sideways push of the stick is as fast as a forward one (1.0) |
drive.leftFront.direction and the others |
each motor’s settings |
MecanumDrive(this, velocityControl = true) lets the hub hold each wheel’s speed with its encoder: powers then ask for
speeds, so even small ones move the robot, whatever the battery. It needs the drive encoders plugged in (see
Motors and servos). ticksPerTurn is only needed to read the
wheels’ positions or speeds: 384.5 for goBILDA 435 RPM motors, 537.7 for 312 RPM.
Driving with powers
Code drives an omni drivetrain with powers relative to the robot, each from -1.0 to 1.0:
drive.drive(forward = 0.5, right = 0.2, turn = 0.1) // turn is counterclockwise, like all angles
drive.stopMoving()
When that needs more than full power from a wheel, all powers are scaled down together: the robot moves the same way, only slower. Code that drives for a while takes the drivetrain over and stops it at the end:
suspend fun MyRobot.backOff() = drive.takeOver("back off") {
try {
drive.drive(forward = -0.3)
delay(500.milliseconds)
} finally {
drive.stopMoving()
}
}
To drive to places on the field, use path following.
Driving with the sticks
Robot-centric: the left stick moves the robot forward and sideways, the right stick turns it, clockwise when pushed right.
everyTick { robot.drive.driveWith(controller1.leftStick, controller1.rightStick.x) }
Field-centric: pushing the stick forward drives away from the drivers, whichever way the robot faces. It needs the robot’s heading from its localizer, and the way the drivers look:
val driversFace = Field2026.driversFace(alliance)
everyTick {
robot.drive.driveWith(controller1.leftStick, controller1.rightStick.x, robot.localizer.pose.heading, driversFace)
}
Field2026.driversFace(alliance) comes from the season’s field (see
Units and geometry). Without one, use the heading the robot had
when it pointed away from the drivers, like at the start of autonomous, and let a button set it again:
var driversFace = Heading.ZERO // the way the robot faced when its pose was zeroed
controller1.y.onPress { driversFace = robot.localizer.pose.heading } // the robot faces away from me now
Sticks and actions
When an action drives the robot, like driving to a target, the sticks leave it alone until one is pushed past halfway:
then the action stops, and the sticks drive. A thumb resting on a stick doesn’t stop it. The threshold is
driveWith(..., takeOverAbove = 0.5).
So TeleOp can have both: a button starts a drive to the basket, and the drivers take over any time.
Tank drives
A tank drive is a TankDrivetrain, driven with powers forward and turning:
drive.drive(forward = 0.5, turn = 0.2) // the left wheels get forward - turn, the right ones forward + turn
In TeleOp, with tank controls, each stick drives its side:
everyTick { robot.drive.driveWith(controller1.leftStick.y, controller1.rightStick.y) }
The platform doesn’t have an FTC tank drive class yet; one is a few lines:
class TankDrive(parent: Component) : TankDrivetrain(parent, "drive") {
val left = Motor(this, "left", direction = DcMotorSimple.Direction.REVERSE)
val right = Motor(this, "right")
override fun drive(forward: Double, turn: Double) {
val l = forward - turn
val r = forward + turn
val scale = maxOf(1.0, abs(l), abs(r)) // keep the direction when a side would need more than full power
left.power = l / scale
right.power = r / scale
}
}
Path following and field-centric driving need to move sideways, so they work with omni drivetrains only.