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.wpilibj.SynchronousInterrupt.WaitResult;
010import java.util.concurrent.atomic.AtomicBoolean;
011import java.util.function.BiConsumer;
012
013/**
014 * Class for handling asynchronous interrupts using a callback thread.
015 *
016 * <p>By default, interrupts will occur on rising edge. Callbacks are disabled by default, and
017 * enable() must be called before they will occur.
018 *
019 * <p>Both rising and falling edges can be indicated in one callback if both a rising and falling
020 * edge occurred since the previous callback.
021 *
022 * <p>Synchronous (blocking) interrupts are handled by the SynchronousInterrupt class.
023 */
024public class AsynchronousInterrupt implements AutoCloseable {
025  private final BiConsumer<Boolean, Boolean> m_callback;
026  private final SynchronousInterrupt m_interrupt;
027
028  private final AtomicBoolean m_keepRunning = new AtomicBoolean(false);
029  private Thread m_thread;
030
031  /**
032   * Construct a new asynchronous interrupt using a Digital Source.
033   *
034   * <p>At construction, the interrupt will trigger on the rising edge.
035   *
036   * <p>Callbacks will not be triggered until enable() is called.
037   *
038   * <p>The first bool in the callback indicates the rising edge triggered the interrupt, the second
039   * bool is falling edge.
040   *
041   * @param source The digital source to use.
042   * @param callback The callback to call on an interrupt
043   */
044  public AsynchronousInterrupt(DigitalSource source, BiConsumer<Boolean, Boolean> callback) {
045    m_callback = requireNonNullParam(callback, "callback", "AsynchronousInterrupt");
046    m_interrupt = new SynchronousInterrupt(source);
047  }
048
049  /**
050   * Closes the interrupt.
051   *
052   * <p>This does not close the associated digital source.
053   *
054   * <p>This will disable the interrupt if it is enabled.
055   */
056  @Override
057  public void close() {
058    disable();
059    m_interrupt.close();
060  }
061
062  /**
063   * Enables interrupt callbacks. Before this, callbacks will not occur. Does nothing if already
064   * enabled.
065   */
066  public void enable() {
067    if (m_keepRunning.get()) {
068      return;
069    }
070
071    m_keepRunning.set(true);
072    m_thread = new Thread(this::threadMain);
073    m_thread.start();
074  }
075
076  /** Disables interrupt callbacks. Does nothing if already disabled. */
077  public void disable() {
078    m_keepRunning.set(false);
079    m_interrupt.wakeupWaitingInterrupt();
080    if (m_thread != null) {
081      if (m_thread.isAlive()) {
082        try {
083          m_thread.interrupt();
084          m_thread.join();
085        } catch (InterruptedException ex) {
086          Thread.currentThread().interrupt();
087        }
088      }
089      m_thread = null;
090    }
091  }
092
093  /**
094   * Set which edges to trigger the interrupt on.
095   *
096   * @param risingEdge Trigger on rising edge
097   * @param fallingEdge Trigger on falling edge
098   */
099  public void setInterruptEdges(boolean risingEdge, boolean fallingEdge) {
100    m_interrupt.setInterruptEdges(risingEdge, fallingEdge);
101  }
102
103  /**
104   * Get the timestamp of the last rising edge.
105   *
106   * <p>This function does not require the interrupt to be enabled to work.
107   *
108   * <p>This only works if rising edge was configured using setInterruptEdges.
109   *
110   * @return the timestamp in seconds relative to getFPGATime
111   */
112  public double getRisingTimestamp() {
113    return m_interrupt.getRisingTimestamp();
114  }
115
116  /**
117   * Get the timestamp of the last falling edge.
118   *
119   * <p>This function does not require the interrupt to be enabled to work.
120   *
121   * <p>This only works if falling edge was configured using setInterruptEdges.
122   *
123   * @return the timestamp in seconds relative to getFPGATime
124   */
125  public double getFallingTimestamp() {
126    return m_interrupt.getFallingTimestamp();
127  }
128
129  private void threadMain() {
130    while (m_keepRunning.get()) {
131      var result = m_interrupt.waitForInterrupt(10, false);
132      if (!m_keepRunning.get()) {
133        break;
134      }
135      if (result == WaitResult.kTimeout) {
136        continue;
137      }
138
139      boolean rising = false;
140      boolean falling = false;
141      switch (result) {
142        case kBoth:
143          rising = true;
144          falling = true;
145          break;
146        case kFallingEdge:
147          falling = true;
148          break;
149        case kRisingEdge:
150          rising = true;
151          break;
152        default:
153          break;
154      }
155
156      m_callback.accept(rising, falling);
157    }
158  }
159}