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.shuffleboard;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.cscore.VideoSource;
010import edu.wpi.first.networktables.NetworkTable;
011import edu.wpi.first.networktables.NetworkTableInstance;
012import edu.wpi.first.networktables.StringArrayPublisher;
013import edu.wpi.first.networktables.StringArrayTopic;
014import edu.wpi.first.util.sendable.Sendable;
015import edu.wpi.first.util.sendable.SendableBuilder;
016import edu.wpi.first.util.sendable.SendableRegistry;
017import java.util.Map;
018import java.util.Objects;
019import java.util.WeakHashMap;
020
021/** A wrapper to make video sources sendable and usable from Shuffleboard. */
022public final class SendableCameraWrapper implements Sendable, AutoCloseable {
023  private static final String kProtocol = "camera_server://";
024
025  private static Map<String, SendableCameraWrapper> m_wrappers = new WeakHashMap<>();
026
027  private static NetworkTable m_table;
028
029  static {
030    setNetworkTableInstance(NetworkTableInstance.getDefault());
031  }
032
033  private final String m_uri;
034  private StringArrayPublisher m_streams;
035
036  /**
037   * Creates a new sendable wrapper. Private constructor to avoid direct instantiation with multiple
038   * wrappers floating around for the same camera.
039   *
040   * @param source the source to wrap
041   */
042  private SendableCameraWrapper(VideoSource source) {
043    this(source.getName());
044  }
045
046  private SendableCameraWrapper(String cameraName) {
047    SendableRegistry.add(this, cameraName);
048    m_uri = kProtocol + cameraName;
049  }
050
051  private SendableCameraWrapper(String cameraName, String[] cameraUrls) {
052    this(cameraName);
053
054    StringArrayTopic streams = new StringArrayTopic(m_table.getTopic(cameraName + "/streams"));
055    if (streams.exists()) {
056      throw new IllegalStateException(
057          "A camera is already being streamed with the name '" + cameraName + "'");
058    }
059
060    m_streams = streams.publish();
061    m_streams.set(cameraUrls);
062  }
063
064  /** Clears all cached wrapper objects. This should only be used in tests. */
065  static void clearWrappers() {
066    m_wrappers.clear();
067  }
068
069  @Override
070  public void close() {
071    SendableRegistry.remove(this);
072    if (m_streams != null) {
073      m_streams.close();
074    }
075  }
076
077  /*
078   * Sets NetworkTable instance used for camera publisher entries.
079   *
080   * @param inst NetworkTable instance
081   */
082  public static synchronized void setNetworkTableInstance(NetworkTableInstance inst) {
083    m_table = inst.getTable("CameraPublisher");
084  }
085
086  /**
087   * Gets a sendable wrapper object for the given video source, creating the wrapper if one does not
088   * already exist for the source.
089   *
090   * @param source the video source to wrap
091   * @return a sendable wrapper object for the video source, usable in Shuffleboard via {@link
092   *     ShuffleboardTab#add(Sendable)} and {@link ShuffleboardLayout#add(Sendable)}
093   */
094  public static SendableCameraWrapper wrap(VideoSource source) {
095    return m_wrappers.computeIfAbsent(source.getName(), name -> new SendableCameraWrapper(source));
096  }
097
098  /**
099   * Creates a wrapper for an arbitrary camera stream. The stream URLs <i>must</i> be specified
100   * using a host resolvable by a program running on a different host (such as a dashboard); prefer
101   * using static IP addresses (if known) or DHCP identifiers such as {@code "raspberrypi.local"}.
102   *
103   * <p>If a wrapper already exists for the given camera, that wrapper is returned and the specified
104   * URLs are ignored.
105   *
106   * @param cameraName the name of the camera. Cannot be null or empty
107   * @param cameraUrls the URLs with which the camera stream may be accessed. At least one URL must
108   *     be specified
109   * @return a sendable wrapper object for the video source, usable in Shuffleboard via {@link
110   *     ShuffleboardTab#add(Sendable)} and {@link ShuffleboardLayout#add(Sendable)}
111   */
112  public static SendableCameraWrapper wrap(String cameraName, String... cameraUrls) {
113    if (m_wrappers.containsKey(cameraName)) {
114      return m_wrappers.get(cameraName);
115    }
116
117    requireNonNullParam(cameraName, "cameraName", "wrap");
118    requireNonNullParam(cameraUrls, "cameraUrls", "wrap");
119    if (cameraName.isEmpty()) {
120      throw new IllegalArgumentException("Camera name not specified");
121    }
122    if (cameraUrls.length == 0) {
123      throw new IllegalArgumentException("No camera URLs specified");
124    }
125    for (int i = 0; i < cameraUrls.length; i++) {
126      Objects.requireNonNull(cameraUrls[i], "Camera URL at index " + i + " was null");
127    }
128
129    SendableCameraWrapper wrapper = new SendableCameraWrapper(cameraName, cameraUrls);
130    m_wrappers.put(cameraName, wrapper);
131    return wrapper;
132  }
133
134  @Override
135  public void initSendable(SendableBuilder builder) {
136    builder.addStringProperty(".ShuffleboardURI", () -> m_uri, null);
137  }
138}