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}