diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 88c55cfa..e99a911e 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -64,6 +64,8 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v2 - name: Build - run: cargo build + # Build the project with the `managed_subscribe`, `digital_twin_graph` and `digital_twin_registry` features enabled. + run: cargo build --features "managed_subscribe,digital_twin_graph,digital_twin_registry" - name: Test - run: cargo test + # Test the project with the `managed_subscribe`, `digital_twin_graph` and `digital_twin_registry` features enabled. + run: cargo test --features "managed_subscribe,digital_twin_graph,digital_twin_registry" diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index ac32ecde..9b9979f5 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -18,5 +18,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 + # Ignored advisories: + # - https://rustsec.org/advisories/RUSTSEC-2024-0320 : yaml-rust is unmaintained + # - This is a dependency of the config crate, which does not have a version without yaml-rust. + # See https://github.com/mehcode/config-rs/issues/473 - run: | cargo audit --deny warnings --ignore RUSTSEC-2024-0320 diff --git a/Cargo.toml b/Cargo.toml index 256d73d6..29ab2dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ members = [ # extension "core/module/managed_subscribe", + "core/module/digital_twin_graph", + "core/module/digital_twin_registry", # DTDL tools "dtdl-tools", @@ -27,6 +29,7 @@ members = [ "samples/common", "samples/protobuf_data_access", "samples/command", + "samples/digital_twin_graph", "samples/managed_subscribe", "samples/mixed", "samples/property", @@ -67,6 +70,7 @@ strum = "0.26.1" strum_macros = "0.26.1" tokio = "1.29.1" tokio-console-subscriber = { version = "0.2.0", package = "console-subscriber" } +tokio-retry = "0.3" tokio-stream = "0.1.14" tonic = "0.11.0" tonic-build = "0.11.0" diff --git a/README.md b/README.md index 36a5f1de..625fcec0 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,7 @@ - [Tokio Console Support](#tokio-console-support) - [Running the Tests](#running-the-tests) - [Running the Samples](#running-the-samples) - - [Property Sample](#property-sample) - - [Command Sample](#command-sample) - - [Mixed Sample](#mixed-sample) - - [Seat Massager Sample](#seat-massager-sample) - - [Streaming Sample](#streaming-sample) - - [Using Chariott](#using-chariott) +- [Using Chariott](#using-chariott) - [Running in a Container](#running-in-a-container) - [Trademarks](#trademarks) @@ -95,24 +90,33 @@ Instructions for installing Mosquitto can be found [here](https://github.com/ecl ## Cloning the Repo -The repo has two submodules [opendigitaltwins-dtdl](https://github.com/Azure/opendigitaltwins-dtdl) and [iot-plugandplay-models](https://github.com/Azure/iot-plugandplay-models) that provide DTDL context files -and DTDL samples file. To ensure that these are included, please use the following command when cloning Ibeji's github repo: +The repo has two submodules [opendigitaltwins-dtdl](https://github.com/Azure/opendigitaltwins-dtdl) and [iot-plugandplay-models](https://github.com/Azure/iot-plugandplay-models) that provide DTDL context files and DTDL samples file. To ensure that these are included, please use the following command when cloning Ibeji's github repo: -`git clone --recurse-submodules https://github.com/eclipse-ibeji/ibeji` +````shell +git clone --recurse-submodules https://github.com/eclipse-ibeji/ibeji` +```` ## Building Once you have installed the prerequisites, go to your enlistment's root directory and run: -`cargo build` +````shell +cargo build +```` -This should build all of the libraries and executables. +This will build all of the foundation libraries and executables. + +Ibeji also has add-on modules that rely on feature flags to include them in the build. For example, to build Ibeji with the Digital Twin Graph and the Digital Twin Registry modules run: + +````shell +cargo build --features "digital_twin_graph,digital_twin_registry" +```` ### Tokio Console Support Ibeji has support for using the [tokio console](https://github.com/tokio-rs/console) for advanced debugging. To enable this support, you need to build with the `tokio_console` feature enabled and with the `tokio_unstable` config flag for the rust compiler: -```bash +```shell RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio_console ``` @@ -124,178 +128,36 @@ Note that the tokio console will intercept trace-level logs, so these will not b After successfully building Ibeji, you can run all of the unit tests. To do this go to the enlistment's root directory and run: -`cargo test` +````shell +cargo test +```` Currently, we have no integration tests or end-to-end tests. ## Running the Samples -There are currently four samples: one that demonstrates the use of a property, one that demonstrates the use of a command, one that -demonstrates the mixed use of properties and commands and one that demonstrates the use of get/set for a seat massager. +There are currently six samples: -The demos use config files and we have provided a templated version of each config file. These templates can be found in: +- [Property Sample](docs/samples/property/README.md) - demonstrates the use of a property +- [Command Sample](docs/samples/command/README.md) - demonstrates the use of a command +- [Mixed Sample](docs/samples/mixed/README.md) - demonstrates the mixed use of properties and commands +- [Seat Massager Sample](docs/samples/seat_massager/README.md) - demonstrates the use of get/set for a seat massager +- [Streaming Sample](docs/samples/streaming/README.md) - demonstrates the use of streaming +- [Digital Twin Graph Sample](docs/samples/digital_twin_graph/README.md) - demonstrates the use of the Digital Twin Graph Service -- {repo-root-dir}/core/invehicle-digital-twin/template -- {repo-root-dir}/samples/common/template - -Configuration files will be loaded from the current working directory by default +The samples' configuration files will be loaded from the current working directory by default, but an `IBEJI_HOME` environment variable can be used to change the base configuration directory to a different one: -```bash +```shell IBEJI_HOME=/etc/ibeji ./invehicle-digital-twin ``` The above example tells `invehicle-digital-twin` to load configuration files from `/etc/ibeji` instead of using the current working directory. -Chariott may be used to discover the in-vehicle digital twin service. We will discuss how to enable this feature. - -### Property Sample - -The following instructions are for the demo for the use of a property. This sample uses a MQTT Broker; please make sure that it is running. - -Steps: - -1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. -Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. -The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
-1. In each window, change directory to the directory containing the build artifacts. -Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

-`cd {repo-root-dir}/target/debug`
-1. Create the three config files with the following contents, if they are not already there:

----- consumer_settings.yaml ----
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

----- invehicle_digital_twin_settings.yaml ----
-`invehicle_digital_twin_authority: "0.0.0.0:5010"`

----- provider_settings.yaml ----
-`provider_authority: "0.0.0.0:1883"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-1. In the top window, run:

-`./invehicle-digital-twin`
-1. In the middle window, run:

-`./property-provider`
-1. In the bottom window, run:

-`./property-consumer`
-1. Use control-c in each of the windows when you wish to stop the demo. - -### Command Sample - -The following instructions are for the demo for the use of a command. - -Steps: - -1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. -Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. -The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
-1. In each window, change directory to the directory containing the build artifacts. -Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

-`cd {repo-root-dir}/target/debug`
-1. Create the three config files with the following contents, if they are not already there:

----- consumer_settings.yaml ----
-`consumer_authority: "0.0.0.0:6010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

----- invehicle_digital_twin_settings.yaml ----
-`invehicle_digital_twin_authority: "0.0.0.0:5010"`

----- provider_settings.yaml ----
-`provider_authority: "0.0.0.0:4010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-1. In the top window, run:

-`./invehicle-digital-twin`
-1. In the middle window, run:

-`./command-provider`
-1. In the bottom window, run:

-`./command-consumer`
-1. Use control-c in each of the windows when you wish to stop the demo. - -### Mixed Sample - -The following instructions are for the demo for the mixed use of commands and properties. - -Steps: - -1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. -Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. -The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
-1. In each window, change directory to the directory containing the build artifacts. -Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

-`cd {repo-root-dir}/target/debug`
-1. Create the three config files with the following contents, if they are not already there:

----- consumer_settings.yaml ----
-`consumer_authority: "0.0.0.0:6010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

----- invehicle_digital_twin_settings.yaml ----
-`invehicle_digital_twin_authority: "0.0.0.0:5010"`

----- provider_settings.yaml ----
-`provider_authority: "0.0.0.0:4010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-1. In the top window, run:

-`./invehicle-digital-twin`
-1. In the middle window, run:

-`./mixed-provider`
-1. In the bottom window, run:

-`./mixed-consumer`
-1. Use control-c in each of the windows when you wish to stop the demo. - -### Seat Massager Sample - -The following instructions are for the demo for a seat massager. - -Steps: - -1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. -Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. -The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
-1. In each window, change directory to the directory containing the build artifacts. -Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

-`cd {repo-root-dir}/target/debug`
-1. Create the three config files with the following contents, if they are not already there:

----- consumer_settings.yaml ----
-`consumer_authority: "0.0.0.0:6010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

----- invehicle_digital_twin_settings.yaml ----
-`invehicle_digital_twin_authority: "0.0.0.0:5010"`

----- provider_settings.yaml ----
-`provider_authority: "0.0.0.0:4010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-1. In the top window, run:

-`./invehicle-digital-twin`
-1. In the middle window, run:

-`./seat-massager-provider`
-1. In the bottom window, run:

-`./seat-massager-consumer`
-1. Use control-c in each of the windows when you wish to stop the demo. - -### Streaming Sample - -The following instructions are for the demo for streaming. - -Steps: - -1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. -Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. -The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
-1. In each window, change directory to the directory containing the build artifacts. -Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

-`cd {repo-root-dir}/target/debug`
-1. Create the three config files with the following contents, if they are not already there:

----- streaming_consumer_settings.yaml ----
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-`number_of_images: 20`

----- invehicle_digital_twin_settings.yaml ----
-`invehicle_digital_twin_authority: "0.0.0.0:5010"`

----- streaming_provider_settings.yaml ----
-`provider_authority: "0.0.0.0:4010"`
-`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

-`image_directory: "<>/examples/applications/simulated-camera/images"` -1. In the top window, run:

-`./invehicle-digital-twin`
-1. In the middle window, run:

-`./streaming-provider`
-1. In the bottom window, run:

