diff --git a/.gitignore b/.gitignore index 0a39405..a7711be 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ bazel-* MODULE.bazel.lock user.bazelrc +external # Ruff .ruff_cache @@ -54,5 +55,12 @@ styles/ # Python .venv +venv/* __pycache__/ /.coverage + +# Rust +rust-project.json + +# vscode +.vscode/ diff --git a/BUILD b/BUILD index 929aaef..9b5e377 100644 --- a/BUILD +++ b/BUILD @@ -26,6 +26,7 @@ setup_starpls( copyright_checker( name = "copyright", srcs = [ + "feo/ad-demo", "scorex", "src", "tests", diff --git a/MODULE.bazel b/MODULE.bazel index 0a4d844..8f8805d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -123,3 +123,37 @@ use_repo( go_deps, "com_github_spf13_cobra", ) + +#### Following section for Feo framework examples ### +bazel_dep(name = "score_crates", version = "0.0.6") + +crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") +crate.from_cargo( + name = "ad-demo-local-crates", + cargo_lockfile = "//feo/ad-demo:Cargo.lock", + manifests = [ + "//feo/ad-demo:Cargo.toml", + ], +) +use_repo(crate, "ad-demo-local-crates") + +# Override feo's dependency on score_docs_as_code to use 2.3.3 +single_version_override( + module_name = "score_docs_as_code", + version = "2.3.3", +) + +bazel_dep(name = "feo", version = "1.0.1") +git_override( + module_name = "feo", + commit = "05702d0a386daf23912c26ccb2b35b584d3a9e8c", + remote = "https://github.com/eclipse-score/feo.git", +) + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "lichtblick_com_pypi", + python_version = "3.12", + requirements_lock = "//feo/ad-demo/lichtblick-com:requirements_lock.txt", +) +use_repo(pip, "lichtblick_com_pypi") diff --git a/README.md b/README.md index b312a36..847f380 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ QNX cross-compilation requires: Future extensions planned for SCRAMPLE: - Additional S-CORE platform module demonstrations + - [FEO demo application](feo/ad-demo/README.md) - More complex communication patterns - Performance benchmarking utilities - Integration with other S-CORE components diff --git a/feo/ad-demo/BUILD.bazel b/feo/ad-demo/BUILD.bazel new file mode 100644 index 0000000..b2e4d07 --- /dev/null +++ b/feo/ad-demo/BUILD.bazel @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") + +rust_library( + name = "activities_lib", + srcs = [ + "src/activities/application_config.rs", + "src/activities/camera_activity.rs", + "src/activities/common.rs", + "src/activities/mcap_activity.rs", + "src/activities/messages.rs", + "src/activities/mod.rs", + "src/activities/render_activity.rs", + "src/lib.rs", + ], + crate_features = [ + "com_iox2", + "signalling_relayed_tcp", + ], + crate_name = "ad_demo", + data = ["src/assets/gps_route.mcap"], + visibility = ["//visibility:public"], + deps = [ + "@ad-demo-local-crates//:camino", + "@ad-demo-local-crates//:mcap", + "@ad-demo-local-crates//:memmap2", + "@feo//feo:libfeo_rust", + "@feo//feo-com:libfeo_com_rust", + "@feo//feo-log:libfeo_log_rust", + "@feo//feo-logger:libfeo_logger_rust", + "@feo//feo-time:libfeo_time_rust", + "@feo//feo-tracing:libfeo_tracing_rust", + "@score_crates//:anyhow", + "@score_crates//:postcard", + "@score_crates//:rand", + "@score_crates//:serde", + "@score_crates//:serde_json", + "@score_crates//:tracing", + ], +) + +rust_binary( + name = "agent_primary", + srcs = [ + "src/agents/primary.rs", + ], + visibility = ["//visibility:public"], + deps = [ + ":activities_lib", + "@feo//feo:libfeo_rust", + "@feo//feo-log:libfeo_log_rust", + "@feo//feo-logger:libfeo_logger_rust", + "@feo//feo-time:libfeo_time_rust", + "@feo//feo-tracing:libfeo_tracing_rust", + ], +) + +rust_binary( + name = "agent_secondary", + srcs = [ + "src/agents/secondary.rs", + ], + visibility = ["//visibility:public"], + deps = [ + ":activities_lib", + "@feo//feo:libfeo_rust", + "@feo//feo-log:libfeo_log_rust", + "@feo//feo-logger:libfeo_logger_rust", + "@feo//feo-time:libfeo_time_rust", + "@feo//feo-tracing:libfeo_tracing_rust", + ], +) diff --git a/feo/ad-demo/Cargo.lock b/feo/ad-demo/Cargo.lock new file mode 100644 index 0000000..03024ba --- /dev/null +++ b/feo/ad-demo/Cargo.lock @@ -0,0 +1,415 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ad-demo-local-crates" +version = "0.0.1" +dependencies = [ + "camino", + "mcap", + "memmap2", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "binrw" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8318fda24dc135cdd838f57a2b5ccb6e8f04ff6b6c65528c4bd9b5fcdc5cf6" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0832bed83248115532dfb25af54fae1c83d67a2e4e3e2f591c13062e372e7e" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" + +[[package]] +name = "cc" +version = "1.2.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mcap" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3ca583fed649ed7cd8c976b59422996472c6b08067308443e0b60c56e0a4ca" +dependencies = [ + "bimap", + "binrw", + "byteorder", + "crc32fast", + "enumset", + "log", + "lz4", + "num_cpus", + "paste", + "static_assertions", + "thiserror", + "zstd", +] + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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.110", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/feo/ad-demo/Cargo.toml b/feo/ad-demo/Cargo.toml new file mode 100644 index 0000000..b1125d3 --- /dev/null +++ b/feo/ad-demo/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ad-demo-local-crates" +version = "0.0.1" +edition = "2024" + +[dependencies] +# crate dependencies specific to ad-demo and not available in @score_crates +mcap = "0.23.4" +camino = "1.2.1" +memmap2 = "0.9.9" diff --git a/feo/ad-demo/README.md b/feo/ad-demo/README.md new file mode 100644 index 0000000..36f6fd3 --- /dev/null +++ b/feo/ad-demo/README.md @@ -0,0 +1,40 @@ +# AD (Autonomous Driving) Demo using FEO Framework + +### Activities overview in ad-demo FEO (Fixed Execution Order) application +This [FEO](https://eclipse-score.github.io/score/main/features/frameworks/feo/index.html#) application contains three activities to demonstrate activity and topic creation, define their dependencies and how they communicate: +1. **Camera** - Simulates a camera sensor generating person detection data with fake object count, car count, obstacle distance and outputs as **CameraImage** topic message. +2. **SceneRender** - This activity simulates processing of camera images and extracting fake lane distance information to create enhanced scene data on top of Camera data. This activity commuincates with Camera activity to receive CameraImage topic and outputs **Scene** topic message. +3. **Mcap** - This activity reads GPS route data from an MCAP file and publishes to a TCP server for visualization in Lichtblick via Foxglove WebSocket Server. This activity is standalone and neither does commuincate with any other activity nor outputs any message. +This application tries to demostrate the basic usage of FEO framework. + +### First time setup +1. Follow the steps mentioned here: https://code.visualstudio.com/docs/languages/rust + 1. It gives all the required steps to get started with rust on linux + 1. Install [rustup](https://www.rust-lang.org/tools/install) in your system + 1. These are the set of available [components](https://rust-lang.github.io/rustup/concepts/components.html) once you install rustup in your system: + 1. Install [rust analyzer extension](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) for Rust lint and debug and support +1. So that rust analyzer works with the rust intelligence since you have a bazel project with minimal cargo.toml. You need to have a `rust_project.json` so that your rust code intelligence works properly. Run the following, from project root, to generate it: + 1. `bazelisk run @rules_rust//tools/rust_analyzer:gen_rust_project -- //feo/ad-demo/...` + 1. _Tip_ : Run it every time you would update the project or reorganize it. + +### Running ad-demo application +1. Run **agent_primary** (one terminal) : + 1. `bazelisk run //feo/ad-demo:agent_primary 2000` + 1. The FEO application cycles every 2000 milliseconds. + 1. This will run the Camera and Environment Renderer activity +2. Run **agent_secondary** (another terminal) : + 1. `bazelisk run //feo/ad-demo:agent_secondary` + 1. This will run Mcap activity + 1. Now, you can see in the logs the interaction between Camera and SceneRender activity, whereas MCap activity is independent to any of them. + 1. Camera and SceneRender logs are visible in previous terminal whereas MCap logs are visible in this terminal. +3. Run [foxglove websocket server script (in yet another terminal)](lichtblick-com/README.md). + 1. `bazelisk run //feo/ad-demo/lichtblick-com:foxglove_ws_server` + 1. Pleae check out the [Readme](lichtblick-com/README.md) for your first run. +1. Now open [Lichtblick](https://github.com/Lichtblick-Suite/lichtblick/releases) and + 1. Go to "Open connection" → "Foxglove WebSocket" + 1. Enter: `ws://localhost:8765` + 1. usually this is default and already exists + 1. if hosted on remote machine Enter: `ws://:8765` + 1. Now open [map panel](https://lichtblick-suite.github.io/docs/docs/visualization/message-schemas/location-fix) and listen to topic name **/gps/fix**. + 1. you can also open [raw message panel](https://lichtblick-suite.github.io/docs/docs/visualization/panels/raw-messages-panel) and listen to topic name **/gps/fix** - to see the lat long. + 1. **You should be able to see that the location (latitude, longitude) message from Mcap activity in FEO application is visible in map panel coming via foxglove webserver.** diff --git a/feo/ad-demo/lichtblick-com/BUILD.bazel b/feo/ad-demo/lichtblick-com/BUILD.bazel new file mode 100644 index 0000000..14a1f76 --- /dev/null +++ b/feo/ad-demo/lichtblick-com/BUILD.bazel @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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 +# ******************************************************************************* +load("@lichtblick_com_pypi//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +compile_pip_requirements( + name = "requirements", + requirements_in = "requirements.in", + requirements_txt = "requirements_lock.txt", +) + +py_binary( + name = "foxglove_ws_server", + srcs = ["foxglove_ws_server.py"], + main = "foxglove_ws_server.py", + visibility = ["//visibility:public"], + deps = [ + requirement("foxglove_websocket"), + ], +) diff --git a/feo/ad-demo/lichtblick-com/README.md b/feo/ad-demo/lichtblick-com/README.md new file mode 100644 index 0000000..6156481 --- /dev/null +++ b/feo/ad-demo/lichtblick-com/README.md @@ -0,0 +1,14 @@ +### Steps to run the foxglove websocket server script +1. **For first run only**: + 1. `bazelisk run //feo/ad-demo/lichtblick-com:requirements.update` + 1. It will use [requirements file](requirements.in) to create [requirements lock file](requirements_lock.txt) to install required packages in the pip environment. + 1. you can always test if the requirements are up to date for your copy + 1. `bazelisk test //feo/ad-demo/lichtblick-com:requirements_test` +1. **In one terminal**: run the FoxGloveServer & TCP server on localhost + 1. `bazelisk run //feo/ad-demo/lichtblick-com:foxglove_ws_server` + 1. By default this runs the web socket server and tcp server on localhost + 1. To run the server on some other machine or remote host, use the ip of the remote machine: + 1. `bazelisk run //feo/ad-demo/lichtblick-com:foxglove_ws_server ` + 1. This will run the python binary target (the servers) on a bazel environment which isolates it from the sytem - similar to python virtual environment. +1. **In other terminals**: [Start the primary and secondary agents in other terminals to run the FEO application](../README.md#running). + 1. This message will then be forwarded via the foxglove web socket server to lichtblick with topic name **/gps/fix** in the **messaging schema format [locationfix](https://lichtblick-suite.github.io/docs/docs/visualization/message-schemas/location-fix)**. diff --git a/feo/ad-demo/lichtblick-com/foxglove_ws_server.py b/feo/ad-demo/lichtblick-com/foxglove_ws_server.py new file mode 100644 index 0000000..edbe421 --- /dev/null +++ b/feo/ad-demo/lichtblick-com/foxglove_ws_server.py @@ -0,0 +1,154 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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 +# ******************************************************************************* +import asyncio +import json +import time +import argparse +from foxglove_websocket import run_cancellable +from foxglove_websocket.server import FoxgloveServer, FoxgloveServerListener +from foxglove_websocket.types import ChannelId + +LOCALHOST = "127.0.0.1" +TCP_PORT = 9001 + +""" +TCP listener that receives Json messages and forwards them to Foxglove channel +""" + + +async def tcp_listener(foxglove_server: FoxgloveServer, channel_id: ChannelId): + async def handle_client(reader, writer): + address = writer.get_extra_info("peername") + print(f"TCP connection opened at : {address}") + + try: + while True: + data = await reader.readline() + if not data: + break + + tcp_message = data.decode("utf-8").strip() + if tcp_message: + message_fxg = json.loads(tcp_message) + # covariance to just make location more visible in Lichtblick + message_fxg.update( + { + "position_covariance": [ + 700, + 0.0, + 0.0, + 0.0, + 700, + 0.0, + 0.0, + 0.0, + 700, + ], + "position_covariance_type": 2, + }, + ) + + print(f"Received TCP message: {message_fxg}") + + await foxglove_server.send_message( + channel_id, + time.time_ns(), + json.dumps(message_fxg).encode("utf8"), + ) + except Exception as e: + print(f"Error in TCP client: {e}") + finally: + writer.close() + await writer.wait_closed() + print(f"TCP connection closed at : {address}") + + tcp_server = await asyncio.start_server(handle_client, LOCALHOST, TCP_PORT) + + tcp_address = tcp_server.sockets[0].getsockname() + print(f"TCP server listening on {tcp_address}") + + async with tcp_server: + await tcp_server.serve_forever() + + +""" +Main function to start Foxglove server and TCP listener +""" + + +async def main(host): + class Listener(FoxgloveServerListener): + async def on_subscribe(self, server: FoxgloveServer, channel_id: ChannelId): + print("First client subscribed to", channel_id) + + async def on_unsubscribe(self, server: FoxgloveServer, channel_id: ChannelId): + print("Last client unsubscribed from", channel_id) + + async with FoxgloveServer( + host, + 8765, + "foxglove server", + supported_encodings=["json"], + ) as server: + server.set_listener(Listener()) + channel_id = await server.add_channel( + { + "topic": "/gps/fix", + "encoding": "json", + "schemaName": "foxglove.LocationFix", + "schema": json.dumps( + { + "type": "object", + "properties": { + "altitude": {"type": "number"}, + "latitude": {"type": "number"}, + "longitude": {"type": "number"}, + "position_covariance": { + "type": "array", + "items": {"type": "number"}, + "minItems": 9, + "maxItems": 9, + }, + "position_covariance_type": {"type": "integer"}, + }, + "required": ["latitude", "longitude"], + } + ), + "schemaEncoding": "jsonschema", + } + ) + + tcp_task = asyncio.create_task(tcp_listener(server, channel_id)) + + try: + await asyncio.Event().wait() + finally: + tcp_task.cancel() + try: + await tcp_task + except asyncio.CancelledError: + pass + print("TCP server closed.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "host", + type=str, + nargs="?", + default=LOCALHOST, + help="Host the server in this machine. If not provided, defaults to localhost", + ) + args = parser.parse_args() + run_cancellable(main(args.host)) diff --git a/feo/ad-demo/lichtblick-com/requirements.in b/feo/ad-demo/lichtblick-com/requirements.in new file mode 100644 index 0000000..ff2df4f --- /dev/null +++ b/feo/ad-demo/lichtblick-com/requirements.in @@ -0,0 +1 @@ +foxglove-websocket>=0.1.4 diff --git a/feo/ad-demo/lichtblick-com/requirements_lock.txt b/feo/ad-demo/lichtblick-com/requirements_lock.txt new file mode 100644 index 0000000..0ef8c73 --- /dev/null +++ b/feo/ad-demo/lichtblick-com/requirements_lock.txt @@ -0,0 +1,81 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# bazel run //feo/ad-demo/lichtblick-com:requirements.update +# +foxglove-websocket==0.1.4 \ + --hash=sha256:2ec8936982e478d103dd90268a572599fc0cce45a4ab95490d5bc31f7c8a8af8 \ + --hash=sha256:772e24e2c98bdfc704df53f7177c8ff5bab0abc4dac59a91463aca16debdd83a + # via -r feo/ad-demo/lichtblick-com/requirements.in +websockets==15.0.1 \ + --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ + --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ + --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ + --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ + --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ + --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ + --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ + --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ + --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ + --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ + --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ + --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ + --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ + --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ + --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ + --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ + --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ + --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ + --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ + --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ + --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ + --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ + --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ + --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ + --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ + --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ + --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ + --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ + --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ + --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ + --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ + --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ + --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ + --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ + --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ + --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ + --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ + --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ + --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ + --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ + --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ + --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ + --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ + --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ + --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ + --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ + --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ + --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ + --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ + --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ + --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ + --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ + --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ + --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ + --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ + --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ + --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ + --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ + --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ + --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ + --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ + --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ + --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ + --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ + --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ + --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ + --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ + --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 + # via foxglove-websocket diff --git a/feo/ad-demo/src/activities/application_config.rs b/feo/ad-demo/src/activities/application_config.rs new file mode 100644 index 0000000..6383539 --- /dev/null +++ b/feo/ad-demo/src/activities/application_config.rs @@ -0,0 +1,127 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::activities::common::*; + +use crate::activities::camera_activity::Camera; +use crate::activities::mcap_activity::Mcap; +use crate::activities::render_activity::SceneRender; + +use core::net::{IpAddr, Ipv4Addr, SocketAddr}; +use feo::activity::{ActivityBuilder, ActivityIdAndBuilder}; +use feo::ids::{ActivityId, AgentId, WorkerId}; +use feo::topicspec::{Direction, TopicSpecification}; +use feo_com::interface::ComBackend; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +pub type WorkerAssignment = (WorkerId, Vec<(ActivityId, Box)>); + +pub type ActivityDependencies = HashMap>; + +pub const COM_BACKEND: ComBackend = ComBackend::Iox2; + +pub const BIND_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); +pub const BIND_ADDR2: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8082); + +pub const TOPIC_INFERRED_SCENE: &str = "feo/com/vehicle/inferred/scene"; +pub const TOPIC_CAMERA_FRONT: &str = "feo/com/vehicle/camera/front"; + +/// Just for demonstration purposes, currently we dont use recorders. +pub const MAX_ADDITIONAL_SUBSCRIBERS: usize = 2; + +pub fn socket_paths() -> (PathBuf, PathBuf) { + ( + Path::new("/tmp/feo_listener1.socket").to_owned(), + Path::new("/tmp/feo_listener2.socket").to_owned(), + ) +} + +pub fn agent_assignments() -> HashMap)>> { + let worker_40: WorkerAssignment = ( + 40.into(), + vec![ + ( + 0.into(), + Box::new(|id| Camera::build(id, TOPIC_CAMERA_FRONT)), + ), + ( + 1.into(), + Box::new(|id| SceneRender::build(id, TOPIC_CAMERA_FRONT, TOPIC_INFERRED_SCENE)), + ), + ], + ); + let worker_41: WorkerAssignment = (41.into(), vec![(2.into(), Box::new(|id| Mcap::build(id)))]); + + let assignment = [(100.into(), vec![worker_40]), (101.into(), vec![worker_41])] + .into_iter() + .collect(); + + assignment +} + +pub fn activity_dependencies() -> ActivityDependencies { + let dependencies = [ + (0.into(), vec![]), + (1.into(), vec![0.into()]), + (2.into(), vec![]), + ]; + + dependencies.into() +} + +pub fn topic_dependencies<'a>() -> Vec> { + use Direction::*; + + vec![ + TopicSpecification::new::( + TOPIC_CAMERA_FRONT, + vec![(0.into(), Outgoing), (1.into(), Incoming)], + ), + TopicSpecification::new::(TOPIC_INFERRED_SCENE, vec![(1.into(), Outgoing)]), + ] +} + +pub fn worker_agent_map() -> HashMap { + agent_assignments() + .iter() + .flat_map(|(agent_id, worker_activity_map)| { + worker_activity_map + .iter() + .map(move |(worker_id, _)| (*worker_id, *agent_id)) + }) + .collect() +} + +pub fn agent_assignments_ids() -> HashMap)>> { + agent_assignments() + .into_iter() + .map(|(agent_id, worker_activity_map)| { + ( + agent_id, + worker_activity_map + .into_iter() + .map(|(worker_id, activity_and_builder)| { + ( + worker_id, + activity_and_builder + .into_iter() + .map(|(activity_id, _)| activity_id) + .collect(), + ) + }) + .collect(), + ) + }) + .collect() +} diff --git a/feo/ad-demo/src/activities/camera_activity.rs b/feo/ad-demo/src/activities/camera_activity.rs new file mode 100644 index 0000000..16b4f91 --- /dev/null +++ b/feo/ad-demo/src/activities/camera_activity.rs @@ -0,0 +1,66 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::activities::common::*; + +/// Camera activity +/// +/// This activity emulates a camera generating a [CameraImage]. +#[derive(Debug)] +pub struct Camera { + activity_id: ActivityId, + output_image: Box>, +} + +impl Camera { + pub fn build(activity_id: ActivityId, image_topic: &str) -> Box { + Box::new(Self { + activity_id, + output_image: activity_output(image_topic), + }) + } + + fn get_image(&mut self) -> CameraImage { + let mut rnd_generator = rand::rng(); + + CameraImage { + num_people: rnd_generator.random_range(0..20), + num_cars: rnd_generator.random_range(0..7), + obstacle_distance: rnd_generator.random_range(20.0..50.0), + } + } +} + +impl Activity for Camera { + fn id(&self) -> ActivityId { + self.activity_id + } + + #[instrument(name = "Camera startup")] + fn startup(&mut self) {} + + #[instrument(name = "Camera")] + fn step(&mut self) { + debug!("Stepping Camera"); + + if let Ok(camera) = self.output_image.write_uninit() { + let image = self.get_image(); + debug!("Sending image: {image:?}"); + let camera = camera.write_payload(image); + camera.send().unwrap(); + } + } + + #[instrument(name = "Camera shutdown")] + fn shutdown(&mut self) {} +} diff --git a/feo/ad-demo/src/activities/common.rs b/feo/ad-demo/src/activities/common.rs new file mode 100644 index 0000000..b425a48 --- /dev/null +++ b/feo/ad-demo/src/activities/common.rs @@ -0,0 +1,43 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// common imports for activities and config +pub(in crate::activities) use crate::activities::messages::{CameraImage, Scene}; + +pub(in crate::activities) use core::fmt; +pub(in crate::activities) use rand::Rng; + +pub(in crate::activities) use feo::activity::Activity; +pub(in crate::activities) use feo::ids::ActivityId; +pub(in crate::activities) use feo_com::interface::{ActivityInput, ActivityOutput}; +pub(in crate::activities) use feo_log::debug; +pub(in crate::activities) use feo_tracing::instrument; + +// imports specific to this file +use feo_com::iox2::{Iox2Input, Iox2Output}; + +/// Create an activity input. +pub(in crate::activities) fn activity_input(topic: &str) -> Box> +where + T: fmt::Debug + 'static, +{ + return Box::new(Iox2Input::new(topic)); +} + +/// Create an activity output. +pub(in crate::activities) fn activity_output(topic: &str) -> Box> +where + T: fmt::Debug + 'static, +{ + return Box::new(Iox2Output::new(topic)); +} diff --git a/feo/ad-demo/src/activities/mcap_activity.rs b/feo/ad-demo/src/activities/mcap_activity.rs new file mode 100644 index 0000000..b4492a5 --- /dev/null +++ b/feo/ad-demo/src/activities/mcap_activity.rs @@ -0,0 +1,130 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::activities::common::*; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use mcap::MessageStream; +use memmap2::Mmap; +use serde_json; +use serde_json::Value; +use std::fs; + +/// Mcap activity +/// +/// This activity reads mcap msgs in Mcap file and publish it to tcp server, +/// to be forwarded to lichtblick via foxglove websocket server. +pub struct Mcap { + activity_id: ActivityId, + message_stream: MessageStream<'static>, +} + +impl fmt::Debug for Mcap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Mcap") + .field("activity_id", &self.activity_id) + .field("message_stream", &"") + .finish() + } +} + +impl Mcap { + fn map_mcap>(p: P) -> Result { + let path = p.as_ref(); + debug!("Opening Mcap file at path: {}", path); + let file = fs::File::open(path).context("Couldn't open MCAP file at")?; + unsafe { Mmap::map(&file) }.context("Couldn't map MCAP file") + } + + pub fn build(activity_id: ActivityId) -> Box { + let mcap_to_map = Self::map_mcap("feo/ad-demo/src/assets/gps_route.mcap") + .expect("Could not open MCAP file"); + let static_slice: &'static [u8] = Box::leak(mcap_to_map.to_vec().into_boxed_slice()); + let message_stream = + MessageStream::new(static_slice).expect("Failed to create MessageStream"); + Box::new(Self { + activity_id, + message_stream, + }) + } + + fn get_single_msg(&mut self) -> Result> { + match self.message_stream.next() { + Some(Ok(message)) => { + let data_json = serde_json::from_slice(&message.data) + .context("Failed to convert msg data as JSON")?; + + debug!("single mcap message data: {}", data_json); + + Ok(Some(data_json)) + } + Some(Err(e)) => { + debug!("Error reading MCAP message: {}", e); + Ok(None) + } + None => { + debug!("No more messages in MCAP file"); + Ok(None) + } + } + } + + fn send_tcp_msg(&mut self, msg: &str) { + use std::io::Write; + use std::net::{Shutdown, TcpStream}; + if let Ok(mut stream) = TcpStream::connect("127.0.0.1:9001") { + debug!("Connected to the tcp server!"); + + let _ = stream.write(msg.as_bytes()); + + debug!("Message sent to the TCP server!"); + + let _ = stream.shutdown(Shutdown::Both); + } else { + debug!("Couldn't connect to tcp server..."); + } + } +} + +impl Activity for Mcap { + fn id(&self) -> ActivityId { + self.activity_id + } + + #[instrument(name = "Mcap startup")] + fn startup(&mut self) { + debug!("Mcap Startup"); + } + + #[instrument(name = "Mcap")] + fn step(&mut self) { + debug!("Stepping Mcap"); + + if let Ok(Some(mcap_msg_json)) = self.get_single_msg() { + let compact_json = + serde_json::to_string(&mcap_msg_json).expect("failed to stringify Json"); + + debug!("Read Mcap message: {compact_json:?}"); + self.send_tcp_msg(&compact_json); + } else { + debug!("No message in mcap left."); + self.send_tcp_msg("No message in mcap left"); + } + } + + #[instrument(name = "Mcap shutdown")] + fn shutdown(&mut self) { + debug!("Mcap Shutdown"); + } +} diff --git a/feo/ad-demo/src/activities/messages.rs b/feo/ad-demo/src/activities/messages.rs new file mode 100644 index 0000000..8495b77 --- /dev/null +++ b/feo/ad-demo/src/activities/messages.rs @@ -0,0 +1,45 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// Messages +/// +/// This module contains the definition of messages +/// to be used within this application. +use postcard::experimental::max_size::MaxSize; +use serde::{Deserialize, Serialize}; + +/// Camera image +/// +/// Fake camera frame with number of detected people and cars, +/// and distance to the closest obstacle. +#[derive(Serialize, Deserialize, MaxSize, Debug, Default)] +#[repr(C)] +pub struct CameraImage { + pub num_people: usize, + pub num_cars: usize, + pub obstacle_distance: f64, +} + +/// Scene +/// +/// Fake inferred scene with number of detected people and cars, +/// distance to the closest obstacle, and distances to left and right lane. +#[derive(Serialize, Deserialize, MaxSize, Debug, Default)] +#[repr(C)] +pub struct Scene { + pub num_people: usize, + pub num_cars: usize, + pub obstacle_distance: f64, + pub distance_left_lane: f64, + pub distance_right_lane: f64, +} diff --git a/feo/ad-demo/src/activities/mod.rs b/feo/ad-demo/src/activities/mod.rs new file mode 100644 index 0000000..0e06adf --- /dev/null +++ b/feo/ad-demo/src/activities/mod.rs @@ -0,0 +1,21 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// Module declarations for activities, messages, and config +pub(in crate::activities) mod camera_activity; +pub(in crate::activities) mod common; +pub(in crate::activities) mod mcap_activity; +pub(in crate::activities) mod messages; +pub(in crate::activities) mod render_activity; + +pub mod application_config; diff --git a/feo/ad-demo/src/activities/render_activity.rs b/feo/ad-demo/src/activities/render_activity.rs new file mode 100644 index 0000000..1c89185 --- /dev/null +++ b/feo/ad-demo/src/activities/render_activity.rs @@ -0,0 +1,81 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::activities::common::*; +use crate::activities::messages::{CameraImage, Scene}; + +/// Scene render activity +/// +/// This activity emulates rendering a scene from a [CameraImage] which can be rendered to a display. +#[derive(Debug)] +pub struct SceneRender { + activity_id: ActivityId, + input_image: Box>, + output_scene: Box>, +} + +impl SceneRender { + pub fn build( + activity_id: ActivityId, + image_topic: &str, + scene_topic: &str, + ) -> Box { + Box::new(Self { + activity_id, + input_image: activity_input(image_topic), + output_scene: activity_output(scene_topic), + }) + } +} + +impl Activity for SceneRender { + fn id(&self) -> ActivityId { + self.activity_id + } + + #[instrument(name = "SceneRender startup")] + fn startup(&mut self) {} + + #[instrument(name = "SceneRender")] + fn step(&mut self) { + debug!("Stepping SceneRender"); + + let received_image = self.input_image.read(); + let output_scene = self.output_scene.write_uninit(); + + if let (Ok(received_image), Ok(output_scene)) = (received_image, output_scene) { + debug!("Received image: {:?}", *received_image); + + let mut rnd_generator = rand::rng(); + + let op_scene = Scene { + num_people: received_image.num_people, + num_cars: received_image.num_cars, + obstacle_distance: received_image.obstacle_distance, + distance_left_lane: rnd_generator.random_range(10.0..50.0), + distance_right_lane: rnd_generator.random_range(10.0..50.0), + }; + + debug!("Sending scene: {op_scene:?}"); + + let send_output_scene = output_scene.write_payload(op_scene); + + send_output_scene.send().unwrap(); + + debug!("Rendering scene"); + } + } + + #[instrument(name = "SceneRender shutdown")] + fn shutdown(&mut self) {} +} diff --git a/feo/ad-demo/src/agents/primary.rs b/feo/ad-demo/src/agents/primary.rs new file mode 100644 index 0000000..035779d --- /dev/null +++ b/feo/ad-demo/src/agents/primary.rs @@ -0,0 +1,88 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use feo::agent::com_init::initialize_com_primary; +use feo::agent::relayed::primary::{Primary, PrimaryConfig}; +use feo::agent::NodeAddress; +use feo::ids::{ActivityId, AgentId, WorkerId}; +use feo_log::{info, LevelFilter}; +use feo_time::Duration; +use std::collections::HashMap; + +use ad_demo::activities::application_config::{ + activity_dependencies, agent_assignments, agent_assignments_ids, topic_dependencies, + worker_agent_map, BIND_ADDR, BIND_ADDR2, COM_BACKEND, MAX_ADDITIONAL_SUBSCRIBERS, +}; + +const AGENT_ID: AgentId = AgentId::new(100); +const DEFAULT_FEO_CYCLE_TIME: Duration = Duration::from_secs(5); + +/// Primary agent for the ad-demo FEO application. +fn main() { + feo_logger::init(LevelFilter::Debug, true, true); + feo_tracing::init(feo_tracing::LevelFilter::TRACE); + + info!("Starting primary agent {AGENT_ID}"); + + // Initialize topics. Make it alive until application runs. + let _topic_guards = initialize_com_primary( + COM_BACKEND, + AGENT_ID, + topic_dependencies(), + &agent_assignments_ids(), + MAX_ADDITIONAL_SUBSCRIBERS, + ); + + let mut primary_agent = Primary::new(generate_primary_config()); + + primary_agent.run().unwrap() +} + +fn get_cycle_time_from_args() -> Duration { + let args: Vec = std::env::args().collect(); + + args.get(1) + .and_then(|argument| argument.parse::().ok()) + .map(Duration::from_millis) + .unwrap_or(DEFAULT_FEO_CYCLE_TIME) +} + +fn generate_primary_config() -> PrimaryConfig { + let activity_worker_map: HashMap = agent_assignments() + .values() + .flat_map(|worker_activity_builder| { + worker_activity_builder + .iter() + .flat_map(move |(worker_id, activity_id_builder_vec)| { + activity_id_builder_vec + .iter() + .map(|(activity_id, _)| (*activity_id, *worker_id)) + }) + }) + .collect(); + + let with_no_recorders: Vec = vec![]; + + PrimaryConfig { + cycle_time: get_cycle_time_from_args(), + activity_dependencies: activity_dependencies(), + recorder_ids: with_no_recorders, + worker_assignments: agent_assignments().remove(&AGENT_ID).unwrap(), + timeout: Duration::from_secs(10), + bind_address_senders: NodeAddress::Tcp(BIND_ADDR), + bind_address_receivers: NodeAddress::Tcp(BIND_ADDR2), + id: AGENT_ID, + worker_agent_map: worker_agent_map(), + activity_worker_map, + } +} diff --git a/feo/ad-demo/src/agents/secondary.rs b/feo/ad-demo/src/agents/secondary.rs new file mode 100644 index 0000000..29e1af9 --- /dev/null +++ b/feo/ad-demo/src/agents/secondary.rs @@ -0,0 +1,58 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use ad_demo::activities::application_config::{ + agent_assignments, agent_assignments_ids, topic_dependencies, BIND_ADDR, BIND_ADDR2, + COM_BACKEND, +}; +use core::time::Duration; +use feo::agent::com_init::initialize_com_secondary; +use feo::agent::relayed::secondary::{Secondary, SecondaryConfig}; +use feo::agent::NodeAddress; +use feo::ids::ActivityId; +use feo::ids::AgentId; +use feo_log::{info, LevelFilter}; +use std::collections::HashSet; + +/// One Secondary agent for the ad-demo FEO application. +fn main() { + feo_logger::init(LevelFilter::Debug, true, true); + feo_tracing::init(feo_tracing::LevelFilter::TRACE); + + let secondary_agent_id = AgentId::new(101); + + let config = SecondaryConfig { + id: secondary_agent_id, + worker_assignments: agent_assignments().remove(&secondary_agent_id).unwrap(), + timeout: Duration::from_secs(10), + bind_address_senders: NodeAddress::Tcp(BIND_ADDR), + bind_address_receivers: NodeAddress::Tcp(BIND_ADDR2), + }; + + let local_activities: HashSet = agent_assignments_ids() + .remove(&secondary_agent_id) + .unwrap() + .iter() + .flat_map(|(_, activity_ids)| activity_ids.iter()) + .copied() + .collect(); + + // Initialize topics. Make it alive until application runs. + let _topic_guards = + initialize_com_secondary(COM_BACKEND, topic_dependencies(), &local_activities); + + info!("Starting secondary agent {}", secondary_agent_id); + + let secondary_agent = Secondary::new(config); + secondary_agent.run(); +} diff --git a/feo/ad-demo/src/assets/gps_route.mcap b/feo/ad-demo/src/assets/gps_route.mcap new file mode 100644 index 0000000..2d1667b Binary files /dev/null and b/feo/ad-demo/src/assets/gps_route.mcap differ diff --git a/feo/ad-demo/src/lib.rs b/feo/ad-demo/src/lib.rs new file mode 100644 index 0000000..c7fd114 --- /dev/null +++ b/feo/ad-demo/src/lib.rs @@ -0,0 +1,17 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +#![deny(clippy::std_instead_of_core)] + +/// share activity dependencies between primary and secondary agents +pub mod activities;