From 46f767f232164bf07fa11abb06c1814b941c498f Mon Sep 17 00:00:00 2001 From: Hayodea Hekol Date: Sat, 13 Jun 2026 12:02:04 -0400 Subject: [PATCH] lcameraDev: Add session mgr lib for libcamera device binding --- cmake/CPackConfig.cmake | 8 +- commonLibs/CMakeLists.txt | 1 + commonLibs/lcameraDev/CMakeLists.txt | 93 +++++++ commonLibs/lcameraDev/cameraIdentity.cpp | 66 +++++ commonLibs/lcameraDev/cameraIdentity.h | 28 ++ commonLibs/lcameraDev/cameraManagerState.cpp | 251 ++++++++++++++++++ commonLibs/lcameraDev/cameraManagerState.h | 55 ++++ commonLibs/lcameraDev/cameraSession.cpp | 29 ++ commonLibs/lcameraDev/cameraSession.h | 46 ++++ commonLibs/lcameraDev/lcameraDev.cpp | 61 +++++ commonLibs/lcameraDev/lcameraDev.h | 59 ++++ commonLibs/lcameraDev/selectorParse.cpp | 127 +++++++++ commonLibs/lcameraDev/selectorParse.h | 31 +++ commonLibs/lcameraDev/selectorResolve.cpp | 171 ++++++++++++ commonLibs/lcameraDev/selectorResolve.h | 20 ++ .../tools/lcameraDevListCameras.cpp | 71 +++++ .../lcameraDev/tools/lcameraDevProbe.cpp | 78 ++++++ commonLibs/lcameraDev/tools/probeRunner.cpp | 88 ++++++ commonLibs/lcameraDev/tools/probeRunner.h | 16 ++ docs/lcamera-dev-lib.md | 127 ++++----- include/config.h.in | 1 + 21 files changed, 1363 insertions(+), 64 deletions(-) create mode 100644 commonLibs/lcameraDev/CMakeLists.txt create mode 100644 commonLibs/lcameraDev/cameraIdentity.cpp create mode 100644 commonLibs/lcameraDev/cameraIdentity.h create mode 100644 commonLibs/lcameraDev/cameraManagerState.cpp create mode 100644 commonLibs/lcameraDev/cameraManagerState.h create mode 100644 commonLibs/lcameraDev/cameraSession.cpp create mode 100644 commonLibs/lcameraDev/cameraSession.h create mode 100644 commonLibs/lcameraDev/lcameraDev.cpp create mode 100644 commonLibs/lcameraDev/lcameraDev.h create mode 100644 commonLibs/lcameraDev/selectorParse.cpp create mode 100644 commonLibs/lcameraDev/selectorParse.h create mode 100644 commonLibs/lcameraDev/selectorResolve.cpp create mode 100644 commonLibs/lcameraDev/selectorResolve.h create mode 100644 commonLibs/lcameraDev/tools/lcameraDevListCameras.cpp create mode 100644 commonLibs/lcameraDev/tools/lcameraDevProbe.cpp create mode 100644 commonLibs/lcameraDev/tools/probeRunner.cpp create mode 100644 commonLibs/lcameraDev/tools/probeRunner.h diff --git a/cmake/CPackConfig.cmake b/cmake/CPackConfig.cmake index e3eb6bc..5153126 100644 --- a/cmake/CPackConfig.cmake +++ b/cmake/CPackConfig.cmake @@ -60,7 +60,7 @@ set(CPACK_DEBIAN_PACKAGE_DISTRIBUTION "ubuntu") # Build dependencies (from builddeps file) # These are needed to build the package from source set(CPACK_DEBIAN_PACKAGE_BUILD_DEPENDS - "build-essential, cmake (>= 3.16), libboost-all-dev, flex, bison, ocl-icd-opencl-dev, liburing-dev") + "build-essential, cmake (>= 3.16), libboost-all-dev, flex, bison, ocl-icd-opencl-dev, liburing-dev, libcamera-dev") # Runtime dependencies. # Let dpkg-shlibdeps derive the actual ELF dependencies from the built @@ -69,14 +69,16 @@ set(CPACK_DEBIAN_PACKAGE_BUILD_DEPENDS # tree the generated binaries are not currently linked against Boost DSOs. set(CPACK_DEBIAN_PACKAGE_DEPENDS "") set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libxcb1, libx11-6") -set(CPACK_DEBIAN_PACKAGE_SUGGESTS "livox-sdk") +set(CPACK_DEBIAN_PACKAGE_SUGGESTS + "livox-sdk, libcamera0.2") # RPM package specific settings set(CPACK_RPM_PACKAGE_LICENSE "Proprietary") set(CPACK_RPM_PACKAGE_GROUP "Applications/Engineering") set(CPACK_RPM_PACKAGE_URL "https://github.com/salmanoff/salmanoff") set(CPACK_RPM_PACKAGE_REQUIRES "boost-system >= 1.72.0, boost-log >= 1.72.0, glibc, libstdc++, ocl-icd, liburing") -set(CPACK_RPM_PACKAGE_SUGGESTS "xcb, libX11, livox-sdk") +set(CPACK_RPM_PACKAGE_SUGGESTS + "xcb, libX11, livox-sdk, libcamera") # Package file naming using Debian's architecture naming when available. set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}") diff --git a/commonLibs/CMakeLists.txt b/commonLibs/CMakeLists.txt index 968d0d9..1bcb907 100644 --- a/commonLibs/CMakeLists.txt +++ b/commonLibs/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(xcbXorg) add_subdirectory(livoxProto1) +add_subdirectory(lcameraDev) add_subdirectory(attachmentSupport) diff --git a/commonLibs/lcameraDev/CMakeLists.txt b/commonLibs/lcameraDev/CMakeLists.txt new file mode 100644 index 0000000..37325e2 --- /dev/null +++ b/commonLibs/lcameraDev/CMakeLists.txt @@ -0,0 +1,93 @@ +option(ENABLE_LIB_lcameraDev "Enable libcamera device provider backend lib" OFF) + +if(ENABLE_LIB_lcameraDev) + pkg_check_modules(LIBCAMERA libcamera) + if(NOT LIBCAMERA_FOUND) + message(FATAL_ERROR + "libcamera not found. Install libcamera-dev (and runtime libcamera0.2 " + "+ libcamera-ipa), then reconfigure with -DENABLE_LIB_lcameraDev=ON.") + endif() + + add_compile_definitions(CONFIG_LIB_LCAMERADEV_ENABLED) + + add_library(lcameraDev SHARED + lcameraDev.cpp + cameraIdentity.cpp + selectorParse.cpp + selectorResolve.cpp + cameraManagerState.cpp + cameraSession.cpp + ) + + set_target_properties(lcameraDev PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + + target_compile_definitions(lcameraDev PRIVATE CONFIG_LIB_LCAMERADEV_ENABLED) + + target_include_directories(lcameraDev PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${LIBCAMERA_INCLUDE_DIRS} + ) + + target_link_libraries(lcameraDev PUBLIC + Boost::system + Boost::log + spinscale + ${LIBCAMERA_LIBRARIES} + ) + + target_link_directories(lcameraDev PUBLIC + ${LIBCAMERA_LIBRARY_DIRS} + ) + + add_custom_command(TARGET lcameraDev POST_BUILD + COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$" + -P ${CMAKE_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake + COMMENT "Verifying Boost dynamic dependencies for lcameraDev" + ) + + install(TARGETS lcameraDev + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP + ) + + option(ENABLE_LCAMERADEV_TOOLS "Build lcameraDev probe/list tools" ON) + if(ENABLE_LCAMERADEV_TOOLS) + add_executable(lcameraDev_list_cameras + tools/lcameraDevListCameras.cpp + tools/probeRunner.cpp + ) + target_include_directories(lcameraDev_list_cameras PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tools + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ) + target_link_libraries(lcameraDev_list_cameras PRIVATE + lcameraDev + spinscale + Boost::system + Boost::log + ) + + add_executable(lcameraDev_probe + tools/lcameraDevProbe.cpp + tools/probeRunner.cpp + ) + target_include_directories(lcameraDev_probe PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tools + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ) + target_link_libraries(lcameraDev_probe PRIVATE + lcameraDev + spinscale + Boost::system + Boost::log + ) + endif() +endif() diff --git a/commonLibs/lcameraDev/cameraIdentity.cpp b/commonLibs/lcameraDev/cameraIdentity.cpp new file mode 100644 index 0000000..1bf489b --- /dev/null +++ b/commonLibs/lcameraDev/cameraIdentity.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +std::string locationPropertyToLabel(int32_t locationValue) +{ + using namespace libcamera::properties; + + if (locationValue == CameraLocationFront) { + return "front"; + } + if (locationValue == CameraLocationBack) { + return "back"; + } + if (locationValue == CameraLocationExternal) { + return "external"; + } + + return ""; +} + +CameraIdentityRecord buildIdentityRecord( + const std::shared_ptr& camera) +{ + CameraIdentityRecord record; + record.id = camera->id(); + + const libcamera::ControlList& props = camera->properties(); + + const std::optional model = + props.get(libcamera::properties::Model); + if (model) { + record.model = *model; + } + + const std::optional location = + props.get(libcamera::properties::Location); + if (location) + { + record.locationLabel = locationPropertyToLabel(*location); + } + + return record; +} + +std::vector buildIdentityRecords( + const std::vector>& cameras) +{ + std::vector records; + records.reserve(cameras.size()); + + for (const auto& camera : cameras) + { + if (!camera) { continue; } + + records.push_back(buildIdentityRecord(camera)); + } + + return records; +} + +} // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/cameraIdentity.h b/commonLibs/lcameraDev/cameraIdentity.h new file mode 100644 index 0000000..252e0bc --- /dev/null +++ b/commonLibs/lcameraDev/cameraIdentity.h @@ -0,0 +1,28 @@ +#ifndef LCAMERA_DEV_CAMERA_IDENTITY_H +#define LCAMERA_DEV_CAMERA_IDENTITY_H + +#include +#include +#include +#include + +namespace lcamera_dev { + +struct CameraIdentityRecord +{ + std::string id; + std::string model; + std::string locationLabel; +}; + +CameraIdentityRecord buildIdentityRecord( + const std::shared_ptr& camera); + +std::vector buildIdentityRecords( + const std::vector>& cameras); + +std::string locationPropertyToLabel(int32_t locationValue); + +} // namespace lcamera_dev + +#endif // LCAMERA_DEV_CAMERA_IDENTITY_H diff --git a/commonLibs/lcameraDev/cameraManagerState.cpp b/commonLibs/lcameraDev/cameraManagerState.cpp new file mode 100644 index 0000000..dae47f3 --- /dev/null +++ b/commonLibs/lcameraDev/cameraManagerState.cpp @@ -0,0 +1,251 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +namespace { + +static LcameraDevState lcameraDevState; + +void startCameraManager(CameraManagerResources& resources) +{ + resources.cameraManager = std::make_unique(); + + if (resources.cameraManager->start()) + { + resources.cameraManager.reset(); + throw std::runtime_error( + "lcameraDev: failed to start libcamera CameraManager"); + } +} + +void stopCameraManager(CameraManagerResources& resources) +{ + if (!resources.cameraManager) { + return; + } + + resources.cameraManager->stop(); + resources.cameraManager.reset(); +} + +std::shared_ptr findCameraById( + const std::vector>& cameras, + const std::string& cameraId) +{ + for (const std::shared_ptr& camera : cameras) + { + if (camera && camera->id() == cameraId) { + return camera; + } + } + + return nullptr; +} + +} // namespace + +LcameraDevState& getLcameraDevState() +{ + return lcameraDevState; +} + +std::vector> listLibcameraCameras() +{ + LcameraDevState& state = getLcameraDevState(); + if (!state.isInitialized || !state.managerState.rsrc.cameraManager) { + return {}; + } + + return state.managerState.rsrc.cameraManager->cameras(); +} + +void lcameraDevMain( + const std::shared_ptr& componentThread) +{ + LcameraDevState& state = getLcameraDevState(); + if (state.isInitialized) { + return; + } + + if (!componentThread) + { + throw std::runtime_error( + "lcameraDev_main: componentThread must be non-null"); + } + + startCameraManager(state.managerState.rsrc); + state.componentThread = componentThread; + state.isInitialized = true; +} + +void lcameraDevExit() +{ + LcameraDevState& state = getLcameraDevState(); + if (!state.isInitialized) { + return; + } + + CameraManagerResources& resources = state.managerState.rsrc; + for (auto& entry : resources.sessionsByCameraId) + { + std::shared_ptr session = entry.second; + if (!session || !session->s.rsrc.camera) { + continue; + } + + session->s.rsrc.camera->release(); + } + + resources.sessionsByCameraId.clear(); + stopCameraManager(resources); + + state.componentThread.reset(); + state.isInitialized = false; +} + +sscl::co::ViralNonPostingInvoker +getOrCreateDeviceSessionCReq(const std::string& deviceSelector) +{ + if (deviceSelector.empty()) + { + throw std::runtime_error( + "lcameraDev_getOrCreateDeviceCReq: deviceSelector is empty"); + } + + LcameraDevState& state = getLcameraDevState(); + + sscl::co::CoQutex::ReleaseHandle managerGuard = + co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); + + const std::vector criteria = + parseDeviceSelector(deviceSelector); + const std::vector> cameras = + state.managerState.rsrc.cameraManager->cameras(); + const std::vector identityRecords = + buildIdentityRecords(cameras); + + const CameraIdentityRecord resolvedRecord = + resolveSelectorAgainstRecords(criteria, identityRecords); + const std::string& resolvedCameraId = resolvedRecord.id; + + auto sessionIt = + state.managerState.rsrc.sessionsByCameraId.find(resolvedCameraId); + + if (sessionIt != state.managerState.rsrc.sessionsByCameraId.end()) + { + std::shared_ptr session = sessionIt->second; + sscl::co::CoQutex::ReleaseHandle sessionGuard = + co_await session->s.lock.getAcquireInvocationAndSuspensionPolicy(); + + session->incrementRefcount(); + + LcameraDevGetOrCreateResult result; + result.deviceSession = session; + result.resolvedIdentity = session->getIdentityRecord(); + co_return result; + } + + std::shared_ptr camera = + findCameraById(cameras, resolvedCameraId); + if (!camera) + { + throw std::runtime_error( + "lcameraDev: resolved camera is no longer available: " + + resolvedCameraId); + } + + if (camera->acquire()) + { + throw std::runtime_error( + "lcameraDev: failed to acquire camera: " + resolvedCameraId); + } + + std::shared_ptr session = + std::make_shared(resolvedRecord, camera); + + session->incrementRefcount(); + state.managerState.rsrc.sessionsByCameraId.emplace( + resolvedCameraId, session); + + LcameraDevGetOrCreateResult result; + result.deviceSession = session; + result.resolvedIdentity = session->getIdentityRecord(); + co_return result; +} + +sscl::co::ViralNonPostingInvoker +releaseDeviceSessionCReq( + const std::shared_ptr& deviceSession) +{ + if (!deviceSession) { co_return; } + + LcameraDevState& state = getLcameraDevState(); + sscl::co::CoQutex::ReleaseHandle managerGuard = + co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); + + const auto sessionIt = std::find_if( + state.managerState.rsrc.sessionsByCameraId.begin(), + state.managerState.rsrc.sessionsByCameraId.end(), + [&deviceSession](const auto& entry) { + return entry.second == deviceSession; + }); + + if (sessionIt == state.managerState.rsrc.sessionsByCameraId.end()) { + co_return; + } + + bool shouldDestroy = false; + { + sscl::co::CoQutex::ReleaseHandle sessionGuard = + co_await deviceSession->s.lock.getAcquireInvocationAndSuspensionPolicy(); + + shouldDestroy = deviceSession->decrementRefcount(); + } + + if (!shouldDestroy) { + co_return; + } + + if (deviceSession->s.rsrc.camera) { + deviceSession->s.rsrc.camera->release(); + } + + state.managerState.rsrc.sessionsByCameraId.erase(sessionIt); + co_return; +} + +sscl::co::ViralNonPostingInvoker> +enumerateCamerasCReq() +{ + LcameraDevState& state = getLcameraDevState(); + sscl::co::CoQutex::ReleaseHandle managerGuard = + co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); + + const std::vector> cameras = + state.managerState.rsrc.cameraManager->cameras(); + const std::vector identityRecords = + buildIdentityRecords(cameras); + + std::vector cameraInfos; + cameraInfos.reserve(identityRecords.size()); + + for (const CameraIdentityRecord& record : identityRecords) + { + cameraInfos.push_back(LcameraDevCameraInfo{ + record.id, + record.model, + record.locationLabel + }); + } + + co_return cameraInfos; +} + +} // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/cameraManagerState.h b/commonLibs/lcameraDev/cameraManagerState.h new file mode 100644 index 0000000..31d44e5 --- /dev/null +++ b/commonLibs/lcameraDev/cameraManagerState.h @@ -0,0 +1,55 @@ +#ifndef LCAMERA_DEV_CAMERA_MANAGER_STATE_H +#define LCAMERA_DEV_CAMERA_MANAGER_STATE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +struct CameraManagerResources +{ + std::unique_ptr cameraManager; + std::map> sessionsByCameraId; +}; + +struct LcameraDevState +{ + LcameraDevState() + : managerState("lcameraDev::CameraManager") + {} + + bool isInitialized = false; + std::shared_ptr componentThread; + sscl::SharedResourceGroup + managerState; +}; + +LcameraDevState& getLcameraDevState(); + +void lcameraDevMain( + const std::shared_ptr& componentThread); +void lcameraDevExit(); + +std::vector> listLibcameraCameras(); + +sscl::co::ViralNonPostingInvoker +getOrCreateDeviceSessionCReq(const std::string& deviceSelector); + +sscl::co::ViralNonPostingInvoker +releaseDeviceSessionCReq( + const std::shared_ptr& deviceSession); + +sscl::co::ViralNonPostingInvoker> +enumerateCamerasCReq(); + +} // namespace lcamera_dev + +#endif // LCAMERA_DEV_CAMERA_MANAGER_STATE_H diff --git a/commonLibs/lcameraDev/cameraSession.cpp b/commonLibs/lcameraDev/cameraSession.cpp new file mode 100644 index 0000000..c10aba7 --- /dev/null +++ b/commonLibs/lcameraDev/cameraSession.cpp @@ -0,0 +1,29 @@ +#include +#include + +namespace lcamera_dev { + +CameraSession::CameraSession( + const CameraIdentityRecord& identity, + const std::shared_ptr& camera) +: s("lcameraDev::CameraSession", CameraSessionResources{identity, camera}) +{} + +void CameraSession::incrementRefcount() +{ + ++s.rsrc.refcount; +} + +bool CameraSession::decrementRefcount() +{ + if (s.rsrc.refcount <= 0) + { + throw std::logic_error( + "lcameraDev: releaseDeviceCReq refcount underflow"); + } + + --s.rsrc.refcount; + return s.rsrc.refcount == 0; +} + +} // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/cameraSession.h b/commonLibs/lcameraDev/cameraSession.h new file mode 100644 index 0000000..f4ea14f --- /dev/null +++ b/commonLibs/lcameraDev/cameraSession.h @@ -0,0 +1,46 @@ +#ifndef LCAMERA_DEV_CAMERA_SESSION_H +#define LCAMERA_DEV_CAMERA_SESSION_H + +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +struct CameraSessionResources +{ + CameraSessionResources( + const CameraIdentityRecord& identity, + const std::shared_ptr& camera) + : identity(identity), camera(camera) + {} + + int refcount = 0; + CameraIdentityRecord identity; + std::shared_ptr camera; +}; + +class CameraSession +{ +public: + CameraSession( + const CameraIdentityRecord& identity, + const std::shared_ptr& camera); + + const CameraIdentityRecord& getIdentityRecord() const + { return s.rsrc.identity; } + + const std::shared_ptr& getCamera() const + { return s.rsrc.camera; } + + void incrementRefcount(); + bool decrementRefcount(); + + sscl::SharedResourceGroup s; +}; + +} // namespace lcamera_dev + +#endif // LCAMERA_DEV_CAMERA_SESSION_H diff --git a/commonLibs/lcameraDev/lcameraDev.cpp b/commonLibs/lcameraDev/lcameraDev.cpp new file mode 100644 index 0000000..65ce030 --- /dev/null +++ b/commonLibs/lcameraDev/lcameraDev.cpp @@ -0,0 +1,61 @@ +#include + +#include +#include +#include + +extern "C" { + +void lcameraDev_main( + const std::shared_ptr& componentThread) +{ + lcamera_dev::lcameraDevMain(componentThread); +} + +void lcameraDev_exit(void) +{ + lcamera_dev::lcameraDevExit(); +} + +sscl::co::ViralNonPostingInvoker +lcameraDev_getOrCreateDeviceCReq(const std::string& deviceSelector) +{ + lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState(); + if (!state.isInitialized) + { + throw std::runtime_error( + "lcameraDev_getOrCreateDeviceCReq: call lcameraDev_main first"); + } + + co_return co_await lcamera_dev::getOrCreateDeviceSessionCReq(deviceSelector); +} + +sscl::co::ViralNonPostingInvoker +lcameraDev_releaseDeviceCReq( + const std::shared_ptr& deviceSession) +{ + lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState(); + if (!state.isInitialized) + { + throw std::runtime_error( + "lcameraDev_releaseDeviceCReq: call lcameraDev_main first"); + } + + co_await lcamera_dev::releaseDeviceSessionCReq(deviceSession); + co_return; +} + +sscl::co::ViralNonPostingInvoker> +lcameraDev_enumerateCamerasCReq(void) +{ + lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState(); + if (!state.isInitialized) + { + throw std::runtime_error( + "lcameraDev_enumerateCamerasCReq: call lcameraDev_main first"); + } + + co_return co_await lcamera_dev::enumerateCamerasCReq(); +} + +} // extern "C" diff --git a/commonLibs/lcameraDev/lcameraDev.h b/commonLibs/lcameraDev/lcameraDev.h new file mode 100644 index 0000000..de2939c --- /dev/null +++ b/commonLibs/lcameraDev/lcameraDev.h @@ -0,0 +1,59 @@ +#ifndef LCAMERA_DEV_H +#define LCAMERA_DEV_H + +#include +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +class CameraSession; + +struct LcameraDevGetOrCreateResult +{ + std::shared_ptr deviceSession; + CameraIdentityRecord resolvedIdentity; +}; + +struct LcameraDevCameraInfo +{ + std::string id; + std::string model; + std::string location; +}; + +} // namespace lcamera_dev + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void lcameraDev_mainFn( + const std::shared_ptr& componentThread); + +typedef void lcameraDev_exitFn(void); + +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector); + +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_releaseDeviceCReqFn( + const std::shared_ptr& deviceSession); + +typedef sscl::co::ViralNonPostingInvoker> + lcameraDev_enumerateCamerasCReqFn(void); + +lcameraDev_mainFn lcameraDev_main; +lcameraDev_exitFn lcameraDev_exit; +lcameraDev_getOrCreateDeviceCReqFn lcameraDev_getOrCreateDeviceCReq; +lcameraDev_releaseDeviceCReqFn lcameraDev_releaseDeviceCReq; +lcameraDev_enumerateCamerasCReqFn lcameraDev_enumerateCamerasCReq; + +#ifdef __cplusplus +} +#endif + +#endif // LCAMERA_DEV_H diff --git a/commonLibs/lcameraDev/selectorParse.cpp b/commonLibs/lcameraDev/selectorParse.cpp new file mode 100644 index 0000000..c9fadfc --- /dev/null +++ b/commonLibs/lcameraDev/selectorParse.cpp @@ -0,0 +1,127 @@ +#include +#include +#include + +namespace lcamera_dev { + +namespace { + +bool startsWith(const std::string& text, const std::string& prefix) +{ + return text.size() >= prefix.size() + && text.compare(0, prefix.size(), prefix) == 0; +} + +SelectorCriterionKind parseCriterionKind(const std::string& prefixToken) +{ + if (prefixToken == "lcamera-id") { + return SelectorCriterionKind::LibcameraId; + } + if (prefixToken == "index") { + return SelectorCriterionKind::Index; + } + if (prefixToken == "model-substr") { + return SelectorCriterionKind::ModelSubstr; + } + if (prefixToken == "model") { + return SelectorCriterionKind::Model; + } + if (prefixToken == "location") { + return SelectorCriterionKind::Location; + } + + throw std::runtime_error( + "Unknown deviceSelector prefix: " + prefixToken); +} + +SelectorCriterion parseCriterionClause(const std::string& clause) +{ + const std::string trimmedClause = trimWhitespace(clause); + if (trimmedClause.empty()) { + throw std::runtime_error("Empty deviceSelector clause"); + } + + const size_t colonPos = trimmedClause.find(':'); + if (colonPos == std::string::npos) + { + return SelectorCriterion{ + SelectorCriterionKind::LibcameraId, + trimmedClause + }; + } + + const std::string prefixToken = trimWhitespace( + trimmedClause.substr(0, colonPos)); + const std::string value = trimWhitespace( + trimmedClause.substr(colonPos + 1)); + + if (value.empty()) + { + throw std::runtime_error( + "deviceSelector clause has empty value for prefix " + + prefixToken); + } + + return SelectorCriterion{ + parseCriterionKind(prefixToken), + value + }; +} + +} // namespace + +std::string trimWhitespace(const std::string& text) +{ + size_t start = 0; + while (start < text.size() && std::isspace(static_cast(text[start]))) { + ++start; + } + + size_t end = text.size(); + while (end > start + && std::isspace(static_cast(text[end - 1]))) + { + --end; + } + + return text.substr(start, end - start); +} + +std::vector parseDeviceSelector( + const std::string& deviceSelector) +{ + const std::string trimmedSelector = trimWhitespace(deviceSelector); + if (trimmedSelector.empty()) { + throw std::runtime_error("deviceSelector is empty"); + } + + std::vector criteria; + std::string currentClause; + currentClause.reserve(trimmedSelector.size()); + + for (size_t i = 0; i < trimmedSelector.size(); ++i) + { + const char ch = trimmedSelector[i]; + if (ch == '\\' && i + 1 < trimmedSelector.size() + && trimmedSelector[i + 1] == ';') + { + currentClause.push_back(';'); + ++i; + continue; + } + + if (ch == ';') + { + criteria.push_back(parseCriterionClause(currentClause)); + currentClause.clear(); + continue; + } + + currentClause.push_back(ch); + } + + criteria.push_back(parseCriterionClause(currentClause)); + return criteria; +} + +} // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/selectorParse.h b/commonLibs/lcameraDev/selectorParse.h new file mode 100644 index 0000000..a7bb386 --- /dev/null +++ b/commonLibs/lcameraDev/selectorParse.h @@ -0,0 +1,31 @@ +#ifndef LCAMERA_DEV_SELECTOR_PARSE_H +#define LCAMERA_DEV_SELECTOR_PARSE_H + +#include +#include + +namespace lcamera_dev { + +enum class SelectorCriterionKind +{ + LibcameraId, + Index, + Model, + ModelSubstr, + Location, +}; + +struct SelectorCriterion +{ + SelectorCriterionKind kind = SelectorCriterionKind::LibcameraId; + std::string value; +}; + +std::vector parseDeviceSelector( + const std::string& deviceSelector); + +std::string trimWhitespace(const std::string& text); + +} // namespace lcamera_dev + +#endif // LCAMERA_DEV_SELECTOR_PARSE_H diff --git a/commonLibs/lcameraDev/selectorResolve.cpp b/commonLibs/lcameraDev/selectorResolve.cpp new file mode 100644 index 0000000..3f60efb --- /dev/null +++ b/commonLibs/lcameraDev/selectorResolve.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +namespace lcamera_dev { + +namespace { + +std::string toLowerAscii(const std::string& text) +{ + std::string lowered = text; + for (char& ch : lowered) { + ch = static_cast(std::tolower(static_cast(ch))); + } + return lowered; +} + +bool recordMatchesCriterion( + const CameraIdentityRecord& record, const SelectorCriterion& criterion) +{ + switch (criterion.kind) + { + case SelectorCriterionKind::LibcameraId: + return record.id == criterion.value; + + case SelectorCriterionKind::Index: + return false; + + case SelectorCriterionKind::Model: + return record.model == criterion.value; + + case SelectorCriterionKind::ModelSubstr: + return record.model.find(criterion.value) != std::string::npos; + + case SelectorCriterionKind::Location: + return toLowerAscii(record.locationLabel) + == toLowerAscii(criterion.value); + } + + return false; +} + +int parseIndexCriterion(const SelectorCriterion& criterion) +{ + try { + return std::stoi(criterion.value); + } + catch (const std::exception&) { + throw std::runtime_error("Invalid index: value in deviceSelector"); + } +} + +} // namespace + +std::string formatCameraListForDiagnostics( + const std::vector& records) +{ + std::ostringstream result; + result << "Known cameras:\n"; + + for (size_t i = 0; i < records.size(); ++i) + { + const CameraIdentityRecord& record = records[i]; + result << " [" << i << "] id=" << record.id; + if (!record.model.empty()) { + result << " model=" << record.model; + } + if (!record.locationLabel.empty()) { + result << " location=" << record.locationLabel; + } + result << '\n'; + } + + return result.str(); +} + +CameraIdentityRecord resolveSelectorAgainstRecords( + const std::vector& criteria, + const std::vector& records) +{ + std::optional indexCriterion; + for (const SelectorCriterion& criterion : criteria) + { + if (criterion.kind != SelectorCriterionKind::Index) { + continue; + } + + const int index = parseIndexCriterion(criterion); + if (index < 0 + || static_cast(index) >= records.size()) + { + throw std::runtime_error("index: selector out of range"); + } + + indexCriterion = index; + } + + std::vector matches; + matches.reserve(records.size()); + + for (const CameraIdentityRecord& record : records) + { + bool matchesAll = true; + for (const SelectorCriterion& criterion : criteria) + { + if (criterion.kind == SelectorCriterionKind::Index) { + continue; + } + + if (!recordMatchesCriterion(record, criterion)) + { + matchesAll = false; + break; + } + } + + if (!matchesAll) { + continue; + } + + matches.push_back(&record); + } + + if (indexCriterion.has_value()) + { + const CameraIdentityRecord& indexedRecord = + records.at(static_cast(*indexCriterion)); + + auto it = std::find_if( + matches.begin(), matches.end(), + [&indexedRecord](const CameraIdentityRecord* candidate) { + return candidate->id == indexedRecord.id; + }); + + if (it == matches.end()) + { + throw std::runtime_error( + "index: criterion conflicts with other selector clauses"); + } + + if (matches.size() > 1) + { + throw std::runtime_error( + "Ambiguous deviceSelector: multiple cameras match\n" + + formatCameraListForDiagnostics(records)); + } + + return indexedRecord; + } + + if (matches.empty()) + { + throw std::runtime_error( + "No camera matches deviceSelector\n" + + formatCameraListForDiagnostics(records)); + } + + if (matches.size() > 1) + { + throw std::runtime_error( + "Ambiguous deviceSelector: multiple cameras match\n" + + formatCameraListForDiagnostics(records)); + } + + return *matches.front(); +} + +} // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/selectorResolve.h b/commonLibs/lcameraDev/selectorResolve.h new file mode 100644 index 0000000..46dae32 --- /dev/null +++ b/commonLibs/lcameraDev/selectorResolve.h @@ -0,0 +1,20 @@ +#ifndef LCAMERA_DEV_SELECTOR_RESOLVE_H +#define LCAMERA_DEV_SELECTOR_RESOLVE_H + +#include +#include +#include +#include + +namespace lcamera_dev { + +CameraIdentityRecord resolveSelectorAgainstRecords( + const std::vector& criteria, + const std::vector& records); + +std::string formatCameraListForDiagnostics( + const std::vector& records); + +} // namespace lcamera_dev + +#endif // LCAMERA_DEV_SELECTOR_RESOLVE_H diff --git a/commonLibs/lcameraDev/tools/lcameraDevListCameras.cpp b/commonLibs/lcameraDev/tools/lcameraDevListCameras.cpp new file mode 100644 index 0000000..b203c1e --- /dev/null +++ b/commonLibs/lcameraDev/tools/lcameraDevListCameras.cpp @@ -0,0 +1,71 @@ +#include + +#include +#include +#include +#include + +namespace { + +sscl::co::NonViralNonPostingInvoker listCamerasCInd( + std::exception_ptr& exceptionStorage, + std::function callerLambda) +{ + (void)exceptionStorage; + (void)callerLambda; + + const std::vector cameras = + co_await lcameraDev_enumerateCamerasCReq(); + + std::cout << "lcameraDev: found " << cameras.size() << " camera(s)\n"; + + for (size_t i = 0; i < cameras.size(); ++i) + { + const lcamera_dev::LcameraDevCameraInfo& info = cameras[i]; + + std::cout << " [" << i << "] id=" << info.id; + if (!info.model.empty()) { + std::cout << " model=" << info.model; + } + if (!info.location.empty()) { + std::cout << " location=" << info.location; + } + std::cout << '\n'; + } + + co_return; +} + +void runListCameras( + const std::shared_ptr& componentThread) +{ + lcameraDev_main(componentThread); + + sscl::co::NonViralTaskNursery nursery; + nursery.openAdmission(); + nursery.launch( + [](sscl::co::NonViralTaskNursery::Slot::Lease& lease) + { + return listCamerasCInd( + lease.getExceptionStorage(), + lease.getCallerLambda()); + }); + nursery.closeAdmission(); + nursery.syncAwaitAllSettlements(componentThread->getIoContext()); + + lcameraDev_exit(); +} + +} // namespace + +int main() +{ + try { + lcamera_dev_probe::runOnComponentThread(runListCameras); + return 0; + } + catch (const std::exception& exc) { + std::cerr << "lcameraDev_list_cameras: " << exc.what() << '\n'; + return 1; + } +} diff --git a/commonLibs/lcameraDev/tools/lcameraDevProbe.cpp b/commonLibs/lcameraDev/tools/lcameraDevProbe.cpp new file mode 100644 index 0000000..ff241c9 --- /dev/null +++ b/commonLibs/lcameraDev/tools/lcameraDevProbe.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include +#include +#include + +namespace { + +sscl::co::NonViralNonPostingInvoker probeSelectorCInd( + std::exception_ptr& exceptionStorage, + std::function callerLambda, + const std::string& deviceSelector) +{ + (void)exceptionStorage; + (void)callerLambda; + + const lcamera_dev::LcameraDevGetOrCreateResult createResult = + co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector); + + std::cout << "lcameraDev_probe: opened session for camera id=" + << createResult.resolvedIdentity.id << '\n'; + + co_await lcameraDev_releaseDeviceCReq(createResult.deviceSession); + + std::cout << "lcameraDev_probe: released session\n"; + co_return; +} + +void runProbe( + const std::shared_ptr& componentThread, + const std::string& deviceSelector) +{ + lcameraDev_main(componentThread); + + sscl::co::NonViralTaskNursery nursery; + nursery.openAdmission(); + nursery.launch( + [deviceSelector](sscl::co::NonViralTaskNursery::Slot::Lease& lease) + { + return probeSelectorCInd( + lease.getExceptionStorage(), + lease.getCallerLambda(), + deviceSelector); + }); + nursery.closeAdmission(); + nursery.syncAwaitAllSettlements(componentThread->getIoContext()); + + lcameraDev_exit(); +} + +} // namespace + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + std::cerr << "Usage: lcameraDev_probe \n"; + return 1; + } + + try { + const std::string deviceSelector = argv[1]; + + lcamera_dev_probe::runOnComponentThread( + [&](const std::shared_ptr& componentThread) + { + runProbe(componentThread, deviceSelector); + }); + + return 0; + } + catch (const std::exception& exc) { + std::cerr << "lcameraDev_probe: " << exc.what() << '\n'; + return 1; + } +} diff --git a/commonLibs/lcameraDev/tools/probeRunner.cpp b/commonLibs/lcameraDev/tools/probeRunner.cpp new file mode 100644 index 0000000..b3349e3 --- /dev/null +++ b/commonLibs/lcameraDev/tools/probeRunner.cpp @@ -0,0 +1,88 @@ +#include + +#include +#include +#include +#include + +namespace lcamera_dev_probe { + +namespace { + +constexpr sscl::ThreadId PROBE_PUPPETEER_THREAD_ID = 1; + +class DummyPuppeteerComponent +: public sscl::pptr::PuppeteerComponent +{ +public: + explicit DummyPuppeteerComponent( + const std::shared_ptr& componentThread) + : sscl::pptr::PuppeteerComponent(componentThread) + {} + + void handleLoopExceptionHook() override + { + std::cerr << "lcameraDev probe: puppeteer loop exception\n"; + } +}; + +void probePuppeteerMain( + const sscl::PuppeteerThread::EntryFnArguments& args, + const std::function&)>& work, + std::promise& donePromise) +{ + sscl::PuppeteerThread& thr = args.usableBeforeJolt; + thr.initializeTls(); + sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID); + + std::shared_ptr thrPtr = + std::static_pointer_cast(thr.shared_from_this()); + sscl::ComponentThread::setPuppeteerThread(thrPtr); + + try { + work(thrPtr); + donePromise.set_value(nullptr); + } + catch (...) { + donePromise.set_value(std::current_exception()); + } + + thr.getIoContext().stop(); +} + +} // namespace + +void runOnComponentThread( + const std::function&)>& work) +{ + std::promise donePromise; + std::future doneFuture = donePromise.get_future(); + + DummyPuppeteerComponent dummyComponent{ + std::shared_ptr()}; + + std::shared_ptr probeThread = + std::make_shared( + PROBE_PUPPETEER_THREAD_ID, + "lcameraDev-probe", + [&work, &donePromise]( + const sscl::PuppeteerThread::EntryFnArguments& args) + { + probePuppeteerMain(args, work, donePromise); + }, + dummyComponent, + nullptr); + + dummyComponent.thread = probeThread; + + probeThread->thread.join(); + + std::exception_ptr probeException = doneFuture.get(); + if (probeException) { + std::rethrow_exception(probeException); + } +} + +} // namespace lcamera_dev_probe diff --git a/commonLibs/lcameraDev/tools/probeRunner.h b/commonLibs/lcameraDev/tools/probeRunner.h new file mode 100644 index 0000000..89938c5 --- /dev/null +++ b/commonLibs/lcameraDev/tools/probeRunner.h @@ -0,0 +1,16 @@ +#ifndef LCAMERA_DEV_PROBE_RUNNER_H +#define LCAMERA_DEV_PROBE_RUNNER_H + +#include +#include +#include + +namespace lcamera_dev_probe { + +void runOnComponentThread( + const std::function&)>& work); + +} // namespace lcamera_dev_probe + +#endif // LCAMERA_DEV_PROBE_RUNNER_H diff --git a/docs/lcamera-dev-lib.md b/docs/lcamera-dev-lib.md index f1d9f99..5eafa02 100644 --- a/docs/lcamera-dev-lib.md +++ b/docs/lcamera-dev-lib.md @@ -34,8 +34,10 @@ lcameraBuff (stimBuffApi) SMO-facing producers, channel fan-out, intrins Channel splitting, colourspace conversion, threshold masks, and stencils belong in a separate shared raster library (`rasterStimulus`, future) and in -`lcameraBuff`; `lcameraDev` stops at “here is a stable, acquired libcamera -`Camera` you can configure and stream from.” +`lcameraBuff`; `lcameraDev` stops at selector resolution, `CameraManager` +lifecycle, and a refcounted, **acquired** `libcamera::Camera` handle per +resolved device. Stream negotiation, pixel-format selection, frame buffers, +and capture timing belong in `lcameraBuff` (and supporting libraries), not here. ## Why libcamera (for now) @@ -120,7 +122,8 @@ Possible future params: * `fps-hz=30` * `width=640|height=480` -* `pixfmt=NV12` — negotiation hint passed down to `lcameraDev` +* `pixfmt=NV12` — negotiation hint for `lcameraBuff` stream setup (not + `lcameraDev`) ## Device selector format @@ -180,7 +183,7 @@ tokens). Resolution algorithm (summary): -1. Start `CameraManager` (once per process, inside `lcameraDev`). +1. Start `CameraManager` in `lcameraDev_main` (once per process). 2. Enumerate cameras; build an identity record per camera (`id`, `model`, `location`, `systemDevices`). 3. Parse `deviceSelector` into criterion clauses; apply **all** clauses (AND). @@ -197,9 +200,9 @@ each camera’s `id()`, `model`, and `location` so operators can copy a stable ## Shared session and refcounting Unlike X11 windows, a camera has no “sub-objects” inside its feed — the selector -always designates the whole camera stream. Multiple DAP lines for H, S, and V -are **views** over one capture session, all under the same `dev-identifier` -(e.g. `cam0`). +always designates the whole physical camera. Multiple DAP lines for H, S, and V +share one **device session** (acquired `libcamera::Camera`), all under the same +`dev-identifier` (e.g. `cam0`). ```text dev-identifier cam0 + deviceSelector (resolved) ──> LcameraDeviceSession (refcounted) @@ -213,11 +216,10 @@ dev-identifier cam0 + deviceSelector (resolved) ──> LcameraDeviceSession (re Rules: * First `getOrCreate` for a resolved camera ID: enumerate, `acquire()` the - `libcamera::Camera`, negotiate and start the shared stream. + `libcamera::Camera`, create the session entry. * Subsequent `getOrCreate` for the same ID: increment refcount; return the same session handle. -* Last `release`: `release()` the libcamera camera, stop streaming, tear down - buffers. +* Last `release`: `release()` the libcamera camera and erase the session entry. * Different `deviceSelector` strings that resolve to the same libcamera ID share one session (even if the selector text differs, e.g. one line uses `lcamera-id:...` and another uses a compound selector that resolves to the @@ -226,84 +228,89 @@ Rules: the same physical device from SMO’s perspective; channel differences come from the qualeIface name on each DAP line. -## dlopen API (planned) +Streaming, frame delivery, and colourspace work are **out of scope** for +`lcameraDev`; `lcameraBuff` uses the session’s acquired camera handle to set up +capture on attach. + +## dlopen API Exported from `liblcameraDev.so` using `extern "C"` symbols (mirroring `livoxProto1`). `lcameraBuff` loads the library with `dlopen` + `dlsym` and -calls through function pointers. +calls through function pointers. Hot-path operations are **`*CReq` coroutine +invokers** (`sscl::co::ViralNonPostingInvoker`); `main` / `exit` remain +synchronous. + +Header: `commonLibs/lcameraDev/lcameraDev.h`. ### Lifecycle ```c -/* Start CameraManager; idempotent per process. */ -typedef void lcameraDev_mainFn(void); +typedef void lcameraDev_mainFn( + const std::shared_ptr& componentThread); -/* Stop manager, release all devices; called on SMO shutdown. */ typedef void lcameraDev_exitFn(void); ``` +`lcameraDev_main` records the Body `ComponentThread`, starts libcamera's +`CameraManager` (idempotent), and must succeed before any `*CReq` runs. + ### Device acquisition -```c +```cpp struct LcameraDevGetOrCreateResult { - bool success; - /* Opaque session handle; valid until matching release call. */ - void* deviceSession; - /* Resolved libcamera camera ID (stable session key). */ - const char* resolvedCameraId; + std::shared_ptr deviceSession; + CameraIdentityRecord resolvedIdentity; }; -typedef LcameraDevGetOrCreateResult lcameraDev_getOrCreateDeviceFn( - const char* deviceSelector); +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector); -typedef void lcameraDev_releaseDeviceFn(void* deviceSession); +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_releaseDeviceCReqFn( + const std::shared_ptr& deviceSession); ``` -`deviceSession` is opaque to callers. `lcameraBuff` passes it back when -detaching; it must not call libcamera APIs directly. +Failures throw `std::exception`. `lcameraBuff` holds the returned +`shared_ptr` and passes it back to `releaseDeviceCReq`. The +session wraps the acquired `libcamera::Camera`; higher layers configure and +stream from that handle — `lcameraDev` does not expose frame or stream APIs. ### Enumeration (discovery) -```c +```cpp struct LcameraDevCameraInfo { - const char* id; - const char* model; /* empty string if unavailable */ - const char* location; /* "front", "back", "external", or "" */ + std::string id; + std::string model; + std::string location; }; -struct LcameraDevEnumerateResult -{ - size_t count; - LcameraDevCameraInfo* cameras; /* caller frees via lcameraDev_freeEnumerateResult */ -}; - -typedef LcameraDevEnumerateResult lcameraDev_enumerateCamerasFn(void); -typedef void lcameraDev_freeEnumerateResultFn(LcameraDevEnumerateResult* result); +typedef sscl::co::ViralNonPostingInvoker> + lcameraDev_enumerateCamerasCReqFn(void); ``` -### Frame access (boundary with lcameraBuff) +### Manual verification tools -Exact frame-delivery API is TBD and will likely use a callback or a “dequeue -latest frame buffer” method on the session handle. `lcameraDev` owns libcamera -`FrameBuffer` allocation and `Request` requeue; `lcameraBuff` copies or maps the -plane it needs for the active qualeIface channel. +When built with `-DENABLE_LIB_lcameraDev=ON`: -Principle: **one negotiated stream format per session** (prefer native YUV such -as NV12/YUYV). `lcameraBuff` + `rasterStimulus` derive H/S/V greyscale planes -from that single stream. +* `lcameraDev_list_cameras` — runs `enumerateCamerasCReq` on a minimal probe + `ComponentThread`. +* `lcameraDev_probe ` — `getOrCreateDeviceCReq`, then + `releaseDeviceCReq` (selector and session attach/detach only). -## Module layout (planned) +## Module layout ```text commonLibs/lcameraDev/ CMakeLists.txt - lcameraDev.h Public C API + C++ internal headers - lcameraDev.cpp dlopen exports, CameraManager singleton - cameraSession.cpp Refcounted session, stream negotiation - selectorResolve.cpp deviceSelector parsing and matching - cameraEnumerate.cpp Discovery / identity records + lcameraDev.h / lcameraDev.cpp Public C API and dlopen exports + cameraManagerState.h / .cpp CameraManager singleton, session map + cameraSession.h / .cpp Refcounted acquired-camera session + cameraIdentity.h / .cpp Discovery identity records + selectorParse.h / .cpp Compound selector parsing + selectorResolve.h / .cpp AND-match resolution + tools/ lcameraDev_list_cameras, lcameraDev_probe ``` Build links against `libcamera` (pkg-config). Does **not** link Salmanoff @@ -313,8 +320,8 @@ Build links against `libcamera` (pkg-config). Does **not** link Salmanoff | Component | Responsibility | |---|---| -| `lcameraDev` | libcamera lifecycle, selector resolution, shared capture session | -| `lcameraBuff` | `StimBuffApiDesc`, `StimulusProducer`, per-channel ring buffers, intrins | +| `lcameraDev` | libcamera lifecycle, selector resolution, refcounted acquired camera session | +| `lcameraBuff` | Stream setup, frames, `StimBuffApiDesc`, channel fan-out, intrins | | `rasterStimulus` (future) | YUV↔HSV, plane extraction, threshold masks, stencil geometry | | `xcbWindow` / `waylandWindow` | Separate capture path; reuse `rasterStimulus` only | @@ -324,12 +331,10 @@ If libcamera IDs prove insufficient in practice, selector policies can gain ## Open questions -1. **Stream format defaults** — prefer first supported YUV format, or allow - `lcameraBuff(pixfmt=...)` override? -2. **Hot-unplug** — on camera removal, fail all attached `lcameraBuff` producers +1. **Hot-unplug** — on camera removal, fail all attached `lcameraBuff` producers and drop the session, or attempt re-enumeration by stored `lcamera-id:`? -3. **Thread model** — libcamera callbacks vs polling in the Body thread - production daemon; align with `StimulusProducer::productionCDaemon` timing - (`CONFIG_STIMBUFF_FRAME_PERIOD_MS`). -4. **IPA packaging** — document per-platform `apt install` requirements in the +2. **IPA packaging** — document per-platform `apt install` requirements in the main README when `lcameraBuff` lands (RPi needs `libcamera-ipa`). + +Stream format, frame timing, and libcamera callback threading are owned by +`lcameraBuff`, not `lcameraDev`. diff --git a/include/config.h.in b/include/config.h.in index 40413d6..46f2ef7 100644 --- a/include/config.h.in +++ b/include/config.h.in @@ -30,6 +30,7 @@ /* Common Libraries */ #cmakedefine CONFIG_LIB_XCBXORG_ENABLED #cmakedefine CONFIG_LIB_LIVOXPROTO1_ENABLED +#cmakedefine CONFIG_LIB_LCAMERADEV_ENABLED #cmakedefine CONFIG_LIB_ALSA_ENABLED /* Stim Buff APIs */