001/*----------------------------------------------------------------------------*/ 002/* Copyright (c) 2008-2018 FIRST. All Rights Reserved. */ 003/* Open Source Software - may be modified and shared by FRC teams. The code */ 004/* must be accompanied by the FIRST BSD license file in the root directory of */ 005/* the project. */ 006/*----------------------------------------------------------------------------*/ 007 008package edu.wpi.first.wpilibj; 009 010import edu.wpi.first.wpilibj.hal.FRCNetComm.tResourceType; 011import edu.wpi.first.wpilibj.hal.HAL; 012import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder; 013 014import static java.util.Objects.requireNonNull; 015 016/** 017 * Ultrasonic rangefinder class. The Ultrasonic rangefinder measures absolute distance based on the 018 * round-trip time of a ping generated by the controller. These sensors use two transducers, a 019 * speaker and a microphone both tuned to the ultrasonic range. A common ultrasonic sensor, the 020 * Daventech SRF04 requires a short pulse to be generated on a digital channel. This causes the 021 * chirp to be emitted. A second line becomes high as the ping is transmitted and goes low when the 022 * echo is received. The time that the line is high determines the round trip distance (time of 023 * flight). 024 */ 025public class Ultrasonic extends SensorBase implements PIDSource, Sendable { 026 /** 027 * The units to return when PIDGet is called. 028 */ 029 public enum Unit { 030 /** 031 * Use inches for PIDGet. 032 */ 033 kInches, 034 /** 035 * Use millimeters for PIDGet. 036 */ 037 kMillimeters 038 } 039 040 // Time (sec) for the ping trigger pulse. 041 private static final double kPingTime = 10 * 1e-6; 042 private static final double kSpeedOfSoundInchesPerSec = 1130.0 * 12.0; 043 // head of the ultrasonic sensor list 044 private static Ultrasonic m_firstSensor = null; 045 // automatic round robin mode 046 private static boolean m_automaticEnabled = false; 047 private DigitalInput m_echoChannel; 048 private DigitalOutput m_pingChannel = null; 049 private boolean m_allocatedChannels; 050 private boolean m_enabled = false; 051 private Counter m_counter = null; 052 private Ultrasonic m_nextSensor = null; 053 // task doing the round-robin automatic sensing 054 private static Thread m_task = null; 055 private Unit m_units; 056 private static int m_instances = 0; 057 protected PIDSourceType m_pidSource = PIDSourceType.kDisplacement; 058 059 /** 060 * Background task that goes through the list of ultrasonic sensors and pings each one in turn. 061 * The counter is configured to read the timing of the returned echo pulse. 062 * 063 * <p><b>DANGER WILL ROBINSON, DANGER WILL ROBINSON:</b> This code runs as a task and assumes that 064 * none of the ultrasonic sensors will change while it's running. If one does, then this will 065 * certainly break. Make sure to disable automatic mode before changing anything with the 066 * sensors!! 067 */ 068 private class UltrasonicChecker extends Thread { 069 @Override 070 public synchronized void run() { 071 Ultrasonic ultrasonic = null; 072 while (m_automaticEnabled) { 073 if (ultrasonic == null) { 074 ultrasonic = m_firstSensor; 075 } 076 if (ultrasonic == null) { 077 return; 078 } 079 if (ultrasonic.isEnabled()) { 080 // Do the ping 081 ultrasonic.m_pingChannel.pulse(kPingTime); 082 } 083 ultrasonic = ultrasonic.m_nextSensor; 084 Timer.delay(.1); // wait for ping to return 085 } 086 } 087 } 088 089 /** 090 * Initialize the Ultrasonic Sensor. This is the common code that initializes the ultrasonic 091 * sensor given that there are two digital I/O channels allocated. If the system was running in 092 * automatic mode (round robin) when the new sensor is added, it is stopped, the sensor is added, 093 * then automatic mode is restored. 094 */ 095 private synchronized void initialize() { 096 if (m_task == null) { 097 m_task = new UltrasonicChecker(); 098 } 099 final boolean originalMode = m_automaticEnabled; 100 setAutomaticMode(false); // kill task when adding a new sensor 101 m_nextSensor = m_firstSensor; 102 m_firstSensor = this; 103 104 m_counter = new Counter(m_echoChannel); // set up counter for this 105 addChild(m_counter); 106 // sensor 107 m_counter.setMaxPeriod(1.0); 108 m_counter.setSemiPeriodMode(true); 109 m_counter.reset(); 110 m_enabled = true; // make it available for round robin scheduling 111 setAutomaticMode(originalMode); 112 113 m_instances++; 114 HAL.report(tResourceType.kResourceType_Ultrasonic, m_instances); 115 setName("Ultrasonic", m_echoChannel.getChannel()); 116 } 117 118 /** 119 * Create an instance of the Ultrasonic Sensor. This is designed to supchannel the Daventech SRF04 120 * and Vex ultrasonic sensors. 121 * 122 * @param pingChannel The digital output channel that sends the pulse to initiate the sensor 123 * sending the ping. 124 * @param echoChannel The digital input channel that receives the echo. The length of time that 125 * the echo is high represents the round trip time of the ping, and the 126 * distance. 127 * @param units The units returned in either kInches or kMilliMeters 128 */ 129 public Ultrasonic(final int pingChannel, final int echoChannel, Unit units) { 130 m_pingChannel = new DigitalOutput(pingChannel); 131 m_echoChannel = new DigitalInput(echoChannel); 132 addChild(m_pingChannel); 133 addChild(m_echoChannel); 134 m_allocatedChannels = true; 135 m_units = units; 136 initialize(); 137 } 138 139 /** 140 * Create an instance of the Ultrasonic Sensor. This is designed to supchannel the Daventech SRF04 141 * and Vex ultrasonic sensors. Default unit is inches. 142 * 143 * @param pingChannel The digital output channel that sends the pulse to initiate the sensor 144 * sending the ping. 145 * @param echoChannel The digital input channel that receives the echo. The length of time that 146 * the echo is high represents the round trip time of the ping, and the 147 * distance. 148 */ 149 public Ultrasonic(final int pingChannel, final int echoChannel) { 150 this(pingChannel, echoChannel, Unit.kInches); 151 } 152 153 /** 154 * Create an instance of an Ultrasonic Sensor from a DigitalInput for the echo channel and a 155 * DigitalOutput for the ping channel. 156 * 157 * @param pingChannel The digital output object that starts the sensor doing a ping. Requires a 158 * 10uS pulse to start. 159 * @param echoChannel The digital input object that times the return pulse to determine the 160 * range. 161 * @param units The units returned in either kInches or kMilliMeters 162 */ 163 public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel, Unit units) { 164 requireNonNull(pingChannel, "Provided ping channel was null"); 165 requireNonNull(echoChannel, "Provided echo channel was null"); 166 167 m_allocatedChannels = false; 168 m_pingChannel = pingChannel; 169 m_echoChannel = echoChannel; 170 m_units = units; 171 initialize(); 172 } 173 174 /** 175 * Create an instance of an Ultrasonic Sensor from a DigitalInput for the echo channel and a 176 * DigitalOutput for the ping channel. Default unit is inches. 177 * 178 * @param pingChannel The digital output object that starts the sensor doing a ping. Requires a 179 * 10uS pulse to start. 180 * @param echoChannel The digital input object that times the return pulse to determine the 181 * range. 182 */ 183 public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel) { 184 this(pingChannel, echoChannel, Unit.kInches); 185 } 186 187 /** 188 * Destructor for the ultrasonic sensor. Delete the instance of the ultrasonic sensor by freeing 189 * the allocated digital channels. If the system was in automatic mode (round robin), then it is 190 * stopped, then started again after this sensor is removed (provided this wasn't the last 191 * sensor). 192 */ 193 @Override 194 public synchronized void free() { 195 super.free(); 196 final boolean wasAutomaticMode = m_automaticEnabled; 197 setAutomaticMode(false); 198 if (m_allocatedChannels) { 199 if (m_pingChannel != null) { 200 m_pingChannel.free(); 201 } 202 if (m_echoChannel != null) { 203 m_echoChannel.free(); 204 } 205 } 206 207 if (m_counter != null) { 208 m_counter.free(); 209 m_counter = null; 210 } 211 212 m_pingChannel = null; 213 m_echoChannel = null; 214 215 if (this == m_firstSensor) { 216 m_firstSensor = m_nextSensor; 217 if (m_firstSensor == null) { 218 setAutomaticMode(false); 219 } 220 } else { 221 for (Ultrasonic s = m_firstSensor; s != null; s = s.m_nextSensor) { 222 if (this == s.m_nextSensor) { 223 s.m_nextSensor = s.m_nextSensor.m_nextSensor; 224 break; 225 } 226 } 227 } 228 if (m_firstSensor != null && wasAutomaticMode) { 229 setAutomaticMode(true); 230 } 231 } 232 233 /** 234 * Turn Automatic mode on/off. When in Automatic mode, all sensors will fire in round robin, 235 * waiting a set time between each sensor. 236 * 237 * @param enabling Set to true if round robin scheduling should start for all the ultrasonic 238 * sensors. This scheduling method assures that the sensors are non-interfering 239 * because no two sensors fire at the same time. If another scheduling algorithm 240 * is preferred, it can be implemented by pinging the sensors manually and waiting 241 * for the results to come back. 242 */ 243 public void setAutomaticMode(boolean enabling) { 244 if (enabling == m_automaticEnabled) { 245 return; // ignore the case of no change 246 } 247 m_automaticEnabled = enabling; 248 249 if (enabling) { 250 /* Clear all the counters so no data is valid. No synchronization is 251 * needed because the background task is stopped. 252 */ 253 for (Ultrasonic u = m_firstSensor; u != null; u = u.m_nextSensor) { 254 u.m_counter.reset(); 255 } 256 257 // Start round robin task 258 m_task.start(); 259 } else { 260 // Wait for background task to stop running 261 try { 262 m_task.join(); 263 } catch (InterruptedException ex) { 264 Thread.currentThread().interrupt(); 265 ex.printStackTrace(); 266 } 267 268 /* Clear all the counters (data now invalid) since automatic mode is 269 * disabled. No synchronization is needed because the background task is 270 * stopped. 271 */ 272 for (Ultrasonic u = m_firstSensor; u != null; u = u.m_nextSensor) { 273 u.m_counter.reset(); 274 } 275 } 276 } 277 278 /** 279 * Single ping to ultrasonic sensor. Send out a single ping to the ultrasonic sensor. This only 280 * works if automatic (round robin) mode is disabled. A single ping is sent out, and the counter 281 * should count the semi-period when it comes in. The counter is reset to make the current value 282 * invalid. 283 */ 284 public void ping() { 285 setAutomaticMode(false); // turn off automatic round robin if pinging 286 // single sensor 287 m_counter.reset(); // reset the counter to zero (invalid data now) 288 // do the ping to start getting a single range 289 m_pingChannel.pulse(kPingTime); 290 } 291 292 /** 293 * Check if there is a valid range measurement. The ranges are accumulated in a counter that will 294 * increment on each edge of the echo (return) signal. If the count is not at least 2, then the 295 * range has not yet been measured, and is invalid. 296 * 297 * @return true if the range is valid 298 */ 299 public boolean isRangeValid() { 300 return m_counter.get() > 1; 301 } 302 303 /** 304 * Get the range in inches from the ultrasonic sensor. If there is no valid value yet, i.e. at 305 * least one measurement hasn't completed, then return 0. 306 * 307 * @return double Range in inches of the target returned from the ultrasonic sensor. 308 */ 309 public double getRangeInches() { 310 if (isRangeValid()) { 311 return m_counter.getPeriod() * kSpeedOfSoundInchesPerSec / 2.0; 312 } else { 313 return 0; 314 } 315 } 316 317 /** 318 * Get the range in millimeters from the ultrasonic sensor. If there is no valid value yet, i.e. 319 * at least one measurement hasn't completed, then return 0. 320 * 321 * @return double Range in millimeters of the target returned by the ultrasonic sensor. 322 */ 323 public double getRangeMM() { 324 return getRangeInches() * 25.4; 325 } 326 327 @Override 328 public void setPIDSourceType(PIDSourceType pidSource) { 329 if (!pidSource.equals(PIDSourceType.kDisplacement)) { 330 throw new IllegalArgumentException("Only displacement PID is allowed for ultrasonics."); 331 } 332 m_pidSource = pidSource; 333 } 334 335 @Override 336 public PIDSourceType getPIDSourceType() { 337 return m_pidSource; 338 } 339 340 /** 341 * Get the range in the current DistanceUnit for the PIDSource base object. 342 * 343 * @return The range in DistanceUnit 344 */ 345 @Override 346 public double pidGet() { 347 switch (m_units) { 348 case kInches: 349 return getRangeInches(); 350 case kMillimeters: 351 return getRangeMM(); 352 default: 353 return 0.0; 354 } 355 } 356 357 /** 358 * Set the current DistanceUnit that should be used for the PIDSource base object. 359 * 360 * @param units The DistanceUnit that should be used. 361 */ 362 public void setDistanceUnits(Unit units) { 363 m_units = units; 364 } 365 366 /** 367 * Get the current DistanceUnit that is used for the PIDSource base object. 368 * 369 * @return The type of DistanceUnit that is being used. 370 */ 371 public Unit getDistanceUnits() { 372 return m_units; 373 } 374 375 /** 376 * Is the ultrasonic enabled. 377 * 378 * @return true if the ultrasonic is enabled 379 */ 380 public boolean isEnabled() { 381 return m_enabled; 382 } 383 384 /** 385 * Set if the ultrasonic is enabled. 386 * 387 * @param enable set to true to enable the ultrasonic 388 */ 389 public void setEnabled(boolean enable) { 390 m_enabled = enable; 391 } 392 393 @Override 394 public void initSendable(SendableBuilder builder) { 395 builder.setSmartDashboardType("Ultrasonic"); 396 builder.addDoubleProperty("Value", this::getRangeInches, null); 397 } 398}