001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.drive;
006
007import static org.wpilib.util.ErrorMessages.requireNonNullParam;
008
009import java.util.function.DoubleConsumer;
010import org.wpilib.hardware.hal.HAL;
011import org.wpilib.hardware.motor.MotorController;
012import org.wpilib.math.util.MathUtil;
013import org.wpilib.util.sendable.Sendable;
014import org.wpilib.util.sendable.SendableBuilder;
015import org.wpilib.util.sendable.SendableRegistry;
016
017/**
018 * A class for driving differential drive/skid-steer drive platforms such as the Kit of Parts drive
019 * base, "tank drive", or West Coast Drive.
020 *
021 * <p>These drive bases typically have drop-center / skid-steer with two or more wheels per side
022 * (e.g., 6WD or 8WD). This class takes a setter per side. For four and six motor drivetrains, use
023 * CAN motor controller followers or {@link
024 * org.wpilib.hardware.motor.PWMMotorController#addFollower(PWMMotorController)}.
025 *
026 * <p>A differential drive robot has left and right wheels separated by an arbitrary width.
027 *
028 * <p>Drive base diagram:
029 *
030 * <pre>
031 * |_______|
032 * | |   | |
033 *   |   |
034 * |_|___|_|
035 * |       |
036 * </pre>
037 *
038 * <p>Each drive function provides different inverse kinematic relations for a differential drive
039 * robot.
040 *
041 * <p>This library uses the NWU axes convention (North-West-Up as external reference in the world
042 * frame). The positive X axis points ahead, the positive Y axis points to the left, and the
043 * positive Z axis points up. Rotations follow the right-hand rule, so counterclockwise rotation
044 * around the Z axis is positive.
045 *
046 * <p>Inputs smaller then {@value org.wpilib.drive.RobotDriveBase#kDefaultDeadband} will be set to
047 * 0, and larger values will be scaled so that the full range is still used. This deadband value can
048 * be changed with {@link #setDeadband}.
049 *
050 * <p>{@link org.wpilib.hardware.motor.MotorSafety} is enabled by default. The tankDrive,
051 * arcadeDrive, or curvatureDrive methods should be called periodically to avoid Motor Safety
052 * timeouts.
053 */
054public class DifferentialDrive extends RobotDriveBase implements Sendable, AutoCloseable {
055  private static int instances;
056
057  private final DoubleConsumer m_leftMotor;
058  private final DoubleConsumer m_rightMotor;
059
060  // Used for Sendable property getters
061  private double m_leftOutput;
062  private double m_rightOutput;
063
064  private boolean m_reported;
065
066  /**
067   * Wheel velocities for a differential drive.
068   *
069   * <p>Uses normalized voltage [-1.0..1.0].
070   */
071  @SuppressWarnings("MemberName")
072  public static class WheelVelocities {
073    /** Left wheel velocity. */
074    public double left;
075
076    /** Right wheel velocity. */
077    public double right;
078
079    /** Constructs a WheelVelocities with zeroes for left and right velocities. */
080    public WheelVelocities() {}
081
082    /**
083     * Constructs a WheelVelocities.
084     *
085     * @param left The left velocity [-1.0..1.0].
086     * @param right The right velocity [-1.0..1.0].
087     */
088    public WheelVelocities(double left, double right) {
089      this.left = left;
090      this.right = right;
091    }
092  }
093
094  /**
095   * Construct a DifferentialDrive.
096   *
097   * <p>To pass multiple motors per side, use CAN motor controller followers or {@link
098   * org.wpilib.hardware.motor.PWMMotorController#addFollower(PWMMotorController)}. If a motor needs
099   * to be inverted, do so before passing it in.
100   *
101   * @param leftMotor Left motor.
102   * @param rightMotor Right motor.
103   */
104  @SuppressWarnings({"removal", "this-escape"})
105  public DifferentialDrive(MotorController leftMotor, MotorController rightMotor) {
106    this(
107        (double output) -> leftMotor.setDutyCycle(output),
108        (double output) -> rightMotor.setDutyCycle(output));
109    SendableRegistry.addChild(this, leftMotor);
110    SendableRegistry.addChild(this, rightMotor);
111  }
112
113  /**
114   * Construct a DifferentialDrive.
115   *
116   * <p>To pass multiple motors per side, use CAN motor controller followers or {@link
117   * org.wpilib.hardware.motor.PWMMotorController#addFollower(PWMMotorController)}. If a motor needs
118   * to be inverted, do so before passing it in.
119   *
120   * @param leftMotor Left motor setter.
121   * @param rightMotor Right motor setter.
122   */
123  @SuppressWarnings("this-escape")
124  public DifferentialDrive(DoubleConsumer leftMotor, DoubleConsumer rightMotor) {
125    requireNonNullParam(leftMotor, "leftMotor", "DifferentialDrive");
126    requireNonNullParam(rightMotor, "rightMotor", "DifferentialDrive");
127
128    m_leftMotor = leftMotor;
129    m_rightMotor = rightMotor;
130    instances++;
131    SendableRegistry.add(this, "DifferentialDrive", instances);
132  }
133
134  @Override
135  public void close() {
136    SendableRegistry.remove(this);
137  }
138
139  /**
140   * Arcade drive method for differential drive platform. The calculated values will be squared to
141   * decrease sensitivity at low velocities.
142   *
143   * @param xVelocity The robot's velocity along the X axis [-1.0..1.0]. Forward is positive.
144   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Counterclockwise is
145   *     positive.
146   */
147  public void arcadeDrive(double xVelocity, double zRotation) {
148    arcadeDrive(xVelocity, zRotation, true);
149  }
150
151  /**
152   * Arcade drive method for differential drive platform.
153   *
154   * @param xVelocity The robot's velocity along the X axis [-1.0..1.0]. Forward is positive.
155   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Counterclockwise is
156   *     positive.
157   * @param squareInputs If set, decreases the input sensitivity at low velocities.
158   */
159  public void arcadeDrive(double xVelocity, double zRotation, boolean squareInputs) {
160    if (!m_reported) {
161      HAL.reportUsage("RobotDrive", "DifferentialArcade");
162      m_reported = true;
163    }
164
165    xVelocity = MathUtil.applyDeadband(xVelocity, m_deadband);
166    zRotation = MathUtil.applyDeadband(zRotation, m_deadband);
167
168    var velocities = arcadeDriveIK(xVelocity, zRotation, squareInputs);
169
170    m_leftOutput = velocities.left * m_maxOutput;
171    m_rightOutput = velocities.right * m_maxOutput;
172
173    m_leftMotor.accept(m_leftOutput);
174    m_rightMotor.accept(m_rightOutput);
175
176    feed();
177  }
178
179  /**
180   * Curvature drive method for differential drive platform.
181   *
182   * <p>The rotation argument controls the curvature of the robot's path rather than its rate of
183   * heading change. This makes the robot more controllable at high velocities.
184   *
185   * @param xVelocity The robot's velocity along the X axis [-1.0..1.0]. Forward is positive.
186   * @param zRotation The normalized curvature [-1.0..1.0]. Counterclockwise is positive.
187   * @param allowTurnInPlace If set, overrides constant-curvature turning for turn-in-place
188   *     maneuvers. zRotation will control turning rate instead of curvature.
189   */
190  public void curvatureDrive(double xVelocity, double zRotation, boolean allowTurnInPlace) {
191    if (!m_reported) {
192      HAL.reportUsage("RobotDrive", "DifferentialCurvature");
193      m_reported = true;
194    }
195
196    xVelocity = MathUtil.applyDeadband(xVelocity, m_deadband);
197    zRotation = MathUtil.applyDeadband(zRotation, m_deadband);
198
199    var velocities = curvatureDriveIK(xVelocity, zRotation, allowTurnInPlace);
200
201    m_leftOutput = velocities.left * m_maxOutput;
202    m_rightOutput = velocities.right * m_maxOutput;
203
204    m_leftMotor.accept(m_leftOutput);
205    m_rightMotor.accept(m_rightOutput);
206
207    feed();
208  }
209
210  /**
211   * Tank drive method for differential drive platform. The calculated values will be squared to
212   * decrease sensitivity at low velocities.
213   *
214   * @param leftVelocity The robot's left side velocity along the X axis [-1.0..1.0]. Forward is
215   *     positive.
216   * @param rightVelocity The robot's right side velocity along the X axis [-1.0..1.0]. Forward is
217   *     positive.
218   */
219  public void tankDrive(double leftVelocity, double rightVelocity) {
220    tankDrive(leftVelocity, rightVelocity, true);
221  }
222
223  /**
224   * Tank drive method for differential drive platform.
225   *
226   * @param leftVelocity The robot left side's velocity along the X axis [-1.0..1.0]. Forward is
227   *     positive.
228   * @param rightVelocity The robot right side's velocity along the X axis [-1.0..1.0]. Forward is
229   *     positive.
230   * @param squareInputs If set, decreases the input sensitivity at low velocities.
231   */
232  public void tankDrive(double leftVelocity, double rightVelocity, boolean squareInputs) {
233    if (!m_reported) {
234      HAL.reportUsage("RobotDrive", "DifferentialTank");
235      m_reported = true;
236    }
237
238    leftVelocity = MathUtil.applyDeadband(leftVelocity, m_deadband);
239    rightVelocity = MathUtil.applyDeadband(rightVelocity, m_deadband);
240
241    var velocities = tankDriveIK(leftVelocity, rightVelocity, squareInputs);
242
243    m_leftOutput = velocities.left * m_maxOutput;
244    m_rightOutput = velocities.right * m_maxOutput;
245
246    m_leftMotor.accept(m_leftOutput);
247    m_rightMotor.accept(m_rightOutput);
248
249    feed();
250  }
251
252  /**
253   * Arcade drive inverse kinematics for differential drive platform.
254   *
255   * @param xVelocity The robot's velocity along the X axis [-1.0..1.0]. Forward is positive.
256   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Counterclockwise is
257   *     positive.
258   * @param squareInputs If set, decreases the input sensitivity at low velocities.
259   * @return Wheel velocities [-1.0..1.0].
260   */
261  public static WheelVelocities arcadeDriveIK(
262      double xVelocity, double zRotation, boolean squareInputs) {
263    xVelocity = Math.clamp(xVelocity, -1.0, 1.0);
264    zRotation = Math.clamp(zRotation, -1.0, 1.0);
265
266    // Square the inputs (while preserving the sign) to increase fine control
267    // while permitting full power.
268    if (squareInputs) {
269      xVelocity = MathUtil.copyDirectionPow(xVelocity, 2);
270      zRotation = MathUtil.copyDirectionPow(zRotation, 2);
271    }
272
273    double leftVelocity = xVelocity - zRotation;
274    double rightVelocity = xVelocity + zRotation;
275
276    // Find the maximum possible value of (throttle + turn) along the vector
277    // that the joystick is pointing, then desaturate the wheel velocities
278    double greaterInput = Math.max(Math.abs(xVelocity), Math.abs(zRotation));
279    double lesserInput = Math.min(Math.abs(xVelocity), Math.abs(zRotation));
280    if (greaterInput == 0.0) {
281      return new WheelVelocities(0.0, 0.0);
282    }
283    double saturatedInput = (greaterInput + lesserInput) / greaterInput;
284    leftVelocity /= saturatedInput;
285    rightVelocity /= saturatedInput;
286
287    return new WheelVelocities(leftVelocity, rightVelocity);
288  }
289
290  /**
291   * Curvature drive inverse kinematics for differential drive platform.
292   *
293   * <p>The rotation argument controls the curvature of the robot's path rather than its rate of
294   * heading change. This makes the robot more controllable at high velocities.
295   *
296   * @param xVelocity The robot's velocity along the X axis [-1.0..1.0]. Forward is positive.
297   * @param zRotation The normalized curvature [-1.0..1.0]. Counterclockwise is positive.
298   * @param allowTurnInPlace If set, overrides constant-curvature turning for turn-in-place
299   *     maneuvers. zRotation will control rotation rate around the Z axis instead of curvature.
300   * @return Wheel velocities [-1.0..1.0].
301   */
302  public static WheelVelocities curvatureDriveIK(
303      double xVelocity, double zRotation, boolean allowTurnInPlace) {
304    xVelocity = Math.clamp(xVelocity, -1.0, 1.0);
305    zRotation = Math.clamp(zRotation, -1.0, 1.0);
306
307    double leftVelocity;
308    double rightVelocity;
309
310    if (allowTurnInPlace) {
311      leftVelocity = xVelocity - zRotation;
312      rightVelocity = xVelocity + zRotation;
313    } else {
314      leftVelocity = xVelocity - Math.abs(xVelocity) * zRotation;
315      rightVelocity = xVelocity + Math.abs(xVelocity) * zRotation;
316    }
317
318    // Desaturate wheel velocities
319    double maxMagnitude = Math.max(Math.abs(leftVelocity), Math.abs(rightVelocity));
320    if (maxMagnitude > 1.0) {
321      leftVelocity /= maxMagnitude;
322      rightVelocity /= maxMagnitude;
323    }
324
325    return new WheelVelocities(leftVelocity, rightVelocity);
326  }
327
328  /**
329   * Tank drive inverse kinematics for differential drive platform.
330   *
331   * @param leftVelocity The robot left side's velocity along the X axis [-1.0..1.0]. Forward is
332   *     positive.
333   * @param rightVelocity The robot right side's velocity along the X axis [-1.0..1.0]. Forward is
334   *     positive.
335   * @param squareInputs If set, decreases the input sensitivity at low velocities.
336   * @return Wheel velocities [-1.0..1.0].
337   */
338  public static WheelVelocities tankDriveIK(
339      double leftVelocity, double rightVelocity, boolean squareInputs) {
340    leftVelocity = Math.clamp(leftVelocity, -1.0, 1.0);
341    rightVelocity = Math.clamp(rightVelocity, -1.0, 1.0);
342
343    // Square the inputs (while preserving the sign) to increase fine control
344    // while permitting full power.
345    if (squareInputs) {
346      leftVelocity = MathUtil.copyDirectionPow(leftVelocity, 2);
347      rightVelocity = MathUtil.copyDirectionPow(rightVelocity, 2);
348    }
349
350    return new WheelVelocities(leftVelocity, rightVelocity);
351  }
352
353  @Override
354  public void stopMotor() {
355    m_leftOutput = 0.0;
356    m_rightOutput = 0.0;
357
358    m_leftMotor.accept(0.0);
359    m_rightMotor.accept(0.0);
360
361    feed();
362  }
363
364  @Override
365  public String getDescription() {
366    return "DifferentialDrive";
367  }
368
369  @Override
370  public void initSendable(SendableBuilder builder) {
371    builder.setSmartDashboardType("DifferentialDrive");
372    builder.setActuator(true);
373    builder.addDoubleProperty("Left Motor Velocity", () -> m_leftOutput, m_leftMotor);
374    builder.addDoubleProperty("Right Motor Velocity", () -> m_rightOutput, m_rightMotor);
375  }
376}