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.hardware.motor;
006
007import java.util.ArrayList;
008import org.wpilib.hardware.discrete.PWM;
009import org.wpilib.hardware.hal.SimDevice;
010import org.wpilib.hardware.hal.SimDevice.Direction;
011import org.wpilib.hardware.hal.SimDouble;
012import org.wpilib.system.RobotController;
013import org.wpilib.util.sendable.Sendable;
014import org.wpilib.util.sendable.SendableBuilder;
015import org.wpilib.util.sendable.SendableRegistry;
016
017/** Common base class for all PWM Motor Controllers. */
018@SuppressWarnings("removal")
019public abstract class PWMMotorController extends MotorSafety
020    implements MotorController, Sendable, AutoCloseable {
021  private boolean m_isInverted;
022  private final ArrayList<PWMMotorController> m_followers = new ArrayList<>();
023
024  /** PWM instances for motor controller. */
025  protected PWM m_pwm;
026
027  private SimDevice m_simDevice;
028  private SimDouble m_simThrottle;
029
030  private boolean m_eliminateDeadband;
031  private int m_minPwm;
032  private int m_deadbandMinPwm;
033  private int m_centerPwm;
034  private int m_deadbandMaxPwm;
035  private int m_maxPwm;
036
037  /**
038   * Constructor.
039   *
040   * @param name Name to use for SendableRegistry
041   * @param channel The SmartIO channel that the controller is attached to.
042   */
043  @SuppressWarnings("this-escape")
044  protected PWMMotorController(final String name, final int channel) {
045    m_pwm = new PWM(channel, false);
046    SendableRegistry.add(this, name, channel);
047
048    m_simDevice = SimDevice.create("PWMMotorController", channel);
049    if (m_simDevice != null) {
050      m_simThrottle = m_simDevice.createDouble("Throttle", Direction.OUTPUT, 0.0);
051      m_pwm.setSimDevice(m_simDevice);
052    }
053  }
054
055  /** Free the resource associated with the PWM channel and set the value to 0. */
056  @Override
057  public void close() {
058    SendableRegistry.remove(this);
059    m_pwm.close();
060
061    if (m_simDevice != null) {
062      m_simDevice.close();
063      m_simDevice = null;
064      m_simThrottle = null;
065    }
066  }
067
068  private int getMinPositivePwm() {
069    if (m_eliminateDeadband) {
070      return m_deadbandMaxPwm;
071    } else {
072      return m_centerPwm + 1;
073    }
074  }
075
076  private int getMaxNegativePwm() {
077    if (m_eliminateDeadband) {
078      return m_deadbandMinPwm;
079    } else {
080      return m_centerPwm - 1;
081    }
082  }
083
084  private int getPositiveScaleFactor() {
085    return m_maxPwm - getMinPositivePwm();
086  }
087
088  private int getNegativeScaleFactor() {
089    return getMaxNegativePwm() - m_minPwm;
090  }
091
092  /**
093   * Takes a duty cycle from -1 to 1 (the sign indicates direction), and outputs it in the
094   * microsecond format.
095   *
096   * @param dutyCycle The duty cycle between -1 and 1 (sign indicates direction).
097   */
098  protected final void setDutyCycleInternal(double dutyCycle) {
099    if (Double.isFinite(dutyCycle)) {
100      dutyCycle = Math.clamp(dutyCycle, -1.0, 1.0);
101    } else {
102      dutyCycle = 0.0;
103    }
104
105    if (m_simThrottle != null) {
106      m_simThrottle.set(dutyCycle);
107    }
108
109    int rawValue;
110    if (dutyCycle == 0.0) {
111      rawValue = m_centerPwm;
112    } else if (dutyCycle > 0.0) {
113      rawValue = (int) Math.round(dutyCycle * getPositiveScaleFactor()) + getMinPositivePwm();
114    } else {
115      rawValue = (int) Math.round(dutyCycle * getNegativeScaleFactor()) + getMaxNegativePwm();
116    }
117
118    m_pwm.setPulseTimeMicroseconds(rawValue);
119  }
120
121  /**
122   * Gets the duty cycle from -1 to 1 (the sign indicates direction), from the currently set pulse
123   * time.
124   *
125   * @return motor controller duty cycle
126   */
127  protected final double getDutyCycleInternal() {
128    int rawValue = m_pwm.getPulseTimeMicroseconds();
129
130    if (rawValue == 0) {
131      return 0.0;
132    } else if (rawValue > m_maxPwm) {
133      return 1.0;
134    } else if (rawValue < m_minPwm) {
135      return -1.0;
136    } else if (rawValue > getMinPositivePwm()) {
137      return (rawValue - getMinPositivePwm()) / (double) getPositiveScaleFactor();
138    } else if (rawValue < getMaxNegativePwm()) {
139      return (rawValue - getMaxNegativePwm()) / (double) getNegativeScaleFactor();
140    } else {
141      return 0.0;
142    }
143  }
144
145  /**
146   * Sets the bounds in microseconds for the controller.
147   *
148   * @param maxPwm maximum
149   * @param deadbandMaxPwm deadband max
150   * @param centerPwm center
151   * @param deadbandMinPwm deadband min
152   * @param minPwm minimum
153   */
154  protected final void setBoundsMicroseconds(
155      int maxPwm, int deadbandMaxPwm, int centerPwm, int deadbandMinPwm, int minPwm) {
156    m_maxPwm = maxPwm;
157    m_deadbandMaxPwm = deadbandMaxPwm;
158    m_centerPwm = centerPwm;
159    m_deadbandMinPwm = deadbandMinPwm;
160    m_minPwm = minPwm;
161  }
162
163  @Override
164  public void setThrottle(double throttle) {
165    if (m_isInverted) {
166      throttle = -throttle;
167    }
168    setDutyCycleInternal(throttle);
169
170    for (var follower : m_followers) {
171      follower.setThrottle(throttle);
172    }
173
174    feed();
175  }
176
177  @Override
178  public double getThrottle() {
179    return getDutyCycleInternal() * (m_isInverted ? -1.0 : 1.0);
180  }
181
182  /**
183   * Gets the voltage output of the motor controller, nominally between -12 V and 12 V.
184   *
185   * @return The voltage of the motor controller, nominally between -12 V and 12 V.
186   */
187  public double getVoltage() {
188    return getThrottle() * RobotController.getBatteryVoltage();
189  }
190
191  @Override
192  public void setInverted(boolean isInverted) {
193    m_isInverted = isInverted;
194  }
195
196  @Override
197  public boolean getInverted() {
198    return m_isInverted;
199  }
200
201  @Override
202  public void disable() {
203    m_pwm.setDisabled();
204
205    if (m_simThrottle != null) {
206      m_simThrottle.set(0.0);
207    }
208
209    for (var follower : m_followers) {
210      follower.disable();
211    }
212  }
213
214  @Override
215  public void stopMotor() {
216    disable();
217  }
218
219  @Override
220  public String getDescription() {
221    return "PWM " + getChannel();
222  }
223
224  /**
225   * Gets the backing PWM handle.
226   *
227   * @return The pwm handle.
228   */
229  public int getPwmHandle() {
230    return m_pwm.getHandle();
231  }
232
233  /**
234   * Gets the PWM channel number.
235   *
236   * @return The channel number.
237   */
238  public int getChannel() {
239    return m_pwm.getChannel();
240  }
241
242  /**
243   * Optionally eliminate the deadband from a motor controller.
244   *
245   * @param eliminateDeadband If true, set the motor curve for the motor controller to eliminate the
246   *     deadband in the middle of the range. Otherwise, keep the full range without modifying any
247   *     values.
248   */
249  public void enableDeadbandElimination(boolean eliminateDeadband) {
250    m_eliminateDeadband = eliminateDeadband;
251  }
252
253  /**
254   * Make the given PWM motor controller follow the output of this one.
255   *
256   * @param follower The motor controller follower.
257   */
258  public void addFollower(PWMMotorController follower) {
259    m_followers.add(follower);
260  }
261
262  @Override
263  public void initSendable(SendableBuilder builder) {
264    builder.setSmartDashboardType("Motor Controller");
265    builder.setActuator(true);
266    builder.addDoubleProperty("Value", this::getThrottle, this::setThrottle);
267  }
268}