LCamDev: implement configureSessionModeCReq

We can, theoretically, now change the v4l camera's mode.
This commit is contained in:
2026-06-13 20:56:33 -04:00
parent 25d7b9c013
commit 3e85b920fb
20 changed files with 1926 additions and 11 deletions
+3 -1
View File
@@ -189,7 +189,9 @@ endif()
# Add third-party dependencies
if(ENABLE_TESTS)
add_subdirectory(third_party)
add_subdirectory(third_party)
set(LIBSPINSCALE_BUILD_TESTS ON CACHE BOOL
"Build libspinscale unit tests" FORCE)
endif()
add_subdirectory(buildmach)
add_subdirectory(libspinscale)
+24 -1
View File
@@ -13,6 +13,9 @@ if(ENABLE_LIB_lcameraDev)
add_library(lcameraDev SHARED
lcameraDev.cpp
cameraIdentity.cpp
cameraModeRequest.cpp
planarYuvFormatPolicy.cpp
sessionModeConfigure.cpp
selectorParse.cpp
selectorResolve.cpp
cameraManagerState.cpp
@@ -59,7 +62,8 @@ if(ENABLE_LIB_lcameraDev)
if(NOT TARGET spinscale_test_support)
message(FATAL_ERROR
"lcameraDev probe tools require spinscale_test_support. "
"Configure with -DENABLE_TESTS=ON.")
"Configure with -DLIBSPINSCALE_BUILD_TESTS=ON "
"(salmanoff sets this automatically when -DENABLE_TESTS=ON).")
endif()
add_executable(lcameraDev_list_cameras
@@ -99,6 +103,25 @@ if(ENABLE_LIB_lcameraDev)
Boost::system
Boost::log
)
add_executable(lcameraDev_configure_probe
tools/lcameraDevConfigureProbe.cpp
tools/probeRunner.cpp
)
target_include_directories(lcameraDev_configure_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tools
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(lcameraDev_configure_probe PRIVATE
lcameraDev
spinscale
spinscale_test_support
Boost::system
Boost::log
)
endif()
if(ENABLE_TESTS)
@@ -0,0 +1,50 @@
#include <cameraModeRequest.h>
#include <sstream>
#include <stdexcept>
namespace lcamera_dev {
namespace {
constexpr const char *incompleteOptionalPlanarMessage =
"lcameraDev: fullPlanarIsOptional/opt-planar is not honored yet "
"(non-planar producer deinterleaving is not implemented)";
} // namespace
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request)
{
if (request.width == 0 || request.height == 0)
{
throw std::runtime_error(
"lcameraDev: camera mode request width and height must be "
"non-zero");
}
if (request.colourSpace != LcameraDevColourSpace::Yuv)
{
throw std::runtime_error(
"lcameraDev: unsupported colour-space for camera mode request");
}
}
void rejectFullPlanarOptionalAtConfigureApi(
const LcameraDevCameraModeRequest& request)
{
if (request.fullPlanarIsOptional)
{
throw std::runtime_error(incompleteOptionalPlanarMessage);
}
}
bool cameraModeRequestsEqual(
const LcameraDevCameraModeRequest& left,
const LcameraDevCameraModeRequest& right)
{
return left.width == right.width
&& left.height == right.height
&& left.colourSpace == right.colourSpace
&& left.fullPlanarIsOptional == right.fullPlanarIsOptional;
}
} // namespace lcamera_dev
+48
View File
@@ -0,0 +1,48 @@
#ifndef LCAMERA_DEV_CAMERA_MODE_REQUEST_H
#define LCAMERA_DEV_CAMERA_MODE_REQUEST_H
#include <string>
namespace lcamera_dev {
enum class LcameraDevColourSpace
{
Yuv,
};
struct LcameraDevCameraModeRequest
{
unsigned width = 0;
unsigned height = 0;
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
/** EXPLANATION:
* When false, configure must select a fully planar YUV pixel format.
* When true, relaxed non-planar formats are not honored at the configure
* API yet — producer-side deinterleaving is not implemented (Stage 2).
*/
bool fullPlanarIsOptional = false;
};
struct LcameraDevConfiguredCameraMode
{
unsigned width = 0, height = 0;
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
std::string pixelFormatName;
bool isFullyPlanar = false;
unsigned planeCount = 0;
};
void validateCameraModeRequest(const LcameraDevCameraModeRequest& request);
/** Rejects fullPlanarIsOptional at the configure API until non-planar fallback
* paths exist in lcameraBuff. */
void rejectFullPlanarOptionalAtConfigureApi(
const LcameraDevCameraModeRequest& request);
bool cameraModeRequestsEqual(
const LcameraDevCameraModeRequest& left,
const LcameraDevCameraModeRequest& right);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_MODE_REQUEST_H
+55
View File
@@ -1,4 +1,7 @@
#include <boostAsioLinkageFix.h>
#include <cameraSession.h>
#include <sessionModeConfigure.h>
#include <stdexcept>
namespace lcamera_dev {
@@ -26,4 +29,56 @@ bool CameraSession::decrementRefcount()
return s.rsrc.refcount == 0;
}
sscl::co::ViralNonPostingInvoker<LcameraDevConfiguredCameraMode>
CameraSession::configureSessionModeCReq(
const LcameraDevCameraModeRequest& request)
{
sscl::co::CoQutex::ReleaseHandle sessionGuard =
co_await s.lock.getAcquireInvocationAndSuspensionPolicy();
if (s.rsrc.isStreaming)
{
throw std::runtime_error(
"lcameraDev: cannot configure session mode while streaming");
}
validateCameraModeRequest(request);
rejectFullPlanarOptionalAtConfigureApi(request);
if (s.rsrc.configuredMode.has_value()
&& cameraModeRequestsEqual(s.rsrc.configuredRequest, request))
{
co_return *s.rsrc.configuredMode;
}
if (s.rsrc.configuredMode.has_value())
{
throw std::runtime_error(
"lcameraDev: conflicting camera mode request on configured "
"session");
}
std::shared_ptr<libcamera::CameraConfiguration> heldConfiguration;
const LcameraDevConfiguredCameraMode resolvedMode =
configureLibcameraSessionMode(
s.rsrc.camera,
request,
heldConfiguration);
const ConfigureSessionModeStatus status =
applyModeRequestToSessionState(
s.rsrc,
request,
resolvedMode,
heldConfiguration);
if (status != ConfigureSessionModeStatus::Configured)
{
throw std::logic_error(
"lcameraDev: unexpected configure session mode status");
}
co_return *s.rsrc.configuredMode;
}
} // namespace lcamera_dev
+30
View File
@@ -2,9 +2,13 @@
#define LCAMERA_DEV_CAMERA_SESSION_H
#include <cameraIdentity.h>
#include <cameraModeRequest.h>
#include <libcamera/camera.h>
#include <memory>
#include <optional>
#include <stdexcept>
#include <spinscale/co/coQutex.h>
#include <spinscale/co/invokers.h>
#include <spinscale/sharedResourceGroup.h>
namespace lcamera_dev {
@@ -20,6 +24,12 @@ struct CameraSessionResources
int refcount = 0;
CameraIdentityRecord identity;
std::shared_ptr<libcamera::Camera> camera;
bool isStreaming = false;
LcameraDevCameraModeRequest configuredRequest;
std::optional<LcameraDevConfiguredCameraMode> configuredMode;
std::shared_ptr<libcamera::CameraConfiguration> heldConfiguration;
int libcameraConfigureCallCount = 0;
};
class CameraSession
@@ -35,9 +45,29 @@ public:
const std::shared_ptr<libcamera::Camera>& getCamera() const
{ return s.rsrc.camera; }
bool isModeConfigured() const
{ return s.rsrc.configuredMode.has_value(); }
const LcameraDevConfiguredCameraMode& getConfiguredMode() const
{
if (!s.rsrc.configuredMode.has_value())
{
throw std::logic_error(
"lcameraDev: session mode is not configured");
}
return *s.rsrc.configuredMode;
}
int getLibcameraConfigureCallCount() const
{ return s.rsrc.libcameraConfigureCallCount; }
void incrementRefcount();
bool decrementRefcount();
sscl::co::ViralNonPostingInvoker<LcameraDevConfiguredCameraMode>
configureSessionModeCReq(const LcameraDevCameraModeRequest& request);
sscl::SharedResourceGroup<sscl::co::CoQutex, CameraSessionResources> s;
};
+22
View File
@@ -58,4 +58,26 @@ lcameraDev_enumerateCamerasCReq(void)
co_return co_await lcamera_dev::enumerateCamerasCReq();
}
sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevConfiguredCameraMode>
lcameraDev_configureSessionModeCReq(
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const lcamera_dev::LcameraDevCameraModeRequest& request)
{
lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState();
if (!state.isInitialized)
{
throw std::runtime_error(
"lcameraDev_configureSessionModeCReq: call lcameraDev_main "
"first");
}
if (!deviceSession)
{
throw std::runtime_error(
"lcameraDev_configureSessionModeCReq: deviceSession is null");
}
co_return co_await deviceSession->configureSessionModeCReq(request);
}
} // extern "C"
+7
View File
@@ -2,6 +2,7 @@
#define LCAMERA_DEV_H
#include <cameraIdentity.h>
#include <cameraModeRequest.h>
#include <memory>
#include <spinscale/co/invokers.h>
#include <spinscale/componentThread.h>
@@ -46,11 +47,17 @@ typedef sscl::co::ViralNonPostingInvoker<void>
typedef sscl::co::ViralNonPostingInvoker<std::vector<lcamera_dev::LcameraDevCameraInfo>>
lcameraDev_enumerateCamerasCReqFn(void);
typedef sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevConfiguredCameraMode>
lcameraDev_configureSessionModeCReqFn(
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const lcamera_dev::LcameraDevCameraModeRequest& request);
lcameraDev_mainFn lcameraDev_main;
lcameraDev_exitFn lcameraDev_exit;
lcameraDev_getOrCreateDeviceCReqFn lcameraDev_getOrCreateDeviceCReq;
lcameraDev_releaseDeviceCReqFn lcameraDev_releaseDeviceCReq;
lcameraDev_enumerateCamerasCReqFn lcameraDev_enumerateCamerasCReq;
lcameraDev_configureSessionModeCReqFn lcameraDev_configureSessionModeCReq;
#ifdef __cplusplus
}
@@ -0,0 +1,146 @@
#include <planarYuvFormatPolicy.h>
#include <libcamera/formats.h>
#include <sstream>
#include <stdexcept>
namespace lcamera_dev {
namespace {
using libcamera::formats::NV12;
using libcamera::formats::NV16;
using libcamera::formats::NV21;
using libcamera::formats::NV24;
using libcamera::formats::NV42;
using libcamera::formats::NV61;
using libcamera::formats::UYVY;
using libcamera::formats::VYUY;
using libcamera::formats::YUYV;
using libcamera::formats::YUV420;
using libcamera::formats::YUV422;
using libcamera::formats::YUV444;
using libcamera::formats::YVU420;
using libcamera::formats::YVU422;
using libcamera::formats::YVU444;
using libcamera::formats::YVYU;
bool pixelFormatMatches(
const libcamera::PixelFormat& pixelFormat,
const libcamera::PixelFormat& expectedFormat)
{
return pixelFormat == expectedFormat;
}
bool isFullyPlanarYuvFourcc(const libcamera::PixelFormat& pixelFormat)
{
return pixelFormatMatches(pixelFormat, YUV420)
|| pixelFormatMatches(pixelFormat, YVU420)
|| pixelFormatMatches(pixelFormat, YUV422)
|| pixelFormatMatches(pixelFormat, YVU422)
|| pixelFormatMatches(pixelFormat, YUV444)
|| pixelFormatMatches(pixelFormat, YVU444);
}
bool isSemiPlanarYuvFourcc(const libcamera::PixelFormat& pixelFormat)
{
return pixelFormatMatches(pixelFormat, NV12)
|| pixelFormatMatches(pixelFormat, NV21)
|| pixelFormatMatches(pixelFormat, NV16)
|| pixelFormatMatches(pixelFormat, NV61)
|| pixelFormatMatches(pixelFormat, NV24)
|| pixelFormatMatches(pixelFormat, NV42);
}
bool isPackedYuvFourcc(const libcamera::PixelFormat& pixelFormat)
{
return pixelFormatMatches(pixelFormat, YUYV)
|| pixelFormatMatches(pixelFormat, YVYU)
|| pixelFormatMatches(pixelFormat, UYVY)
|| pixelFormatMatches(pixelFormat, VYUY);
}
} // namespace
bool isFullyPlanarYuv(const libcamera::PixelFormat& pixelFormat)
{
return isFullyPlanarYuvFourcc(pixelFormat);
}
bool isKnownYuvCaptureFormat(const libcamera::PixelFormat& pixelFormat)
{
return isFullyPlanarYuvFourcc(pixelFormat)
|| isSemiPlanarYuvFourcc(pixelFormat)
|| isPackedYuvFourcc(pixelFormat);
}
unsigned yuvCapturePlaneCount(const libcamera::PixelFormat& pixelFormat)
{
if (isFullyPlanarYuvFourcc(pixelFormat)) {
return 3;
}
if (isSemiPlanarYuvFourcc(pixelFormat)) {
return 2;
}
if (isPackedYuvFourcc(pixelFormat)) {
return 1;
}
return 0;
}
std::string formatCandidateListForDiagnostics(
const std::vector<libcamera::PixelFormat>& candidates)
{
std::ostringstream stream;
for (std::size_t i = 0; i < candidates.size(); ++i)
{
if (i > 0) {
stream << ", ";
}
stream << candidates[i].toString();
}
return stream.str();
}
std::optional<libcamera::PixelFormat>
selectYuvCaptureFormat(
const std::vector<libcamera::PixelFormat>& candidates,
bool fullPlanarIsOptional)
{
if (candidates.empty())
{
throw std::runtime_error(
"lcameraDev: no YUV pixel-format candidates available");
}
if (!fullPlanarIsOptional)
{
for (const libcamera::PixelFormat& candidate : candidates)
{
if (isFullyPlanarYuv(candidate)) {
return candidate;
}
}
throw std::runtime_error(
"lcameraDev: no fully planar YUV format among candidates: "
+ formatCandidateListForDiagnostics(candidates));
}
for (const libcamera::PixelFormat& candidate : candidates)
{
if (isKnownYuvCaptureFormat(candidate)) {
return candidate;
}
}
throw std::runtime_error(
"lcameraDev: no known YUV capture format among candidates: "
+ formatCandidateListForDiagnostics(candidates));
}
} // namespace lcamera_dev
@@ -0,0 +1,26 @@
#ifndef LCAMERA_DEV_PLANAR_YUV_FORMAT_POLICY_H
#define LCAMERA_DEV_PLANAR_YUV_FORMAT_POLICY_H
#include <libcamera/pixel_format.h>
#include <optional>
#include <vector>
namespace lcamera_dev {
bool isFullyPlanarYuv(const libcamera::PixelFormat& pixelFormat);
bool isKnownYuvCaptureFormat(const libcamera::PixelFormat& pixelFormat);
unsigned yuvCapturePlaneCount(const libcamera::PixelFormat& pixelFormat);
std::optional<libcamera::PixelFormat>
selectYuvCaptureFormat(
const std::vector<libcamera::PixelFormat>& candidates,
bool fullPlanarIsOptional);
std::string formatCandidateListForDiagnostics(
const std::vector<libcamera::PixelFormat>& candidates);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_PLANAR_YUV_FORMAT_POLICY_H
@@ -0,0 +1,143 @@
#include <boostAsioLinkageFix.h>
#include <cameraModeRequest.h>
#include <planarYuvFormatPolicy.h>
#include <sessionModeConfigure.h>
#include <libcamera/camera.h>
#include <libcamera/stream.h>
#include <stdexcept>
#include <utility>
namespace lcamera_dev {
namespace {
LcameraDevConfiguredCameraMode buildConfiguredModeFromStreamConfig(
const LcameraDevCameraModeRequest& request,
const libcamera::StreamConfiguration& streamConfig)
{
LcameraDevConfiguredCameraMode configuredMode;
configuredMode.width = streamConfig.size.width;
configuredMode.height = streamConfig.size.height;
configuredMode.colourSpace = request.colourSpace;
configuredMode.pixelFormatName = streamConfig.pixelFormat.toString();
configuredMode.isFullyPlanar =
isFullyPlanarYuv(streamConfig.pixelFormat);
configuredMode.planeCount =
yuvCapturePlaneCount(streamConfig.pixelFormat);
return configuredMode;
}
std::unique_ptr<libcamera::CameraConfiguration> generateCaptureConfiguration(
const std::shared_ptr<libcamera::Camera>& camera)
{
std::unique_ptr<libcamera::CameraConfiguration> config =
camera->generateConfiguration(
{libcamera::StreamRole::VideoRecording});
if (!config || config->empty())
{
config = camera->generateConfiguration(
{libcamera::StreamRole::Viewfinder});
}
if (!config || config->empty())
{
throw std::runtime_error(
"lcameraDev: camera does not support VideoRecording or "
"Viewfinder stream roles");
}
return config;
}
void validateConfigurationStatus(
libcamera::CameraConfiguration::Status status,
const std::string& cameraId)
{
if (status == libcamera::CameraConfiguration::Valid
|| status == libcamera::CameraConfiguration::Adjusted)
{
return;
}
throw std::runtime_error(
"lcameraDev: libcamera configuration invalid for camera "
+ cameraId);
}
} // namespace
ConfigureSessionModeStatus applyModeRequestToSessionState(
CameraSessionResources& resources,
const LcameraDevCameraModeRequest& request,
const LcameraDevConfiguredCameraMode& resolvedMode,
std::shared_ptr<libcamera::CameraConfiguration> heldConfiguration)
{
if (resources.configuredMode.has_value())
{
if (cameraModeRequestsEqual(resources.configuredRequest, request)) {
return ConfigureSessionModeStatus::NoOpAlreadyConfigured;
}
throw std::runtime_error(
"lcameraDev: conflicting camera mode request on configured "
"session");
}
resources.configuredRequest = request;
resources.configuredMode = resolvedMode;
resources.heldConfiguration = heldConfiguration;
++resources.libcameraConfigureCallCount;
return ConfigureSessionModeStatus::Configured;
}
LcameraDevConfiguredCameraMode configureLibcameraSessionMode(
const std::shared_ptr<libcamera::Camera>& camera,
const LcameraDevCameraModeRequest& request,
std::shared_ptr<libcamera::CameraConfiguration>& heldConfiguration)
{
if (!camera)
{
throw std::runtime_error(
"lcameraDev: configureSessionModeCReq camera is null");
}
std::unique_ptr<libcamera::CameraConfiguration> config =
generateCaptureConfiguration(camera);
libcamera::StreamConfiguration& streamConfig = config->at(0);
streamConfig.size = libcamera::Size(request.width, request.height);
const std::vector<libcamera::PixelFormat> pixelFormatCandidates =
streamConfig.formats().pixelformats();
const std::optional<libcamera::PixelFormat> selectedPixelFormat =
selectYuvCaptureFormat(pixelFormatCandidates, false);
if (!selectedPixelFormat.has_value())
{
throw std::runtime_error(
"lcameraDev: failed to select YUV capture format");
}
streamConfig.pixelFormat = *selectedPixelFormat;
const libcamera::CameraConfiguration::Status validateStatus =
config->validate();
validateConfigurationStatus(validateStatus, camera->id());
const int configureRc = camera->configure(config.get());
if (configureRc != 0)
{
throw std::runtime_error(
"lcameraDev: libcamera configure failed for camera "
+ camera->id());
}
const LcameraDevConfiguredCameraMode configuredMode =
buildConfiguredModeFromStreamConfig(request, streamConfig);
heldConfiguration = std::move(config);
return configuredMode;
}
} // namespace lcamera_dev
@@ -0,0 +1,34 @@
#ifndef LCAMERA_DEV_SESSION_MODE_CONFIGURE_H
#define LCAMERA_DEV_SESSION_MODE_CONFIGURE_H
#include <cameraModeRequest.h>
#include <cameraSession.h>
#include <memory>
namespace libcamera {
class CameraConfiguration;
}
namespace lcamera_dev {
enum class ConfigureSessionModeStatus
{
Configured,
NoOpAlreadyConfigured,
RejectedConflictingRequest,
};
ConfigureSessionModeStatus applyModeRequestToSessionState(
CameraSessionResources& resources,
const LcameraDevCameraModeRequest& request,
const LcameraDevConfiguredCameraMode& resolvedMode,
std::shared_ptr<libcamera::CameraConfiguration> heldConfiguration);
LcameraDevConfiguredCameraMode configureLibcameraSessionMode(
const std::shared_ptr<libcamera::Camera>& camera,
const LcameraDevCameraModeRequest& request,
std::shared_ptr<libcamera::CameraConfiguration>& heldConfiguration);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_SESSION_MODE_CONFIGURE_H
+39 -1
View File
@@ -2,6 +2,9 @@ add_executable(lcameraDev_unit_tests
selectorParse_tests.cpp
selectorResolve_tests.cpp
cameraIdentity_tests.cpp
cameraModeRequest_tests.cpp
planarYuvFormatPolicy_tests.cpp
sessionModeConfigure_state_tests.cpp
)
target_include_directories(lcameraDev_unit_tests PRIVATE
@@ -9,18 +12,26 @@ target_include_directories(lcameraDev_unit_tests PRIVATE
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev/tests
${CMAKE_SOURCE_DIR}/tests/fixtures
${CMAKE_SOURCE_DIR}/libspinscale/tests
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${LIBCAMERA_INCLUDE_DIRS}
)
target_link_libraries(lcameraDev_unit_tests
gtest_main
lcameraDev
spinscale
spinscale_test_support
${Boost_LIBRARIES}
${LIBCAMERA_LIBRARIES}
)
add_dependencies(lcameraDev_unit_tests gtest_main)
target_link_directories(lcameraDev_unit_tests PRIVATE
${LIBCAMERA_LIBRARY_DIRS}
)
add_dependencies(lcameraDev_unit_tests gtest_main spinscale_test_support)
add_test(NAME lcameraDev_unit_tests COMMAND lcameraDev_unit_tests)
@@ -50,3 +61,30 @@ add_dependencies(lcameraDev_hil_tests gtest_main spinscale_test_support)
add_test(NAME lcameraDev_hil_tests COMMAND lcameraDev_hil_tests)
set_tests_properties(lcameraDev_hil_tests PROPERTIES LABELS "HIL")
add_executable(lcameraDev_configure_hil_tests
lcameraDev_configure_hil_tests.cpp
)
target_include_directories(lcameraDev_configure_hil_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev/tests
${CMAKE_SOURCE_DIR}/tests/fixtures
${CMAKE_SOURCE_DIR}/libspinscale/tests
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
)
target_link_libraries(lcameraDev_configure_hil_tests
gtest_main
lcameraDev
spinscale
spinscale_test_support
${Boost_LIBRARIES}
)
add_dependencies(lcameraDev_configure_hil_tests gtest_main spinscale_test_support)
add_test(NAME lcameraDev_configure_hil_tests COMMAND lcameraDev_configure_hil_tests)
set_tests_properties(lcameraDev_configure_hil_tests PROPERTIES LABELS "HIL")
@@ -0,0 +1,90 @@
#include <cameraModeRequest.h>
#include <gtest/gtest.h>
#include <support/exceptionAssertions.h>
namespace lcamera_dev {
namespace {
TEST(CameraModeRequestTest, DefaultFullPlanarIsOptionalIsFalse)
{
LcameraDevCameraModeRequest request;
EXPECT_FALSE(request.fullPlanarIsOptional);
}
TEST(CameraModeRequestTest, ZeroWidthThrows)
{
LcameraDevCameraModeRequest request;
request.width = 0;
request.height = 480;
EXPECT_THROW(
validateCameraModeRequest(request),
std::runtime_error);
}
TEST(CameraModeRequestTest, ZeroHeightThrows)
{
LcameraDevCameraModeRequest request;
request.width = 640;
request.height = 0;
EXPECT_THROW(
validateCameraModeRequest(request),
std::runtime_error);
}
TEST(CameraModeRequestTest, UnsupportedColourSpaceThrows)
{
LcameraDevCameraModeRequest request;
request.width = 640;
request.height = 480;
static_assert(
static_cast<int>(LcameraDevColourSpace::Yuv) == 0,
"test assumes Yuv is first enumerator");
const LcameraDevColourSpace unsupportedColourSpace =
static_cast<LcameraDevColourSpace>(1);
request.colourSpace = unsupportedColourSpace;
EXPECT_THROW(
validateCameraModeRequest(request),
std::runtime_error);
}
TEST(CameraModeRequestTest, FullPlanarOptionalRejectedAtConfigureApi)
{
LcameraDevCameraModeRequest request;
request.width = 640;
request.height = 480;
request.fullPlanarIsOptional = true;
try {
rejectFullPlanarOptionalAtConfigureApi(request);
FAIL() << "Expected runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"not honored yet");
}
}
TEST(CameraModeRequestTest, CameraModeRequestsEqualComparesAllFields)
{
LcameraDevCameraModeRequest left;
left.width = 640;
left.height = 480;
left.colourSpace = LcameraDevColourSpace::Yuv;
left.fullPlanarIsOptional = false;
LcameraDevCameraModeRequest right = left;
EXPECT_TRUE(cameraModeRequestsEqual(left, right));
right.height = 720;
EXPECT_FALSE(cameraModeRequestsEqual(left, right));
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,541 @@
#include <boostAsioLinkageFix.h>
#include <catalogHelpers.h>
#include <cameraSession.h>
#include <lcameraDev.h>
#include <gtest/gtest.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <support/bakedDeviceCatalog.h>
#include <support/exceptionAssertions.h>
#include <support/probeComponentThread.h>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace lcamera_dev {
namespace {
constexpr const char *hilEnvVar = "LCAMERADEV_HIL";
constexpr const char *machineEnvVar = "LCAMERADEV_MACHINE";
constexpr const char *configureWidthEnvVar = "LCAMERADEV_CONFIGURE_W";
constexpr const char *configureHeightEnvVar = "LCAMERADEV_CONFIGURE_H";
constexpr const char *defaultMachineTag = "dell-laptop";
constexpr unsigned defaultConfigureWidth = 640;
constexpr unsigned defaultConfigureHeight = 480;
bool hilTestsEnabled()
{
const char *value = std::getenv(hilEnvVar);
return value != nullptr && std::string(value) == "1";
}
std::string machineTagFromEnvironment()
{
const char *value = std::getenv(machineEnvVar);
if (value != nullptr && std::string(value).size() > 0) {
return value;
}
return defaultMachineTag;
}
unsigned unsignedFromEnvironmentOrDefault(
const char *envVar,
unsigned defaultValue)
{
const char *value = std::getenv(envVar);
if (value == nullptr || std::string(value).empty()) {
return defaultValue;
}
return static_cast<unsigned>(std::stoul(value));
}
LcameraDevCameraModeRequest configureRequestFromEnvironment()
{
LcameraDevCameraModeRequest request;
request.width = unsignedFromEnvironmentOrDefault(
configureWidthEnvVar,
defaultConfigureWidth);
request.height = unsignedFromEnvironmentOrDefault(
configureHeightEnvVar,
defaultConfigureHeight);
request.colourSpace = LcameraDevColourSpace::Yuv;
request.fullPlanarIsOptional = false;
return request;
}
sscl::co::NonViralNonPostingInvoker getOrCreateCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const char *deviceSelector,
lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
(void)exceptionStorage;
(void)callerLambda;
createResult = co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);
co_return;
}
sscl::co::NonViralNonPostingInvoker configureCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const LcameraDevCameraModeRequest& request,
LcameraDevConfiguredCameraMode& configuredMode)
{
(void)callerLambda;
configuredMode =
co_await lcameraDev_configureSessionModeCReq(deviceSession, request);
if (exceptionStorage) {
std::rethrow_exception(exceptionStorage);
}
co_return;
}
sscl::co::NonViralNonPostingInvoker releaseCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
(void)exceptionStorage;
(void)callerLambda;
co_await lcameraDev_releaseDeviceCReq(deviceSession);
co_return;
}
void runLcameraDevMainAndNurseryTask(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-configure-hil");
harness.runSync(
[&work](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcameraDev_main(componentThread);
work(componentThread);
lcameraDev_exit();
});
}
void runNonViralNurseryRethrowingOnComponentThread(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::function<sscl::co::NonViralNonPostingInvoker(
sscl::co::NonViralTaskNursery::Slot::Lease&)>& invokerFactory)
{
std::exception_ptr slotException;
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
invokerFactory,
[&slotException](std::exception_ptr& exceptionPtr)
{
slotException = exceptionPtr;
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
if (slotException) {
std::rethrow_exception(slotException);
}
}
bool configureSessionOrExpectPlanarFailure(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const LcameraDevCameraModeRequest& request,
LcameraDevConfiguredCameraMode& configuredMode,
const char *profileTag)
{
try {
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&deviceSession, &request, &configuredMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
deviceSession,
request,
configuredMode);
});
EXPECT_TRUE(configuredMode.isFullyPlanar) << profileTag;
EXPECT_GE(configuredMode.width, 1u) << profileTag;
EXPECT_GE(configuredMode.height, 1u) << profileTag;
return true;
}
catch (const std::exception& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"planar");
return false;
}
}
void configureProfileExpectingPlanarOrExplicitFailure(
const test_fixtures::BakedCameraProfile *profile,
const LcameraDevCameraModeRequest& request,
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
EXPECT_TRUE(createResult.deviceSession != nullptr)
<< profile->profileTag;
LcameraDevConfiguredCameraMode configuredMode;
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
request,
configuredMode,
profile->profileTag);
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
class LcameraDevConfigureHilTest : public ::testing::Test
{
protected:
void SetUp() override
{
if (!hilTestsEnabled()) {
GTEST_SKIP() << "Set " << hilEnvVar << "=1 to run hardware tests";
}
machineTag = machineTagFromEnvironment();
requiredProfiles =
sscl::tests::requiredProfilesForMachine(machineTag.c_str());
configureRequest = configureRequestFromEnvironment();
if (requiredProfiles.empty()) {
GTEST_SKIP() << "No baked profiles for machine tag "
<< machineTag;
}
}
const test_fixtures::BakedCameraProfile *findProfile(
const char *profileTag) const
{
for (const test_fixtures::BakedCameraProfile *profile :
requiredProfiles)
{
if (std::string(profile->profileTag) == profileTag) {
return profile;
}
}
return nullptr;
}
std::string machineTag;
std::vector<const test_fixtures::BakedCameraProfile *> requiredProfiles;
LcameraDevCameraModeRequest configureRequest;
};
TEST_F(LcameraDevConfigureHilTest, ConfigureUsbHdmiYuvRequiresPlanar)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("usb_hdmi_camera");
if (!profile) {
GTEST_SKIP() << "usb_hdmi_camera profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileExpectingPlanarOrExplicitFailure(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileExpectingPlanarOrExplicitFailure(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfiguredModeMatchesRequestDimensions)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
LcameraDevConfiguredCameraMode configuredMode;
const bool configureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
configuredMode,
profile->profileTag);
if (!configureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
EXPECT_GE(configuredMode.width, 1u);
EXPECT_GE(configuredMode.height, 1u);
EXPECT_LE(
configuredMode.width,
configureRequest.width);
EXPECT_LE(
configuredMode.height,
configureRequest.height);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
TEST_F(LcameraDevConfigureHilTest, IdenticalReconfigureIsNoOp)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
LcameraDevConfiguredCameraMode firstMode;
LcameraDevConfiguredCameraMode secondMode;
const bool firstConfigureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
firstMode,
profile->profileTag);
if (!firstConfigureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
const int configureCallCountAfterFirst =
createResult.deviceSession
->getLibcameraConfigureCallCount();
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &secondMode, this](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
configureRequest,
secondMode);
});
EXPECT_EQ(
createResult.deviceSession
->getLibcameraConfigureCallCount(),
configureCallCountAfterFirst);
EXPECT_EQ(secondMode.pixelFormatName, firstMode.pixelFormatName);
EXPECT_EQ(secondMode.width, firstMode.width);
EXPECT_EQ(secondMode.height, firstMode.height);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
TEST_F(LcameraDevConfigureHilTest, ConflictingReconfigureThrows)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
LcameraDevConfiguredCameraMode configuredMode;
const bool configureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
configuredMode,
profile->profileTag);
if (!configureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
LcameraDevCameraModeRequest conflictingRequest =
configureRequest;
conflictingRequest.width = configureRequest.width + 64;
EXPECT_THROW(
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &conflictingRequest](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
LcameraDevConfiguredCameraMode ignoredMode;
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
conflictingRequest,
ignoredMode);
}),
std::runtime_error);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,71 @@
#include <planarYuvFormatPolicy.h>
#include <gtest/gtest.h>
#include <libcamera/formats.h>
#include <support/exceptionAssertions.h>
#include <vector>
namespace lcamera_dev {
namespace {
using libcamera::formats::NV12;
using libcamera::formats::YUYV;
using libcamera::formats::YUV420;
TEST(PlanarYuvFormatPolicyTest, FullyPlanarRequiredPicksYuv420OverNv12)
{
const std::vector<libcamera::PixelFormat> candidates = {YUV420, NV12};
const std::optional<libcamera::PixelFormat> selected =
selectYuvCaptureFormat(candidates, false);
EXPECT_TRUE(selected.has_value());
EXPECT_EQ(*selected, YUV420);
EXPECT_TRUE(isFullyPlanarYuv(*selected));
}
TEST(PlanarYuvFormatPolicyTest, FullyPlanarRequiredThrowsWhenOnlyNonPlanar)
{
const std::vector<libcamera::PixelFormat> candidates = {NV12, YUYV};
try {
selectYuvCaptureFormat(candidates, false);
FAIL() << "Expected runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"planar");
}
}
TEST(PlanarYuvFormatPolicyTest, FullyPlanarOptionalPicksNv12)
{
const std::vector<libcamera::PixelFormat> candidates = {NV12};
const std::optional<libcamera::PixelFormat> selected =
selectYuvCaptureFormat(candidates, true);
EXPECT_TRUE(selected.has_value());
EXPECT_EQ(*selected, NV12);
EXPECT_FALSE(isFullyPlanarYuv(*selected));
EXPECT_EQ(yuvCapturePlaneCount(*selected), 2u);
}
TEST(PlanarYuvFormatPolicyTest, EmptyCandidateListThrows)
{
const std::vector<libcamera::PixelFormat> candidates;
EXPECT_THROW(
selectYuvCaptureFormat(candidates, false),
std::runtime_error);
}
TEST(PlanarYuvFormatPolicyTest, IsFullyPlanarYuvRecognizesYuv420)
{
EXPECT_TRUE(isFullyPlanarYuv(YUV420));
EXPECT_FALSE(isFullyPlanarYuv(NV12));
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,138 @@
#include <cameraModeRequest.h>
#include <cameraSession.h>
#include <gtest/gtest.h>
#include <sessionModeConfigure.h>
#include <memory>
namespace lcamera_dev {
namespace {
LcameraDevConfiguredCameraMode syntheticResolvedMode(
unsigned width,
unsigned height,
const char *pixelFormatName)
{
LcameraDevConfiguredCameraMode mode;
mode.width = width;
mode.height = height;
mode.colourSpace = LcameraDevColourSpace::Yuv;
mode.pixelFormatName = pixelFormatName;
mode.isFullyPlanar = true;
mode.planeCount = 3;
return mode;
}
LcameraDevCameraModeRequest makeRequest(unsigned width, unsigned height)
{
LcameraDevCameraModeRequest request;
request.width = width;
request.height = height;
request.colourSpace = LcameraDevColourSpace::Yuv;
request.fullPlanarIsOptional = false;
return request;
}
TEST(SessionModeConfigureStateTest, FirstConfigureMarksSessionConfigured)
{
CameraSessionResources resources(
CameraIdentityRecord{},
std::shared_ptr<libcamera::Camera>());
const LcameraDevCameraModeRequest request = makeRequest(640, 480);
const LcameraDevConfiguredCameraMode resolvedMode =
syntheticResolvedMode(640, 480, "YU12");
const ConfigureSessionModeStatus status =
applyModeRequestToSessionState(
resources,
request,
resolvedMode,
nullptr);
EXPECT_EQ(status, ConfigureSessionModeStatus::Configured);
EXPECT_TRUE(resources.configuredMode.has_value());
EXPECT_EQ(resources.configuredMode->width, 640u);
EXPECT_EQ(resources.configuredMode->pixelFormatName, "YU12");
EXPECT_EQ(resources.libcameraConfigureCallCount, 1);
}
TEST(SessionModeConfigureStateTest, IdenticalReconfigureIsNoOp)
{
CameraSessionResources resources(
CameraIdentityRecord{},
std::shared_ptr<libcamera::Camera>());
const LcameraDevCameraModeRequest request = makeRequest(640, 480);
const LcameraDevConfiguredCameraMode resolvedMode =
syntheticResolvedMode(640, 480, "YU12");
applyModeRequestToSessionState(
resources,
request,
resolvedMode,
nullptr);
const ConfigureSessionModeStatus status =
applyModeRequestToSessionState(
resources,
request,
resolvedMode,
nullptr);
EXPECT_EQ(status, ConfigureSessionModeStatus::NoOpAlreadyConfigured);
EXPECT_EQ(resources.libcameraConfigureCallCount, 1);
EXPECT_EQ(resources.configuredMode->pixelFormatName, "YU12");
}
TEST(SessionModeConfigureStateTest, ConflictingReconfigureThrows)
{
CameraSessionResources resources(
CameraIdentityRecord{},
std::shared_ptr<libcamera::Camera>());
const LcameraDevCameraModeRequest firstRequest = makeRequest(640, 480);
const LcameraDevConfiguredCameraMode firstMode =
syntheticResolvedMode(640, 480, "YU12");
applyModeRequestToSessionState(
resources,
firstRequest,
firstMode,
nullptr);
const LcameraDevCameraModeRequest conflictingRequest = makeRequest(1280, 720);
EXPECT_THROW(
applyModeRequestToSessionState(
resources,
conflictingRequest,
syntheticResolvedMode(1280, 720, "YU12"),
nullptr),
std::runtime_error);
}
TEST(SessionModeConfigureStateTest, GetConfiguredModeReturnsStoredValues)
{
CameraSessionResources resources(
CameraIdentityRecord{},
std::shared_ptr<libcamera::Camera>());
const LcameraDevCameraModeRequest request = makeRequest(800, 600);
const LcameraDevConfiguredCameraMode resolvedMode =
syntheticResolvedMode(800, 600, "YU12");
applyModeRequestToSessionState(
resources,
request,
resolvedMode,
nullptr);
EXPECT_EQ(resources.configuredMode->width, 800u);
EXPECT_EQ(resources.configuredMode->height, 600u);
EXPECT_EQ(resources.configuredMode->colourSpace, LcameraDevColourSpace::Yuv);
EXPECT_TRUE(resources.configuredMode->isFullyPlanar);
EXPECT_EQ(resources.configuredMode->planeCount, 3u);
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,376 @@
#include <boostAsioLinkageFix.h>
#include <cameraSession.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
#include <stdexcept>
#include <string>
namespace {
constexpr const char *optPlanarFlag = "--opt-planar";
constexpr const char *fullPlanarOptionalFlag = "--full-planar-is-optional";
constexpr const char *reconfigureTwiceFlag = "--reconfigure-twice";
constexpr const char *colourSpacePrefix = "--colour-space=";
constexpr const char *colorSpacePrefix = "--color-space=";
struct ConfigureProbeArgs
{
std::string deviceSelector;
unsigned width = 0;
unsigned height = 0;
lcamera_dev::LcameraDevColourSpace colourSpace =
lcamera_dev::LcameraDevColourSpace::Yuv;
bool fullPlanarIsOptional = false;
bool reconfigureTwice = false;
};
void printUsage(std::ostream& stream)
{
stream <<
"Usage: lcameraDev_configure_probe <deviceSelector> <width> <height> "
"[options]\n"
"Options:\n"
" --colour-space=yuv Semantic colour-space (only yuv today)\n"
" --opt-planar Set fullPlanarIsOptional=true on request\n"
" --full-planar-is-optional Same as --opt-planar\n"
" --reconfigure-twice Configure twice with identical request "
"(no-op check)\n"
"Examples:\n"
" lcameraDev_configure_probe model-substr:Integrated 640 480\n"
" lcameraDev_configure_probe model-substr:HDMI 1280 720 --opt-planar\n"
" lcameraDev_configure_probe index:0 640 480 --reconfigure-twice\n";
}
std::string colourSpaceToString(lcamera_dev::LcameraDevColourSpace colourSpace)
{
switch (colourSpace)
{
case lcamera_dev::LcameraDevColourSpace::Yuv:
return "yuv";
default:
return "unknown";
}
}
bool parseColourSpaceValue(const std::string& value)
{
if (value == "yuv" || value == "YUV") {
return true;
}
throw std::runtime_error(
"unsupported colour-space \"" + value + "\" (only yuv is supported)");
}
unsigned parseDimensionArg(const char *arg, const char *label)
{
try {
const unsigned long parsed = std::stoul(arg);
if (parsed == 0) {
throw std::runtime_error(
std::string(label) + " must be non-zero");
}
return static_cast<unsigned>(parsed);
}
catch (const std::exception&)
{
throw std::runtime_error(
std::string("invalid ") + label + " \"" + arg + "\"");
}
}
ConfigureProbeArgs parseConfigureProbeArgs(int argc, char *argv[])
{
if (argc < 4) {
throw std::runtime_error("missing required arguments");
}
ConfigureProbeArgs args;
args.deviceSelector = argv[1];
args.width = parseDimensionArg(argv[2], "width");
args.height = parseDimensionArg(argv[3], "height");
for (int i = 4; i < argc; ++i)
{
const std::string token = argv[i];
if (token == optPlanarFlag || token == fullPlanarOptionalFlag) {
args.fullPlanarIsOptional = true;
continue;
}
if (token == reconfigureTwiceFlag) {
args.reconfigureTwice = true;
continue;
}
const std::string colourSpacePrefixStr = colourSpacePrefix;
const std::string colorSpacePrefixStr = colorSpacePrefix;
if (token.rfind(colourSpacePrefixStr, 0) == 0) {
parseColourSpaceValue(token.substr(colourSpacePrefixStr.size()));
continue;
}
if (token.rfind(colorSpacePrefixStr, 0) == 0) {
parseColourSpaceValue(token.substr(colorSpacePrefixStr.size()));
continue;
}
throw std::runtime_error("unknown option \"" + token + "\"");
}
return args;
}
void printRequest(const lcamera_dev::LcameraDevCameraModeRequest& request)
{
std::cout << "request:"
<< " width=" << request.width
<< " height=" << request.height
<< " colour-space=" << colourSpaceToString(request.colourSpace)
<< " fullPlanarIsOptional="
<< (request.fullPlanarIsOptional ? "true" : "false")
<< '\n';
}
void printConfiguredMode(
const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode)
{
std::cout << "configured:"
<< " width=" << configuredMode.width
<< " height=" << configuredMode.height
<< " colour-space=" << colourSpaceToString(configuredMode.colourSpace)
<< " pixelFormat=" << configuredMode.pixelFormatName
<< " isFullyPlanar="
<< (configuredMode.isFullyPlanar ? "true" : "false")
<< " planeCount=" << configuredMode.planeCount
<< '\n';
}
void runNurseryRethrowing(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::function<sscl::co::NonViralNonPostingInvoker(
sscl::co::NonViralTaskNursery::Slot::Lease&)>& invokerFactory)
{
std::exception_ptr slotException;
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
invokerFactory,
[&slotException](std::exception_ptr& exceptionPtr)
{
slotException = exceptionPtr;
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
if (slotException) {
std::rethrow_exception(slotException);
}
}
sscl::co::NonViralNonPostingInvoker getOrCreateCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::string& deviceSelector,
lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
(void)callerLambda;
createResult = co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);
if (exceptionStorage) {
std::rethrow_exception(exceptionStorage);
}
co_return;
}
sscl::co::NonViralNonPostingInvoker configureProbeCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const lcamera_dev::LcameraDevCameraModeRequest& request,
lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode)
{
(void)callerLambda;
configuredMode =
co_await lcameraDev_configureSessionModeCReq(deviceSession, request);
if (exceptionStorage) {
std::rethrow_exception(exceptionStorage);
}
co_return;
}
sscl::co::NonViralNonPostingInvoker releaseCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
(void)exceptionStorage;
(void)callerLambda;
co_await lcameraDev_releaseDeviceCReq(deviceSession);
co_return;
}
void releaseSessionAndExit(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
if (!deviceSession) {
lcameraDev_exit();
return;
}
runNurseryRethrowing(
componentThread,
[&deviceSession](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
deviceSession);
});
std::cout << "lcameraDev_configure_probe: released session\n";
/** Drop the session before stopping CameraManager so libcamera does not
* tear down media devices while a Camera handle is still alive. */
deviceSession.reset();
lcameraDev_exit();
}
int runConfigureProbe(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const ConfigureProbeArgs& args)
{
lcameraDev_main(componentThread);
lcamera_dev::LcameraDevGetOrCreateResult createResult;
runNurseryRethrowing(
componentThread,
[&args, &createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
args.deviceSelector,
createResult);
});
std::cout << "lcameraDev_configure_probe: opened session for camera id="
<< createResult.resolvedIdentity.id << '\n';
lcamera_dev::LcameraDevCameraModeRequest request;
request.width = args.width;
request.height = args.height;
request.colourSpace = args.colourSpace;
request.fullPlanarIsOptional = args.fullPlanarIsOptional;
printRequest(request);
lcamera_dev::LcameraDevConfiguredCameraMode firstMode;
try {
runNurseryRethrowing(
componentThread,
[&createResult, &request, &firstMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureProbeCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
request,
firstMode);
});
printConfiguredMode(firstMode);
const int configureCallCountAfterFirst =
createResult.deviceSession->getLibcameraConfigureCallCount();
std::cout << "libcameraConfigureCallCount="
<< configureCallCountAfterFirst << '\n';
if (args.reconfigureTwice)
{
lcamera_dev::LcameraDevConfiguredCameraMode secondMode;
runNurseryRethrowing(
componentThread,
[&createResult, &request, &secondMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureProbeCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
request,
secondMode);
});
printConfiguredMode(secondMode);
std::cout << "libcameraConfigureCallCount="
<< createResult.deviceSession->getLibcameraConfigureCallCount()
<< " (after identical reconfigure)\n";
}
}
catch (const std::exception& configureException)
{
std::cerr << "lcameraDev_configure_probe: configure failed: "
<< configureException.what() << '\n';
releaseSessionAndExit(
componentThread,
createResult.deviceSession);
return 1;
}
releaseSessionAndExit(
componentThread,
createResult.deviceSession);
return 0;
}
} // namespace
int main(int argc, char *argv[])
{
try {
const ConfigureProbeArgs args = parseConfigureProbeArgs(argc, argv);
int exitCode = 0;
lcamera_dev_probe::runOnComponentThread(
[&args, &exitCode](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
exitCode = runConfigureProbe(componentThread, args);
});
return exitCode;
}
catch (const std::runtime_error& exception)
{
std::cerr << "lcameraDev_configure_probe: " << exception.what() << '\n';
printUsage(std::cerr);
return 1;
}
catch (const std::exception& exception)
{
std::cerr << "lcameraDev_configure_probe: " << exception.what() << '\n';
return 1;
}
}
+79 -8
View File
@@ -36,8 +36,10 @@ Channel splitting, colourspace conversion, threshold masks, and stencils belong
in a separate shared raster library (`rasterStimulus`, future) and in
`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.
resolved device. Session mode negotiation (width, height, colour-space,
fully-planar YUV requirement) happens in `lcameraDev` before capture starts.
Frame buffers, request queues, and capture timing belong in `lcameraBuff` (and
supporting libraries), not here.
## Why libcamera (for now)
@@ -228,9 +230,39 @@ Rules:
the same physical device from SMOs perspective; channel differences come from
the qualeIface name on each DAP line.
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.
## Session mode configuration (Stage 1)
Before `lcameraBuff` starts capture, each producer calls
`lcameraDev_configureSessionModeCReq` on the shared `CameraSession` with the
requested mode:
| Field | Role |
|---|---|
| `width` / `height` | Requested capture dimensions (non-zero) |
| `colourSpace` | Semantic colour model (`Yuv` in v1) |
| `fullPlanarIsOptional` | Default `false` — must select fully planar YUV |
`lcameraDev` chooses a concrete libcamera pixel format (e.g. YUV420) from the
cameras supported formats. DAPS lines name the semantic colour-space, not a
raw fourcc.
Rules:
* Configure only while the session exists and capture has not started.
* **Identical** configure request on an already-configured session is a **no-op**
(multiple `lcameraBuff` producers sharing the same device selector each call
get-or-create then configure with the same mode).
* **Different** configure request on an already-configured session throws
(conflicting mode requests on the same physical camera).
* `fullPlanarIsOptional == false` (default): must select a fully planar YUV
format or throw with candidate-format diagnostics.
* `fullPlanarIsOptional == true`: rejected at the configure API until
`lcameraBuff` implements non-planar producer deinterleaving (Stage 2). The
policy helper still accepts optional planar selection for future use.
Streaming, frame delivery, and per-frame colourspace work remain **out of scope**
for `lcameraDev`; `lcameraBuff` uses the configured session to allocate buffers
and start capture on attach.
## dlopen API
@@ -273,8 +305,42 @@ typedef sscl::co::ViralNonPostingInvoker<void>
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.
session wraps the acquired `libcamera::Camera`; higher layers configure the mode
then stream from that handle.
### Session mode configuration
```cpp
enum class LcameraDevColourSpace { Yuv };
struct LcameraDevCameraModeRequest
{
unsigned width = 0;
unsigned height = 0;
LcameraDevColourSpace colourSpace = LcameraDevColourSpace::Yuv;
bool fullPlanarIsOptional = false;
};
struct LcameraDevConfiguredCameraMode
{
unsigned width;
unsigned height;
LcameraDevColourSpace colourSpace;
std::string pixelFormatName;
bool isFullyPlanar;
unsigned planeCount;
};
typedef sscl::co::ViralNonPostingInvoker<LcameraDevConfiguredCameraMode>
lcameraDev_configureSessionModeCReqFn(
const std::shared_ptr<CameraSession>& deviceSession,
const LcameraDevCameraModeRequest& request);
```
`lcameraDev_configureSessionModeCReq` delegates to
`CameraSession::configureSessionModeCReq`, which runs libcamera
`generateConfiguration` + `configure` and stores the result on the session.
Identical reconfigure is a no-op; conflicting reconfigure throws.
### Enumeration (discovery)
@@ -298,6 +364,10 @@ When built with `-DENABLE_LIB_lcameraDev=ON`:
`ComponentThread`.
* `lcameraDev_probe <deviceSelector>``getOrCreateDeviceCReq`, then
`releaseDeviceCReq` (selector and session attach/detach only).
* `lcameraDev_configure_probe <deviceSelector> <width> <height> [options]`
`getOrCreateDeviceCReq`, `configureSessionModeCReq`, print resolved mode (or
exception), then `releaseDeviceCReq`. Options: `--colour-space=yuv`,
`--opt-planar` / `--full-planar-is-optional`, `--reconfigure-twice`.
## Module layout
@@ -310,7 +380,8 @@ commonLibs/lcameraDev/
cameraIdentity.h / .cpp Discovery identity records
selectorParse.h / .cpp Compound selector parsing
selectorResolve.h / .cpp AND-match resolution
tools/ lcameraDev_list_cameras, lcameraDev_probe
tools/ lcameraDev_list_cameras, lcameraDev_probe,
lcameraDev_configure_probe
```
Build links against `libcamera` (pkg-config). Does **not** link Salmanoff
+4
View File
@@ -28,6 +28,10 @@ struct BakedCameraProfile
// MACHINE: dell-laptop
// Captured via lcameraDev_list_cameras on 2026-06-10.
// Configure HIL note (2026-06-10): both cameras expose MJPEG and packed YUYV
// via libcamera VideoRecording/Viewfinder — no fully planar YUV420/NV12 at
// 640x480. configureSessionModeCReq correctly throws when fullPlanarIsOptional
// is false; success-path HIL tests skip on this machine.
inline constexpr BakedCameraProfile bakedCameraProfiles[] = {
{
"dell-laptop",