176 Commits

Author SHA1 Message Date
hayodea 026ba608a1 distro/yocto: add meta-salmanoff Yocto layer for QEMU x86 images
Vendor the BitBake layer (recipes, network config, boost pin, kernel
append, runqemu bridge script) alongside SMO for packaging as a Yocto image.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 16:18:33 -04:00
hayodea 63ff0aa264 cmake: use repo-relative paths for flex/bison #line output
Invoke flex and bison from CMAKE_SOURCE_DIR with relative input and
output paths so generated parser/lexer sources do not embed absolute
build directory paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 15:56:41 -04:00
hayodea a9de2bb1ab bodies: add yocto-qemu-x86-headless DAPSS target
Headless QEMU x86 guest body for Livox-only lab setup at 10.42.0.16.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 15:33:40 -04:00
hayodea 5f4665b221 ComparatorLib:core: Add menu option to conditionally enable 2026-06-14 16:08:18 -04:00
hayodea 99ee28629e lcameraDev: libcamera has minor API difference on ARM/RPi5 2026-06-14 16:01:38 -04:00
hayodea 83698ded42 Bugs,Agent: Update AGENTS.md and bugs.log 2026-06-14 15:35:32 -04:00
hayodea acb684ad35 livoxGen1🐛 Call stop() on all producers before deleting in _exit 2026-06-14 15:34:07 -04:00
hayodea 24eee2d240 Devices: ELP 4K USB cam: add intrins (-&+) 2026-06-14 12:56:52 -04:00
hayodea 8d03dcf9b5 Rename elp cam dev to use hyphens instead of underscores 2026-06-14 11:46:45 -04:00
hayodea 959229c2a0 Add invocations for SMO 2026-06-14 11:43:58 -04:00
hayodea 1431214b95 Devices: Add new ELP 4K USB camera DAPS 2026-06-14 11:29:30 -04:00
hayodea bb83a86fe0 CMake: use GCC dependency generation for DAPSS files 2026-06-14 11:28:51 -04:00
hayodea e261787cfe Add env-gated lcameraBuff configure HIL tests on baked USB profile.
HIL attaches Y/U/V channels, verifies shared producer state, and detaches
using an alternate selector string for the U channel quale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:05:17 -04:00
hayodea 63532a6ee2 Resolve device selector on detach and add YuvStimProducer state tests.
Detach finds the shared producer via lcameraDev_resolveDeviceSelectorCReq
then removes buffers by attach identity; unit tests cover quale guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:04:22 -04:00
hayodea e7b7a311f7 Add lcameraBuff Stage 2 plugin with YUV attach and unit tests.
Introduce params parsing, pixel/format decisions, capture layout, shared
YuvStimProducer per camera, and channel stimulus buffers with attach flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:03:19 -04:00
hayodea 809861be2b StimulusProducer: add duplicate-quale guard and attach-identity buffer lookup.
Provide ensureNoDuplicateQualeIface and getAttachedStimulusBufferByAttachIdentity
so session-scoped stimBuff plugins can reject duplicate quales and detach by
stable DAP line identity rather than full spec equality.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:02:18 -04:00
hayodea 7af684039d lcameraDev: add resolve-only deviceSelector API and deduplicate resolve paths.
Export lcameraDev_resolveDeviceSelectorCReq for attach-identity consumers,
factor live-camera snapshot helpers, and share resolveDeviceSelectorAgainstRecords
with get-or-create session acquisition.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:02:11 -04:00
hayodea 7a47f2bd49 lcameraDev: honor opt-planar when selecting YUV capture format.
Pass fullPlanarIsOptional through session configure so optional planar
mode can succeed with packed YUYV; extend unit and configure HIL coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:01:40 -04:00
hayodea 5f3d5c7818 Refactor LivoxGen1 provider param parsing to shared DAP helpers.
Replace local param parsing with DeviceAttachmentSpec primitives for
consistency with lcameraBuff and intrin threshold modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:01:04 -04:00
hayodea 42c9fcdfdf Refactor intrinThresholdParams to use shared DAP helpers.
Delegate threshold param parsing to the new DeviceAttachmentSpec primitives
so intrin modules stay aligned with stimBuff param conventions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:00:30 -04:00
hayodea b198f6a42b Add shared DeviceAttachmentSpec param parsing helpers.
Centralize DAP param lookup, parsing, and validation primitives so stimBuff
and threshold modules can share one implementation instead of duplicating
parse logic per plugin.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:00:22 -04:00
hayodea 3e85b920fb LCamDev: implement configureSessionModeCReq
We can, theoretically, now change the v4l camera's mode.
2026-06-13 20:56:33 -04:00
hayodea 25d7b9c013 LcamDev: Add baked in camera profiles; use new test supports 2026-06-13 18:50:31 -04:00
hayodea f3ca20ac1d Spinscale: upgrade for new test support primitives 2026-06-13 18:49:10 -04:00
hayodea dd0642535c Upgrade libspinscale: test cleanups 2026-06-13 18:08:47 -04:00
hayodea 10697acd61 Libspinscale: upgrade for new tests 2026-06-13 17:19:34 -04:00
hayodea 4bcc30671b Tests: Move qutex and nursery tests into libspinscale 2026-06-13 16:19:30 -04:00
hayodea 2458c83c6b Tests: add tests for lcameraDev, fix qutex tests 2026-06-13 16:08:21 -04:00
hayodea 46f767f232 lcameraDev: Add session mgr lib for libcamera device binding 2026-06-13 12:02:04 -04:00
hayodea cc7f4fcd9b Update libspinscale 2026-06-13 11:46:53 -04:00
hayodea e383453278 Docs: Add lcameraDev lib notes; add stencil notes 2026-06-11 23:05:17 -04:00
hayodea 69a4782e19 Libspinscale: upgrade 2026-06-11 20:21:41 -04:00
hayodea 5935917204 Docs: Add comparators.md with no content 2026-06-11 13:14:50 -04:00
hayodea a42d8f8a07 Docs: Tech stack for 2d+3d mathobjs 2026-06-11 13:14:16 -04:00
hayodea 54dcc92c2b Printing: print fewer newlines 2026-06-11 11:17:06 -04:00
hayodea e1d299859d Docs: Stencil notes 2026-06-10 22:49:01 -04:00
hayodea d118181766 ApiMgrs: fix segfault from use-after-free 2026-06-10 22:44:53 -04:00
hayodea 82b99e680c ComparatorLibs: Add stringify to hierarchy for easy printing 2026-06-10 22:43:39 -04:00
hayodea 549f0c04f4 Tame clangd
Stop it from running on all CPUs;
Tell it not to index/build the build dirs
2026-06-10 22:05:37 -04:00
hayodea 0722ef8209 Comparators: Rename core comparators lib 2026-06-10 21:43:29 -04:00
hayodea 8836ab470b Wire comparator CLI, marionette threading model, and final load order.
Initialize SmoThreadingModelDesc from marionette before body startup, load
comparator libs before stimbuff via -c/--comparator-lib, and drop the hardcoded
libcomparatorCore.so load path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:17:52 -04:00
hayodea 560e5364a0 Extract SmoCallbacks and SmoThreadingModelDesc into smoHooks.h.
Move shared hook and threading-model types out of senseApiDesc.h so both
stimbuff and comparator libraries can include them without pulling in API descs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:16:55 -04:00
hayodea ab930a2df3 Add ComparatorApiManager with SmoCallbacks hooks and startup load.
Register comparator types via a dedicated manager and expose lookup/create
hooks through SmoCallbacks so stimbuff libs can resolve comparators at load time.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:16:05 -04:00
hayodea 7eda755c15 Add LoadableLibraryManager and refactor StimBuffApiManager to use it.
Centralize dlopen/search in LoadableLibraryManager so typed library managers
can share one loaded-shlib registry without duplicating load/unload logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:14:55 -04:00
hayodea 10234bc422 Add comparator API descriptor and libcomparatorCore scaffold.
Introduce ExportedComparatorTypeDesc/ComparatorLibDesc with inline
sanity checks, and add a core comparator shlib exporting three stub types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:13:19 -04:00
hayodea f118947b5e Extract comparator core types into include/user/comparator.h.
Move Comparator and ComparatorExpression out of cologex.h so comparator
types can be shared by loadable comparator libraries without pulling in
the full cologex surface.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:12:54 -04:00
hayodea cda9d432f4 Spinscale: Upgrade version 2026-06-10 07:03:15 -04:00
hayodea f9c64cf363 livoxProto1: Convert heartbeat sender into daemon coro 2026-06-10 07:02:07 -04:00
hayodea facb665217 BroadcastListener: Port to use nurseries and CDaemon pattern 2026-06-10 05:41:39 -04:00
hayodea 22a4cf283e These should always run on caller's thread. 2026-06-10 04:13:25 -04:00
hayodea 2602094139 StimProd: disable debug msg 2026-06-10 04:12:32 -04:00
hayodea a31a21be65 IoUringAssmEngn: fix Io_uring CQE cancel path 2026-06-09 20:40:39 -04:00
hayodea 4a4c76b5ec Production: log durations and results for debugging 2026-06-09 19:53:48 -04:00
hayodea ad88a5e1c8 CMake: Fix unnecessary build steps 2026-06-09 19:51:51 -04:00
hayodea 87a8de9a2b StimProd,DevReattacher: use CDaemon nonviral nursery coro
We ported these two daemons over to the new nursery mechanism and
they work nicely.
2026-06-09 19:47:44 -04:00
hayodea 165c846700 Spinscale: upgrade docs 2026-06-09 16:49:50 -04:00
hayodea 91fc655b25 Nursery: Initial integration
StimulusProducer: syncAwaitAllSettlements should pump caller io_context
2026-06-09 16:03:25 -04:00
hayodea 5b81ea893c Tests: Add sscl Nursery tests.
We'll evetually move these into sscl.
2026-06-09 05:50:28 -04:00
hayodea b0d67596d0 Libspinscale: Add a nursery 2026-06-09 05:48:08 -04:00
hayodea b2644f17c6 Exceptions: All of smocore likely now uses exceptions 2026-06-07 19:37:50 -04:00
hayodea 241e8a6798 Spinscale: now we can acquire exc_ptr from Group aggregate exceptions 2026-06-07 19:34:17 -04:00
hayodea f97641f8b5 Exceptions: Straight line convention refactor 2026-06-06 19:24:19 -04:00
hayodea 86c036a505 Docs: Update notes on stencils 2026-06-06 19:21:28 -04:00
hayodea 986e1833d0 Docs: add these prompts, I guess 2026-06-06 19:20:43 -04:00
hayodea d4905f53df Docs: Add initial straight-line coding guidelines 2026-06-06 19:18:30 -04:00
hayodea 2c1c994896 Don't auto-throw before callerLambda anymore 2026-06-06 13:04:32 -04:00
hayodea d1c74a027c Use DynamicViralPostingInvoker in at/detachStimBuffDeviceCReq 2026-05-31 07:13:53 -04:00
hayodea c2eea37a7b PostingPromise: reorder post-to target to be 1st arg 2026-05-30 21:46:19 -04:00
hayodea ac39a8b876 Spinscale: add dynamic coro post-to targeting; test on dev reattacher 2026-05-30 20:46:41 -04:00
hayodea 35eb466a60 OClCollMeshEngn,PcloudStimProd: port to sscl::co coros
We've now ported the OpenClCollMeshEngn and PcloudStimProd::produceFrameReq
portions of the Livox pipeline to coros.
2026-05-30 19:32:19 -04:00
hayodea 1cf1be4194 Parameterize XORG_DISPLAY for win0 2026-05-30 12:57:10 -04:00
hayodea acf62e61da Remove boostAsioLinkageFix 2026-05-30 12:12:59 -04:00
hayodea 4266af545a Boost.ASIO: upgrade io_service=>io_context, finally 2026-05-30 12:01:00 -04:00
hayodea f100764bd8 Async: Drop-in SyncCancelerForAsyncWork without execUncancelableSegment*
We're doing this to prep for the coro port
2026-05-30 10:52:15 -04:00
hayodea c7dee57072 Bmach: rseqsliceprobe: improve output 2026-05-30 10:42:47 -04:00
hayodea 2967a4d6ba Require boost between 1.69 and 1.89
Why? Because from boost 1.9 onwards, shlibs are simply not
available anymore.
2026-05-30 10:40:52 -04:00
hayodea 322a8137b2 Revert "LivoxGen1: Use syncCancelerForAsyncWork in producer pipeline"
This reverts commit d788810a05.

We're doing this because it's not necessary. We will be porting to
coros soon and we can just use brace-scopes.
2026-05-30 07:18:29 -04:00
hayodea d788810a05 LivoxGen1: Use syncCancelerForAsyncWork in producer pipeline 2026-05-29 14:10:45 -04:00
hayodea 5a9fe12057 Libspinscale: upgrade to SyncCancelerForAsyncWork 2026-05-29 12:09:47 -04:00
hayodea 25efccf6c5 LivoxProto1: port to sscl::co framework
Code now actually looks a lot cleaner, tbh.
2026-05-28 20:13:12 -04:00
hayodea bbc16dc4c4 Add coding style for LLMs. 2026-05-28 16:33:08 -04:00
hayodea 958c57b3ff livoxGen1: Disable pcloudData IFF stimbuff is last being removed 2026-05-28 15:55:23 -04:00
hayodea fc5ebb72b9 LivoxGen1: Port to coros
No longer uses CPS. We also found and documented a potential bug in
the way we deal with disablePcloudData during detachDeviceReq.
2026-05-28 15:17:50 -04:00
hayodea 7643cf7fed StimBuffApiMgr: set isBeingDestroyed just before calling finalize 2026-05-25 10:40:29 -04:00
hayodea 4186ff141e StimBuffApiMgr: initializeStimBuffApiLib default arg acquires lock 2026-05-25 09:24:49 -04:00
hayodea 3e19d39853 SenseApiDesc,xcbWindow: port to sscl coro framework
SenseApiDesc's exported API now uses coro pointers instead of
CPS fn pointers.
* Do not build this version of SMO with the Livox drivers enabled,
  because SMO has been changed at the smocore level to use coros
  when calling into stimbuffAPI libs. But the Livox drivers
  haven't yet been ported from CPS to coros.

xcbWindow has been ported to expose coros to SMO in its
senseApiDesc exported iface.
2026-05-25 08:58:36 -04:00
hayodea b5fa20a2b8 Mrntt:lifetime: if no devs attach, fail initializeCReq 2026-05-25 08:50:25 -04:00
hayodea baa9b7b499 Fix rseq ext detection on x86 and RPI 2026-05-24 23:26:18 -04:00
latentprion 9feadd0820 RSeq: use portable sys/prctl and not asm/prctl 2026-05-24 23:26:18 -04:00
hayodea f9ac41c56e Update libspinscale, add debug printing for pcloudStimProd 2026-05-24 23:26:18 -04:00
hayodea cde2737876 Libspinscale: Initial top-level SMO port to coroutine framework
We haven't ported everything. Just the top-level methods. We'll
dig in to the leaf stuff later. Surprisingly, this all went without
any real difficulties.

Runs like a charm on first try.
2026-05-24 23:26:18 -04:00
hayodea c539e6e924 Add awaiters for DeviceManager continuations 2026-05-24 23:26:18 -04:00
hayodea 72134aeac5 Buildmach: Add Linux rseq support detection utility 2026-05-06 02:47:15 -04:00
hayodea ea3fc4873c README: Update to reflect that this is no longer an ROS rewrite 2026-04-18 15:40:55 -04:00
latentprion eae01c2d4c DAPS:Rpi5: Don't bind to XcbWindow on headless 2026-04-18 15:25:10 -04:00
hayodea 479229d0da Lg1:pcloudStimProd: Add ambience trigger debugging 2026-04-18 15:21:40 -04:00
hayodea 0bc8bfea3d DAPS:avia0: Remove trailing line ender 2026-04-18 15:05:38 -04:00
hayodea 66dc227d31 CMake: Add clean target for DAPS specs 2026-04-18 15:05:16 -04:00
hayodea 27a5d48451 Lg1: Implement both light|darkAmbience stimBuffs & their production
We now produce both light and dark ambience stimframes into
stimbuffs for the LivoxGen1 lidar devices.
2026-04-18 14:54:14 -04:00
hayodea 632a227985 DAPS: Add intrin specs to nontrin specs
We no longer do intrin specs as a separate stimbuff; rather now we
do them as a specifier segment within the pipeline spec of a normal
nontrin spec.
2026-04-18 12:02:27 -04:00
hayodea fc1fcae0b0 DAP: Add intrin DAPSpecs
We now specify intrins as separate DAPS lines. This syntax is much
nicer and well-grouped than the previous negtrin-*/postrin-* param
names.

Alas, we're about to replace it in the next few commits already though.
2026-04-12 04:06:47 -04:00
hayodea c696316a1e Rename buildmach stuff to buildmach/ 2026-04-04 13:25:28 -04:00
hayodea e8044a0d17 OClCollMeshEngn: produce ambience into stimBuff frames directly 2026-04-04 13:17:43 -04:00
hayodea 1c0f028de0 Genericized intrin parsing from Ambience 2026-04-04 00:44:48 -04:00
hayodea 7516da6aa8 Api improvements: ambience-count-[l|g]t-val and Comparator 2026-04-03 23:54:22 -04:00
hayodea bedcf78b29 Update AGENTS.md 2026-04-03 23:40:44 -04:00
hayodea 3dd627c91c Implement new intrin params; fix parseSynonyms
Previously parseSynonyms didn't properly return the last provided
synonym of the param in question, so fix it here to do that.
2026-04-03 23:14:23 -04:00
hayodea 5811af7cb1 Docs: Intrins now require units. 2026-04-03 21:57:41 -04:00
hayodea 156da322b6 Add rudimentary pcloud dumper and meshing with OFM & GP3
The OFM algo runs in fractions of a millisecond. GP3 runs in
fractions of a second. I think if we can get more input data to
the OFM or something akin to it, we will have a winner.
2026-04-03 21:23:29 -04:00
hayodea 7435c6e393 Move prompts into docs 2026-04-02 23:30:55 -04:00
hayodea 88513c0f4d Prompts: add new prompts dir for specs 2026-04-02 23:30:19 -04:00
hayodea ba76bb0b00 Silence this debugging spam 2026-04-02 22:48:07 -04:00
latentprion 8116d19741 DAPS: Add headless RPi5 target 2026-04-02 04:17:43 -04:00
hayodea 1d64ce0c7e StagingBuff: support both Mlock & IOUring pin; Use in IoUAssmEngn
We use io_uring_register_buffers() for IoUringAssemblyEngine instead
of using mlock(). This __appears__ to have reduced CPU utilization on
the Dell laptop. Could also be that we recently upgraded total RAM
from 8GiB to 32GiB.
2026-04-02 03:51:22 -04:00
hayodea 26dd686ebf PCloudStimBuff: call IoUringAssemEngn.finalize when setup fails 2026-04-02 03:45:18 -04:00
hayodea 06996d166e Opts: Solve OptionsParser::Exception diamond inheritance problem 2026-04-02 01:28:37 -04:00
hayodea 3f2d7c24ee StimBuff: Move supportsQuleIfaceApi into base class 2026-04-01 23:17:10 -04:00
hayodea cbf9d418b7 Add AGENTS.md 2026-04-01 21:56:47 -04:00
hayodea bfaba8cc0e Improve CPack deb generation 2026-03-06 03:03:10 -04:00
hayodea a1fd39eb05 Improve CPack deb generation 2026-03-06 01:12:46 -04:00
hayodea c90f974bcb DevMgr: LockSet is no longer a template 2026-03-05 23:45:53 -04:00
hayodea aec3cbedf2 Split: Expose name via CompThr's derivatives' ctors
This completes the functional work of splitting libspinscale off
from SMO. Spinscale shouldn't have any real dependencies on SMO
from here on out.
2026-02-22 18:54:56 -04:00
hayodea 1c397dfeb5 Split: Split libspinscale off from SMO.
Now we can probably begin using libspinscale in Couresilient
without worrying about excessive technical debt later on.
2026-02-22 17:46:27 -04:00
hayodea 9361a43e40 New libspinscale version 2026-02-19 19:54:41 -04:00
hayodea 8011fe12bc mindThread: remove parent ref; might remove class entirely 2026-02-19 19:20:21 -04:00
hayodea ab399cafeb Improve CONFIG_WORLD_USE_BODY_THREAD handling
We no longer need to do ugly ifdefs in the constructor for
Mind::Mind because we preprocess the handling for BODY and WORLD
in SmoThreadIds.
2026-02-18 02:05:44 -04:00
hayodea 687bab53b5 Change type: PuppetComponent::thread to PuppetThread 2026-02-18 02:05:18 -04:00
hayodea 9159e9f7b4 Update: rename mrntt=>pptr 2026-02-18 01:14:26 -04:00
hayodea c0752b5e84 Move SequenceLock into libspinscale 2026-02-17 11:19:53 -04:00
hayodea 686bd6d38b Dbg:TraceCallables: set cmdline flag for libspinscale 2026-02-17 11:10:24 -04:00
hayodea e4adfa0e61 Main: move CRT cmdline obj into libspinscale 2026-02-17 10:41:47 -04:00
hayodea 4520306f4e Update libspinscale gitmodule 2025-12-28 04:23:10 -04:00
hayodea c08563c8e8 Convert libspinscale to git submodule 2025-12-28 03:58:06 -04:00
hayodea 5a4f498663 Libspinscale: Add separate CMake project config 2025-12-28 03:44:01 -04:00
hayodea 7acdfcc337 Revert "Use ref in ComponentThread::joltThreadReq"
This reverts commit 2222491c21.

The thread lifetime ops need to use sh_ptrs because apparently the
thread objects go out of scope at some point during shutdown, before
the threads can actually finish shutting down.
2025-12-27 18:02:54 -04:00
hayodea 34d76df7d9 Spinscale: create new namespace sscl 2025-12-27 16:21:22 -04:00
hayodea 0c4f427c0a Spinscale: PuppetComponent takes PuppetApplication& 2025-12-27 14:15:17 -04:00
hayodea f862db922e spinscale: Move thread init/jolt/exit logic into PuppetApplication 2025-12-27 14:01:15 -04:00
hayodea cd77f4b02d Component: Rename MindComponent=>PuppetComponent 2025-12-27 13:29:49 -04:00
hayodea 2222491c21 Use ref in ComponentThread::joltThreadReq 2025-12-26 13:46:28 -04:00
hayodea bfe5eb12af Remove unnecessary includes 2025-12-26 02:59:54 -04:00
hayodea b6cf1c656f Dampen warning 2025-12-26 02:52:05 -04:00
hayodea 45959f9d1c Libspinscale: begin splitting it off 2025-12-26 01:18:39 -04:00
hayodea d5c2b61d4c Turn off these annoying type annotating hints 2025-12-25 19:22:22 -04:00
hayodea 2dc6b729e0 Update for Autogoalation 2025-12-25 19:21:47 -04:00
hayodea d39bc4b475 Add these partial fixes for Windows WSL 2025-12-25 19:21:30 -04:00
hayodea 6e89c7e72f Disable prints 2025-12-07 19:49:32 -04:00
hayodea 7167cea62c OClCollMeshEng: Use RAII for unmapBuffer() event destruction 2025-12-07 19:29:20 -04:00
hayodea 702855a27d OClCollMeshEngn: Use uniq_ptr for Cl handle RAII
We no longer use a goto slide to deal with initialization errors
in setup()/finalize(). We use RAII instead.
2025-12-07 19:12:26 -04:00
hayodea dc5587bfcc Debug: Silence excessive prints 2025-12-02 16:23:29 -04:00
hayodea 5dffbd0c91 PcloudAmbienceStimBuff: Updated to use postrin percentage 2025-12-02 16:17:46 -04:00
hayodea 7ebdf14eb7 CMake: Document install requirements 2025-12-02 16:11:17 -04:00
hayodea 4f3462626d VSCode: move this into user config 2025-12-02 15:34:51 -04:00
hayodea e06b2d7e06 PcloudAmbienceStimBuff: Parse postrinThreshold as percentage 2025-12-02 14:15:43 -04:00
hayodea 33681059b0 Restore logic.h which was unintentionally deleted previously 2025-12-02 14:15:38 -04:00
hayodea 30f599cde3 Rename RangeDescriptor::bodySpot=>stimulusBufferSpot 2025-11-28 14:57:53 -04:00
hayodea 0116523a66 LG1PCloudAmbStncl: Use RangeDescriptor obj instead of StagingBuffer
We directly use an instance of RangeDescriptor to avoid incurring
the memory cost of using a StagingBuffer here. It's unnecessary
since these stencils will always be 32bits large.
2025-11-28 03:34:35 -04:00
hayodea 1f35dba2ca OClCollMeshEngn: use proper alignment for small StagingBuffer size
Aligning to uint32_t saves about 1 page of mem?
2025-11-28 03:13:55 -04:00
hayodea 280b6f7d1c OClCollMeshEngn: Produce ambience count; set postrin threshold
We modify the semantics/meaning of the ambience stim feature.
It now represents the number of frames whose average intensity
is below the ambienceLowVal.

We can now implement the postrin as the event wherein the number
of frames whose intensity <= ambienceLowVal exceeds
postrin-interest-threshold.
2025-11-28 02:55:24 -04:00
hayodea 5b19a70c75 Todo: document LivoxProto1/Gen1 port demux needs 2025-11-27 22:56:36 -04:00
hayodea 2a8d320f7a DevReattacher: Spinlock-protect stop() call
Replace the current delay timeout mechanism with a spinlock.
Both mechanisms try to eliminate the possibility of an in-flight
async op accessing state that has been destroyed by stop().

But the spinlock is less arbitrary.
2025-11-27 22:52:09 -04:00
hayodea 1e76d51c41 Todo: Update
By solving the issues in finalize() for IoUringAssmEngn and
OClCollMeshEngn, we've solved this as a side-effect.
2025-11-27 22:29:05 -04:00
hayodea 313454c426 OClCollMeshEngn: Add bridged delay in finalize()
See the diff of the todo file within this commit for more details.

In short, we do this to prevent the possibility of an in-flight async
contin accessing metadata that we've already destroyed after finalize()
has been called.
2025-11-27 22:26:50 -04:00
hayodea d49594ef88 IoUringAssmEngn: Add ~16ms bridged delay in finalize()
See the diff of the todo file within this patch for more
details.

This is to eliminate the possibility of having an in-flight async
contin access metadata that we destroyed in finalize().
2025-11-27 22:24:48 -04:00
hayodea e51d371f58 LG1PclAmbienceStencil: allocate stencils in constructor 2025-11-26 13:00:24 -04:00
hayodea 8eb7eaba3d Add PcloudAmbienceStencil, LG1PcloudAmbienceStencil
These two classes represent our first foray into stencil
construction. One of them standardizes PcloudAmbience stencils
across all stimbuffs, and the other specifies the internal
memory constraints and requirements for a LivoxGen1 device's
stencils.
2025-11-26 12:32:42 -04:00
hayodea ce0456d472 Docs: Add stencil notes 2025-11-26 11:16:28 -04:00
hayodea 49191b3a15 PcloudStimProd: ambienceHighVal is now a negtrin threshold 2025-11-26 03:39:39 -04:00
hayodea e5782b0af7 LivoxGen1: Add postrin thresholds (0, 10, and 30) 2025-11-26 03:34:47 -04:00
hayodea b6c426871e Docs: add params for postrin/negtrin thresholds 2025-11-26 03:33:38 -04:00
hayodea 5cce473e01 PcloudStimProd: make sh_ptrs to Pcloud*StimBuff atomic<>
This change is a bit pedantic, but since these vars aren't accessed
in any hotpath, it's fine to be pedantic. We made these sh_ptrs
atomic so we can use acquire and release side effects when loading
and storing them. This doesn't eliminate the problem of seeing
inconsistent state across microcontrollers, but it helps with simple
accesses like these ones we already do.
2025-11-26 00:14:15 -04:00
hayodea edab71b823 Todo: update 2025-11-23 23:13:23 -04:00
hayodea 3b8c1dac32 Stencil: use better examples in documentation comment 2025-11-23 23:12:49 -04:00
hayodea b0df1ef3d0 [Pcloud]StimProducer: Impl addAttachedStimBuffIfNotExists
Reduces code duplication, centralizes checking and enforces consistent
behaviour across producers.

