Compare commits
704 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 026ba608a1 | |||
| 63ff0aa264 | |||
| a9de2bb1ab | |||
| 5f4665b221 | |||
| 99ee28629e | |||
| 83698ded42 | |||
| acb684ad35 | |||
| 24eee2d240 | |||
| 8d03dcf9b5 | |||
| 959229c2a0 | |||
| 1431214b95 | |||
| bb83a86fe0 | |||
| e261787cfe | |||
| 63532a6ee2 | |||
| e7b7a311f7 | |||
| 809861be2b | |||
| 7af684039d | |||
| 7a47f2bd49 | |||
| 5f3d5c7818 | |||
| 42c9fcdfdf | |||
| b198f6a42b | |||
| 3e85b920fb | |||
| 25d7b9c013 | |||
| f3ca20ac1d | |||
| dd0642535c | |||
| 10697acd61 | |||
| 4bcc30671b | |||
| 2458c83c6b | |||
| 46f767f232 | |||
| cc7f4fcd9b | |||
| e383453278 | |||
| 69a4782e19 | |||
| 5935917204 | |||
| a42d8f8a07 | |||
| 54dcc92c2b | |||
| e1d299859d | |||
| d118181766 | |||
| 82b99e680c | |||
| 549f0c04f4 | |||
| 0722ef8209 | |||
| 8836ab470b | |||
| 560e5364a0 | |||
| ab930a2df3 | |||
| 7eda755c15 | |||
| 10234bc422 | |||
| f118947b5e | |||
| cda9d432f4 | |||
| f9c64cf363 | |||
| facb665217 | |||
| 22a4cf283e | |||
| 2602094139 | |||
| a31a21be65 | |||
| 4a4c76b5ec | |||
| ad88a5e1c8 | |||
| 87a8de9a2b | |||
| 165c846700 | |||
| 91fc655b25 | |||
| 5b81ea893c | |||
| b0d67596d0 | |||
| b2644f17c6 | |||
| 241e8a6798 | |||
| f97641f8b5 | |||
| 86c036a505 | |||
| 986e1833d0 | |||
| d4905f53df | |||
| 2c1c994896 | |||
| d1c74a027c | |||
| c2eea37a7b | |||
| ac39a8b876 | |||
| 35eb466a60 | |||
| 1cf1be4194 | |||
| acf62e61da | |||
| 4266af545a | |||
| f100764bd8 | |||
| c7dee57072 | |||
| 2967a4d6ba | |||
| 322a8137b2 | |||
| d788810a05 | |||
| 5a9fe12057 | |||
| 25efccf6c5 | |||
| bbc16dc4c4 | |||
| 958c57b3ff | |||
| fc5ebb72b9 | |||
| 7643cf7fed | |||
| 4186ff141e | |||
| 3e19d39853 | |||
| b5fa20a2b8 | |||
| baa9b7b499 | |||
| 9feadd0820 | |||
| f9ac41c56e | |||
| cde2737876 | |||
| c539e6e924 | |||
| 72134aeac5 | |||
| ea3fc4873c | |||
| eae01c2d4c | |||
| 479229d0da | |||
| 0bc8bfea3d | |||
| 66dc227d31 | |||
| 27a5d48451 | |||
| 632a227985 | |||
| fc1fcae0b0 | |||
| c696316a1e | |||
| e8044a0d17 | |||
| 1c0f028de0 | |||
| 7516da6aa8 | |||
| bedcf78b29 | |||
| 3dd627c91c | |||
| 5811af7cb1 | |||
| 156da322b6 | |||
| 7435c6e393 | |||
| 88513c0f4d | |||
| ba76bb0b00 | |||
| 8116d19741 | |||
| 1d64ce0c7e | |||
| 26dd686ebf | |||
| 06996d166e | |||
| 3f2d7c24ee | |||
| cbf9d418b7 | |||
| bfaba8cc0e | |||
| a1fd39eb05 | |||
| c90f974bcb | |||
| aec3cbedf2 | |||
| 1c397dfeb5 | |||
| 9361a43e40 | |||
| 8011fe12bc | |||
| ab399cafeb | |||
| 687bab53b5 | |||
| 9159e9f7b4 | |||
| c0752b5e84 | |||
| 686bd6d38b | |||
| e4adfa0e61 | |||
| 4520306f4e | |||
| c08563c8e8 | |||
| 5a4f498663 | |||
| 7acdfcc337 | |||
| 34d76df7d9 | |||
| 0c4f427c0a | |||
| f862db922e | |||
| cd77f4b02d | |||
| 2222491c21 | |||
| bfe5eb12af | |||
| b6cf1c656f | |||
| 45959f9d1c | |||
| d5c2b61d4c | |||
| 2dc6b729e0 | |||
| d39bc4b475 | |||
| 6e89c7e72f | |||
| 7167cea62c | |||
| 702855a27d | |||
| dc5587bfcc | |||
| 5dffbd0c91 | |||
| 7ebdf14eb7 | |||
| 4f3462626d | |||
| e06b2d7e06 | |||
| 33681059b0 | |||
| 30f599cde3 | |||
| 0116523a66 | |||
| 1f35dba2ca | |||
| 280b6f7d1c | |||
| 5b19a70c75 | |||
| 2a8d320f7a | |||
| 1e76d51c41 | |||
| 313454c426 | |||
| d49594ef88 | |||
| e51d371f58 | |||
| 8eb7eaba3d | |||
| ce0456d472 | |||
| 49191b3a15 | |||
| e5782b0af7 | |||
| b6c426871e | |||
| 5cce473e01 | |||
| edab71b823 | |||
| 3b8c1dac32 | |||
| b0df1ef3d0 | |||
| 617020b534 | |||
| d01e448b40 | |||
| 17c0e10be8 | |||
| 9f839df36a | |||
| 601c7857f4 | |||
| 0c2a14434b | |||
| ce690bc3f4 | |||
| e689063a8c | |||
| f57236530d | |||
| 79df8b3f74 | |||
| a025d13fce | |||
| 2c891bd2f3 | |||
| 3747dee8a7 | |||
| 9ce1ced92d | |||
| 9e64c510cc | |||
| 9d9644cb31 | |||
| 1bf0a195aa | |||
| e233dc51d6 | |||
| 51d2a70a3f | |||
| ee6405048a | |||
| 2c7e090ef1 | |||
| 0cfb0a9c07 | |||
| 5789a31e23 | |||
| 27b43c6686 | |||
| a910909ad5 | |||
| 41b8385cb2 | |||
| 3f04d1b387 | |||
| a4493b26a1 | |||
| a18fab04a5 | |||
| f919385088 | |||
| 3bcb83894b | |||
| 44435c61eb | |||
| af5046c933 | |||
| c5ed453bb4 | |||
| addd2e275d | |||
| 336bc52a9d | |||
| c060463e82 | |||
| 1f7c7f5f28 | |||
| bed10df499 | |||
| b3743560bb | |||
| 8e48ce6ceb | |||
| d277c29394 | |||
| 2d1c026cc2 | |||
| 340604c4ea | |||
| 2632917c63 | |||
| 7a51f02d97 | |||
| e215e78aa5 | |||
| 188b09319c | |||
| 475f67d36e | |||
| 16b51a3b66 | |||
| 7d86ecadc4 | |||
| 98a493a8a1 | |||
| 8a7dc10892 | |||
| 70c0175a8b | |||
| 74e3896ae4 | |||
| 7b7ff06219 | |||
| 51a2858214 | |||
| 2e75dd40aa | |||
| c08e075763 | |||
| 3995f57489 | |||
| 0720ed9c76 | |||
| c268414b0d | |||
| a1625eb562 | |||
| 324e3d1f6a | |||
| 39691175e7 | |||
| 1df43665c3 | |||
| 501effe6d5 | |||
| d01f06904a | |||
| 16a74a3eb0 | |||
| a17072c8d9 | |||
| 67923d5f86 | |||
| 972d5fc9db | |||
| 5c3debecf4 | |||
| e446d42b3c | |||
| 63fa0be91a | |||
| 6d669ee8b2 | |||
| d60fd98887 | |||
| 5031b22a31 | |||
| df58f324a9 | |||
| 7e672bcc9a | |||
| 371ae5803d | |||
| 1a9c96c857 | |||
| 116a642a9f | |||
| d87c71b794 | |||
| 33b534355a | |||
| 96e64e24b8 | |||
| 1dc74065fb | |||
| d687ca0164 | |||
| 91e0fd0f8e | |||
| 4dbb27fd1f | |||
| b55e7a8b19 | |||
| f58f908366 | |||
| a52685fbdf | |||
| 5bb9c9e90b | |||
| 401c844fcc | |||
| eedeb4b803 | |||
| 19a79faabe | |||
| 1ac6fa4a16 | |||
| 7cae3452fc | |||
| 582aefb02c | |||
| aef251b7e5 | |||
| ad0b8058a4 | |||
| b331af4f03 | |||
| 683e107b04 | |||
| c8cbaed3b1 | |||
| 5f03e4c392 | |||
| 55116b1d41 | |||
| 7977f0bcc9 | |||
| 6264a128a8 | |||
| 4b60a10bc6 | |||
| 01ba68f2b5 | |||
| 511f1796e8 | |||
| a0a5aa49ad | |||
| d2e2d9bc3b | |||
| 010ba9c7bd | |||
| 72a3415553 | |||
| a0ab5538df | |||
| 5b7b4f215a | |||
| d8a3999ad5 | |||
| 5ff6a4ee0b | |||
| 6a5bb47e0e | |||
| 073cdde08f | |||
| 869160b782 | |||
| e1042724fc | |||
| 28e56653ea | |||
| 5dbed56e38 | |||
| 9233f7fdc8 | |||
| b460c8b2d3 | |||
| bc56c83fad | |||
| cb493d7598 | |||
| 1c50fc0e29 | |||
| 7497f2fd95 | |||
| 5f11a9d6c7 | |||
| 0b21cdd2ba | |||
| f5146738e1 | |||
| 479219db2d | |||
| 1afa085fd4 | |||
| 7b092956c0 | |||
| e0c0976e0b | |||
| 887fa1ab6f | |||
| 7d2cb58200 | |||
| b598ca8594 | |||
| a7a85b0c1f | |||
| 457d0f9345 | |||
| eeb057effd | |||
| c7e117b08e | |||
| af57c4dfd1 | |||
| db30001140 | |||
| d69636bf7b | |||
| 59a584561d | |||
| f7aba4af4e | |||
| 21bbaf846e | |||
| aacbdd5864 | |||
| bb59f47549 | |||
| 1c7277d141 | |||
| d29ebafea0 | |||
| 94982d50b9 | |||
| 0503705a13 | |||
| ef9eef2bc3 | |||
| 2b3b318abe | |||
| f3a4c69597 | |||
| 032e9ef8d5 | |||
| 4a1bcb1516 | |||
| d6e1e7ebc0 | |||
| 9a4f80a9d6 | |||
| 7a55a65589 | |||
| 14b97a52ed | |||
| 5845f1a41d | |||
| 6ea90c2dae | |||
| 121b7db045 | |||
| d88dd2cf44 | |||
| b3bf0e2cb9 | |||
| 45ad5c83ee | |||
| 10e615e75e | |||
| 05515743c5 | |||
| b2c73f6bed | |||
| 797a95e6a1 | |||
| 972979cc10 | |||
| ba955ef633 | |||
| a32b4f05d1 | |||
| c8474edad7 | |||
| 58e9b09995 | |||
| c2c6d409dd | |||
| 8dba0fdfc4 | |||
| 67af9f02da | |||
| e824685c19 | |||
| 9cf1398f5c | |||
| f76f718e80 | |||
| cdade17905 | |||
| 5af7e531b6 | |||
| 018c1f1e1d | |||
| 5e522178d8 | |||
| 7574f3f59a | |||
| 0de031c74b | |||
| ebbb2b1345 | |||
| 3bf8146ca3 | |||
| f32a472c5d | |||
| 7994c2f6e2 | |||
| 9ab155560a | |||
| 720babd39d | |||
| 5c3bc6c324 | |||
| b53ef42124 | |||
| babfda4d0f | |||
| 88dd872c95 | |||
| b8255234de | |||
| 1a4f7f97bd | |||
| 13a948a2d3 | |||
| 07c48d78d1 | |||
| 7b6bfbad68 | |||
| 393326052c | |||
| b3d0565e11 | |||
| 287dd6be56 | |||
| 0b2fde3484 | |||
| c1286627ab | |||
| 2234df1de2 | |||
| 4db3581be9 | |||
| 7efe622dd2 | |||
| f658e97ed0 | |||
| f8c5fad841 | |||
| 626a84cc78 | |||
| a68143810e | |||
| 109cd9eb03 | |||
| 7b830f0a68 | |||
| b65b0f2370 | |||
| 2a8a6edf22 | |||
| 10e19a3237 | |||
| 9e83a99c9c | |||
| b576d41595 | |||
| b89c8cdc4f | |||
| 682b9e121b | |||
| bcf81594e7 | |||
| 1b9acd5603 | |||
| dc23a61410 | |||
| e9b4e15b79 | |||
| fca665d44e | |||
| 862acf0fe3 | |||
| 6650664529 | |||
| e297a260d9 | |||
| d54ef04c47 | |||
| 92399ba283 | |||
| 6f4a2dd649 | |||
| 0e872042ee | |||
| 266cabcddb | |||
| 444555e9b6 | |||
| 3373393755 | |||
| dc864bad00 | |||
| 452d1966fc | |||
| f7dcb7307d | |||
| bd0118531f | |||
| 4bfcdf37da | |||
| 2b8f6b6ad5 | |||
| a5cf996ed2 | |||
| 71c2b855ec | |||
| 06c5f4503f | |||
| bede123691 | |||
| 83c937ae8f | |||
| d39dfb5334 | |||
| 44cfd7ab69 | |||
| b277baa76d | |||
| 5db1cfdac8 | |||
| a4d99e5d4d | |||
| 01ad1ff073 | |||
| 8e1d609ca1 | |||
| 10afec2532 | |||
| 66a9db13c3 | |||
| d9042c6510 | |||
| 870057a680 | |||
| e444cd1e04 | |||
| 6bc5bd30d5 | |||
| 56367402d7 | |||
| 035accf553 | |||
| 55f21c5436 | |||
| d1b99852a8 | |||
| 66bb30cef5 | |||
| 068a885bff | |||
| 7d453beb65 | |||
| 49c9caa317 | |||
| b06e9693c5 | |||
| e4e700c362 | |||
| d317f1fb06 | |||
| edd223b083 | |||
| 945c5b397b | |||
| 5017bf5f92 | |||
| 95d5c46e43 | |||
| 4a55ff9bf2 | |||
| 27ff4a3a0a | |||
| eddee05e41 | |||
| ccc7fd8e04 | |||
| eb810e62e9 | |||
| 3a50be05f8 | |||
| 168d8d616c | |||
| 16775c6f1e | |||
| 385b7d5a3c | |||
| d857999fdf | |||
| fa2609f4ce | |||
| a91a995407 | |||
| eb5875fe0d | |||
| 56b8e83a09 | |||
| a66d91fa31 | |||
| b69572eee7 | |||
| b771856330 | |||
| c7ca889e9c | |||
| da1ca774e7 | |||
| abc11bc7fc | |||
| 1b29f0e521 | |||
| f40225681c | |||
| e9e273c012 | |||
| 942f9d8515 | |||
| 1e22656299 | |||
| 80004ab1e7 | |||
| 55fe2675df | |||
| af19125ac2 | |||
| eb7fe11de4 | |||
| 7ddbde1a2f | |||
| ac3d97b3ec | |||
| 274143e41d | |||
| d75430ef82 | |||
| 06f3f2eebe | |||
| 07609c6d6c | |||
| 1bad358921 | |||
| 77e123ff4a | |||
| 9c3a8ea695 | |||
| 0ff86a0a5e | |||
| 8ad5179a61 | |||
| e600b0f96e | |||
| b0d61c3e38 | |||
| 296e517389 | |||
| 758586bb3c | |||
| ccf0ab77bf | |||
| 774661e1f0 | |||
| 33b61f429a | |||
| 7ded9d287d | |||
| a55bcc2a03 | |||
| f15c07bc83 | |||
| e3ce533fe4 | |||
| 2075c0b797 | |||
| 37b8cb0c7f | |||
| 4ed36eb88a | |||
| e53b0be7e2 | |||
| e299e956e5 | |||
| ca9eae197f | |||
| 08122c086c | |||
| cb8b13d0cd | |||
| 46686db07e | |||
| 97d93c670e | |||
| 9d77e8b345 | |||
| 4bf19fda90 | |||
| ed9635582f | |||
| b7ff100499 | |||
| d06ba8957a | |||
| 1aec779351 | |||
| 6baa0bb008 | |||
| 274edc1013 | |||
| c0eecf76d4 | |||
| 2390042892 | |||
| d32cb1ddac | |||
| 0cf86cf18a | |||
| 5c7a92b3a4 | |||
| 21da27649e | |||
| 71564b4d83 | |||
| 8123ec1227 | |||
| 2a60fdd9df | |||
| 462247d743 | |||
| 6b4fe05fc0 | |||
| 0090aa6e3a | |||
| 7e514a1fa3 | |||
| f1ce1ab19c | |||
| 542e3081ad | |||
| 65b9460a3a | |||
| cea65dcd00 | |||
| 6cebf6856e | |||
| 20d568a063 | |||
| bd52e49ba3 | |||
| 93103aa8d4 | |||
| 2be78401b5 | |||
| eca3f47884 | |||
| f3909fc000 | |||
| c0798d1bdb | |||
| e45a9ee5d1 | |||
| b43ffcb677 | |||
| 2c60248127 | |||
| 993bf568fc | |||
| 27e707a22d | |||
| 7ab6e7b2c3 | |||
| 5b2354bfe0 | |||
| 42ab935da6 | |||
| 572a8612ed | |||
| 51b70b179c | |||
| 1a56e2a107 | |||
| e6b8d3e85d | |||
| 52567406ca | |||
| 2f18ade4ab | |||
| f8bf8083af | |||
| 782bcd4567 | |||
| 2212aec080 | |||
| d0303becd7 | |||
| 27bebeb702 | |||
| 09a0041f20 | |||
| c6c3d6c9e8 | |||
| 092a0954a0 | |||
| d2ed525106 | |||
| dbc9569775 | |||
| 1e2cc5ef16 | |||
| 31cadb2ee4 | |||
| 329d57a16d | |||
| 32179eee5e | |||
| f05c465d61 | |||
| 79c50ff191 | |||
| d10217f3f5 | |||
| 4b0e832e27 | |||
| dd9ce63cb8 | |||
| b9322c5e89 | |||
| 2dfa615eb7 | |||
| 596bc1fbd2 | |||
| ec50526804 | |||
| eb3366cfd2 | |||
| b49e281010 | |||
| 9a23dbbe95 | |||
| 5a3c0699f7 | |||
| 816a047920 | |||
| 33c006b178 | |||
| 0733bb9a68 | |||
| eeaa4ed2df | |||
| 0788bbd799 | |||
| a6eccede4a | |||
| 437f7ea10f | |||
| 58f7df49ae | |||
| 03da91f5e5 | |||
| 02c071394b | |||
| c52c447a78 | |||
| ddd6f6d6c6 | |||
| f5c359a6a9 | |||
| af33b7f097 | |||
| 92e55641a0 | |||
| 9e00cd1530 | |||
| 8fd8826f8d | |||
| 5d30941aab | |||
| 429bd2a349 | |||
| 5c79a89cd4 | |||
| a931f9f01a | |||
| 7f3bfec835 | |||
| 19b39d391f | |||
| 674d74cfb9 | |||
| b768739b96 | |||
| 77acbdd8de | |||
| 7b699d5d36 | |||
| db8f047322 | |||
| 472184bbbc | |||
| dd3d5fea66 | |||
| 6573a1b14d | |||
| 0759461c69 | |||
| 62cada2547 | |||
| e755383e72 | |||
| d1e4c1a2ea | |||
| 29b192b2ee | |||
| 7c48abbcca | |||
| 0ec227cf9e | |||
| baad2a9890 | |||
| 91ccd16b33 | |||
| 16865dc36f | |||
| da0ef64f62 | |||
| 83af74f4be | |||
| 7cb6c8521e | |||
| 1d3d929ddd | |||
| 25a9721f92 | |||
| b99b147959 | |||
| 96bf653167 | |||
| 4429135539 | |||
| 89947dfc71 | |||
| fb17c51ef6 | |||
| b8c931397d | |||
| f5195450e4 | |||
| b4102e6ee1 | |||
| 20034513ad | |||
| 3a852bfb9d | |||
| 4f74e1cd31 | |||
| b7cf4c9135 | |||
| e08dc0678b | |||
| 81842e4571 | |||
| 428a32a950 | |||
| 01da06b051 | |||
| a989256f22 | |||
| 21d2df4d34 | |||
| 8d18765a3a | |||
| 0c43c88554 | |||
| 07937a4846 | |||
| 067c928e47 | |||
| 680977b211 | |||
| 4143541adf | |||
| 1b6b12256d | |||
| 5b5a701c69 | |||
| 3457efcbf8 | |||
| bb1c7e4be1 | |||
| cc33b333d2 | |||
| 0449e557b0 | |||
| 73b2d981f9 | |||
| 20cdf64afb | |||
| 48121ec84c | |||
| 725f8772b1 | |||
| 8413277847 | |||
| 576d3ed7a5 | |||
| 381b29c12d | |||
| 0dcb4ce65f | |||
| c880f5b73e | |||
| 6ba6cb9cf0 | |||
| f587b45b38 | |||
| 1ededb85b9 | |||
| 9a500e39ab | |||
| 8f0e945f0c | |||
| 5d55157ffd | |||
| 7f367fd6e3 | |||
| b0596d12f9 | |||
| 3b07a15e11 | |||
| 38b29ddfc0 | |||
| d2bf5aceee | |||
| a0d577bf81 | |||
| 25234c4229 | |||
| 71c448a1d7 | |||
| 93fd2ac0ab | |||
| 3e9eecc279 | |||
| 5e4597b8fd | |||
| 6eb6fa1eb0 | |||
| f00e1c7cf1 | |||
| 26be261cff | |||
| e5a3c41c20 | |||
| 1d9b8a2cf6 | |||
| 4ce4c9f9f8 | |||
| fe5a7d480d | |||
| 83273ae806 | |||
| 04a3631881 |
@@ -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
|
||||
@@ -12,3 +12,6 @@ config.h.in
|
||||
configure
|
||||
*.swp
|
||||
cscope.out
|
||||
*.tmp
|
||||
.cache
|
||||
.codex
|
||||
|
||||
@@ -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
|
||||
Vendored
+4
-4
@@ -3,12 +3,11 @@
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"${workspaceFolder}/include",
|
||||
"${workspaceFolder}/smocore/include",
|
||||
"${workspaceFolder}/b/include",
|
||||
"/usr/include",
|
||||
"/usr/local/include",
|
||||
"${workspaceFolder}/b/include"
|
||||
"/usr/local/include"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/g++",
|
||||
@@ -24,7 +23,8 @@
|
||||
},
|
||||
"forcedInclude": [
|
||||
"${workspaceFolder}/b/include/config.h"
|
||||
]
|
||||
],
|
||||
"configurationProvider": "ms-vscode.cmake-tools"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
|
||||
Vendored
+110
-77
@@ -1,83 +1,116 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"cstdint": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"string": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"format": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ostream": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp",
|
||||
"cstring": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"any": "cpp",
|
||||
"codecvt": "cpp",
|
||||
"complex": "cpp",
|
||||
"coroutine": "cpp",
|
||||
"csignal": "cpp",
|
||||
"list": "cpp",
|
||||
"source_location": "cpp",
|
||||
"future": "cpp",
|
||||
"shared_mutex": "cpp",
|
||||
"typeindex": "cpp",
|
||||
"bitset": "cpp"
|
||||
},
|
||||
"*.cl": "c",
|
||||
"cstdint": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"string": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"format": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ostream": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp",
|
||||
"cstring": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"any": "cpp",
|
||||
"codecvt": "cpp",
|
||||
"complex": "cpp",
|
||||
"coroutine": "cpp",
|
||||
"csignal": "cpp",
|
||||
"list": "cpp",
|
||||
"source_location": "cpp",
|
||||
"future": "cpp",
|
||||
"shared_mutex": "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.tabSize": 4,
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -1,20 +1,35 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(salmanoff VERSION 0.00.004 LANGUAGES CXX)
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
if(POLICY CMP0167)
|
||||
cmake_policy(SET CMP0167 NEW)
|
||||
endif()
|
||||
project(salmanoff VERSION 0.01.001 LANGUAGES CXX)
|
||||
|
||||
include(CMakeDependentOption)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DAPSS.cmake)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DebugOpts.cmake)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Set C++ standard
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Build type
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Debug FORCE)
|
||||
set(CMAKE_BUILD_TYPE Debug FORCE)
|
||||
endif()
|
||||
|
||||
# Compiler flags
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
|
||||
|
||||
# Ensure installed directories use Debian-standard permissions instead of
|
||||
# inheriting group-writable bits from the build tree.
|
||||
set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
# Mind oscillator configuration
|
||||
set(MIND_VOSCILLATOR_PERIOD_MS 33 CACHE STRING "Mind's virtual osc clock rate (ms)")
|
||||
if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
|
||||
@@ -22,10 +37,57 @@ if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
|
||||
endif()
|
||||
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_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/config.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/config.h
|
||||
@ONLY
|
||||
)
|
||||
|
||||
# Include directories
|
||||
@@ -36,32 +98,162 @@ include_directories(
|
||||
)
|
||||
|
||||
# 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(FLEX REQUIRED)
|
||||
find_package(BISON REQUIRED)
|
||||
|
||||
# It's important to note that as of 2025-12-02, RustICL on the RPi5 suddenly
|
||||
# began requiring that the user running Smo be a member of the "render" group.
|
||||
# We need to take that into account when we eventually build installer packages.
|
||||
# Users may also need to be members of the "video" group.
|
||||
|
||||
# Find OpenCL 1.2 or higher: try find_package first, fall back to pkg-config
|
||||
find_package(OpenCL 1.2 QUIET)
|
||||
if(OpenCL_FOUND)
|
||||
# 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()
|
||||
find_library(DL_LIBRARY NAMES dl ldl)
|
||||
if(NOT DL_LIBRARY)
|
||||
message(FATAL_ERROR "Dynamic linking library (libdl/libldl) not found")
|
||||
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_subdirectory(smocore)
|
||||
add_subdirectory(commonLibs)
|
||||
add_subdirectory(senseApis)
|
||||
add_subdirectory(comparatorLibs)
|
||||
add_subdirectory(stimBuffApis)
|
||||
add_subdirectory(wilzorApis)
|
||||
add_subdirectory(devices)
|
||||
|
||||
# Main executable
|
||||
add_executable(salmanoff main.cpp)
|
||||
target_link_libraries(salmanoff
|
||||
Boost::system Boost::log
|
||||
smocore
|
||||
marionette
|
||||
deviceManager
|
||||
senseApis
|
||||
${Boost_LIBRARIES}
|
||||
${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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1,10 +1,16 @@
|
||||
# The Salmanoff Project:
|
||||
|
||||

|
||||
<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.
|
||||
* David Harri`MAN`.
|
||||
* 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.
|
||||
|
||||
For package generation instructions, see [PACKAGING.md](PACKAGING.md).
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -31,7 +31,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
# Search for libraries and headers in the target directories
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 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(ENV{PKG_CONFIG_PATH} "/usr/aarch64-linux-gnu/lib/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig")
|
||||
|
||||
@@ -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}")
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# ldconfig is handled via the package trigger.
|
||||
exit 0
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# ldconfig is handled via the package trigger.
|
||||
exit 0
|
||||
@@ -0,0 +1 @@
|
||||
activate-noawait ldconfig
|
||||
@@ -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})
|
||||
@@ -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
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -10,11 +10,18 @@ if(ENABLE_LIB_xcbXorg)
|
||||
xcbXorg.cpp
|
||||
)
|
||||
|
||||
set_target_properties(xcbXorg PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
|
||||
# Set config define for header generation
|
||||
add_compile_definitions(CONFIG_LIB_XCBXORG_ENABLED)
|
||||
target_include_directories(xcbXorg PUBLIC ${XCB_INCLUDE_DIRS})
|
||||
target_link_libraries(xcbXorg ${XCB_LIBRARIES})
|
||||
|
||||
# Install rules
|
||||
install(TARGETS xcbXorg DESTINATION lib)
|
||||
install(TARGETS xcbXorg
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
add_subdirectory(core)
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user