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.SimDevice;
008import edu.wpi.first.hal.SimDevice.Direction;
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/** Class for supporting continuous analog encoders, such as the US Digital MA3. */
017public class AnalogEncoder implements Sendable, AutoCloseable {
018  private final AnalogInput m_analogInput;
019  private AnalogTrigger m_analogTrigger;
020  private Counter m_counter;
021  private double m_positionOffset;
022  private double m_distancePerRotation = 1.0;
023  private double m_lastPosition;
024
025  protected SimDevice m_simDevice;
026  protected SimDouble m_simPosition;
027  protected SimDouble m_simAbsolutePosition;
028
029  /**
030   * Construct a new AnalogEncoder attached to a specific AnalogIn channel.
031   *
032   * @param channel the analog input channel to attach to
033   */
034  public AnalogEncoder(int channel) {
035    this(new AnalogInput(channel));
036  }
037
038  /**
039   * Construct a new AnalogEncoder attached to a specific AnalogInput.
040   *
041   * @param analogInput the analog input to attach to
042   */
043  @SuppressWarnings("this-escape")
044  public AnalogEncoder(AnalogInput analogInput) {
045    m_analogInput = analogInput;
046    init();
047  }
048
049  private void init() {
050    m_analogTrigger = new AnalogTrigger(m_analogInput);
051    m_counter = new Counter();
052
053    m_simDevice = SimDevice.create("AnalogEncoder", m_analogInput.getChannel());
054
055    if (m_simDevice != null) {
056      m_simPosition = m_simDevice.createDouble("Position", Direction.kInput, 0.0);
057      m_simAbsolutePosition = m_simDevice.createDouble("absPosition", Direction.kInput, 0.0);
058    }
059
060    // Limits need to be 25% from each end
061    m_analogTrigger.setLimitsVoltage(1.25, 3.75);
062    m_counter.setUpSource(m_analogTrigger, AnalogTriggerType.kRisingPulse);
063    m_counter.setDownSource(m_analogTrigger, AnalogTriggerType.kFallingPulse);
064
065    SendableRegistry.addLW(this, "Analog Encoder", m_analogInput.getChannel());
066  }
067
068  private boolean doubleEquals(double a, double b) {
069    double epsilon = 0.00001d;
070    return Math.abs(a - b) < epsilon;
071  }
072
073  /**
074   * Get the encoder value since the last reset.
075   *
076   * <p>This is reported in rotations since the last reset.
077   *
078   * @return the encoder value in rotations
079   */
080  public double get() {
081    if (m_simPosition != null) {
082      return m_simPosition.get();
083    }
084
085    // As the values are not atomic, keep trying until we get 2 reads of the same
086    // value. If we don't within 10 attempts, warn
087    for (int i = 0; i < 10; i++) {
088      double counter = m_counter.get();
089      double pos = m_analogInput.getVoltage();
090      double counter2 = m_counter.get();
091      double pos2 = m_analogInput.getVoltage();
092      if (counter == counter2 && doubleEquals(pos, pos2)) {
093        pos = pos / RobotController.getVoltage5V();
094        double position = counter + pos - m_positionOffset;
095        m_lastPosition = position;
096        return position;
097      }
098    }
099
100    DriverStation.reportWarning(
101        "Failed to read Analog Encoder. Potential Speed Overrun. Returning last value", false);
102    return m_lastPosition;
103  }
104
105  /**
106   * Get the absolute position of the analog encoder.
107   *
108   * <p>getAbsolutePosition() - getPositionOffset() will give an encoder absolute position relative
109   * to the last reset. This could potentially be negative, which needs to be accounted for.
110   *
111   * <p>This will not account for rollovers, and will always be just the raw absolute position.
112   *
113   * @return the absolute position
114   */
115  public double getAbsolutePosition() {
116    if (m_simAbsolutePosition != null) {
117      return m_simAbsolutePosition.get();
118    }
119
120    return m_analogInput.getVoltage() / RobotController.getVoltage5V();
121  }
122
123  /**
124   * Get the offset of position relative to the last reset.
125   *
126   * <p>getAbsolutePosition() - getPositionOffset() will give an encoder absolute position relative
127   * to the last reset. This could potentially be negative, which needs to be accounted for.
128   *
129   * @return the position offset
130   */
131  public double getPositionOffset() {
132    return m_positionOffset;
133  }
134
135  /**
136   * Set the position offset.
137   *
138   * <p>This must be in the range of 0-1.
139   *
140   * @param offset the offset
141   */
142  public void setPositionOffset(double offset) {
143    m_positionOffset = MathUtil.clamp(offset, 0.0, 1.0);
144  }
145
146  /**
147   * Set the distance per rotation of the encoder. This sets the multiplier used to determine the
148   * distance driven based on the rotation value from the encoder. Set this value based on how far
149   * the mechanism travels in 1 rotation of the encoder, and factor in gearing reductions following
150   * the encoder shaft. This distance can be in any units you like, linear or angular.
151   *
152   * @param distancePerRotation the distance per rotation of the encoder
153   */
154  public void setDistancePerRotation(double distancePerRotation) {
155    m_distancePerRotation = distancePerRotation;
156  }
157
158  /**
159   * Get the distance per rotation for this encoder.
160   *
161   * @return The scale factor that will be used to convert rotation to useful units.
162   */
163  public double getDistancePerRotation() {
164    return m_distancePerRotation;
165  }
166
167  /**
168   * Get the distance the sensor has driven since the last reset as scaled by the value from {@link
169   * #setDistancePerRotation(double)}.
170   *
171   * @return The distance driven since the last reset
172   */
173  public double getDistance() {
174    return get() * getDistancePerRotation();
175  }
176
177  /**
178   * Get the channel number.
179   *
180   * @return The channel number.
181   */
182  public int getChannel() {
183    return m_analogInput.getChannel();
184  }
185
186  /** Reset the Encoder distance to zero. */
187  public void reset() {
188    m_counter.reset();
189    m_positionOffset = m_analogInput.getVoltage() / RobotController.getVoltage5V();
190  }
191
192  @Override
193  public void close() {
194    m_counter.close();
195    m_analogTrigger.close();
196    if (m_simDevice != null) {
197      m_simDevice.close();
198    }
199  }
200
201  @Override
202  public void initSendable(SendableBuilder builder) {
203    builder.setSmartDashboardType("AbsoluteEncoder");
204    builder.addDoubleProperty("Distance", this::getDistance, null);
205    builder.addDoubleProperty("Distance Per Rotation", this::getDistancePerRotation, null);
206  }
207}