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;
006
007import edu.wpi.first.hal.SimBoolean;
008import edu.wpi.first.hal.SimDevice;
009import edu.wpi.first.hal.SimDouble;
010import edu.wpi.first.math.MathUtil;
011import edu.wpi.first.util.sendable.Sendable;
012import edu.wpi.first.util.sendable.SendableBuilder;
013import edu.wpi.first.util.sendable.SendableRegistry;
014import edu.wpi.first.wpilibj.AnalogTriggerOutput.AnalogTriggerType;
015
016/**
017 * Class for supporting duty cycle/PWM encoders, such as the US Digital MA3 with PWM Output, the
018 * CTRE Mag Encoder, the Rev Hex Encoder, and the AM Mag Encoder.
019 */
020public class DutyCycleEncoder implements Sendable, AutoCloseable {
021  private final DutyCycle m_dutyCycle;
022  private boolean m_ownsDutyCycle;
023  private DigitalInput m_digitalInput;
024  private AnalogTrigger m_analogTrigger;
025  private Counter m_counter;
026  private int m_frequencyThreshold = 100;
027  private double m_positionOffset;
028  private double m_distancePerRotation = 1.0;
029  private double m_lastPosition;
030  private double m_sensorMin;
031  private double m_sensorMax = 1.0;
032
033  protected SimDevice m_simDevice;
034  protected SimDouble m_simPosition;
035  protected SimDouble m_simAbsolutePosition;
036  protected SimDouble m_simDistancePerRotation;
037  protected SimBoolean m_simIsConnected;
038
039  /**
040   * Construct a new DutyCycleEncoder on a specific channel.
041   *
042   * @param channel the channel to attach to
043   */
044  public DutyCycleEncoder(int channel) {
045    m_digitalInput = new DigitalInput(channel);
046    m_ownsDutyCycle = true;
047    m_dutyCycle = new DutyCycle(m_digitalInput);
048    init();
049  }
050
051  /**
052   * Construct a new DutyCycleEncoder attached to an existing DutyCycle object.
053   *
054   * @param dutyCycle the duty cycle to attach to
055   */
056  public DutyCycleEncoder(DutyCycle dutyCycle) {
057    m_dutyCycle = dutyCycle;
058    init();
059  }
060
061  /**
062   * Construct a new DutyCycleEncoder attached to a DigitalSource object.
063   *
064   * @param source the digital source to attach to
065   */
066  public DutyCycleEncoder(DigitalSource source) {
067    m_ownsDutyCycle = true;
068    m_dutyCycle = new DutyCycle(source);
069    init();
070  }
071
072  private void init() {
073    m_simDevice = SimDevice.create("DutyCycle:DutyCycleEncoder", m_dutyCycle.getSourceChannel());
074
075    if (m_simDevice != null) {
076      m_simPosition = m_simDevice.createDouble("position", SimDevice.Direction.kInput, 0.0);
077      m_simDistancePerRotation =
078          m_simDevice.createDouble("distance_per_rot", SimDevice.Direction.kOutput, 1.0);
079      m_simAbsolutePosition =
080          m_simDevice.createDouble("absPosition", SimDevice.Direction.kInput, 0.0);
081      m_simIsConnected = m_simDevice.createBoolean("connected", SimDevice.Direction.kInput, true);
082    } else {
083      m_counter = new Counter();
084      m_analogTrigger = new AnalogTrigger(m_dutyCycle);
085      m_analogTrigger.setLimitsDutyCycle(0.25, 0.75);
086      m_counter.setUpSource(m_analogTrigger, AnalogTriggerType.kRisingPulse);
087      m_counter.setDownSource(m_analogTrigger, AnalogTriggerType.kFallingPulse);
088    }
089
090    SendableRegistry.addLW(this, "DutyCycle Encoder", m_dutyCycle.getSourceChannel());
091  }
092
093  private double mapSensorRange(double pos) {
094    // map sensor range
095    if (pos < m_sensorMin) {
096      pos = m_sensorMin;
097    }
098    if (pos > m_sensorMax) {
099      pos = m_sensorMax;
100    }
101    pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
102    return pos;
103  }
104
105  private boolean doubleEquals(double a, double b) {
106    double epsilon = 0.00001d;
107    return Math.abs(a - b) < epsilon;
108  }
109
110  /**
111   * Get the encoder value since the last reset.
112   *
113   * <p>This is reported in rotations since the last reset.
114   *
115   * @return the encoder value in rotations
116   */
117  public double get() {
118    if (m_simPosition != null) {
119      return m_simPosition.get();
120    }
121
122    // As the values are not atomic, keep trying until we get 2 reads of the same
123    // value
124    // If we don't within 10 attempts, error
125    for (int i = 0; i < 10; i++) {
126      double counter = m_counter.get();
127      double pos = m_dutyCycle.getOutput();
128      double counter2 = m_counter.get();
129      double pos2 = m_dutyCycle.getOutput();
130      if (counter == counter2 && doubleEquals(pos, pos2)) {
131        // map sensor range
132        pos = mapSensorRange(pos);
133        double position = counter + pos - m_positionOffset;
134        m_lastPosition = position;
135        return position;
136      }
137    }
138
139    DriverStation.reportWarning(
140        "Failed to read Analog Encoder. Potential Speed Overrun. Returning last value", false);
141    return m_lastPosition;
142  }
143
144  /**
145   * Get the absolute position of the duty cycle encoder.
146   *
147   * <p>getAbsolutePosition() - getPositionOffset() will give an encoder absolute position relative
148   * to the last reset. This could potentially be negative, which needs to be accounted for.
149   *
150   * <p>This will not account for rollovers, and will always be just the raw absolute position.
151   *
152   * @return the absolute position
153   */
154  public double getAbsolutePosition() {
155    if (m_simAbsolutePosition != null) {
156      return m_simAbsolutePosition.get();
157    }
158
159    return mapSensorRange(m_dutyCycle.getOutput());
160  }
161
162  /**
163   * Get the offset of position relative to the last reset.
164   *
165   * <p>getAbsolutePosition() - getPositionOffset() will give an encoder absolute position relative
166   * to the last reset. This could potentially be negative, which needs to be accounted for.
167   *
168   * @return the position offset
169   */
170  public double getPositionOffset() {
171    return m_positionOffset;
172  }
173
174  /**
175   * Set the position offset.
176   *
177   * <p>This must be in the range of 0-1.
178   *
179   * @param offset the offset
180   */
181  public void setPositionOffset(double offset) {
182    m_positionOffset = MathUtil.clamp(offset, 0.0, 1.0);
183  }
184
185  /**
186   * Set the encoder duty cycle range. As the encoder needs to maintain a duty cycle, the duty cycle
187   * cannot go all the way to 0% or all the way to 100%. For example, an encoder with a 4096 us
188   * period might have a minimum duty cycle of 1 us / 4096 us and a maximum duty cycle of 4095 /
189   * 4096 us. Setting the range will result in an encoder duty cycle less than or equal to the
190   * minimum being output as 0 rotation, the duty cycle greater than or equal to the maximum being
191   * output as 1 rotation, and values in between linearly scaled from 0 to 1.
192   *
193   * @param min minimum duty cycle (0-1 range)
194   * @param max maximum duty cycle (0-1 range)
195   */
196  public void setDutyCycleRange(double min, double max) {
197    m_sensorMin = MathUtil.clamp(min, 0.0, 1.0);
198    m_sensorMax = MathUtil.clamp(max, 0.0, 1.0);
199  }
200
201  /**
202   * Set the distance per rotation of the encoder. This sets the multiplier used to determine the
203   * distance driven based on the rotation value from the encoder. Set this value based on how far
204   * the mechanism travels in 1 rotation of the encoder, and factor in gearing reductions following
205   * the encoder shaft. This distance can be in any units you like, linear or angular.
206   *
207   * @param distancePerRotation the distance per rotation of the encoder
208   */
209  public void setDistancePerRotation(double distancePerRotation) {
210    m_distancePerRotation = distancePerRotation;
211    if (m_simDistancePerRotation != null) {
212      m_simDistancePerRotation.set(distancePerRotation);
213    }
214  }
215
216  /**
217   * Get the distance per rotation for this encoder.
218   *
219   * @return The scale factor that will be used to convert rotation to useful units.
220   */
221  public double getDistancePerRotation() {
222    return m_distancePerRotation;
223  }
224
225  /**
226   * Get the distance the sensor has driven since the last reset as scaled by the value from {@link
227   * #setDistancePerRotation(double)}.
228   *
229   * @return The distance driven since the last reset
230   */
231  public double getDistance() {
232    return get() * getDistancePerRotation();
233  }
234
235  /**
236   * Get the frequency in Hz of the duty cycle signal from the encoder.
237   *
238   * @return duty cycle frequency in Hz
239   */
240  public int getFrequency() {
241    return m_dutyCycle.getFrequency();
242  }
243
244  /** Reset the Encoder distance to zero. */
245  public void reset() {
246    if (m_counter != null) {
247      m_counter.reset();
248    }
249    m_positionOffset = m_dutyCycle.getOutput();
250  }
251
252  /**
253   * Get if the sensor is connected
254   *
255   * <p>This uses the duty cycle frequency to determine if the sensor is connected. By default, a
256   * value of 100 Hz is used as the threshold, and this value can be changed with {@link
257   * #setConnectedFrequencyThreshold(int)}.
258   *
259   * @return true if the sensor is connected
260   */
261  public boolean isConnected() {
262    if (m_simIsConnected != null) {
263      return m_simIsConnected.get();
264    }
265    return getFrequency() > m_frequencyThreshold;
266  }
267
268  /**
269   * Change the frequency threshold for detecting connection used by {@link #isConnected()}.
270   *
271   * @param frequency the minimum frequency in Hz.
272   */
273  public void setConnectedFrequencyThreshold(int frequency) {
274    if (frequency < 0) {
275      frequency = 0;
276    }
277
278    m_frequencyThreshold = frequency;
279  }
280
281  @Override
282  public void close() {
283    if (m_counter != null) {
284      m_counter.close();
285    }
286    if (m_analogTrigger != null) {
287      m_analogTrigger.close();
288    }
289    if (m_ownsDutyCycle) {
290      m_dutyCycle.close();
291    }
292    if (m_digitalInput != null) {
293      m_digitalInput.close();
294    }
295    if (m_simDevice != null) {
296      m_simDevice.close();
297    }
298  }
299
300  /**
301   * Get the FPGA index for the DutyCycleEncoder.
302   *
303   * @return the FPGA index
304   */
305  public int getFPGAIndex() {
306    return m_dutyCycle.getFPGAIndex();
307  }
308
309  /**
310   * Get the channel of the source.
311   *
312   * @return the source channel
313   */
314  public int getSourceChannel() {
315    return m_dutyCycle.getSourceChannel();
316  }
317
318  @Override
319  public void initSendable(SendableBuilder builder) {
320    builder.setSmartDashboardType("AbsoluteEncoder");
321    builder.addDoubleProperty("Distance", this::getDistance, null);
322    builder.addDoubleProperty("Distance Per Rotation", this::getDistancePerRotation, null);
323    builder.addBooleanProperty("Is Connected", this::isConnected, null);
324  }
325}