From 4d2e328b12d3db225d85bbfe716a3471cceac2be Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 13 Jan 2019 15:44:59 -0800 Subject: [PATCH] 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 --- deps/allwpilib | 2 +- deps/tools/Makefile | 8 +- .../rpiConfigServer_src/VisionStatus.cpp | 93 +++++++++++++++++++ deps/tools/rpiConfigServer_src/VisionStatus.h | 15 ++- .../rpiConfigServer_src/WebSocketHandlers.cpp | 4 + .../resources/frcvision.js | 84 ++++++++++++++++- .../rpiConfigServer_src/resources/index.html | 28 +++++- 7 files changed, 223 insertions(+), 11 deletions(-) diff --git a/deps/allwpilib b/deps/allwpilib index 453a904..05d6660 160000 --- a/deps/allwpilib +++ b/deps/allwpilib @@ -1 +1 @@ -Subproject commit 453a9047e4f39825ea9993eea16e952be786c48d +Subproject commit 05d6660a6be0d37064a9fab44232b8379884ccab diff --git a/deps/tools/Makefile b/deps/tools/Makefile index 74aef55..1b822d8 100644 --- a/deps/tools/Makefile +++ b/deps/tools/Makefile @@ -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}"' \ diff --git a/deps/tools/rpiConfigServer_src/VisionStatus.cpp b/deps/tools/rpiConfigServer_src/VisionStatus.cpp index ab8b8b2..6b0e949 100644 --- a/deps/tools/rpiConfigServer_src/VisionStatus.cpp +++ b/deps/tools/rpiConfigServer_src/VisionStatus.cpp @@ -15,13 +15,16 @@ #include +#include #include #include #include #include #include +#include #include #include +#include #include namespace uv = wpi::uv; @@ -33,6 +36,96 @@ std::shared_ptr VisionStatus::GetInstance() { return visStatus; } +void VisionStatus::SetLoop(std::shared_ptr 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; + }; + auto workReq = std::make_shared(); + 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 onFail) { struct SvcWorkReq : public uv::WorkReq { diff --git a/deps/tools/rpiConfigServer_src/VisionStatus.h b/deps/tools/rpiConfigServer_src/VisionStatus.h index 2811681..c9fcd1d 100644 --- a/deps/tools/rpiConfigServer_src/VisionStatus.h +++ b/deps/tools/rpiConfigServer_src/VisionStatus.h @@ -10,7 +10,9 @@ #include #include +#include +#include #include #include #include @@ -31,9 +33,7 @@ class VisionStatus { VisionStatus(const VisionStatus&) = delete; VisionStatus& operator=(const VisionStatus&) = delete; - void SetLoop(std::shared_ptr loop) { - m_loop = std::move(loop); - } + void SetLoop(std::shared_ptr loop); void Up(std::function onFail); void Down(std::function onFail); @@ -42,16 +42,25 @@ class VisionStatus { void UpdateStatus(); void ConsoleLog(wpi::uv::Buffer& buf, size_t len); + void UpdateCameraList(); wpi::sig::Signal update; wpi::sig::Signal log; + wpi::sig::Signal cameraList; static std::shared_ptr GetInstance(); private: void RunSvc(const char* cmd, std::function onFail); + void RefreshCameraList(); std::shared_ptr m_loop; + + struct CameraInfo { + cs::UsbCameraInfo info; + std::vector modes; + }; + std::vector m_cameraInfo; }; #endif // RPICONFIGSERVER_VISIONSTATUS_H_ diff --git a/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp b/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp index 4b9995f..05e0b7e 100644 --- a/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp +++ b/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp @@ -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(); diff --git a/deps/tools/rpiConfigServer_src/resources/frcvision.js b/deps/tools/rpiConfigServer_src/resources/frcvision.js index 4c038dc..5b4a953 100644 --- a/deps/tools/rpiConfigServer_src/resources/frcvision.js +++ b/deps/tools/rpiConfigServer_src/resources/frcvision.js @@ -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(''); + }); + + 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(''); + paths.forEach(function (path, j) { + addConnectedDropdown.append(''); + }); + } + }); + + $('#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 diff --git a/deps/tools/rpiConfigServer_src/resources/index.html b/deps/tools/rpiConfigServer_src/resources/index.html index 59dbe6f..9e0f919 100644 --- a/deps/tools/rpiConfigServer_src/resources/index.html +++ b/deps/tools/rpiConfigServer_src/resources/index.html @@ -244,6 +244,7 @@ + Disconnected