diff --git a/deps/examples/cpp-multiCameraServer/main.cpp b/deps/examples/cpp-multiCameraServer/main.cpp index e90ecc3..fb44453 100644 --- a/deps/examples/cpp-multiCameraServer/main.cpp +++ b/deps/examples/cpp-multiCameraServer/main.cpp @@ -52,6 +52,14 @@ } } ] + "switched cameras": [ + { + "name": + "key": + // if NT value is a string, it's treated as a name + // if NT value is a double, it's treated as an integer index + } + ] } */ @@ -69,7 +77,14 @@ struct CameraConfig { wpi::json streamConfig; }; +struct SwitchedCameraConfig { + std::string name; + std::string key; +}; + std::vector cameraConfigs; +std::vector switchedCameraConfigs; +std::vector cameras; wpi::raw_ostream& ParseError() { return wpi::errs() << "config error in '" << configFile << "': "; @@ -104,6 +119,30 @@ bool ReadCameraConfig(const wpi::json& config) { return true; } +bool ReadSwitchedCameraConfig(const wpi::json& config) { + SwitchedCameraConfig c; + + // name + try { + c.name = config.at("name").get(); + } catch (const wpi::json::exception& e) { + ParseError() << "could not read switched camera name: " << e.what() << '\n'; + return false; + } + + // key + try { + c.key = config.at("key").get(); + } catch (const wpi::json::exception& e) { + ParseError() << "switched camera '" << c.name + << "': could not read key: " << e.what() << '\n'; + return false; + } + + switchedCameraConfigs.emplace_back(std::move(c)); + return true; +} + bool ReadConfig() { // open config file std::error_code ec; @@ -164,6 +203,18 @@ bool ReadConfig() { return false; } + // switched cameras (optional) + if (j.count("switched cameras") != 0) { + try { + for (auto&& camera : j.at("switched cameras")) { + if (!ReadSwitchedCameraConfig(camera)) return false; + } + } catch (const wpi::json::exception& e) { + ParseError() << "could not read switched cameras: " << e.what() << '\n'; + return false; + } + } + return true; } @@ -183,6 +234,34 @@ cs::UsbCamera StartCamera(const CameraConfig& config) { return camera; } +cs::MjpegServer StartSwitchedCamera(const SwitchedCameraConfig& config) { + wpi::outs() << "Starting switched camera '" << config.name << "' on " + << config.key << '\n'; + auto server = + frc::CameraServer::GetInstance()->AddSwitchedCamera(config.name); + + nt::NetworkTableInstance::GetDefault() + .GetEntry(config.key) + .AddListener( + [server](const auto& event) mutable { + if (event.value->IsDouble()) { + int i = event.value->GetDouble(); + if (i >= 0 && i < cameras.size()) server.SetSource(cameras[i]); + } else if (event.value->IsString()) { + auto str = event.value->GetString(); + for (int i = 0; i < cameraConfigs.size(); ++i) { + if (str == cameraConfigs[i].name) { + server.SetSource(cameras[i]); + break; + } + } + } + }, + NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); + + return server; +} + // example pipeline class MyPipeline : public frc::VisionPipeline { public: @@ -211,9 +290,11 @@ int main(int argc, char* argv[]) { } // start cameras - std::vector cameras; - for (auto&& cameraConfig : cameraConfigs) - cameras.emplace_back(StartCamera(cameraConfig)); + for (const auto& config : cameraConfigs) + cameras.emplace_back(StartCamera(config)); + + // start switched cameras + for (const auto& config : switchedCameraConfigs) StartSwitchedCamera(config); // start image processing on camera 0 if present if (cameras.size() >= 1) { diff --git a/deps/examples/java-multiCameraServer/src/main/java/Main.java b/deps/examples/java-multiCameraServer/src/main/java/Main.java index ad200db..41ac5b8 100644 --- a/deps/examples/java-multiCameraServer/src/main/java/Main.java +++ b/deps/examples/java-multiCameraServer/src/main/java/Main.java @@ -22,6 +22,7 @@ import edu.wpi.cscore.MjpegServer; import edu.wpi.cscore.UsbCamera; import edu.wpi.cscore.VideoSource; import edu.wpi.first.cameraserver.CameraServer; +import edu.wpi.first.networktables.EntryListenerFlags; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.vision.VisionPipeline; import edu.wpi.first.vision.VisionThread; @@ -60,6 +61,14 @@ import org.opencv.core.Mat; } } ] + "switched cameras": [ + { + "name": + "key": + // if NT value is a string, it's treated as a name + // if NT value is a double, it's treated as an integer index + } + ] } */ @@ -74,9 +83,17 @@ public final class Main { public JsonElement streamConfig; } + @SuppressWarnings("MemberName") + public static class SwitchedCameraConfig { + public String name; + public String key; + }; + public static int team; public static boolean server; public static List cameraConfigs = new ArrayList<>(); + public static List switchedCameraConfigs = new ArrayList<>(); + public static List cameras = new ArrayList<>(); private Main() { } @@ -119,6 +136,32 @@ public final class Main { return true; } + /** + * Read single switched camera configuration. + */ + public static boolean readSwitchedCameraConfig(JsonObject config) { + SwitchedCameraConfig cam = new SwitchedCameraConfig(); + + // name + JsonElement nameElement = config.get("name"); + if (nameElement == null) { + parseError("could not read switched camera name"); + return false; + } + cam.name = nameElement.getAsString(); + + // path + JsonElement keyElement = config.get("key"); + if (keyElement == null) { + parseError("switched camera '" + cam.name + "': could not read key"); + return false; + } + cam.key = keyElement.getAsString(); + + switchedCameraConfigs.add(cam); + return true; + } + /** * Read configuration file. */ @@ -173,6 +216,15 @@ public final class Main { } } + if (obj.has("switched cameras")) { + JsonArray switchedCameras = obj.get("switched cameras").getAsJsonArray(); + for (JsonElement camera : switchedCameras) { + if (!readSwitchedCameraConfig(camera.getAsJsonObject())) { + return false; + } + } + } + return true; } @@ -197,6 +249,36 @@ public final class Main { return camera; } + /** + * Start running the switched camera. + */ + public static MjpegServer startSwitchedCamera(SwitchedCameraConfig config) { + System.out.println("Starting switched camera '" + config.name + "' on " + config.key); + MjpegServer server = CameraServer.getInstance().addSwitchedCamera(config.name); + + NetworkTableInstance.getDefault() + .getEntry(config.key) + .addListener(event -> { + if (event.value.isDouble()) { + int i = (int) event.value.getDouble(); + if (i >= 0 && i < cameras.size()) { + server.setSource(cameras.get(i)); + } + } else if (event.value.isString()) { + String str = event.value.getString(); + for (int i = 0; i < cameraConfigs.size(); i++) { + if (str.equals(cameraConfigs.get(i).name)) { + server.setSource(cameras.get(i)); + break; + } + } + } + }, + EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate); + + return server; + } + /** * Example pipeline. */ @@ -233,9 +315,13 @@ public final class Main { } // start cameras - List cameras = new ArrayList<>(); - for (CameraConfig cameraConfig : cameraConfigs) { - cameras.add(startCamera(cameraConfig)); + for (CameraConfig config : cameraConfigs) { + cameras.add(startCamera(config)); + } + + // start switched cameras + for (SwitchedCameraConfig config : switchedCameraConfigs) { + startSwitchedCamera(config); } // start image processing on camera 0 if present diff --git a/deps/examples/python-multiCameraServer/multiCameraServer.py b/deps/examples/python-multiCameraServer/multiCameraServer.py index 5818a65..830fcb4 100755 --- a/deps/examples/python-multiCameraServer/multiCameraServer.py +++ b/deps/examples/python-multiCameraServer/multiCameraServer.py @@ -12,6 +12,7 @@ import sys from cscore import CameraServer, VideoSource, UsbCamera, MjpegServer from networktables import NetworkTablesInstance +import ntcore # JSON format: # { @@ -44,6 +45,14 @@ from networktables import NetworkTablesInstance # } # } # ] +# "switched cameras": [ +# { +# "name": +# "key": +# // if NT value is a string, it's treated as a name +# // if NT value is a double, it's treated as an integer index +# } +# ] # } configFile = "/boot/frc.json" @@ -53,6 +62,8 @@ class CameraConfig: pass team = None server = False cameraConfigs = [] +switchedCameraConfigs = [] +cameras = [] def parseError(str): """Report parse error.""" @@ -84,6 +95,27 @@ def readCameraConfig(config): cameraConfigs.append(cam) return True +def readSwitchedCameraConfig(config): + """Read single switched camera configuration.""" + cam = CameraConfig() + + # name + try: + cam.name = config["name"] + except KeyError: + parseError("could not read switched camera name") + return False + + # path + try: + cam.key = config["key"] + except KeyError: + parseError("switched camera '{}': could not read key".format(cam.name)) + return False + + switchedCameraConfigs.append(cam) + return True + def readConfig(): """Read configuration file.""" global team @@ -129,6 +161,12 @@ def readConfig(): if not readCameraConfig(camera): return False + # switched cameras + if "switched cameras" in j: + for camera in j["switched cameras"]: + if not readSwitchedCameraConfig(camera): + return False + return True def startCamera(config): @@ -146,6 +184,30 @@ def startCamera(config): return camera +def startSwitchedCamera(config): + """Start running the switched camera.""" + print("Starting switched camera '{}' on {}".format(config.name, config.key)) + server = CameraServer.getInstance().addSwitchedCamera(config.name) + + def listener(fromobj, key, value, isNew): + if isinstance(value, float): + i = int(value) + if i >= 0 and i < len(cameras): + server.setSource(cameras[i]) + elif isinstance(value, str): + for i in range(len(cameraConfigs)): + if value == cameraConfigs[i].name: + server.setSource(cameras[i]) + break + + NetworkTablesInstance.getDefault().getEntry(config.key).addListener( + listener, + ntcore.constants.NT_NOTIFY_IMMEDIATE | + ntcore.constants.NT_NOTIFY_NEW | + ntcore.constants.NT_NOTIFY_UPDATE) + + return server + if __name__ == "__main__": if len(sys.argv) >= 2: configFile = sys.argv[1] @@ -164,9 +226,12 @@ if __name__ == "__main__": ntinst.startClientTeam(team) # start cameras - cameras = [] - for cameraConfig in cameraConfigs: - cameras.append(startCamera(cameraConfig)) + for config in cameraConfigs: + cameras.append(startCamera(config)) + + # start switched cameras + for config in switchedCameraConfigs: + startSwitchedCamera(config) # loop forever while True: diff --git a/deps/tools/configServer/src/resources/frcvision.js b/deps/tools/configServer/src/resources/frcvision.js index 21e96a3..21f1011 100644 --- a/deps/tools/configServer/src/resources/frcvision.js +++ b/deps/tools/configServer/src/resources/frcvision.js @@ -49,7 +49,7 @@ function dismissStatus() { // Enable and disable buttons based on connection status var connectedButtonIds = ['systemRestart', 'networkApproach', 'networkAddress', 'networkMask', 'networkGateway', 'networkDNS', 'visionUp', 'visionDown', 'visionTerm', 'visionKill', 'systemReadOnly', 'systemWritable', 'visionClient', 'visionTeam', 'visionDiscard', 'addConnectedCamera', 'addCamera', 'applicationType']; -var connectedButtonClasses = ['cameraName', 'cameraPath', 'cameraAlternatePaths', 'cameraPixelFormat', 'cameraWidth', 'cameraHeight', 'cameraFps', 'cameraBrightness', 'cameraWhiteBalance', 'cameraExposure', 'cameraProperties', 'streamWidth', 'streamHeight', 'streamFps', 'streamCompression', 'streamDefaultCompression', 'cameraRemove', 'cameraCopyConfig'] +var connectedButtonClasses = ['cameraName', 'cameraPath', 'cameraAlternatePaths', 'cameraPixelFormat', 'cameraWidth', 'cameraHeight', 'cameraFps', 'cameraBrightness', 'cameraWhiteBalance', 'cameraExposure', 'cameraProperties', 'streamWidth', 'streamHeight', 'streamFps', 'streamCompression', 'streamDefaultCompression', 'cameraRemove', 'cameraCopyConfig', 'cameraKey'] var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave']; var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s', 'systemMemoryAvail1s', 'systemMemoryAvail5s', @@ -121,8 +121,8 @@ $('#systemWritable').click(function() { }); // Vision settings -var visionSettingsServer = {}; -var visionSettingsDisplay = {'cameras': []}; +var visionSettingsServer = {'cameras': [], 'switched cameras': []}; +var visionSettingsDisplay = {'cameras': [], 'switched cameras': []}; var cameraList = []; function pushVisionLogEnabled() { @@ -194,7 +194,7 @@ function connect() { break; case 'visionSettings': visionSettingsServer = msg.settings; - visionSettingsDisplay = $.extend(true, {}, visionSettingsServer); + visionSettingsDisplay = $.extend(true, {'cameras': [], 'switched cameras': []}, visionSettingsServer); updateVisionSettingsView(); break; case 'applicationSettings': @@ -507,6 +507,45 @@ function appendNewVisionCameraView(value, i) { $('#cameras').append(camera); } +function updateVisionSwitchedCameraView(camera, value) { + if ('name' in value) { + camera.find('.cameraTitle').text('Switched Camera ' + value.name); + camera.find('.cameraName').val(value.name); + } + if ('key' in value) { + camera.find('.cameraKey').val(value.key); + } +} + +function appendNewVisionSwitchedCameraView(value, i) { + var camera = $('#switchedCameraNEW').clone(); + camera.attr('id', 'switchedCamera' + i); + camera.addClass('cameraSetting'); + camera.removeAttr('style'); + + updateVisionSwitchedCameraView(camera, value); + camera.find('.cameraStream').attr('href', 'http://' + window.location.hostname + ':' + (1181 + visionSettingsDisplay.cameras.length + i) + '/'); + camera.find('.cameraRemove').click(function() { + visionSettingsDisplay['switched cameras'].splice(i, 1); + camera.remove(); + }); + + camera.find('[id]').each(function() { + $(this).attr('id', $(this).attr('id').replace('NEW', i)); + }); + camera.find('[for]').each(function() { + $(this).attr('for', $(this).attr('for').replace('NEW', i)); + }); + camera.find('[data-target]').each(function() { + $(this).attr('data-target', $(this).attr('data-target').replace('NEW', i)); + }); + camera.find('[aria-labelledby]').each(function() { + $(this).attr('aria-labelledby', $(this).attr('aria-labelledby').replace('NEW', i)); + }); + + $('#switchedCameras').append(camera); +} + function updateVisionSettingsView() { var isClient = !visionSettingsDisplay.ntmode || visionSettingsDisplay.ntmode === 'client'; $('#visionClient').prop('checked', isClient); @@ -521,6 +560,9 @@ function updateVisionSettingsView() { visionSettingsDisplay.cameras.forEach(function (value, i) { appendNewVisionCameraView(value, i); }); + visionSettingsDisplay['switched cameras'].forEach(function (value, i) { + appendNewVisionSwitchedCameraView(value, i); + }); updateCameraListView(); feather.replace(); } @@ -610,6 +652,11 @@ $('#visionSave').click(function() { value.stream.properties.push({'name': 'default_compression', 'value': streamDefaultCompression}); } }); + visionSettingsDisplay['switched cameras'].forEach(function (value, i) { + var camera = $('#switchedCamera' + i); + value.name = camera.find('.cameraName').val(); + value.key = camera.find('.cameraKey').val(); + }); var msg = { type: 'visionSave', settings: visionSettingsDisplay @@ -622,11 +669,19 @@ $('#visionDiscard').click(function() { updateVisionSettingsView(); }); -$('#addCamera').click(function() { +$('#addUsbCamera').click(function() { var i = visionSettingsDisplay.cameras.length; visionSettingsDisplay.cameras.push({}); appendNewVisionCameraView({}, i); updateCameraListView(); + $('#cameraBody' + i).collapse('show'); +}); + +$('#addSwitchedCamera').click(function() { + var i = visionSettingsDisplay['switched cameras'].length; + visionSettingsDisplay['switched cameras'].push({}); + appendNewVisionSwitchedCameraView({}, i); + $('#switchedCameraBody' + i).collapse('show'); }); function updateCameraListView() { @@ -694,6 +749,7 @@ function updateCameraListView() { visionSettingsDisplay.cameras.push(camera); appendNewVisionCameraView(camera, i); updateCameraListView(); + $('#cameraBody' + i).collapse('show'); }); } diff --git a/deps/tools/configServer/src/resources/index.html b/deps/tools/configServer/src/resources/index.html index 5ab7a9c..a64e6ef 100644 --- a/deps/tools/configServer/src/resources/index.html +++ b/deps/tools/configServer/src/resources/index.html @@ -214,7 +214,7 @@
-
Network Tables
+
Network Tables
@@ -236,143 +236,196 @@
-
-