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