704 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
hayodea 17c0e10be8 Salmanoff: Version increment to v0.01.001 2025-11-23 07:35:21 -04:00
hayodea 9f839df36a Docs: Document ambience stimbuff and high-val param 2025-11-23 07:34:59 -04:00
hayodea 601c7857f4 VSCode: don't display inline greyed out hints 2025-11-23 07:28:04 -04:00
hayodea 0c2a14434b livoxGen1:OCl:collate: cast comparison to float 2025-11-23 07:25:53 -04:00
hayodea ce690bc3f4 PcloudStimProducer,OClCollMeshEngn: Produce ambience stim feature
The collation kernel now also produces the ambience stim feature
values into the ambience stimbuff frames.
2025-11-23 07:20:55 -04:00
hayodea e689063a8c StimFrame: Store ringbuff index as member var
Now each StimFrame knows its index within its parent SpMcRingbuff
object.
2025-11-23 06:15:54 -04:00
hayodea f57236530d OClCollMeshEngn: print intensities from intensity stimframes 2025-11-23 06:07:37 -04:00
hayodea 79df8b3f74 OClCollMeshEngn,PcloudStimProd: Produce into intensity stimbuff
PcloudStimulusBuffer::produceFrameReq():
Now correctly produces into the stim frames for the
PcloudIntensityStimulusBuffer object that's attached to the
PcloudStimulusProducer. If there's no attached I stimbuff, then
the OpenCL kernel will simply not write out the intensity data.

This is the first moment when we actually use the SP-MC ringbuffer
properly and actually cycle through the frames, producing into
them one by one.
2025-11-23 05:57:20 -04:00
hayodea a025d13fce CMake: Add support got clangd in cursor 2025-11-23 04:05:18 -04:00
hayodea 2c891bd2f3 Mrntt: Re-add exceptionInd
This now ensures that finalizeReq is indeed called from mrntt,
since exception-experiencing threads will post an exceptionInd
to mrntt, which will then call finalizeReq.
2025-11-23 03:27:18 -04:00
hayodea 3747dee8a7 CPack: Target ubuntu; deb now works in gdebi 2025-11-20 22:18:05 -04:00
hayodea 9ce1ced92d PcloudStimBuff,IoUringAssmEngn: add frame assembly perf profiling
We now time the frame assembly sequence.
2025-11-20 03:26:43 -04:00
hayodea 9e64c510cc SpMcRingBuff: Add getNextIndexForProducer/abortProduction
These two methods form the core of the SpMcRingbuffer's wrap-around
behaviour.
2025-11-20 03:06:35 -04:00
hayodea 9d9644cb31 PCloudStimBuff: Call stop=>start in destroyAttachedStimBuff
This ensures that we can avoid races when adding and removing
stimbuffs to a stimproducer.

At least in theory. I can think of some ways in which this current
design may result in races or other bad conditions.
2025-11-20 02:18:25 -04:00
hayodea 1bf0a195aa PcloudStimBuff: call stop=>start when adding new StimBuff 2025-11-20 02:10:11 -04:00
hayodea e233dc51d6 OClCollMeshEngn: hide StagingBuffer's firstSlotOffset 2025-11-20 01:48:59 -04:00
hayodea 51d2a70a3f StimProducer: add destroyAttachedStimulusBuffer virtual method
Implemented in base class and in derived class
PcloudStimulusProducer.
2025-11-20 01:25:46 -04:00
hayodea ee6405048a OClCollMeshEngn: use the central ComputeMgr APIs
We no longer create our own context and get our own OpenCL device
in OClCollMeshEngn::setup. We now request a device from the central
ComputeManager.
2025-11-20 00:55:19 -04:00
hayodea 2c7e090ef1 Move ClBuffer/ComputeDevice methods into libattachmentSupport 2025-11-20 00:53:28 -04:00
hayodea 0cfb0a9c07 StagingBuffer: Large slots should be aligned to alignment
Slots whose stride size is larger than the slot alignment value
should have their size rounded up to the alignment size so that
the slots that follow them will also be aligned.
2025-11-20 00:03:50 -04:00
hayodea 5789a31e23 StagingBuffer: add OpenCL buff handles to StimFrames 2025-11-19 23:43:17 -04:00
hayodea 27b43c6686 Add ComputeManager; add SmoHooks for getting ClDevices, buffers
We added a new centralized OpenCL Compute manager. This can later
be extended to support CUDA, SyCL, etc. SMO can be configured at
build time to choose which API it will use for compute.

Moreover, the ComputeMgr allows us to register buffers which are
available to all cl_contexts.
2025-11-19 22:34:25 -04:00
hayodea a910909ad5 Tests: Add test for StagingBuffer 2025-11-19 03:12:43 -04:00
hayodea 41b8385cb2 StimBuff: Use a single StagingBuffer for all StimFrames
We now allocate all the stimFrames for a StimBuffer using a
single StagingBuffer. This gives us all the benefits we're
looking for (pinning, alignment, etc).
2025-11-19 03:11:20 -04:00
hayodea 3f04d1b387 Stimulus[Buffer|Frame]: initial impl, unoptimized for mem use 2025-11-16 16:09:35 -04:00
hayodea a4493b26a1 Move/RN computeNSlotsPerDgram to Device::getNSlotsPerDgram 2025-11-16 12:37:25 -04:00
hayodea a18fab04a5 livoxGen1:Add openClIntensityConstraints to prep for StimBuff impl 2025-11-16 04:54:13 -04:00
hayodea f919385088 DASpec:genericize synonymous param parsing 2025-11-16 04:46:42 -04:00
hayodea 3bcb83894b livoxGen1:PcloudStimProd: move qualeApi param parsing here 2025-11-16 04:38:25 -04:00
hayodea 44435c61eb Formatting 2025-11-16 02:40:37 -04:00
hayodea af5046c933 Devices: Avia0: add mesh, pcloudIntensity and pcloudAmbience qualeIfaceApis 2025-11-16 02:34:33 -04:00
hayodea c5ed453bb4 StimBuff: Make virtual so we can dynamic_cast in getOrCreateStimBuff 2025-11-16 02:23:53 -04:00
hayodea addd2e275d livoxGen1: Rn PcloudIStimulusBuffer=>PcloudIntensityStimulusBuffer 2025-11-16 00:31:47 -04:00
hayodea 336bc52a9d livoxGen1: Rename PcloudXyzStimulusBuffer=>MeshStimulusBuffer 2025-11-16 00:10:33 -04:00
hayodea c060463e82 Gitignore: ignore .tmp files 2025-11-16 00:03:18 -04:00
hayodea 1f7c7f5f28 livoxGen1: n-dgrams-per-frame default value 30=>84 2025-11-15 22:22:08 -04:00
hayodea bed10df499 livoxGen1: add n-dgrams-per-frame param 2025-11-15 22:12:48 -04:00
hayodea b3743560bb IoUringAssmEngn: detect assembly end condition w/eventfdDesc validity
We can simplify and universalize the logic here by acknowledging that
assemblyCycleComplete() will always destroy the current eventfdDesc
object, so we can just check that to see whether we should continue
the assembly cycle.
2025-11-15 22:02:30 -04:00
hayodea 8e48ce6ceb stagingBuffer: rename nDgramsPerFrame=>nSlots 2025-11-15 21:29:59 -04:00
hayodea d277c29394 Move StagingBuffer+FrameAssemblyDesc into libattachmentSupport
This is in preparation for using StagingBuffer to implement
StimulusFrame and StimulusBuffer.
2025-11-15 20:47:40 -04:00
hayodea 2d1c026cc2 livoxGen1: Add correctness checks to attachDeviceReq 2025-11-15 19:46:24 -04:00
hayodea 340604c4ea Avia: update qualeIface to mesh 2025-11-15 16:21:48 -04:00
hayodea 2632917c63 livoxGen1: Execute delays on lib's assigned CompThread 2025-11-15 15:59:34 -04:00
hayodea 7a51f02d97 livoxGen1: Implement StimBuff add/del from StimProducers
There seems to be a bug where two or more stimProducers
or stimBuffs get initialized at once but we can deal with that
tomorrow.
2025-11-15 04:02:25 -04:00
hayodea e215e78aa5 StimulusBuffer should take ref to parent; not sh_ptr to common instance 2025-11-15 01:15:57 -04:00
hayodea 188b09319c livoxProto1: Rn Device::nAttachedStimBuffs=>nAttachedStimulusProducers
More semantically precise name.
2025-11-15 00:56:20 -04:00
hayodea 475f67d36e Todo: update 2025-11-15 00:08:35 -04:00
hayodea 16b51a3b66 Rename PcloudDataProducer=>PcloudStimulusProducer 2025-11-14 23:50:31 -04:00
hayodea 7d86ecadc4 livoxGen1: Rn attachedDataProducers=>attachedStimulusProducers
Also compare producers only by device selector and not by the rest
of their stored DASpec.
2025-11-14 23:26:13 -04:00
hayodea 98a493a8a1 livoxGen1: Add StimBuffs to PcloudStimProd
* PcloudStimulusProducer now has member sh_ptr<StimulusBuffer>s.
* StimulusProducer now has a vector<sh_ptr<StimulusBuffer>s.

Created new stimbuff-type-specific
Pcloud[Xyz|I|Ambience]StimulusBuffer classes for representing each
stim feature exposed by livoxGen1's PcloudStimulusProducer.
2025-11-14 23:19:32 -04:00
hayodea 8a7dc10892 Split StimulusProducer=>StimulusBuffer+StimulusProducer
We're getting ready for the last mile of the StimulusBuffer API
and the proto-completion of the LivoxGen1 StimBuffApi.
2025-11-14 20:44:37 -04:00
hayodea 70c0175a8b Rename StimulusBuffer=>StimulusProducer
Next we'll split the StimulusBuffer-related stuff into a new class
StimulusBuffer.
2025-11-14 19:50:51 -04:00
hayodea 74e3896ae4 Rename PcloudStimulusBuffer=>PcloudDataProducer
This prepares us for the split up of classes. We're going to split
StimulusBuffer into two base classes: StimulusBuffer and
StimulusProducer.
2025-11-14 19:44:18 -04:00
hayodea 7b7ff06219 PcloudStimBuff:start: check engine setup()s for error 2025-11-14 18:07:20 -04:00
hayodea 51a2858214 OClCollMeshEngn:*KernelComplete: use WRITE_INVLDT during finalize()
Doing it this way enables us to get the mapBuffer() call working
during finalize. But we couldn't get the unmap call working. That
has to do with a bug in the Rusticl event waiting code.
2025-11-14 18:04:12 -04:00
hayodea 2e75dd40aa OClCollMeshEngn: Rearrange steps in startCollateKernel
Just to make it match startCompactKernel. No other reason.
2025-11-14 18:01:48 -04:00
hayodea c08e075763 Bug:Rusticl: segfault on waitForEvents(clEnqueueUnmapMemObject) in finalize
For some reason, waiting on the event object returned by
clEnqueueUnmapMemObject, but only when called from within finalize().
Under normal operating conditions when we map and then unmap our
HOST_PTR buffers, everything works just fine.

I can't discern any relevant difference.
Adding a bridged 300ms delay in setup() doesn't help either so it
doesn't seem to be solved by allowing the rusticl worker threads
to finish initializing.

GDB output from the segfault appended. Sadly, no debug symbols for
the ubuntu rusticl package.

```
[New Thread 0xffffdd2ce140 (LWP 1056313)]
validateOpenClVersion: OpenCL platform version: OpenCL 3.0
validateOpenClVersion: OpenCL device version: OpenCL 3.0
[New Thread 0xffffdcabe140 (LWP 1056314)]
[New Thread 0xffffc9a8f140 (LWP 1056315)]
start: Starting stimulus buffer for device 3JEDK380010Z39
attachDeviceReq3: Got return mode (0) for device: 3JEDK380010Z39
discardHeartbeatAck: Lidar not ready for operation: work_state: 0x0 (Initializing), ack_msg: 0x1b
discardHeartbeatAck: Lidar not ready for operation: work_state: 0x0 (Initializing), ack_msg: 0x45
discardHeartbeatAck: Lidar not ready for operation: work_state: 0x0 (Initializing), ack_msg: 0x45
discardHeartbeatAck: Lidar not ready for operation: work_state: 0x0 (Initializing), ack_msg: 0x45
attachDeviceReq5: Failed to enable pcloud data for dev 3JEDK380010Z39
newDeviceAttachmentSpecInd2: Attach failed for device spec Device Identifier: avia0, Sensor Type: e, QualeIface API: structural-qualeiface, QualeIface API Params: (), StimBuff API: livoxGen1, StimBuff API Params: (), Provider: livoxProto1, Provider Params: (smo-ip=10.42.0.2 ), Device Selector: 3JEDK380010Z39

attachAllUnattachedDevicesFromReq2: Failed to attach device: avia0
Mrntt: attached 0 of 2 sense devices.
Mrntt: Body component initialized.
initializeReq2: Failed to initialize globalMind
marionetteInitializeReqCb: Failed to initialize Marionette. Shutting down.
Mrntt: About to detach all sense devices.
Mrntt: Successfully detached 0 of 0 sense devices.
Mrntt: About to finalize all stim buff api libs.
compactKernelCompletecalling w/mapFlags=4. INV=4; READ=1.
mapBuffer 1
mapBuffer 2
mapBuffer 3: cmdQ: 0xffffec013d68, buffer: 0xffffec07b6b8, mapflags: 4
mapBuffer 4
mapBuffer 5
unmapBuffer 1
unmapBuffer 2
unmapBuffer 3
unmapBuffer 4
unmapBuffer 4.1
unmapBuffer 5

Thread 9 "rusticl queue t" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xffffdcabe140 (LWP 1056314)]
Download failed: Invalid argument.  Continuing without source file ./string/../sysdeps/aarch64/multiarch/../memcpy.S.
__memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:155
warning: 155    ../sysdeps/aarch64/multiarch/../memcpy.S: No such file or directory
(gdb)
(gdb) bt
(gdb)
```
2025-11-14 17:45:34 -04:00
hayodea 3995f57489 OClCollMeshEngn: call clFlush+clFinish after setup()
This ensures that all operations enqueued during setup() get fully
executed before any requests come in.
2025-11-14 17:42:46 -04:00
hayodea 0720ed9c76 StimBuff: Make produceFrameReq responsive to stop() 2025-11-14 02:25:51 -04:00
hayodea c268414b0d Fix comment 2025-11-14 02:08:03 -04:00
hayodea a1625eb562 OClCollMeshEngn: Use shouldAcceptRequests stop/finalize() pattern
This makes the stop() method capable of synchronously stopping all
engine/server-type async services which don't act in a self-moved
fashion but instead wait for a request.
2025-11-14 01:41:03 -04:00
hayodea 324e3d1f6a SpinLock: Add releasePrematurely for early releases 2025-11-14 01:38:06 -04:00
hayodea 39691175e7 Formatting 2025-11-14 01:03:58 -04:00
hayodea 1df43665c3 IoUringAssmEngn: Implement shouldAcceptRequests daemon/async control
We've reworked the synchronous control functions that govern the
async daemon and in-flight requests for this class. The
shouldAcceptRequests flag represents the readiness state of the
whole engine class. The in-flight async operations consult the
shouldAcceptRequests flag to determine whether they should return
early.

Now the stop() method is solely for setting the locked flag
shouldAcceptRequests=false.

The pair resetAndAssembleFrame()/assemblyCycleComplete manage the
per-assembly cycle state machine, and they don't need to set or
interfere with the shouldAcceptRequests flag.
2025-11-13 23:53:31 -04:00
hayodea 501effe6d5 IoUringAssmEngn: assemFrameReq: exit responsively on stop() 2025-11-13 21:00:26 -04:00
hayodea d01f06904a assembleFrameReq: fix bug where we don't CB before ret 2025-11-13 20:57:10 -04:00
hayodea 16a74a3eb0 IoUringAssmEngn,OClCollMeshEngn: start/stop aren't public iface
Placing these functions in the public section kind of conceptually
confuses the reader since start/stop are indeed public interface
members in StimulusBuffer -- but they're not in the member objects.
2025-11-13 20:54:54 -04:00
hayodea a17072c8d9 IoUringEngn:assembleFrameReq: Implement and use callOriginalCallback 2025-11-13 20:53:53 -04:00
hayodea 67923d5f86 VSCode: idk 2025-11-13 20:52:52 -04:00
hayodea 972d5fc9db AsyncLoop: Add setRemainingIterationsToFailure 2025-11-13 20:52:08 -04:00
hayodea 5c3debecf4 OClCollMeshEngn: fix mem leak in [un]mapBuffer() 2025-11-13 01:41:59 -04:00
hayodea e446d42b3c StimBuff: Deferral: print message at start and end; timestamp too 2025-11-13 01:09:30 -04:00
hayodea 63fa0be91a Document good explanations of ROS transforms
Although I don't think they're good for our project. We don't care
to map our standpoint to some external point/"frame". SMO retains
the FPoV without any external reference point.
2025-11-13 00:33:52 -04:00
hayodea 6d669ee8b2 Docs: document the pcloud vis research 2025-11-12 22:48:59 -04:00
hayodea d60fd98887 Rusticl: document mapping bug's cause: karolherbst
There's a bug in the Rusticl implementation of clEnqueueMapBuffer/
clEnqueueUnmapMemObject because karolherbst doesn't understand
how CL_MEM_USE_HOST_PTR works.
2025-11-12 20:44:42 -04:00
hayodea 5031b22a31 OClCollMeshEngn: use helper fns for parsing version numbers 2025-11-12 20:43:48 -04:00
hayodea df58f324a9 CMake:LivoxGen1: Require OpenCL 1.2+, printf & WRITE_INVALIDATE_REGION 2025-11-12 20:26:29 -04:00
hayodea 7e672bcc9a UdpCmdDemux: formatting 2025-11-12 17:25:55 -04:00
hayodea 371ae5803d Document bugs in OClCollMeshEngn 2025-11-12 16:20:58 -04:00
hayodea 1a9c96c857 whitespace 2025-11-12 15:09:15 -04:00
hayodea 116a642a9f StimBuff: Add opportunity for early lock release 2025-11-12 15:08:44 -04:00
hayodea d87c71b794 OClCollMeshEngn: perf profile and print kernel exec durations 2025-11-12 13:05:13 -04:00
hayodea 33b534355a OpenCL minimum version is 1.2
We use CL_MAP_WRITE_INVALIDATE, and I think one other feature which
both require v1.2 minimum
2025-11-12 13:05:13 -04:00
latentprion 96e64e24b8 OClCollMeshEngn: collBuff only needs MAP_WRITE; silence dbg prints
When mapping in the collationBuff we only need to supply CL_MAP_WRITE
and not CL_MAP_WRITE_INVALIDATE_REGION since we don't care to
preserve the contents of the collation buff as input to the
collation kernel.
2025-11-12 12:49:54 -04:00
hayodea 1dc74065fb OClCollMeshEngn: cleanup and get it working on RPi5+Rusticl+V3D GPU
It seems that whenever you have an HOST_PTR input buffer to be
"transferred" from the host to the GPU, whose contents must be
preserved, you must map it with WRITE_INVALIDATE_REGION on the
RPi5.

This makes little sense, but we'll have to let it be for now.
At least the code works now and we don't have to abandon using
OpenCL.
2025-11-12 12:36:41 -04:00
hayodea d687ca0164 PcloudStimBuff: remove printf clutter 2025-11-12 12:34:30 -04:00
hayodea 91e0fd0f8e IoUringAssmEngn: Disable debugging for compact kernel results 2025-11-12 12:33:38 -04:00
hayodea 4dbb27fd1f StimulusBuffer: properly serialize timeslices
We previously unintentionally allowed multiple production operations
to occur in the same timeslice because we were calling for production
even when deferring timeslices.
2025-11-12 12:31:37 -04:00
hayodea b55e7a8b19 livoxGen1:OpenCL kernels: add debug printfs 2025-11-12 12:30:41 -04:00
hayodea f58f908366 OpenCL checks: Add check for the need to clFlush kernel cmds 2025-11-12 12:29:19 -04:00
hayodea a52685fbdf New bug documented 2025-11-12 12:28:33 -04:00
hayodea 5bb9c9e90b Dbg: Useful printfs for the raspi5 2025-11-10 01:05:20 -04:00
hayodea 401c844fcc PcloudStimBuff: add skeleton produceFrameReq :)
Big waves.
This function wraps the operation of getting a stimframe from
the SpMcRingBuffer, and then eventually assigning it a
SimultaneityStamp. For now we just always pass in the first
stim frame and we don't get any simulstamps.

Its callOriginalCallback() automatically calls
allowNextStimulusFrame() to ensure that it doesn't deadlock future
timeslices.
2025-11-10 01:04:07 -04:00
hayodea eedeb4b803 OClCollMeshEngn: Add method compactCollateAndMeshFrameReq
This method takes an input assembly buffer and selects which
OpenCL kernels need to be executed on that buffer to transform
the input data into the eventual output StimulusFrame for the
current timeslice period.
2025-11-10 00:58:48 -04:00
hayodea 19a79faabe OClCollMeshEngn: stop now just calls stop*Kernel 2025-11-10 00:54:41 -04:00
hayodea 1ac6fa4a16 Rename StimFrame=>StimulusFrame 2025-11-09 22:09:19 -04:00
hayodea 7cae3452fc OClMeshCollEngn: temporarily call stop in CL cbs 2025-11-09 20:23:14 -04:00
hayodea 582aefb02c OClEngn: Split isSetup/Running into collate+compact 2025-11-09 19:58:45 -04:00
hayodea aef251b7e5 IoUringEngn: add random dummy slot generator for debugging 2025-11-09 19:34:02 -04:00
hayodea ad0b8058a4 ClCollMeshEngn: big reworks to clean up. 2025-11-09 19:28:55 -04:00
hayodea b331af4f03 ClCollMeshEngn: Split start into start[Collate|Compact]Kernel()
These prepare each kernel separately. We'll unify them further.
2025-11-09 16:12:10 -04:00
hayodea 683e107b04 livoxG1:OClCollMeshEngn: Wrestling and massaging 2025-11-09 15:18:53 -04:00
hayodea c8cbaed3b1 OClCollAndMeshEngn: formatting 2025-11-09 12:37:30 -04:00
hayodea 5f03e4c392 livoxG1:collateDgrams.cl: Clarify collation offsetting 2025-11-09 12:12:08 -04:00
hayodea 55116b1d41 livoxG1:collateDgrams.cl: Fix unaligned reads 2025-11-09 11:48:53 -04:00
hayodea 7977f0bcc9 OClCollatingMeshingEngn: Compile both kernels side by side 2025-11-09 04:49:37 -04:00
hayodea 6264a128a8 livoxG1: Add point cloud frame collator OpenCL kernel 2025-11-09 04:48:15 -04:00
hayodea 4b60a10bc6 VSCode config: idk 2025-11-09 04:46:00 -04:00
hayodea 01ba68f2b5 livoxG1:OCLEngine: compile compactor program 2025-11-09 03:44:56 -04:00
hayodea 511f1796e8 livoxG1:slotCompactor.cl: mental-validate and refactor 2025-11-09 03:40:46 -04:00
hayodea a0a5aa49ad livoxG1: Add new OpenCl kernel to compact dgrams before collation 2025-11-09 02:39:09 -04:00
hayodea d2e2d9bc3b StagingBuffer: Prefer mlock to io_uring_register_buffers 2025-11-09 01:16:17 -04:00
hayodea 010ba9c7bd Bugfix,IoUringEngn: fill unassembled slots w/dummy; use separate iovecs
We implemented the feature to fill unassembled slots w/dummy header
values for the livox pcloud header.

