From 93938b6d37b2b21a15a61231fd84879ed95f26df Mon Sep 17 00:00:00 2001 From: Pawel Rutka Date: Fri, 4 Apr 2025 22:06:23 +0200 Subject: [PATCH 1/2] feo-runtime: mvp for using qor-runtime Use qor-runtime to implement FEO Signed-off-by: Pawel Rutka --- Cargo.lock | 576 +++++++++++++++++- Cargo.toml | 8 +- README.md | 4 + examples/rust/feo-mini-adas/Cargo.toml | 5 + .../src/activities/components.rs | 327 +++++++++- .../rust/feo-mini-adas/src/activities/mod.rs | 1 + .../src/activities/runtime_adapters.rs | 297 +++++++++ .../feo-mini-adas/src/bin/adas_primary.rs | 150 +++-- .../feo-mini-adas/src/bin/adas_secondary_1.rs | 97 ++- .../feo-mini-adas/src/bin/adas_secondary_2.rs | 109 +++- .../src/bin/adas_single_process.rs | 196 ++++++ examples/rust/feo-mini-adas/src/bin/run.sh | 29 + examples/rust/feo-mini-adas/src/config.rs | 178 +++--- feo/Cargo.toml | 2 + feo/src/activity.rs | 2 +- 15 files changed, 1729 insertions(+), 252 deletions(-) create mode 100644 examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs create mode 100644 examples/rust/feo-mini-adas/src/bin/adas_single_process.rs create mode 100755 examples/rust/feo-mini-adas/src/bin/run.sh diff --git a/Cargo.lock b/Cargo.lock index a1520c7..a81bc04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,15 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "async_runtime" +version = "0.1.0" +source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +dependencies = [ + "foundation", + "iceoryx2-pal-concurrency-sync", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -225,6 +234,8 @@ version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -253,6 +264,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clang-sys" version = "1.8.1" @@ -264,12 +281,49 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "cobs" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -295,6 +349,94 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cxx" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4ab2681454aacfe7ce296ebc6df86791009f237f8020b0c752e8b245ba7c1d" +dependencies = [ + "cc", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e431f7ba795550f2b11c32509b3b35927d899f0ad13a1d1e030a317a08facbe" +dependencies = [ + "cc", + "codespan-reporting", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.96", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbc41933767955d04c2a90151806029b93df5fd8b682ba22a967433347480a9" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9133547634329a5b76e5f58d1e53c16d627699bbcd421b9007796311165f9667" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e89d77ad5fd6066a3d42d94de3f72a2f23f95006da808177624429b5183596" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" @@ -420,12 +562,16 @@ dependencies = [ name = "feo-mini-adas" version = "0.1.0" dependencies = [ + "async_runtime", "cc", "feo", "feo-log", "feo-logger", "feo-time", "feo-tracing", + "foundation", + "logging_tracing", + "orchestration", ] [[package]] @@ -487,6 +633,31 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foundation" +version = "0.1.0" +source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +dependencies = [ + "iceoryx2-bb-container", + "iceoryx2-bb-derive-macros", + "iceoryx2-bb-elementary", + "iceoryx2-bb-lock-free", + "iceoryx2-bb-memory", + "iceoryx2-bb-posix", + "iceoryx2-bb-system-types", + "iceoryx2-bb-testing", + "iceoryx2-bb-threadsafe", + "iceoryx2-pal-concurrency-sync", + "tracing", + "tracing-subscriber", +] + [[package]] name = "futures" version = "0.3.31" @@ -584,7 +755,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -619,6 +802,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" @@ -800,6 +989,26 @@ dependencies = [ "serde", ] +[[package]] +name = "iceoryx2-bb-testing" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bd3a76016023d6b5bf48b0a284248454f2b273158e924f3c0802d069b26fc1" +dependencies = [ + "iceoryx2-pal-configuration", +] + +[[package]] +name = "iceoryx2-bb-threadsafe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db459bf7eb91676e87e3fa6623be86dcfc2d87a7bb2b5e2552692d6d6ab9400" +dependencies = [ + "iceoryx2-bb-container", + "iceoryx2-bb-log", + "iceoryx2-bb-posix", +] + [[package]] name = "iceoryx2-cal" version = "0.5.0" @@ -857,7 +1066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -913,6 +1122,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -937,9 +1156,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" @@ -951,6 +1170,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "link-cplusplus" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -992,25 +1220,32 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +name = "logging_tracing" +version = "0.1.0" +source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +dependencies = [ + "tracing", + "tracing-appender", + "tracing-perfetto-sdk-layer", + "tracing-perfetto-sdk-schema", + "tracing-subscriber", +] [[package]] -name = "mini-adas-recording" +name = "matchers" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "cc", - "feo", - "feo-log", - "feo-logger", - "feo-time", - "feo-tracing", - "postcard", - "serde", + "regex-automata 0.1.10", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1034,7 +1269,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1044,6 +1279,18 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1054,6 +1301,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1081,6 +1338,20 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "orchestration" +version = "0.1.0" +source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +dependencies = [ + "async_runtime", + "foundation", + "iceoryx2", + "libc", + "logging_tracing", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ouroboros" version = "0.18.5" @@ -1105,6 +1376,25 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "perfetto-model" version = "0.1.0" @@ -1293,6 +1583,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -1320,7 +1616,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", ] [[package]] @@ -1331,8 +1636,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1343,9 +1657,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1392,6 +1712,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.18" @@ -1404,6 +1730,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" + [[package]] name = "semver" version = "1.0.24" @@ -1457,6 +1789,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1481,6 +1822,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + [[package]] name = "socket2" version = "0.5.8" @@ -1512,6 +1859,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1542,12 +1895,21 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termsize" version = "0.1.9" @@ -1558,6 +1920,66 @@ dependencies = [ "winapi", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thread-id" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99043e46c5a15af379c06add30d9c93a6c0e8849de00d244c4a2c417da128d80" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -1691,6 +2113,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.28" @@ -1709,6 +2143,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-perfetto-sdk-layer" +version = "0.12.0" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +dependencies = [ + "bytes", + "cxx", + "dashmap", + "libc", + "nix", + "prost", + "thiserror 2.0.12", + "thread-id", + "thread_local", + "tracing", + "tracing-perfetto-sdk-schema", + "tracing-perfetto-sdk-sys", + "tracing-subscriber", +] + +[[package]] +name = "tracing-perfetto-sdk-schema" +version = "0.12.0" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +dependencies = [ + "anyhow", + "prost", + "prost-build", + "prost-types", +] + +[[package]] +name = "tracing-perfetto-sdk-sys" +version = "0.12.0" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +dependencies = [ + "anyhow", + "bytes", + "cxx", + "cxx-build", ] [[package]] @@ -1730,7 +2218,16 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -1751,6 +2248,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1763,6 +2266,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1858,6 +2370,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2021,6 +2542,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 14e118d..a052a4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "examples/rust/feo-mini-adas", "examples/rust/greeter", "examples/rust/greetings", - "examples/rust/mini-adas-recording", + # "examples/rust/mini-adas-recording", "feo", "feo-log", "feo-logger", @@ -16,6 +16,12 @@ members = [ resolver = "2" [workspace.dependencies] + +async_runtime = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } +orchestration = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } +logging_tracing = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } +foundation = { git = "https://github.com/qorix-group/qor-runtime.git", branch = "feature/runtime" } + anyhow = "1.0.49" argh = "0.1.13" async-stream = "0.3.6" diff --git a/README.md b/README.md index 19d2b15..006dcff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # score-feo +## Disclaimer +This is port of FEO for `qor-runtime`. Currently, this builds only with `cargo build` as there are some issues +with integration for `bazel` due to using native perfetto sdk as tracing for system events. This is WIP. + ## Rust setup You can build the Rust code both with `bazel` and with `cargo`. diff --git a/examples/rust/feo-mini-adas/Cargo.toml b/examples/rust/feo-mini-adas/Cargo.toml index bad9b98..136cca0 100644 --- a/examples/rust/feo-mini-adas/Cargo.toml +++ b/examples/rust/feo-mini-adas/Cargo.toml @@ -10,6 +10,11 @@ feo-logger = { workspace = true } feo-time = { workspace = true } feo-tracing = { workspace = true } +async_runtime = { workspace = true } +orchestration = { workspace = true } +logging_tracing = { workspace = true } +foundation = { workspace = true } + [build-dependencies] cc = { workspace = true } diff --git a/examples/rust/feo-mini-adas/src/activities/components.rs b/examples/rust/feo-mini-adas/src/activities/components.rs index ff15b09..60e5fc1 100644 --- a/examples/rust/feo-mini-adas/src/activities/components.rs +++ b/examples/rust/feo-mini-adas/src/activities/components.rs @@ -11,10 +11,12 @@ use feo::com::{ActivityInput, ActivityOutput}; use feo::prelude::{Activity, ActivityId}; use feo_log::debug; use feo_tracing::{instrument, tracing}; +use orchestration::prelude::*; use std::ffi::c_void; use std::hash::{BuildHasher as _, Hasher as _, RandomState}; use std::mem::MaybeUninit; use std::ops::Range; +use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; @@ -37,14 +39,14 @@ pub struct Camera { } impl Camera { - pub fn build(activity_id: ActivityId, image_topic: &str) -> Box { - Box::new(Self { + pub fn build(activity_id: ActivityId, image_topic: &str) -> Self { + Self { activity_id, output_image: ActivityOutput::get(image_topic), num_people: 4, num_cars: 10, distance_obstacle: 40.0, - }) + } } fn get_image(&mut self) -> CameraImage { @@ -65,6 +67,8 @@ impl Camera { } } +unsafe impl Send for Camera {} + impl Activity for Camera { fn id(&self) -> ActivityId { self.activity_id @@ -105,12 +109,12 @@ pub struct Radar { } impl Radar { - pub fn build(activity_id: ActivityId, radar_topic: &str) -> Box { - Box::new(Self { + pub fn build(activity_id: ActivityId, radar_topic: &str) -> Self { + Self { activity_id, output_scan: ActivityOutput::get(radar_topic), distance_obstacle: 40.0, - }) + } } fn get_scan(&mut self) -> RadarScan { @@ -128,6 +132,8 @@ impl Radar { } } +unsafe impl Send for Radar {} + impl Activity for Radar { fn id(&self) -> ActivityId { self.activity_id @@ -170,19 +176,54 @@ pub struct NeuralNet { output_scene: ActivityOutput, } +unsafe impl Send for NeuralNet {} // TODO: Both Feo and runtime has to fix this, runtime will support not send soon, but maybe + // feo itself shall not provoke !Send without any good reason - issues comes out of iceoryx2 + +pub trait ActivityAdapterTrait: Send { + type T; // Activity Type + + /// + /// This let you use async context in step function so You are free now to use non blocking sleep, non blocking wait on IO etc. + /// There is no problem to create trait with plain `fn` but then async context is lost for activity + /// + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send; + + fn start(&mut self) -> ActionResult; + + fn stop(&mut self) -> ActionResult; + + fn get_named_id(&self) -> &'static str; +} + impl NeuralNet { pub fn build( activity_id: ActivityId, image_topic: &str, scan_topic: &str, scene_topic: &str, - ) -> Box { - Box::new(Self { + ) -> Self { + Self { activity_id, input_image: ActivityInput::get(image_topic), input_scan: ActivityInput::get(scan_topic), output_scene: ActivityOutput::get(scene_topic), - }) + } + } + + pub fn build_val( + activity_id: ActivityId, + image_topic: &str, + scan_topic: &str, + scene_topic: &str, + ) -> Self { + Self { + activity_id, + input_image: ActivityInput::get(image_topic), + input_scan: ActivityInput::get(scan_topic), + output_scene: ActivityOutput::get(scene_topic), + } } fn infer(image: &CameraImage, radar: &RadarScan, scene: &mut MaybeUninit) { @@ -258,17 +299,19 @@ pub struct EmergencyBraking { output_brake_instruction: ActivityOutput, } +unsafe impl Send for EmergencyBraking {} + impl EmergencyBraking { pub fn build( activity_id: ActivityId, scene_topic: &str, brake_instruction_topic: &str, - ) -> Box { - Box::new(Self { + ) -> Self { + Self { activity_id, input_scene: ActivityInput::get(scene_topic), output_brake_instruction: ActivityOutput::get(brake_instruction_topic), - }) + } } } @@ -333,12 +376,14 @@ pub struct BrakeController { input_brake_instruction: ActivityInput, } +unsafe impl Send for BrakeController {} + impl BrakeController { - pub fn build(activity_id: ActivityId, brake_instruction_topic: &str) -> Box { - Box::new(Self { + pub fn build(activity_id: ActivityId, brake_instruction_topic: &str) -> Self { + Self { activity_id, input_brake_instruction: ActivityInput::get(brake_instruction_topic), - }) + } } } @@ -382,12 +427,14 @@ pub struct EnvironmentRenderer { input_scene: ActivityInput, } +unsafe impl Send for EnvironmentRenderer {} + impl EnvironmentRenderer { - pub fn build(activity_id: ActivityId, scene_topic: &str) -> Box { - Box::new(Self { + pub fn build(activity_id: ActivityId, scene_topic: &str) -> Self { + Self { activity_id, input_scene: ActivityInput::get(scene_topic), - }) + } } } @@ -431,21 +478,19 @@ pub struct LaneAssist { cpp_activity: *mut c_void, } +unsafe impl Send for LaneAssist {} + impl LaneAssist { - pub fn build( - activity_id: ActivityId, - scene_topic: &str, - steering_topic: &str, - ) -> Box { + pub fn build(activity_id: ActivityId, scene_topic: &str, steering_topic: &str) -> Self { // Create C++ activity in heap memory of C++ let cpp_activity = unsafe { create_lane_assist(activity_id.into()) }; - Box::new(Self { + Self { activity_id, input_scene: ActivityInput::get(scene_topic), output_steering: ActivityOutput::get(steering_topic), cpp_activity, - }) + } } } @@ -507,12 +552,14 @@ pub struct SteeringController { input_steering: ActivityInput, } +unsafe impl Send for SteeringController {} + impl SteeringController { - pub fn build(activity_id: ActivityId, steering_topic: &str) -> Box { - Box::new(Self { + pub fn build(activity_id: ActivityId, steering_topic: &str) -> Self { + Self { activity_id, input_steering: ActivityInput::get(steering_topic), - }) + } } } @@ -582,3 +629,227 @@ fn sleep_random() { gen_random_in_range(SLEEP_RANGE) as u64 )); } + +impl ActivityAdapterTrait for EnvironmentRenderer { + type T = EnvironmentRenderer; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + ENV_READER_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for NeuralNet { + type T = NeuralNet; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + NEURAL_NET_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for EmergencyBraking { + type T = EmergencyBraking; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + EMG_BREAK_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for BrakeController { + type T = BrakeController; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + BREAK_CTL_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for LaneAssist { + type T = LaneAssist; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + LANE_ASST_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for SteeringController { + type T = SteeringController; + + async fn step_runtime(instance: Arc>) -> ActionResult { + instance.lock().unwrap().step(); + Ok(()) + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + STR_CTL_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for Radar { + type T = Radar; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + RADAR_ACTIVITY_NAME + } +} + +impl ActivityAdapterTrait for Camera { + type T = Camera; + + fn step_runtime( + instance: Arc>, + ) -> impl std::future::Future + Send { + async move { + instance.lock().unwrap().step(); + Ok(()) + } + } + + fn start(&mut self) -> ActionResult { + self.startup(); + Ok(()) + } + + fn stop(&mut self) -> ActionResult { + self.shutdown(); + Ok(()) + } + + fn get_named_id(&self) -> &'static str { + CAM_ACTIVITY_NAME + } +} + +pub const CAM_ACTIVITY_NAME: &'static str = "cam_activity"; +pub const RADAR_ACTIVITY_NAME: &'static str = "radar_activity"; +pub const NEURAL_NET_ACTIVITY_NAME: &'static str = "neuralnet_activity"; +pub const ENV_READER_ACTIVITY_NAME: &'static str = "env_reader_activity"; +pub const EMG_BREAK_ACTIVITY_NAME: &'static str = "emg_break_activity"; +pub const BREAK_CTL_ACTIVITY_NAME: &'static str = "break_ctl_activity"; +pub const LANE_ASST_ACTIVITY_NAME: &'static str = "lane_asst_activity"; +pub const STR_CTL_ACTIVITY_NAME: &'static str = "str_ctl_activity"; + +pub const PRIMARY_NAME: &'static str = "primary_agent"; +pub const SECONDARY1_NAME: &'static str = "secondary1_agent"; +pub const SECONDARY2_NAME: &'static str = "secondary2_agent"; diff --git a/examples/rust/feo-mini-adas/src/activities/mod.rs b/examples/rust/feo-mini-adas/src/activities/mod.rs index 2a065ef..a3f6dd8 100644 --- a/examples/rust/feo-mini-adas/src/activities/mod.rs +++ b/examples/rust/feo-mini-adas/src/activities/mod.rs @@ -4,3 +4,4 @@ pub mod components; pub mod messages; +pub mod runtime_adapters; diff --git a/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs b/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs new file mode 100644 index 0000000..26cb7e7 --- /dev/null +++ b/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs @@ -0,0 +1,297 @@ +// Copyright (c) 2025 Qorix GmbH +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// +// Well known issues: +// - currently activity must be hidden behind Mutex - subject to be lifted +// - !Send issues due to iceoryx +// - ... +// +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +use orchestration::{ + prelude::*, + program::{Program, ProgramBuilder}, +}; + +use super::components::ActivityAdapterTrait; +use logging_tracing::prelude::*; + +pub struct ActivityDetails { + binded_hooks: ( + Option>, + Option>, + Option>, + ), + + name: &'static str, +} + +/// +/// Returns startup, step, shutdown for activity as invoke actions +/// +pub fn activity_into_invokes(obj: &Arc>) -> ActivityDetails +where + T: 'static + Send + ActivityAdapterTrait, +{ + let start = Invoke::from_arc(obj.clone(), T::start); + let step = Invoke::from_arc_mtx(obj.clone(), T::step_runtime); + let stop = Invoke::from_arc(obj.clone(), T::stop); + ActivityDetails { + binded_hooks: (Some(start), Some(step), Some(stop)), + name: obj.lock().unwrap().get_named_id(), + } +} + +/// +/// Responsible to react on request coming from primary process +/// +pub struct LocalFeoAgent { + activities: Vec, + agent_name: &'static str, +} + +impl LocalFeoAgent { + pub fn new(activities: Vec, agent_name: &'static str) -> Self { + Self { + activities, + agent_name, + } + } + + pub fn create_program(&mut self) -> Program { + let mut program = ProgramBuilder::new("local"); + + program = program.with_startup_hook(self.create_startup()); + program = program.with_body(self.create_body()); + program = program.with_shutdown_notification(self.create_shutdown_notification()); + program = program.with_shutdown_hook(self.create_shutdown()); + + program.build() + } + + fn create_startup(&mut self) -> Box { + let mut seq = Sequence::new() + .with_step(Trigger::new(format!("{}_alive", self.agent_name).as_str())) + .with_step(Sync::new( + format!("{}_waiting_startup", self.agent_name).as_str(), + )); + + let mut concurrent = Concurrency::new(); + + // startups from al activities + for e in &mut self.activities { + concurrent = concurrent.with_branch(e.binded_hooks.0.take().unwrap()); + } + + seq = seq.with_step(concurrent); + seq.with_step(Trigger::new( + format!("{}_startup_done", self.agent_name).as_str(), + )) + } + + fn create_body(&mut self) -> Box { + let mut concurrent = Concurrency::new(); + + for e in &mut self.activities { + concurrent = concurrent.with_branch( + Sequence::new() + .with_step(Sync::new(format!("{}_start", e.name).as_str())) + .with_step(e.binded_hooks.1.take().unwrap()) + .with_step(Trigger::new(format!("{}_done", e.name).as_str())), + ); + } + + concurrent + } + + fn create_shutdown_notification(&mut self) -> Box { + let seq = Sequence::new().with_step(Sync::new( + format!("{}_waiting_shutdown", self.agent_name).as_str(), + )); + + seq + } + + fn create_shutdown(&mut self) -> Box { + let mut seq = Sequence::new(); + + let mut concurrent = Concurrency::new(); + + // shutdown from all activities + for e in &mut self.activities { + concurrent = concurrent.with_branch(e.binded_hooks.2.take().unwrap()); + } + + seq = seq.with_step(concurrent); + seq.with_step(Trigger::new( + format!("{}_shutdown_done", self.agent_name).as_str(), + )) + } +} + +/// +/// Responsible for controlling Task Chain execution across processes according to provided configuration +/// +pub struct GlobalOrchestrator { + agents: Vec, + cycle: Duration, +} + +impl GlobalOrchestrator { + pub fn new(agents: Vec, cycle: Duration) -> Self { + Self { agents, cycle } + } + + pub async fn run(&self, graph: &Vec<(Vec<&str>, bool)>) { + let mut program = ProgramBuilder::new("main") + .with_startup_hook(self.startup()) + .with_body(self.generate_body(&graph)) + .with_shutdown_notification(self.orch_shutdown_notification()) + .with_shutdown_hook(self.shutdown()) + .with_cycle_time(self.cycle) + .build(); + + info!("Executor starts syncing with agents and execution of activity chain 20 times for demo..."); + info!("{:?}", program); + + program.run_n(20).await; + + info!("Done"); + } + + fn sync_to_agents(&self) -> Box { + let mut top = Concurrency::new_with_id(NamedId::new_static("sync_to_agents")); + + for name in &self.agents { + let sub_sequence = Sync::new(format!("{}_alive", name).as_str()); + + top = top.with_branch(sub_sequence); + } + + top + } + + fn release_agents(&self) -> Box { + let mut top = Sequence::new_with_id(NamedId::new_static("release_agents")); + + for name in &self.agents { + let sub_sequence = Trigger::new(format!("{}_waiting_startup", name).as_str()); + + top = top.with_step(sub_sequence); + } + + top + } + + fn wait_startup_completed(&self) -> Box { + let mut top = Sequence::new_with_id(NamedId::new_static("wait_startup_completed")); + + for name in &self.agents { + let sub_sequence = Sync::new(format!("{}_startup_done", name).as_str()); + + top = top.with_step(sub_sequence); + } + + top + } + + fn startup(&self) -> Box { + let seq = Sequence::new_with_id(NamedId::new_static("startup")) + .with_step(self.sync_to_agents()) + .with_step(self.release_agents()) + .with_step(self.wait_startup_completed()); + + seq + } + + fn shutdown_agents(&self) -> Box { + let mut top = Sequence::new_with_id(NamedId::new_static("shutdown_agents")); + + for name in &self.agents { + let sub_sequence = Trigger::new(format!("{}_waiting_shutdown", name).as_str()); + + top = top.with_step(sub_sequence); + } + + top + } + + fn wait_shutdown_completed(&self) -> Box { + let mut top = Sequence::new_with_id(NamedId::new_static("wait_shutdown_completed")); + + for name in &self.agents { + let sub_sequence = Sync::new(format!("{}_shutdown_done", name).as_str()); + + top = top.with_step(sub_sequence); + } + + top + } + + fn shutdown(&self) -> Box { + let seq = Sequence::new_with_id(NamedId::new_static("shutdown")) + .with_step(self.shutdown_agents()) + .with_step(self.wait_shutdown_completed()); + + seq + } + + // This can be used to stop orchestration from another application for demo. + fn orch_shutdown_notification(&self) -> Box { + let seq = Sequence::new_with_id(NamedId::new_static("shutdown")) + .with_step(Sync::new("qorix_orch_shutdown_event")); + + seq + } + + // Converts a dependency graph into an execution sequence. + fn generate_body(&self, execution_structure: &Vec<(Vec<&str>, bool)>) -> Box { + let mut sequence = Sequence::new(); // The overall execution sequence + let mut concurrency_action = Concurrency::new(); + + let mut concurrent_block_added = false; + + for task_group in execution_structure { + if task_group.1 == false { + // Add the concurrency block into sequence + if concurrent_block_added { + sequence = sequence.with_step(concurrency_action); + concurrency_action = Concurrency::new(); + concurrent_block_added = false; + } + // sequence + let action = self.generate_step(task_group.0.clone()); + sequence = sequence.with_step(action); + } else { + // concurrency block + let action = self.generate_step(task_group.0.clone()); + concurrency_action = concurrency_action.with_branch(action); + concurrent_block_added = true; + } + } + if concurrent_block_added { + sequence = sequence.with_step(concurrency_action); + } + sequence + } + + fn generate_step(&self, names: Vec<&str>) -> Box { + let mut sequence = Sequence::new(); + for name in names { + sequence = sequence + .with_step(Trigger::new(format!("{}_start", name).as_str())) + .with_step(Sync::new(format!("{}_done", name).as_str())); + } + return sequence; + } +} diff --git a/examples/rust/feo-mini-adas/src/bin/adas_primary.rs b/examples/rust/feo-mini-adas/src/bin/adas_primary.rs index d2de884..c03a9e9 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_primary.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_primary.rs @@ -2,82 +2,112 @@ // // SPDX-License-Identifier: Apache-2.0 +use async_runtime::runtime::runtime::AsyncRuntimeBuilder; +use async_runtime::scheduler::execution_engine::ExecutionEngineBuilder; use configuration::primary_agent::Builder; use feo::configuration::worker_pool; use feo::prelude::*; use feo::signalling::{channel, Signal}; use feo_log::{info, LevelFilter}; -use feo_mini_adas::config; +use feo_mini_adas::activities::components::{ + Camera, Radar, BREAK_CTL_ACTIVITY_NAME, CAM_ACTIVITY_NAME, EMG_BREAK_ACTIVITY_NAME, + ENV_READER_ACTIVITY_NAME, LANE_ASST_ACTIVITY_NAME, NEURAL_NET_ACTIVITY_NAME, PRIMARY_NAME, + RADAR_ACTIVITY_NAME, SECONDARY1_NAME, SECONDARY2_NAME, STR_CTL_ACTIVITY_NAME, +}; +use feo_mini_adas::activities::runtime_adapters::{ + activity_into_invokes, GlobalOrchestrator, LocalFeoAgent, +}; +use feo_mini_adas::config::{self, *}; use feo_time::Duration; -use std::collections::HashMap; +use foundation::threading::thread_wait_barrier::*; +use logging_tracing::prelude::*; +use logging_tracing::{TraceScope, TracingLibraryBuilder}; +use orchestration::prelude::Event; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::{Arc, Mutex}; const AGENT_ID: AgentId = AgentId::new(100); const BIND_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); -const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(5); +const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(1); fn main() { - feo_logger::init(LevelFilter::Debug, true, true); - feo_tracing::init(feo_tracing::LevelFilter::TRACE); - let params = Params::from_args(); - info!("Starting primary agent {AGENT_ID}. Waiting for connections",); - - // Initialize topics. Do not drop. - let _topic_guards = config::initialize_topics(); - - // Create local worker pool - let (worker_pool, agent_map, ready_channel) = { - let pool_configuration = config::pool_configuration(); - let mut worker_pool_builder = worker_pool::Builder::default(); - let mut agent_map: HashMap>> = HashMap::new(); - - // Recreate the HashMap without the builder on the lowest level. - for (agent_id, assignments) in pool_configuration.into_iter() { - for (worker_id, activities) in assignments.into_iter() { - for (activity_id, builder) in activities { - if agent_id == AGENT_ID { - worker_pool_builder.activity(worker_id, activity_id, builder); - } - - // Reinsert with same structure but without the builder on the lowest level. - agent_map - .entry(agent_id) - .or_default() - .entry(worker_id) - .and_modify(|act_ids| act_ids.push(activity_id)) - .or_insert_with(|| vec![activity_id]); - } - } - } - - let (worker_pool, ready_channel) = match worker_pool_builder.build() { - Some((pool, sender, receiver)) => (Some(pool), (sender, receiver)), - None => { - let ready_channel = channel::(); - (None, ready_channel) - } - }; - - (worker_pool, agent_map, ready_channel) - }; - - let activity_dependencies = config::activity_dependencies(); - - // Construct the agent - let agent = Builder::default() - .id(AGENT_ID) - .cycle_time(params.feo_cycle_time) - .bind(BIND_ADDR) - .agent_map(agent_map) - .worker_pool(worker_pool) - .activity_dependencies(activity_dependencies) - .intra_proc_ready_channel(ready_channel.0, ready_channel.1) + //Initialize in LogMode with AppScope + let mut logger = TracingLibraryBuilder::new() + .global_log_level(Level::DEBUG) + .enable_tracing(TraceScope::SystemScope) + .enable_logging(true) .build(); - // Start the agent loop and never return. - primary::run(agent); + logger.init_log_trace(); + + let _topic_guards = initialize_topics(); + + let agents: Vec = vec![ + PRIMARY_NAME.to_string(), + SECONDARY1_NAME.to_string(), + SECONDARY2_NAME.to_string(), + ]; + + let mut runtime = AsyncRuntimeBuilder::new() + .with_engine( + ExecutionEngineBuilder::new() + .task_queue_size(256) + .workers(3), + ) + .build() + .unwrap(); + + Event::get_instance() + .lock() + .unwrap() + .create_polling_thread(); + + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); + + runtime + .enter_engine(async move { + // VEC of activities(s) which has to be executed in sequence, TRUE: if the activities(s) can be executed concurrently. + let execution_structure = vec![ + (vec![CAM_ACTIVITY_NAME], true), + (vec![RADAR_ACTIVITY_NAME], true), + (vec![NEURAL_NET_ACTIVITY_NAME], false), + (vec![ENV_READER_ACTIVITY_NAME], true), + (vec![EMG_BREAK_ACTIVITY_NAME, BREAK_CTL_ACTIVITY_NAME], true), + (vec![LANE_ASST_ACTIVITY_NAME, STR_CTL_ACTIVITY_NAME], true), + ]; + + let local_agent_program = async_runtime::spawn(async { + let cam_act = Arc::new(Mutex::new(Camera::build(1.into(), TOPIC_CAMERA_FRONT))); + let radar_act = Arc::new(Mutex::new(Radar::build(2.into(), TOPIC_RADAR_FRONT))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&cam_act)); + acts.push(activity_into_invokes(&radar_act)); + + let mut agent = LocalFeoAgent::new(acts, PRIMARY_NAME); + let mut program = agent.create_program(); + println!("{:?}", program); + + program.run().await; + }); + + let global_orch = GlobalOrchestrator::new(agents, params.feo_cycle_time); + + global_orch.run(&execution_structure).await; + local_agent_program.await; + + notifier.ready(); + }) + .unwrap_or_default(); + + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); } /// Parameters of the primary diff --git a/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs b/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs index 6d1c54d..c5599eb 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs @@ -2,12 +2,32 @@ // // SPDX-License-Identifier: Apache-2.0 +use async_runtime::{ + runtime::{runtime::AsyncRuntimeBuilder, *}, + scheduler::execution_engine::ExecutionEngineBuilder, +}; +use feo_mini_adas::activities::{ + components::SECONDARY1_NAME, + runtime_adapters::{activity_into_invokes, LocalFeoAgent}, +}; +use foundation::threading::thread_wait_barrier::*; + use configuration::secondary_agent::Builder; use feo::configuration::worker_pool; use feo::prelude::*; use feo_log::{info, LevelFilter}; -use feo_mini_adas::config; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use feo_mini_adas::{ + activities::components::{EnvironmentRenderer, NeuralNet}, + config::{self, *}, +}; + +use logging_tracing::{prelude::*, TracingLibrary}; +use orchestration::actions::event::Event; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, + time::Duration, +}; /// This agent's ID const AGENT_ID: AgentId = AgentId::new(101); @@ -15,35 +35,62 @@ const AGENT_ID: AgentId = AgentId::new(101); const PRIMARY_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); fn main() { - feo_logger::init(LevelFilter::Debug, true, true); - feo_tracing::init(feo_tracing::LevelFilter::TRACE); + let mut logger = TracingLibraryBuilder::new() + .global_log_level(Level::DEBUG) + .enable_tracing(TraceScope::SystemScope) + .enable_logging(false) + .build(); + + logger.init_log_trace(); info!("Starting agent {AGENT_ID}"); - // Create worker pool builder activity builder for local worker pool - let mut worker_pool_builder = worker_pool::Builder::default(); + let mut runtime = AsyncRuntimeBuilder::new() + .with_engine( + ExecutionEngineBuilder::new() + .task_queue_size(256) + .workers(2), + ) + .build() + .unwrap(); - let mut worker_pool_configuration = config::pool_configuration(); - let assignments = worker_pool_configuration - .remove(&AGENT_ID) - .expect("missing agent id in pool configuration"); + Event::get_instance() + .lock() + .unwrap() + .create_polling_thread(); - // Assign activities to workers - for (worker_id, activities) in assignments { - for (activity_id, builder) in activities { - worker_pool_builder.activity(worker_id, activity_id, builder); - } - } + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); - let (worker_pool, _, receiver) = worker_pool_builder.build().expect("Worker pool is empty"); + runtime + .enter_engine(async { + let neural_net_act = Arc::new(Mutex::new(NeuralNet::build_val( + 3.into(), + TOPIC_CAMERA_FRONT, + TOPIC_RADAR_FRONT, + TOPIC_INFERRED_SCENE, + ))); - // Construct the agent - let agent = Builder::default() - .id(AGENT_ID) - .primary(PRIMARY_ADDR) - .worker_pool(worker_pool, receiver) - .build(); + let environ_renderer_act = Arc::new(Mutex::new(EnvironmentRenderer::build( + 4.into(), + TOPIC_INFERRED_SCENE, + ))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&neural_net_act)); + acts.push(activity_into_invokes(&environ_renderer_act)); + + let mut agent = LocalFeoAgent::new(acts, SECONDARY1_NAME); + let mut program = agent.create_program(); + + program.run().await; + info!("Finished"); + notifier.ready(); + }) + .unwrap_or_default(); - // Start the agent loop and never return. - secondary::run(agent); + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); } diff --git a/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs b/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs index 8252ed7..b1899a8 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs @@ -2,12 +2,30 @@ // // SPDX-License-Identifier: Apache-2.0 +use async_runtime::{ + runtime::runtime::AsyncRuntimeBuilder, scheduler::execution_engine::ExecutionEngineBuilder, +}; use configuration::secondary_agent::Builder; use feo::configuration::worker_pool; use feo::prelude::*; use feo_log::{info, LevelFilter}; -use feo_mini_adas::config; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use feo_mini_adas::{ + activities::{ + components::{ + BrakeController, EmergencyBraking, LaneAssist, SteeringController, SECONDARY2_NAME, + }, + runtime_adapters::{activity_into_invokes, LocalFeoAgent}, + }, + config::{self, *}, +}; +use foundation::threading::thread_wait_barrier::*; +use logging_tracing::{prelude::*, TraceScope, TracingLibraryBuilder}; +use orchestration::prelude::Event; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, + time::Duration, +}; /// This agent's ID const AGENT_ID: AgentId = AgentId::new(102); @@ -15,35 +33,76 @@ const AGENT_ID: AgentId = AgentId::new(102); const PRIMARY_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); fn main() { - feo_logger::init(LevelFilter::Debug, true, true); - feo_tracing::init(feo_tracing::LevelFilter::TRACE); + // feo_logger::init(LevelFilter::Debug, true, true); + // feo_tracing::init(feo_tracing::LevelFilter::TRACE); + + let mut logger = TracingLibraryBuilder::new() + .global_log_level(Level::DEBUG) + .enable_tracing(TraceScope::SystemScope) + .enable_logging(false) + .build(); + + logger.init_log_trace(); info!("Starting agent {AGENT_ID}"); - // Create worker pool builder activity builder for local worker pool - let mut worker_pool_builder = worker_pool::Builder::default(); + let mut runtime = AsyncRuntimeBuilder::new() + .with_engine( + ExecutionEngineBuilder::new() + .task_queue_size(256) + .workers(2), + ) + .build() + .unwrap(); - let mut worker_pool_configuration = config::pool_configuration(); - let assignments = worker_pool_configuration - .remove(&AGENT_ID) - .expect("missing agent id in pool configuration"); + Event::get_instance() + .lock() + .unwrap() + .create_polling_thread(); - // Assign activities to workers - for (worker_id, activities) in assignments { - for (activity_id, builder) in activities { - worker_pool_builder.activity(worker_id, activity_id, builder); - } - } + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); - let (worker_pool, _, receiver) = worker_pool_builder.build().expect("Worker pool is empty"); + runtime + .enter_engine(async { + let emg_brk_act = Arc::new(Mutex::new(EmergencyBraking::build( + 5.into(), + TOPIC_INFERRED_SCENE, + TOPIC_CONTROL_BRAKES, + ))); + let brk_ctr_act = Arc::new(Mutex::new(BrakeController::build( + 6.into(), + TOPIC_CONTROL_BRAKES, + ))); + let lane_asst_act = Arc::new(Mutex::new(LaneAssist::build( + 7.into(), + TOPIC_INFERRED_SCENE, + TOPIC_CONTROL_STEERING, + ))); - // Construct the agent - let agent = Builder::default() - .id(AGENT_ID) - .primary(PRIMARY_ADDR) - .worker_pool(worker_pool, receiver) - .build(); + let str_ctr_act = Arc::new(Mutex::new(SteeringController::build( + 8.into(), + TOPIC_CONTROL_STEERING, + ))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&emg_brk_act)); + acts.push(activity_into_invokes(&brk_ctr_act)); + acts.push(activity_into_invokes(&lane_asst_act)); + acts.push(activity_into_invokes(&str_ctr_act)); + + let mut agent = LocalFeoAgent::new(acts, SECONDARY2_NAME); + let mut program = agent.create_program(); + info!("{:?}", program); + + program.run().await; + info!("Finished"); + notifier.ready(); + }) + .unwrap_or_default(); - // Start the agent loop and never return. - secondary::run(agent); + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); } diff --git a/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs b/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs new file mode 100644 index 0000000..29167fc --- /dev/null +++ b/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs @@ -0,0 +1,196 @@ +// Copyright 2025 Qorix +// +// This is the single process version of the feo-mini-adas example +// +// Copyright 2025 Accenture. +// +// SPDX-License-Identifier: Apache-2.0 +use async_runtime::runtime::runtime::AsyncRuntimeBuilder; +use async_runtime::scheduler::execution_engine::ExecutionEngineBuilder; +use feo::com::TopicHandle; +use feo_mini_adas::activities::components::{ + BrakeController, Camera, EmergencyBraking, EnvironmentRenderer, LaneAssist, NeuralNet, Radar, + SteeringController, BREAK_CTL_ACTIVITY_NAME, CAM_ACTIVITY_NAME, EMG_BREAK_ACTIVITY_NAME, + ENV_READER_ACTIVITY_NAME, LANE_ASST_ACTIVITY_NAME, NEURAL_NET_ACTIVITY_NAME, PRIMARY_NAME, + RADAR_ACTIVITY_NAME, SECONDARY1_NAME, SECONDARY2_NAME, STR_CTL_ACTIVITY_NAME, +}; +use feo_mini_adas::activities::runtime_adapters::{ + activity_into_invokes, GlobalOrchestrator, LocalFeoAgent, +}; +use feo_mini_adas::config::*; +use feo_time::Duration; +use foundation::threading::thread_wait_barrier::*; +use logging_tracing::prelude::*; +use logging_tracing::{TraceScope, TracingLibrary, TracingLibraryBuilder}; +use orchestration::prelude::Event; +use std::sync::{Arc, Mutex}; + +// ****************************************************************** // +// ** RUNTIME SETTINGS ** // +// ****************************************************************** // + +const ENGINE_TASK_QUEUE_SIZE: usize = 256; +const ENGINE_NUM_OF_WORKERS: usize = 3; + +const TRACE_SCOPE: TraceScope = TraceScope::AppScope; +const LOG_LEVEL: Level = Level::DEBUG; +const LOG_ENABLE: bool = true; + +const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(1); + +// ****************************************************************** // +// ** PROGRAMS ** // +// ****************************************************************** // + +/// The entry point of the application +async fn main_program() { + let agents: Vec = vec![ + PRIMARY_NAME.to_string(), + SECONDARY1_NAME.to_string(), + SECONDARY2_NAME.to_string(), + ]; + + // VEC of activitie(s) which has to be executed in sequence, TRUE: if the activitie(s) can be executed concurrently. + let execution_structure = vec![ + (vec![CAM_ACTIVITY_NAME], true), + (vec![RADAR_ACTIVITY_NAME], true), + (vec![NEURAL_NET_ACTIVITY_NAME], false), + (vec![ENV_READER_ACTIVITY_NAME], true), + (vec![EMG_BREAK_ACTIVITY_NAME, BREAK_CTL_ACTIVITY_NAME], true), + (vec![LANE_ASST_ACTIVITY_NAME, STR_CTL_ACTIVITY_NAME], true), + ]; + + let primary_agent_program = async_runtime::spawn(primary_agent_program()); + let secondary_1_agent_program = async_runtime::spawn(secondary_1_agent_program()); + let secondary_2_agent_program = async_runtime::spawn(secondary_2_agent_program()); + + let global_orch = GlobalOrchestrator::new(agents, DEFAULT_FEO_CYCLE_TIME); + global_orch.run(&execution_structure).await; + primary_agent_program.await; + secondary_1_agent_program.await; + secondary_2_agent_program.await; +} + +async fn primary_agent_program() { + info!("Starting primary agent {PRIMARY_NAME}. Waiting for connections",); + + let cam_act = Arc::new(Mutex::new(Camera::build(1.into(), TOPIC_CAMERA_FRONT))); + let radar_act = Arc::new(Mutex::new(Radar::build(2.into(), TOPIC_RADAR_FRONT))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&cam_act)); + acts.push(activity_into_invokes(&radar_act)); + + let mut agent = LocalFeoAgent::new(acts, PRIMARY_NAME); + let mut program = agent.create_program(); + + program.run().await; +} + +async fn secondary_1_agent_program() { + info!("Starting secondary_1 agent {SECONDARY1_NAME}",); + + let neural_net_act = Arc::new(Mutex::new(NeuralNet::build_val( + 3.into(), + TOPIC_CAMERA_FRONT, + TOPIC_RADAR_FRONT, + TOPIC_INFERRED_SCENE, + ))); + + let environ_renderer_act = Arc::new(Mutex::new(EnvironmentRenderer::build( + 4.into(), + TOPIC_INFERRED_SCENE, + ))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&neural_net_act)); + acts.push(activity_into_invokes(&environ_renderer_act)); + + let mut agent = LocalFeoAgent::new(acts, SECONDARY1_NAME); + let mut program = agent.create_program(); + + program.run().await; +} + +async fn secondary_2_agent_program() { + info!("Starting secondary_2 agent {SECONDARY2_NAME}",); + + let emg_brk_act = Arc::new(Mutex::new(EmergencyBraking::build( + 5.into(), + TOPIC_INFERRED_SCENE, + TOPIC_CONTROL_BRAKES, + ))); + let brk_ctr_act = Arc::new(Mutex::new(BrakeController::build( + 6.into(), + TOPIC_CONTROL_BRAKES, + ))); + let lane_asst_act = Arc::new(Mutex::new(LaneAssist::build( + 7.into(), + TOPIC_INFERRED_SCENE, + TOPIC_CONTROL_STEERING, + ))); + + let str_ctr_act = Arc::new(Mutex::new(SteeringController::build( + 8.into(), + TOPIC_CONTROL_STEERING, + ))); + + let mut acts = Vec::new(); + acts.push(activity_into_invokes(&emg_brk_act)); + acts.push(activity_into_invokes(&brk_ctr_act)); + acts.push(activity_into_invokes(&lane_asst_act)); + acts.push(activity_into_invokes(&str_ctr_act)); + + let mut agent = LocalFeoAgent::new(acts, SECONDARY2_NAME); + let mut program = agent.create_program(); + + program.run().await; +} + +/// Init routine that should be called before the application execution +fn init() -> (TracingLibrary, Vec) { + // Initialize logger and topic registration + let mut logger = TracingLibraryBuilder::new() + .global_log_level(LOG_LEVEL) + .enable_tracing(TRACE_SCOPE) + .enable_logging(LOG_ENABLE) + .build(); + logger.init_log_trace(); + let topic_handles = initialize_topics(); + + // Start the orchestrator's event polling thread + Event::get_instance() + .lock() + .unwrap() + .create_polling_thread(); + + (logger, topic_handles) +} + +fn main() { + // Init and keep logger and topic handles alive + let (_logger_guard, _topics_guard) = init(); + + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); + + // Run the main program + AsyncRuntimeBuilder::new() + .with_engine( + ExecutionEngineBuilder::new() + .task_queue_size(ENGINE_TASK_QUEUE_SIZE) + .workers(ENGINE_NUM_OF_WORKERS), + ) + .build() + .unwrap() + .enter_engine(async { + main_program().await; + notifier.ready(); + }) + .unwrap_or_default(); + + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); +} diff --git a/examples/rust/feo-mini-adas/src/bin/run.sh b/examples/rust/feo-mini-adas/src/bin/run.sh new file mode 100755 index 0000000..68a97b9 --- /dev/null +++ b/examples/rust/feo-mini-adas/src/bin/run.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +SESSION_NAME="feo" +BIN1="../../../../../target/debug/adas_primary" +BIN2="../../../../../target/debug/adas_secondary_1" +BIN3="../../../../../target/debug/adas_secondary_2" + +tmux new-session -d -s $SESSION_NAME -n main + + +tmux set-option -g mouse on + +tmux send-keys "$BIN1" + +tmux split-window -v + +tmux send-keys "$BIN2" + +tmux split-window -v + +tmux send-keys "$BIN3" + +tmux select-layout even-vertical + +tmux bind -n C-q kill-session + + +tmux attach-session -t $SESSION_NAME + diff --git a/examples/rust/feo-mini-adas/src/config.rs b/examples/rust/feo-mini-adas/src/config.rs index 9c60177..cb91e66 100644 --- a/examples/rust/feo-mini-adas/src/config.rs +++ b/examples/rust/feo-mini-adas/src/config.rs @@ -26,95 +26,95 @@ pub const TOPIC_CONTROL_STEERING: &str = "feo/com/vehicle/control/steering"; pub const TOPIC_CAMERA_FRONT: &str = "feo/com/vehicle/camera/front"; pub const TOPIC_RADAR_FRONT: &str = "feo/com/vehicle/radar/front"; -pub fn pool_configuration() -> HashMap>> { - // Assign activities to different workers - let w40: WorkerAssignment = ( - 40.into(), - vec![( - 0.into(), - Box::new(|id| Camera::build(id, TOPIC_CAMERA_FRONT)), - )], - ); - let w41: WorkerAssignment = ( - 41.into(), - vec![(1.into(), Box::new(|id| Radar::build(id, TOPIC_RADAR_FRONT)))], - ); - - let w42: WorkerAssignment = ( - 42.into(), - vec![ - ( - 2.into(), - Box::new(|id| { - NeuralNet::build( - id, - TOPIC_CAMERA_FRONT, - TOPIC_RADAR_FRONT, - TOPIC_INFERRED_SCENE, - ) - }), - ), - ( - 3.into(), - Box::new(|id| EnvironmentRenderer::build(id, TOPIC_INFERRED_SCENE)), - ), - ], - ); - - let w43: WorkerAssignment = ( - 43.into(), - vec![ - ( - 4.into(), - Box::new(|id| { - EmergencyBraking::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_BRAKES) - }), - ), - ( - 6.into(), - Box::new(|id| BrakeController::build(id, TOPIC_CONTROL_BRAKES)), - ), - ], - ); - let w44: WorkerAssignment = ( - 44.into(), - vec![ - ( - 5.into(), - Box::new(|id| LaneAssist::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_STEERING)), - ), - ( - 7.into(), - Box::new(|id| SteeringController::build(id, TOPIC_CONTROL_STEERING)), - ), - ], - ); - - // Assign workers to pools with exactly one pool belonging to one agent - let a0: AgentAssignment = (100.into(), vec![w40, w41]); - let a1: AgentAssignment = (101.into(), vec![w42]); - let a2: AgentAssignment = (102.into(), vec![w43, w44]); - - let assignments = vec![a0, a1, a2]; - - let mut agent_map = HashMap::new(); - for (agent, workers) in assignments { - let mut worker_map = HashMap::new(); - for (worker_id, activities) in workers { - let previous = worker_map.insert(worker_id, activities); - assert!( - previous.is_none(), - "Duplicate worker {worker_id} in assignment list" - ); - } - let previous = agent_map.insert(agent, worker_map); - assert!( - previous.is_none(), - "Duplicate agent {agent} in assignment list" - ); - } - agent_map -} +// pub fn pool_configuration() -> HashMap>> { +// // Assign activities to different workers +// let w40: WorkerAssignment = ( +// 40.into(), +// vec![( +// 0.into(), +// Box::new(|id| Camera::build(id, TOPIC_CAMERA_FRONT)), +// )], +// ); +// let w41: WorkerAssignment = ( +// 41.into(), +// vec![(1.into(), Box::new(|id| Radar::build(id, TOPIC_RADAR_FRONT)))], +// ); + +// let w42: WorkerAssignment = ( +// 42.into(), +// vec![ +// ( +// 2.into(), +// Box::new(|id| { +// NeuralNet::build( +// id, +// TOPIC_CAMERA_FRONT, +// TOPIC_RADAR_FRONT, +// TOPIC_INFERRED_SCENE, +// ) +// }), +// ), +// ( +// 3.into(), +// Box::new(|id| EnvironmentRenderer::build(id, TOPIC_INFERRED_SCENE)), +// ), +// ], +// ); + +// let w43: WorkerAssignment = ( +// 43.into(), +// vec![ +// ( +// 4.into(), +// Box::new(|id| { +// EmergencyBraking::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_BRAKES) +// }), +// ), +// ( +// 6.into(), +// Box::new(|id| BrakeController::build(id, TOPIC_CONTROL_BRAKES)), +// ), +// ], +// ); +// let w44: WorkerAssignment = ( +// 44.into(), +// vec![ +// ( +// 5.into(), +// Box::new(|id| LaneAssist::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_STEERING)), +// ), +// ( +// 7.into(), +// Box::new(|id| SteeringController::build(id, TOPIC_CONTROL_STEERING)), +// ), +// ], +// ); + +// // Assign workers to pools with exactly one pool belonging to one agent +// let a0: AgentAssignment = (100.into(), vec![w40, w41]); +// let a1: AgentAssignment = (101.into(), vec![w42]); +// let a2: AgentAssignment = (102.into(), vec![w43, w44]); + +// let assignments = vec![a0, a1, a2]; + +// let mut agent_map = HashMap::new(); +// for (agent, workers) in assignments { +// let mut worker_map = HashMap::new(); +// for (worker_id, activities) in workers { +// let previous = worker_map.insert(worker_id, activities); +// assert!( +// previous.is_none(), +// "Duplicate worker {worker_id} in assignment list" +// ); +// } +// let previous = agent_map.insert(agent, worker_map); +// assert!( +// previous.is_none(), +// "Duplicate agent {agent} in assignment list" +// ); +// } +// agent_map +// } pub fn activity_dependencies() -> ActivityDependencies { // Primary | Secondary1 | Secondary2 diff --git a/feo/Cargo.toml b/feo/Cargo.toml index 50c6bf7..74798a7 100644 --- a/feo/Cargo.toml +++ b/feo/Cargo.toml @@ -13,6 +13,8 @@ mio = { workspace = true } postcard = { workspace = true, features = ["experimental-derive"], optional = true} serde = { workspace = true, optional = true } + + [dev-dependencies] feo-logger = { workspace = true } diff --git a/feo/src/activity.rs b/feo/src/activity.rs index dbfaf71..70a4284 100644 --- a/feo/src/activity.rs +++ b/feo/src/activity.rs @@ -40,7 +40,7 @@ impl Display for ActivityId { } /// Activity trait, to be implemented by any activity intended to run in a WorkerPool -pub trait Activity { +pub trait Activity: Send { /// Get the ID of the activity fn id(&self) -> ActivityId; From 8cdd6abf94ee734cff79373be8e7515a303a4144 Mon Sep 17 00:00:00 2001 From: Pawel Rutka Date: Mon, 14 Apr 2025 12:03:22 +0200 Subject: [PATCH 2/2] orch: better coop with FEO Increase readability and structure --- Cargo.lock | 15 +- Cargo.toml | 16 +- doc/feo_agent.png | Bin 0 -> 52431 bytes doc/feo_executor.png | Bin 0 -> 46253 bytes doc/orch_feo.md | 383 ++++++++++++++++++ doc/static_view.drawio.svg | 258 ++++++++++++ .../src/activities/components.rs | 47 +-- .../src/activities/runtime_adapters.rs | 237 +++++++---- .../feo-mini-adas/src/bin/adas_primary.rs | 73 +--- .../feo-mini-adas/src/bin/adas_secondary_1.rs | 46 +-- .../feo-mini-adas/src/bin/adas_secondary_2.rs | 59 +-- .../src/bin/adas_single_process.rs | 101 ++--- examples/rust/feo-mini-adas/src/config.rs | 102 +---- 13 files changed, 930 insertions(+), 407 deletions(-) create mode 100755 doc/feo_agent.png create mode 100755 doc/feo_executor.png create mode 100644 doc/orch_feo.md create mode 100644 doc/static_view.drawio.svg diff --git a/Cargo.lock b/Cargo.lock index a81bc04..15ba0e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,7 +145,7 @@ dependencies = [ [[package]] name = "async_runtime" version = "0.1.0" -source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +source = "git+https://github.com/qorix-group/qor-runtime.git?rev=cfb01d7c55fe9021dd5539660167792826b88182#cfb01d7c55fe9021dd5539660167792826b88182" dependencies = [ "foundation", "iceoryx2-pal-concurrency-sync", @@ -642,7 +642,7 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foundation" version = "0.1.0" -source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +source = "git+https://github.com/qorix-group/qor-runtime.git?rev=cfb01d7c55fe9021dd5539660167792826b88182#cfb01d7c55fe9021dd5539660167792826b88182" dependencies = [ "iceoryx2-bb-container", "iceoryx2-bb-derive-macros", @@ -1222,7 +1222,7 @@ dependencies = [ [[package]] name = "logging_tracing" version = "0.1.0" -source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +source = "git+https://github.com/qorix-group/qor-runtime.git?rev=cfb01d7c55fe9021dd5539660167792826b88182#cfb01d7c55fe9021dd5539660167792826b88182" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "orchestration" version = "0.1.0" -source = "git+https://github.com/qorix-group/qor-runtime.git?branch=feature%2Fruntime#f3ddc7129ff98d40227b9f84e5009299fa6a8d44" +source = "git+https://github.com/qorix-group/qor-runtime.git?rev=cfb01d7c55fe9021dd5539660167792826b88182#cfb01d7c55fe9021dd5539660167792826b88182" dependencies = [ "async_runtime", "foundation", @@ -2160,7 +2160,7 @@ dependencies = [ [[package]] name = "tracing-perfetto-sdk-layer" version = "0.12.0" -source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#1ac565c2bff2715a3e34714e5777e28572c6e6d6" dependencies = [ "bytes", "cxx", @@ -2180,7 +2180,7 @@ dependencies = [ [[package]] name = "tracing-perfetto-sdk-schema" version = "0.12.0" -source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#1ac565c2bff2715a3e34714e5777e28572c6e6d6" dependencies = [ "anyhow", "prost", @@ -2191,10 +2191,11 @@ dependencies = [ [[package]] name = "tracing-perfetto-sdk-sys" version = "0.12.0" -source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#62759e172c1cdec2ad8fad18a568a6b994316b35" +source = "git+https://github.com/qorix-group/tracing-perfetto-sdk.git?branch=qor_adaptations#1ac565c2bff2715a3e34714e5777e28572c6e6d6" dependencies = [ "anyhow", "bytes", + "cc", "cxx", "cxx-build", ] diff --git a/Cargo.toml b/Cargo.toml index a052a4a..7534f85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,20 @@ members = [ ] resolver = "2" + [workspace.dependencies] -async_runtime = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } -orchestration = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } -logging_tracing = { git = "https://github.com/qorix-group/qor-runtime.git" , branch = "feature/runtime" } -foundation = { git = "https://github.com/qorix-group/qor-runtime.git", branch = "feature/runtime" } +async_runtime = { git = "https://github.com/qorix-group/qor-runtime.git" , rev = "cfb01d7c55fe9021dd5539660167792826b88182" } +orchestration = { git = "https://github.com/qorix-group/qor-runtime.git" , rev = "cfb01d7c55fe9021dd5539660167792826b88182" } +logging_tracing = { git = "https://github.com/qorix-group/qor-runtime.git" , rev = "cfb01d7c55fe9021dd5539660167792826b88182" } +foundation = { git = "https://github.com/qorix-group/qor-runtime.git", rev = "cfb01d7c55fe9021dd5539660167792826b88182" } + + +#async_runtime = { path = "../performance_stack_rust/async_runtime" } +#orchestration = { path = "../performance_stack_rust/orchestration" } +#logging_tracing = { path = "../performance_stack_rust/logging_tracing" } +#foundation = { path = "../performance_stack_rust/foundation" } + anyhow = "1.0.49" argh = "0.1.13" diff --git a/doc/feo_agent.png b/doc/feo_agent.png new file mode 100755 index 0000000000000000000000000000000000000000..0d423974109eb02f914a89565bf07d3ded873f12 GIT binary patch literal 52431 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

A+Z$S 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["Sync(application_id/startup)"] + + SyncS --> Activity1["Activity1.startup()"]; + SyncS --> Activity2["Activity2.startup()"]; + SyncS --> Activity3["Activity3.startup()"]; + + Activity1 --> TriggerE["Trigger(application_id/agent_id/startup_completed)"] + Activity2 -->TriggerE; + Activity3 -->TriggerE; + + TriggerE --> SyncSS["Sync(application_id/start)"] + + subgraph Concurrency + Activity1 + Activity2 + Activity3 + end + + subgraph Sequence + SyncS + TriggerE + Concurrency + Trigger + SyncSS + end + +``` + +### Step (B) +Each activity step function is encoded by simple `Sequence` of `actions`: + +```mermaid +graph LR; + Sync["Sync(application_id/activity_id/step)"]-->Step; + Step["ActivityN.step()"]--> Trigger["Sync(application_id/activity_id/step_completed)"]; + + subgraph Sequence + Sync + Step + Trigger + end +``` + +### Shutdown (C) + +The shutdown functionality is encoded by simple `Sequence` and `Concurrency` `actions`: + +```mermaid +graph LR; + + Activity1["Activity1.shutdown()"]; + Activity2["Activity2.shutdown()"]; + Activity3["Activity3.shutdown()"]; + + Activity1 --> TriggerE["Trigger(application_id/agent_id/shutdown_completed)"] + Activity2 -->TriggerE; + Activity3 -->TriggerE; + + subgraph Concurrency + Activity1 + Activity2 + Activity3 + end + + subgraph Sequence + SyncS + TriggerE + Concurrency + end + +``` + +### Shutdown request (D) +The agent shall all time wait for a request to shutdown, this is relized simply by registering hook: + +```mermaid +graph LR; + Sync["Sync(application_id/shutdown)"] +``` + +### Overall FEO agent +The overall FEO agent implemented by `Orchestration API` is a `Program` that is composed using above functionalities as below: + +![Alt text](feo_agent.png) + + + +## FEO Executor +The executor is responsible for +- coordinate startup +- executing graph logic for activities +- coordinate shutdown +- .... + +### Startup (A) +The startup coordination is build as below: + +```mermaid +graph LR; + SyncA1["Sync(application_id/agent_id_1/alive)"]; + SyncA2["Sync(application_id/agent_id_2/alive)"]; + SyncA3["Sync(application_id/agent_id_2/alive)"]; + + SyncA1 --> TriggerS; + SyncA2 --> TriggerS; + SyncA3 --> TriggerS; + + TriggerS["TriggerA1(application_id/startup)"]; + + TriggerS --> SyncA1C; + TriggerS --> SyncA2C; + TriggerS --> SyncA3C; + + + SyncA1C["Sync(application_id/agent_id_1/startup_completed)"]; + SyncA2C["Sync(application_id/agent_id_2/startup_completed)"]; + SyncA3C["Sync(application_id/agent_id_2/startup_completed)"]; + + TriggerSE["TriggerA1(application_id/start)"]; + + SyncA1C --> TriggerSE; + SyncA2C --> TriggerSE; + SyncA3C --> TriggerSE; + + subgraph Concurrency2 + SyncA1C + SyncA2C + SyncA3C + end + + subgraph Concurrency1 + SyncA1 + SyncA2 + SyncA3 + end + + subgraph Sequence + Concurrency1 + TriggerS + Concurrency2 + TriggerSE + end +``` + +### Graph execution (B) +The graph of activities is translated currently with below schema: +- each activity is separate concurrent branch that can run as soon as all conditions are met +- each activity without dependency can run immediately in single cycle +- each activity with dependencies is translated into `Sequence` of `Sync` actions for corresponding "done" event before it can run +- each run execution is encoded as `Trigger` (start) & `Sync` (wait for finish) action + + +The above bolis down to translate below graph: + +```mermaid +graph TD; +0 --> 2; +1 --> 2; + +2 --> 3; + +2 --> 4; +2 --> 5; + +4 --> 6; +5 --> 7; +``` + +into + +```mermaid +graph TD; + + %% Activity 0 + TriggerA0E["Trigger(application_id/0/step)"]; + SyncA0EC["Sync(application_id/0/step_completed)"]; + + + TriggerA0E --> SyncA0EC; + + subgraph Sequence0 + direction TB + TriggerA0E + SyncA0EC + end + + %% Activity 1 + TriggerA1E["Trigger(application_id/1/step)"]; + SyncA1EC["Sync(application_id/1/step_completed)"]; + + + TriggerA1E --> SyncA1EC; + + subgraph Sequence1 + direction TB + TriggerA1E + SyncA1EC + end + + + + %% Activity 2 + + SyncA2D1["Sync(application_id/0/step_completed)"]; + SyncA2D2["Sync(application_id/1/step_completed)"]; + + TriggerA2E["Trigger(application_id/2/step)"]; + SyncA2EC["Sync(application_id/2/step_completed)"]; + + SyncA2D1 --> SyncA2D2; + SyncA2D2 --> TriggerA2E; + TriggerA2E --> SyncA2EC; + + subgraph Sequence2 + direction TB + SyncA2D1 + SyncA2D2 + TriggerA2E + SyncA2EC + end + + %% Activity 3 + + SyncA3D1["Sync(application_id/2/step_completed)"]; + + TriggerA3E["Trigger(application_id/3/step)"]; + SyncA3EC["Sync(application_id/3/step_completed)"]; + + + SyncA3D1 --> TriggerA3E; + TriggerA3E --> SyncA3EC; + + subgraph Sequence3 + direction TB + SyncA3D1 + TriggerA3E + SyncA3EC + end + + %% Activity 4 + + SyncA4D1["Sync(application_id/2/step_completed)"]; + + TriggerA4E["Trigger(application_id/4/step)"]; + SyncA4EC["Sync(application_id/4/step_completed)"]; + + + SyncA4D1 --> TriggerA4E; + TriggerA4E --> SyncA4EC; + + subgraph Sequence4 + direction TB + SyncA4D1 + TriggerA4E + SyncA4EC + end + + %% Activity 5 + + SyncA5D1["Sync(application_id/2/step_completed)"]; + + TriggerA5E["Trigger(application_id/5/step)"]; + SyncA5EC["Sync(application_id/5/step_completed)"]; + + + SyncA5D1 --> TriggerA5E; + TriggerA5E --> SyncA5EC; + + subgraph Sequence5 + direction TB + SyncA5D1 + TriggerA5E + SyncA5EC + end + + %% Activity 6 + + SyncA6D1["Sync(application_id/4/step_completed)"]; + + TriggerA6E["Trigger(application_id/6/step)"]; + SyncA6EC["Sync(application_id/6/step_completed)"]; + + + SyncA6D1 --> TriggerA6E; + TriggerA6E --> SyncA6EC; + + subgraph Sequence6 + direction TB + SyncA6D1 + TriggerA6E + SyncA6EC + end + + %% Activity 7 + + SyncA7D1["Sync(application_id/5/step_completed)"]; + + TriggerA7E["Trigger(application_id/7/step)"]; + SyncA7EC["Sync(application_id/7/step_completed)"]; + + + SyncA7D1 --> TriggerA7E; + TriggerA7E --> SyncA7EC; + + subgraph Sequence7 + direction TB + SyncA7D1 + TriggerA7E + SyncA7EC + end + + + subgraph Concurrency + Sequence0 + Sequence1 + Sequence2 + Sequence3 + Sequence4 + Sequence5 + Sequence6 + Sequence7 + end + +``` + +### Shutdown (C) +The shutdown coordination is build as below: + +```mermaid +graph LR; + + ExecS["Trigger(application_id/shutdown)"] --> Sync1["Sync(application_id/agent_1_id/shutdown_completed)"]; + + ExecS --> Sync2["Sync(application_id/agent_2_id/shutdown_completed)"]; + ExecS --> Sync3["Sync(application_id/agent_3_id/shutdown_completed)"]; + + subgraph Sequence + ExecS + Concurrency + end + + subgraph Concurrency + Sync1 + Sync2 + Sync3 + end + +``` + + +### Shutdown request (D) + +The primary process can connect any source to start shutdown routine. Currently this is not used in FEO as such signals is not defined. + + +### Overall FEO executor +The overall FEO executor implemented by `Orchestration API` is a `Program` that is composed using above functionalities as below: + +![Alt text](feo_executor.png) + +Additionally `ProgramBuilder` let us configure: +- cycle time +- overall error reaction +- ... \ No newline at end of file diff --git a/doc/static_view.drawio.svg b/doc/static_view.drawio.svg new file mode 100644 index 0000000..30fb60e --- /dev/null +++ b/doc/static_view.drawio.svg @@ -0,0 +1,258 @@ + + + + + + + + + + +
+
+
+ FEO +
+
+
+
+ + FEO + +
+
+
+ + + + + + + +
+
+
+ ASYNC RUNTIME +
+
+
+
+ + ASYNC RUNTIME + +
+
+
+ + + + + + + +
+
+
+ ORCHESTRATION +
+
+
+
+ + ORCHESTRATION + +
+
+
+ + + + + + + +
+
+
+ MIDDLWARE API 1 +
+
+
+
+ + MIDDLWARE API 1 + +
+
+
+ + + + + + + +
+
+
+ + MIDDLWARE API N + +
+
+
+
+ + MIDDLWARE API N + +
+
+
+ + + + + + + + + + +
+
+
+ Task chain control +
+
+
+
+ + Task chain... + +
+
+
+ + + + + + + + + + + + + +
+
+
+ Potential usage of async API in FEO +
+
+
+
+ + Potential usage... + +
+
+
+ + + + + + + +
+
+
+ MD API 2 +
+
+
+
+ + MD API 2 + +
+
+
+ + + + + + + + + + +
+
+
+ Current integration +
+
+
+
+ + Current integration + +
+
+
+ + + + + + + +
+
+
+ Possible usage +
+
+
+
+ + Possible usage + +
+
+
+ + + + + + + +
+
+
+ Legend +
+
+
+
+ + Legend + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/examples/rust/feo-mini-adas/src/activities/components.rs b/examples/rust/feo-mini-adas/src/activities/components.rs index 60e5fc1..1f6141a 100644 --- a/examples/rust/feo-mini-adas/src/activities/components.rs +++ b/examples/rust/feo-mini-adas/src/activities/components.rs @@ -194,7 +194,7 @@ pub trait ActivityAdapterTrait: Send { fn stop(&mut self) -> ActionResult; - fn get_named_id(&self) -> &'static str; + fn get_act_id(&self) -> ActivityId; } impl NeuralNet { @@ -652,8 +652,8 @@ impl ActivityAdapterTrait for EnvironmentRenderer { Ok(()) } - fn get_named_id(&self) -> &'static str { - ENV_READER_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -679,8 +679,8 @@ impl ActivityAdapterTrait for NeuralNet { Ok(()) } - fn get_named_id(&self) -> &'static str { - NEURAL_NET_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -706,8 +706,8 @@ impl ActivityAdapterTrait for EmergencyBraking { Ok(()) } - fn get_named_id(&self) -> &'static str { - EMG_BREAK_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -732,8 +732,8 @@ impl ActivityAdapterTrait for BrakeController { Ok(()) } - fn get_named_id(&self) -> &'static str { - BREAK_CTL_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -759,8 +759,8 @@ impl ActivityAdapterTrait for LaneAssist { Ok(()) } - fn get_named_id(&self) -> &'static str { - LANE_ASST_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -782,8 +782,8 @@ impl ActivityAdapterTrait for SteeringController { Ok(()) } - fn get_named_id(&self) -> &'static str { - STR_CTL_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -809,8 +809,8 @@ impl ActivityAdapterTrait for Radar { Ok(()) } - fn get_named_id(&self) -> &'static str { - RADAR_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } @@ -836,20 +836,7 @@ impl ActivityAdapterTrait for Camera { Ok(()) } - fn get_named_id(&self) -> &'static str { - CAM_ACTIVITY_NAME + fn get_act_id(&self) -> ActivityId { + self.activity_id } } - -pub const CAM_ACTIVITY_NAME: &'static str = "cam_activity"; -pub const RADAR_ACTIVITY_NAME: &'static str = "radar_activity"; -pub const NEURAL_NET_ACTIVITY_NAME: &'static str = "neuralnet_activity"; -pub const ENV_READER_ACTIVITY_NAME: &'static str = "env_reader_activity"; -pub const EMG_BREAK_ACTIVITY_NAME: &'static str = "emg_break_activity"; -pub const BREAK_CTL_ACTIVITY_NAME: &'static str = "break_ctl_activity"; -pub const LANE_ASST_ACTIVITY_NAME: &'static str = "lane_asst_activity"; -pub const STR_CTL_ACTIVITY_NAME: &'static str = "str_ctl_activity"; - -pub const PRIMARY_NAME: &'static str = "primary_agent"; -pub const SECONDARY1_NAME: &'static str = "secondary1_agent"; -pub const SECONDARY2_NAME: &'static str = "secondary2_agent"; diff --git a/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs b/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs index 26cb7e7..f56953a 100644 --- a/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs +++ b/examples/rust/feo-mini-adas/src/activities/runtime_adapters.rs @@ -18,6 +18,9 @@ use std::{ time::Duration, }; +use async_runtime::runtime::runtime::AsyncRuntime; +use feo::{configuration::primary_agent::ActivityDependencies, prelude::ActivityId}; +use foundation::threading::thread_wait_barrier::{ThreadReadyNotifier, ThreadWaitBarrier}; use orchestration::{ prelude::*, program::{Program, ProgramBuilder}, @@ -33,7 +36,30 @@ pub struct ActivityDetails { Option>, ), - name: &'static str, + id: ActivityId, +} + +pub struct ActivityDetailsBuilder { + data: Vec, +} + +impl ActivityDetailsBuilder { + pub fn new() -> Self { + Self { data: vec![] } + } + + pub fn add_activity, U: FnMut() -> T>( + mut self, + mut builder: U, + ) -> Self { + let wrapped = Arc::new(Mutex::new(builder())); + self.data.push(activity_into_invokes(&wrapped)); + self + } + + pub fn build(self) -> Vec { + self.data + } } /// @@ -48,7 +74,7 @@ where let stop = Invoke::from_arc(obj.clone(), T::stop); ActivityDetails { binded_hooks: (Some(start), Some(step), Some(stop)), - name: obj.lock().unwrap().get_named_id(), + id: obj.lock().unwrap().get_act_id(), } } @@ -58,13 +84,50 @@ where pub struct LocalFeoAgent { activities: Vec, agent_name: &'static str, + app_name: &'static str, } impl LocalFeoAgent { - pub fn new(activities: Vec, agent_name: &'static str) -> Self { + pub fn run_agent( + app_name: &'static str, + activities: Vec, + agent_name: &'static str, + runtime: &mut AsyncRuntime, + ) { + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); + + runtime + .enter_engine( + // + async move { + let mut agent = LocalFeoAgent::new(app_name, activities, agent_name); + let mut program = agent.create_program(); + + info!("{:?}", program); + + program.run().await; + info!("Finished"); + notifier.ready(); + }, + ) + .unwrap_or_default(); + + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); + } + + pub fn new( + app_name: &'static str, + activities: Vec, + agent_name: &'static str, + ) -> Self { Self { activities, agent_name, + app_name, } } @@ -81,10 +144,10 @@ impl LocalFeoAgent { fn create_startup(&mut self) -> Box { let mut seq = Sequence::new() - .with_step(Trigger::new(format!("{}_alive", self.agent_name).as_str())) - .with_step(Sync::new( - format!("{}_waiting_startup", self.agent_name).as_str(), - )); + .with_step(Trigger::new( + format!("{}/{}/alive", self.app_name, self.agent_name).as_str(), + )) + .with_step(Sync::new(format!("{}/startup", self.app_name).as_str())); let mut concurrent = Concurrency::new(); @@ -95,7 +158,7 @@ impl LocalFeoAgent { seq = seq.with_step(concurrent); seq.with_step(Trigger::new( - format!("{}_startup_done", self.agent_name).as_str(), + format!("{}/{}/startup_completed", self.app_name, self.agent_name).as_str(), )) } @@ -105,9 +168,13 @@ impl LocalFeoAgent { for e in &mut self.activities { concurrent = concurrent.with_branch( Sequence::new() - .with_step(Sync::new(format!("{}_start", e.name).as_str())) + .with_step(Sync::new( + format!("{}/{}/step", self.app_name, e.id).as_str(), + )) .with_step(e.binded_hooks.1.take().unwrap()) - .with_step(Trigger::new(format!("{}_done", e.name).as_str())), + .with_step(Trigger::new( + format!("{}/{}/step_completed", self.app_name, e.id).as_str(), + )), ); } @@ -115,9 +182,8 @@ impl LocalFeoAgent { } fn create_shutdown_notification(&mut self) -> Box { - let seq = Sequence::new().with_step(Sync::new( - format!("{}_waiting_shutdown", self.agent_name).as_str(), - )); + let seq = + Sequence::new().with_step(Sync::new(format!("{}/shutdown", self.app_name).as_str())); seq } @@ -134,7 +200,7 @@ impl LocalFeoAgent { seq = seq.with_step(concurrent); seq.with_step(Trigger::new( - format!("{}_shutdown_done", self.agent_name).as_str(), + format!("{}/{}/shutdown_completed", self.app_name, self.agent_name).as_str(), )) } } @@ -145,14 +211,59 @@ impl LocalFeoAgent { pub struct GlobalOrchestrator { agents: Vec, cycle: Duration, + app_name: &'static str, } impl GlobalOrchestrator { - pub fn new(agents: Vec, cycle: Duration) -> Self { - Self { agents, cycle } + pub fn run_primary( + app_name: &'static str, + agents: Vec, + cycle: Duration, + graph: ActivityDependencies, + local_activities: Vec, + local_agent_name: &'static str, + runtime: &mut AsyncRuntime, + ) { + // Since runtime `enter_engine` is now not blocking, we do it manually here. + let waiter = Arc::new(ThreadWaitBarrier::new(1)); + let notifier = waiter.get_notifier().unwrap(); + + runtime + .enter_engine( + // + async move { + let local_agent_program = async_runtime::spawn(async move { + let mut agent = + LocalFeoAgent::new(app_name, local_activities, local_agent_name); + let mut program = agent.create_program(); + + program.run().await; + info!("Finished local program"); + }); + + let global_orch = GlobalOrchestrator::new(app_name, agents, cycle); + global_orch.run(&graph).await; + + local_agent_program.await; + notifier.ready(); + }, + ) + .unwrap_or_default(); + + waiter + .wait_for_all(Duration::new(2000, 0)) + .unwrap_or_default(); } - pub async fn run(&self, graph: &Vec<(Vec<&str>, bool)>) { + pub fn new(app_name: &'static str, agents: Vec, cycle: Duration) -> Self { + Self { + agents, + cycle, + app_name, + } + } + + pub async fn run(&self, graph: &ActivityDependencies) { let mut program = ProgramBuilder::new("main") .with_startup_hook(self.startup()) .with_body(self.generate_body(&graph)) @@ -173,7 +284,7 @@ impl GlobalOrchestrator { let mut top = Concurrency::new_with_id(NamedId::new_static("sync_to_agents")); for name in &self.agents { - let sub_sequence = Sync::new(format!("{}_alive", name).as_str()); + let sub_sequence = Sync::new(format!("{}/{}/alive", self.app_name, name).as_str()); top = top.with_branch(sub_sequence); } @@ -182,22 +293,16 @@ impl GlobalOrchestrator { } fn release_agents(&self) -> Box { - let mut top = Sequence::new_with_id(NamedId::new_static("release_agents")); - - for name in &self.agents { - let sub_sequence = Trigger::new(format!("{}_waiting_startup", name).as_str()); - - top = top.with_step(sub_sequence); - } - - top + Sequence::new_with_id(NamedId::new_static("release_agents")) + .with_step(Trigger::new(format!("{}/startup", self.app_name).as_str())) } fn wait_startup_completed(&self) -> Box { let mut top = Sequence::new_with_id(NamedId::new_static("wait_startup_completed")); for name in &self.agents { - let sub_sequence = Sync::new(format!("{}_startup_done", name).as_str()); + let sub_sequence = + Sync::new(format!("{}/{}/startup_completed", self.app_name, name).as_str()); top = top.with_step(sub_sequence); } @@ -215,22 +320,16 @@ impl GlobalOrchestrator { } fn shutdown_agents(&self) -> Box { - let mut top = Sequence::new_with_id(NamedId::new_static("shutdown_agents")); - - for name in &self.agents { - let sub_sequence = Trigger::new(format!("{}_waiting_shutdown", name).as_str()); - - top = top.with_step(sub_sequence); - } - - top + Sequence::new_with_id(NamedId::new_static("shutdown_agents")) + .with_step(Trigger::new(format!("{}/shutdown", self.app_name).as_str())) } fn wait_shutdown_completed(&self) -> Box { let mut top = Sequence::new_with_id(NamedId::new_static("wait_shutdown_completed")); for name in &self.agents { - let sub_sequence = Sync::new(format!("{}_shutdown_done", name).as_str()); + let sub_sequence = + Sync::new(format!("{}/{}/shutdown_completed", self.app_name, name).as_str()); top = top.with_step(sub_sequence); } @@ -255,43 +354,37 @@ impl GlobalOrchestrator { } // Converts a dependency graph into an execution sequence. - fn generate_body(&self, execution_structure: &Vec<(Vec<&str>, bool)>) -> Box { - let mut sequence = Sequence::new(); // The overall execution sequence - let mut concurrency_action = Concurrency::new(); - - let mut concurrent_block_added = false; - - for task_group in execution_structure { - if task_group.1 == false { - // Add the concurrency block into sequence - if concurrent_block_added { - sequence = sequence.with_step(concurrency_action); - concurrency_action = Concurrency::new(); - concurrent_block_added = false; - } - // sequence - let action = self.generate_step(task_group.0.clone()); - sequence = sequence.with_step(action); - } else { - // concurrency block - let action = self.generate_step(task_group.0.clone()); - concurrency_action = concurrency_action.with_branch(action); - concurrent_block_added = true; + fn generate_body(&self, graph: &ActivityDependencies) -> Box { + let mut body = Concurrency::new(); + + // For now simply mapping, without optimization + for node in graph { + if node.1.is_empty() { + body = body.with_branch(self.generate_step(node.0)); + continue; } + + let mut s = Sequence::new(); + for dep in node.1 { + s = s.with_step(Sync::new( + format!("{}/{}/step_completed", self.app_name, dep).as_str(), + )); + } + + s = s.with_step(self.generate_step(node.0)); + + body = body.with_branch(s); } - if concurrent_block_added { - sequence = sequence.with_step(concurrency_action); - } - sequence + body } - fn generate_step(&self, names: Vec<&str>) -> Box { - let mut sequence = Sequence::new(); - for name in names { - sequence = sequence - .with_step(Trigger::new(format!("{}_start", name).as_str())) - .with_step(Sync::new(format!("{}_done", name).as_str())); - } - return sequence; + fn generate_step(&self, id: &ActivityId) -> Box { + Sequence::new() + .with_step(Trigger::new( + format!("{}/{}/step", self.app_name, id).as_str(), + )) + .with_step(Sync::new( + format!("{}/{}/step_completed", self.app_name, id).as_str(), + )) } } diff --git a/examples/rust/feo-mini-adas/src/bin/adas_primary.rs b/examples/rust/feo-mini-adas/src/bin/adas_primary.rs index c03a9e9..361b81e 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_primary.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_primary.rs @@ -9,13 +9,9 @@ use feo::configuration::worker_pool; use feo::prelude::*; use feo::signalling::{channel, Signal}; use feo_log::{info, LevelFilter}; -use feo_mini_adas::activities::components::{ - Camera, Radar, BREAK_CTL_ACTIVITY_NAME, CAM_ACTIVITY_NAME, EMG_BREAK_ACTIVITY_NAME, - ENV_READER_ACTIVITY_NAME, LANE_ASST_ACTIVITY_NAME, NEURAL_NET_ACTIVITY_NAME, PRIMARY_NAME, - RADAR_ACTIVITY_NAME, SECONDARY1_NAME, SECONDARY2_NAME, STR_CTL_ACTIVITY_NAME, -}; +use feo_mini_adas::activities::components::{Camera, Radar}; use feo_mini_adas::activities::runtime_adapters::{ - activity_into_invokes, GlobalOrchestrator, LocalFeoAgent, + activity_into_invokes, ActivityDetailsBuilder, GlobalOrchestrator, LocalFeoAgent, }; use feo_mini_adas::config::{self, *}; use feo_time::Duration; @@ -29,16 +25,16 @@ use std::sync::{Arc, Mutex}; const AGENT_ID: AgentId = AgentId::new(100); const BIND_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); -const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(1); +const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_millis(500); fn main() { let params = Params::from_args(); //Initialize in LogMode with AppScope let mut logger = TracingLibraryBuilder::new() - .global_log_level(Level::DEBUG) + .global_log_level(Level::TRACE) .enable_tracing(TraceScope::SystemScope) - .enable_logging(true) + .enable_logging(false) .build(); logger.init_log_trace(); @@ -55,7 +51,7 @@ fn main() { .with_engine( ExecutionEngineBuilder::new() .task_queue_size(256) - .workers(3), + .workers(2), ) .build() .unwrap(); @@ -65,49 +61,20 @@ fn main() { .unwrap() .create_polling_thread(); - // Since runtime `enter_engine` is now not blocking, we do it manually here. - let waiter = Arc::new(ThreadWaitBarrier::new(1)); - let notifier = waiter.get_notifier().unwrap(); - - runtime - .enter_engine(async move { - // VEC of activities(s) which has to be executed in sequence, TRUE: if the activities(s) can be executed concurrently. - let execution_structure = vec![ - (vec![CAM_ACTIVITY_NAME], true), - (vec![RADAR_ACTIVITY_NAME], true), - (vec![NEURAL_NET_ACTIVITY_NAME], false), - (vec![ENV_READER_ACTIVITY_NAME], true), - (vec![EMG_BREAK_ACTIVITY_NAME, BREAK_CTL_ACTIVITY_NAME], true), - (vec![LANE_ASST_ACTIVITY_NAME, STR_CTL_ACTIVITY_NAME], true), - ]; - - let local_agent_program = async_runtime::spawn(async { - let cam_act = Arc::new(Mutex::new(Camera::build(1.into(), TOPIC_CAMERA_FRONT))); - let radar_act = Arc::new(Mutex::new(Radar::build(2.into(), TOPIC_RADAR_FRONT))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&cam_act)); - acts.push(activity_into_invokes(&radar_act)); - - let mut agent = LocalFeoAgent::new(acts, PRIMARY_NAME); - let mut program = agent.create_program(); - println!("{:?}", program); - - program.run().await; - }); - - let global_orch = GlobalOrchestrator::new(agents, params.feo_cycle_time); - - global_orch.run(&execution_structure).await; - local_agent_program.await; - - notifier.ready(); - }) - .unwrap_or_default(); - - waiter - .wait_for_all(Duration::new(2000, 0)) - .unwrap_or_default(); + let activities = ActivityDetailsBuilder::new() + .add_activity(|| Radar::build(1.into(), TOPIC_RADAR_FRONT)) + .add_activity(|| Camera::build(0.into(), TOPIC_CAMERA_FRONT)) + .build(); + + GlobalOrchestrator::run_primary( + APPLICATION_NAME, + agents, + params.feo_cycle_time, + activity_dependencies(), + activities, + PRIMARY_NAME, + &mut runtime, + ) } /// Parameters of the primary diff --git a/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs b/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs index c5599eb..98e016a 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_secondary_1.rs @@ -6,9 +6,8 @@ use async_runtime::{ runtime::{runtime::AsyncRuntimeBuilder, *}, scheduler::execution_engine::ExecutionEngineBuilder, }; -use feo_mini_adas::activities::{ - components::SECONDARY1_NAME, - runtime_adapters::{activity_into_invokes, LocalFeoAgent}, +use feo_mini_adas::activities::runtime_adapters::{ + activity_into_invokes, ActivityDetailsBuilder, LocalFeoAgent, }; use foundation::threading::thread_wait_barrier::*; @@ -43,13 +42,11 @@ fn main() { logger.init_log_trace(); - info!("Starting agent {AGENT_ID}"); - let mut runtime = AsyncRuntimeBuilder::new() .with_engine( ExecutionEngineBuilder::new() .task_queue_size(256) - .workers(2), + .workers(1), ) .build() .unwrap(); @@ -59,38 +56,17 @@ fn main() { .unwrap() .create_polling_thread(); - // Since runtime `enter_engine` is now not blocking, we do it manually here. - let waiter = Arc::new(ThreadWaitBarrier::new(1)); - let notifier = waiter.get_notifier().unwrap(); - - runtime - .enter_engine(async { - let neural_net_act = Arc::new(Mutex::new(NeuralNet::build_val( - 3.into(), + let activities = ActivityDetailsBuilder::new() + .add_activity(|| EnvironmentRenderer::build(3.into(), TOPIC_INFERRED_SCENE)) + .add_activity(|| { + NeuralNet::build_val( + 2.into(), TOPIC_CAMERA_FRONT, TOPIC_RADAR_FRONT, TOPIC_INFERRED_SCENE, - ))); - - let environ_renderer_act = Arc::new(Mutex::new(EnvironmentRenderer::build( - 4.into(), - TOPIC_INFERRED_SCENE, - ))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&neural_net_act)); - acts.push(activity_into_invokes(&environ_renderer_act)); - - let mut agent = LocalFeoAgent::new(acts, SECONDARY1_NAME); - let mut program = agent.create_program(); - - program.run().await; - info!("Finished"); - notifier.ready(); + ) }) - .unwrap_or_default(); + .build(); - waiter - .wait_for_all(Duration::new(2000, 0)) - .unwrap_or_default(); + LocalFeoAgent::run_agent(APPLICATION_NAME, activities, SECONDARY1_NAME, &mut runtime) } diff --git a/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs b/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs index b1899a8..b299faf 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_secondary_2.rs @@ -11,10 +11,8 @@ use feo::prelude::*; use feo_log::{info, LevelFilter}; use feo_mini_adas::{ activities::{ - components::{ - BrakeController, EmergencyBraking, LaneAssist, SteeringController, SECONDARY2_NAME, - }, - runtime_adapters::{activity_into_invokes, LocalFeoAgent}, + components::{BrakeController, EmergencyBraking, LaneAssist, SteeringController}, + runtime_adapters::{activity_into_invokes, ActivityDetailsBuilder, LocalFeoAgent}, }, config::{self, *}, }; @@ -44,8 +42,6 @@ fn main() { logger.init_log_trace(); - info!("Starting agent {AGENT_ID}"); - let mut runtime = AsyncRuntimeBuilder::new() .with_engine( ExecutionEngineBuilder::new() @@ -60,49 +56,14 @@ fn main() { .unwrap() .create_polling_thread(); - // Since runtime `enter_engine` is now not blocking, we do it manually here. - let waiter = Arc::new(ThreadWaitBarrier::new(1)); - let notifier = waiter.get_notifier().unwrap(); - - runtime - .enter_engine(async { - let emg_brk_act = Arc::new(Mutex::new(EmergencyBraking::build( - 5.into(), - TOPIC_INFERRED_SCENE, - TOPIC_CONTROL_BRAKES, - ))); - let brk_ctr_act = Arc::new(Mutex::new(BrakeController::build( - 6.into(), - TOPIC_CONTROL_BRAKES, - ))); - let lane_asst_act = Arc::new(Mutex::new(LaneAssist::build( - 7.into(), - TOPIC_INFERRED_SCENE, - TOPIC_CONTROL_STEERING, - ))); - - let str_ctr_act = Arc::new(Mutex::new(SteeringController::build( - 8.into(), - TOPIC_CONTROL_STEERING, - ))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&emg_brk_act)); - acts.push(activity_into_invokes(&brk_ctr_act)); - acts.push(activity_into_invokes(&lane_asst_act)); - acts.push(activity_into_invokes(&str_ctr_act)); - - let mut agent = LocalFeoAgent::new(acts, SECONDARY2_NAME); - let mut program = agent.create_program(); - info!("{:?}", program); - - program.run().await; - info!("Finished"); - notifier.ready(); + let activities = ActivityDetailsBuilder::new() + .add_activity(|| { + EmergencyBraking::build(4.into(), TOPIC_INFERRED_SCENE, TOPIC_CONTROL_BRAKES) }) - .unwrap_or_default(); + .add_activity(|| BrakeController::build(6.into(), TOPIC_CONTROL_BRAKES)) + .add_activity(|| LaneAssist::build(5.into(), TOPIC_INFERRED_SCENE, TOPIC_CONTROL_STEERING)) + .add_activity(|| SteeringController::build(7.into(), TOPIC_CONTROL_STEERING)) + .build(); - waiter - .wait_for_all(Duration::new(2000, 0)) - .unwrap_or_default(); + LocalFeoAgent::run_agent(APPLICATION_NAME, activities, SECONDARY2_NAME, &mut runtime) } diff --git a/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs b/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs index 29167fc..941a2f6 100644 --- a/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs +++ b/examples/rust/feo-mini-adas/src/bin/adas_single_process.rs @@ -10,12 +10,10 @@ use async_runtime::scheduler::execution_engine::ExecutionEngineBuilder; use feo::com::TopicHandle; use feo_mini_adas::activities::components::{ BrakeController, Camera, EmergencyBraking, EnvironmentRenderer, LaneAssist, NeuralNet, Radar, - SteeringController, BREAK_CTL_ACTIVITY_NAME, CAM_ACTIVITY_NAME, EMG_BREAK_ACTIVITY_NAME, - ENV_READER_ACTIVITY_NAME, LANE_ASST_ACTIVITY_NAME, NEURAL_NET_ACTIVITY_NAME, PRIMARY_NAME, - RADAR_ACTIVITY_NAME, SECONDARY1_NAME, SECONDARY2_NAME, STR_CTL_ACTIVITY_NAME, + SteeringController, }; use feo_mini_adas::activities::runtime_adapters::{ - activity_into_invokes, GlobalOrchestrator, LocalFeoAgent, + activity_into_invokes, ActivityDetailsBuilder, GlobalOrchestrator, LocalFeoAgent, }; use feo_mini_adas::config::*; use feo_time::Duration; @@ -23,6 +21,7 @@ use foundation::threading::thread_wait_barrier::*; use logging_tracing::prelude::*; use logging_tracing::{TraceScope, TracingLibrary, TracingLibraryBuilder}; use orchestration::prelude::Event; +use orchestration::program::ProgramBuilder; use std::sync::{Arc, Mutex}; // ****************************************************************** // @@ -32,8 +31,8 @@ use std::sync::{Arc, Mutex}; const ENGINE_TASK_QUEUE_SIZE: usize = 256; const ENGINE_NUM_OF_WORKERS: usize = 3; -const TRACE_SCOPE: TraceScope = TraceScope::AppScope; -const LOG_LEVEL: Level = Level::DEBUG; +const TRACE_SCOPE: TraceScope = TraceScope::SystemScope; +const LOG_LEVEL: Level = Level::INFO; const LOG_ENABLE: bool = true; const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(1); @@ -50,22 +49,12 @@ async fn main_program() { SECONDARY2_NAME.to_string(), ]; - // VEC of activitie(s) which has to be executed in sequence, TRUE: if the activitie(s) can be executed concurrently. - let execution_structure = vec![ - (vec![CAM_ACTIVITY_NAME], true), - (vec![RADAR_ACTIVITY_NAME], true), - (vec![NEURAL_NET_ACTIVITY_NAME], false), - (vec![ENV_READER_ACTIVITY_NAME], true), - (vec![EMG_BREAK_ACTIVITY_NAME, BREAK_CTL_ACTIVITY_NAME], true), - (vec![LANE_ASST_ACTIVITY_NAME, STR_CTL_ACTIVITY_NAME], true), - ]; - let primary_agent_program = async_runtime::spawn(primary_agent_program()); let secondary_1_agent_program = async_runtime::spawn(secondary_1_agent_program()); let secondary_2_agent_program = async_runtime::spawn(secondary_2_agent_program()); - let global_orch = GlobalOrchestrator::new(agents, DEFAULT_FEO_CYCLE_TIME); - global_orch.run(&execution_structure).await; + let global_orch = GlobalOrchestrator::new(APPLICATION_NAME, agents, DEFAULT_FEO_CYCLE_TIME); + global_orch.run(&activity_dependencies()).await; primary_agent_program.await; secondary_1_agent_program.await; secondary_2_agent_program.await; @@ -74,14 +63,12 @@ async fn main_program() { async fn primary_agent_program() { info!("Starting primary agent {PRIMARY_NAME}. Waiting for connections",); - let cam_act = Arc::new(Mutex::new(Camera::build(1.into(), TOPIC_CAMERA_FRONT))); - let radar_act = Arc::new(Mutex::new(Radar::build(2.into(), TOPIC_RADAR_FRONT))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&cam_act)); - acts.push(activity_into_invokes(&radar_act)); + let activities = ActivityDetailsBuilder::new() + .add_activity(|| Radar::build(1.into(), TOPIC_RADAR_FRONT)) + .add_activity(|| Camera::build(0.into(), TOPIC_CAMERA_FRONT)) + .build(); - let mut agent = LocalFeoAgent::new(acts, PRIMARY_NAME); + let mut agent = LocalFeoAgent::new(APPLICATION_NAME, activities, PRIMARY_NAME); let mut program = agent.create_program(); program.run().await; @@ -90,23 +77,19 @@ async fn primary_agent_program() { async fn secondary_1_agent_program() { info!("Starting secondary_1 agent {SECONDARY1_NAME}",); - let neural_net_act = Arc::new(Mutex::new(NeuralNet::build_val( - 3.into(), - TOPIC_CAMERA_FRONT, - TOPIC_RADAR_FRONT, - TOPIC_INFERRED_SCENE, - ))); - - let environ_renderer_act = Arc::new(Mutex::new(EnvironmentRenderer::build( - 4.into(), - TOPIC_INFERRED_SCENE, - ))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&neural_net_act)); - acts.push(activity_into_invokes(&environ_renderer_act)); + let activities = ActivityDetailsBuilder::new() + .add_activity(|| EnvironmentRenderer::build(3.into(), TOPIC_INFERRED_SCENE)) + .add_activity(|| { + NeuralNet::build_val( + 2.into(), + TOPIC_CAMERA_FRONT, + TOPIC_RADAR_FRONT, + TOPIC_INFERRED_SCENE, + ) + }) + .build(); - let mut agent = LocalFeoAgent::new(acts, SECONDARY1_NAME); + let mut agent = LocalFeoAgent::new(APPLICATION_NAME, activities, SECONDARY1_NAME); let mut program = agent.create_program(); program.run().await; @@ -114,34 +97,16 @@ async fn secondary_1_agent_program() { async fn secondary_2_agent_program() { info!("Starting secondary_2 agent {SECONDARY2_NAME}",); + let activities = ActivityDetailsBuilder::new() + .add_activity(|| { + EmergencyBraking::build(4.into(), TOPIC_INFERRED_SCENE, TOPIC_CONTROL_BRAKES) + }) + .add_activity(|| BrakeController::build(6.into(), TOPIC_CONTROL_BRAKES)) + .add_activity(|| LaneAssist::build(5.into(), TOPIC_INFERRED_SCENE, TOPIC_CONTROL_STEERING)) + .add_activity(|| SteeringController::build(7.into(), TOPIC_CONTROL_STEERING)) + .build(); - let emg_brk_act = Arc::new(Mutex::new(EmergencyBraking::build( - 5.into(), - TOPIC_INFERRED_SCENE, - TOPIC_CONTROL_BRAKES, - ))); - let brk_ctr_act = Arc::new(Mutex::new(BrakeController::build( - 6.into(), - TOPIC_CONTROL_BRAKES, - ))); - let lane_asst_act = Arc::new(Mutex::new(LaneAssist::build( - 7.into(), - TOPIC_INFERRED_SCENE, - TOPIC_CONTROL_STEERING, - ))); - - let str_ctr_act = Arc::new(Mutex::new(SteeringController::build( - 8.into(), - TOPIC_CONTROL_STEERING, - ))); - - let mut acts = Vec::new(); - acts.push(activity_into_invokes(&emg_brk_act)); - acts.push(activity_into_invokes(&brk_ctr_act)); - acts.push(activity_into_invokes(&lane_asst_act)); - acts.push(activity_into_invokes(&str_ctr_act)); - - let mut agent = LocalFeoAgent::new(acts, SECONDARY2_NAME); + let mut agent = LocalFeoAgent::new(APPLICATION_NAME, activities, SECONDARY2_NAME); let mut program = agent.create_program(); program.run().await; diff --git a/examples/rust/feo-mini-adas/src/config.rs b/examples/rust/feo-mini-adas/src/config.rs index cb91e66..a9b765a 100644 --- a/examples/rust/feo-mini-adas/src/config.rs +++ b/examples/rust/feo-mini-adas/src/config.rs @@ -26,95 +26,19 @@ pub const TOPIC_CONTROL_STEERING: &str = "feo/com/vehicle/control/steering"; pub const TOPIC_CAMERA_FRONT: &str = "feo/com/vehicle/camera/front"; pub const TOPIC_RADAR_FRONT: &str = "feo/com/vehicle/radar/front"; -// pub fn pool_configuration() -> HashMap>> { -// // Assign activities to different workers -// let w40: WorkerAssignment = ( -// 40.into(), -// vec![( -// 0.into(), -// Box::new(|id| Camera::build(id, TOPIC_CAMERA_FRONT)), -// )], -// ); -// let w41: WorkerAssignment = ( -// 41.into(), -// vec![(1.into(), Box::new(|id| Radar::build(id, TOPIC_RADAR_FRONT)))], -// ); - -// let w42: WorkerAssignment = ( -// 42.into(), -// vec![ -// ( -// 2.into(), -// Box::new(|id| { -// NeuralNet::build( -// id, -// TOPIC_CAMERA_FRONT, -// TOPIC_RADAR_FRONT, -// TOPIC_INFERRED_SCENE, -// ) -// }), -// ), -// ( -// 3.into(), -// Box::new(|id| EnvironmentRenderer::build(id, TOPIC_INFERRED_SCENE)), -// ), -// ], -// ); - -// let w43: WorkerAssignment = ( -// 43.into(), -// vec![ -// ( -// 4.into(), -// Box::new(|id| { -// EmergencyBraking::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_BRAKES) -// }), -// ), -// ( -// 6.into(), -// Box::new(|id| BrakeController::build(id, TOPIC_CONTROL_BRAKES)), -// ), -// ], -// ); -// let w44: WorkerAssignment = ( -// 44.into(), -// vec![ -// ( -// 5.into(), -// Box::new(|id| LaneAssist::build(id, TOPIC_INFERRED_SCENE, TOPIC_CONTROL_STEERING)), -// ), -// ( -// 7.into(), -// Box::new(|id| SteeringController::build(id, TOPIC_CONTROL_STEERING)), -// ), -// ], -// ); - -// // Assign workers to pools with exactly one pool belonging to one agent -// let a0: AgentAssignment = (100.into(), vec![w40, w41]); -// let a1: AgentAssignment = (101.into(), vec![w42]); -// let a2: AgentAssignment = (102.into(), vec![w43, w44]); - -// let assignments = vec![a0, a1, a2]; - -// let mut agent_map = HashMap::new(); -// for (agent, workers) in assignments { -// let mut worker_map = HashMap::new(); -// for (worker_id, activities) in workers { -// let previous = worker_map.insert(worker_id, activities); -// assert!( -// previous.is_none(), -// "Duplicate worker {worker_id} in assignment list" -// ); -// } -// let previous = agent_map.insert(agent, worker_map); -// assert!( -// previous.is_none(), -// "Duplicate agent {agent} in assignment list" -// ); -// } -// agent_map -// } +pub const CAM_ACTIVITY_NAME: &'static str = "cam_activity"; +pub const RADAR_ACTIVITY_NAME: &'static str = "radar_activity"; +pub const NEURAL_NET_ACTIVITY_NAME: &'static str = "neuralnet_activity"; +pub const ENV_READER_ACTIVITY_NAME: &'static str = "env_reader_activity"; +pub const EMG_BREAK_ACTIVITY_NAME: &'static str = "emg_break_activity"; +pub const BREAK_CTL_ACTIVITY_NAME: &'static str = "break_ctl_activity"; +pub const LANE_ASST_ACTIVITY_NAME: &'static str = "lane_asst_activity"; +pub const STR_CTL_ACTIVITY_NAME: &'static str = "str_ctl_activity"; + +pub const APPLICATION_NAME: &'static str = "adas_feo"; +pub const PRIMARY_NAME: &'static str = "primary_agent"; +pub const SECONDARY1_NAME: &'static str = "secondary1_agent"; +pub const SECONDARY2_NAME: &'static str = "secondary2_agent"; pub fn activity_dependencies() -> ActivityDependencies { // Primary | Secondary1 | Secondary2