List available cameras on web dashboard (#63)

- Available cameras can be added with a specific path
- Individual cameras show connection status
- Individual cameras show a list of alternate paths
This commit is contained in:
Peter Johnson 2019-01-13 15:44:59 -08:00 committed by GitHub
parent b0ecb03407
commit 4d2e328b12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 11 deletions

2
deps/allwpilib vendored

@ -1 +1 @@
Subproject commit 453a9047e4f39825ea9993eea16e952be786c48d
Subproject commit 05d6660a6be0d37064a9fab44232b8379884ccab

8
deps/tools/Makefile vendored
View File

@ -81,7 +81,12 @@ rpiConfigServer: ${RPICONFIGSERVER_OBJS}
${COMPILER}g++ -pthread -g -o $@ \
${RPICONFIGSERVER_OBJS} \
-L${WPILIB_STATIC_BUILD}/lib \
-lwpiutil
-L${OPENCV_STATIC_INSTALL}/lib \
-L${OPENCV_STATIC_INSTALL}/share/OpenCV/3rdparty/lib \
-lcscore \
-lwpiutil \
-lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_core \
-ltegra_hal -llibpng -llibjpeg-turbo -lzlib
${COMPILER}objcopy --only-keep-debug $@ $@.debug
${COMPILER}strip -g $@
${COMPILER}objcopy --add-gnu-debuglink=$@.debug $@
@ -89,6 +94,7 @@ rpiConfigServer: ${RPICONFIGSERVER_OBJS}
%.o: %.cpp
${COMPILER}g++ -g -O -Wall -c -o $@ \
-I${WPILIB_SRC}/wpiutil/src/main/native/include \
-I${WPILIB_SRC}/cscore/src/main/native/include \
'-DEXEC_HOME="${EXEC_HOME}"' \
'-DFRC_JSON="${FRC_JSON}"' \
'-DDHCPCD_CONF="${DHCPCD_CONF}"' \

View File

@ -15,13 +15,16 @@
#include <cstring>
#include <cscore.h>
#include <wpi/SmallString.h>
#include <wpi/StringRef.h>
#include <wpi/json.h>
#include <wpi/raw_ostream.h>
#include <wpi/uv/Buffer.h>
#include <wpi/uv/FsEvent.h>
#include <wpi/uv/Pipe.h>
#include <wpi/uv/Process.h>
#include <wpi/uv/Timer.h>
#include <wpi/uv/Work.h>
namespace uv = wpi::uv;
@ -33,6 +36,96 @@ std::shared_ptr<VisionStatus> VisionStatus::GetInstance() {
return visStatus;
}
void VisionStatus::SetLoop(std::shared_ptr<wpi::uv::Loop> loop) {
m_loop = std::move(loop);
auto refreshTimer = wpi::uv::Timer::Create(m_loop);
refreshTimer->timeout.connect([this] { RefreshCameraList(); });
refreshTimer->Unreference();
auto devEvents = wpi::uv::FsEvent::Create(m_loop);
devEvents->fsEvent.connect([refreshTimer](const char* fn, int flags) {
if (wpi::StringRef(fn).startswith("video"))
refreshTimer->Start(wpi::uv::Timer::Time(200));
});
devEvents->Start("/dev");
devEvents->Unreference();
refreshTimer->Start(wpi::uv::Timer::Time(200));
}
void VisionStatus::UpdateCameraList() {
wpi::json j = {{"type", "cameraList"}, {"cameras", wpi::json::array()}};
auto& cams = j["cameras"];
for (const auto& caminfo : m_cameraInfo) {
wpi::json cam = {{"dev", caminfo.info.dev},
{"path", caminfo.info.path},
{"name", caminfo.info.name},
{"otherPaths", wpi::json::array()},
{"modes", wpi::json::array()}};
auto& otherPaths = cam["otherPaths"];
for (const auto& path : caminfo.info.otherPaths)
otherPaths.emplace_back(path);
auto& modes = cam["modes"];
for (const auto& mode : caminfo.modes) {
wpi::json jmode;
wpi::StringRef pixelFormatStr;
switch (mode.pixelFormat) {
case cs::VideoMode::kMJPEG:
pixelFormatStr = "mjpeg";
break;
case cs::VideoMode::kYUYV:
pixelFormatStr = "yuyv";
break;
case cs::VideoMode::kRGB565:
pixelFormatStr = "rgb565";
break;
case cs::VideoMode::kBGR:
pixelFormatStr = "bgr";
break;
case cs::VideoMode::kGray:
pixelFormatStr = "gray";
break;
default:
continue;
}
jmode.emplace("pixelFormat", pixelFormatStr);
jmode.emplace("width", mode.width);
jmode.emplace("height", mode.height);
jmode.emplace("fps", mode.fps);
modes.emplace_back(jmode);
}
cams.emplace_back(cam);
}
cameraList(j);
}
void VisionStatus::RefreshCameraList() {
struct RefreshCameraWorkReq : public uv::WorkReq {
std::vector<CameraInfo> cameraInfo;
};
auto workReq = std::make_shared<RefreshCameraWorkReq>();
workReq->work.connect([r = workReq.get()] {
CS_Status status = 0;
for (auto&& caminfo : cs::EnumerateUsbCameras(&status)) {
cs::UsbCamera camera{"usbcam", caminfo.dev};
r->cameraInfo.emplace_back();
r->cameraInfo.back().info = std::move(caminfo);
r->cameraInfo.back().modes = camera.EnumerateVideoModes();
}
});
workReq->afterWork.connect([ this, r = workReq.get() ] {
m_cameraInfo = std::move(r->cameraInfo);
UpdateCameraList();
});
uv::QueueWork(m_loop, workReq);
}
void VisionStatus::RunSvc(const char* cmd,
std::function<void(wpi::StringRef)> onFail) {
struct SvcWorkReq : public uv::WorkReq {

View File

@ -10,7 +10,9 @@
#include <functional>
#include <memory>
#include <vector>
#include <cscore.h>
#include <wpi/Signal.h>
#include <wpi/StringRef.h>
#include <wpi/uv/Loop.h>
@ -31,9 +33,7 @@ class VisionStatus {
VisionStatus(const VisionStatus&) = delete;
VisionStatus& operator=(const VisionStatus&) = delete;
void SetLoop(std::shared_ptr<wpi::uv::Loop> loop) {
m_loop = std::move(loop);
}
void SetLoop(std::shared_ptr<wpi::uv::Loop> loop);
void Up(std::function<void(wpi::StringRef)> onFail);
void Down(std::function<void(wpi::StringRef)> onFail);
@ -42,16 +42,25 @@ class VisionStatus {
void UpdateStatus();
void ConsoleLog(wpi::uv::Buffer& buf, size_t len);
void UpdateCameraList();
wpi::sig::Signal<const wpi::json&> update;
wpi::sig::Signal<const wpi::json&> log;
wpi::sig::Signal<const wpi::json&> cameraList;
static std::shared_ptr<VisionStatus> GetInstance();
private:
void RunSvc(const char* cmd, std::function<void(wpi::StringRef)> onFail);
void RefreshCameraList();
std::shared_ptr<wpi::uv::Loop> m_loop;
struct CameraInfo {
cs::UsbCameraInfo info;
std::vector<cs::VideoMode> modes;
};
std::vector<CameraInfo> m_cameraInfo;
};
#endif // RPICONFIGSERVER_VISIONSTATUS_H_

View File

@ -45,6 +45,7 @@ struct WebSocketData {
wpi::sig::ScopedConnection sysWritableConn;
wpi::sig::ScopedConnection visStatusConn;
wpi::sig::ScopedConnection visLogConn;
wpi::sig::ScopedConnection cameraListConn;
wpi::sig::ScopedConnection netSettingsConn;
wpi::sig::ScopedConnection visSettingsConn;
wpi::sig::ScopedConnection appSettingsConn;
@ -134,6 +135,9 @@ void InitWs(wpi::WebSocket& ws) {
if (d->visionLogEnabled) SendWsText(ws, j);
});
visStatus->UpdateStatus();
data->cameraListConn = visStatus->cameraList.connect_connection(
[&ws](const wpi::json& j) { SendWsText(ws, j); });
visStatus->UpdateCameraList();
// send initial network settings
auto netSettings = NetworkSettings::GetInstance();

View File

@ -40,8 +40,8 @@ function displayStatus(message) {
}
// 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', 'addCamera', 'applicationType'];
var connectedButtonClasses = ['cameraName', 'cameraPath', 'cameraPixelFormat', 'cameraWidth', 'cameraHeight', 'cameraFps', 'cameraBrightness', 'cameraWhiteBalance', 'cameraExposure', 'cameraProperties', 'cameraRemove', 'cameraCopyConfig']
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', 'cameraRemove', 'cameraCopyConfig']
var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave'];
var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s',
'systemMemoryAvail1s', 'systemMemoryAvail5s',
@ -54,6 +54,7 @@ function displayDisconnected() {
displayReadOnly();
$('#connectionBadge').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected');
$('#visionServiceStatus').removeClass('badge-primary').removeClass('badge-secondary').addClass('badge-dark').text('Unknown Status');
$('.cameraConnectionBadge').removeClass('badge-primary').removeClass('badge-secondary').addClass('badge-dark').text('Unknown Status');
for (var i = 0; i < connectedButtonIds.length; i++) {
$('#' + connectedButtonIds[i]).prop('disabled', true);
}
@ -114,6 +115,7 @@ $('#systemWritable').click(function() {
// Vision settings
var visionSettingsServer = {};
var visionSettingsDisplay = {'cameras': []};
var cameraList = [];
function pushVisionLogEnabled() {
var msg = {
@ -204,6 +206,10 @@ function connect() {
case 'status':
displayStatus(msg.message);
break;
case 'cameraList':
cameraList = msg.cameras;
updateCameraListView();
break;
}
};
}
@ -425,6 +431,7 @@ function appendNewVisionCameraView(value, i) {
camera.find('.cameraRemove').click(function() {
visionSettingsDisplay.cameras.splice(i, 1);
camera.remove();
updateCameraListView();
});
camera.find('.cameraSettingsFile').change(function() {
if (this.files.length <= 0) {
@ -459,6 +466,9 @@ function appendNewVisionCameraView(value, 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));
});
$('#cameras').append(camera);
}
@ -477,6 +487,7 @@ function updateVisionSettingsView() {
visionSettingsDisplay.cameras.forEach(function (value, i) {
appendNewVisionCameraView(value, i);
});
updateCameraListView();
feather.replace();
}
@ -554,8 +565,77 @@ $('#addCamera').click(function() {
var i = visionSettingsDisplay.cameras.length;
visionSettingsDisplay.cameras.push({});
appendNewVisionCameraView({}, i);
updateCameraListView();
});
function updateCameraListView() {
var addConnectedDropdown = $('#addConnectedCameraList');
addConnectedDropdown.html('');
// disable all the alternate paths by default
visionSettingsDisplay.cameras.forEach(function (value, k) {
var cameraElem = $('#camera' + k);
cameraElem.find('.cameraConnectionBadge').removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected');
cameraElem.find('.cameraAlternatePathsList').html('');
cameraElem.find('.cameraAlternatePaths').prop('disabled', true);
});
cameraList.forEach(function (camera, i) {
// See if one of the paths is an already existing camera
// Include the "main path" as the first path
var matchedCamera = false;
var paths = [camera.path];
camera.otherPaths.forEach(function (path, j) {
paths.push(path);
});
paths.forEach(function (path, j) {
visionSettingsDisplay.cameras.forEach(function (value, k) {
var cameraElem = $('#camera' + k);
var pathElem = cameraElem.find('.cameraPath');
if (path === pathElem.val()) {
matchedCamera = true;
// show camera as connected
cameraElem.find('.cameraConnectionBadge').removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary').text('Connected');
// build alternate path list
var setAlternateDropdown = cameraElem.find('.cameraAlternatePathsList');
setAlternateDropdown.html('');
paths.forEach(function (altPath, j) {
setAlternateDropdown.append('<button class="dropdown-item cameraSetAlternatePath" type="button">' + altPath + '</button>');
});
cameraElem.find('.cameraAlternatePaths').prop('disabled', setAlternateDropdown.html() === '');
// hook up dropdown items to set alternate path
setAlternateDropdown.find('.cameraSetAlternatePath').click(function() {
pathElem.val($(this).text());
});
}
});
});
if (!matchedCamera) {
// add it to add connected camera list
addConnectedDropdown.append('<h5 class="dropdown-header">' + camera.name + '</h5>');
paths.forEach(function (path, j) {
addConnectedDropdown.append('<button class="dropdown-item addConnectedCameraItem" type="button">' + path + '</button>');
});
}
});
$('#addConnectedCamera').prop('disabled', addConnectedDropdown.html() === '');
// hook up dropdown items to create cameras
addConnectedDropdown.find('.addConnectedCameraItem').click(function() {
var i = visionSettingsDisplay.cameras.length;
var camera = {"path": $(this).text()};
visionSettingsDisplay.cameras.push(camera);
appendNewVisionCameraView(camera, i);
updateCameraListView();
});
}
var applicationFiles = [];
// Show details when appropriate for application type

View File

@ -244,6 +244,7 @@
<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">
@ -261,13 +262,22 @@
<div class="card-body">
<form>
<div class="form-row">
<div class="form-group col">
<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>
<input type="text" class="form-control cameraPath" id="cameraPathNEW">
<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="form-group">
@ -336,15 +346,25 @@
<span data-feather="save"></span>
Save
</button>
<button type="button" class="btn btn-secondary" id="visionDiscard">
<button type="button" class="btn btn-warning" id="visionDiscard">
<span data-feather="x"></span>
Discard Changes
</button>
</div>
<div class="col-auto">
<div class="dropdown">
<button type="button" class="btn btn-info dropdown-toggle" id="addConnectedCamera" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="true">
<span data-feather="plus"></span>
Add Connected Camera
</button>
<div class="dropdown-menu" aria-labelledby="addConnectedCamera" id="addConnectedCameraList">
</div>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-success" id="addCamera">
<span data-feather="plus"></span>
Add Camera
Add Other Camera
</button>
</div>
</div>