Add NetworkTable-selectable switched camera support (#117)
Fixes #100. Also expand the newest added camera.
This commit is contained in:
parent
e84e55cd72
commit
9fe4460068
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;
|
||||
};
|
||||
|
||||
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() {
|
||||
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<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() {
|
||||
// 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<cs::VideoSource> 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) {
|
||||
|
|
|
@ -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": <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;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class SwitchedCameraConfig {
|
||||
public String name;
|
||||
public String key;
|
||||
};
|
||||
|
||||
public static int team;
|
||||
public static boolean server;
|
||||
public static List<CameraConfig> cameraConfigs = new ArrayList<>();
|
||||
public static List<SwitchedCameraConfig> switchedCameraConfigs = new ArrayList<>();
|
||||
public static List<VideoSource> 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<VideoSource> 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
|
||||
|
|
|
@ -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": <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"
|
||||
|
@ -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:
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
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="card">
|
||||
<div class="card-header">
|
||||
<h6>Network Tables</h6>
|
||||
<h5>Network Tables</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
|
@ -236,143 +236,196 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion" id="cameras">
|
||||
<div class="card" style="display:none" id="cameraNEW">
|
||||
<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="#cameraBodyNEW">
|
||||
<h6 class="cameraTitle">Camera NEW</h6>
|
||||
</button>
|
||||
<span class="badge badge-secondary align-text-top cameraConnectionBadge">Disconnected</span>
|
||||
</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 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 class="card">
|
||||
<div class="card-header">
|
||||
<h5>USB Cameras</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="cameras">
|
||||
<div class="card" style="display:none" id="cameraNEW">
|
||||
<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="#cameraBodyNEW">
|
||||
<h6 class="cameraTitle">Camera NEW</h6>
|
||||
</button>
|
||||
<span class="badge badge-secondary align-text-top cameraConnectionBadge">Disconnected</span>
|
||||
</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 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 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 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 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>
|
||||
<div class="collapse" id="switchedCameraBodyNEW" data-parent="#switchedCameras">
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<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 class="form-group col col-4">
|
||||
<label for="switchedCameraNameNEW">Name</label>
|
||||
<input type="text" class="form-control cameraName" id="switchedCameraNameNEW">
|
||||
</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 class="form-group col">
|
||||
<label for="switchedCameraKeyNEW">NetworkTable Key</label>
|
||||
<input type="text" class="form-control cameraKey" id="switchedCameraKeyNEW">
|
||||
</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>
|
||||
</form>
|
||||
</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>
|
||||
|
@ -400,10 +453,20 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-success" id="addCamera">
|
||||
<span data-feather="plus"></span>
|
||||
Add Other Camera
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" id="addCamera" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span data-feather="plus"></span>
|
||||
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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
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() {
|
||||
return wpi::errs() << "config error in '" << configFile << "': ";
|
||||
|
@ -101,7 +116,31 @@ bool ReadCameraConfig(const wpi::json& 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;
|
||||
}
|
||||
|
||||
|
@ -165,10 +204,22 @@ 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;
|
||||
}
|
||||
|
||||
void StartCamera(const CameraConfig& config) {
|
||||
cs::UsbCamera StartCamera(const CameraConfig& config) {
|
||||
wpi::outs() << "Starting camera '" << config.name << "' on " << config.path
|
||||
<< '\n';
|
||||
auto inst = frc::CameraServer::GetInstance();
|
||||
|
@ -180,6 +231,36 @@ void StartCamera(const CameraConfig& config) {
|
|||
|
||||
if (config.streamConfig.is_object())
|
||||
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
|
||||
|
||||
|
@ -200,7 +281,11 @@ int main(int argc, char* argv[]) {
|
|||
}
|
||||
|
||||
// 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
|
||||
for (;;) std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
|
|
Loading…
Reference in New Issue
Block a user