-`./streaming-consumer`
-1. Use control-c in each of the windows when you wish to stop the demo. - -### Using Chariott +With the samples, Chariott may be used to discover the in-vehicle digital twin service. We will discuss how to enable this feature in the section on [Using Chariott](#using-chariott). + +## Using Chariott If you want the digital twin consumers and digital twin providers for each demo to use Chariott to discover the URI for the In-Vehicle Digital Twin Service, rather than having it statically provided in their respective config file, then do the following before starting each demo: diff --git a/core/common/src/utils.rs b/core/common/src/utils.rs index 7acfdc03..03139218 100644 --- a/core/common/src/utils.rs +++ b/core/common/src/utils.rs @@ -185,6 +185,17 @@ pub async fn get_service_uri( Ok(result) } +/// Is the provided subset a subset of the provided superset? +/// +/// # Arguments +/// * `subset` - The provided subset. +/// * `superset` - The provided superset. +pub fn is_subset(subset: &[String], superset: &[String]) -> bool { + subset.iter().all(|subset_member| { + superset.iter().any(|supserset_member| subset_member == supserset_member) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -228,4 +239,26 @@ mod tests { .await; assert!(result.is_err()); } + + #[test] + fn is_subset_test() { + assert!(is_subset(&[], &[])); + assert!(is_subset(&[], &["one".to_string()])); + assert!(is_subset(&[], &["one".to_string(), "two".to_string()])); + assert!(is_subset(&["one".to_string()], &["one".to_string()])); + assert!(is_subset(&["one".to_string()], &["one".to_string(), "two".to_string()])); + assert!(is_subset( + &["one".to_string(), "two".to_string()], + &["one".to_string(), "two".to_string()] + )); + assert!(!is_subset( + &["one".to_string(), "two".to_string(), "three".to_string()], + &["one".to_string(), "two".to_string()] + )); + assert!(!is_subset( + &["one".to_string(), "two".to_string(), "three".to_string()], + &["one".to_string()] + )); + assert!(!is_subset(&["one".to_string(), "two".to_string(), "three".to_string()], &[])); + } } diff --git a/core/invehicle-digital-twin/Cargo.toml b/core/invehicle-digital-twin/Cargo.toml index 213a8bd3..abceae65 100644 --- a/core/invehicle-digital-twin/Cargo.toml +++ b/core/invehicle-digital-twin/Cargo.toml @@ -19,6 +19,8 @@ http = { workspace = true } iref = { workspace = true } log = { workspace = true } common = { path = "../common" } +digital_twin_graph = { path = "../module/digital_twin_graph", optional = true } +digital_twin_registry = { path = "../module/digital_twin_registry", optional = true } managed_subscribe = { path = "../module/managed_subscribe", optional = true } parking_lot = { workspace = true } prost = { workspace = true } @@ -38,5 +40,7 @@ yaml-rust = { workspace = true } tonic-build = { workspace = true } [features] +digital_twin_graph = ["dep:digital_twin_graph"] +digital_twin_registry = ["dep:digital_twin_registry"] managed_subscribe = ["dep:managed_subscribe"] tokio_console = ["dep:tokio-console-subscriber", "tokio/tracing"] diff --git a/core/invehicle-digital-twin/src/main.rs b/core/invehicle-digital-twin/src/main.rs index 017af43c..6e5916e0 100644 --- a/core/invehicle-digital-twin/src/main.rs +++ b/core/invehicle-digital-twin/src/main.rs @@ -8,6 +8,12 @@ #[cfg(feature = "managed_subscribe")] use managed_subscribe::managed_subscribe_module::ManagedSubscribeModule; +#[cfg(feature = "digital_twin_graph")] +use digital_twin_graph::digital_twin_graph_module::DigitalTwinGraphModule; + +#[cfg(feature = "digital_twin_registry")] +use digital_twin_registry::digital_twin_registry_module::DigitalTwinRegistryModule; + // End: Module references. #[allow(unused_imports)] @@ -78,7 +84,7 @@ async fn register_invehicle_digital_twin_service_with_chariott( Ok(()) } -/// Builds the enabled modules for the grpc server and starts the server. +/// Builds the enabled modules for the app server and starts the app server. /// /// # Arguments /// * `addr` - The address the server will be hosted on. @@ -92,7 +98,7 @@ async fn register_invehicle_digital_twin_service_with_chariott( /// 5. Call and return from the block `.add_module()` on the server with the updated middleware and /// module. #[allow(unused_assignments, unused_mut)] // Necessary when no extra modules are built. -async fn build_server_and_serve( +async fn build_app_server_and_serve( addr: SocketAddr, base_service: S, ) -> Result<(), Box> @@ -107,32 +113,63 @@ where let mut server: GrpcServer = GrpcServer::new(addr); #[cfg(feature = "managed_subscribe")] - // (1) Adds the Managed Subscribe module to the service. - let server = { - // (2) Initialize the Managed Subscribe module, which implements GrpcModule. + // Adds the Managed Subscribe module to the app server. + let mut server = { + // Initialize the Managed Subscribe module, which implements GrpcModule. let managed_subscribe_module = ManagedSubscribeModule::new().await.map_err(|error| { error!("Unable to create Managed Subscribe module."); error })?; - // (3) Create interceptor layer to be added to the server. + // Create the interceptor layer to be added to the app server. let managed_subscribe_layer = GrpcInterceptorLayer::new(Box::new(managed_subscribe_module.create_interceptor())); - // (4) Add the interceptor(s) to the middleware stack. + // Add the interceptor(s) to the middleware stack. let current_middleware = server.middleware.clone(); let new_middleware = current_middleware.layer(managed_subscribe_layer); info!("Initialized Managed Subscribe module."); - // (5) Add the module with the updated middleware stack to the server. + // Add the module with the updated middleware stack to the server. server.add_module(new_middleware, Box::new(managed_subscribe_module)) }; - // Construct the server. + #[cfg(feature = "digital_twin_graph")] + // Adds the Digital Twin Graph module to the app server. + let mut server = { + // Initialize the Digital Twin Graph module, which implements GrpcModule. + let digital_twin_graph_module = DigitalTwinGraphModule::new().await.map_err(|error| { + error!("Unable to create Digital Twin Graph module."); + error + })?; + + info!("Initialized Digital Twin Graph module."); + + // Add the module with the updated middleware stack to the server. + server.add_module(server.middleware.clone(), Box::new(digital_twin_graph_module)) + }; + + #[cfg(feature = "digital_twin_registry")] + // Adds the Digital Twin Registry module to the app server. + let mut server = { + // Initialize the Digital Twin Registry module, which implements GrpcModule. + let digital_twin_registry_module = + DigitalTwinRegistryModule::new().await.map_err(|error| { + error!("Unable to create Digital Twin Registry module."); + error + })?; + + info!("Initialized Digital Twin Registry module."); + + // Add the module with the updated middleware stack to the server. + server.add_module(server.middleware.clone(), Box::new(digital_twin_registry_module)) + }; + + // Construct the app server. let builder = server.construct_server().add_service(base_service); - // Start the server. + // Start the app server. builder.serve(addr).await.map_err(|error| error.into()) } @@ -206,8 +243,8 @@ async fn main() -> Result<(), Box> { let base_service = InvehicleDigitalTwinServer::new(invehicle_digital_twin_impl); - // Build and start the grpc server. - build_server_and_serve(addr, base_service).await?; + // Build and start the app server. + build_app_server_and_serve(addr, base_service).await?; debug!("The Digital Twin Service has completed."); diff --git a/core/module/digital_twin_graph/Cargo.toml b/core/module/digital_twin_graph/Cargo.toml new file mode 100644 index 00000000..9991eca8 --- /dev/null +++ b/core/module/digital_twin_graph/Cargo.toml @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[package] +name = "digital_twin_graph" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +common = { path = "../../common" } +core-protobuf-data-access = { path = "../../protobuf_data_access" } +log = { workspace = true } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true , features = ["full"] } +tokio-retry = { workspace = true } +tonic = { workspace = true } +tower = { workspace = true } +yaml-rust = { workspace = true } +uuid = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/core/module/digital_twin_graph/src/digital_twin_graph_config.rs b/core/module/digital_twin_graph/src/digital_twin_graph_config.rs new file mode 100644 index 00000000..ddd1fb42 --- /dev/null +++ b/core/module/digital_twin_graph/src/digital_twin_graph_config.rs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use common::utils; +use serde_derive::Deserialize; + +const DEFAULT_CONFIG_FILENAME: &str = "digital_twin_graph_settings"; + +/// The settings for the digital twin graph service. +#[derive(Debug, Deserialize)] +pub struct Settings { + /// The authority (address + optional port in the format "
[:]") for the Ibeji application server. + pub base_authority: String, +} + +/// Load the settings. +/// The settings are loaded from the default config file name. +pub fn load_settings() -> Settings { + utils::load_settings(DEFAULT_CONFIG_FILENAME).unwrap() +} + +/// Load the settings with the specified config file name. +/// +/// # Arguments +/// * `config_filename` - The name of the config file. +pub fn load_settings_with_config_filename(config_filename: &str) -> Settings { + utils::load_settings(config_filename).unwrap() +} diff --git a/core/module/digital_twin_graph/src/digital_twin_graph_impl.rs b/core/module/digital_twin_graph/src/digital_twin_graph_impl.rs new file mode 100644 index 00000000..02de43cd --- /dev/null +++ b/core/module/digital_twin_graph/src/digital_twin_graph_impl.rs @@ -0,0 +1,494 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use common::utils::is_subset; +use core_protobuf_data_access::async_rpc::v1::request::{ + request_client::RequestClient, AskRequest, +}; +use core_protobuf_data_access::async_rpc::v1::respond::AnswerRequest; +use core_protobuf_data_access::module::digital_twin_graph::v1::{ + digital_twin_graph_server::DigitalTwinGraph, FindRequest, FindResponse, GetRequest, + GetResponse, InvokeRequest, InvokeResponse, SetRequest, SetResponse, +}; +use core_protobuf_data_access::module::digital_twin_registry::v1::digital_twin_registry_client::DigitalTwinRegistryClient; +use core_protobuf_data_access::module::digital_twin_registry::v1::{ + EntityAccessInfo, FindByInstanceIdRequest, FindByInstanceIdResponse, FindByModelIdRequest, + FindByModelIdResponse, +}; +use log::{debug, warn}; +use std::sync::Arc; +use tokio::sync::broadcast; +use tokio::time::{sleep, timeout, Duration}; +use tokio_retry::strategy::{jitter, ExponentialBackoff}; +use tokio_retry::Retry; +use uuid::Uuid; + +use crate::{digital_twin_operation, digital_twin_protocol, TargetedPayload}; + +#[derive(Debug)] +pub struct DigitalTwinGraphImpl { + /// Digital Twin Registry URI. + digital_twin_registry_uri: String, + /// Respond URI. + respond_uri: String, + /// The sender for the asynchronous channel for AnswerRequests. + tx: Arc>, +} + +impl DigitalTwinGraphImpl { + /// The base duration in milliseconds for the backoff strategy. + const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + + /// The maximum number of retries for the backoff strategy. + const MAX_RETRIES: usize = 100; + + /// The timeout period in milliseconds for the backoff strategy. + const TIMEOUT_PERIOD_IN_MILLIS: u64 = 5000; + + /// Create a new instance of a DigitalTwinGraphImpl. + /// + /// # Arguments + /// * `digital_twin_registry_uri` - The uri for the digital twin registry service. + /// * `respond_uri` - The uri for the respond service. + /// * `tx` - The sender for the asynchronous channel for AnswerRequest's. + pub fn new( + digital_twin_registry_uri: &str, + respond_uri: &str, + tx: Arc>, + ) -> DigitalTwinGraphImpl { + DigitalTwinGraphImpl { + digital_twin_registry_uri: digital_twin_registry_uri.to_string(), + respond_uri: respond_uri.to_string(), + tx, + } + } + + /// Use the Digital Twin Registery service to find the endpoints for digital twin providers that support + /// the specified model id, protocol and operations. + /// + /// # Arguments + /// * `model_id` - The matching model id. + /// * `protocol` - The required protocol. + /// * `operations` - The required operations. + pub async fn find_digital_twin_providers_with_model_id( + &self, + model_id: &str, + protocol: &str, + operations: &[String], + ) -> Result, tonic::Status> { + // Define the retry strategy. + let retry_strategy = ExponentialBackoff::from_millis(Self::BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(Self::MAX_RETRIES); + + let response: FindByModelIdResponse = Retry::spawn(retry_strategy.clone(), || async { + let mut client = + DigitalTwinRegistryClient::connect(self.digital_twin_registry_uri.to_string()) + .await + .map_err(|error| tonic::Status::internal(format!("{error}")))?; + + let request = + tonic::Request::new(FindByModelIdRequest { model_id: model_id.to_string() }); + + client.find_by_model_id(request).await + }) + .await? + .into_inner(); + + Ok(response + .entity_access_info_list + .iter() + .filter(|entity_access_info| { + entity_access_info.protocol == protocol + && is_subset(operations, &entity_access_info.operations) + }) + .cloned() + .collect()) + } + + /// Use the Digital Twin Registry service to find the endpoints for digital twin providers that support the specified instance id, protocol and operations. + /// + /// # Arguments + /// * `instance_id` - The matching instance id. + /// * `protocol` - The required protocol. + /// * `operations` - The required operations. + pub async fn find_digital_twin_providers_with_instance_id( + &self, + instance_id: &str, + protocol: &str, + operations: &[String], + ) -> Result, tonic::Status> { + // Define the retry strategy. + let retry_strategy = ExponentialBackoff::from_millis(Self::BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(Self::MAX_RETRIES); + + let response: FindByInstanceIdResponse = Retry::spawn(retry_strategy.clone(), || async { + let mut client = + DigitalTwinRegistryClient::connect(self.digital_twin_registry_uri.to_string()) + .await + .map_err(|error| tonic::Status::internal(format!("{error}")))?; + + let request = tonic::Request::new(FindByInstanceIdRequest { + instance_id: instance_id.to_string(), + }); + + client.find_by_instance_id(request).await + }) + .await? + .into_inner(); + + Ok(response + .entity_access_info_list + .iter() + .filter(|entity_access_info| { + entity_access_info.protocol == protocol + && is_subset(operations, &entity_access_info.operations) + }) + .cloned() + .collect()) + } + + /// Send an ask to the provider. + /// + /// # Arguments + /// * `client` - The client to use to send the ask. + /// * `respond_uri` - The respond uri. + /// * `ask_id` - The ask id. + /// * `targeted_payload` - The targeted payload. + pub async fn send_ask( + &self, + mut client: RequestClient, + respond_uri: &str, + ask_id: &str, + targeted_payload: &TargetedPayload, + ) -> Result<(), tonic::Status> { + // Serialize the targeted payload. + let targeted_payload_json = serde_json::to_string_pretty(&targeted_payload).unwrap(); + + let request = tonic::Request::new(AskRequest { + respond_uri: respond_uri.to_string(), + ask_id: ask_id.to_string(), + payload: targeted_payload_json.clone(), + }); + + // Send the ask. + let _ = client.ask(request).await.map_err(|error| { + tonic::Status::internal(format!("Unable to call ask, due to {error}")) + })?; + + Ok(()) + } + + /// Wait for the answer. + /// + /// # Arguments + /// * `ask_id` - The ask id. + /// * `rx` - The receiver for the asynchronous channel for AnswerRequest's. + pub async fn wait_for_answer( + &self, + ask_id: String, + rx: &mut broadcast::Receiver, + ) -> Result { + let mut answer_request: AnswerRequest = Default::default(); + let mut attempts_after_failure = 0; + const MAX_ATTEMPTS_AFTER_FAILURE: u8 = 10; + while attempts_after_failure < MAX_ATTEMPTS_AFTER_FAILURE { + match timeout(Duration::from_millis(Self::TIMEOUT_PERIOD_IN_MILLIS), rx.recv()).await { + Ok(Ok(request)) => { + if ask_id == request.ask_id { + // We have received the answer request that we are expecting. + answer_request = request; + break; + } else { + // Ignore this answer request, as it is not the one that we are expecting. + // Immediately try again. This was not a failure, so we do not increment attempts_after_failure or sleep. + continue; + } + } + Ok(Err(error_message)) => { + warn!("Failed to receive the answer request. The error message is '{}'. We may retry in a moment.", error_message); + sleep(Duration::from_secs(1)).await; + attempts_after_failure += 1; + continue; + } + Err(error_message) => { + warn!("Failed to receive the answer request. The error message is '{}'. We may retry in a moment.", error_message); + sleep(Duration::from_secs(1)).await; + attempts_after_failure += 1; + continue; + } + } + } + + Ok(answer_request) + } +} + +#[tonic::async_trait] +impl DigitalTwinGraph for DigitalTwinGraphImpl { + /// Find implementation. + /// + /// # Arguments + /// * `request` - Find request. + async fn find( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let find_request = request.into_inner(); + let model_id = find_request.model_id; + + if model_id.is_empty() { + return Err(tonic::Status::invalid_argument("Model id is required")); + } + + debug!("Received a find request for model id {model_id}"); + + // Retrieve the provider details. + let provider_entity_access_info_list = self + .find_digital_twin_providers_with_model_id( + model_id.as_str(), + digital_twin_protocol::GRPC, + &[digital_twin_operation::GET.to_string()], + ) + .await?; + + // Build a map of instance id to its associated endpoint infos. + let instance_provider_map: std::collections::HashMap> = + provider_entity_access_info_list + .iter() + .map(|provider_entity_access_info| { + ( + provider_entity_access_info.instance_id.clone(), + provider_entity_access_info.clone(), + ) + }) + .fold( + // fold is used to group the endpoint infos by instance id. + std::collections::HashMap::new(), + |mut accumulator, (instance_id, entity_access_info)| { + accumulator + .entry(instance_id) + .or_insert_with(Vec::new) + .push(entity_access_info); + accumulator + }, + ); + + let mut values = vec![]; + + for instance_id in instance_provider_map.keys() { + // We will only use the first provider. For a high availability scenario, we can try multiple providers. + let provider_entity_access_info = &instance_provider_map[instance_id][0]; + + let provider_uri = provider_entity_access_info.uri.clone(); + let instance_id = provider_entity_access_info.instance_id.clone(); + + let tx = self.tx.clone(); + let mut rx = tx.subscribe(); + + let client_result = RequestClient::connect(provider_uri.clone()).await; + if client_result.is_err() { + warn!("Unable to connect. We will skip this one."); + continue; + } + let client = client_result.unwrap(); + + // Note: The ask id must be a universally unique value. + let ask_id = Uuid::new_v4().to_string(); + + // Create the targeted payload. + let targeted_payload = TargetedPayload { + instance_id: instance_id.to_string(), + member_path: "".to_string(), // The get operation does not need a member_path for this case, as we want to get the entire entity. + operation: digital_twin_operation::GET.to_string(), + payload: "".to_string(), // The get operation does not require a payload. + }; + + // Send the ask. + self.send_ask(client, &self.respond_uri, &ask_id, &targeted_payload).await?; + + // Wait for the answer. + let answer_request = self.wait_for_answer(ask_id, &mut rx).await?; + + debug!( + "Received an answer request. The ask_id is '{}'. The payload is '{}'", + answer_request.ask_id, answer_request.payload + ); + + values.push(answer_request.payload); + } + + debug!("Completed the find request"); + + Ok(tonic::Response::new(FindResponse { values })) + } + + /// Get implementation. + /// + /// # Arguments + /// * `request` - Get request. + async fn get( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let get_request = request.into_inner(); + let instance_id = get_request.instance_id; + let member_path = get_request.member_path; + + if instance_id.is_empty() { + return Err(tonic::Status::invalid_argument("Model id is required")); + } + + // Note: The member path is optional. + + debug!("Received a get request for instance id {instance_id}"); + + // Retrieve the provider details. + let provider_endpoint_info_list = self + .find_digital_twin_providers_with_instance_id( + instance_id.as_str(), + digital_twin_protocol::GRPC, + &[digital_twin_operation::GET.to_string()], + ) + .await?; + + if provider_endpoint_info_list.is_empty() { + return Err(tonic::Status::not_found("No providers found")); + } + + // We will only use the first provider. + let provider_endpoint_info = &provider_endpoint_info_list[0]; + + let provider_uri = provider_endpoint_info.uri.clone(); + let instance_id = provider_endpoint_info.instance_id.clone(); + + let tx = self.tx.clone(); + let mut rx = tx.subscribe(); + + // Connect to the provider where we will send the ask to get the instance's value. + let client_result = RequestClient::connect(provider_uri.clone()).await; + if client_result.is_err() { + return Err(tonic::Status::internal("Unable to connect to the provider.")); + } + let client = client_result.unwrap(); + + // Note: The ask id must be a universally unique value. + let ask_id = Uuid::new_v4().to_string(); + + // Create the targeted payload. + let targeted_payload = TargetedPayload { + instance_id: instance_id.to_string(), + member_path: member_path.to_string(), + operation: digital_twin_operation::GET.to_string(), + payload: "".to_string(), // The get operation does not require a payload. + }; + + // Send the ask. + self.send_ask(client, &self.respond_uri, &ask_id, &targeted_payload).await?; + + // Wait for the answer. + let answer_request = self.wait_for_answer(ask_id, &mut rx).await?; + + debug!( + "Received an answer request. The ask_id is '{}'. The payload is '{}", + answer_request.ask_id, answer_request.payload + ); + + Ok(tonic::Response::new(GetResponse { value: answer_request.payload.clone() })) + } + + /// Set implementation. + /// + /// # Arguments + /// * `request` - Set request. + async fn set( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + warn!("Got a set request: {request:?}"); + + Err(tonic::Status::unimplemented("set has not been implemented")) + } + + /// Invoke implementation. + /// + /// # Arguments + /// * `request` - Invoke request. + async fn invoke( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let invoke_request = request.into_inner(); + let instance_id = invoke_request.instance_id; + let member_path = invoke_request.member_path; + let request_payload = invoke_request.request_payload; + + if instance_id.is_empty() { + return Err(tonic::Status::invalid_argument("Instance id is required")); + } + + if member_path.is_empty() { + return Err(tonic::Status::invalid_argument("Member path is required")); + } + + // Note: The request payload is optional. + + debug!("Received an invoke request for instance id {instance_id}"); + + // Retrieve the provider details. + let provider_endpoint_info_list = self + .find_digital_twin_providers_with_instance_id( + instance_id.as_str(), + digital_twin_protocol::GRPC, + &[digital_twin_operation::INVOKE.to_string()], + ) + .await?; + + if provider_endpoint_info_list.is_empty() { + return Err(tonic::Status::not_found("No providers found")); + } + + // We will only use the first provider. + let provider_endpoint_info = &provider_endpoint_info_list[0]; + + let provider_uri = provider_endpoint_info.uri.clone(); + let instance_id = provider_endpoint_info.instance_id.clone(); + + let tx = self.tx.clone(); + let mut rx = tx.subscribe(); + + let client_result = RequestClient::connect(provider_uri.clone()).await; + if client_result.is_err() { + return Err(tonic::Status::internal("Unable to connect to the provider.")); + } + let client = client_result.unwrap(); + + // Note: The ask id must be a universally unique value. + let ask_id = Uuid::new_v4().to_string(); + + // Create the targeted payload. + let targeted_payload = TargetedPayload { + instance_id: instance_id.to_string(), + member_path: member_path.to_string(), + operation: digital_twin_operation::INVOKE.to_string(), + payload: request_payload.to_string(), + }; + + // Send the ask. + self.send_ask(client, &self.respond_uri, &ask_id, &targeted_payload).await?; + + // Wait for the answer. + let answer_request = self.wait_for_answer(ask_id, &mut rx).await?; + + debug!( + "Received an answer request. The ask_id is '{}'. The payload is '{}", + answer_request.ask_id, answer_request.payload + ); + + Ok(tonic::Response::new(InvokeResponse { + response_payload: answer_request.payload.clone(), + })) + } +} diff --git a/core/module/digital_twin_graph/src/digital_twin_graph_module.rs b/core/module/digital_twin_graph/src/digital_twin_graph_module.rs new file mode 100644 index 00000000..90f1e5c8 --- /dev/null +++ b/core/module/digital_twin_graph/src/digital_twin_graph_module.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use common::grpc_module::GrpcModule; +use core_protobuf_data_access::async_rpc::v1::respond::respond_server::RespondServer; +use core_protobuf_data_access::module::digital_twin_graph::v1::digital_twin_graph_server::DigitalTwinGraphServer; +use std::sync::Arc; +use tokio::sync::broadcast; +use tonic::transport::server::RoutesBuilder; + +use crate::digital_twin_graph_config; +use crate::digital_twin_graph_impl::DigitalTwinGraphImpl; +use crate::respond_impl::RespondImpl; + +/// The capacity of the broadcast channel. +const BROADCAST_CHANNEL_CAPACITY: usize = 100; + +/// Digital Twin Graph Module. +#[derive(Clone, Debug)] +pub struct DigitalTwinGraphModule {} + +impl DigitalTwinGraphModule { + /// Creates a new instance of the DigitalTwinGraphModule. + pub async fn new() -> Result { + Ok(Self {}) + } +} + +impl GrpcModule for DigitalTwinGraphModule { + /// Adds the gRPC services for this module to the server builder. + /// + /// # Arguments + /// * `builder` - A tonic::RoutesBuilder that contains the grpc services to build. + fn add_grpc_services(&self, builder: &mut RoutesBuilder) { + // Load the config. + let settings = digital_twin_graph_config::load_settings(); + let base_authority = settings.base_authority; + + let invehicle_digital_twin_uri = format!("http://{base_authority}"); // Devskim: ignore DS137138 + let respond_uri = format!("http://{base_authority}"); // Devskim: ignore DS137138 + + let (tx, _rx) = broadcast::channel(BROADCAST_CHANNEL_CAPACITY); + let tx = Arc::new(tx); + + // Setup the respond service. + let respond_impl = RespondImpl::new(tx.clone()); + let respond_service = RespondServer::new(respond_impl); + + // Setup the digital twin graph service. + let digital_twin_graph_impl = + DigitalTwinGraphImpl::new(&invehicle_digital_twin_uri, &respond_uri, tx); + let digital_twin_graph_service = DigitalTwinGraphServer::new(digital_twin_graph_impl); + + builder.add_service(digital_twin_graph_service); + builder.add_service(respond_service); + } +} diff --git a/core/module/digital_twin_graph/src/lib.rs b/core/module/digital_twin_graph/src/lib.rs new file mode 100644 index 00000000..4b67f3ba --- /dev/null +++ b/core/module/digital_twin_graph/src/lib.rs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +pub mod digital_twin_graph_config; +pub mod digital_twin_graph_impl; +pub mod digital_twin_graph_module; +pub mod respond_impl; + +use serde_derive::{Deserialize, Serialize}; + +/// A targeted payload. +/// The targeting details helps on the receiver's side to dispatch the request. +#[derive(Serialize, Deserialize, Debug)] +pub struct TargetedPayload { + /// The instance id for the target entity. + pub instance_id: String, + /// The path within the target entity to the specific member that we are targeting. + /// It will be empty when we want to target the entire entity. + pub member_path: String, + /// The operation to be performed on the target entity's member. + pub operation: String, + /// The operation's payload. + /// It will be empty when the operation does not require a payload. + pub payload: String, +} + +/// Status codes and messages. +pub mod status { + pub mod ok { + pub const CODE: i32 = 200; + pub const MESSAGE: &str = "Ok"; + } +} + +/// Supported digital twin operations. +pub mod digital_twin_operation { + pub const GET: &str = "Get"; + pub const SET: &str = "Set"; + pub const SUBSCRIBE: &str = "Subscribe"; + pub const UNSUBSCRIBE: &str = "Unsubscribe"; + pub const INVOKE: &str = "Invoke"; + pub const STREAM: &str = "Stream"; + pub const MANAGEDSUBSCRIBE: &str = "ManagedSubscribe"; +} + +/// Supported digital twin protocols. +pub mod digital_twin_protocol { + pub const GRPC: &str = "grpc"; + pub const MQTT: &str = "mqtt"; +} diff --git a/core/module/digital_twin_graph/src/respond_impl.rs b/core/module/digital_twin_graph/src/respond_impl.rs new file mode 100644 index 00000000..d4e9494b --- /dev/null +++ b/core/module/digital_twin_graph/src/respond_impl.rs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use core_protobuf_data_access::async_rpc::v1::respond::respond_server::Respond; +use core_protobuf_data_access::async_rpc::v1::respond::{AnswerRequest, AnswerResponse}; +use log::debug; +use std::sync::Arc; +use tokio::sync::broadcast; + +#[derive(Debug)] +pub struct RespondImpl { + pub tx: Arc>, +} + +impl RespondImpl { + /// Create a new instance of a RespondImpl. + /// + /// # Arguments + /// * `tx` - The sender for the asynchronous channel for AnswerRequests. + pub fn new(tx: Arc>) -> RespondImpl { + RespondImpl { tx } + } +} + +#[tonic::async_trait] +impl Respond for RespondImpl { + /// Answer implementation. + /// + /// # Arguments + /// * `request` - The answer's request. + async fn answer( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + debug!("Received an answer request"); + + let tx = Arc::clone(&self.tx); + + // Send the request to the channel. + if let Err(err_msg) = tx.send(request.into_inner()) { + return Err(tonic::Status::internal(format!( + "Failed to send the answer request due to: {err_msg}" + ))); + } + + debug!("Completed the answer request."); + + Ok(tonic::Response::new(AnswerResponse {})) + } +} diff --git a/core/module/digital_twin_registry/Cargo.toml b/core/module/digital_twin_registry/Cargo.toml new file mode 100644 index 00000000..0c3ea93c --- /dev/null +++ b/core/module/digital_twin_registry/Cargo.toml @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[package] +name = "digital_twin_registry" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +common = { path = "../../common" } +core-protobuf-data-access = { path = "../../protobuf_data_access" } +iref = { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true , features = ["macros", "rt-multi-thread"] } +tonic = { workspace = true } +tower = { workspace = true } +yaml-rust = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/core/module/digital_twin_registry/src/digital_twin_registry_impl.rs b/core/module/digital_twin_registry/src/digital_twin_registry_impl.rs new file mode 100644 index 00000000..b8dd1960 --- /dev/null +++ b/core/module/digital_twin_registry/src/digital_twin_registry_impl.rs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +extern crate iref; + +use core_protobuf_data_access::module::digital_twin_registry::v1::digital_twin_registry_server::DigitalTwinRegistry; +use core_protobuf_data_access::module::digital_twin_registry::v1::{ + EntityAccessInfo, FindByInstanceIdRequest, FindByInstanceIdResponse, FindByModelIdRequest, + FindByModelIdResponse, RegisterRequest, RegisterResponse, +}; +use log::{debug, info}; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::collections::HashMap; +use std::sync::Arc; +use std::vec::Vec; +use tonic::{Request, Response, Status}; + +#[derive(Debug, Default)] +pub struct DigitalTwinRegistryImpl { + /// Entity access info map. + pub entity_access_info_map: Arc>>>, +} + +#[tonic::async_trait] +impl DigitalTwinRegistry for DigitalTwinRegistryImpl { + /// Find by model id implementation. + /// + /// # Arguments + /// * `request` - Find by model id request. + async fn find_by_model_id( + &self, + request: Request, + ) -> Result, Status> { + let model_id = request.into_inner().model_id; + + debug!("Received a find_by_model_id request for entity id {model_id}"); + + let entity_access_info_list; + + // This block controls the lifetime of the lock. + { + let lock: RwLockReadGuard>> = + self.entity_access_info_map.read(); + entity_access_info_list = lock.get(&model_id).cloned(); + } + + if entity_access_info_list.is_none() { + return Err(Status::not_found("Unable to find any entities with model id {model_id}")); + } + + let response = + FindByModelIdResponse { entity_access_info_list: entity_access_info_list.unwrap() }; + + debug!("Completed the find_by_model_id request."); + + Ok(Response::new(response)) + } + + /// Find by instance id implementation. + /// + /// # Arguments + /// * `request` - Find by instamce id request. + async fn find_by_instance_id( + &self, + request: Request, + ) -> Result, Status> { + let instance_id = request.into_inner().instance_id; + + debug!("Received a find_by_instance_id request for instance id {instance_id}"); + + let mut matching_entity_access_info_list = Vec::::new(); + + // This block controls the lifetime of the lock. + { + let lock: RwLockReadGuard>> = + self.entity_access_info_map.read(); + for entity_access_info_list in lock.values() { + for entity_access_info in entity_access_info_list { + if entity_access_info.instance_id == instance_id { + matching_entity_access_info_list.push(entity_access_info.clone()); + } + } + } + } + + if matching_entity_access_info_list.is_empty() { + return Err(Status::not_found( + "Unable to find any entities with instance id {instance_id}", + )); + } + + let response = + FindByInstanceIdResponse { entity_access_info_list: matching_entity_access_info_list }; + + debug!("Completed the find_by_instance_id request."); + + Ok(Response::new(response)) + } + + /// Register implementation. + /// + /// # Arguments + /// * `request` - Publish request. + async fn register( + &self, + request: Request, + ) -> Result, Status> { + let request_inner = request.into_inner(); + + for entity_access_info in &request_inner.entity_access_info_list { + self.register_entity(entity_access_info)?; + + info!( + "Registered the entity with provider id: {} instance id: {} model id: {}", + entity_access_info.provider_id, + entity_access_info.instance_id, + entity_access_info.model_id + ); + } + + let response = RegisterResponse {}; + + debug!("Completed the register request."); + + Ok(Response::new(response)) + } +} + +impl DigitalTwinRegistryImpl { + /// Register an entity. + /// + /// # Arguments + /// * `entity` - The entity. + fn register_entity(&self, entity_access_info: &EntityAccessInfo) -> Result<(), Status> { + if entity_access_info.provider_id.is_empty() { + return Err(Status::invalid_argument("Provider id is required")); + } + + if entity_access_info.model_id.is_empty() { + return Err(Status::invalid_argument("Model id is required")); + } + + if entity_access_info.instance_id.is_empty() { + return Err(Status::invalid_argument("Instance id is required")); + } + + if entity_access_info.protocol.is_empty() { + return Err(Status::invalid_argument("Protocol is required")); + } + + if entity_access_info.uri.is_empty() { + return Err(Status::invalid_argument("Uri is required")); + } + + if entity_access_info.operations.is_empty() { + return Err(Status::invalid_argument("Operations is required")); + } + + // This block controls the lifetime of the lock. + { + // Note: the context is optional. + + let mut lock: RwLockWriteGuard>> = + self.entity_access_info_map.write(); + let get_result = lock.get(&entity_access_info.model_id); + match get_result { + Some(_) => { + info!( + "Registered another entity access info for entity {}", + &entity_access_info.model_id + ); + lock.get_mut(&entity_access_info.model_id) + .unwrap() + .push(entity_access_info.clone()); + } + None => { + info!("Registered entity {}", &entity_access_info.model_id); + lock.insert( + entity_access_info.model_id.clone(), + vec![entity_access_info.clone()], + ); + } + }; + } + + Ok(()) + } +} + +#[cfg(test)] +mod digital_twin_registry_impl_tests { + use super::*; + + #[tokio::test] + async fn find_by_model_id_test() { + let operations = vec![String::from("Subscribe"), String::from("Unsubscribe")]; + + let entity_access_info = EntityAccessInfo { + provider_id: String::from("test-provider"), + instance_id: String::from("1234567890"), + model_id: String::from("dtmi:sdv:hvac:ambient_air_temperature;1"), + protocol: String::from("grpc"), + uri: String::from("http://[::1]:40010"), // Devskim: ignore DS137138 + context: String::from(""), + operations, + }; + + let entity_access_info_map = Arc::new(RwLock::new(HashMap::new())); + + let digital_twin_registry_impl = + DigitalTwinRegistryImpl { entity_access_info_map: entity_access_info_map.clone() }; + + // This block controls the lifetime of the lock. + { + let mut lock: RwLockWriteGuard>> = + entity_access_info_map.write(); + lock.insert(entity_access_info.model_id.clone(), vec![entity_access_info.clone()]); + } + + let request = tonic::Request::new(FindByModelIdRequest { + model_id: String::from("dtmi:sdv:hvac:ambient_air_temperature;1"), + }); + let result = digital_twin_registry_impl.find_by_model_id(request).await; + assert!(result.is_ok()); + let response = result.unwrap(); + let response_inner = response.into_inner(); + + assert!(response_inner.entity_access_info_list.len() == 1); + + let response_entity_access_info = response_inner.entity_access_info_list[0].clone(); + + assert_eq!(response_entity_access_info.model_id, "dtmi:sdv:hvac:ambient_air_temperature;1"); + assert_eq!( + response_entity_access_info.uri, + "http://[::1]:40010" // Devskim: ignore DS137138 + ); + } + + #[tokio::test] + async fn find_by_instance_id_test() { + let operations = vec![String::from("Subscribe"), String::from("Unsubscribe")]; + + let entity_access_info = EntityAccessInfo { + provider_id: String::from("test-provider"), + instance_id: String::from("1234567890"), + model_id: String::from("dtmi:sdv:hvac:ambient_air_temperature;1"), + protocol: String::from("grpc"), + uri: String::from("http://[::1]:40010"), // Devskim: ignore DS137138 + context: String::from(""), + operations, + }; + + let entity_access_info_map = Arc::new(RwLock::new(HashMap::new())); + + let digital_twin_registry_impl = + DigitalTwinRegistryImpl { entity_access_info_map: entity_access_info_map.clone() }; + + // This block controls the lifetime of the lock. + { + let mut lock: RwLockWriteGuard>> = + entity_access_info_map.write(); + lock.insert(entity_access_info.model_id.clone(), vec![entity_access_info.clone()]); + } + + let request = tonic::Request::new(FindByInstanceIdRequest { + instance_id: String::from("1234567890"), + }); + let result = digital_twin_registry_impl.find_by_instance_id(request).await; + assert!(result.is_ok()); + let response = result.unwrap(); + let response_inner = response.into_inner(); + + assert!(response_inner.entity_access_info_list.len() == 1); + + let response_entity_access_info = response_inner.entity_access_info_list[0].clone(); + + assert_eq!(response_entity_access_info.instance_id, "1234567890"); + assert_eq!( + response_entity_access_info.uri, + "http://[::1]:40010" // Devskim: ignore DS137138 + ); + } + + #[tokio::test] + async fn register_test() { + let entity_access_info = EntityAccessInfo { + provider_id: String::from("test-provider"), + instance_id: String::from("1234567890"), + model_id: String::from("dtmi:sdv:hvac:ambient_air_temperature;1"), + protocol: String::from("grpc"), + uri: String::from("http://[::1]:40010"), // Devskim: ignore DS137138 + context: String::from(""), + operations: vec![String::from("Subscribe"), String::from("Unsubscribe")], + }; + + let entity_access_info_map = Arc::new(RwLock::new(HashMap::new())); + + let digital_twin_registry_impl = + DigitalTwinRegistryImpl { entity_access_info_map: entity_access_info_map.clone() }; + + let request = tonic::Request::new(RegisterRequest { + entity_access_info_list: vec![entity_access_info], + }); + let result = digital_twin_registry_impl.register(request).await; + assert!(result.is_ok(), "register result is not okay: {result:?}"); + + // This block controls the lifetime of the lock. + { + let lock: RwLockReadGuard>> = + entity_access_info_map.read(); + // Make sure that we populated the entity map from the contents of the DTDL. + assert_eq!(lock.len(), 1, "expected length was 1, actual length is {}", lock.len()); + } + } +} diff --git a/core/module/digital_twin_registry/src/digital_twin_registry_module.rs b/core/module/digital_twin_registry/src/digital_twin_registry_module.rs new file mode 100644 index 00000000..d0ecacef --- /dev/null +++ b/core/module/digital_twin_registry/src/digital_twin_registry_module.rs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use common::grpc_module::GrpcModule; +use core_protobuf_data_access::module::digital_twin_registry::v1::digital_twin_registry_server::DigitalTwinRegistryServer; + +use tonic::transport::server::RoutesBuilder; + +use crate::digital_twin_registry_impl::DigitalTwinRegistryImpl; + +/// Digital Twin Registry Module. +#[derive(Clone, Debug)] +pub struct DigitalTwinRegistryModule {} + +impl DigitalTwinRegistryModule { + /// Creates a new instance of the DigitalTwinRegistryModule. + pub async fn new() -> Result { + Ok(Self {}) + } +} + +impl GrpcModule for DigitalTwinRegistryModule { + /// Adds the gRPC services for this module to the server builder. + /// + /// # Arguments + /// * `builder` - A tonic::RoutesBuilder that contains the grpc services to build. + fn add_grpc_services(&self, builder: &mut RoutesBuilder) { + // Create the gRPC services. + let digital_twin_registry_service = + DigitalTwinRegistryServer::new(DigitalTwinRegistryImpl::default()); + + builder.add_service(digital_twin_registry_service); + } +} diff --git a/core/module/digital_twin_registry/src/lib.rs b/core/module/digital_twin_registry/src/lib.rs new file mode 100644 index 00000000..e8b763f5 --- /dev/null +++ b/core/module/digital_twin_registry/src/lib.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +pub mod digital_twin_registry_impl; +pub mod digital_twin_registry_module; diff --git a/core/protobuf_data_access/build.rs b/core/protobuf_data_access/build.rs index fa1cbc65..ed0acb4a 100644 --- a/core/protobuf_data_access/build.rs +++ b/core/protobuf_data_access/build.rs @@ -18,6 +18,14 @@ fn main() -> Result<(), Box> { &["../../interfaces/module/managed_subscribe/v1/managed_subscribe.proto"], &["../../interfaces/module/managed_subscribe/v1/"], )?; + tonic_build::configure().compile( + &["../../interfaces/module/digital_twin_graph/v1/digital_twin_graph.proto"], + &["../../interfaces/module/digital_twin_graph/v1/"], + )?; + tonic_build::configure().compile( + &["../../interfaces/module/digital_twin_registry/v1/digital_twin_registry.proto"], + &["../../interfaces/module/digital_twin_registry/v1/"], + )?; tonic_build::configure().compile( &["../../external/chariott/service_discovery/proto/core/v1/service_registry.proto"], &["../../external/chariott/service_discovery/proto/core/v1/"], @@ -30,6 +38,14 @@ fn main() -> Result<(), Box> { &["../../external/agemo/proto/publisher/v1/publisher.proto"], &["../../external/agemo/proto/publisher/v1/"], )?; + tonic_build::configure().compile( + &["../../interfaces/async_rpc/v1/request.proto"], + &["../../interfaces/async_rpc/v1/"], + )?; + tonic_build::configure().compile( + &["../../interfaces/async_rpc/v1/respond.proto"], + &["../../interfaces/async_rpc/v1/"], + )?; Ok(()) } diff --git a/core/protobuf_data_access/src/lib.rs b/core/protobuf_data_access/src/lib.rs index f421f4d8..0b9e4def 100644 --- a/core/protobuf_data_access/src/lib.rs +++ b/core/protobuf_data_access/src/lib.rs @@ -8,12 +8,33 @@ pub mod invehicle_digital_twin { } } +pub mod async_rpc { + pub mod v1 { + pub mod respond { + tonic::include_proto!("async_rpc.v1.respond"); + } + pub mod request { + tonic::include_proto!("async_rpc.v1.request"); + } + } +} + pub mod module { pub mod managed_subscribe { pub mod v1 { tonic::include_proto!("managed_subscribe"); } } + pub mod digital_twin_graph { + pub mod v1 { + tonic::include_proto!("digital_twin_graph.v1.digital_twin_graph"); + } + } + pub mod digital_twin_registry { + pub mod v1 { + tonic::include_proto!("digital_twin_registry.v1.digital_twin_registry"); + } + } } pub mod chariott { diff --git a/digital-twin-model/dtdl/dtmi/sdv/cabin-1.json b/digital-twin-model/dtdl/dtmi/sdv/cabin-1.json index b0e223ce..d63475a7 100644 --- a/digital-twin-model/dtdl/dtmi/sdv/cabin-1.json +++ b/digital-twin-model/dtdl/dtmi/sdv/cabin-1.json @@ -6,22 +6,22 @@ "contents": [ { "@type": "Relationship", - "@id": "dtmi:sdv:cabin:has_infotainment;1", + "@id": "dtmi:sdv:cabin:infotainment;1", "target": "dtmi:sdv:infotainment;1", - "name": "has_infotainment", + "name": "infotainment", "maxMultiplicity": 1 }, { "@type": "Relationship", - "@id": "dtmi:sdv:cabin:has_hvac;1", + "@id": "dtmi:sdv:cabin:hvac;1", "target": "dtmi:sdv:hvac;1", - "name": "has_hvac", + "name": "hvac", "maxMultiplicity": 1 }, { "@type": "Relationship", - "@id": "dtmi:sdv:cabin:has_seat;1", - "name": "has_seat", + "@id": "dtmi:sdv:cabin:seat;1", + "name": "seat", "target": "dtmi:sdv:seat;1", "properties": [ { diff --git a/digital-twin-model/dtdl/dtmi/sdv/infotainment-1.json b/digital-twin-model/dtdl/dtmi/sdv/infotainment-1.json index e55ada8e..5153fc11 100644 --- a/digital-twin-model/dtdl/dtmi/sdv/infotainment-1.json +++ b/digital-twin-model/dtdl/dtmi/sdv/infotainment-1.json @@ -6,9 +6,9 @@ "contents": [ { "@type": "Relationship", - "@id": "dtmi:sdv:infotainment:has_hmi;1", + "@id": "dtmi:sdv:infotainment:hmi;1", "target": "dtmi:sdv:hmi;1", - "name": "has_hmi", + "name": "hmi", "maxMultiplicity": 1 } ] diff --git a/digital-twin-model/dtdl/dtmi/sdv/seat-1.json b/digital-twin-model/dtdl/dtmi/sdv/seat-1.json index ab973ecb..ac92f4c4 100644 --- a/digital-twin-model/dtdl/dtmi/sdv/seat-1.json +++ b/digital-twin-model/dtdl/dtmi/sdv/seat-1.json @@ -2,5 +2,14 @@ "@context": ["dtmi:dtdl:context;3"], "@type": "Interface", "@id": "dtmi:sdv:seat;1", - "description": "Seat Interface." + "description": "Seat Interface.", + "contents": [ + { + "@type": "Relationship", + "@id": "dtmi:sdv:seat:seat_massager;1", + "target": "dtmi:sdv:seatmassager;1", + "name": "seat_massager", + "maxMultiplicity": 1 + } + ] } diff --git a/digital-twin-model/dtdl/dtmi/sdv/seat_with_massager-1.json b/digital-twin-model/dtdl/dtmi/sdv/seat_with_massager-1.json deleted file mode 100644 index 37b9976e..00000000 --- a/digital-twin-model/dtdl/dtmi/sdv/seat_with_massager-1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "@context": ["dtmi:dtdl:context;3"], - "@type": "Interface", - "@id": "dtmi:sdv:seat_with_massager;1", - "description": "Seat with Massager Interface", - "extends": "dtmi:sdv:seat;1", - "contents": [ - { - "@type": "Relationship", - "@id": "dtmi:sdv:seat_with_massager:has_seat_massager;1", - "target": "dtmi:sdv:seatmassager;1", - "name": "has_seat_massager", - "maxMultiplicity": 1 - } - ] -} diff --git a/digital-twin-model/dtdl/dtmi/sdv/vehicle-1.json b/digital-twin-model/dtdl/dtmi/sdv/vehicle-1.json index 63237871..b97346c0 100644 --- a/digital-twin-model/dtdl/dtmi/sdv/vehicle-1.json +++ b/digital-twin-model/dtdl/dtmi/sdv/vehicle-1.json @@ -23,9 +23,9 @@ }, { "@type": "Relationship", - "@id": "dtmi:sdv:vehicle:has_cabin;1", + "@id": "dtmi:sdv:vehicle:cabin;1", "target": "dtmi:sdv:cabin;1", - "name": "has_cabin", + "name": "cabin", "maxMultiplicity": 1 } ] diff --git a/digital-twin-model/src/sdv_v1.rs b/digital-twin-model/src/sdv_v1.rs index 319d110b..69e0cc72 100644 --- a/digital-twin-model/src/sdv_v1.rs +++ b/digital-twin-model/src/sdv_v1.rs @@ -5,6 +5,8 @@ // This file contains the generated code for the Software Defined Vehicle (SDV) model. // This code is manually generated today, but in the future it should be automatically generated from the DTDL. +#![allow(non_camel_case_types)] + /// The context value for all JSON-LD generated by the code in this file. fn context() -> Vec { vec!["dtmi:dtdl:context;3".to_string(), "dtmi:sdv:context;1".to_string()] @@ -29,7 +31,7 @@ pub mod airbag_seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -39,7 +41,7 @@ pub mod airbag_seat_massager { ))] pub model_id: String, pub sequence_name: String, - pub sequence: crate::sdv_v1::airbag_seat_massager::massage_step::TYPE, + pub sequence: crate::sdv_v1::airbag_seat_massager::massage_step::SCHEMA_TYPE, } } @@ -51,7 +53,7 @@ pub mod airbag_seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -60,7 +62,7 @@ pub mod airbag_seat_massager { value = "crate::sdv_v1::airbag_seat_massager::store_sequence::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::airbag_seat_massager::status::TYPE, + pub status: crate::sdv_v1::airbag_seat_massager::status::SCHEMA_TYPE, } } } @@ -78,7 +80,7 @@ pub mod airbag_seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -87,7 +89,7 @@ pub mod airbag_seat_massager { value = "crate::sdv_v1::airbag_seat_massager::perform_step::request::ID.to_string()" ))] pub model_id: String, - pub step: crate::sdv_v1::airbag_seat_massager::massage_step::TYPE, + pub step: crate::sdv_v1::airbag_seat_massager::massage_step::SCHEMA_TYPE, } } @@ -99,7 +101,7 @@ pub mod airbag_seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -108,7 +110,7 @@ pub mod airbag_seat_massager { value = "crate::sdv_v1::airbag_seat_massager::perform_step::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::airbag_seat_massager::status::TYPE, + pub status: crate::sdv_v1::airbag_seat_massager::status::SCHEMA_TYPE, } } } @@ -119,7 +121,7 @@ pub mod airbag_seat_massager { pub const DESCRIPTION: &str = "An airbag adjustment."; #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct SCHEMA_TYPE { pub airbag_identifier: i32, pub inflation_level: i32, pub inflation_duration_in_seconds: i32, @@ -131,7 +133,8 @@ pub mod airbag_seat_massager { pub const NAME: &str = "massage_step"; pub const DESCRIPTION: &str = "The massage step."; - pub type TYPE = Vec; + pub type SCHEMA_TYPE = + Vec; } pub mod status { @@ -140,7 +143,7 @@ pub mod airbag_seat_massager { pub const DESCRIPTION: &str = "The status."; #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug, Default)] - pub struct TYPE { + pub struct SCHEMA_TYPE { pub code: i32, pub message: String, } @@ -150,11 +153,103 @@ pub mod airbag_seat_massager { pub mod basic_airbag_seat_massager { pub const ID: &str = "dtmi:sdv:basic_airbag_seat_massager;1"; pub const DESCRIPTION: &str = "Basic Airbag Seat Massager Interface."; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default( + value = "crate::sdv_v1::basic_airbag_seat_massager::ID.to_string()" + ))] + pub model_id: String, + pub sequence_names: crate::sdv_v1::seat_massager::sequence_names::SCHEMA_TYPE, + } } pub mod cabin { pub const ID: &str = "dtmi:sdv:cabin;1"; pub const DESCRIPTION: &str = "Cabin Interface."; + + pub mod infotainment { + pub const ID: &str = "dtmi:sdv:vehicle:infotainment;1"; + pub const NAME: &str = "infotainment"; + pub const DESCRIPTION: &str = "The infotainment unit."; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct RELATIONSHIP_TYPE { + #[serde(rename = "@id")] + pub instance_id: String, + } + } + + pub mod hvac { + pub const ID: &str = "dtmi:sdv:cabin:hvac;1"; + pub const NAME: &str = "hvac"; + pub const DESCRIPTION: &str = "The HVAC unit."; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct RELATIONSHIP_TYPE { + #[serde(rename = "@id")] + pub instance_id: String, + } + } + + pub mod seat { + pub const ID: &str = "dtmi:sdv:cabin:seat;1"; + pub const NAME: &str = "seat"; + pub const DESCRIPTION: &str = "The seats."; + + pub type SEAT_ROW_TYPE = i32; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug, PartialEq)] + pub enum SEAT_POSITION_TYPE { + #[derivative(Default)] + /// No value + none, + left, + center, + right, + } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct RELATIONSHIP_TYPE { + #[serde(rename = "@id")] + pub instance_id: String, + pub seat_row: SEAT_ROW_TYPE, + pub seat_position: SEAT_POSITION_TYPE, + } + } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::cabin::ID.to_string()"))] + pub model_id: String, + pub infotainment: Vec, + pub hvac: Vec, + pub seat: Vec, + } } #[allow(dead_code)] @@ -170,7 +265,7 @@ pub mod camera { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -181,6 +276,20 @@ pub mod camera { pub media_content: Vec, } } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::camera::ID.to_string()"))] + pub model_id: String, + } } pub mod hmi { @@ -200,7 +309,7 @@ pub mod hmi { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -221,7 +330,7 @@ pub mod hmi { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -230,18 +339,32 @@ pub mod hmi { value = "crate::sdv_v1::hmi::show_notification::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::hmi::status::TYPE, + pub status: crate::sdv_v1::hmi::status::SCHEMA_TYPE, } } } + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::hmi::ID.to_string()"))] + pub model_id: String, + } + pub mod status { pub const ID: &str = "dtmi:sdv:hmi:status;1"; pub const NAME: &str = "status"; pub const DESCRIPTION: &str = "The status."; #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug, Default)] - pub struct TYPE { + pub struct SCHEMA_TYPE { pub code: i32, pub message: String, } @@ -257,7 +380,7 @@ pub mod hvac { pub const NAME: &str = "ambient_air_temperature"; pub const DESCRIPTION: &str = "The immediate surroundings air temperature (in Fahrenheit)."; - pub type TYPE = i32; + pub type SCHEMA_TYPE = i32; } pub mod is_air_conditioning_active { @@ -265,7 +388,21 @@ pub mod hvac { pub const NAME: &str = "is_air_conditioning_active"; pub const DESCRIPTION: &str = "Is air conditioning active?"; - pub type TYPE = bool; + pub type SCHEMA_TYPE = bool; + } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::hvac::ID.to_string()"))] + pub model_id: String, } } @@ -278,18 +415,78 @@ pub mod obd { pub const NAME: &str = "hybrid_battery_remaining"; pub const DESCRIPTION: &str = "The remaining hybrid battery life."; - pub type TYPE = i32; + pub type SCHEMA_TYPE = i32; + } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::obd::ID.to_string()"))] + pub model_id: String, } } pub mod premium_airbag_seat_massager { pub const ID: &str = "dtmi:sdv:premium_airbag_seat_massager;1"; pub const DESCRIPTION: &str = "Premium Airbag Seat Massager Interface."; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default( + value = "crate::sdv_v1::premium_airbag_seat_massager::ID.to_string()" + ))] + pub model_id: String, + pub sequence_names: crate::sdv_v1::seat_massager::sequence_names::SCHEMA_TYPE, + } } pub mod seat { pub const ID: &str = "dtmi:sdv:seat;1"; pub const DESCRIPTION: &str = "Seat Interface."; + + pub mod seat_massager { + pub const ID: &str = "dtmi:sdv:seat:seat_massager;1"; + pub const NAME: &str = "seat_massager"; + pub const DESCRIPTION: &str = "The seat massager."; + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct RELATIONSHIP_TYPE { + #[serde(rename = "@id")] + pub instance_id: String, + } + } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::seat::ID.to_string()"))] + pub model_id: String, + pub seat_massager: Vec, + } } pub mod seat_massager { @@ -301,20 +498,7 @@ pub mod seat_massager { pub const NAME: &str = "sequence_names"; pub const DESCRIPTION: &str = "The name of each of the stored sequences."; - #[derive(derivative::Derivative)] - #[derivative(Default)] - #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { - #[serde(rename = "@context")] - #[derivative(Default(value = "crate::sdv_v1::context()"))] - pub context: Vec, - #[serde(rename = "@type")] - #[derivative(Default( - value = "crate::sdv_v1::seat_massager::sequence_names::ID.to_string()" - ))] - pub model_id: String, - pub sequence_names: Vec, - } + pub type SCHEMA_TYPE = Vec; } pub mod load_sequence { @@ -330,7 +514,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -351,7 +535,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -360,7 +544,7 @@ pub mod seat_massager { value = "crate::sdv_v1::seat_massager::load_sequence::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::seat_massager::status::TYPE, + pub status: crate::sdv_v1::seat_massager::status::SCHEMA_TYPE, } } } @@ -378,7 +562,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -398,7 +582,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -407,7 +591,7 @@ pub mod seat_massager { value = "crate::sdv_v1::seat_massager::pause::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::seat_massager::status::TYPE, + pub status: crate::sdv_v1::seat_massager::status::SCHEMA_TYPE, } } } @@ -425,7 +609,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -445,7 +629,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -454,7 +638,7 @@ pub mod seat_massager { value = "crate::sdv_v1::seat_massager::play::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::seat_massager::status::TYPE, + pub status: crate::sdv_v1::seat_massager::status::SCHEMA_TYPE, } } } @@ -472,7 +656,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -492,7 +676,7 @@ pub mod seat_massager { #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { + pub struct PAYLOAD_TYPE { #[serde(rename = "@context")] #[derivative(Default(value = "crate::sdv_v1::context()"))] pub context: Vec, @@ -501,7 +685,7 @@ pub mod seat_massager { value = "crate::sdv_v1::seat_massager::reset::response::ID.to_string()" ))] pub model_id: String, - pub status: crate::sdv_v1::seat_massager::status::TYPE, + pub status: crate::sdv_v1::seat_massager::status::SCHEMA_TYPE, } } } @@ -512,7 +696,7 @@ pub mod seat_massager { pub const DESCRIPTION: &str = "The status."; #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug, Default)] - pub struct TYPE { + pub struct SCHEMA_TYPE { pub code: i32, pub message: String, } @@ -520,7 +704,7 @@ pub mod seat_massager { } pub mod vehicle { - pub const ID: &str = "dtmi:sdv:vehcile;1"; + pub const ID: &str = "dtmi:sdv:vehicle;1"; pub const DESCRIPTION: &str = "Vehicle Interface."; pub mod vehicle_identification { @@ -533,22 +717,42 @@ pub mod vehicle { pub const NAME: &str = "vin"; pub const DESCRIPTION: &str = "Vehicle Identification Number."; - pub type TYPE = String; + pub type SCHEMA_TYPE = String; + } + + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug, Default)] + pub struct SCHEMA_TYPE { + pub vin: crate::sdv_v1::vehicle::vehicle_identification::vin::SCHEMA_TYPE, } + } + + pub mod cabin { + pub const ID: &str = "dtmi:sdv:vehicle:cabin;1"; + pub const NAME: &str = "cabin"; + pub const DESCRIPTION: &str = "The cabin."; #[derive(derivative::Derivative)] #[derivative(Default)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] - pub struct TYPE { - #[serde(rename = "@context")] - #[derivative(Default(value = "crate::sdv_v1::context()"))] - pub context: Vec, - #[serde(rename = "@type")] - #[derivative(Default( - value = "crate::sdv_v1::vehicle::vehicle_identification::ID.to_string()" - ))] - pub model_id: String, - pub vin: crate::sdv_v1::vehicle::vehicle_identification::vin::TYPE, + pub struct RELATIONSHIP_TYPE { + #[serde(rename = "@id")] + pub instance_id: String, } } + + #[derive(derivative::Derivative)] + #[derivative(Default)] + #[derive(serde_derive::Serialize, serde_derive::Deserialize, Debug)] + pub struct ENTITY_TYPE { + #[serde(rename = "@context")] + #[derivative(Default(value = "crate::sdv_v1::context()"))] + pub context: Vec, + #[serde(rename = "@id")] + pub instance_id: String, + #[serde(rename = "@type")] + #[derivative(Default(value = "crate::sdv_v1::vehicle::ID.to_string()"))] + pub model_id: String, + pub vehicle_identification: crate::sdv_v1::vehicle::vehicle_identification::SCHEMA_TYPE, + pub cabin: Vec, + } } diff --git a/docs/design/modules/digital_twin_graph/.accepted_words.txt b/docs/design/modules/digital_twin_graph/.accepted_words.txt new file mode 100644 index 00000000..97c74957 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/.accepted_words.txt @@ -0,0 +1,22 @@ +Agemo +App +async +com +dtdl +DTDL +DTMI +freyja +Freyja +github +ibeji +Ibeji +Ibeji's +IDs +IoT +Invehiclemd +li +opendigitaltwins +md +rpc's +svg +ul diff --git a/docs/design/modules/digital_twin_graph/README.md b/docs/design/modules/digital_twin_graph/README.md new file mode 100644 index 00000000..b3f647d0 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/README.md @@ -0,0 +1,122 @@ +# Design Specification for Digital Twin Graph Service + +- [Introduction](#introduction) +- [Architecture](#architecture) +- [Identifiers](#identifiers) +- [Provider Contract](#provider-contract) +- [Operations](#operations) + +## Introduction + +The initial Ibeji implementation provided the foundations for constructing and interacting with a digital twin on an edge device. These foundations are low-level abilities +and they do not necessarily provide a consumer with the best interaction experience. However, they can be used as building blocks to build facades that +provide a consumer with an abstraction that delivers a much better interaction experience. Ibeji may support multiple facades and the user can select the one +that they prefer to use. + +This design specifies a graph-based facade, which will be named the Digital Twin Graph Service. With this facade, the digital twin will be represented as a +graph of digital twin entities whose edges represent the relationships between those entities. Instance IDs will be used to refer to entities. + +Please note that Ibeji is only intended for use on an IoT edge device. It is not intended for use in the cloud. The data that it manages can be +transferred to the cloud, through components like [Eclipse Freyja](https://github.com/eclipse-ibeji/freyja). + +## Architecture + +Ibeji's Application Server, which we will refer to as "Digital Twin App Server", has a modular architecture that allows services to readily be added and removed. +It also has build-time feature switches for controlling which service should be available at run-time. Ibeji's initial service, the In-vehicle Digital Twin +service, was developed before the adoption of the modular architecture, so it cannot be readily removed. + +We will introduce a new service named "Digital Twin Graph" that will provide a facade for interactions with the In-vehicle Digital Twin Service and the +providers. Ideally, the consumer will not need to directly interact with provider endpoints. Instead, the consumer will interact with a graph structure that +represents the digital twin. + +Ibeji's In-vehicle Digital Twin Service needs some adjustments to support the Digital Twin Graph Service. We will introduce a modified form of the service under the name "Digital Twin Registry" and keep the existing functionality intact, for now, under the original In-vehicle Digital Twin Service. + +The Managed Subscriber Service is an optional service that provides integration with Agemo. The Managed Subscriber Service has been included in the component diagram for completeness' sake. + +![Component Diagram](diagrams/digital_twin_graph_component.svg) + +## Identifiers + +The Digital Twin Graph will use a variety of identifiers. We will discuss the purpose of each. + +The model ID is the identifier for a DTDL fragment. It is expressed as a [DTMI](https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v3/DTDL.v3.md#digital-twin-model-identifier). + +A digital twin may be decomposed into digital twin entities. Each digital twin entity is defined by a fragment of the digital twin's model (specified in DTDL). The instance ID is the identifier for a digital twin entity. The instance ID must be universally unique. + +The provider ID is the identifier for a Digital Twin Provider. The provider ID must be universally unique and it is up to the provider to ensure this. The provider id may be associated with multiple instance IDs. + +## Provider Contract + +The provider operations that will initially be supported by the digital twin graph are: Get, Set and Invoke. + +Providers that want to participate in the digital twin graph, will need to do the following: +
    +
  • Provide the async_rpc's Request interface with an ask operation that will use a targeted payload that has the following: +
      +
    • Get: +
        +
      • instance_id: set to the the target's instance ID +
      • member_path: is optional; if it is empty, then it means the entire entity; if it is not empty, then it targets a specific member +
      • operation: set to "Get"
      • +
      • payload: is not required
      • +
      +
    • +
    • Set: +
        +
      • instance_id: set to the the target's instance ID
      • +
      • member_path: is optional; if it is empty, then it means the entire entity; if it is not empty, then it targets a specific member
      • +
      • operation: set to "Set"
      • +
      • payload: the value
      • +
      +
    • Invoke: +
        +
      • instance_id: set to the the target's instance ID
      • +
      • member_path: the name of command to invoke
      • +
      • operation: set to "Invoke"
      • +
      • payload: the command's request payload
      • +
      +
    • +
    +
  • Return the result from a provider operation to the async_rpc's Response interface using with the answer operation that has a payload that has the following: +
      +
    • Get: The value of the target. +
    • +
    • Set: The payload is not required. +
    • +
    • Invoke: The command's response payload. +
    • + +
    +
