Add NetworkTable-selectable switched camera support (#117)

Fixes #100.

Also expand the newest added camera.
pull/311/head
Peter Johnson 2019-02-18 19:47:05 -08:00 committed by GitHub
parent e84e55cd72
commit 9fe4460068
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 585 additions and 149 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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:

View File

@ -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');
});
}

View File

@ -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>

View File

@ -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));