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}