We also fixed a bug where io uring was writing into the last slot
only because we were using the same iovec for every SQE.
2025-11-09 00:55:58 -04:00
hayodea 72a3415553 Bugfix: Don't use eventfdDesc after stop()
We call stop() inside the assembleFrameReq3, so when it returns,
the eventfdDesc should be destroyed. Don't allow a possibly stale
eventfdDesc obj to permit us to re-arm the eventfdDesc read_some
call.
2025-11-08 23:09:14 -04:00
hayodea a0ab5538df StimBuff: Add mnemonic wrapper for unlocking frameAssmLimiter 2025-11-08 22:07:52 -04:00
hayodea 5b7b4f215a IoUringAssmEngine: Acquire spinlock in stall timeout branch 2025-11-08 21:54:11 -04:00
hayodea d8a3999ad5 PcloudStimBuff: call OClCollMessEngn::setup/finalize in start/stop 2025-11-08 12:23:13 -04:00
hayodea 5ff6a4ee0b OClCollMeshEngn: implement start/stop/setup/finalize 2025-11-08 12:23:13 -04:00
hayodea 6a5bb47e0e PcloudStimBuff: Add OpenClCollatingAndMeshingEngine instance 2025-11-08 12:23:10 -04:00
hayodea 073cdde08f livoxG1: StagingBuff: add getClEngineIovec 2025-11-08 12:18:55 -04:00
hayodea 869160b782 Add bugs.log for heisenbugs 2025-11-08 11:29:27 -04:00
hayodea e1042724fc livoxGen1: nitpicking: use .-prefixed symbol for end 2025-11-08 11:11:05 -04:00
hayodea 28e56653ea livoxGen1: unmangle symbols, add .sizes 2025-11-08 11:09:09 -04:00
hayodea 5dbed56e38 livoxG1: Make collateKernelNBytes a uint32_t for 32bit portability 2025-11-08 10:59:08 -04:00
hayodea 9233f7fdc8 livoxG1: Add OpenCl kernels for collation 2025-11-08 10:26:17 -04:00
hayodea b460c8b2d3 CLTests: Add test for USE_HOST_PTR; fix build warnings 2025-11-08 02:07:43 -04:00
hayodea bc56c83fad Rename: OpenGlSplittingEngine=>OpenGlCollatingAndMeshingEngine 2025-11-08 01:48:56 -04:00
hayodea cb493d7598 StagingBuff: set OpenCL constraints 2025-11-08 01:45:47 -04:00
hayodea 1c50fc0e29 StagingBuff: Move constructor into .cpp file 2025-11-08 00:21:24 -04:00
hayodea 7497f2fd95 StagingBuff: Enhance IoConstraints with frame constraints
Now StagingBuff instances must meed both frame and slot
constraints.
2025-11-08 00:15:29 -04:00
hayodea 5f11a9d6c7 VSCode: Highlight OpenCL C files as C 2025-11-07 23:05:49 -04:00
hayodea 0b21cdd2ba OClSplitEngn: fix build warnings 2025-11-07 22:20:44 -04:00
hayodea f5146738e1 PcloudStimBuff: Add collationBuffer 2025-11-07 22:07:27 -04:00
hayodea 479219db2d StagingBuff: Unify constraints into IOEngineConstraints 2025-11-07 22:05:01 -04:00
hayodea 1afa085fd4 livoxProto1:Device: Spinlock guard heartbeat stop() for races 2025-11-07 21:36:00 -04:00
hayodea 7b092956c0 UdpCmdDemux: Guard start() w/spinlock for races 2025-11-07 21:12:40 -04:00
hayodea e0c0976e0b BcastListener: Guard start() w/spinlock for races 2025-11-07 21:12:05 -04:00
hayodea 887fa1ab6f Bug:UdpCmdDemux: Add SpinLock for races around stop() 2025-11-07 20:45:16 -04:00
hayodea 7d2cb58200 Bug:BcastListener: Add SpinLock for races around stop() 2025-11-07 20:44:44 -04:00
hayodea b598ca8594 libs: Add smohook for getting cmdline opts 2025-11-07 14:59:28 -04:00
hayodea a7a85b0c1f CMake:Boost: Try -DBOOST_ALL_DYN_LINK (didn't work) 2025-11-06 21:55:15 -04:00
hayodea 457d0f9345 Dbg:Add CallableTracer for callables post()ed to boost.asio
This class and its macro allow us to trace the invocation of
callbacks as they're invoked by Boost.asio.
2025-11-06 21:45:16 -04:00
hayodea eeb057effd Dbg:Threading: prefix thread names with "smo:" so they group 2025-11-06 15:09:15 -04:00
hayodea c7e117b08e Dbg:Threading: use pthread_setname_np for debugging ease 2025-11-06 15:04:04 -04:00
hayodea af57c4dfd1 Boost: move top_ link fixer to top of files 2025-11-06 15:03:26 -04:00
hayodea db30001140 livoxG1: Rename stagingBuffer=>assemblyBuffer
This is in preparation for re-using StagingBuffer to also serve
as the collation buffer that we'll use as the intermediate stage
for producing the final output mesh.
2025-11-06 14:09:10 -04:00
latentprion d69636bf7b IoUringAssmEngn: destroy prev cb obj
This should ensure that the sh_ptr<contin> from the prev object
gets destroyed properly.
2025-11-06 09:10:44 -04:00
hayodea 59a584561d CMake:livoxGen1: advise on libOpenCL.so ubuntu pkg name 2025-11-06 09:09:51 -04:00
hayodea f7aba4af4e livoxGen1: find OpenCL using both CMake & pkgConfig 2025-11-06 09:02:05 -04:00
latentprion 21bbaf846e Todo: update 2025-11-06 01:50:03 -04:00
hayodea aacbdd5864 livoxGen1: Add OpenClSplittingEngine 2025-11-06 01:20:02 -04:00
hayodea bb59f47549 IoUringAssmEngn: add assembleFrameReq
Invoke it instimFrameProductionTimesliceInd.

Also, we discovered:
* stream_descriptor::release() doesn't fully release all metadata
  from the fd it was assigned. This suggests that we should go
  through the codebase and do: release()=>reset() whenever we
  wish to release().
* We've confirmed that spinlocks can be used to prevent race
  conditions between stop() and handler methods.
2025-11-06 01:04:10 -04:00
hayodea 1c7277d141 SpinLock: add RAII Guard class 2025-11-05 22:38:04 -04:00
hayodea d29ebafea0 IoUringAssmEngine: Add start/stop() for frame assembly.
With these two functions implemented, this class now actually
assembles frames.
2025-11-05 19:23:30 -04:00
hayodea 94982d50b9 IoUringAssmEngn: map StagingBuff w/mmap; reg w/io_uring; add eventFd
StagingBuffer:
We now allocate memory with mmap(MAP_ANONYMOUS) so that we can be
sure it can be pinned with io_uring_register_buffers(). This
ensures that if DMA is possible, it should be usable.

	IoUringAssemblyEngine:
We now register an eventfd with io_uring so that we can listen
for CQEs with boost::asio.
2025-11-05 15:34:23 -04:00
hayodea 0503705a13 PcloudStimBuff: invoke IOUringAsmEngn::setup/stop in start() 2025-11-04 15:33:55 -04:00
hayodea ef9eef2bc3 PCloudStimBuff: Add useful dbg msg for thread affinity rigour 2025-11-04 00:56:46 -04:00
hayodea 2b3b318abe Formatting 2025-11-04 00:49:15 -04:00
hayodea f3a4c69597 StimBuff: DevAttSpec should be a sh_ptr, not a plain ref 2025-11-04 00:47:01 -04:00
hayodea 032e9ef8d5 DevMgt: Update comments on bg daemon thread selection 2025-11-04 00:33:43 -04:00
hayodea 4a1bcb1516 TODO: update 2025-11-04 00:32:39 -04:00
hayodea d6e1e7ebc0 DevMgmt: StimBuffApi bg tasks should execute on body thread 2025-11-04 00:32:14 -04:00
hayodea 9a4f80a9d6 libAttachmentSupport: convert into shared lib
This ensures that the support routines in this library will be
exposed at the same vaddr to each object in the vaddrspace.
2025-11-04 00:22:25 -04:00
hayodea 7a55a65589 LivoxProto1: Rename pcloudDataSocketDesc=>pcloudDataFdDesc 2025-11-03 23:22:50 -04:00
hayodea 14b97a52ed IoUringAsmEngn: Use pcloudDataSocket from UdpCmdDemux 2025-11-03 23:10:24 -04:00
hayodea 5845f1a41d Bug:Boost: Use shlibs instead of header-only for call_stack::top_
This symbol is defined as a static member object inside of a
boost detail header. When boost headers are used in a project
that uses Boost in both the main binary as well as dlopen()'d
shlibs, the top_ symbol gets duplicated and the metadata gets
partitioned.

We use the Boost shlib to unify both the main binary and the
shlibs to use the same memory address for top_.

This involves marking the templated object call_stack::top_ as
"extern" and then declaring to Boost that we intend to use the
shlibs.
2025-11-03 22:59:52 -04:00
hayodea 6ea90c2dae Squash: into "make use of pcloudDataFdDesc" 2025-11-03 22:12:43 -04:00
hayodea 121b7db045 CMake:attachmentSupport: Don't link against Boost_LIBRARIES 2025-11-03 21:09:55 -04:00
hayodea d88dd2cf44 livoxGen1: Make use of livoxProto1_getPcloudDataFdDesc 2025-11-02 19:16:22 -04:00
hayodea b3bf0e2cb9 StimBuff,commonLibs: Add libattachmentSupport, move fnptrs into .cpp files
We move the methods in StimulusBuffer whose addresses are taken during
program execution into a separate static lib. This guarantees that
they'll have their own, single vaddr at runtime, at least within
each independent code module.
2025-11-02 19:12:28 -04:00
hayodea 45ad5c83ee livoxProto1: Open pcloudDataSocket in UdpCmdDemux
The pcloudData socket is now opened by UdpCommandDemuxer, when
libLivoxProto1 is initialized. We can now just pick up the socket
and be certain it'll be there if the lib is being executed.
2025-11-01 22:41:58 -04:00
hayodea 10e615e75e StimBuff: make start/stop virtual;
We can now have the derived StimBuff class implement its own
start()/stop() preamble and epilogue.
2025-11-01 22:05:52 -04:00
hayodea 05515743c5 Put continuation near frontend func 2025-11-01 21:33:35 -04:00
hayodea b2c73f6bed IoUringAssmEngine: Add skeleton setup/finalize
Also add dependency on liburing.
This patch adds basic io_uring_queue_init and io_uring_exit
support and calls.
2025-11-01 21:30:47 -04:00
hayodea 797a95e6a1 IoUringAssmEngine: formatting/indentation 2025-11-01 20:21:49 -04:00
hayodea 972979cc10 IoUringAssmEngine: Remove dead wood 2025-11-01 20:18:05 -04:00
hayodea ba955ef633 PcloudStimBuff: Add skeleton assembleAndProduceStimulusFrameReq impl 2025-11-01 04:14:07 -04:00
hayodea a32b4f05d1 livoxGen1: call PcloudStimulusBuffer::start/stop
We now call start()/stop() in attach/detachDeviceReq.
2025-11-01 03:32:05 -04:00
hayodea c8474edad7 livox: rename handshakeTimeoutMs=>commandTimeoutMs 2025-11-01 02:45:24 -04:00
hayodea 58e9b09995 livoxGen1: Use RAII to close dlopen handle 2025-11-01 01:54:49 -04:00
hayodea c2c6d409dd DAPSpec: QualeIfaceApiParams: Add histbuffMs 2025-11-01 01:35:29 -04:00
hayodea 8dba0fdfc4 Todo: update 2025-11-01 01:26:51 -04:00
hayodea 67af9f02da DAPSpecs: Update parseRequiredParamAsInt to support all param lists 2025-11-01 01:11:34 -04:00
hayodea e824685c19 DAPSpec: Add params to quale-iface-api
We add params to the quale iface (soon to be renamed to cologex-api).
This allows us to eventually set the history length for stimbuffs.
2025-11-01 00:57:04 -04:00
hayodea 9cf1398f5c PcloudStimBuff: specify that openCl constraints are for input 2025-11-01 00:19:06 -04:00
hayodea f76f718e80 PcloudStimBuff: make OpenCL constraints explicit 2025-11-01 00:17:10 -04:00
hayodea cdade17905 Add SpMcRingBuffer to base class StimulusBuffer
This will hopefully genericise the interface for Stimbuffs.
2025-11-01 00:09:53 -04:00
hayodea 5af7e531b6 Style: use modern C++ style 2025-11-01 00:05:43 -04:00
hayodea 018c1f1e1d SpMcRingBuffer: Added this new class
This will be the foundation for all StimulusBuffers. We can most
likely add this generically to the StimulusBuffer base class rather
than adding it only to StimulusBuffer's derived classes.
2025-10-31 22:58:18 -04:00
hayodea 5e522178d8 SequenceLock: Newly added
What can I say? It's a very portable primitive that allows us to
create many RCU-type data structures with many readers and one
writer, locklessly.
2025-10-31 22:12:23 -04:00
hayodea 7574f3f59a StimBuff: customize stop() delay; add stimFrameProductionTimesliceInd 2025-10-31 13:54:50 -04:00
hayodea 0de031c74b Formatting: move big inlines out of class def 2025-10-31 13:47:00 -04:00
hayodea ebbb2b1345 StimBuff: Add skeleton common impl with rate limiting 2025-10-31 13:43:23 -04:00
hayodea 3bf8146ca3 CMake: STIMBUFF_FRAME_RETRY_PERIOD_MS 3=>1 2025-10-31 12:34:34 -04:00
hayodea f32a472c5d PcloudStimBuff: Add skeleton daemon; frame rate limiting
Basic implementation of the stimbuff mechanism's frame rate
limiting behaviour.
2025-10-31 12:22:07 -04:00
hayodea 7994c2f6e2 CMake,config.h: Add -DSTIMBUFF_FRAME_RETRY_DELAY_MS
This determines how long a stimbuff should wait before retrying
to produce a stimframe, if the previous stimframe is still being
produced.
2025-10-31 12:08:40 -04:00
hayodea 9ab155560a livoxGen1:ioUringAssmEngine: Fix build 2025-10-31 11:49:56 -04:00
hayodea 720babd39d CMake,config.h: Add -DSTIMBUFF_FRAME_PERIOD_MS
This determines the maximum rate at which stimbuffs will be refreshed
with data from their device.
A device may refresh less frequently than this, but not more
frequently. The goal here is to give us control over the max
rate at which a device produces data.
2025-10-31 11:31:04 -04:00
hayodea 5c3bc6c324 livoxGen1: Reduce nDgramsPerFrame 500=>30 ~86400 points/sec 2025-10-31 11:27:43 -04:00
hayodea b53ef42124 livoxGen1: documentation and formatting 2025-10-31 08:57:37 -04:00
hayodea babfda4d0f livoxGen1: Reduce handshakeTimeoutMs default to 5ms
We have no real reason to set it as high as 300.
2025-10-31 08:57:37 -04:00
hayodea 88dd872c95 Todo: Add async bridging idea as sync wait mechanism 2025-10-31 08:57:37 -04:00
hayodea b8255234de Todo: add idea for dealing with late timeouts 2025-10-31 08:57:37 -04:00
hayodea 1a4f7f97bd BUG: Late-timeout during finalize
This adds a script which reproduces this bug after a lot of
iterations in gdb and lets us get a backtrace
2025-10-31 08:57:37 -04:00
hayodea 13a948a2d3 Formatting: use early continue pattern 2025-10-31 08:57:37 -04:00
hayodea 07c48d78d1 Todo: update comments 2025-10-31 08:57:37 -04:00
hayodea 7b6bfbad68 Fix linker error 2025-10-31 08:57:37 -04:00
hayodea 393326052c Todo: update and add comments to finalizeInd 2025-10-31 08:57:37 -04:00
hayodea b3d0565e11 livoxGen1: Committing intermediate state before daemon design 2025-10-31 08:57:37 -04:00
hayodea 287dd6be56 livoxGen1: Fix include name 2025-10-31 08:57:37 -04:00
hayodea 0b2fde3484 livoxGen1:StaginBuffer: simplify buff size/stride calcs 2025-10-31 08:57:37 -04:00
hayodea c1286627ab LivoxGen1: attachDevReq sets nDgramsPerFrame 2025-10-31 08:57:37 -04:00
hayodea 2234df1de2 livoxGen1:attach: alloc PcloudStimBuff after getting return mode 2025-10-31 08:57:37 -04:00
hayodea 4db3581be9 VSCode config: idk 2025-10-31 08:57:37 -04:00
hayodea 7efe622dd2 livoxGen1:attach: call getReturnModeReq before enPcloudDataReq 2025-10-31 08:57:37 -04:00
hayodea f658e97ed0 livoxProto1: export getReturnModeReq; cache result in Device 2025-10-31 08:57:37 -04:00
hayodea f8c5fad841 AssemblyBuffer changes 2025-10-31 08:57:37 -04:00
hayodea 626a84cc78 Formatting 2025-10-31 08:57:37 -04:00
hayodea a68143810e DeviceReattacher: use provided ioThread; not mrntt directly 2025-10-31 08:57:22 -04:00
hayodea 109cd9eb03 DevReattacher: Add 20ms sync delay during stop()
This fixes the bug where in-flight async ops that were triggered
by DevReattacher (such as livoxGen1_attachDeviceReq) that have
unconditional delays in them would fire late, after the state
var they rely upon had already been de-initialized.

We use an async bridge to ensure that those ops get executed in
the background and then resume execution after the delay.
2025-10-31 08:20:33 -04:00
hayodea 7b830f0a68 AsyncBridge: Comment why we don't checkException in loop 2025-10-31 08:20:29 -04:00
hayodea b65b0f2370 UdpCmdDemux: remove stop-"responsiveness" timer
I think it's best to remove the timer tick from UdpCommandDemuxer.
I looked at it again and it doesn't actually help with responsiveness.
Whatever it contributes is no different from what stop() does.
They both just call timer.cancel and cmdsocket.cancel.
So if that doesn't stop the socket in stop(), it won't magically
stop it more effectively if I call it from a timer handler.
2025-10-31 08:20:27 -04:00
hayodea 2a8a6edf22 LivoxGen1: Add basic stimbuff creation & destruction 2025-10-25 23:04:59 -04:00
hayodea 10e19a3237 StimulusBuffer: Fix linker errors 2025-10-25 23:04:28 -04:00
hayodea 9e83a99c9c LivoxProto1:Device: Refcount num stimbuffs attached 2025-10-25 23:03:00 -04:00
hayodea b576d41595 VSCode config 2025-10-25 23:02:08 -04:00
hayodea b89c8cdc4f More work on PcloudStimulusBuffer 2025-10-25 19:42:48 -04:00
hayodea 682b9e121b Some initial notes on postrin path design 2025-10-25 19:35:53 -04:00
hayodea bcf81594e7 Add PcloudFormatDesc 2025-10-25 19:32:10 -04:00
hayodea 1b9acd5603 Rename LidarStimulusBuffer=>PcloudStimulusBuffer 2025-10-25 19:28:18 -04:00
hayodea dc23a61410 Add StimulusBuffer and LidarStimulusBuffer 2025-10-25 19:05:32 -04:00
hayodea e9b4e15b79 Add base class StimulusBuffer 2025-10-25 18:56:30 -04:00
hayodea fca665d44e LivoxGen1: StagingBuff: Update iface 2025-10-25 15:15:52 -04:00
hayodea 862acf0fe3 LivoxGen1: Add StagingBuffer class. 2025-10-25 14:49:24 -04:00
hayodea 6650664529 LivoxProto1: Move data members to one place 2025-10-25 14:43:51 -04:00
hayodea e297a260d9 Contin: Rename Exception propagation macros 2025-10-25 14:12:12 -04:00
latentprion d54ef04c47 Todo: Update 2025-10-25 13:25:14 -04:00
hayodea 92399ba283 Use piecemeal boost headers 2025-10-25 12:55:19 -04:00
hayodea 6f4a2dd649 LivoxGen/Proto1: Move en/disablePcloudData call to Gen1
We no longer try to enable pcloud data as part of the connectReq()
sequence. Instead we separate them so that a device can be connected
but not be issuing pcloud data.
2025-10-25 12:55:19 -04:00
latentprion 0e872042ee Add some compile-time CL utilities 2025-10-25 03:39:42 -04:00
hayodea 266cabcddb LivoxGen1: Add get/setReturnModeReq() 2025-10-25 00:19:06 -04:00
hayodea 444555e9b6 LivoxProto1: Remove superfluous proto methods 2025-10-24 22:23:59 -04:00
hayodea 3373393755 LivoxGen1: Add Get/SetReturn rate proto headers 2025-10-24 22:11:31 -04:00
latentprion dc864bad00 Todo: update with optimization ideas 2025-10-24 16:03:03 -04:00
hayodea 452d1966fc LivoxProto1: Document UDP demuxer purpose & use cases
Also fix bug in transient resource transfer from continuation
to Device object.
2025-10-24 03:09:17 -04:00
hayodea f7dcb7307d LivoxProto1: registerUdpHandler: enforce overwriting
Enforce handler overwriting in the devicesUnderConstruction
collection, for combos of cmd_set+cmd_id that pre-exist the
current invocation
2025-10-24 02:42:50 -04:00
hayodea bd0118531f LivoxProto1: Pcloud socket is set up transiently in contin 2025-10-24 02:05:32 -04:00
hayodea 4bfcdf37da LivoxProto1:enPcloudData: reorder success case here 2025-10-24 01:46:08 -04:00
hayodea 2b8f6b6ad5 Formatting 2025-10-24 01:45:03 -04:00
hayodea a5cf996ed2 LivoxProto1: Get rid of raw FD for pcloudData recv 2025-10-24 01:44:19 -04:00
hayodea 71c2b855ec LivoxProto1: Device: misc logging, cleanup, bugfix 2025-10-24 01:29:18 -04:00
hayodea 06c5f4503f Todo: update 2025-10-24 01:11:19 -04:00
hayodea bede123691 LivoxProto1: Print when Lidar isn't ready for work 2025-10-24 00:51:28 -04:00
hayodea 83c937ae8f LivoxProto1: Cancel async contin branch cbs in oracle fn
We were canceling the registered handler CBs in their branch
segments instead of in the unifying oracle fn.
This caused a bug where handlers were left behind in the
udpCmdDemuxer and thus a handler was invoked twice when a UDP
msg came in.
2025-10-24 00:46:13 -04:00
hayodea d39dfb5334 livoxProto1: connectReq: Add 5ms delay before Sample enable msg 2025-10-23 01:27:35 -04:00
hayodea 44cfd7ab69 LivoxProto1: Add 105ms delay before Sample msg 2025-10-23 01:06:34 -04:00
hayodea b277baa76d Whitespace 2025-10-23 00:24:57 -04:00
hayodea 5db1cfdac8 LivoxProto1: Pcloud data stream now working 2025-10-23 00:24:23 -04:00
hayodea a4d99e5d4d LivoxProto1: ExecuteHandshake uses udpCommandDemuxer
UdpCommandDemuxer also now supports devices "under construction".
2025-10-22 22:13:38 -04:00
hayodea 01ad1ff073 LivoxProto1: udpCmdDemux: use piecemeal boost.asio headers 2025-10-22 13:37:54 -04:00
hayodea 8e1d609ca1 livoxProto1: udpCmdDemux now consults per-device handler registry
Device class objects now have a per-Device unordered_map of handlers
keyed by cmd_set+cmd_id.
2025-10-22 07:28:00 -04:00
hayodea 10afec2532 LivoxProto1: Add UdpCommandDemuxer.
We haven't genericized it with an unordered_map or integrated it
into device.cpp's async methods yet.
2025-10-22 06:17:42 -04:00
hayodea 66a9db13c3 LivoxProto1: invoke enablePcloudDataReq
Sadly we don't get to immediately see the results of our
work because we have to do a unified dispatcher for the incoming
UDP messages on the command channel.
2025-10-22 01:59:04 -04:00
hayodea d9042c6510 LivoxProto1: Add en/disablePcloudDataReq()
Untested, but this should enable us to enable and disable data
from the device.
2025-10-22 01:21:35 -04:00
hayodea 870057a680 Add Stencil class for describing threshold ranges 2025-10-21 20:50:59 -04:00
hayodea e444cd1e04 Move files around for cologex and sitbuff lib impl 2025-10-21 20:02:36 -04:00
hayodea 6bc5bd30d5 Rename: ChronoFrame=>PhenoFrame and ChronoSeq=>PhenoSeq 2025-10-21 19:38:49 -04:00
hayodea 56367402d7 Move intrinEventInd()s from MarionetteComponent=>Director 2025-10-21 19:38:49 -04:00
hayodea 035accf553 Rename: painfulQuale.cpp=>negtrinEvent.cpp 2025-10-21 19:38:49 -04:00
hayodea 55f21c5436 Add skeleton negtrinEventInd, build painfulQuale.cpp again 2025-10-21 19:38:49 -04:00
hayodea d1b99852a8 Move MarionetteComponent out of component.h=>marionette.h 2025-10-21 19:38:49 -04:00
hayodea 66bb30cef5 Move painfulQuale.cpp into marionette 2025-10-21 19:38:49 -04:00
hayodea 068a885bff Goal: now is a MentalEntity 2025-10-21 19:38:49 -04:00
latentprion 7d453beb65 Remove superfluous #includes from headers 2025-10-18 20:16:29 -04:00
latentprion 49c9caa317 Remove superfluous #includes from headers 2025-10-17 16:19:25 -04:00
latentprion b06e9693c5 CMake: toolchain file updates 2025-10-17 14:07:30 -04:00
latentprion e4e700c362 Changes to CMake toolchain files
We still haven't successfully xcompiled, but we're working
toward it.
2025-10-17 12:43:06 -04:00
latentprion d317f1fb06 Whitespace 2025-10-16 03:41:22 -04:00
latentprion edd223b083 Remove unnecessary componentThread.h #includes
Since componentThread #includes boost::asio components, removing
it should improve compile times.
2025-10-16 02:15:16 -04:00
latentprion 945c5b397b Build: remove superfluous io_service.hpp #includes 2025-10-16 01:08:05 -04:00
latentprion 5017bf5f92 Build: use piecemeal boost headers.
Reduces compile time on a 4-core ARM Rpi5 from 1m12s wallclock
(i.e: "real") time to 0m55s; and from 4m+ "user" time to 3m8s.
2025-10-16 01:00:48 -04:00
hayodea 95d5c46e43 Rename: CologexSet=>CologexSeq to be consistent with chronomena 2025-10-05 21:44:12 -04:00
hayodea 4a55ff9bf2 Revamp Chronomenon: ChronoFrame and ChronoSeq are new classes
Chronomenon is now the base class category for stored raw
stim data.
2025-10-05 21:44:12 -04:00
hayodea 27ff4a3a0a Concept,Cologex,Goal: refactor headers
All Cologexes (both Cologexes and CologexSets) are now categorized
as Concepts.
Goals are now also Concepts -- they inherit from Concept as a base
class.

Using Concept as the base class for both Cologex and CologexSet
allows us to treat both cologexes and cologexsets the same way
when comparing in the abstract.
2025-10-05 21:44:12 -04:00
hayodea eddee05e41 Delete: qualeBundle.h 2025-10-05 21:44:12 -04:00
hayodea ccc7fd8e04 Rename: Concept=>Cologex 2025-10-05 21:43:46 -04:00
hayodea eb810e62e9 Todo: update 2025-10-04 14:52:25 -04:00
hayodea 3a50be05f8 Qutex: nRequiredLocks==1 at front should never call backoff 2025-10-04 14:52:02 -04:00
latentprion 168d8d616c Todo: update 2025-10-04 11:16:04 -04:00
latentprion 16775c6f1e Todo: update 2025-10-04 10:42:39 -04:00
hayodea 385b7d5a3c Todo: Investigate MWait to reduce spinlock power usage 2025-10-02 20:19:54 -04:00
hayodea d857999fdf Update comment notes 2025-10-02 01:14:04 -04:00
hayodea fa2609f4ce User /includes: Add common stim frame types header 2025-10-02 00:29:09 -04:00
hayodea a91a995407 Config.h.in: Remove these "legacy" artifacts 2025-10-01 20:04:21 -04:00
hayodea eb5875fe0d Rename: Sense API => Stim Buff API 2025-10-01 20:03:47 -04:00
hayodea 56b8e83a09 Update senseApiDesc.h and opts.cpp for clarity in device attachment and usage examples 2025-10-01 18:20:59 -04:00
hayodea a66d91fa31 DevAttachment:Rename: api=>stimbuffapi, implexor=>qualeiface 2025-10-01 18:10:58 -04:00
hayodea b69572eee7 Update livoxGen1 stimbuffapi 2025-10-01 14:13:27 -04:00
hayodea b771856330 Update docs on DAP specs and DA specs 2025-10-01 13:27:37 -04:00
hayodea c7ca889e9c Rename DASpec.md=>deviceAttachmentPipelineSpec.md 2025-10-01 13:15:11 -04:00
hayodea da1ca774e7 Version: Bump version to v0.01.000 2025-10-01 11:30:19 -04:00
hayodea abc11bc7fc LivoxGen1: Remove obsolete locking comment
We have now implemented serialization for device at/detachment
2025-10-01 11:28:59 -04:00
hayodea 1b29f0e521 CMake: Terser config tokens 2025-10-01 11:08:59 -04:00
hayodea f40225681c Update todo 2025-10-01 11:02:53 -04:00
hayodea e9e273c012 CMake: PRefix config opt with CONFIG_ 2025-10-01 11:02:28 -04:00
hayodea 942f9d8515 DevMgr: formatting 2025-10-01 10:21:32 -04:00
hayodea 1e22656299 DevMgr: Call uniq_ptr.release on DeviceReattacher 2025-10-01 10:19:08 -04:00
hayodea 80004ab1e7 CMake: Device Reattacher period is now 2000ms 2025-09-30 23:10:10 -04:00
hayodea 55fe2675df DevMgr: Use releaseQutexEarly 2025-09-30 22:53:17 -04:00
hayodea af19125ac2 Qutex: Add check for double-release() 2025-09-30 22:53:01 -04:00
hayodea eb7fe11de4 SerialAsyncContin/LockSet: add releaseQutexEarly()
This method allows us to avoid double-release()ing qutexes when we
want to release a qutex early. If we don't use releaseQutexEarly(),
then inside of callOriginalCb when LockSet::release is called, the
Qutex will be release()d a second time.
2025-09-30 22:47:54 -04:00
hayodea 7ddbde1a2f DevMgr:removeDASpec: Make serialized; acquire DevMgr qutex 2025-09-30 22:10:39 -04:00
hayodea ac3d97b3ec DevMgr:newDASpecInd: now lockvoked, acquires DevMgr qutex
We now acquire the DevMgr qutex when doing the newDASpecInd async
op. The qutex is held across an async sequence with potentially
a real hardware blocking bottleneck.
2025-09-30 22:01:34 -04:00
hayodea 274143e41d Formatting 2025-09-30 21:40:26 -04:00
hayodea d75430ef82 Qutex: Document the need to deal with early release()s 2025-09-30 21:25:58 -04:00
hayodea 06f3f2eebe DevMgr:attachAllUnattachedDevicesFromKnownListReq: acquire qutex
We should acquire the qutex here before iterating through the list
of DA specs.
2025-09-30 21:24:51 -04:00
hayodea 07609c6d6c SenseApiLib:Add isBeingDestroyed atomic flag for getter bailout
Since we have no choice but to access the sh_ptr<SenseApiLib> for
a lib before we can get its Qutex, we use this flag to ensure that
we can know whether the SenseApiLib data structure and its Qutex
are still valid when we enter -- i.e, we ensure that the SenseApiLib
object wasn't destroyed under our feet.
2025-09-30 21:01:00 -04:00
hayodea 1bad358921 DevMgr:at/detachSenseDevReq: release SenseApiMgr Qutex early
Release it early when we no longer need it.
2025-09-30 20:29:39 -04:00
hayodea 77e123ff4a DevMgr:at/detachSenseDevReq: acquire SenseApiMgr qutex 2025-09-30 19:53:57 -04:00
hayodea 9c3a8ea695 Indentation 2025-09-30 19:51:50 -04:00
hayodea 0ff86a0a5e SenseApiMgr: Add Qutex 2025-09-30 19:51:01 -04:00
hayodea 8ad5179a61 DevMgr:at/detachSenseDevReq: only acquire API lib lock
We don't actually manipulate any of SenseApiMgr or DevMgr's state
so there's no need to acquire their locks.
2025-09-30 19:47:23 -04:00
hayodea e600b0f96e DevMgr:at/detachSenseDevReq: acquire Api lib Qutex 2025-09-30 18:12:20 -04:00
hayodea b0d61c3e38 DevMgr:at/detachSenseDevReq: Acquire DevMgr Qutex 2025-09-30 14:22:51 -04:00
hayodea 296e517389 SerAsyncContin: Make LockSet vector constructor arg mandatory 2025-09-30 14:22:12 -04:00
hayodea 758586bb3c LockerAndInvoker: Move template method impls around 2025-09-30 14:07:13 -04:00
hayodea ccf0ab77bf Move SerializedAsyncContin template method impls into header 2025-09-30 13:59:07 -04:00
hayodea 774661e1f0 AsyncContinChainLink: Make getCallersContinuationShPtr() const 2025-09-30 13:59:07 -04:00
hayodea 33b61f429a LockSet: Move method impls into template header 2025-09-30 13:42:13 -04:00
hayodea 7ded9d287d Split comment into relevant locations 2025-09-30 13:27:06 -04:00
hayodea a55bcc2a03 LockerAndInvoker: better var naming 2025-09-30 10:43:30 -04:00
hayodea f15c07bc83 LockSet:tryAcquireOrBackoff use optional<ref_wrapper<Qutex>>
Replace the previous Qutex** raw pointer with a std::optional.
2025-09-30 10:36:19 -04:00
hayodea e3ce533fe4 Use optional for type-safe retval 2025-09-30 10:22:28 -04:00
hayodea 2075c0b797 Get Lval for sh_ptr and fix build error 2025-09-30 03:53:47 -04:00
hayodea 37b8cb0c7f Lockvoker: Take singleton by ref and not by copy 2025-09-30 03:53:24 -04:00
hayodea 4ed36eb88a LockerAndInvoker: Reorder this to silence warning 2025-09-30 03:52:37 -04:00
hayodea e53b0be7e2 Lockset: make locks vec public so LockerAndInvoker can access 2025-09-30 03:51:36 -04:00
hayodea e299e956e5 Move LockSet::release into header 2025-09-30 03:01:13 -04:00
hayodea ca9eae197f Lockvoker: Add registerInLockSet
We now create the sh_ptr to the copy of a lockvoker object in its
own supplied register method. This enables us to retain type safety
when calling make_shared, by copying the most derived version of
the lockvoker object. Then we can pass in the LockerAndInvokerBase
to the rest of the call chain.
2025-09-30 02:55:25 -04:00
hayodea 08122c086c LockSet:tryAcquireOrBackoff: should take Qutex**, not Qutex*
No idea how this error survived in here for so long.
2025-09-30 02:53:54 -04:00
hayodea cb8b13d0cd LockSet:release() Remove superfluous Lockvoker arg
We decoupled unregistration from release() so we no longer need
to pass in a lockvoker to release().
2025-09-30 02:17:46 -04:00
hayodea 46686db07e SerializedAsyncContin: LockSet should take Qutex, not SpinLock
Apparently this error was here the whole time and it flew under
the radar.
2025-09-30 02:12:04 -04:00
hayodea 97d93c670e Indentation 2025-09-30 01:15:30 -04:00
hayodea 9d77e8b345 SenseApiLib: Add qutex for lib state access serialization 2025-09-30 01:14:00 -04:00
hayodea 4bf19fda90 Get rid of rvalue ref returns 2025-09-30 00:37:54 -04:00
hayodea ed9635582f Lockvoker: add handleGridlock and invoke it 2025-09-30 00:32:54 -04:00
hayodea b7ff100499 VSCode config 2025-09-30 00:26:41 -04:00
hayodea d06ba8957a QtxAcqHistTracker: Implement complete algo; add depend graph
The dependency graph class enables us to perform analysis on the
qutex acquisition history data. By generating the graph and
detecting cycles in it, we can find true gridlocks.

We use this graph analysis code to implement the algorithmically
complete version of the gridlock detector.
2025-09-30 00:24:33 -04:00
hayodea 1aec779351 CMake: Conditionally build QtxAcquisitionHistTracker 2025-09-29 21:09:31 -04:00
hayodea 6baa0bb008 Silence build warning 2025-09-29 20:48:49 -04:00
hayodea 274edc1013 QtxHistTracker: Print message on heuristic gridlock find 2025-09-29 20:47:04 -04:00
hayodea c0eecf76d4 QtxAcqHistTracker: Add spinlock and use it 2025-09-29 20:34:56 -04:00
hayodea 2390042892 QtxAcqHistTracker: implement heuristic gridlock detector 2025-09-29 20:02:23 -04:00
hayodea d32cb1ddac Lockvoker: print contin vaddr in false-positive ack 2025-09-29 19:46:44 -04:00
hayodea 0cf86cf18a Lockvoker: Gridlock only algo likely if heuristically likely
Only run the algorithmically complete scan if the heuristic
scan detected it as likely.
2025-09-29 19:43:28 -04:00
hayodea 5c7a92b3a4 SerializedAsyncContin:getAcquiredQutexHistory returns rval ref
This ensures that the caller takes ownership.
2025-09-29 19:40:51 -04:00
hayodea 21da27649e Formattign 2025-09-29 19:30:20 -04:00
hayodea 71564b4d83 Add QutexAcquisitionHistoryTracker; integrate plumbing
We add the new Qutex acquisision history tracker that allows us
to dynamically detect qutex gridlocks. We've integrated it into
LockerAndInvoker::operator() in a preliminary way.

We also moved all of the trace*ForGridlockOn() methods into the
new QutexAcquisitionHistoryTracker singleton class. They're
more appropriately located there. They're still unimplemented
though.
2025-09-29 19:27:02 -04:00
hayodea 8123ec1227 Qutex: Deprecate old gridlock trace; add skeleton new trace
We add the skeleton of a correct history tracer for gridlocks. The
previous history tracer made the incorrect assumption that we would
find the foreign sequence's currently desired LockSet inside of the
lockvoker that it has stored inside of Qutex::currOwner.
2025-09-29 18:14:10 -04:00
hayodea 2a60fdd9df Qutex: currOwner should use registered sh_ptr to Lockvoker
This ensures that the Lockvoker object we access from currOwner
remains valid past the lifetime of the Lockvoker object that
gets copied and invoked by boost::asio
2025-09-29 14:37:16 -04:00
hayodea 462247d743 Qutex: Add gridlock detection 2025-09-29 13:38:53 -04:00
hayodea 6b4fe05fc0 Qutex: Add gridlock detection wireframing
This is the basic wireframing required to enable us to detect
gridlocks among requests.
2025-09-29 12:58:41 -04:00
hayodea 0090aa6e3a Qutex: Add getter for currOwner pointer 2025-09-29 12:46:58 -04:00
hayodea 7e514a1fa3 Qutex: Add currOwner pointer for debugging
We'll use this to detect gridlock-type deadlocks between
two requests
2025-09-29 12:40:43 -04:00
hayodea f1ce1ab19c xcbWindow: Callback w/early ret if window not found 2025-09-29 12:19:03 -04:00
hayodea 542e3081ad DevReattacher: Use aggregate init for Callback<> 2025-09-29 01:59:52 -04:00
hayodea 65b9460a3a LivoxProto1:connectReq: Remove retryDelay; DevReattacher replaces
The new DeviceReattacher daemon in DeviceManager makes this retry
delay feature obsolete.
2025-09-29 01:18:36 -04:00
hayodea cea65dcd00 Mrntt: Call initializeDeviceReattacher in init
It works surprisingly well on the first try.
2025-09-29 01:07:32 -04:00
hayodea 6cebf6856e Indentation 2025-09-28 23:35:20 -04:00
hayodea 20d568a063 DevMgr: Call attachAll*FromKnownListReq in DeviceReattacher
Well, I guess now we have auto-reattaching devices.
2025-09-28 23:34:39 -04:00
hayodea bd52e49ba3 DevMgr: Add attachAllUnattachedDevicesFromKnownListReq
This function is the backbone for the DeviceReattacher daemon. It
assembles a list of all DA specs which are known to Mrntt, but which
haven't been successfully attached as yet, and attempts to attach
them.
2025-09-28 23:15:22 -04:00
hayodea 93103aa8d4 DevMgr: Add DeviceReattacher daemon plumbing
This is the plumbing for a periodic polling thread that tries
to reattach DA specs that failed to attach in the startup phase.
2025-09-28 23:07:39 -04:00
hayodea 2be78401b5 Center logo 2025-09-28 15:20:09 -04:00
hayodea eca3f47884 Add 512 and 256p logos 2025-09-28 15:15:40 -04:00
hayodea f3909fc000 New salmanoff logo 2025-09-28 15:09:23 -04:00
hayodea c0798d1bdb DevMgr: detachAll only detaches from attachedDeviceRoles
We no longer try to detach from the collection of specs. We
detach from the collection of attachedDeviceRoles. This means
our cleanup sequence no longer tries to clean up things that were
never set up to begin with.
2025-09-28 13:04:07 -04:00
hayodea e45a9ee5d1 DAP.yy: use cmdlineDASpecs; DevMgr: add attachAllUnattachedDevicesFromCmdlineReq
This method wraps around attachAllUnattachedDevicesFromReq and supplies
it with a sh_ptr<> collection of all DASpecs parsed by the DAP parser
from the cmdline.

The initialization sequence now correctly initializes all DAP specs
given on the cmdline again.
2025-09-28 12:52:59 -04:00
hayodea b43ffcb677 DevMgr: attachAllUnattachedDevsFrom: now takes sh_ptr<vector<Spec>>
This method now accepts a sh_ptr<vector<DeviceAttachmentSpec>> to
tell it specifically which specs to attempt to attach.

This enables us to implement different frontends that supply it
with collections of devices from different sources (GUI, cmdline,
previously failed-to-attach/hot-removed devices, etc).

SMO temporarily initializes none of the devices from the cmdline
during this commit as we transition to implementing the cmdline
collection frontend.
2025-09-28 12:39:45 -04:00
hayodea 2c60248127 DevMgr: Rename at/detachAll*Req():
We've renamed these now to better reflect what they do.
* attachAllSenseDevicesFromSpecsReq=>attachAllUnattachedDevicesFromReq
* detachAllSenseDevicesReq=>detachAllAttachedDeviceRoles

This is also the first step in changing
attachAllUnattachedDevicesFrom to accept a sh_ptr<> to a collection
of DeviceAttachmentSpecs. This will enable us to unify the underlying
spec attachment logic and just create several front-ends for attaching
specs from multiple sources.
2025-09-28 12:19:56 -04:00
hayodea 993bf568fc DevMgr: implement removeDeviceAttachmentSpecReq
This reverts all state changes made by newDeviceAttachmentSpecInd.
2025-09-28 11:41:20 -04:00
hayodea 27e707a22d DevMgr:newDevAttSpecInd: take ref & not sh_ptr to DASpec
Because NewDevAttSpecInd should be the function that creates the
persistent sh_ptr state within DevMgr.
2025-09-28 11:14:55 -04:00
hayodea 7ab6e7b2c3 Formatting 2025-09-28 11:05:04 -04:00
hayodea 5b2354bfe0 DevMgr:newDevAttSpecInd: creates Device in frontend
There's no reason why we have to only create the Device at the
end if everything succeeds. The Device isn't the same thing as the
DeviceRole.
2025-09-28 10:57:43 -04:00
hayodea 42ab935da6 Reorder operations 2025-09-28 09:19:09 -04:00
hayodea 572a8612ed DevMgr,Dev: Add Qutexes
Add Qutex locks to both DeviceManager and Device. These will be
properly used in the upcoming patches.
2025-09-28 01:27:32 -04:00
hayodea 51b70b179c DevMgr: call newDevAttSpecInd & not attDevReq in body:initReq
This performs a more complete device initialization and attachment
sequence. We'll do the corresponding teardown in the shutdown
sequence later.

We might probably do it as deviceRoleGoneAwayInd()
2025-09-28 01:15:36 -04:00
hayodea 1a56e2a107 DevMgr: Add DeviceRoles; attachedDevices unrelated to device state now
We've decided to add a separate notion of a DeviceRole to track attached
device roles now. We no longer use the collection of deviceSpecs to
track which roles have been attached. Rather, this list will simply
collate all known deviceAttachment specs which are expected to be
maintained in an attached state.

SMO can periodically scan through these and cross-reference this
collection with the collection of attachedDeviceRoles. Then it can
re-try to attach those which aren't currently attached at any given
moment. This will give resilience against device attachment failures
or device resets/malfunctions, at runtime.
2025-09-28 00:50:05 -04:00
hayodea e6b8d3e85d DevMgr: Move at/detachSenseDevs[FromSpecs] into DeviceMgr::
This is logically cleaner and it begins preparing our next set
of restructuring changes. To wit: we're revamping the device
manager to distinguish between devices and their roles.
2025-09-27 23:16:46 -04:00
hayodea 52567406ca Lockset: check for registration b4 unregistering 2025-09-27 22:22:35 -04:00
hayodea 2f18ade4ab Locking: pre-emptively detect deadlocks in Lockvoker constructor
We can now check for deadlocks both pre-emptively and reactively.
We can eventually add function vaddrs and even function names to
our tracing code.
2025-09-27 21:27:04 -04:00
hayodea f8bf8083af Locking: Add contin tracing to detect deadlocks
We added the code to trace all the contins linked to a particular
Lockvoker, into SerializedAsyncContinuation. This basically
ensures that we'll almost never deal with a deadlock. So cool.
2025-09-27 20:51:20 -04:00
hayodea 782bcd4567 Async: add sh_ptr<ContinuationChainLink> to Callback<>
This change enables us to finally implement the tracing of
continuations backward from the point of acquisition for deadlock
debugging.
2025-09-27 18:30:09 -04:00
hayodea 2212aec080 VScode stuff that didn't work to make it use /b and not /build 2025-09-22 21:32:17 -04:00
hayodea d0303becd7 Async: Add a new Callback class for accessing caller's contin
We previously passed a sh_ptr to the caller's contin as arguments
to the std::bind() callable. In order for us to be able to trace
deadlocks, we need to be able to access them explicitly.

So here's that change.
2025-09-22 21:30:33 -04:00
hayodea 27bebeb702 Squash: Into debug locks commit 2025-09-22 21:30:14 -04:00
hayodea 09a0041f20 Squash: into debug locks intro commit 2025-09-22 21:29:43 -04:00
hayodea c6c3d6c9e8 Todo: Update 2025-09-22 20:49:00 -04:00
hayodea 092a0954a0 Locking: Add basic reactive deadlock detection foundation
We added a timestamp to each Lockvoker so that we can detect when
a lockvoker has been in a qutex for "too long", where "too long"
is defined arbitrarily as 500ms.

Next we're going to change the way we create callbacks to enable
us to more explicitly access the sh_ptr<AsyncContin> via
the callback object.
2025-09-22 20:45:36 -04:00
hayodea d2ed525106 Debug:Qutex: Add deadlock detection based on elapsed time
We now detect that a deadlock is likely when
CONFIG_DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS has elapsed. This is the
preliminary work required to do a backtrace through the call
stack and figure out if a deadlock has really occured.

To do this, we'd have to go through the async call chain and
search for a previous caller which acquired the same qutex as
the one that first failed during this Lockvoker LockSet acquisition
attempt.
2025-09-21 15:11:28 -04:00
hayodea dbc9569775 Async: Add exception bubbling
CONT_SET_EXC: Set exception on the continuation, to be rethrown
	by the caller.
CONT_SET_EXC_AND_RET: Convenience which returns immediately
	after setting the exception.
2025-09-21 14:21:55 -04:00
hayodea 1e2cc5ef16 scripts: Add sloc 2025-09-20 19:57:13 -04:00
hayodea 31cadb2ee4 Test: Add googletest; Add qutex_tests.cpp
Well, we now have this.
2025-09-20 19:41:57 -04:00
hayodea 329d57a16d Formatting 2025-09-20 18:23:03 -04:00
hayodea 32179eee5e Qutexes: Implement them and supporting classes
Implements: LockSet, SerializedAsynchronousContinuation,
	LockerAndInvoker, LockerAndInvokerBase, Qutex.

Very big leap in functionality here. See qutexes.md for
an explanation of what we've done.
2025-09-20 18:20:52 -04:00
hayodea f05c465d61 Docs:Qutexes Update design but it's not reflective of impl 2025-09-20 18:14:17 -04:00
hayodea 79c50ff191 Docs: Qutex.md: update 2025-09-19 21:36:59 -04:00
hayodea d10217f3f5 Docs: update qutex algo plan 2025-09-19 18:52:27 -04:00
hayodea 4b0e832e27 Asnc: LockSet is now a member of serializedAsyncCont; not base class
We needed to pass in a ref to the parent SerializedAsyncContin, so
we had to make it a member var instead of a base class.
2025-09-19 01:32:52 -04:00
hayodea dd9ce63cb8 Locking:Qutex: Skeleton for Qutex class 2025-09-19 01:19:09 -04:00
hayodea b9322c5e89 Docs: Qutex updated notes -- still WIP 2025-09-19 01:18:32 -04:00
hayodea 2dfa615eb7 Rename: lockSpec.h=>lockSet.h 2025-09-18 23:05:44 -04:00
hayodea 596bc1fbd2 SpinLock: Add acquire/release() and spinPause()
We're very careful to ensure that we add a cpu_relax hint.
2025-09-18 23:01:49 -04:00
hayodea ec50526804 Rename: LockSpec=>LockSet 2025-09-18 22:21:06 -04:00
hayodea eb3366cfd2 Async: Move callOriginalCb out of AsyncCont base, into NonPostedAC
The reasoning here is to prevent silent invocations of
callOriginalCb from a base class pointer/ref.
2025-09-18 22:10:17 -04:00
hayodea b49e281010 Docs: New qutexes design 2025-09-18 20:29:37 -04:00
hayodea 9a23dbbe95 Whitespace 2025-09-17 18:18:09 -04:00
hayodea 5a3c0699f7 Locking: Add LockerAndInvoker to provide spinQing behaviour
We haven't implemented or tested this yet but it looks promising.
Also, it's surprisingly clean!
2025-09-17 16:39:11 -04:00
hayodea 816a047920 Async: new hierachy; manages reply posting and unlocking
Async: Use new [Non]PostedAsyncCont and callOriginalCb

This new hierarchy of classes gives us a central mechanism for
managing both reply-posting and lockSpec unlocking.

* callOriginalCb: Now uses a modern C++ variadic template design
  enabling it to handle both direct calling and std::bind()
  re-binding of an arbitrary number of arguments from the caller.

This enables us to mostly eliminate the repeated, bespoke
definitions of callOriginalCb littered throughout the codebase.

We've also propagated these changes throughout the codebase in
this patch.
2025-09-17 16:38:48 -04:00
hayodea 33c006b178 Locking: Add LockSpec class which manages sets of spinlocks
This class allows us to list a series of spinlocks that are all
acquired and released together. It has simple, primitive detection
for deadlocks and will throw if it detects one.
2025-09-17 16:29:58 -04:00
hayodea 0733bb9a68 Locking: add SpinLock class
Nothing much to add: add a spinlock which has a tryAcquire method.
This will be used as a primitive for building our spinQing locking
system.
2025-09-17 16:28:07 -04:00
hayodea eeaa4ed2df CMake: Add preliminary CPack support
We'll tune it later.
2025-09-16 22:19:48 -04:00
hayodea 0788bbd799 Cmake,DAPS: Add support for DAPS preprocessing
We add a new extension, .dapss for preprocessable DAPS spec files.
We now run the C preprocessor on the .dapss files to produce
.daps files.
2025-09-16 21:30:50 -04:00
hayodea a6eccede4a DAPSpecs: Formatting 2025-09-16 20:44:46 -04:00
hayodea 437f7ea10f Postfix these files with .dapss 2025-09-16 20:08:38 -04:00
hayodea 58f7df49ae Move DAP specs into /devices 2025-09-16 20:04:05 -04:00
hayodea 03da91f5e5 Move DAP specs into /bodies/devices 2025-09-16 19:57:19 -04:00
hayodea 02c071394b Update todos 2025-09-16 19:54:32 -04:00
hayodea c52c447a78 Formatting 2025-09-16 18:46:30 -04:00
hayodea ddd6f6d6c6 CompThr: Comment on posted CBs 2025-09-16 18:45:55 -04:00
hayodea f5c359a6a9 Update todos 2025-09-16 18:40:40 -04:00
hayodea af33b7f097 SenseApiMgr: Make at/detachSenseDev & at/detachAllSenseDevs posted
They are posted to Marionette.

* We also fixed callOriginCb invocations;
* Also made posted CBs use std::bind instead of greedily
  early-invoking the CB on the servicing thread's stack.
2025-09-16 18:38:06 -04:00
hayodea 92e55641a0 LivoxGen1: Replace throw with cb+early ret 2025-09-16 18:36:50 -04:00
hayodea 9e00cd1530 Formatting, indentation 2025-09-16 18:36:11 -04:00
hayodea 8fd8826f8d Mind:threadMgmt ops: move 0-iter callback to top 2025-09-16 18:24:16 -04:00
hayodea 5d30941aab AsyncLoop: Add copy constr + assignment op
Make nTotal non-const.
2025-09-16 18:20:08 -04:00
hayodea 429bd2a349 Exc: Replace with cb+ret 2025-09-16 15:10:28 -04:00
hayodea 5c79a89cd4 Body: Don't forget to finalizeAllLibs 2025-09-16 15:09:56 -04:00
hayodea a931f9f01a CompThr: Name segments to indicate that they're posted 2025-09-15 15:15:40 -04:00
hayodea 7f3bfec835 Mind:init/finiReq: now posted to mrntt; callbacks std:bind
We now have mind::initialize/finalizeReq post their requests
to Mrntt instead of executing on the caller's thread context.

We also fixed the way that we invoke callbacks by properly wrapping
it in a std::bind.
2025-09-15 15:01:26 -04:00
hayodea 19b39d391f DevMgr: Move helper function to top 2025-09-15 14:33:42 -04:00
hayodea 674d74cfb9 DevMgr:newDevSpecInd: fix posting and async pattern conformance 2025-09-15 14:32:26 -04:00
hayodea b768739b96 CompThr: Delete shutdownInd & exceptionInd
We no longer need them because we now have
mrntt::mrntt.finalizeReq(), which does a more holistic job of
shutting down Marionette (and thus, ultimately, Salmanoff).
2025-09-15 13:43:11 -04:00
hayodea 77acbdd8de Indentation 2025-09-15 13:10:30 -04:00
hayodea 7b699d5d36 LivoxGen1: Check for 3 '.'s in smo-ip 2025-09-15 13:05:58 -04:00
hayodea db8f047322 SApiMgr:attDevReq: use body||world thread for i/e-devs
We now pass in the correct ComponentThread based on the type of
device that's being attached.
2025-09-15 12:47:37 -04:00
hayodea 472184bbbc Fix build errors with mind::globalMind and Qualia headers 2025-09-15 12:47:09 -04:00
hayodea dd3d5fea66 Todo: update completed tasks 2025-09-15 12:45:52 -04:00
hayodea 6573a1b14d CMake: delete subdir CMakeLists; use one CMakeList for smocore 2025-09-15 12:44:57 -04:00
hayodea 0759461c69 Mrntt:main: call mrntt:finalizeReq on exception
We'll wrap this in some exception-specific wrapper later.
2025-09-15 11:42:38 -04:00
hayodea 62cada2547 Bodies:dev: rename d0 to win0 2025-09-15 08:36:14 -04:00
hayodea e755383e72 Body: postfix _posted to posted sequence methods 2025-09-15 08:33:49 -04:00
hayodea d1e4c1a2ea Body:finalize: Will run if even one initReq step was executed
If even one step in Body.initializeReq was executed at all, then
whether or not it succeeded, we consider the body component to have
been initialized, at least with respect to whether finalizeReq
ought to run.
2025-09-15 08:30:17 -04:00
hayodea 29b192b2ee Formatting, spam-reduction 2025-09-15 08:25:49 -04:00
hayodea 7c48abbcca Body:init: Return true if any devices were initialized at all 2025-09-15 08:25:20 -04:00
hayodea 0ec227cf9e Body:finalize: Run even if body.init wasn't called
We now run body.finalizeReq even if body.init wasn't called. We'll
do a finer-grained check on each aspect of Body that needs to be
finalized now. This check was too large-grained.
2025-09-15 08:23:54 -04:00
hayodea baad2a9890 mrntt:main: Get rid of finalizeInd
This leverages the new clean dynamic allocation of the globalMind
object to make the mrntt::main and SMO's initialization and
shutdown much cleaner. We no longer concern ourselves with
shutting down the Mind threads inside of mrntt::main, but rather
we leave that state machine to the Mind class and Mrntt component.
2025-09-14 23:31:12 -04:00
hayodea 91ccd16b33 Add Mrntt component; init globalMind in mrntt.initializeReq
This makes the initialization sequence much cleaner and conceptually
well encapsulated.

We also now dynamically allocate the Mind objects. They're allocated
dynamically by Mrntt inside of initializeReq. This means that we no
longer have to worry about jolting and cleaning up the running threads
of global mind object even when we never explicitly called
Mind.initializeReq.

Along with other conceptual improvements to our abstractions, this
patch also gets us to a real "end of program initialization" point
for the first time.
2025-09-14 22:17:19 -04:00
hayodea 16865dc36f Rename these files and change ifdef guards 2025-09-14 13:16:02 -04:00
hayodea da0ef64f62 Split CompThread=>MindThr+MrnttThr; alloc globalMind in mrnttMain
We now allocate globalMind locally inside of marionetteMain. Why?

Before now, we had an asymmetric threading situation where the
globalMind's threads were initialized at during global constructor
invocation and not on demand. This meant that we had to shut down
those threads even if we had never got to the point of calling
Mind::initializeReq.

This significantly complicated our shutdown sequence since we had
to factor in the lifetime of the std::thread objects inside of the
ComponentThreads which were inside of the globalMind object.

Now, if we hadn't called Mind::initializeReq, we don't have to
perform any Mind::finalizeReq or adjacent operations. Shutdown is
symmetrically mirrored against the operations we actually performed
during execution.

We introduced some complexity by splitting ComponentThreads into
two derivative types (MindThread and MarionetteThread) but I think
in the long term we'll be able to massage this split into a much
cleaner situation overall.
2025-09-14 11:07:05 -04:00
hayodea 83af74f4be Update todo 2025-09-14 11:01:52 -04:00
hayodea 7cb6c8521e MindThread:shutdownInd: explicitly invoke on globalMind 2025-09-14 10:59:52 -04:00
hayodea 1d3d929ddd Mind: Use state variables to manage shutdown
We now allow the shutdown*Req() methods of Mind:: to return early
if their aspect of the object in question hasn't actually been
initialized.
2025-09-13 18:59:44 -04:00
hayodea 25a9721f92 Mind: Implement initialize/finalizeBodyReq()
We've done a lot of general work on the init sequencing.
2025-09-12 16:09:26 -04:00
hayodea b99b147959 CMake: libsmocore links against libsenseApis 2025-09-12 16:00:19 -04:00
hayodea 96bf653167 Add comments about Mrntt/Manager vs Mind sync/async design 2025-09-12 09:52:02 -04:00
hayodea 4429135539 Mind,Mrntt: Use async pattern in Mind; init threads before initializeSmo
In Mrntt, we now initialize Mind:: object threads before calling
initializeSalmanoffReq().

We've also propagated the spinscale async pattern into the Mind
class.
2025-09-11 20:11:10 -04:00
hayodea 89947dfc71 AsyncLoop: Add test for zero-iteration loops
This mnemonically ensures that we'll remember to check for
these kinds of async loops.
2025-09-11 20:10:01 -04:00
hayodea fb17c51ef6 ComponentThread: Remove lambdas; use standard async pattern
We've finally cleaned this code up by removing these dirty lambdas.
Next we do the Mind:: class sequences.
2025-09-11 18:41:45 -04:00
hayodea b8c931397d Async: add TargetedContinuation
This class enables us to consistently represent continuations
that are intended to be posted on a particular target handling
thread. It hols a sh_ptr to the caller so that the target thread
can re-enqueue the response on the caller after processing the
REQ/IND op.
2025-09-11 18:37:48 -04:00
hayodea f5195450e4 Formatting 2025-09-11 09:10:12 -04:00
hayodea b4102e6ee1 CMake: enable livoxGen1 and xcbWindow by default 2025-09-10 19:51:34 -04:00
hayodea 20034513ad CMake: fix usage of cmake_dependent_option 2025-09-10 19:44:17 -04:00
hayodea 3a852bfb9d Update todos 2025-09-10 18:17:15 -04:00
hayodea 4f74e1cd31 Async: Document bridging pattern 2025-09-10 18:16:55 -04:00
hayodea b7cf4c9135 Docs: Document the locking mechanism we plan to use
This new locking mechanism is very cumbersome, but highly
throughput maximizing. It trades high memory usage to gain
high throughput.

We may end up even being able to get the high throughput
without incurring the high memory usage by using std::bind
objects, etc.
2025-09-10 18:14:20 -04:00
hayodea e08dc0678b Make [at|de]tachAllSenseDevices[FromSpecs] and initializeSalmanoff async
This is the culmination of a lot of changes over the last week. We're
making SMO basically fully async in many areas, and then preparing to
implement the spinqueueing mechanism for locking.
2025-09-10 18:12:08 -04:00
hayodea 81842e4571 SenseApiDesc: SAL_MLO: attach/detachDeviceReq: return parsed DA spec 2025-09-10 18:11:08 -04:00
hayodea 428a32a950 asyncLoop: Make nTotal const 2025-09-10 15:13:02 -04:00
hayodea 01da06b051 Rename this file 2025-09-10 15:10:10 -04:00
hayodea a989256f22 SenseApiMgr: Use new loop convenience methods 2025-09-10 11:55:53 -04:00
hayodea 21d2df4d34 AsyncLoop: Add auto-incrementing & completion testing methods
These basically make it almost impossible to commit errors in async
loop logic implementation.
2025-09-10 11:54:26 -04:00
hayodea 8d18765a3a SenseApi: Fix async loop logic 2025-09-10 11:42:05 -04:00
hayodea 0c43c88554 AsyncBridge: Fix build warning 2025-09-10 11:38:33 -04:00
hayodea 07937a4846 SenseApiMgr: Use new bridge and loop classes 2025-09-10 11:37:35 -04:00
hayodea 067c928e47 AsyncBridge: Add new wrapper class that bridges async sequences
This class encapsulates all the logic and operations required to
correctly bridge an async operation into a sync function.
In particular, it also makes it less easy to forget to check if the
io_service exited because it was stop()ped.
2025-09-10 11:35:09 -04:00
hayodea 680977b211 formatting 2025-09-10 11:07:42 -04:00
hayodea 4143541adf Async: add AsyncLoop bounds tracker 2025-09-10 11:06:29 -04:00
hayodea 1b6b12256d SenseApis: Make attachDeviceReq async in drivers and SenseApiMgr
Slowly retrogressively making these sequences async
2025-09-10 06:51:55 -04:00
hayodea 5b5a701c69 Formatting 2025-09-10 04:02:27 -04:00
hayodea 3457efcbf8 Whitespace 2025-09-10 03:57:37 -04:00
hayodea bb1c7e4be1 Todo: update TODO list 2025-09-09 20:12:59 -04:00
hayodea cc33b333d2 livoxProto1: Document potential bug 2025-09-09 20:09:57 -04:00
hayodea 0449e557b0 livoxGen/Proto1: Refine auto-detection vs heuristic logic 2025-09-09 19:54:14 -04:00
hayodea 73b2d981f9 livoxGen1: Implement attach/detachDeviceReq by sync-bridging Proto1
We perform bridged synchronous calls into liblivoxProto1 in order
to support attach/detachDeviceReq. We'll eventually make
attachDetachDeviceReq fully asynchronous but for now we're happy
that we have this working driver for this fairly tricky device.
2025-09-09 12:09:59 -04:00
hayodea 20cdf64afb livoxProto1: Implement async getOrCreateDeviceReq+destroyDeviceReq
These are now both fully asynchronous. They also work fully
and both connect and disconnect to/from the Avia just fine,
in all tested scenarios.
2025-09-09 12:07:49 -04:00
hayodea 48121ec84c SenseApiMgr: Provide ComponentThread::getSelf access to libs 2025-09-09 12:03:50 -04:00
hayodea 725f8772b1 Indentation 2025-09-09 12:02:31 -04:00
hayodea 8413277847 Verbose: Print these messages only in verbose mode 2025-09-09 12:02:03 -04:00
hayodea 576d3ed7a5 VSCode: no idea 2025-09-09 12:00:15 -04:00
hayodea 381b29c12d Bodies:dev: Avia0: Rely on default source-code timeout/retry values 2025-09-09 11:59:07 -04:00
hayodea 0dcb4ce65f Async: Add a new AsyncContinuation base class
This will hold the std::function callback that persists
caller's async context.
2025-09-09 11:50:19 -04:00
hayodea c880f5b73e Threading: call initializeSalmanoff after io_service.reset()
This enables us to do asynchronous bridging in the functions called
by initializeSalmanoff
2025-09-07 18:42:28 -04:00
hayodea 6ba6cb9cf0 Mrntt: Add ability to listen for SIGSEGV
But don't actually use it because it makes it difficult to
exit SMO from the cmdline.
2025-09-07 11:45:54 -04:00
hayodea f587b45b38 livoxProto1: Connecting to bcast-advertised device works :)
We tested it.
It's important to note that between test runs, we need to take
into account the fact that the Avia stops sending bcast adverts
when it's been handshaken.

