Add NetworkTable-selectable switched camera support (#117)
Fixes #100. Also expand the newest added camera.
This commit is contained in:
parent
e84e55cd72
commit
9fe4460068
deps
examples
cpp-multiCameraServer
java-multiCameraServer/src/main/java
python-multiCameraServer
tools
87
deps/examples/cpp-multiCameraServer/main.cpp
vendored
87
deps/examples/cpp-multiCameraServer/main.cpp
vendored
|
@ -52,6 +52,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
"switched cameras": [
|
||||||
|
{
|
||||||
|
"name": <virtual camera name>
|
||||||
|
"key": <network table key used for selection>
|
||||||
|
// 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;
|
wpi::json streamConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SwitchedCameraConfig {
|
||||||
|
std::string name;
|
||||||
|
std::string key;
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<CameraConfig> cameraConfigs;
|
std::vector<CameraConfig> cameraConfigs;
|
||||||
|
std::vector<SwitchedCameraConfig> switchedCameraConfigs;
|
||||||
|
std::vector<cs::VideoSource> cameras;
|
||||||
|
|
||||||
wpi::raw_ostream& ParseError() {
|
wpi::raw_ostream& ParseError() {
|
||||||
return wpi::errs() << "config error in '" << configFile << "': ";
|
return wpi::errs() << "config error in '" << configFile << "': ";
|
||||||
|
@ -104,6 +119,30 @@ bool ReadCameraConfig(const wpi::json& config) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ReadSwitchedCameraConfig(const wpi::json& config) {
|
||||||
|
SwitchedCameraConfig c;
|
||||||
|
|
||||||
|
// name
|
||||||
|
try {
|
||||||
|
c.name = config.at("name").get<std::string>();
|
||||||
|
} 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<std::string>();
|
||||||
|
} 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() {
|
bool ReadConfig() {
|
||||||
// open config file
|
// open config file
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
|
@ -164,6 +203,18 @@ bool ReadConfig() {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +234,34 @@ cs::UsbCamera StartCamera(const CameraConfig& config) {
|
||||||
return camera;
|
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
|
// example pipeline
|
||||||
class MyPipeline : public frc::VisionPipeline {
|
class MyPipeline : public frc::VisionPipeline {
|
||||||
public:
|
public:
|
||||||
|
@ -211,9 +290,11 @@ int main(int argc, char* argv[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// start cameras
|
// start cameras
|
||||||
std::vector<cs::VideoSource> cameras;
|
for (const auto& config : cameraConfigs)
|
||||||
for (auto&& cameraConfig : cameraConfigs)
|
cameras.emplace_back(StartCamera(config));
|
||||||
cameras.emplace_back(StartCamera(cameraConfig));
|
|
||||||
|
// start switched cameras
|
||||||
|
for (const auto& config : switchedCameraConfigs) StartSwitchedCamera(config);
|
||||||
|
|
||||||
// start image processing on camera 0 if present
|
// start image processing on camera 0 if present
|
||||||
if (cameras.size() >= 1) {
|
if (cameras.size() >= 1) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import edu.wpi.cscore.MjpegServer;
|
||||||
import edu.wpi.cscore.UsbCamera;
|
import edu.wpi.cscore.UsbCamera;
|
||||||
import edu.wpi.cscore.VideoSource;
|
import edu.wpi.cscore.VideoSource;
|
||||||
import edu.wpi.first.cameraserver.CameraServer;
|
import edu.wpi.first.cameraserver.CameraServer;
|
||||||
|
import edu.wpi.first.networktables.EntryListenerFlags;
|
||||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||||
import edu.wpi.first.vision.VisionPipeline;
|
import edu.wpi.first.vision.VisionPipeline;
|
||||||
import edu.wpi.first.vision.VisionThread;
|
import edu.wpi.first.vision.VisionThread;
|
||||||
|
@ -60,6 +61,14 @@ import org.opencv.core.Mat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
"switched cameras": [
|
||||||
|
{
|
||||||
|
"name": <virtual camera name>
|
||||||
|
"key": <network table key used for selection>
|
||||||
|
// 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;
|
public JsonElement streamConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("MemberName")
|
||||||
|
public static class SwitchedCameraConfig {
|
||||||
|
public String name;
|
||||||
|
public String key;
|
||||||
|
};
|
||||||
|
|
||||||
public static int team;
|
public static int team;
|
||||||
public static boolean server;
|
public static boolean server;
|
||||||
public static List<CameraConfig> cameraConfigs = new ArrayList<>();
|
public static List<CameraConfig> cameraConfigs = new ArrayList<>();
|
||||||
|
public static List<SwitchedCameraConfig> switchedCameraConfigs = new ArrayList<>();
|
||||||
|
public static List<VideoSource> cameras = new ArrayList<>();
|
||||||
|
|
||||||
private Main() {
|
private Main() {
|
||||||
}
|
}
|
||||||
|
@ -119,6 +136,32 @@ public final class Main {
|
||||||
return true;
|
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.
|
* 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +249,36 @@ public final class Main {
|
||||||
return camera;
|
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.
|
* Example pipeline.
|
||||||
*/
|
*/
|
||||||
|
@ -233,9 +315,13 @@ public final class Main {
|
||||||
}
|
}
|
||||||
|
|
||||||
// start cameras
|
// start cameras
|
||||||
List<VideoSource> cameras = new ArrayList<>();
|
for (CameraConfig config : cameraConfigs) {
|
||||||
for (CameraConfig cameraConfig : cameraConfigs) {
|
cameras.add(startCamera(config));
|
||||||
cameras.add(startCamera(cameraConfig));
|
}
|
||||||
|
|
||||||
|
// start switched cameras
|
||||||
|
for (SwitchedCameraConfig config : switchedCameraConfigs) {
|
||||||
|
startSwitchedCamera(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// start image processing on camera 0 if present
|
// start image processing on camera 0 if present
|
||||||
|
|
|
@ -12,6 +12,7 @@ import sys
|
||||||
|
|
||||||
from cscore import CameraServer, VideoSource, UsbCamera, MjpegServer
|
from cscore import CameraServer, VideoSource, UsbCamera, MjpegServer
|
||||||
from networktables import NetworkTablesInstance
|
from networktables import NetworkTablesInstance
|
||||||
|
import ntcore
|
||||||
|
|
||||||
# JSON format:
|
# JSON format:
|
||||||
# {
|
# {
|
||||||
|
@ -44,6 +45,14 @@ from networktables import NetworkTablesInstance
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
# ]
|
# ]
|
||||||
|
# "switched cameras": [
|
||||||
|
# {
|
||||||
|
# "name": <virtual camera name>
|
||||||
|
# "key": <network table key used for selection>
|
||||||
|
# // 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"
|
configFile = "/boot/frc.json"
|
||||||
|
@ -53,6 +62,8 @@ class CameraConfig: pass
|
||||||
team = None
|
team = None
|
||||||
server = False
|
server = False
|
||||||
cameraConfigs = []
|
cameraConfigs = []
|
||||||
|
switchedCameraConfigs = []
|
||||||
|
cameras = []
|
||||||
|
|
||||||
def parseError(str):
|
def parseError(str):
|
||||||
"""Report parse error."""
|
"""Report parse error."""
|
||||||
|
@ -84,6 +95,27 @@ def readCameraConfig(config):
|
||||||
cameraConfigs.append(cam)
|
cameraConfigs.append(cam)
|
||||||
return True
|
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():
|
def readConfig():
|
||||||
"""Read configuration file."""
|
"""Read configuration file."""
|
||||||
global team
|
global team
|
||||||
|
@ -129,6 +161,12 @@ def readConfig():
|
||||||
if not readCameraConfig(camera):
|
if not readCameraConfig(camera):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# switched cameras
|
||||||
|
if "switched cameras" in j:
|
||||||
|
for camera in j["switched cameras"]:
|
||||||
|
if not readSwitchedCameraConfig(camera):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def startCamera(config):
|
def startCamera(config):
|
||||||
|
@ -146,6 +184,30 @@ def startCamera(config):
|
||||||
|
|
||||||
return camera
|
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 __name__ == "__main__":
|
||||||
if len(sys.argv) >= 2:
|
if len(sys.argv) >= 2:
|
||||||
configFile = sys.argv[1]
|
configFile = sys.argv[1]
|
||||||
|
@ -164,9 +226,12 @@ if __name__ == "__main__":
|
||||||
ntinst.startClientTeam(team)
|
ntinst.startClientTeam(team)
|
||||||
|
|
||||||
# start cameras
|
# start cameras
|
||||||
cameras = []
|
for config in cameraConfigs:
|
||||||
for cameraConfig in cameraConfigs:
|
cameras.append(startCamera(config))
|
||||||
cameras.append(startCamera(cameraConfig))
|
|
||||||
|
# start switched cameras
|
||||||
|
for config in switchedCameraConfigs:
|
||||||
|
startSwitchedCamera(config)
|
||||||
|
|
||||||
# loop forever
|
# loop forever
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -49,7 +49,7 @@ function dismissStatus() {
|
||||||
|
|
||||||
// Enable and disable buttons based on connection status
|
// 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 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 writableButtonIds = ['networkSave', 'visionSave', 'applicationSave'];
|
||||||
var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s',
|
var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s',
|
||||||
'systemMemoryAvail1s', 'systemMemoryAvail5s',
|
'systemMemoryAvail1s', 'systemMemoryAvail5s',
|
||||||
|
@ -121,8 +121,8 @@ $('#systemWritable').click(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vision settings
|
// Vision settings
|
||||||
var visionSettingsServer = {};
|
var visionSettingsServer = {'cameras': [], 'switched cameras': []};
|
||||||
var visionSettingsDisplay = {'cameras': []};
|
var visionSettingsDisplay = {'cameras': [], 'switched cameras': []};
|
||||||
var cameraList = [];
|
var cameraList = [];
|
||||||
|
|
||||||
function pushVisionLogEnabled() {
|
function pushVisionLogEnabled() {
|
||||||
|
@ -194,7 +194,7 @@ function connect() {
|
||||||
break;
|
break;
|
||||||
case 'visionSettings':
|
case 'visionSettings':
|
||||||
visionSettingsServer = msg.settings;
|
visionSettingsServer = msg.settings;
|
||||||
visionSettingsDisplay = $.extend(true, {}, visionSettingsServer);
|
visionSettingsDisplay = $.extend(true, {'cameras': [], 'switched cameras': []}, visionSettingsServer);
|
||||||
updateVisionSettingsView();
|
updateVisionSettingsView();
|
||||||
break;
|
break;
|
||||||
case 'applicationSettings':
|
case 'applicationSettings':
|
||||||
|
@ -507,6 +507,45 @@ function appendNewVisionCameraView(value, i) {
|
||||||
$('#cameras').append(camera);
|
$('#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() {
|
function updateVisionSettingsView() {
|
||||||
var isClient = !visionSettingsDisplay.ntmode || visionSettingsDisplay.ntmode === 'client';
|
var isClient = !visionSettingsDisplay.ntmode || visionSettingsDisplay.ntmode === 'client';
|
||||||
$('#visionClient').prop('checked', isClient);
|
$('#visionClient').prop('checked', isClient);
|
||||||
|
@ -521,6 +560,9 @@ function updateVisionSettingsView() {
|
||||||
visionSettingsDisplay.cameras.forEach(function (value, i) {
|
visionSettingsDisplay.cameras.forEach(function (value, i) {
|
||||||
appendNewVisionCameraView(value, i);
|
appendNewVisionCameraView(value, i);
|
||||||
});
|
});
|
||||||
|
visionSettingsDisplay['switched cameras'].forEach(function (value, i) {
|
||||||
|
appendNewVisionSwitchedCameraView(value, i);
|
||||||
|
});
|
||||||
updateCameraListView();
|
updateCameraListView();
|
||||||
feather.replace();
|
feather.replace();
|
||||||
}
|
}
|
||||||
|
@ -610,6 +652,11 @@ $('#visionSave').click(function() {
|
||||||
value.stream.properties.push({'name': 'default_compression', 'value': streamDefaultCompression});
|
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 = {
|
var msg = {
|
||||||
type: 'visionSave',
|
type: 'visionSave',
|
||||||
settings: visionSettingsDisplay
|
settings: visionSettingsDisplay
|
||||||
|
@ -622,11 +669,19 @@ $('#visionDiscard').click(function() {
|
||||||
updateVisionSettingsView();
|
updateVisionSettingsView();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#addCamera').click(function() {
|
$('#addUsbCamera').click(function() {
|
||||||
var i = visionSettingsDisplay.cameras.length;
|
var i = visionSettingsDisplay.cameras.length;
|
||||||
visionSettingsDisplay.cameras.push({});
|
visionSettingsDisplay.cameras.push({});
|
||||||
appendNewVisionCameraView({}, i);
|
appendNewVisionCameraView({}, i);
|
||||||
updateCameraListView();
|
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() {
|
function updateCameraListView() {
|
||||||
|
@ -694,6 +749,7 @@ function updateCameraListView() {
|
||||||
visionSettingsDisplay.cameras.push(camera);
|
visionSettingsDisplay.cameras.push(camera);
|
||||||
appendNewVisionCameraView(camera, i);
|
appendNewVisionCameraView(camera, i);
|
||||||
updateCameraListView();
|
updateCameraListView();
|
||||||
|
$('#cameraBody' + i).collapse('show');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
325
deps/tools/configServer/src/resources/index.html
vendored
325
deps/tools/configServer/src/resources/index.html
vendored
|
@ -214,7 +214,7 @@
|
||||||
<div class="tab-pane" id="vision-settings" role="tabpanel" aria-labelledby="vision-settings-tab">
|
<div class="tab-pane" id="vision-settings" role="tabpanel" aria-labelledby="vision-settings-tab">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6>Network Tables</h6>
|
<h5>Network Tables</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form>
|
<form>
|
||||||
|
@ -236,143 +236,196 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion" id="cameras">
|
<div class="card">
|
||||||
<div class="card" style="display:none" id="cameraNEW">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
<h5>USB Cameras</h5>
|
||||||
<div class="row align-items-center">
|
</div>
|
||||||
<div class="col-auto mr-auto">
|
<div class="card-body">
|
||||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#cameraBodyNEW">
|
<div class="accordion" id="cameras">
|
||||||
<h6 class="cameraTitle">Camera NEW</h6>
|
<div class="card" style="display:none" id="cameraNEW">
|
||||||
</button>
|
<div class="card-header">
|
||||||
<span class="badge badge-secondary align-text-top cameraConnectionBadge">Disconnected</span>
|
<div class="row align-items-center">
|
||||||
</div>
|
<div class="col-auto mr-auto">
|
||||||
<div class="col-auto">
|
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#cameraBodyNEW">
|
||||||
<a class="btn btn-sm btn-success cameraStream" href="" target="_blank" role="button">
|
<h6 class="cameraTitle">Camera NEW</h6>
|
||||||
<span data-feather="play"></span>
|
</button>
|
||||||
Open Stream
|
<span class="badge badge-secondary align-text-top cameraConnectionBadge">Disconnected</span>
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger cameraRemove">
|
|
||||||
<span data-feather="x"></span>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="cameraBodyNEW" data-parent="#cameras">
|
|
||||||
<div class="card-body">
|
|
||||||
<form>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group col col-4">
|
|
||||||
<label for="cameraNameNEW">Name</label>
|
|
||||||
<input type="text" class="form-control cameraName" id="cameraNameNEW">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col">
|
<div class="col-auto">
|
||||||
<label for="cameraPathNEW">Path</label>
|
<a class="btn btn-sm btn-success cameraStream" href="" target="_blank" role="button">
|
||||||
<div class="input-group">
|
<span data-feather="play"></span>
|
||||||
<input type="text" class="form-control cameraPath" id="cameraPathNEW">
|
Open Stream
|
||||||
<div class="input-group-append">
|
</a>
|
||||||
<button type="button" class="btn dropdown-toggle cameraAlternatePaths" id="cameraAlternatePathsNEW" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="true">
|
<button type="button" class="btn btn-sm btn-danger cameraRemove">
|
||||||
Alternate Path
|
<span data-feather="x"></span>
|
||||||
</button>
|
Remove
|
||||||
<div class="dropdown-menu cameraAlternatePathsList" aria-labelledby="cameraAlternatePathsNEW">
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="cameraBodyNEW" data-parent="#cameras">
|
||||||
|
<div class="card-body">
|
||||||
|
<form>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col col-4">
|
||||||
|
<label for="cameraNameNEW">Name</label>
|
||||||
|
<input type="text" class="form-control cameraName" id="cameraNameNEW">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col">
|
||||||
|
<label for="cameraPathNEW">Path</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control cameraPath" id="cameraPathNEW">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" class="btn dropdown-toggle cameraAlternatePaths" id="cameraAlternatePathsNEW" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="true">
|
||||||
|
Alternate Path
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu cameraAlternatePathsList" aria-labelledby="cameraAlternatePathsNEW">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Camera Settings
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input cameraSettingsFile" accept=".json,text/json,application/json" id="cameraSettingsFileNEW">
|
||||||
|
<label class="custom-file-label" for="cameraSettingsFileNEW">Load Source Config From JSON File</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-sm cameraCopyConfig" type="button">
|
||||||
|
<span data-feather="copy"></span>
|
||||||
|
Copy Source Config From Camera
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraPixelFormatNEW">Pixel Format</label>
|
||||||
|
<select class="form-control cameraPixelFormat" id="cameraPixelFormatNEW">
|
||||||
|
<option value="mjpeg">MJPEG</option>
|
||||||
|
<option value="yuyv">YUYV</option>
|
||||||
|
<option value="rgb565">RGB565</option>
|
||||||
|
<option value="bgr">BGR</option>
|
||||||
|
<option value="gray">Gray</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraWidthNEW">Width</label>
|
||||||
|
<input type="number" class="form-control cameraWidth" id="cameraWidthNEW" size="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraHeightNEW">Height</label>
|
||||||
|
<input type="number" class="form-control cameraHeight" id="cameraHeightNEW" size="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraFpsNEW">FPS</label>
|
||||||
|
<input type="number" class="form-control cameraFps" id="cameraFpsNEW" size="4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraBrightnessNEW">Brightness</label>
|
||||||
|
<input type="text" class="form-control cameraBrightness" id="cameraBrightnessNEW" size="4">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraWhiteBalanceNEW">White Balance</label>
|
||||||
|
<input type="text" class="form-control cameraWhiteBalance" id="cameraWhiteBalanceNEW" size="4">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="cameraExposureNEW">Exposure</label>
|
||||||
|
<input type="text" class="form-control cameraExposure" id="cameraExposureNEW" size="4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cameraPropertiesNEW">Custom Properties JSON</label>
|
||||||
|
<textarea class="form-control cameraProperties" id="cameraPropertiesNEW" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Video Stream Defaults
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="streamWidthNEW">Width</label>
|
||||||
|
<input type="number" class="form-control streamWidth" id="streamWidthNEW" size="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="streamHeightNEW">Height</label>
|
||||||
|
<input type="number" class="form-control streamHeight" id="streamHeightNEW" size="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="streamFpsNEW">FPS</label>
|
||||||
|
<input type="number" class="form-control streamFps" id="streamFpsNEW" size="4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="streamCompressionNEW">Compression</label>
|
||||||
|
<input type="number" class="form-control streamCompression" id="streamWidthNEW" size="5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-auto">
|
||||||
|
<label for="streamDefaultCompressionNEW">Default Compression</label>
|
||||||
|
<input type="number" class="form-control streamDefaultCompression" id="streamDefaultCompressionNEW" size="4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Switched Cameras</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="accordion" id="switchedCameras">
|
||||||
|
<div class="card" style="display:none" id="switchedCameraNEW">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto mr-auto">
|
||||||
|
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#switchedCameraBodyNEW">
|
||||||
|
<h6 class="cameraTitle">Switched Camera NEW</h6>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a class="btn btn-sm btn-success cameraStream" href="" target="_blank" role="button">
|
||||||
|
<span data-feather="play"></span>
|
||||||
|
Open Stream
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger cameraRemove">
|
||||||
|
<span data-feather="x"></span>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-header">
|
<div class="collapse" id="switchedCameraBodyNEW" data-parent="#switchedCameras">
|
||||||
Camera Settings
|
<div class="card-body">
|
||||||
</div>
|
<form>
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-file">
|
|
||||||
<input type="file" class="custom-file-input cameraSettingsFile" accept=".json,text/json,application/json" id="cameraSettingsFileNEW">
|
|
||||||
<label class="custom-file-label" for="cameraSettingsFileNEW">Load Source Config From JSON File</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<button class="btn btn-sm cameraCopyConfig" type="button">
|
|
||||||
<span data-feather="copy"></span>
|
|
||||||
Copy Source Config From Camera
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-auto">
|
<div class="form-group col col-4">
|
||||||
<label for="cameraPixelFormatNEW">Pixel Format</label>
|
<label for="switchedCameraNameNEW">Name</label>
|
||||||
<select class="form-control cameraPixelFormat" id="cameraPixelFormatNEW">
|
<input type="text" class="form-control cameraName" id="switchedCameraNameNEW">
|
||||||
<option value="mjpeg">MJPEG</option>
|
|
||||||
<option value="yuyv">YUYV</option>
|
|
||||||
<option value="rgb565">RGB565</option>
|
|
||||||
<option value="bgr">BGR</option>
|
|
||||||
<option value="gray">Gray</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-auto">
|
<div class="form-group col">
|
||||||
<label for="cameraWidthNEW">Width</label>
|
<label for="switchedCameraKeyNEW">NetworkTable Key</label>
|
||||||
<input type="number" class="form-control cameraWidth" id="cameraWidthNEW" size="5">
|
<input type="text" class="form-control cameraKey" id="switchedCameraKeyNEW">
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="cameraHeightNEW">Height</label>
|
|
||||||
<input type="number" class="form-control cameraHeight" id="cameraHeightNEW" size="5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="cameraFpsNEW">FPS</label>
|
|
||||||
<input type="number" class="form-control cameraFps" id="cameraFpsNEW" size="4">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
</form>
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="cameraBrightnessNEW">Brightness</label>
|
|
||||||
<input type="text" class="form-control cameraBrightness" id="cameraBrightnessNEW" size="4">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="cameraWhiteBalanceNEW">White Balance</label>
|
|
||||||
<input type="text" class="form-control cameraWhiteBalance" id="cameraWhiteBalanceNEW" size="4">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="cameraExposureNEW">Exposure</label>
|
|
||||||
<input type="text" class="form-control cameraExposure" id="cameraExposureNEW" size="4">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cameraPropertiesNEW">Custom Properties JSON</label>
|
|
||||||
<textarea class="form-control cameraProperties" id="cameraPropertiesNEW" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-header">
|
|
||||||
Video Stream Defaults
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="streamWidthNEW">Width</label>
|
|
||||||
<input type="number" class="form-control streamWidth" id="streamWidthNEW" size="5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="streamHeightNEW">Height</label>
|
|
||||||
<input type="number" class="form-control streamHeight" id="streamHeightNEW" size="5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="streamFpsNEW">FPS</label>
|
|
||||||
<input type="number" class="form-control streamFps" id="streamFpsNEW" size="4">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="streamCompressionNEW">Compression</label>
|
|
||||||
<input type="number" class="form-control streamCompression" id="streamWidthNEW" size="5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-auto">
|
|
||||||
<label for="streamDefaultCompressionNEW">Default Compression</label>
|
|
||||||
<input type="number" class="form-control streamDefaultCompression" id="streamDefaultCompressionNEW" size="4">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -400,10 +453,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button type="button" class="btn btn-success" id="addCamera">
|
<div class="dropdown">
|
||||||
<span data-feather="plus"></span>
|
<button type="button" class="btn btn-success dropdown-toggle" id="addCamera" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
Add Other Camera
|
<span data-feather="plus"></span>
|
||||||
</button>
|
Add Other Camera
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="addCamera" id="addCameraList">
|
||||||
|
<button type="button" class="dropdown-item" id="addUsbCamera">
|
||||||
|
USB Camera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dropdown-item" id="addSwitchedCamera">
|
||||||
|
Switched Camera
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,6 +49,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
"switched cameras": [
|
||||||
|
{
|
||||||
|
"name": <virtual camera name>
|
||||||
|
"key": <network table key used for selection>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -70,7 +78,14 @@ struct CameraConfig {
|
||||||
wpi::json streamConfig;
|
wpi::json streamConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<CameraConfig> cameras;
|
struct SwitchedCameraConfig {
|
||||||
|
std::string name;
|
||||||
|
std::string key;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<CameraConfig> cameraConfigs;
|
||||||
|
std::vector<SwitchedCameraConfig> switchedCameraConfigs;
|
||||||
|
std::vector<cs::VideoSource> cameras;
|
||||||
|
|
||||||
wpi::raw_ostream& ParseError() {
|
wpi::raw_ostream& ParseError() {
|
||||||
return wpi::errs() << "config error in '" << configFile << "': ";
|
return wpi::errs() << "config error in '" << configFile << "': ";
|
||||||
|
@ -101,7 +116,31 @@ bool ReadCameraConfig(const wpi::json& config) {
|
||||||
|
|
||||||
c.config = config;
|
c.config = config;
|
||||||
|
|
||||||
cameras.emplace_back(std::move(c));
|
cameraConfigs.emplace_back(std::move(c));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadSwitchedCameraConfig(const wpi::json& config) {
|
||||||
|
SwitchedCameraConfig c;
|
||||||
|
|
||||||
|
// name
|
||||||
|
try {
|
||||||
|
c.name = config.at("name").get<std::string>();
|
||||||
|
} 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<std::string>();
|
||||||
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,10 +204,22 @@ bool ReadConfig() {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartCamera(const CameraConfig& config) {
|
cs::UsbCamera StartCamera(const CameraConfig& config) {
|
||||||
wpi::outs() << "Starting camera '" << config.name << "' on " << config.path
|
wpi::outs() << "Starting camera '" << config.name << "' on " << config.path
|
||||||
<< '\n';
|
<< '\n';
|
||||||
auto inst = frc::CameraServer::GetInstance();
|
auto inst = frc::CameraServer::GetInstance();
|
||||||
|
@ -180,6 +231,36 @@ void StartCamera(const CameraConfig& config) {
|
||||||
|
|
||||||
if (config.streamConfig.is_object())
|
if (config.streamConfig.is_object())
|
||||||
server.SetConfigJson(config.streamConfig);
|
server.SetConfigJson(config.streamConfig);
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -200,7 +281,11 @@ int main(int argc, char* argv[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// start cameras
|
// start cameras
|
||||||
for (auto&& camera : cameras) StartCamera(camera);
|
for (const auto& config : cameraConfigs)
|
||||||
|
cameras.emplace_back(StartCamera(config));
|
||||||
|
|
||||||
|
// start switched cameras
|
||||||
|
for (const auto& config : switchedCameraConfigs) StartSwitchedCamera(config);
|
||||||
|
|
||||||
// loop forever
|
// loop forever
|
||||||
for (;;) std::this_thread::sleep_for(std::chrono::seconds(10));
|
for (;;) std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||||
|
|
Loading…
Reference in New Issue
Block a user