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.livewindow;
006
007import edu.wpi.first.networktables.BooleanPublisher;
008import edu.wpi.first.networktables.NetworkTable;
009import edu.wpi.first.networktables.NetworkTableInstance;
010import edu.wpi.first.networktables.StringPublisher;
011import edu.wpi.first.networktables.StringTopic;
012import edu.wpi.first.util.sendable.Sendable;
013import edu.wpi.first.util.sendable.SendableRegistry;
014import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl;
015
016/**
017 * The LiveWindow class is the public interface for putting sensors and actuators on the LiveWindow.
018 */
019public final class LiveWindow {
020  private static final class Component implements AutoCloseable {
021    @Override
022    public void close() {
023      if (m_namePub != null) {
024        m_namePub.close();
025        m_namePub = null;
026      }
027      if (m_typePub != null) {
028        m_typePub.close();
029        m_typePub = null;
030      }
031    }
032
033    boolean m_firstTime = true;
034    boolean m_telemetryEnabled;
035    StringPublisher m_namePub;
036    StringPublisher m_typePub;
037  }
038
039  private static final String kSmartDashboardType = "LW Subsystem";
040
041  private static final int dataHandle = SendableRegistry.getDataHandle();
042  private static final NetworkTable liveWindowTable =
043      NetworkTableInstance.getDefault().getTable("LiveWindow");
044  private static final NetworkTable statusTable = liveWindowTable.getSubTable(".status");
045  private static final BooleanPublisher enabledPub =
046      statusTable.getBooleanTopic("LW Enabled").publish();
047  private static boolean startLiveWindow;
048  private static boolean liveWindowEnabled;
049  private static boolean telemetryEnabled;
050
051  private static Runnable enabledListener;
052  private static Runnable disabledListener;
053
054  static {
055    SendableRegistry.setLiveWindowBuilderFactory(SendableBuilderImpl::new);
056    enabledPub.set(false);
057  }
058
059  private static Component getOrAdd(Sendable sendable) {
060    Component data = (Component) SendableRegistry.getData(sendable, dataHandle);
061    if (data == null) {
062      data = new Component();
063      SendableRegistry.setData(sendable, dataHandle, data);
064    }
065    return data;
066  }
067
068  private LiveWindow() {
069    throw new UnsupportedOperationException("This is a utility class!");
070  }
071
072  /**
073   * Sets function to be called when LiveWindow is enabled.
074   *
075   * @param runnable function (or null for none)
076   */
077  public static synchronized void setEnabledListener(Runnable runnable) {
078    enabledListener = runnable;
079  }
080
081  /**
082   * Sets function to be called when LiveWindow is disabled.
083   *
084   * @param runnable function (or null for none)
085   */
086  public static synchronized void setDisabledListener(Runnable runnable) {
087    disabledListener = runnable;
088  }
089
090  /**
091   * Returns true if LiveWindow is enabled.
092   *
093   * @return True if LiveWindow is enabled.
094   */
095  public static synchronized boolean isEnabled() {
096    return liveWindowEnabled;
097  }
098
099  /**
100   * Set the enabled state of LiveWindow.
101   *
102   * <p>If it's being enabled, turn off the scheduler and remove all the commands from the queue and
103   * enable all the components registered for LiveWindow. If it's being disabled, stop all the
104   * registered components and re-enable the scheduler.
105   *
106   * <p>TODO: add code to disable PID loops when enabling LiveWindow. The commands should re-enable
107   * the PID loops themselves when they get rescheduled. This prevents arms from starting to move
108   * around, etc. after a period of adjusting them in LiveWindow mode.
109   *
110   * @param enabled True to enable LiveWindow.
111   */
112  public static synchronized void setEnabled(boolean enabled) {
113    if (liveWindowEnabled != enabled) {
114      startLiveWindow = enabled;
115      liveWindowEnabled = enabled;
116      updateValues(); // Force table generation now to make sure everything is defined
117      if (enabled) {
118        System.out.println("Starting live window mode.");
119        if (enabledListener != null) {
120          enabledListener.run();
121        }
122      } else {
123        System.out.println("stopping live window mode.");
124        SendableRegistry.foreachLiveWindow(
125            dataHandle, cbdata -> ((SendableBuilderImpl) cbdata.builder).stopLiveWindowMode());
126        if (disabledListener != null) {
127          disabledListener.run();
128        }
129      }
130      enabledPub.set(enabled);
131    }
132  }
133
134  /**
135   * Enable telemetry for a single component.
136   *
137   * @param sendable component
138   */
139  public static synchronized void enableTelemetry(Sendable sendable) {
140    // Re-enable global setting in case disableAllTelemetry() was called.
141    telemetryEnabled = true;
142    getOrAdd(sendable).m_telemetryEnabled = true;
143  }
144
145  /**
146   * Disable telemetry for a single component.
147   *
148   * @param sendable component
149   */
150  public static synchronized void disableTelemetry(Sendable sendable) {
151    getOrAdd(sendable).m_telemetryEnabled = false;
152  }
153
154  /** Disable ALL telemetry. */
155  public static synchronized void disableAllTelemetry() {
156    telemetryEnabled = false;
157    SendableRegistry.foreachLiveWindow(
158        dataHandle,
159        cbdata -> {
160          if (cbdata.data == null) {
161            cbdata.data = new Component();
162          }
163          ((Component) cbdata.data).m_telemetryEnabled = false;
164        });
165  }
166
167  /** Enable ALL telemetry. */
168  public static synchronized void enableAllTelemetry() {
169    telemetryEnabled = true;
170    SendableRegistry.foreachLiveWindow(
171        dataHandle,
172        cbdata -> {
173          if (cbdata.data == null) {
174            cbdata.data = new Component();
175          }
176          ((Component) cbdata.data).m_telemetryEnabled = true;
177        });
178  }
179
180  /**
181   * Tell all the sensors to update (send) their values.
182   *
183   * <p>Actuators are handled through callbacks on their value changing from the SmartDashboard
184   * widgets.
185   */
186  public static synchronized void updateValues() {
187    // Only do this if either LiveWindow mode or telemetry is enabled.
188    if (!liveWindowEnabled && !telemetryEnabled) {
189      return;
190    }
191
192    SendableRegistry.foreachLiveWindow(
193        dataHandle,
194        cbdata -> {
195          if (cbdata.sendable == null || cbdata.parent != null) {
196            return;
197          }
198
199          if (cbdata.data == null) {
200            cbdata.data = new Component();
201          }
202
203          Component component = (Component) cbdata.data;
204
205          if (!liveWindowEnabled && !component.m_telemetryEnabled) {
206            return;
207          }
208
209          if (component.m_firstTime) {
210            // By holding off creating the NetworkTable entries, it allows the
211            // components to be redefined. This allows default sensor and actuator
212            // values to be created that are replaced with the custom names from
213            // users calling setName.
214            if (cbdata.name.isEmpty()) {
215              return;
216            }
217            NetworkTable ssTable = liveWindowTable.getSubTable(cbdata.subsystem);
218            NetworkTable table;
219            // Treat name==subsystem as top level of subsystem
220            if (cbdata.name.equals(cbdata.subsystem)) {
221              table = ssTable;
222            } else {
223              table = ssTable.getSubTable(cbdata.name);
224            }
225            component.m_namePub = new StringTopic(table.getTopic(".name")).publish();
226            component.m_namePub.set(cbdata.name);
227            ((SendableBuilderImpl) cbdata.builder).setTable(table);
228            cbdata.sendable.initSendable(cbdata.builder);
229            component.m_typePub =
230                new StringTopic(ssTable.getTopic(".type"))
231                    .publishEx(
232                        StringTopic.kTypeString,
233                        "{\"SmartDashboard\":\"" + kSmartDashboardType + "\"}");
234            component.m_typePub.set(kSmartDashboardType);
235
236            component.m_firstTime = false;
237          }
238
239          if (startLiveWindow) {
240            ((SendableBuilderImpl) cbdata.builder).startLiveWindowMode();
241          }
242          cbdata.builder.update();
243        });
244
245    startLiveWindow = false;
246  }
247}