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}