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}