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 edu.wpi.first.hal.DriverStationJNI;
008import edu.wpi.first.hal.FRCNetComm.tInstances;
009import edu.wpi.first.hal.FRCNetComm.tResourceType;
010import edu.wpi.first.hal.HAL;
011import edu.wpi.first.hal.NotifierJNI;
012import java.util.PriorityQueue;
013
014/**
015 * TimedRobot implements the IterativeRobotBase robot program framework.
016 *
017 * <p>The TimedRobot class is intended to be subclassed by a user creating a robot program.
018 *
019 * <p>periodic() functions from the base class are called on an interval by a Notifier instance.
020 */
021public class TimedRobot extends IterativeRobotBase {
022  @SuppressWarnings("MemberName")
023  static class Callback implements Comparable<Callback> {
024    public Runnable func;
025    public double period;
026    public double expirationTime;
027
028    /**
029     * Construct a callback container.
030     *
031     * @param func The callback to run.
032     * @param startTimeSeconds The common starting point for all callback scheduling in seconds.
033     * @param periodSeconds The period at which to run the callback in seconds.
034     * @param offsetSeconds The offset from the common starting time in seconds.
035     */
036    Callback(Runnable func, double startTimeSeconds, double periodSeconds, double offsetSeconds) {
037      this.func = func;
038      this.period = periodSeconds;
039      this.expirationTime =
040          startTimeSeconds
041              + offsetSeconds
042              + Math.floor((Timer.getFPGATimestamp() - startTimeSeconds) / this.period)
043                  * this.period
044              + this.period;
045    }
046
047    @Override
048    public boolean equals(Object rhs) {
049      if (rhs instanceof Callback) {
050        return Double.compare(expirationTime, ((Callback) rhs).expirationTime) == 0;
051      }
052      return false;
053    }
054
055    @Override
056    public int hashCode() {
057      return Double.hashCode(expirationTime);
058    }
059
060    @Override
061    public int compareTo(Callback rhs) {
062      // Elements with sooner expiration times are sorted as lesser. The head of
063      // Java's PriorityQueue is the least element.
064      return Double.compare(expirationTime, rhs.expirationTime);
065    }
066  }
067
068  public static final double kDefaultPeriod = 0.02;
069
070  // The C pointer to the notifier object. We don't use it directly, it is
071  // just passed to the JNI bindings.
072  private final int m_notifier = NotifierJNI.initializeNotifier();
073
074  private double m_startTime;
075
076  private final PriorityQueue<Callback> m_callbacks = new PriorityQueue<>();
077
078  /** Constructor for TimedRobot. */
079  protected TimedRobot() {
080    this(kDefaultPeriod);
081  }
082
083  /**
084   * Constructor for TimedRobot.
085   *
086   * @param period Period in seconds.
087   */
088  protected TimedRobot(double period) {
089    super(period);
090    m_startTime = Timer.getFPGATimestamp();
091    addPeriodic(this::loopFunc, period);
092    NotifierJNI.setNotifierName(m_notifier, "TimedRobot");
093
094    HAL.report(tResourceType.kResourceType_Framework, tInstances.kFramework_Timed);
095  }
096
097  @Override
098  public void close() {
099    NotifierJNI.stopNotifier(m_notifier);
100    NotifierJNI.cleanNotifier(m_notifier);
101  }
102
103  /** Provide an alternate "main loop" via startCompetition(). */
104  @Override
105  public void startCompetition() {
106    robotInit();
107
108    if (isSimulation()) {
109      simulationInit();
110    }
111
112    // Tell the DS that the robot is ready to be enabled
113    System.out.println("********** Robot program startup complete **********");
114    DriverStationJNI.observeUserProgramStarting();
115
116    // Loop forever, calling the appropriate mode-dependent function
117    while (true) {
118      // We don't have to check there's an element in the queue first because
119      // there's always at least one (the constructor adds one). It's reenqueued
120      // at the end of the loop.
121      var callback = m_callbacks.poll();
122
123      NotifierJNI.updateNotifierAlarm(m_notifier, (long) (callback.expirationTime * 1e6));
124
125      long curTime = NotifierJNI.waitForNotifierAlarm(m_notifier);
126      if (curTime == 0) {
127        break;
128      }
129
130      callback.func.run();
131
132      callback.expirationTime += callback.period;
133      m_callbacks.add(callback);
134
135      // Process all other callbacks that are ready to run
136      while ((long) (m_callbacks.peek().expirationTime * 1e6) <= curTime) {
137        callback = m_callbacks.poll();
138
139        callback.func.run();
140
141        callback.expirationTime += callback.period;
142        m_callbacks.add(callback);
143      }
144    }
145  }
146
147  /** Ends the main loop in startCompetition(). */
148  @Override
149  public void endCompetition() {
150    NotifierJNI.stopNotifier(m_notifier);
151  }
152
153  /**
154   * Add a callback to run at a specific period.
155   *
156   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
157   * synchronously. Interactions between them are thread-safe.
158   *
159   * @param callback The callback to run.
160   * @param periodSeconds The period at which to run the callback in seconds.
161   */
162  public final void addPeriodic(Runnable callback, double periodSeconds) {
163    m_callbacks.add(new Callback(callback, m_startTime, periodSeconds, 0.0));
164  }
165
166  /**
167   * Add a callback to run at a specific period with a starting time offset.
168   *
169   * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
170   * synchronously. Interactions between them are thread-safe.
171   *
172   * @param callback The callback to run.
173   * @param periodSeconds The period at which to run the callback in seconds.
174   * @param offsetSeconds The offset from the common starting time in seconds. This is useful for
175   *     scheduling a callback in a different timeslot relative to TimedRobot.
176   */
177  public final void addPeriodic(Runnable callback, double periodSeconds, double offsetSeconds) {
178    m_callbacks.add(new Callback(callback, m_startTime, periodSeconds, offsetSeconds));
179  }
180}