001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.wpilibj; 006 007import edu.wpi.first.util.sendable.Sendable; 008import edu.wpi.first.util.sendable.SendableBuilder; 009import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; 010import java.util.Comparator; 011import java.util.EnumMap; 012import java.util.HashMap; 013import java.util.Map; 014import java.util.Set; 015import java.util.TreeSet; 016 017/** 018 * Persistent alert to be sent via NetworkTables. Alerts are tagged with a type of {@code kError}, 019 * {@code kWarning}, or {@code kInfo} to denote urgency. See {@link 020 * edu.wpi.first.wpilibj.Alert.AlertType AlertType} for suggested usage of each type. Alerts can be 021 * displayed on supported dashboards, and are shown in a priority order based on type and recency of 022 * activation, with newly activated alerts first. 023 * 024 * <p>Alerts should be created once and stored persistently, then updated to "active" or "inactive" 025 * as necessary. {@link #set(boolean)} can be safely called periodically. 026 * 027 * <p><b>This API is new for 2025, but is likely to change in future seasons to facilitate deeper 028 * integration with the robot control system.</b> 029 * 030 * <pre> 031 * class Robot { 032 * Alert alert = new Alert("Something went wrong", AlertType.kWarning); 033 * 034 * periodic() { 035 * alert.set(...); 036 * } 037 * } 038 * </pre> 039 * 040 * <p>Alternatively, alerts which are only used once at startup can be created and activated inline. 041 * 042 * <pre> 043 * public Robot() { 044 * new Alert("Failed to load auto paths", AlertType.kError).set(true); 045 * } 046 * </pre> 047 */ 048public class Alert implements AutoCloseable { 049 /** Represents an alert's level of urgency. */ 050 public enum AlertType { 051 /** 052 * High priority alert - displayed first on the dashboard with a red "X" symbol. Use this type 053 * for problems which will seriously affect the robot's functionality and thus require immediate 054 * attention. 055 */ 056 kError, 057 058 /** 059 * Medium priority alert - displayed second on the dashboard with a yellow "!" symbol. Use this 060 * type for problems which could affect the robot's functionality but do not necessarily require 061 * immediate attention. 062 */ 063 kWarning, 064 065 /** 066 * Low priority alert - displayed last on the dashboard with a green "i" symbol. Use this type 067 * for problems which are unlikely to affect the robot's functionality, or any other alerts 068 * which do not fall under the other categories. 069 */ 070 kInfo 071 } 072 073 private final AlertType m_type; 074 private boolean m_active; 075 private long m_activeStartTime; 076 private String m_text; 077 private Set<PublishedAlert> m_activeAlerts; 078 079 /** 080 * Creates a new alert in the default group - "Alerts". If this is the first to be instantiated, 081 * the appropriate entries will be added to NetworkTables. 082 * 083 * @param text Text to be displayed when the alert is active. 084 * @param type Alert urgency level. 085 */ 086 public Alert(String text, AlertType type) { 087 this("Alerts", text, type); 088 } 089 090 /** 091 * Creates a new alert. If this is the first to be instantiated in its group, the appropriate 092 * entries will be added to NetworkTables. 093 * 094 * @param group Group identifier, used as the entry name in NetworkTables. 095 * @param text Text to be displayed when the alert is active. 096 * @param type Alert urgency level. 097 */ 098 @SuppressWarnings("this-escape") 099 public Alert(String group, String text, AlertType type) { 100 m_type = type; 101 m_text = text; 102 m_activeAlerts = SendableAlerts.forGroup(group).getActiveAlertsStorage(type); 103 } 104 105 /** 106 * Sets whether the alert should currently be displayed. This method can be safely called 107 * periodically. 108 * 109 * @param active Whether to display the alert. 110 */ 111 public void set(boolean active) { 112 if (active == m_active) { 113 return; 114 } 115 116 if (active) { 117 m_activeStartTime = RobotController.getTime(); 118 m_activeAlerts.add(new PublishedAlert(m_activeStartTime, m_text)); 119 } else { 120 m_activeAlerts.remove(new PublishedAlert(m_activeStartTime, m_text)); 121 } 122 m_active = active; 123 } 124 125 /** 126 * Gets whether the alert is active. 127 * 128 * @return whether the alert is active. 129 */ 130 public boolean get() { 131 return m_active; 132 } 133 134 /** 135 * Updates current alert text. Use this method to dynamically change the displayed alert, such as 136 * including more details about the detected problem. 137 * 138 * @param text Text to be displayed when the alert is active. 139 */ 140 public void setText(String text) { 141 if (text.equals(m_text)) { 142 return; 143 } 144 var oldText = m_text; 145 m_text = text; 146 if (m_active) { 147 m_activeAlerts.remove(new PublishedAlert(m_activeStartTime, oldText)); 148 m_activeAlerts.add(new PublishedAlert(m_activeStartTime, m_text)); 149 } 150 } 151 152 /** 153 * Gets the current alert text. 154 * 155 * @return the current text. 156 */ 157 public String getText() { 158 return m_text; 159 } 160 161 /** 162 * Get the type of this alert. 163 * 164 * @return the type 165 */ 166 public AlertType getType() { 167 return m_type; 168 } 169 170 @Override 171 public void close() { 172 set(false); 173 } 174 175 private record PublishedAlert(long timestamp, String text) implements Comparable<PublishedAlert> { 176 private static final Comparator<PublishedAlert> comparator = 177 Comparator.comparingLong((PublishedAlert alert) -> alert.timestamp()) 178 .reversed() 179 .thenComparing(Comparator.comparing((PublishedAlert alert) -> alert.text())); 180 181 @Override 182 public int compareTo(PublishedAlert o) { 183 return comparator.compare(this, o); 184 } 185 } 186 187 private static final class SendableAlerts implements Sendable { 188 private static final Map<String, SendableAlerts> groups = new HashMap<String, SendableAlerts>(); 189 190 private final EnumMap<AlertType, Set<PublishedAlert>> m_alerts = new EnumMap<>(AlertType.class); 191 192 /** 193 * Returns a reference to the set of active alerts for the given type. 194 * 195 * @param type the type 196 * @return reference to the set of active alerts for the type 197 */ 198 public Set<PublishedAlert> getActiveAlertsStorage(AlertType type) { 199 return m_alerts.computeIfAbsent(type, _type -> new TreeSet<>()); 200 } 201 202 private String[] getStrings(AlertType type) { 203 return getActiveAlertsStorage(type).stream().map(a -> a.text()).toArray(String[]::new); 204 } 205 206 @Override 207 public void initSendable(SendableBuilder builder) { 208 builder.setSmartDashboardType("Alerts"); 209 builder.addStringArrayProperty("errors", () -> getStrings(AlertType.kError), null); 210 builder.addStringArrayProperty("warnings", () -> getStrings(AlertType.kWarning), null); 211 builder.addStringArrayProperty("infos", () -> getStrings(AlertType.kInfo), null); 212 } 213 214 /** 215 * Returns the SendableAlerts for a given group, initializing and publishing if it does not 216 * already exist. 217 * 218 * @param group the group name 219 * @return the SendableAlerts for the group 220 */ 221 private static SendableAlerts forGroup(String group) { 222 return groups.computeIfAbsent( 223 group, 224 _group -> { 225 var sendable = new SendableAlerts(); 226 SmartDashboard.putData(_group, sendable); 227 return sendable; 228 }); 229 } 230 } 231}