+ +## Operations + +The Digital Twin Graph Service will support four operations: + +- Find +- Get +- Invoke +- Set (this operation will be implemented in a later phase) + +### Find + +The Digital Twin Graph's find operation allows you to retrieve all instance values that have a specific model id. + +![Find Sequence Diagram Diagram](diagrams/find_sequence.svg) + +### Get + +The Digital Twin's get operation allows you to retrieve an instance value. You can reduce the scope of the result by specifying a specific member path within the instance. + +![Get Sequence Diagram](diagrams/get_sequence.svg) + +### Set + +The Digital Twin's set operation allows you to modify an instance value. You can reduce the scope of the change by specifying a specific member path within the instance. + +![Get Sequence Diagram](diagrams/set_sequence.svg) + +### Invoke + +The Digital Twin's invoke operation allows you to call an instance's command. You can use the member path to specify which of the instance's command is to be performed. +![Invoke Sequence Diagram](diagrams/invoke_sequence.svg) diff --git a/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.puml b/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.puml new file mode 100644 index 00000000..1e3f16a5 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.puml @@ -0,0 +1,36 @@ +@startuml + +component "Digital Twin Consumer" { +} + +component "Digital Twin App Server" { + component "Digital Twin Graph Service" { + interface "Digital Twin Graph Interface" + interface "Respond Interface" + } + component "Digital Twin Registry Service" { + interface "Digital Twin Registry Interface" + } + component "Invehicle Digital Twin Service" { + interface "Invehicle Digital Twin Interface" + } + component "Managed Subscribe Service" { + interface "Managed Subscribe Interface" + } +} + +component "Digital Twin Provider" { + interface "Request Interface" +} + +"Digital Twin Provider" -up-> "Digital Twin Registry Interface" : Register + +"Digital Twin Consumer" -down-> "Digital Twin Graph Interface" : Find/Get/Set/Invoke + +"Digital Twin Graph Service" -left-> "Digital Twin Registry Interface": FindByModelId + +"Digital Twin Graph Service" -down-> "Request Interface": Ask + +"Digital Twin Provider" -up-> "Respond Interface": Answer + +@enduml diff --git a/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.svg b/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.svg new file mode 100644 index 00000000..80585b63 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/digital_twin_graph_component.svg @@ -0,0 +1,67 @@ +Digital Twin App ServerDigital Twin Graph ServiceDigital Twin Registry ServiceInvehicle Digital Twin ServiceManaged Subscribe ServiceDigital Twin ProviderDigital Twin ConsumerDigital Twin Graph InterfaceRespond InterfaceDigital Twin Registry InterfaceInvehicle Digital Twin InterfaceManaged Subscribe InterfaceRequest InterfaceRegisterFind/Get/Set/InvokeFindByModelIdAskAnswer \ No newline at end of file diff --git a/docs/design/modules/digital_twin_graph/diagrams/find_sequence.puml b/docs/design/modules/digital_twin_graph/diagrams/find_sequence.puml new file mode 100644 index 00000000..f3ca6b7f --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/find_sequence.puml @@ -0,0 +1,71 @@ +@startuml + +autonumber + +participant "Digital Twin Consumer" as CONSUMER +participant "Digital Twin Graph" as DIGITAL_TWIN_GRAPH +participant "Digital Twin Registry" as DIGITAL_TWIN_REGISTRY +participant "Digital Twin Provider" as PROVIDER + +CONSUMER -> DIGITAL_TWIN_GRAPH: Find(model_id: "dtmi:sdv:seat;1") - request + +DIGITAL_TWIN_GRAPH -> DIGITAL_TWIN_REGISTRY: FindByModeld(model_id: "dtmi:sdv:seat;1") - request + +DIGITAL_TWIN_GRAPH <- DIGITAL_TWIN_REGISTRY: FindByModelId - response +note left + list of EntityAcessInfo + + [ + { + provider_id: "vehicle-core" + model_id: "dtmi:sdv:seat;1" + instance_id: "front left seat" + protocol: "grpc" + operations: ["Get", "Invoke"] + uri: Digital Twin Provider's uri + }, + { + provider_id: "vehicle-core" + model_id: "dtmi:sdv:seat;1" + instance_id: "front right seat" + protocol: "grpc" + operations: ["Get", "Invoke"] + uri: Digital Twin Provider's uri + } + ] +end note + +loop Iterate over the results from the FindByModelId call + DIGITAL_TWIN_GRAPH -> PROVIDER: Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "1", payload: {instance_id: "front left seat", operation: "Get" }) + DIGITAL_TWIN_GRAPH <- PROVIDER: Answer(ask_id: "1", payload: instance value as JSON-LD string) +end + +CONSUMER <- DIGITAL_TWIN_GRAPH: Find - response +note left + list of instance values as JSON-LD string + + [ + { + "@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"] + "@id": "front left seat", + "@type": "dtmi:sdv:seat;1", + "seat_massager": [ + { + "@id": "front left seat massager" + } + ] + }, + { + "@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"] + "@id": "front right seat", + "@type": "dtmi:sdv:seat;1", + "seat_massager": [ + { + "@id": "front right seat massager" + } + ] + } + ] +end note + +@enduml diff --git a/docs/design/modules/digital_twin_graph/diagrams/find_sequence.svg b/docs/design/modules/digital_twin_graph/diagrams/find_sequence.svg new file mode 100644 index 00000000..62d3b75b --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/find_sequence.svg @@ -0,0 +1,84 @@ +Digital Twin ConsumerDigital Twin ConsumerDigital Twin GraphDigital Twin GraphDigital Twin RegistryDigital Twin RegistryDigital Twin ProviderDigital Twin Provider1Find(model_id: "dtmi:sdv:seat;1") - request2FindByModeld(model_id: "dtmi:sdv:seat;1") - request3FindByModelId - responselist of EntityAcessInfo[{provider_id: "vehicle-core"model_id: "dtmi:sdv:seat;1"instance_id: "front left seat"protocol: "grpc"operations: ["Get", "Invoke"]uri: Digital Twin Provider's uri},{provider_id: "vehicle-core"model_id: "dtmi:sdv:seat;1"instance_id: "front right seat"protocol: "grpc"operations: ["Get", "Invoke"]uri: Digital Twin Provider's uri}]loop[Iterate over the results from the FindByModelId call]4Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "1", payload: {instance_id: "front left seat", operation: "Get" })5Answer(ask_id: "1", payload: instance value as JSON-LD string)6Find - responselist of instance values as JSON-LD string[{"@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"]"@id": "front left seat","@type": "dtmi:sdv:seat;1","seat_massager": [{"@id": "front left seat massager"}]},{"@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"]"@id": "front right seat","@type": "dtmi:sdv:seat;1","seat_massager": [{"@id": "front right seat massager"}]}] \ No newline at end of file diff --git a/docs/design/modules/digital_twin_graph/diagrams/get_sequence.puml b/docs/design/modules/digital_twin_graph/diagrams/get_sequence.puml new file mode 100644 index 00000000..23dfd2b4 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/get_sequence.puml @@ -0,0 +1,48 @@ +@startuml + +autonumber + +participant "Digital Twin Consumer" as CONSUMER +participant "Digital Twin Graph" as DIGITAL_TWIN_GRAPH +participant "Digital Twin Registry" as DIGITAL_TWIN_REGISTRY +participant "Digital Twin Provider" as PROVIDER + +CONSUMER -> DIGITAL_TWIN_GRAPH: Get(instance_id: "the vehicle", member_path: "vehicle_identification") - request + +DIGITAL_TWIN_GRAPH -> DIGITAL_TWIN_REGISTRY: FindByInstanceId(instance_id: "the vehicle") - request +DIGITAL_TWIN_GRAPH <- DIGITAL_TWIN_REGISTRY: FindByInstanceId - response +note left + list of EntityAccessInfo + + [ + { + provider_id: "vehicle-core" + model_id : "dtmi:sdv:vehicle;1" + instance_id: "the vehicle" + protocol: "grpc" + operations: ["Get"] + uri: Digital Twin Provider's uri + } + ] +end note + +DIGITAL_TWIN_GRAPH -> PROVIDER: Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "3", payload: {instance_id: "the vehicle", operation: "Get", member_path: "vehicle_identification"}) +DIGITAL_TWIN_GRAPH <- PROVIDER: Answer(ask_id: "3", payload: instance value as JSON-LD string) + +CONSUMER <- DIGITAL_TWIN_GRAPH: Get - response +note left + instance value as JSON-LD string + + { + "@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"] + "@type": "dtmi:sdv:vehicle:vehicle_identification;1", + "vehicle_identification": [ + { + "vin": "00000000000000000" + } + ] + } + +end note + +@enduml diff --git a/docs/design/modules/digital_twin_graph/diagrams/get_sequence.svg b/docs/design/modules/digital_twin_graph/diagrams/get_sequence.svg new file mode 100644 index 00000000..2fa310cb --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/get_sequence.svg @@ -0,0 +1,61 @@ +Digital Twin ConsumerDigital Twin ConsumerDigital Twin GraphDigital Twin GraphDigital Twin RegistryDigital Twin RegistryDigital Twin ProviderDigital Twin Provider1Get(instance_id: "the vehicle", member_path: "vehicle_identification") - request2FindByInstanceId(instance_id: "the vehicle") - request3FindByInstanceId - responselist of EntityAccessInfo[{provider_id: "vehicle-core"model_id : "dtmi:sdv:vehicle;1"instance_id: "the vehicle"protocol: "grpc"operations: ["Get"]uri: Digital Twin Provider's uri}]4Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "3", payload: {instance_id: "the vehicle", operation: "Get", member_path: "vehicle_identification"})5Answer(ask_id: "3", payload: instance value as JSON-LD string)6Get - responseinstance value as JSON-LD string{"@context": [ "dtmi:dtdl:context;3", "dtmi:sdv:context;1"]"@type": "dtmi:sdv:vehicle:vehicle_identification;1","vehicle_identification": [{"vin": "00000000000000000"}]} \ No newline at end of file diff --git a/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.puml b/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.puml new file mode 100644 index 00000000..05033459 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.puml @@ -0,0 +1,38 @@ +@startuml + +autonumber + +participant "Digital Twin Consumer" as CONSUMER +participant "Digital Twin Graph" as DIGITAL_TWIN_GRAPH +participant "Digital Twin Registry" as DIGITAL_TWIN_REGISTRY +participant "Digital Twin Provider" as PROVIDER + +CONSUMER -> DIGITAL_TWIN_GRAPH: Invoke(instance_id: "front left seat massager", member_path: "perform_step", request_payload: request payload as JSON-LD string) - request + +DIGITAL_TWIN_GRAPH -> DIGITAL_TWIN_REGISTRY: FindByInstanceId(id: "front left seat massager") - request +DIGITAL_TWIN_GRAPH <- DIGITAL_TWIN_REGISTRY: FindByInstanceId - response +note left + list of EntityAccessInfo + + [ + { + provider_id: "vehicle-core" + model_id : "dtmi:sdv:premium_airbag_seat_massager;1" + instance_id: "front left seat massager" + protocol: "grpc" + operations: ["Get", "Invoke"] + uri: Digital Twin Provider's uri + } + ] + +end note + +DIGITAL_TWIN_GRAPH -> PROVIDER: Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "5", payload: {instance_id: "front left seat massager", operation: "Invoke", member_path: "perform_step", payload: the request payload as JSON-LD string}) +DIGITAL_TWIN_GRAPH <- PROVIDER: Answer(ask_id: "5", payload: response payload as JSON-LD string) + +CONSUMER <- DIGITAL_TWIN_GRAPH: Invoke - response +note left + response payload as JSON-LD string +end note + +@enduml diff --git a/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.svg b/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.svg new file mode 100644 index 00000000..bdfafda4 --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/invoke_sequence.svg @@ -0,0 +1,51 @@ +Digital Twin ConsumerDigital Twin ConsumerDigital Twin GraphDigital Twin GraphDigital Twin RegistryDigital Twin RegistryDigital Twin ProviderDigital Twin Provider1Invoke(instance_id: "front left seat massager", member_path: "perform_step", request_payload: request payload as JSON-LD string) - request2FindByInstanceId(id: "front left seat massager") - request3FindByInstanceId - responselist of EntityAccessInfo[{provider_id: "vehicle-core"model_id : "dtmi:sdv:premium_airbag_seat_massager;1"instance_id: "front left seat massager"protocol: "grpc"operations: ["Get", "Invoke"]uri: Digital Twin Provider's uri}]4Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "5", payload: {instance_id: "front left seat massager", operation: "Invoke", member_path: "perform_step", payload: the request payload as JSON-LD string})5Answer(ask_id: "5", payload: response payload as JSON-LD string)6Invoke - responseresponse payload as JSON-LD string \ No newline at end of file diff --git a/docs/design/modules/digital_twin_graph/diagrams/set_sequence.puml b/docs/design/modules/digital_twin_graph/diagrams/set_sequence.puml new file mode 100644 index 00000000..1629958d --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/set_sequence.puml @@ -0,0 +1,34 @@ +@startuml + +autonumber + +participant "Digital Twin Consumer" as CONSUMER +participant "Digital Twin Graph" as DIGITAL_TWIN_GRAPH +participant "Digital Twin Registry" as DIGITAL_TWIN_REGISTRY +participant "Digital Twin Provider" as PROVIDER + +CONSUMER -> DIGITAL_TWIN_GRAPH: Set(instance_id: "the vehicle", member_path: "vehicle_identification/vin", value: value as JSON-LD string) - request + +DIGITAL_TWIN_GRAPH -> DIGITAL_TWIN_REGISTRY: FindByInstanceId(instance_id: "the vehicle") - request +DIGITAL_TWIN_GRAPH <- DIGITAL_TWIN_REGISTRY: FindByInstanceId - response +note left + list of EntityAccessInfo + + [ + { + provider_id: "vehicle-core" + model_id : "dtmi:sdv:vehicle;1" + instance_id: "the vehicle" + protocol: "grpc" + operations: ["Set"] + uri: Digital Twin Provider's uri + } + ] +end note + +DIGITAL_TWIN_GRAPH -> PROVIDER: Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "4", payload: {instance_id: "the vehicle", operation: "Set", member_path: "vehicle_identification/vin", payload: the value as JSON-LD string}) +DIGITAL_TWIN_GRAPH <- PROVIDER: Answer(ask_id: "4", payload: "") + +CONSUMER <- DIGITAL_TWIN_GRAPH: Set - response + +@enduml diff --git a/docs/design/modules/digital_twin_graph/diagrams/set_sequence.svg b/docs/design/modules/digital_twin_graph/diagrams/set_sequence.svg new file mode 100644 index 00000000..16aafeaa --- /dev/null +++ b/docs/design/modules/digital_twin_graph/diagrams/set_sequence.svg @@ -0,0 +1,47 @@ +Digital Twin ConsumerDigital Twin ConsumerDigital Twin GraphDigital Twin GraphDigital Twin RegistryDigital Twin RegistryDigital Twin ProviderDigital Twin Provider1Set(instance_id: "the vehicle", member_path: "vehicle_identification/vin", value: value as JSON-LD string) - request2FindByInstanceId(instance_id: "the vehicle") - request3FindByInstanceId - responselist of EntityAccessInfo[{provider_id: "vehicle-core"model_id : "dtmi:sdv:vehicle;1"instance_id: "the vehicle"protocol: "grpc"operations: ["Set"]uri: Digital Twin Provider's uri}]4Ask(respond_uri: respond uri for Digital Twin Graph, ask_id: "4", payload: {instance_id: "the vehicle", operation: "Set", member_path: "vehicle_identification/vin", payload: the value as JSON-LD string})5Answer(ask_id: "4", payload: "")6Set - response \ No newline at end of file diff --git a/docs/samples/command/.accepted_words.txt b/docs/samples/command/.accepted_words.txt new file mode 100644 index 00000000..79caae2b --- /dev/null +++ b/docs/samples/command/.accepted_words.txt @@ -0,0 +1,8 @@ +br +cd +config +dir +invehicle +repo +uri +yaml diff --git a/docs/samples/command/README.md b/docs/samples/command/README.md new file mode 100644 index 00000000..0ed0b189 --- /dev/null +++ b/docs/samples/command/README.md @@ -0,0 +1,35 @@ +# Sample: Command + +The command sample demonstrates the use of a command. + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. +Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

