LCamDev: implement configureSessionModeCReq
We can, theoretically, now change the v4l camera's mode.
This commit is contained in:
+3
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 SMO’s 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 session’s 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
|
||||
camera’s 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
|
||||
|
||||
Vendored
+4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user