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