+`cd {repo-root-dir}/target/debug`
+1. Create the three config files with the following contents, if they are not already there:

+---- consumer_settings.yaml ----
+`consumer_authority: "0.0.0.0:6010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- provider_settings.yaml ----
+`provider_authority: "0.0.0.0:4010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+1. In the top window, run:

+`./invehicle-digital-twin`
+1. In the middle window, run:

+`./command-provider`
+1. In the bottom window, run:

+`./command-consumer`
+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/docs/samples/digital_twin_graph/.accepted_words.txt b/docs/samples/digital_twin_graph/.accepted_words.txt new file mode 100644 index 00000000..e8da4eb2 --- /dev/null +++ b/docs/samples/digital_twin_graph/.accepted_words.txt @@ -0,0 +1,10 @@ +br +cd +config +dir +invehicle +massagers +repo +svg +uri +yaml diff --git a/docs/samples/digital_twin_graph/README.md b/docs/samples/digital_twin_graph/README.md new file mode 100644 index 00000000..335925be --- /dev/null +++ b/docs/samples/digital_twin_graph/README.md @@ -0,0 +1,46 @@ +# Sample: Digital Twin Graph + +The Digital Twin Graph sample demonstrates the use of the Digital Twin Graph Service. + +This sample has two providers. The vehicle-core provider handles the vehicle, the vehicle's cabin and the cabin's seats. +The seat-massager provider handles all of the seats' seat massagers. + +The graph representation for the vehicle in this sample is shown below. + +![Graph Diagram](diagrams/vehicle_graph.svg) + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using four windows: one running the In-Vehicle Digital Twin, two running the Digital Twin Providers and one running the Digital Twin Consumer. +Orientate the four windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle two windows can be used for the Digital Twin Providers. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.
+`cd {repo-root-dir}/target/debug`

