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}