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;
014
015/** Class for supporting continuous analog encoders, such as the US Digital MA3. */
016public class AnalogEncoder implements Sendable, AutoCloseable {
017  private final AnalogInput m_analogInput;
018  private boolean m_ownsAnalogInput;
019  private double m_fullRange;
020  private double m_expectedZero;
021  private double m_sensorMin;
022  private double m_sensorMax = 1.0;
023  private boolean m_isInverted;
024
025  private SimDevice m_simDevice;
026  private SimDouble m_simPosition;
027
028  /**
029   * Construct a new AnalogEncoder attached to a specific AnalogIn channel.
030   *
031   * @param channel the analog input channel to attach to
032   * @param fullRange the value to report at maximum travel
033   * @param expectedZero the reading where you would expect a 0 from get()
034   */
035  public AnalogEncoder(int channel, double fullRange, double expectedZero) {
036    this(new AnalogInput(channel), fullRange, expectedZero);
037    m_ownsAnalogInput = true;
038  }
039
040  /**
041   * Construct a new AnalogEncoder attached to a specific AnalogInput.
042   *
043   * @param analogInput the analog input to attach to
044   * @param fullRange the value to report at maximum travel
045   * @param expectedZero the reading where you would expect a 0 from get()
046   */
047  @SuppressWarnings("this-escape")
048  public AnalogEncoder(AnalogInput analogInput, double fullRange, double expectedZero) {
049    m_analogInput = analogInput;
050    init(fullRange, expectedZero);
051  }
052
053  /**
054   * Construct a new AnalogEncoder attached to a specific AnalogIn channel.
055   *
056   * <p>This has a fullRange of 1 and an expectedZero of 0.
057   *
058   * @param channel the analog input channel to attach to
059   */
060  public AnalogEncoder(int channel) {
061    this(channel, 1.0, 0.0);
062  }
063
064  /**
065   * Construct a new AnalogEncoder attached to a specific AnalogInput.
066   *
067   * <p>This has a fullRange of 1 and an expectedZero of 0.
068   *
069   * @param analogInput the analog input to attach to
070   */
071  @SuppressWarnings("this-escape")
072  public AnalogEncoder(AnalogInput analogInput) {
073    this(analogInput, 1.0, 0.0);
074  }
075
076  private void init(double fullRange, double expectedZero) {
077    m_simDevice = SimDevice.create("AnalogEncoder", m_analogInput.getChannel());
078
079    if (m_simDevice != null) {
080      m_simPosition = m_simDevice.createDouble("Position", Direction.kInput, 0.0);
081    }
082
083    m_fullRange = fullRange;
084    m_expectedZero = expectedZero;
085
086    SendableRegistry.addLW(this, "Analog Encoder", m_analogInput.getChannel());
087  }
088
089  private double mapSensorRange(double pos) {
090    // map sensor range
091    if (pos < m_sensorMin) {
092      pos = m_sensorMin;
093    }
094    if (pos > m_sensorMax) {
095      pos = m_sensorMax;
096    }
097    pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
098    return pos;
099  }
100
101  /**
102   * Get the encoder value.
103   *
104   * @return the encoder value scaled by the full range input
105   */
106  public double get() {
107    if (m_simPosition != null) {
108      return m_simPosition.get();
109    }
110
111    double analog = m_analogInput.getVoltage();
112    double pos = analog / RobotController.getVoltage5V();
113
114    // Map sensor range if range isn't full
115    pos = mapSensorRange(pos);
116
117    // Compute full range and offset
118    pos = pos * m_fullRange - m_expectedZero;
119
120    // Map from 0 - Full Range
121    double result = MathUtil.inputModulus(pos, 0, m_fullRange);
122    // Invert if necessary
123    if (m_isInverted) {
124      return m_fullRange - result;
125    }
126    return result;
127  }
128
129  /**
130   * Set the encoder voltage percentage range. Analog sensors are not always fully stable at the end
131   * of their travel ranges. Shrinking this range down can help mitigate issues with that.
132   *
133   * @param min minimum voltage percentage (0-1 range)
134   * @param max maximum voltage percentage (0-1 range)
135   */
136  public void setVoltagePercentageRange(double min, double max) {
137    m_sensorMin = MathUtil.clamp(min, 0.0, 1.0);
138    m_sensorMax = MathUtil.clamp(max, 0.0, 1.0);
139  }
140
141  /**
142   * Set if this encoder is inverted.
143   *
144   * @param inverted true to invert the encoder, false otherwise
145   */
146  public void setInverted(boolean inverted) {
147    m_isInverted = inverted;
148  }
149
150  /**
151   * Get the channel number.
152   *
153   * @return The channel number.
154   */
155  public int getChannel() {
156    return m_analogInput.getChannel();
157  }
158
159  @Override
160  public void close() {
161    if (m_ownsAnalogInput) {
162      m_analogInput.close();
163    }
164    if (m_simDevice != null) {
165      m_simDevice.close();
166    }
167  }
168
169  @Override
170  public void initSendable(SendableBuilder builder) {
171    builder.setSmartDashboardType("AbsoluteEncoder");
172    builder.addDoubleProperty("Position", this::get, null);
173  }
174}