+1. Create the four config files with the following contents, if they are not already there:
+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- digital_twin_graph_settings.yaml ----
+`base_authority: "0.0.0.0:5010"`

+---- consumer_settings.yaml ----
+`consumer_authority: "0.0.0.0:6010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+---- seat_massager_provider_settings.yaml ----
+`provider_authority: "0.0.0.0:4020"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+1. In the top window, run:
+`./invehicle-digital-twin`

+1. In the second window, run:
+`./graph-vehicle-core-provider`

+1. In the third window, run:
+`./graph-seat-massager-provider`

+1. In the bottom window, run:
+`./graph-consumer`

+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/docs/samples/digital_twin_graph/diagrams/vehicle_graph.puml b/docs/samples/digital_twin_graph/diagrams/vehicle_graph.puml new file mode 100644 index 00000000..86ced2e6 --- /dev/null +++ b/docs/samples/digital_twin_graph/diagrams/vehicle_graph.puml @@ -0,0 +1,78 @@ +@startuml + +object Vehicle { + model_id: "dtmi:sdv:vehicle;1" + instance_id: "550e8400-e29b-41d4-a716-446655440000" + vehicle_identification.vin: "00000000000000000" +} + +object Cabin { + model_id: "dtmi:sdv:cabin;1" + instance_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +} + +object "Seat" as front_left_seat { + model_id: "dtmi:sdv:seat;1" + instance_id: "7a9c5fe2-2c16-2f1f-b3c8-9a1b76b21f00" +} + +object "Seat" as front_right_seat { + model_id: "dtmi:sdv:seat;1" + instance_id: "8b3d6eg3-3c16-3f1f-c3c8-ba1c76c31f00" +} + +object "Seat" as back_left_seat { + model_id: "dtmi:sdv:seat;1" + instance_id: "9c4e7fh4-4c16-4f1f-d3c8-ca1d76d41f00" +} + +object "Seat" as back_center_seat { + model_id: "dtmi:sdv:seat;1" + instance_id: "ad5f8ig5-5c16-5f1f-e3c8-da1e76e51f00" +} + +object "Seat" as back_right_seat { + model_id: "dtmi:sdv:seat;1" + instance_id: "be6g9jh6-6c16-6f1f-f3c8-ea1f76f61f00" +} + +object "Seat Massager" as front_left_seat_massager { + model_id: "dtmi:sdv:premium_airbag_seat_massager;1" + instance_id: "front_left_airbag_seat_massager" +} + +object "Seat Massager" as front_right_seat_massager { + model_id: "dtmi:sdv:premium_airbag_seat_massager;1" + instance_id: "front_right_airbag_seat_massager" +} + +object "Seat Massager" as back_left_seat_massager { + model_id: "dtmi:sdv:basic_airbag_seat_massager;1" + instance_id: "back_left_airbag_seat_massager" +} + +object "Seat Massager" as back_center_seat_massager { + model_id: "dtmi:sdv:basic_airbag_seat_massager;1" + instance_id: "back_center_airbag_seat_massager" +} + +object "Seat Massager" as back_right_seat_massager { + model_id: "dtmi:sdv:basic_airbag_seat_massager;1" + instance_id: "back_right_airbag_seat_massager" +} + +Vehicle --|> Cabin + +Cabin --|> front_left_seat: seat_row = "1"; seat_position = "left" +Cabin --|> front_right_seat: seat_row = "1"; seat_position = "right" +Cabin --|> back_left_seat: seat_row = "2"; seat_position = "left" +Cabin --|> back_center_seat: seat_row = "2"; seat_position = "center" +Cabin --|> back_right_seat: seat_row = "2"; seat_position = "right" + +front_left_seat --|> front_left_seat_massager +front_right_seat --|> front_right_seat_massager +back_left_seat --|> back_left_seat_massager +back_center_seat --|> back_center_seat_massager +back_right_seat --|> back_right_seat_massager + +@enduml diff --git a/docs/samples/digital_twin_graph/diagrams/vehicle_graph.svg b/docs/samples/digital_twin_graph/diagrams/vehicle_graph.svg new file mode 100644 index 00000000..d14cdb03 --- /dev/null +++ b/docs/samples/digital_twin_graph/diagrams/vehicle_graph.svg @@ -0,0 +1,102 @@ +Vehiclemodel_id: "dtmi:sdv:vehicle;1"instance_id: "550e8400-e29b-41d4-a716-446655440000"vehicle_identification.vin: "00000000000000000"Cabinmodel_id: "dtmi:sdv:cabin;1"instance_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"Seatmodel_id: "dtmi:sdv:seat;1"instance_id: "7a9c5fe2-2c16-2f1f-b3c8-9a1b76b21f00"Seatmodel_id: "dtmi:sdv:seat;1"instance_id: "8b3d6eg3-3c16-3f1f-c3c8-ba1c76c31f00"Seatmodel_id: "dtmi:sdv:seat;1"instance_id: "9c4e7fh4-4c16-4f1f-d3c8-ca1d76d41f00"Seatmodel_id: "dtmi:sdv:seat;1"instance_id: "ad5f8ig5-5c16-5f1f-e3c8-da1e76e51f00"Seatmodel_id: "dtmi:sdv:seat;1"instance_id: "be6g9jh6-6c16-6f1f-f3c8-ea1f76f61f00"Seat Massagermodel_id: "dtmi:sdv:premium_airbag_seat_massager;1"instance_id: "front_left_airbag_seat_massager"Seat Massagermodel_id: "dtmi:sdv:premium_airbag_seat_massager;1"instance_id: "front_right_airbag_seat_massager"Seat Massagermodel_id: "dtmi:sdv:basic_airbag_seat_massager;1"instance_id: "back_left_airbag_seat_massager"Seat Massagermodel_id: "dtmi:sdv:basic_airbag_seat_massager;1"instance_id: "back_center_airbag_seat_massager"Seat Massagermodel_id: "dtmi:sdv:basic_airbag_seat_massager;1"instance_id: "back_right_airbag_seat_massager"seat_row = "1"; seat_position = "left"seat_row = "1"; seat_position = "right"seat_row = "2"; seat_position = "left"seat_row = "2"; seat_position = "center"seat_row = "2"; seat_position = "right" \ No newline at end of file diff --git a/docs/samples/mixed/.accepted_words.txt b/docs/samples/mixed/.accepted_words.txt new file mode 100644 index 00000000..79caae2b --- /dev/null +++ b/docs/samples/mixed/.accepted_words.txt @@ -0,0 +1,8 @@ +br +cd +config +dir +invehicle +repo +uri +yaml diff --git a/docs/samples/mixed/README.md b/docs/samples/mixed/README.md new file mode 100644 index 00000000..f0582452 --- /dev/null +++ b/docs/samples/mixed/README.md @@ -0,0 +1,35 @@ +# Sample: Mixed + +The mixed sample demonstrates the use of both properties and commands. + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. +Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

+`cd {repo-root-dir}/target/debug`
+1. Create the three config files with the following contents, if they are not already there:

+---- consumer_settings.yaml ----
+`consumer_authority: "0.0.0.0:6010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- provider_settings.yaml ----
+`provider_authority: "0.0.0.0:4010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+1. In the top window, run:

+`./invehicle-digital-twin`
+1. In the middle window, run:

+`./mixed-provider`
+1. In the bottom window, run:

+`./mixed-consumer`
+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/docs/samples/property/.accepted_words.txt b/docs/samples/property/.accepted_words.txt new file mode 100644 index 00000000..e4431803 --- /dev/null +++ b/docs/samples/property/.accepted_words.txt @@ -0,0 +1,9 @@ +br +cd +config +dir +invehicle +MQTT +repo +uri +yaml diff --git a/docs/samples/property/README.md b/docs/samples/property/README.md new file mode 100644 index 00000000..66ebeac8 --- /dev/null +++ b/docs/samples/property/README.md @@ -0,0 +1,36 @@ +# Sample: Property + +The property sample demonstrates the use of a property. + +This sample uses a MQTT Broker; please make sure that it is running. + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. +Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

