[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

@@ -4,11 +4,11 @@
#include "wpi/framework/OpModeRobot.hpp"
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <fmt/format.h>
@@ -17,206 +17,196 @@
#include "wpi/hal/DriverStationTypes.h"
#include "wpi/hal/HAL.h"
#include "wpi/hal/Notifier.hpp"
#include "wpi/hal/UsageReporting.hpp"
#include "wpi/nt/NetworkTableInstance.hpp"
#include "wpi/opmode/OpMode.hpp"
#include "wpi/smartdashboard/SmartDashboard.hpp"
#include "wpi/system/Errors.hpp"
#include "wpi/system/RobotController.hpp"
#include "wpi/util/SafeThread.hpp"
#include "wpi/util/Synchronization.h"
using namespace wpi;
namespace {
class MonitorThread : public wpi::util::SafeThreadEvent {
public:
MonitorThread(int64_t modeId, wpi::util::Event& dsEvent,
HAL_NotifierHandle notifier, std::weak_ptr<OpMode> activeOpMode)
: m_modeId{modeId},
m_dsEvent{dsEvent.GetHandle()},
m_notifier{notifier},
m_activeOpMode{std::move(activeOpMode)} {}
private:
void Main() override {
// Wait for DS to disable or change modes
WPI_EventHandle events[] = {m_dsEvent, m_stopEvent.GetHandle()};
WPI_Handle signaledBuf[2];
for (;;) {
auto signaled = wpi::util::WaitForObjects(events, signaledBuf);
if (signaled.empty()) {
return; // handles destroyed
}
for (auto signal : signaled) {
if ((signal & 0x80000000) != 0 || signal == m_stopEvent.GetHandle()) {
return; // handle destroyed or transitioned
}
}
// did the opmode or enable state change?
HAL_ControlWord word;
HAL_GetUncachedControlWord(&word);
if (!HAL_ControlWord_IsEnabled(word) ||
HAL_ControlWord_GetOpModeId(word) != m_modeId) {
break;
}
}
// call opmode stop
auto opMode = m_activeOpMode.lock();
if (opMode) {
opMode->OpModeStop();
}
events[0] = m_notifier;
int32_t status = 0;
HAL_SetNotifierAlarm(m_notifier, 1000000, 0, false, true, &status); // 1s
auto signaled = wpi::util::WaitForObjects(events, signaledBuf);
if (signaled.empty()) {
return; // handles destroyed
}
for (auto signal : signaled) {
if ((signal & 0x80000000) != 0 || signal == m_stopEvent.GetHandle()) {
return; // handle destroyed or transitioned
}
}
// if it hasn't transitioned after 1 second, terminate the program
WPILIB_ReportError(err::Error, "OpMode did not exit, terminating program");
HAL_Shutdown();
std::exit(0);
}
int64_t m_modeId;
WPI_EventHandle m_dsEvent;
HAL_NotifierHandle m_notifier;
std::weak_ptr<OpMode> m_activeOpMode;
};
} // namespace
void OpModeRobotBase::StartCompetition() {
fmt::print("********** Robot program startup complete **********\n");
wpi::util::Event event;
struct DSListener {
wpi::util::Event& event;
explicit DSListener(wpi::util::Event& event) : event{event} {
HAL_ProvideNewDataEventHandle(event.GetHandle());
}
~DSListener() { HAL_RemoveNewDataEventHandle(event.GetHandle()); }
} listener{event};
OpModeRobotBase::OpModeRobotBase(wpi::units::second_t period)
: m_period{period},
m_loopOverrunAlert{
fmt::format("Loop time of {:.6f}s overrun", m_period.value()),
Alert::Level::MEDIUM},
m_watchdog{period, [this] { m_loopOverrunAlert.Set(true); }} {
// Create our own notifier and callback queue
int32_t status = 0;
m_notifier = HAL_CreateNotifier(&status);
HAL_SetNotifierName(m_notifier, "OpModeRobot", &status);
int64_t lastModeId = -1;
bool calledObserveUserProgramStarting = false;
bool calledDriverStationConnected = false;
std::shared_ptr<OpMode> opMode;
WPI_EventHandle events[] = {event.GetHandle(),
static_cast<WPI_EventHandle>(m_notifier)};
WPI_Handle signaledBuf[2];
for (;;) {
// Wait for new data from the driver station, with 50 ms timeout
HAL_SetNotifierAlarm(m_notifier, 50000, 0, false, true, &status);
m_startTime = std::chrono::microseconds{RobotController::GetMonotonicTime()};
// Call DriverStation::ObserveUserProgramStarting() here as a one-shot to
// ensure it is called after the notifier alarm is set. The notifier alarm
// is set using relative time, so tests that wait on the user program to
// start and then step time won't work correctly if we call this before
// setting the alarm.
if (!calledObserveUserProgramStarting) {
calledObserveUserProgramStarting = true;
DriverStation::ObserveUserProgramStarting();
}
// Add LoopFunc as periodic callback
AddPeriodic([this] { LoopFunc(); }, period);
auto signaled = wpi::util::WaitForObjects(events, signaledBuf);
if (signaled.empty()) {
return; // handles destroyed
}
for (auto signal : signaled) {
if ((signal & 0x80000000) != 0) {
return; // handle destroyed
HAL_ReportUsage("Framework", "OpModeRobot");
}
OpModeRobotBase::OpModeRobotBase() : OpModeRobotBase(DEFAULT_PERIOD) {}
void OpModeRobotBase::AddPeriodic(std::function<void()> callback,
wpi::units::second_t period) {
m_callbacks.Add(std::move(callback), m_startTime, period);
}
void OpModeRobotBase::LoopFunc() {
DriverStation::RefreshData();
// Get current enabled state and opmode
const hal::ControlWord word = DriverStation::GetControlWord();
m_watchdog.Reset();
const bool enabled = word.IsEnabled();
int64_t modeId = word.IsDSAttached() ? word.GetOpModeId() : 0;
if (!m_calledDriverStationConnected && word.IsDSAttached()) {
m_calledDriverStationConnected = true;
DriverStationConnected();
m_watchdog.AddEpoch("DriverStationConnected()");
}
// Handle opmode changes
if (modeId != m_lastModeId) {
// Clean up current opmode
if (m_currentOpMode) {
// Remove opmode callbacks
if (m_opmodePeriodic) {
m_callbacks.Remove(*m_opmodePeriodic);
m_opmodePeriodic.reset();
}
}
// Get the latest control word and opmode
DriverStation::RefreshData();
hal::ControlWord ctlWord = DriverStation::GetControlWord();
if (!calledDriverStationConnected && ctlWord.IsDSAttached()) {
calledDriverStationConnected = true;
DriverStationConnected();
}
int64_t modeId;
if (!ctlWord.IsDSAttached()) {
modeId = 0;
} else {
modeId = ctlWord.GetOpModeId();
}
if (!opMode || modeId != lastModeId) {
if (opMode) {
// no or different opmode selected
opMode.reset();
}
if (modeId == 0) {
// no opmode selected
NonePeriodic();
HAL_ObserveUserProgram(ctlWord.GetValue());
continue;
for (auto& cb : m_activeOpModeCallbacks) {
m_callbacks.Remove(cb);
}
m_activeOpModeCallbacks.clear();
m_currentOpMode.reset();
}
// Set up new opmode
if (modeId != 0) {
auto data = m_opModes.lookup(modeId);
if (!data.factory) {
if (data.factory) {
// Instantiate the new opmode
fmt::print("********** Starting OpMode {} **********\n", data.name);
m_currentOpMode = data.factory();
if (m_currentOpMode) {
// Ensure disabledPeriodic is called at least once
m_currentOpMode->DisabledPeriodic();
m_watchdog.AddEpoch("OpMode::DisabledPeriodic()");
// Register the opmode's periodic callbacks
m_opmodePeriodic = wpi::internal::PeriodicPriorityQueue::Callback{
[op = m_currentOpMode.get()] { op->Periodic(); }, m_startTime,
m_period};
m_callbacks.Add(*m_opmodePeriodic);
m_activeOpModeCallbacks = m_currentOpMode->GetCallbacks();
for (auto& cb : m_activeOpModeCallbacks) {
m_callbacks.Add(cb);
}
}
} else {
WPILIB_ReportError(err::Error, "No OpMode found for mode {}", modeId);
ctlWord.SetOpModeId(0);
HAL_ObserveUserProgram(ctlWord.GetValue());
continue;
}
}
m_lastModeId = modeId;
}
// Instantiate the opmode
fmt::print("********** Starting OpMode {} **********\n", data.name);
opMode = data.factory();
if (!opMode) {
// could not construct
ctlWord.SetOpModeId(0);
HAL_ObserveUserProgram(ctlWord.GetValue());
continue;
// Handle enabled state changes
bool justCalledDisabledInit = false;
if (m_lastEnabledState != enabled) {
if (enabled) {
// Transitioning to enabled
DisabledExit();
m_watchdog.AddEpoch("DisabledExit()");
if (m_currentOpMode) {
m_currentOpMode->Start();
m_watchdog.AddEpoch("OpMode::Start()");
}
{
std::scoped_lock lock(m_opModeMutex);
m_activeOpMode = opMode;
} else {
// Transitioning to disabled
if (m_currentOpMode && m_lastEnabledState) {
// Was enabled, now disabled
m_currentOpMode->End();
m_watchdog.AddEpoch("OpMode::End()");
}
lastModeId = modeId;
// Ensure disabledPeriodic is called at least once
opMode->DisabledPeriodic();
DisabledInit();
m_watchdog.AddEpoch("DisabledInit()");
justCalledDisabledInit = true;
}
m_lastEnabledState = enabled;
}
// Call periodic functions based on current state
if (!enabled) {
// Only call DisabledPeriodic if we didn't just call DisabledInit
if (!justCalledDisabledInit) {
DisabledPeriodic();
m_watchdog.AddEpoch("DisabledPeriodic()");
}
HAL_ObserveUserProgram(ctlWord.GetValue());
// Call opmode DisabledPeriodic if we have one
if (m_currentOpMode) {
m_currentOpMode->DisabledPeriodic();
m_watchdog.AddEpoch("OpMode::DisabledPeriodic()");
}
}
if (ctlWord.IsEnabled()) {
// When enabled, call the opmode run function, then close and clear
wpi::util::SafeThreadOwner<MonitorThread> monitor;
monitor.Start(modeId, event, static_cast<HAL_NotifierHandle>(m_notifier),
opMode);
opMode->OpModeRun(modeId);
opMode.reset();
} else {
// When disabled, call the DisabledPeriodic function
opMode->DisabledPeriodic();
// Call NonePeriodic when no opmode is selected
if (modeId == 0) {
NonePeriodic();
m_watchdog.AddEpoch("NonePeriodic()");
}
// Always call RobotPeriodic
RobotPeriodic();
m_watchdog.AddEpoch("RobotPeriodic()");
// Always observe user program state
HAL_ObserveUserProgram(word.GetValue());
SmartDashboard::UpdateValues();
m_watchdog.AddEpoch("SmartDashboard::UpdateValues()");
if constexpr (IsSimulation()) {
HAL_SimPeriodicBefore();
SimulationPeriodic();
HAL_SimPeriodicAfter();
m_watchdog.AddEpoch("SimulationPeriodic()");
}
m_watchdog.Disable();
// Flush NetworkTables
wpi::nt::NetworkTableInstance::GetDefault().FlushLocal();
// Warn on loop time overruns
if (m_watchdog.IsExpired()) {
m_watchdog.PrintEpochs();
}
}
void OpModeRobotBase::StartCompetition() {
fmt::print("********** Robot program startup complete **********\n");
if constexpr (IsSimulation()) {
SimulationInit();
}
// Tell the DS that the robot is ready to be enabled
DriverStation::ObserveUserProgramStarting();
// Loop forever, calling the callback system which handles periodic functions
while (true) {
if (!m_callbacks.RunCallbacks(m_notifier)) {
break;
}
}
}
void OpModeRobotBase::EndCompetition() {
m_notifier = {};
std::shared_ptr<OpMode> opMode;
{
std::scoped_lock lock(m_opModeMutex);
opMode = m_activeOpMode.lock();
}
if (opMode) {
opMode->OpModeStop();
if (m_notifier != HAL_INVALID_HANDLE) {
HAL_DestroyNotifier(m_notifier);
}
}

View File

@@ -10,9 +10,10 @@
#include <utility>
#include "wpi/driverstation/DriverStation.hpp"
#include "wpi/hal/Notifier.hpp"
#include "wpi/hal/DriverStation.hpp"
#include "wpi/hal/UsageReporting.hpp"
#include "wpi/system/Errors.hpp"
#include "wpi/system/RobotController.hpp"
using namespace wpi;
@@ -27,45 +28,9 @@ void TimedRobot::StartCompetition() {
// Loop forever, calling the appropriate mode-dependent function
while (true) {
// We don't have to check there's an element in the queue first because
// there's always at least one (the constructor adds one). It's reenqueued
// at the end of the loop.
auto callback = m_callbacks.pop();
int32_t status = 0;
HAL_SetNotifierAlarm(m_notifier, callback.expirationTime.count(), 0, true,
true, &status);
WPILIB_CheckErrorStatus(status, "SetNotifierAlarm");
if (WPI_WaitForObject(m_notifier) == 0) {
if (!m_callbacks.RunCallbacks(m_notifier)) {
break;
}
m_loopStartTimeUs = RobotController::GetMonotonicTime();
std::chrono::microseconds currentTime{m_loopStartTimeUs};
callback.func();
// Increment the expiration time by the number of full periods it's behind
// plus one to avoid rapid repeat fires from a large loop overrun. We assume
// currentTime ≥ expirationTime rather than checking for it since the
// callback wouldn't be running otherwise.
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_callbacks.push(std::move(callback));
// Process all other callbacks that are ready to run
while (m_callbacks.top().expirationTime <= currentTime) {
callback = m_callbacks.pop();
callback.func();
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_callbacks.push(std::move(callback));
}
}
}
@@ -96,15 +61,8 @@ TimedRobot::~TimedRobot() {
}
}
uint64_t TimedRobot::GetLoopStartTime() {
return m_loopStartTimeUs;
}
void TimedRobot::AddPeriodic(std::function<void()> callback,
wpi::units::second_t period,
wpi::units::second_t offset) {
m_callbacks.emplace(
callback, m_startTime,
std::chrono::microseconds{static_cast<int64_t>(period.value() * 1e6)},
std::chrono::microseconds{static_cast<int64_t>(offset.value() * 1e6)});
m_callbacks.Add(std::move(callback), m_startTime, period, offset);
}

View File

@@ -0,0 +1,127 @@
// 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.
#include "wpi/internal/PeriodicPriorityQueue.hpp"
#include <utility>
#include "wpi/hal/Notifier.h"
#include "wpi/system/Errors.hpp"
#include "wpi/system/RobotController.hpp"
#include "wpi/util/Synchronization.h"
using namespace wpi::internal;
PeriodicPriorityQueue::Callback::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{RobotController::GetMonotonicTime()} -
startTime) /
period * period) {}
PeriodicPriorityQueue::Callback::Callback(std::function<void()> func,
std::chrono::microseconds startTime,
wpi::units::second_t period,
wpi::units::second_t offset)
: Callback{
std::move(func), startTime,
std::chrono::microseconds{static_cast<int64_t>(period.value() * 1e6)},
std::chrono::microseconds{
static_cast<int64_t>(offset.value() * 1e6)}} {}
PeriodicPriorityQueue::Callback::Callback(std::function<void()> func,
std::chrono::microseconds startTime,
wpi::units::second_t period)
: Callback{std::move(func), startTime, period,
std::chrono::microseconds{0}} {}
void PeriodicPriorityQueue::Add(std::function<void()> func,
std::chrono::microseconds startTime,
std::chrono::microseconds period) {
Add(std::move(func), startTime, period, std::chrono::microseconds{0});
}
void PeriodicPriorityQueue::Add(std::function<void()> func,
std::chrono::microseconds startTime,
std::chrono::microseconds period,
std::chrono::microseconds offset) {
m_queue.emplace(std::move(func), startTime, period, offset);
}
void PeriodicPriorityQueue::Add(std::function<void()> func,
std::chrono::microseconds startTime,
wpi::units::second_t period) {
Add(std::move(func), startTime, period, wpi::units::second_t{0});
}
void PeriodicPriorityQueue::Add(std::function<void()> func,
std::chrono::microseconds startTime,
wpi::units::second_t period,
wpi::units::second_t offset) {
m_queue.emplace(std::move(func), startTime, period, offset);
}
void PeriodicPriorityQueue::Add(Callback callback) {
m_queue.push(std::move(callback));
}
bool PeriodicPriorityQueue::Remove(const Callback& callback) {
return m_queue.remove(callback);
}
void PeriodicPriorityQueue::Clear() {
while (!m_queue.empty()) {
m_queue.pop();
}
}
bool PeriodicPriorityQueue::RunCallbacks(HAL_NotifierHandle notifier) {
// We don't have to check there's an element in the queue first because
// there's always at least one (the constructor adds one). It's reenqueued
// at the end of the loop.
auto callback = m_queue.pop();
int32_t status = 0;
HAL_SetNotifierAlarm(notifier, callback.expirationTime.count(), 0, true, true,
&status);
WPILIB_CheckErrorStatus(status, "SetNotifierAlarm");
if (WPI_WaitForObject(notifier) == 0) {
return false;
}
const std::chrono::microseconds currentTime{
RobotController::GetMonotonicTime()};
m_loopStartTime = wpi::units::microsecond_t{currentTime};
callback.func();
// Increment the expiration time by the number of full periods it's behind
// plus one to avoid rapid repeat fires from a large loop overrun. We assume
// currentTime >= expirationTime rather than checking for it since the
// callback wouldn't be running otherwise.
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_queue.push(std::move(callback));
// Process all other callbacks that are ready to run
while (m_queue.top().expirationTime <= currentTime) {
callback = m_queue.pop();
callback.func();
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_queue.push(std::move(callback));
}
return true;
}

View File

@@ -6,154 +6,22 @@
#include <utility>
#include "wpi/driverstation/DriverStation.hpp"
#include "wpi/hal/DriverStation.h"
#include "wpi/hal/Notifier.hpp"
#include "wpi/hal/UsageReporting.hpp"
#include "wpi/nt/NetworkTableInstance.hpp"
#include "wpi/smartdashboard/SmartDashboard.hpp"
#include "wpi/system/Errors.hpp"
#include "wpi/system/RobotController.hpp"
using namespace wpi;
PeriodicOpMode::Callback::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) {}
PeriodicOpMode::~PeriodicOpMode() {
if (m_notifier != HAL_INVALID_HANDLE) {
HAL_DestroyNotifier(m_notifier);
}
}
PeriodicOpMode::PeriodicOpMode(wpi::units::second_t period)
: m_period{period},
m_watchdog(period, [this] { PrintLoopOverrunMessage(); }) {
m_startTime = std::chrono::microseconds{RobotController::GetMonotonicTime()};
AddPeriodic([=, this] { LoopFunc(); }, period);
int32_t status = 0;
m_notifier = HAL_CreateNotifier(&status);
WPILIB_CheckErrorStatus(status, "CreateNotifier");
HAL_SetNotifierName(m_notifier, "PeriodicOpMode", &status);
PeriodicOpMode::PeriodicOpMode()
: m_startTime{
std::chrono::microseconds{RobotController::GetMonotonicTime()}} {
HAL_ReportUsage("OpMode", "PeriodicOpMode");
}
void PeriodicOpMode::AddPeriodic(std::function<void()> callback,
wpi::units::second_t period,
wpi::units::second_t offset) {
m_callbacks.emplace(
callback, m_startTime,
m_callbacks.emplace_back(
std::move(callback), m_startTime,
std::chrono::microseconds{static_cast<int64_t>(period.value() * 1e6)},
std::chrono::microseconds{static_cast<int64_t>(offset.value() * 1e6)});
}
void PeriodicOpMode::LoopFunc() {
DriverStation::RefreshData();
HAL_ControlWord word;
HAL_GetControlWord(&word);
HAL_ControlWord_SetOpModeId(&word, m_opModeId);
HAL_ObserveUserProgram(word);
if (!DriverStation::IsEnabled() ||
DriverStation::GetOpModeId() != m_opModeId) {
m_running = false;
return;
}
m_watchdog.Reset();
Periodic();
m_watchdog.AddEpoch("Periodic()");
SmartDashboard::UpdateValues();
m_watchdog.AddEpoch("SmartDashboard::UpdateValues()");
// if constexpr (IsSimulation()) {
// HAL_SimPeriodicBefore();
// SimulationPeriodic();
// HAL_SimPeriodicAfter();
// m_watchdog.AddEpoch("SimulationPeriodic()");
// }
m_watchdog.Disable();
// Flush NetworkTables
nt::NetworkTableInstance::GetDefault().FlushLocal();
// Warn on loop time overruns
if (m_watchdog.IsExpired()) {
m_watchdog.PrintEpochs();
}
}
void PeriodicOpMode::OpModeRun(int64_t opModeId) {
m_opModeId = opModeId;
Start();
while (m_running) {
// We don't have to check there's an element in the queue first because
// there's always at least one (the constructor adds one). It's reenqueued
// at the end of the loop.
auto callback = m_callbacks.pop();
int32_t status = 0;
HAL_SetNotifierAlarm(m_notifier, callback.expirationTime.count(), 0, true,
true, &status);
WPILIB_CheckErrorStatus(status, "SetNotifierAlarm");
if (WPI_WaitForObject(m_notifier) == 0) {
break;
}
m_loopStartTimeUs = RobotController::GetMonotonicTime();
std::chrono::microseconds currentTime{m_loopStartTimeUs};
callback.func();
// Increment the expiration time by the number of full periods it's behind
// plus one to avoid rapid repeat fires from a large loop overrun. We assume
// currentTime ≥ expirationTime rather than checking for it since the
// callback wouldn't be running otherwise.
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_callbacks.push(std::move(callback));
// Process all other callbacks that are ready to run
while (m_callbacks.top().expirationTime <= currentTime) {
callback = m_callbacks.pop();
callback.func();
callback.expirationTime +=
callback.period + (currentTime - callback.expirationTime) /
callback.period * callback.period;
m_callbacks.push(std::move(callback));
}
}
End();
}
void PeriodicOpMode::OpModeStop() {
HAL_DestroyNotifier(m_notifier);
m_notifier = HAL_INVALID_HANDLE;
}
void PeriodicOpMode::PrintLoopOverrunMessage() {
WPILIB_ReportWarning("Loop time of {:.6f}s overrun", m_period.value());
}
void PeriodicOpMode::PrintWatchdogEpochs() {
m_watchdog.PrintEpochs();
}

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