diff --git a/glass/CMakeLists.txt b/glass/CMakeLists.txt index 1fe76a4ce14..8701c7e6942 100644 --- a/glass/CMakeLists.txt +++ b/glass/CMakeLists.txt @@ -59,6 +59,27 @@ install(TARGETS libglassnt EXPORT libglassnt) export(TARGETS libglassnt FILE libglassnt.cmake NAMESPACE libglassnt::) install(DIRECTORY src/libnt/native/include/ DESTINATION "${include_dest}/glass") +# +# libglasscs +# +file(GLOB_RECURSE libglasscs_src src/libcs/native/cpp/*.cpp) + +add_library(libglasscs STATIC ${libglasscs_src}) +set_target_properties(libglasscs PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME "glasscs") +set_property(TARGET libglasscs PROPERTY POSITION_INDEPENDENT_CODE ON) + +set_property(TARGET libglasscs PROPERTY FOLDER "libraries") + +wpilib_target_warnings(libglasscs) +target_link_libraries(libglasscs PUBLIC cscore libglassnt) + +target_include_directories(libglasscs PUBLIC + $ + $) + +install(TARGETS libglasscs EXPORT libglasscs DESTINATION "${main_lib_dest}") +install(DIRECTORY src/libcs/native/include/ DESTINATION "${include_dest}/glass") + # # glass application # @@ -79,7 +100,7 @@ endif() add_executable(glass ${glass_src} ${glass_resources_src} ${glass_rc} ${APP_ICON_MACOSX}) wpilib_link_macos_gui(glass) wpilib_target_warnings(glass) -target_link_libraries(glass libglassnt libglass) +target_link_libraries(glass libglasscs libglassnt libglass) if(WIN32) set_target_properties(glass PROPERTIES WIN32_EXECUTABLE YES) diff --git a/glass/build.gradle b/glass/build.gradle index 551e6ffaefe..bbf5ed80723 100644 --- a/glass/build.gradle +++ b/glass/build.gradle @@ -101,6 +101,38 @@ model { } appendDebugPathToBinaries(binaries) } + "${nativeName}cs"(NativeLibrarySpec) { + sources.cpp { + source { + srcDirs = ['src/libcs/native/cpp'] + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/libcs/native/include' + } + } + binaries.all { + if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) { + it.buildable = false + return + } + if (it instanceof SharedLibraryBinarySpec) { + it.buildable = false + return + } + lib library: nativeName, linkage: 'static' + project(':ntcore').addNtcoreDependency(it, 'shared') + lib project: ':cscore', library: 'cscore', linkage: 'shared' + nativeUtils.useRequiredLibrary(it, 'opencv_shared') + lib project: ':wpinet', library: 'wpinet', linkage: 'shared' + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib project: ':wpimath', library: 'wpimath', linkage: 'shared' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'shared' + nativeUtils.useRequiredLibrary(it, 'imgui') + } + appendDebugPathToBinaries(binaries) + } "${nativeName}nt"(NativeLibrarySpec) { sources.cpp { source { @@ -157,9 +189,10 @@ model { it.buildable = false return } - lib project: ':cscore', library: 'cscore', linkage: 'static' + lib library: 'glasscs', linkage: 'static' lib library: 'glassnt', linkage: 'static' lib library: nativeName, linkage: 'static' + lib project: ':cscore', library: 'cscore', linkage: 'static' project(':ntcore').addNtcoreDependency(it, 'static') lib project: ':wpinet', library: 'wpinet', linkage: 'static' lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' diff --git a/glass/publish.gradle b/glass/publish.gradle index ab8df4f9ac5..4e19674cd2d 100644 --- a/glass/publish.gradle +++ b/glass/publish.gradle @@ -12,6 +12,10 @@ def libntBaseArtifactId = 'libglassnt' def libntArtifactGroupId = 'edu.wpi.first.glass' def libntZipBaseName = '_GROUP_edu_wpi_first_glass_ID_libglassnt_CLS' +def libcsBaseArtifactId = 'libglasscs' +def libcsArtifactGroupId = 'edu.wpi.first.glass' +def libcsZipBaseName = '_GROUP_edu_wpi_first_glass_ID_libglasscs_CLS' + def outputsFolder = file("$project.buildDir/outputs") task libCppSourcesZip(type: Zip) { @@ -50,15 +54,37 @@ task libntCppHeadersZip(type: Zip) { from('src/libnt/native/include') { into '/' } } +task libcsCppSourcesZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = libcsZipBaseName + classifier = "sources" + + from(licenseFile) { into '/' } + from('src/libcs/native/cpp') { into '/' } +} + +task libcsCppHeadersZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = libcsZipBaseName + classifier = "headers" + + from(licenseFile) { into '/' } + from('src/libcs/native/include') { into '/' } +} + build.dependsOn libCppHeadersZip build.dependsOn libCppSourcesZip build.dependsOn libntCppHeadersZip build.dependsOn libntCppSourcesZip +build.dependsOn libcsCppHeadersZip +build.dependsOn libcsCppSourcesZip addTaskToCopyAllOutputs(libCppHeadersZip) addTaskToCopyAllOutputs(libCppSourcesZip) addTaskToCopyAllOutputs(libntCppHeadersZip) addTaskToCopyAllOutputs(libntCppSourcesZip) +addTaskToCopyAllOutputs(libcsCppHeadersZip) +addTaskToCopyAllOutputs(libcsCppSourcesZip) model { tasks { @@ -168,6 +194,7 @@ model { def libGlassTaskList = createComponentZipTasks($.components, ['glass'], libZipBaseName, Zip, project, includeStandardZipFormat) def libGlassntTaskList = createComponentZipTasks($.components, ['glassnt'], libntZipBaseName, Zip, project, includeStandardZipFormat) + def libGlasscsTaskList = createComponentZipTasks($.components, ['glasscs'], libcsZipBaseName, Zip, project, includeStandardZipFormat) publications { glassApp(MavenPublication) { @@ -197,6 +224,16 @@ model { groupId = libntArtifactGroupId version wpilibVersioning.version.get() } + libglasscs(MavenPublication) { + libGlasscsTaskList.each { artifact it } + + artifact libcsCppHeadersZip + artifact libcsCppSourcesZip + + artifactId = libcsBaseArtifactId + groupId = libcsArtifactGroupId + version wpilibVersioning.version.get() + } } } } diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp index 2ec8f10cb29..4c277655a63 100644 --- a/glass/src/app/native/cpp/main.cpp +++ b/glass/src/app/native/cpp/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,9 @@ #include "glass/MainMenuBar.h" #include "glass/Storage.h" #include "glass/Window.h" +#include "glass/camera/Camera.h" +#include "glass/camera/NTCameraProvider.h" +#include "glass/camera/UsbCameraList.h" #include "glass/networktables/NetworkTables.h" #include "glass/networktables/NetworkTablesProvider.h" #include "glass/networktables/NetworkTablesSettings.h" @@ -39,6 +43,8 @@ std::string_view GetResource_glass_512_png(); static std::unique_ptr gPlotProvider; static std::unique_ptr gNtProvider; +static std::unique_ptr gNtCameraProvider; +static std::unique_ptr gUsbCameraList; static std::unique_ptr gNetworkTablesModel; static std::unique_ptr gNetworkTablesSettings; @@ -47,6 +53,7 @@ static glass::Window* gNetworkTablesWindow; static glass::Window* gNetworkTablesInfoWindow; static glass::Window* gNetworkTablesSettingsWindow; static glass::Window* gNetworkTablesLogWindow; +static glass::Window* gCameraListWindow; static glass::MainMenuBar gMainMenu; static bool gAbout = false; @@ -171,6 +178,41 @@ static void NtInitialize() { }); } +static void CsInitialize() { + cs::SetTelemetryPeriod(1.0); + cs::SetLogger([](unsigned int level, const char* file, unsigned int line, + const char* msg) { fmt::print("CS: {}\n", msg); }, + CS_LOG_INFO); + + gNtCameraProvider = std::make_unique( + glass::GetStorageRoot().GetChild("NT Cameras")); + gNtCameraProvider->GlobalInit(); + + gUsbCameraList = std::make_unique(); + + gCameraListWindow = + glass::imm::CreateWindow("Camera List", false, glass::Window::kHide); + gCameraListWindow->SetDefaultPos(250, 250); + gCameraListWindow->SetDefaultSize(500, 200); + gCameraListWindow->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + gui::AddWindowScaler( + [](float scale) { gCameraListWindow->ScaleDefault(scale); }); + + gui::AddEarlyExecute([] { + glass::Storage& cameras = glass::GetStorageRoot().GetChild("cameras"); + for (auto&& storage : cameras.GetChildren()) { + glass::CameraModel* model = glass::GetCameraModel(cameras, storage.key()); + if (!model) { + model = glass::GetOrNewCameraModel(cameras, storage.key()); + model->Start(); + } + model->Update(); + } + + gUsbCameraList->Update(); + }); +} + #ifdef _WIN32 int __stdcall WinMain(void* hInstance, void* hPrevInstance, char* pCmdLine, int nCmdShow) { @@ -212,6 +254,7 @@ int main(int argc, char** argv) { gui::AddInit([] { glass::ResetTime(); }); gNtProvider->GlobalInit(); NtInitialize(); + CsInitialize(); glass::AddStandardNetworkTablesViews(*gNtProvider); @@ -236,6 +279,27 @@ int main(int argc, char** argv) { gNtProvider->DisplayMenu(); ImGui::EndMenu(); } + if (ImGui::BeginMenu("Camera")) { + gCameraListWindow->DisplayMenuItem(); + if (ImGui::MenuItem("New Camera View")) { + auto& viewsStorage = glass::GetStorageRoot().GetChild("camera views"); + auto& views = viewsStorage.GetValues(); + // this is an inefficient algorithm, but the number of windows is small + char id[32]; + size_t numViews = views.size(); + for (size_t i = 0; i <= numViews; ++i) { + wpi::format_to_n_c_str(id, sizeof(id), "Camera View <{}>", + static_cast(i)); + if (!views.contains(id)) { + break; + } + } + if (auto win = glass::imm::CreateWindow(viewsStorage, id)) { + win->SetDefaultSize(700, 400); + } + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Plot")) { bool paused = gPlotProvider->IsPaused(); if (ImGui::MenuItem("Pause All Plots", nullptr, &paused)) { @@ -300,6 +364,54 @@ int main(int argc, char** argv) { } glass::imm::EndWindow(); + glass::Storage& cameras = glass::GetStorageRoot().GetChild("cameras"); + + if (glass::imm::BeginWindow(gCameraListWindow)) { + glass::DisplayCameraModelTable(cameras); + if (ImGui::Button("Add USB")) { + ImGui::OpenPopup("Add USB Camera"); + } + if (ImGui::BeginPopup("Add USB Camera")) { + std::string path = gUsbCameraList->DisplayMenu(); + if (!path.empty()) { + auto id = fmt::format("glass::usb::{}", path); + glass::CameraModel* model = glass::GetOrNewCameraModel(cameras, id); + if (!model->GetSource()) { + model->SetSource(cs::UsbCamera(id, path)); + } + model->Start(); + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + ImGui::Button("Add HTTP"); + ImGui::SameLine(); + ImGui::Button("Add CameraServer"); + } + glass::imm::EndWindow(); + + for (auto&& kv : + glass::GetStorageRoot().GetChild("camera views").GetChildren()) { + glass::PushStorageStack(kv.value()); + if (!glass::imm::GetWindow()) { + glass::imm::CreateWindow( + glass::GetStorageRoot().GetChild("camera views"), kv.key()); + } + if (glass::imm::BeginWindow()) { + if (glass::CameraModel* model = glass::GetCameraModel( + cameras, glass::GetStorage().GetString("camera"))) { + if (glass::imm::BeginWindowSettingsPopup()) { + glass::imm::GetWindow()->EditName(); + glass::DisplayCameraSettings(model); + ImGui::EndPopup(); + } + glass::DisplayCameraWindow(model); + } + } + glass::imm::EndWindow(); + glass::PopStorageStack(); + } + if (gAbout) { ImGui::OpenPopup("About"); gAbout = false; @@ -364,6 +476,9 @@ int main(int argc, char** argv) { } gui::Main(); + // cs::Shutdown(); + gUsbCameraList.reset(); + gNtCameraProvider.reset(); gNetworkTablesModel.reset(); gNtProvider.reset(); gPlotProvider.reset(); diff --git a/glass/src/libcs/native/cpp/CSCameraProvider.cpp b/glass/src/libcs/native/cpp/CSCameraProvider.cpp new file mode 100644 index 00000000000..45e86647295 --- /dev/null +++ b/glass/src/libcs/native/cpp/CSCameraProvider.cpp @@ -0,0 +1,94 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "glass/camera/CSCameraProvider.h" + +#include + +#include +#include + +using namespace glass; + +CSCameraProvider::CSCameraProvider(Storage& storage) + : CameraProviderBase{storage}, m_poller{cs::CreateListenerPoller()} { + CS_Status status = 0; + cs::AddPolledListener(m_poller, + CS_SOURCE_CREATED | CS_SOURCE_DESTROYED | + CS_SOURCE_CONNECTED | CS_SOURCE_DISCONNECTED, + true, &status); +} + +CSCameraProvider::~CSCameraProvider() { + cs::DestroyListenerPoller(m_poller); +} + +void CSCameraProvider::DisplayMenu() { + bool any = false; + for (auto&& info : m_sourceInfo) { + ImGui::MenuItem(info.name.c_str()); + any = true; + } + if (!any) { + ImGui::MenuItem("(None)", nullptr, false, false); + } +} + +void CSCameraProvider::Update() { + bool timedOut = false; + for (auto&& event : cs::PollListener(m_poller, 0, &timedOut)) { + if (event.kind == cs::RawEvent::kSourceCreated) { + CS_Status status = 0; + CSSourceInfo info; + info.name = event.name; + info.handle = event.sourceHandle; + info.kind = cs::GetSourceKind(event.sourceHandle, &status); + info.connected = cs::IsSourceConnected(event.sourceHandle, &status); + m_sourceInfo.emplace_back(std::move(info)); + continue; + } + + auto it = std::find_if( + m_sourceInfo.begin(), m_sourceInfo.end(), + [&](const auto& info) { return info.handle == event.sourceHandle; }); + if (it == m_sourceInfo.end()) { + continue; + } + switch (event.kind) { + case cs::RawEvent::kSourceDestroyed: + m_sourceInfo.erase(it); + break; + case cs::RawEvent::kSourceConnected: + it->connected = true; + break; + case cs::RawEvent::kSourceDisconnected: + it->connected = false; + break; + default: + break; + } + } +#if 0 + // check for visible windows that need displays (typically this is due to + // file loading) + for (auto&& window : m_windows) { + if (!window->IsVisible() || window->HasView()) { + continue; + } + auto id = window->GetId(); + + // only handle ones where we have a builder + auto builderIt = m_typeMap.find(typeIt->second.GetName()); + if (builderIt == m_typeMap.end()) { + continue; + } + + auto entry = GetOrCreateView( + builderIt->second, nt::GetEntry(m_nt.GetInstance(), id + "/.type"), id); + if (entry) { + Show(entry, window.get()); + } + } +#endif +} diff --git a/glass/src/libcs/native/cpp/Camera.cpp b/glass/src/libcs/native/cpp/Camera.cpp new file mode 100644 index 00000000000..2b7c793a26e --- /dev/null +++ b/glass/src/libcs/native/cpp/Camera.cpp @@ -0,0 +1,549 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "glass/camera/Camera.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cscore_c.h" +#include "cscore_oo.h" +#include "glass/Context.h" +#include "glass/Storage.h" + +using namespace glass; + +static const char* PixelFormatToString(int pixelFormat) { + switch (pixelFormat) { + case cs::VideoMode::kBGR: + return "BGR"; + case cs::VideoMode::kGray: + return "Gray"; + case cs::VideoMode::kMJPEG: + return "MJPEG"; + case cs::VideoMode::kRGB565: + return "RGB565"; + case cs::VideoMode::kYUYV: + return "YUYV"; + default: + return "Unknown"; + } +} + +static int StringToPixelFormat(std::string_view str) { + if (str == "BGR") { + return cs::VideoMode::kBGR; + } else if (str == "Gray") { + return cs::VideoMode::kGray; + } else if (str == "MJPEG") { + return cs::VideoMode::kMJPEG; + } else if (str == "RGB565") { + return cs::VideoMode::kRGB565; + } else if (str == "YUYV") { + return cs::VideoMode::kYUYV; + } else { + return cs::VideoMode::kUnknown; + } +} + +static void VideoModeToString(char* buf, size_t len, + const cs::VideoMode& mode) { + wpi::format_to_n_c_str(buf, len, "{} ({}x{} {} FPS)", + PixelFormatToString(mode.pixelFormat), mode.width, + mode.height, mode.fps); +} + +CameraModel::CameraModel(Storage& storage, std::string_view id) + : m_id{id}, + m_cameraType{storage.GetString("cameraType")}, + m_usbPath{storage.GetString("usbPath")}, + m_urls{storage.GetStringArray("httpUrls")}, + m_pixelFormatStr{storage.GetString("pixelFormat")}, + m_width{storage.GetInt("width")}, + m_height{storage.GetInt("height")}, + m_fps{storage.GetInt("fps")} {} + +CameraModel::~CameraModel() { + Stop(); + if (m_frameThread.joinable()) { + m_frameThread.join(); + } + delete m_latestFrame.load(); + for (auto frame : m_sharedFreeList) { + delete frame; + } + for (auto frame : m_sourceFreeList) { + delete frame; + } +} + +void CameraModel::Update() { + // create or update texture when we get a new frame + if (auto frame = m_latestFrame.exchange(nullptr)) { + if (!m_tex || frame->cols != m_tex.GetWidth() || + frame->rows != m_tex.GetHeight()) { + m_tex = wpi::gui::Texture(wpi::gui::kPixelRGBA, frame->cols, frame->rows, + frame->data); + } else { + m_tex.Update(frame->data); + } + // put back on shared freelist + std::scoped_lock lock(m_sharedFreeListMutex); + m_sharedFreeList.emplace_back(frame); + } +} + +bool CameraModel::Exists() { + return m_source && m_source.IsConnected(); +} + +void CameraModel::Start() { + if (m_frameThread.joinable()) { + return; + } + + if (!m_source) { + if (m_cameraType == "usb") { + m_source = cs::UsbCamera(m_id, m_usbPath); + } else if (m_cameraType == "http") { + m_source = cs::HttpCamera(m_id, m_urls, cs::HttpCamera::kUnknown); + } else { + return; + } + } + + if (!m_pixelFormatStr.empty() && m_width != 0 && m_height != 0 && + m_fps != 0) { + cs::VideoMode mode; + mode.pixelFormat = StringToPixelFormat(m_pixelFormatStr); + mode.width = m_width; + mode.height = m_height; + mode.fps = m_fps; + SetVideoMode(mode); + } + + m_frameThread = std::thread([this, source = m_source] { + cs::CvSink cvSink{fmt::format("{}_view", m_id), cs::VideoMode::kBGR}; + cvSink.SetSource(source); + cv::Mat frame; + while (!m_stopCamera) { + // get frame from camera + uint64_t time = cvSink.GrabFrame(frame, 0.25); + if (m_stopCamera) { + break; + } + + cv::Mat* out = AllocMat(); + + if (time == 0) { + *out = cv::Mat::zeros(16, 16, CV_8UC4); + } else { + // convert to RGBA + cv::cvtColor(frame, *out, cv::COLOR_BGR2RGBA); + } + + // make available + auto prev = m_latestFrame.exchange(out); + + // put prev on free list + if (prev) { + m_sourceFreeList.emplace_back(prev); + } + } + }); +} + +void CameraModel::Stop() { + m_stopCamera = true; +} + +void CameraModel::SetSource(cs::VideoSource source) { + m_source = source; + switch (GetKind()) { + case cs::VideoSource::Kind::kUsb: + m_cameraType = "usb"; + m_usbPath = static_cast(source).GetPath(); + break; + case cs::VideoSource::Kind::kHttp: + m_cameraType = "http"; + m_urls = static_cast(source).GetUrls(); + break; + case cs::VideoSource::Kind::kCv: + m_cameraType = "cv"; + break; + case cs::VideoSource::Kind::kRaw: + m_cameraType = "raw"; + break; + default: + m_cameraType.clear(); + break; + } +} + +void CameraModel::SetUrls(std::span urls) { + CS_Status status = 0; + cs::SetHttpCameraUrls(m_source.GetHandle(), urls, &status); + m_urls.assign(urls.begin(), urls.end()); +} + +void CameraModel::SetVideoMode(const cs::VideoMode& mode) { + m_source.SetVideoMode(mode); + m_videoMode = mode; + m_pixelFormatStr = PixelFormatToString(m_videoMode.pixelFormat); + m_width = mode.width; + m_height = mode.height; + m_fps = mode.fps; +} + +void CameraModel::ResetVideoMode() { + SetVideoMode({cs::VideoMode::PixelFormat::kMJPEG, 0, 0, 0}); +} + +cv::Mat* CameraModel::AllocMat() { + // get or create a mat, prefer sourceFreeList over sharedFreeList + cv::Mat* out; + if (!m_sourceFreeList.empty()) { + out = m_sourceFreeList.back(); + m_sourceFreeList.pop_back(); + } else { + { + std::scoped_lock lock(m_sharedFreeListMutex); + for (auto mat : m_sharedFreeList) { + m_sourceFreeList.emplace_back(mat); + } + m_sharedFreeList.clear(); + } + if (!m_sourceFreeList.empty()) { + out = m_sourceFreeList.back(); + m_sourceFreeList.pop_back(); + } else { + out = new cv::Mat; + } + } + return out; +} + +static void EditProperty(const std::string& name, cs::VideoProperty& prop) { + switch (prop.GetKind()) { + case cs::VideoProperty::kBoolean: { + bool val = prop.Get(); + if (ImGui::Checkbox(name.c_str(), &val)) { + prop.Set(val); + } + break; + } + case cs::VideoProperty::kEnum: { + int val = prop.Get(); + std::vector options = prop.GetChoices(); + if (ImGui::BeginCombo(name.c_str(), options[val].c_str())) { + for (int i = 0; i < static_cast(options.size()); ++i) { + if (options[i].empty()) { + continue; + } + bool selected = (val == i); + if (ImGui::Selectable(options[i].c_str(), selected)) { + prop.Set(i); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + break; + } + case cs::VideoProperty::kInteger: { + int val = prop.Get(); + // int step = prop.GetStep(); + int min = prop.GetMin(); + int max = prop.GetMax(); + if (min == 0 && max == 1) { + // treat like boolean + bool boolVal = val; + if (ImGui::Checkbox(name.c_str(), &boolVal)) { + prop.Set(boolVal); + } + break; + } + // if (ImGui::InputInt(name.c_str(), &val, step, step * 10)) { + if (ImGui::SliderInt(name.c_str(), &val, min, max)) { + if (val < min) { + val = min; + } + if (val > max) { + val = max; + } + prop.Set(val); + } + break; + } + case cs::VideoProperty::kString: { + std::string val = prop.GetString(); + if (ImGui::InputText(name.c_str(), &val)) { + prop.SetString(val); + } + break; + } + default: + break; + } +} + +static bool EditMode(CameraModel* model) { + bool rv = false; + cs::VideoSource& source = model->GetSource(); + cs::VideoSource::Kind kind = model->GetKind(); + + // video mode; split out width/height/fps for HTTP cameras + if (kind == cs::VideoSource::kHttp) { + static int res[2]; + static int fps; + if (ImGui::IsWindowAppearing()) { + auto mode = model->GetVideoMode(); + res[0] = mode.width; + res[1] = mode.height; + fps = mode.fps; + } + ImGui::Separator(); + ImGui::InputInt2("Resolution", res); + ImGui::SliderInt("FPS", &fps, 0, 30); + if (ImGui::Button("Apply")) { + model->SetVideoMode( + {cs::VideoMode::PixelFormat::kMJPEG, res[0], res[1], fps}); + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + model->ResetVideoMode(); + ImGui::CloseCurrentPopup(); + } + } else { + auto mode = source.GetVideoMode(); + char modeStr[64]; + VideoModeToString(modeStr, sizeof(modeStr), mode); + if (ImGui::BeginCombo("Video Mode", modeStr)) { + auto modes = source.EnumerateVideoModes(); + for (auto&& amode : modes) { + bool selected = (amode == mode); + VideoModeToString(modeStr, sizeof(modeStr), amode); + if (ImGui::Selectable(modeStr, selected)) { + model->SetVideoMode(amode); + rv = true; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + } + return rv; +} + +static void EditProperties(cs::VideoSource& source) { + if (ImGui::CollapsingHeader("Properties")) { + for (auto&& prop : source.EnumerateProperties()) { + std::string name = prop.GetName(); + if (!wpi::starts_with(name, "raw_")) { + EditProperty(name, prop); + } + } + } + + if (ImGui::CollapsingHeader("Raw Properties")) { + for (auto&& prop : source.EnumerateProperties()) { + std::string name = prop.GetName(); + if (wpi::starts_with(name, "raw_")) { + EditProperty(name, prop); + } + } + } +} + +void glass::DisplayCameraSettings(CameraModel* model) { + auto& storage = GetStorage(); + + bool& showFpsDataRate = storage.GetBool("showFpsDataRate", true); + ImGui::Checkbox("Show FPS and data rate", &showFpsDataRate); + + bool& streamEnabled = storage.GetBool("streamEnabled", true); + ImGui::Checkbox("Stream enabled", &streamEnabled); + + EditMode(model); + EditProperties(model->GetSource()); +} + +void glass::DisplayCamera(CameraModel* model, const ImVec2& contentSize, + bool showFpsDataRate) { + ImVec2 size = contentSize; + if (showFpsDataRate) { + size.y -= ImGui::GetStyle().ItemSpacing.y; + } + + // render (best fit) + auto& tex = model->GetTexture(); + if (tex && tex.GetWidth() != 0 && tex.GetHeight() != 0) { + auto drawList = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetWindowPos() + ImGui::GetCursorPos(); + ImVec2 imageMin = pos; + ImVec2 imageMax = pos + size; + wpi::gui::MaxFit(&imageMin, &imageMax, tex.GetWidth(), tex.GetHeight()); + drawList->AddImage(tex, imageMin, imageMax); + } + + // fill the space + ImGui::Dummy(size); + if (ImGui::BeginDragDropTarget()) { + if (auto payload = ImGui::AcceptDragDropPayload("Camera")) { + glass::GetStorage().GetString("camera").assign( + static_cast(payload->Data), payload->DataSize); + } + ImGui::EndDragDropTarget(); + } + + if (showFpsDataRate) { + ImGui::Text("%2.1f FPS", model->GetActualFPS()); + + char buf[64]; + wpi::format_to_n_c_str(buf, sizeof(buf), "{:.1f} Mbps", + model->GetActualDataRate() * 8 / 1000000); + ImGui::SameLine(size.x - ImGui::CalcTextSize(buf).x - + ImGui::GetStyle().ItemSpacing.x); + ImGui::TextUnformatted(buf); + } +} + +void glass::DisplayCameraWindow(CameraModel* model) { + auto& storage = GetStorage(); + bool showFpsDataRate = storage.GetBool("showFpsDataRate", true); + + ImVec2 size = + ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + if (showFpsDataRate) { + size -= ImVec2(0, ImGui::GetTextLineHeightWithSpacing()); + } + DisplayCamera(model, size, showFpsDataRate); +} + +void CameraView::Display() { + if (ImGui::BeginPopupContextItem()) { + DisplayCameraSettings(m_model); + ImGui::EndPopup(); + } + auto& storage = GetStorage(); + bool showFpsDataRate = storage.GetBool("showFpsDataRate", true); + + ImVec2 size = + ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + if (showFpsDataRate) { + size -= ImVec2(0, ImGui::GetTextLineHeightWithSpacing()); + } + DisplayCamera(m_model, size, showFpsDataRate); +} + +void CameraView::Hidden() {} + +void glass::DisplayCameraModelTable(Storage& root, int kinds) { + if (!ImGui::BeginTable("cameras", 7, + ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingFixedFit)) { + return; + } + + ImGui::TableSetupColumn("Id"); + ImGui::TableSetupColumn("Mode"); + ImGui::TableSetupColumn("Enabled"); + ImGui::TableSetupColumn("Active"); + ImGui::TableSetupColumn("FPS", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetFontSize() * 5); + ImGui::TableSetupColumn("Data Rate", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetFontSize() * 5); + ImGui::TableSetupColumn("Actions"); + ImGui::TableHeadersRow(); + + std::string toDelete; + for (auto&& kv : root.GetChildren()) { + CameraModel* model = kv.value().GetData(); + if (!model) { + continue; + } + if ((model->GetKind() & kinds) == 0) { + continue; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Selectable(model->GetId().c_str()); + if (ImGui::BeginDragDropSource()) { + ImGui::SetDragDropPayload("Camera", model->GetId().data(), + model->GetId().size()); + ImGui::Text("Camera: %s", model->GetId().c_str()); + ImGui::EndDragDropSource(); + } + + ImGui::TableNextColumn(); + char buf[64]; + VideoModeToString(buf, sizeof(buf), model->GetVideoMode()); + ImGui::TextUnformatted(buf); + ImGui::SameLine(); + if (ImGui::SmallButton(">")) { + ImGui::OpenPopup("mode"); + } + if (ImGui::BeginPopup("mode")) { + if (EditMode(model)) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(model->Exists() ? "Yes" : "No"); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(model->GetSource().IsEnabled() ? "Yes" : "No"); + + ImGui::TableNextColumn(); + ImGui::Text("%2.1f FPS", model->GetActualFPS()); + + ImGui::TableNextColumn(); + wpi::format_to_n_c_str(buf, sizeof(buf), "{:.1f} Mbps", + model->GetActualDataRate() * 8 / 1000000); + ImGui::TextUnformatted(buf); + + ImGui::TableNextColumn(); + if (ImGui::SmallButton(model->GetSource().IsEnabled() ? "Stop" : "Start")) { + if (model->Exists()) { + model->Stop(); + } else { + model->Start(); + } + } + ImGui::SameLine(); + if (ImGui::SmallButton("Delete")) { + toDelete = kv.key(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Properties")) { + ImGui::OpenPopup("properties"); + } + if (ImGui::BeginPopup("properties")) { + EditProperties(model->GetSource()); + ImGui::EndPopup(); + } + } + if (!toDelete.empty()) { + root.Erase(toDelete); + } + + ImGui::EndTable(); +} diff --git a/glass/src/libcs/native/cpp/CameraProviderBase.cpp b/glass/src/libcs/native/cpp/CameraProviderBase.cpp new file mode 100644 index 00000000000..dfb49977ee6 --- /dev/null +++ b/glass/src/libcs/native/cpp/CameraProviderBase.cpp @@ -0,0 +1,91 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "glass/camera/CameraProviderBase.h" + +#include + +#include + +#include "glass/camera/Camera.h" +#include "glass/Storage.h" + +using namespace glass; + +CameraProviderBase::CameraProviderBase(Storage& storage) + : WindowManager{storage.GetChild("windows")}, + m_modelStorage{storage.GetChildArray("cameras")} { + for (auto&& v : m_modelStorage) { + m_models.emplace_back( + std::make_unique(*v, v->ReadString("id"))); + } +} + +CameraProviderBase::~CameraProviderBase() = default; + +void CameraProviderBase::GlobalInit() { + WindowManager::GlobalInit(); + wpi::gui::AddEarlyExecute([this] { + Update(); + UpdateModels(); + }); +} + +void CameraProviderBase::MenuItem(const char* label, SourceInfo* info) { + bool visible = info->window && info->window->IsVisible(); + bool wasVisible = visible; + ImGui::MenuItem(label, nullptr, &visible); + if (!wasVisible && visible) { + Show(info, info->window); + } else if (wasVisible && !visible && info->window) { + info->window->SetVisible(false); + } +} + +void CameraProviderBase::Show(SourceInfo* info, Window* window) { + // if there's already a window, just show it + if (info->window) { + info->window->SetVisible(true); + return; + } + + // get or create model + if (!info->camera) { + InitCamera(info); + } + if (!info->camera) { + return; + } + + // the window might exist and we're just not associated to it yet + if (!window) { + window = GetOrAddWindow(info->name, true); + } + if (!window) { + return; + } + info->window = window; + + // create view + auto view = std::make_unique(info->camera); + if (!view) { + return; + } + window->SetView(std::move(view)); + + info->window->SetVisible(true); +} + +CameraModel* CameraProviderBase::CreateModel(std::string_view id) { + m_modelStorage.emplace_back(std::make_unique()); + m_models.emplace_back( + std::make_unique(*m_modelStorage.back(), id)); + return m_models.back().get(); +} + +void CameraProviderBase::UpdateModels() { + for (auto&& model : m_models) { + model->Update(); + } +} diff --git a/glass/src/libcs/native/cpp/NTCameraProvider.cpp b/glass/src/libcs/native/cpp/NTCameraProvider.cpp new file mode 100644 index 00000000000..c1e73ce5212 --- /dev/null +++ b/glass/src/libcs/native/cpp/NTCameraProvider.cpp @@ -0,0 +1,131 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "glass/camera/NTCameraProvider.h" + +#include + +#include +#include +#include +#include + +#include "cscore_oo.h" +#include "glass/camera/Camera.h" +#include "ntcore_c.h" + +using namespace glass; + +NTCameraProvider::NTCameraProvider(Storage& storage) + : NTCameraProvider{storage, nt::NetworkTableInstance::GetDefault()} {} + +NTCameraProvider::NTCameraProvider(Storage& storage, + nt::NetworkTableInstance inst) + : CameraProviderBase{storage}, m_inst{inst}, m_poller{inst} { + m_listener = + m_poller.AddListener({{"/CameraPublisher/"}}, + nt::EventFlags::kTopic | nt::EventFlags::kValueAll); +} + +void NTCameraProvider::DisplayMenu() { + bool any = false; + for (auto&& info : m_sourceInfo) { + if (info->connected) { + MenuItem(info->name.c_str(), info.get()); + any = true; + } + } + if (!any) { + ImGui::MenuItem("(None)", nullptr, false, false); + } +} + +void NTCameraProvider::Update() { + for (nt::Event& event : m_poller.ReadQueue()) { + if (nt::TopicInfo* topicInfo = event.GetTopicInfo()) { + wpi::SmallVector parts; + wpi::split(topicInfo->name, parts, '/', -1, false); + if (parts.size() < 3) { + continue; + } + + // find/create NTSourceInfo by name + std::string_view sourceName = parts[1]; + auto it = std::find_if( + m_sourceInfo.begin(), m_sourceInfo.end(), + [&](const auto& info) { return info->name == sourceName; }); + NTSourceInfo* info; + if (it == m_sourceInfo.end()) { + info = + m_sourceInfo.emplace_back(std::make_unique()).get(); + info->name = sourceName; + // keep sorted by name + std::sort( + m_sourceInfo.begin(), m_sourceInfo.end(), + [](const auto& a, const auto& b) { return a->name < b->name; }); + } else { + info = it->get(); + } + + // update NTSourceInfo + std::string_view key = parts[2]; + if ((event.flags & NT_EVENT_PUBLISH) != 0) { + if (key == "description" && topicInfo->type == NT_STRING) { + info->descTopic = topicInfo->topic; + m_topicMap.try_emplace(topicInfo->topic, info); + } else if (key == "connected" && topicInfo->type == NT_BOOLEAN) { + info->connTopic = topicInfo->topic; + m_topicMap.try_emplace(topicInfo->topic, info); + } else if (key == "streams" && topicInfo->type == NT_STRING_ARRAY) { + info->streamsTopic = topicInfo->topic; + m_topicMap.try_emplace(topicInfo->topic, info); + } + } else if ((event.flags & NT_EVENT_UNPUBLISH) != 0) { + if (key == "description") { + m_topicMap.erase(info->connTopic); + info->descTopic = 0; + } else if (key == "connected") { + m_topicMap.erase(info->connTopic); + info->connTopic = 0; + info->connected = false; + } else if (key == "streams") { + m_topicMap.erase(info->connTopic); + info->streamsTopic = 0; + } + } + } + + if (nt::ValueEventData* valueData = event.GetValueEventData()) { + auto it = m_topicMap.find(valueData->topic); + if (it != m_topicMap.end()) { + NTSourceInfo* info = it->getSecond(); + if (valueData->topic == info->descTopic && + valueData->value.IsString()) { + info->description = valueData->value.GetString(); + } else if (valueData->topic == info->connTopic && + valueData->value.IsBoolean()) { + info->connected = valueData->value.GetBoolean(); + } else if (valueData->topic == info->streamsTopic && + valueData->value.IsStringArray()) { + info->streams.clear(); + for (auto&& stream : valueData->value.GetStringArray()) { + if (wpi::starts_with(stream, "mjpg:")) { + info->streams.emplace_back(wpi::drop_front(stream, 5)); + } + } + if (info->camera) { + info->camera->SetUrls(info->streams); + } + } + } + } + } +} + +void NTCameraProvider::InitCamera(SourceInfo* info) { + info->camera = CreateModel(info->name); + info->camera->SetSource(cs::HttpCamera( + fmt::format("glass::http::{}", info->name), + static_cast(info)->streams, cs::HttpCamera::kUnknown)); +} diff --git a/glass/src/libcs/native/cpp/UsbCameraList.cpp b/glass/src/libcs/native/cpp/UsbCameraList.cpp new file mode 100644 index 00000000000..b8ecf0b0fb3 --- /dev/null +++ b/glass/src/libcs/native/cpp/UsbCameraList.cpp @@ -0,0 +1,82 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "glass/camera/UsbCameraList.h" + +#include +#include +#include + +using namespace glass; + +struct UsbCameraList::UsbInfoThread : public wpi::SafeThread { + void Main() override; + + std::vector m_usbInfo; +}; + +void UsbCameraList::UsbInfoThread::Main() { + std::unique_lock lock{m_mutex}; + while (m_active) { + // enumerate without lock held + lock.unlock(); + CS_Status status = 0; + auto usbInfo = cs::EnumerateUsbCameras(&status); + + lock.lock(); + m_usbInfo = std::move(usbInfo); + + // wait for change + m_cond.wait(lock); + } +} + +UsbCameraList::UsbCameraList() : m_poller{cs::CreateListenerPoller()} { + CS_Status status = 0; + cs::AddPolledListener(m_poller, CS_USB_CAMERAS_CHANGED, false, &status); + + m_usbInfoThread.Start(); +} + +UsbCameraList::~UsbCameraList() { + cs::DestroyListenerPoller(m_poller); +} + +std::string UsbCameraList::DisplayMenu() { + std::string rv; + bool any = false; + if (auto thr = m_usbInfoThread.GetThread()) { + // TODO: hide cameras that already exist as sources + for (auto&& info : thr->m_usbInfo) { + ImGui::SeparatorText(fmt::format("{} ({:04x}:{:04x})", info.name, + info.vendorId, info.productId) + .c_str()); + if (ImGui::MenuItem(info.path.c_str())) { + rv = info.path; + } + for (auto&& path : info.otherPaths) { + if (ImGui::MenuItem(path.c_str())) { + rv = info.path; + } + } + } + any = !thr->m_usbInfo.empty(); + } + if (!any) { + ImGui::MenuItem("(None)", nullptr, false, false); + } + return rv; +} + +void UsbCameraList::Update() { + bool timedOut = false; + for (auto&& event : cs::PollListener(m_poller, 0, &timedOut)) { + if (event.kind == cs::RawEvent::kUsbCamerasChanged) { + // enumerate on separate thread + if (auto thr = m_usbInfoThread.GetThread()) { + thr->m_cond.notify_all(); + } + } + } +} diff --git a/glass/src/libcs/native/include/glass/camera/CSCameraProvider.h b/glass/src/libcs/native/include/glass/camera/CSCameraProvider.h new file mode 100644 index 00000000000..69104be530b --- /dev/null +++ b/glass/src/libcs/native/include/glass/camera/CSCameraProvider.h @@ -0,0 +1,44 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include + +#include "glass/camera/CameraProviderBase.h" + +namespace glass { + +class Storage; + +/** + * Provider for CameraServer models and views that pulls directly from + * cscore sources. + */ +class CSCameraProvider : public CameraProviderBase { + public: + explicit CSCameraProvider(Storage& storage); + ~CSCameraProvider() override; + + /** + * Displays menu contents. + */ + void DisplayMenu() override; + + private: + void Update() override; + + struct CSSourceInfo : public SourceInfo { + CS_Source handle; + CS_SourceKind kind; + }; + + std::vector m_sourceInfo; + + CS_ListenerPoller m_poller; +}; + +} // namespace glass diff --git a/glass/src/libcs/native/include/glass/camera/Camera.h b/glass/src/libcs/native/include/glass/camera/Camera.h new file mode 100644 index 00000000000..363c68312a8 --- /dev/null +++ b/glass/src/libcs/native/include/glass/camera/Camera.h @@ -0,0 +1,148 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "cscore_c.h" + +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include +#include +#include +#include +#include + +#include "glass/Model.h" +#include "glass/Storage.h" +#include "glass/View.h" + +namespace glass { + +class Storage; + +class CameraModel : public Model { + public: + explicit CameraModel(Storage& storage, std::string_view id); + ~CameraModel() override; + + CameraModel(const CameraModel&) = delete; + CameraModel& operator=(const CameraModel&) = delete; + + void Update() override; + bool Exists() override; + bool IsReadOnly() override { return false; } + + void Start(); + void Stop(); + + const std::string& GetId() { return m_id; } + + wpi::gui::Texture& GetTexture() { return m_tex; } + + /** + * Set the source handle. + */ + void SetSource(cs::VideoSource source); + + /** + * Get the source handle. + */ + cs::VideoSource& GetSource() { return m_source; } + + /** + * Get the actual FPS. + * + * @return Actual FPS averaged over a 1 second period. + */ + double GetActualFPS() { return m_source.GetActualFPS(); } + + /** + * Get the data rate (in bytes per second). + * + *

