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:
-`./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:
-`./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:
-`./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:
-`./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:
-`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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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:
+`./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"`
+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 @@
+
\ 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:
+`./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:
+`./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:
+`./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: