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;
014
015/**
016 * Class for supporting duty cycle/PWM encoders, such as the US Digital MA3 with PWM Output, the
017 * CTRE Mag Encoder, the Rev Hex Encoder, and the AM Mag Encoder.
018 */
019public class DutyCycleEncoder implements Sendable, AutoCloseable {
020  private final DutyCycle m_dutyCycle;
021  private boolean m_ownsDutyCycle;
022  private DigitalInput m_digitalInput;
023  private int m_frequencyThreshold = 100;
024  private double m_fullRange;
025  private double m_expectedZero;
026  private double m_periodNanos;
027  private double m_sensorMin;
028  private double m_sensorMax = 1.0;
029  private boolean m_isInverted;
030
031  private SimDevice m_simDevice;
032  private SimDouble m_simPosition;
033  private SimBoolean m_simIsConnected;
034
035  /**
036   * Construct a new DutyCycleEncoder on a specific channel.
037   *
038   * @param channel the channel to attach to
039   * @param fullRange the value to report at maximum travel
040   * @param expectedZero the reading where you would expect a 0 from get()
041   */
042  @SuppressWarnings("this-escape")
043  public DutyCycleEncoder(int channel, double fullRange, double expectedZero) {
044    m_digitalInput = new DigitalInput(channel);
045    m_ownsDutyCycle = true;
046    m_dutyCycle = new DutyCycle(m_digitalInput);
047    init(fullRange, expectedZero);
048  }
049
050  /**
051   * Construct a new DutyCycleEncoder attached to an existing DutyCycle object.
052   *
053   * @param dutyCycle the duty cycle to attach to
054   * @param fullRange the value to report at maximum travel
055   * @param expectedZero the reading where you would expect a 0 from get()
056   */
057  @SuppressWarnings("this-escape")
058  public DutyCycleEncoder(DutyCycle dutyCycle, double fullRange, double expectedZero) {
059    m_dutyCycle = dutyCycle;
060    init(fullRange, expectedZero);
061  }
062
063  /**
064   * Construct a new DutyCycleEncoder attached to a DigitalSource object.
065   *
066   * @param source the digital source to attach to
067   * @param fullRange the value to report at maximum travel
068   * @param expectedZero the reading where you would expect a 0 from get()
069   */
070  @SuppressWarnings("this-escape")
071  public DutyCycleEncoder(DigitalSource source, double fullRange, double expectedZero) {
072    m_ownsDutyCycle = true;
073    m_dutyCycle = new DutyCycle(source);
074    init(fullRange, expectedZero);
075  }
076
077  /**
078   * Construct a new DutyCycleEncoder on a specific channel.
079   *
080   * <p>This has a fullRange of 1 and an expectedZero of 0.
081   *
082   * @param channel the channel to attach to
083   */
084  @SuppressWarnings("this-escape")
085  public DutyCycleEncoder(int channel) {
086    this(channel, 1.0, 0.0);
087  }
088
089  /**
090   * Construct a new DutyCycleEncoder attached to an existing DutyCycle object.
091   *
092   * <p>This has a fullRange of 1 and an expectedZero of 0.
093   *
094   * @param dutyCycle the duty cycle to attach to
095   */
096  @SuppressWarnings("this-escape")
097  public DutyCycleEncoder(DutyCycle dutyCycle) {
098    this(dutyCycle, 1.0, 0.0);
099  }
100
101  /**
102   * Construct a new DutyCycleEncoder attached to a DigitalSource object.
103   *
104   * <p>This has a fullRange of 1 and an expectedZero of 0.
105   *
106   * @param source the digital source to attach to
107   */
108  @SuppressWarnings("this-escape")
109  public DutyCycleEncoder(DigitalSource source) {
110    this(source, 1.0, 0.0);
111  }
112
113  private void init(double fullRange, double expectedZero) {
114    m_simDevice = SimDevice.create("DutyCycle:DutyCycleEncoder", m_dutyCycle.getSourceChannel());
115
116    if (m_simDevice != null) {
117      m_simPosition = m_simDevice.createDouble("Position", SimDevice.Direction.kInput, 0.0);
118      m_simIsConnected = m_simDevice.createBoolean("Connected", SimDevice.Direction.kInput, true);
119    }
120
121    m_fullRange = fullRange;
122    m_expectedZero = expectedZero;
123
124    SendableRegistry.addLW(this, "DutyCycle Encoder", m_dutyCycle.getSourceChannel());
125  }
126
127  private double mapSensorRange(double pos) {
128    // map sensor range
129    if (pos < m_sensorMin) {
130      pos = m_sensorMin;
131    }
132    if (pos > m_sensorMax) {
133      pos = m_sensorMax;
134    }
135    pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
136    return pos;
137  }
138
139  /**
140   * Get the encoder value since the last reset.
141   *
142   * <p>This is reported in rotations since the last reset.
143   *
144   * @return the encoder value in rotations
145   */
146  public double get() {
147    if (m_simPosition != null) {
148      return m_simPosition.get();
149    }
150
151    double pos;
152    // Compute output percentage (0-1)
153    if (m_periodNanos == 0.0) {
154      pos = m_dutyCycle.getOutput();
155    } else {
156      int highTime = m_dutyCycle.getHighTimeNanoseconds();
157      pos = highTime / m_periodNanos;
158    }
159
160    // Map sensor range if range isn't full
161    pos = mapSensorRange(pos);
162
163    // Compute full range and offset
164    pos = pos * m_fullRange - m_expectedZero;
165
166    // Map from 0 - Full Range
167    double result = MathUtil.inputModulus(pos, 0, m_fullRange);
168    // Invert if necessary
169    if (m_isInverted) {
170      return m_fullRange - result;
171    }
172    return result;
173  }
174
175  /**
176   * Set the encoder duty cycle range. As the encoder needs to maintain a duty cycle, the duty cycle
177   * cannot go all the way to 0% or all the way to 100%. For example, an encoder with a 4096 us
178   * period might have a minimum duty cycle of 1 us / 4096 us and a maximum duty cycle of 4095 /
179   * 4096 us. Setting the range will result in an encoder duty cycle less than or equal to the
180   * minimum being output as 0 rotation, the duty cycle greater than or equal to the maximum being
181   * output as 1 rotation, and values in between linearly scaled from 0 to 1.
182   *
183   * @param min minimum duty cycle (0-1 range)
184   * @param max maximum duty cycle (0-1 range)
185   */
186  public void setDutyCycleRange(double min, double max) {
187    m_sensorMin = MathUtil.clamp(min, 0.0, 1.0);
188    m_sensorMax = MathUtil.clamp(max, 0.0, 1.0);
189  }
190
191  /**
192   * Get the frequency in Hz of the duty cycle signal from the encoder.
193   *
194   * @return duty cycle frequency in Hz
195   */
196  public int getFrequency() {
197    return m_dutyCycle.getFrequency();
198  }
199
200  /**
201   * Get if the sensor is connected
202   *
203   * <p>This uses the duty cycle frequency to determine if the sensor is connected. By default, a
204   * value of 100 Hz is used as the threshold, and this value can be changed with {@link
205   * #setConnectedFrequencyThreshold(int)}.
206   *
207   * @return true if the sensor is connected
208   */
209  public boolean isConnected() {
210    if (m_simIsConnected != null) {
211      return m_simIsConnected.get();
212    }
213    return getFrequency() > m_frequencyThreshold;
214  }
215
216  /**
217   * Change the frequency threshold for detecting connection used by {@link #isConnected()}.
218   *
219   * @param frequency the minimum frequency in Hz.
220   */
221  public void setConnectedFrequencyThreshold(int frequency) {
222    if (frequency < 0) {
223      frequency = 0;
224    }
225
226    m_frequencyThreshold = frequency;
227  }
228
229  /**
230   * Sets the assumed frequency of the connected device.
231   *
232   * <p>By default, the DutyCycle engine has to compute the frequency of the input signal. This can
233   * result in both delayed readings and jumpy readings. To solve this, you can pass the expected
234   * frequency of the sensor to this function. This will use that frequency to compute the DutyCycle
235   * percentage, rather than the computed frequency.
236   *
237   * @param frequency the assumed frequency of the sensor
238   */
239  public void setAssumedFrequency(double frequency) {
240    if (frequency == 0.0) {
241      m_periodNanos = 0.0;
242    } else {
243      m_periodNanos = 1000000000 / frequency;
244    }
245  }
246
247  /**
248   * Set if this encoder is inverted.
249   *
250   * @param inverted true to invert the encoder, false otherwise
251   */
252  public void setInverted(boolean inverted) {
253    m_isInverted = inverted;
254  }
255
256  @Override
257  public void close() {
258    if (m_ownsDutyCycle) {
259      m_dutyCycle.close();
260    }
261    if (m_digitalInput != null) {
262      m_digitalInput.close();
263    }
264    if (m_simDevice != null) {
265      m_simDevice.close();
266    }
267  }
268
269  /**
270   * Get the FPGA index for the DutyCycleEncoder.
271   *
272   * @return the FPGA index
273   */
274  public int getFPGAIndex() {
275    return m_dutyCycle.getFPGAIndex();
276  }
277
278  /**
279   * Get the channel of the source.
280   *
281   * @return the source channel
282   */
283  public int getSourceChannel() {
284    return m_dutyCycle.getSourceChannel();
285  }
286
287  @Override
288  public void initSendable(SendableBuilder builder) {
289    builder.setSmartDashboardType("AbsoluteEncoder");
290    builder.addDoubleProperty("Position", this::get, null);
291    builder.addBooleanProperty("Is Connected", this::isConnected, null);
292  }
293}