Also reordered the writes to the sh_ptr<StimulusBuffer>s such that
the pointers are written last.
2025-11-23 23:11:23 -04:00
hayodea 617020b534 OClCollMeshEngn: Print values >= 116; not only > 2025-11-23 13:30:04 -04:00
hayodea d01e448b40 Git:ignore: ignore .cache dir with clangd metadata 2025-11-23 13:29:19 -04:00
260 changed files with 20908 additions and 10245 deletions
+11
View File
@@ -0,0 +1,11 @@
# Project clangd configuration.
# Worker thread count (-j=1) is a clangd CLI flag; see .vscode/settings.json.
CompileFlags:
CompilationDatabase: build-agent
---
If:
PathMatch: (build/.*|b/.*|build-[^/]+/.*|b-[^/]+/.*)
Index:
Background: Skip
+2
View File
@@ -13,3 +13,5 @@ configure
*.swp
cscope.out
*.tmp
.cache
.codex
+3
View File
@@ -1,3 +1,6 @@
[submodule "third_party/googletest"]
path = third_party/googletest
url = https://github.com/google/googletest.git
[submodule "libspinscale"]
path = libspinscale
url = git@zbz-gitea-as-latentprion:latentprion/libspinscale
+5 -1
View File
@@ -96,7 +96,6 @@
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"editor.inlayHints.enabled": "off",
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
"C_Cpp.default.browse.limitSymbolsToIncludedHeaders": true,
"C_Cpp.default.browse.path": [
"${workspaceFolder}",
@@ -108,5 +107,10 @@
"${workspaceFolder}/b/include",
"/usr/include",
"/usr/local/include"
],
"clangd.arguments": [
"--enable-config",
"-j=1",
"--compile-commands-dir=${workspaceFolder}/build-agent"
]
}
+81
View File
@@ -0,0 +1,81 @@
# Project Instructions
- Always break functions into logical subfunctions. No long-scrolling functions, in any language. This applies to source code, scripts, build scripts, CMake, Makefiles, and similar project files. Preserve this subfunction splitting discipline during refactors.
- Modularity is non-negotiable. Always group logically related functions together into a module. Preserve modularity during refactors.
- Reuse or extend existing abstractions instead of duplicating logic wherever possible. Don't repeat yourself. The goal here is to prevent duplication. Not to discourage appropriate logical separation of prior abstractions into new logical abstractions where sensible.
- Always isolate configurable behaviour into configuration variables appropriate for the language and framework being used.
- Never bake in literals; at minimum, declare them at the top of the file with a semantically meaningful name.
- Don't add unnecessary getters and setters in classes. Just access member vars directly.
- Don't use hungarian notation in var names. Class member vars should not have prefixed or postfixed underscores. Instead do the inverse: use prefixed or postfixed underscores for auto-scoped var names when they shadow static or member var names.
- UI should be responsive. Always prefer to use pre-packaged UI toolkit widgets, containers and colour sets harmoniously, instead of writing custom CSS overrides. Write custom CSS only if there's no UI toolkit mechanism available.
- Aggressively isolate, split off, deduplicate and reuse code which can be made into common library code. Do the same with UI elements. Do this both when implementing new features and opportunistically while refactoring or changing old code/UI elements.
- Names of files, functions, classes, abstractions, database fields, etc should be aimed at disambiguating purpose and function, rather than at brevity.
- Any source or header file that includes a Boost header must include `<boostAsioLinkageFix.h>` first (at the top of the file, or immediately after the include guard in headers), before all other includes, so Boost.Asio is used as a non-header-only library correctly.
- When refactoring code, moving code around or splitting code into new files, don't omit or remove source code comments. Preserve source code comments across refactors.
- Do not omit or remove code comments when refactoruing code. Preserve code comments.
## Style:
* Project uses 4-char-width tabs for indentation.
* UpperCamelCase for class/struct names, lowerCamelCase for var and member var
names; underscores_between_words for namespace names. No hungarian notation.
* Differentiate overshadowing local arg var names from class member var names
by prefixing local arg var names with underscore (_). Don't do things like
postfixing the local arg var name with "In", etc.
* Single-line blocks after a selection/iteration statement must always be
enclosed in braces, but you can choose whether opening brace is on same line,
versus whether you put both braces on same line. I.e:
```
if (...) {
// Acceptable
}
if (...)
{ /* Also acceptable */ }
if (...)
// But never do this anywhere.
```
* Multiline brace blocks always have opening brace on next line.
* Classes/structs:
```
class/struct MyClass
// Opening brace always on next line.
{
public:
void function();
/* Member vars always go at the bottom of the class.
* always put a visibility marker before member vars begin.
*/
public:
int memberVar1, memberVar2;
/* When a member var's value is agnostic of its construction signature,
* use assignment in the class def to reduce code duplication and error
* surface.
*/
int memberVarWhoseDefaultValIsAgnosticOfConstruction = AGNOSTIC_DEFAULT_VAL;
};
class/struct MyClass
// Base classes have tab after colon.
: public MyClassBase1, public MyClassBase2,
// Tabulation persisted when base classes spill onto multiple lines.
public MyClassBase3
{
};
```
* Constructors:
```
MyCtor()
// No tab after colon for member vars.
: memberVar1(...), memberVar2(...)
// If ctor body empty, just do braces together on their own line.
{}
MyCtor()
// Base classes have a tab after the colon.
: MyCtorBase1(...), MyCtorBase2(...),
// Base class tabulation continues for multi-line situations.
MyCtorBase3(...)
memberVar1(...), memberVar2(...)
{}
```
+55 -22
View File
@@ -1,10 +1,14 @@
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.19)
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
project(salmanoff VERSION 0.01.001 LANGUAGES CXX)
include(CMakeDependentOption)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DAPSS.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DebugOpts.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake)
include(GNUInstallDirs)
# Set C++ standard
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -19,6 +23,13 @@ endif()
# Compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
# Ensure installed directories use Debian-standard permissions instead of
# inheriting group-writable bits from the build tree.
set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
# Mind oscillator configuration
set(MIND_VOSCILLATOR_PERIOD_MS 33 CACHE STRING "Mind's virtual osc clock rate (ms)")
if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
@@ -42,14 +53,6 @@ if(NOT STIMBUFF_FRAME_PERIOD_MS GREATER 0)
"STIMBUFF_FRAME_PERIOD_MS must be a positive integer > 0")
endif()
# Stimulus buffer frame retry delay configuration
set(STIMBUFF_FRAME_RETRY_DELAY_MS 1
CACHE STRING "Stimulus buffer frame retry delay (ms)")
if(NOT STIMBUFF_FRAME_RETRY_DELAY_MS GREATER 0)
message(FATAL_ERROR
"STIMBUFF_FRAME_RETRY_DELAY_MS must be a positive integer > 0")
endif()
# World thread configuration
option(WORLD_USE_BODY_THREAD
"Use body thread for world component instead of separate world thread" OFF)
@@ -79,8 +82,6 @@ endif()
set(CONFIG_DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS ${DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS})
# Set the stimulus buffer frame period variable for config.h
set(CONFIG_STIMBUFF_FRAME_PERIOD_MS ${STIMBUFF_FRAME_PERIOD_MS})
# Set the stimulus buffer frame retry delay variable for config.h
set(CONFIG_STIMBUFF_FRAME_RETRY_DELAY_MS ${STIMBUFF_FRAME_RETRY_DELAY_MS})
# Configure config.h
configure_file(
@@ -103,13 +104,27 @@ include_directories(
# its own copy of boost::asio's metadata, which will cause a segfault if
# boost::asio objects are used inside of a dlopen()'d library.
#
# Honestly, I never liked this whole "header-only" idea so I'm happy to be rid
# of it.
# Boost.System became header-only in Boost 1.69, and Boost 1.89 removed the
# compiled stub library that older package sets still exposed as
# libboost_system. This project intentionally links Boost.System as a shared
# library to keep one Boost symbol set across the main binary and dlopen()'d
# libraries, so Boost 1.89+ is not acceptable for this build.
#
# Tell CMake we're linking against the shared library (not header-only)
# Tell CMake we're linking against shared libraries, not static Boost libraries.
set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_HEADER_ONLY OFF)
find_package(Boost REQUIRED COMPONENTS system log)
find_package(Boost 1.69...<1.89 REQUIRED)
if(Boost_VERSION VERSION_GREATER_EQUAL "1.89.0")
message(FATAL_ERROR
"Boost ${Boost_VERSION} was found, but salmanoff requires Boost < 1.89 "
"because Boost 1.89 removed the dynamically linkable Boost.System "
"library. Install a Boost 1.69 through 1.88 development package set "
"that provides Boost::system as a shared library. On Ubuntu 26.04, "
"install libboost1.88-dev, libboost-system1.88-dev, and "
"libboost-log1.88-dev, then clear this build directory's CMake cache "
"or configure with -DBoost_DIR=/usr/lib/<multiarch>/cmake/Boost-1.88.0.")
endif()
find_package(Boost 1.69...<1.89 REQUIRED COMPONENTS system log)
# Define BOOST_ALL_DYN_LINK project-wide to ensure all Boost libraries use dynamic linking
add_compile_definitions(BOOST_ALL_DYN_LINK)
@@ -117,6 +132,11 @@ find_package(PkgConfig REQUIRED)
find_package(FLEX REQUIRED)
find_package(BISON REQUIRED)
# It's important to note that as of 2025-12-02, RustICL on the RPi5 suddenly
# began requiring that the user running Smo be a member of the "render" group.
# We need to take that into account when we eventually build installer packages.
# Users may also need to be members of the "video" group.
# Find OpenCL 1.2 or higher: try find_package first, fall back to pkg-config
find_package(OpenCL 1.2 QUIET)
if(OpenCL_FOUND)
@@ -169,12 +189,16 @@ 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(compile)
add_subdirectory(buildmach)
add_subdirectory(libspinscale)
# Add core components
add_subdirectory(smocore)
add_subdirectory(commonLibs)
add_subdirectory(comparatorLibs)
add_subdirectory(stimBuffApis)
add_subdirectory(wilzorApis)
add_subdirectory(devices)
@@ -197,6 +221,7 @@ add_custom_command(TARGET salmanoff POST_BUILD
# Add all registered DAPSS targets as dependencies
add_all_daps_dependencies()
add_daps_clean_target()
# Add tests if enabled
if(ENABLE_TESTS)
@@ -204,21 +229,29 @@ if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
install(TARGETS salmanoff DESTINATION bin)
install(TARGETS salmanoff DESTINATION ${CMAKE_INSTALL_BINDIR})
# Install device configuration files (preprocessed .daps files)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/devices/
DESTINATION share/salmanoff/devices
DESTINATION ${CMAKE_INSTALL_DATADIR}/salmanoff/devices
DIRECTORY_PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
FILE_PERMISSIONS
OWNER_READ OWNER_WRITE
GROUP_READ
WORLD_READ
FILES_MATCHING PATTERN "*.daps"
)
# Install documentation
install(FILES README.md DESTINATION share/doc/salmanoff)
install(FILES LICENSE DESTINATION share/doc/salmanoff)
install(FILES README.md DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# Install example configurations if they exist
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/examples")
install(DIRECTORY examples/ DESTINATION share/salmanoff/examples)
install(DIRECTORY examples/ DESTINATION ${CMAKE_INSTALL_DATADIR}/salmanoff/examples)
endif()
# Include CPack configuration
+3 -1
View File
@@ -4,7 +4,9 @@
<img src="docs/img/salmanoff-logo-512.png" alt="Salmanoff project logo" />
</p>
This project, Salmanoff (pronounced: Sal-man-off), is an ROS rewrite of the Harikoff project. The name is more reflective of the people whose ideas sparked the solutions in my mind. These people are:
This project is Salmanoff (pronounced: Sal-man-off, previously known as Harikoff)).
The new name is more reflective of the people whose ideas sparked the solutions in my mind.
These people are:
* Gregory `SAL`mieri.
* David Harri`MAN`.
* Leonard Peik`OFF`.
+35 -80
View File
@@ -1,86 +1,41 @@
# Bug somehow related to either OpenClCollateAndMeshingEngine or PcloudStimBuff:
# Bug 2: SIGINT before successful frame production results in "Corrupted double-linked list" error:
printSlotBytes: Slot 21 vaddr=0xfffff7fb4000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 22 vaddr=0xfffff7fb5000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 23 vaddr=0xfffff7fb6000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 24 vaddr=0xfffff7fb7000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 25 vaddr=0xfffff7fb8000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 26 vaddr=0xfffff7fb9000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 27 vaddr=0xfffff7fba000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 28 vaddr=0xfffff7fbb000 (4 bytes):
0000: 05 01 01 00 |....|
printSlotBytes: Slot 29 vaddr=0xfffff7fbc000 (4 bytes):
0000: 05 01 01 00 |....|
produceFrameReq2_assembleDone: Successfully assembled frame 29 slots succeeded out of 30 total slots
compactCollateAndMeshFrameReq: Started compact kernel
startKernel: already running, call stop() first
produceFrameReq3_compactCollateDone: Failed to compact and collate frame
## Details:
Oddly, this only happened in GDB; not when running the program normally.
## Logs:
```
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
AssembleFrameReq: no_slots_succeeded callbackSuccess=0 timerFired=yes nSucceeded=0 nFailed=0 nTotal=84 trackerAssembled=0
stimFrameProductionTimesliceCInd: Failed to assemble frame.
Mrntt: About to detach all sense devices.
xcbWindow_detachDeviceReq: Detached X11 window device:
xcbWindow_detachDeviceCReq: Detached X11 window device:
Device Identifier: win0, Sensor Type: e, QualeIface API: visual-qualeiface, QualeIface API Params: (), StimBuff API: xcb, StimBuff API Params: (dev-substring ), Provider: xorg, Provider Params: (display=1 screen=0 ), Device Selector: mut
enDisablePcloudDataReq2: Command timeout for device 3JEDK380010Z39
detachDeviceReq1: Failed to disable pcloud data for stimbuff 3JEDK380010Z39
stop: Stopped stimulus buffer for device 3JEDK380010Z39
disconnectReq: Sent disconnect message to 10.42.0.139:65000
detachDeviceReq2: Successfully detached pcloud stimbuff for device 3JEDK380010Z39 and possibly also destroyed device.
Mrntt: Successfully detached 2 of 2 sense devices.
discardHeartbeatAck: Lidar not ready for operation: work_state: 0x0 (Initializing), ack_msg: 0x45
Mrntt: Successfully detached 4 of 4 sense devices.
Mrntt: About to finalize all stim buff api libs.
stop: UDP Command Demuxer stopped
stop: BroadcastListener stopped
[Thread 0x7fffb9ff36c0 (LWP 611384) exited]
corrupted double-linked list
Thread 9 "rusticl queue t" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xffffca4ee140 (LWP 11695)]
0x0000fffff48517b0 in std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)>::_Bind(std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&) (
this=0xffffdc000c70) at /usr/include/c++/13/functional:581
581 _Bind(const _Bind&) = default;
(gdb) bt
#0 0x0000fffff48517b0 in std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)>::_Bind(std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&) (this=0xffffdc000c70) at /usr/include/c++/13/functional:581
#1 0x0000fffff4851818 in std::_Function_base::_Base_manager<std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> >::_M_create<std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&>(std::_Any_data&, std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&, std::integral_constant<bool, false>) (__dest=..., __f=...)
at /usr/include/c++/13/bits/std_function.h:161
#2 0x0000fffff4850704 in std::_Function_base::_Base_manager<std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr--Type <RET> for more, q to quit, c to continue without paging--c
<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> >::_M_init_functor<std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&>(std::_Any_data&, std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> const&) (__functor=..., __f=...) at /usr/include/c++/13/bits/std_function.h:215
#3 0x0000fffff484fbf0 in std::_Function_base::_Base_manager<std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation) (__dest=...,
__source=..., __op=std::__clone_functor) at /usr/include/c++/13/bits/std_function.h:198
#4 0x0000fffff484f0bc in std::_Function_handler<void (int), std::_Bind<void (smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq::*(smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq*, std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, std::_Placeholder<1>))(std::shared_ptr<smo::stim_buff::OpenClCollatingAndMeshingEngine::CompactCollateAndMeshFrameReq>, int)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation) (__dest=...,
__source=..., __op=std::__clone_functor) at /usr/include/c++/13/bits/std_function.h:282
#5 0x0000fffff484f2b0 in std::function<void (int)>::function(std::function<void (int)> const&) (this=0xffffca4ecd40, __x=...) at /usr/include/c++/13/bits/std_function.h:391
#6 0x0000fffff484e9c0 in std::_Bind<std::function<void (int)> (int)>::_Bind<int&>(std::function<void (int)> const&, int&) (this=0xffffca4ecd40, __f=...)
at /usr/include/c++/13/functional:572
#7 0x0000fffff484e170 in std::bind<std::function<void (int)>&, int&>(std::function<void (int)>&, int&) (__f=...) at /usr/include/c++/13/functional:885
#8 0x0000fffff484aa68 in smo::stim_buff::OpenClCollatingAndMeshingEngine::compactKernelEventCallback (event_command_exec_status=0, user_data=0xffffe4009e80)
at /home/latentprion/gits/salmanoff-git/stimBuffApis/livoxGen1/openClCollatingAndMeshingEngine.cpp:249
#9 0x0000ffffcb3e34b4 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#10 0x0000ffffcb3d173c in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#11 0x0000ffffcb3d1d28 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#12 0x0000ffffcb3b0b34 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#13 0x0000ffffcb40886c in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#14 0x0000ffffcb39a728 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#15 0x0000ffffcb39a7b0 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#16 0x0000ffffcb3b0a40 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#17 0x0000ffffcb3b130c in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#18 0x0000ffffcb3d2dfc in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#19 0x0000ffffcb371148 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#20 0x0000ffffcb3f9b40 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#21 0x0000ffffcb3713c8 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#22 0x0000ffffcb378988 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#23 0x0000ffffcb37120c in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#24 0x0000ffffcb371000 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#25 0x0000ffffcb392888 in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#26 0x0000ffffcb45f23c in ?? () from /lib/aarch64-linux-gnu/libRusticlOpenCL.so.1
#27 0x0000fffff7ac595c in start_thread (arg=0xfffff58cf880) at ./nptl/pthread_create.c:447
#28 0x0000fffff7b2bb0c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone3.S:76
Thread 14 "body" received signal SIGABRT, Aborted.
[Switching to Thread 0x7fffbbff76c0 (LWP 611380)]
Download failed: Invalid argument. Continuing without source file ./nptl/./nptl/pthread_kill.c.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
warning: 44 ./nptl/pthread_kill.c: No such file or directory
(gdb)
## Race conditions in OClCollMeshEngn:
engine not set up or invalid
```
@@ -1,4 +1,10 @@
cmake_minimum_required(VERSION 3.16)
project(buildmach CXX C)
option(COMPILE_CL_CHECKS "Compile CL checks" OFF)
option(COMPILE_PCL_TOOLS "Compile PCL-based validation tools" ON)
add_executable(rseqsliceprobe rseqsliceprobe.cpp)
if(COMPILE_CL_CHECKS)
# Find OpenCL: try find_package first, fall back to pkg-config
@@ -55,3 +61,18 @@ if(COMPILE_CL_CHECKS)
target_link_libraries(clzerocopycheck
${OPENCL_LIBRARIES})
endif()
if(COMPILE_PCL_TOOLS)
enable_language(C)
find_package(MPI REQUIRED COMPONENTS C)
find_package(PCL QUIET COMPONENTS common io surface features search kdtree)
if(PCL_FOUND)
add_executable(meshFromPcd meshFromPcd.cpp)
target_include_directories(meshFromPcd PUBLIC ${PCL_INCLUDE_DIRS})
target_link_directories(meshFromPcd PUBLIC ${PCL_LIBRARY_DIRS})
target_link_libraries(meshFromPcd ${PCL_LIBRARIES})
target_compile_options(meshFromPcd PRIVATE ${PCL_DEFINITIONS})
else()
message(WARNING "PCL not found; skipping meshFromPcd build")
endif()
endif()
+700
View File
@@ -0,0 +1,700 @@
#include <chrono>
#include <cstdint>
#include <cmath>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
#include <pcl/PolygonMesh.h>
#include <pcl/common/io.h>
#include <pcl/features/normal_3d.h>
#include <pcl/io/pcd_io.h>
#include <pcl/io/vtk_io.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/search/kdtree.h>
#include <pcl/surface/gp3.h>
#include <pcl/surface/organized_fast_mesh.h>
namespace {
enum class MeshAlgorithm
{
Ofm,
Gp3,
};
struct ToolOptions
{
std::vector<std::filesystem::path> inputPaths;
std::filesystem::path outputDirectory;
bool hasOutputDirectory = false;
MeshAlgorithm algorithm = MeshAlgorithm::Ofm;
int ofmTrianglePixelSize = 1;
float ofmMaxEdgeLength = 0.25f;
pcl::OrganizedFastMesh<pcl::PointXYZ>::TriangulationType ofmTriangulationType =
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_ADAPTIVE_CUT;
double gp3SearchRadius = 0.05;
double gp3Mu = 2.5;
int gp3MaxNeighbors = 100;
double gp3MaxSurfaceAngleDeg = 45.0;
double gp3MinAngleDeg = 10.0;
double gp3MaxAngleDeg = 120.0;
int gp3NormalK = 20;
bool gp3NormalConsistency = false;
};
struct ConversionStats
{
std::filesystem::path inputPath;
std::filesystem::path outputPath;
MeshAlgorithm algorithm = MeshAlgorithm::Ofm;
std::size_t pointCount = 0;
std::size_t finitePointCount = 0;
std::size_t polygonCount = 0;
double normalDurationMs = 0.0;
double meshDurationMs = 0.0;
double totalDurationMs = 0.0;
bool success = false;
std::string errorMessage;
};
constexpr double kPi = 3.14159265358979323846;
constexpr std::size_t kMinimumTriangulationPoints = 3;
double degreesToRadians(double degrees)
{
return degrees * kPi / 180.0;
}
std::string algorithmToString(MeshAlgorithm algorithm)
{
switch (algorithm)
{
case MeshAlgorithm::Ofm:
return "ofm";
case MeshAlgorithm::Gp3:
return "gp3";
}
throw std::runtime_error("Unsupported mesh algorithm enum");
}
void printUsage(const char* argv0)
{
std::cerr
<< "Usage: " << argv0
<< " [--algorithm ofm|gp3]"
<< " [--output-dir DIR]"
<< " [--ofm-triangle-pixel-size N]"
<< " [--ofm-max-edge-length F]"
<< " [--ofm-triangulation adaptive|left|right|quad]"
<< " [--gp3-search-radius F]"
<< " [--gp3-mu F]"
<< " [--gp3-max-neighbors N]"
<< " [--gp3-max-surface-angle-deg F]"
<< " [--gp3-min-angle-deg F]"
<< " [--gp3-max-angle-deg F]"
<< " [--gp3-normal-k N]"
<< " [--gp3-normal-consistency true|false]"
<< " input1.pcd [input2.pcd ...]\n";
}
bool parseBooleanValue(const std::string& value)
{
if (value == "true" || value == "1")
{
return true;
}
if (value == "false" || value == "0")
{
return false;
}
throw std::runtime_error(
"Expected boolean value true|false|1|0, got: " + value);
}
MeshAlgorithm parseAlgorithm(const std::string& value)
{
if (value == "ofm")
{
return MeshAlgorithm::Ofm;
}
if (value == "gp3")
{
return MeshAlgorithm::Gp3;
}
throw std::runtime_error("Unsupported algorithm: " + value);
}
bool parseTriangulationType(
const std::string& value,
pcl::OrganizedFastMesh<pcl::PointXYZ>::TriangulationType& ofmTriangulationType)
{
if (value == "adaptive")
{
ofmTriangulationType =
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_ADAPTIVE_CUT;
return true;
}
if (value == "left")
{
ofmTriangulationType =
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_LEFT_CUT;
return true;
}
if (value == "right")
{
ofmTriangulationType =
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_RIGHT_CUT;
return true;
}
if (value == "quad")
{
ofmTriangulationType =
pcl::OrganizedFastMesh<pcl::PointXYZ>::QUAD_MESH;
return true;
}
return false;
}
void parseToolOption(
ToolOptions& options,
const std::string& arg,
int& i,
int argc,
char** argv)
{
auto requireValue = [&](const char* optionName) -> std::string {
if (i + 1 >= argc)
{
throw std::runtime_error(std::string(optionName) + " requires a value");
}
return argv[++i];
};
if (arg == "--algorithm")
{
options.algorithm = parseAlgorithm(requireValue("--algorithm"));
return;
}
if (arg == "--output-dir")
{
options.outputDirectory = requireValue("--output-dir");
options.hasOutputDirectory = true;
return;
}
if (arg == "--ofm-triangle-pixel-size")
{
options.ofmTrianglePixelSize = std::stoi(
requireValue("--ofm-triangle-pixel-size"));
return;
}
if (arg == "--ofm-max-edge-length")
{
options.ofmMaxEdgeLength = std::stof(
requireValue("--ofm-max-edge-length"));
return;
}
if (arg == "--ofm-triangulation")
{
std::string triangulationValue = requireValue("--ofm-triangulation");
if (!parseTriangulationType(
triangulationValue, options.ofmTriangulationType))
{
throw std::runtime_error(
"Unsupported triangulation type: " + triangulationValue);
}
return;
}
if (arg == "--gp3-search-radius")
{
options.gp3SearchRadius = std::stod(
requireValue("--gp3-search-radius"));
return;
}
if (arg == "--gp3-mu")
{
options.gp3Mu = std::stod(requireValue("--gp3-mu"));
return;
}
if (arg == "--gp3-max-neighbors")
{
options.gp3MaxNeighbors = std::stoi(
requireValue("--gp3-max-neighbors"));
return;
}
if (arg == "--gp3-max-surface-angle-deg")
{
options.gp3MaxSurfaceAngleDeg = std::stod(
requireValue("--gp3-max-surface-angle-deg"));
return;
}
if (arg == "--gp3-min-angle-deg")
{
options.gp3MinAngleDeg = std::stod(
requireValue("--gp3-min-angle-deg"));
return;
}
if (arg == "--gp3-max-angle-deg")
{
options.gp3MaxAngleDeg = std::stod(
requireValue("--gp3-max-angle-deg"));
return;
}
if (arg == "--gp3-normal-k")
{
options.gp3NormalK = std::stoi(requireValue("--gp3-normal-k"));
return;
}
if (arg == "--gp3-normal-consistency")
{
options.gp3NormalConsistency = parseBooleanValue(
requireValue("--gp3-normal-consistency"));
return;
}
throw std::runtime_error("Unknown option: " + arg);
}
ToolOptions parseArgs(int argc, char** argv)
{
ToolOptions options;
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (!arg.empty() && arg[0] == '-')
{
parseToolOption(options, arg, i, argc, argv);
continue;
}
options.inputPaths.emplace_back(arg);
}
if (options.inputPaths.empty())
{
throw std::runtime_error("At least one input .pcd file is required");
}
return options;
}
std::filesystem::path computeOutputPath(
const ToolOptions& options,
const std::filesystem::path& inputPath)
{
std::filesystem::path outputDirectory = options.hasOutputDirectory
? options.outputDirectory
: inputPath.parent_path();
std::filesystem::path outputFileName = inputPath.filename();
outputFileName.replace_extension(".vtk");
return outputDirectory / outputFileName;
}
pcl::PointCloud<pcl::PointXYZ>::Ptr loadInputCloud(
const std::filesystem::path& inputPath)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(
new pcl::PointCloud<pcl::PointXYZ>());
if (pcl::io::loadPCDFile(inputPath.string(), *cloud) != 0)
{
throw std::runtime_error("Failed to load input PCD");
}
return cloud;
}
void ensureOutputDirectoryExists(const std::filesystem::path& outputPath)
{
std::filesystem::create_directories(outputPath.parent_path());
}
void ensureOrganizedCloudForOfm(const pcl::PointCloud<pcl::PointXYZ>& cloud)
{
if (!cloud.isOrganized())
{
throw std::runtime_error("Input point cloud is not organized");
}
}
pcl::PointCloud<pcl::PointXYZ>::Ptr buildFinitePointCloud(
const pcl::PointCloud<pcl::PointXYZ>& cloud)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr finiteCloud(
new pcl::PointCloud<pcl::PointXYZ>());
finiteCloud->reserve(cloud.size());
for (const auto& point : cloud.points)
{
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
!std::isfinite(point.z))
{
continue;
}
finiteCloud->push_back(point);
}
finiteCloud->width = static_cast<std::uint32_t>(finiteCloud->size());
finiteCloud->height = 1;
finiteCloud->is_dense = false;
return finiteCloud;
}
void ensureEnoughFinitePoints(
const pcl::PointCloud<pcl::PointXYZ>& finiteCloud,
int normalK)
{
if (finiteCloud.size() < kMinimumTriangulationPoints)
{
throw std::runtime_error("Too few finite points to triangulate");
}
if (finiteCloud.size() <= static_cast<std::size_t>(normalK))
{
throw std::runtime_error(
"Too few finite points for requested GP3 normal-k");
}
}
pcl::PointCloud<pcl::Normal>::Ptr estimateNormals(
const pcl::PointCloud<pcl::PointXYZ>::Ptr& finiteCloud,
int normalK)
{
pcl::search::KdTree<pcl::PointXYZ>::Ptr searchTree(
new pcl::search::KdTree<pcl::PointXYZ>());
searchTree->setInputCloud(finiteCloud);
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
normalEstimation.setInputCloud(finiteCloud);
normalEstimation.setSearchMethod(searchTree);
normalEstimation.setKSearch(normalK);
normalEstimation.setViewPoint(0.0f, 0.0f, 0.0f);
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());
normalEstimation.compute(*normals);
return normals;
}
pcl::PointCloud<pcl::PointNormal>::Ptr buildPointNormalsCloud(
const pcl::PointCloud<pcl::PointXYZ>& finiteCloud,
const pcl::PointCloud<pcl::Normal>& normals)
{
pcl::PointCloud<pcl::PointNormal>::Ptr pointNormals(
new pcl::PointCloud<pcl::PointNormal>());
pcl::concatenateFields(finiteCloud, normals, *pointNormals);
return pointNormals;
}
void ensureGp3NormalsAreFinite(
const pcl::PointCloud<pcl::PointNormal>& pointNormals)
{
for (const auto& point : pointNormals.points)
{
if (std::isfinite(point.normal_x) && std::isfinite(point.normal_y) &&
std::isfinite(point.normal_z))
{
return;
}
}
throw std::runtime_error("Normal estimation produced no finite normals");
}
void saveMeshOrThrow(
const std::filesystem::path& outputPath,
const pcl::PolygonMesh& mesh)
{
if (mesh.polygons.empty())
{
throw std::runtime_error("Reconstruction produced no polygons");
}
if (pcl::io::saveVTKFile(outputPath.string(), mesh) != 0)
{
throw std::runtime_error("Failed to save output VTK");
}
}
void configureOfm(
pcl::OrganizedFastMesh<pcl::PointXYZ>& ofm,
const ToolOptions& options,
const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud)
{
ofm.setInputCloud(cloud);
ofm.setTriangulationType(options.ofmTriangulationType);
ofm.setTrianglePixelSize(options.ofmTrianglePixelSize);
ofm.setViewpoint(Eigen::Vector3f::Zero());
ofm.setMaxEdgeLength(options.ofmMaxEdgeLength);
}
void configureGp3(
pcl::GreedyProjectionTriangulation<pcl::PointNormal>& gp3,
const ToolOptions& options,
const pcl::PointCloud<pcl::PointNormal>::Ptr& pointNormals,
const pcl::search::KdTree<pcl::PointNormal>::Ptr& searchTree)
{
gp3.setInputCloud(pointNormals);
gp3.setSearchMethod(searchTree);
gp3.setSearchRadius(options.gp3SearchRadius);
gp3.setMu(options.gp3Mu);
gp3.setMaximumNearestNeighbors(options.gp3MaxNeighbors);
gp3.setMaximumSurfaceAngle(
degreesToRadians(options.gp3MaxSurfaceAngleDeg));
gp3.setMinimumAngle(degreesToRadians(options.gp3MinAngleDeg));
gp3.setMaximumAngle(degreesToRadians(options.gp3MaxAngleDeg));
gp3.setNormalConsistency(options.gp3NormalConsistency);
}
ConversionStats convertWithOfm(
const ToolOptions& options,
const std::filesystem::path& inputPath)
{
ConversionStats stats;
stats.algorithm = MeshAlgorithm::Ofm;
stats.inputPath = inputPath;
stats.outputPath = computeOutputPath(options, inputPath);
auto cloud = loadInputCloud(inputPath);
ensureOrganizedCloudForOfm(*cloud);
ensureOutputDirectoryExists(stats.outputPath);
pcl::OrganizedFastMesh<pcl::PointXYZ> ofm;
configureOfm(ofm, options, cloud);
pcl::PolygonMesh mesh;
auto meshStart = std::chrono::steady_clock::now();
ofm.reconstruct(mesh);
auto meshEnd = std::chrono::steady_clock::now();
saveMeshOrThrow(stats.outputPath, mesh);
stats.pointCount = cloud->size();
stats.polygonCount = mesh.polygons.size();
stats.meshDurationMs = std::chrono::duration<double, std::milli>(
meshEnd - meshStart).count();
stats.totalDurationMs = stats.meshDurationMs;
stats.success = true;
return stats;
}
ConversionStats convertWithGp3(
const ToolOptions& options,
const std::filesystem::path& inputPath)
{
ConversionStats stats;
stats.algorithm = MeshAlgorithm::Gp3;
stats.inputPath = inputPath;
stats.outputPath = computeOutputPath(options, inputPath);
auto cloud = loadInputCloud(inputPath);
auto finiteCloud = buildFinitePointCloud(*cloud);
ensureEnoughFinitePoints(*finiteCloud, options.gp3NormalK);
ensureOutputDirectoryExists(stats.outputPath);
auto normalStart = std::chrono::steady_clock::now();
auto normals = estimateNormals(finiteCloud, options.gp3NormalK);
auto normalEnd = std::chrono::steady_clock::now();
auto pointNormals = buildPointNormalsCloud(*finiteCloud, *normals);
ensureGp3NormalsAreFinite(*pointNormals);
pcl::search::KdTree<pcl::PointNormal>::Ptr searchTree(
new pcl::search::KdTree<pcl::PointNormal>());
searchTree->setInputCloud(pointNormals);
pcl::GreedyProjectionTriangulation<pcl::PointNormal> gp3;
configureGp3(gp3, options, pointNormals, searchTree);
pcl::PolygonMesh mesh;
auto meshStart = std::chrono::steady_clock::now();
gp3.reconstruct(mesh);
auto meshEnd = std::chrono::steady_clock::now();
saveMeshOrThrow(stats.outputPath, mesh);
stats.pointCount = cloud->size();
stats.finitePointCount = finiteCloud->size();
stats.polygonCount = mesh.polygons.size();
stats.normalDurationMs = std::chrono::duration<double, std::milli>(
normalEnd - normalStart).count();
stats.meshDurationMs = std::chrono::duration<double, std::milli>(
meshEnd - meshStart).count();
stats.totalDurationMs = stats.normalDurationMs + stats.meshDurationMs;
stats.success = true;
return stats;
}
ConversionStats convertSingleFile(
const ToolOptions& options,
const std::filesystem::path& inputPath)
{
try
{
if (options.algorithm == MeshAlgorithm::Ofm)
{
return convertWithOfm(options, inputPath);
}
return convertWithGp3(options, inputPath);
}
catch (const std::exception& e)
{
ConversionStats stats;
stats.algorithm = options.algorithm;
stats.inputPath = inputPath;
stats.outputPath = computeOutputPath(options, inputPath);
stats.errorMessage = e.what();
return stats;
}
}
void printOfmStats(const ConversionStats& stats)
{
std::cout << std::fixed << std::setprecision(3)
<< "OK"
<< " algorithm=ofm"
<< " input=" << stats.inputPath
<< " output=" << stats.outputPath
<< " points=" << stats.pointCount
<< " polygons=" << stats.polygonCount
<< " mesh_ms=" << stats.meshDurationMs
<< "\n";
}
void printGp3Stats(const ConversionStats& stats)
{
std::cout << std::fixed << std::setprecision(3)
<< "OK"
<< " algorithm=gp3"
<< " input=" << stats.inputPath
<< " output=" << stats.outputPath
<< " points=" << stats.pointCount
<< " finite_points=" << stats.finitePointCount
<< " polygons=" << stats.polygonCount
<< " normal_ms=" << stats.normalDurationMs
<< " mesh_ms=" << stats.meshDurationMs
<< " total_ms=" << stats.totalDurationMs
<< "\n";
}
void printPerFileStats(const ConversionStats& stats)
{
if (!stats.success)
{
std::cout << "FAIL"
<< " algorithm=" << algorithmToString(stats.algorithm)
<< " input=" << stats.inputPath
<< " error=\"" << stats.errorMessage << "\"\n";
return;
}
if (stats.algorithm == MeshAlgorithm::Ofm)
{
printOfmStats(stats);
return;
}
printGp3Stats(stats);
}
void printAggregateStats(const ToolOptions& options,
const std::vector<ConversionStats>& statsList)
{
std::size_t successCount = 0;
std::size_t failureCount = 0;
double totalNormalMs = 0.0;
double totalMeshMs = 0.0;
double totalConversionMs = 0.0;
double minTotalMs = std::numeric_limits<double>::max();
double maxTotalMs = 0.0;
for (const auto& stats : statsList)
{
if (!stats.success)
{
++failureCount;
continue;
}
++successCount;
totalNormalMs += stats.normalDurationMs;
totalMeshMs += stats.meshDurationMs;
totalConversionMs += stats.totalDurationMs;
minTotalMs = std::min(minTotalMs, stats.totalDurationMs);
maxTotalMs = std::max(maxTotalMs, stats.totalDurationMs);
}
double averageTotalMs = successCount == 0
? 0.0
: totalConversionMs / static_cast<double>(successCount);
if (successCount == 0)
{
minTotalMs = 0.0;
}
std::cout << std::fixed << std::setprecision(3)
<< "SUMMARY"
<< " algorithm=" << algorithmToString(options.algorithm)
<< " processed=" << statsList.size()
<< " succeeded=" << successCount
<< " failed=" << failureCount;
if (options.algorithm == MeshAlgorithm::Gp3)
{
std::cout
<< " total_normal_ms=" << totalNormalMs
<< " total_mesh_ms=" << totalMeshMs
<< " total_conversion_ms=" << totalConversionMs
<< " avg_total_ms=" << averageTotalMs
<< " min_total_ms=" << minTotalMs
<< " max_total_ms=" << maxTotalMs
<< "\n";
return;
}
std::cout
<< " total_mesh_ms=" << totalMeshMs
<< " avg_mesh_ms=" << averageTotalMs
<< " min_mesh_ms=" << minTotalMs
<< " max_mesh_ms=" << maxTotalMs
<< "\n";
}
} // namespace
int main(int argc, char** argv)
{
try
{
ToolOptions options = parseArgs(argc, argv);
std::vector<ConversionStats> statsList;
statsList.reserve(options.inputPaths.size());
for (const auto& inputPath : options.inputPaths)
{
ConversionStats stats = convertSingleFile(options, inputPath);
printPerFileStats(stats);
statsList.push_back(std::move(stats));
}
printAggregateStats(options, statsList);
return 0;
}
catch (const std::exception& e)
{
printUsage(argv[0]);
std::cerr << e.what() << std::endl;
return 1;
}
}
+452
View File
@@ -0,0 +1,452 @@
#include <cerrno>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <linux/auxvec.h>
#include <linux/rseq.h>
#include <sys/auxv.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/utsname.h>
#include <unistd.h>
#ifndef ENOTSUPP
#define ENOTSUPP 524
#endif
#if defined(__x86_64__)
#ifndef ARCH_GET_FS
#define ARCH_GET_FS 0x1003
#endif
#endif
#ifndef PR_RSEQ_SLICE_EXTENSION
#define PR_RSEQ_SLICE_EXTENSION 79
#define PR_RSEQ_SLICE_EXTENSION_GET 1
#define PR_RSEQ_SLICE_EXTENSION_SET 2
#define PR_RSEQ_SLICE_EXT_ENABLE 0x01
#endif
#ifndef RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE
#define RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE (1U << 4)
#define RSEQ_CS_FLAG_SLICE_EXT_ENABLED (1U << 5)
#endif
#ifndef RSEQ_SIG
#if defined(__x86_64__) || defined(__i386__)
#define RSEQ_SIG 0x53053053
#elif defined(__aarch64__)
#define RSEQ_SIG 0x00bc28d4
#else
#error "Add RSEQ_SIG for this architecture"
#endif
#endif
#ifndef SYS_rseq
#ifdef __NR_rseq
#define SYS_rseq __NR_rseq
#endif
#endif
#ifndef SYS_getcpu
#ifdef __NR_getcpu
#define SYS_getcpu __NR_getcpu
#endif
#endif
extern "C" {
extern __attribute__((weak)) ptrdiff_t __rseq_offset;
extern __attribute__((weak)) unsigned int __rseq_size;
extern __attribute__((weak)) unsigned int __rseq_flags;
}
struct rseq_slice_ctrl_fields {
uint8_t request;
uint8_t granted;
uint16_t reserved;
};
struct rseq_slice_ctrl_compat {
union {
uint32_t all;
rseq_slice_ctrl_fields parts;
};
};
struct rseq_compat {
uint32_t cpu_id_start;
int32_t cpu_id;
uint64_t rseq_cs;
uint32_t flags;
uint32_t node_id;
uint32_t mm_cid;
struct rseq_slice_ctrl_compat slice_ctrl;
uint8_t reserved;
} __attribute__((aligned(32)));
struct prctl_probe_result {
bool ok;
int value;
int err;
};
alignas(32) static thread_local unsigned char local_rseq_storage[512];
static unsigned int g_rseq_feature_size;
static unsigned int g_rseq_alloc_size;
static unsigned int g_rseq_align;
static unsigned long g_aux_rseq_feature_size;
static unsigned long g_aux_rseq_align;
static struct rseq *g_registered_rseq;
static struct rseq_compat *g_registered_rseq_compat;
static bool g_own_registration;
static const char *yes_no(bool value)
{
return value ? "yes" : "no";
}
static const char *set_clear(bool value)
{
return value ? "set" : "clear";
}
static int get_thread_pointer(uintptr_t *tp_out)
{
#if defined(__x86_64__)
unsigned long fsbase = 0;
if (syscall(SYS_arch_prctl, ARCH_GET_FS, &fsbase) != 0)
return -1;
*tp_out = fsbase;
return 0;
#elif defined(__aarch64__)
void *tp = nullptr;
__asm__ volatile("mrs %0, tpidr_el0" : "=r"(tp));
*tp_out = reinterpret_cast<uintptr_t>(tp);
return 0;
#else
(void) tp_out;
errno = ENOTSUP;
return -1;
#endif
}
static int sys_rseq(struct rseq *rseq, uint32_t len, int flags, uint32_t sig)
{
#ifdef SYS_rseq
return static_cast<int>(syscall(SYS_rseq, rseq, len, flags, sig));
#else
(void) rseq;
(void) len;
(void) flags;
(void) sig;
errno = ENOSYS;
return -1;
#endif
}
static int sys_getcpu(unsigned int *cpu, unsigned int *node)
{
#ifdef SYS_getcpu
return static_cast<int>(syscall(SYS_getcpu, cpu, node, nullptr));
#else
(void) cpu;
(void) node;
errno = ENOSYS;
return -1;
#endif
}
static const char *errno_name(int err)
{
switch (err) {
case 0: return "0";
case EINVAL: return "EINVAL";
case ENOSYS: return "ENOSYS";
case ENOTSUP: return "ENOTSUP";
case ENOTSUPP: return "ENOTSUPP";
case ENXIO: return "ENXIO";
case EPERM: return "EPERM";
case EBUSY: return "EBUSY";
default: return "UNKNOWN";
}
}
static const char *prctl_failure_meaning(int err)
{
switch (err) {
case EINVAL:
return "the prctl operation or argument is not accepted by this kernel";
case ENOTSUPP:
return "the prctl operation exists, but the slice extension is not supported here";
case EPERM:
return "the kernel denied the requested operation";
default:
return "the kernel returned an unclassified failure";
}
}
static void print_errno_status(const char *label, int err)
{
std::printf("%s: errno=%d (%s: %s)\n",
label, err, errno_name(err), std::strerror(err));
}
static unsigned int max_u32(unsigned int a, unsigned int b)
{
return a > b ? a : b;
}
static bool feature_present(size_t end_offset)
{
return g_rseq_feature_size >= end_offset;
}
static void print_registration_source(void)
{
if (g_own_registration)
std::printf("rseq registration: local syscall registration\n");
else
std::printf("rseq registration: existing libc-owned registration\n");
}
static int setup_rseq(void)
{
g_aux_rseq_feature_size = getauxval(AT_RSEQ_FEATURE_SIZE);
g_aux_rseq_align = getauxval(AT_RSEQ_ALIGN);
g_rseq_feature_size = g_aux_rseq_feature_size ?
static_cast<unsigned int>(g_aux_rseq_feature_size) : 20U;
g_rseq_align = g_aux_rseq_align ?
static_cast<unsigned int>(g_aux_rseq_align) : 32U;
g_rseq_alloc_size = max_u32(g_rseq_feature_size, 32U);
if (&__rseq_size != nullptr && __rseq_size != 0) {
uintptr_t tp = 0;
if (get_thread_pointer(&tp) != 0) {
std::perror("get_thread_pointer");
return -1;
}
g_registered_rseq = reinterpret_cast<struct rseq *>(tp + __rseq_offset);
g_registered_rseq_compat = reinterpret_cast<struct rseq_compat *>(tp + __rseq_offset);
g_own_registration = false;
if (__rseq_size < g_rseq_feature_size)
g_rseq_feature_size = __rseq_size;
if (__rseq_size > g_rseq_alloc_size)
g_rseq_alloc_size = __rseq_size;
return 0;
}
if (g_rseq_alloc_size > sizeof(local_rseq_storage)) {
std::fprintf(stderr,
"local rseq area too small: need %u bytes, have %zu\n",
g_rseq_alloc_size, sizeof(local_rseq_storage));
errno = EOVERFLOW;
return -1;
}
if ((reinterpret_cast<uintptr_t>(local_rseq_storage) % g_rseq_align) != 0) {
std::fprintf(stderr, "local rseq area alignment mismatch: need %u\n",
g_rseq_align);
errno = EINVAL;
return -1;
}
auto *local_rseq = reinterpret_cast<struct rseq *>(local_rseq_storage);
auto *local_rseq_compat = reinterpret_cast<struct rseq_compat *>(local_rseq_storage);
std::memset(local_rseq_storage, 0, sizeof(local_rseq_storage));
local_rseq_compat->cpu_id = RSEQ_CPU_ID_UNINITIALIZED;
if (sys_rseq(local_rseq, g_rseq_alloc_size, 0, RSEQ_SIG) != 0) {
std::perror("rseq register");
return -1;
}
g_registered_rseq = local_rseq;
g_registered_rseq_compat = local_rseq_compat;
g_own_registration = true;
return 0;
}
static void teardown_rseq(void)
{
if (!g_own_registration)
return;
auto *local_rseq = reinterpret_cast<struct rseq *>(local_rseq_storage);
if (sys_rseq(local_rseq, g_rseq_alloc_size, RSEQ_FLAG_UNREGISTER, RSEQ_SIG) != 0)
std::perror("rseq unregister");
}
static prctl_probe_result probe_prctl_get(void)
{
errno = 0;
int rc = prctl(PR_RSEQ_SLICE_EXTENSION, PR_RSEQ_SLICE_EXTENSION_GET,
0UL, 0UL, 0UL);
int saved_errno = errno;
if (rc >= 0)
return { true, rc, 0 };
return { false, -1, saved_errno };
}
static prctl_probe_result probe_prctl_set(unsigned long value)
{
errno = 0;
int rc = prctl(PR_RSEQ_SLICE_EXTENSION, PR_RSEQ_SLICE_EXTENSION_SET,
value, 0UL, 0UL);
int saved_errno = errno;
if (rc >= 0)
return { true, rc, 0 };
return { false, -1, saved_errno };
}
static void print_prctl_result(const char *label, const prctl_probe_result &result)
{
if (result.ok) {
std::printf("%s: ok, value=%d\n", label, result.value);
return;
}
print_errno_status(label, result.err);
std::printf("%s meaning: %s\n", label, prctl_failure_meaning(result.err));
}
static void print_kernel_version(void)
{
struct utsname uts;
if (uname(&uts) != 0) {
std::perror("uname");
return;
}
std::printf("kernel: %s %s %s %s\n",
uts.sysname, uts.release, uts.version, uts.machine);
}
static void print_slice_status_summary(bool has_slice_ctrl, bool flags_available,
const prctl_probe_result &get_result)
{
bool flag_available = flags_available &&
(g_registered_rseq_compat->flags & RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE);
bool flag_enabled = flags_available &&
(g_registered_rseq_compat->flags & RSEQ_CS_FLAG_SLICE_EXT_ENABLED);
bool prctl_enabled = get_result.ok &&
(get_result.value & PR_RSEQ_SLICE_EXT_ENABLE);
std::printf("status: rseq syscall registered: yes\n");
std::printf("status: rseq extensible feature area: %s\n",
yes_no(g_rseq_feature_size > 20U));
std::printf("status: rseq slice_ctrl field present: %s\n",
yes_no(has_slice_ctrl));
std::printf("status: rseq slice extension available flag: %s\n",
flags_available ? set_clear(flag_available) : "unavailable");
std::printf("status: rseq slice extension enabled flag: %s\n",
flags_available ? set_clear(flag_enabled) : "unavailable");
std::printf("status: PR_RSEQ_SLICE_EXTENSION GET usable: %s\n",
yes_no(get_result.ok));
if (get_result.ok) {
std::printf("status: PR_RSEQ_SLICE_EXTENSION enabled: %s\n",
yes_no(prctl_enabled));
std::printf("status: rseq slice extension availability: %s\n",
prctl_enabled || flag_available ? "available" : "available but disabled");
return;
}
if (get_result.err == ENOTSUPP) {
std::printf("status: rseq slice extension availability: not supported by this kernel/arch/config\n");
return;
}
if (get_result.err == EINVAL) {
std::printf("status: rseq slice extension availability: no accepted prctl API on this kernel\n");
return;
}
std::printf("status: rseq slice extension availability: unknown\n");
}
static void probe_slice_extension(void)
{
bool has_slice_ctrl = feature_present(offsetof(struct rseq_compat, slice_ctrl) +
sizeof(g_registered_rseq_compat->slice_ctrl));
bool flags_available = feature_present(offsetof(struct rseq_compat, flags) +
sizeof(g_registered_rseq_compat->flags));
prctl_probe_result prctl_get = probe_prctl_get();
unsigned int cpu = 0;
unsigned int node = 0;
print_kernel_version();
std::printf("AT_RSEQ_FEATURE_SIZE raw: %lu\n", g_aux_rseq_feature_size);
std::printf("AT_RSEQ_ALIGN raw: %lu\n", g_aux_rseq_align);
std::printf("effective rseq feature size: %u\n", g_rseq_feature_size);
std::printf("effective rseq alignment: %u\n", g_rseq_align);
std::printf("registered rseq size: %u\n", g_rseq_alloc_size);
print_registration_source();
if (&__rseq_size != nullptr) {
std::printf("libc __rseq_size=%u __rseq_offset=%td __rseq_flags=0x%x\n",
__rseq_size, __rseq_offset, __rseq_flags);
}
std::printf("registered rseq addr: %p\n", static_cast<void *>(g_registered_rseq));
std::printf("struct rseq has slice_ctrl field available: %s\n",
yes_no(has_slice_ctrl));
if (sys_getcpu(&cpu, &node) == 0)
std::printf("getcpu(): cpu=%u node=%u\n", cpu, node);
std::printf("rseq cpu_id_start=%u cpu_id=%d\n",
g_registered_rseq->cpu_id_start,
static_cast<int32_t>(g_registered_rseq->cpu_id));
if (feature_present(offsetof(struct rseq_compat, node_id) +
sizeof(g_registered_rseq_compat->node_id))) {
std::printf("rseq node_id=%u\n", g_registered_rseq_compat->node_id);
}
if (feature_present(offsetof(struct rseq_compat, mm_cid) +
sizeof(g_registered_rseq_compat->mm_cid))) {
std::printf("rseq mm_cid=%u\n", g_registered_rseq_compat->mm_cid);
}
if (flags_available) {
std::printf("rseq flags=0x%x\n", g_registered_rseq_compat->flags);
std::printf("slice ext available bit: %s\n",
set_clear(g_registered_rseq_compat->flags & RSEQ_CS_FLAG_SLICE_EXT_AVAILABLE));
std::printf("slice ext enabled bit: %s\n",
set_clear(g_registered_rseq_compat->flags & RSEQ_CS_FLAG_SLICE_EXT_ENABLED));
}
if (has_slice_ctrl) {
std::printf("slice_ctrl.request=%u granted=%u raw=0x%x\n",
g_registered_rseq_compat->slice_ctrl.parts.request,
g_registered_rseq_compat->slice_ctrl.parts.granted,
g_registered_rseq_compat->slice_ctrl.all);
}
print_prctl_result("prctl(PR_RSEQ_SLICE_EXTENSION, GET)", prctl_get);
if (prctl_get.ok) {
prctl_probe_result set_enable = probe_prctl_set(PR_RSEQ_SLICE_EXT_ENABLE);
print_prctl_result("prctl(PR_RSEQ_SLICE_EXTENSION, SET enable)",
set_enable);
prctl_probe_result after_enable = probe_prctl_get();
print_prctl_result("prctl(PR_RSEQ_SLICE_EXTENSION, GET after enable)",
after_enable);
prctl_probe_result set_disable = probe_prctl_set(0UL);
print_prctl_result("prctl(PR_RSEQ_SLICE_EXTENSION, SET disable)",
set_disable);
}
print_slice_status_summary(has_slice_ctrl, flags_available, prctl_get);
}
int main()
{
if (setup_rseq() != 0)
return 1;
probe_slice_extension();
teardown_rseq();
return 0;
}
+67 -15
View File
@@ -5,22 +5,48 @@
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY
"Salmanoff - A sensor management and control system")
"Cognitive robotics runtime")
set(CPACK_PACKAGE_VENDOR "Salmanoff Project")
set(CPACK_PACKAGE_CONTACT "maintainer@salmanoff.org")
# Set package description
set(CPACK_PACKAGE_DESCRIPTION
"Salmanoff is a comprehensive sensor management and control system that\n"
"provides unified interfaces for various sensor devices including LiDAR\n"
"systems. It features modular architecture with support for multiple\n"
"device types, asynchronous processing, and real-time data handling."
)
# Set package description.
# Use a single joined string so CMake does not serialize it as a semicolon list
# in the generated Debian control file.
string(JOIN "\n "
CPACK_PACKAGE_DESCRIPTION
"Comprehensive sensor management and control runtime."
""
"Salmanoff provides unified interfaces for various sensor devices,"
"including LiDAR systems."
"It features modular architecture with support for multiple device"
"types, asynchronous processing, and real-time data handling.")
# License information
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
# Debian-specific packaged documentation.
set(SALMANOFF_CHANGELOG_TXT "${CMAKE_CURRENT_BINARY_DIR}/changelog")
set(SALMANOFF_CHANGELOG_GZ "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz")
file(WRITE "${SALMANOFF_CHANGELOG_TXT}"
"salmanoff (${PROJECT_VERSION}) unstable; urgency=medium\n\n"
" * Package release.\n\n"
" -- Salmanoff Project <maintainer@salmanoff.org> Fri, 06 Mar 2026 00:00:00 +0000\n")
execute_process(
COMMAND gzip -n -9 -c "${SALMANOFF_CHANGELOG_TXT}"
OUTPUT_FILE "${SALMANOFF_CHANGELOG_GZ}"
)
set(SALMANOFF_MANPAGE_GZ "${CMAKE_CURRENT_BINARY_DIR}/salmanoff.1.gz")
execute_process(
COMMAND gzip -n -9 -c "${CMAKE_CURRENT_SOURCE_DIR}/docs/salmanoff.1"
OUTPUT_FILE "${SALMANOFF_MANPAGE_GZ}"
)
install(FILES "${SALMANOFF_CHANGELOG_GZ}" DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES "${SALMANOFF_MANPAGE_GZ}" DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR} RENAME copyright)
# Enable deb and rpm generators
set(CPACK_GENERATOR "DEB;RPM")
@@ -34,24 +60,39 @@ set(CPACK_DEBIAN_PACKAGE_DISTRIBUTION "ubuntu")
# Build dependencies (from builddeps file)
# These are needed to build the package from source
set(CPACK_DEBIAN_PACKAGE_BUILD_DEPENDS
"build-essential, cmake (>= 3.16), libboost-all-dev, flex, bison, ocl-icd-opencl-dev, liburing-dev")
"build-essential, cmake (>= 3.16), libboost-all-dev, flex, bison, ocl-icd-opencl-dev, liburing-dev, libcamera-dev")
# Runtime dependencies (from builddeps file - runtime equivalents)
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"libboost-system1.74.0 | libboost-system1.73.0 | libboost-system1.72.0, libboost-log1.74.0 | libboost-log1.73.0 | libboost-log1.72.0, libc6, libstdc++6, ocl-icd-libopencl1 | libopencl1, liburing2 | liburing1")
# Runtime dependencies.
# Let dpkg-shlibdeps derive the actual ELF dependencies from the built
# binaries/libraries. Hard-coding old Boost soname packages here makes the
# package unnecessarily pull obsolete runtimes on newer distros, and in this
# tree the generated binaries are not currently linked against Boost DSOs.
set(CPACK_DEBIAN_PACKAGE_DEPENDS "")
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libxcb1, libx11-6")
set(CPACK_DEBIAN_PACKAGE_SUGGESTS "livox-sdk")
set(CPACK_DEBIAN_PACKAGE_SUGGESTS
"livox-sdk, libcamera0.2")
# RPM package specific settings
set(CPACK_RPM_PACKAGE_LICENSE "Proprietary")
set(CPACK_RPM_PACKAGE_GROUP "Applications/Engineering")
set(CPACK_RPM_PACKAGE_URL "https://github.com/salmanoff/salmanoff")
set(CPACK_RPM_PACKAGE_REQUIRES "boost-system >= 1.72.0, boost-log >= 1.72.0, glibc, libstdc++, ocl-icd, liburing")
set(CPACK_RPM_PACKAGE_SUGGESTS "xcb, libX11, livox-sdk")
set(CPACK_RPM_PACKAGE_SUGGESTS
"xcb, libX11, livox-sdk, libcamera")
# Package file naming using Debian's architecture naming when available.
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}")
if(EXISTS "/usr/bin/dpkg")
execute_process(
COMMAND /usr/bin/dpkg --print-architecture
OUTPUT_VARIABLE CPACK_DEBIAN_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
# Package file naming using project variables
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_PROCESSOR}")
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}")
# Enable automatic dependency detection for Debian packages
# This uses dpkg-shlibdeps to automatically detect shared library dependencies
@@ -60,3 +101,14 @@ set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
# Set compression
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_RPM_COMPONENT_INSTALL ON)
# Generate standard Debian shared-library metadata and use triggers instead of
# maintainer scripts for ldconfig handling.
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/postinst"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/postrm"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/triggers")
# Ship stripped binaries in the primary package.
set(CPACK_STRIP_FILES ON)
+53 -9
View File
@@ -6,6 +6,8 @@
# add_daps_target(target_name SOURCES file1.dapss file2.dapss ...)
# register_daps_target(target_name) # In subdirectories
# add_all_daps_dependencies() # In main CMakeLists.txt
# add_daps_clean_target() # Optional: clean_daps_specs removes
# all .daps outputs from add_daps_target.
#
# Examples:
# add_daps_target(device_specs SOURCES devices/avia0.dapss devices/win0.dapss)
@@ -52,13 +54,12 @@ function(add_daps_target target_name)
list(APPEND include_flags "-I${include_dir}")
endforeach()
# Add current source directory to includes if it's not already there
if(source_dir)
list(APPEND include_flags "-I${source_dir}")
endif()
list(APPEND include_flags "-I${CMAKE_CURRENT_SOURCE_DIR}")
# Convert list to space-separated string
string(REPLACE ";" " " include_flags_str "${include_flags}")
# Add source subdirectory to includes when SOURCES lists a path prefix
if(source_dir)
list(APPEND include_flags "-I${CMAKE_CURRENT_SOURCE_DIR}/${source_dir}")
endif()
# Find C compiler if not already set
if(NOT CMAKE_C_COMPILER)
@@ -68,16 +69,29 @@ function(add_daps_target target_name)
endif()
endif()
# Create custom command to preprocess the file
set(depfile "${output_file}.d")
# Preprocess with -MD so #included .dapss files become build dependencies
# (e.g. body specs that #include ../elp-4k-usb-cam.dapss).
add_custom_command(
OUTPUT ${output_file}
COMMAND sh -c "\"${CMAKE_C_COMPILER}\" -E -P -x c ${include_flags_str} \"${CMAKE_CURRENT_SOURCE_DIR}/${source_file}\" > \"${output_file}\""
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${source_file}
COMMAND "${CMAKE_C_COMPILER}"
-E -P -x c
${include_flags}
-MD -MQ "${output_file}" -MF "${depfile}"
-o "${output_file}"
"${CMAKE_CURRENT_SOURCE_DIR}/${source_file}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${source_file}"
DEPFILE "${depfile}"
COMMENT "Preprocessing ${source_file} to ${base_name}.daps"
VERBATIM
)
endforeach()
get_property(_daps_all_outputs GLOBAL PROPERTY DAPS_ALL_OUTPUT_FILES)
list(APPEND _daps_all_outputs ${output_files})
set_property(GLOBAL PROPERTY DAPS_ALL_OUTPUT_FILES "${_daps_all_outputs}")
# Create custom target that depends on all output files
add_custom_target(${target_name} DEPENDS ${output_files})
@@ -151,3 +165,33 @@ function(add_all_daps_dependencies)
message(STATUS "No DAPSS targets registered for dependency addition")
endif()
endfunction()
# Custom target that deletes every generated .daps file from add_daps_target (all subdirs).
# Usage: add_daps_clean_target([NAME clean_daps_specs])
# Call once from the top-level CMakeLists.txt after every add_subdirectory that uses DAPSS.
function(add_daps_clean_target)
set(options)
set(oneValueArgs NAME)
set(multiValueArgs)
cmake_parse_arguments(DC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(DC_NAME)
set(_daps_clean_target ${DC_NAME})
else()
set(_daps_clean_target clean_daps_specs)
endif()
if(TARGET ${_daps_clean_target})
message(FATAL_ERROR "add_daps_clean_target: target '${_daps_clean_target}' already exists")
endif()
get_property(_daps_clean_files GLOBAL PROPERTY DAPS_ALL_OUTPUT_FILES)
if(_daps_clean_files)
add_custom_target(${_daps_clean_target}
COMMAND ${CMAKE_COMMAND} -E rm -f ${_daps_clean_files}
COMMENT "Removing generated .daps specification files"
)
else()
add_custom_target(${_daps_clean_target})
endif()
endfunction()
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
# ldconfig is handled via the package trigger.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
# ldconfig is handled via the package trigger.
exit 0
+1
View File
@@ -0,0 +1 @@
activate-noawait ldconfig
+53 -36
View File
@@ -1,6 +1,13 @@
# Generic Flex/Yacc Generation Functions
# This file provides reusable functions for generating C++ files from Flex/Bison sources
# Emit #line paths relative to CMAKE_SOURCE_DIR so generated sources do not embed
# absolute build-tree paths (helps reproducible builds and Yocto QA buildpaths).
function(_flexyacc_path_relative_to_source OUT_VAR ABS_PATH)
file(RELATIVE_PATH ${OUT_VAR} ${CMAKE_SOURCE_DIR} ${ABS_PATH})
set(${OUT_VAR} ${${OUT_VAR}} PARENT_SCOPE)
endfunction()
# Function to generate Flex lexer files
# Usage: generate_flex_lexer(OUTPUT_VAR INPUT_FILE [PREFIX] [HEADER_DEPENDENCY])
# OUTPUT_VAR: Variable name to store the output file path
@@ -8,31 +15,36 @@
# PREFIX: Optional prefix for the generated files (defaults to basename of input file)
# HEADER_DEPENDENCY: Optional header file that the lexer depends on (e.g., from Bison)
function(generate_flex_lexer OUTPUT_VAR INPUT_FILE)
get_filename_component(INPUT_BASENAME ${INPUT_FILE} NAME_WE)
get_filename_component(INPUT_BASENAME ${INPUT_FILE} NAME_WE)
if(ARGC GREATER 2)
set(PREFIX ${ARGV2})
else()
set(PREFIX ${INPUT_BASENAME})
endif()
if(ARGC GREATER 2)
set(PREFIX ${ARGV2})
else()
set(PREFIX ${INPUT_BASENAME})
endif()
set(LEX_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.cc)
set(LEX_HEADER ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.hh)
set(LEX_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.cc)
set(LEX_HEADER ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.hh)
# Set up dependencies
set(DEPENDENCIES ${INPUT_FILE})
if(ARGC GREATER 3)
list(APPEND DEPENDENCIES ${ARGV3})
endif()
_flexyacc_path_relative_to_source(LEX_INPUT ${INPUT_FILE})
_flexyacc_path_relative_to_source(LEX_OUTPUT_REL ${LEX_OUTPUT})
_flexyacc_path_relative_to_source(LEX_HEADER_REL ${LEX_HEADER})
add_custom_command(
OUTPUT ${LEX_OUTPUT}
DEPENDS ${DEPENDENCIES}
COMMAND ${FLEX_EXECUTABLE} --header-file=${LEX_HEADER} -o ${LEX_OUTPUT} ${INPUT_FILE}
COMMENT "Generating ${PREFIX}.cc from ${INPUT_FILE}"
)
# Set up dependencies
set(DEPENDENCIES ${INPUT_FILE})
if(ARGC GREATER 3)
list(APPEND DEPENDENCIES ${ARGV3})
endif()
set(${OUTPUT_VAR} ${LEX_OUTPUT} PARENT_SCOPE)
add_custom_command(
OUTPUT ${LEX_OUTPUT}
DEPENDS ${DEPENDENCIES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${FLEX_EXECUTABLE} --header-file=${LEX_HEADER_REL} -o ${LEX_OUTPUT_REL} ${LEX_INPUT}
COMMENT "Generating ${PREFIX}.cc from ${INPUT_FILE}"
)
set(${OUTPUT_VAR} ${LEX_OUTPUT} PARENT_SCOPE)
endfunction()
# Function to generate Bison parser files
@@ -42,26 +54,31 @@ endfunction()
# INPUT_FILE: Path to the .yy input file
# PREFIX: Optional prefix for the generated files (defaults to basename of input file)
function(generate_bison_parser OUTPUT_VAR HEADER_VAR INPUT_FILE)
get_filename_component(INPUT_BASENAME ${INPUT_FILE} NAME_WE)
get_filename_component(INPUT_BASENAME ${INPUT_FILE} NAME_WE)
if(ARGC GREATER 3)
set(PREFIX ${ARGV3})
else()
set(PREFIX ${INPUT_BASENAME})
endif()
if(ARGC GREATER 3)
set(PREFIX ${ARGV3})
else()
set(PREFIX ${INPUT_BASENAME})
endif()
set(YACC_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.cc)
set(YACC_HEADER ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.hh)
set(YACC_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.cc)
set(YACC_HEADER ${CMAKE_CURRENT_BINARY_DIR}/${PREFIX}.hh)
add_custom_command(
OUTPUT ${YACC_OUTPUT} ${YACC_HEADER}
DEPENDS ${INPUT_FILE}
COMMAND ${BISON_EXECUTABLE} -p ${PREFIX} --header=${YACC_HEADER} -o ${YACC_OUTPUT} ${INPUT_FILE}
COMMENT "Generating ${PREFIX}.cc and ${PREFIX}.hh from ${INPUT_FILE}"
)
_flexyacc_path_relative_to_source(YACC_INPUT ${INPUT_FILE})
_flexyacc_path_relative_to_source(YACC_OUTPUT_REL ${YACC_OUTPUT})
_flexyacc_path_relative_to_source(YACC_HEADER_REL ${YACC_HEADER})
set(${OUTPUT_VAR} ${YACC_OUTPUT} PARENT_SCOPE)
set(${HEADER_VAR} ${YACC_HEADER} PARENT_SCOPE)
add_custom_command(
OUTPUT ${YACC_OUTPUT} ${YACC_HEADER}
DEPENDS ${INPUT_FILE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${BISON_EXECUTABLE} -p ${PREFIX} --header=${YACC_HEADER_REL} -o ${YACC_OUTPUT_REL} ${YACC_INPUT}
COMMENT "Generating ${PREFIX}.cc and ${PREFIX}.hh from ${INPUT_FILE}"
)
set(${OUTPUT_VAR} ${YACC_OUTPUT} PARENT_SCOPE)
set(${HEADER_VAR} ${YACC_HEADER} PARENT_SCOPE)
endfunction()
# Generate device attachment parser files using the generic functions
+1
View File
@@ -1,3 +1,4 @@
add_subdirectory(xcbXorg)
add_subdirectory(livoxProto1)
add_subdirectory(lcameraDev)
add_subdirectory(attachmentSupport)
+25 -1
View File
@@ -1,18 +1,36 @@
pkg_check_modules(ATTACHMENT_SUPPORT_URING REQUIRED liburing)
add_library(attachmentSupport SHARED
compute.cpp
stimulusProducer.cpp
stagingBuffer.cpp
)
set_target_properties(attachmentSupport PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
target_include_directories(attachmentSupport PUBLIC
${Boost_INCLUDE_DIRS}
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
)
target_include_directories(attachmentSupport PRIVATE
${ATTACHMENT_SUPPORT_URING_INCLUDE_DIRS}
)
target_link_libraries(attachmentSupport PUBLIC
Boost::system
Boost::log
spinscale
)
target_link_libraries(attachmentSupport PRIVATE
${ATTACHMENT_SUPPORT_URING_LIBRARIES}
${OPENCL_LIBRARIES}
)
target_link_directories(attachmentSupport PRIVATE
${ATTACHMENT_SUPPORT_URING_LIBRARY_DIRS}
)
# Verify Boost dynamic dependencies after build
@@ -23,4 +41,10 @@ add_custom_command(TARGET attachmentSupport POST_BUILD
)
# Install rules
install(TARGETS attachmentSupport DESTINATION lib)
install(TARGETS attachmentSupport
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
+129 -12
View File
@@ -1,15 +1,33 @@
#include <user/stagingBuffer.h>
#include <cassert>
#include <unistd.h>
#include <cstdint>
#include <stdexcept>
#include <sys/mman.h>
#include <vector>
#include <liburing.h>
#include <user/frameAssemblyDesc.h>
namespace smo {
namespace stim_buff {
static const char* pinningMechanismToString(
StagingBuffer::PinningMechanism mechanism)
{
switch (mechanism)
{
case StagingBuffer::PinningMechanism::NONE:
return "NONE";
case StagingBuffer::PinningMechanism::MLOCK:
return "MLOCK";
case StagingBuffer::PinningMechanism::IO_URING:
return "IO_URING";
}
return "Unknown";
}
// Static defaults for io_uring
const StagingBuffer::IOEngineConstraints
StagingBuffer::IOEngineConstraints::ioUringConstraints(
@@ -169,11 +187,9 @@ StagingBuffer::StagingBuffer(
const IOEngineConstraints& inputEngineConstraints_,
const IOEngineConstraints& /*outputEngineConstraints*/,
size_t nSlots)
: buffer(nullptr, MmapDeleter(0)), bufferNBytes(0),
nSlots(nSlots), slotStrideNBytes(0),
firstSlotOffsetNBytes(0),
inputConstraints(inputEngineConstraints_),
assemblingFlag(false)
: buffer(nullptr, MmapDeleter(0)),
nSlots(nSlots),
inputConstraints(inputEngineConstraints_)
{
if (nSlots == 0)
{
@@ -202,13 +218,6 @@ assemblingFlag(false)
static_cast<uint8_t*>(mmapped), MmapDeleter(bufferNBytes));
currentNBytes.store(0);
// Lock the buffer in memory to prevent swapping
if (mlock(buffer.get(), bufferNBytes) != 0)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: mlock() failed");
}
// Calculate offset and validate invariants (helper function in .cpp)
firstSlotOffsetNBytes = StagingBuffer::calculateFirstSlotOffsetAndValidate(
buffer.get(), bufferNBytes, nSlots,
@@ -232,5 +241,113 @@ assemblingFlag(false)
std::move(slots));
}
StagingBuffer::~StagingBuffer()
{
assert(!currentlyPinned);
}
StagingBuffer::Pinner::Pinner(StagingBuffer& parent_)
: parent(parent_)
{}
void StagingBuffer::assertUnpinnedAndMarkPinned(PinningMechanism mechanism)
{
if (currentlyPinned)
{
throw std::runtime_error(
std::string(__func__) + ": StagingBuffer already pinned with "
+ pinningMechanismToString(currentPinningMechanism));
}
currentlyPinned = true;
currentPinningMechanism = mechanism;
}
std::unique_ptr<StagingBuffer::MlockPinner> StagingBuffer::makeMlockPinner()
{
return std::make_unique<MlockPinner>(*this);
}
std::unique_ptr<StagingBuffer::IoUringPinner> StagingBuffer::makeIoUringPinner(
struct io_uring* ring)
{
return std::make_unique<IoUringPinner>(*this, ring);
}
StagingBuffer::MlockPinner::MlockPinner(StagingBuffer& parent_)
: Pinner(parent_)
{
if (!parent.buffer || parent.bufferNBytes == 0)
{
throw std::runtime_error(std::string(__func__)
+ ": Cannot mlock an uninitialized StagingBuffer");
}
parent.assertUnpinnedAndMarkPinned(PinningMechanism::MLOCK);
if (mlock(parent.buffer.get(), parent.bufferNBytes) != 0)
{
parent.currentlyPinned = false;
parent.currentPinningMechanism = PinningMechanism::NONE;
throw std::runtime_error(std::string(__func__)
+ ": mlock() failed");
}
}
StagingBuffer::MlockPinner::~MlockPinner()
{
assert(parent.currentlyPinned);
assert(parent.currentPinningMechanism == PinningMechanism::MLOCK);
int ret = munlock(parent.buffer.get(), parent.bufferNBytes);
assert(ret == 0);
(void)ret;
parent.currentlyPinned = false;
parent.currentPinningMechanism = PinningMechanism::NONE;
}
StagingBuffer::IoUringPinner::IoUringPinner(
StagingBuffer& parent_, struct io_uring* ring_)
: Pinner(parent_), ring(ring_)
{
if (!ring)
{
throw std::runtime_error(std::string(__func__)
+ ": io_uring ring pointer is null");
}
if (!parent.buffer || parent.bufferNBytes == 0)
{
throw std::runtime_error(std::string(__func__)
+ ": Cannot register an uninitialized StagingBuffer");
}
parent.assertUnpinnedAndMarkPinned(PinningMechanism::IO_URING);
struct iovec iov = parent.getIoUringRegisterIoVec();
int ret = io_uring_register_buffers(ring, &iov, 1);
if (ret < 0)
{
parent.currentlyPinned = false;
parent.currentPinningMechanism = PinningMechanism::NONE;
throw std::runtime_error(std::string(__func__)
+ ": io_uring_register_buffers failed");
}
}
StagingBuffer::IoUringPinner::~IoUringPinner()
{
assert(parent.currentlyPinned);
assert(parent.currentPinningMechanism == PinningMechanism::IO_URING);
int ret = io_uring_unregister_buffers(ring);
assert(ret == 0);
(void)ret;
parent.currentlyPinned = false;
parent.currentPinningMechanism = PinningMechanism::NONE;
}
} // namespace stim_buff
} // namespace smo
+198 -110
View File
@@ -1,20 +1,72 @@
#include <boostAsioLinkageFix.h>
#include <config.h>
#include <iostream>
#include <chrono>
#include <algorithm>
#include <boost/asio/io_service.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/system/error_code.hpp>
#include <boost/asio/io_context.hpp>
#include <opts.h>
#include <componentThread.h>
#include <spinLock.h>
#include <adapters/boostAsio/deadlineTimerAReq.h>
#include <user/stimulusProducer.h>
#include <user/stimulusBuffer.h>
namespace smo {
namespace stim_buff {
namespace {
long computeTimesliceResidueMs(
long productionDurationMs, long periodMs)
{
if (productionDurationMs >= periodMs) {
return 0;
}
return periodMs - productionDurationMs;
}
void logProductionOverrunIfNeeded(
const char *daemonName,
long productionDurationMs, long periodMs,
size_t &nTimesliceOverruns)
{
if (productionDurationMs <= periodMs) {
return;
}
++nTimesliceOverruns;
const long overrunByMs = productionDurationMs - periodMs;
std::cerr << daemonName << ": production overrun: actual="
<< productionDurationMs << "ms budget=" << periodMs
<< "ms overrunBy=" << overrunByMs << "ms nOverruns="
<< nTimesliceOverruns << std::endl;
}
long durationMsSince(
const std::chrono::high_resolution_clock::time_point &startStamp,
const std::chrono::high_resolution_clock::time_point &endStamp)
{
const auto duration = endStamp - startStamp;
return std::chrono::duration_cast<std::chrono::milliseconds>(
duration).count();
}
void logDaemonDurationsIfVerbose(
const char *daemonName,
long productionDurationMs,
long timesliceDurationMs,
long periodMs)
{
if (!OptionParser::getOptions().verbose) {
return;
}
std::cerr << daemonName << ": daemon durations: production="
<< productionDurationMs << "ms timeslice="
<< timesliceDurationMs << "ms period=" << periodMs
<< "ms" << std::endl;
}
} // namespace
std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>& spec) const
{
@@ -30,6 +82,32 @@ std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBuffer(
return nullptr;
}
std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBufferByAttachIdentity(
const std::string& deviceIdentifier,
const std::string& qualeIfaceApi) const
{
for (const auto& buffer : attachedStimulusBuffers)
{
if (!buffer || !buffer->deviceAttachmentSpec)
{
throw std::runtime_error(
"StimulusProducer::getAttachedStimulusBufferByAttachIdentity: "
"encountered null buffer or null deviceAttachmentSpec in "
"attachedStimulusBuffers (should never happen)");
}
if (buffer->deviceAttachmentSpec->deviceIdentifier != deviceIdentifier)
{ continue; }
if (buffer->deviceAttachmentSpec->qualeIfaceApi != qualeIfaceApi)
{ continue; }
return buffer;
}
return nullptr;
}
bool StimulusProducer::hasBufferWithQualeIfaceApi(
const std::string& qualeIfaceApi) const
{
@@ -52,6 +130,40 @@ bool StimulusProducer::hasBufferWithQualeIfaceApi(
return false;
}
void StimulusProducer::ensureNoDuplicateQualeIface(
const std::string& qualeIfaceApi) const
{
if (!hasBufferWithQualeIfaceApi(qualeIfaceApi)) {
return;
}
throw std::runtime_error(
"duplicate qualeIface '" + qualeIfaceApi
+ "' for this producer session");
}
bool StimulusProducer::addAttachedStimulusBufferIfNotExists(
const std::shared_ptr<StimulusBuffer>& buffer)
{
if (!buffer) { return false; }
auto it = std::find_if(
attachedStimulusBuffers.begin(),
attachedStimulusBuffers.end(),
[&](const std::shared_ptr<StimulusBuffer>& buf) {
return buf && buffer &&
buf->deviceAttachmentSpec && buffer->deviceAttachmentSpec &&
*(buf->deviceAttachmentSpec) == *(buffer->deviceAttachmentSpec);
});
if (it != attachedStimulusBuffers.end()) {
return false;
}
attachedStimulusBuffers.push_back(buffer);
return true;
}
void StimulusProducer::destroyAttachedStimulusBuffer(
const std::shared_ptr<StimulusBuffer>& buffer)
{
@@ -67,118 +179,94 @@ void StimulusProducer::destroyAttachedStimulusBuffer(
}
}
sscl::co::NonViralNonPostingInvoker StimulusProducer::productionCDaemon(
std::exception_ptr &, std::function<void()>,
sscl::SyncCancelerForAsyncWork &canceler)
{
const long framePeriodMs = CONFIG_STIMBUFF_FRAME_PERIOD_MS;
do
{
if (canceler.isCancellationRequested()) {
break;
}
const auto timesliceStartStamp =
std::chrono::high_resolution_clock::now();
const auto productionStartStamp =
std::chrono::high_resolution_clock::now();
co_await stimFrameProductionTimesliceCInd(canceler);
const auto productionEndStamp =
std::chrono::high_resolution_clock::now();
const long productionDurationMs = durationMsSince(
productionStartStamp, productionEndStamp);
logProductionOverrunIfNeeded(
"productionCDaemon",
productionDurationMs, framePeriodMs, nTimesliceOverruns);
const long residueMs = computeTimesliceResidueMs(
productionDurationMs, framePeriodMs);
// Schedule the next timeout based on timeslice remaining time.
const bool expiredNormally = co_await
adapters::boostAsio::getDeadlineTimerAReqAwaiter(
ioContext,
daemonTimer,
boost::posix_time::milliseconds(residueMs));
if (!expiredNormally) {
// Timer was cancelled, which is expected when stopping
break;
}
const auto timesliceEndStamp =
std::chrono::high_resolution_clock::now();
const long timesliceDurationMs = durationMsSince(
timesliceStartStamp, timesliceEndStamp);
logDaemonDurationsIfVerbose(
"productionCDaemon",
productionDurationMs, timesliceDurationMs,
framePeriodMs);
} while (!canceler.isCancellationRequested());
co_return;
}
void StimulusProducer::start()
{
std::cout << __func__ << ": Starting stimulus producer for device "
<< deviceAttachmentSpec->deviceSelector << std::endl;
nTimesliceOverruns = 0;
taskNursery.openAdmission();
taskNursery.launch(
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
{
return productionCDaemon(
lease.getExceptionStorage(),
lease.getCallerLambda(),
lease.getSyncCanceler());
});
}
void StimulusProducer::stop()
{
{
SpinLock::Guard lock(shouldContinueLock);
shouldContinue = false;
}
// Cancel timer immediately
timer.cancel();
daemonTimer.cancel();
taskNursery.requestCancelOnAll();
taskNursery.closeAdmission();
taskNursery.syncAwaitAllSettlements(
sscl::ComponentThread::getSelf()->getIoContext());
std::cout << __func__ << ": Stopped stimulus producer for device "
<< deviceAttachmentSpec->deviceSelector << std::endl;
}
void StimulusProducer::scheduleNextTimeout(int delayMs)
{
if (!shouldContinue)
{ return; }
// Schedule the next timeout using the provided delay
timer.expires_from_now(
boost::posix_time::milliseconds(delayMs));
timer.async_wait(
std::bind(
&StimulusProducer::onTimeout, this, std::placeholders::_1));
}
void StimulusProducer::onTimeout(const boost::system::error_code& error)
{
// Timer was cancelled, which is expected when stopping
if (error == boost::asio::error::operation_aborted) {
return;
}
if (error)
{
std::cerr << "StimulusProducer: Timer error: " << error.message()
<< std::endl;
return;
}
SpinLock::Guard lock(shouldContinueLock);
if (!shouldContinue)
{ return; }
/** EXPLANATION:
* We need to ensure that there's only ever one stimframe being produced
* during any CONFIG_STIMBUFF_FRAME_PERIOD_MS period. To guarantee this, we
* use a spinlock.
*
* When a new frame is to be produced, the async producer will first acquire
* the frameAssemblyLimiter spinlock. This way, when the next timeout is
* fired it can check whether its predecessor stimframe has finished being
* produced. If the preceding stimframe is still being produced, then we'll
* sleep for CONFIG_STIMBUFF_FRAME_RETRY_DELAY_MS ms before trying again.
*/
int nextWakeupDelayMs;
bool deferred = false;
if (frameAssemblyRateLimiter.tryAcquire())
{
nextWakeupDelayMs = CONFIG_STIMBUFF_FRAME_PERIOD_MS;
// Check if we're ending a deferral period
if (nDeferrals > 0)
{
auto deferralEndTime = std::chrono::high_resolution_clock::now();
auto duration = deferralEndTime - deferralStartTime;
auto durationMs = std::chrono::duration_cast<
std::chrono::milliseconds>(duration);
std::cout << __func__ << ": Deferral period ended. "
<< "Total deferrals: " << nDeferrals
<< ", Duration: " << durationMs.count() << "ms" << std::endl;
nDeferrals = 0;
}
/** EXPLANATION:
* Call the derived class's frame production handler
* Note: The derived class's frame production handler (aka
* its implementation of stimFrameProductionTimesliceInd()) must
* release the lock when frame production completes
*/
stimFrameProductionTimesliceInd();
}
else
{
nextWakeupDelayMs = CONFIG_STIMBUFF_FRAME_RETRY_DELAY_MS;
deferred = true;
++nDeferrals;
// If this is first deferral, capture start stamp and print message
if (nDeferrals == 1)
{
deferralStartTime = std::chrono::high_resolution_clock::now();
std::cerr << __func__ << ": Deferral period beginning. "
"Configured deferral period: " << nextWakeupDelayMs << "ms"
<< std::endl;
}
}
scheduleNextTimeout(nextWakeupDelayMs);
// FIXME: We should be able to release the start/stop lock at this point.
if (deferred && OptionParser::getOptions().verbose)
{
std::cerr << __func__ << ": Deferring frame by " << nextWakeupDelayMs
<< "ms due to rate limit." << std::endl;
}
}
} // namespace stim_buff
} // namespace smo
@@ -0,0 +1,69 @@
add_executable(deviceAttachmentSpecParams_tests
deviceAttachmentSpecParams_tests.cpp
)
target_include_directories(deviceAttachmentSpecParams_tests PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(deviceAttachmentSpecParams_tests
gtest_main
spinscale_test_support
${Boost_LIBRARIES}
)
add_dependencies(deviceAttachmentSpecParams_tests gtest_main)
add_test(
NAME deviceAttachmentSpecParams_tests
COMMAND deviceAttachmentSpecParams_tests)
add_executable(intrinThresholdParams_tests
intrinThresholdParams_tests.cpp
)
target_include_directories(intrinThresholdParams_tests PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(intrinThresholdParams_tests
gtest_main
spinscale_test_support
${Boost_LIBRARIES}
)
add_dependencies(intrinThresholdParams_tests gtest_main)
add_test(
NAME intrinThresholdParams_tests
COMMAND intrinThresholdParams_tests)
add_executable(stimulusProducerQualeIface_tests
stimulusProducerQualeIface_tests.cpp
)
target_include_directories(stimulusProducerQualeIface_tests PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
${CMAKE_SOURCE_DIR}/smocore/include
)
target_link_libraries(stimulusProducerQualeIface_tests
gtest_main
attachmentSupport
spinscale
spinscale_test_support
${Boost_LIBRARIES}
${OPENCL_LIBRARIES}
)
add_dependencies(stimulusProducerQualeIface_tests gtest_main)
add_test(
NAME stimulusProducerQualeIface_tests
COMMAND stimulusProducerQualeIface_tests)
@@ -0,0 +1,413 @@
#include <array>
#include <gtest/gtest.h>
#include <support/exceptionAssertions.h>
#include <user/deviceAttachmentSpec.h>
#include <vector>
namespace smo {
namespace device {
namespace {
using ParamList = std::vector<std::pair<std::string, std::string>>;
ParamList makeParams(std::initializer_list<std::pair<const char*, const char*>> entries)
{
ParamList params;
for (const auto& entry : entries)
{
params.emplace_back(entry.first, entry.second);
}
return params;
}
TEST(DeviceAttachmentSpecNamesContainTest, MatchesVectorAndArraySynonyms)
{
const std::vector<std::string> widthSynonyms = {
"frame-width",
"dim-w",
};
EXPECT_TRUE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-w"));
EXPECT_FALSE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-h"));
const std::array<std::string_view, 2> heightSynonyms = {
"frame-height",
"dim-h",
};
EXPECT_TRUE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-height"));
EXPECT_FALSE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-width"));
}
TEST(DeviceAttachmentSpecParamsContainTest, DetectsExactParamName)
{
const ParamList params = makeParams({
{"display", "0"},
{"screen", "1"},
});
EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "display"));
EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "screen"));
EXPECT_FALSE(DeviceAttachmentSpec::paramsContain(params, "width"));
}
TEST(DeviceAttachmentSpecParamNameMatchesAnySynonymGroupTest, MatchesAcrossGroups)
{
const std::vector<std::string> widthSynonyms = {"dim-w", "frame-w"};
const std::vector<std::string> heightSynonyms = {"dim-h", "frame-h"};
EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
"dim-w", widthSynonyms, heightSynonyms));
EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
"frame-h", widthSynonyms, heightSynonyms));
EXPECT_FALSE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
"colour-space", widthSynonyms, heightSynonyms));
}
TEST(DeviceAttachmentSpecNormalizeParamTokenTest, TrimsAndLowercases)
{
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(" TRUE "), "true");
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("YUV"), "yuv");
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("480p"), "480p");
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(""), "");
}
TEST(DeviceAttachmentSpecFindOptionalParamTest, LattermostExactNameWins)
{
const ParamList params = makeParams({
{"display", "0"},
{"other", "ignored"},
{"display", "2"},
});
const std::optional<std::pair<std::string, std::string>> matched =
DeviceAttachmentSpec::findOptionalParam(params, "display");
ASSERT_TRUE(matched.has_value());
EXPECT_EQ(matched->first, "display");
EXPECT_EQ(matched->second, "2");
}
TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, LattermostSynonymWins)
{
const std::vector<std::string> synonyms = {
"cmd-timeout-ms",
"command-timeout-ms",
};
const ParamList params = makeParams({
{"cmd-timeout-ms", "5"},
{"command-timeout-ms", "25"},
});
const std::optional<std::pair<std::string, std::string>> matched =
DeviceAttachmentSpec::findOptionalParamWithSynonyms(params, synonyms);
ASSERT_TRUE(matched.has_value());
EXPECT_EQ(matched->first, "command-timeout-ms");
EXPECT_EQ(matched->second, "25");
}
TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, AbsentReturnsNullopt)
{
const std::vector<std::string> synonyms = {"data-port"};
const ParamList params = makeParams({{"cmd-port", "56001"}});
EXPECT_FALSE(DeviceAttachmentSpec::findOptionalParamWithSynonyms(
params, synonyms).has_value());
}
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, ParsesValidInteger)
{
EXPECT_EQ(
DeviceAttachmentSpec::parseParamValueAsInt("width", "1280"),
1280);
EXPECT_EQ(
DeviceAttachmentSpec::parseParamValueAsInt("width", "-42"),
-42);
}
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, InvalidIntegerThrows)
{
try {
DeviceAttachmentSpec::parseParamValueAsInt("width", "not-a-number");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "Failed to parse 'width'");
}
}
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, OutOfRangeThrows)
{
try {
DeviceAttachmentSpec::parseParamValueAsInt(
"width",
"999999999999999999999");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "out of range");
}
}
TEST(DeviceAttachmentSpecParseParamValueAsPositiveIntTest, RejectsNonPositive)
{
EXPECT_EQ(
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "640"),
640);
try {
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "0");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "must be positive");
}
try {
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "-10");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "must be positive");
}
}
TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, ParsesRecognizedValues)
{
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(""));
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("true"));
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(" YES "));
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("1"));
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("false"));
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("0"));
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("no"));
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool(
"", /*emptyMeansTrue=*/false));
}
TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, UnknownValueThrows)
{
EXPECT_THROW(
DeviceAttachmentSpec::parseParamValueAsBool("maybe"),
std::runtime_error);
}
TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, MissingParamThrows)
{
const ParamList params = makeParams({{"screen", "1"}});
try {
DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "No display specified in params");
}
}
TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, LattermostValueWins)
{
const ParamList params = makeParams({
{"display", "0"},
{"display", "3"},
});
EXPECT_EQ(
DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display"),
3);
}
TEST(DeviceAttachmentSpecParseOptionalParamAsIntTest, ReturnsDefaultWhenAbsent)
{
const ParamList params = makeParams({{"screen", "1"}});
EXPECT_EQ(
DeviceAttachmentSpec::parseOptionalParamAsInt(params, "display", 42),
42);
}
TEST(DeviceAttachmentSpecParseOptionalParamAsIntWithSynonymsTest, SynonymLattermostWins)
{
const std::vector<std::string> synonyms = {
"n-dgrams-per-frame",
"num-dgrams-per-frame",
};
const ParamList params = makeParams({
{"n-dgrams-per-frame", "10"},
{"num-dgrams-per-frame", "99"},
});
EXPECT_EQ(
DeviceAttachmentSpec::parseOptionalParamAsIntWithSynonyms(
params, synonyms, 84),
99);
}
TEST(DeviceAttachmentSpecParseRequiredParamAsIntWithSynonymsTest, ParsesMatchedSynonym)
{
const std::vector<std::string> synonyms = {
"cmd-timeout-ms",
"command-timeout-ms",
};
const ParamList params = makeParams({
{"command-timeout-ms", "15"},
});
EXPECT_EQ(
DeviceAttachmentSpec::parseRequiredParamAsIntWithSynonyms(
params,
synonyms,
"missing timeout"),
15);
}
TEST(DeviceAttachmentSpecParseRequiredParamValueWithSynonymsTest, RequiresNonEmptyValue)
{
const std::vector<std::string> synonyms = {"colour-space"};
const ParamList missing = makeParams({{"width", "640"}});
try {
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
missing, synonyms, "colour-space required");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "colour-space required");
}
const ParamList emptyValue = makeParams({{"colour-space", ""}});
try {
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
emptyValue, synonyms, "colour-space required");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "colour-space required");
}
const ParamList present = makeParams({{"colour-space", "yuv"}});
EXPECT_EQ(
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
present, synonyms, "colour-space required"),
"yuv");
}
TEST(DeviceAttachmentSpecParseOptionalParamValueWithSynonymsTest, OptionalStringValue)
{
const std::vector<std::string> synonyms = {"colour-space"};
const ParamList absent = makeParams({{"width", "640"}});
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
absent, synonyms, "empty colour-space").has_value());
const ParamList emptyValue = makeParams({{"colour-space", ""}});
try {
DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
emptyValue, synonyms, "empty colour-space");
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "empty colour-space");
}
const ParamList present = makeParams({{"colour-space", "yuv"}});
const std::optional<std::string> parsed =
DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
present, synonyms, "empty colour-space");
ASSERT_TRUE(parsed.has_value());
EXPECT_EQ(*parsed, "yuv");
}
TEST(DeviceAttachmentSpecParseOptionalParamAsBoolWithSynonymsTest, ParsesPresenceAndValues)
{
const std::vector<std::string> synonyms = {
"full-planar-is-optional",
"opt-planar",
};
const ParamList absent = makeParams({{"width", "640"}});
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
absent, synonyms, false));
const ParamList presentEmpty = makeParams({{"opt-planar", ""}});
EXPECT_TRUE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
presentEmpty, synonyms, false));
const ParamList presentFalse = makeParams({{"opt-planar", "false"}});
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
presentFalse, synonyms, true));
const ParamList lattermostWins = makeParams({
{"opt-planar", "true"},
{"full-planar-is-optional", "false"},
});
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
lattermostWins, synonyms, true));
}
TEST(DeviceAttachmentSpecRejectUnknownParamsTest, RejectsUnknownNames)
{
const std::vector<std::string> knownA = {"width", "dim-w"};
const std::vector<std::string> knownB = {"height", "dim-h"};
const ParamList params = makeParams({
{"dim-w", "640"},
{"unknown-param", "1"},
});
try {
DeviceAttachmentSpec::rejectUnknownParams(
params,
"unknown param '",
knownA,
knownB);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "unknown-param");
}
}
TEST(DeviceAttachmentSpecRejectUnknownParamsTest, AcceptsAllKnownNames)
{
const std::vector<std::string> knownA = {"width", "dim-w"};
const std::vector<std::string> knownB = {"height", "dim-h"};
const ParamList params = makeParams({
{"dim-w", "640"},
{"dim-h", "480"},
});
EXPECT_NO_THROW(DeviceAttachmentSpec::rejectUnknownParams(
params,
"unknown param '",
knownA,
knownB));
}
} // namespace
} // namespace device
} // namespace smo
@@ -0,0 +1,331 @@
#include <gtest/gtest.h>
#include <support/exceptionAssertions.h>
#include <user/intrinThresholdParams.h>
#include <vector>
namespace smo {
namespace intrin {
namespace {
using ParamList = std::vector<std::pair<std::string, std::string>>;
ParamList makeParams(std::initializer_list<std::pair<const char*, const char*>> entries)
{
ParamList params;
for (const auto& entry : entries)
{
params.emplace_back(entry.first, entry.second);
}
return params;
}
TEST(IntrinThresholdArrayContainsTest, MatchesCanonicalNames)
{
EXPECT_TRUE(arrayContains(kIntrinInterestPcNames, "interest-pc"));
EXPECT_TRUE(arrayContains(kIntrinInterestThrNames, "interest-thr"));
EXPECT_FALSE(arrayContains(kForbiddenUnitlessIntrinStems, "interest-pc"));
}
TEST(IntrinThresholdIsKnownParamNameTest, RecognizesAllCanonicalFamilies)
{
EXPECT_TRUE(isKnownIntrinThresholdParamName("interest-pc"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("interest-threshold"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("distraction-percentage"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("distraction-thr"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("stupefying-pc"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("stupefaction-thresh"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("intolerable-pc"));
EXPECT_TRUE(isKnownIntrinThresholdParamName("intolerable-threshold"));
EXPECT_FALSE(isKnownIntrinThresholdParamName("interest"));
EXPECT_FALSE(isKnownIntrinThresholdParamName("passband-count-gt-val"));
}
TEST(IntrinThresholdValidateNoIntrinParamsOnQualeIfaceTest, RejectsKnownIntrinNames)
{
const ParamList params = makeParams({{"interest-pc", "85"}});
try {
validateNoIntrinParamsOnQualeIface("pcloudLightAmbience", params);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "not valid on qualeIfaceApi");
}
}
TEST(IntrinThresholdValidateNoIntrinParamsOnQualeIfaceTest, RejectsUnitlessStems)
{
const ParamList params = makeParams({{"distraction", "10"}});
try {
validateNoIntrinParamsOnQualeIface("mesh", params);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "invalid without a unit suffix");
}
}
TEST(IntrinThresholdValidateNoIntrinParamsOnQualeIfaceTest, RejectsFromStimbuffMarker)
{
const ParamList params = makeParams({{"from-stimbuff", ""}});
try {
validateNoIntrinParamsOnQualeIface("mesh", params);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "from-stimbuff");
}
}
TEST(IntrinThresholdValidateNoIntrinParamsOnQualeIfaceTest, AllowsSensoryParams)
{
const ParamList params = makeParams({
{"passband-count-gt-val", "12"},
{"histbuff-ms", "30000"},
});
EXPECT_NO_THROW(validateNoIntrinParamsOnQualeIface(
"pcloudLightAmbience", params));
}
TEST(IntrinThresholdValidateIntrinSegmentParamsTest, RejectsUnitlessStems)
{
const ParamList params = makeParams({{"stupefaction", "5"}});
try {
validateIntrinSegmentParams("postrin", params);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "invalid without a unit suffix");
}
}
TEST(IntrinThresholdValidateIntrinSegmentParamsTest, AllowsSuffixedNames)
{
const ParamList params = makeParams({
{"interest-pc", "85"},
{"distraction-thr", "12"},
});
EXPECT_NO_THROW(validateIntrinSegmentParams("negtrin", params));
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, ReturnsDefaultWhenAbsent)
{
const ParamList params = makeParams({{"other-param", "1"}});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
/*defaultValue=*/7,
/*defaultUnit=*/ThresholdUnit::Absolute);
EXPECT_FALSE(parsed.wasSpecified);
EXPECT_EQ(parsed.value, 7);
EXPECT_EQ(parsed.unit, ThresholdUnit::Absolute);
EXPECT_TRUE(parsed.matchedName.empty());
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, ParsesPercentageParam)
{
const ParamList params = makeParams({{"interest-percentage", "50"}});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Absolute);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.value, 50);
EXPECT_EQ(parsed.unit, ThresholdUnit::Percentage);
EXPECT_EQ(parsed.matchedName, "interest-percentage");
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, ParsesAbsoluteParam)
{
const ParamList params = makeParams({{"interest-threshold", "42"}});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Percentage);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.value, 42);
EXPECT_EQ(parsed.unit, ThresholdUnit::Absolute);
EXPECT_EQ(parsed.matchedName, "interest-threshold");
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, LattermostMatchingParamWins)
{
const ParamList params = makeParams({
{"interest-pc", "10"},
{"interest-threshold", "99"},
});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Percentage);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.value, 99);
EXPECT_EQ(parsed.unit, ThresholdUnit::Absolute);
EXPECT_EQ(parsed.matchedName, "interest-threshold");
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, LattermostPercentageWinsOverEarlierAbsolute)
{
const ParamList params = makeParams({
{"interest-thr", "12"},
{"interest-pc", "75"},
});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Absolute);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.value, 75);
EXPECT_EQ(parsed.unit, ThresholdUnit::Percentage);
EXPECT_EQ(parsed.matchedName, "interest-pc");
}
TEST(IntrinThresholdParseOptionalThresholdParamTest, InvalidIntegerThrows)
{
const ParamList params = makeParams({{"interest-pc", "not-int"}});
try {
parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Absolute);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception, "Failed to parse 'interest-pc'");
}
}
TEST(IntrinThresholdResolveThresholdValueTest, PercentageUsesBase)
{
const ParsedThresholdParam percentageParam = {
.value = 50,
.unit = ThresholdUnit::Percentage,
.matchedName = "interest-pc",
.wasSpecified = true,
};
EXPECT_EQ(resolveThresholdValue(percentageParam, 84), 42u);
}
TEST(IntrinThresholdResolveThresholdValueTest, AbsolutePassesThrough)
{
const ParsedThresholdParam absoluteParam = {
.value = 17,
.unit = ThresholdUnit::Absolute,
.matchedName = "interest-thr",
.wasSpecified = true,
};
EXPECT_EQ(resolveThresholdValue(absoluteParam, 84), 17u);
}
TEST(IntrinThresholdResolveThresholdValueTest, PercentageTruncatesTowardZero)
{
const ParsedThresholdParam percentageParam = {
.value = 33,
.unit = ThresholdUnit::Percentage,
.matchedName = "interest-pc",
.wasSpecified = true,
};
EXPECT_EQ(resolveThresholdValue(percentageParam, 100), 33u);
EXPECT_EQ(resolveThresholdValue(percentageParam, 10), 3u);
}
TEST(IntrinThresholdSynonymCoverageTest, AllThrSynonymsParseAsAbsolute)
{
const std::array<std::string_view, 3> thrNames = {
"interest-threshold",
"interest-thresh",
"interest-thr",
};
for (const std::string_view thrName : thrNames)
{
const ParamList params = makeParams({
{thrName.data(), "11"},
});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Percentage);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.unit, ThresholdUnit::Absolute);
EXPECT_EQ(parsed.value, 11);
EXPECT_EQ(parsed.matchedName, thrName);
}
}
TEST(IntrinThresholdSynonymCoverageTest, AllPcSynonymsParseAsPercentage)
{
const std::array<std::string_view, 2> pcNames = {
"interest-percentage",
"interest-pc",
};
for (const std::string_view pcName : pcNames)
{
const ParamList params = makeParams({
{pcName.data(), "25"},
});
const ParsedThresholdParam parsed = parseOptionalThresholdParam(
params,
kIntrinInterestPcNames,
kIntrinInterestThrNames,
0,
ThresholdUnit::Absolute);
EXPECT_TRUE(parsed.wasSpecified);
EXPECT_EQ(parsed.unit, ThresholdUnit::Percentage);
EXPECT_EQ(parsed.value, 25);
EXPECT_EQ(parsed.matchedName, pcName);
}
}
} // namespace
} // namespace intrin
} // namespace smo
@@ -0,0 +1,281 @@
#include <boost/asio/io_context.hpp>
#include <gtest/gtest.h>
#include <opts.h>
#include <spinscale/componentThread.h>
#include <spinscale/co/invokers.h>
#include <spinscale/syncCancelerForAsyncWork.h>
#include <support/exceptionAssertions.h>
#include <unistd.h>
#include <user/comparatorApiDesc.h>
#include <user/compute.h>
#include <user/stimulusBuffer.h>
#include <user/stimulusProducer.h>
#define CL_TARGET_OPENCL_VERSION 120
#include <CL/cl.h>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace smo {
namespace stim_buff {
namespace {
class TestStimulusProducer
: public StimulusProducer
{
public:
using StimulusProducer::StimulusProducer;
std::shared_ptr<StimulusBuffer> getOrCreateAttachedStimulusBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>&
deviceAttachmentSpec) override
{
(void)deviceAttachmentSpec;
throw std::logic_error("not used in these unit tests");
}
bool exportsQualeIfaceApi(const std::string& qualeIfaceApi) const override
{
(void)qualeIfaceApi;
return true;
}
protected:
sscl::co::ViralNonPostingInvoker<void> stimFrameProductionTimesliceCInd(
sscl::SyncCancelerForAsyncWork& canceler) override
{
(void)canceler;
co_return;
}
};
std::optional<std::string> searchForLibInSmoSearchPathsStub(
const std::string& libraryPath)
{
(void)libraryPath;
return std::nullopt;
}
std::shared_ptr<sscl::ComponentThread> componentThreadGetSelfStub()
{
return sscl::ComponentThread::getSelf();
}
OptionParser& optionParserGetOptionsStub()
{
return OptionParser::getOptions();
}
void releaseUseHostPtrBufferStub(
std::shared_ptr<smo::compute::ClBuffer> buffer)
{
(void)buffer;
}
std::shared_ptr<smo::compute::ComputeDevice> computeManagerGetDeviceStub()
{
return nullptr;
}
void computeManagerReleaseDeviceStub(
std::shared_ptr<smo::compute::ComputeDevice> device)
{
(void)device;
}
std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>
comparatorManagerGetComparatorTypeStub(smo::cologex::ComparatorTypeId typeId)
{
(void)typeId;
return nullptr;
}
std::unique_ptr<smo::cologex::Comparator> comparatorGetNewInstanceStub(
const std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>& comparatorType)
{
(void)comparatorType;
return nullptr;
}
std::shared_ptr<smo::compute::ClBuffer> createUseHostPtrBufferStub(
void* hostPtr,
size_t size,
cl_mem_flags flags)
{
static const std::vector<std::shared_ptr<smo::compute::ComputeDevice>>
emptyDevices;
return std::make_shared<smo::compute::ClBuffer>(
hostPtr, size, flags, emptyDevices);
}
const SmoCallbacks kTestSmoCallbacks = {
.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPathsStub,
.ComponentThread_getSelf = componentThreadGetSelfStub,
.OptionParser_getOptions = optionParserGetOptionsStub,
.ComputeManager_createUseHostPtrBuffer = createUseHostPtrBufferStub,
.ComputeManager_releaseUseHostPtrBuffer = releaseUseHostPtrBufferStub,
.ComputeManager_getDevice = computeManagerGetDeviceStub,
.ComputeManager_releaseDevice = computeManagerReleaseDeviceStub,
.ComparatorManager_getComparatorType =
comparatorManagerGetComparatorTypeStub,
.Comparator_getNewInstance = comparatorGetNewInstanceStub,
};
StagingBuffer::IOEngineConstraints testBufferConstraints()
{
const size_t pageSize = static_cast<size_t>(sysconf(_SC_PAGE_SIZE));
const size_t channelByteSize = pageSize;
return StagingBuffer::IOEngineConstraints(
1,
channelByteSize,
pageSize,
pageSize);
}
std::shared_ptr<device::DeviceAttachmentSpec> makeAttachSpec(
const std::string& deviceSelector,
const std::string& qualeIfaceApi,
const std::string& deviceIdentifier = "cam0")
{
auto spec = std::make_shared<device::DeviceAttachmentSpec>();
spec->deviceIdentifier = deviceIdentifier;
spec->sensorType = 'e';
spec->qualeIfaceApi = qualeIfaceApi;
spec->stimBuffApi = "testBuff";
spec->provider = "testProvider";
spec->deviceSelector = deviceSelector;
return spec;
}
std::shared_ptr<StimulusBuffer> makeAttachedTestBuffer(
TestStimulusProducer& producer,
const std::shared_ptr<device::DeviceAttachmentSpec>& attachSpec)
{
const StagingBuffer::IOEngineConstraints constraints =
testBufferConstraints();
return std::make_shared<StimulusBuffer>(
producer,
attachSpec,
CONFIG_STIMBUFF_FRAME_PERIOD_MS,
constraints,
constraints,
kTestSmoCallbacks,
CL_MEM_READ_WRITE);
}
class StimulusProducerQualeIfaceTest
: public ::testing::Test
{
protected:
boost::asio::io_context ioContext;
std::shared_ptr<device::DeviceAttachmentSpec> producerSpec =
makeAttachSpec("model-substr:USB;location:external", "colour-yuv-y");
TestStimulusProducer producer{producerSpec, ioContext};
};
TEST_F(StimulusProducerQualeIfaceTest, HasBufferWithQualeIfaceApiDetectsChannel)
{
EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
const auto attachSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
const auto buffer = makeAttachedTestBuffer(producer, attachSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(buffer));
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-u"));
}
TEST_F(StimulusProducerQualeIfaceTest,
AddAttachedStimulusBufferIfNotExistsAllowsSameQualeDifferentSelector)
{
const auto firstSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));
const auto secondSpec = makeAttachSpec(
"model-substr:USB Video Device;location:external",
"colour-yuv-y",
"cam-y-1");
const auto secondBuffer = makeAttachedTestBuffer(producer, secondSpec);
EXPECT_TRUE(producer.addAttachedStimulusBufferIfNotExists(secondBuffer));
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 2u);
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
EXPECT_NE(firstSpec->deviceSelector, secondSpec->deviceSelector);
}
TEST_F(StimulusProducerQualeIfaceTest,
EnsureNoDuplicateQualeIfaceRejectsAlternateSelector)
{
const auto firstSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));
const auto duplicateQualeSpec = makeAttachSpec(
"model-substr:USB Video Device;location:external",
"colour-yuv-y",
"cam-y-1");
try {
producer.ensureNoDuplicateQualeIface(
duplicateQualeSpec->qualeIfaceApi);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"duplicate qualeIface 'colour-yuv-y'");
}
}
TEST_F(StimulusProducerQualeIfaceTest,
EnsureNoDuplicateQualeIfaceAllowsDistinctChannels)
{
const auto ySpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
makeAttachedTestBuffer(producer, ySpec)));
EXPECT_NO_THROW(
producer.ensureNoDuplicateQualeIface("colour-yuv-u"));
EXPECT_NO_THROW(
producer.ensureNoDuplicateQualeIface("colour-yuv-v"));
}
TEST_F(StimulusProducerQualeIfaceTest,
GetAttachedStimulusBufferByAttachIdentityIgnoresSelectorString)
{
const auto attachedSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-u", "cam-u");
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
makeAttachedTestBuffer(producer, attachedSpec)));
const auto foundBuffer =
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-u");
ASSERT_NE(foundBuffer, nullptr);
EXPECT_EQ(foundBuffer, producer.attachedStimulusBuffers.front());
EXPECT_NE(
foundBuffer->deviceAttachmentSpec->deviceSelector,
"model-substr:USB Video Device;location:external");
EXPECT_EQ(
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-y"),
nullptr);
EXPECT_EQ(
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-y", "colour-yuv-u"),
nullptr);
}
} // namespace
} // namespace stim_buff
} // namespace smo
+130
View File
@@ -0,0 +1,130 @@
option(ENABLE_LIB_lcameraDev "Enable libcamera device provider backend lib" OFF)
if(ENABLE_LIB_lcameraDev)
pkg_check_modules(LIBCAMERA libcamera)
if(NOT LIBCAMERA_FOUND)
message(FATAL_ERROR
"libcamera not found. Install libcamera-dev (and runtime libcamera0.2 "
"+ libcamera-ipa), then reconfigure with -DENABLE_LIB_lcameraDev=ON.")
endif()
add_compile_definitions(CONFIG_LIB_LCAMERADEV_ENABLED)
add_library(lcameraDev SHARED
lcameraDev.cpp
cameraIdentity.cpp
cameraModeRequest.cpp
planarYuvFormatPolicy.cpp
sessionModeConfigure.cpp
selectorParse.cpp
selectorResolve.cpp
cameraManagerState.cpp
cameraSession.cpp
)
set_target_properties(lcameraDev PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
target_compile_definitions(lcameraDev PRIVATE CONFIG_LIB_LCAMERADEV_ENABLED)
target_include_directories(lcameraDev PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${LIBCAMERA_INCLUDE_DIRS}
)
target_link_libraries(lcameraDev PUBLIC
Boost::system
Boost::log
spinscale
${LIBCAMERA_LIBRARIES}
)
target_link_directories(lcameraDev PUBLIC
${LIBCAMERA_LIBRARY_DIRS}
)
add_custom_command(TARGET lcameraDev POST_BUILD
COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$<TARGET_FILE:lcameraDev>"
-P ${CMAKE_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake
COMMENT "Verifying Boost dynamic dependencies for lcameraDev"
)
install(TARGETS lcameraDev
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
option(ENABLE_LCAMERADEV_TOOLS "Build lcameraDev probe/list tools" ON)
if(ENABLE_LCAMERADEV_TOOLS)
if(NOT TARGET spinscale_test_support)
message(FATAL_ERROR
"lcameraDev probe tools require spinscale_test_support. "
"Configure with -DLIBSPINSCALE_BUILD_TESTS=ON "
"(salmanoff sets this automatically when -DENABLE_TESTS=ON).")
endif()
add_executable(lcameraDev_list_cameras
tools/lcameraDevListCameras.cpp
tools/probeRunner.cpp
)
target_include_directories(lcameraDev_list_cameras PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tools
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(lcameraDev_list_cameras PRIVATE
lcameraDev
spinscale
spinscale_test_support
Boost::system
Boost::log
)
add_executable(lcameraDev_probe
tools/lcameraDevProbe.cpp
tools/probeRunner.cpp
)
target_include_directories(lcameraDev_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tools
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(lcameraDev_probe PRIVATE
lcameraDev
spinscale
spinscale_test_support
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)
add_subdirectory(tests)
endif()
endif()
+67
View File
@@ -0,0 +1,67 @@
#include <cameraIdentity.h>
#include <libcamera/property_ids.h>
#include <algorithm>
#include <cctype>
#include <optional>
#include <string_view>
namespace lcamera_dev {
std::string locationPropertyToLabel(int32_t locationValue)
{
using namespace libcamera::properties;
if (locationValue == CameraLocationFront) {
return "front";
}
if (locationValue == CameraLocationBack) {
return "back";
}
if (locationValue == CameraLocationExternal) {
return "external";
}
return "";
}
CameraIdentityRecord buildIdentityRecord(
const std::shared_ptr<libcamera::Camera>& camera)
{
CameraIdentityRecord record;
record.id = camera->id();
const libcamera::ControlList& props = camera->properties();
const std::optional<std::string_view> model =
props.get(libcamera::properties::Model);
if (model) {
record.model.assign(model->begin(), model->end());
}
const std::optional<int> location =
props.get(libcamera::properties::Location);
if (location)
{
record.locationLabel = locationPropertyToLabel(*location);
}
return record;
}
std::vector<CameraIdentityRecord> buildIdentityRecords(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras)
{
std::vector<CameraIdentityRecord> records;
records.reserve(cameras.size());
for (const auto& camera : cameras)
{
if (!camera) { continue; }
records.push_back(buildIdentityRecord(camera));
}
return records;
}
} // namespace lcamera_dev
+28
View File
@@ -0,0 +1,28 @@
#ifndef LCAMERA_DEV_CAMERA_IDENTITY_H
#define LCAMERA_DEV_CAMERA_IDENTITY_H
#include <libcamera/camera.h>
#include <optional>
#include <string>
#include <vector>
namespace lcamera_dev {
struct CameraIdentityRecord
{
std::string id;
std::string model;
std::string locationLabel;
};
CameraIdentityRecord buildIdentityRecord(
const std::shared_ptr<libcamera::Camera>& camera);
std::vector<CameraIdentityRecord> buildIdentityRecords(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras);
std::string locationPropertyToLabel(int32_t locationValue);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_IDENTITY_H
@@ -0,0 +1,291 @@
#include <boostAsioLinkageFix.h>
#include <cameraIdentity.h>
#include <cameraManagerState.h>
#include <selectorResolve.h>
#include <algorithm>
#include <stdexcept>
namespace lcamera_dev {
namespace {
static LcameraDevState lcameraDevState;
void startCameraManager(CameraManagerResources& resources)
{
resources.cameraManager = std::make_unique<libcamera::CameraManager>();
if (resources.cameraManager->start())
{
resources.cameraManager.reset();
throw std::runtime_error(
"lcameraDev: failed to start libcamera CameraManager");
}
}
void stopCameraManager(CameraManagerResources& resources)
{
if (!resources.cameraManager) {
return;
}
resources.cameraManager->stop();
resources.cameraManager.reset();
}
std::shared_ptr<libcamera::Camera> findCameraById(
const std::vector<std::shared_ptr<libcamera::Camera>>& cameras,
const std::string& cameraId)
{
for (const std::shared_ptr<libcamera::Camera>& camera : cameras)
{
if (camera && camera->id() == cameraId) {
return camera;
}
}
return nullptr;
}
struct LiveCameraSnapshot
{
std::vector<std::shared_ptr<libcamera::Camera>> cameras;
std::vector<CameraIdentityRecord> 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()
{
return lcameraDevState;
}
std::vector<std::shared_ptr<libcamera::Camera>> listLibcameraCameras()
{
LcameraDevState& state = getLcameraDevState();
if (!state.isInitialized || !state.managerState.rsrc.cameraManager) {
return {};
}
return state.managerState.rsrc.cameraManager->cameras();
}
void lcameraDevMain(
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
LcameraDevState& state = getLcameraDevState();
if (state.isInitialized) {
return;
}
if (!componentThread)
{
throw std::runtime_error(
"lcameraDev_main: componentThread must be non-null");
}
startCameraManager(state.managerState.rsrc);
state.componentThread = componentThread;
state.isInitialized = true;
}
void lcameraDevExit()
{
LcameraDevState& state = getLcameraDevState();
if (!state.isInitialized) {
return;
}
CameraManagerResources& resources = state.managerState.rsrc;
for (auto& entry : resources.sessionsByCameraId)
{
std::shared_ptr<CameraSession> session = entry.second;
if (!session || !session->s.rsrc.camera) {
continue;
}
session->s.rsrc.camera->release();
}
resources.sessionsByCameraId.clear();
stopCameraManager(resources);
state.componentThread.reset();
state.isInitialized = false;
}
sscl::co::ViralNonPostingInvoker<CameraIdentityRecord>
resolveDeviceSelectorCReq(const std::string& deviceSelector)
{
requireNonEmptyDeviceSelector(
deviceSelector, "lcameraDev_resolveDeviceSelectorCReq");
LcameraDevState& state = getLcameraDevState();
sscl::co::CoQutex::ReleaseHandle managerGuard =
co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();
co_return resolveDeviceSelectorForLiveCameras(
deviceSelector, state.managerState.rsrc);
}
sscl::co::ViralNonPostingInvoker<LcameraDevGetOrCreateResult>
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 =
resolveDeviceSelectorAgainstRecords(
deviceSelector, snapshot.identityRecords);
const std::string& resolvedCameraId = resolvedRecord.id;
auto sessionIt =
state.managerState.rsrc.sessionsByCameraId.find(resolvedCameraId);
if (sessionIt != state.managerState.rsrc.sessionsByCameraId.end())
{
std::shared_ptr<CameraSession> session = sessionIt->second;
sscl::co::CoQutex::ReleaseHandle sessionGuard =
co_await session->s.lock.getAcquireInvocationAndSuspensionPolicy();
session->incrementRefcount();
LcameraDevGetOrCreateResult result;
result.deviceSession = session;
result.resolvedIdentity = session->getIdentityRecord();
co_return result;
}
std::shared_ptr<libcamera::Camera> camera =
findCameraById(snapshot.cameras, resolvedCameraId);
if (!camera)
{
throw std::runtime_error(
"lcameraDev: resolved camera is no longer available: "
+ resolvedCameraId);
}
if (camera->acquire())
{
throw std::runtime_error(
"lcameraDev: failed to acquire camera: " + resolvedCameraId);
}
std::shared_ptr<CameraSession> session =
std::make_shared<CameraSession>(resolvedRecord, camera);
session->incrementRefcount();
state.managerState.rsrc.sessionsByCameraId.emplace(
resolvedCameraId, session);
LcameraDevGetOrCreateResult result;
result.deviceSession = session;
result.resolvedIdentity = session->getIdentityRecord();
co_return result;
}
sscl::co::ViralNonPostingInvoker<void>
releaseDeviceSessionCReq(
const std::shared_ptr<CameraSession>& deviceSession)
{
if (!deviceSession) { co_return; }
LcameraDevState& state = getLcameraDevState();
sscl::co::CoQutex::ReleaseHandle managerGuard =
co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();
const auto sessionIt = std::find_if(
state.managerState.rsrc.sessionsByCameraId.begin(),
state.managerState.rsrc.sessionsByCameraId.end(),
[&deviceSession](const auto& entry) {
return entry.second == deviceSession;
});
if (sessionIt == state.managerState.rsrc.sessionsByCameraId.end()) {
co_return;
}
bool shouldDestroy = false;
{
sscl::co::CoQutex::ReleaseHandle sessionGuard =
co_await deviceSession->s.lock.getAcquireInvocationAndSuspensionPolicy();
shouldDestroy = deviceSession->decrementRefcount();
}
if (!shouldDestroy) {
co_return;
}
if (deviceSession->s.rsrc.camera) {
deviceSession->s.rsrc.camera->release();
}
state.managerState.rsrc.sessionsByCameraId.erase(sessionIt);
co_return;
}
sscl::co::ViralNonPostingInvoker<std::vector<LcameraDevCameraInfo>>
enumerateCamerasCReq()
{
LcameraDevState& state = getLcameraDevState();
sscl::co::CoQutex::ReleaseHandle managerGuard =
co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();
const LiveCameraSnapshot snapshot =
snapshotLiveCameras(state.managerState.rsrc);
std::vector<LcameraDevCameraInfo> cameraInfos;
cameraInfos.reserve(snapshot.identityRecords.size());
for (const CameraIdentityRecord& record : snapshot.identityRecords)
{
cameraInfos.push_back(LcameraDevCameraInfo{
record.id,
record.model,
record.locationLabel
});
}
co_return cameraInfos;
}
} // namespace lcamera_dev
@@ -0,0 +1,58 @@
#ifndef LCAMERA_DEV_CAMERA_MANAGER_STATE_H
#define LCAMERA_DEV_CAMERA_MANAGER_STATE_H
#include <lcameraDev.h>
#include <cameraSession.h>
#include <libcamera/camera_manager.h>
#include <map>
#include <memory>
#include <spinscale/co/coQutex.h>
#include <spinscale/componentThread.h>
#include <spinscale/sharedResourceGroup.h>
#include <string>
#include <vector>
namespace lcamera_dev {
struct CameraManagerResources
{
std::unique_ptr<libcamera::CameraManager> cameraManager;
std::map<std::string, std::shared_ptr<CameraSession>> sessionsByCameraId;
};
struct LcameraDevState
{
LcameraDevState()
: managerState("lcameraDev::CameraManager")
{}
bool isInitialized = false;
std::shared_ptr<sscl::ComponentThread> componentThread;
sscl::SharedResourceGroup<sscl::co::CoQutex, CameraManagerResources>
managerState;
};
LcameraDevState& getLcameraDevState();
void lcameraDevMain(
const std::shared_ptr<sscl::ComponentThread>& componentThread);
void lcameraDevExit();
std::vector<std::shared_ptr<libcamera::Camera>> listLibcameraCameras();
sscl::co::ViralNonPostingInvoker<CameraIdentityRecord>
resolveDeviceSelectorCReq(const std::string& deviceSelector);
sscl::co::ViralNonPostingInvoker<LcameraDevGetOrCreateResult>
getOrCreateDeviceSessionCReq(const std::string& deviceSelector);
sscl::co::ViralNonPostingInvoker<void>
releaseDeviceSessionCReq(
const std::shared_ptr<CameraSession>& deviceSession);
sscl::co::ViralNonPostingInvoker<std::vector<LcameraDevCameraInfo>>
enumerateCamerasCReq();
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_MANAGER_STATE_H
@@ -0,0 +1,33 @@
#include <cameraModeRequest.h>
#include <sstream>
#include <stdexcept>
namespace lcamera_dev {
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");
}
}
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
+43
View File
@@ -0,0 +1,43 @@
#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, configure may accept semi-planar or packed YUV; lcameraBuff
* deinterleaves components in later capture stages.
*/
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);
bool cameraModeRequestsEqual(
const LcameraDevCameraModeRequest& left,
const LcameraDevCameraModeRequest& right);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_MODE_REQUEST_H
+83
View File
@@ -0,0 +1,83 @@
#include <boostAsioLinkageFix.h>
#include <cameraSession.h>
#include <sessionModeConfigure.h>
#include <stdexcept>
namespace lcamera_dev {
CameraSession::CameraSession(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& camera)
: s("lcameraDev::CameraSession", CameraSessionResources{identity, camera})
{}
void CameraSession::incrementRefcount()
{
++s.rsrc.refcount;
}
bool CameraSession::decrementRefcount()
{
if (s.rsrc.refcount <= 0)
{
throw std::logic_error(
"lcameraDev: releaseDeviceCReq refcount underflow");
}
--s.rsrc.refcount;
return s.rsrc.refcount == 0;
}
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);
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
+76
View File
@@ -0,0 +1,76 @@
#ifndef LCAMERA_DEV_CAMERA_SESSION_H
#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 {
struct CameraSessionResources
{
CameraSessionResources(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& camera)
: identity(identity), camera(camera)
{}
int refcount = 0;
CameraIdentityRecord identity;
std::shared_ptr<libcamera::Camera> camera;
bool isStreaming = false;
LcameraDevCameraModeRequest configuredRequest;
std::optional<LcameraDevConfiguredCameraMode> configuredMode;
std::shared_ptr<libcamera::CameraConfiguration> heldConfiguration;
int libcameraConfigureCallCount = 0;
};
class CameraSession
{
public:
CameraSession(
const CameraIdentityRecord& identity,
const std::shared_ptr<libcamera::Camera>& camera);
const CameraIdentityRecord& getIdentityRecord() const
{ return s.rsrc.identity; }
const std::shared_ptr<libcamera::Camera>& getCamera() const
{ return s.rsrc.camera; }
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;
};
} // namespace lcamera_dev
#endif // LCAMERA_DEV_CAMERA_SESSION_H
+96
View File
@@ -0,0 +1,96 @@
#include <boostAsioLinkageFix.h>
#include <cameraManagerState.h>
#include <lcameraDev.h>
#include <stdexcept>
extern "C" {
void lcameraDev_main(
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcamera_dev::lcameraDevMain(componentThread);
}
void lcameraDev_exit(void)
{
lcamera_dev::lcameraDevExit();
}
sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevGetOrCreateResult>
lcameraDev_getOrCreateDeviceCReq(const std::string& deviceSelector)
{
lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState();
if (!state.isInitialized)
{
throw std::runtime_error(
"lcameraDev_getOrCreateDeviceCReq: call lcameraDev_main first");
}
co_return co_await lcamera_dev::getOrCreateDeviceSessionCReq(deviceSelector);
}
sscl::co::ViralNonPostingInvoker<lcamera_dev::CameraIdentityRecord>
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<void>
lcameraDev_releaseDeviceCReq(
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState();
if (!state.isInitialized)
{
throw std::runtime_error(
"lcameraDev_releaseDeviceCReq: call lcameraDev_main first");
}
co_await lcamera_dev::releaseDeviceSessionCReq(deviceSession);
co_return;
}
sscl::co::ViralNonPostingInvoker<std::vector<lcamera_dev::LcameraDevCameraInfo>>
lcameraDev_enumerateCamerasCReq(void)
{
lcamera_dev::LcameraDevState& state = lcamera_dev::getLcameraDevState();
if (!state.isInitialized)
{
throw std::runtime_error(
"lcameraDev_enumerateCamerasCReq: call lcameraDev_main first");
}
co_return co_await lcamera_dev::enumerateCamerasCReq();
}
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"
+70
View File
@@ -0,0 +1,70 @@
#ifndef LCAMERA_DEV_H
#define LCAMERA_DEV_H
#include <cameraIdentity.h>
#include <cameraModeRequest.h>
#include <memory>
#include <spinscale/co/invokers.h>
#include <spinscale/componentThread.h>
#include <string>
#include <vector>
namespace lcamera_dev {
class CameraSession;
struct LcameraDevGetOrCreateResult
{
std::shared_ptr<CameraSession> deviceSession;
CameraIdentityRecord resolvedIdentity;
};
struct LcameraDevCameraInfo
{
std::string id;
std::string model;
std::string location;
};
} // namespace lcamera_dev
#ifdef __cplusplus
extern "C" {
#endif
typedef void lcameraDev_mainFn(
const std::shared_ptr<sscl::ComponentThread>& componentThread);
typedef void lcameraDev_exitFn(void);
typedef sscl::co::ViralNonPostingInvoker<lcamera_dev::LcameraDevGetOrCreateResult>
lcameraDev_getOrCreateDeviceCReqFn(const std::string& deviceSelector);
typedef sscl::co::ViralNonPostingInvoker<lcamera_dev::CameraIdentityRecord>
lcameraDev_resolveDeviceSelectorCReqFn(const std::string& deviceSelector);
typedef sscl::co::ViralNonPostingInvoker<void>
lcameraDev_releaseDeviceCReqFn(
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession);
typedef sscl::co::ViralNonPostingInvoker<std::vector<lcamera_dev::LcameraDevCameraInfo>>
lcameraDev_enumerateCamerasCReqFn(void);
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_resolveDeviceSelectorCReqFn lcameraDev_resolveDeviceSelectorCReq;
lcameraDev_releaseDeviceCReqFn lcameraDev_releaseDeviceCReq;
lcameraDev_enumerateCamerasCReqFn lcameraDev_enumerateCamerasCReq;
lcameraDev_configureSessionModeCReqFn lcameraDev_configureSessionModeCReq;
#ifdef __cplusplus
}
#endif
#endif // LCAMERA_DEV_H
@@ -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
+121
View File
@@ -0,0 +1,121 @@
#include <selectorParse.h>
#include <stdexcept>
#include <cctype>
namespace lcamera_dev {
namespace {
SelectorCriterionKind parseCriterionKind(const std::string& prefixToken)
{
if (prefixToken == "lcamera-id") {
return SelectorCriterionKind::LibcameraId;
}
if (prefixToken == "index") {
return SelectorCriterionKind::Index;
}
if (prefixToken == "model-substr") {
return SelectorCriterionKind::ModelSubstr;
}
if (prefixToken == "model") {
return SelectorCriterionKind::Model;
}
if (prefixToken == "location") {
return SelectorCriterionKind::Location;
}
throw std::runtime_error(
"Unknown deviceSelector prefix: " + prefixToken);
}
SelectorCriterion parseCriterionClause(const std::string& clause)
{
const std::string trimmedClause = trimWhitespace(clause);
if (trimmedClause.empty()) {
throw std::runtime_error("Empty deviceSelector clause");
}
const size_t colonPos = trimmedClause.find(':');
if (colonPos == std::string::npos)
{
return SelectorCriterion{
SelectorCriterionKind::LibcameraId,
trimmedClause
};
}
const std::string prefixToken = trimWhitespace(
trimmedClause.substr(0, colonPos));
const std::string value = trimWhitespace(
trimmedClause.substr(colonPos + 1));
if (value.empty())
{
throw std::runtime_error(
"deviceSelector clause has empty value for prefix "
+ prefixToken);
}
return SelectorCriterion{
parseCriterionKind(prefixToken),
value
};
}
} // namespace
std::string trimWhitespace(const std::string& text)
{
size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start]))) {
++start;
}
size_t end = text.size();
while (end > start
&& std::isspace(static_cast<unsigned char>(text[end - 1])))
{
--end;
}
return text.substr(start, end - start);
}
std::vector<SelectorCriterion> parseDeviceSelector(
const std::string& deviceSelector)
{
const std::string trimmedSelector = trimWhitespace(deviceSelector);
if (trimmedSelector.empty()) {
throw std::runtime_error("deviceSelector is empty");
}
std::vector<SelectorCriterion> criteria;
std::string currentClause;
currentClause.reserve(trimmedSelector.size());
for (size_t i = 0; i < trimmedSelector.size(); ++i)
{
const char ch = trimmedSelector[i];
if (ch == '\\' && i + 1 < trimmedSelector.size()
&& trimmedSelector[i + 1] == ';')
{
currentClause.push_back(';');
++i;
continue;
}
if (ch == ';')
{
criteria.push_back(parseCriterionClause(currentClause));
currentClause.clear();
continue;
}
currentClause.push_back(ch);
}
criteria.push_back(parseCriterionClause(currentClause));
return criteria;
}
} // namespace lcamera_dev
+31
View File
@@ -0,0 +1,31 @@
#ifndef LCAMERA_DEV_SELECTOR_PARSE_H
#define LCAMERA_DEV_SELECTOR_PARSE_H
#include <string>
#include <vector>
namespace lcamera_dev {
enum class SelectorCriterionKind
{
LibcameraId,
Index,
Model,
ModelSubstr,
Location,
};
struct SelectorCriterion
{
SelectorCriterionKind kind = SelectorCriterionKind::LibcameraId;
std::string value;
};
std::vector<SelectorCriterion> parseDeviceSelector(
const std::string& deviceSelector);
std::string trimWhitespace(const std::string& text);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_SELECTOR_PARSE_H
+185
View File
@@ -0,0 +1,185 @@
#include <selectorResolve.h>
#include <algorithm>
#include <cctype>
#include <optional>
#include <sstream>
#include <stdexcept>
namespace lcamera_dev {
namespace {
std::string toLowerAscii(const std::string& text)
{
std::string lowered = text;
for (char& ch : lowered) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}
return lowered;
}
bool recordMatchesCriterion(
const CameraIdentityRecord& record, const SelectorCriterion& criterion)
{
switch (criterion.kind)
{
case SelectorCriterionKind::LibcameraId:
return record.id == criterion.value;
case SelectorCriterionKind::Index:
return false;
case SelectorCriterionKind::Model:
return record.model == criterion.value;
case SelectorCriterionKind::ModelSubstr:
return record.model.find(criterion.value) != std::string::npos;
case SelectorCriterionKind::Location:
return toLowerAscii(record.locationLabel)
== toLowerAscii(criterion.value);
}
return false;
}
int parseIndexCriterion(const SelectorCriterion& criterion)
{
try {
return std::stoi(criterion.value);
}
catch (const std::exception&) {
throw std::runtime_error("Invalid index: value in deviceSelector");
}
}
} // namespace
std::string formatCameraListForDiagnostics(
const std::vector<CameraIdentityRecord>& records)
{
std::ostringstream result;
result << "Known cameras:\n";
for (size_t i = 0; i < records.size(); ++i)
{
const CameraIdentityRecord& record = records[i];
result << " [" << i << "] id=" << record.id;
if (!record.model.empty()) {
result << " model=" << record.model;
}
if (!record.locationLabel.empty()) {
result << " location=" << record.locationLabel;
}
result << '\n';
}
return result.str();
}
CameraIdentityRecord resolveSelectorAgainstRecords(
const std::vector<SelectorCriterion>& criteria,
const std::vector<CameraIdentityRecord>& records)
{
std::optional<int> indexCriterion;
for (const SelectorCriterion& criterion : criteria)
{
if (criterion.kind != SelectorCriterionKind::Index) {
continue;
}
const int index = parseIndexCriterion(criterion);
if (index < 0
|| static_cast<size_t>(index) >= records.size())
{
throw std::runtime_error("index: selector out of range");
}
indexCriterion = index;
}
std::vector<const CameraIdentityRecord*> matches;
matches.reserve(records.size());
for (const CameraIdentityRecord& record : records)
{
bool matchesAll = true;
for (const SelectorCriterion& criterion : criteria)
{
if (criterion.kind == SelectorCriterionKind::Index) {
continue;
}
if (!recordMatchesCriterion(record, criterion))
{
matchesAll = false;
break;
}
}
if (!matchesAll) {
continue;
}
matches.push_back(&record);
}
if (indexCriterion.has_value())
{
const CameraIdentityRecord& indexedRecord =
records.at(static_cast<size_t>(*indexCriterion));
bool hasNonIndexCriteria = false;
for (const SelectorCriterion& criterion : criteria)
{
if (criterion.kind != SelectorCriterionKind::Index)
{
hasNonIndexCriteria = true;
break;
}
}
if (!hasNonIndexCriteria) { return indexedRecord; }
auto it = std::find_if(
matches.begin(), matches.end(),
[&indexedRecord](const CameraIdentityRecord* candidate) {
return candidate->id == indexedRecord.id;
});
if (it == matches.end())
{
throw std::runtime_error(
"index: criterion conflicts with other selector clauses");
}
return indexedRecord;
}
if (matches.empty())
{
throw std::runtime_error(
"No camera matches deviceSelector\n"
+ formatCameraListForDiagnostics(records));
}
if (matches.size() > 1)
{
throw std::runtime_error(
"Ambiguous deviceSelector: multiple cameras match\n"
+ formatCameraListForDiagnostics(records));
}
return *matches.front();
}
CameraIdentityRecord resolveDeviceSelectorAgainstRecords(
const std::string& deviceSelector,
const std::vector<CameraIdentityRecord>& records)
{
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector(deviceSelector);
return resolveSelectorAgainstRecords(criteria, records);
}
} // namespace lcamera_dev
+24
View File
@@ -0,0 +1,24 @@
#ifndef LCAMERA_DEV_SELECTOR_RESOLVE_H
#define LCAMERA_DEV_SELECTOR_RESOLVE_H
#include <cameraIdentity.h>
#include <selectorParse.h>
#include <string>
#include <vector>
namespace lcamera_dev {
CameraIdentityRecord resolveSelectorAgainstRecords(
const std::vector<SelectorCriterion>& criteria,
const std::vector<CameraIdentityRecord>& records);
CameraIdentityRecord resolveDeviceSelectorAgainstRecords(
const std::string& deviceSelector,
const std::vector<CameraIdentityRecord>& records);
std::string formatCameraListForDiagnostics(
const std::vector<CameraIdentityRecord>& records);
} // namespace lcamera_dev
#endif // LCAMERA_DEV_SELECTOR_RESOLVE_H
@@ -0,0 +1,145 @@
#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,
request.fullPlanarIsOptional);
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
@@ -0,0 +1,90 @@
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
${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
${LIBCAMERA_INCLUDE_DIRS}
)
target_link_libraries(lcameraDev_unit_tests
gtest_main
lcameraDev
spinscale
spinscale_test_support
${Boost_LIBRARIES}
${LIBCAMERA_LIBRARIES}
)
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)
add_executable(lcameraDev_hil_tests
lcameraDev_hil_tests.cpp
)
target_include_directories(lcameraDev_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_hil_tests
gtest_main
lcameraDev
spinscale
spinscale_test_support
${Boost_LIBRARIES}
)
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,24 @@
#include <gtest/gtest.h>
#include <cameraIdentity.h>
#include <libcamera/property_ids.h>
namespace lcamera_dev {
namespace {
TEST(LocationPropertyToLabelTest, MapsKnownLocations)
{
using namespace libcamera::properties;
EXPECT_EQ(locationPropertyToLabel(CameraLocationFront), "front");
EXPECT_EQ(locationPropertyToLabel(CameraLocationBack), "back");
EXPECT_EQ(locationPropertyToLabel(CameraLocationExternal), "external");
}
TEST(LocationPropertyToLabelTest, UnknownLocationReturnsEmptyString)
{
EXPECT_EQ(locationPropertyToLabel(-1), "");
EXPECT_EQ(locationPropertyToLabel(99), "");
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,86 @@
#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, CameraModeRequestsEqualComparesOptionalPlanarFlag)
{
LcameraDevCameraModeRequest left;
left.width = 640;
left.height = 480;
left.colourSpace = LcameraDevColourSpace::Yuv;
left.fullPlanarIsOptional = false;
LcameraDevCameraModeRequest right = left;
EXPECT_TRUE(cameraModeRequestsEqual(left, right));
right.fullPlanarIsOptional = true;
EXPECT_FALSE(cameraModeRequestsEqual(left, right));
}
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,45 @@
#ifndef LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
#define LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
#include <bakedCameraProfiles.h>
#include <cameraIdentity.h>
#include <string>
#include <vector>
namespace lcamera_dev {
namespace tests {
inline CameraIdentityRecord profileToIdentityRecord(
const test_fixtures::BakedCameraProfile& profile)
{
CameraIdentityRecord record;
record.id = profile.libcameraId;
record.model = profile.model;
record.locationLabel = profile.location;
return record;
}
inline std::vector<CameraIdentityRecord> bakedProfilesAsRecords(
const char *machineTag)
{
std::vector<CameraIdentityRecord> records;
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
{
const test_fixtures::BakedCameraProfile& profile =
test_fixtures::bakedCameraProfiles[i];
if (std::string(profile.machineTag) != machineTag) {
continue;
}
records.push_back(profileToIdentityRecord(profile));
}
return records;
}
} // namespace tests
} // namespace lcamera_dev
#endif // LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
@@ -0,0 +1,635 @@
#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";
}
}
void configureProfileWithOptPlanarExpectingYuyv(
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;
LcameraDevCameraModeRequest optPlanarRequest = request;
optPlanarRequest.fullPlanarIsOptional = true;
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &optPlanarRequest, &configuredMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
optPlanarRequest,
configuredMode);
});
EXPECT_FALSE(configuredMode.isFullyPlanar) << profile->profileTag;
EXPECT_EQ(configuredMode.planeCount, 1u) << profile->profileTag;
EXPECT_EQ(configuredMode.pixelFormatName, "YUYV") << profile->profileTag;
EXPECT_GE(configuredMode.width, 1u) << profile->profileTag;
EXPECT_GE(configuredMode.height, 1u) << profile->profileTag;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureUsbHdmiYuvWithOptPlanarSelectsYuyv)
{
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)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuvWithOptPlanarSelectsYuyv)
{
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)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,260 @@
#include <boostAsioLinkageFix.h>
#include <catalogHelpers.h>
#include <lcameraDev.h>
#include <gtest/gtest.h>
#include <support/bakedDeviceCatalog.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 *defaultMachineTag = "dell-laptop";
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;
}
sscl::co::NonViralNonPostingInvoker enumerateCamerasCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
std::vector<lcamera_dev::LcameraDevCameraInfo>& enumeratedCameras)
{
(void)exceptionStorage;
(void)callerLambda;
enumeratedCameras = co_await lcameraDev_enumerateCamerasCReq();
co_return;
}
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 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;
}
sscl::co::NonViralNonPostingInvoker resolveSelectorCInd(
std::exception_ptr &, std::function<void()>,
const char *deviceSelector,
lcamera_dev::CameraIdentityRecord& resolvedIdentity)
{
resolvedIdentity =
co_await lcameraDev_resolveDeviceSelectorCReq(deviceSelector);
co_return;
}
void runLcameraDevMainAndNurseryTask(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-hil");
harness.runSync(
[&work](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcameraDev_main(componentThread);
work(componentThread);
lcameraDev_exit();
});
}
class LcameraDevHilTest : 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());
if (requiredProfiles.empty()) {
GTEST_SKIP() << "No baked profiles for machine tag "
<< machineTag;
}
}
std::string machineTag;
std::vector<const test_fixtures::BakedCameraProfile *> requiredProfiles;
};
TEST_F(LcameraDevHilTest, EnumerateMatchesBakedCatalog)
{
std::vector<lcamera_dev::LcameraDevCameraInfo> enumeratedCameras;
runLcameraDevMainAndNurseryTask(
[&enumeratedCameras](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&enumeratedCameras](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return enumerateCamerasCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
enumeratedCameras);
});
});
EXPECT_GE(enumeratedCameras.size(), requiredProfiles.size());
for (const test_fixtures::BakedCameraProfile *profile : requiredProfiles)
{
bool found = false;
for (const lcamera_dev::LcameraDevCameraInfo& camera : enumeratedCameras)
{
if (camera.id == profile->libcameraId) {
found = true;
break;
}
}
EXPECT_TRUE(found)
<< "Missing baked profile camera id=" << profile->libcameraId;
}
}
TEST_F(LcameraDevHilTest, GetOrCreateByBakedSelector)
{
runLcameraDevMainAndNurseryTask(
[this](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
for (const test_fixtures::BakedCameraProfile *profile :
requiredProfiles)
{
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;
EXPECT_EQ(
createResult.resolvedIdentity.id,
profile->libcameraId)
<< profile->profileTag;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
});
}
TEST_F(LcameraDevHilTest, ResolveDeviceSelectorMatchesGetOrCreateIdentity)
{
runLcameraDevMainAndNurseryTask(
[this](const std::shared_ptr<sscl::ComponentThread>& 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
@@ -0,0 +1,85 @@
#include <planarYuvFormatPolicy.h>
#include <gtest/gtest.h>
#include <libcamera/formats.h>
#include <support/exceptionAssertions.h>
#include <vector>
namespace lcamera_dev {
namespace {
using libcamera::formats::MJPEG;
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));
}
TEST(PlanarYuvFormatPolicyTest, FullyPlanarOptionalPicksYuyvOverMjpeg)
{
const std::vector<libcamera::PixelFormat> candidates = {MJPEG, YUYV};
const std::optional<libcamera::PixelFormat> selected =
selectYuvCaptureFormat(candidates, true);
EXPECT_TRUE(selected.has_value());
EXPECT_EQ(*selected, YUYV);
EXPECT_FALSE(isFullyPlanarYuv(*selected));
EXPECT_EQ(yuvCapturePlaneCount(*selected), 1u);
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,93 @@
#include <gtest/gtest.h>
#include <selectorParse.h>
#include <stdexcept>
namespace lcamera_dev {
namespace {
TEST(TrimWhitespaceTest, StripsLeadingAndTrailingWhitespace)
{
EXPECT_EQ(trimWhitespace(" foo bar "), "foo bar");
EXPECT_EQ(trimWhitespace("\t\nvalue\r\n"), "value");
EXPECT_EQ(trimWhitespace("no-trim"), "no-trim");
EXPECT_EQ(trimWhitespace(""), "");
}
TEST(ParseDeviceSelectorTest, BareOpaqueIdIsLibcameraId)
{
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("/base/soc/i2c@1/imx219@10");
ASSERT_EQ(criteria.size(), 1u);
EXPECT_EQ(criteria[0].kind, SelectorCriterionKind::LibcameraId);
EXPECT_EQ(criteria[0].value, "/base/soc/i2c@1/imx219@10");
}
TEST(ParseDeviceSelectorTest, ExplicitLcameraIdPrefix)
{
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("lcamera-id:foo bar baz");
ASSERT_EQ(criteria.size(), 1u);
EXPECT_EQ(criteria[0].kind, SelectorCriterionKind::LibcameraId);
EXPECT_EQ(criteria[0].value, "foo bar baz");
}
TEST(ParseDeviceSelectorTest, ParsesTypedPrefixes)
{
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector(
"index:0;model:imx219;model-substr:Logi;location:external");
ASSERT_EQ(criteria.size(), 4u);
EXPECT_EQ(criteria[0].kind, SelectorCriterionKind::Index);
EXPECT_EQ(criteria[0].value, "0");
EXPECT_EQ(criteria[1].kind, SelectorCriterionKind::Model);
EXPECT_EQ(criteria[1].value, "imx219");
EXPECT_EQ(criteria[2].kind, SelectorCriterionKind::ModelSubstr);
EXPECT_EQ(criteria[2].value, "Logi");
EXPECT_EQ(criteria[3].kind, SelectorCriterionKind::Location);
EXPECT_EQ(criteria[3].value, "external");
}
TEST(ParseDeviceSelectorTest, EscapedSemicolonInValue)
{
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("lcamera-id:foo\\;bar;model:aaaa");
ASSERT_EQ(criteria.size(), 2u);
EXPECT_EQ(criteria[0].kind, SelectorCriterionKind::LibcameraId);
EXPECT_EQ(criteria[0].value, "foo;bar");
EXPECT_EQ(criteria[1].kind, SelectorCriterionKind::Model);
EXPECT_EQ(criteria[1].value, "aaaa");
}
TEST(ParseDeviceSelectorTest, EmptySelectorThrows)
{
EXPECT_THROW(parseDeviceSelector(""), std::runtime_error);
EXPECT_THROW(parseDeviceSelector(" "), std::runtime_error);
}
TEST(ParseDeviceSelectorTest, UnknownPrefixThrows)
{
EXPECT_THROW(
parseDeviceSelector("serial:abc"),
std::runtime_error);
}
TEST(ParseDeviceSelectorTest, EmptyClauseThrows)
{
EXPECT_THROW(
parseDeviceSelector("model:imx219;;location:front"),
std::runtime_error);
}
TEST(ParseDeviceSelectorTest, EmptyValueThrows)
{
EXPECT_THROW(
parseDeviceSelector("model:"),
std::runtime_error);
}
} // namespace
} // namespace lcamera_dev
@@ -0,0 +1,226 @@
#include <gtest/gtest.h>
#include <catalogHelpers.h>
#include <selectorParse.h>
#include <selectorResolve.h>
#include <stdexcept>
#include <string>
#include <vector>
namespace lcamera_dev {
namespace {
constexpr const char *dellLaptopMachineTag = "dell-laptop";
static CameraIdentityRecord makeRecord(
const std::string& id,
const std::string& model = "",
const std::string& locationLabel = "")
{
CameraIdentityRecord record;
record.id = id;
record.model = model;
record.locationLabel = locationLabel;
return record;
}
static std::vector<CameraIdentityRecord> syntheticAmbiguityRecords()
{
return {
makeRecord("/base/cam0", "imx219", "back"),
makeRecord("/base/cam1", "Logitech C920", "external"),
makeRecord("/base/cam2", "imx219", "front"),
};
}
TEST(FormatCameraListForDiagnosticsTest, ListsIndexedCameras)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::string text = formatCameraListForDiagnostics(records);
EXPECT_NE(text.find("Known cameras:"), std::string::npos);
EXPECT_NE(text.find("HDMI USB Camera"), std::string::npos);
EXPECT_NE(text.find("location=external"), std::string::npos);
EXPECT_NE(text.find("Integrated_Webcam_HD"), std::string::npos);
EXPECT_NE(text.find("location=front"), std::string::npos);
}
TEST(ResolveDeviceSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector)
{
const std::vector<CameraIdentityRecord> 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<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:HDMI;location:EXTERNAL");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
EXPECT_EQ(resolved.locationLabel, "external");
}
TEST(ResolveSelectorAgainstRecordsTest, BakedIntegratedWebcamMatchesExampleSelector)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:Integrated;location:front");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS04-4:1.0-1bcf:2b8a");
EXPECT_EQ(resolved.locationLabel, "front");
}
TEST(ResolveSelectorAgainstRecordsTest, BakedUsbCameraMatchesLibcameraId)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector(
"lcamera-id:\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
}
TEST(ResolveSelectorAgainstRecordsTest, MatchesLibcameraId)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("lcamera-id:/base/cam1");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(resolved.id, "/base/cam1");
}
TEST(ResolveSelectorAgainstRecordsTest, MatchesModelSubstrAndLocation)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:Logitech;location:EXTERNAL");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(resolved.id, "/base/cam1");
}
TEST(ResolveSelectorAgainstRecordsTest, IndexSelectsNthCamera)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:2");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(resolved.id, "/base/cam2");
}
TEST(ResolveSelectorAgainstRecordsTest, IndexCombinedWithModel)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:0;model:imx219");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(resolved.id, "/base/cam0");
}
TEST(ResolveSelectorAgainstRecordsTest, ZeroMatchesThrowsWithDiagnostics)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model:does-not-exist");
try {
resolveSelectorAgainstRecords(criteria, records);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exc)
{
EXPECT_NE(
std::string(exc.what()).find("No camera matches deviceSelector"),
std::string::npos);
EXPECT_NE(
std::string(exc.what()).find("Known cameras:"),
std::string::npos);
}
}
TEST(ResolveSelectorAgainstRecordsTest, AmbiguousSelectorThrows)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model:imx219");
EXPECT_THROW(
resolveSelectorAgainstRecords(criteria, records),
std::runtime_error);
}
TEST(ResolveSelectorAgainstRecordsTest, IndexOutOfRangeThrows)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:9");
EXPECT_THROW(
resolveSelectorAgainstRecords(criteria, records),
std::runtime_error);
}
TEST(ResolveSelectorAgainstRecordsTest, IndexConflictsWithOtherClauses)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:0;location:front");
EXPECT_THROW(
resolveSelectorAgainstRecords(criteria, records),
std::runtime_error);
}
TEST(ResolveSelectorAgainstRecordsTest, InvalidIndexValueThrows)
{
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:not-a-number");
EXPECT_THROW(
resolveSelectorAgainstRecords(criteria, records),
std::runtime_error);
}
} // 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;
}
}
@@ -0,0 +1,71 @@
#include <boostAsioLinkageFix.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
namespace {
sscl::co::NonViralNonPostingInvoker listCamerasCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda)
{
(void)exceptionStorage;
(void)callerLambda;
const std::vector<lcamera_dev::LcameraDevCameraInfo> cameras =
co_await lcameraDev_enumerateCamerasCReq();
std::cout << "lcameraDev: found " << cameras.size() << " camera(s)\n";
for (size_t i = 0; i < cameras.size(); ++i)
{
const lcamera_dev::LcameraDevCameraInfo& info = cameras[i];
std::cout << " [" << i << "] id=" << info.id;
if (!info.model.empty()) {
std::cout << " model=" << info.model;
}
if (!info.location.empty()) {
std::cout << " location=" << info.location;
}
std::cout << '\n';
}
co_return;
}
void runListCameras(
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcameraDev_main(componentThread);
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
[](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return listCamerasCInd(
lease.getExceptionStorage(),
lease.getCallerLambda());
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
lcameraDev_exit();
}
} // namespace
int main()
{
try {
lcamera_dev_probe::runOnComponentThread(runListCameras);
return 0;
}
catch (const std::exception& exc) {
std::cerr << "lcameraDev_list_cameras: " << exc.what() << '\n';
return 1;
}
}
@@ -0,0 +1,78 @@
#include <boostAsioLinkageFix.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
#include <string>
namespace {
sscl::co::NonViralNonPostingInvoker probeSelectorCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::string& deviceSelector)
{
(void)exceptionStorage;
(void)callerLambda;
const lcamera_dev::LcameraDevGetOrCreateResult createResult =
co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);
std::cout << "lcameraDev_probe: opened session for camera id="
<< createResult.resolvedIdentity.id << '\n';
co_await lcameraDev_releaseDeviceCReq(createResult.deviceSession);
std::cout << "lcameraDev_probe: released session\n";
co_return;
}
void runProbe(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::string& deviceSelector)
{
lcameraDev_main(componentThread);
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
[deviceSelector](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return probeSelectorCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
deviceSelector);
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
lcameraDev_exit();
}
} // namespace
int main(int argc, char* argv[])
{
if (argc < 2)
{
std::cerr << "Usage: lcameraDev_probe <deviceSelector>\n";
return 1;
}
try {
const std::string deviceSelector = argv[1];
lcamera_dev_probe::runOnComponentThread(
[&](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
runProbe(componentThread, deviceSelector);
});
return 0;
}
catch (const std::exception& exc) {
std::cerr << "lcameraDev_probe: " << exc.what() << '\n';
return 1;
}
}
@@ -0,0 +1,15 @@
#include <probeRunner.h>
#include <support/probeComponentThread.h>
namespace lcamera_dev_probe {
void runOnComponentThread(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-probe");
harness.runSync(work);
}
} // namespace lcamera_dev_probe
+16
View File
@@ -0,0 +1,16 @@
#ifndef LCAMERA_DEV_PROBE_RUNNER_H
#define LCAMERA_DEV_PROBE_RUNNER_H
#include <functional>
#include <memory>
#include <spinscale/componentThread.h>
namespace lcamera_dev_probe {
void runOnComponentThread(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work);
} // namespace lcamera_dev_probe
#endif // LCAMERA_DEV_PROBE_RUNNER_H
+16 -3
View File
@@ -10,12 +10,23 @@ if(ENABLE_LIB_livoxProto1)
udpCommandDemuxer.cpp
)
set_target_properties(livoxProto1 PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
# Set config define for header generation
add_compile_definitions(CONFIG_LIB_LIVOXPROTO1_ENABLED)
target_include_directories(livoxProto1 PUBLIC ${Boost_INCLUDE_DIRS})
target_include_directories(livoxProto1 PUBLIC
${Boost_INCLUDE_DIRS}
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/smocore/include
${CMAKE_SOURCE_DIR}/commonLibs
)
target_link_libraries(livoxProto1 PUBLIC
Boost::system Boost::log
attachmentSupport)
spinscale
)
# Verify Boost dynamic dependencies after build
add_custom_command(TARGET livoxProto1 POST_BUILD
@@ -25,5 +36,7 @@ if(ENABLE_LIB_livoxProto1)
)
# Install rules
install(TARGETS livoxProto1 DESTINATION lib)
install(TARGETS livoxProto1
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
endif()
+284 -107
View File
@@ -1,127 +1,309 @@
#include <boostAsioLinkageFix.h>
#include <array>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <functional>
#include <opts.h>
#include <componentThread.h>
#include <adapters/boostAsio/udpReceiveFromAReq.h>
#include <spinscale/co/nonViralCompletion.h>
#include "broadcastListener.h"
#include "core.h"
#include "protocol.h"
namespace livoxProto1 {
namespace comms {
namespace {
bool isShutdownReceiveError(const boost::system::error_code &ec)
{
return ec == boost::asio::error::operation_aborted
|| ec == boost::asio::error::bad_descriptor;
}
void logEventHandlerException(std::exception_ptr &exceptionPtr)
{
sscl::co::NonViralCompletion nvc(exceptionPtr);
if (!nvc.hasException()) {
return;
}
try {
nvc.checkAndRethrowException();
} catch (const std::exception &e) {
std::cerr << "BroadcastListener: event handler: "
<< e.what() << std::endl;
}
}
void logReceiveDaemonException(std::exception_ptr &exceptionPtr)
{
sscl::co::NonViralCompletion nvc(exceptionPtr);
if (!nvc.hasException()) {
return;
}
try {
nvc.checkAndRethrowException();
} catch (const std::exception &e) {
std::cerr << "BroadcastListener: receive daemon: "
<< e.what() << std::endl;
}
}
bool parseAndValidateBroadcastMessage(
const std::array<uint8_t, UDP_BCAST_MSG_BUFFER_NBYTES> &bytes,
std::size_t nbytes,
BroadcastMessage &msgOut)
{
if (nbytes < sizeof(BroadcastMessage))
{
std::cerr << "broadcastMsgIndCInd"
<< ": Received packet too small: " << nbytes
<< " bytes (expected at least "
<< sizeof(BroadcastMessage) << ")" << std::endl;
return false;
}
std::memcpy(&msgOut, bytes.data(), sizeof(BroadcastMessage));
// Following the clean receiving flow:
// 1. Swap CRC32 to host endianness first
msgOut.footer.swapCrc32ToHostEndianness();
// 2. Validate CRC32 (on whole message excluding footer CRC32 field)
if (!msgOut.validateCrc32())
{
std::cerr << "broadcastMsgIndCInd"
<< ": Broadcast message failed CRC32 validation"
<< std::endl;
return false;
}
// 3. Swap CRC16 to host endianness
msgOut.header.swapCrc16ToHostEndianness();
// 4. Validate CRC16 (on header only)
if (!msgOut.header.validateCrc16())
{
std::cerr << "broadcastMsgIndCInd"
<< ": Broadcast message failed CRC16 validation"
<< std::endl;
return false;
}
// 5. Swap content to host endianness
msgOut.swapContentsToHostEndianness();
// 6. Validate message sanity
if (!msgOut.sanityCheck())
{
std::cerr << "broadcastMsgIndCInd"
<< ": Broadcast message failed sanity check"
<< std::endl;
return false;
}
return true;
}
/** RAII: open eventHandlerNursery at CDaemon entry; drain at exit. */
class EventHandlerNurseryScopeGuard
{
public:
explicit EventHandlerNurseryScopeGuard(
sscl::co::NonViralTaskNursery &nursery)
: nursery(nursery)
{
nursery.openAdmission();
}
~EventHandlerNurseryScopeGuard()
{
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(
sscl::ComponentThread::getSelf()->getIoContext());
}
EventHandlerNurseryScopeGuard(
const EventHandlerNurseryScopeGuard &) = delete;
EventHandlerNurseryScopeGuard &operator=(
const EventHandlerNurseryScopeGuard &) = delete;
private:
sscl::co::NonViralTaskNursery &nursery;
};
} // namespace
BroadcastListener::BroadcastIndPayload::BroadcastIndPayload(
const uint8_t *receiveBytes, std::size_t receiveNbytes,
const boost::asio::ip::udp::endpoint &endpoint)
: nbytes(receiveNbytes),
senderEndpoint(endpoint)
{
if (receiveNbytes > 0) {
std::memcpy(bytes.data(), receiveBytes, receiveNbytes);
}
}
BroadcastListener::BroadcastIndPayload::BroadcastIndPayload(
BroadcastIndPayload &&other) noexcept
: nbytes(other.nbytes),
senderEndpoint(std::move(other.senderEndpoint))
{
if (nbytes > 0) {
std::memcpy(bytes.data(), other.bytes.data(), nbytes);
}
other.nbytes = 0;
}
BroadcastListener::BroadcastListener(
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
uint16_t listeningPort, uint16_t connectPort
)
: componentThread(componentThread),
listeningPort(listeningPort),
connectPort(connectPort),
deviceGoneAwayCb(nullptr),
socket(componentThread->getIoService()),
listeningEndpoint(boost::asio::ip::udp::v4(), listeningPort),
isListening(false)
socket(componentThread->getIoContext()),
listeningEndpoint(boost::asio::ip::udp::v4(), listeningPort)
{
}
std::shared_ptr<DiscoveredDevice>
BroadcastListener::getDevice(const std::string &deviceIdentifier) const
{
auto it = std::find_if(discoveredDevices.begin(), discoveredDevices.end(),
sscl::SpinLock::Guard lock(discoveredDevices.lock);
auto it = std::find_if(
discoveredDevices.rsrc.devices.begin(),
discoveredDevices.rsrc.devices.end(),
[&deviceIdentifier](const std::shared_ptr<DiscoveredDevice>& device) {
return comms::deviceIdentifiersEqual(
device->deviceIdentifier, deviceIdentifier);
}
);
return it != discoveredDevices.end() ? *it : nullptr;
return it != discoveredDevices.rsrc.devices.end() ? *it : nullptr;
}
void BroadcastListener::broadcastMsgInd(
const boost::system::error_code& ec, std::size_t bytes_received)
void BroadcastListener::registerDiscoveredDevice(
const BroadcastMessage &msg, const std::string &senderIp)
{
if (ec)
{
std::cerr << __func__ << ": Error receiving broadcast message: "
<< ec.message() << std::endl;
return;
}
if (bytes_received < sizeof(BroadcastMessage))
{
std::cerr << __func__
<< ": Received packet too small: " << bytes_received
<< " bytes (expected at least "
<< sizeof(BroadcastMessage) << ")" << std::endl;
return;
}
// Use placement new to construct BroadcastMessage in the buffer
BroadcastMessage* msg = new (bcastMsgRecvBuffer) BroadcastMessage;
// Following the clean receiving flow:
// 1. Swap CRC32 to host endianness first
msg->footer.swapCrc32ToHostEndianness();
// 2. Validate CRC32 (on whole message excluding footer CRC32 field)
if (!msg->validateCrc32())
{
std::cerr << __func__
<< ": Broadcast message failed CRC32 validation" << std::endl;
return;
}
// 3. Swap CRC16 to host endianness
msg->header.swapCrc16ToHostEndianness();
// 4. Validate CRC16 (on header only)
if (!msg->header.validateCrc16())
{
std::cerr << __func__
<< ": Broadcast message failed CRC16 validation" << std::endl;
return;
}
// 5. Swap content to host endianness
msg->swapContentsToHostEndianness();
// 6. Validate message sanity
if (!msg->sanityCheck())
{
std::cerr << __func__
<< ": Broadcast message failed sanity check" << std::endl;
return;
}
// Extract device information
std::string senderIP = senderEndpoint.address().to_string();
std::string broadcastCode(
reinterpret_cast<const char*>(msg->broadcast_code));
reinterpret_cast<const char*>(msg.broadcast_code));
sscl::SpinLock::Guard lock(discoveredDevices.lock);
// Early return if device already exists
smo::SpinLock::Guard lock(isListeningLock);
const auto existingIt = std::find_if(
discoveredDevices.rsrc.devices.begin(),
discoveredDevices.rsrc.devices.end(),
[&broadcastCode](const std::shared_ptr<DiscoveredDevice> &device) {
return comms::deviceIdentifiersEqual(
device->deviceIdentifier, broadcastCode);
});
if (deviceExists(broadcastCode))
if (existingIt != discoveredDevices.rsrc.devices.end())
{
// Device already exists, just log the update
if (getProtoState().smoCallbacks.OptionParser_getOptions().verbose)
{
std::cout << __func__
std::cout << "broadcastMsgIndCInd"
<< ": Received broadcast from known device: "
<< broadcastCode << " at " << senderIP << "\n";
<< broadcastCode << " at " << senderIp << "\n";
}
}
else
{
// Create new DiscoveredDevice using conversion constructor
auto device = std::make_shared<DiscoveredDevice>(*msg, senderIP);
discoveredDevices.push_back(device);
// Output device information using stringify
std::cout << __func__ << ": Discovered new Livox device: "
<< device->stringify() << "\n";
return;
}
startReceive();
// Create new DiscoveredDevice using conversion constructor
auto device = std::make_shared<DiscoveredDevice>(msg, senderIp);
discoveredDevices.rsrc.devices.push_back(device);
std::cout << "broadcastMsgIndCInd: Discovered new Livox device: "
<< device->stringify() << "\n";
}
sscl::co::NonViralNonPostingInvoker
BroadcastListener::broadcastMsgIndCInd(
[[maybe_unused]] std::exception_ptr &exceptionPtr,
[[maybe_unused]] std::function<void()> callback,
[[maybe_unused]] sscl::SyncCancelerForAsyncWork &canceler,
BroadcastIndPayload payload)
{
BroadcastMessage msg;
if (!parseAndValidateBroadcastMessage(
payload.bytes, payload.nbytes, msg)) {
co_return;
}
const std::string senderIp = payload.senderEndpoint.address().to_string();
registerDiscoveredDevice(msg, senderIp);
co_return;
}
sscl::co::DynamicNonViralPostingInvoker
BroadcastListener::broadcastReceiveCDaemon(
[[maybe_unused]] sscl::co::ExplicitPostTarget postTarget,
[[maybe_unused]] std::exception_ptr &exceptionPtr,
[[maybe_unused]] std::function<void()> callback,
sscl::SyncCancelerForAsyncWork &canceler)
{
EventHandlerNurseryScopeGuard eventHandlerScope(eventHandlerNursery);
boost::asio::io_context &ioContext =
sscl::ComponentThread::getSelf()->getIoContext();
std::array<uint8_t, UDP_BCAST_MSG_BUFFER_NBYTES> receiveBuffer{};
boost::asio::ip::udp::endpoint senderEndpoint;
while (!canceler.isCancellationRequested())
{
const adapters::boostAsio::UdpReceiveFromAReq::Result receiveResult =
co_await adapters::boostAsio::getUdpReceiveFromAReqAwaiter(
ioContext, socket,
boost::asio::buffer(receiveBuffer), senderEndpoint);
if (isShutdownReceiveError(receiveResult.ec)) { break; }
if (receiveResult.ec)
{
std::cerr << __func__
<< ": Error receiving broadcast message: "
<< receiveResult.ec.message() << std::endl;
continue;
}
if (receiveResult.nbytes == 0) { continue; }
if (!canceler.execUncancelableSegmentOrAbort([&]()
{
eventHandlerNursery.launch(
[this,
payload = BroadcastIndPayload(
receiveBuffer.data(),
receiveResult.nbytes,
senderEndpoint)](
sscl::co::NonViralTaskNursery::Slot::Lease &lease) mutable
{
return broadcastMsgIndCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
lease.getSyncCanceler(),
std::move(payload));
},
logEventHandlerException);
})) {
break;
}
}
co_return;
}
void BroadcastListener::start(void)
{
if (isListening) { return; }
if (daemonNursery.admissionIsOpen()) { return; }
try
{
@@ -133,62 +315,57 @@ void BroadcastListener::start(void)
* We should also set up a timer to check for devices that have gone
* away.
*/
{
smo::SpinLock::Guard lock(isListeningLock);
socket.open(boost::asio::ip::udp::v4());
socket.bind(listeningEndpoint);
socket.open(boost::asio::ip::udp::v4());
socket.bind(listeningEndpoint);
daemonNursery.openAdmission();
// Launch the receive daemon coroutine on componentThread
daemonNursery.launch(
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
{
return broadcastReceiveCDaemon(
sscl::co::ExplicitPostTarget{
componentThread->getIoContext()},
lease.getExceptionStorage(),
lease.getCallerLambda(),
lease.getSyncCanceler());
},
logReceiveDaemonException);
isListening = true;
}
// Start the first async receive operation
startReceive();
std::cout << __func__ << ": BroadcastListener started on port "
<< listeningPort << std::endl;
}
catch (const boost::system::system_error& e)
{
isListening = false;
std::cerr << __func__ << ": Failed to start BroadcastListener: "
<< e.what() << std::endl;
throw;
}
}
void BroadcastListener::startReceive(void)
{
if (!isListening) { return; }
socket.async_receive_from(
boost::asio::buffer(bcastMsgRecvBuffer, sizeof(bcastMsgRecvBuffer)),
senderEndpoint,
std::bind(
&BroadcastListener::broadcastMsgInd, this,
std::placeholders::_1, std::placeholders::_2)
);
}
void BroadcastListener::stop(void)
{
{
smo::SpinLock::Guard lock(isListeningLock);
if (!isListening) { return; }
if (!daemonNursery.admissionIsOpen()) { return; }
isListening = false;
}
daemonNursery.requestCancelOnAll();
try
{
socket.cancel();
socket.close();
std::cout << __func__ << ": BroadcastListener stopped" << std::endl;
}
catch (const boost::system::system_error& e)
{
std::cerr << __func__ << ": Error stopping BroadcastListener: " << e.what()
<< std::endl;
std::cerr << __func__ << ": Error stopping BroadcastListener: "
<< e.what() << std::endl;
throw;
}
daemonNursery.closeAdmission();
daemonNursery.syncAwaitAllSettlements(
sscl::ComponentThread::getSelf()->getIoContext());
std::cout << __func__ << ": BroadcastListener stopped" << std::endl;
}
} // namespace comms
+67 -14
View File
@@ -2,13 +2,19 @@
#define BROADCAST_LISTENER_H
#include <boostAsioLinkageFix.h>
#include <array>
#include <vector>
#include <string>
#include <memory>
#include <atomic>
#include <functional>
#include <boost/asio/ip/udp.hpp>
#include <user/senseApiDesc.h>
#include <spinLock.h>
#include <spinscale/spinLock.h>
#include <spinscale/sharedResourceGroup.h>
#include <spinscale/co/dynamicPostingInvoker.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <spinscale/syncCancelerForAsyncWork.h>
#include "device.h"
namespace livoxProto1 {
@@ -29,7 +35,7 @@ class BroadcastListener
{
public:
BroadcastListener(
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
uint16_t listeningPort=55000, uint16_t connectPort=65000);
~BroadcastListener() = default;
@@ -47,14 +53,53 @@ public:
void start(void);
void stop(void);
void broadcastMsgInd(
const boost::system::error_code& ec, std::size_t bytes_received);
private:
/** Per-datagram snapshot; bytes are copied in ctor/move so handlers remain
* safe if broadcastMsgIndCInd is ever posted asynchronously.
*/
struct BroadcastIndPayload
{
std::array<uint8_t, UDP_BCAST_MSG_BUFFER_NBYTES> bytes{};
std::size_t nbytes = 0;
boost::asio::ip::udp::endpoint senderEndpoint;
BroadcastIndPayload() = default;
BroadcastIndPayload(
const uint8_t *receiveBytes, std::size_t receiveNbytes,
const boost::asio::ip::udp::endpoint &endpoint);
BroadcastIndPayload(BroadcastIndPayload &&other) noexcept;
BroadcastIndPayload(const BroadcastIndPayload &) = delete;
BroadcastIndPayload &operator=(const BroadcastIndPayload &) = delete;
BroadcastIndPayload &operator=(BroadcastIndPayload &&) = delete;
};
/** EXPLANATION:
* broadcastReceiveCDaemon is a dynamic posting non-viral coroutine: start()
* passes ExplicitPostTarget{componentThread->getIoContext()} so the daemon
* body always runs on componentThread. Synchronous listener state is
* touched only inside canceler.execUncancelableSegmentOrAbort() so stop()
* cannot tear down *this while the daemon is in a critical section.
*/
sscl::co::DynamicNonViralPostingInvoker broadcastReceiveCDaemon(
sscl::co::ExplicitPostTarget postTarget,
std::exception_ptr &exceptionPtr,
std::function<void()> callback,
sscl::SyncCancelerForAsyncWork &canceler);
sscl::co::NonViralNonPostingInvoker broadcastMsgIndCInd(
std::exception_ptr &exceptionPtr,
std::function<void()> callback,
sscl::SyncCancelerForAsyncWork &canceler,
BroadcastIndPayload payload);
void registerDiscoveredDevice(
const BroadcastMessage &msg, const std::string &senderIp);
private:
void startReceive(void);
private:
std::shared_ptr<smo::ComponentThread> componentThread;
std::shared_ptr<sscl::ComponentThread> componentThread;
/** EXPLANATION:
* The Livox proto says that client devices will spam broadcast UDP
* dgrams to us on the listening port. We can then use the source IP from
@@ -64,14 +109,22 @@ private:
*/
uint16_t listeningPort, connectPort;
DeviceGoneAwayCbFn *deviceGoneAwayCb;
std::vector<std::shared_ptr<DiscoveredDevice>> discoveredDevices;
struct DiscoveredDevicesResources
{
std::vector<std::shared_ptr<DiscoveredDevice>> devices;
};
mutable sscl::SharedResourceGroup<
sscl::SpinLock, DiscoveredDevicesResources> discoveredDevices;
boost::asio::ip::udp::socket socket;
boost::asio::ip::udp::endpoint listeningEndpoint, senderEndpoint;
smo::SpinLock isListeningLock;
bool isListening;
boost::asio::ip::udp::endpoint listeningEndpoint;
uint8_t bcastMsgRecvBuffer[UDP_BCAST_MSG_BUFFER_NBYTES];
/** Hosts broadcastReceiveCDaemon; stop() cancels and syncAwaitAll's it. */
sscl::co::NonViralTaskNursery daemonNursery;
/** Hosts per-datagram broadcastMsgIndCInd; admission/drain RAII inside CDaemon. */
sscl::co::NonViralTaskNursery eventHandlerNursery;
};
} // namespace comms
+42 -127
View File
@@ -1,10 +1,9 @@
#include <algorithm>
#include <iostream>
#include <functional>
#include <optional>
#include <opts.h>
#include <asynchronousContinuation.h>
#include <callback.h>
#include <user/senseApiDesc.h>
#include "protocol.h"
#include "core.h"
@@ -72,68 +71,16 @@ std::optional<std::shared_ptr<Device>> DeviceManager::getDevice(
return std::nullopt;
}
// GetOrCreateDeviceReq nested class implementation
class DeviceManager::GetOrCreateDeviceReq
: public smo::NonPostedAsynchronousContinuation<
livoxProto1_getOrCreateDeviceReqCbFn>
{
public:
DeviceManager& deviceManager;
// The device we're trying to connect (holds all connection parameters)
std::shared_ptr<Device> pendingDevice;
public:
GetOrCreateDeviceReq(
DeviceManager& mgr,
std::shared_ptr<Device> device,
smo::Callback<livoxProto1_getOrCreateDeviceReqCbFn> cb)
: smo::NonPostedAsynchronousContinuation<
livoxProto1_getOrCreateDeviceReqCbFn>(std::move(cb)),
deviceManager(mgr), pendingDevice(device)
{}
// Public accessor for the original callback
void callOriginalCallback(bool success, std::shared_ptr<Device> device)
{ callOriginalCb(success, device); }
void callOriginalCallbackWithFailure()
{ callOriginalCallback(false, nullptr); }
void getOrCreateDeviceReq1(
std::shared_ptr<GetOrCreateDeviceReq> context, bool connectSuccess
)
{
if (!connectSuccess)
{
std::cerr << __func__ << ": Connection failed for device "
<< context->pendingDevice->discoveredDevice.deviceIdentifier
<< std::endl;
context->callOriginalCallbackWithFailure();
return;
}
// Connection successful, add device to collection
context->deviceManager.devices.push_back(context->pendingDevice);
if (getProtoState().smoCallbacks.OptionParser_getOptions().verbose)
{
std::cout << __func__ << ": Successfully connected and added device "
<< context->pendingDevice->discoveredDevice.deviceIdentifier
<< std::endl;
}
// Return success with the connected device
context->callOriginalCallback(true, context->pendingDevice);
}
};
void DeviceManager::getOrCreateDeviceReq(
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
DeviceManager::getOrCreateDeviceCReq(
const std::string &deviceIdentifier,
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
int commandTimeoutMs, int retryDelayMs,
const std::string& smoIp, uint8_t smoSubnetNbits,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort,
smo::Callback<livoxProto1_getOrCreateDeviceReqCbFn> callback)
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort)
{
LivoxProto1GetOrCreateDeviceResult result;
// Validate smoIp format using Boost.Asio IPv4 validation
if (!smoIp.empty() && !comms::isValidIPv4(smoIp))
{
@@ -155,9 +102,9 @@ void DeviceManager::getOrCreateDeviceReq(
auto existingDevice = getDevice(deviceIdentifier);
if (existingDevice)
{
// Device already exists and is connected, return it
callback.callbackFn(true, existingDevice.value());
return;
result.success = true;
result.device = existingDevice.value();
co_return result;
}
// Device doesn't exist, create a new one but don't add it to collection yet
@@ -167,85 +114,53 @@ void DeviceManager::getOrCreateDeviceReq(
smoIp, smoSubnetNbits,
dataPort, cmdPort, imuPort);
// Create the continuation request object to hold state and callbacks
auto request = std::make_shared<GetOrCreateDeviceReq>(
*this, newDevice, std::move(callback));
// Start the connection process - only add to collection on success
request->pendingDevice->connectReq(
{request, std::bind(
&DeviceManager::GetOrCreateDeviceReq::getOrCreateDeviceReq1,
request.get(), request, std::placeholders::_1)});
const bool connectSuccess = co_await newDevice->connectCReq();
if (!connectSuccess)
{
std::cerr << __func__ << ": Connection failed for device "
<< newDevice->discoveredDevice.deviceIdentifier
<< std::endl;
co_return result;
}
devices.push_back(newDevice);
if (getProtoState().smoCallbacks.OptionParser_getOptions().verbose)
{
std::cout << __func__ << ": Successfully connected and added device "
<< newDevice->discoveredDevice.deviceIdentifier
<< std::endl;
}
result.success = true;
result.device = newDevice;
co_return result;
}
class DeviceManager::DestroyDeviceReq
: public smo::NonPostedAsynchronousContinuation<
livoxProto1_destroyDeviceReqCbFn>
{
public:
DeviceManager& deviceManager;
std::shared_ptr<Device> pendingDevice;
public:
DestroyDeviceReq(
DeviceManager& mgr,
std::shared_ptr<Device> device,
smo::Callback<livoxProto1_destroyDeviceReqCbFn> cb)
: smo::NonPostedAsynchronousContinuation<
livoxProto1_destroyDeviceReqCbFn>(std::move(cb)),
deviceManager(mgr), pendingDevice(device)
{}
// Public accessor for the original callback
void callOriginalCallback(bool success)
{ callOriginalCb(success); }
void callOriginalCallbackWithFailure()
{ callOriginalCallback(false); }
void destroyDeviceReq1(
std::shared_ptr<DestroyDeviceReq> context, bool success
)
{
context->deviceManager.devices.erase(
std::remove(
context->deviceManager.devices.begin(),
context->deviceManager.devices.end(),
context->pendingDevice),
context->deviceManager.devices.end());
context->callOriginalCallback(success);
}
};
void DeviceManager::destroyDeviceReq(
std::shared_ptr<Device> dev,
smo::Callback<livoxProto1_destroyDeviceReqCbFn> callback
)
sscl::co::ViralNonPostingInvoker<bool> DeviceManager::destroyDeviceCReq(
std::shared_ptr<Device> dev)
{
/** EXPLANATION:
* Check to see if the device is in our collection. If so, call
* disconnectReq and then remove it.
* disconnectCReq and then remove it.
*/
std::shared_ptr<Device> device = getDevice(dev->discoveredDevice).
value_or(nullptr);
if (!device || device->nAttachedStimulusProducers > 0)
{
callback.callbackFn(false);
return;
if (!device || device->nAttachedStimulusProducers > 0) {
co_return false;
}
auto request = std::make_shared<DestroyDeviceReq>(
*this, device, std::move(callback));
const bool success = co_await device->disconnectCReq();
device->disconnectReq(
{request, std::bind(
&DeviceManager::DestroyDeviceReq::destroyDeviceReq1,
request.get(), request, std::placeholders::_1)});
devices.erase(
std::remove(devices.begin(), devices.end(), device),
devices.end());
co_return success;
}
void main(const std::shared_ptr<smo::ComponentThread> &componentThread,
void main(const std::shared_ptr<sscl::ComponentThread> &componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks)
{
if (protoState.isInitialized) {
+9 -14
View File
@@ -11,7 +11,7 @@
#include "broadcastListener.h"
#include "udpCommandDemuxer.h"
#include "livoxProto1.h"
#include <callback.h>
#include <spinscale/co/invokers.h>
namespace livoxProto1 {
@@ -23,17 +23,16 @@ public:
static void deviceGoneAwayInd(const comms::DiscoveredDevice &device);
void getOrCreateDeviceReq(
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
getOrCreateDeviceCReq(
const std::string &deviceIdentifier,
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
int commandTimeoutMs, int retryDelayMs,
const std::string& smoIp, uint8_t smoSubnetNbits,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort,
smo::Callback<livoxProto1_getOrCreateDeviceReqCbFn> callback);
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort);
void destroyDeviceReq(
std::shared_ptr<Device> device,
smo::Callback<livoxProto1_destroyDeviceReqCbFn> callback);
sscl::co::ViralNonPostingInvoker<bool> destroyDeviceCReq(
std::shared_ptr<Device> device);
std::optional<std::shared_ptr<Device>> getDevice(
const std::string &deviceIdentifier);
@@ -52,14 +51,10 @@ public:
std::vector<std::shared_ptr<Device>> devices;
comms::BroadcastListener broadcastListener;
comms::UdpCommandDemuxer udpCommandDemuxer;
// Nested continuation class for async device creation
class GetOrCreateDeviceReq;
class DestroyDeviceReq;
};
void main(
const std::shared_ptr<smo::ComponentThread> &componentThread,
const std::shared_ptr<sscl::ComponentThread> &componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks);
void exit(void);
@@ -67,7 +62,7 @@ void exit(void);
struct ProtoState
{
bool isInitialized = false;
std::shared_ptr<smo::ComponentThread> componentThread;
std::shared_ptr<sscl::ComponentThread> componentThread;
std::unique_ptr<DeviceManager> deviceManager;
smo::stim_buff::SmoCallbacks smoCallbacks;
};
File diff suppressed because it is too large Load Diff
+46 -49
View File
@@ -2,6 +2,7 @@
#define LIVOX_PROTO1_DEVICE_H
#include <boostAsioLinkageFix.h>
#include <string>
#include <cstdint>
#include <cstddef>
@@ -18,8 +19,10 @@
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#include "protocol.h"
#include <callback.h>
#include <spinLock.h>
#include <spinscale/co/dynamicPostingInvoker.h>
#include <spinscale/co/invokers.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <spinscale/syncCancelerForAsyncWork.h>
// Custom hash function for std::pair<uint8_t, uint8_t>
namespace std {
@@ -77,7 +80,7 @@ class Device
{
public:
Device(const std::string &deviceIdentifier,
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
int commandTimeoutMs, int retryDelayMs,
const std::string& smoIp, uint8_t smoSubnetNbits,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort);
@@ -87,8 +90,20 @@ private:
// Heartbeat mechanism
void startHeartbeat();
void stopHeartbeat();
void sendHeartbeat();
void onHeartbeatTimer(const boost::system::error_code& error);
void sendHeartbeatOnce();
/** EXPLANATION:
* deviceCDaemon is a dynamic posting non-viral coroutine: startHeartbeat()
* passes ExplicitPostTarget{componentThread->getIoContext()} so the daemon
* body always runs on componentThread. Extensible for future per-device
* background work beyond heartbeats.
*/
sscl::co::DynamicNonViralPostingInvoker deviceCDaemon(
sscl::co::ExplicitPostTarget postTarget,
std::exception_ptr &exceptionPtr,
std::function<void()> callback,
sscl::SyncCancelerForAsyncWork &canceler);
std::string generateClientDeviceIpFromSerialNumber(
const std::string& broadcastCode);
@@ -96,16 +111,6 @@ private:
std::optional<std::string> detectSmoIp(const std::string& deviceIP);
uint32_t getSubnetMaskFor(uint8_t nbits);
class ConnectReq;
class ConnectToKnownDeviceReq;
class ConnectByDeviceIdentifierReq;
class ExecuteHandshakeReq;
class DisconnectReq;
class EnablePcloudDataReq;
class DisablePcloudDataReq;
class SetReturnModeReq;
class GetReturnModeReq;
public:
enum class ReturnMode : uint8_t
{
@@ -146,54 +151,46 @@ public:
// Utility methods
std::optional<std::string> getSmoIp(const std::string& deviceIP);
// Callback function type definitions for async methods
typedef std::function<void(bool success)> connectReqCbFn;
typedef std::function<
void(bool success, const std::string& ipAddr)>
connectToKnownDeviceReqCbFn;
typedef std::function<
void(bool success, const std::string& ipAddr)>
connectByDeviceIdentifierReqCbFn;
typedef std::function<void(bool success)> executeHandshakeReqCbFn;
typedef std::function<void(bool success)> disconnectReqCbFn;
typedef std::function<void(bool success)> enablePcloudDataReqCbFn;
typedef std::function<void(bool success)> disablePcloudDataReqCbFn;
typedef std::function<void(bool success)> setReturnModeReqCbFn;
typedef std::function<void(bool success, uint8_t returnMode)>
getReturnModeReqCbFn;
struct ConnectIpResult
{
bool success = false;
std::string ipAddr;
};
// Async connection methods
void connectReq(smo::Callback<connectReqCbFn> callback);
void connectToKnownDeviceReq(
smo::Callback<connectToKnownDeviceReqCbFn> callback);
void connectByDeviceIdentifierReq(
smo::Callback<connectByDeviceIdentifierReqCbFn> callback);
void executeHandshakeReq(
const std::string& deviceIP,
smo::Callback<executeHandshakeReqCbFn> callback);
void disconnectReq(smo::Callback<disconnectReqCbFn> callback);
void enablePcloudDataReq(smo::Callback<enablePcloudDataReqCbFn> callback);
void disablePcloudDataReq(smo::Callback<disablePcloudDataReqCbFn> callback);
void setReturnModeReq(
uint8_t returnMode, smo::Callback<setReturnModeReqCbFn> callback);
void getReturnModeReq(smo::Callback<getReturnModeReqCbFn> callback);
sscl::co::ViralNonPostingInvoker<bool> connectCReq();
sscl::co::ViralNonPostingInvoker<ConnectIpResult> connectToKnownDeviceCReq();
sscl::co::ViralNonPostingInvoker<ConnectIpResult> connectByDeviceIdentifierCReq();
sscl::co::ViralNonPostingInvoker<bool> executeHandshakeCReq(
const std::string& deviceIP);
sscl::co::ViralNonPostingInvoker<bool> disconnectCReq();
sscl::co::ViralNonPostingInvoker<bool> enablePcloudDataCReq();
sscl::co::ViralNonPostingInvoker<bool> disablePcloudDataCReq();
struct GetReturnModeResult
{
bool success = false;
uint8_t returnMode = 0;
};
sscl::co::ViralNonPostingInvoker<bool> setReturnModeCReq(uint8_t returnMode);
sscl::co::ViralNonPostingInvoker<GetReturnModeResult> getReturnModeCReq();
public:
comms::DiscoveredDevice discoveredDevice;
std::atomic<size_t> nAttachedStimulusProducers;
// Configuration
std::shared_ptr<smo::ComponentThread> componentThread;
std::shared_ptr<sscl::ComponentThread> componentThread;
int commandTimeoutMs, retryDelayMs;
std::string smoIp;
std::string detectedSmoListeningIp;
uint8_t smoSubnetNbits;
uint16_t dataPort, cmdPort, imuPort;
// Heartbeat state
std::unique_ptr<boost::asio::deadline_timer> heartbeatTimer;
std::atomic<bool> heartbeatActive;
smo::SpinLock heartbeatActiveLock;
// Heartbeat state (timer lifetime tied to Device ctor/dtor)
boost::asio::deadline_timer heartbeatTimer;
sscl::co::NonViralTaskNursery daemonNursery;
// Point cloud data state
std::atomic<bool> pcloudDataActive;
+26 -33
View File
@@ -1,6 +1,4 @@
#include <boostAsioLinkageFix.h>
#include <stdexcept>
#include <callback.h>
#include <boost/asio/posix/stream_descriptor.hpp>
#include "livoxProto1.h"
#include "device.h"
@@ -10,14 +8,13 @@
extern "C" {
void livoxProto1_getOrCreateDeviceReq(
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
livoxProto1_getOrCreateDeviceCReq(
const std::string& deviceIdentifier,
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
int commandTimeoutMs, int retryDelayMs,
const std::string& smoIp, uint8_t smoSubnetNbits,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort,
smo::Callback<livoxProto1_getOrCreateDeviceReqCbFn> callback
)
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort)
{
// Get the global DeviceManager instance
auto& protoState = livoxProto1::getProtoState();
@@ -28,19 +25,16 @@ void livoxProto1_getOrCreateDeviceReq(
"livoxProto1_main first");
}
// Delegate to DeviceManager
protoState.deviceManager->getOrCreateDeviceReq(
// Delegate to the DeviceManager to create the device
co_return co_await protoState.deviceManager->getOrCreateDeviceCReq(
deviceIdentifier, componentThread,
commandTimeoutMs, retryDelayMs,
smoIp, smoSubnetNbits,
dataPort, cmdPort, imuPort,
callback);
dataPort, cmdPort, imuPort);
}
void livoxProto1_destroyDeviceReq(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_destroyDeviceReqCbFn> callback
)
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_destroyDeviceCReq(
std::shared_ptr<livoxProto1::Device> device)
{
auto& protoState = livoxProto1::getProtoState();
if (!protoState.deviceManager)
@@ -49,12 +43,11 @@ void livoxProto1_destroyDeviceReq(
+ ": DeviceManager not initialized");
}
protoState.deviceManager->destroyDeviceReq(
device, callback);
co_return co_await protoState.deviceManager->destroyDeviceCReq(device);
}
void livoxProto1_main(
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks)
{
livoxProto1::main(componentThread, smoCallbacks);
@@ -65,10 +58,8 @@ void livoxProto1_exit(void)
livoxProto1::exit();
}
void livoxProto1_device_enablePcloudDataReq(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_enablePcloudDataReqCbFn> callback
)
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_device_enablePcloudDataCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
@@ -76,13 +67,11 @@ void livoxProto1_device_enablePcloudDataReq(
+ ": Device pointer is null");
}
device->enablePcloudDataReq(callback);
co_return co_await device->enablePcloudDataCReq();
}
void livoxProto1_device_disablePcloudDataReq(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_disablePcloudDataReqCbFn> callback
)
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_device_disablePcloudDataCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
@@ -90,13 +79,12 @@ void livoxProto1_device_disablePcloudDataReq(
+ ": Device pointer is null");
}
device->disablePcloudDataReq(callback);
co_return co_await device->disablePcloudDataCReq();
}
void livoxProto1_device_getReturnModeReq(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_getReturnModeReqCbFn> callback
)
sscl::co::ViralNonPostingInvoker<LivoxProto1GetReturnModeResult>
livoxProto1_device_getReturnModeCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
@@ -104,7 +92,12 @@ void livoxProto1_device_getReturnModeReq(
+ ": Device pointer is null");
}
device->getReturnModeReq(callback);
livoxProto1::Device::GetReturnModeResult deviceResult =
co_await device->getReturnModeCReq();
LivoxProto1GetReturnModeResult result;
result.success = deviceResult.success;
result.returnMode = deviceResult.returnMode;
co_return result;
}
std::shared_ptr<boost::asio::posix::stream_descriptor>
+39 -37
View File
@@ -1,12 +1,11 @@
#ifndef LIVOXPROTO1_H
#define LIVOXPROTO1_H
#include <boostAsioLinkageFix.h>
#include <memory>
#include <string>
#include <cstdint>
#include <functional>
#include <callback.h>
#include <spinscale/co/invokers.h>
#include <boost/asio/posix/stream_descriptor.hpp>
// Forward declarations
@@ -14,6 +13,8 @@ namespace smo {
namespace stim_buff {
struct SmoCallbacks;
}
}
namespace sscl {
class ComponentThread;
}
@@ -21,6 +22,18 @@ namespace livoxProto1 {
class Device;
}
struct LivoxProto1GetOrCreateDeviceResult
{
bool success = false;
std::shared_ptr<livoxProto1::Device> device;
};
struct LivoxProto1GetReturnModeResult
{
bool success = false;
uint8_t returnMode = 0;
};
#ifdef __cplusplus
extern "C" {
#endif
@@ -31,7 +44,7 @@ extern "C" {
* @param smoCallbacks Callbacks provided by SMO
*/
typedef void livoxProto1_mainFn(
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks);
/**
@@ -50,54 +63,43 @@ typedef void livoxProto1_exitFn(void);
* @param dataPort Data port for point cloud (default: 56000)
* @param cmdPort Command port (default: 56001)
* @param imuPort IMU port (default: 56002)
* @return Device pointer on success, nullptr on failure
* @return LivoxProto1GetOrCreateDeviceResult (success + device on success,
* null device on failure)
*/
typedef std::function<
void(bool success, std::shared_ptr<livoxProto1::Device> device)>
livoxProto1_getOrCreateDeviceReqCbFn;
typedef void livoxProto1_getOrCreateDeviceReqFn(
typedef sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
livoxProto1_getOrCreateDeviceCReqFn(
const std::string& deviceIdentifier,
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
int commandTimeoutMs, int retryDelayMs,
const std::string& smoIp, uint8_t smoSubnetNbits,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort,
smo::Callback<livoxProto1_getOrCreateDeviceReqCbFn> callback);
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort);
typedef std::function<void(bool success)> livoxProto1_destroyDeviceReqCbFn;
typedef void livoxProto1_destroyDeviceReqFn(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_destroyDeviceReqCbFn> callback);
typedef sscl::co::ViralNonPostingInvoker<bool> livoxProto1_destroyDeviceCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef std::function<void(bool success)>
livoxProto1_device_enablePcloudDataReqCbFn;
typedef void livoxProto1_device_enablePcloudDataReqFn(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_enablePcloudDataReqCbFn> callback);
typedef sscl::co::ViralNonPostingInvoker<bool>
livoxProto1_device_enablePcloudDataCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef std::function<void(bool success)>
livoxProto1_device_disablePcloudDataReqCbFn;
typedef void livoxProto1_device_disablePcloudDataReqFn(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_disablePcloudDataReqCbFn> callback);
typedef sscl::co::ViralNonPostingInvoker<bool>
livoxProto1_device_disablePcloudDataCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef std::function<void(bool success, uint8_t returnMode)>
livoxProto1_device_getReturnModeReqCbFn;
typedef void livoxProto1_device_getReturnModeReqFn(
std::shared_ptr<livoxProto1::Device> device,
smo::Callback<livoxProto1_device_getReturnModeReqCbFn> callback);
typedef sscl::co::ViralNonPostingInvoker<LivoxProto1GetReturnModeResult>
livoxProto1_device_getReturnModeCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef std::shared_ptr<boost::asio::posix::stream_descriptor>
livoxProto1_getPcloudDataFdDescFn(void);
livoxProto1_mainFn livoxProto1_main;
livoxProto1_exitFn livoxProto1_exit;
livoxProto1_getOrCreateDeviceReqFn livoxProto1_getOrCreateDeviceReq;
livoxProto1_destroyDeviceReqFn livoxProto1_destroyDeviceReq;
livoxProto1_device_enablePcloudDataReqFn livoxProto1_device_enablePcloudDataReq;
livoxProto1_device_disablePcloudDataReqFn
livoxProto1_device_disablePcloudDataReq;
livoxProto1_device_getReturnModeReqFn livoxProto1_device_getReturnModeReq;
livoxProto1_getOrCreateDeviceCReqFn livoxProto1_getOrCreateDeviceCReq;
livoxProto1_destroyDeviceCReqFn livoxProto1_destroyDeviceCReq;
livoxProto1_device_enablePcloudDataCReqFn livoxProto1_device_enablePcloudDataCReq;
livoxProto1_device_disablePcloudDataCReqFn
livoxProto1_device_disablePcloudDataCReq;
livoxProto1_device_getReturnModeCReqFn livoxProto1_device_getReturnModeCReq;
livoxProto1_getPcloudDataFdDescFn livoxProto1_getPcloudDataFdDesc;
#ifdef __cplusplus
+1 -2
View File
@@ -1,7 +1,6 @@
#ifndef LIVOXPROTO1_PROTOCOL_H
#define LIVOXPROTO1_PROTOCOL_H
#include <boostAsioLinkageFix.h>
#include <vector>
#include <string>
#include <memory>
@@ -35,7 +34,7 @@ namespace endian {
// IPv4 address validation
inline bool isValidIPv4(const std::string& ipAddress) {
boost::system::error_code ec;
boost::asio::ip::address_v4::from_string(ipAddress, ec);
(void)boost::asio::ip::make_address_v4(ipAddress, ec);
return !ec;
}
+282 -7
View File
@@ -1,6 +1,9 @@
#include <iostream>
#include <cstring>
#include <coroutine>
#include <functional>
#include <optional>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
@@ -8,7 +11,11 @@
#include <fcntl.h>
#include <errno.h>
#include <adapters/boostAsio/deadlineTimerAReq.h>
#include <boost/asio/post.hpp>
#include <spinscale/co/group.h>
#include "udpCommandDemuxer.h"
#include "protocol.h"
#include "core.h"
#include "device.h"
@@ -16,7 +23,7 @@ namespace livoxProto1 {
namespace comms {
UdpCommandDemuxer::UdpCommandDemuxer(
const std::shared_ptr<smo::ComponentThread> &componentThread,
const std::shared_ptr<sscl::ComponentThread> &componentThread,
DeviceManager &deviceManager,
uint16_t commandPort,
uint16_t dataPort
@@ -44,7 +51,7 @@ void UdpCommandDemuxer::start()
try
{
{
smo::SpinLock::Guard lock(isActiveAndShouldStopLock);
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
setupSockets();
isActive.store(true);
@@ -71,7 +78,7 @@ void UdpCommandDemuxer::start()
void UdpCommandDemuxer::stop()
{
{
smo::SpinLock::Guard lock(isActiveAndShouldStopLock);
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
if (!isActive.load())
{ return; }
@@ -125,6 +132,17 @@ void UdpCommandDemuxer::setupCommandSocket()
+ ": Failed to create socket: " + strerror(errno));
}
// Set SO_REUSEADDR to allow binding even if port is in TIME_WAIT
int reuse = 1;
if (setsockopt(
socketGuard.getFd(), SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse)) < 0)
{
throw std::runtime_error(
std::string(__func__)
+ ": Failed to set SO_REUSEADDR: " + strerror(errno));
}
// Set socket to non-blocking mode
int flags = fcntl(socketGuard.getFd(), F_GETFL, 0);
if (flags < 0 || fcntl(
@@ -135,7 +153,23 @@ void UdpCommandDemuxer::setupCommandSocket()
+ ": Failed to set non-blocking mode: " + strerror(errno));
}
// Bind to command port
/** EXPLANATION:
* Bind to command port.
*
* WSL2 NAT PORT TRANSLATION ISSUE:
* On Windows 10, WSL2 uses NAT that translates UDP source ports when
* forwarding packets from WSL to the physical network. A socket bound to
* port 56001 in WSL may send from port 52511 on the wire.
*
* The device should use the cmd_port from the handshake message (56001),
* not the translated source port, so this may not break functionality.
* However, Windows NAT behavior can be unpredictable.
*
* Solutions:
* - Windows 11 22H2+: Use WSL2 mirror networking mode (.wslconfig)
* - Run natively on Linux
* - Accept the limitation (may work if device uses cmd_port from handshake)
*/
struct sockaddr_in localAddr;
memset(&localAddr, 0, sizeof(localAddr));
localAddr.sin_family = AF_INET;
@@ -151,9 +185,34 @@ void UdpCommandDemuxer::setupCommandSocket()
+ std::to_string(commandPort) + ": " + strerror(errno));
}
/* Verify the socket is actually bound to the expected port
* This helps catch WSL/Windows networking issues.
*/
struct sockaddr_in boundAddr;
socklen_t boundAddrLen = sizeof(boundAddr);
if (getsockname(
socketGuard.getFd(),
(struct sockaddr *)&boundAddr, &boundAddrLen) == 0)
{
uint16_t boundPort = ntohs(boundAddr.sin_port);
if (boundPort != commandPort)
{
std::cerr << __func__ << ": WARNING: Socket bound to port "
<< boundPort << " instead of expected port "
<< commandPort << std::endl;
}
#if 1
else
{
std::cout << __func__ << ": Successfully bound command socket "
"to port " << boundPort << std::endl;
}
#endif
}
// Create boost wrapper for async operations
cmdEndpointFdDesc = std::make_shared<boost::asio::posix::stream_descriptor>(
componentThread->getIoService(), socketGuard.getFd());
componentThread->getIoContext(), socketGuard.getFd());
// Transfer ownership, prevent auto-close
socketGuard.commit();
@@ -209,7 +268,7 @@ void UdpCommandDemuxer::setupPcloudDataSocket()
// Create boost wrapper for async operations
pcloudDataFdDesc = std::make_shared<boost::asio::posix::stream_descriptor>(
componentThread->getIoService(), socketGuard.getFd());
componentThread->getIoContext(), socketGuard.getFd());
// Transfer ownership, prevent auto-close
socketGuard.commit();
@@ -239,7 +298,7 @@ void UdpCommandDemuxer::onDataReady(const boost::system::error_code &error)
return;
}
smo::SpinLock::Guard lock(isActiveAndShouldStopLock);
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
if (!isActive.load() || shouldStop.load())
{ return; }
@@ -278,6 +337,19 @@ void UdpCommandDemuxer::processIncomingData()
char sourceIP[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &senderAddr.sin_addr, sourceIP, INET_ADDRSTRLEN);
if (bytesReceived >= static_cast<ssize_t>(
sizeof(Header) + sizeof(Command)))
{
const uint8_t cmdSet = receiveBuffer[sizeof(Header)];
const uint8_t cmdId = receiveBuffer[sizeof(Header) + 1];
if (tryCompletePendingCommandWait(
sourceIP, cmdSet, cmdId, receiveBuffer, bytesReceived))
{
return;
}
}
// First, find device with matching IP address in DeviceManager collection
for (const auto &device : deviceManager.devices)
{
@@ -343,5 +415,208 @@ void UdpCommandDemuxer::processIncomingData()
<< sourceIP << ", discarding datagram" << std::endl;
}
struct UdpCommandDemuxer::PendingCommandWaitDesc
{
CommandWaitKey key;
boost::asio::io_context &resumeIoContext;
std::atomic<bool> settled{false};
UdpCommandResponseResult result{};
std::coroutine_handle<> callerSchedHandle;
PendingCommandWaitDesc(
CommandWaitKey keyIn,
boost::asio::io_context &resumeIoContextIn)
: key(std::move(keyIn)),
resumeIoContext(resumeIoContextIn)
{}
};
void UdpCommandDemuxer::settlePendingCommandWait(
const std::shared_ptr<PendingCommandWaitDesc> &wait,
UdpCommandResponseResult::Outcome outcome,
const uint8_t *data, ssize_t bytesReceived)
{
if (wait->settled.exchange(true)) {
return;
}
wait->result.outcome = outcome;
wait->result.bytesReceived = bytesReceived;
if (outcome == UdpCommandResponseResult::Outcome::Response
&& data != nullptr
&& bytesReceived > 0
&& bytesReceived
<= static_cast<ssize_t>(sizeof(wait->result.buffer)))
{
memcpy(wait->result.buffer, data, bytesReceived);
}
std::coroutine_handle<> handle = wait->callerSchedHandle;
if (!handle) {
return;
}
boost::asio::post(wait->resumeIoContext, handle);
}
std::shared_ptr<UdpCommandDemuxer::PendingCommandWaitDesc>
UdpCommandDemuxer::findAndRemovePendingCommandWait(const CommandWaitKey &key)
{
sscl::SpinLock::Guard guard(pendingWaits.lock);
const auto iterator = pendingWaits.rsrc.pendingWaits.find(key);
if (iterator == pendingWaits.rsrc.pendingWaits.end()) {
return nullptr;
}
std::shared_ptr<PendingCommandWaitDesc> wait = iterator->second;
pendingWaits.rsrc.pendingWaits.erase(iterator);
return wait;
}
void UdpCommandDemuxer::cancelPendingCommandWait(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp)
{
std::shared_ptr<PendingCommandWaitDesc> wait = findAndRemovePendingCommandWait(
{deviceIp, cmdSet, cmdId});
if (!wait) { return; }
settlePendingCommandWait(
wait,
UdpCommandResponseResult::Outcome::Timeout,
nullptr, -1);
}
bool UdpCommandDemuxer::tryCompletePendingCommandWait(
const char *sourceIp,
uint8_t cmdSet, uint8_t cmdId,
const uint8_t *data, ssize_t bytesReceived)
{
std::shared_ptr<PendingCommandWaitDesc> wait = findAndRemovePendingCommandWait(
{sourceIp, cmdSet, cmdId});
if (!wait) { return false; }
const UdpCommandResponseResult::Outcome outcome =
(bytesReceived > 0
&& bytesReceived
<= static_cast<ssize_t>(sizeof(wait->result.buffer)))
? UdpCommandResponseResult::Outcome::Response
: UdpCommandResponseResult::Outcome::RecvError;
settlePendingCommandWait(wait, outcome, data, bytesReceived);
return true;
}
sscl::co::ViralNonPostingInvoker<UdpCommandResponseResult>
UdpCommandDemuxer::waitForCommandResponseCReq(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp)
{
const CommandWaitKey key{deviceIp, cmdSet, cmdId};
auto wait = std::make_shared<PendingCommandWaitDesc>(
key, componentThread->getIoContext());
{
sscl::SpinLock::Guard guard(pendingWaits.lock);
pendingWaits.rsrc.pendingWaits[key] = wait;
}
struct PendingCommandWaitDescAwaiter
{
std::shared_ptr<PendingCommandWaitDesc> wait;
bool await_ready() const noexcept
{
return wait->settled.load(std::memory_order_acquire);
}
bool await_suspend(std::coroutine_handle<> caller) noexcept
{
if (wait->settled.load(std::memory_order_acquire)) {
return false;
}
wait->callerSchedHandle = caller;
return true;
}
UdpCommandResponseResult await_resume() const noexcept
{
return wait->result;
}
};
const UdpCommandResponseResult result =
co_await PendingCommandWaitDescAwaiter{wait};
if (findAndRemovePendingCommandWait(key))
{
std::cerr << __func__ << ": pending wait still registered after "
"settle for device " << deviceIp << " (cmd_set="
<< static_cast<int>(cmdSet) << ", cmd_id="
<< static_cast<int>(cmdId) << "); program error"
<< std::endl;
}
co_return result;
}
sscl::co::ViralNonPostingInvoker<UdpCommandResponseResult>
UdpCommandDemuxer::waitForCommandResponseCReq(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp,
int timeoutMs)
{
/** EXPLANATION:
* We setup an async timer event to detect timeout, and register a UDP
* command handler to wait for the device to respond to the incoming command
* request. If the device does not respond within the timeout period,
* we will consider the command to have failed.
*/
boost::asio::io_context &ioContext = componentThread->getIoContext();
boost::asio::deadline_timer raceTimer(ioContext);
auto timerAwaiter = adapters::boostAsio::getDeadlineTimerAReqAwaiter(
ioContext,
raceTimer,
boost::posix_time::milliseconds(timeoutMs));
auto responseInvoker = waitForCommandResponseCReq(cmdSet, cmdId, deviceIp);
static constexpr int timerMemberSettlementIndex = 0;
sscl::co::Group group;
group.add(timerAwaiter);
group.add(responseInvoker);
co_await group.getAwaitFirstSettlementInvoker();
group.checkForAndReThrowGroupExceptions();
const bool timerWonFirst =
group.s.rsrc.firstSettledInvokerIdx == timerMemberSettlementIndex;
if (timerWonFirst) {
cancelPendingCommandWait(cmdSet, cmdId, deviceIp);
} else {
raceTimer.cancel();
}
/** Group member adapter coros are fire-and-forget; keep group alive until
* both members settle so the loser adapter does not touch freed state.
*/
co_await group.getAwaitAllSettlementsInvoker();
group.checkForAndReThrowGroupExceptions();
if (timerWonFirst)
{
UdpCommandResponseResult timeoutResult;
timeoutResult.outcome = UdpCommandResponseResult::Outcome::Timeout;
co_return timeoutResult;
}
co_return responseInvoker.completedReturnValues().myReturnValue;
}
} // namespace comms
} // namespace livoxProto1
+93 -11
View File
@@ -1,12 +1,17 @@
#ifndef UDP_COMMAND_DEMUXER_H
#define UDP_COMMAND_DEMUXER_H
#include <boostAsioLinkageFix.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <boost/asio/posix/stream_descriptor.hpp>
#include <componentThread.h>
#include <spinLock.h>
#include <spinscale/spinLock.h>
#include <spinscale/sharedResourceGroup.h>
#include <spinscale/co/invokers.h>
namespace livoxProto1 {
@@ -15,6 +20,45 @@ class DeviceManager;
namespace comms {
struct UdpCommandResponseResult
{
enum class Outcome
{
Timeout,
Response,
RecvError
};
Outcome outcome = Outcome::Timeout;
uint8_t buffer[1024]{};
ssize_t bytesReceived = -1;
};
struct CommandWaitKey
{
std::string deviceIp;
uint8_t cmdSet;
uint8_t cmdId;
bool operator==(const CommandWaitKey &other) const
{
return deviceIp == other.deviceIp
&& cmdSet == other.cmdSet
&& cmdId == other.cmdId;
}
};
struct CommandWaitKeyHash
{
std::size_t operator()(const CommandWaitKey &key) const
{
std::size_t hash = std::hash<std::string>{}(key.deviceIp);
hash ^= (static_cast<std::size_t>(key.cmdSet) << 8)
| static_cast<std::size_t>(key.cmdId);
return hash;
}
};
/**
* UdpCommandDemuxer - Routes UDP command datagrams to appropriate devices
*
@@ -37,7 +81,7 @@ class UdpCommandDemuxer
{
public:
UdpCommandDemuxer(
const std::shared_ptr<smo::ComponentThread>& componentThread,
const std::shared_ptr<sscl::ComponentThread>& componentThread,
DeviceManager& deviceManager,
uint16_t commandPort = 56001,
uint16_t dataPort = 56000);
@@ -62,13 +106,20 @@ public:
return pcloudDataFdDesc;
}
private:
// Socket and async objects
std::shared_ptr<boost::asio::posix::stream_descriptor> pcloudDataFdDesc;
// Socket and async objects
std::shared_ptr<boost::asio::posix::stream_descriptor> cmdEndpointFdDesc;
sscl::co::ViralNonPostingInvoker<UdpCommandResponseResult>
waitForCommandResponseCReq(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp,
int timeoutMs);
private:
struct PendingCommandWaitDesc;
sscl::co::ViralNonPostingInvoker<UdpCommandResponseResult>
waitForCommandResponseCReq(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp);
void setupSockets();
void setupCommandSocket();
void setupPcloudDataSocket();
@@ -76,17 +127,48 @@ private:
void onDataReady(const boost::system::error_code& error);
void processIncomingData();
std::shared_ptr<smo::ComponentThread> componentThread;
bool tryCompletePendingCommandWait(
const char *sourceIp,
uint8_t cmdSet, uint8_t cmdId,
const uint8_t *data, ssize_t bytesReceived);
void cancelPendingCommandWait(
uint8_t cmdSet, uint8_t cmdId,
const std::string &deviceIp);
std::shared_ptr<PendingCommandWaitDesc> findAndRemovePendingCommandWait(
const CommandWaitKey &key);
void settlePendingCommandWait(
const std::shared_ptr<PendingCommandWaitDesc> &wait,
UdpCommandResponseResult::Outcome outcome,
const uint8_t *data, ssize_t bytesReceived);
std::shared_ptr<sscl::ComponentThread> componentThread;
DeviceManager& deviceManager;
uint16_t commandPort;
uint16_t dataPort;
// State management
smo::SpinLock isActiveAndShouldStopLock;
sscl::SpinLock isActiveAndShouldStopLock;
std::atomic<bool> isActive{false};
std::atomic<bool> shouldStop{false};
// Receive buffer
struct PendingWaitsResources
{
std::unordered_map<
CommandWaitKey,
std::shared_ptr<PendingCommandWaitDesc>,
CommandWaitKeyHash>
pendingWaits;
};
sscl::SharedResourceGroup<sscl::SpinLock, PendingWaitsResources>
pendingWaits;
std::shared_ptr<boost::asio::posix::stream_descriptor> pcloudDataFdDesc;
std::shared_ptr<boost::asio::posix::stream_descriptor> cmdEndpointFdDesc;
uint8_t receiveBuffer[1024];
struct sockaddr_in senderAddr;
socklen_t senderAddrLen;
+9 -2
View File
@@ -10,11 +10,18 @@ if(ENABLE_LIB_xcbXorg)
xcbXorg.cpp
)
set_target_properties(xcbXorg PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
# Set config define for header generation
add_compile_definitions(CONFIG_LIB_XCBXORG_ENABLED)
target_include_directories(xcbXorg PUBLIC ${XCB_INCLUDE_DIRS})
target_link_libraries(xcbXorg ${XCB_LIBRARIES} attachmentSupport)
target_link_libraries(xcbXorg ${XCB_LIBRARIES})
# Install rules
install(TARGETS xcbXorg DESTINATION lib)
install(TARGETS xcbXorg
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
endif()
+1
View File
@@ -0,0 +1 @@
add_subdirectory(core)
+26
View File
@@ -0,0 +1,26 @@
option(ENABLE_COMPARATORLIB_core
"Enable comparator lib core (libcoreComp.so)" ON)
if(ENABLE_COMPARATORLIB_core)
add_library(coreComp SHARED
coreComp.cpp
)
set_target_properties(coreComp PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
target_include_directories(coreComp PUBLIC
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/smocore/include
)
target_link_libraries(coreComp PUBLIC
spinscale
)
install(TARGETS coreComp
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
endif()
+129
View File
@@ -0,0 +1,129 @@
#include <user/comparatorApiDesc.h>
#include <user/senseApiDesc.h>
namespace smo {
namespace comparator_lib {
namespace core {
namespace {
static const char COMPARATOR_LIB_NAME[] = "coreComp";
static const char BODY_SPOT_COMPARATOR_TYPE_NAME[] = "bodySpot";
static const char STIM_FEAT_COMPARATOR_TYPE_NAME[] = "stimFeat";
static const char SIMULTANEITY_STAMP_COMPARATOR_TYPE_NAME[] =
"simultaneityStamp";
class BodySpotComparator
: public cologex::Comparator
{
public:
BodySpotComparator()
: cologex::Comparator(MentalEntity::Id{0})
{}
cologex::ComparatorTypeId getTypeId() const override
{
return cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_BODY_SPOT
};
}
};
class StimFeatComparator
: public cologex::Comparator
{
public:
StimFeatComparator()
: cologex::Comparator(MentalEntity::Id{0})
{}
cologex::ComparatorTypeId getTypeId() const override
{
return cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_STIM_FEAT
};
}
};
class SimultaneityStampComparator
: public cologex::Comparator
{
public:
SimultaneityStampComparator()
: cologex::Comparator(MentalEntity::Id{0})
{}
cologex::ComparatorTypeId getTypeId() const override
{
return cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_SIMULTANEITY_STAMP
};
}
};
std::unique_ptr<cologex::Comparator> makeBodySpotComparator()
{
return std::make_unique<BodySpotComparator>();
}
std::unique_ptr<cologex::Comparator> makeStimFeatComparator()
{
return std::make_unique<StimFeatComparator>();
}
std::unique_ptr<cologex::Comparator> makeSimultaneityStampComparator()
{
return std::make_unique<SimultaneityStampComparator>();
}
cologex::ComparatorLibDesc buildComparatorLibDesc()
{
return cologex::ComparatorLibDesc{
.name = COMPARATOR_LIB_NAME,
.exportedComparatorTypes = {
cologex::ExportedComparatorTypeDesc{
.name = BODY_SPOT_COMPARATOR_TYPE_NAME,
.typeId = cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_BODY_SPOT
},
.getNewInstance = makeBodySpotComparator
},
cologex::ExportedComparatorTypeDesc{
.name = STIM_FEAT_COMPARATOR_TYPE_NAME,
.typeId = cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_STIM_FEAT
},
.getNewInstance = makeStimFeatComparator
},
cologex::ExportedComparatorTypeDesc{
.name = SIMULTANEITY_STAMP_COMPARATOR_TYPE_NAME,
.typeId = cologex::ComparatorTypeId{
cologex::SMO_COMPARATOR_VENDOR_ID,
cologex::SMO_COMPARATOR_TYPE_SIMULTANEITY_STAMP
},
.getNewInstance = makeSimultaneityStampComparator
}
}
};
}
} // namespace
} // namespace core
} // namespace comparator_lib
} // namespace smo
extern "C" smo::SMO_GET_COMPARATOR_LIB_DESC_FN_TYPEDEF
SMO_GET_COMPARATOR_LIB_DESC_FN_NAME;
const smo::cologex::ComparatorLibDesc& SMO_GET_COMPARATOR_LIB_DESC_FN_NAME(
const smo::stim_buff::SmoCallbacks& callbacks)
{
(void)callbacks;
static const smo::cologex::ComparatorLibDesc comparatorLibDesc =
smo::comparator_lib::core::buildComparatorLibDesc();
return comparatorLibDesc;
}
+1
View File
@@ -4,6 +4,7 @@ add_daps_target(all_device_specs
SOURCES
avia0.dapss
win0.dapss
elp-4k-usb-cam.dapss
)
# Register this target for later dependency addition from main CMakeLists.txt
+24 -3
View File
@@ -1,3 +1,24 @@
+edev|avia0|mesh()|livoxGen1()|livoxProto1()|3JEDK380010Z39||
+edev|avia0|pcloudIntensity()|livoxGen1()|livoxProto1()|3JEDK380010Z39||
+edev|avia0|pcloudAmbience()|livoxGen1()|livoxProto1()|3JEDK380010Z39
#ifndef SMO_IP
# define SMO_IP
#endif
+edev|avia0|mesh()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39||
+edev|avia0|pcloudIntensity()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39||
/* pcloudLightAmbience with negtrin: be negatively disposed to high ambience
* (high passband counts above passband-count-gt-val), to the point of feeling
* un-ignorable pain when it's sufficiently high.
*/
+edev|avia0
|negtrin(interest-pc=85|distraction-pc=90|intolerable-pc=95)
|pcloudLightAmbience(passband-count-gt-val=120)
|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39||
/* pcloudDarkAmbience with postrin: be positively disposed to dim ambience
* (passband counts below passband-count-lt-val), but not so drawn that eyelids
* droop — no distraction, no stupefaction.
*/
+edev|avia0
|postrin(interest-pc=85)
|pcloudDarkAmbience(passband-count-lt-val=8)
|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39
+12
View File
@@ -1,3 +1,8 @@
add_daps_target(body_rpi5_persys_headless
SOURCES
rpi5-persys-headless.dapss
)
add_daps_target(body_rpi5_persys
SOURCES
rpi5-persys.dapss
@@ -8,10 +13,17 @@ add_daps_target(body_dell_laptop
dell-laptop.dapss
)
add_daps_target(body_yocto_qemu_x86_headless
SOURCES
yocto-qemu-x86-headless.dapss
)
# Register this target for later dependency addition from main CMakeLists.txt
register_daps_target(body_rpi5_persys)
register_daps_target(body_dell_laptop)
register_daps_target(body_yocto_qemu_x86_headless)
# Make the DAPSS target part of the ALL target for this subdirectory
# This ensures DAPSS targets are built when building just this subdirectory
set_property(TARGET body_rpi5_persys PROPERTY FOLDER "devices/bodies")
set_property(TARGET body_dell_laptop PROPERTY FOLDER "devices/bodies")
set_property(TARGET body_yocto_qemu_x86_headless PROPERTY FOLDER "devices/bodies")
+2
View File
@@ -1,3 +1,5 @@
#include "../win0.dapss"
||
#include "../avia0.dapss"
||
#include "../elp-4k-usb-cam.dapss"
@@ -0,0 +1,4 @@
//#include "../win0.dapss"
//||
#define SMO_IP smo-ip=10.42.0.2
#include "../avia0.dapss"
+1
View File
@@ -1,3 +1,4 @@
#define XORG_DISPLAY 0
#include "../win0.dapss"
||
#include "../avia0.dapss"
@@ -0,0 +1,4 @@
//#include "../win0.dapss"
//||
#define SMO_IP smo-ip=10.42.0.16
#include "../avia0.dapss"
+25
View File
@@ -0,0 +1,25 @@
/* ELP 4K USB camera on dell-laptop (USB port 1, location external).
*
* V4L2/libcamera model: "HDMI USB Camera: HDMI USB Camer"
* USB VID:PID 32e4:9415, serial 1020181e58586223.
* Native capture up to 3840x2160 @ 30fps (MJPEG / YUYV via opt-planar).
*/
+edev|elp-4k-usb-cam|negtrin()|postrin()|colour-yuv-y()
|lcameraBuff(
vres=720p
|colour-space=yuv|opt-planar)
|lcameraDev()
|model-substr:HDMI\ USB;location:external||
+edev|elp-4k-usb-cam|colour-yuv-u()
|lcameraBuff(
vres=720p
|colour-space=yuv|opt-planar)
|lcameraDev()
|model-substr:HDMI\ USB;location:external||
+edev|elp-4k-usb-cam|colour-yuv-v()
|lcameraBuff(
vres=720p
|colour-space=yuv|opt-planar)
|lcameraDev()
|model-substr:HDMI\ USB;location:external
+6 -1
View File
@@ -1 +1,6 @@
+edev|win0|visual-qualeiface()|xcb(dev-substring)|xorg(display=1|screen=0)|mut
#define XORG_DISPLAY_DEFAULT 1
#ifndef XORG_DISPLAY
# define XORG_DISPLAY XORG_DISPLAY_DEFAULT
#endif
+edev|win0|visual-qualeiface()|xcb(dev-substring)|xorg(display=XORG_DISPLAY|screen=0)|mut
@@ -0,0 +1,17 @@
BBPATH .= ":${LAYERDIR}"
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
${LAYERDIR}/recipes-*/*/*.bbappend"
BBFILE_COLLECTIONS += "salmanoff"
BBFILE_PATTERN_salmanoff = "^${LAYERDIR}/"
BBFILE_PRIORITY_salmanoff = "10"
LAYERSERIES_COMPAT_salmanoff = "wrynose"
# Salmanoff requires shared Boost.System (< 1.89). Pin 1.86 from this layer.
BBMASK += ".*/boost_1.90.0.bb"
BBMASK += ".*/boost-build-native_1.90.0.bb"
PREFERRED_VERSION_boost = "1.86.%"
PREFERRED_VERSION_boost-build-native = "1.86.%"
@@ -0,0 +1,3 @@
# Layer-local build settings (require from build/conf/local.conf).
# Enables Mesa Rusticl so libopencl-mesa provides a software OpenCL ICD (llvmpipe).
DISTRO_FEATURES:append = " opencl"
@@ -0,0 +1,22 @@
require recipes-core/images/core-image-minimal.bb
SUMMARY = "Salmanoff runtime image"
IMAGE_FEATURES += "ssh-server-openssh"
IMAGE_INSTALL:append = " \
rseqsliceprobe \
iproute2 \
boost-system \
boost-log \
libcamera \
opencl-icd-loader \
libopencl-mesa \
libgallium \
clinfo \
v4l-utils \
salmanoff-rusticl-env \
liburing \
salmanoff \
"
@@ -0,0 +1,12 @@
# Salmanoff lab LAN — static eth0 for bridged QEMU on 10.42.0.0/24.
# Used with: runqemu salmanoff-image snapshot bridge=<host-bridge>
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 10.42.0.16
netmask 255.255.255.0
gateway 10.42.0.1
dns-nameservers 10.42.0.1
@@ -0,0 +1,7 @@
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
SRC_URI:append = " file://salmanoff-interfaces"
do_install:append() {
install -m 0644 ${UNPACKDIR}/salmanoff-interfaces ${D}${sysconfdir}/network/interfaces
}
@@ -0,0 +1,4 @@
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
SRC_URI:append = " file://rseq-slice-extension.cfg"
@@ -0,0 +1,3 @@
CONFIG_RSEQ=y
CONFIG_RSEQ_SLICE_EXTENSION=y
@@ -0,0 +1,59 @@
SUMMARY = "Salmanoff cognitive robotics runtime"
DESCRIPTION = "Sensor management runtime with Livox LiDAR support"
HOMEPAGE = "https://github.com/salmanoff/salmanoff"
LICENSE = "CLOSED"
LIC_FILES_CHKSUM = "file://${S}/LICENSE;beginline=1;endline=1;md5=7902e8a44912c18af1caf7499fb00176"
DEPENDS += " \
boost \
liburing \
libcamera \
opencl-headers \
virtual/libopencl1 \
flex-native \
bison-native \
"
RDEPENDS:${PN} += " \
boost-system \
boost-log \
liburing \
libcamera \
opencl-icd-loader \
libopencl-mesa \
libgallium \
salmanoff-rusticl-env \
"
SRC_URI = "gitsm://git@zbz-gitea-as-hayodea/hayodea/salmanoff.git;protocol=ssh;branch=clast"
SRCREV = "63ff0aa264b62977c5d41781d7802628f771aac0"
PV = "0.01.001"
inherit cmake pkgconfig
# Remap DWARF source paths from WORKDIR to the installed debug source tree.
DEBUG_PREFIX_MAP_EXTRA:append = " -fdebug-prefix-map=${WORKDIR}=${TARGET_DBGSRC_DIR}"
EXTRA_OECMAKE += " \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DENABLE_TESTS=OFF \
-DENABLE_LIB_xcbXorg=OFF \
-DENABLE_LIB_lcameraDev=ON \
-DENABLE_STIMBUFFAPI_lcameraBuff=ON \
-DENABLE_LCAMERADEV_TOOLS=OFF \
-DCOMPILE_PCL_TOOLS=OFF \
"
FILES:${PN} += " \
${libdir}/lib*.so.* \
${datadir}/salmanoff \
"
do_install:append() {
# CMake installs .daps under share/salmanoff/devices; ensure bodies path exists.
if [ ! -d ${D}${datadir}/salmanoff/devices/bodies ]; then
install -d ${D}${datadir}/salmanoff/devices/bodies
fi
}
@@ -0,0 +1,20 @@
# The Boost web site provides free peer-reviewed portable
# C++ source libraries. The emphasis is on libraries which
# work well with the C++ Standard Library. The libraries are
# intended to be widely useful, and are in regular use by
# thousands of programmers across a broad spectrum of applications.
HOMEPAGE = "http://www.boost.org/"
LICENSE = "BSL-1.0 & MIT & Python-2.0"
LIC_FILES_CHKSUM = "file://LICENSE_1_0.txt;md5=e4224ccaecb14d942c71d31bef20d78c"
BOOST_VER = "${@"_".join(d.getVar("PV").split("."))}"
BOOST_MAJ = "${@"_".join(d.getVar("PV").split(".")[0:2])}"
BOOST_P = "boost_${BOOST_VER}"
SRC_URI = "https://archives.boost.io/release/${PV}/source/${BOOST_P}.tar.bz2"
SRC_URI[sha256sum] = "1bed88e40401b2cb7a1f76d4bab499e352fa4d0c5f31c0dbae64e24d34d7513b"
UPSTREAM_CHECK_URI = "http://www.boost.org/users/download/"
UPSTREAM_CHECK_REGEX = "release/(?P<pver>.*)/source/"
S = "${UNPACKDIR}/${BOOST_P}"
@@ -0,0 +1,26 @@
SUMMARY = "Boost.Build"
DESCRIPTION = "B2 makes it easy to build C++ projects, everywhere."
HOMEPAGE = "https://github.com/boostorg/build"
SECTION = "devel"
LICENSE = "BSL-1.0"
LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=e4224ccaecb14d942c71d31bef20d78c"
SRC_URI = "git://github.com/boostorg/build;protocol=https;branch=master;tag=boost-${PV}"
SRCREV = "4a52d8c06635435b64e31a56eaf7ca5dc912a71d"
PE = "1"
UPSTREAM_CHECK_GITTAGREGEX = "boost-(?P<pver>(\d+(\.\d+)+))"
inherit native
do_compile() {
./bootstrap.sh
}
do_install() {
HOME=/var/run ./b2 install --prefix=${D}${prefix}
}
# The build is either release mode (pre-stripped) or debug (-O0).
INSANE_SKIP:${PN} = "already-stripped"
@@ -0,0 +1,223 @@
SUMMARY = "Free peer-reviewed portable C++ source libraries"
DESCRIPTION = "Provides free peer-reviewed portable C++ source libraries. The emphasis is on libraries which work well with the C++ \
Standard Library. One goal is to establish 'existing practice' and \
provide reference implementations so that the Boost libraries are suitable for eventual standardization. Some of the libraries have already been proposed for inclusion in the C++ Standards Committee's \
upcoming C++ Standard Library Technical Report."
SECTION = "libs"
DEPENDS = "boost-build-native zlib bzip2"
CVE_PRODUCT = "boost:boost"
ARM_INSTRUCTION_SET:armv4 = "arm"
ARM_INSTRUCTION_SET:armv5 = "arm"
B = "${WORKDIR}/build"
do_configure[cleandirs] = "${B}"
BOOST_LIBS = "\
atomic \
charconv \
chrono \
container \
context \
contract \
coroutine \
date_time \
exception \
fiber \
filesystem \
graph \
headers \
iostreams \
json \
log \
math \
process \
program_options \
random \
regex \
serialization \
system \
test \
thread \
timer \
type_erasure \
url \
wave \
${@bb.utils.filter('PACKAGECONFIG', 'locale python', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'graph_parallel', 'graph_parallel mpi', \
bb.utils.filter('PACKAGECONFIG', 'mpi', d), d)} \
"
# optional libraries
PACKAGECONFIG ??= "locale python"
PACKAGECONFIG[locale] = ",,icu"
PACKAGECONFIG[graph_parallel] = ",,,boost-mpi mpich"
PACKAGECONFIG[mpi] = ",,mpich"
PACKAGECONFIG[python] = ",,python3"
inherit python3-dir
PYTHON_ROOT = "${STAGING_DIR_HOST}/${prefix}"
# Make a package for each library, plus -dev
PACKAGES = "${PN}-dbg ${BOOST_PACKAGES}"
python __anonymous () {
packages = []
extras = []
pn = d.getVar("PN")
mlprefix = d.getVar("MLPREFIX")
for lib in d.getVar('BOOST_LIBS').split():
extras.append("--with-%s" % lib)
pkg = "%s-%s" % (d.getVar("BPN"), lib.replace("_", "-"))
if "-native" in pn:
pkg = pkg + "-native"
packages.append(mlprefix + pkg)
if not d.getVar("FILES:%s%s" % (mlprefix, pkg)):
d.setVar("FILES:%s%s" % (mlprefix, pkg), "${libdir}/libboost_%s*.so.*" % lib)
d.setVar("BOOST_PACKAGES", " ".join(packages))
d.setVar("BJAM_EXTRA", " ".join(extras))
}
# Override the contents of specific packages
FILES:${PN}-graph_parallel = "${libdir}/libboost_graph_parallel.so.*"
FILES:${PN}-locale = "${libdir}/libboost_locale.so.*"
FILES:${PN}-mpi = "${libdir}/mpi.so ${libdir}/libboost_mpi*.so.*"
FILES:${PN}-serialization = "${libdir}/libboost_serialization*.so.* \
${libdir}/libboost_wserialization*.so.*"
FILES:${PN}-test = "${libdir}/libboost_prg_exec_monitor*.so.* \
${libdir}/libboost_unit_test_framework*.so.*"
# -dev last to pick up the remaining stuff
PACKAGES += "${PN}-dev ${PN}-staticdev"
FILES:${PN}-dev = "${includedir} ${libdir}/libboost_*.so ${libdir}/cmake"
FILES:${PN}-staticdev = "${libdir}/libboost_*.a"
# "boost" is a metapackage which pulls in all boost librabries
PACKAGES += "${PN}"
FILES:${PN} = ""
ALLOW_EMPTY:${PN} = "1"
RRECOMMENDS:${PN} += "${BOOST_PACKAGES}"
RRECOMMENDS:${PN}:class-native = ""
# to avoid GNU_HASH QA errors added LDFLAGS to ARCH; a little bit dirty but at least it works
TARGET_CC_ARCH += "${LDFLAGS}"
# Oh yippee, a new build system, it's sooo cooool I could eat my own
# foot. inlining=on lets the compiler choose, I think. At least this
# stuff is documented...
# NOTE: if you leave <debug-symbols>on then in a debug build the build sys
# objcopy will be invoked, and that won't work. Building debug apparently
# requires hacking gcc-tools.jam
#
# Sometimes I wake up screaming. Famous figures are gathered in the nightmare,
# Steve Bourne, Larry Wall, the whole of the ANSI C committee. They're just
# standing there, waiting, but the truely terrifying thing is what they carry
# in their hands. At first sight each seems to bear the same thing, but it is
# not so for the forms in their grasp are ever so slightly different one from
# the other. Each is twisted in some grotesque way from the other to make each
# an unspeakable perversion impossible to perceive without the onset of madness.
# True insanity awaits anyone who perceives all of these horrors together.
#
# Quotation marks, there might be an easier way to do this, but I can't find
# it. The problem is that the user.hpp configuration file must receive a
# pre-processor macro defined as the appropriate string - complete with "'s
# around it. (<> is a possibility here but the danger to that is that the
# failure case interprets the < and > as shell redirections, creating
# random files in the source tree.)
#
#bjam: '-DBOOST_PLATFORM_CONFIG=\"config\"'
#do_compile: '-sGCC=... '"'-DBOOST_PLATFORM_CONFIG=\"config\"'"
SQD = '"'
EQD = '\"'
#boost.bb: "... '-sGCC=... '${SQD}'-DBOOST_PLATFORM_CONFIG=${EQD}config${EQD}'${SQD} ..."
BJAM_CONF = "${SQD}'-DBOOST_PLATFORM_CONFIG=${EQD}boost/config/platform/${TARGET_OS}.hpp${EQD}'${SQD}"
BJAM_TOOLS = "--ignore-site-config \
'-sTOOLS=gcc' \
'-sGCC=${CC} '${BJAM_CONF} \
'-sGXX=${CXX} '${BJAM_CONF} \
'-sGCC_INCLUDE_DIRECTORY=${STAGING_INCDIR}' \
'-sGCC_STDLIB_DIRECTORY=${STAGING_LIBDIR}' \
'-sBUILD=release <optimization>space <threading>multi <inlining>on <debug-symbols>off' \
'-sPYTHON_ROOT=${PYTHON_ROOT}' \
'--layout=system' \
"
# use PARALLEL_MAKE to speed up the build
BOOST_PARALLEL_MAKE = "${@oe.utils.parallel_make_argument(d, '-j%d')}"
BJAM_OPTS = '${BOOST_PARALLEL_MAKE} -d+2 -q \
${BJAM_TOOLS} \
-sBOOST_BUILD_USER_CONFIG=${WORKDIR}/user-config.jam \
-sICU_PATH=${STAGING_EXECPREFIXDIR} \
--build-dir=${B} \
--disable-icu \
${BJAM_EXTRA}'
# Native compilation of bzip2 isn't working
BJAM_OPTS:append:class-native = ' -sNO_BZIP2=1'
# Adjust the build for x32
BJAM_OPTS:append:x86-x32 = " abi=x32 address-model=64"
# cross compiling for arm fails to detect abi, so provide some help
BJAM_OPTS:append:arm = " abi=aapcs architecture=arm"
BJAM_OPTS:append:aarch64 = " abi=aapcs address-model=64 architecture=arm"
do_configure() {
cd ${S}
cp -f ${S}/boost/config/platform/linux.hpp ${S}/boost/config/platform/linux-gnueabi.hpp
# D2194:Fixing the failure of "error: duplicate initialization of gcc with the following parameters" during compilation.
rm -f ${WORKDIR}/user-config.jam
echo 'using gcc : : ${CXX} : <cflags>"${CFLAGS}" <cxxflags>"${CXXFLAGS}" <linkflags>"${LDFLAGS}" ;' >> ${WORKDIR}/user-config.jam
# If we want Python then we need to tell Boost *exactly* where to find it
if ${@bb.utils.contains('BOOST_LIBS', 'python', 'true', 'false', d)}; then
echo "using python : ${PYTHON_BASEVERSION} : ${STAGING_DIR_HOST}${bindir}/python3 : ${STAGING_DIR_HOST}${includedir}/${PYTHON_DIR}${PYTHON_ABI} : ${STAGING_DIR_HOST}${libdir}/${PYTHON_DIR} ;" >> ${WORKDIR}/user-config.jam
fi
if ${@bb.utils.contains('BOOST_LIBS', 'mpi', 'true', 'false', d)}; then
echo "using mpi : : <find-shared-library>mpi ;" >> ${WORKDIR}/user-config.jam
fi
CC="${BUILD_CC}" CFLAGS="${BUILD_CFLAGS}" ./bootstrap.sh --with-bjam=b2 --with-toolset=gcc
# Boost can't be trusted to find Python on it's own, so remove any mention
# of it from the boost configuration
sed -i '/using python/d' ${S}/project-config.jam
}
do_compile() {
cd ${S}
b2 ${BJAM_OPTS} \
--prefix=${prefix} \
--exec-prefix=${exec_prefix} \
--libdir=${libdir} \
--includedir=${includedir} \
--debug-configuration
}
do_install() {
cd ${S}
b2 ${BJAM_OPTS} \
--libdir=${D}${libdir} \
--includedir=${D}${includedir} \
install
for lib in ${BOOST_LIBS}; do
if [ -e ${D}${libdir}/libboost_${lib}.a ]; then
ln -s libboost_${lib}.a ${D}${libdir}/libboost_${lib}-mt.a
fi
if [ -e ${D}${libdir}/libboost_${lib}.so ]; then
ln -s libboost_${lib}.so ${D}${libdir}/libboost_${lib}-mt.so
fi
done
# Cmake files reference full paths to image
find ${D}${libdir}/cmake -type f | \
grep 'cmake$' | \
xargs -n 1 sed -e 's,${D}${libdir}/cmake,${libdir}/cmake,' -i
}
BBCLASSEXTEND = "native nativesdk"

Some files were not shown because too many files have changed in this diff Show More