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}