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(); - } - } -}