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 static edu.wpi.first.util.ErrorMessages.requireNonNullParam; 008 009import edu.wpi.first.hal.FRCNetComm.tResourceType; 010import edu.wpi.first.hal.HAL; 011import edu.wpi.first.hal.SimBoolean; 012import edu.wpi.first.hal.SimDevice; 013import edu.wpi.first.hal.SimDevice.Direction; 014import edu.wpi.first.hal.SimDouble; 015import edu.wpi.first.util.sendable.Sendable; 016import edu.wpi.first.util.sendable.SendableBuilder; 017import edu.wpi.first.util.sendable.SendableRegistry; 018import java.util.ArrayList; 019import java.util.List; 020 021/** 022 * Ultrasonic rangefinder class. The Ultrasonic rangefinder measures absolute distance based on the 023 * round-trip time of a ping generated by the controller. These sensors use two transducers, a 024 * speaker and a microphone both tuned to the ultrasonic range. A common ultrasonic sensor, the 025 * Daventech SRF04 requires a short pulse to be generated on a digital channel. This causes the 026 * chirp to be emitted. A second line becomes high as the ping is transmitted and goes low when the 027 * echo is received. The time that the line is high determines the round trip distance (time of 028 * flight). 029 */ 030public class Ultrasonic implements Sendable, AutoCloseable { 031 // Time (sec) for the ping trigger pulse. 032 private static final double kPingTime = 10 * 1e-6; 033 private static final double kSpeedOfSoundInchesPerSec = 1130.0 * 12.0; 034 // ultrasonic sensor list 035 private static final List<Ultrasonic> m_sensors = new ArrayList<>(); 036 // automatic round robin mode 037 private static volatile boolean m_automaticEnabled; 038 private DigitalInput m_echoChannel; 039 private DigitalOutput m_pingChannel; 040 private final boolean m_allocatedChannels; 041 private boolean m_enabled; 042 private Counter m_counter; 043 // task doing the round-robin automatic sensing 044 private static Thread m_task; 045 private static int m_instances; 046 047 @SuppressWarnings("PMD.SingularField") 048 private SimDevice m_simDevice; 049 050 private SimBoolean m_simRangeValid; 051 private SimDouble m_simRange; 052 053 /** 054 * Background task that goes through the list of ultrasonic sensors and pings each one in turn. 055 * The counter is configured to read the timing of the returned echo pulse. 056 * 057 * <p><b>DANGER WILL ROBINSON, DANGER WILL ROBINSON:</b> This code runs as a task and assumes that 058 * none of the ultrasonic sensors will change while it's running. If one does, then this will 059 * certainly break. Make sure to disable automatic mode before changing anything with the 060 * sensors!! 061 */ 062 private static final class UltrasonicChecker extends Thread { 063 @Override 064 public synchronized void run() { 065 while (m_automaticEnabled) { 066 for (Ultrasonic sensor : m_sensors) { 067 if (!m_automaticEnabled) { 068 break; 069 } 070 071 if (sensor.isEnabled()) { 072 sensor.m_pingChannel.pulse(kPingTime); // do the ping 073 } 074 075 Timer.delay(0.1); // wait for ping to return 076 } 077 } 078 } 079 } 080 081 /** 082 * Initialize the Ultrasonic Sensor. This is the common code that initializes the ultrasonic 083 * sensor given that there are two digital I/O channels allocated. If the system was running in 084 * automatic mode (round-robin) when the new sensor is added, it is stopped, the sensor is added, 085 * then automatic mode is restored. 086 */ 087 private synchronized void initialize() { 088 m_simDevice = SimDevice.create("Ultrasonic", m_echoChannel.getChannel()); 089 if (m_simDevice != null) { 090 m_simRangeValid = m_simDevice.createBoolean("Range Valid", Direction.kInput, true); 091 m_simRange = m_simDevice.createDouble("Range (in)", Direction.kInput, 0.0); 092 m_pingChannel.setSimDevice(m_simDevice); 093 m_echoChannel.setSimDevice(m_simDevice); 094 } 095 final boolean originalMode = m_automaticEnabled; 096 setAutomaticMode(false); // kill task when adding a new sensor 097 m_sensors.add(this); 098 099 m_counter = new Counter(m_echoChannel); // set up counter for this 100 SendableRegistry.addChild(this, m_counter); 101 // sensor 102 m_counter.setMaxPeriod(1.0); 103 m_counter.setSemiPeriodMode(true); 104 m_counter.reset(); 105 m_enabled = true; // make it available for round-robin scheduling 106 setAutomaticMode(originalMode); 107 108 m_instances++; 109 HAL.report(tResourceType.kResourceType_Ultrasonic, m_instances); 110 SendableRegistry.addLW(this, "Ultrasonic", m_echoChannel.getChannel()); 111 } 112 113 /** 114 * Returns the echo channel. 115 * 116 * @return The echo channel. 117 */ 118 public int getEchoChannel() { 119 return m_echoChannel.getChannel(); 120 } 121 122 /** 123 * Create an instance of the Ultrasonic Sensor. This is designed to support the Daventech SRF04 124 * and Vex ultrasonic sensors. 125 * 126 * @param pingChannel The digital output channel that sends the pulse to initiate the sensor 127 * sending the ping. 128 * @param echoChannel The digital input channel that receives the echo. The length of time that 129 * the echo is high represents the round trip time of the ping, and the distance. 130 */ 131 @SuppressWarnings("this-escape") 132 public Ultrasonic(final int pingChannel, final int echoChannel) { 133 m_pingChannel = new DigitalOutput(pingChannel); 134 m_echoChannel = new DigitalInput(echoChannel); 135 SendableRegistry.addChild(this, m_pingChannel); 136 SendableRegistry.addChild(this, m_echoChannel); 137 m_allocatedChannels = true; 138 initialize(); 139 } 140 141 /** 142 * Create an instance of an Ultrasonic Sensor from a DigitalInput for the echo channel and a 143 * DigitalOutput for the ping channel. 144 * 145 * @param pingChannel The digital output object that starts the sensor doing a ping. Requires a 146 * 10uS pulse to start. 147 * @param echoChannel The digital input object that times the return pulse to determine the range. 148 */ 149 @SuppressWarnings("this-escape") 150 public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel) { 151 requireNonNullParam(pingChannel, "pingChannel", "Ultrasonic"); 152 requireNonNullParam(echoChannel, "echoChannel", "Ultrasonic"); 153 154 m_allocatedChannels = false; 155 m_pingChannel = pingChannel; 156 m_echoChannel = echoChannel; 157 initialize(); 158 } 159 160 /** 161 * Destructor for the ultrasonic sensor. Delete the instance of the ultrasonic sensor by freeing 162 * the allocated digital channels. If the system was in automatic mode (round-robin), then it is 163 * stopped, then started again after this sensor is removed (provided this wasn't the last 164 * sensor). 165 */ 166 @Override 167 public synchronized void close() { 168 SendableRegistry.remove(this); 169 final boolean wasAutomaticMode = m_automaticEnabled; 170 setAutomaticMode(false); 171 if (m_allocatedChannels) { 172 if (m_pingChannel != null) { 173 m_pingChannel.close(); 174 } 175 if (m_echoChannel != null) { 176 m_echoChannel.close(); 177 } 178 } 179 180 if (m_counter != null) { 181 m_counter.close(); 182 m_counter = null; 183 } 184 185 m_pingChannel = null; 186 m_echoChannel = null; 187 synchronized (m_sensors) { 188 m_sensors.remove(this); 189 } 190 if (!m_sensors.isEmpty() && wasAutomaticMode) { 191 setAutomaticMode(true); 192 } 193 194 if (m_simDevice != null) { 195 m_simDevice.close(); 196 m_simDevice = null; 197 } 198 } 199 200 /** 201 * Turn Automatic mode on/off for all sensors. 202 * 203 * <p>When in Automatic mode, all sensors will fire in round-robin, waiting a set time between 204 * each sensor. 205 * 206 * @param enabling Set to true if round-robin scheduling should start for all the ultrasonic 207 * sensors. This scheduling method assures that the sensors are non-interfering because no two 208 * sensors fire at the same time. If another scheduling algorithm is preferred, it can be 209 * implemented by pinging the sensors manually and waiting for the results to come back. 210 */ 211 public static synchronized void setAutomaticMode(boolean enabling) { 212 if (enabling == m_automaticEnabled) { 213 return; // ignore the case of no change 214 } 215 m_automaticEnabled = enabling; 216 217 if (enabling) { 218 /* Clear all the counters so no data is valid. No synchronization is 219 * needed because the background task is stopped. 220 */ 221 for (Ultrasonic u : m_sensors) { 222 u.m_counter.reset(); 223 } 224 225 // Start round robin task 226 m_task = new UltrasonicChecker(); 227 m_task.start(); 228 } else { 229 if (m_task != null) { 230 // Wait for background task to stop running 231 try { 232 m_task.join(); 233 m_task = null; 234 } catch (InterruptedException ex) { 235 Thread.currentThread().interrupt(); 236 ex.printStackTrace(); 237 } 238 } 239 240 /* Clear all the counters (data now invalid) since automatic mode is 241 * disabled. No synchronization is needed because the background task is 242 * stopped. 243 */ 244 for (Ultrasonic u : m_sensors) { 245 u.m_counter.reset(); 246 } 247 } 248 } 249 250 /** 251 * Single ping to ultrasonic sensor. Send out a single ping to the ultrasonic sensor. This only 252 * works if automatic (round-robin) mode is disabled. A single ping is sent out, and the counter 253 * should count the semi-period when it comes in. The counter is reset to make the current value 254 * invalid. 255 */ 256 public void ping() { 257 setAutomaticMode(false); // turn off automatic round-robin if pinging 258 // single sensor 259 m_counter.reset(); // reset the counter to zero (invalid data now) 260 // do the ping to start getting a single range 261 m_pingChannel.pulse(kPingTime); 262 } 263 264 /** 265 * Check if there is a valid range measurement. The ranges are accumulated in a counter that will 266 * increment on each edge of the echo (return) signal. If the count is not at least 2, then the 267 * range has not yet been measured, and is invalid. 268 * 269 * @return true if the range is valid 270 */ 271 public boolean isRangeValid() { 272 if (m_simRangeValid != null) { 273 return m_simRangeValid.get(); 274 } 275 return m_counter.get() > 1; 276 } 277 278 /** 279 * Get the range in inches from the ultrasonic sensor. If there is no valid value yet, i.e. at 280 * least one measurement hasn't completed, then return 0. 281 * 282 * @return double Range in inches of the target returned from the ultrasonic sensor. 283 */ 284 public double getRangeInches() { 285 if (isRangeValid()) { 286 if (m_simRange != null) { 287 return m_simRange.get(); 288 } 289 return m_counter.getPeriod() * kSpeedOfSoundInchesPerSec / 2.0; 290 } else { 291 return 0; 292 } 293 } 294 295 /** 296 * Get the range in millimeters from the ultrasonic sensor. If there is no valid value yet, i.e. 297 * at least one measurement hasn't completed, then return 0. 298 * 299 * @return double Range in millimeters of the target returned by the ultrasonic sensor. 300 */ 301 public double getRangeMM() { 302 return getRangeInches() * 25.4; 303 } 304 305 /** 306 * Is the ultrasonic enabled. 307 * 308 * @return true if the ultrasonic is enabled 309 */ 310 public boolean isEnabled() { 311 return m_enabled; 312 } 313 314 /** 315 * Set if the ultrasonic is enabled. 316 * 317 * @param enable set to true to enable the ultrasonic 318 */ 319 public void setEnabled(boolean enable) { 320 m_enabled = enable; 321 } 322 323 @Override 324 public void initSendable(SendableBuilder builder) { 325 builder.setSmartDashboardType("Ultrasonic"); 326 builder.addDoubleProperty("Value", this::getRangeInches, null); 327 } 328}