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