SetTelemetryPeriod() must be called for this to be valid. + * + * @return Data rate averaged over a 1 second period. + */ + double GetActualDataRate() { return m_source.GetActualDataRate(); } + + /** + * For a HTTP camera, change the URLs used to connect to the camera. + */ + void SetUrls(std::span urls); + + cs::VideoSource::Kind GetKind() const { return m_source.GetKind(); } + + cs::VideoMode GetVideoMode() const { return m_source.GetVideoMode(); } + void SetVideoMode(const cs::VideoMode& mode); + void ResetVideoMode(); + + private: + cv::Mat* AllocMat(); + + std::string m_id; + cs::VideoSource m_source; + + std::string& m_cameraType; + std::string& m_usbPath; + std::vector& m_urls; + std::string& m_pixelFormatStr; + int& m_width; + int& m_height; + int& m_fps; + + cs::VideoMode m_videoMode; + std::vector> m_properties; + + std::atomic m_latestFrame{nullptr}; + std::vector m_sharedFreeList; + wpi::spinlock m_sharedFreeListMutex; + std::vector m_sourceFreeList; + std::atomic m_stopCamera{false}; + std::thread m_frameThread; + + wpi::gui::Texture m_tex; +}; + +void DisplayCameraSettings(CameraModel* model); + +void DisplayCamera(CameraModel* model, const ImVec2& contentSize, + bool showFpsDataRate = true); + +void DisplayCameraWindow(CameraModel* model); + +inline CameraModel* GetOrNewCameraModel(Storage& root, std::string_view id) { + Storage& storage = root.GetChild(id); + return &storage.GetOrNewData(storage, id); +} + +inline CameraModel* GetCameraModel(Storage& root, std::string_view id) { + return root.GetChild(id).GetData(); +} + +void DisplayCameraModelTable(Storage& root, + int kinds = CS_SOURCE_USB | CS_SOURCE_HTTP | + CS_SOURCE_CV | CS_SOURCE_RAW); + +class CameraView : public View { + public: + explicit CameraView(CameraModel* model) : m_model{model} {} + + void Display() override; + void Hidden() override; + + CameraModel* GetModel() { return m_model; } + + private: + CameraModel* m_model; +}; + +} // namespace glass diff --git a/glass/src/libcs/native/include/glass/camera/CameraProviderBase.h b/glass/src/libcs/native/include/glass/camera/CameraProviderBase.h new file mode 100644 index 00000000000..43cf23b8d20 --- /dev/null +++ b/glass/src/libcs/native/include/glass/camera/CameraProviderBase.h @@ -0,0 +1,59 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include + +#include "glass/Storage.h" +#include "glass/WindowManager.h" + +namespace glass { + +class CameraModel; + +/** + * Base class for provider for CameraServer models and views. + */ +class CameraProviderBase : protected WindowManager { + public: + explicit CameraProviderBase(Storage& storage); + ~CameraProviderBase() override; + + /** + * Perform global initialization. This should be called prior to + * wpi::gui::Initialize(). + */ + void GlobalInit() override; + + CameraModel* GetModel(std::string_view name); + + protected: + struct SourceInfo { + std::string name; + std::string description; + bool connected = false; + CameraModel* camera = nullptr; + Window* window = nullptr; + }; + + virtual void Update() = 0; + virtual void InitCamera(SourceInfo* info) = 0; + + void MenuItem(const char* label, SourceInfo* info); + void Show(SourceInfo* info, Window* window); + + CameraModel* CreateModel(std::string_view id); + + std::vector>& m_modelStorage; + std::vector> m_models; + + private: + void UpdateModels(); +}; + +} // namespace glass diff --git a/glass/src/libcs/native/include/glass/camera/NTCameraProvider.h b/glass/src/libcs/native/include/glass/camera/NTCameraProvider.h new file mode 100644 index 00000000000..47b34dec253 --- /dev/null +++ b/glass/src/libcs/native/include/glass/camera/NTCameraProvider.h @@ -0,0 +1,54 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "glass/camera/CameraProviderBase.h" + +namespace glass { + +class Storage; + +/** + * Provider for CameraServer models and views for NetworkTables-published + * cameras. + */ +class NTCameraProvider : public CameraProviderBase { + public: + explicit NTCameraProvider(Storage& storage); + NTCameraProvider(Storage& storage, nt::NetworkTableInstance inst); + + /** + * Displays menu contents. + */ + void DisplayMenu() override; + + private: + struct NTSourceInfo : public SourceInfo { + NT_Topic descTopic{0}; + NT_Topic connTopic{0}; + NT_Topic streamsTopic{0}; + std::vector streams; + }; + + void Update() override; + void InitCamera(SourceInfo* info) override; + + std::vector> m_sourceInfo; + wpi::DenseMap m_topicMap; + + nt::NetworkTableInstance m_inst; + nt::NetworkTableListenerPoller m_poller; + NT_Listener m_listener{0}; +}; + +} // namespace glass diff --git a/glass/src/libcs/native/include/glass/camera/UsbCameraList.h b/glass/src/libcs/native/include/glass/camera/UsbCameraList.h new file mode 100644 index 00000000000..14a857502e3 --- /dev/null +++ b/glass/src/libcs/native/include/glass/camera/UsbCameraList.h @@ -0,0 +1,43 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include + +namespace glass { + +class Storage; + +/** + * Provider for CameraServer models and views for USB cameras. + */ +class UsbCameraList { + public: + explicit UsbCameraList(); + ~UsbCameraList(); + + UsbCameraList(const UsbCameraList&) = delete; + UsbCameraList& operator=(const UsbCameraList&) = delete; + + void Update(); + + /** + * Displays menu contents. + * + * @return camera path to add, if not empty + */ + std::string DisplayMenu(); + + private: + struct UsbInfoThread; + wpi::SafeThreadOwner m_usbInfoThread; + + CS_ListenerPoller m_poller; +}; + +} // namespace glass