lcameraDev: Add session mgr lib for libcamera device binding

This commit is contained in:
2026-06-13 12:02:04 -04:00
parent cc7f4fcd9b
commit 46f767f232
21 changed files with 1363 additions and 64 deletions
+5 -3
View File
@@ -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}")
+1
View File
@@ -1,3 +1,4 @@
add_subdirectory(xcbXorg)
add_subdirectory(livoxProto1)
add_subdirectory(lcameraDev)
add_subdirectory(attachmentSupport)
+93
View File
@@ -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="$<TARGET_FILE:lcameraDev>"
-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()
+66
View File
@@ -0,0 +1,66 @@
#include <cameraIdentity.h>
#include <libcamera/property_ids.h>
#include <algorithm>
#include <cctype>
#include <optional>
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<libcamera::Camera>& camera)
{
CameraIdentityRecord record;
record.id = camera->id();
const libcamera::ControlList& props = camera->properties();
const std::optional<std::string> model =
props.get(libcamera::properties::Model);
if (model) {
record.model = *model;
}
const std::optional<int> location =
props.get(libcamera::properties::Location);
if (location)
{
record.locationLabel = locationPropertyToLabel(*location);
}
return record;
}
std::vector<CameraIdentityRecord> buildIdentityRecords(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras)
{
std::vector<CameraIdentityRecord> records;
records.reserve(cameras.size());
for (const auto& camera : cameras)
{
if (!camera) { continue; }
records.push_back(buildIdentityRecord(camera));
}
return records;
}
} // namespace lcamera_dev
+28
View File
@@ -0,0 +1,28 @@
#ifndef LCAMERA_DEV_CAMERA_IDENTITY_H
#define LCAMERA_DEV_CAMERA_IDENTITY_H
#include <libcamera/camera.h>
#include <optional>
#include <string>
#include <vector>
namespace lcamera_dev {
struct CameraIdentityRecord
{
std::string id;
std::string model;
std::string locationLabel;
};
CameraIdentityRecord buildIdentityRecord(
const std::shared_ptr<libcamera::Camera>& camera);
std::vector<CameraIdentityRecord> buildIdentityRecords(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras);
std::string locationPropertyToLabel(int32_t locationValue);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_IDENTITY_H
@@ -0,0 +1,251 @@
#include <boostAsioLinkageFix.h>
#include <cameraIdentity.h>
#include <cameraManagerState.h>
#include <selectorParse.h>
#include <selectorResolve.h>
#include <algorithm>
#include <stdexcept>
namespace lcamera_dev {
namespace {
static LcameraDevState lcameraDevState;
void startCameraManager(CameraManagerResources& resources)
{
resources.cameraManager = std::make_unique<libcamera::CameraManager>();
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<libcamera::Camera> findCameraById(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras,
const std::string& cameraId)
{
for (const std::shared_ptr<libcamera::Camera>& camera : cameras)
{
if (camera && camera->id() == cameraId) {
return camera;
}
}
return nullptr;
}
} // namespace
LcameraDevState& getLcameraDevState()
{
return lcameraDevState;
}
std::vector<std::shared_ptr<libcamera::Camera>> listLibcameraCameras()
{
LcameraDevState& state = getLcameraDevState();
if (!state.isInitialized || !state.managerState.rsrc.cameraManager) {
return {};
}
return state.managerState.rsrc.cameraManager->cameras();
}
void lcameraDevMain(
const std::shared_ptr<sscl::ComponentThread>& 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<CameraSession> 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<LcameraDevGetOrCreateResult>
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<SelectorCriterion> criteria =
parseDeviceSelector(deviceSelector);
const std::vector<std::shared_ptr<libcamera::Camera>> cameras =
state.managerState.rsrc.cameraManager->cameras();
const std::vector<CameraIdentityRecord> 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<CameraSession> 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<libcamera::Camera> 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<CameraSession> session =
std::make_shared<CameraSession>(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<void>
releaseDeviceSessionCReq(
const std::shared_ptr<CameraSession>& 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<std::vector<LcameraDevCameraInfo>>
enumerateCamerasCReq()
{
LcameraDevState& state = getLcameraDevState();
sscl::co::CoQutex::ReleaseHandle managerGuard =
co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();
const std::vector<std::shared_ptr<libcamera::Camera>> cameras =
state.managerState.rsrc.cameraManager->cameras();
const std::vector<CameraIdentityRecord> identityRecords =
buildIdentityRecords(cameras);
std::vector<LcameraDevCameraInfo> 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
@@ -0,0 +1,55 @@
#ifndef LCAMERA_DEV_CAMERA_MANAGER_STATE_H
#define LCAMERA_DEV_CAMERA_MANAGER_STATE_H
#include <lcameraDev.h>
#include <cameraSession.h>
#include <libcamera/camera_manager.h>
#include <map>
#include <memory>
#include <spinscale/co/coQutex.h>
#include <spinscale/componentThread.h>
#include <spinscale/sharedResourceGroup.h>
#include <string>
#include <vector>
namespace lcamera_dev {
struct CameraManagerResources
{
std::unique_ptr<libcamera::CameraManager> cameraManager;
std::map<std::string, std::shared_ptr<CameraSession>> sessionsByCameraId;
};
struct LcameraDevState
{
LcameraDevState()
: managerState("lcameraDev::CameraManager")
{}
bool isInitialized = false;
std::shared_ptr<sscl::ComponentThread> componentThread;
sscl::SharedResourceGroup<sscl::co::CoQutex, CameraManagerResources>
managerState;
};
LcameraDevState& getLcameraDevState();
void lcameraDevMain(
const std::shared_ptr<sscl::ComponentThread>& componentThread);
void lcameraDevExit();
std::vector<std::shared_ptr<libcamera::Camera>> listLibcameraCameras();
sscl::co::ViralNonPostingInvoker<LcameraDevGetOrCreateResult>
getOrCreateDeviceSessionCReq(const std::string& deviceSelector);
sscl::co::ViralNonPostingInvoker<void>
releaseDeviceSessionCReq(
const std::shared_ptr<CameraSession>& deviceSession);
sscl::co::ViralNonPostingInvoker<std::vector<LcameraDevCameraInfo>>
enumerateCamerasCReq();
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_MANAGER_STATE_H
+29
View File
@@ -0,0 +1,29 @@
#include <cameraSession.h>
#include <stdexcept>
namespace lcamera_dev {
CameraSession::CameraSession(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& 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
+46
View File
@@ -0,0 +1,46 @@
#ifndef LCAMERA_DEV_CAMERA_SESSION_H
#define LCAMERA_DEV_CAMERA_SESSION_H
#include <cameraIdentity.h>
#include <libcamera/camera.h>
#include <memory>
#include <spinscale/co/coQutex.h>
#include <spinscale/sharedResourceGroup.h>
namespace lcamera_dev {
struct CameraSessionResources
{
CameraSessionResources(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& camera)
: identity(identity), camera(camera)
{}
int refcount = 0;
CameraIdentityRecord identity;
std::shared_ptr<libcamera::Camera> camera;
};
class CameraSession
{
public:
CameraSession(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& camera);
const CameraIdentityRecord& getIdentityRecord() const
{ return s.rsrc.identity; }
const std::shared_ptr<libcamera::Camera>& getCamera() const
{ return s.rsrc.camera; }
void incrementRefcount();
bool decrementRefcount();
sscl::SharedResourceGroup<sscl::co::CoQutex, CameraSessionResources> s;
};
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_SESSION_H
+61
View File
@@ -0,0 +1,61 @@
#include <boostAsioLinkageFix.h>
#include <cameraManagerState.h>
#include <lcameraDev.h>
#include <stdexcept>
extern "C" {
void lcameraDev_main(
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::lcameraDevMain(componentThread);
}
void lcameraDev_exit(void)
{
lcamera_dev::lcameraDevExit();
}
sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevGetOrCreateResult>
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<void>
lcameraDev_releaseDeviceCReq(
const std::shared_ptr<lcamera_dev::CameraSession>& 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<std::vector<lcamera_dev::LcameraDevCameraInfo>>
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"
+59
View File
@@ -0,0 +1,59 @@
#ifndef LCAMERA_DEV_H
#define LCAMERA_DEV_H
#include <cameraIdentity.h>
#include <memory>
#include <spinscale/co/invokers.h>
#include <spinscale/componentThread.h>
#include <string>
#include <vector>
namespace lcamera_dev {
class CameraSession;
struct LcameraDevGetOrCreateResult
{
std::shared_ptr<CameraSession> 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<sscl::ComponentThread>& componentThread);
typedef void lcameraDev_exitFn(void);
typedef sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevGetOrCreateResult>
lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector);
typedef sscl::co::ViralNonPostingInvoker<void>
lcameraDev_releaseDeviceCReqFn(
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession);
typedef sscl::co::ViralNonPostingInvoker<std::vector<lcamera_dev::LcameraDevCameraInfo>>
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
+127
View File
@@ -0,0 +1,127 @@
#include <selectorParse.h>
#include <stdexcept>
#include <cctype>
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<unsigned char>(text[start]))) {
++start;
}
size_t end = text.size();
while (end > start
&& std::isspace(static_cast<unsigned char>(text[end - 1])))
{
--end;
}
return text.substr(start, end - start);
}
std::vector<SelectorCriterion> parseDeviceSelector(
const std::string& deviceSelector)
{
const std::string trimmedSelector = trimWhitespace(deviceSelector);
if (trimmedSelector.empty()) {
throw std::runtime_error("deviceSelector is empty");
}
std::vector<SelectorCriterion> 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
+31
View File
@@ -0,0 +1,31 @@
#ifndef LCAMERA_DEV_SELECTOR_PARSE_H
#define LCAMERA_DEV_SELECTOR_PARSE_H
#include <string>
#include <vector>
namespace lcamera_dev {
enum class SelectorCriterionKind
{
LibcameraId,
Index,
Model,
ModelSubstr,
Location,
};
struct SelectorCriterion
{
SelectorCriterionKind kind = SelectorCriterionKind::LibcameraId;
std::string value;
};
std::vector<SelectorCriterion> parseDeviceSelector(
const std::string& deviceSelector);
std::string trimWhitespace(const std::string& text);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_SELECTOR_PARSE_H
+171
View File
@@ -0,0 +1,171 @@
#include <selectorResolve.h>
#include <algorithm>
#include <cctype>
#include <optional>
#include <sstream>
#include <stdexcept>
namespace lcamera_dev {
namespace {
std::string toLowerAscii(const std::string& text)
{
std::string lowered = text;
for (char& ch : lowered) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(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<CameraIdentityRecord>& 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<SelectorCriterion>& criteria,
const std::vector<CameraIdentityRecord>& records)
{
std::optional<int> indexCriterion;
for (const SelectorCriterion& criterion : criteria)
{
if (criterion.kind != SelectorCriterionKind::Index) {
continue;
}
const int index = parseIndexCriterion(criterion);
if (index < 0
|| static_cast<size_t>(index) >= records.size())
{
throw std::runtime_error("index: selector out of range");
}
indexCriterion = index;
}
std::vector<const CameraIdentityRecord*> 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<size_t>(*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
+20
View File
@@ -0,0 +1,20 @@
#ifndef LCAMERA_DEV_SELECTOR_RESOLVE_H
#define LCAMERA_DEV_SELECTOR_RESOLVE_H
#include <cameraIdentity.h>
#include <selectorParse.h>
#include <string>
#include <vector>
namespace lcamera_dev {
CameraIdentityRecord resolveSelectorAgainstRecords(
const std::vector<SelectorCriterion>& criteria,
const std::vector<CameraIdentityRecord>& records);
std::string formatCameraListForDiagnostics(
const std::vector<CameraIdentityRecord>& records);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_SELECTOR_RESOLVE_H
@@ -0,0 +1,71 @@
#include <boostAsioLinkageFix.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
namespace {
sscl::co::NonViralNonPostingInvoker listCamerasCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda)
{
(void)exceptionStorage;
(void)callerLambda;
const std::vector<lcamera_dev::LcameraDevCameraInfo> 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<sscl::ComponentThread>& 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;
}
}
@@ -0,0 +1,78 @@
#include <boostAsioLinkageFix.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
#include <string>
namespace {
sscl::co::NonViralNonPostingInvoker probeSelectorCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> 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<sscl::ComponentThread>& 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 <deviceSelector>\n";
return 1;
}
try {
const std::string deviceSelector = argv[1];
lcamera_dev_probe::runOnComponentThread(
[&](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
runProbe(componentThread, deviceSelector);
});
return 0;
}
catch (const std::exception& exc) {
std::cerr << "lcameraDev_probe: " << exc.what() << '\n';
return 1;
}
}
@@ -0,0 +1,88 @@
#include <boostAsioLinkageFix.h>
#include <probeRunner.h>
#include <spinscale/component.h>
#include <future>
#include <iostream>
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<sscl::PuppeteerThread>& 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<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work,
std::promise<std::exception_ptr>& donePromise)
{
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
thr.initializeTls();
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
std::shared_ptr<sscl::PuppeteerThread> thrPtr =
std::static_pointer_cast<sscl::PuppeteerThread>(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<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
std::promise<std::exception_ptr> donePromise;
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
DummyPuppeteerComponent dummyComponent{
std::shared_ptr<sscl::PuppeteerThread>()};
std::shared_ptr<sscl::PuppeteerThread> probeThread =
std::make_shared<sscl::PuppeteerThread>(
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
+16
View File
@@ -0,0 +1,16 @@
#ifndef LCAMERA_DEV_PROBE_RUNNER_H
#define LCAMERA_DEV_PROBE_RUNNER_H
#include <functional>
#include <memory>
#include <spinscale/componentThread.h>
namespace lcamera_dev_probe {
void runOnComponentThread(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work);
} // namespace lcamera_dev_probe
#endif // LCAMERA_DEV_PROBE_RUNNER_H
+66 -61
View File
@@ -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 cameras `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 SMOs 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 sessions 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<sscl::ComponentThread>& 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<CameraSession> deviceSession;
CameraIdentityRecord resolvedIdentity;
};
typedef LcameraDevGetOrCreateResult lcameraDev_getOrCreateDeviceFn(
const char* deviceSelector);
typedef sscl::co::ViralNonPostingInvoker<LcameraDevGetOrCreateResult>
lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector);
typedef void lcameraDev_releaseDeviceFn(void* deviceSession);
typedef sscl::co::ViralNonPostingInvoker<void>
lcameraDev_releaseDeviceCReqFn(
const std::shared_ptr<CameraSession>& 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<CameraSession>` 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<std::vector<LcameraDevCameraInfo>>
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 <deviceSelector>``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`.
+1
View File
@@ -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 */