diff --git a/commonLibs/lcameraDev/cameraManagerState.cpp b/commonLibs/lcameraDev/cameraManagerState.cpp index dae47f3..f3cb458 100644 --- a/commonLibs/lcameraDev/cameraManagerState.cpp +++ b/commonLibs/lcameraDev/cameraManagerState.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -49,6 +48,40 @@ std::shared_ptr findCameraById( return nullptr; } +struct LiveCameraSnapshot +{ + std::vector> cameras; + std::vector identityRecords; +}; + +LiveCameraSnapshot snapshotLiveCameras(CameraManagerResources& resources) +{ + LiveCameraSnapshot snapshot; + snapshot.cameras = resources.cameraManager->cameras(); + snapshot.identityRecords = buildIdentityRecords(snapshot.cameras); + return snapshot; +} + +CameraIdentityRecord resolveDeviceSelectorForLiveCameras( + const std::string& deviceSelector, + CameraManagerResources& resources) +{ + const LiveCameraSnapshot snapshot = snapshotLiveCameras(resources); + return resolveDeviceSelectorAgainstRecords( + deviceSelector, snapshot.identityRecords); +} + +void requireNonEmptyDeviceSelector( + const std::string& deviceSelector, + const char *apiName) +{ + if (deviceSelector.empty()) + { + throw std::runtime_error( + std::string(apiName) + ": deviceSelector is empty"); + } +} + } // namespace LcameraDevState& getLcameraDevState() @@ -110,29 +143,38 @@ void lcameraDevExit() state.isInitialized = false; } -sscl::co::ViralNonPostingInvoker -getOrCreateDeviceSessionCReq(const std::string& deviceSelector) +sscl::co::ViralNonPostingInvoker +resolveDeviceSelectorCReq(const std::string& deviceSelector) { - if (deviceSelector.empty()) - { - throw std::runtime_error( - "lcameraDev_getOrCreateDeviceCReq: deviceSelector is empty"); - } + requireNonEmptyDeviceSelector( + deviceSelector, "lcameraDev_resolveDeviceSelectorCReq"); LcameraDevState& state = getLcameraDevState(); sscl::co::CoQutex::ReleaseHandle managerGuard = co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); - const std::vector criteria = - parseDeviceSelector(deviceSelector); - const std::vector> cameras = - state.managerState.rsrc.cameraManager->cameras(); - const std::vector identityRecords = - buildIdentityRecords(cameras); + co_return resolveDeviceSelectorForLiveCameras( + deviceSelector, state.managerState.rsrc); +} + +sscl::co::ViralNonPostingInvoker +getOrCreateDeviceSessionCReq(const std::string& deviceSelector) +{ + requireNonEmptyDeviceSelector( + deviceSelector, "lcameraDev_getOrCreateDeviceCReq"); + + LcameraDevState& state = getLcameraDevState(); + + sscl::co::CoQutex::ReleaseHandle managerGuard = + co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); + + const LiveCameraSnapshot snapshot = + snapshotLiveCameras(state.managerState.rsrc); const CameraIdentityRecord resolvedRecord = - resolveSelectorAgainstRecords(criteria, identityRecords); + resolveDeviceSelectorAgainstRecords( + deviceSelector, snapshot.identityRecords); const std::string& resolvedCameraId = resolvedRecord.id; auto sessionIt = @@ -153,7 +195,7 @@ getOrCreateDeviceSessionCReq(const std::string& deviceSelector) } std::shared_ptr camera = - findCameraById(cameras, resolvedCameraId); + findCameraById(snapshot.cameras, resolvedCameraId); if (!camera) { throw std::runtime_error( @@ -228,15 +270,13 @@ enumerateCamerasCReq() sscl::co::CoQutex::ReleaseHandle managerGuard = co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy(); - const std::vector> cameras = - state.managerState.rsrc.cameraManager->cameras(); - const std::vector identityRecords = - buildIdentityRecords(cameras); + const LiveCameraSnapshot snapshot = + snapshotLiveCameras(state.managerState.rsrc); std::vector cameraInfos; - cameraInfos.reserve(identityRecords.size()); + cameraInfos.reserve(snapshot.identityRecords.size()); - for (const CameraIdentityRecord& record : identityRecords) + for (const CameraIdentityRecord& record : snapshot.identityRecords) { cameraInfos.push_back(LcameraDevCameraInfo{ record.id, diff --git a/commonLibs/lcameraDev/cameraManagerState.h b/commonLibs/lcameraDev/cameraManagerState.h index 31d44e5..6d8539a 100644 --- a/commonLibs/lcameraDev/cameraManagerState.h +++ b/commonLibs/lcameraDev/cameraManagerState.h @@ -40,6 +40,9 @@ void lcameraDevExit(); std::vector> listLibcameraCameras(); +sscl::co::ViralNonPostingInvoker +resolveDeviceSelectorCReq(const std::string& deviceSelector); + sscl::co::ViralNonPostingInvoker getOrCreateDeviceSessionCReq(const std::string& deviceSelector); diff --git a/commonLibs/lcameraDev/lcameraDev.cpp b/commonLibs/lcameraDev/lcameraDev.cpp index d061b83..284e76c 100644 --- a/commonLibs/lcameraDev/lcameraDev.cpp +++ b/commonLibs/lcameraDev/lcameraDev.cpp @@ -30,6 +30,19 @@ lcameraDev_getOrCreateDeviceCReq(const std::string& deviceSelector) co_return co_await lcamera_dev::getOrCreateDeviceSessionCReq(deviceSelector); } +sscl::co::ViralNonPostingInvoker +lcameraDev_resolveDeviceSelectorCReq(const std::string& deviceSelector) +{ + lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState(); + if (!state.isInitialized) + { + throw std::runtime_error( + "lcameraDev_resolveDeviceSelectorCReq: call lcameraDev_main first"); + } + + co_return co_await lcamera_dev::resolveDeviceSelectorCReq(deviceSelector); +} + sscl::co::ViralNonPostingInvoker lcameraDev_releaseDeviceCReq( const std::shared_ptr& deviceSession) diff --git a/commonLibs/lcameraDev/lcameraDev.h b/commonLibs/lcameraDev/lcameraDev.h index b606a07..5c9e5dd 100644 --- a/commonLibs/lcameraDev/lcameraDev.h +++ b/commonLibs/lcameraDev/lcameraDev.h @@ -40,6 +40,9 @@ typedef void lcameraDev_exitFn(void); typedef sscl::co::ViralNonPostingInvoker lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector); +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_resolveDeviceSelectorCReqFn(const std::string& deviceSelector); + typedef sscl::co::ViralNonPostingInvoker lcameraDev_releaseDeviceCReqFn( const std::shared_ptr& deviceSession); @@ -55,6 +58,7 @@ typedef sscl::co::ViralNonPostingInvoker& records) +{ + const std::vector criteria = + parseDeviceSelector(deviceSelector); + return resolveSelectorAgainstRecords(criteria, records); +} + } // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/selectorResolve.h b/commonLibs/lcameraDev/selectorResolve.h index 46dae32..35880c0 100644 --- a/commonLibs/lcameraDev/selectorResolve.h +++ b/commonLibs/lcameraDev/selectorResolve.h @@ -12,6 +12,10 @@ CameraIdentityRecord resolveSelectorAgainstRecords( const std::vector& criteria, const std::vector& records); +CameraIdentityRecord resolveDeviceSelectorAgainstRecords( + const std::string& deviceSelector, + const std::vector& records); + std::string formatCameraListForDiagnostics( const std::vector& records); diff --git a/commonLibs/lcameraDev/tests/lcameraDev_hil_tests.cpp b/commonLibs/lcameraDev/tests/lcameraDev_hil_tests.cpp index 5674c3f..40d0847 100644 --- a/commonLibs/lcameraDev/tests/lcameraDev_hil_tests.cpp +++ b/commonLibs/lcameraDev/tests/lcameraDev_hil_tests.cpp @@ -71,6 +71,16 @@ sscl::co::NonViralNonPostingInvoker releaseCInd( co_return; } +sscl::co::NonViralNonPostingInvoker resolveSelectorCInd( + std::exception_ptr &, std::function, + const char *deviceSelector, + lcamera_dev::CameraIdentityRecord& resolvedIdentity) +{ + resolvedIdentity = + co_await lcameraDev_resolveDeviceSelectorCReq(deviceSelector); + co_return; +} + void runLcameraDevMainAndNurseryTask( const std::function&)>& work) @@ -189,5 +199,62 @@ TEST_F(LcameraDevHilTest, GetOrCreateByBakedSelector) }); } +TEST_F(LcameraDevHilTest, ResolveDeviceSelectorMatchesGetOrCreateIdentity) +{ + runLcameraDevMainAndNurseryTask( + [this](const std::shared_ptr& componentThread) + { + for (const test_fixtures::BakedCameraProfile *profile : + requiredProfiles) + { + lcamera_dev::CameraIdentityRecord resolvedIdentity; + lcamera_dev::LcameraDevGetOrCreateResult createResult; + + sscl::tests::runNonViralNurseryOnComponentThread( + componentThread, + [profile, &resolvedIdentity]( + sscl::co::NonViralTaskNursery::Slot::Lease& lease) + { + return resolveSelectorCInd( + lease.getExceptionStorage(), + lease.getCallerLambda(), + profile->exampleSelector, + resolvedIdentity); + }); + + EXPECT_EQ(resolvedIdentity.id, profile->libcameraId) + << profile->profileTag; + + sscl::tests::runNonViralNurseryOnComponentThread( + componentThread, + [profile, &createResult]( + sscl::co::NonViralTaskNursery::Slot::Lease& lease) + { + return getOrCreateCInd( + lease.getExceptionStorage(), + lease.getCallerLambda(), + profile->exampleSelector, + createResult); + }); + + EXPECT_EQ( + createResult.resolvedIdentity.id, + resolvedIdentity.id) + << profile->profileTag; + + sscl::tests::runNonViralNurseryOnComponentThread( + componentThread, + [&createResult]( + sscl::co::NonViralTaskNursery::Slot::Lease& lease) + { + return releaseCInd( + lease.getExceptionStorage(), + lease.getCallerLambda(), + createResult.deviceSession); + }); + } + }); +} + } // namespace } // namespace lcamera_dev diff --git a/commonLibs/lcameraDev/tests/selectorResolve_tests.cpp b/commonLibs/lcameraDev/tests/selectorResolve_tests.cpp index b584fbd..309e678 100644 --- a/commonLibs/lcameraDev/tests/selectorResolve_tests.cpp +++ b/commonLibs/lcameraDev/tests/selectorResolve_tests.cpp @@ -45,6 +45,22 @@ TEST(FormatCameraListForDiagnosticsTest, ListsIndexedCameras) EXPECT_NE(text.find("location=front"), std::string::npos); } +TEST(ResolveDeviceSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector) +{ + const std::vector records = + tests::bakedProfilesAsRecords(dellLaptopMachineTag); + + const CameraIdentityRecord resolved = + resolveDeviceSelectorAgainstRecords( + "model-substr:HDMI;location:EXTERNAL", + records); + + EXPECT_EQ( + resolved.id, + "\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415"); + EXPECT_EQ(resolved.locationLabel, "external"); +} + TEST(ResolveSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector) { const std::vector records = diff --git a/docs/lcamera-dev-lib.md b/docs/lcamera-dev-lib.md index 6eb45c0..2c6ded6 100644 --- a/docs/lcamera-dev-lib.md +++ b/docs/lcamera-dev-lib.md @@ -117,15 +117,33 @@ Possible future params (not implemented in v1): ### `lcameraBuff()` stim-buff-api params -Reserved for per-producer options (frame rate caps, pixel format preference). -Channel selection is driven by the qualeIface name, not by stim-buff-api params. +Channel selection is driven by the qualeIface name (`colour-yuv-y/u/v`), not by +stim-buff-api params. Params configure the shared camera mode for all three +attachments on the same device. -Possible future params: +**Resolution (mutually exclusive — use one group only):** -* `fps-hz=30` -* `width=640|height=480` -* `pixfmt=NV12` — negotiation hint for `lcameraBuff` stream setup (not - `lcameraDev`) +| Group | Synonyms | Meaning | +|---|---|---| +| Explicit dimensions | `frame-width`, `frame-w`, `dimension-width`, `dimension-w`, `dim-width`, `dim-w` | Capture width in pixels | +| Explicit dimensions | `frame-height`, `frame-h`, `dimension-height`, `dimension-h`, `dim-height`, `dim-h` | Capture height in pixels | +| Vertical shorthand | `vertical-resolution`, `v-resolution`, `v-res`, `vres` | Preset token: `360`, `360p`, `480`, `480p`, `720`, `720p`, `1080`, `1080p` | + +**Other params:** + +| Synonyms | Meaning | +|---|---| +| `colorspace`, `color-space`, `colourspace`, `colour-space` | Semantic colour model (`yuv` in v1) | +| `full-planar-is-optional`, `opt-planar` | Allow semi-planar or packed YUV when fully planar is unavailable (presence or empty value means enabled; use `=false` to disable) | + +Example: + +```text +lcameraBuff(v-res=480p|colour-space=yuv|opt-planar) +``` + +Stage 2 (`lcameraBuff`) attach/configure/classify is implemented; capture daemon +and frame publication remain future work. ## Device selector format @@ -256,13 +274,13 @@ Rules: (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. +* `fullPlanarIsOptional == true`: may select semi-planar or packed YUV (e.g. + `YUYV` on UVC webcams). `lcameraBuff` classifies the layout and sizes channel + placeholder buffers; OpenCL deinterleave and capture start are later stages. 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. +for `lcameraDev`; `lcameraBuff` uses the configured session for attach-time +setup and will allocate capture buffers / start streaming in later stages. ## dlopen API @@ -298,6 +316,9 @@ struct LcameraDevGetOrCreateResult typedef sscl::co::ViralNonPostingInvoker lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector); +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_resolveDeviceSelectorCReqFn(const std::string& deviceSelector); + typedef sscl::co::ViralNonPostingInvoker lcameraDev_releaseDeviceCReqFn( const std::shared_ptr& deviceSession); @@ -308,6 +329,24 @@ Failures throw `std::exception`. `lcameraBuff` holds the returned session wraps the acquired `libcamera::Camera`; higher layers configure the mode then stream from that handle. +### Selector resolution (resolve-only) + +```cpp +typedef sscl::co::ViralNonPostingInvoker + lcameraDev_resolveDeviceSelectorCReqFn(const std::string& deviceSelector); +``` + +Parses and resolves a `deviceSelector` against the live libcamera camera list +using the same narrowing rules as `getOrCreateDeviceCReq`, but does **not** +create a session, acquire a camera, or change session refcounts. Use this when +a consumer already holds session-scoped state keyed by `CameraIdentityRecord::id` +(for example `lcameraBuff` detach) and only needs the canonical resolved camera +identity from an input DAP line. + +Internally both resolve-only and get-or-create paths share +`resolveDeviceSelectorAgainstRecords()` in `selectorResolve.cpp` and the live +camera snapshot helpers in `cameraManagerState.cpp`. + ### Session mode configuration ```cpp