diff --git a/hal/src/main/java/org/wpilib/hardware/hal/AlertJNI.java b/hal/src/main/java/org/wpilib/hardware/hal/AlertJNI.java
new file mode 100644
index 0000000000..9b744f128c
--- /dev/null
+++ b/hal/src/main/java/org/wpilib/hardware/hal/AlertJNI.java
@@ -0,0 +1,94 @@
+// 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.
+
+package org.wpilib.hardware.hal;
+
+/**
+ * The AlertJNI class directly wraps the C++ HAL Alert.
+ *
+ *
This class is not meant for direct use by teams. Instead, the org.wpilib.driverstation.Alert
+ * class, which corresponds to the C++ Alert class, should be used.
+ *
+ * @see "wpi/hal/Alert.h"
+ */
+public class AlertJNI extends JNIWrapper {
+ /**
+ * 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.
+ */
+ public static final int LEVEL_HIGH = 0;
+
+ /**
+ * 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.
+ */
+ public static final int LEVEL_MEDIUM = 1;
+
+ /**
+ * 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.
+ */
+ public static final int LEVEL_LOW = 2;
+
+ /**
+ * Creates an alert.
+ *
+ * @param group Group identifier
+ * @param text Text to be displayed when the alert is active
+ * @param level Alert urgency level (LEVEL_HIGH, LEVEL_MEDIUM, LEVEL_LOW)
+ * @return the created alert handle
+ * @see "HAL_CreateAlert"
+ */
+ public static native int createAlert(String group, String text, int level);
+
+ /**
+ * Destroys an alert.
+ *
+ * @param alertHandle the alert handle
+ * @see "HAL_DestroyAlert"
+ */
+ public static native void destroyAlert(int alertHandle);
+
+ /**
+ * Sets whether the alert should be displayed. This method can be safely be called periodically.
+ *
+ * @param alertHandle the alert handle
+ * @param active true to display the alert, false to hide it
+ * @see "HAL_SetAlertActive"
+ */
+ public static native void setAlertActive(int alertHandle, boolean active);
+
+ /**
+ * Checks if an alert is active.
+ *
+ * @param alertHandle the alert handle
+ * @return true if the alert is active
+ * @see "HAL_IsAlertActive"
+ */
+ public static native boolean isAlertActive(int alertHandle);
+
+ /**
+ * Sets the text of the alert. Use this method to dynamically change the displayed alert, such as
+ * including more details about the detected problem.
+ *
+ * @param alertHandle Alert handle.
+ * @param text new text to be displayed when the alert is active
+ * @see "HAL_SetAlertText"
+ */
+ public static native void setAlertText(int alertHandle, String text);
+
+ /**
+ * Gets the text of the alert.
+ *
+ * @param alertHandle Alert handle.
+ * @return the text displayed when the alert is active
+ * @see "HAL_GetAlertText"
+ */
+ public static native String getAlertText(int alertHandle);
+
+ /** Utility class. */
+ private AlertJNI() {}
+}
diff --git a/hal/src/main/java/org/wpilib/hardware/hal/simulation/AlertDataJNI.java b/hal/src/main/java/org/wpilib/hardware/hal/simulation/AlertDataJNI.java
new file mode 100644
index 0000000000..9d65a3898b
--- /dev/null
+++ b/hal/src/main/java/org/wpilib/hardware/hal/simulation/AlertDataJNI.java
@@ -0,0 +1,58 @@
+// 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.
+
+package org.wpilib.hardware.hal.simulation;
+
+import org.wpilib.hardware.hal.JNIWrapper;
+
+/** JNI for alert data. */
+public class AlertDataJNI extends JNIWrapper {
+ /** Information about an alert. */
+ public static class AlertInfo {
+ public AlertInfo(int handle, String group, String text, long activeStartTime, int level) {
+ this.handle = handle;
+ this.group = group;
+ this.text = text;
+ this.activeStartTime = activeStartTime;
+ this.level = level;
+ }
+
+ @SuppressWarnings("MemberName")
+ public final int handle;
+
+ @SuppressWarnings("MemberName")
+ public final String group;
+
+ @SuppressWarnings("MemberName")
+ public final String text;
+
+ @SuppressWarnings("MemberName")
+ public final long activeStartTime; // 0 if not active
+
+ @SuppressWarnings("MemberName")
+ public final int level; // ALERT_LEVEL_HIGH, ALERT_LEVEL_MEDIUM, ALERT_LEVEL_LOW
+ }
+
+ /**
+ * Gets the number of alerts. Note: this is not guaranteed to be consistent with the number of
+ * alerts returned by getAlerts, so the latter's return value should be used to determine how many
+ * alerts were actually filled in.
+ *
+ * @return the number of alerts
+ */
+ public static native int getNumAlerts();
+
+ /**
+ * Gets detailed information about each alert.
+ *
+ * @return Array of information about each alert
+ */
+ public static native AlertInfo[] getAlerts();
+
+ /** Resets all alert simulation data. */
+ public static native void resetData();
+
+ /** Utility class. */
+ private AlertDataJNI() {}
+}
diff --git a/hal/src/main/native/cpp/jni/AlertJNI.cpp b/hal/src/main/native/cpp/jni/AlertJNI.cpp
new file mode 100644
index 0000000000..913ba19f33
--- /dev/null
+++ b/hal/src/main/native/cpp/jni/AlertJNI.cpp
@@ -0,0 +1,124 @@
+// 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
+
+#include
+#include
+
+#include "HALUtil.h"
+#include "org_wpilib_hardware_hal_AlertJNI.h"
+#include "wpi/hal/Alert.h"
+#include "wpi/util/jni_util.hpp"
+#include "wpi/util/string.h"
+
+using namespace wpi::hal;
+using namespace wpi::util::java;
+
+extern "C" {
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: createAlert
+ * Signature: (Ljava/lang/String;Ljava/lang/String;I)I
+ */
+JNIEXPORT jint JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_createAlert
+ (JNIEnv* env, jclass, jstring group, jstring text, jint level)
+{
+ int32_t status = 0;
+ wpi::util::java::JStringRef jgroup{env, group};
+ wpi::util::java::JStringRef jtext{env, text};
+ WPI_String wpiGroup = wpi::util::make_string(jgroup);
+ WPI_String wpiText = wpi::util::make_string(jtext);
+ HAL_AlertHandle alertHandle =
+ HAL_CreateAlert(&wpiGroup, &wpiText, level, &status);
+
+ if (alertHandle <= 0 || !CheckStatusForceThrow(env, status)) {
+ return 0; // something went wrong in HAL
+ }
+
+ return (jint)alertHandle;
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: destroyAlert
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_destroyAlert
+ (JNIEnv* env, jclass, jint alertHandle)
+{
+ if (alertHandle != HAL_kInvalidHandle) {
+ HAL_DestroyAlert((HAL_AlertHandle)alertHandle);
+ }
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: setAlertActive
+ * Signature: (IZ)V
+ */
+JNIEXPORT void JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_setAlertActive
+ (JNIEnv* env, jclass cls, jint alertHandle, jboolean active)
+{
+ int32_t status = 0;
+ HAL_SetAlertActive((HAL_AlertHandle)alertHandle, active, &status);
+ CheckStatus(env, status);
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: isAlertActive
+ * Signature: (I)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_isAlertActive
+ (JNIEnv* env, jclass cls, jint alertHandle)
+{
+ int32_t status = 0;
+ jboolean active = HAL_IsAlertActive((HAL_AlertHandle)alertHandle, &status);
+ CheckStatus(env, status);
+ return active;
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: setAlertText
+ * Signature: (ILjava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_setAlertText
+ (JNIEnv* env, jclass cls, jint alertHandle, jstring text)
+{
+ int32_t status = 0;
+ wpi::util::java::JStringRef jtext{env, text};
+ WPI_String wpiText = wpi::util::make_string(jtext);
+ HAL_SetAlertText((HAL_AlertHandle)alertHandle, &wpiText, &status);
+ CheckStatus(env, status);
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_AlertJNI
+ * Method: getAlertText
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_org_wpilib_hardware_hal_AlertJNI_getAlertText
+ (JNIEnv* env, jclass cls, jint alertHandle)
+{
+ int32_t status = 0;
+ WPI_String text;
+ HAL_GetAlertText((HAL_AlertHandle)alertHandle, &text, &status);
+ if (!CheckStatus(env, status)) {
+ return nullptr;
+ }
+ jstring rv = MakeJString(env, wpi::util::to_string_view(&text));
+ WPI_FreeString(&text);
+ return rv;
+}
+
+} // extern "C"
diff --git a/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.cpp b/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.cpp
new file mode 100644
index 0000000000..f5322c7960
--- /dev/null
+++ b/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.cpp
@@ -0,0 +1,91 @@
+// 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 "AlertDataJNI.hpp"
+
+#include
+
+#include "org_wpilib_hardware_hal_simulation_AlertDataJNI.h"
+#include "wpi/hal/simulation/AlertData.h"
+#include "wpi/util/jni_util.hpp"
+
+using namespace wpi::util::java;
+
+static JClass alertInfoCls;
+
+static jobject MakeAlertInfoJava(JNIEnv* env, const HALSIM_AlertInfo& info) {
+ static jmethodID func = env->GetMethodID(
+ alertInfoCls, "", "(ILjava/lang/String;Ljava/lang/String;JI)V");
+ return env->NewObject(
+ alertInfoCls, func, static_cast(info.handle),
+ MakeJString(env, wpi::util::to_string_view(&info.group)),
+ MakeJString(env, wpi::util::to_string_view(&info.text)),
+ static_cast(info.activeStartTime), static_cast(info.level));
+}
+namespace wpi::hal::sim {
+
+bool InitializeAlertDataJNI(JNIEnv* env) {
+ alertInfoCls =
+ JClass(env, "org/wpilib/hardware/hal/simulation/AlertDataJNI$AlertInfo");
+ if (!alertInfoCls) {
+ return false;
+ }
+ return true;
+}
+
+void FreeAlertDataJNI(JNIEnv* env) {
+ alertInfoCls.free(env);
+}
+
+} // namespace wpi::hal::sim
+
+extern "C" {
+
+/*
+ * Class: org_wpilib_hardware_hal_simulation_AlertDataJNI
+ * Method: getNumAlerts
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL
+Java_org_wpilib_hardware_hal_simulation_AlertDataJNI_getNumAlerts
+ (JNIEnv*, jclass)
+{
+ return HALSIM_GetNumAlerts();
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_simulation_AlertDataJNI
+ * Method: getAlerts
+ * Signature: ()[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_org_wpilib_hardware_hal_simulation_AlertDataJNI_getAlerts
+ (JNIEnv* env, jclass)
+{
+ int32_t allocLen = HALSIM_GetNumAlerts();
+ HALSIM_AlertInfo* arr = new HALSIM_AlertInfo[allocLen];
+ int32_t len = HALSIM_GetAlerts(arr, allocLen);
+
+ jobjectArray ret = env->NewObjectArray(len, alertInfoCls, nullptr);
+ for (int32_t i = 0; i < len; ++i) {
+ env->SetObjectArrayElement(ret, i, MakeAlertInfoJava(env, arr[i]));
+ }
+ HALSIM_FreeAlerts(arr, len < allocLen ? len : allocLen);
+ delete[] arr;
+ return ret;
+}
+
+/*
+ * Class: org_wpilib_hardware_hal_simulation_AlertDataJNI
+ * Method: resetData
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL
+Java_org_wpilib_hardware_hal_simulation_AlertDataJNI_resetData
+ (JNIEnv*, jclass)
+{
+ HALSIM_ResetAlertData();
+}
+
+} // extern "C"
diff --git a/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.hpp b/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.hpp
new file mode 100644
index 0000000000..81dd1aca08
--- /dev/null
+++ b/hal/src/main/native/cpp/jni/simulation/AlertDataJNI.hpp
@@ -0,0 +1,12 @@
+// 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
+
+namespace wpi::hal::sim {
+bool InitializeAlertDataJNI(JNIEnv* env);
+void FreeAlertDataJNI(JNIEnv* env);
+} // namespace wpi::hal::sim
diff --git a/hal/src/main/native/cpp/jni/simulation/SimulatorJNI.cpp b/hal/src/main/native/cpp/jni/simulation/SimulatorJNI.cpp
index d6547d3932..1a156ea7af 100644
--- a/hal/src/main/native/cpp/jni/simulation/SimulatorJNI.cpp
+++ b/hal/src/main/native/cpp/jni/simulation/SimulatorJNI.cpp
@@ -4,6 +4,7 @@
#include "SimulatorJNI.h"
+#include "AlertDataJNI.hpp"
#include "BufferCallbackStore.h"
#include "CallbackStore.h"
#include "ConstBufferCallbackStore.h"
@@ -78,6 +79,9 @@ jint SimOnLoad(JavaVM* vm, void* reserved) {
InitializeBufferStore();
InitializeConstBufferStore();
InitializeOpModeOptionsStore();
+ if (!InitializeAlertDataJNI(env)) {
+ return JNI_ERR;
+ }
if (!InitializeSimDeviceDataJNI(env)) {
return JNI_ERR;
}
@@ -95,6 +99,7 @@ void SimOnUnload(JavaVM* vm, void* reserved) {
bufferCallbackCls.free(env);
constBufferCallbackCls.free(env);
biConsumerCls.free(env);
+ FreeAlertDataJNI(env);
FreeSimDeviceDataJNI(env);
jvm = nullptr;
}
diff --git a/hal/src/main/native/include/wpi/hal/Alert.h b/hal/src/main/native/include/wpi/hal/Alert.h
new file mode 100644
index 0000000000..4a1f7d0341
--- /dev/null
+++ b/hal/src/main/native/include/wpi/hal/Alert.h
@@ -0,0 +1,113 @@
+// 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
+
+#include "wpi/hal/Types.h"
+#include "wpi/util/string.h"
+
+/**
+ * @defgroup hal_alert Alert Functions
+ * @ingroup hal_capi
+ * @{
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Represents an alert's level of urgency. */
+HAL_ENUM(HAL_AlertLevel) {
+ /**
+ * 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.
+ */
+ HAL_ALERT_HIGH = 0,
+ HAL_ALERT_ERROR = HAL_ALERT_HIGH,
+
+ /**
+ * 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.
+ */
+ HAL_ALERT_MEDIUM = 1,
+ HAL_ALERT_WARNING = HAL_ALERT_MEDIUM,
+
+ /**
+ * 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.
+ */
+ HAL_ALERT_LOW = 2,
+ HAL_ALERT_INFO = HAL_ALERT_LOW
+};
+
+/**
+ * Creates an alert.
+ *
+ * @param group Group identifier
+ * @param text Text to be displayed when the alert is active
+ * @param level Alert urgency level (HAL_AlertLevel)
+ * @param[out] status Error status variable. 0 on success.
+ * @return the created alert
+ */
+HAL_AlertHandle HAL_CreateAlert(const struct WPI_String* group,
+ const struct WPI_String* text, int32_t level,
+ int32_t* status);
+
+/**
+ * Destroys an alert.
+ *
+ * @param alertHandle the alert handle
+ */
+void HAL_DestroyAlert(HAL_AlertHandle alertHandle);
+
+/**
+ * Sets whether the alert should be displayed. This method can be
+ * safely be called periodically.
+ *
+ * @param alertHandle the alert handle
+ * @param active true to display the alert, false to hide it
+ * @param[out] status Error status variable. 0 on success.
+ */
+void HAL_SetAlertActive(HAL_AlertHandle alertHandle, HAL_Bool active,
+ int32_t* status);
+
+/**
+ * Checks if an alert is active.
+ *
+ * @param alertHandle the alert handle
+ * @param[out] status Error status variable. 0 on success.
+ * @return true if the alert is active
+ */
+HAL_Bool HAL_IsAlertActive(HAL_AlertHandle alertHandle, int32_t* status);
+
+/**
+ * Updates the text of an alert. Use this method to dynamically change the
+ * displayed alert, such as including more details about the detected problem.
+ *
+ * @param alertHandle the alert handle
+ * @param text new text to be displayed when the alert is active
+ * @param[out] status Error status variable. 0 on success.
+ */
+void HAL_SetAlertText(HAL_AlertHandle alertHandle,
+ const struct WPI_String* text, int32_t* status);
+
+/**
+ * Gets the text of an alert.
+ *
+ * @param alertHandle the alert handle
+ * @param text pointer to a WPI_String to be filled with the current text
+ * @param[out] status Error status variable. 0 on success.
+ */
+void HAL_GetAlertText(HAL_AlertHandle alertHandle, struct WPI_String* text,
+ int32_t* status);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+/** @} */
diff --git a/hal/src/main/native/include/wpi/hal/Types.h b/hal/src/main/native/include/wpi/hal/Types.h
index a84f324abf..5f332ffa5e 100644
--- a/hal/src/main/native/include/wpi/hal/Types.h
+++ b/hal/src/main/native/include/wpi/hal/Types.h
@@ -16,6 +16,8 @@
typedef int32_t HAL_Handle;
+typedef HAL_Handle HAL_AlertHandle;
+
typedef HAL_Handle HAL_AnalogInputHandle;
typedef HAL_Handle HAL_AnalogOutputHandle;
diff --git a/hal/src/main/native/include/wpi/hal/handles/HandlesInternal.h b/hal/src/main/native/include/wpi/hal/handles/HandlesInternal.h
index 85e5e0c045..2c95e759f7 100644
--- a/hal/src/main/native/include/wpi/hal/handles/HandlesInternal.h
+++ b/hal/src/main/native/include/wpi/hal/handles/HandlesInternal.h
@@ -72,6 +72,7 @@ enum class HAL_HandleEnum {
REVPDH = 26,
REVPH = 27,
CANStream = 28,
+ Alert = 29,
};
/**
diff --git a/hal/src/main/native/include/wpi/hal/simulation/AlertData.h b/hal/src/main/native/include/wpi/hal/simulation/AlertData.h
new file mode 100644
index 0000000000..f0d3dadc96
--- /dev/null
+++ b/hal/src/main/native/include/wpi/hal/simulation/AlertData.h
@@ -0,0 +1,60 @@
+// 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
+
+#include "wpi/hal/Types.h"
+#include "wpi/util/string.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Information about an alert. */
+struct HALSIM_AlertInfo {
+ HAL_AlertHandle handle;
+ struct WPI_String group;
+ struct WPI_String text;
+ int64_t activeStartTime; // 0 if not active
+ int32_t level; // HAL_AlertLevel
+};
+
+/**
+ * Gets the number of alerts. Note: this is not guaranteed to be consistent
+ * with the number of alerts returned by HALSIM_GetAlerts, so the latter's
+ * return value should be used to determine how many alerts were actually filled
+ * in.
+ *
+ * @return the number of alerts
+ */
+int32_t HALSIM_GetNumAlerts(void);
+
+/**
+ * Gets detailed information about each alert.
+ *
+ * @param arr array of information to be filled
+ * @param length length of arr
+ * @return Number of alerts; note: may be larger or smaller than passed-in
+ * length
+ */
+int32_t HALSIM_GetAlerts(struct HALSIM_AlertInfo* arr, int32_t length);
+
+/**
+ * Frees an array of alert information returned by HALSIM_GetAlerts.
+ *
+ * @param arr array to free
+ * @param length number of alerts in arr (as returned by HALSIM_GetAlerts)
+ */
+void HALSIM_FreeAlerts(struct HALSIM_AlertInfo* arr, int32_t length);
+
+/**
+ * Resets all alert simulation data.
+ */
+void HALSIM_ResetAlertData(void);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/hal/src/main/native/sim/Alert.cpp b/hal/src/main/native/sim/Alert.cpp
new file mode 100644
index 0000000000..6d0d3785b5
--- /dev/null
+++ b/hal/src/main/native/sim/Alert.cpp
@@ -0,0 +1,155 @@
+// 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/hal/Alert.h"
+
+#include
+#include
+#include
+#include
+
+#include "HALInitializer.h"
+#include "wpi/hal/Errors.h"
+#include "wpi/hal/HALBase.h"
+#include "wpi/hal/Types.h"
+#include "wpi/hal/handles/UnlimitedHandleResource.h"
+#include "wpi/hal/simulation/AlertData.h"
+#include "wpi/util/mutex.hpp"
+#include "wpi/util/string.h"
+
+namespace {
+struct Alert {
+ Alert(std::string_view group, std::string_view text, int32_t level)
+ : group{group}, text{text}, level{level} {}
+
+ wpi::util::mutex textMutex;
+ std::string group;
+ std::string text;
+ std::atomic activeStartTime{0}; // non-zero when active
+ int32_t level = 0;
+};
+} // namespace
+
+using namespace wpi::hal;
+
+static UnlimitedHandleResource*
+ alertHandles;
+
+namespace wpi::hal::init {
+void InitializeAlert() {
+ static UnlimitedHandleResource
+ aH;
+ alertHandles = &aH;
+}
+} // namespace wpi::hal::init
+
+extern "C" {
+
+HAL_AlertHandle HAL_CreateAlert(const WPI_String* group, const WPI_String* text,
+ int32_t level, int32_t* status) {
+ wpi::hal::init::CheckInit();
+ std::shared_ptr alert = std::make_shared(
+ wpi::util::to_string_view(group), wpi::util::to_string_view(text), level);
+ HAL_AlertHandle handle = alertHandles->Allocate(alert);
+ if (handle == HAL_kInvalidHandle) {
+ *status = HAL_HANDLE_ERROR;
+ return HAL_kInvalidHandle;
+ }
+ return handle;
+}
+
+void HAL_DestroyAlert(HAL_AlertHandle alertHandle) {
+ alertHandles->Free(alertHandle);
+}
+
+void HAL_SetAlertActive(HAL_AlertHandle alertHandle, HAL_Bool active,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return;
+ }
+ if (active) {
+ if (alert->activeStartTime.load(std::memory_order_relaxed) != 0) {
+ // Already active, do nothing (avoids cost of getting time)
+ return;
+ }
+ int64_t now = HAL_GetFPGATime(status);
+ int64_t expected = 0;
+ // use compare-exchange to avoid potential race
+ alert->activeStartTime.compare_exchange_strong(expected, now);
+ } else {
+ alert->activeStartTime = 0;
+ }
+}
+
+HAL_Bool HAL_IsAlertActive(HAL_AlertHandle alertHandle, int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return false;
+ }
+ return alert->activeStartTime != 0;
+}
+
+void HAL_SetAlertText(HAL_AlertHandle alertHandle, const WPI_String* text,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return;
+ }
+ std::scoped_lock lock(alert->textMutex);
+ alert->text = wpi::util::to_string_view(text);
+}
+
+void HAL_GetAlertText(HAL_AlertHandle alertHandle, struct WPI_String* text,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (alert) {
+ std::scoped_lock lock(alert->textMutex);
+ *text = wpi::util::alloc_wpi_string(alert->text);
+ } else {
+ *status = HAL_HANDLE_ERROR;
+ *text = WPI_String{};
+ }
+}
+
+int32_t HALSIM_GetNumAlerts(void) {
+ int32_t count = 0;
+ alertHandles->ForEach([&](auto, auto) { ++count; });
+ return count;
+}
+
+int32_t HALSIM_GetAlerts(struct HALSIM_AlertInfo* arr, int32_t length) {
+ int32_t num = 0;
+ alertHandles->ForEach([&](HAL_AlertHandle handle, Alert* alert) {
+ if (num < length) {
+ arr[num].handle = handle;
+ arr[num].group = wpi::util::alloc_wpi_string(alert->group);
+ {
+ std::scoped_lock lock(alert->textMutex);
+ arr[num].text = wpi::util::alloc_wpi_string(alert->text);
+ }
+ arr[num].activeStartTime =
+ alert->activeStartTime.load(std::memory_order_relaxed);
+ arr[num].level = alert->level;
+ }
+ ++num;
+ });
+ return num;
+}
+
+void HALSIM_FreeAlerts(struct HALSIM_AlertInfo* arr, int32_t length) {
+ for (int32_t i = 0; i < length; ++i) {
+ WPI_FreeString(&arr[i].group);
+ WPI_FreeString(&arr[i].text);
+ }
+}
+
+void HALSIM_ResetAlertData(void) {
+ alertHandles->ResetHandles();
+}
+
+} // extern "C"
diff --git a/hal/src/main/native/sim/HAL.cpp b/hal/src/main/native/sim/HAL.cpp
index 3570562732..e38e99a4f5 100644
--- a/hal/src/main/native/sim/HAL.cpp
+++ b/hal/src/main/native/sim/HAL.cpp
@@ -82,6 +82,7 @@ void InitializeHAL() {
InitializeRoboRioData();
InitializeSimDeviceData();
InitializeAddressableLED();
+ InitializeAlert();
InitializeAnalogInput();
InitializeAnalogInternal();
InitializeCAN();
diff --git a/hal/src/main/native/sim/HALInitializer.h b/hal/src/main/native/sim/HALInitializer.h
index 1336500b8d..5ac35a0238 100644
--- a/hal/src/main/native/sim/HALInitializer.h
+++ b/hal/src/main/native/sim/HALInitializer.h
@@ -34,6 +34,7 @@ extern void InitializePWMData();
extern void InitializeRoboRioData();
extern void InitializeSimDeviceData();
extern void InitializeAddressableLED();
+extern void InitializeAlert();
extern void InitializeAnalogInput();
extern void InitializeAnalogInternal();
extern void InitializeCAN();
diff --git a/hal/src/main/native/sim/mockdata/Reset.cpp b/hal/src/main/native/sim/mockdata/Reset.cpp
index bb2449f54d..19efd65083 100644
--- a/hal/src/main/native/sim/mockdata/Reset.cpp
+++ b/hal/src/main/native/sim/mockdata/Reset.cpp
@@ -4,6 +4,7 @@
#include "../PortsInternal.h"
#include "wpi/hal/simulation/AddressableLEDData.h"
+#include "wpi/hal/simulation/AlertData.h"
#include "wpi/hal/simulation/AnalogInData.h"
#include "wpi/hal/simulation/CTREPCMData.h"
#include "wpi/hal/simulation/CanData.h"
@@ -70,4 +71,5 @@ extern "C" void HALSIM_ResetAllSimData(void) {
HALSIM_ResetRoboRioData();
HALSIM_ResetSimDeviceData();
+ HALSIM_ResetAlertData();
}
diff --git a/hal/src/main/native/systemcore/Alert.cpp b/hal/src/main/native/systemcore/Alert.cpp
new file mode 100644
index 0000000000..a193efb779
--- /dev/null
+++ b/hal/src/main/native/systemcore/Alert.cpp
@@ -0,0 +1,131 @@
+// 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/hal/Alert.h"
+
+#include
+#include
+#include
+#include
+
+#include "HALInitializer.h"
+#include "wpi/hal/Errors.h"
+#include "wpi/hal/HALBase.h"
+#include "wpi/hal/Types.h"
+#include "wpi/hal/handles/UnlimitedHandleResource.h"
+#include "wpi/hal/simulation/AlertData.h"
+#include "wpi/util/mutex.hpp"
+#include "wpi/util/string.h"
+
+namespace {
+struct Alert {
+ Alert(std::string_view group, std::string_view text, int32_t level)
+ : group{group}, text{text}, level{level} {}
+
+ wpi::util::mutex textMutex;
+ std::string group;
+ std::string text;
+ std::atomic activeStartTime{0}; // non-zero when active
+ int32_t level = 0;
+};
+} // namespace
+
+using namespace wpi::hal;
+
+static UnlimitedHandleResource*
+ alertHandles;
+
+namespace wpi::hal::init {
+void InitializeAlert() {
+ static UnlimitedHandleResource
+ aH;
+ alertHandles = &aH;
+}
+} // namespace wpi::hal::init
+
+extern "C" {
+
+HAL_AlertHandle HAL_CreateAlert(const WPI_String* group, const WPI_String* text,
+ int32_t level, int32_t* status) {
+ wpi::hal::init::CheckInit();
+ std::shared_ptr alert = std::make_shared(
+ wpi::util::to_string_view(group), wpi::util::to_string_view(text), level);
+ HAL_AlertHandle handle = alertHandles->Allocate(alert);
+ if (handle == HAL_kInvalidHandle) {
+ *status = HAL_HANDLE_ERROR;
+ return HAL_kInvalidHandle;
+ }
+ return handle;
+}
+
+void HAL_DestroyAlert(HAL_AlertHandle alertHandle) {
+ alertHandles->Free(alertHandle);
+}
+
+void HAL_SetAlertActive(HAL_AlertHandle alertHandle, HAL_Bool active,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return;
+ }
+ if (active) {
+ if (alert->activeStartTime.load(std::memory_order_relaxed) != 0) {
+ // Already active, do nothing (avoids cost of getting time)
+ return;
+ }
+ int64_t now = HAL_GetFPGATime(status);
+ int64_t expected = 0;
+ // use compare-exchange to avoid potential race
+ alert->activeStartTime.compare_exchange_strong(expected, now);
+ } else {
+ alert->activeStartTime = 0;
+ }
+}
+
+HAL_Bool HAL_IsAlertActive(HAL_AlertHandle alertHandle, int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return false;
+ }
+ return alert->activeStartTime != 0;
+}
+
+void HAL_SetAlertText(HAL_AlertHandle alertHandle, const WPI_String* text,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (!alert) {
+ *status = HAL_HANDLE_ERROR;
+ return;
+ }
+ std::scoped_lock lock(alert->textMutex);
+ alert->text = wpi::util::to_string_view(text);
+}
+
+void HAL_GetAlertText(HAL_AlertHandle alertHandle, struct WPI_String* text,
+ int32_t* status) {
+ auto alert = alertHandles->Get(alertHandle);
+ if (alert) {
+ std::scoped_lock lock(alert->textMutex);
+ *text = wpi::util::alloc_wpi_string(alert->text);
+ } else {
+ *status = HAL_HANDLE_ERROR;
+ *text = WPI_String{};
+ }
+}
+
+int32_t HALSIM_GetNumAlerts(void) {
+ return 0;
+}
+
+int32_t HALSIM_GetAlerts(struct HALSIM_AlertInfo* arr, int32_t length) {
+ return 0;
+}
+
+void HALSIM_FreeAlerts(struct HALSIM_AlertInfo* arr, int32_t length) {}
+
+void HALSIM_ResetAlertData(void) {}
+
+} // extern "C"
diff --git a/wpilibc/robotpy_pybind_build_info.bzl b/wpilibc/robotpy_pybind_build_info.bzl
index 391555d0f6..5072b1b1cf 100644
--- a/wpilibc/robotpy_pybind_build_info.bzl
+++ b/wpilibc/robotpy_pybind_build_info.bzl
@@ -96,6 +96,16 @@ def wpilib_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includ
("wpi::RobotDriveBase", "wpi__RobotDriveBase.hpp"),
],
),
+ struct(
+ class_name = "Alert",
+ yml_file = "semiwrap/Alert.yml",
+ header_root = "$(execpath :robotpy-native-wpilib.copy_headers)",
+ header_file = "$(execpath :robotpy-native-wpilib.copy_headers)/wpi/driverstation/Alert.hpp",
+ tmpl_class_names = [],
+ trampolines = [
+ ("wpi::Alert", "wpi__Alert.hpp"),
+ ],
+ ),
struct(
class_name = "DriverStation",
yml_file = "semiwrap/DriverStation.yml",
@@ -960,16 +970,6 @@ def wpilib_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includ
("wpi::Watchdog", "wpi__Watchdog.hpp"),
],
),
- struct(
- class_name = "Alert",
- yml_file = "semiwrap/Alert.yml",
- header_root = "$(execpath :robotpy-native-wpilib.copy_headers)",
- header_file = "$(execpath :robotpy-native-wpilib.copy_headers)/wpi/util/Alert.hpp",
- tmpl_class_names = [],
- trampolines = [
- ("wpi::Alert", "wpi__Alert.hpp"),
- ],
- ),
struct(
class_name = "Preferences",
yml_file = "semiwrap/Preferences.yml",
@@ -1106,6 +1106,17 @@ def wpilib_simulation_extension(srcs = [], header_to_dat_deps = [], extra_hdrs =
("wpi::sim::AddressableLEDSim", "wpi__sim__AddressableLEDSim.hpp"),
],
),
+ struct(
+ class_name = "AlertSim",
+ yml_file = "semiwrap/simulation/AlertSim.yml",
+ header_root = "$(execpath :robotpy-native-wpilib.copy_headers)",
+ header_file = "$(execpath :robotpy-native-wpilib.copy_headers)/wpi/simulation/AlertSim.hpp",
+ tmpl_class_names = [],
+ trampolines = [
+ ("wpi::sim::AlertSim", "wpi__sim__AlertSim.hpp"),
+ ("wpi::sim::AlertSim::AlertInfo", "wpi__sim__AlertSim__AlertInfo.hpp"),
+ ],
+ ),
struct(
class_name = "AnalogEncoderSim",
yml_file = "semiwrap/simulation/AnalogEncoderSim.yml",
diff --git a/wpilibc/src/main/native/cpp/driverstation/Alert.cpp b/wpilibc/src/main/native/cpp/driverstation/Alert.cpp
new file mode 100644
index 0000000000..9c784886fa
--- /dev/null
+++ b/wpilibc/src/main/native/cpp/driverstation/Alert.cpp
@@ -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
+
+#include
+
+#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(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;
+}
diff --git a/wpilibc/src/main/native/cpp/simulation/AlertSim.cpp b/wpilibc/src/main/native/cpp/simulation/AlertSim.cpp
new file mode 100644
index 0000000000..4267c8b549
--- /dev/null
+++ b/wpilibc/src/main/native/cpp/simulation/AlertSim.cpp
@@ -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
+#include
+
+#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::GetAll() {
+ int32_t allocLen = HALSIM_GetNumAlerts();
+ HALSIM_AlertInfo* cInfos = new HALSIM_AlertInfo[allocLen];
+ int32_t len = HALSIM_GetAlerts(cInfos, allocLen);
+ std::vector 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(cInfo.level));
+ }
+ HALSIM_FreeAlerts(cInfos, len < allocLen ? len : allocLen);
+ delete[] cInfos;
+ return infos;
+}
+
+void AlertSim::ResetData() {
+ HALSIM_ResetAlertData();
+}
diff --git a/wpilibc/src/main/native/cpp/util/Alert.cpp b/wpilibc/src/main/native/cpp/util/Alert.cpp
deleted file mode 100644
index 87e85f8d1c..0000000000
--- a/wpilibc/src/main/native/cpp/util/Alert.cpp
+++ /dev/null
@@ -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
-#include
-#include
-#include
-
-#include
-
-#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 {
- 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& GetActiveAlertsStorage(AlertType type) {
- return const_cast&>(
- std::as_const(*this).GetActiveAlertsStorage(type));
- }
-
- const std::set& GetActiveAlertsStorage(AlertType type) const {
- switch (type) {
- case AlertType::kInfo:
- case AlertType::kWarning:
- case AlertType::kError:
- return m_alerts[static_cast(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(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 GetStrings(AlertType type) const {
- auto& set = GetActiveAlertsStorage(type);
- std::vector output;
- output.reserve(set.size());
- for (auto& alert : set) {
- output.emplace_back(alert.text);
- }
- return output;
- }
-
- std::array, 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(type));
- }
-}
diff --git a/wpilibc/src/main/native/include/wpi/util/Alert.hpp b/wpilibc/src/main/native/include/wpi/driverstation/Alert.hpp
similarity index 51%
rename from wpilibc/src/main/native/include/wpi/util/Alert.hpp
rename to wpilibc/src/main/native/include/wpi/driverstation/Alert.hpp
index 6286413ba1..b32aac2dad 100644
--- a/wpilibc/src/main/native/include/wpi/util/Alert.hpp
+++ b/wpilibc/src/main/native/include/wpi/driverstation/Alert.hpp
@@ -4,30 +4,28 @@
#pragma once
-#include
-
-#include
#include
+#include
+
+#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.
- *
*
* 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* m_activeAlerts;
- bool m_active = false;
- uint64_t m_activeStartTime;
+ Level m_type;
+ wpi::hal::Handle m_handle;
};
-std::string format_as(Alert::AlertType type);
-
} // namespace wpi
diff --git a/wpilibc/src/main/native/include/wpi/simulation/AlertSim.hpp b/wpilibc/src/main/native/include/wpi/simulation/AlertSim.hpp
new file mode 100644
index 0000000000..2f6aea6057
--- /dev/null
+++ b/wpilibc/src/main/native/include/wpi/simulation/AlertSim.hpp
@@ -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
+
+#include
+#include
+
+#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 GetAll();
+
+ /**
+ * Resets all alert simulation data.
+ */
+ static void ResetData();
+};
+
+} // namespace wpi::sim
diff --git a/wpilibc/src/main/python/pyproject.toml b/wpilibc/src/main/python/pyproject.toml
index bf23e05a1c..4bb6d354c4 100644
--- a/wpilibc/src/main/python/pyproject.toml
+++ b/wpilibc/src/main/python/pyproject.toml
@@ -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"
diff --git a/wpilibc/src/main/python/semiwrap/Alert.yml b/wpilibc/src/main/python/semiwrap/Alert.yml
index f98b7e12bf..39c9451e7b 100644
--- a/wpilibc/src/main/python/semiwrap/Alert.yml
+++ b/wpilibc/src/main/python/semiwrap/Alert.yml
@@ -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:
diff --git a/wpilibc/src/main/python/semiwrap/simulation/AlertSim.yml b/wpilibc/src/main/python/semiwrap/simulation/AlertSim.yml
new file mode 100644
index 0000000000..c157b426ce
--- /dev/null
+++ b/wpilibc/src/main/python/semiwrap/simulation/AlertSim.yml
@@ -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).")
+ );
diff --git a/wpilibc/src/main/python/wpilib/simulation/__init__.py b/wpilibc/src/main/python/wpilib/simulation/__init__.py
index 131d4729d7..c322d62b4c 100644
--- a/wpilibc/src/main/python/wpilib/simulation/__init__.py
+++ b/wpilibc/src/main/python/wpilib/simulation/__init__.py
@@ -4,6 +4,7 @@ from . import _init__simulation
from ._simulation import (
ADXL345Sim,
AddressableLEDSim,
+ AlertSim,
AnalogEncoderSim,
AnalogInputSim,
BatterySim,
diff --git a/wpilibc/src/test/native/cpp/AlertTest.cpp b/wpilibc/src/test/native/cpp/AlertTest.cpp
deleted file mode 100644
index f30f8bbe38..0000000000
--- a/wpilibc/src/test/native/cpp/AlertTest.cpp
+++ /dev/null
@@ -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
-#include
-#include
-#include
-
-#include
-#include
-
-#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
- Alert MakeAlert(Args&&... args) {
- return Alert(GetGroupName(), std::forward(args)...);
- }
-
- std::vector 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{__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();
-}
diff --git a/wpilibc/src/test/native/cpp/simulation/AlertSimTest.cpp b/wpilibc/src/test/native/cpp/simulation/AlertSimTest.cpp
new file mode 100644
index 0000000000..64f0ea168b
--- /dev/null
+++ b/wpilibc/src/test/native/cpp/simulation/AlertSimTest.cpp
@@ -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
+#include
+#include
+#include
+
+#include
+#include
+
+#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
+ Alert MakeAlert(Args&&... args) {
+ return Alert(GetGroupName(), std::forward(args)...);
+ }
+
+ std::vector GetActiveAlerts(Alert::Level type) {
+ auto alerts = AlertSim::GetAll();
+ std::vector 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{__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
diff --git a/wpilibc/src/test/python/test_alert.py b/wpilibc/src/test/python/test_alert.py
index ceecc3d879..dc585ebef1 100644
--- a/wpilibc/src/test/python/test_alert.py
+++ b/wpilibc/src/test/python/test_alert.py
@@ -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)
diff --git a/wpilibj/src/main/java/org/wpilib/driverstation/Alert.java b/wpilibj/src/main/java/org/wpilib/driverstation/Alert.java
new file mode 100644
index 0000000000..84f7c6e853
--- /dev/null
+++ b/wpilibj/src/main/java/org/wpilib/driverstation/Alert.java
@@ -0,0 +1,155 @@
+// 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.
+
+package org.wpilib.driverstation;
+
+import org.wpilib.hardware.hal.AlertJNI;
+
+/**
+ * Persistent alert to be sent via NetworkTables. Alerts are tagged with a type of {@code ERROR},
+ * {@code WARNING}, or {@code INFO} to denote urgency. See {@link
+ * org.wpilib.driverstation.Alert.Level 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. {@link #set(boolean)} can be safely called periodically.
+ *
+ *
+ * class Robot {
+ * Alert alert = new Alert("Something went wrong", Alert.Level.WARNING);
+ *
+ * periodic() {
+ * alert.set(...);
+ * }
+ * }
+ *
+ *
+ * Alternatively, alerts which are only used once at startup can be created and activated inline.
+ *
+ *
+ * public Robot() {
+ * new Alert("Failed to load auto paths", Alert.Level.ERROR).set(true);
+ * }
+ *
+ */
+public class Alert implements AutoCloseable {
+ /** Represents an alert's level of urgency. */
+ public enum Level {
+ /**
+ * 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.
+ */
+ HIGH(AlertJNI.LEVEL_HIGH),
+
+ /** Alternate name for a high priority alert. */
+ ERROR(HIGH.m_value),
+
+ /**
+ * 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.
+ */
+ MEDIUM(AlertJNI.LEVEL_MEDIUM),
+
+ /** Alternate name for a medium priority alert. */
+ WARNING(MEDIUM.m_value),
+
+ /**
+ * 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.
+ */
+ LOW(AlertJNI.LEVEL_LOW),
+
+ /** Alternate name for a low priority alert. */
+ INFO(LOW.m_value);
+
+ private final int m_value;
+
+ Level(int value) {
+ m_value = value;
+ }
+ }
+
+ private final Level m_type;
+ private int m_handle;
+
+ /**
+ * Creates a new alert in the default group - "Alerts". If this is the first 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.
+ */
+ public Alert(String text, Level type) {
+ this("Alerts", text, type);
+ }
+
+ /**
+ * Creates a new alert. If this is the first to be instantiated in its group, the appropriate
+ * entries will be added to NetworkTables.
+ *
+ * @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.
+ */
+ public Alert(String group, String text, Level type) {
+ m_type = type;
+ m_handle = AlertJNI.createAlert(group, text, type.m_value);
+ }
+
+ /**
+ * Sets whether the alert should currently be displayed. This method can be safely called
+ * periodically.
+ *
+ * @param active Whether to display the alert.
+ */
+ public void set(boolean active) {
+ AlertJNI.setAlertActive(m_handle, active);
+ }
+
+ /**
+ * Gets whether the alert is active.
+ *
+ * @return whether the alert is active.
+ */
+ public boolean get() {
+ return AlertJNI.isAlertActive(m_handle);
+ }
+
+ /**
+ * Updates current alert text. Use this method to dynamically change the displayed alert, such as
+ * including more details about the detected problem.
+ *
+ * @param text Text to be displayed when the alert is active.
+ */
+ public void setText(String text) {
+ AlertJNI.setAlertText(m_handle, text);
+ }
+
+ /**
+ * Gets the current alert text.
+ *
+ * @return the current text.
+ */
+ public String getText() {
+ return AlertJNI.getAlertText(m_handle);
+ }
+
+ /**
+ * Get the type of this alert.
+ *
+ * @return the type
+ */
+ public Level getType() {
+ return m_type;
+ }
+
+ @Override
+ public void close() {
+ AlertJNI.destroyAlert(m_handle);
+ m_handle = 0;
+ }
+}
diff --git a/wpilibj/src/main/java/org/wpilib/simulation/AlertSim.java b/wpilibj/src/main/java/org/wpilib/simulation/AlertSim.java
new file mode 100644
index 0000000000..eba775a994
--- /dev/null
+++ b/wpilibj/src/main/java/org/wpilib/simulation/AlertSim.java
@@ -0,0 +1,91 @@
+// 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.
+
+package org.wpilib.simulation;
+
+import org.wpilib.driverstation.Alert.Level;
+import org.wpilib.hardware.hal.AlertJNI;
+import org.wpilib.hardware.hal.simulation.AlertDataJNI;
+
+/** Simulation for alerts. */
+public final class AlertSim {
+ private AlertSim() {
+ throw new UnsupportedOperationException("This is a utility class!");
+ }
+
+ /** Information about an alert. */
+ public static class AlertInfo {
+ AlertInfo(AlertDataJNI.AlertInfo info) {
+ this.handle = info.handle;
+ this.group = info.group;
+ this.text = info.text;
+ this.activeStartTime = info.activeStartTime;
+ this.level =
+ switch (info.level) {
+ case AlertJNI.LEVEL_HIGH -> Level.HIGH;
+ case AlertJNI.LEVEL_MEDIUM -> Level.MEDIUM;
+ case AlertJNI.LEVEL_LOW -> Level.LOW;
+ default -> throw new IllegalArgumentException("Unknown alert level: " + info.level);
+ };
+ }
+
+ /** The handle of the alert. */
+ @SuppressWarnings("MemberName")
+ public final int handle;
+
+ /** The group of the alert. */
+ @SuppressWarnings("MemberName")
+ public final String group;
+
+ /** The text of the alert. */
+ @SuppressWarnings("MemberName")
+ public final String text;
+
+ /** The time the alert became active. 0 if not active. */
+ @SuppressWarnings("MemberName")
+ public final long activeStartTime;
+
+ /** The level of the alert (HIGH, MEDIUM, or LOW). */
+ @SuppressWarnings("MemberName")
+ public final Level level;
+
+ /**
+ * Returns whether the alert is currently active.
+ *
+ * @return true if the alert is active, false otherwise
+ */
+ public boolean isActive() {
+ 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
+ */
+ public static int getCount() {
+ return AlertDataJNI.getNumAlerts();
+ }
+
+ /**
+ * Gets detailed information about each alert.
+ *
+ * @return Alerts
+ */
+ public static AlertInfo[] getAll() {
+ AlertDataJNI.AlertInfo[] alertInfos = AlertDataJNI.getAlerts();
+ AlertInfo[] infos = new AlertInfo[alertInfos.length];
+ for (int i = 0; i < alertInfos.length; i++) {
+ infos[i] = new AlertInfo(alertInfos[i]);
+ }
+ return infos;
+ }
+
+ /** Resets all alert simulation data. */
+ public static void resetData() {
+ AlertDataJNI.resetData();
+ }
+}
diff --git a/wpilibj/src/main/java/org/wpilib/util/Alert.java b/wpilibj/src/main/java/org/wpilib/util/Alert.java
deleted file mode 100644
index 58c722cf49..0000000000
--- a/wpilibj/src/main/java/org/wpilib/util/Alert.java
+++ /dev/null
@@ -1,232 +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.
-
-package org.wpilib.util;
-
-import java.util.Comparator;
-import java.util.EnumMap;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import org.wpilib.smartdashboard.SmartDashboard;
-import org.wpilib.system.RobotController;
-import org.wpilib.util.sendable.Sendable;
-import org.wpilib.util.sendable.SendableBuilder;
-
-/**
- * Persistent alert to be sent via NetworkTables. Alerts are tagged with a type of {@code kError},
- * {@code kWarning}, or {@code kInfo} to denote urgency. See {@link org.wpilib.util.Alert.AlertType
- * 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.
- *
- * Alerts should be created once and stored persistently, then updated to "active" or "inactive"
- * as necessary. {@link #set(boolean)} 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.
- *
- *
- * class Robot {
- * Alert alert = new Alert("Something went wrong", AlertType.kWarning);
- *
- * periodic() {
- * alert.set(...);
- * }
- * }
- *
- *
- * Alternatively, alerts which are only used once at startup can be created and activated inline.
- *
- *
- * public Robot() {
- * new Alert("Failed to load auto paths", AlertType.kError).set(true);
- * }
- *
- */
-public class Alert implements AutoCloseable {
- /** Represents an alert's level of urgency. */
- public enum AlertType {
- /**
- * High priority alert - displayed first on the dashboard with a red "X" symbol. Use this type
- * for problems which will seriously affect the robot's functionality and thus require immediate
- * attention.
- */
- kError,
-
- /**
- * 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.
- */
- kWarning,
-
- /**
- * 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.
- */
- kInfo
- }
-
- private final AlertType m_type;
- private boolean m_active;
- private long m_activeStartTime;
- private String m_text;
- private Set m_activeAlerts;
-
- /**
- * Creates a new alert in the default group - "Alerts". If this is the first 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.
- */
- public Alert(String text, AlertType type) {
- this("Alerts", text, type);
- }
-
- /**
- * Creates a new alert. If this is the first to be instantiated in its group, the appropriate
- * entries will be added to NetworkTables.
- *
- * @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.
- */
- @SuppressWarnings("this-escape")
- public Alert(String group, String text, AlertType type) {
- m_type = type;
- m_text = text;
- m_activeAlerts = SendableAlerts.forGroup(group).getActiveAlertsStorage(type);
- }
-
- /**
- * Sets whether the alert should currently be displayed. This method can be safely called
- * periodically.
- *
- * @param active Whether to display the alert.
- */
- public void set(boolean active) {
- if (active == m_active) {
- return;
- }
-
- if (active) {
- m_activeStartTime = RobotController.getTime();
- m_activeAlerts.add(new PublishedAlert(m_activeStartTime, m_text));
- } else {
- m_activeAlerts.remove(new PublishedAlert(m_activeStartTime, m_text));
- }
- m_active = active;
- }
-
- /**
- * Gets whether the alert is active.
- *
- * @return whether the alert is active.
- */
- public boolean get() {
- return m_active;
- }
-
- /**
- * Updates current alert text. Use this method to dynamically change the displayed alert, such as
- * including more details about the detected problem.
- *
- * @param text Text to be displayed when the alert is active.
- */
- public void setText(String text) {
- if (text.equals(m_text)) {
- return;
- }
- var oldText = m_text;
- m_text = text;
- if (m_active) {
- m_activeAlerts.remove(new PublishedAlert(m_activeStartTime, oldText));
- m_activeAlerts.add(new PublishedAlert(m_activeStartTime, m_text));
- }
- }
-
- /**
- * Gets the current alert text.
- *
- * @return the current text.
- */
- public String getText() {
- return m_text;
- }
-
- /**
- * Get the type of this alert.
- *
- * @return the type
- */
- public AlertType getType() {
- return m_type;
- }
-
- @Override
- public void close() {
- set(false);
- }
-
- private record PublishedAlert(long timestamp, String text) implements Comparable {
- private static final Comparator comparator =
- Comparator.comparingLong((PublishedAlert alert) -> alert.timestamp())
- .reversed()
- .thenComparing(Comparator.comparing((PublishedAlert alert) -> alert.text()));
-
- @Override
- public int compareTo(PublishedAlert o) {
- return comparator.compare(this, o);
- }
- }
-
- private static final class SendableAlerts implements Sendable {
- private static final Map groups = new HashMap();
-
- private final EnumMap> m_alerts = new EnumMap<>(AlertType.class);
-
- /**
- * 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
- */
- public Set getActiveAlertsStorage(AlertType type) {
- return m_alerts.computeIfAbsent(type, _type -> new TreeSet<>());
- }
-
- private String[] getStrings(AlertType type) {
- return getActiveAlertsStorage(type).stream().map(a -> a.text()).toArray(String[]::new);
- }
-
- @Override
- public void initSendable(SendableBuilder builder) {
- builder.setSmartDashboardType("Alerts");
- builder.addStringArrayProperty("errors", () -> getStrings(AlertType.kError), null);
- builder.addStringArrayProperty("warnings", () -> getStrings(AlertType.kWarning), null);
- builder.addStringArrayProperty("infos", () -> getStrings(AlertType.kInfo), null);
- }
-
- /**
- * 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
- */
- private static SendableAlerts forGroup(String group) {
- return groups.computeIfAbsent(
- group,
- _group -> {
- var sendable = new SendableAlerts();
- SmartDashboard.putData(_group, sendable);
- return sendable;
- });
- }
- }
-}
diff --git a/wpilibj/src/test/java/org/wpilib/simulation/AlertSimTest.java b/wpilibj/src/test/java/org/wpilib/simulation/AlertSimTest.java
new file mode 100644
index 0000000000..653d47366d
--- /dev/null
+++ b/wpilibj/src/test/java/org/wpilib/simulation/AlertSimTest.java
@@ -0,0 +1,158 @@
+// 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.
+
+package org.wpilib.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.wpilib.driverstation.Alert;
+import org.wpilib.driverstation.Alert.Level;
+import org.wpilib.hardware.hal.HAL;
+
+class AlertSimTest {
+ private String m_groupName;
+
+ @BeforeEach
+ void setup(TestInfo info) {
+ HAL.initialize(500, 0);
+ m_groupName = "AlertTest_" + info.getDisplayName();
+ }
+
+ @AfterEach
+ void cleanup() {
+ AlertSim.resetData();
+ }
+
+ private String[] getActiveAlerts(Level type) {
+ return Arrays.stream(AlertSim.getAll())
+ .filter(a -> a.isActive() && a.level == type)
+ .map(a -> a.text)
+ .toArray(String[]::new);
+ }
+
+ private boolean isAlertActive(String text, Alert.Level type) {
+ return Arrays.stream(AlertSim.getAll())
+ .filter(a -> a.isActive() && a.level == type)
+ .anyMatch(a -> a.text.equals(text));
+ }
+
+ private void assertState(Alert.Level type, List state) {
+ assertEquals(state, Arrays.asList(getActiveAlerts(type)));
+ }
+
+ private Alert makeAlert(String text, Alert.Level type) {
+ return new Alert(m_groupName, text, type);
+ }
+
+ @Test
+ void testInitialization() {
+ assertEquals(0, AlertSim.getCount());
+ assertEquals(0, AlertSim.getAll().length);
+ }
+
+ @Test
+ void testReset() {
+ try (var alert = makeAlert("alert", Level.HIGH)) {
+ alert.set(true);
+ assertTrue(isAlertActive("alert", Level.HIGH));
+ }
+ AlertSim.resetData();
+ assertFalse(isAlertActive("alert", Level.HIGH));
+ }
+
+ @Test
+ void setUnsetSingle() {
+ try (var one = makeAlert("one", Level.LOW)) {
+ assertFalse(isAlertActive("one", Level.LOW));
+ one.set(true);
+ assertTrue(isAlertActive("one", Level.LOW));
+ one.set(false);
+ assertFalse(isAlertActive("one", Level.LOW));
+ }
+ }
+
+ @Test
+ void setUnsetMultiple() {
+ try (var one = makeAlert("one", Level.HIGH);
+ var two = makeAlert("two", Level.LOW)) {
+ assertFalse(isAlertActive("one", Level.HIGH));
+ assertFalse(isAlertActive("two", Level.LOW));
+ one.set(true);
+ assertTrue(isAlertActive("one", Level.HIGH));
+ assertFalse(isAlertActive("two", Level.LOW));
+ one.set(true);
+ two.set(true);
+ assertTrue(isAlertActive("one", Level.HIGH));
+ assertTrue(isAlertActive("two", Level.LOW));
+ one.set(false);
+ assertFalse(isAlertActive("one", Level.HIGH));
+ assertTrue(isAlertActive("two", Level.LOW));
+ }
+ }
+
+ @Test
+ void setIsIdempotent() {
+ try (var a = makeAlert("A", Level.LOW);
+ var b = makeAlert("B", Level.LOW);
+ var c = makeAlert("C", Level.LOW)) {
+ a.set(true);
+ b.set(true);
+ c.set(true);
+
+ var startState = List.of(getActiveAlerts(Level.LOW));
+
+ b.set(true);
+ assertState(Level.LOW, startState);
+
+ a.set(true);
+ assertState(Level.LOW, startState);
+ }
+ }
+
+ @Test
+ void closeUnsetsAlert() {
+ try (var alert = makeAlert("alert", Level.MEDIUM)) {
+ alert.set(true);
+ assertTrue(isAlertActive("alert", Level.MEDIUM));
+ }
+ assertFalse(isAlertActive("alert", Level.MEDIUM));
+ }
+
+ @Test
+ void setTextWhileUnset() {
+ try (var alert = makeAlert("BEFORE", Level.LOW)) {
+ assertEquals("BEFORE", alert.getText());
+ alert.set(true);
+ assertTrue(isAlertActive("BEFORE", Level.LOW));
+ alert.set(false);
+ assertFalse(isAlertActive("BEFORE", Level.LOW));
+ alert.setText("AFTER");
+ assertEquals("AFTER", alert.getText());
+ alert.set(true);
+ assertFalse(isAlertActive("BEFORE", Level.LOW));
+ assertTrue(isAlertActive("AFTER", Level.LOW));
+ }
+ }
+
+ @Test
+ void setTextWhileSet() {
+ try (var alert = makeAlert("BEFORE", Level.LOW)) {
+ assertEquals("BEFORE", alert.getText());
+ alert.set(true);
+ assertTrue(isAlertActive("BEFORE", Level.LOW));
+ alert.setText("AFTER");
+ assertEquals("AFTER", alert.getText());
+ assertFalse(isAlertActive("BEFORE", Level.LOW));
+ assertTrue(isAlertActive("AFTER", Level.LOW));
+ }
+ }
+}
diff --git a/wpilibj/src/test/java/org/wpilib/util/AlertTest.java b/wpilibj/src/test/java/org/wpilib/util/AlertTest.java
deleted file mode 100644
index 2757a73a31..0000000000
--- a/wpilibj/src/test/java/org/wpilib/util/AlertTest.java
+++ /dev/null
@@ -1,243 +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.
-
-package org.wpilib.util;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInfo;
-import org.junit.jupiter.api.parallel.ResourceLock;
-import org.wpilib.networktables.NetworkTableInstance;
-import org.wpilib.networktables.StringArraySubscriber;
-import org.wpilib.simulation.SimHooks;
-import org.wpilib.smartdashboard.SmartDashboard;
-import org.wpilib.util.Alert.AlertType;
-
-class AlertTest {
- private NetworkTableInstance m_inst;
- private String m_groupName;
-
- @BeforeEach
- void setup(TestInfo info) {
- m_groupName = "AlertTest_" + info.getDisplayName();
-
- m_inst = NetworkTableInstance.create();
- SmartDashboard.setNetworkTableInstance(m_inst);
- }
-
- @AfterEach
- void checkClean() {
- update();
- assertEquals(0, getActiveAlerts(AlertType.kError).length);
- assertEquals(0, getActiveAlerts(AlertType.kWarning).length);
- assertEquals(0, getActiveAlerts(AlertType.kInfo).length);
-
- m_inst.close();
- SmartDashboard.setNetworkTableInstance(NetworkTableInstance.getDefault());
- }
-
- private String getSubtableName(Alert.AlertType type) {
- switch (type) {
- case kError:
- return "errors";
- case kWarning:
- return "warnings";
- case kInfo:
- return "infos";
- default:
- return "unknown";
- }
- }
-
- private StringArraySubscriber getSubscriberForType(Alert.AlertType type) {
- return m_inst
- .getStringArrayTopic("/SmartDashboard/" + m_groupName + "/" + getSubtableName(type))
- .subscribe(new String[] {});
- }
-
- private String[] getActiveAlerts(AlertType type) {
- update();
- try (var sub = getSubscriberForType(type)) {
- return sub.get();
- }
- }
-
- private void update() {
- SmartDashboard.updateValues();
- }
-
- private boolean isAlertActive(String text, Alert.AlertType type) {
- return Arrays.asList(getActiveAlerts(type)).contains(text);
- }
-
- private void assertState(Alert.AlertType type, List state) {
- assertEquals(state, Arrays.asList(getActiveAlerts(type)));
- }
-
- private Alert makeAlert(String text, Alert.AlertType type) {
- return new Alert(m_groupName, text, type);
- }
-
- @Test
- void setUnsetSingle() {
- try (var one = makeAlert("one", AlertType.kInfo)) {
- assertFalse(isAlertActive("one", AlertType.kInfo));
- one.set(true);
- assertTrue(isAlertActive("one", AlertType.kInfo));
- one.set(false);
- assertFalse(isAlertActive("one", AlertType.kInfo));
- }
- }
-
- @Test
- void setUnsetMultiple() {
- try (var one = makeAlert("one", AlertType.kError);
- var two = makeAlert("two", AlertType.kInfo)) {
- assertFalse(isAlertActive("one", AlertType.kError));
- assertFalse(isAlertActive("two", AlertType.kInfo));
- one.set(true);
- assertTrue(isAlertActive("one", AlertType.kError));
- assertFalse(isAlertActive("two", AlertType.kInfo));
- one.set(true);
- two.set(true);
- assertTrue(isAlertActive("one", AlertType.kError));
- assertTrue(isAlertActive("two", AlertType.kInfo));
- one.set(false);
- assertFalse(isAlertActive("one", AlertType.kError));
- assertTrue(isAlertActive("two", AlertType.kInfo));
- }
- }
-
- @Test
- void setIsIdempotent() {
- try (var a = makeAlert("A", AlertType.kInfo);
- var b = makeAlert("B", AlertType.kInfo);
- var c = makeAlert("C", AlertType.kInfo)) {
- a.set(true);
- b.set(true);
- c.set(true);
-
- var startState = List.of(getActiveAlerts(AlertType.kInfo));
-
- b.set(true);
- assertState(AlertType.kInfo, startState);
-
- a.set(true);
- assertState(AlertType.kInfo, startState);
- }
- }
-
- @Test
- void closeUnsetsAlert() {
- try (var alert = makeAlert("alert", AlertType.kWarning)) {
- alert.set(true);
- assertTrue(isAlertActive("alert", AlertType.kWarning));
- }
- assertFalse(isAlertActive("alert", AlertType.kWarning));
- }
-
- @Test
- void setTextWhileUnset() {
- try (var alert = makeAlert("BEFORE", AlertType.kInfo)) {
- assertEquals("BEFORE", alert.getText());
- alert.set(true);
- assertTrue(isAlertActive("BEFORE", AlertType.kInfo));
- alert.set(false);
- assertFalse(isAlertActive("BEFORE", AlertType.kInfo));
- alert.setText("AFTER");
- assertEquals("AFTER", alert.getText());
- alert.set(true);
- assertFalse(isAlertActive("BEFORE", AlertType.kInfo));
- assertTrue(isAlertActive("AFTER", AlertType.kInfo));
- }
- }
-
- @Test
- void setTextWhileSet() {
- try (var alert = makeAlert("BEFORE", AlertType.kInfo)) {
- assertEquals("BEFORE", alert.getText());
- alert.set(true);
- assertTrue(isAlertActive("BEFORE", AlertType.kInfo));
- alert.setText("AFTER");
- assertEquals("AFTER", alert.getText());
- assertFalse(isAlertActive("BEFORE", AlertType.kInfo));
- assertTrue(isAlertActive("AFTER", AlertType.kInfo));
- }
- }
-
- @ResourceLock("timing")
- @Test
- void setTextDoesNotAffectFirstOrderSort() {
- SimHooks.pauseTiming();
- try (var a = makeAlert("A", AlertType.kInfo);
- var b = makeAlert("B", AlertType.kInfo);
- var c = makeAlert("C", AlertType.kInfo)) {
- a.set(true);
- SimHooks.stepTiming(1);
- b.set(true);
- SimHooks.stepTiming(1);
- c.set(true);
-
- var expectedEndState = new ArrayList<>(List.of(getActiveAlerts(AlertType.kInfo)));
- expectedEndState.replaceAll(s -> "B".equals(s) ? "AFTER" : s);
- b.setText("AFTER");
-
- assertState(AlertType.kInfo, expectedEndState);
- } finally {
- SimHooks.resumeTiming();
- }
- }
-
- @ResourceLock("timing")
- @Test
- void sortOrder() {
- SimHooks.pauseTiming();
- try (var a = makeAlert("A", AlertType.kInfo);
- var b = makeAlert("B", AlertType.kInfo);
- var c = makeAlert("C", AlertType.kInfo)) {
- a.set(true);
- assertState(AlertType.kInfo, List.of("A"));
- SimHooks.stepTiming(1);
- b.set(true);
- assertState(AlertType.kInfo, List.of("B", "A"));
- SimHooks.stepTiming(1);
- c.set(true);
- assertState(AlertType.kInfo, List.of("C", "B", "A"));
-
- SimHooks.stepTiming(1);
- c.set(false);
- assertState(AlertType.kInfo, List.of("B", "A"));
-
- SimHooks.stepTiming(1);
- c.set(true);
- assertState(AlertType.kInfo, List.of("C", "B", "A"));
-
- SimHooks.stepTiming(1);
- a.set(false);
- assertState(AlertType.kInfo, List.of("C", "B"));
-
- SimHooks.stepTiming(1);
- b.set(false);
- assertState(AlertType.kInfo, List.of("C"));
-
- SimHooks.stepTiming(1);
- b.set(true);
- assertState(AlertType.kInfo, List.of("B", "C"));
-
- SimHooks.stepTiming(1);
- a.set(true);
- assertState(AlertType.kInfo, List.of("A", "B", "C"));
- } finally {
- SimHooks.resumeTiming();
- }
- }
-}