[hal] Revamp notifiers (#8424)

This changes the HAL notifier interface to:
- Use wpiutil signal objects. This means waiting is done through the
`WPI_WaitObject` API instead of a dedicated function and allows for
higher level code to simultaneously wait on notifiers and other events.
- Interval timers are supported at the HAL layer
- Handlers are now required to acknowledge notifications. This is
invisible to users unless they're directly using the HAL API.
- For interval timers, an overrun count is maintained to detect if the
handler didn't acknowledge

The underlying implementation still uses condition variables for the
actual waiting. In basic testing using this approach seemed to be lower
jitter than timerfd.

Currently, the simulation and systemcore implementations are nearly
identical except for a few additional sim hook bits. This could be
refactored, but keeping them separate may make sense to keep the
systemcore implementation easy to read and reason about, or if we ever
choose to use a different underlying timer implementation on systemcore.

The simulation side API is unchanged in form but does change in
function--waiting for notifiers now only waits for currently running (or
newly signaled) notifiers to acknowledge. To avoid a race condition in
sim stepTiming, users of the low level API must make any alarm updates
(especially for one-shot alarms) prior to acknowledging the previous
alarm.

The only current use of the interval timer feature is the `Notifier`
class. The `TimedRobot` implementation still uses a single notifier and
its own interval timing logic to ensure consistent callback order. Using
separate notifiers for each user-level interval would substantially
increase complexity. `Watchdog` also doesn't use the interval timer, as
it's looking for an amount of time since the last `set` call rather than
a recurring interval time.

To reduce flicker, the sim GUI uses a fade out when a timeout goes from
set to unset.

This fixes tsan for wpilib and commands, and also fixes some spurious
test failures.
This commit is contained in:
Peter Johnson
2025-11-29 11:00:18 -08:00
committed by GitHub
parent 704b6ccaee
commit 02c8d5c9db
22 changed files with 738 additions and 712 deletions

View File

@@ -7,11 +7,12 @@
#include <utility>
#include <fmt/format.h>
#include "wpi/hal/Notifier.h"
#include "wpi/hal/Threads.h"
#include "wpi/system/Errors.hpp"
#include "wpi/system/Timer.hpp"
#include "wpi/util/Synchronization.h"
#include <pybind11/functional.h>
#include <gilsafe_object.h>
@@ -37,8 +38,8 @@ PyNotifier::PyNotifier(std::function<void()> handler) {
}
m_handler = handler;
int32_t status = 0;
m_notifier = HAL_InitializeNotifier(&status);
WPILIB_CheckErrorStatus(status, "InitializeNotifier");
m_notifier = HAL_CreateNotifier(&status);
WPILIB_CheckErrorStatus(status, "CreateNotifier");
std::function<void()> target([this] {
py::gil_scoped_release release;
@@ -50,8 +51,7 @@ PyNotifier::PyNotifier(std::function<void()> handler) {
if (notifier == 0) {
break;
}
uint64_t curTime = HAL_WaitForNotifierAlarm(notifier, &status);
if (curTime == 0 || status != 0) {
if (WPI_WaitForObject(notifier) == 0) {
break;
}
@@ -59,13 +59,6 @@ PyNotifier::PyNotifier(std::function<void()> handler) {
{
std::scoped_lock lock(m_processMutex);
handler = m_handler;
if (m_periodic) {
m_expirationTime += m_period;
UpdateAlarm();
} else {
// need to update the alarm to cause it to wait again
UpdateAlarm(UINT64_MAX);
}
}
// call callback
@@ -76,6 +69,10 @@ PyNotifier::PyNotifier(std::function<void()> handler) {
handler();
}
// Ack notifier
HAL_AcknowledgeNotifierAlarm(notifier, &status);
WPILIB_CheckErrorStatus(status, "AcknowledgeNotifier");
}
} catch (...) {
_hang_thread_if_finalizing();
@@ -95,27 +92,20 @@ PyNotifier::PyNotifier(std::function<void()> handler) {
}
PyNotifier::~PyNotifier() {
int32_t status = 0;
// atomically set handle to 0, then clean
HAL_NotifierHandle handle = m_notifier.exchange(0);
HAL_StopNotifier(handle, &status);
WPILIB_ReportError(status, "StopNotifier");
HAL_DestroyNotifier(handle);
// Join the thread to ensure the handler has exited.
if (m_thread) {
m_thread.attr("join")();
}
HAL_CleanNotifier(handle);
}
PyNotifier::PyNotifier(PyNotifier &&rhs)
: m_thread(std::move(rhs.m_thread)),
m_notifier(rhs.m_notifier.load()),
m_handler(std::move(rhs.m_handler)),
m_expirationTime(std::move(rhs.m_expirationTime)),
m_period(std::move(rhs.m_period)),
m_periodic(std::move(rhs.m_periodic)) {
m_handler(std::move(rhs.m_handler)) {
rhs.m_notifier = HAL_kInvalidHandle;
}
@@ -124,19 +114,12 @@ PyNotifier &PyNotifier::operator=(PyNotifier &&rhs) {
m_notifier = rhs.m_notifier.load();
rhs.m_notifier = HAL_kInvalidHandle;
m_handler = std::move(rhs.m_handler);
m_expirationTime = std::move(rhs.m_expirationTime);
m_period = std::move(rhs.m_period);
m_periodic = std::move(rhs.m_periodic);
return *this;
}
void PyNotifier::SetName(std::string_view name) {
fmt::memory_buffer buf;
fmt::format_to(fmt::appender{buf}, "{}", name);
buf.push_back('\0'); // null terminate
int32_t status = 0;
HAL_SetNotifierName(m_notifier, buf.data(), &status);
HAL_SetNotifierName(m_notifier, name, &status);
}
void PyNotifier::SetCallback(std::function<void()> handler) {
@@ -145,45 +128,31 @@ void PyNotifier::SetCallback(std::function<void()> handler) {
}
void PyNotifier::StartSingle(wpi::units::second_t delay) {
std::scoped_lock lock(m_processMutex);
m_periodic = false;
m_period = delay;
m_expirationTime = Timer::GetFPGATimestamp() + m_period;
UpdateAlarm();
int32_t status = 0;
HAL_SetNotifierAlarm(m_notifier, static_cast<uint64_t>(delay * 1e6), 0, false,
&status);
}
void PyNotifier::StartPeriodic(wpi::units::second_t period) {
std::scoped_lock lock(m_processMutex);
m_periodic = true;
m_period = period;
m_expirationTime = Timer::GetFPGATimestamp() + m_period;
UpdateAlarm();
int32_t status = 0;
HAL_SetNotifierAlarm(m_notifier, static_cast<uint64_t>(period * 1e6),
static_cast<uint64_t>(period * 1e6), false, &status);
}
void PyNotifier::Stop() {
std::scoped_lock lock(m_processMutex);
m_periodic = false;
int32_t status = 0;
HAL_CancelNotifierAlarm(m_notifier, &status);
WPILIB_CheckErrorStatus(status, "CancelNotifierAlarm");
}
void PyNotifier::UpdateAlarm(uint64_t triggerTime) {
int32_t PyNotifier::GetOverrun() const {
int32_t status = 0;
// Return if we are being destructed, or were not created successfully
auto notifier = m_notifier.load();
if (notifier == 0) {
return;
}
HAL_UpdateNotifierAlarm(notifier, triggerTime, &status);
WPILIB_CheckErrorStatus(status, "UpdateNotifierAlarm");
}
void PyNotifier::UpdateAlarm() {
UpdateAlarm(static_cast<uint64_t>(m_expirationTime * 1e6));
int32_t overrun = HAL_GetNotifierOverrun(m_notifier, &status);
WPILIB_CheckErrorStatus(status, "GetNotifierOverrun");
return overrun;
}
bool PyNotifier::SetHALThreadPriority(bool realTime, int32_t priority) {
int32_t status = 0;
return HAL_SetNotifierThreadPriority(realTime, priority, &status);
}
}

View File

@@ -91,6 +91,16 @@ class PyNotifier {
*/
void Stop();
/**
* Gets the overrun count.
*
* An overrun occurs when a notifier's alarm is not serviced before the next
* scheduled alarm time.
*
* @return overrun count
*/
int32_t GetOverrun() const;
/**
* Sets the HAL notifier thread priority.
*
@@ -109,18 +119,6 @@ class PyNotifier {
static bool SetHALThreadPriority(bool realTime, int32_t priority);
private:
/**
* Update the HAL alarm time.
*
* @param triggerTime the time at which the next alarm will be triggered
*/
void UpdateAlarm(uint64_t triggerTime);
/**
* Update the HAL alarm time based on m_expirationTime.
*/
void UpdateAlarm();
// The thread waiting on the HAL alarm
py::object m_thread;
@@ -132,15 +130,6 @@ private:
// Address of the handler
std::function<void()> m_handler;
// The absolute expiration time
wpi::units::second_t m_expirationTime = 0_s;
// The relative time (either periodic or single)
wpi::units::second_t m_period = 0_s;
// True if this is a periodic event
bool m_periodic = false;
};
} // namespace wpi