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:
parent
b0ecb03407
commit
4d2e328b12
2
deps/allwpilib
vendored
2
deps/allwpilib
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 453a9047e4f39825ea9993eea16e952be786c48d
|
||||
Subproject commit 05d6660a6be0d37064a9fab44232b8379884ccab
|
8
deps/tools/Makefile
vendored
8
deps/tools/Makefile
vendored
|
@ -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}"' \
|
||||
|
|
93
deps/tools/rpiConfigServer_src/VisionStatus.cpp
vendored
93
deps/tools/rpiConfigServer_src/VisionStatus.cpp
vendored
|
@ -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 {
|
||||
|
|
15
deps/tools/rpiConfigServer_src/VisionStatus.h
vendored
15
deps/tools/rpiConfigServer_src/VisionStatus.h
vendored
|
@ -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_
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user