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