So the retry-delay-ms may be longer due to the fact that the Avia
may not be sending adverts for a good portion of that retry-delay-ms
time.
2025-09-07 11:42:32 -04:00
hayodea 1ededb85b9 livoxProto1:bug workaround: for boost:asio:udp async ops on dlopen libs
Updated Boost dependency to version 1.73.0 to address segfault issues with boost::asio in dynamic libraries. Refactored heartbeat socket management to use raw UDP sockets instead of boost::asio, improving compatibility and error handling during socket operations.
2025-09-07 08:14:46 -04:00
hayodea 9a500e39ab livoxProto1: Fix source and dest port for handshake & heartbeat
Handshake: We must wait for the handshake ACK on the same port as
the one we sent the handshake REQ from.
Heartbeat: We must send the heartbeats from the same source port
as the one we sent the handshake REQ from.
2025-09-07 06:47:53 -04:00
hayodea 8f0e945f0c livoxGen1: Validate smo-ip provider-param 2025-09-06 22:46:03 -04:00
hayodea 5d55157ffd livoxGen1: Implement attachDevice/detachDevice
Preliminary draft implementation. Unsurprisingly it doesn't
work. The handshake doesn't get accepted by the device it seems?
And we can't receive bcast dgrams since we're blocking
synchronously -- therefore we're not processing the incoming UDP
dgrams.
2025-09-06 22:46:03 -04:00
hayodea 7f367fd6e3 Debug: silence excessive couts 2025-09-06 22:46:03 -04:00
hayodea b0596d12f9 Formatting 2025-09-06 22:46:03 -04:00
hayodea 3b07a15e11 Move getRequiredParamAsInt into deviceAttachmentSpec.h
Make it accessible for all senseApis.
2025-09-06 22:46:03 -04:00
hayodea 38b29ddfc0 Comments and formatting 2025-09-06 22:46:03 -04:00
hayodea d2bf5aceee livoxProto1: detectSmoIp should be based on target dev IP
We previously used the smoIp provided by the user, but this
function is intended to enable us to figure out the correct
IP to send to the target device in the Handshake message.
2025-09-06 22:46:03 -04:00
hayodea a0d577bf81 LivoxProto1: Implemented Device and getOrCreateDevice
Includes everything from sending heartbeat msgs to performing
the connection handshake. We also accept many params to
provider-params to customize and make things easier.
2025-09-06 22:46:03 -04:00
hayodea 25234c4229 Docs: LivoxGen1 DAP spec: re-add ports for cmd/data/imu 2025-09-06 22:46:03 -04:00
hayodea 71c448a1d7 Docs: Update DAP spec for livoxGen1 2025-09-06 22:46:03 -04:00
hayodea 93fd2ac0ab VSCode config 2025-09-06 22:46:03 -04:00
hayodea 3e9eecc279 livoxProto1: Keep protocol headers pure;
Split the BroadcastListener, DiscoveredDevice and other concerns
out of the protocol header and implementation files.
2025-09-06 22:46:03 -04:00
hayodea 5e4597b8fd Docs: update DAP specs for livoxGen1 devices 2025-09-06 22:46:03 -04:00
hayodea 6eb6fa1eb0 Livox: Add new loadable libs for Protocolv1 and gen1 lidars
We've added two new libs:
* commonLibs/livoxProto1
* senseApis/livoxGen1

