[wpilib] Change opmodes to purely periodic (#8652)

1. Make the OpMode interface itself periodic; this means the only
differences between `OpMode` and `PeriodicOpMode` are the latter's
methods to add sideloaded periodic callbacks
2. Make OpModeRobot process callbacks in a similar fashion to TimedRobot
and
3. Add some lifecycle functions (discussed below)
4. Pull the callback priority queue from TimedRobot to a new class
called `PeriodicPriorityQueue` so that `TimedRobot` and `OpModeRobot`
have less duplication
5. Fix a typo in the DriverStationJNI class that causes a memory leak
when certain driver station sim calls
6. Port the C++ OpModeRobot tests to Java 

`OpModeRobot` now possesses some `IterativeRobotBase`-stye lifecycle
functions; these functions
1. `robotPeriodic` 
2. `simulationInit` and `simulationPeriodic` 
3. `disabledInit`, `disabledPeriodic`, and `disabledExit`
(note that `simulationInit` and `disabledInit` may be renamed to match
wpilibsuite#8719)

`OpModeRobot` also now processes `OpMode` changes (by the Driver
Station) in its `loopFunc` method, similar to
`IterativeRobotBase.loopFunc` processing game mode changes; `loopFunc`
is, similarly to `TimedRobot`, provided as a default `Callback`

---------

Signed-off-by: Zach Harel <zach@zharel.me>
Co-authored-by: Joseph Eng <91924258+KangarooKoala@users.noreply.github.com>
This commit is contained in:
Zach Harel
2026-04-10 16:40:17 -04:00
committed by GitHub
parent 84295180cd
commit a8c7f3e3c6
29 changed files with 1954 additions and 1340 deletions

View File

@@ -5,17 +5,22 @@
#pragma once
#include <concepts>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "wpi/driverstation/Alert.hpp"
#include "wpi/framework/RobotBase.hpp"
#include "wpi/hal/DriverStationTypes.hpp"
#include "wpi/hal/Notifier.h"
#include "wpi/hal/Types.hpp"
#include "wpi/internal/PeriodicPriorityQueue.hpp"
#include "wpi/opmode/OpMode.hpp"
#include "wpi/system/Watchdog.hpp"
#include "wpi/units/time.hpp"
#include "wpi/util/DenseMap.hpp"
#include "wpi/util/SafeThread.hpp"
#include "wpi/util/Synchronization.hpp"
#include "wpi/util/mutex.hpp"
namespace wpi::util {
@@ -37,7 +42,7 @@ concept OneArgOpMode = std::constructible_from<T, R&> && OpModeDerived<T>;
/**
* Concept indicating a class is derived from OpMode and has either a
* no-argument constructor or a constructorthat accepts R&.
* no-argument constructor or a constructor that accepts R&.
*
* @tparam T opmode class
* @tparam R robot class
@@ -60,6 +65,9 @@ class OpModeRobotBase : public RobotBase {
public:
using OpModeFactory = std::function<std::unique_ptr<OpMode>()>;
/// Default loop period.
static constexpr auto DEFAULT_PERIOD = 20_ms;
/**
* Provide an alternate "main loop" via StartCompetition().
*/
@@ -72,14 +80,20 @@ class OpModeRobotBase : public RobotBase {
/**
* Constructor.
*
* @param period The period of the robot loop function.
*/
OpModeRobotBase() = default;
explicit OpModeRobotBase(wpi::units::second_t period);
/**
* Constructor for an OpModeRobot with a default loop time of 0.02 seconds.
*/
explicit OpModeRobotBase();
OpModeRobotBase(OpModeRobotBase&&) = delete;
OpModeRobotBase& operator=(OpModeRobotBase&&) = delete;
/**
* Function called exactly once after the DS is connected.
*
* Code that needs to know the DS state should go here.
*
* Users should override this method for initialization that needs to occur
@@ -87,12 +101,65 @@ class OpModeRobotBase : public RobotBase {
*/
virtual void DriverStationConnected() {}
/**
* Function called periodically every loop, regardless of enabled state or
* OpMode selection.
*/
virtual void RobotPeriodic() {}
/**
* Function called once during robot initialization in simulation.
*/
virtual void SimulationInit() {}
/**
* Function called periodically in simulation.
*/
virtual void SimulationPeriodic() {}
/**
* Function called once when the robot becomes disabled.
*/
virtual void DisabledInit() {}
/**
* Function called periodically while the robot is disabled.
*/
virtual void DisabledPeriodic() {}
/**
* Function called once when the robot exits disabled state.
*/
virtual void DisabledExit() {}
/**
* Function called periodically anytime when no opmode is selected, including
* when the Driver Station is disconnected.
*/
virtual void NonePeriodic() {}
/**
* Add a callback to run at a specific period.
*
* @param callback The callback to run.
* @param period The period at which to run the callback.
*/
void AddPeriodic(std::function<void()> callback, wpi::units::second_t period);
/**
* Return the system clock time in microseconds for the start of the current
* periodic loop. This is in the same time base as
* Timer.getMonotonicTimeStamp(), but is stable through a loop. It is updated
* at the beginning of every periodic callback (including the normal periodic
* loop).
*
* @return Robot running time in microseconds, as of the start of the current
* periodic function.
*/
wpi::units::microsecond_t GetLoopStartTime() const {
return m_callbacks.GetLoopStartTime();
}
/**
* Adds an operating mode option using a factory function that creates the
* opmode. It's necessary to call PublishOpModes() to make the added modes
@@ -147,15 +214,37 @@ class OpModeRobotBase : public RobotBase {
*/
void ClearOpModes();
protected:
/**
* Main robot loop function. Handles disabled state logic.
*/
void LoopFunc();
private:
struct OpModeData {
std::string name;
OpModeFactory factory;
};
wpi::util::DenseMap<int64_t, OpModeData> m_opModes;
wpi::hal::Handle<HAL_NotifierHandle, HAL_DestroyNotifier> m_notifier;
wpi::util::mutex m_opModeMutex;
std::weak_ptr<OpMode> m_activeOpMode;
wpi::internal::PeriodicPriorityQueue m_callbacks;
HAL_NotifierHandle m_notifier;
wpi::units::second_t m_period;
std::chrono::microseconds m_startTime;
Alert m_loopOverrunAlert;
Watchdog m_watchdog;
// OpMode lifecycle state
int64_t m_lastModeId = -1;
bool m_calledDriverStationConnected = false;
bool m_lastEnabledState = false;
std::shared_ptr<OpMode> m_currentOpMode;
std::vector<wpi::internal::PeriodicPriorityQueue::Callback>
m_activeOpModeCallbacks;
std::optional<wpi::internal::PeriodicPriorityQueue::Callback>
m_opmodePeriodic;
};
/**
@@ -176,6 +265,18 @@ class OpModeRobotBase : public RobotBase {
template <typename Derived>
class OpModeRobot : public OpModeRobotBase {
public:
/**
* Constructor.
*
* @param period The period of the robot loop function.
*/
explicit OpModeRobot(wpi::units::second_t period) : OpModeRobotBase{period} {}
/**
* Constructor for an OpModeRobot with a default loop time of 0.02 seconds.
*/
explicit OpModeRobot() : OpModeRobotBase{} {}
/**
* Adds an operating mode option. It's necessary to call PublishOpModes() to
* make the added modes visible to the driver station.

View File

@@ -4,18 +4,14 @@
#pragma once
#include <chrono>
#include <functional>
#include <utility>
#include <vector>
#include "wpi/framework/IterativeRobotBase.hpp"
#include "wpi/hal/Notifier.h"
#include "wpi/hal/Notifier.hpp"
#include "wpi/hal/Types.hpp"
#include "wpi/system/RobotController.hpp"
#include "wpi/internal/PeriodicPriorityQueue.hpp"
#include "wpi/units/frequency.hpp"
#include "wpi/units/time.hpp"
#include "wpi/util/priority_queue.hpp"
namespace wpi {
@@ -64,7 +60,7 @@ class TimedRobot : public IterativeRobotBase {
/**
* Return the system clock time in microseconds for the start of the current
* periodic loop. This is in the same time base as
* periodic loop. This is in the same time base as
* Timer.GetMonotonicTimestamp(), but is stable through a loop. It is updated
* at the beginning of every periodic callback (including the normal periodic
* loop).
@@ -72,7 +68,9 @@ class TimedRobot : public IterativeRobotBase {
* @return Robot running time in microseconds, as of the start of the current
* periodic function.
*/
uint64_t GetLoopStartTime();
wpi::units::microsecond_t GetLoopStartTime() const {
return m_callbacks.GetLoopStartTime();
}
/**
* Add a callback to run at a specific period with a starting time offset.
@@ -89,43 +87,12 @@ class TimedRobot : public IterativeRobotBase {
void AddPeriodic(std::function<void()> callback, wpi::units::second_t period,
wpi::units::second_t offset = 0_s);
private:
class Callback {
public:
std::function<void()> func;
std::chrono::microseconds period;
std::chrono::microseconds expirationTime;
/**
* Construct a callback container.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
Callback(std::function<void()> func, std::chrono::microseconds startTime,
std::chrono::microseconds period, std::chrono::microseconds offset)
: func{std::move(func)},
period{period},
expirationTime(startTime + offset + period +
(std::chrono::microseconds{
wpi::RobotController::GetMonotonicTime()} -
startTime) /
period * period) {}
bool operator>(const Callback& rhs) const {
return expirationTime > rhs.expirationTime;
}
};
protected:
wpi::hal::Handle<HAL_NotifierHandle, HAL_DestroyNotifier> m_notifier;
std::chrono::microseconds m_startTime;
uint64_t m_loopStartTimeUs = 0;
wpi::util::priority_queue<Callback, std::vector<Callback>,
std::greater<Callback>>
m_callbacks;
private:
wpi::internal::PeriodicPriorityQueue m_callbacks;
};
} // namespace wpi

View File

@@ -0,0 +1,206 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <chrono>
#include <functional>
#include <vector>
#include "wpi/hal/Notifier.h"
#include "wpi/units/time.hpp"
#include "wpi/util/priority_queue.hpp"
namespace wpi::internal {
/**
* A priority queue for scheduling periodic callbacks based on their next
* execution time.
*
* <p>This class manages a collection of periodic callbacks that execute at
* specified intervals. Callbacks are scheduled using FPGA timestamps and
* automatically rescheduled after execution to maintain their periodic
* behavior. The queue uses a priority heap to efficiently determine the next
* callback to execute.
*
* <p>This is an internal scheduling primitive used by robot frameworks like
* TimedRobot.
*/
class PeriodicPriorityQueue {
public:
/**
* A periodic callback with scheduling metadata.
*
* Each callback tracks its target function, period, and next expiration
* time. After execution, the expiration time is automatically advanced by
* full periods to maintain precise timing even when execution is delayed.
*/
class Callback {
public:
/** The function to execute when the callback fires. */
std::function<void()> func;
/** The period at which to run the callback. */
std::chrono::microseconds period;
/** The next scheduled execution time in FPGA timestamp microseconds. */
std::chrono::microseconds expirationTime;
/**
* Construct a callback container.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
Callback(std::function<void()> func, std::chrono::microseconds startTime,
std::chrono::microseconds period,
std::chrono::microseconds offset);
/**
* Construct a callback container using units-based period and offset.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
Callback(std::function<void()> func, std::chrono::microseconds startTime,
wpi::units::second_t period, wpi::units::second_t offset);
/**
* Construct a callback container using units-based period.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
*/
Callback(std::function<void()> func, std::chrono::microseconds startTime,
wpi::units::second_t period);
bool operator>(const Callback& rhs) const {
return expirationTime > rhs.expirationTime;
}
bool operator==(const Callback& rhs) const {
return expirationTime == rhs.expirationTime;
}
};
/**
* Adds a periodic callback to the queue.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
*/
void Add(std::function<void()> func, std::chrono::microseconds startTime,
std::chrono::microseconds period);
/**
* Adds a periodic callback to the queue.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
void Add(std::function<void()> func, std::chrono::microseconds startTime,
std::chrono::microseconds period, std::chrono::microseconds offset);
/**
* Adds a periodic callback to the queue.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling in
* FPGA timestamp microseconds.
* @param period The period at which to run the callback.
*/
void Add(std::function<void()> func, std::chrono::microseconds startTime,
wpi::units::second_t period);
/**
* Adds a periodic callback to the queue.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling in
* FPGA timestamp microseconds.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
void Add(std::function<void()> func, std::chrono::microseconds startTime,
wpi::units::second_t period, wpi::units::second_t offset);
/**
* Adds a pre-constructed callback to the queue.
*
* @param callback The callback to add.
*/
void Add(Callback callback);
/**
* Removes a specific callback from the queue.
*
* @param callback The callback to remove.
* @return true if the callback was found and removed, false otherwise.
*/
bool Remove(const Callback& callback);
/**
* Removes all callbacks from the queue.
*/
void Clear();
/**
* Executes all callbacks that are due, then waits for the next callback's
* scheduled time.
*
* This method performs the following steps:
* -# Retrieves the callback with the earliest expiration time from the queue
* -# Sets a hardware notifier alarm to wait until that callback's expiration
* time
* -# Blocks until the notifier signals or is interrupted
* -# Executes the callback and reschedules it for its next period
* -# Processes any additional callbacks that have become due during execution
*
* When rescheduling callbacks, this method automatically compensates for
* execution delays by advancing the expiration time by the number of full
* periods that have elapsed, ensuring callbacks maintain their scheduled
* phase over time.
*
* @param notifier The HAL notifier handle to use for timing.
* @return false if the notifier was destroyed (loop should exit), true
* otherwise.
*/
bool RunCallbacks(HAL_NotifierHandle notifier);
/**
* Returns the underlying priority queue.
*/
wpi::util::priority_queue<Callback, std::vector<Callback>, std::greater<>>&
GetQueue() {
return m_queue;
}
/**
* Return the system clock time in microseconds for the start of the current
* periodic loop. This is in the same time base as
* Timer.getMonotonicTimeStamp(), but is stable through a loop. It is updated
* at the beginning of every periodic callback (including the normal periodic
* loop).
*
* @return Robot running time in microseconds, as of the start of the current
* periodic function.
*/
wpi::units::microsecond_t GetLoopStartTime() const { return m_loopStartTime; }
private:
wpi::util::priority_queue<Callback, std::vector<Callback>, std::greater<>>
m_queue;
wpi::units::microsecond_t m_loopStartTime{0};
};
} // namespace wpi::internal

View File

@@ -4,7 +4,9 @@
#pragma once
#include <stdint.h>
#include <vector>
#include "wpi/internal/PeriodicPriorityQueue.hpp"
namespace wpi {
@@ -12,34 +14,69 @@ namespace wpi {
* Top-level interface for opmode classes. Users should generally extend one of
* the abstract implementations of this interface (e.g. PeriodicOpMode) rather
* than directly implementing this interface.
*
* <p><b>Lifecycle</b>:
* <ul>
* <li>constructed when opmode selected on driver station
* <li>DisabledPeriodic() called periodically as long as DS is disabled
* <li>when DS transitions from disabled to enabled, Start() is called once
* <li>while DS is enabled, Periodic() is called periodically and additional
* periodic callbacks added via GetCallbacks() are called periodically
* <li>when DS transitions from enabled to disabled, or a different opmode is
* selected while enabled, End() is called, followed by Close(); the
* object is not reused
* <li>if a different opmode is selected while disabled, only Close() is
* called; the object is not reused
* </ul>
*/
class OpMode {
public:
/**
* The object is destroyed when the opmode is no longer selected on the DS or
* after OpModeRun() returns.
* after an enabled run ends. The object will not be reused after the
* destructor is called.
*/
virtual ~OpMode() = default;
/**
* This function is called periodically while the opmode is selected on the DS
* (robot is disabled). Code that should only run once when the opmode is
* This function is called periodically while the opmode is selected on the
* DS (robot is disabled). Code that should only run once when the opmode is
* selected should go in the opmode constructor.
*/
virtual void DisabledPeriodic() {}
/**
* This function is called when the opmode starts (robot is enabled).
*
* @param opModeId opmode unique ID
*/
virtual void OpModeRun(int64_t opModeId) = 0;
/** Called once when this opmode transitions to enabled. */
virtual void Start() {}
/**
* This function is called asynchronously when the robot is disabled, to
* request the opmode return from OpModeRun().
* This function is called periodically while the opmode is enabled.
*/
virtual void OpModeStop() = 0;
virtual void Periodic() {}
/**
* This function is called when the robot disables or switches opmodes while
* this opmode is enabled. Implementations should stop blocking work
* promptly.
*/
virtual void End() {}
/**
* Returns a vector of custom periodic callbacks to be executed while the
* opmode is enabled.
*
* <p>This method allows opmodes to register arbitrary periodic callbacks
* with custom execution intervals. The callbacks are executed by the robot
* framework at their scheduled times, in addition to the primary Periodic()
* callback.
*
* @return A vector of custom callbacks to execute, or an empty vector if no
* custom callbacks are needed. The default implementation returns an
* empty vector.
*/
virtual std::vector<wpi::internal::PeriodicPriorityQueue::Callback>
GetCallbacks() {
return {};
}
};
} // namespace wpi

View File

@@ -4,29 +4,24 @@
#pragma once
#include <stdint.h>
#include <chrono>
#include <functional>
#include <utility>
#include <vector>
#include "wpi/hal/Notifier.h"
#include "wpi/hal/Types.hpp"
#include "wpi/internal/PeriodicPriorityQueue.hpp"
#include "wpi/opmode/OpMode.hpp"
#include "wpi/system/Watchdog.hpp"
#include "wpi/system/RobotController.hpp"
#include "wpi/units/time.hpp"
#include "wpi/util/priority_queue.hpp"
namespace wpi {
/**
* An opmode structure for periodic operation. This base class implements a loop
* that runs one or more functions periodically (on a set time interval aka loop
* period). The primary periodic callback function is the Periodic() function;
* the time interval for this callback is 20 ms by default, but may be changed
* via passing a different time interval to the constructor. Additional periodic
* callbacks with different intervals can be added using the AddPeriodic() set
* of functions.
* period). The primary periodic callback function is the abstract Periodic()
* function. Additional periodic callbacks with different intervals can be added
* using the AddPeriodic() set of functions.
*
* Lifecycle:
*
@@ -39,33 +34,25 @@ namespace wpi {
* - When DS transitions from disabled to enabled, Start() is called once
*
* - While DS is enabled, Periodic() is called periodically on the time interval
* set by the constructor, and additional periodic callbacks added via
* set by the robot framework, and additional periodic callbacks added via
* AddPeriodic() are called periodically on their set time intervals
*
* - When DS transitions from enabled to disabled, or a different opmode is
* selected on the driver station when the DS is enabled, End() is called,
* followed by the object being destroyed; the object is not reused
* followed by Close(); the object is not reused
*
* - If a different opmode is selected on the driver station when the DS is
* disabled, the object is destroyed (without End() being called); the object
* is not reused
* disabled, only Close() is called; the object is not reused
*/
class PeriodicOpMode : public OpMode {
public:
/** Default loop period. */
static constexpr auto DEFAULT_PERIOD = 20_ms;
protected:
/**
* Constructor. Periodic opmodes may specify the period used for the
* Periodic() function.
*
* @param period period for callbacks to the Periodic() function
* Constructor.
*/
explicit PeriodicOpMode(wpi::units::second_t period = DEFAULT_PERIOD);
PeriodicOpMode();
public:
~PeriodicOpMode() override;
~PeriodicOpMode() override = default;
/**
* Called periodically while the opmode is selected on the DS (robot is
@@ -77,35 +64,52 @@ class PeriodicOpMode : public OpMode {
* Called a single time when the robot transitions from disabled to enabled.
* This is called prior to Periodic() being called.
*/
virtual void Start() {}
void Start() override {}
/** Called periodically while the robot is enabled. */
virtual void Periodic() = 0;
/**
* Called periodically while the robot is enabled. The framework calls this
* at OpModeRobot's configured loop period.
*/
void Periodic() override {}
/**
* Called a single time when the robot transitions from enabled to disabled,
* or just before the destructor is called if a different opmode is selected
* while the robot is enabled.
* or just before Close() is called if a different opmode is selected while
* the robot is enabled.
*/
virtual void End() {}
void End() override {}
/**
* Return the system clock time in microseconds for the start of the current
* periodic loop. This is in the same time base as
* Timer::GetMonotonicTimestamp(), but is stable through a loop. It is updated
* at the beginning of every periodic callback (including the normal periodic
* loop).
* Returns the set of additional periodic callbacks registered via
* AddPeriodic(). These are executed by the robot framework while the opmode
* is enabled, in addition to the primary Periodic() callback.
*
* @return Robot running time in microseconds, as of the start of the current
* periodic function.
* @return The vector of additional periodic callbacks.
*/
int64_t GetLoopStartTime() const { return m_loopStartTimeUs; }
std::vector<wpi::internal::PeriodicPriorityQueue::Callback> GetCallbacks()
override {
return m_callbacks;
}
/**
* Add a callback to run at a specific period.
*
* This is scheduled on the same Notifier as Periodic(), so Periodic() and
* the callback run synchronously. Interactions between them are thread-safe.
*
* @param callback The callback to run.
* @param period The period at which to run the callback.
*/
void AddPeriodic(std::function<void()> callback,
wpi::units::second_t period) {
AddPeriodic(std::move(callback), period, period);
}
/**
* Add a callback to run at a specific period with a starting time offset.
*
* This is scheduled on the same Notifier as Periodic(), so Periodic() and the
* callback run synchronously. Interactions between them are thread-safe.
* This is scheduled on the same Notifier as Periodic(), so Periodic() and
* the callback run synchronously. Interactions between them are thread-safe.
*
* @param callback The callback to run.
* @param period The period at which to run the callback.
@@ -114,66 +118,11 @@ class PeriodicOpMode : public OpMode {
* to TimedRobot.
*/
void AddPeriodic(std::function<void()> callback, wpi::units::second_t period,
wpi::units::second_t offset = 0_s);
/**
* Gets time period between calls to Periodic() functions.
*/
wpi::units::second_t GetPeriod() const { return m_period; }
/**
* Prints list of epochs added so far and their times.
*/
void PrintWatchdogEpochs();
protected:
/** Loop function. */
void LoopFunc();
public:
// implements OpMode interface
void OpModeRun(int64_t opModeId) final;
void OpModeStop() final;
wpi::units::second_t offset);
private:
class Callback {
public:
std::function<void()> func;
std::chrono::microseconds period;
std::chrono::microseconds expirationTime;
/**
* Construct a callback container.
*
* @param func The callback to run.
* @param startTime The common starting point for all callback scheduling.
* @param period The period at which to run the callback.
* @param offset The offset from the common starting time.
*/
Callback(std::function<void()> func, std::chrono::microseconds startTime,
std::chrono::microseconds period,
std::chrono::microseconds offset);
bool operator>(const Callback& rhs) const {
return expirationTime > rhs.expirationTime;
}
};
int64_t m_opModeId;
bool m_running = true;
wpi::hal::Handle<HAL_NotifierHandle, HAL_DestroyNotifier> m_notifier;
std::vector<wpi::internal::PeriodicPriorityQueue::Callback> m_callbacks;
std::chrono::microseconds m_startTime;
int64_t m_loopStartTimeUs = 0;
wpi::units::second_t m_period;
Watchdog m_watchdog;
wpi::util::priority_queue<Callback, std::vector<Callback>,
std::greater<Callback>>
m_callbacks;
void PrintLoopOverrunMessage();
};
} // namespace wpi