+`cd {repo-root-dir}/target/debug`
+1. Create the three config files with the following contents, if they are not already there:

+---- consumer_settings.yaml ----
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- provider_settings.yaml ----
+`provider_authority: "0.0.0.0:1883"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+1. In the top window, run:

+`./invehicle-digital-twin`
+1. In the middle window, run:

+`./property-provider`
+1. In the bottom window, run:

+`./property-consumer`
+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/docs/samples/seat_massager/.accepted_words.txt b/docs/samples/seat_massager/.accepted_words.txt new file mode 100644 index 00000000..79caae2b --- /dev/null +++ b/docs/samples/seat_massager/.accepted_words.txt @@ -0,0 +1,8 @@ +br +cd +config +dir +invehicle +repo +uri +yaml diff --git a/docs/samples/seat_massager/README.md b/docs/samples/seat_massager/README.md new file mode 100644 index 00000000..5330217d --- /dev/null +++ b/docs/samples/seat_massager/README.md @@ -0,0 +1,35 @@ +# Sample: Seat Massager + +The seat massager sample demonstrates how a seat massager may be implemented. + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. +Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

+`cd {repo-root-dir}/target/debug`
+1. Create the three config files with the following contents, if they are not already there:

+---- consumer_settings.yaml ----
+`consumer_authority: "0.0.0.0:6010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- provider_settings.yaml ----
+`provider_authority: "0.0.0.0:4010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+1. In the top window, run:

+`./invehicle-digital-twin`
+1. In the middle window, run:

+`./seat-massager-provider`
+1. In the bottom window, run:

+`./seat-massager-consumer`
+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/docs/samples/streaming/.accepted_words.txt b/docs/samples/streaming/.accepted_words.txt new file mode 100644 index 00000000..d11b301e --- /dev/null +++ b/docs/samples/streaming/.accepted_words.txt @@ -0,0 +1,9 @@ +br +cd +chariott +config +dir +invehicle +repo +uri +yaml diff --git a/docs/samples/streaming/README.md b/docs/samples/streaming/README.md new file mode 100644 index 00000000..d469579f --- /dev/null +++ b/docs/samples/streaming/README.md @@ -0,0 +1,36 @@ +# Sample: Streaming + +The streaming sample demonstrates the streaming of a video stream. + +Follow these instructions to run the demo. + +Steps: + +1. The best way to run the demo is by using three windows: one running the In-Vehicle Digital Twin, one running the Digital Twin Provider and one running the Digital Twin Consumer. +Orientate the three windows so that they are lined up in a column. The top window can be used for the In-Vehicle Digital Twin. +The middle window can be used for the Digital Twin Provider. The bottom window can be used for the Digital Twin Consumer.
+1. In each window, change directory to the directory containing the build artifacts. +Make sure that you replace "{repo-root-dir}" with the repository root directory on the machine where you are running the demo.

+`cd {repo-root-dir}/target/debug`
+1. Create the three config files with the following contents, if they are not already there:

+---- streaming_consumer_settings.yaml ----
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+`number_of_images: 20`

+---- invehicle_digital_twin_settings.yaml ----
+`invehicle_digital_twin_authority: "0.0.0.0:5010"`

+---- streaming_provider_settings.yaml ----
+`provider_authority: "0.0.0.0:4010"`
+`invehicle_digital_twin_uri: "http://0.0.0.0:5010"`

+`image_directory: "<>/examples/applications/simulated-camera/images"` +1. In the top window, run:

+`./invehicle-digital-twin`
+1. In the middle window, run:

+`./streaming-provider`
+1. In the bottom window, run:

+`./streaming-consumer`
+1. Use control-c in each of the windows when you wish to stop the demo. + +A templated version of each config file can be found in: + +- {repo-root-dir}/core/invehicle-digital-twin/template +- {repo-root-dir}/samples/common/template diff --git a/samples/interfaces/async_rpc/v1/request.proto b/interfaces/async_rpc/v1/request.proto similarity index 100% rename from samples/interfaces/async_rpc/v1/request.proto rename to interfaces/async_rpc/v1/request.proto diff --git a/samples/interfaces/async_rpc/v1/respond.proto b/interfaces/async_rpc/v1/respond.proto similarity index 100% rename from samples/interfaces/async_rpc/v1/respond.proto rename to interfaces/async_rpc/v1/respond.proto diff --git a/interfaces/module/digital_twin_graph/v1/digital_twin_graph.proto b/interfaces/module/digital_twin_graph/v1/digital_twin_graph.proto new file mode 100644 index 00000000..642c7daf --- /dev/null +++ b/interfaces/module/digital_twin_graph/v1/digital_twin_graph.proto @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +syntax = "proto3"; + +package digital_twin_graph.v1.digital_twin_graph; + +service DigitalTwinGraph { + // Find the values of all instances that have the provided model id. + rpc Find (FindRequest) returns (FindResponse); + // Get an instance or an instance's member's value. + rpc Get (GetRequest) returns (GetResponse); + // Set an instance's or an instance memeber's value. + rpc Set (SetRequest) returns (SetResponse); + // Invoke an instance's command. + rpc Invoke (InvokeRequest) returns (InvokeResponse); +} + +message FindRequest { + // The model id. + string model_id = 1; +} + +message FindResponse { + // The JSON-LD string for each matching value. + repeated string values = 1; +} + +message GetRequest { + // The instance id. + string instance_id = 1; + // Scopes the request to a specific instance member located at the provided path. + // An empty string means the entire instance. + string member_path = 2; +} + +message GetResponse { + // The JSON-LD string for the retieved value. + string value = 1; +} + +message SetRequest { + // The instance id. + string instance_id = 1; + // Scopes the request to a specific instance member located at the provided path. + // An empty string means the entire instance. + string member_path = 2; + // The JSON-LD string for the value to be set. + string value = 3; +} + +message SetResponse { +} + +message InvokeRequest { + // The instance id. + string instance_id = 1; + // The instance's command that is to be invoked. + string member_path = 2; + // The JSON-LD string for the command's request payload. + string request_payload = 3; +} + +message InvokeResponse { + // The JSON-LD string for the command's response payload. + string response_payload = 1; +} \ No newline at end of file diff --git a/interfaces/module/digital_twin_registry/v1/digital_twin_registry.proto b/interfaces/module/digital_twin_registry/v1/digital_twin_registry.proto new file mode 100644 index 00000000..1b79c533 --- /dev/null +++ b/interfaces/module/digital_twin_registry/v1/digital_twin_registry.proto @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +syntax = "proto3"; + +package digital_twin_registry.v1.digital_twin_registry; + +service DigitalTwinRegistry { + // Find the access details that have the provided model id. + rpc FindByModelId (FindByModelIdRequest) returns (FindByModelIdResponse); + // Find the access details that have the provided instance id. + rpc FindByInstanceId (FindByInstanceIdRequest) returns (FindByInstanceIdResponse); + // Register access details. + rpc Register (RegisterRequest) returns (RegisterResponse); +} + +message EntityAccessInfo { + // The id of the provider that registered these access details. + string provider_id = 1; + // The provider id. + string instance_id = 2; + // The model id. + string model_id = 3; + // The protocol that should be used to access the instance. + string protocol = 4; + // The URI speific to the protocol that should be used to access the instance. + string uri = 5; + // Additional context specific tp the protocol that should be used to acess the instance. + // For example, with MQTT the URI will represent the address of the MQTT provider and the context will represent the topic name. + string context = 6; + // The names of the operations that are available at this endpoint. + repeated string operations = 7; +} + +message FindByModelIdRequest { + // The model id. + string model_id = 1; +} + +message FindByModelIdResponse { + // The matching entries. + repeated EntityAccessInfo entityAccessInfoList = 1; +} + +message FindByInstanceIdRequest { + // The instance id. + string instance_id = 1; +} + +message FindByInstanceIdResponse { + // The matching entries. + repeated EntityAccessInfo entityAccessInfoList = 1; +} + +message RegisterRequest { + // The entries to register. + repeated EntityAccessInfo entityAccessInfoList = 1; +} + +message RegisterResponse { +} diff --git a/samples/common/src/provider_config.rs b/samples/common/src/provider_config.rs index 109a5873..7de7b27c 100644 --- a/samples/common/src/provider_config.rs +++ b/samples/common/src/provider_config.rs @@ -6,7 +6,7 @@ use crate::utils; use serde_derive::Deserialize; -const CONFIG_FILENAME: &str = "provider_settings"; +const DEFAULT_CONFIG_FILENAME: &str = "provider_settings"; #[derive(Debug, Deserialize)] pub struct Settings { @@ -15,7 +15,14 @@ pub struct Settings { pub invehicle_digital_twin_uri: Option, } -/// Load the settings. +/// Load the settings using the default config filename. pub fn load_settings() -> Settings { - utils::load_settings(CONFIG_FILENAME).unwrap() + load_settings_with_config_filename(DEFAULT_CONFIG_FILENAME) +} + +// Load the settings using the specified config filename. +/// # Arguments +/// * `config_filename` - The name of the configuration file to load. +pub fn load_settings_with_config_filename(config_filename: &str) -> Settings { + utils::load_settings(config_filename).unwrap() } diff --git a/samples/common/template/digital_twin_graph_settings.yaml b/samples/common/template/digital_twin_graph_settings.yaml new file mode 100644 index 00000000..3904d059 --- /dev/null +++ b/samples/common/template/digital_twin_graph_settings.yaml @@ -0,0 +1,7 @@ +# +# Digital Twin Graph Settings +# + +# The IP address and port number that the Digital Twin Provider listens on for requests. +# Example: "0.0.0.0:80" +base_authority: <> diff --git a/samples/digital_twin_graph/Cargo.toml b/samples/digital_twin_graph/Cargo.toml new file mode 100644 index 00000000..c1b13245 --- /dev/null +++ b/samples/digital_twin_graph/Cargo.toml @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[package] +name = "samples-digital-twin-graph" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-std = { workspace = true, features = ["attributes"] } +digital_twin_graph = { path = "../../core/module/digital_twin_graph" } +digital_twin_registry = { path = "../../core/module/digital_twin_registry" } +digital-twin-model = { path = "../../digital-twin-model" } +env_logger= { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } +samples-common = { path = "../common" } +samples-protobuf-data-access = { path = "../protobuf_data_access" } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-retry = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } +uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] } + +[build-dependencies] +tonic-build = { workspace = true } + +[[bin]] +name = "graph-vehicle-core-provider" +path = "vehicle_core_provider/src/main.rs" + +[[bin]] +name = "graph-seat-massager-provider" +path = "seat_massager_provider/src/main.rs" + +[[bin]] +name = "graph-consumer" +path = "consumer/src/main.rs" diff --git a/samples/digital_twin_graph/consumer/src/main.rs b/samples/digital_twin_graph/consumer/src/main.rs new file mode 100644 index 00000000..e5a8e3e3 --- /dev/null +++ b/samples/digital_twin_graph/consumer/src/main.rs @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use digital_twin_model::sdv_v1 as sdv; +use env_logger::{Builder, Target}; +use log::{info, LevelFilter}; +use rand::rngs::StdRng; +use rand::Rng; // trait needed for gen_range +use rand::SeedableRng; // trait needed to initialize StdRng +use samples_common::consumer_config; +use samples_common::utils::retrieve_invehicle_digital_twin_uri; +use samples_protobuf_data_access::digital_twin_graph::v1::digital_twin_graph::digital_twin_graph_client::DigitalTwinGraphClient; +use samples_protobuf_data_access::digital_twin_graph::v1::digital_twin_graph::{FindRequest, FindResponse, GetRequest, GetResponse, InvokeRequest, InvokeResponse}; +use tokio_retry::Retry; +use tokio_retry::strategy::{ExponentialBackoff, jitter}; + +// The base duration in milliseconds for the exponential backoff strategy. +const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + +// The maximum number of retries for the exponential backoff strategy. +const MAX_RETRIES: usize = 100; + +/// Connect to the digital twin graph service. +/// +/// # Arguments +/// * `invehicle_digital_twin_uri` - The in-vehicle digital twin uri. +/// # Returns +/// The digital twin graph client. +async fn connect_to_digital_twin_graph_service( + invehicle_digital_twin_uri: String, +) -> Result, String> { + let retry_strategy = ExponentialBackoff::from_millis(BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(MAX_RETRIES); + + let client: DigitalTwinGraphClient = + Retry::spawn(retry_strategy.clone(), || async { + DigitalTwinGraphClient::connect(invehicle_digital_twin_uri.clone()).await.map_err( + |err_msg| { + format!("Unable to connect to the digital twin graph service due to: {err_msg}") + }, + ) + }) + .await?; + + Ok(client) +} + +/// Find all instances of a model. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `model_id` - The model id. +/// # Returns +/// The find response. +async fn find( + client: DigitalTwinGraphClient, + model_id: String, +) -> Result { + let retry_strategy = ExponentialBackoff::from_millis(BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(MAX_RETRIES); + + let request = FindRequest { model_id: model_id.clone() }; + + let find_vehicle_response = Retry::spawn(retry_strategy.clone(), || async { + let mut client = client.clone(); + client + .find(request.clone()) + .await + .map_err(|err_msg| format!("Unable to call find the instances due to: {err_msg}")) + }) + .await? + .into_inner(); + + Ok(find_vehicle_response) +} + +/// Get an instance. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `instance_id` - The instance id. +/// * `member_path` - The member path. +/// # Returns +/// The get response. +async fn get( + client: DigitalTwinGraphClient, + instance_id: String, + member_path: String, +) -> Result { + let retry_strategy = ExponentialBackoff::from_millis(BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(MAX_RETRIES); + + let request = GetRequest { instance_id: instance_id.clone(), member_path: member_path.clone() }; + + let get_response = Retry::spawn(retry_strategy.clone(), || async { + let mut client = client.clone(); + + client + .get(request.clone()) + .await + .map_err(|err_msg| format!("Unable to get the instance due to: {err_msg}")) + }) + .await? + .into_inner(); + + Ok(get_response) +} + +/// Invoke an instance's operation. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `instance_id` - The instance id. +/// * `member_path` - The member path. +/// * `request_payload` - The request payload. +/// # Returns +/// The invoke response. +async fn invoke( + client: DigitalTwinGraphClient, + instance_id: String, + member_path: String, + request_payload: String, +) -> Result { + let mut client = client.clone(); + + let request = InvokeRequest { + instance_id: instance_id.clone(), + member_path: member_path.clone(), + request_payload: request_payload.clone(), + }; + + let invoke_response = client + .invoke(request.clone()) + .await + .map_err(|err_msg| format!("Unable to invoke the instance's operation due to: {err_msg}"))? + .into_inner(); + + Ok(invoke_response) +} + +/// Find a vehicle instance. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// # Returns +/// The vehicle instance. +async fn find_vehicle( + client: DigitalTwinGraphClient, +) -> Result { + // Find all vehicle instances. + let find_vehicle_response: FindResponse = find(client, sdv::vehicle::ID.to_string()).await?; + if find_vehicle_response.values.is_empty() { + return Err("Unable to find vehicle instances".to_string()); + } + + // For now, we will just use the first vehicle instance. + let vehicle: sdv::vehicle::ENTITY_TYPE = + serde_json::from_str(&find_vehicle_response.values[0]).unwrap(); + + info!("The vehicle's instance id is: {}", vehicle.instance_id); + + Ok(vehicle) +} + +/// Find a cabin instance. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `vehicle` - The vehicle instance. +/// # Returns +/// The cabin instance. +async fn find_cabin( + client: DigitalTwinGraphClient, + vehicle: &sdv::vehicle::ENTITY_TYPE, +) -> Result { + // Get the cabin instance id. + if vehicle.cabin.is_empty() { + return Err("The vehicle does not have a cabin".to_string()); + } + + // A vehicle has at most one cabin instance. We will use the first cabin instance. + let cabin_instance_id = vehicle.cabin[0].instance_id.clone(); + + info!("The cabin's instance id is: {:?}", cabin_instance_id); + + // Get the cabin instance. + let get_cabin_response: GetResponse = + get(client.clone(), cabin_instance_id.clone(), "".to_string()).await?; + + // Deserialize the cabin instance. + let cabin: sdv::cabin::ENTITY_TYPE = serde_json::from_str(&get_cabin_response.value).unwrap(); + + Ok(cabin) +} + +/// Find a seat instance. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `cabin` - The cabin instance. +/// * `seat_row` - The seat row. +/// * `seat_posotion` - The seat position. +/// # Returns +/// The seat instance. +async fn find_seat( + client: DigitalTwinGraphClient, + cabin: &sdv::cabin::ENTITY_TYPE, + seat_row: i32, + seat_posotion: sdv::cabin::seat::SEAT_POSITION_TYPE, +) -> Result { + if cabin.seat.is_empty() { + return Err("The cabin does not have any seats".to_string()); + } + + // Find the specified seat instance. + for seat_relationship in cabin.seat.iter() { + if (seat_relationship.seat_row == seat_row) + && (seat_relationship.seat_position == seat_posotion) + { + // Get the seat instance. + let get_seat_response: GetResponse = + get(client.clone(), seat_relationship.instance_id.clone(), "".to_string()).await?; + + // Deserialize the seat instance. + let seat: sdv::seat::ENTITY_TYPE = + serde_json::from_str(&get_seat_response.value).unwrap(); + + info!("The seat's instance id is: {}", seat.instance_id); + + return Ok(seat); + } + } + + Err("The seat was not found".to_string()) +} + +/// Find a premium airbag seat massager instance. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `seat` - The seat instance. +/// # Returns +/// The premium airbag seat massager instance. +async fn find_premium_airbag_seat_massager( + client: DigitalTwinGraphClient, + seat: &sdv::seat::ENTITY_TYPE, +) -> Result { + if seat.seat_massager.is_empty() { + return Err("The seat does not have a seat massage".to_string()); + } + + // Get the seat massager instance id. + let seat_massager_instance_id = seat.seat_massager[0].instance_id.clone(); + + // Get the seat massager instance. + let get_seat_massager_response: GetResponse = + get(client.clone(), seat_massager_instance_id.clone(), "".to_string()).await?; + + // Deserialize the seat massager instance to a JSON object. + let seat_massager_json: serde_json::Value = + serde_json::from_str(&get_seat_massager_response.value).unwrap(); + + // Check that that the seat massager's modei_id (marked by @type) is the expected model (premium_airbag_seat_massager). + if seat_massager_json["@type"] != sdv::premium_airbag_seat_massager::ID { + return Err(format!( + "The seat massager instance is not of the expected model, instead it is a {0}", + seat_massager_json["@type"] + )); + } + + // Deserialize the seat massager instance. + let seat_massager: sdv::premium_airbag_seat_massager::ENTITY_TYPE = + serde_json::from_str(&get_seat_massager_response.value).unwrap(); + + info!("The seat massager's instance id is: {}", seat_massager.instance_id); + + Ok(seat_massager) +} + +/// Perform the perform_step operation. +/// +/// # Arguments +/// * `client` - The digital twin graph client. +/// * `seat_massager` - The premium airbag seat massager instance. +/// * `airbag_identifier` - The airbag identifier. +/// * `inflation_level` - The inflation level. +/// * `inflation_duration_in_seconds` - The inflation duration in seconds. +/// # Returns +/// An empty result if the operation is successful. +async fn perform_step( + client: DigitalTwinGraphClient, + seat_massager: sdv::premium_airbag_seat_massager::ENTITY_TYPE, + airbag_identifier: i32, + inflation_level: i32, + inflation_duration_in_seconds: i32, +) -> Result<(), String> { + // Generate the perform_step operation's request payload. + let request_payload: sdv::airbag_seat_massager::perform_step::request::PAYLOAD_TYPE = + sdv::airbag_seat_massager::perform_step::request::PAYLOAD_TYPE { + step: vec![sdv::airbag_seat_massager::airbag_adjustment::SCHEMA_TYPE { + airbag_identifier, + inflation_level, + inflation_duration_in_seconds, + }], + ..Default::default() + }; + + // Serialize the request payload to a JSON string. + let request_payload_json: String = serde_json::to_string_pretty(&request_payload).unwrap(); + + // Invoke the perform_step operation. + let perform_step_response: InvokeResponse = invoke( + client.clone(), + seat_massager.instance_id.clone(), + sdv::airbag_seat_massager::perform_step::NAME.to_string(), + request_payload_json.clone(), + ) + .await?; + + info!("The perform_step operation response is:\n{}", perform_step_response.response_payload); + + Ok(()) +} + +/// Perform a series of interactions with a vehicle digital twin. +/// +/// # Arguments +/// * `invehicle_digital_twin_uri` - The in-vehicle digital twin uri. +/// # Returns +/// An empty result if the interactions are successful. +async fn interact_with_digital_twin(invehicle_digital_twin_uri: String) -> Result<(), String> { + // Connect to the digital twin graph service. + let client: DigitalTwinGraphClient = + connect_to_digital_twin_graph_service(invehicle_digital_twin_uri.clone()).await?; + + // Find the vehicle instance. + let vehicle: sdv::vehicle::ENTITY_TYPE = find_vehicle(client.clone()).await.unwrap(); + + // Find the cabin instance. + let cabin: sdv::cabin::ENTITY_TYPE = find_cabin(client.clone(), &vehicle).await.unwrap(); + + // Find the front left seat instance. + let front_left_seat: sdv::seat::ENTITY_TYPE = + find_seat(client.clone(), &cabin, 1, sdv::cabin::seat::SEAT_POSITION_TYPE::left) + .await + .unwrap(); + + // Find the premium airbag seat massager instance. + let seat_massager: sdv::premium_airbag_seat_massager::ENTITY_TYPE = + find_premium_airbag_seat_massager(client.clone(), &front_left_seat).await.unwrap(); + + // Randomly generate the airbag adjustment field values. + let mut rng = StdRng::from_entropy(); + let airbag_identifier = rng.gen_range(1..=15); + let inflation_level = rng.gen_range(1..=10); + let inflation_duration_in_seconds = rng.gen_range(1..=5); + + // Perform the perform_step operation. + perform_step( + client.clone(), + seat_massager, + airbag_identifier, + inflation_level, + inflation_duration_in_seconds, + ) + .await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logging. + Builder::new().filter(None, LevelFilter::Info).target(Target::Stdout).init(); + + info!("The Consumer has started."); + + let settings = consumer_config::load_settings(); + + let invehicle_digital_twin_uri = retrieve_invehicle_digital_twin_uri( + settings.invehicle_digital_twin_uri, + settings.chariott_uri, + ) + .await?; + + interact_with_digital_twin(invehicle_digital_twin_uri).await?; + + info!("The Consumer has completed."); + + Ok(()) +} diff --git a/samples/digital_twin_graph/seat_massager_provider/src/main.rs b/samples/digital_twin_graph/seat_massager_provider/src/main.rs new file mode 100644 index 00000000..e3ea6347 --- /dev/null +++ b/samples/digital_twin_graph/seat_massager_provider/src/main.rs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod request_impl; + +use digital_twin_model::sdv_v1 as sdv; +use env_logger::{Builder, Target}; +use log::{info, LevelFilter}; +use parking_lot::Mutex; +use samples_common::constants::{digital_twin_operation, digital_twin_protocol}; +use samples_common::provider_config; +use samples_common::utils::retrieve_invehicle_digital_twin_uri; +use samples_protobuf_data_access::async_rpc::v1::request::request_server::RequestServer; +use samples_protobuf_data_access::digital_twin_registry::v1::digital_twin_registry::digital_twin_registry_client::DigitalTwinRegistryClient; +use samples_protobuf_data_access::digital_twin_registry::v1::digital_twin_registry::{ + EntityAccessInfo, RegisterRequest, RegisterResponse, +}; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tonic::{transport::Server, Status}; +use tokio_retry::Retry; +use tokio_retry::strategy::{ExponentialBackoff, jitter}; + +use crate::request_impl::{InstanceData, ProviderState, RequestImpl}; + +/// The base duration in milliseconds for the exponential backoff strategy. +const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + +/// The maximum number of retries. +const MAX_RETRIES: usize = 100; + +/// The provider ID. +const PROVIDER_ID: &str = "seat_massager_provider"; + +// The seat massager ids. +// Issue #120 have been created to move these to a config file shared by the providers. +const FRONT_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "front_left_airbag_seat_massager"; +const FRONT_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "front_right_airbag_seat_massager"; +const BACK_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_left_airbag_seat_massager"; +const BACK_CENTER_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_center_airbag_seat_massager"; +const BACK_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_right_airbag_seat_massager"; + +/// Add an entry to the instance map. +/// # Arguments +/// * `instance_map` - The instance map. +/// * `instance_id` - The instance id. +/// * `model_id` - The model id. +/// * `serialized_value` - The serialized value. +fn add_entry_to_instance_map( + instance_map: &mut HashMap, + instance_id: String, + model_id: String, + serialized_value: String, +) { + instance_map.insert(instance_id, InstanceData { model_id, serialized_value }); +} + +/// Create the provider's state. +fn create_provider_state() -> ProviderState { + let mut result: ProviderState = ProviderState { instance_map: HashMap::new() }; + + // Create the seat massagers. + + let front_left_airbag_seat_massager: sdv::premium_airbag_seat_massager::ENTITY_TYPE = + sdv::premium_airbag_seat_massager::ENTITY_TYPE { + instance_id: FRONT_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sequence_names: Vec::::new(), + ..Default::default() + }; + + let front_right_airbag_seat_massager: sdv::premium_airbag_seat_massager::ENTITY_TYPE = + sdv::premium_airbag_seat_massager::ENTITY_TYPE { + instance_id: FRONT_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sequence_names: Vec::::new(), + ..Default::default() + }; + + let back_left_airbag_seat_massager: sdv::basic_airbag_seat_massager::ENTITY_TYPE = + sdv::basic_airbag_seat_massager::ENTITY_TYPE { + instance_id: BACK_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sequence_names: Vec::::new(), + ..Default::default() + }; + + let back_center_airbag_seat_massager: sdv::basic_airbag_seat_massager::ENTITY_TYPE = + sdv::basic_airbag_seat_massager::ENTITY_TYPE { + instance_id: BACK_CENTER_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sequence_names: Vec::::new(), + ..Default::default() + }; + + let back_right_airbag_seat_massager: sdv::basic_airbag_seat_massager::ENTITY_TYPE = + sdv::basic_airbag_seat_massager::ENTITY_TYPE { + instance_id: BACK_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sequence_names: Vec::::new(), + ..Default::default() + }; + + // Build the instance map. + + add_entry_to_instance_map( + &mut result.instance_map, + FRONT_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sdv::premium_airbag_seat_massager::ID.to_string(), + serde_json::to_string(&front_left_airbag_seat_massager).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + FRONT_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sdv::premium_airbag_seat_massager::ID.to_string(), + serde_json::to_string(&front_right_airbag_seat_massager).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + BACK_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sdv::basic_airbag_seat_massager::ID.to_string(), + serde_json::to_string(&back_left_airbag_seat_massager).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + BACK_CENTER_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sdv::basic_airbag_seat_massager::ID.to_string(), + serde_json::to_string(&back_center_airbag_seat_massager).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + BACK_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + sdv::basic_airbag_seat_massager::ID.to_string(), + serde_json::to_string(&back_right_airbag_seat_massager).unwrap(), + ); + + result +} + +/// Register the seat massagers. +/// +/// # Arguments +/// * `provider_id` - The provider's ID. +/// * `invehicle_digital_twin_uri` - The In-Vehicle Digital Twin URI. +/// * `provider_uri` - The provider's URI. +/// * `provider_state` - The provider's state. +async fn register_seat_massagers( + provider_id: &str, + invehicle_digital_twin_uri: &str, + provider_uri: &str, + provider_state: Arc>, +) -> Result<(), Status> { + let mut entity_access_info_list: Vec = Vec::new(); + + provider_state.lock().instance_map.iter().for_each(|(instance_id, instance_data)| { + info!( + "Registering the instance with the instance id '{}' and the model id '{}'", + instance_id, instance_data.model_id + ); + + let entity_access_info = EntityAccessInfo { + provider_id: provider_id.to_string(), + instance_id: instance_id.to_string(), + model_id: instance_data.model_id.to_string(), + protocol: digital_twin_protocol::GRPC.to_string(), + operations: vec![ + digital_twin_operation::GET.to_string(), + digital_twin_operation::INVOKE.to_string(), + ], + uri: provider_uri.to_string(), + context: "".to_string(), + }; + + entity_access_info_list.push(entity_access_info); + }); + + let retry_strategy = ExponentialBackoff::from_millis(BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(MAX_RETRIES); + + let result: Result = Retry::spawn(retry_strategy.clone(), || async { + let mut client = DigitalTwinRegistryClient::connect(invehicle_digital_twin_uri.to_string()) + .await + .map_err(|e: tonic::transport::Error| Status::internal(e.to_string()))?; + + let request = tonic::Request::new(RegisterRequest { + entity_access_info_list: entity_access_info_list.clone(), + }); + + info!("Sending a register request to the In-Vehicle Digital Twin Service URI {invehicle_digital_twin_uri}"); + + let response: RegisterResponse = client + .register(request) + .await + .map_err(|e| Status::internal(e.to_string()))? + .into_inner(); + Ok(response) + }) + .await; + + match result { + Ok(_) => Ok(()), + Err(status) => Err(status), + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logging. + Builder::new().filter(None, LevelFilter::Info).target(Target::Stdout).init(); + + info!("The Seat Massager Provider has started."); + + let settings = + provider_config::load_settings_with_config_filename("seat_massager_provider_settings"); + + let provider_authority = settings.provider_authority; + + let invehicle_digital_twin_uri = retrieve_invehicle_digital_twin_uri( + settings.invehicle_digital_twin_uri, + settings.chariott_uri, + ) + .await?; + + // Construct the provider URI from the provider authority. + let provider_uri = format!("http://{provider_authority}"); // Devskim: ignore DS137138 + + // Setup the HTTP server. + let addr: SocketAddr = provider_authority.parse()?; + let state = Arc::new(Mutex::new(create_provider_state())); + let request_impl = RequestImpl { provider_state: state.clone() }; + let server_future = Server::builder().add_service(RequestServer::new(request_impl)).serve(addr); + info!("The HTTP server is listening on address '{provider_authority}'"); + + register_seat_massagers(PROVIDER_ID, &invehicle_digital_twin_uri, &provider_uri, state.clone()) + .await?; + + server_future.await?; + + Ok(()) +} diff --git a/samples/digital_twin_graph/seat_massager_provider/src/request_impl.rs b/samples/digital_twin_graph/seat_massager_provider/src/request_impl.rs new file mode 100644 index 00000000..d9195c59 --- /dev/null +++ b/samples/digital_twin_graph/seat_massager_provider/src/request_impl.rs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use digital_twin_graph::TargetedPayload; +use digital_twin_model::sdv_v1 as sdv; +use log::{info, warn}; +use parking_lot::{Mutex, MutexGuard}; +use samples_common::constants::digital_twin_operation; +use samples_protobuf_data_access::async_rpc::v1::request::{ + request_server::Request, AskRequest, AskResponse, NotifyRequest, NotifyResponse, +}; +use samples_protobuf_data_access::async_rpc::v1::respond::{ + respond_client::RespondClient, AnswerRequest, +}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio_retry::strategy::{jitter, ExponentialBackoff}; +use tokio_retry::Retry; + +/// Instance data. +#[derive(Clone, Debug, Default)] +pub struct InstanceData { + /// Model Id. + pub model_id: String, + /// Serialized value (using JSON-LD as a string). + pub serialized_value: String, +} + +/// The provider's state. +#[derive(Debug, Default)] +pub struct ProviderState { + /// Maps an instance id to its associated instance data. + pub instance_map: HashMap, +} + +#[derive(Debug, Default)] +pub struct RequestImpl { + /// Provider state. + pub provider_state: Arc>, +} + +/// The implementation for the Request interface, which is used to handle requests from the consumer. +impl RequestImpl { + /// The base duration for the exponential backoff strategy in milliseconds. + const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + + /// The maximum number of retries. + const MAX_RETRIES: usize = 100; + + /// Get implementation. + /// + /// # Arguments + /// * `respond_uri` - Respond URI. + /// * `ask_id` - Ask Id. + /// * `targeted_payload` - Targeted payload. + async fn get( + &self, + respond_uri: String, + ask_id: String, + targeted_payload: TargetedPayload, + ) -> Result, tonic::Status> { + if !targeted_payload.payload.is_empty() { + return Err(tonic::Status::invalid_argument( + "Unexpected payload, it should be empty".to_string(), + )); + } + + let provider_state: Arc> = self.provider_state.clone(); + + // Define a retry strategy. + let retry_strategy = ExponentialBackoff::from_millis(Self::BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(Self::MAX_RETRIES); + + // Asynchronously perform the get. + tokio::spawn(async move { + // Retrieve the instance's value (it will be represented as a JSON string). + let instance_value: String = { + let instance_data: InstanceData = { + let lock: MutexGuard = provider_state.lock(); + match lock.instance_map.get(&targeted_payload.instance_id) { + Some(instance_data) => instance_data.clone(), + None => { + return Err(format!( + "Instance not found for instance id '{}'", + targeted_payload.instance_id + )); + } + } + }; + + instance_data.serialized_value.clone() + }; + + // Send the answer to the consumer. + Retry::spawn(retry_strategy, || async { + // Connect to the consumer. + let mut client = RespondClient::connect(respond_uri.clone()) + .await + .map_err(|err_msg| format!("Unable to connect due to: {err_msg}"))?; + + // Prepare the answer request. + let answer_request = tonic::Request::new(AnswerRequest { + ask_id: ask_id.clone(), + payload: instance_value.clone(), + }); + + // Send the answer to the consumer. + client + .answer(answer_request) + .await + .map_err(|status| format!("Answer failed: {status:?}")) + }) + .await + }); + + Ok(tonic::Response::new(AskResponse {})) + } + + /// Invoke implementation. + /// + /// # Arguments + /// * `respond_uri` - Respond URI. + /// * `ask_id` - Ask ID. + /// * `targeted_payload` - Targeted payload. + #[allow(unused_assignments)] + async fn invoke( + &self, + respond_uri: String, + ask_id: String, + targeted_payload: TargetedPayload, + ) -> Result, tonic::Status> { + if targeted_payload.payload.is_empty() { + return Err(tonic::Status::invalid_argument( + "Unexpected payload, it should NOT be empty".to_string(), + )); + } + + let provider_state: Arc> = self.provider_state.clone(); + + // Define a retry strategy. + let retry_strategy = ExponentialBackoff::from_millis(Self::BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(Self::MAX_RETRIES); + + // Asynchronously perform the step. + tokio::spawn(async move { + // Retrieve the instance's value (it will be represented as a JSON string). + let instance_value: String = { + let instance_data: InstanceData = { + let lock: MutexGuard = provider_state.lock(); + match lock.instance_map.get(&targeted_payload.instance_id) { + Some(instance_data) => instance_data.clone(), + None => { + return Err(format!( + "Instance not found for instance id '{}'", + targeted_payload.instance_id + )); + } + } + }; + + instance_data.serialized_value.clone() + }; + + // Deserialize the instance value JSON. + let instance_value_json: serde_json::Value = + serde_json::from_str(&instance_value).unwrap(); + + let mut response_payload: String = String::new(); + + if (instance_value_json["@type"] == sdv::premium_airbag_seat_massager::ID + || instance_value_json["@type"] == sdv::basic_airbag_seat_massager::ID) + && targeted_payload.member_path == sdv::airbag_seat_massager::perform_step::NAME + { + response_payload = Self::perform_step(&targeted_payload)?; + } else { + return Err(format!( + "The instance with the instance id '{}' does not support the command '{}'", + targeted_payload.instance_id, targeted_payload.operation + )); + } + + // Send the answer to the consumer. + Retry::spawn(retry_strategy, || async { + // Connect to the consumer. + let mut client = RespondClient::connect(respond_uri.clone()) + .await + .map_err(|err_msg| format!("Unable to connect due to: {err_msg}"))?; + + // Prepare the answer request. + let answer_request = tonic::Request::new(AnswerRequest { + ask_id: ask_id.clone(), + payload: response_payload.clone(), + }); + + // Send the answer to the consumer. + client + .answer(answer_request) + .await + .map_err(|status| format!("Answer failed: {status:?}")) + }) + .await + }); + + Ok(tonic::Response::new(AskResponse {})) + } + + /// Perform step implementation. + /// # Arguments + /// * `targeted_payload` - Targeted payload. + fn perform_step(targeted_payload: &TargetedPayload) -> Result { + info!( + "Executed the operation {} on instance {}", + targeted_payload.member_path, targeted_payload.instance_id + ); + + let response = sdv::airbag_seat_massager::perform_step::response::PAYLOAD_TYPE { + status: sdv::airbag_seat_massager::status::SCHEMA_TYPE { + code: 200, + message: "The step was performed successfully".to_string(), + }, + ..Default::default() + }; + + serde_json::to_string(&response) + .map_err(|e| format!("Failed to serialize the response: {}", e)) + } +} + +#[tonic::async_trait] +impl Request for RequestImpl { + /// Ask implementation. + /// + /// # Arguments + /// * `request` - Ask request. + async fn ask( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let ask_request: AskRequest = request.into_inner(); + + info!("Received an ask request:"); + info!(" respond_uri: {}", ask_request.respond_uri); + info!(" ask_id: {}", ask_request.ask_id); + + // Deserialize the targeted payload. + let targeted_payload_json: TargetedPayload = + serde_json::from_str(&ask_request.payload).unwrap(); + + info!(" instance_id: {}", targeted_payload_json.instance_id); + info!(" member_path: {}", targeted_payload_json.member_path); + info!(" operation: {}", targeted_payload_json.operation); + + if targeted_payload_json.operation == digital_twin_operation::GET { + self.get(ask_request.respond_uri, ask_request.ask_id, targeted_payload_json).await + } else if targeted_payload_json.operation == digital_twin_operation::INVOKE { + self.invoke(ask_request.respond_uri, ask_request.ask_id, targeted_payload_json).await + } else { + Err(tonic::Status::invalid_argument(format!( + "Unexpected operation '{}'. Expected '{}' or '{}'.", + targeted_payload_json.operation, + digital_twin_operation::GET, + digital_twin_operation::INVOKE + ))) + } + } + + /// Notify implementation. + /// + /// # Arguments + /// * `request` - Notify request. + async fn notify( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + warn!("Got a notify request: {request:?}"); + + Err(tonic::Status::unimplemented("notify has not been implemented")) + } +} diff --git a/samples/digital_twin_graph/vehicle_core_provider/src/main.rs b/samples/digital_twin_graph/vehicle_core_provider/src/main.rs new file mode 100644 index 00000000..1ac65c3c --- /dev/null +++ b/samples/digital_twin_graph/vehicle_core_provider/src/main.rs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod request_impl; + +use digital_twin_model::sdv_v1 as sdv; +use env_logger::{Builder, Target}; +use log::{info, LevelFilter}; +use parking_lot::Mutex; +use samples_common::constants::{digital_twin_operation, digital_twin_protocol}; +use samples_common::provider_config; +use samples_common::utils::retrieve_invehicle_digital_twin_uri; +use samples_protobuf_data_access::async_rpc::v1::request::request_server::RequestServer; +use samples_protobuf_data_access::digital_twin_registry::v1::digital_twin_registry::digital_twin_registry_client::DigitalTwinRegistryClient; +use samples_protobuf_data_access::digital_twin_registry::v1::digital_twin_registry::{ + EntityAccessInfo, RegisterRequest, RegisterResponse, +}; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tonic::{transport::Server, Status}; +use tokio_retry::Retry; +use tokio_retry::strategy::{ExponentialBackoff, jitter}; + +use crate::request_impl::{InstanceData, ProviderState, RequestImpl}; + +/// The base duration in milliseconds for the exponential backoff strategy. +const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + +/// The maximum number of retries. +const MAX_RETRIES: usize = 100; + +/// The provider ID. +const PROVIDER_ID: &str = "vehicle_provider"; + +// The seat massager ids. +// Issue #120 have been created to move these to a config file shared by the providers. +const FRONT_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "front_left_airbag_seat_massager"; +const FRONT_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "front_right_airbag_seat_massager"; +const BACK_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_left_airbag_seat_massager"; +const BACK_CENTER_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_center_airbag_seat_massager"; +const BACK_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID: &str = "back_right_airbag_seat_massager"; + +/// Add an entry to the instance map. +/// # Arguments +/// * `instance_map` - The instance map. +/// * `instance_id` - The instance id. +/// * `model_id` - The model id. +/// * `serialized_value` - The serialized value. +fn add_entry_to_instance_map( + instance_map: &mut HashMap, + instance_id: String, + model_id: String, + serialized_value: String, +) { + instance_map.insert(instance_id, InstanceData { model_id, serialized_value }); +} + +/// Create the provider's state. +fn create_provider_state() -> ProviderState { + let mut result: ProviderState = ProviderState { instance_map: HashMap::new() }; + + // Create the seats. + + let front_left_seat_instance_id = format!("{}", uuid::Uuid::new_v4()); + let front_left_seat: sdv::seat::ENTITY_TYPE = sdv::seat::ENTITY_TYPE { + instance_id: front_left_seat_instance_id.clone(), + seat_massager: vec![sdv::seat::seat_massager::RELATIONSHIP_TYPE { + instance_id: FRONT_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + }], + ..Default::default() + }; + + let front_right_seat_instance_id = format!("{}", uuid::Uuid::new_v4()); + let front_right_seat: sdv::seat::ENTITY_TYPE = sdv::seat::ENTITY_TYPE { + instance_id: front_right_seat_instance_id.clone(), + seat_massager: vec![sdv::seat::seat_massager::RELATIONSHIP_TYPE { + instance_id: FRONT_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + }], + ..Default::default() + }; + + let back_left_seat_instance_id = format!("{}", uuid::Uuid::new_v4()); + let back_left_seat: sdv::seat::ENTITY_TYPE = sdv::seat::ENTITY_TYPE { + instance_id: back_left_seat_instance_id.clone(), + seat_massager: vec![sdv::seat::seat_massager::RELATIONSHIP_TYPE { + instance_id: BACK_LEFT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + }], + ..Default::default() + }; + + let back_center_seat_instance_id = format!("{}", uuid::Uuid::new_v4()); + let back_center_seat: sdv::seat::ENTITY_TYPE = sdv::seat::ENTITY_TYPE { + instance_id: back_center_seat_instance_id.clone(), + seat_massager: vec![sdv::seat::seat_massager::RELATIONSHIP_TYPE { + instance_id: BACK_CENTER_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + }], + ..Default::default() + }; + + let back_right_seat_instance_id = format!("{}", uuid::Uuid::new_v4()); + let back_right_seat: sdv::seat::ENTITY_TYPE = sdv::seat::ENTITY_TYPE { + instance_id: back_right_seat_instance_id.clone(), + seat_massager: vec![sdv::seat::seat_massager::RELATIONSHIP_TYPE { + instance_id: BACK_RIGHT_AIRBAG_SEAT_MASSAGER_INSTANCE_ID.to_string(), + }], + ..Default::default() + }; + + // Create the cabin. + let cabin_instance_id = format!("{}", uuid::Uuid::new_v4()); + let cabin_value: sdv::cabin::ENTITY_TYPE = sdv::cabin::ENTITY_TYPE { + instance_id: cabin_instance_id.clone(), + seat: vec![ + sdv::cabin::seat::RELATIONSHIP_TYPE { + instance_id: front_left_seat_instance_id.to_string(), + seat_row: 1, + seat_position: sdv::cabin::seat::SEAT_POSITION_TYPE::left, + }, + sdv::cabin::seat::RELATIONSHIP_TYPE { + instance_id: front_right_seat_instance_id.to_string(), + seat_row: 1, + seat_position: sdv::cabin::seat::SEAT_POSITION_TYPE::right, + }, + sdv::cabin::seat::RELATIONSHIP_TYPE { + instance_id: back_left_seat_instance_id.to_string(), + seat_row: 2, + seat_position: sdv::cabin::seat::SEAT_POSITION_TYPE::left, + }, + sdv::cabin::seat::RELATIONSHIP_TYPE { + instance_id: back_center_seat_instance_id.to_string(), + seat_row: 2, + seat_position: sdv::cabin::seat::SEAT_POSITION_TYPE::center, + }, + sdv::cabin::seat::RELATIONSHIP_TYPE { + instance_id: back_right_seat_instance_id.to_string(), + seat_row: 2, + seat_position: sdv::cabin::seat::SEAT_POSITION_TYPE::right, + }, + ], + ..Default::default() + }; + + // Create the vehicle. + let vehicle_instance_id = format!("{}", uuid::Uuid::new_v4()); + let vehicle_identification: sdv::vehicle::vehicle_identification::SCHEMA_TYPE = + sdv::vehicle::vehicle_identification::SCHEMA_TYPE { vin: "1M8GDM9AXKP042788".to_string() }; + let vehicle_value: sdv::vehicle::ENTITY_TYPE = sdv::vehicle::ENTITY_TYPE { + instance_id: vehicle_instance_id.clone(), + vehicle_identification, + cabin: vec![sdv::vehicle::cabin::RELATIONSHIP_TYPE { + instance_id: cabin_instance_id.clone(), + }], + ..Default::default() + }; + + // Build the instance map. + + add_entry_to_instance_map( + &mut result.instance_map, + front_left_seat_instance_id.clone(), + sdv::seat::ID.to_string(), + serde_json::to_string(&front_left_seat).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + front_right_seat_instance_id.clone(), + sdv::seat::ID.to_string(), + serde_json::to_string(&front_right_seat).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + back_left_seat_instance_id.clone(), + sdv::seat::ID.to_string(), + serde_json::to_string(&back_left_seat).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + back_center_seat_instance_id.clone(), + sdv::seat::ID.to_string(), + serde_json::to_string(&back_center_seat).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + back_right_seat_instance_id.clone(), + sdv::seat::ID.to_string(), + serde_json::to_string(&back_right_seat).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + cabin_instance_id, + sdv::cabin::ID.to_string(), + serde_json::to_string(&cabin_value).unwrap(), + ); + + add_entry_to_instance_map( + &mut result.instance_map, + vehicle_instance_id.clone(), + sdv::vehicle::ID.to_string(), + serde_json::to_string(&vehicle_value).unwrap(), + ); + + result +} + +/// Register the vehicle parts. +/// +/// # Arguments +/// * `provider_id` - The provider's ID. +/// * `invehicle_digital_twin_uri` - The In-Vehicle Digital Twin URI. +/// * `provider_uri` - The provider's URI. +/// * `provider_state` - The provider's state. +async fn register_vehicle_parts( + provider_id: &str, + invehicle_digital_twin_uri: &str, + provider_uri: &str, + provider_state: Arc>, +) -> Result<(), Status> { + let mut entity_access_info_list: Vec = Vec::new(); + + provider_state.lock().instance_map.iter().for_each(|(instance_id, instance_data)| { + info!( + "Registering the instance with the instance id '{}' and the model id '{}'", + instance_id, instance_data.model_id + ); + + let entity_access_info = EntityAccessInfo { + provider_id: provider_id.to_string(), + instance_id: instance_id.to_string(), + model_id: instance_data.model_id.to_string(), + protocol: digital_twin_protocol::GRPC.to_string(), + operations: vec![digital_twin_operation::GET.to_string()], + uri: provider_uri.to_string(), + context: "".to_string(), + }; + + entity_access_info_list.push(entity_access_info); + }); + + let retry_strategy = ExponentialBackoff::from_millis(BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(MAX_RETRIES); + + let result: Result = Retry::spawn(retry_strategy.clone(), || async { + let mut client = DigitalTwinRegistryClient::connect(invehicle_digital_twin_uri.to_string()) + .await + .map_err(|e: tonic::transport::Error| Status::internal(e.to_string()))?; + + let request = tonic::Request::new(RegisterRequest { + entity_access_info_list: entity_access_info_list.clone(), + }); + + info!("Sending a register request to the In-Vehicle Digital Twin Service URI {invehicle_digital_twin_uri}"); + + let response: RegisterResponse = client + .register(request) + .await + .map_err(|e| Status::internal(e.to_string()))? + .into_inner(); + Ok(response) + }) + .await; + + match result { + Ok(_) => Ok(()), + Err(status) => Err(status), + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logging. + Builder::new().filter(None, LevelFilter::Info).target(Target::Stdout).init(); + + info!("The Vehicle Provider has started."); + + let settings = provider_config::load_settings(); + + let provider_authority = settings.provider_authority; + + let invehicle_digital_twin_uri = retrieve_invehicle_digital_twin_uri( + settings.invehicle_digital_twin_uri, + settings.chariott_uri, + ) + .await?; + + // Construct the provider URI from the provider authority. + let provider_uri = format!("http://{provider_authority}"); // Devskim: ignore DS137138 + + // Setup the HTTP server. + let addr: SocketAddr = provider_authority.parse()?; + let provider_state = Arc::new(Mutex::new(create_provider_state())); + let request_impl = RequestImpl { provider_state: provider_state.clone() }; + let server_future = Server::builder().add_service(RequestServer::new(request_impl)).serve(addr); + info!("The HTTP server is listening on address '{provider_authority}'"); + + register_vehicle_parts( + PROVIDER_ID, + &invehicle_digital_twin_uri, + &provider_uri, + provider_state.clone(), + ) + .await?; + + server_future.await?; + + Ok(()) +} diff --git a/samples/digital_twin_graph/vehicle_core_provider/src/request_impl.rs b/samples/digital_twin_graph/vehicle_core_provider/src/request_impl.rs new file mode 100644 index 00000000..c56ba79f --- /dev/null +++ b/samples/digital_twin_graph/vehicle_core_provider/src/request_impl.rs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use digital_twin_graph::TargetedPayload; +use log::{info, warn}; +use parking_lot::{Mutex, MutexGuard}; +use samples_common::constants::digital_twin_operation; +use samples_protobuf_data_access::async_rpc::v1::request::{ + request_server::Request, AskRequest, AskResponse, NotifyRequest, NotifyResponse, +}; +use samples_protobuf_data_access::async_rpc::v1::respond::{ + respond_client::RespondClient, AnswerRequest, +}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio_retry::strategy::{jitter, ExponentialBackoff}; +use tokio_retry::Retry; + +/// Instance data. +#[derive(Clone, Debug, Default)] +pub struct InstanceData { + /// Model Id. + pub model_id: String, + /// Serialized value (using JSON-LD as a string). + pub serialized_value: String, +} + +/// Provider state. +#[derive(Debug, Default)] +pub struct ProviderState { + /// Maps an instance id to its associated instance data. + pub instance_map: HashMap, +} + +#[derive(Debug, Default)] +pub struct RequestImpl { + /// Provider state. + pub provider_state: Arc>, +} + +/// The implementation for the Request interface, which is used to handle requests from the consumer. +impl RequestImpl { + /// Backoff base duration in milliseconds. + const BACKOFF_BASE_DURATION_IN_MILLIS: u64 = 100; + + /// Maximum number of retries. + const MAX_RETRIES: usize = 100; + + /// Get implementation. + /// + /// # Arguments + /// * `respond_uri` - Respond URI. + /// * `ask_id` - Ask Id. + /// * `targeted_payload` - Targeted payload. + async fn get( + &self, + respond_uri: String, + ask_id: String, + targeted_payload: TargetedPayload, + ) -> Result, tonic::Status> { + if !targeted_payload.payload.is_empty() { + return Err(tonic::Status::invalid_argument( + "Unexpected payload, it should be empty".to_string(), + )); + } + + let provider_state: Arc> = self.provider_state.clone(); + + // Define a retry strategy. + let retry_strategy = ExponentialBackoff::from_millis(Self::BACKOFF_BASE_DURATION_IN_MILLIS) + .map(jitter) // add jitter to delays + .take(Self::MAX_RETRIES); + + // Asynchronously perform the get. + tokio::spawn(async move { + // Retrieve the instance's value (it will be represented as a JSON string). + let instance_value: String = { + let instance_data: InstanceData = { + let lock: MutexGuard = provider_state.lock(); + match lock.instance_map.get(&targeted_payload.instance_id) { + Some(instance_data) => instance_data.clone(), + None => { + return Err(format!( + "Instance not found for instance id '{}'", + targeted_payload.instance_id + )); + } + } + }; + + instance_data.serialized_value.clone() + }; + + // Send the answer to the consumer. + Retry::spawn(retry_strategy, || async { + // Connect to the consumer. + let mut client = RespondClient::connect(respond_uri.clone()) + .await + .map_err(|err_msg| format!("Unable to connect due to: {err_msg}"))?; + + // Prepare the answer request. + let answer_request = tonic::Request::new(AnswerRequest { + ask_id: ask_id.clone(), + payload: instance_value.clone(), + }); + + // Send the answer to the consumer. + client + .answer(answer_request) + .await + .map_err(|status| format!("Answer failed: {status:?}")) + }) + .await + }); + + Ok(tonic::Response::new(AskResponse {})) + } +} + +#[tonic::async_trait] +impl Request for RequestImpl { + /// Ask implementation. + /// + /// # Arguments + /// * `request` - Ask request. + async fn ask( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let ask_request = request.into_inner(); + + info!("Received an ask request:"); + info!(" respond_uri: {}", ask_request.respond_uri); + info!(" ask_id: {}", ask_request.ask_id); + + // Deserialize the targeted payload. + let targeted_payload_json: TargetedPayload = + serde_json::from_str(&ask_request.payload).unwrap(); + + info!(" instance_id: {}", targeted_payload_json.instance_id); + info!(" member_path: {}", targeted_payload_json.member_path); + info!(" operation: {}", targeted_payload_json.operation); + + if targeted_payload_json.operation == digital_twin_operation::GET { + self.get(ask_request.respond_uri, ask_request.ask_id, targeted_payload_json).await + } else { + Err(tonic::Status::invalid_argument(format!( + "Unexpected operation '{}'. Expected '{}'.", + targeted_payload_json.operation, + digital_twin_operation::GET + ))) + } + } + + /// Notify implementation. + /// + /// # Arguments + /// * `request` - Notify request. + async fn notify( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + warn!("Got a notify request: {request:?}"); + + Err(tonic::Status::unimplemented("notify has not been implemented")) + } +} diff --git a/samples/protobuf_data_access/build.rs b/samples/protobuf_data_access/build.rs index 6d5cafbe..d91a0ba9 100644 --- a/samples/protobuf_data_access/build.rs +++ b/samples/protobuf_data_access/build.rs @@ -5,8 +5,14 @@ fn main() -> Result<(), Box> { tonic_build::compile_protos("../interfaces/sample_grpc/v1/digital_twin_consumer.proto")?; tonic_build::compile_protos("../interfaces/sample_grpc/v1/digital_twin_provider.proto")?; - tonic_build::compile_protos("../interfaces/async_rpc/v1/respond.proto")?; - tonic_build::compile_protos("../interfaces/async_rpc/v1/request.proto")?; + tonic_build::configure().compile( + &["../../interfaces/async_rpc/v1/respond.proto"], + &["../../interfaces/async_rpc/v1/"], + )?; + tonic_build::configure().compile( + &["../../interfaces/async_rpc/v1/request.proto"], + &["../../interfaces/async_rpc/v1/"], + )?; tonic_build::configure() .message_attribute("EndpointInfo", "#[derive(serde::Deserialize, serde::Serialize)]") .message_attribute("EntityAccessInfo", "#[derive(serde::Deserialize, serde::Serialize)]") @@ -22,6 +28,14 @@ fn main() -> Result<(), Box> { &["../../interfaces/module/managed_subscribe/v1/managed_subscribe.proto"], &["../../interfaces/module/managed_subscribe/v1/"], )?; + tonic_build::configure().compile( + &["../../interfaces/module/digital_twin_graph/v1/digital_twin_graph.proto"], + &["../../interfaces/module/digital_twin_graph/v1/"], + )?; + tonic_build::configure().compile( + &["../../interfaces/module/digital_twin_registry/v1/digital_twin_registry.proto"], + &["../../interfaces/module/digital_twin_registry/v1/"], + )?; tonic_build::configure().compile( &["../../external/chariott/service_discovery/proto/core/v1/service_registry.proto"], &["../../external/chariott/service_discovery/proto/core/v1/"], diff --git a/samples/protobuf_data_access/src/lib.rs b/samples/protobuf_data_access/src/lib.rs index c61eb076..70487034 100644 --- a/samples/protobuf_data_access/src/lib.rs +++ b/samples/protobuf_data_access/src/lib.rs @@ -8,6 +8,22 @@ pub mod invehicle_digital_twin { } } +pub mod digital_twin_graph { + pub mod v1 { + pub mod digital_twin_graph { + tonic::include_proto!("digital_twin_graph.v1.digital_twin_graph"); + } + } +} + +pub mod digital_twin_registry { + pub mod v1 { + pub mod digital_twin_registry { + tonic::include_proto!("digital_twin_registry.v1.digital_twin_registry"); + } + } +} + pub mod module { pub mod managed_subscribe { pub mod v1 { diff --git a/samples/seat_massager/consumer/src/main.rs b/samples/seat_massager/consumer/src/main.rs index 132c9d06..e410cde7 100644 --- a/samples/seat_massager/consumer/src/main.rs +++ b/samples/seat_massager/consumer/src/main.rs @@ -61,9 +61,9 @@ fn start_seat_massage_steps( let inflation_level = rng.gen_range(1..=10); let inflation_duration_in_seconds = rng.gen_range(1..=5); - let request_payload: sdv::airbag_seat_massager::perform_step::request::TYPE = - sdv::airbag_seat_massager::perform_step::request::TYPE { - step: vec![sdv::airbag_seat_massager::airbag_adjustment::TYPE { + let request_payload: sdv::airbag_seat_massager::perform_step::request::PAYLOAD_TYPE = + sdv::airbag_seat_massager::perform_step::request::PAYLOAD_TYPE { + step: vec![sdv::airbag_seat_massager::airbag_adjustment::SCHEMA_TYPE { airbag_identifier, inflation_level, inflation_duration_in_seconds, diff --git a/samples/seat_massager/provider/src/main.rs b/samples/seat_massager/provider/src/main.rs index 2caf99f1..66937e0f 100644 --- a/samples/seat_massager/provider/src/main.rs +++ b/samples/seat_massager/provider/src/main.rs @@ -81,7 +81,7 @@ async fn main() -> Result<(), Box> { // Construct the provider URI from the provider authority. let provider_uri = format!("http://{provider_authority}"); // Devskim: ignore DS137138 - let instance_id = format!("pub_{}", uuid::Uuid::new_v4()); + let instance_id = format!("{}", uuid::Uuid::new_v4()); // Setup the HTTP server. let addr: SocketAddr = provider_authority.parse()?; diff --git a/samples/seat_massager/provider/src/request_impl.rs b/samples/seat_massager/provider/src/request_impl.rs index f918376e..d5102c7f 100644 --- a/samples/seat_massager/provider/src/request_impl.rs +++ b/samples/seat_massager/provider/src/request_impl.rs @@ -47,6 +47,7 @@ impl Request for RequestImpl { info!(" instance_id: {}", targeted_payload_json.instance_id); info!(" member_path: {}", targeted_payload_json.member_path); info!(" operation: {}", targeted_payload_json.operation); + info!(" payload: {}", targeted_payload_json.payload); // Deserialize the request payload. let request_payload_json: serde_json::Value = @@ -73,7 +74,7 @@ impl Request for RequestImpl { // Extract the request from the request payload. let perform_step_request_opt: Option< - sdv::airbag_seat_massager::perform_step::request::TYPE, + sdv::airbag_seat_massager::perform_step::request::PAYLOAD_TYPE, > = serde_json::from_value(request_payload_json) .expect("Failed to deserialize the request."); if perform_step_request_opt.is_none() { @@ -85,9 +86,9 @@ impl Request for RequestImpl { info!("Performing the step: {:?}", perform_step_request.step); // Prepare the perform_step response payload. - let response_payload: sdv::airbag_seat_massager::perform_step::response::TYPE = - sdv::airbag_seat_massager::perform_step::response::TYPE { - status: sdv::airbag_seat_massager::status::TYPE { + let response_payload: sdv::airbag_seat_massager::perform_step::response::PAYLOAD_TYPE = + sdv::airbag_seat_massager::perform_step::response::PAYLOAD_TYPE { + status: sdv::airbag_seat_massager::status::SCHEMA_TYPE { code: status::ok::CODE, message: status::ok::MESSAGE.to_string(), },