They currently get to the point of detecting my Livox Avia on the
network over UDP. This was really easy to get done in one night
using boost::asio and Cursor, honestly.
2025-09-06 22:46:03 -04:00
hayodea f00e1c7cf1 Formatting 2025-09-06 22:46:03 -04:00
hayodea 26be261cff SenseApiDesc: Rename SalmanoffCallbacks=> SmoCallbacks 2025-09-06 22:46:03 -04:00
hayodea e5a3c41c20 SenseApis: Add threading model info to initialization info
We provide access to a thread whose event queue the sense API
libs can use for device-independent event management.
2025-09-06 22:46:03 -04:00
hayodea 1d9b8a2cf6 Todo: Add note to rename struct 2025-09-06 22:46:03 -04:00
hayodea 4ce4c9f9f8 DAP Specs: Add specs for LivoxGen1 devices 2025-09-06 22:46:03 -04:00
hayodea fe5a7d480d CMake: tried to get project building in Clang. Failed. 2025-09-06 22:45:25 -04:00
hayodea 83273ae806 Bodies:dev: avia: don't set default subnet to 192.168.1.0 2025-09-06 22:06:37 -04:00
hayodea 04a3631881 Bodies:dev: Add DAPS files for test window and Avia 2025-09-06 22:02:12 -04:00
313 changed files with 34083 additions and 2411 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
+3
View File
@@ -12,3 +12,6 @@ config.h.in
configure configure
*.swp *.swp
cscope.out cscope.out
*.tmp
.cache
.codex
+6
View File
@@ -0,0 +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
+4 -4
View File
@@ -3,12 +3,11 @@
{ {
"name": "Linux", "name": "Linux",
"includePath": [ "includePath": [
"${workspaceFolder}/**",
"${workspaceFolder}/include", "${workspaceFolder}/include",
"${workspaceFolder}/smocore/include", "${workspaceFolder}/smocore/include",
"${workspaceFolder}/b/include",
"/usr/include", "/usr/include",
"/usr/local/include", "/usr/local/include"
"${workspaceFolder}/b/include"
], ],
"defines": [], "defines": [],
"compilerPath": "/usr/bin/g++", "compilerPath": "/usr/bin/g++",
@@ -24,7 +23,8 @@
}, },
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/b/include/config.h" "${workspaceFolder}/b/include/config.h"
] ],
"configurationProvider": "ms-vscode.cmake-tools"
} }
], ],
"version": 4 "version": 4
+110 -77
View File
@@ -1,83 +1,116 @@
{ {
"files.associations": { "files.associations": {
"cstdint": "cpp", "*.cl": "c",
"array": "cpp", "cstdint": "cpp",
"atomic": "cpp", "array": "cpp",
"bit": "cpp", "atomic": "cpp",
"*.tcc": "cpp", "bit": "cpp",
"cctype": "cpp", "*.tcc": "cpp",
"charconv": "cpp", "cctype": "cpp",
"chrono": "cpp", "charconv": "cpp",
"clocale": "cpp", "chrono": "cpp",
"cmath": "cpp", "clocale": "cpp",
"compare": "cpp", "cmath": "cpp",
"concepts": "cpp", "compare": "cpp",
"condition_variable": "cpp", "concepts": "cpp",
"cstdarg": "cpp", "condition_variable": "cpp",
"cstddef": "cpp", "cstdarg": "cpp",
"cstdio": "cpp", "cstddef": "cpp",
"cstdlib": "cpp", "cstdio": "cpp",
"ctime": "cpp", "cstdlib": "cpp",
"cwchar": "cpp", "ctime": "cpp",
"cwctype": "cpp", "cwchar": "cpp",
"deque": "cpp", "cwctype": "cpp",
"map": "cpp", "deque": "cpp",
"set": "cpp", "map": "cpp",
"string": "cpp", "set": "cpp",
"unordered_map": "cpp", "string": "cpp",
"vector": "cpp", "unordered_map": "cpp",
"exception": "cpp", "vector": "cpp",
"algorithm": "cpp", "exception": "cpp",
"functional": "cpp", "algorithm": "cpp",
"iterator": "cpp", "functional": "cpp",
"memory": "cpp", "iterator": "cpp",
"memory_resource": "cpp", "memory": "cpp",
"numeric": "cpp", "memory_resource": "cpp",
"optional": "cpp", "numeric": "cpp",
"random": "cpp", "optional": "cpp",
"ratio": "cpp", "random": "cpp",
"string_view": "cpp", "ratio": "cpp",
"system_error": "cpp", "string_view": "cpp",
"tuple": "cpp", "system_error": "cpp",
"type_traits": "cpp", "tuple": "cpp",
"utility": "cpp", "type_traits": "cpp",
"format": "cpp", "utility": "cpp",
"fstream": "cpp", "format": "cpp",
"initializer_list": "cpp", "fstream": "cpp",
"iomanip": "cpp", "initializer_list": "cpp",
"iosfwd": "cpp", "iomanip": "cpp",
"iostream": "cpp", "iosfwd": "cpp",
"istream": "cpp", "iostream": "cpp",
"limits": "cpp", "istream": "cpp",
"mutex": "cpp", "limits": "cpp",
"new": "cpp", "mutex": "cpp",
"numbers": "cpp", "new": "cpp",
"ostream": "cpp", "numbers": "cpp",
"semaphore": "cpp", "ostream": "cpp",
"span": "cpp", "semaphore": "cpp",
"sstream": "cpp", "span": "cpp",
"stdexcept": "cpp", "sstream": "cpp",
"stop_token": "cpp", "stdexcept": "cpp",
"streambuf": "cpp", "stop_token": "cpp",
"thread": "cpp", "streambuf": "cpp",
"typeinfo": "cpp", "thread": "cpp",
"variant": "cpp", "typeinfo": "cpp",
"cstring": "cpp", "variant": "cpp",
"cinttypes": "cpp", "cstring": "cpp",
"any": "cpp", "cinttypes": "cpp",
"codecvt": "cpp", "any": "cpp",
"complex": "cpp", "codecvt": "cpp",
"coroutine": "cpp", "complex": "cpp",
"csignal": "cpp", "coroutine": "cpp",
"list": "cpp", "csignal": "cpp",
"source_location": "cpp", "list": "cpp",
"future": "cpp", "source_location": "cpp",
"shared_mutex": "cpp", "future": "cpp",
"typeindex": "cpp", "shared_mutex": "cpp",
"bitset": "cpp" "typeindex": "cpp",
}, "bitset": "cpp",
"*.ipp": "cpp",
"unordered_set": "cpp",
"forward_list": "cpp",
"barrier": "cpp",
"strstream": "cpp",
"regex": "cpp",
"stacktrace": "cpp",
"stdfloat": "cpp",
"cfenv": "cpp",
"expected": "cpp",
"valarray": "cpp",
"core": "cpp",
"nonlinearoptimization": "cpp",
"*.txx": "cpp"
},
"editor.rulers": [80, 120], "editor.rulers": [80, 120],
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"editor.detectIndentation": false "editor.detectIndentation": false,
"editor.inlayHints.enabled": "off",
"C_Cpp.default.browse.limitSymbolsToIncludedHeaders": true,
"C_Cpp.default.browse.path": [
"${workspaceFolder}",
"${workspaceFolder}/b"
],
"C_Cpp.default.includePath": [
"${workspaceFolder}/include",
"${workspaceFolder}/smocore/include",
"${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(...)
{}
```
+202 -10
View File
@@ -1,20 +1,35 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.19)
project(salmanoff VERSION 0.00.004 LANGUAGES CXX) if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
project(salmanoff VERSION 0.01.001 LANGUAGES CXX)
include(CMakeDependentOption) 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 C++ standard
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Build type # Build type
if(NOT CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug FORCE) set(CMAKE_BUILD_TYPE Debug FORCE)
endif() endif()
# Compiler flags # Compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic") 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 # Mind oscillator configuration
set(MIND_VOSCILLATOR_PERIOD_MS 33 CACHE STRING "Mind's virtual osc clock rate (ms)") set(MIND_VOSCILLATOR_PERIOD_MS 33 CACHE STRING "Mind's virtual osc clock rate (ms)")
if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0) if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
@@ -22,10 +37,57 @@ if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
endif() endif()
math(EXPR MIND_VOSCILLATOR_FREQ_MS "1000 / ${MIND_VOSCILLATOR_PERIOD_MS}") math(EXPR MIND_VOSCILLATOR_FREQ_MS "1000 / ${MIND_VOSCILLATOR_PERIOD_MS}")
# Device manager reattacher configuration
set(MRNTT_DEVMGR_REATTACHER_PERIOD_MS 2000
CACHE STRING "Device manager reattacher period (ms)")
if(NOT MRNTT_DEVMGR_REATTACHER_PERIOD_MS GREATER 0)
message(FATAL_ERROR
"MRNTT_DEVMGR_REATTACHER_PERIOD_MS must be a positive integer > 0")
endif()
# Stimulus buffer frame period configuration
set(STIMBUFF_FRAME_PERIOD_MS 33
CACHE STRING "Stimulus buffer frame period (ms)")
if(NOT STIMBUFF_FRAME_PERIOD_MS GREATER 0)
message(FATAL_ERROR
"STIMBUFF_FRAME_PERIOD_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)
# Test configuration
option(ENABLE_TESTS "Enable building tests" OFF)
# Set the debug locks variable for config.h
if(ENABLE_DEBUG_LOCKS)
set(CONFIG_ENABLE_DEBUG_LOCKS TRUE)
endif()
# Set the debug trace callables variable for config.h
if(ENABLE_DEBUG_TRACE_CALLABLES)
set(CONFIG_DEBUG_TRACE_CALLABLES TRUE)
# Suppress frame-address warnings when using __builtin_return_address()
# with values above 0 (See callableTracer.h).
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-frame-address")
endif()
# Set the world thread variable for config.h
if(WORLD_USE_BODY_THREAD)
set(CONFIG_WORLD_USE_BODY_THREAD TRUE)
endif()
# Set the timeout variable for config.h
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})
# Configure config.h # Configure config.h
configure_file( configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/include/config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/include/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/include/config.h ${CMAKE_CURRENT_BINARY_DIR}/include/config.h
@ONLY
) )
# Include directories # Include directories
@@ -36,32 +98,162 @@ include_directories(
) )
# Find core dependencies # Find core dependencies
find_package(Boost 1.69.0 REQUIRED COMPONENTS system) # We cannot use header-only Boost.Asio because we need both our dlopen()'d
# libraries and the main binary to refer to the same instances of boost::asio's
# metadata. If we use header-only Boost.Asio, each dlopen()'d library will have
# 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.
#
# 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 shared libraries, not static Boost libraries.
set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_HEADER_ONLY OFF)
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)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(FLEX REQUIRED) find_package(FLEX REQUIRED)
find_package(BISON 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)
# Normalize find_package variables to match pkg_check_modules naming
set(OPENCL_FOUND TRUE)
set(OPENCL_INCLUDE_DIRS ${OpenCL_INCLUDE_DIRS})
# Handle both OpenCL_LIBRARY (singular) and OpenCL_LIBRARIES (plural)
if(OpenCL_LIBRARIES)
set(OPENCL_LIBRARIES ${OpenCL_LIBRARIES})
else()
set(OPENCL_LIBRARIES ${OpenCL_LIBRARY})
endif()
set(OPENCL_LIBRARY_DIRS "")
message(STATUS "Found OpenCL using find_package")
# Check if version is available and validate
if(OpenCL_VERSION)
if(OpenCL_VERSION VERSION_LESS "1.2")
message(FATAL_ERROR
"OpenCL version ${OpenCL_VERSION} found, but 1.2 or higher is required")
endif()
message(STATUS "OpenCL version: ${OpenCL_VERSION}")
else()
message(WARNING
"OpenCL version could not be determined. "
"Version 1.2+ is required at runtime.")
endif()
else()
# Fall back to pkg-config
pkg_check_modules(OPENCL OpenCL)
if(NOT OPENCL_FOUND)
message(FATAL_ERROR
"Failed to find OpenCL: both find_package and "
"pkg_check_modules failed. Try installing the "
"'ocl-icd-opencl-dev' package (or the appropriate "
"OpenCL development package for your system)."
)
endif()
message(STATUS "Found OpenCL using pkg-config")
message(WARNING
"OpenCL version could not be determined via pkg-config. "
"Version 1.2+ is required at runtime.")
endif()
# Need dlopen() and dlsym() # Need dlopen() and dlsym()
find_library(DL_LIBRARY NAMES dl ldl) find_library(DL_LIBRARY NAMES dl ldl)
if(NOT DL_LIBRARY) if(NOT DL_LIBRARY)
message(FATAL_ERROR "Dynamic linking library (libdl/libldl) not found") message(FATAL_ERROR "Dynamic linking library (libdl/libldl) not found")
endif() endif()
# Add third-party dependencies
if(ENABLE_TESTS)
add_subdirectory(third_party)
set(LIBSPINSCALE_BUILD_TESTS ON CACHE BOOL
"Build libspinscale unit tests" FORCE)
endif()
add_subdirectory(buildmach)
add_subdirectory(libspinscale)
# Add core components # Add core components
add_subdirectory(smocore) add_subdirectory(smocore)
add_subdirectory(commonLibs) add_subdirectory(commonLibs)
add_subdirectory(senseApis) add_subdirectory(comparatorLibs)
add_subdirectory(stimBuffApis)
add_subdirectory(wilzorApis) add_subdirectory(wilzorApis)
add_subdirectory(devices)
# Main executable # Main executable
add_executable(salmanoff main.cpp) add_executable(salmanoff main.cpp)
target_link_libraries(salmanoff target_link_libraries(salmanoff
Boost::system Boost::log
smocore smocore
marionette
deviceManager
senseApis
${Boost_LIBRARIES}
${DL_LIBRARY} ${DL_LIBRARY}
attachmentSupport
) )
install(TARGETS salmanoff DESTINATION bin) # Verify Boost dynamic dependencies after build
add_custom_command(TARGET salmanoff POST_BUILD
COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$<TARGET_FILE:salmanoff>"
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake
COMMENT "Verifying Boost dynamic dependencies for salmanoff"
)
# Add all registered DAPSS targets as dependencies
add_all_daps_dependencies()
add_daps_clean_target()
# Add tests if enabled
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
install(TARGETS salmanoff DESTINATION ${CMAKE_INSTALL_BINDIR})
# Install device configuration files (preprocessed .daps files)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/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 ${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 ${CMAKE_INSTALL_DATADIR}/salmanoff/examples)
endif()
# Include CPack configuration
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CPackConfig.cmake)
include(CPack)
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2024 Salmanoff Project. All rights reserved.
PROPRIETARY SOFTWARE LICENSE
This software and associated documentation files (the "Software") are
proprietary and confidential. The Software is owned exclusively by the
Salmanoff Project and is protected by copyright laws and international
treaty provisions.
NO LICENSE GRANTED. No person or entity is granted any rights or
permissions to use, copy, modify, merge, publish, distribute, sublicense,
sell, or otherwise transfer the Software or any portion thereof without
explicit written permission from the Salmanoff Project.
UNAUTHORIZED USE PROHIBITED. Any unauthorized use, reproduction, or
distribution of the Software is strictly prohibited and may result in
severe civil and criminal penalties.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+84
View File
@@ -0,0 +1,84 @@
# Package Generation
This project supports generating both Debian (.deb) and RPM (.rpm) packages
using CPack.
## Manual Package Generation
### Prerequisites
- CMake 3.16 or later
- Make or Ninja build system
- For RPM packages: `rpmbuild` utility
### Build Process
1. **Create build directory:**
```bash
mkdir -p build-package
cd build-package
```
2. **Configure with CMake:**
```bash
cmake .. -DCMAKE_BUILD_TYPE=Release
```
3. **Build the project:**
```bash
make -j$(nproc)
```
4. **Generate packages:**
```bash
cpack -G DEB # Generate Debian package
cpack -G RPM # Generate RPM package (requires rpmbuild)
```
### Requirements for RPM Generation
To generate RPM packages, you need `rpmbuild` installed:
- **Ubuntu/Debian**: `sudo apt-get install rpm`
- **CentOS/RHEL**: `sudo yum install rpm-build`
- **Fedora**: `sudo dnf install rpm-build`
### Package Contents
The generated packages include:
- **Main executable**: `/usr/bin/salmanoff`
- **Shared libraries**: `/usr/lib/lib*.so`
- **Device configurations**: `/usr/share/salmanoff/devices/` (preprocessed
.daps files)
- **Documentation**: `/usr/share/doc/salmanoff/`
### Installing Packages
**Debian/Ubuntu:**
```bash
sudo dpkg -i salmanoff-0.00.004-x86_64.deb
```
**CentOS/RHEL/Fedora:**
```bash
sudo rpm -i salmanoff-0.00.004-x86_64.rpm
```
### Package Configuration
Package metadata and configuration is defined in
`cmake/CPackConfig.cmake`. This includes:
- Package name, version, and description
- Dependencies and recommendations
- License information
- File naming conventions
### Troubleshooting
- **RPM generation fails**: Ensure `rpmbuild` is installed
- **Missing dependencies**: Check that all build dependencies are
installed
- **Permission errors**: Ensure you have write permissions in the build
directory
+8 -2
View File
@@ -1,10 +1,16 @@
# The Salmanoff Project: # The Salmanoff Project:
![Salmanoff project](docs/img/salmanoff-logo-dark-512.png) <p align="center">
<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. * Gregory `SAL`mieri.
* David Harri`MAN`. * David Harri`MAN`.
* Leonard Peik`OFF`. * Leonard Peik`OFF`.
Would you like to know what this project is and does? Well, it's a secret! But you can find out by reading the code. Or you could just ask me. Or you could wait until I release it. But that's no fun. Would you like to know what this project is and does? Well, it's a secret! But you can find out by reading the code. Or you could just ask me. Or you could wait until I release it. But that's no fun.
For package generation instructions, see [PACKAGING.md](PACKAGING.md).
+41
View File
@@ -0,0 +1,41 @@
# Bug 2: SIGINT before successful frame production results in "Corrupted double-linked list" error:
## 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_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
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.
[Thread 0x7fffb9ff36c0 (LWP 611384) exited]
corrupted double-linked list
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)
```
+78
View File
@@ -0,0 +1,78 @@
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
find_package(OpenCL QUIET)
if(OpenCL_FOUND)
# Normalize find_package variables to match pkg_check_modules naming
set(OPENCL_FOUND TRUE)
set(OPENCL_INCLUDE_DIRS ${OpenCL_INCLUDE_DIRS})
# Handle both OpenCL_LIBRARY (singular) and OpenCL_LIBRARIES (plural)
if(OpenCL_LIBRARIES)
set(OPENCL_LIBRARIES ${OpenCL_LIBRARIES})
else()
set(OPENCL_LIBRARIES ${OpenCL_LIBRARY})
endif()
set(OPENCL_LIBRARY_DIRS "")
message(STATUS "Found OpenCL using find_package")
else()
# Fall back to pkg-config
pkg_check_modules(OPENCL OpenCL)
if(NOT OPENCL_FOUND)
message(FATAL_ERROR
"Failed to find OpenCL: both find_package and "
"pkg_check_modules failed. Try installing the "
"'ocl-icd-opencl-dev' package (or the appropriate "
"OpenCL development package for your system)."
)
endif()
message(STATUS "Found OpenCL using pkg-config")
endif()
add_executable(clhostshmemptrcheck clhostshmemptrcheck.cpp)
target_include_directories(clhostshmemptrcheck
PUBLIC ${OPENCL_INCLUDE_DIRS})
target_link_libraries(clhostshmemptrcheck
${OPENCL_LIBRARIES})
add_executable(clshmemlatency clshmemlatency.cpp)
target_include_directories(clshmemlatency
PUBLIC ${OPENCL_INCLUDE_DIRS})
target_link_libraries(clshmemlatency
${OPENCL_LIBRARIES})
add_executable(clshmemlatency_callback clshmemlatency_callback.cpp)
target_include_directories(clshmemlatency_callback
PUBLIC ${OPENCL_INCLUDE_DIRS})
target_link_libraries(clshmemlatency_callback
${OPENCL_LIBRARIES})
add_executable(clshmemcheck clshmemcheck.cpp)
target_include_directories(clshmemcheck
PUBLIC ${OPENCL_INCLUDE_DIRS})
target_link_libraries(clshmemcheck
${OPENCL_LIBRARIES})
add_executable(clzerocopycheck clzerocopycheck.cpp)
target_include_directories(clzerocopycheck
PUBLIC ${OPENCL_INCLUDE_DIRS})
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()
+125
View File
@@ -0,0 +1,125 @@
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <cstring>
static const char* clErrorToStr(cl_int err)
{
switch(err) {
case CL_SUCCESS: return "CL_SUCCESS";
case CL_INVALID_VALUE: return "CL_INVALID_VALUE";
case CL_INVALID_CONTEXT: return "CL_INVALID_CONTEXT";
case CL_INVALID_MEM_OBJECT: return "CL_INVALID_MEM_OBJECT";
case CL_OUT_OF_HOST_MEMORY: return "CL_OUT_OF_HOST_MEMORY";
case CL_INVALID_OPERATION: return "CL_INVALID_OPERATION";
case CL_MEM_OBJECT_ALLOCATION_FAILURE: return "CL_MEM_OBJECT_ALLOCATION_FAILURE";
default: return "UNKNOWN_ERROR";
}
}
// Try creating a USE_HOST_PTR buffer on this device
bool testUseHostPtr(cl_context ctx, cl_device_id dev)
{
const size_t bufSize = 1024;
std::vector<char> host(bufSize, 0);
cl_int err = 0;
cl_mem buf = clCreateBuffer(
ctx,
CL_MEM_USE_HOST_PTR | CL_MEM_READ_WRITE,
bufSize,
host.data(),
&err
);
if(err != CL_SUCCESS) {
std::cerr << " clCreateBuffer(CL_MEM_USE_HOST_PTR) failed: "
<< clErrorToStr(err) << "\n";
return false;
}
// Try to enqueue a trivial write to verify it works
cl_queue_properties queueProps[] = {CL_QUEUE_PROPERTIES, 0, 0};
cl_command_queue q = clCreateCommandQueueWithProperties(ctx, dev, queueProps, &err);
if(err != CL_SUCCESS){
std::cerr << " Failed to create command queue: "
<< clErrorToStr(err) << "\n";
clReleaseMemObject(buf);
return false;
}
err = clEnqueueWriteBuffer(q, buf, CL_TRUE, 0, bufSize, host.data(), 0, nullptr, nullptr);
clFinish(q);
bool ok = (err == CL_SUCCESS);
if(!ok) {
std::cerr << " clEnqueueWriteBuffer failed: " << clErrorToStr(err) << "\n";
}
clReleaseCommandQueue(q);
clReleaseMemObject(buf);
return ok;
}
int main()
{
cl_uint numPlatforms = 0;
clGetPlatformIDs(0, nullptr, &numPlatforms);
if(numPlatforms == 0){
std::cout << "No OpenCL platforms.\n";
return 0;
}
std::vector<cl_platform_id> plats(numPlatforms);
clGetPlatformIDs(numPlatforms, plats.data(), nullptr);
for(cl_uint p = 0; p < numPlatforms; ++p)
{
char buf[256];
clGetPlatformInfo(plats[p], CL_PLATFORM_NAME, sizeof(buf), buf, nullptr);
std::cout << "Platform: " << buf << "\n";
cl_uint numDevs = 0;
clGetDeviceIDs(plats[p], CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevs);
if(numDevs == 0) {
std::cout << " No devices found on this platform.\n";
continue;
}
std::vector<cl_device_id> devs(numDevs);
clGetDeviceIDs(plats[p], CL_DEVICE_TYPE_ALL, numDevs, devs.data(), nullptr);
for(cl_uint d = 0; d < numDevs; ++d)
{
clGetDeviceInfo(devs[d], CL_DEVICE_NAME, sizeof(buf), buf, nullptr);
std::cout << " Device: " << buf << "\n";
// Create a context for this device
cl_int err;
cl_context ctx = clCreateContext(nullptr, 1, &devs[d], nullptr, nullptr, &err);
if(err != CL_SUCCESS) {
std::cout << " Failed to create context: "
<< clErrorToStr(err) << "\n";
continue;
}
bool supported = testUseHostPtr(ctx, devs[d]);
if(supported)
std::cout << " HOST_PTR appears supported.\n";
else
std::cout << " HOST_PTR appears NOT supported.\n";
clReleaseContext(ctx);
}
}
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <cstring>
#include <cstdlib>
void checkCLError(cl_int err, const char* msg) {
if (err != CL_SUCCESS) {
std::cerr << "OpenCL Error " << err << " at: " << msg << std::endl;
exit(1);
}
}
int main() {
cl_uint numPlatforms = 0;
checkCLError(clGetPlatformIDs(0, nullptr, &numPlatforms), "get num platforms");
std::vector<cl_platform_id> platforms(numPlatforms);
checkCLError(clGetPlatformIDs(numPlatforms, platforms.data(), nullptr), "get platforms");
std::cout << "Found " << numPlatforms << " OpenCL platforms\n\n";
for (cl_uint p = 0; p < numPlatforms; ++p) {
char platformName[256];
clGetPlatformInfo(platforms[p], CL_PLATFORM_NAME, sizeof(platformName), platformName, nullptr);
std::cout << "Platform " << p << ": " << platformName << "\n";
cl_uint numDevices = 0;
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevices);
std::vector<cl_device_id> devices(numDevices);
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, numDevices, devices.data(), nullptr);
for (cl_uint d = 0; d < numDevices; ++d) {
char deviceName[256];
clGetDeviceInfo(devices[d], CL_DEVICE_NAME, sizeof(deviceName), deviceName, nullptr);
std::cout << " Device " << d << ": " << deviceName << "\n";
cl_bool unifiedMem = CL_FALSE;
clGetDeviceInfo(devices[d], CL_DEVICE_HOST_UNIFIED_MEMORY, sizeof(unifiedMem), &unifiedMem, nullptr);
std::cout << " Host-Device unified memory: " << (unifiedMem ? "Yes" : "No") << "\n";
#ifdef CL_DEVICE_SVM_CAPABILITIES
cl_device_svm_capabilities svmCaps = 0;
clGetDeviceInfo(devices[d], CL_DEVICE_SVM_CAPABILITIES, sizeof(svmCaps), &svmCaps, nullptr);
std::cout << " SVM capabilities:\n";
if (!svmCaps) std::cout << " None\n";
if (svmCaps & CL_DEVICE_SVM_COARSE_GRAIN_BUFFER)
std::cout << " - Coarse-grain buffer sharing\n";
if (svmCaps & CL_DEVICE_SVM_FINE_GRAIN_BUFFER)
std::cout << " - Fine-grain buffer sharing\n";
if (svmCaps & CL_DEVICE_SVM_FINE_GRAIN_SYSTEM)
std::cout << " - Fine-grain system sharing\n";
if (svmCaps & CL_DEVICE_SVM_ATOMICS)
std::cout << " - Atomics supported\n";
#endif
// Optional runtime test: check if CL_MEM_USE_HOST_PTR buffer reuses pointer
const size_t bufSize = 1024 * 1024;
std::vector<char> hostBuffer(bufSize, 42);
cl_int err;
cl_context ctx = clCreateContext(nullptr, 1, &devices[d], nullptr, nullptr, &err);
checkCLError(err, "create context");
cl_mem buf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, bufSize, hostBuffer.data(), &err);
checkCLError(err, "create buffer");
cl_queue_properties queueProps[] = {CL_QUEUE_PROPERTIES, 0, 0};
cl_command_queue q = clCreateCommandQueueWithProperties(ctx, devices[d], queueProps, &err);
checkCLError(err, "create queue");
// Simple host → device → host round-trip test
cl_event evt;
auto start = std::chrono::high_resolution_clock::now();
void* mapped = clEnqueueMapBuffer(q, buf, CL_TRUE, CL_MAP_READ, 0, bufSize, 0, nullptr, &evt, &err);
checkCLError(err, "map buffer");
clWaitForEvents(1, &evt);
clEnqueueUnmapMemObject(q, buf, mapped, 0, nullptr, nullptr);
clReleaseMemObject(buf);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
std::cout << " Map latency: " << elapsed.count() << " ms (lower → likely zero-copy)\n";
clReleaseCommandQueue(q);
clReleaseContext(ctx);
}
std::cout << std::endl;
}
return 0;
}
+184
View File
@@ -0,0 +1,184 @@
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <cstring>
#include <cstdlib>
void checkCLError(cl_int err, const char* msg) {
if (err != CL_SUCCESS) {
std::cerr << "OpenCL Error " << err << " at: " << msg << std::endl;
exit(1);
}
}
// --------------------
// Kernel source
// Simple mock kernel that simulates splitting XYZ/I
// Each "point" is 16 bytes (XYZ + Intensity)
const char* kernelSrc = R"CLC(
__kernel void xyz_i_split(__global uchar* assembly,
__global uchar* xyzOut,
__global uchar* iOut,
const uint numPoints) {
uint gid = get_global_id(0);
if (gid >= numPoints) return;
uint offset = gid * 16;
// Copy XYZ (12 bytes) to xyzOut
for (int i=0; i<12; ++i)
xyzOut[gid*12 + i] = assembly[offset + i];
// Copy Intensity (4 bytes) to iOut
for (int i=0; i<4; ++i)
iOut[gid*4 + i] = assembly[offset + 12 + i];
}
)CLC";
int main() {
// --------------------
// CHANGE THIS VALUE to set number of points per assembly buffer
const size_t numPointsPerAssembly = 100000; // e.g., ~3333 points per fill
const size_t bytesPerPoint = 16; // 12 bytes XYZ + 4 bytes I
const size_t assemblyBufSize = numPointsPerAssembly * bytesPerPoint;
const size_t xyzBufSize = numPointsPerAssembly * 12;
const size_t iBufSize = numPointsPerAssembly * 4;
cl_uint numPlatforms = 0;
checkCLError(clGetPlatformIDs(0, nullptr, &numPlatforms), "get num platforms");
std::vector<cl_platform_id> platforms(numPlatforms);
checkCLError(clGetPlatformIDs(numPlatforms, platforms.data(), nullptr), "get platforms");
std::cout << "Found " << numPlatforms << " OpenCL platforms\n\n";
for (cl_uint p = 0; p < numPlatforms; ++p) {
char platformName[256];
clGetPlatformInfo(platforms[p], CL_PLATFORM_NAME, sizeof(platformName), platformName, nullptr);
std::cout << "Platform " << p << ": " << platformName << "\n";
cl_uint numDevices = 0;
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevices);
std::vector<cl_device_id> devices(numDevices);
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, numDevices, devices.data(), nullptr);
for (cl_uint d = 0; d < numDevices; ++d) {
char deviceName[256];
clGetDeviceInfo(devices[d], CL_DEVICE_NAME, sizeof(deviceName), deviceName, nullptr);
std::cout << " Device " << d << ": " << deviceName << "\n";
cl_int err;
cl_context ctx = clCreateContext(nullptr, 1, &devices[d], nullptr, nullptr, &err);
checkCLError(err, "create context");
cl_queue_properties queueProps[] = {CL_QUEUE_PROPERTIES, 0, 0};
cl_command_queue q = clCreateCommandQueueWithProperties(ctx, devices[d], queueProps, &err);
checkCLError(err, "create queue");
// --------------------
// Allocate host buffers
std::vector<unsigned char> assemblyHost(assemblyBufSize, 42);
std::vector<unsigned char> xyzHost(xyzBufSize, 0);
std::vector<unsigned char> iHost(iBufSize, 0);
std::vector<unsigned char> xyzHostCPU(xyzBufSize, 0);
std::vector<unsigned char> iHostCPU(iBufSize, 0);
// Create CL buffers
cl_mem assemblyBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, assemblyBufSize, assemblyHost.data(), &err);
checkCLError(err, "create assembly buffer");
cl_mem xyzBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, xyzBufSize, xyzHost.data(), &err);
checkCLError(err, "create xyz buffer");
cl_mem iBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, iBufSize, iHost.data(), &err);
checkCLError(err, "create i buffer");
// Build program
cl_program prog = clCreateProgramWithSource(ctx, 1, &kernelSrc, nullptr, &err);
checkCLError(err, "create program");
err = clBuildProgram(prog, 1, &devices[d], nullptr, nullptr, nullptr);
if (err != CL_SUCCESS) {
// Print build log
size_t logSize = 0;
clGetProgramBuildInfo(prog, devices[d], CL_PROGRAM_BUILD_LOG, 0, nullptr, &logSize);
std::vector<char> log(logSize);
clGetProgramBuildInfo(prog, devices[d], CL_PROGRAM_BUILD_LOG, logSize, log.data(), nullptr);
std::cerr << log.data() << "\n";
}
checkCLError(err, "build program");
cl_kernel kernel = clCreateKernel(prog, "xyz_i_split", &err);
checkCLError(err, "create kernel");
// Set kernel args
clSetKernelArg(kernel, 0, sizeof(cl_mem), &assemblyBuf);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &xyzBuf);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &iBuf);
clSetKernelArg(kernel, 3, sizeof(cl_uint), &numPointsPerAssembly);
const size_t globalWorkSize = numPointsPerAssembly;
// --------------------
// Run a few iterations
for (int iter = 0; iter < 10; ++iter) {
cl_event evt;
auto t0 = std::chrono::high_resolution_clock::now();
void* mappedAssembly = clEnqueueMapBuffer(q, assemblyBuf, CL_TRUE, CL_MAP_READ, 0, assemblyBufSize, 0, nullptr, &evt, &err);
checkCLError(err, "map assembly buffer");
clWaitForEvents(1, &evt);
auto t1 = std::chrono::high_resolution_clock::now();
err = clEnqueueNDRangeKernel(q, kernel, 1, nullptr, &globalWorkSize, nullptr, 0, nullptr, &evt);
checkCLError(err, "enqueue kernel");
clWaitForEvents(1, &evt);
auto t2 = std::chrono::high_resolution_clock::now();
cl_event unmapEvt;
err = clEnqueueUnmapMemObject(q, assemblyBuf, mappedAssembly, 0, nullptr, &unmapEvt);
checkCLError(err, "unmap assembly buffer");
clWaitForEvents(1, &unmapEvt);
auto t3 = std::chrono::high_resolution_clock::now();
// --------------------
// Host CPU split
auto cpuStart = std::chrono::high_resolution_clock::now();
for (size_t pt = 0; pt < numPointsPerAssembly; ++pt) {
size_t off = pt * 16;
for (int i = 0; i < 12; ++i)
xyzHostCPU[pt*12 + i] = assemblyHost[off + i];
for (int i = 0; i < 4; ++i)
iHostCPU[pt*4 + i] = assemblyHost[off + 12 + i];
}
auto cpuEnd = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> mapElapsed = t1 - t0;
std::chrono::duration<double, std::milli> kernelElapsed = t2 - t1;
std::chrono::duration<double, std::milli> unmapElapsed = t3 - t2;
std::chrono::duration<double, std::milli> cpuElapsed = cpuEnd - cpuStart;
std::cout << "Iteration " << iter
<< " | Map: " << mapElapsed.count()
<< " ms | Kernel: " << kernelElapsed.count()
<< " ms | Unmap: " << unmapElapsed.count()
<< " ms | CPU Split: " << cpuElapsed.count() << " ms\n";
}
// Cleanup
clReleaseKernel(kernel);
clReleaseProgram(prog);
clReleaseMemObject(assemblyBuf);
clReleaseMemObject(xyzBuf);
clReleaseMemObject(iBuf);
clReleaseCommandQueue(q);
clReleaseContext(ctx);
}
std::cout << std::endl;
}
return 0;
}
+300
View File
@@ -0,0 +1,300 @@
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <cstring>
#include <cstdlib>
#include <mutex>
#include <condition_variable>
void checkCLError(cl_int err, const char* msg) {
if (err != CL_SUCCESS) {
std::cerr << "OpenCL Error " << err << " at: " << msg << std::endl;
exit(1);
}
}
// Callback context for waiting on events
struct CallbackContext {
std::mutex mtx;
std::condition_variable cv;
bool completed;
cl_int status;
std::chrono::high_resolution_clock::time_point* timestamp;
};
// Helper function to wait for callback completion
void waitForCallback(CallbackContext& ctx) {
std::unique_lock<std::mutex> lock(ctx.mtx);
ctx.cv.wait(lock, [&ctx] { return ctx.completed; });
std::cout <<"waitForCallback cv.wait() returned.\n";
}
// Static callback for map buffer event
void CL_CALLBACK mapEventCallback(cl_event /*event*/, cl_int event_command_exec_status, void* user_data) {
CallbackContext* ctx = static_cast<CallbackContext*>(user_data);
std::cout <<"mapEventCallback called and about to lock mutex.\n";
std::unique_lock<std::mutex> lock(ctx->mtx);
ctx->status = event_command_exec_status;
if (ctx->timestamp) {
*ctx->timestamp = std::chrono::high_resolution_clock::now();
}
ctx->completed = true;
ctx->cv.notify_one();
std::cout <<"mapEventCallback just notified.\n";
}
// Static callback for kernel execution event
void CL_CALLBACK kernelEventCallback(cl_event /*event*/, cl_int event_command_exec_status, void* user_data) {
CallbackContext* ctx = static_cast<CallbackContext*>(user_data);
std::cout <<"mapEventCallback called and about to lock mutex.\n";
std::unique_lock<std::mutex> lock(ctx->mtx);
ctx->status = event_command_exec_status;
if (ctx->timestamp) {
*ctx->timestamp = std::chrono::high_resolution_clock::now();
}
ctx->completed = true;
ctx->cv.notify_one();
std::cout <<"mapEventCallback just notified.\n";
}
// Static callback for unmap buffer event
void CL_CALLBACK unmapEventCallback(cl_event /*event*/, cl_int event_command_exec_status, void* user_data) {
CallbackContext* ctx = static_cast<CallbackContext*>(user_data);
std::cout <<"mapEventCallback called and about to lock mutex.\n";
std::unique_lock<std::mutex> lock(ctx->mtx);
ctx->status = event_command_exec_status;
if (ctx->timestamp) {
*ctx->timestamp = std::chrono::high_resolution_clock::now();
}
ctx->completed = true;
ctx->cv.notify_one();
std::cout <<"mapEventCallback just notified.\n";
}
// --------------------
// Kernel source
// Simple mock kernel that simulates splitting XYZ/I
// Each "point" is 16 bytes (XYZ + Intensity)
const char* kernelSrc = R"CLC(
__kernel void xyz_i_split(__global uchar* assembly,
__global uchar* xyzOut,
__global uchar* iOut,
const uint numPoints) {
uint gid = get_global_id(0);
if (gid >= numPoints) return;
uint offset = gid * 16;
// Copy XYZ (12 bytes) to xyzOut
for (int i=0; i<12; ++i)
xyzOut[gid*12 + i] = assembly[offset + i];
// Copy Intensity (4 bytes) to iOut
for (int i=0; i<4; ++i)
iOut[gid*4 + i] = assembly[offset + 12 + i];
}
)CLC";
int main() {
// --------------------
// CHANGE THIS VALUE to set number of points per assembly buffer
const size_t numPointsPerAssembly = 100000; // e.g., ~3333 points per fill
const size_t bytesPerPoint = 16; // 12 bytes XYZ + 4 bytes I
const size_t assemblyBufSize = numPointsPerAssembly * bytesPerPoint;
const size_t xyzBufSize = numPointsPerAssembly * 12;
const size_t iBufSize = numPointsPerAssembly * 4;
cl_uint numPlatforms = 0;
checkCLError(clGetPlatformIDs(0, nullptr, &numPlatforms), "get num platforms");
std::vector<cl_platform_id> platforms(numPlatforms);
checkCLError(clGetPlatformIDs(numPlatforms, platforms.data(), nullptr), "get platforms");
std::cout << "Found " << numPlatforms << " OpenCL platforms\n\n";
for (cl_uint p = 0; p < numPlatforms; ++p) {
char platformName[256];
clGetPlatformInfo(platforms[p], CL_PLATFORM_NAME, sizeof(platformName), platformName, nullptr);
std::cout << "Platform " << p << ": " << platformName << "\n";
cl_uint numDevices = 0;
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevices);
std::vector<cl_device_id> devices(numDevices);
clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, numDevices, devices.data(), nullptr);
for (cl_uint d = 0; d < numDevices; ++d) {
char deviceName[256];
clGetDeviceInfo(devices[d], CL_DEVICE_NAME, sizeof(deviceName), deviceName, nullptr);
std::cout << " Device " << d << ": " << deviceName << "\n";
cl_int err;
cl_context ctx = clCreateContext(nullptr, 1, &devices[d], nullptr, nullptr, &err);
checkCLError(err, "create context");
cl_queue_properties queueProps[] = {CL_QUEUE_PROPERTIES, 0, 0};
cl_command_queue q = clCreateCommandQueueWithProperties(ctx, devices[d], queueProps, &err);
checkCLError(err, "create queue");
// --------------------
// Allocate host buffers
std::vector<unsigned char> assemblyHost(assemblyBufSize, 42);
std::vector<unsigned char> xyzHost(xyzBufSize, 0);
std::vector<unsigned char> iHost(iBufSize, 0);
std::vector<unsigned char> xyzHostCPU(xyzBufSize, 0);
std::vector<unsigned char> iHostCPU(iBufSize, 0);
// Create CL buffers
cl_mem assemblyBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, assemblyBufSize, assemblyHost.data(), &err);
checkCLError(err, "create assembly buffer");
cl_mem xyzBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, xyzBufSize, xyzHost.data(), &err);
checkCLError(err, "create xyz buffer");
cl_mem iBuf = clCreateBuffer(ctx, CL_MEM_USE_HOST_PTR, iBufSize, iHost.data(), &err);
checkCLError(err, "create i buffer");
// Build program
cl_program prog = clCreateProgramWithSource(ctx, 1, &kernelSrc, nullptr, &err);
checkCLError(err, "create program");
err = clBuildProgram(prog, 1, &devices[d], nullptr, nullptr, nullptr);
if (err != CL_SUCCESS) {
// Print build log
size_t logSize = 0;
clGetProgramBuildInfo(prog, devices[d], CL_PROGRAM_BUILD_LOG, 0, nullptr, &logSize);
std::vector<char> log(logSize);
clGetProgramBuildInfo(prog, devices[d], CL_PROGRAM_BUILD_LOG, logSize, log.data(), nullptr);
std::cerr << log.data() << "\n";
}
checkCLError(err, "build program");
cl_kernel kernel = clCreateKernel(prog, "xyz_i_split", &err);
checkCLError(err, "create kernel");
// Set kernel args
clSetKernelArg(kernel, 0, sizeof(cl_mem), &assemblyBuf);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &xyzBuf);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &iBuf);
clSetKernelArg(kernel, 3, sizeof(cl_uint), &numPointsPerAssembly);
const size_t globalWorkSize = numPointsPerAssembly;
// --------------------
// Run a few iterations
for (int iter = 0; iter < 10; ++iter) {
auto t0 = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point t1, t2, t3;
cl_event mapEvt;
void* mappedAssembly = clEnqueueMapBuffer(q, assemblyBuf, CL_FALSE, CL_MAP_READ, 0, assemblyBufSize, 0, nullptr, &mapEvt, &err);
checkCLError(err, "map assembly buffer");
// Retain event to keep it alive until callback completes
err = clRetainEvent(mapEvt);
checkCLError(err, "retain map event");
// Wait for map event using callback
CallbackContext mapCtx;
mapCtx.completed = false;
mapCtx.timestamp = &t1;
err = clSetEventCallback(mapEvt, CL_COMPLETE, mapEventCallback, &mapCtx);
checkCLError(err, "set map event callback");
// Force queue flush to ensure event processing
err = clFlush(q);
checkCLError(err, "flush queue");
std::cout <<"About to waitForCalllback for clEnqueueMapBuffer.\n";
waitForCallback(mapCtx);
checkCLError(mapCtx.status, "map buffer");
// Release event after callback completes
err = clReleaseEvent(mapEvt);
checkCLError(err, "release map event");
cl_event kernelEvt;
err = clEnqueueNDRangeKernel(q, kernel, 1, nullptr, &globalWorkSize, nullptr, 0, nullptr, &kernelEvt);
checkCLError(err, "enqueue kernel");
// Retain event to keep it alive until callback completes
err = clRetainEvent(kernelEvt);
checkCLError(err, "retain kernel event");
// Wait for kernel event using callback
CallbackContext kernelCtx;
kernelCtx.completed = false;
kernelCtx.timestamp = &t2;
err = clSetEventCallback(kernelEvt, CL_COMPLETE, kernelEventCallback, &kernelCtx);
checkCLError(err, "set kernel event callback");
// Force queue flush to ensure event processing
err = clFlush(q);
checkCLError(err, "flush queue");
std::cout <<"About to waitForCalllback for clEnqueueNDRangeKernel.\n";
waitForCallback(kernelCtx);
checkCLError(kernelCtx.status, "kernel execution");
// Release event after callback completes
err = clReleaseEvent(kernelEvt);
checkCLError(err, "release kernel event");
cl_event unmapEvt;
err = clEnqueueUnmapMemObject(q, assemblyBuf, mappedAssembly, 0, nullptr, &unmapEvt);
checkCLError(err, "unmap assembly buffer");
// Retain event to keep it alive until callback completes
err = clRetainEvent(unmapEvt);
checkCLError(err, "retain unmap event");
// Wait for unmap event using callback
CallbackContext unmapCtx;
unmapCtx.completed = false;
unmapCtx.timestamp = &t3;
err = clSetEventCallback(unmapEvt, CL_COMPLETE, unmapEventCallback, &unmapCtx);
checkCLError(err, "set unmap event callback");
// Force queue flush to ensure event processing
err = clFlush(q);
checkCLError(err, "flush queue");
std::cout <<"About to waitForCalllback for clEnqueueUnmapMemObject.\n";
waitForCallback(unmapCtx);
checkCLError(unmapCtx.status, "unmap buffer");
// Release event after callback completes
err = clReleaseEvent(unmapEvt);
checkCLError(err, "release unmap event");
// --------------------
// Host CPU split
auto cpuStart = std::chrono::high_resolution_clock::now();
for (size_t pt = 0; pt < numPointsPerAssembly; ++pt) {
size_t off = pt * 16;
for (int i = 0; i < 12; ++i)
xyzHostCPU[pt*12 + i] = assemblyHost[off + i];
for (int i = 0; i < 4; ++i)
iHostCPU[pt*4 + i] = assemblyHost[off + 12 + i];
}
auto cpuEnd = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> mapElapsed = t1 - t0;
std::chrono::duration<double, std::milli> kernelElapsed = t2 - t1;
std::chrono::duration<double, std::milli> unmapElapsed = t3 - t2;
std::chrono::duration<double, std::milli> cpuElapsed = cpuEnd - cpuStart;
std::cout << "Iteration " << iter
<< " | Map: " << mapElapsed.count()
<< " ms | Kernel: " << kernelElapsed.count()
<< " ms | Unmap: " << unmapElapsed.count()
<< " ms | CPU Split: " << cpuElapsed.count() << " ms\n";
}
// Cleanup
clReleaseKernel(kernel);
clReleaseProgram(prog);
clReleaseMemObject(assemblyBuf);
clReleaseMemObject(xyzBuf);
clReleaseMemObject(iBuf);
clReleaseCommandQueue(q);
clReleaseContext(ctx);
}
std::cout << std::endl;
}
return 0;
}
+117
View File
@@ -0,0 +1,117 @@
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <cstring>
#define CHECK(err, msg) \
if (err != CL_SUCCESS) { \
std::cerr << "ERROR: " << msg << " (" << err << ")\n"; \
return 1; \
}
const char *kernelSrc = R"CLC(
__kernel void check_shared(__global const int* in, __global int* out) {
int gid = get_global_id(0);
out[gid] = in[gid] + 42; // simple deterministic transform
}
)CLC";
int main() {
cl_int err;
// Pick first available device
cl_uint numPlatforms;
CHECK(clGetPlatformIDs(0, nullptr, &numPlatforms), "clGetPlatformIDs count");
std::vector<cl_platform_id> plats(numPlatforms);
CHECK(clGetPlatformIDs(numPlatforms, plats.data(), nullptr), "clGetPlatformIDs");
cl_platform_id plat = plats[0];
cl_device_id dev;
CHECK(clGetDeviceIDs(plat, CL_DEVICE_TYPE_GPU, 1, &dev, nullptr), "clGetDeviceIDs");
cl_context ctx = clCreateContext(nullptr, 1, &dev, nullptr, nullptr, &err);
CHECK(err, "clCreateContext");
cl_queue_properties queueProps[] = {CL_QUEUE_PROPERTIES, 0, 0};
cl_command_queue q = clCreateCommandQueueWithProperties(ctx, dev, queueProps, &err);
CHECK(err, "clCreateCommandQueueWithProperties");
// Create program and kernel
const size_t srcLen = std::strlen(kernelSrc);
cl_program prog = clCreateProgramWithSource(ctx, 1, &kernelSrc, &srcLen, &err);
CHECK(err, "clCreateProgramWithSource");
err = clBuildProgram(prog, 1, &dev, nullptr, nullptr, nullptr);
if (err != CL_SUCCESS) {
size_t logSize;
clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, 0, nullptr, &logSize);
std::vector<char> log(logSize);
clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, logSize, log.data(), nullptr);
std::cerr << "--- Build Log ---\n" << log.data() << "\n";
return 1;
}
cl_kernel krn = clCreateKernel(prog, "check_shared", &err);
CHECK(err, "clCreateKernel");
const size_t N = 8;
size_t bufSize = N * sizeof(int);
// Allocate host-visible buffer
cl_mem bufIn = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_ALLOC_HOST_PTR, bufSize, nullptr, &err);
CHECK(err, "clCreateBuffer input");
cl_mem bufOut = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY | CL_MEM_ALLOC_HOST_PTR, bufSize, nullptr, &err);
CHECK(err, "clCreateBuffer output");
// Map the buffer (should return pointer to real host memory if unified)
int* hostPtr = (int*)clEnqueueMapBuffer(q, bufIn, CL_TRUE, CL_MAP_WRITE, 0, bufSize, 0, nullptr, nullptr, &err);
CHECK(err, "clEnqueueMapBuffer");
std::cout << "Mapped host pointer: " << static_cast<void*>(hostPtr) << "\n";
// Write pattern directly into mapped memory
for (size_t i = 0; i < N; ++i)
hostPtr[i] = 100 + i;
// No clEnqueueWriteBuffer call! We rely on shared memory.
clEnqueueUnmapMemObject(q, bufIn, hostPtr, 0, nullptr, nullptr);
clFinish(q);
// Set kernel args
clSetKernelArg(krn, 0, sizeof(cl_mem), &bufIn);
clSetKernelArg(krn, 1, sizeof(cl_mem), &bufOut);
size_t global = N;
err = clEnqueueNDRangeKernel(q, krn, 1, nullptr, &global, nullptr, 0, nullptr, nullptr);
CHECK(err, "clEnqueueNDRangeKernel");
clFinish(q);
// Read back result
int* outPtr = (int*)clEnqueueMapBuffer(q, bufOut, CL_TRUE, CL_MAP_READ, 0, bufSize, 0, nullptr, nullptr, &err);
CHECK(err, "map output");
std::cout << "Result: ";
for (size_t i = 0; i < N; ++i)
std::cout << outPtr[i] << " ";
std::cout << "\n";
// Validate
bool ok = true;
for (size_t i = 0; i < N; ++i)
if (outPtr[i] != static_cast<int>(142 + i)) ok = false;
std::cout << (ok ? "✅ GPU saw host writes (zero-copy confirmed)\n"
: "❌ GPU did not see host writes (copying or staging occurred)\n");
clEnqueueUnmapMemObject(q, bufOut, outPtr, 0, nullptr, nullptr);
clFinish(q);
clReleaseMemObject(bufIn);
clReleaseMemObject(bufOut);
clReleaseKernel(krn);
clReleaseProgram(prog);
clReleaseCommandQueue(q);
clReleaseContext(ctx);
return 0;
}
+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;
}
+114
View File
@@ -0,0 +1,114 @@
# CPack configuration for package generation
# This file contains all CPack settings for generating deb and rpm packages
# Set package metadata using project variables
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY
"Cognitive robotics runtime")
set(CPACK_PACKAGE_VENDOR "Salmanoff Project")
set(CPACK_PACKAGE_CONTACT "maintainer@salmanoff.org")
# 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")
# DEB package specific settings (Ubuntu)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER
"Salmanoff Project <maintainer@salmanoff.org>")
set(CPACK_DEBIAN_PACKAGE_SECTION "science")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
# Target Ubuntu distribution
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, libcamera-dev")
# 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, 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, 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}-${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}")
# Enable automatic dependency detection for Debian packages
# This uses dpkg-shlibdeps to automatically detect shared library dependencies
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)
+197
View File
@@ -0,0 +1,197 @@
# DAPSS (Device Attachment Pipe Specification Source) preprocessing module
# This module provides functionality to preprocess .dapss files to .daps files
# using the C preprocessor, respecting include directories and target dependencies.
#
# Usage:
# 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)
# register_daps_target(device_specs)
# add_all_daps_dependencies()
#
# The preprocessed .daps files will be placed in ${CMAKE_CURRENT_BINARY_DIR}/
# Function to add a DAPSS preprocessing target
# Usage: add_daps_target(target_name SOURCES file1.dapss file2.dapss ...)
function(add_daps_target target_name)
set(options)
set(oneValueArgs)
set(multiValueArgs SOURCES)
cmake_parse_arguments(DAPS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT DAPS_SOURCES)
message(FATAL_ERROR "add_daps_target: No SOURCES specified for target ${target_name}")
endif()
# Use binary directory directly for processed files
# This ensures files are created in the same directory as the target
set(output_dir "${CMAKE_CURRENT_BINARY_DIR}")
# List to store all output files
set(output_files)
# Process each source file
foreach(source_file ${DAPS_SOURCES})
# Get the base name without extension
get_filename_component(base_name ${source_file} NAME_WE)
get_filename_component(source_dir ${source_file} DIRECTORY)
# Create output file path
set(output_file "${output_dir}/${base_name}.daps")
list(APPEND output_files ${output_file})
# Get include directories from current directory and target
get_directory_property(include_dirs INCLUDE_DIRECTORIES)
# Build include flags
set(include_flags)
foreach(include_dir ${include_dirs})
list(APPEND include_flags "-I${include_dir}")
endforeach()
list(APPEND include_flags "-I${CMAKE_CURRENT_SOURCE_DIR}")
# 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)
find_program(CMAKE_C_COMPILER gcc cc clang)
if(NOT CMAKE_C_COMPILER)
message(FATAL_ERROR "No C compiler found for DAPSS preprocessing")
endif()
endif()
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 "${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})
# Make the target part of the ALL target so it gets built by default
# This ensures it gets built when building just this subdirectory
set_target_properties(${target_name} PROPERTIES
FOLDER "${CMAKE_CURRENT_SOURCE_DIR}"
EXCLUDE_FROM_ALL FALSE
)
# Set target properties
set_target_properties(${target_name} PROPERTIES
DAPS_OUTPUT_DIR ${output_dir}
DAPS_OUTPUT_FILES "${output_files}"
)
# Make the target available globally
set(${target_name}_OUTPUT_DIR ${output_dir} PARENT_SCOPE)
set(${target_name}_OUTPUT_FILES "${output_files}" PARENT_SCOPE)
endfunction()
# Function to register a DAPSS target for later dependency addition
# Usage: register_daps_target(target_name)
# This stores the target name in a global property for later use
function(register_daps_target target_name)
# Store the target name in a global property
get_property(registered_targets GLOBAL PROPERTY DAPS_REGISTERED_TARGETS)
list(APPEND registered_targets ${target_name})
set_property(GLOBAL PROPERTY DAPS_REGISTERED_TARGETS ${registered_targets})
message(STATUS "Registered DAPSS target ${target_name} for later dependency addition")
endfunction()
# Function to add all registered DAPSS targets as dependencies
# Usage: add_all_daps_dependencies([TARGET main_target] [CONDITION condition_expression])
# This should be called from the main CMakeLists.txt after all subdirectories are processed
function(add_all_daps_dependencies)
set(options)
set(oneValueArgs TARGET CONDITION)
set(multiValueArgs)
cmake_parse_arguments(DAPS_ALL "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# Default target is PROJECT_NAME
if(DAPS_ALL_TARGET)
set(dep_target ${DAPS_ALL_TARGET})
else()
set(dep_target ${PROJECT_NAME})
endif()
# Get all registered targets
get_property(registered_targets GLOBAL PROPERTY DAPS_REGISTERED_TARGETS)
if(registered_targets)
foreach(target_name ${registered_targets})
if(TARGET ${target_name})
if(DAPS_ALL_CONDITION)
if(${DAPS_ALL_CONDITION})
add_dependencies(${dep_target} ${target_name})
message(STATUS "Added registered DAPSS target ${target_name} as dependency of ${dep_target} (condition: ${DAPS_ALL_CONDITION})")
else()
message(STATUS "Skipped registered DAPSS target ${target_name} (condition: ${DAPS_ALL_CONDITION} not met)")
endif()
else()
add_dependencies(${dep_target} ${target_name})
message(STATUS "Added registered DAPSS target ${target_name} as dependency of ${dep_target}")
endif()
else()
message(WARNING "Registered DAPSS target ${target_name} does not exist")
endif()
endforeach()
else()
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()
+25
View File
@@ -0,0 +1,25 @@
# DebugOpts.cmake - Debug configuration options
# Enable debug locking features
option(ENABLE_DEBUG_LOCKS "Enable debug features for locking system" ON)
# Enable callable tracing for debugging boost::asio post operations
option(ENABLE_DEBUG_TRACE_CALLABLES "Enable callable tracing for debugging boost::asio post operations" OFF)
# Qutex deadlock detection configuration
# Always define the variable in cache so it appears in ccmake
set(DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS 500 CACHE STRING
"Timeout in milliseconds for deadlock detection in qutex system")
if(ENABLE_DEBUG_LOCKS)
# Validate the timeout value
if(NOT DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS OR DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS STREQUAL "")
message(FATAL_ERROR "DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS must be a positive integer > 0")
endif()
# Convert to integer and validate
math(EXPR timeout_int "${DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS}")
if(timeout_int LESS_EQUAL 0)
message(FATAL_ERROR "DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS must be a positive integer > 0")
endif()
endif()
+63
View File
@@ -0,0 +1,63 @@
# SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY
# Verifies that a target file (executable or shared library) has Boost libraries
# in its dynamic dependency list via ldd.
#
# Usage as function:
# SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY(<target_file>)
#
# Usage as script (with -P):
# cmake -DVERIFY_FILE=<target_file> -P VerifyBoostDynamic.cmake
#
# This function/script:
# 1. Runs ldd on the target file
# 2. Checks for boost libraries in the dependency list
# 3. Reports success or failure with appropriate messages
#
function(SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY target_file)
_verify_boost_dynamic_dependency("${target_file}")
endfunction()
# Internal implementation that can be called from script mode or function mode
function(_verify_boost_dynamic_dependency target_file)
if(NOT EXISTS "${target_file}")
message(WARNING "SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY: Target file '${target_file}' does not exist")
return()
endif()
# Run ldd on the target file
execute_process(
COMMAND ldd "${target_file}"
OUTPUT_VARIABLE ldd_output
ERROR_VARIABLE ldd_error
RESULT_VARIABLE ldd_result
)
if(ldd_result)
message(WARNING "SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY: Failed to run ldd on '${target_file}': ${ldd_error}")
return()
endif()
# Check if output contains boost libraries
string(TOLOWER "${ldd_output}" ldd_output_lower)
string(FIND "${ldd_output_lower}" "libboost" boost_found)
if(boost_found EQUAL -1)
message(STATUS "SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY: WARNING - No Boost libraries found in dependencies of '${target_file}'")
message(STATUS "ldd output:")
message(STATUS "${ldd_output}")
else()
# Extract boost library lines
string(REGEX MATCHALL "libboost[^\n]*" boost_libs "${ldd_output}")
message(STATUS "SMO_VERIFY_BOOST_DYNAMIC_DEPENDENCY: SUCCESS - Boost libraries found in '${target_file}':")
foreach(boost_lib ${boost_libs})
string(STRIP "${boost_lib}" boost_lib_stripped)
message(STATUS " ${boost_lib_stripped}")
endforeach()
endif()
endfunction()
# Script mode: if VERIFY_FILE is defined, run the verification
if(VERIFY_FILE)
_verify_boost_dynamic_dependency("${VERIFY_FILE}")
endif()
+1 -1
View File
@@ -31,7 +31,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Search for libraries and headers in the target directories # Search for libraries and headers in the target directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Set pkg-config to use the cross-compiled libraries # Set pkg-config to use the cross-compiled libraries
set(ENV{PKG_CONFIG_PATH} "/usr/aarch64-linux-gnu/lib/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig") set(ENV{PKG_CONFIG_PATH} "/usr/aarch64-linux-gnu/lib/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig")
+91
View File
@@ -0,0 +1,91 @@
# Clang toolchain file for native builds
# This file should be used with cmake -DCMAKE_TOOLCHAIN_FILE=cmake/clang-native.cmake
# Disable cross-compilation
set(CMAKE_CROSSCOMPILING FALSE)
# Target OS (native)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR})
# Specify the Clang compilers
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
# Set Clang-specific compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic")
# Enable C++20 standard (as specified in main CMakeLists.txt)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Set Clang-specific optimization flags
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -DNDEBUG")
# Set debug flags
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
# Enable address sanitizer in debug builds (optional)
# Uncomment the following lines if you want to enable address sanitizer
# set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fno-omit-frame-pointer")
# set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address -fno-omit-frame-pointer")
# set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=address")
# set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} -fsanitize=address")
# Enable undefined behavior sanitizer in debug builds (optional)
# Uncomment the following lines if you want to enable UBSan
# set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=undefined -fno-omit-frame-pointer")
# set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=undefined -fno-omit-frame-pointer")
# set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=undefined")
# set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} -fsanitize=undefined")
# Set native search paths (use system defaults)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
# Clang-specific linker flags
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
# Enable link-time optimization in release builds (optional)
# Uncomment the following lines if you want to enable LTO
# set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")
# set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -flto")
# set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -flto")
# set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -flto")
# Set Clang-specific C++ features
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++")
# Alternative: Use libstdc++ instead of libc++ (uncomment if preferred)
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libstdc++")
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libstdc++")
# set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libstdc++")
# Set compiler-specific features
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcolor-diagnostics")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fcolor-diagnostics")
# Enable all warnings and treat them as errors in debug builds (optional)
# Uncomment the following lines if you want to treat warnings as errors
# set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror")
# set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Werror")
# Set Clang-specific optimization flags
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native -mtune=native")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -march=native -mtune=native")
# Print configuration information
message(STATUS "Clang toolchain configuration:")
message(STATUS " C Compiler: ${CMAKE_C_COMPILER}")
message(STATUS " CXX Compiler: ${CMAKE_CXX_COMPILER}")
message(STATUS " CXX Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS " Cross-compiling: ${CMAKE_CROSSCOMPILING}")
+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
+89
View File
@@ -0,0 +1,89 @@
# 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
# INPUT_FILE: Path to the .ll input file
# 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)
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)
_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})
# Set up dependencies
set(DEPENDENCIES ${INPUT_FILE})
if(ARGC GREATER 3)
list(APPEND DEPENDENCIES ${ARGV3})
endif()
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
# Usage: generate_bison_parser(OUTPUT_VAR HEADER_VAR INPUT_FILE [PREFIX])
# OUTPUT_VAR: Variable name to store the output .cc file path
# HEADER_VAR: Variable name to store the output .hh file path
# 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)
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)
_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})
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
# Generate Bison parser first (creates the header file)
generate_bison_parser(YACC_OUTPUT YACC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/deviceManager/deviceAttachmentPipeSpecp.yy deviceAttachmentPipeSpecp)
# Generate Flex lexer with dependency on Bison header
generate_flex_lexer(LEX_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/deviceManager/deviceAttachmentPipeSpecl.ll deviceAttachmentPipeSpecl ${YACC_HEADER})
+70
View File
@@ -0,0 +1,70 @@
# ----------------------------------------------------------------------------------
# MANDATORY USER VARIABLE
# ----------------------------------------------------------------------------------
# IMPORTANT: This variable MUST be set when running CMake to specify where the
# laptop's sysroot (the root directory of the mounted laptop filesystem) is located.
#
# Usage example: cmake -DCMAKE_TOOLCHAIN_FILE=laptop_x86_sysroot.cmake
# -DTARGET_SYSROOT=/mnt/laptop_sysroot/ <path_to_source>
#
# If the variable is not defined, we fall back to a common system root path for safety.
if(NOT DEFINED TARGET_SYSROOT)
set(TARGET_SYSROOT "/usr/lib/x86_64-linux-gnu")
message(STATUS "TARGET_SYSROOT not explicitly defined. Defaulting to ${TARGET_SYSROOT}")
endif()
message(STATUS "Using TARGET_SYSROOT: ${TARGET_SYSROOT}")
set(TARGET_TRIPLE x86_64-linux-gnu) # Standard Debian/Ubuntu triple
# ----------------------------------------------------------------------------------
# SYSROOT and COMPILER CONFIGURATION
# ----------------------------------------------------------------------------------
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_SYSROOT ${TARGET_SYSROOT})
message(STATUS "Using CMAKE_SYSROOT: ${CMAKE_SYSROOT}")
# The CMAKE_FIND_ROOT_PATH tells CMake where to look for programs, libraries, etc.
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# 1. Architecture and Platform Identification
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER ${TARGET_TRIPLE}-gcc)
set(CMAKE_CXX_COMPILER ${TARGET_TRIPLE}-g++)
# ----------------------------------------------------------------------------------
# PKG-CONFIG CONFIGURATION (CRUCIAL FOR CROSS-COMPILING)
# ----------------------------------------------------------------------------------
# 1. Define the search path for .pc files, relative to the sysroot.
# This ensures we look in the target's standard pkgconfig locations.
set(PKG_CONFIG_SEARCH_PATHS
"${CMAKE_SYSROOT}/usr/lib/${TARGET_TRIPLE}/pkgconfig" # Primary location on Debian/Ubuntu
"${CMAKE_SYSROOT}/usr/share/pkgconfig" # Secondary shared location
"${CMAKE_SYSROOT}/usr/lib/pkgconfig" # Another common location
)
# Join the paths using the system's path separator (colon on Linux)
string(REPLACE ";" ":" PKG_CONFIG_LIBDIR_STRING "${PKG_CONFIG_SEARCH_PATHS}")
# Set the environment variable PKG_CONFIG_LIBDIR
# This tells pkg-config exactly where to find the x86_64 .pc files.
# 2. Set the sysroot directory for pkg-config
# This tells pkg-config to prepend CMAKE_SYSROOT to any paths it finds in the .pc files.
set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT})
set(ENV{PKG_CONFIG_LIBDIR} ${PKG_CONFIG_LIBDIR_STRING})
set(ENV{PKG_CONFIG_PATH} "")
message(STATUS "PKG_CONFIG_SYSROOT_DIR set to: ${CMAKE_SYSROOT}")
message(STATUS "PKG_CONFIG_LIBDIR set to: ${PKG_CONFIG_LIBDIR_STRING}")
# ----------------------------------------------------------------------------------
# CMAkE FIND BEHAVIOR
# ----------------------------------------------------------------------------------
+3
View File
@@ -1 +1,4 @@
add_subdirectory(xcbXorg) add_subdirectory(xcbXorg)
add_subdirectory(livoxProto1)
add_subdirectory(lcameraDev)
add_subdirectory(attachmentSupport)
@@ -0,0 +1,50 @@
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
add_custom_command(TARGET attachmentSupport POST_BUILD
COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$<TARGET_FILE:attachmentSupport>"
-P ${CMAKE_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake
COMMENT "Verifying Boost dynamic dependencies for attachmentSupport"
)
# Install rules
install(TARGETS attachmentSupport
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
+147
View File
@@ -0,0 +1,147 @@
#include <user/compute.h>
#include <stdexcept>
#include <string>
#include <vector>
#include <iostream>
#include <string_view>
namespace smo {
namespace compute {
// Helper function to parse OpenCL version string
static std::pair<int, int> parseOpenClVersion(const std::string& versionStr)
{
size_t spacePos = versionStr.find(' ');
if (spacePos == std::string::npos) { return {-1, -1}; }
std::string versionNum = versionStr.substr(spacePos + 1);
size_t dotPos = versionNum.find('.');
if (dotPos == std::string::npos) { return {-1, -1}; }
try {
int major = std::stoi(versionNum.substr(0, dotPos));
int minor = std::stoi(versionNum.substr(dotPos + 1));
return {major, minor};
} catch (const std::exception&) {
return {-1, -1};
}
}
// Implementation of validateOpenClVersion (declared in user/compute.h)
bool validateOpenClVersion(
std::string_view versionStr, std::string_view versionType,
int minMajor, int minMinor)
{
auto [major, minor] = parseOpenClVersion(std::string(versionStr));
if (major == -1 && minor == -1)
{
std::cerr << __func__ << ": failed to parse OpenCL " << versionType
<< " version: " << versionStr << std::endl;
return false;
}
if (major < minMajor || (major == minMajor && minor < minMinor))
{
std::cerr << __func__ << ": OpenCL " << versionType << " version "
<< major << "." << minor << " found, but " << minMajor << "."
<< minMinor << " or higher is required" << std::endl;
return false;
}
std::cout << __func__ << ": OpenCL " << versionType << " version: "
<< versionStr << std::endl;
return true;
}
ComputeDevice::ComputeDevice(cl_platform_id platformId, cl_device_id deviceId)
: platform(platformId), device(deviceId),
context(nullptr), commandQueue(nullptr)
{
cl_int err;
// Create context for this device
context = clCreateContext(
nullptr, 1, &device,
nullptr, nullptr, &err);
if (err != CL_SUCCESS || !context)
{
throw std::runtime_error(
std::string(__func__) + ": failed to create context for device: " +
std::to_string(err));
}
// Create command queue
cl_command_queue_properties queueProps = 0;
commandQueue = clCreateCommandQueue(
context, device, queueProps, &err);
if (err != CL_SUCCESS || !commandQueue)
{
clReleaseContext(context);
context = nullptr;
throw std::runtime_error(
std::string(__func__) + ": failed to create command queue for "
"device: " + std::to_string(err));
}
}
ClBuffer::ClBuffer(void* hostPtr, size_t size, cl_mem_flags flags,
const std::vector<std::shared_ptr<ComputeDevice>>& devices)
: hostPtr(hostPtr), size(size), flags(flags)
{
associations.reserve(devices.size());
// Create a buffer for each device's context
for (const auto& device : devices)
{
if (!device->context) { continue; }
cl_int err;
cl_mem_flags bufferFlags = CL_MEM_USE_HOST_PTR | flags;
cl_mem buffer = clCreateBuffer(
device->context,
bufferFlags,
size, hostPtr,
&err);
if (err != CL_SUCCESS || !buffer)
{
// Release any buffers already created before throwing
for (auto& assoc : associations)
{
if (assoc.buffer) {
clReleaseMemObject(assoc.buffer);
}
}
throw std::runtime_error(
std::string(__func__) + ": failed to create buffer for "
"device: " + std::to_string(err));
}
associations.emplace_back(buffer, device);
}
}
cl_mem ClBuffer::getAssociatedBufferHandleForDevice(
const std::shared_ptr<ComputeDevice>& device) const
{
if (!device)
{
throw std::invalid_argument(std::string(__func__)
+ ": device is nullptr");
}
for (const auto& assoc : associations)
{
if (assoc.device == device) {
return assoc.buffer;
}
}
return nullptr;
}
} // namespace compute
} // namespace smo
@@ -0,0 +1,353 @@
#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(
// slotStartAlignmentByteVal (page alignment for DMA)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE)),
// slotPadToNBytes (MTU 1500 - UDP/IP header 28)
1472,
// frameStartAlignmentByteVal (page alignment for DMA)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE)),
// framePadToNBytes (MTU 1500 - UDP/IP header 28)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE))
);
// Static defaults for OpenCL input
const StagingBuffer::IOEngineConstraints
StagingBuffer::IOEngineConstraints::openClInputConstraints(
// slotStartAlignmentByteVal (page alignment)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE)),
// slotPadToNBytes (XYZI point size)
16,
// frameStartAlignmentByteVal (page alignment)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE)),
// framePadToNBytes (pointer size)
static_cast<size_t>(sysconf(_SC_PAGE_SIZE))
);
// Helper function to calculate maximum alignment needed for first slot
// (must satisfy both frame and slot alignment)
static size_t calculateMaxAlignment(
size_t frameStartAlignmentByteVal,
size_t slotStartAlignmentByteVal)
{
if (frameStartAlignmentByteVal >= slotStartAlignmentByteVal)
{
if (frameStartAlignmentByteVal % slotStartAlignmentByteVal == 0)
{ return frameStartAlignmentByteVal; }
else
{
// Need LCM, but for simplicity use the larger alignment
// In practice, alignments are usually powers of 2, so this should work
return std::max(
frameStartAlignmentByteVal, slotStartAlignmentByteVal);
}
}
else
{
if (slotStartAlignmentByteVal % frameStartAlignmentByteVal == 0)
{ return slotStartAlignmentByteVal; }
else
{
return std::max(
frameStartAlignmentByteVal, slotStartAlignmentByteVal);
}
}
}
void StagingBuffer::computeSlotStrideAndBufferSize()
{
// Slot stride is the maximum of alignment and padding, rounded up to a multiple of alignment
size_t minSlotStride = std::max(
inputConstraints.slotStartAlignmentByteVal,
inputConstraints.slotPadToNBytes);
slotStrideNBytes = ((minSlotStride + inputConstraints.slotStartAlignmentByteVal - 1)
/ inputConstraints.slotStartAlignmentByteVal)
* inputConstraints.slotStartAlignmentByteVal;
// Calculate maximum alignment needed for first slot (must satisfy both frame and slot alignment)
size_t maxAlignment = calculateMaxAlignment(
inputConstraints.frameStartAlignmentByteVal,
inputConstraints.slotStartAlignmentByteVal);
// Calculate minimum buffer size
size_t minBufferSize = std::max(
inputConstraints.framePadToNBytes,
inputConstraints.slotPadToNBytes);
// Calculate total size needed for nSlots slots
size_t slotAreaSize = nSlots * slotStrideNBytes;
// Add padding space at buffer start for alignment offset (worst case: max alignment - 1)
size_t alignmentPadding = maxAlignment - 1;
// Total size needed: alignment padding + slot area, then ensure minimum is met
size_t rawSize = alignmentPadding + slotAreaSize;
if (rawSize < minBufferSize)
{ rawSize = minBufferSize; }
// Align up to the maximum alignment to ensure we can always find a valid offset
bufferNBytes = ((rawSize + maxAlignment - 1) / maxAlignment) * maxAlignment;
}
// Static member function to calculate offset and validate invariants
size_t StagingBuffer::calculateFirstSlotOffsetAndValidate(
uint8_t* buffer,
size_t bufferNBytes,
size_t nSlots,
size_t slotStrideNBytes,
const StagingBuffer::IOEngineConstraints& inputConstraints)
{
// Calculate maximum alignment needed for first slot
size_t maxAlignment = calculateMaxAlignment(
inputConstraints.frameStartAlignmentByteVal,
inputConstraints.slotStartAlignmentByteVal);
// Calculate offset to align first slot to both frame and slot alignment
uintptr_t bufferAddr = reinterpret_cast<uintptr_t>(buffer);
uintptr_t alignedAddr = ((bufferAddr + maxAlignment - 1) / maxAlignment)
* maxAlignment;
size_t firstSlotOffsetNBytes = alignedAddr - bufferAddr;
// Validate invariants with exceptions
uint8_t* firstSlotAddr = buffer + firstSlotOffsetNBytes;
if (
reinterpret_cast<uintptr_t>(firstSlotAddr)
% inputConstraints.frameStartAlignmentByteVal != 0)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: first slot address not aligned to "
+ std::to_string(inputConstraints.frameStartAlignmentByteVal));
}
if (
reinterpret_cast<uintptr_t>(firstSlotAddr)
% inputConstraints.slotStartAlignmentByteVal != 0)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: first slot address not aligned to "
+ std::to_string(inputConstraints.slotStartAlignmentByteVal));
}
size_t minBufferSize = std::max(
inputConstraints.framePadToNBytes,
inputConstraints.slotPadToNBytes);
if (bufferNBytes < minBufferSize)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: buffer size less than minimum required (max of "
+ std::to_string(inputConstraints.framePadToNBytes)
+ " and "
+ std::to_string(inputConstraints.slotPadToNBytes)
+ ")");
}
if (firstSlotOffsetNBytes + nSlots * slotStrideNBytes
> bufferNBytes)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: buffer size insufficient to hold "
+ std::to_string(nSlots)
+ " slots with proper alignment and padding");
}
return firstSlotOffsetNBytes;
}
StagingBuffer::StagingBuffer(
const IOEngineConstraints& inputEngineConstraints_,
const IOEngineConstraints& /*outputEngineConstraints*/,
size_t nSlots)
: buffer(nullptr, MmapDeleter(0)),
nSlots(nSlots),
inputConstraints(inputEngineConstraints_)
{
if (nSlots == 0)
{
throw std::invalid_argument(std::string(__func__)
+ ": StagingBuffer: nSlots must be > 0");
}
computeSlotStrideAndBufferSize();
/* Allocate buffer using mmap() for io_uring registration
* MAP_ANONYMOUS | MAP_PRIVATE creates anonymous, non-file-backed memory
*/
void* mmapped = mmap(
nullptr, bufferNBytes,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);
if (mmapped == MAP_FAILED)
{
throw std::runtime_error(std::string(__func__)
+ ": StagingBuffer: mmap() failed");
}
buffer = std::unique_ptr<uint8_t, MmapDeleter>(
static_cast<uint8_t*>(mmapped), MmapDeleter(bufferNBytes));
currentNBytes.store(0);
// Calculate offset and validate invariants (helper function in .cpp)
firstSlotOffsetNBytes = StagingBuffer::calculateFirstSlotOffsetAndValidate(
buffer.get(), bufferNBytes, nSlots,
slotStrideNBytes, inputConstraints);
// Build FrameAssemblyDesc once
std::vector<FrameAssemblyDesc::SlotDesc> slots;
slots.reserve(nSlots);
uint8_t *frameBase = buffer.get() + firstSlotOffsetNBytes;
for (size_t i = 0; i < nSlots; ++i)
{
size_t off = i * slotStrideNBytes;
FrameAssemblyDesc::SlotDesc s{
off, frameBase + off, inputConstraints.slotPadToNBytes};
slots.push_back(s);
}
frameDesc = std::make_shared<FrameAssemblyDesc>(
nSlots, inputConstraints.slotPadToNBytes, bufferNBytes,
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
@@ -0,0 +1,272 @@
#include <config.h>
#include <iostream>
#include <chrono>
#include <algorithm>
#include <boost/asio/io_context.hpp>
#include <opts.h>
#include <componentThread.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
{
for (const auto& buffer : attachedStimulusBuffers)
{
if (buffer && buffer->deviceAttachmentSpec &&
*buffer->deviceAttachmentSpec == *spec)
{
return buffer;
}
}
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
{
for (const auto& buffer : attachedStimulusBuffers)
{
if (!buffer || !buffer->deviceAttachmentSpec)
{
throw std::runtime_error(
"StimulusProducer::hasBufferWithQualeIfaceApi: encountered "
"null buffer or null deviceAttachmentSpec in "
"attachedStimulusBuffers (should never happen)");
}
if (buffer->deviceAttachmentSpec->qualeIfaceApi != qualeIfaceApi)
{ continue; }
return true;
}
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)
{
if (!buffer) { return; }
auto it = std::find(
attachedStimulusBuffers.begin(),
attachedStimulusBuffers.end(),
buffer);
if (it != attachedStimulusBuffers.end()) {
attachedStimulusBuffers.erase(it);
}
}
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()
{
// Cancel timer immediately
daemonTimer.cancel();
taskNursery.requestCancelOnAll();
taskNursery.closeAdmission();
taskNursery.syncAwaitAllSettlements(
sscl::ComponentThread::getSelf()->getIoContext());
std::cout << __func__ << ": Stopped stimulus producer for device "
<< deviceAttachmentSpec->deviceSelector << 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
+42
View File
@@ -0,0 +1,42 @@
option(ENABLE_LIB_livoxProto1 "Enable Livox Protocol v1 backend lib" ON)
if(ENABLE_LIB_livoxProto1)
add_library(livoxProto1 SHARED
livoxProto1.cpp
core.cpp
device.cpp
protocol.cpp
broadcastListener.cpp
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}
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/smocore/include
${CMAKE_SOURCE_DIR}/commonLibs
)
target_link_libraries(livoxProto1 PUBLIC
Boost::system Boost::log
spinscale
)
# Verify Boost dynamic dependencies after build
add_custom_command(TARGET livoxProto1 POST_BUILD
COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$<TARGET_FILE:livoxProto1>"
-P ${CMAKE_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake
COMMENT "Verifying Boost dynamic dependencies for livoxProto1"
)
# Install rules
install(TARGETS livoxProto1
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
endif()
@@ -0,0 +1,372 @@
#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<sscl::ComponentThread>& componentThread,
uint16_t listeningPort, uint16_t connectPort
)
: componentThread(componentThread),
listeningPort(listeningPort),
connectPort(connectPort),
deviceGoneAwayCb(nullptr),
socket(componentThread->getIoContext()),
listeningEndpoint(boost::asio::ip::udp::v4(), listeningPort)
{
}
std::shared_ptr<DiscoveredDevice>
BroadcastListener::getDevice(const std::string &deviceIdentifier) const
{
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.rsrc.devices.end() ? *it : nullptr;
}
void BroadcastListener::registerDiscoveredDevice(
const BroadcastMessage &msg, const std::string &senderIp)
{
std::string broadcastCode(
reinterpret_cast<const char*>(msg.broadcast_code));
sscl::SpinLock::Guard lock(discoveredDevices.lock);
// Early return if device already exists
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 (existingIt != discoveredDevices.rsrc.devices.end())
{
if (getProtoState().smoCallbacks.OptionParser_getOptions().verbose)
{
std::cout << "broadcastMsgIndCInd"
<< ": Received broadcast from known device: "
<< broadcastCode << " at " << senderIp << "\n";
}
return;
}
// 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 (daemonNursery.admissionIsOpen()) { return; }
try
{
/** EXPLANATION:
* Set up a boost::asio udp listening socket on the broadcast listening
* port.
*
* FIXME:
* We should also set up a timer to check for devices that have gone
* away.
*/
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);
std::cout << __func__ << ": BroadcastListener started on port "
<< listeningPort << std::endl;
}
catch (const boost::system::system_error& e)
{
std::cerr << __func__ << ": Failed to start BroadcastListener: "
<< e.what() << std::endl;
throw;
}
}
void BroadcastListener::stop(void)
{
if (!daemonNursery.admissionIsOpen()) { return; }
daemonNursery.requestCancelOnAll();
try
{
socket.cancel();
socket.close();
}
catch (const boost::system::system_error& e)
{
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
} // namespace livoxProto1
+133
View File
@@ -0,0 +1,133 @@
#ifndef BROADCAST_LISTENER_H
#define BROADCAST_LISTENER_H
#include <boostAsioLinkageFix.h>
#include <array>
#include <vector>
#include <string>
#include <memory>
#include <functional>
#include <boost/asio/ip/udp.hpp>
#include <user/senseApiDesc.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 {
namespace comms {
/** EXPLANATION:
* This class merely listens for UDP bcast dgrams on the designated listening
* port. It then builds a list of client device IP addrs that it has heard from.
* It doesn't connect to them or signal any events to the rest of the lib,
* except in the case that a device which the lib is using has gone away.
*
* Other than that, its role is to tell the lib which devices are available
* on the network.
*/
#define UDP_BCAST_MSG_BUFFER_NBYTES (1024)
class BroadcastListener
{
public:
BroadcastListener(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
uint16_t listeningPort=55000, uint16_t connectPort=65000);
~BroadcastListener() = default;
typedef void (DeviceGoneAwayCbFn)(const DiscoveredDevice &device);
void setDeviceGoneAwayCb(DeviceGoneAwayCbFn *cb)
{ deviceGoneAwayCb = cb; }
bool deviceExists(const std::string &deviceIdentifier) const
{ return getDevice(deviceIdentifier) != nullptr; }
std::shared_ptr<DiscoveredDevice>
getDevice(const std::string &deviceIdentifier) const;
void start(void);
void stop(void);
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:
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
* the bcast dgram to figure out the client device's IP addr. Then we
* should send a connect dgram to the connect port. This will tell the
* client device our IP addr.
*/
uint16_t listeningPort, connectPort;
DeviceGoneAwayCbFn *deviceGoneAwayCb;
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;
/** 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
} // namespace livoxProto1
#endif // BROADCAST_LISTENER_H
+191
View File
@@ -0,0 +1,191 @@
#include <algorithm>
#include <iostream>
#include <functional>
#include <optional>
#include <opts.h>
#include <user/senseApiDesc.h>
#include "protocol.h"
#include "core.h"
#include "device.h"
#include "broadcastListener.h"
#include "livoxProto1.h"
namespace livoxProto1 {
static ProtoState protoState =
{
.isInitialized = false,
.componentThread = nullptr,
.deviceManager = nullptr,
.smoCallbacks = {}
};
ProtoState& getProtoState()
{
return protoState;
}
DeviceManager::DeviceManager()
: broadcastListener(protoState.componentThread),
udpCommandDemuxer(protoState.componentThread, *this)
{
broadcastListener.setDeviceGoneAwayCb(deviceGoneAwayInd);
}
void DeviceManager::deviceGoneAwayInd(const comms::DiscoveredDevice &device)
{
std::cout << "Device gone away: " << device.stringify() << std::endl;
// Check if device exists in our collection
if (!protoState.deviceManager->getDevice(device)) {
return;
}
// Find and remove the device from the collection
auto it = std::find_if(
protoState.deviceManager->devices.begin(),
protoState.deviceManager->devices.end(),
[&device](const std::shared_ptr<Device> &d) {
return d->discoveredDevice == device;
}
);
if (it != protoState.deviceManager->devices.end()) {
protoState.deviceManager->devices.erase(it);
}
}
std::optional<std::shared_ptr<Device>> DeviceManager::getDevice(
const std::string &deviceIdentifier
)
{
for (auto& device : devices)
{
if (comms::deviceIdentifiersEqual(
device->discoveredDevice.deviceIdentifier, deviceIdentifier))
{
return device;
}
}
return std::nullopt;
}
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
DeviceManager::getOrCreateDeviceCReq(
const std::string &deviceIdentifier,
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)
{
LivoxProto1GetOrCreateDeviceResult result;
// Validate smoIp format using Boost.Asio IPv4 validation
if (!smoIp.empty() && !comms::isValidIPv4(smoIp))
{
throw std::invalid_argument(
std::string(__func__) +
": Invalid IPv4 smoIp format: " + smoIp);
}
// Validate subnet nbits
if (smoSubnetNbits > 32)
{
throw std::invalid_argument(
std::string(__func__) +
": smoSubnetNbits must be between 0 and 32, got: " +
std::to_string(smoSubnetNbits));
}
// First try to get existing device
auto existingDevice = getDevice(deviceIdentifier);
if (existingDevice)
{
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
auto newDevice = std::make_shared<Device>(
deviceIdentifier, componentThread,
commandTimeoutMs, retryDelayMs,
smoIp, smoSubnetNbits,
dataPort, cmdPort, imuPort);
// Start the connection process - only add to collection on success
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;
}
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
* disconnectCReq and then remove it.
*/
std::shared_ptr<Device> device = getDevice(dev->discoveredDevice).
value_or(nullptr);
if (!device || device->nAttachedStimulusProducers > 0) {
co_return false;
}
const bool success = co_await device->disconnectCReq();
devices.erase(
std::remove(devices.begin(), devices.end(), device),
devices.end());
co_return success;
}
void main(const std::shared_ptr<sscl::ComponentThread> &componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks)
{
if (protoState.isInitialized) {
return;
}
protoState.isInitialized = true;
protoState.componentThread = componentThread;
protoState.smoCallbacks = smoCallbacks;
protoState.deviceManager = std::make_unique<DeviceManager>();
protoState.deviceManager->broadcastListener.start();
protoState.deviceManager->udpCommandDemuxer.start();
}
void exit(void)
{
if (!protoState.isInitialized) {
return;
}
protoState.deviceManager->udpCommandDemuxer.stop();
protoState.deviceManager->broadcastListener.stop();
protoState.deviceManager.reset();
protoState.componentThread.reset();
protoState.isInitialized = false;
}
} // namespace livoxProto1
+75
View File
@@ -0,0 +1,75 @@
#ifndef LIVOXPROTO1_CORE_H
#define LIVOXPROTO1_CORE_H
#include <vector>
#include <string>
#include <memory>
#include <cstdint>
#include <optional>
#include <user/senseApiDesc.h>
#include "device.h"
#include "broadcastListener.h"
#include "udpCommandDemuxer.h"
#include "livoxProto1.h"
#include <spinscale/co/invokers.h>
namespace livoxProto1 {
class DeviceManager
{
public:
DeviceManager();
~DeviceManager() = default;
static void deviceGoneAwayInd(const comms::DiscoveredDevice &device);
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
getOrCreateDeviceCReq(
const std::string &deviceIdentifier,
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);
sscl::co::ViralNonPostingInvoker<bool> destroyDeviceCReq(
std::shared_ptr<Device> device);
std::optional<std::shared_ptr<Device>> getDevice(
const std::string &deviceIdentifier);
std::optional<std::shared_ptr<Device>> getDevice(
const comms::DiscoveredDevice &device)
{
return getDevice(device.deviceIdentifier);
}
private:
// Configuration
static constexpr int RETRY_DELAY_SECONDS = 3; // <N> seconds delay
public:
std::vector<std::shared_ptr<Device>> devices;
comms::BroadcastListener broadcastListener;
comms::UdpCommandDemuxer udpCommandDemuxer;
};
void main(
const std::shared_ptr<sscl::ComponentThread> &componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks);
void exit(void);
// Global state structure
struct ProtoState
{
bool isInitialized = false;
std::shared_ptr<sscl::ComponentThread> componentThread;
std::unique_ptr<DeviceManager> deviceManager;
smo::stim_buff::SmoCallbacks smoCallbacks;
};
// Access to global state for extern "C" functions
ProtoState& getProtoState();
} // namespace livoxProto1
#endif // LIVOXPROTO1_CORE_H
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
#ifndef LIVOX_PROTO1_DEVICE_H
#define LIVOX_PROTO1_DEVICE_H
#include <boostAsioLinkageFix.h>
#include <string>
#include <cstdint>
#include <cstddef>
#include <memory>
#include <atomic>
#include <optional>
#include <functional>
#include <unordered_map>
#include <stdexcept>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#include "protocol.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 {
template<>
struct hash<std::pair<uint8_t, uint8_t>> {
size_t operator()(const std::pair<uint8_t, uint8_t>& p) const noexcept {
return (static_cast<size_t>(p.first) << 8) | static_cast<size_t>(p.second);
}
};
}
// Forward declaration
namespace smo {
class ComponentThread;
}
namespace livoxProto1 {
namespace comms {
/** EXPLANATION:
* This class represents a discovered device. It is used to store the
* device identifier and IP address of a discovered device.
*/
class DiscoveredDevice
{
public:
DiscoveredDevice(
const std::string &deviceIdentifier,
DeviceType deviceType,
const std::string &ipAddr);
// "Conversion" constructor from BroadcastMessage
DiscoveredDevice(const BroadcastMessage &msg, const std::string &ipAddr);
~DiscoveredDevice() = default;
bool operator==(const DiscoveredDevice &other) const
{
return comms::deviceIdentifiersEqual(
deviceIdentifier, other.deviceIdentifier);
}
std::string stringify(void) const;
std::string getDeviceTypeName(void) const;
public:
std::string deviceIdentifier;
DeviceType deviceType;
std::string ipAddr;
};
} // namespace comms
class Device
{
public:
Device(const std::string &deviceIdentifier,
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);
~Device();
private:
// Heartbeat mechanism
void startHeartbeat();
void stopHeartbeat();
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);
// IP detection methods
std::optional<std::string> detectSmoIp(const std::string& deviceIP);
uint32_t getSubnetMaskFor(uint8_t nbits);
public:
enum class ReturnMode : uint8_t
{
SingleFirst = 0x00,
SingleStrongest = 0x01,
Dual = 0x02,
Triple = 0x03
};
/**
* Get the number of points per datagram based on return mode
* @param returnMode The return mode (0=SingleFirst, 1=SingleStrongest, 2=Dual, 3=Triple)
* @return Number of points per datagram
*/
static inline size_t getNPointsPerDgram(int returnMode)
{
/*
* Map modes to points per datagram based on Livox docs
* 1: first, 2: strongest -> 96 samples => 96 points
* 3: dual -> 48 samples * 2 points = 96
* 4: triple -> 30 samples * 3 points = 90
*/
switch (returnMode)
{
case static_cast<int>(ReturnMode::SingleFirst):
case static_cast<int>(ReturnMode::SingleStrongest):
case static_cast<int>(ReturnMode::Dual):
return 96u;
case static_cast<int>(ReturnMode::Triple):
return 90u;
default:
throw std::runtime_error(
std::string(__func__) + ": Unknown returnMode "
+ std::to_string(returnMode));
}
}
// Utility methods
std::optional<std::string> getSmoIp(const std::string& deviceIP);
struct ConnectIpResult
{
bool success = false;
std::string ipAddr;
};
// Async connection methods
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<sscl::ComponentThread> componentThread;
int commandTimeoutMs, retryDelayMs;
std::string smoIp;
std::string detectedSmoListeningIp;
uint8_t smoSubnetNbits;
uint16_t dataPort, cmdPort, imuPort;
// 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;
// Cached last-known return mode for this device
ReturnMode currentReturnMode = ReturnMode::SingleFirst;
public:
// UDP datagram handling
void handleUdpDgram(
const uint8_t* data, ssize_t bytesReceived,
const struct sockaddr_in& senderAddr);
// Command handler registration
void registerUdpCommandHandler(
uint8_t cmd_set, uint8_t cmd_id,
std::function<void(
const uint8_t* data, ssize_t bytesReceived,
const struct sockaddr_in& senderAddr)> handler,
const std::string& deviceIP = "");
void unregisterUdpCommandHandler(
uint8_t cmd_set, uint8_t cmd_id, const std::string& deviceIP = "");
private:
// Point cloud data setup
void cleanupPcloudDataSocket();
/** EXPLANATION:
* This is the "straightforward" map of command set and command id to
* handlers. This is useful for any commands which are guaranteed to be
* issued to the device *AFTER* the device has successfully been added
* to the DeviceManager's list of devices.
*
* I.e: it cannot be used for commands which are issued to the device before
* getOrCreateDevice() has added the device to the DeviceManager's list of
* devices.
*/
// Command handler map
std::unordered_map<
std::pair<uint8_t, uint8_t>,
std::function<void(
const uint8_t* data, ssize_t bytesReceived,
const struct sockaddr_in& senderAddr)>> udpCommandHandlers;
public:
/** EXPLANATION:
* This is the "temporary" map of command set and command id to
* handlers. This is useful for any commands which are issued to the device
* while it is being constructed.
*
* I.e: it shouldn't be used for cmds which are issued to the device after
* getOrCreateDevice() has added the device to the DeviceManager's list of
* devices. It will work for such commands, but we'd kind of prefer to use
* the "straightforward" map above for such commands.
*
* NOTE:
* There's a strong argument to be made for just getting rid of the
* "straightforward" map above and just using this one, tho.
*/
struct CommandHandler {
uint8_t cmd_set;
uint8_t cmd_id;
std::function<void(
const uint8_t* data, ssize_t bytesReceived,
const struct sockaddr_in& senderAddr)> handler;
};
static std::unordered_map<std::string, std::vector<CommandHandler>>
devicesUnderConstruction;
};
} // namespace livoxProto1
#endif // LIVOX_PROTO1_DEVICE_H
+116
View File
@@ -0,0 +1,116 @@
#include <stdexcept>
#include <boost/asio/posix/stream_descriptor.hpp>
#include "livoxProto1.h"
#include "device.h"
#include "core.h"
#include "udpCommandDemuxer.h"
extern "C" {
sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
livoxProto1_getOrCreateDeviceCReq(
const std::string& deviceIdentifier,
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)
{
// Get the global DeviceManager instance
auto& protoState = livoxProto1::getProtoState();
if (!protoState.deviceManager)
{
throw std::runtime_error(
std::string(__func__) + ": LivoxProto1 not initialized - call "
"livoxProto1_main first");
}
// Delegate to the DeviceManager to create the device
co_return co_await protoState.deviceManager->getOrCreateDeviceCReq(
deviceIdentifier, componentThread,
commandTimeoutMs, retryDelayMs,
smoIp, smoSubnetNbits,
dataPort, cmdPort, imuPort);
}
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_destroyDeviceCReq(
std::shared_ptr<livoxProto1::Device> device)
{
auto& protoState = livoxProto1::getProtoState();
if (!protoState.deviceManager)
{
throw std::runtime_error(std::string(__func__)
+ ": DeviceManager not initialized");
}
co_return co_await protoState.deviceManager->destroyDeviceCReq(device);
}
void livoxProto1_main(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks)
{
livoxProto1::main(componentThread, smoCallbacks);
}
void livoxProto1_exit(void)
{
livoxProto1::exit();
}
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_device_enablePcloudDataCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
throw std::runtime_error(std::string(__func__)
+ ": Device pointer is null");
}
co_return co_await device->enablePcloudDataCReq();
}
sscl::co::ViralNonPostingInvoker<bool> livoxProto1_device_disablePcloudDataCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
throw std::runtime_error(std::string(__func__)
+ ": Device pointer is null");
}
co_return co_await device->disablePcloudDataCReq();
}
sscl::co::ViralNonPostingInvoker<LivoxProto1GetReturnModeResult>
livoxProto1_device_getReturnModeCReq(
std::shared_ptr<livoxProto1::Device> device)
{
if (!device)
{
throw std::runtime_error(std::string(__func__)
+ ": Device pointer is null");
}
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>
livoxProto1_getPcloudDataFdDesc(void)
{
auto& protoState = livoxProto1::getProtoState();
if (!protoState.deviceManager)
{
throw std::runtime_error(std::string(__func__)
+ ": DeviceManager not initialized");
}
return protoState.deviceManager->udpCommandDemuxer.getPcloudDataFdDesc();
}
} // extern "C"
+109
View File
@@ -0,0 +1,109 @@
#ifndef LIVOXPROTO1_H
#define LIVOXPROTO1_H
#include <memory>
#include <string>
#include <cstdint>
#include <functional>
#include <spinscale/co/invokers.h>
#include <boost/asio/posix/stream_descriptor.hpp>
// Forward declarations
namespace smo {
namespace stim_buff {
struct SmoCallbacks;
}
}
namespace sscl {
class ComponentThread;
}
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
/**
* Initialize the Livox protocol library
* @param componentThread Component thread shared pointer
* @param smoCallbacks Callbacks provided by SMO
*/
typedef void livoxProto1_mainFn(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const smo::stim_buff::SmoCallbacks& smoCallbacks);
/**
* Cleanup the Livox protocol library
*/
typedef void livoxProto1_exitFn(void);
/**
* Create a new Livox device connection
* @param deviceIdentifier The device identifier (broadcast code)
* @param componentThread Component thread for async operations
* @param commandTimeoutMs Command timeout in milliseconds (default: 1000)
* @param retryDelayMs Retry delay in milliseconds (default: 3000)
* @param smoIp SMO IP address (empty string for auto-detection)
* @param smoSubnetNbits SMO subnet mask bits (e.g., 24 for /24, 16 for /16)
* @param dataPort Data port for point cloud (default: 56000)
* @param cmdPort Command port (default: 56001)
* @param imuPort IMU port (default: 56002)
* @return LivoxProto1GetOrCreateDeviceResult (success + device on success,
* null device on failure)
*/
typedef sscl::co::ViralNonPostingInvoker<LivoxProto1GetOrCreateDeviceResult>
livoxProto1_getOrCreateDeviceCReqFn(
const std::string& deviceIdentifier,
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);
typedef sscl::co::ViralNonPostingInvoker<bool> livoxProto1_destroyDeviceCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef sscl::co::ViralNonPostingInvoker<bool>
livoxProto1_device_enablePcloudDataCReqFn(
std::shared_ptr<livoxProto1::Device> device);
typedef sscl::co::ViralNonPostingInvoker<bool>
livoxProto1_device_disablePcloudDataCReqFn(
std::shared_ptr<livoxProto1::Device> device);
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_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
}
#endif
#endif // LIVOXPROTO1_H
+854
View File
@@ -0,0 +1,854 @@
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cstring>
#include "protocol.h"
namespace livoxProto1 {
namespace comms {
// Command methods
void Command::swapToHostEndianness()
{
// No multi-byte fields to swap
}
void Command::swapToProtocolEndianness()
{
// No multi-byte fields to swap
}
bool Command::sanityCheck() const
{
// Basic validation - can be extended for specific command sets
return true;
}
// Header methods
void Header::swapToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
length = __builtin_bswap16(length);
seq_num = __builtin_bswap16(seq_num);
crc_16 = __builtin_bswap16(crc_16);
}
void Header::swapToProtocolEndianness()
{
// Protocol is little-endian, so if host is already little-endian, no swap needed
if (endian::isLittleEndian()) { return; }
// Host is big-endian, need to swap to little-endian
length = __builtin_bswap16(length);
seq_num = __builtin_bswap16(seq_num);
crc_16 = __builtin_bswap16(crc_16);
}
bool Header::sanityCheck() const
{
return (sof == 0xAA) && (version == 1);
}
uint16_t Header::calculateCrc16() const
{
// Calculate CRC16 for the header excluding the crc_16 field itself
// This matches the Livox SDK approach: calculate over raw bytes excluding CRC16 field
const uint8_t* headerData = reinterpret_cast<const uint8_t*>(this);
size_t headerSize = sizeof(Header) - sizeof(crc_16); // Exclude CRC16 field
return comms::calculateCrc16(headerData, headerSize);
}
bool Header::validateCrc16() const
{
// Calculate CRC16 for the header excluding the crc_16 field itself
uint16_t calculatedCrc = calculateCrc16();
// Compare with the CRC in the header
bool isValid = (calculatedCrc == crc_16);
// Debug output only if validation fails
if (!isValid) {
std::cout << "CRC16 Debug: calculated=0x" << std::hex << calculatedCrc
<< ", received=0x" << crc_16 << std::dec << std::endl;
}
return isValid;
}
void Header::setCrc16FromRawBytes()
{
// Calculate CRC16 on raw bytes and set it (after endianness swap)
crc_16 = calculateCrc16();
}
void Header::swapCrc16ToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
crc_16 = __builtin_bswap16(crc_16);
}
void Header::swapCrc16ToProtocolEndianness()
{
if (endian::isLittleEndian()) { return; }
crc_16 = __builtin_bswap16(crc_16);
}
// Footer methods
void Footer::swapToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
crc_32 = __builtin_bswap32(crc_32);
}
void Footer::swapToProtocolEndianness()
{
// Protocol is little-endian, so if host is already little-endian, no swap needed
if (endian::isLittleEndian()) { return; }
// Host is big-endian, need to swap to little-endian
crc_32 = __builtin_bswap32(crc_32);
}
void Footer::swapCrc32ToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
crc_32 = __builtin_bswap32(crc_32);
}
void Footer::swapCrc32ToProtocolEndianness()
{
if (endian::isLittleEndian()) { return; }
crc_32 = __builtin_bswap32(crc_32);
}
bool Footer::validateCrc32() const
{
// This method should validate the CRC32 against the message content
// For now, we'll return true since the validation is done on raw bytes
// before struct construction in the receiving flow
return true;
}
bool Footer::sanityCheck() const
{
/** FIXME:
* Add CRC validation here.
*/
return true;
}
// BroadcastMessage methods
void BroadcastMessage::swapContentsToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
// Only swap content fields, not CRC fields
header.swapToHostEndianness();
command.swapToHostEndianness();
reserved = __builtin_bswap16(reserved);
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
bool BroadcastMessage::sanityCheck() const
{
return header.sanityCheck() &&
command.sanityCheck() &&
(command.cmd_set == 0x00) &&
(command.cmd_id == 0x00) &&
(header.cmd_type == 0x02) &&
footer.sanityCheck();
}
bool BroadcastMessage::validateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
// Try calculating on the raw bytes of the entire message (excluding CRC field)
uint32_t calculatedCrc = 0xFFFFFFFF;
// Calculate CRC32 over the entire message except the CRC field itself
// The message structure is: header + command + broadcast_code + dev_type + reserved + footer(without crc_32)
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(BroadcastMessage) - sizeof(footer.crc_32);
calculatedCrc = comms::calculateCrc32(messageData, messageSize);
// Compare with the CRC in the footer
bool isValid = (calculatedCrc == footer.crc_32);
// Debug output only if validation fails
if (!isValid) {
std::cout << "BroadcastMessage CRC32 Debug: calculated=0x" << std::hex << calculatedCrc
<< ", received=0x" << footer.crc_32 << std::dec << std::endl;
}
return isValid;
}
// HandshakeRequest methods
HandshakeRequest::HandshakeRequest(
const std::string& hostIP,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort
)
{
// Initialize header
header.sof = 0xAA;
header.version = 1;
header.length = sizeof(HandshakeRequest);
header.cmd_type = 0x00; // CMD (request)
header.seq_num = 1; // Sequence number
header.crc_16 = 0; // Will be calculated later
// Initialize command
command.cmd_set = 0x00; // General Command Set
command.cmd_id = 0x01; // Handshake Command
// Parse host IP address
std::istringstream iss(hostIP);
std::string token;
int i = 0;
while (std::getline(iss, token, '.') && i < 4)
{
user_ip[i] = static_cast<uint8_t>(std::stoi(token));
i++;
}
// Set ports
this->data_port = dataPort;
this->cmd_port = cmdPort;
this->imu_port = imuPort;
// Initialize footer
footer.crc_32 = 0; // Will be calculated later
// Note: CRC16 will be calculated before sending (in swapToProtocolEndianness)
}
uint32_t HandshakeRequest::calculateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(HandshakeRequest) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void HandshakeRequest::swapContentsToProtocolEndianness()
{
// Protocol uses little-endian, so on little-endian machines, no swap needed
if (endian::isLittleEndian()) { return; }
// On big-endian machines, swap to little-endian for wire transmission
// Only swap content fields, not CRC fields
header.swapToHostEndianness();
command.swapToHostEndianness();
data_port = __builtin_bswap16(data_port);
cmd_port = __builtin_bswap16(cmd_port);
imu_port = __builtin_bswap16(imu_port);
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
// HandshakeResponse methods
void HandshakeResponse::swapContentsToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
// Only swap content fields, not CRC fields
header.swapToHostEndianness();
command.swapToHostEndianness();
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
bool HandshakeResponse::sanityCheck() const
{
return header.sanityCheck() &&
command.sanityCheck() &&
(command.cmd_set == 0x00) && (command.cmd_id == 0x01) &&
footer.sanityCheck();
}
bool HandshakeResponse::validateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(HandshakeResponse) - sizeof(footer.crc_32);
uint32_t calculatedCrc = comms::calculateCrc32(messageData, messageSize);
// Compare with the CRC in the footer
bool isValid = (calculatedCrc == footer.crc_32);
// Debug output only if validation fails
if (!isValid) {
std::cout << "HandshakeResponse CRC32 Debug: calculated=0x" << std::hex << calculatedCrc
<< ", received=0x" << footer.crc_32 << std::dec << std::endl;
}
return isValid;
}
// Standalone CRC16 calculation utility
uint16_t calculateCrc16(const uint8_t* data, size_t length)
{
/** EXPLANATION:
* Livox SDK CRC16 implementation (exact copy from FastCRC library)
* This matches the exact implementation used by Livox devices
*/
static const uint16_t crc_table_mcrf4xx[1024] = {
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78
};
// Livox SDK seed
uint16_t crc = LIVOX_CRC16_SEED;
// Simple implementation for now - can be optimized later
for (size_t i = 0; i < length; ++i) {
crc = (crc >> 8) ^ crc_table_mcrf4xx[(crc & 0xff) ^ data[i]];
}
return crc;
}
// Standalone CRC32 calculation utility
uint32_t calculateCrc32(const uint8_t* data, size_t length)
{
/** EXPLANATION:
* Livox SDK CRC32 implementation (exact copy from FastCRC library)
* This matches the exact implementation used by Livox devices
*/
static const uint32_t crc_table_crc32[256] = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
};
// Livox SDK seed XORed with 0xffffffff
uint32_t crc = LIVOX_CRC32_SEED ^ 0xffffffff;
for (size_t i = 0; i < length; ++i) {
crc = (crc >> 8) ^ crc_table_crc32[(crc & 0xff) ^ data[i]];
}
return crc ^ 0xffffffff;
}
// IP address parsing utility
std::optional<IPOctets> parseIPv4Address(const std::string& ipAddress)
{
IPOctets result;
std::istringstream iss(ipAddress);
if (std::getline(iss, result.octet1, '.') &&
std::getline(iss, result.octet2, '.') &&
std::getline(iss, result.octet3, '.') &&
std::getline(iss, result.octet4, '.'))
{
return result;
}
return std::nullopt;
}
// HeartbeatMessage methods
HeartbeatMessage::HeartbeatMessage()
{
// Initialize header
header.sof = 0xAA;
header.version = 0x01;
header.length = sizeof(Header) + sizeof(Command) + sizeof(Footer);
header.cmd_type = 0x00; // kCommandTypeCmd
header.seq_num = 0x0001; // Simple sequence number
header.crc_16 = 0; // Will be calculated
// Initialize command
command.cmd_set = 0x00; // kCommandSetGeneral
command.cmd_id = 0x03; // kCommandIDGeneralHeartbeat
// Initialize footer
footer.crc_32 = 0; // Will be calculated
// Note: CRC16 will be calculated before sending (in swapToProtocolEndianness)
}
uint32_t HeartbeatMessage::calculateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(HeartbeatMessage) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void HeartbeatMessage::swapContentsToProtocolEndianness()
{
// Protocol is little-endian, so if host is already little-endian, no swap needed
if (endian::isLittleEndian()) {
return;
}
// Host is big-endian, need to swap to little-endian
// Only swap content fields, not CRC fields
header.swapToProtocolEndianness();
command.swapToProtocolEndianness();
// Note: footer.swapToProtocolEndianness() swaps CRC, so we skip it here
}
// DisconnectMessage methods
DisconnectMessage::DisconnectMessage()
{
// Initialize header
header.sof = 0xAA;
header.version = 0x01;
header.length = sizeof(Header) + sizeof(Command) + sizeof(Footer);
header.cmd_type = 0x00; // kCommandTypeCmd
header.seq_num = 0x0001; // Simple sequence number
header.crc_16 = 0; // Will be calculated
// Initialize command
command.cmd_set = 0x00; // kCommandSetGeneral
command.cmd_id = 0x06; // kCommandIDGeneralDisconnect
// Initialize footer
footer.crc_32 = 0; // Will be calculated
// Note: CRC16 will be calculated before sending (in swapToProtocolEndianness)
}
uint32_t DisconnectMessage::calculateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(DisconnectMessage) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void DisconnectMessage::swapContentsToProtocolEndianness()
{
// Protocol is little-endian, so if host is already little-endian, no swap needed
if (endian::isLittleEndian()) {
return;
}
// Host is big-endian, need to swap to little-endian
// Only swap content fields, not CRC fields
header.swapToProtocolEndianness();
command.swapToProtocolEndianness();
// Note: footer.swapToProtocolEndianness() swaps CRC, so we skip it here
}
// StartStopSamplingMessage methods
StartStopSamplingMessage::StartStopSamplingMessage()
{
// Initialize header
header.sof = 0xAA;
header.version = 1;
header.length = sizeof(StartStopSamplingMessage);
header.cmd_type = 0x02; // MSG type
header.seq_num = 0; // Will be set by caller if needed
header.crc_16 = 0; // Will be calculated
// Initialize command
command.cmd_set = 0x00; // General command set
command.cmd_id = 0x04; // Sampling command ID
// Initialize data - enable flag will be set manually by caller
enable = 0x00; // Default to stop, caller will override
// Initialize footer
footer.crc_32 = 0; // Will be calculated
}
uint32_t StartStopSamplingMessage::calculateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer CRC32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(StartStopSamplingMessage) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void StartStopSamplingMessage::swapContentsToProtocolEndianness()
{
header.swapToProtocolEndianness();
command.swapToProtocolEndianness();
}
// SamplingResponse methods
void SamplingResponse::swapContentsToHostEndianness()
{
header.swapToHostEndianness();
command.swapToHostEndianness();
footer.swapToHostEndianness();
}
bool SamplingResponse::sanityCheck() const
{
return header.sanityCheck() && command.sanityCheck() && footer.sanityCheck();
}
bool SamplingResponse::validateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer CRC32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(SamplingResponse) - sizeof(footer.crc_32);
uint32_t calculatedCrc = comms::calculateCrc32(messageData, messageSize);
bool isValid = (calculatedCrc == footer.crc_32);
// Debug output only if validation fails
if (!isValid)
{
std::cout << "SamplingResponse CRC32 Debug: calculated=0x"
<< std::hex << calculatedCrc
<< ", received=0x" << footer.crc_32 << std::dec << std::endl;
}
return isValid;
}
// HeartbeatACK methods
void HeartbeatACK::swapContentsToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
// Only swap content fields, not CRC fields
header.swapToHostEndianness();
command.swapToHostEndianness();
ack_msg = __builtin_bswap32(ack_msg);
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
bool HeartbeatACK::sanityCheck() const
{
return header.sanityCheck() &&
command.sanityCheck() &&
(command.cmd_set == 0x00) && (command.cmd_id == 0x03) &&
footer.sanityCheck();
}
bool HeartbeatACK::validateCrc32() const
{
// Calculate CRC32 for the entire message excluding the footer.crc_32 field
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(HeartbeatACK) - sizeof(footer.crc_32);
uint32_t calculatedCrc = comms::calculateCrc32(messageData, messageSize);
// Compare with the CRC in the footer
bool isValid = (calculatedCrc == footer.crc_32);
// Debug output only if validation fails
if (!isValid) {
std::cout << "HeartbeatACK CRC32 Debug: calculated=0x" << std::hex << calculatedCrc
<< ", received=0x" << footer.crc_32 << std::dec << std::endl;
}
return isValid;
}
// SetLiDARReturnMode methods
SetLiDARReturnMode::SetLiDARReturnMode()
{
// Initialize header
header.sof = 0xAA;
header.version = 0x01;
header.length = sizeof(SetLiDARReturnMode);
header.crc_16 = 0; // Will be calculated later
// Initialize command
command.cmd_set = 0x01; // LiDAR Command
command.cmd_id = 0x06; // Set LiDAR Return Mode
// Initialize mode (default to Single Return First)
mode = 0x00;
// Initialize footer
footer.crc_32 = 0; // Will be calculated later
}
uint32_t SetLiDARReturnMode::calculateCrc32() const
{
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(SetLiDARReturnMode) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void SetLiDARReturnMode::swapContentsToProtocolEndianness()
{
if (endian::isLittleEndian()) { return; }
header.swapToProtocolEndianness();
command.swapToProtocolEndianness();
// mode is uint8_t, no endianness conversion needed
footer.swapToProtocolEndianness();
}
// SetLiDARReturnModeResponse methods
void SetLiDARReturnModeResponse::swapContentsToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
header.swapToHostEndianness();
command.swapToHostEndianness();
// ret_code is uint8_t, no endianness conversion needed
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
bool SetLiDARReturnModeResponse::sanityCheck() const
{
return header.sanityCheck() &&
command.sanityCheck() &&
(command.cmd_set == 0x01) && (command.cmd_id == 0x06) &&
(ret_code <= 0x01) && // Valid return codes: 0x00-0x01
footer.sanityCheck();
}
bool SetLiDARReturnModeResponse::validateCrc32() const
{
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(SetLiDARReturnModeResponse) - sizeof(footer.crc_32);
uint32_t calculatedCrc = comms::calculateCrc32(messageData, messageSize);
return (calculatedCrc == footer.crc_32);
}
// GetLiDARReturnMode methods
GetLiDARReturnMode::GetLiDARReturnMode()
{
// Initialize header
header.sof = 0xAA;
header.version = 0x01;
header.length = sizeof(GetLiDARReturnMode);
header.crc_16 = 0; // Will be calculated later
// Initialize command
command.cmd_set = 0x01; // LiDAR Command
command.cmd_id = 0x07; // Get LiDAR Return Mode
// Initialize footer
footer.crc_32 = 0; // Will be calculated later
}
uint32_t GetLiDARReturnMode::calculateCrc32() const
{
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(GetLiDARReturnMode) - sizeof(footer.crc_32);
return comms::calculateCrc32(messageData, messageSize);
}
void GetLiDARReturnMode::swapContentsToProtocolEndianness()
{
if (endian::isLittleEndian()) { return; }
header.swapToProtocolEndianness();
command.swapToProtocolEndianness();
footer.swapToProtocolEndianness();
}
// GetLiDARReturnModeResponse methods
void GetLiDARReturnModeResponse::swapContentsToHostEndianness()
{
if (endian::isLittleEndian()) { return; }
header.swapToHostEndianness();
command.swapToHostEndianness();
// ret_code and mode are uint8_t, no endianness conversion needed
// Note: footer.swapToHostEndianness() swaps CRC, so we skip it here
}
bool GetLiDARReturnModeResponse::sanityCheck() const
{
return header.sanityCheck() &&
command.sanityCheck() &&
(command.cmd_set == 0x01) && (command.cmd_id == 0x07) &&
(ret_code <= 0x01) && // Valid return codes: 0x00-0x01
(mode <= 0x03) && // Valid modes: 0x00-0x03
footer.sanityCheck();
}
bool GetLiDARReturnModeResponse::validateCrc32() const
{
const uint8_t* messageData = reinterpret_cast<const uint8_t*>(this);
size_t messageSize = sizeof(GetLiDARReturnModeResponse) - sizeof(footer.crc_32);
uint32_t calculatedCrc = comms::calculateCrc32(messageData, messageSize);
return (calculatedCrc == footer.crc_32);
}
} // namespace comms
} // namespace livoxProto1
+360
View File
@@ -0,0 +1,360 @@
#ifndef LIVOXPROTO1_PROTOCOL_H
#define LIVOXPROTO1_PROTOCOL_H
#include <vector>
#include <string>
#include <memory>
#include <sstream>
#include <atomic>
#include <cstdint>
#include <boost/asio/ip/address_v4.hpp>
#include <user/senseApiDesc.h>
namespace livoxProto1 {
namespace comms {
/** EXPLANATION:
* Undocumented Livox SDK CRC seed constants. These were found in the Livox SDK
* source code.
*/
constexpr uint16_t LIVOX_CRC16_SEED = 0x4c49;
constexpr uint32_t LIVOX_CRC32_SEED = 0x564f580a;
// Endianness detection
namespace endian {
inline bool isLittleEndian() {
union {
uint32_t i;
char c[4];
} test = {0x01020304};
return test.c[0] == 4;
}
}
// IPv4 address validation
inline bool isValidIPv4(const std::string& ipAddress) {
boost::system::error_code ec;
(void)boost::asio::ip::make_address_v4(ipAddress, ec);
return !ec;
}
// CRC calculation utilities
uint16_t calculateCrc16(
const uint8_t* data, size_t length);
uint32_t calculateCrc32(
const uint8_t* data, size_t length);
// IP address parsing utility
struct IPOctets {
std::string octet1, octet2, octet3, octet4;
};
std::optional<IPOctets> parseIPv4Address(const std::string& ipAddress);
// Device identifier comparison
inline bool deviceIdentifiersEqual(
const std::string& id1, const std::string& id2
)
{
// Use pointers to avoid unnecessary string copies
const std::string* serial1_ptr;
const std::string* serial2_ptr;
// Local copies only needed for 15-character broadcast codes
std::string serial1_copy, serial2_copy;
// Determine if id1 is serial (14 chars) or broadcast code (15 chars)
if (id1.length() == 14) {
serial1_ptr = &id1; // No copy needed, use original string
} else if (id1.length() == 15) {
serial1_copy = id1.substr(0, 14); // Copy only when necessary
serial1_ptr = &serial1_copy;
} else {
return false; // Invalid length
}
// Determine if id2 is serial (14 chars) or broadcast code (15 chars)
if (id2.length() == 14) {
serial2_ptr = &id2; // No copy needed, use original string
} else if (id2.length() == 15) {
serial2_copy = id2.substr(0, 14); // Copy only when necessary
serial2_ptr = &serial2_copy;
} else {
return false; // Invalid length
}
// Compare the serial numbers using pointers
return *serial1_ptr == *serial2_ptr;
}
/** EXPLANATION:
* Device types as defined in the Livox protocol specification
*/
enum class DeviceType : uint8_t {
Hub = 0,
Mid40 = 1,
Tele15 = 2,
Horizon = 3,
Mid70 = 6,
Avia = 7
};
/** EXPLANATION:
* Protocol frame header structure.
* All multi-byte fields are in little-endian format as per protocol spec.
*/
struct Header
{
uint8_t sof; // 0: Start of Frame (0xAA)
uint8_t version; // 1: Protocol Version (1)
uint16_t length; // 2-3: Frame Length (little-endian)
uint8_t cmd_type; // 4: Command Type (0x02 = MSG for broadcast)
uint16_t seq_num; // 5-6: Sequence Number (little-endian)
uint16_t crc_16; // 7-8: Header Checksum (little-endian)
void swapToHostEndianness();
void swapToProtocolEndianness();
void swapCrc16ToHostEndianness();
void swapCrc16ToProtocolEndianness();
bool sanityCheck() const;
uint16_t calculateCrc16() const;
bool validateCrc16() const;
void setCrc16FromRawBytes();
} __attribute__((packed));
/** EXPLANATION:
* Protocol frame footer structure.
* All multi-byte fields are in little-endian format as per protocol spec.
*/
struct Footer
{
uint32_t crc_32; // 0-3: Whole Frame Checksum (little-endian)
void swapToHostEndianness();
void swapToProtocolEndianness();
void swapCrc32ToHostEndianness();
void swapCrc32ToProtocolEndianness();
bool validateCrc32() const;
bool sanityCheck() const;
} __attribute__((packed));
/** EXPLANATION:
* Command identification structure used in all Livox protocol messages.
* Contains the command set and command ID fields.
*/
struct Command
{
uint8_t cmd_set; // 0: Command Set (0x00 = General, etc.)
uint8_t cmd_id; // 1: Command ID (0x00 = Broadcast, 0x01 = Handshake, etc.)
void swapToHostEndianness();
void swapToProtocolEndianness();
bool sanityCheck() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete wire format for Livox broadcast messages.
* All multi-byte fields are in little-endian format as per protocol spec.
*/
struct BroadcastMessage
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t broadcast_code[16]; // 11-26: Device Broadcast Code (null-terminated string)
uint8_t dev_type; // 27: Device Type
uint16_t reserved; // 28-29: Reserved (little-endian)
Footer footer; // 30-33: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete handshake request frame for connecting to Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct HandshakeRequest
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t user_ip[4]; // 11-14: Host IP Address (little-endian)
uint16_t data_port; // 15-16: Host Point Cloud Data UDP Destination Port (little-endian)
uint16_t cmd_port; // 17-18: Host Control Command UDP Destination Port (little-endian)
uint16_t imu_port; // 19-20: Host IMU UDP Destination Port (little-endian)
Footer footer; // 21-24: Protocol frame footer
HandshakeRequest(
const std::string& hostIP,
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort);
// Calculate CRC32 for the entire message
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete handshake response frame from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct HandshakeResponse
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t ret_code; // 11: Return Code (0x00 = Success, 0x01 = Fail)
Footer footer; // 12-15: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete heartbeat command frame for maintaining connection with Livox devices.
* This is the complete wire format including header, command fields, and footer.
*/
struct HeartbeatMessage
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
Footer footer; // 11-14: Protocol frame footer
HeartbeatMessage();
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete disconnect command frame for disconnecting from Livox devices.
* This is the complete wire format including header, command fields, and footer.
*/
struct DisconnectMessage
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
Footer footer; // 11-14: Protocol frame footer
DisconnectMessage();
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete start/stop sampling command frame for enabling/disabling point cloud data from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct StartStopSamplingMessage
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t enable; // 11: Enable flag (0x01 = Start, 0x00 = Stop)
Footer footer; // 12-15: Protocol frame footer
StartStopSamplingMessage();
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete sampling response frame from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct SamplingResponse
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t ret_code; // 11: Return Code (0x00 = Success, 0x01 = Fail)
Footer footer; // 12-15: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete heartbeat ACK response frame from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct HeartbeatACK
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t ret_code; // 11: Return Code (0x00 = Success, 0x01 = Fail)
uint8_t work_state; // 12: LiDAR/Hub State (0x00: Initializing, 0x01: Normal, 0x02: Power-Saving, 0x03: Standby, 0x04: Error)
uint8_t feature_msg; // 13: LiDAR Feature Message (Bit0: Rain/Fog Suppression Switch)
uint32_t ack_msg; // 14-17: ACK Message (Initialization Progress or Status Code)
Footer footer; // 18-21: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete set LiDAR return mode command frame for Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct SetLiDARReturnMode
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t mode; // 11: Return Mode (0x00: Single Return First, 0x01: Single Return Strongest, 0x02: Dual Return, 0x03: Triple Return)
Footer footer; // 12-15: Protocol frame footer
SetLiDARReturnMode();
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete set LiDAR return mode response frame from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct SetLiDARReturnModeResponse
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t ret_code; // 11: Return Code (0x00 = Success, 0x01 = Fail)
Footer footer; // 12-15: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
/** EXPLANATION:
* Complete get LiDAR return mode command frame for Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct GetLiDARReturnMode
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
Footer footer; // 11-14: Protocol frame footer
GetLiDARReturnMode();
uint32_t calculateCrc32() const;
void swapContentsToProtocolEndianness();
} __attribute__((packed));
/** EXPLANATION:
* Complete get LiDAR return mode response frame from Livox devices.
* This is the complete wire format including header, command fields, data, and footer.
*/
struct GetLiDARReturnModeResponse
{
Header header; // 0-8: Protocol frame header
Command command; // 9-10: Command identification
uint8_t ret_code; // 11: Return Code (0x00 = Success, 0x01 = Fail)
uint8_t mode; // 12: Return Mode (0x00: Single Return First, 0x01: Single Return Strongest, 0x02: Dual Return, 0x03: Triple Return)
Footer footer; // 13-16: Protocol frame footer
void swapContentsToHostEndianness();
bool sanityCheck() const;
bool validateCrc32() const;
} __attribute__((packed));
} // namespace comms
} // namespace livoxProto1
#endif // LIVOXPROTO1_PROTOCOL_H
@@ -0,0 +1,622 @@
#include <iostream>
#include <cstring>
#include <coroutine>
#include <functional>
#include <optional>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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"
namespace livoxProto1 {
namespace comms {
UdpCommandDemuxer::UdpCommandDemuxer(
const std::shared_ptr<sscl::ComponentThread> &componentThread,
DeviceManager &deviceManager,
uint16_t commandPort,
uint16_t dataPort
)
: componentThread(componentThread), deviceManager(deviceManager),
commandPort(commandPort), dataPort(dataPort),
senderAddrLen(sizeof(senderAddr))
{
}
UdpCommandDemuxer::~UdpCommandDemuxer()
{
stop();
}
void UdpCommandDemuxer::start()
{
if (isActive.load())
{
std::cerr << __func__ << ": Demuxer is already running"
<< std::endl;
return;
}
try
{
{
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
setupSockets();
isActive.store(true);
shouldStop.store(false);
}
// Start the async receive loop
startAsyncReceive();
std::cout
<< __func__ << ": UDP Command Demuxer started on port "
<< commandPort << std::endl;
}
catch (const std::exception &e)
{
std::cerr
<< __func__ << ": Failed to start demuxer: "
<< e.what() << std::endl;
isActive.store(false);
throw;
}
}
void UdpCommandDemuxer::stop()
{
{
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
if (!isActive.load())
{ return; }
shouldStop.store(true);
}
// Close socket and cleanup
if (cmdEndpointFdDesc)
{
cmdEndpointFdDesc->cancel();
cmdEndpointFdDesc.reset();
}
if (pcloudDataFdDesc)
{
pcloudDataFdDesc->cancel();
pcloudDataFdDesc.reset();
}
isActive.store(false);
std::cout
<< __func__ << ": UDP Command Demuxer stopped"
<< std::endl;
}
void UdpCommandDemuxer::setupSockets()
{
setupCommandSocket();
setupPcloudDataSocket();
}
void UdpCommandDemuxer::setupCommandSocket()
{
// RAII class to manage socket file descriptor
struct SocketRAII
{
int fd;
SocketRAII(int socketFd) : fd(socketFd) {}
~SocketRAII() { if (fd >= 0) close(fd); }
void commit() { fd = -1; } // Transfer ownership, prevent close
int getFd() const { return fd; }
bool isValid() const { return fd >= 0; }
};
// Create UDP socket
SocketRAII socketGuard(socket(AF_INET, SOCK_DGRAM, 0));
if (!socketGuard.isValid())
{
throw std::runtime_error(
std::string(__func__)
+ ": 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(
socketGuard.getFd(), F_SETFL, flags | O_NONBLOCK) < 0)
{
throw std::runtime_error(
std::string(__func__)
+ ": Failed to set non-blocking mode: " + strerror(errno));
}
/** 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;
localAddr.sin_addr.s_addr = INADDR_ANY;
localAddr.sin_port = htons(commandPort);
if (bind(
socketGuard.getFd(), (struct sockaddr *)&localAddr,
sizeof(localAddr)) < 0)
{
throw std::runtime_error(
std::string(__func__) + ": Failed to bind to port "
+ 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->getIoContext(), socketGuard.getFd());
// Transfer ownership, prevent auto-close
socketGuard.commit();
}
void UdpCommandDemuxer::setupPcloudDataSocket()
{
// RAII class to manage socket file descriptor
struct SocketRAII
{
int fd;
SocketRAII(int socketFd) : fd(socketFd) {}
~SocketRAII() { if (fd >= 0) close(fd); }
void commit() { fd = -1; } // Transfer ownership, prevent close
int getFd() const { return fd; }
bool isValid() const { return fd >= 0; }
};
// Create UDP socket for point cloud data reception
SocketRAII socketGuard(socket(AF_INET, SOCK_DGRAM, 0));
if (!socketGuard.isValid())
{
throw std::runtime_error(
std::string(__func__)
+ ": Failed to create socket: " + strerror(errno));
}
// Set socket to non-blocking mode
int flags = fcntl(socketGuard.getFd(), F_GETFL, 0);
if (flags < 0 ||
fcntl(socketGuard.getFd(), F_SETFL, flags | O_NONBLOCK) < 0)
{
throw std::runtime_error(
std::string(__func__)
+ ": Failed to set non-blocking mode: " + strerror(errno));
}
// Bind to the data port
struct sockaddr_in localAddr;
memset(&localAddr, 0, sizeof(localAddr));
localAddr.sin_family = AF_INET;
localAddr.sin_addr.s_addr = INADDR_ANY;
localAddr.sin_port = htons(dataPort);
if (bind(
socketGuard.getFd(), (struct sockaddr *)&localAddr,
sizeof(localAddr)) < 0)
{
throw std::runtime_error(
std::string(__func__) + ": Failed to bind to data port: "
+ std::to_string(dataPort) + ": " + strerror(errno));
}
// Create boost wrapper for async operations
pcloudDataFdDesc = std::make_shared<boost::asio::posix::stream_descriptor>(
componentThread->getIoContext(), socketGuard.getFd());
// Transfer ownership, prevent auto-close
socketGuard.commit();
}
void UdpCommandDemuxer::startAsyncReceive()
{
if (!isActive.load() || shouldStop.load())
{ return; }
cmdEndpointFdDesc->async_wait(
boost::asio::posix::stream_descriptor::wait_read,
std::bind(
&UdpCommandDemuxer::onDataReady, this, std::placeholders::_1));
}
void UdpCommandDemuxer::onDataReady(const boost::system::error_code &error)
{
if (error)
{
if (error != boost::asio::error::operation_aborted)
{
std::cerr
<< __func__ << ": Socket error: "
<< error.message() << std::endl;
}
return;
}
sscl::SpinLock::Guard lock(isActiveAndShouldStopLock);
if (!isActive.load() || shouldStop.load())
{ return; }
// Read the data
bytesReceived = recvfrom(
cmdEndpointFdDesc->native_handle(), receiveBuffer,
sizeof(receiveBuffer), 0,
(struct sockaddr *)&senderAddr, &senderAddrLen);
if (bytesReceived > 0) {
processIncomingData();
}
else if (bytesReceived < 0)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
std::cerr << __func__ << ": recvfrom error: "
<< strerror(errno) << std::endl;
}
}
// Continue listening for more data
startAsyncReceive();
}
void UdpCommandDemuxer::processIncomingData()
{
if (bytesReceived < 2)
{
// Too small to contain any meaningful data
return;
}
// Extract source IP address
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)
{
if (device->discoveredDevice.ipAddr != sourceIP) { continue; }
// Found matching device, route the datagram to it
try
{
device->handleUdpDgram(
receiveBuffer, bytesReceived, senderAddr);
}
catch (const std::exception &e)
{
std::cerr
<< __func__ << ": Device handler exception for IP "
<< sourceIP << ": " << e.what() << std::endl;
}
return;
}
// If not found in DeviceManager, check temporary collection (devices under construction)
auto tempIt = livoxProto1::Device::devicesUnderConstruction.find(sourceIP);
if (tempIt != livoxProto1::Device::devicesUnderConstruction.end())
{
// Extract command set and command ID from the datagram
if (bytesReceived >= static_cast<ssize_t>(
sizeof(livoxProto1::comms::Header)
+ sizeof(livoxProto1::comms::Command)))
{
uint8_t cmd_set = receiveBuffer[
sizeof(livoxProto1::comms::Header)];
uint8_t cmd_id = receiveBuffer[
sizeof(livoxProto1::comms::Header) + 1];
// Found matching dev in temp collection, invoke matching handlers
for (const auto& cmdHandler : tempIt->second)
{
if (cmdHandler.cmd_set != cmd_set
|| cmdHandler.cmd_id != cmd_id)
{
continue;
}
try
{
cmdHandler.handler(
receiveBuffer, bytesReceived, senderAddr);
}
catch (const std::exception &e)
{
std::cerr << __func__ << ": Temporary device handler "
"exception for IP " << sourceIP << ": " << e.what()
<< std::endl;
}
}
}
return;
}
// No device found with matching IP in either collection, discard the data
std::cerr
<< __func__ << ": No device found for source IP "
<< 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
+181
View File
@@ -0,0 +1,181 @@
#ifndef UDP_COMMAND_DEMUXER_H
#define UDP_COMMAND_DEMUXER_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 <spinscale/spinLock.h>
#include <spinscale/sharedResourceGroup.h>
#include <spinscale/co/invokers.h>
namespace livoxProto1 {
// Forward declarations
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
*
* This class listens on the command port (65000) for incoming UDP datagrams
* from Livox devices and routes them to the appropriate Device based on
* the source IP address.
*
* The reason we need a whole class for this is because we use the same port
* numbers for all connected devices, so we have no way to distinguish between
* devices except based on the devices' IP addrs. Since all commands are sent
* over UDP, our sockets don't have built-in binding to a specific source IP.
*
* So we need to discriminate between source IPs manually, and demultiplex
* the dgrams received from different devices manually.
*
* We'll prolly also have to do the same thing for point cloud and IMU data, so
* we'll prolly end up renaming this class to UdpResponseDemuxer.
*/
class UdpCommandDemuxer
{
public:
UdpCommandDemuxer(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
DeviceManager& deviceManager,
uint16_t commandPort = 56001,
uint16_t dataPort = 56000);
~UdpCommandDemuxer();
void start();
void stop();
bool isRunning() const { return isActive.load(); }
// Get shared pointer to command endpoint for handshake use
std::shared_ptr<boost::asio::posix::stream_descriptor>
getCmdEndpointFdDesc() const
{
return cmdEndpointFdDesc;
}
// Get shared pointer to pcloud data fd for use in IoUringAssemblyEngine
std::shared_ptr<boost::asio::posix::stream_descriptor>
getPcloudDataFdDesc() const
{
return pcloudDataFdDesc;
}
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();
void startAsyncReceive();
void onDataReady(const boost::system::error_code& error);
void processIncomingData();
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
sscl::SpinLock isActiveAndShouldStopLock;
std::atomic<bool> isActive{false};
std::atomic<bool> shouldStop{false};
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;
ssize_t bytesReceived;
};
} // namespace comms
} // namespace livoxProto1
#endif // UDP_COMMAND_DEMUXER_H
+8 -1
View File
@@ -10,11 +10,18 @@ if(ENABLE_LIB_xcbXorg)
xcbXorg.cpp xcbXorg.cpp
) )
set_target_properties(xcbXorg PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
# Set config define for header generation # Set config define for header generation
add_compile_definitions(CONFIG_LIB_XCBXORG_ENABLED) add_compile_definitions(CONFIG_LIB_XCBXORG_ENABLED)
target_include_directories(xcbXorg PUBLIC ${XCB_INCLUDE_DIRS}) target_include_directories(xcbXorg PUBLIC ${XCB_INCLUDE_DIRS})
target_link_libraries(xcbXorg ${XCB_LIBRARIES}) target_link_libraries(xcbXorg ${XCB_LIBRARIES})
# Install rules # Install rules
install(TARGETS xcbXorg DESTINATION lib) install(TARGETS xcbXorg
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
)
endif() 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;
}
+14
View File
@@ -0,0 +1,14 @@
add_subdirectory(bodies)
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
register_daps_target(all_device_specs)
# 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 all_device_specs PROPERTY FOLDER "devices")
+24
View File
@@ -0,0 +1,24 @@
#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
+29
View File
@@ -0,0 +1,29 @@
add_daps_target(body_rpi5_persys_headless
SOURCES
rpi5-persys-headless.dapss
)
add_daps_target(body_rpi5_persys
SOURCES
rpi5-persys.dapss
)
add_daps_target(body_dell_laptop
SOURCES
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")
+5
View File
@@ -0,0 +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"
+4
View File
@@ -0,0 +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
View File
@@ -0,0 +1,6 @@
#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.%"

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