[hal,wpilib] Move Alert to HAL (#8646)

SystemCore implementation is not yet connected to MRCComm.
This commit is contained in:
Peter Johnson
2026-03-03 21:58:47 -07:00
committed by GitHub
parent f4935a2ea9
commit 733cfa4b07
33 changed files with 1719 additions and 1121 deletions

View File

@@ -0,0 +1,52 @@
// 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/driverstation/Alert.hpp"
#include <stdint.h>
#include <string>
#include "wpi/util/string.h"
using namespace wpi;
static HAL_AlertHandle CreateAlert(std::string_view group,
std::string_view text, Alert::Level level) {
WPI_String wpiGroup = wpi::util::make_string(group);
WPI_String wpiText = wpi::util::make_string(text);
int32_t status = 0;
return HAL_CreateAlert(&wpiGroup, &wpiText, static_cast<int32_t>(level),
&status);
}
Alert::Alert(std::string_view text, Level type) : Alert("Alerts", text, type) {}
Alert::Alert(std::string_view group, std::string_view text, Level type)
: m_handle{CreateAlert(group, text, type)} {}
void Alert::Set(bool active) {
int32_t status = 0;
HAL_SetAlertActive(m_handle, active, &status);
}
bool Alert::Get() const {
int32_t status = 0;
return HAL_IsAlertActive(m_handle, &status);
}
void Alert::SetText(std::string_view text) {
WPI_String wpiText = wpi::util::make_string(text);
int32_t status = 0;
HAL_SetAlertText(m_handle, &wpiText, &status);
}
std::string Alert::GetText() const {
WPI_String wpiText;
int32_t status = 0;
HAL_GetAlertText(m_handle, &wpiText, &status);
std::string rv{wpiText.str, wpiText.len};
WPI_FreeString(&wpiText);
return rv;
}

View File

@@ -0,0 +1,40 @@
// 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/simulation/AlertSim.hpp"
#include <string>
#include <vector>
#include "wpi/hal/simulation/AlertData.h"
#include "wpi/util/string.h"
using namespace wpi;
using namespace wpi::sim;
int32_t AlertSim::GetCount() {
return HALSIM_GetNumAlerts();
}
std::vector<AlertSim::AlertInfo> AlertSim::GetAll() {
int32_t allocLen = HALSIM_GetNumAlerts();
HALSIM_AlertInfo* cInfos = new HALSIM_AlertInfo[allocLen];
int32_t len = HALSIM_GetAlerts(cInfos, allocLen);
std::vector<AlertInfo> infos;
infos.reserve(len);
for (int32_t i = 0; i < len; ++i) {
const auto& cInfo = cInfos[i];
infos.emplace_back(
cInfo.handle, std::string{wpi::util::to_string_view(&cInfo.group)},
std::string{wpi::util::to_string_view(&cInfo.text)},
cInfo.activeStartTime, static_cast<Alert::Level>(cInfo.level));
}
HALSIM_FreeAlerts(cInfos, len < allocLen ? len : allocLen);
delete[] cInfos;
return infos;
}
void AlertSim::ResetData() {
HALSIM_ResetAlertData();
}

View File

@@ -1,187 +0,0 @@
// 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/util/Alert.hpp"
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <fmt/format.h>
#include "wpi/nt/NTSendable.hpp"
#include "wpi/nt/NTSendableBuilder.hpp"
#include "wpi/smartdashboard/SmartDashboard.hpp"
#include "wpi/system/Errors.hpp"
#include "wpi/system/RobotController.hpp"
#include "wpi/util/StringMap.hpp"
#include "wpi/util/sendable/SendableHelper.hpp"
#include "wpi/util/sendable/SendableRegistry.hpp"
using namespace wpi;
class Alert::PublishedAlert {
public:
PublishedAlert(uint64_t timestamp, std::string_view text)
: timestamp{timestamp}, text{text} {}
uint64_t timestamp;
std::string text;
auto operator<=>(const PublishedAlert& other) const {
if (timestamp != other.timestamp) {
return other.timestamp <=> timestamp;
} else {
return text <=> other.text;
}
}
};
class Alert::SendableAlerts : public wpi::nt::NTSendable,
public wpi::util::SendableHelper<SendableAlerts> {
public:
SendableAlerts() { m_alerts.fill({}); }
void InitSendable(wpi::nt::NTSendableBuilder& builder) override {
builder.SetSmartDashboardType("Alerts");
builder.AddStringArrayProperty(
"errors", [this]() { return GetStrings(AlertType::kError); }, nullptr);
builder.AddStringArrayProperty(
"warnings", [this]() { return GetStrings(AlertType::kWarning); },
nullptr);
builder.AddStringArrayProperty(
"infos", [this]() { return GetStrings(AlertType::kInfo); }, nullptr);
}
/**
* Returns a reference to the set of active alerts for the given type.
* @param type the type
* @return reference to the set of active alerts for the type
*/
std::set<PublishedAlert>& GetActiveAlertsStorage(AlertType type) {
return const_cast<std::set<Alert::PublishedAlert>&>(
std::as_const(*this).GetActiveAlertsStorage(type));
}
const std::set<PublishedAlert>& GetActiveAlertsStorage(AlertType type) const {
switch (type) {
case AlertType::kInfo:
case AlertType::kWarning:
case AlertType::kError:
return m_alerts[static_cast<int32_t>(type)];
default:
throw WPILIB_MakeError(wpi::err::InvalidParameter,
"Invalid Alert Type: {}", type);
}
}
/**
* Returns the SendableAlerts for a given group, initializing and publishing
* if it does not already exist.
* @param group the group name
* @return the SendableAlerts for the group
*/
static SendableAlerts& ForGroup(std::string_view group) {
SendableAlerts* salert = nullptr;
try {
auto* sendable = wpi::SmartDashboard::GetData(group);
salert = dynamic_cast<SendableAlerts*>(sendable);
} catch (wpi::RuntimeError&) {
}
if (!salert) {
// this leaks if ResetSmartDashboardInstance is called, but that's fine
salert = new Alert::SendableAlerts;
wpi::SmartDashboard::PutData(group, salert);
}
return *salert;
}
private:
std::vector<std::string> GetStrings(AlertType type) const {
auto& set = GetActiveAlertsStorage(type);
std::vector<std::string> output;
output.reserve(set.size());
for (auto& alert : set) {
output.emplace_back(alert.text);
}
return output;
}
std::array<std::set<PublishedAlert>, 3> m_alerts;
};
Alert::Alert(std::string_view text, AlertType type)
: Alert("Alerts", text, type) {}
Alert::Alert(std::string_view group, std::string_view text, AlertType type)
: m_type(type),
m_text(text),
m_activeAlerts{
&SendableAlerts::ForGroup(group).GetActiveAlertsStorage(m_type)} {}
Alert::Alert(Alert&& other)
: m_type{other.m_type},
m_text{std::move(other.m_text)},
m_activeAlerts{std::exchange(other.m_activeAlerts, nullptr)},
m_active{std::exchange(other.m_active, false)},
m_activeStartTime{other.m_activeStartTime} {}
Alert& Alert::operator=(Alert&& other) {
if (&other != this) {
// We want to destroy current state after the move is done
Alert tmp{std::move(*this)};
// Now, swap moved-from state with other state
std::swap(m_type, other.m_type);
std::swap(m_text, other.m_text);
std::swap(m_activeAlerts, other.m_activeAlerts);
std::swap(m_active, other.m_active);
std::swap(m_activeStartTime, other.m_activeStartTime);
}
return *this;
}
Alert::~Alert() {
Set(false);
}
void Alert::Set(bool active) {
if (active == m_active) {
return;
}
if (active) {
m_activeStartTime = wpi::RobotController::GetTime();
m_activeAlerts->emplace(m_activeStartTime, m_text);
} else {
m_activeAlerts->erase({m_activeStartTime, m_text});
}
m_active = active;
}
void Alert::SetText(std::string_view text) {
if (text == m_text) {
return;
}
std::string oldText = std::move(m_text);
m_text = text;
if (m_active) {
auto iter = m_activeAlerts->find({m_activeStartTime, oldText});
auto hint = m_activeAlerts->erase(iter);
m_activeAlerts->emplace_hint(hint, m_activeStartTime, m_text);
}
}
std::string wpi::format_as(Alert::AlertType type) {
switch (type) {
case Alert::AlertType::kInfo:
return "kInfo";
case Alert::AlertType::kWarning:
return "kWarning";
case Alert::AlertType::kError:
return "kError";
default:
return std::to_string(static_cast<int>(type));
}
}

View File

@@ -4,30 +4,28 @@
#pragma once
#include <stdint.h>
#include <set>
#include <string>
#include <string_view>
#include "wpi/hal/Alert.h"
#include "wpi/hal/Types.h"
namespace wpi {
/**
* Persistent alert to be sent via NetworkTables. Alerts are tagged with a type
* of kError, kWarning, or kInfo to denote urgency. See Alert::AlertType for
* suggested usage of each type. Alerts can be displayed on supported
* dashboards, and are shown in a priority order based on type and recency of
* activation, with newly activated alerts first.
* Persistent alert to be sent to the driver station. Alerts are tagged with a
* type of HIGH/ERROR, MEDIUM/WARNING, or LOW/INFO to denote urgency. See
* Alert::Level for suggested usage of each type. Alerts can be displayed on
* supported dashboards, and are shown in a priority order based on type and
* recency of activation, with newly activated alerts first.
*
* Alerts should be created once and stored persistently, then updated to
* "active" or "inactive" as necessary. Set(bool) can be safely called
* periodically.
*
* This API is new for 2025, but is likely to change in future seasons to
* facilitate deeper integration with the robot control system.
*
* <pre>
* class Robot {
* wpi::Alert alert{"Something went wrong", wpi::Alert::AlertType::kWarning};
* wpi::Alert alert{"Something went wrong", wpi::Alert::Level::WARNING};
* }
*
* Robot::periodic() {
@@ -40,28 +38,36 @@ class Alert {
/**
* Represents an alert's level of urgency.
*/
enum class AlertType {
enum class Level {
/**
* High priority alert - displayed first on the dashboard with a red "X"
* High priority alert - displayed first with a red "X"
* symbol. Use this type for problems which will seriously affect the
* robot's functionality and thus require immediate attention.
*/
kError,
HIGH = HAL_ALERT_HIGH,
/** Alternate name for a high priority alert. */
ERROR = HIGH,
/**
* Medium priority alert - displayed second on the dashboard with a yellow
* "!" symbol. Use this type for problems which could affect the robot's
* functionality but do not necessarily require immediate attention.
* Medium priority alert - displayed second with a yellow "!" symbol.
* Use this type for problems which could affect the robot's functionality
* but do not necessarily require immediate attention.
*/
kWarning,
MEDIUM = HAL_ALERT_MEDIUM,
/** Alternate name for a medium priority alert. */
WARNING = MEDIUM,
/**
* Low priority alert - displayed last on the dashboard with a green "i"
* symbol. Use this type for problems which are unlikely to affect the
* robot's functionality, or any other alerts which do not fall under the
* other categories.
* Low priority alert - displayed last with a green "i" symbol. Use this
* type for problems which are unlikely to affect the robot's functionality,
* or any other alerts which do not fall under the other categories.
*/
kInfo
LOW = HAL_ALERT_LOW,
/** Alternate name for a low priority alert. */
INFO = LOW
};
/**
@@ -69,9 +75,9 @@ class Alert {
* to be instantiated, the appropriate entries will be added to NetworkTables.
*
* @param text Text to be displayed when the alert is active.
* @param type Alert urgency level.
* @param level Alert urgency level.
*/
Alert(std::string_view text, AlertType type);
Alert(std::string_view text, Level level);
/**
* Creates a new alert. If this is the first to be instantiated in its group,
@@ -79,17 +85,9 @@ class Alert {
*
* @param group Group identifier, used as the entry name in NetworkTables.
* @param text Text to be displayed when the alert is active.
* @param type Alert urgency level.
* @param level Alert urgency level.
*/
Alert(std::string_view group, std::string_view text, AlertType type);
Alert(Alert&&);
Alert& operator=(Alert&&);
Alert(const Alert&) = default;
Alert& operator=(const Alert&) = default;
~Alert();
Alert(std::string_view group, std::string_view text, Level level);
/**
* Sets whether the alert should currently be displayed. This method can be
@@ -103,7 +101,7 @@ class Alert {
* Gets whether the alert is active.
* @return whether the alert is active.
*/
bool Get() const { return m_active; }
bool Get() const;
/**
* Updates current alert text. Use this method to dynamically change the
@@ -117,25 +115,17 @@ class Alert {
* Gets the current alert text.
* @return the current text.
*/
std::string GetText() const { return m_text; }
std::string GetText() const;
/**
* Get the type of this alert.
* @return the type
*/
AlertType GetType() const { return m_type; }
Level GetType() const { return m_type; }
private:
class PublishedAlert;
class SendableAlerts;
AlertType m_type;
std::string m_text;
std::set<PublishedAlert>* m_activeAlerts;
bool m_active = false;
uint64_t m_activeStartTime;
Level m_type;
wpi::hal::Handle<HAL_AlertHandle, HAL_DestroyAlert> m_handle;
};
std::string format_as(Alert::AlertType type);
} // namespace wpi

View File

@@ -0,0 +1,70 @@
// 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 <stdint.h>
#include <string>
#include <vector>
#include "wpi/driverstation/Alert.hpp"
#include "wpi/hal/Types.h"
namespace wpi::sim {
/**
* Class to get info on simulated alerts.
*/
class AlertSim final {
public:
AlertSim() = delete;
/** Information about an alert. */
struct AlertInfo {
/** The handle of the alert. */
HAL_AlertHandle handle;
/** The group of the alert. */
std::string group;
/** The text of the alert. */
std::string text;
/** The time the alert became active. 0 if not active. */
int64_t activeStartTime;
/** The level of the alert (HIGH, MEDIUM, or LOW). */
Alert::Level level;
/**
* Returns whether the alert is currently active.
*
* @return true if the alert is active, false otherwise
*/
bool isActive() const { return activeStartTime != 0; }
};
/**
* Gets the number of alerts. Note: this is not guaranteed to be consistent
* with the number of alerts returned by GetAll.
*
* @return the number of alerts
*/
static int32_t GetCount();
/**
* Gets detailed information about each alert.
*
* @return Alerts
*/
static std::vector<AlertInfo> GetAll();
/**
* Resets all alert simulation data.
*/
static void ResetData();
};
} // namespace wpi::sim

View File

@@ -105,6 +105,7 @@ MecanumDrive = "wpi/drive/MecanumDrive.hpp"
RobotDriveBase = "wpi/drive/RobotDriveBase.hpp"
# wpi/driverstation
Alert = "wpi/driverstation/Alert.hpp"
DriverStation = "wpi/driverstation/DriverStation.hpp"
Gamepad = "wpi/driverstation/Gamepad.hpp"
GenericHID = "wpi/driverstation/GenericHID.hpp"
@@ -228,7 +229,6 @@ Tracer = "wpi/system/Tracer.hpp"
Watchdog = "wpi/system/Watchdog.hpp"
# wpi/util
Alert = "wpi/util/Alert.hpp"
Preferences = "wpi/util/Preferences.hpp"
SensorUtil = "wpi/util/SensorUtil.hpp"
@@ -243,6 +243,7 @@ yaml_path = "semiwrap/simulation"
# wpi/simulation
ADXL345Sim = "wpi/simulation/ADXL345Sim.hpp"
AddressableLEDSim = "wpi/simulation/AddressableLEDSim.hpp"
AlertSim = "wpi/simulation/AlertSim.hpp"
AnalogEncoderSim = "wpi/simulation/AnalogEncoderSim.hpp"
AnalogInputSim = "wpi/simulation/AnalogInputSim.hpp"
BatterySim = "wpi/simulation/BatterySim.hpp"

View File

@@ -1,16 +1,12 @@
functions:
format_as:
ignore: true
classes:
wpi::Alert:
enums:
AlertType:
Level:
methods:
Alert:
overloads:
std::string_view, AlertType:
std::string_view, std::string_view, AlertType:
std::string_view, Level:
std::string_view, std::string_view, Level:
Set:
Get:
SetText:

View File

@@ -0,0 +1,24 @@
classes:
wpi::sim::AlertSim:
methods:
GetCount:
GetAll:
ResetData:
wpi::sim::AlertSim::AlertInfo:
attributes:
handle:
group:
text:
activeStartTime:
level:
ignore: true
methods:
isActive:
inline_code: |
.def_property(
"level",
[](const AlertInfo& self) { return self.level; },
[](AlertInfo& self, wpi::Alert::Level level) { self.level = level; },
py::return_value_policy::copy,
py::doc("The level of the alert (HIGH, MEDIUM, or LOW).")
);

View File

@@ -4,6 +4,7 @@ from . import _init__simulation
from ._simulation import (
ADXL345Sim,
AddressableLEDSim,
AlertSim,
AnalogEncoderSim,
AnalogInputSim,
BatterySim,

View File

@@ -1,250 +0,0 @@
// 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/util/Alert.hpp"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include <fmt/format.h>
#include <gtest/gtest.h>
#include "wpi/nt/NetworkTableInstance.hpp"
#include "wpi/nt/StringArrayTopic.hpp"
#include "wpi/simulation/SimHooks.hpp"
#include "wpi/smartdashboard/SmartDashboard.hpp"
using namespace wpi;
using enum Alert::AlertType;
class AlertsTest : public ::testing::Test {
public:
~AlertsTest() override {
// test all destructors
Update();
EXPECT_EQ(GetSubscriberForType(kError).Get().size(), 0ul);
EXPECT_EQ(GetSubscriberForType(kWarning).Get().size(), 0ul);
EXPECT_EQ(GetSubscriberForType(kInfo).Get().size(), 0ul);
}
std::string GetGroupName() {
const ::testing::TestInfo* testInfo =
::testing::UnitTest::GetInstance()->current_test_info();
return fmt::format("{}_{}", testInfo->test_suite_name(), testInfo->name());
}
template <typename... Args>
Alert MakeAlert(Args&&... args) {
return Alert(GetGroupName(), std::forward<Args>(args)...);
}
std::vector<std::string> GetActiveAlerts(Alert::AlertType type) {
Update();
return GetSubscriberForType(type).Get();
}
bool IsAlertActive(std::string_view text, Alert::AlertType type) {
auto activeAlerts = GetActiveAlerts(type);
return std::find(activeAlerts.begin(), activeAlerts.end(), text) !=
activeAlerts.end();
}
void Update() { wpi::SmartDashboard::UpdateValues(); }
private:
std::string GetSubtableName(Alert::AlertType type) {
switch (type) {
case kError:
return "errors";
case kWarning:
return "warnings";
case kInfo:
return "infos";
default:
return "unknown";
}
}
const wpi::nt::StringArraySubscriber GetSubscriberForType(
Alert::AlertType type) {
return wpi::nt::NetworkTableInstance::GetDefault()
.GetStringArrayTopic(fmt::format("/SmartDashboard/{}/{}",
GetGroupName(), GetSubtableName(type)))
.Subscribe({});
}
};
#define EXPECT_STATE(type, ...) \
EXPECT_EQ(GetActiveAlerts(type), (std::vector<std::string>{__VA_ARGS__}))
TEST_F(AlertsTest, SetUnsetSingle) {
auto one = MakeAlert("one", kInfo);
EXPECT_FALSE(IsAlertActive("one", kInfo));
one.Set(true);
EXPECT_TRUE(IsAlertActive("one", kInfo));
one.Set(false);
EXPECT_FALSE(IsAlertActive("one", kInfo));
}
TEST_F(AlertsTest, SetUnsetMultiple) {
auto one = MakeAlert("one", kError);
auto two = MakeAlert("two", kInfo);
EXPECT_FALSE(IsAlertActive("one", kError));
EXPECT_FALSE(IsAlertActive("two", kInfo));
one.Set(true);
EXPECT_TRUE(IsAlertActive("one", kError));
EXPECT_FALSE(IsAlertActive("two", kInfo));
one.Set(true);
two.Set(true);
EXPECT_TRUE(IsAlertActive("one", kError));
EXPECT_TRUE(IsAlertActive("two", kInfo));
one.Set(false);
EXPECT_FALSE(IsAlertActive("one", kError));
EXPECT_TRUE(IsAlertActive("two", kInfo));
}
TEST_F(AlertsTest, SetIsIdempotent) {
auto a = MakeAlert("A", kInfo);
auto b = MakeAlert("B", kInfo);
auto c = MakeAlert("C", kInfo);
a.Set(true);
b.Set(true);
c.Set(true);
const auto startState = GetActiveAlerts(kInfo);
b.Set(true);
EXPECT_STATE(kInfo, startState);
a.Set(true);
EXPECT_STATE(kInfo, startState);
}
TEST_F(AlertsTest, DestructorUnsetsAlert) {
{
auto alert = MakeAlert("alert", kWarning);
alert.Set(true);
EXPECT_TRUE(IsAlertActive("alert", kWarning));
}
EXPECT_FALSE(IsAlertActive("alert", kWarning));
}
TEST_F(AlertsTest, SetTextWhileUnset) {
auto alert = MakeAlert("BEFORE", kInfo);
EXPECT_EQ("BEFORE", alert.GetText());
alert.Set(true);
EXPECT_TRUE(IsAlertActive("BEFORE", kInfo));
alert.Set(false);
EXPECT_FALSE(IsAlertActive("BEFORE", kInfo));
alert.SetText("AFTER");
EXPECT_EQ("AFTER", alert.GetText());
alert.Set(true);
EXPECT_FALSE(IsAlertActive("BEFORE", kInfo));
EXPECT_TRUE(IsAlertActive("AFTER", kInfo));
}
TEST_F(AlertsTest, SetTextWhileSet) {
auto alert = MakeAlert("BEFORE", kInfo);
EXPECT_EQ("BEFORE", alert.GetText());
alert.Set(true);
EXPECT_TRUE(IsAlertActive("BEFORE", kInfo));
alert.SetText("AFTER");
EXPECT_EQ("AFTER", alert.GetText());
EXPECT_FALSE(IsAlertActive("BEFORE", kInfo));
EXPECT_TRUE(IsAlertActive("AFTER", kInfo));
}
TEST_F(AlertsTest, SetTextDoesNotAffectFirstOrderSort) {
wpi::sim::PauseTiming();
auto a = MakeAlert("A", kError);
auto b = MakeAlert("B", kError);
auto c = MakeAlert("C", kError);
a.Set(true);
wpi::sim::StepTiming(1_s);
b.Set(true);
wpi::sim::StepTiming(1_s);
c.Set(true);
auto expectedEndState = GetActiveAlerts(kError);
std::replace(expectedEndState.begin(), expectedEndState.end(),
std::string("B"), std::string("AFTER"));
b.SetText("AFTER");
EXPECT_STATE(kError, expectedEndState);
wpi::sim::ResumeTiming();
}
TEST_F(AlertsTest, MoveAssign) {
auto outer = MakeAlert("outer", kInfo);
outer.Set(true);
EXPECT_TRUE(IsAlertActive("outer", kInfo));
{
auto inner = MakeAlert("inner", kWarning);
inner.Set(true);
EXPECT_TRUE(IsAlertActive("inner", kWarning));
outer = std::move(inner);
// Assignment target should be unset and invalidated as part of move, before
// destruction
EXPECT_FALSE(IsAlertActive("outer", kInfo));
}
EXPECT_TRUE(IsAlertActive("inner", kWarning));
}
TEST_F(AlertsTest, MoveConstruct) {
auto a = MakeAlert("A", kInfo);
a.Set(true);
EXPECT_TRUE(IsAlertActive("A", kInfo));
Alert b{std::move(a)};
EXPECT_TRUE(IsAlertActive("A", kInfo));
b.Set(false);
EXPECT_FALSE(IsAlertActive("A", kInfo));
b.Set(true);
EXPECT_TRUE(IsAlertActive("A", kInfo));
}
TEST_F(AlertsTest, SortOrder) {
wpi::sim::PauseTiming();
auto a = MakeAlert("A", kInfo);
auto b = MakeAlert("B", kInfo);
auto c = MakeAlert("C", kInfo);
a.Set(true);
EXPECT_STATE(kInfo, "A");
wpi::sim::StepTiming(1_s);
b.Set(true);
EXPECT_STATE(kInfo, "B", "A");
wpi::sim::StepTiming(1_s);
c.Set(true);
EXPECT_STATE(kInfo, "C", "B", "A");
wpi::sim::StepTiming(1_s);
c.Set(false);
EXPECT_STATE(kInfo, "B", "A");
wpi::sim::StepTiming(1_s);
c.Set(true);
EXPECT_STATE(kInfo, "C", "B", "A");
wpi::sim::StepTiming(1_s);
a.Set(false);
EXPECT_STATE(kInfo, "C", "B");
wpi::sim::StepTiming(1_s);
b.Set(false);
EXPECT_STATE(kInfo, "C");
wpi::sim::StepTiming(1_s);
b.Set(true);
EXPECT_STATE(kInfo, "B", "C");
wpi::sim::StepTiming(1_s);
a.Set(true);
EXPECT_STATE(kInfo, "A", "B", "C");
wpi::sim::ResumeTiming();
}

View File

@@ -0,0 +1,153 @@
// 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/simulation/AlertSim.hpp"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include <fmt/format.h>
#include <gtest/gtest.h>
#include "wpi/driverstation/Alert.hpp"
#include "wpi/hal/HALBase.h"
namespace wpi::sim {
class AlertSimTest : public ::testing::Test {
public:
AlertSimTest() { HAL_Initialize(500, 0); }
~AlertSimTest() override { AlertSim::ResetData(); }
std::string GetGroupName() {
const ::testing::TestInfo* testInfo =
::testing::UnitTest::GetInstance()->current_test_info();
return fmt::format("{}_{}", testInfo->test_suite_name(), testInfo->name());
}
template <typename... Args>
Alert MakeAlert(Args&&... args) {
return Alert(GetGroupName(), std::forward<Args>(args)...);
}
std::vector<std::string> GetActiveAlerts(Alert::Level type) {
auto alerts = AlertSim::GetAll();
std::vector<std::string> activeAlerts;
for (const auto& alert : alerts) {
if (alert.isActive() && alert.level == type) {
activeAlerts.emplace_back(std::move(alert.text));
}
}
return activeAlerts;
}
bool IsAlertActive(std::string_view text, Alert::Level type) {
auto alerts = AlertSim::GetAll();
return std::any_of(alerts.begin(), alerts.end(),
[text, type](const AlertSim::AlertInfo& alert) {
return alert.isActive() && alert.level == type &&
alert.text == text;
});
}
};
#define EXPECT_STATE(type, ...) \
EXPECT_EQ(GetActiveAlerts(type), (std::vector<std::string>{__VA_ARGS__}))
TEST_F(AlertSimTest, NoAlertsInitially) {
EXPECT_EQ(AlertSim::GetCount(), 0);
EXPECT_TRUE(AlertSim::GetAll().empty());
}
TEST_F(AlertSimTest, NoAlertsAfterReset) {
auto alert = MakeAlert("alert", Alert::Level::HIGH);
alert.Set(true);
EXPECT_TRUE(IsAlertActive("alert", Alert::Level::HIGH));
AlertSim::ResetData();
EXPECT_EQ(AlertSim::GetCount(), 0);
EXPECT_TRUE(AlertSim::GetAll().empty());
}
TEST_F(AlertSimTest, SetUnsetSingle) {
auto one = MakeAlert("one", Alert::Level::LOW);
EXPECT_FALSE(IsAlertActive("one", Alert::Level::LOW));
one.Set(true);
EXPECT_TRUE(IsAlertActive("one", Alert::Level::LOW));
one.Set(false);
EXPECT_FALSE(IsAlertActive("one", Alert::Level::LOW));
}
TEST_F(AlertSimTest, SetUnsetMultiple) {
auto one = MakeAlert("one", Alert::Level::HIGH);
auto two = MakeAlert("two", Alert::Level::LOW);
EXPECT_FALSE(IsAlertActive("one", Alert::Level::HIGH));
EXPECT_FALSE(IsAlertActive("two", Alert::Level::LOW));
one.Set(true);
EXPECT_TRUE(IsAlertActive("one", Alert::Level::HIGH));
EXPECT_FALSE(IsAlertActive("two", Alert::Level::LOW));
one.Set(true);
two.Set(true);
EXPECT_TRUE(IsAlertActive("one", Alert::Level::HIGH));
EXPECT_TRUE(IsAlertActive("two", Alert::Level::LOW));
one.Set(false);
EXPECT_FALSE(IsAlertActive("one", Alert::Level::HIGH));
EXPECT_TRUE(IsAlertActive("two", Alert::Level::LOW));
}
TEST_F(AlertSimTest, SetIsIdempotent) {
auto a = MakeAlert("A", Alert::Level::LOW);
auto b = MakeAlert("B", Alert::Level::LOW);
auto c = MakeAlert("C", Alert::Level::LOW);
a.Set(true);
b.Set(true);
c.Set(true);
const auto startState = GetActiveAlerts(Alert::Level::LOW);
b.Set(true);
EXPECT_STATE(Alert::Level::LOW, startState);
a.Set(true);
EXPECT_STATE(Alert::Level::LOW, startState);
}
TEST_F(AlertSimTest, DestructorUnsetsAlert) {
{
auto alert = MakeAlert("alert", Alert::Level::MEDIUM);
alert.Set(true);
EXPECT_TRUE(IsAlertActive("alert", Alert::Level::MEDIUM));
}
EXPECT_FALSE(IsAlertActive("alert", Alert::Level::MEDIUM));
}
TEST_F(AlertSimTest, SetTextWhileUnset) {
auto alert = MakeAlert("BEFORE", Alert::Level::LOW);
EXPECT_EQ("BEFORE", alert.GetText());
alert.Set(true);
EXPECT_TRUE(IsAlertActive("BEFORE", Alert::Level::LOW));
alert.Set(false);
EXPECT_FALSE(IsAlertActive("BEFORE", Alert::Level::LOW));
alert.SetText("AFTER");
EXPECT_EQ("AFTER", alert.GetText());
alert.Set(true);
EXPECT_FALSE(IsAlertActive("BEFORE", Alert::Level::LOW));
EXPECT_TRUE(IsAlertActive("AFTER", Alert::Level::LOW));
}
TEST_F(AlertSimTest, SetTextWhileSet) {
auto alert = MakeAlert("BEFORE", Alert::Level::LOW);
EXPECT_EQ("BEFORE", alert.GetText());
alert.Set(true);
EXPECT_TRUE(IsAlertActive("BEFORE", Alert::Level::LOW));
alert.SetText("AFTER");
EXPECT_EQ("AFTER", alert.GetText());
EXPECT_FALSE(IsAlertActive("BEFORE", Alert::Level::LOW));
EXPECT_TRUE(IsAlertActive("AFTER", Alert::Level::LOW));
}
} // namespace wpi::sim

View File

@@ -2,222 +2,140 @@ import typing as T
import pytest
from ntcore import NetworkTableInstance
from wpilib import Alert, SmartDashboard
from wpilib.simulation import pauseTiming, resumeTiming, stepTiming
from wpilib import Alert
from wpilib.simulation import AlertSim
AlertType = Alert.AlertType
Level = Alert.Level
@pytest.fixture(scope="function")
def group_name(nt, request):
def group_name(request):
group_name = f"AlertTest_{request.node.name}"
yield group_name
SmartDashboard.updateValues()
assert len(get_active_alerts(nt, group_name, AlertType.kError)) == 0
assert len(get_active_alerts(nt, group_name, AlertType.kWarning)) == 0
assert len(get_active_alerts(nt, group_name, AlertType.kInfo)) == 0
def get_subscriber_for_type(
nt: NetworkTableInstance, group_name: str, alert_type: AlertType
):
subtable_name = {
AlertType.kError: "errors",
AlertType.kWarning: "warnings",
AlertType.kInfo: "infos",
}.get(alert_type, "unknown")
topic = f"/SmartDashboard/{group_name}/{subtable_name}"
return nt.getStringArrayTopic(topic).subscribe([])
AlertSim.resetData()
def get_active_alerts(
nt: NetworkTableInstance, group_name: str, alert_type: AlertType
group_name: str, level: Alert.Level
) -> T.List[str]:
SmartDashboard.updateValues()
with get_subscriber_for_type(nt, group_name, alert_type) as sub:
return sub.get()
return [
a.text
for a in AlertSim.getAll()
if a.group == group_name and a.level == level and a.isActive()
]
def is_alert_active(
nt: NetworkTableInstance, group_name: str, text: str, alert_type: AlertType
group_name: str, text: str, level: Alert.Level
):
active_alerts = get_active_alerts(nt, group_name, alert_type)
return text in active_alerts
matches = [
a
for a in AlertSim.getAll()
if a.group == group_name and a.level == level and a.text == text and a.isActive()
]
return len(matches) > 0
def assert_state(
nt: NetworkTableInstance,
group_name: str,
alert_type: AlertType,
level: Alert.Level,
expected_state: T.List[str],
):
assert expected_state == get_active_alerts(nt, group_name, alert_type)
assert expected_state == get_active_alerts(group_name, level)
def test_set_unset_single(nt, group_name):
with Alert(group_name, "one", AlertType.kError) as one:
def test_set_unset_single(group_name):
with Alert(group_name, "one", Alert.Level.HIGH) as one:
assert not is_alert_active(nt, group_name, "one", AlertType.kError)
assert not is_alert_active(nt, group_name, "two", AlertType.kInfo)
assert not is_alert_active(group_name, "one", Alert.Level.HIGH)
assert not is_alert_active(group_name, "two", Alert.Level.LOW)
one.set(True)
assert is_alert_active(nt, group_name, "one", AlertType.kError)
assert is_alert_active(group_name, "one", Alert.Level.HIGH)
one.set(True)
assert is_alert_active(nt, group_name, "one", AlertType.kError)
assert is_alert_active(group_name, "one", Alert.Level.HIGH)
one.set(False)
assert not is_alert_active(nt, group_name, "one", AlertType.kError)
assert not is_alert_active(group_name, "one", Alert.Level.HIGH)
def test_set_unset_multiple(nt, group_name):
def test_set_unset_multiple(group_name):
with (
Alert(group_name, "one", AlertType.kError) as one,
Alert(group_name, "two", AlertType.kInfo) as two,
Alert(group_name, "one", Alert.Level.HIGH) as one,
Alert(group_name, "two", Alert.Level.LOW) as two,
):
assert not is_alert_active(nt, group_name, "one", AlertType.kError)
assert not is_alert_active(nt, group_name, "two", AlertType.kInfo)
assert not is_alert_active(group_name, "one", Alert.Level.HIGH)
assert not is_alert_active(group_name, "two", Alert.Level.LOW)
one.set(True)
assert is_alert_active(nt, group_name, "one", AlertType.kError)
assert not is_alert_active(nt, group_name, "two", AlertType.kInfo)
assert is_alert_active(group_name, "one", Alert.Level.HIGH)
assert not is_alert_active(group_name, "two", Alert.Level.LOW)
one.set(True)
two.set(True)
assert is_alert_active(nt, group_name, "one", AlertType.kError)
assert is_alert_active(nt, group_name, "two", AlertType.kInfo)
assert is_alert_active(group_name, "one", Alert.Level.HIGH)
assert is_alert_active(group_name, "two", Alert.Level.LOW)
one.set(False)
assert not is_alert_active(nt, group_name, "one", AlertType.kError)
assert is_alert_active(nt, group_name, "two", AlertType.kInfo)
assert not is_alert_active(group_name, "one", Alert.Level.HIGH)
assert is_alert_active(group_name, "two", Alert.Level.LOW)
def test_set_is_idempotent(nt, group_name):
def test_set_is_idempotent(group_name):
group_name = group_name
with (
Alert(group_name, "A", AlertType.kInfo) as a,
Alert(group_name, "B", AlertType.kInfo) as b,
Alert(group_name, "C", AlertType.kInfo) as c,
Alert(group_name, "A", Alert.Level.LOW) as a,
Alert(group_name, "B", Alert.Level.LOW) as b,
Alert(group_name, "C", Alert.Level.LOW) as c,
):
a.set(True)
b.set(True)
c.set(True)
start_state = get_active_alerts(nt, group_name, AlertType.kInfo)
start_state = get_active_alerts(group_name, Alert.Level.LOW)
assert set(start_state) == {"A", "B", "C"}
b.set(True)
assert_state(nt, group_name, AlertType.kInfo, start_state)
assert_state(group_name, Alert.Level.LOW, start_state)
a.set(True)
assert_state(nt, group_name, AlertType.kInfo, start_state)
assert_state(group_name, Alert.Level.LOW, start_state)
def test_close_unsets_alert(nt, group_name):
def test_close_unsets_alert(group_name):
group_name = group_name
with Alert(group_name, "alert", AlertType.kWarning) as alert:
with Alert(group_name, "alert", Alert.Level.MEDIUM) as alert:
alert.set(True)
assert is_alert_active(nt, group_name, "alert", AlertType.kWarning)
assert not is_alert_active(nt, group_name, "alert", AlertType.kWarning)
assert is_alert_active(group_name, "alert", Alert.Level.MEDIUM)
assert not is_alert_active(group_name, "alert", Alert.Level.MEDIUM)
def test_set_text_while_unset(nt, group_name):
def test_set_text_while_unset(group_name):
group_name = group_name
with Alert(group_name, "BEFORE", AlertType.kInfo) as alert:
with Alert(group_name, "BEFORE", Alert.Level.LOW) as alert:
assert alert.getText() == "BEFORE"
alert.set(True)
assert is_alert_active(nt, group_name, "BEFORE", AlertType.kInfo)
assert is_alert_active(group_name, "BEFORE", Alert.Level.LOW)
alert.set(False)
assert not is_alert_active(nt, group_name, "BEFORE", AlertType.kInfo)
assert not is_alert_active(group_name, "BEFORE", Alert.Level.LOW)
alert.setText("AFTER")
assert alert.getText() == "AFTER"
alert.set(True)
assert not is_alert_active(nt, group_name, "BEFORE", AlertType.kInfo)
assert is_alert_active(nt, group_name, "AFTER", AlertType.kInfo)
assert not is_alert_active(group_name, "BEFORE", Alert.Level.LOW)
assert is_alert_active(group_name, "AFTER", Alert.Level.LOW)
def test_set_text_while_set(nt, group_name):
with Alert(group_name, "BEFORE", AlertType.kInfo) as alert:
def test_set_text_while_set(group_name):
with Alert(group_name, "BEFORE", Alert.Level.LOW) as alert:
assert alert.getText() == "BEFORE"
alert.set(True)
assert is_alert_active(nt, group_name, "BEFORE", AlertType.kInfo)
assert is_alert_active(group_name, "BEFORE", Alert.Level.LOW)
alert.setText("AFTER")
assert alert.getText() == "AFTER"
assert not is_alert_active(nt, group_name, "BEFORE", AlertType.kInfo)
assert is_alert_active(nt, group_name, "AFTER", AlertType.kInfo)
def test_set_text_does_not_affect_sort(nt, group_name):
pauseTiming()
try:
with (
Alert(group_name, "A", AlertType.kInfo) as a,
Alert(group_name, "B", AlertType.kInfo) as b,
Alert(group_name, "C", AlertType.kInfo) as c,
):
a.set(True)
stepTiming(1)
b.set(True)
stepTiming(1)
c.set(True)
expected_state = get_active_alerts(nt, group_name, AlertType.kInfo)
expected_state[expected_state.index("B")] = "AFTER"
b.setText("AFTER")
assert_state(nt, group_name, AlertType.kInfo, expected_state)
finally:
resumeTiming()
def test_sort_order(nt, group_name):
pauseTiming()
try:
with (
Alert(group_name, "A", AlertType.kInfo) as a,
Alert(group_name, "B", AlertType.kInfo) as b,
Alert(group_name, "C", AlertType.kInfo) as c,
):
a.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["A"])
stepTiming(1)
b.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["B", "A"])
stepTiming(1)
c.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["C", "B", "A"])
stepTiming(1)
c.set(False)
assert_state(nt, group_name, AlertType.kInfo, ["B", "A"])
stepTiming(1)
c.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["C", "B", "A"])
stepTiming(1)
a.set(False)
assert_state(nt, group_name, AlertType.kInfo, ["C", "B"])
stepTiming(1)
b.set(False)
assert_state(nt, group_name, AlertType.kInfo, ["C"])
stepTiming(1)
b.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["B", "C"])
stepTiming(1)
a.set(True)
assert_state(nt, group_name, AlertType.kInfo, ["A", "B", "C"])
finally:
resumeTiming()
assert not is_alert_active(group_name, "BEFORE", Alert.Level.LOW)
assert is_alert_active(group_name, "AFTER", Alert.Level.LOW)