From 6cb95ae6b58f7733a12c44d8af8dbf5dcf1786b1 Mon Sep 17 00:00:00 2001 From: jorchiu <122116059+jorchiu@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:54:30 -0800 Subject: [PATCH] Add Digital Twin Provider Tutorial (#68) --- Cargo.toml | 1 + README.md | 6 +- docs/tutorials/in_vehicle_model/README.md | 11 +- docs/tutorials/provider/README.md | 208 ++++++++++++++++++ .../tutorial/digital_twin_provider.proto | 37 ++++ samples/mixed/provider/src/main.rs | 5 + samples/protobuf_data_access/build.rs | 1 + samples/protobuf_data_access/src/lib.rs | 6 + samples/tutorial/Cargo.toml | 33 +++ samples/tutorial/consumer/src/main.rs | 180 +++++++++++++++ samples/tutorial/provider/src/main.rs | 126 +++++++++++ .../tutorial/provider/src/provider_impl.rs | 65 ++++++ 12 files changed, 673 insertions(+), 6 deletions(-) create mode 100644 docs/tutorials/provider/README.md create mode 100644 samples/interfaces/tutorial/digital_twin_provider.proto create mode 100644 samples/tutorial/Cargo.toml create mode 100644 samples/tutorial/consumer/src/main.rs create mode 100644 samples/tutorial/provider/src/main.rs create mode 100644 samples/tutorial/provider/src/provider_impl.rs diff --git a/Cargo.toml b/Cargo.toml index b857f818..85bd1bba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "samples/property", "samples/seat_massager", "samples/streaming", + "samples/tutorial" ] [workspace.dependencies] diff --git a/README.md b/README.md index 8d786559..14993689 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ through an extensible, open and dynamic architecture that provides access to the ## High-level Design -Ibeji's architecture has an In-Vehicle Digital Twin Service at its core. The In-Vehicle Digital Twin Service captures all of the vehicle's primary capabilities +Ibeji's architecture has an In-Vehicle Digital Twin Service at its core. The In-Vehicle Digital Twin Service captures all of the vehicle's hardware capabilities and makes them available to Ibeji consumers. Another component in Ibeji's architecture is the Provider. A vehicle may have one or more providers. -A provider exposes a subset of the vehicle's primary capabilities by registering them with the In-Vehicle Digital Twin Service. Once registered with the -In-Vehicle Digital Twin Service they can in turn be offered to Ibeji consumers. Each capability includes meta data that allow Ibeji consumers to comprehend +A provider exposes a subset of the vehicle's hardware capabilities by registering them with the In-Vehicle Digital Twin Service. Once registered with the +In-Vehicle Digital Twin Service they can in turn be offered to Ibeji consumers. Each capability includes metadata that allows Ibeji consumers to comprehend the nature of the capability, how to work with it and how it can be remotely accessed. ## Prerequisites diff --git a/docs/tutorials/in_vehicle_model/README.md b/docs/tutorials/in_vehicle_model/README.md index f2860dc5..6f8972c5 100644 --- a/docs/tutorials/in_vehicle_model/README.md +++ b/docs/tutorials/in_vehicle_model/README.md @@ -1,7 +1,7 @@ # Tutorial: Create an In-vehicle Model With DTDL - [Introduction](#introduction) -- [1. Create an In-vehicle Digital Twin Model With DTDL](#1-create-an-in-vehicle-digital-twin-model-with-dtdl) +- [1. Create an In-Vehicle Digital Twin Model with DTDL](#1-create-an-in-vehicle-digital-twin-model-with-dtdl) - [1.1 DTDL Interfaces](#11-dtdl-interfaces) - [1.2 Create HVAC and HMI Interfaces](#12-create-hvac-and-hmi-interfaces) - [1.3 DTDL DTMI ID](#13-dtdl-dtmi-id) @@ -9,6 +9,7 @@ - [1.5 DTDL Commands](#15-dtdl-commands) - [2. DTDL Validation](#2-dtdl-validation) - [3. Translating DTDL to Code](#3-translating-dtdl-to-code) +- [Next Steps](#next-steps) ## Introduction @@ -20,7 +21,7 @@ Please refer to the [DTDL v3](https://azure.github.io/opendigitaltwins-dtdl/DTDL >Note: DTDL is used to define a digital twin model of the in-vehicle's hardware. Currently in Ibeji, DTDL serves as a guide to manually construct the in-vehicle model in code. In the future, the in-vehicle model code should be generated from a DTDL file. -## 1. Create an In-vehicle Digital Twin Model With DTDL +## 1. Create an In-Vehicle Digital Twin Model with DTDL In this section, you will learn how to create an in-vehicle digital twin model with DTDL. @@ -123,7 +124,7 @@ To add properties to the HVAC digital twin model, replace the existing content o ] ``` -This introduces two signals to our HVAC system: ambient air temperature and air conditioning status. Both signals have `@id` values starting with `dtmi:sdv:HVAC`, signifying they belong to the sdv domain and HVAC interface. +This introduces two signals to our HVAC system: *ambient air temperature* and *is air conditioning active*. Both signals have `@id` values starting with `dtmi:sdv:HVAC`, signifying they belong to the sdv domain and HVAC interface. Please see [Property](https://azure.github.io/opendigitaltwins-dtdl/DTDL/v3/DTDL.v3#property) for more information on the property type and the descriptions of each field. Similar to the DTDL interface type, Ibeji mandates the description field. Despite DTDL v3 spec considering the `@id` field for properties as optional, Ibeji requires it. This helps in referring to your DTDL fragments in the code. @@ -243,3 +244,7 @@ Similarly, in the `hvac` module, there are two submodules: `ambient_air_temperat This Rust code is a way to use a DTDL model in a Rust program, with each DTDL element represented as a Rust module, constant, or type. You can translate a DTDL model into other programming languages. Use the `@id` fields in your in-vehicle digital twin model as guidance to code your in-vehicle model. Both Ibeji providers and Ibeji consumers can utilize this code. This code serves as a set of constants to standardize the values used in their communication with Ibeji, which ensures a consistent and reliable exchange of information. + +## Next Steps + +- Learn how to create a digital twin provider in [Tutorial: Create a Digital Twin Provider](../provider/README.md) diff --git a/docs/tutorials/provider/README.md b/docs/tutorials/provider/README.md new file mode 100644 index 00000000..611ef79a --- /dev/null +++ b/docs/tutorials/provider/README.md @@ -0,0 +1,208 @@ +# Tutorial: Create a Digital Twin Provider + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [1. Create an Ibeji Digital Twin Provider](#1-create-an-ibeji-digital-twin-provider) + - [1.1 Define Digital Twin Provider Interface](#11-define-digital-twin-provider-interface) + - [Sample Digital Twin Provider Interface](#sample-digital-twin-provider-interface) + - [1.2 Implement the Operations of a Digital Twin Provider Interface](#12-implement-the-operations-of-a-digital-twin-provider-interface) + - [Rust Sample Implementation of the Sample Interface](#rust-sample-implementation-of-the-sample-interface) +- [2. Register a Digital Twin Provider with the In-Vehicle Digital Twin Service](#2-register-digital-twin-provider-with-the-in-vehicle-digital-twin-service) + - [2.1 Rust Sample Registration of a Digital Twin Provider](#21-rust-sample-registration-of-a-digital-twin-provider) + - [Run the Sample Digital Twin Provider](#run-the-sample-digital-twin-provider) +- [3. Add Managed Subscribe to Digital Twin Provider](#3-add-managed-subscribe-to-digital-twin-provider) +- [Next Steps](#next-steps) + +## Introduction + +>[Digital Twin Provider:](../../../README.md#high-level-design) A provider exposes a subset of the vehicle's hardware capabilities by registering them with the In-Vehicle Digital Twin Service. Once registered with the In-Vehicle Digital Twin Service they can in turn be offered to Ibeji consumers. Each capability includes metadata that allows Ibeji consumers to comprehend the nature of the capability, how to work with it and how it can be remotely accessed. + +In this tutorial, you will leverage your in-vehicle model in code that you have created in [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md) to create a digital twin provider. Additionally, you will learn how to register your digital twin provider with the [In-Vehicle Digital Twin Service](../../design/README.md#in-vehicle-digital-twin-service). + +This tutorial will reference the sample code provided in Ibeji to keep the tutorial concise. Relevant code snippets are explicitly highlighted and discussed to ensure a clear understanding of the concepts. + +## Prerequisites + +- Complete the tutorial in [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md). +- Basic knowledge about [Protocol Buffers (protobufs) version 3.0](https://protobuf.dev/programming-guides/proto3/). +- Basic knowledge about the [gRPC protocol](https://grpc.io/docs/what-is-grpc/introduction/). + +## 1. Create an Ibeji Digital Twin Provider + +In this section, you will learn how to develop a digital twin provider that communicates with its digital twin consumers via [gRPC](https://grpc.io/docs/what-is-grpc/introduction/). It is important to note that digital twin providers in Ibeji are protocol-agnostic. This means they are not restricted to using gRPC and can employ other communication protocols. + +The `{repo-root-dir}/samples/tutorial` directory contains code for the sample digital twin provider used in this tutorial. The `{repo-root-dir}/digital-twin-model/src` contains the in-vehicle model in Rust code that you have constructed in [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md) along with additional signals not needed for this tutorial. + +Throughout this tutorial, the sample contents in the `{repo-root-dir}/samples/tutorial` directory are referenced to guide you through the process of creating a digital twin provider. + +### 1.1 Define Digital Twin Provider Interface + +A digital twin provider needs an interface. The interface will expose operations that allow digital twin consumers to access the in-vehicle signals that your digital provider makes available. + +>Tip: A suggested approach for defining your digital twin provider is to adopt the perspective of a digital twin consumer. This requires consideration of the operations and their corresponding names for interacting with each in-vehicle signal and command. For example, for the [digital twin provider sample interface](../../../samples/interfaces/tutorial/digital_twin_provider.proto), the specified operations are `Get` and `Invoke`. + +The [digital twin provider tutorial interface](../../../samples/interfaces/tutorial/digital_twin_provider.proto) serves as an example of what a digital twin provider's interface could look like. Feel free to replicate these operation names, modify them, or even add new ones as per your requirements. These operations are non-prescriptive. It is up to the developers of the in-vehicle digital twin to come up with their own convention for the operations. + +#### Sample Digital Twin Provider Interface + +This section provides an example of a digital twin provider interface. To reiterate, you are free to use this interface as a starting point or you may come up with your own convention. + +1. Consider the in-vehicle signals *ambient air temperature* and *is air conditioning active*, as well as the command *show notification* that you defined in the [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md). + +1. Reference the [sample digital twin provider tutorial interface](../../../samples/interfaces/tutorial/digital_twin_provider.proto): + +In this digital twin provider sample interface, the conventions that this interface enforces are as follows: + +- A digital twin consumer should utilize the `Get` operation to synchronously consume the *ambient air temperature* and the *is air conditioning active* in-vehicle signals. + +- A digital twin consumer should utilize the `Invoke` operation to send a *show notification* command. + +When introducing additional signals and commands, it is crucial to carefully select the operation(s) that best align with the behavior of each signal or command. This ensures a seamless integration and optimal performance of your system. + +### 1.2 Implement the Operations of a Digital Twin Provider Interface + +You have defined your digital twin provider interface. + +The following lists out the flow for implementing the operations of a digital twin interface in the programming language of your choice: + +1. Choose Your Programming Language: Since operations can be defined in a protobuf file, you can select any programming language that supports protobufs. This includes languages like Rust, Python, Java, C++, Go, etc. However, operations do not need to be defined in a protobuf file to be programming language agnostic. For instance, if you have a subscribe operation you may want to use [MQTT](https://mqtt.org/) for publishing to digital twin consumers that have subscribed to your digital twin provider. Please see the [Managed Subscribe Sample](https://github.com/eclipse-ibeji/ibeji/tree/main/samples/managed_subscribe) and [Property Sample](../../../samples/property/provider/src/main.rs) for Rust examples of a digital twin provider using MQTT. + +1. In your implementation, import the code of your in-vehicle digital twin model that you have created in the [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md#3-translating-dtdl-to-code). + +1. Implement the operations you have defined in your interface. This involves writing the logic for what should happen to each in-vehicle signal or command when each operation is called. If you are using the [sample digital twin provider interface](#sample-digital-twin-provider-interface), you need to implement the functionality for the `Get` and `Invoke` operations. + +1. For each operation you implement, you can reference an in-vehicle signal or command using the code of your in-vehicle digital twin model. + +In order to translate your operations into code, it is important to understand the requirements of each operation. + +#### Rust Sample Implementation of the Sample Interface + +This section uses the [sample digital twin provider interface](#sample-digital-twin-provider-interface) that is defined in a protobuf file, and covers a *sample* Rust implementation of the synchronous `Get` and `Invoke` operations. + +1. Reference the [code for implementing the operations for the sample digital twin provider interface](../../../samples/tutorial/provider/src/provider_impl.rs). + +1. There is an import statement for the Rust in-vehicle digital twin model that you have previously constructed in the [Tutorial: Create an In-Vehicle Model with DTDL](../in_vehicle_model/README.md#3-translating-dtdl-to-code): + +```rust +use digital_twin_model::sdv_v1 as sdv; +``` + +1. The implementation of the `Get` operation references the signals *is air conditioning active* and *ambient air temperature*: + +```rust + /// Get implementation. + /// + /// # Arguments + /// * `request` - Get request. + async fn get(&self, request: Request) -> Result, Status> {..} +``` + +1. The implementation of the `Invoke` operation references the command *show notification*. + +```rust +/// Invoke implementation. +/// +/// # Arguments +/// * `request` - Invoke request. +async fn invoke( + &self, + request: Request, +) -> Result, Status> {..} +``` + +## 2. Register Digital Twin Provider with the In-Vehicle Digital Twin Service + +You have defined your digital twin provider interface, and you have implemented the functionality of each operation in the programming language of your choice. + +You will need to register your digital twin provider with the [In-Vehicle Digital Twin Service](../../../README.md#high-level-design). This registration will make your digital twin provider discoverable by digital twin consumers through the In-Vehicle Digital Twin Service. + +>[In-Vehicle Digital Twin Service:](../../../README.md#high-level-design) Ibeji's architecture has an In-Vehicle Digital Twin Service at its core. The In-Vehicle Digital Twin Service captures all of the vehicle's hardware capabilities and makes them available to Ibeji consumers. + +The following lists out the flow for registering a digital twin provider in the programming language of your choice: + +1. Reference the interface of the [In-Vehicle Digital Twin Service](../../../interfaces/invehicle_digital_twin/v1/invehicle_digital_twin.proto) which is defined as a protobuf file. + +1. In the code for your digital twin provider, you will need to import an `In-Vehicle Digital Twin Service` gRPC client. + +1. Using the `In-Vehicle Digital Twin Service` gRPC client, you will need to define how to register your in-vehicle signals and commands with the In-Vehicle Digital Twin Service. This involves calling the `Register` gRPC method with the gRPC client. + +### 2.1 Rust Sample Registration of a Digital Twin Provider + +This section uses the [sample digital twin provider interface](#sample-digital-twin-provider-interface), and covers a *sample* Rust implementation of a provider registering the signals *ambient air temperature* and *is air conditioning active* and the command *show notification* + +1. Reference the [main.rs file of the sample digital twin provider](../../../samples/tutorial/provider/src/main.rs). The main.rs file outlines the behavior of the signals in your digital twin provider sample. This includes a vehicle simulator that can emulate changes in its signals. These changes are then published to any digital twin consumers that have subscribed to your digital twin provider. + +1. One function of particular interest in the [main.rs](../../../samples/tutorial/provider/src/main.rs) file is the `register_entities` function. + +```rust +/// Register the entities endpoints. +/// +/// # Arguments +/// * `invehicle_digital_twin_uri` - The In-Vehicle Digital Twin URI. +/// * `provider_uri` - The provider's URI. +async fn register_entities( + invehicle_digital_twin_uri: &str, + provider_uri: &str, +) -> Result<(), Status> { .. } +``` + +The `register_entities` function in this Rust sample digital twin provider shows the process of registering with the In-Vehicle Digital Twin Service. + +#### Run the Sample Digital Twin Provider + +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. +In each window, change the current 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. + +1. cd {repo-root-dir}/target/debug +Create the three config files with the following contents, if they are not already there: + +---- consumer_settings.yaml ---- +`invehicle_digital_twin_uri: "http://0.0.0.0:5010"` + +---- invehicle_digital_twin_settings.yaml ---- +`invehicle_digital_twin_authority: "0.0.0.0:5010"` + +---- provider_settings.yaml ---- +`provider_authority: "0.0.0.0:4010"` +`invehicle_digital_twin_uri: "http://0.0.0.0:5010"` + +1. In the top window, run: + +`./invehicle-digital-twin` + +1. In the middle window, run: + +`./digital-twin-provider-tutorial` + +1. In the bottom window, run: + +`./digital-twin-consumer-tutorial` + +1. Use control-c in each of the windows when you wish to stop the demo. + +## 3. Add Managed Subscribe to Digital Twin Provider + +>[Managed Subscribe:](../../../samples/managed_subscribe/README.md#introduction) The managed subscribe sample shows how Ibeji can extend its functionality with modules to give providers and consumers more capabilities. This sample utilizes the 'Managed Subscribe' module to allow a consumer to get an MQTT subscription for the AmbientAirTemperature value of a vehicle at a specific frequency in milliseconds. The provider, signaled with the help of the module, will publish the temperature value at the requested frequency for each consumer on its own topic and once the consumer unsubscribes and disconnects it will stop publishing to that dynamically generated topic. + +The current implementation of the Managed Subscribe Module expects to utilize the [Agemo Service](https://github.com/eclipse-chariott/Agemo/blob/main/README.md). This service currently requires the use of an MQTT broker for communication between publishers and subscribers. + +Adding the `Managed Subscribe` module for your digital twin provider is **optional**. However, here are some reasons why you might want to consider using the `Managed Subscribe` module for your digital twin provider: + +- Efficient Data Management: Allows your digital twin provider to efficiently manage the data being sent to its digital twin consumers. Your digital twin provider only needs to publish data when there is a change, so it reduces unnecessary data transmission. + +- Customized Frequency: Digital twin consumers can specify the frequency at which they want to receive updates. This allows for more tailored data delivery and can improve a digital twin consumer's experience. + +- Automated Topic Cleanup: The feature automatically stops publishing to a topic once all the digital twin consumers have unsubscribed. This helps in resource optimization and ensures that data is not being sent to inactive digital twin consumers. + +- Scalability: Managed Subscribe can handle a large number of digital twin consumers, making it a good choice for your digital twin provider that is expected to have many digital twin consumers subscribed to it. + +- Enhanced Capabilities: The Managed Subscribe module extends the functionality of a digital twin provider. + +If you decide to incorporate the `Managed Subscribe` module into your digital twin provider, please consult the [Managed Subscribe interface](../../../interfaces/module/managed_subscribe/v1/managed_subscribe.proto), and the [documentation for the Managed Subscribe sample](../../../samples/managed_subscribe/README.md) for guidance. You will need to implement the proto methods that are defined in the `Managed Subscribe` interface. Since the interface is defined in a protobuf file, the `Managed Subscribe` module is program language agnostic. + +### 3.1 Rust Sample Implementation of a Managed Subscribe Digital Twin Provider + +Please refer to the [sample Rust code for the Managed Subscribe Sample provider](../../../samples/managed_subscribe/provider/src/) to see an example of how to integrate the Managed Subscribe module into a digital twin provider. +This sample Rust code contains an *ambient air temperature* signal, and does not include the in-vehicle signal *is air conditioning active* and the command *show notification*. + +## Next Steps diff --git a/samples/interfaces/tutorial/digital_twin_provider.proto b/samples/interfaces/tutorial/digital_twin_provider.proto new file mode 100644 index 00000000..6c3376ad --- /dev/null +++ b/samples/interfaces/tutorial/digital_twin_provider.proto @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +// Digital Twin Provider Tutorial definition +// +// The protobuf definitions for the Digital Twin Provider Tutorial that only supports synchronous +// "Get" and "Invoke" operations + +syntax = "proto3"; +package digital_twin_provider_tutorial; + +// The service entry point to the Digital Twin Provider Tutorial. +service DigitalTwinProviderTutorial { + // Method to get the value of the specified property + rpc Get (GetRequest) returns (GetResponse); + + // Method to invoke a command + rpc Invoke (InvokeRequest) returns (InvokeResponse); +} + +message GetRequest { + string entity_id = 1; +} + +message GetResponse { + string property_value = 1; +} + +message InvokeRequest { + string entity_id = 1; + string payload = 2; +} + +message InvokeResponse { + string response = 1; +} \ No newline at end of file diff --git a/samples/mixed/provider/src/main.rs b/samples/mixed/provider/src/main.rs index 4f28d6e6..b7c29c1c 100644 --- a/samples/mixed/provider/src/main.rs +++ b/samples/mixed/provider/src/main.rs @@ -180,6 +180,11 @@ async fn publish(subscription_map: Arc>, entity_id: &str, } } +/// Starts the vehicle simulator. +/// +/// # Arguments +/// * `subscription_map` - Subscription map. +/// * `vehicle` - A vehicle struct that emulates the dynamic changes of in-vehicle signals. async fn start_vehicle_simulator( subscription_map: Arc>, vehicle: Arc>, diff --git a/samples/protobuf_data_access/build.rs b/samples/protobuf_data_access/build.rs index 49288e76..cf214dbd 100644 --- a/samples/protobuf_data_access/build.rs +++ b/samples/protobuf_data_access/build.rs @@ -25,5 +25,6 @@ fn main() -> Result<(), Box> { &["../../external/chariott/service_discovery/proto/core/v1/"], )?; + tonic_build::compile_protos("../interfaces/tutorial/digital_twin_provider.proto")?; Ok(()) } diff --git a/samples/protobuf_data_access/src/lib.rs b/samples/protobuf_data_access/src/lib.rs index 25544bb4..30208109 100644 --- a/samples/protobuf_data_access/src/lib.rs +++ b/samples/protobuf_data_access/src/lib.rs @@ -37,3 +37,9 @@ pub mod sample_grpc { } } } + +pub mod tutorial_grpc { + pub mod v1 { + tonic::include_proto!("digital_twin_provider_tutorial"); + } +} diff --git a/samples/tutorial/Cargo.toml b/samples/tutorial/Cargo.toml new file mode 100644 index 00000000..88fe34c7 --- /dev/null +++ b/samples/tutorial/Cargo.toml @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[package] +name = "digital-twin-tutorial" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +digital-twin-model = { path = "../../digital-twin-model" } +env_logger= { workspace = true } +log = { workspace = true } +samples-common = { path = "../common" } +samples-protobuf-data-access = { path = "../protobuf_data_access" } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } +tonic = { workspace = true } +url = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } + +[[bin]] +name = "digital-twin-provider-tutorial" +path = "provider/src/main.rs" + +[[bin]] +name = "digital-twin-consumer-tutorial" +path = "consumer/src/main.rs" \ No newline at end of file diff --git a/samples/tutorial/consumer/src/main.rs b/samples/tutorial/consumer/src/main.rs new file mode 100644 index 00000000..4d972e1d --- /dev/null +++ b/samples/tutorial/consumer/src/main.rs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use digital_twin_model::{sdv_v1 as sdv, Metadata}; +use env_logger::{Builder, Target}; +use log::{debug, info, warn, LevelFilter}; +use samples_common::constants::{digital_twin_operation, digital_twin_protocol}; +use samples_common::consumer_config; +use samples_common::utils::{ + discover_digital_twin_provider_using_ibeji, retrieve_invehicle_digital_twin_uri, + retry_async_based_on_status, +}; +use samples_protobuf_data_access::tutorial_grpc::v1::digital_twin_provider_tutorial_client::DigitalTwinProviderTutorialClient; +use samples_protobuf_data_access::tutorial_grpc::v1::{GetRequest, InvokeRequest}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use tokio::time::{sleep, Duration}; +use tonic::Status; + +#[derive(Debug, Serialize, Deserialize)] +struct ShowNotificationRequestPayload { + #[serde(rename = "Notification")] + notification: sdv::hmi::show_notification::request::TYPE, + #[serde(rename = "$metadata")] + metadata: Metadata, +} + +/// Start the Get Signals Repeater. +/// This will call the get operation on the digital twin provider synchronously to obtain the entity value. +/// +/// # Arguments +/// `provider_uri_map` - The provider uri map where the key is the entity id and the value is the provider's uri. +async fn start_get_signals_repeater( + provider_uri_map: HashMap, +) -> Result<(), Status> { + debug!("Starting the Consumer's get signals repeater."); + + loop { + for (entity, provider_uri) in &provider_uri_map { + let response = retry_async_based_on_status(30, Duration::from_secs(1), || { + send_get_request(provider_uri, entity) + }) + .await?; + + info!("Get response for entity {entity}: {response}"); + } + + debug!("Completed sending the get requests."); + + sleep(Duration::from_secs(5)).await; + } +} + +/// Start the show-notification repeater. +/// +/// # Arguments +/// `provider_uri` - The provider_uri. +async fn start_show_notification_repeater(provider_uri: String) -> Result<(), Status> { + debug!("Starting the consumer's show-notification repeater."); + + let metadata = Metadata { model: sdv::hmi::show_notification::request::ID.to_string() }; + + let request_payload: ShowNotificationRequestPayload = ShowNotificationRequestPayload { + notification: "Hello world notification.".to_string(), + metadata, + }; + + let request_payload_json = serde_json::to_string(&request_payload).unwrap(); + + loop { + info!("Sending an invoke request on entity {} with payload '{}' to provider URI {provider_uri}", + sdv::hmi::show_notification::ID, &request_payload_json); + + let client_result = DigitalTwinProviderTutorialClient::connect(provider_uri.clone()).await; + if client_result.is_err() { + warn!("Unable to connect. We will retry in a moment."); + sleep(Duration::from_secs(1)).await; + continue; + } + let mut client = client_result.unwrap(); + + let request = tonic::Request::new(InvokeRequest { + entity_id: sdv::hmi::show_notification::ID.to_string(), + payload: request_payload_json.to_string(), + }); + + let response = client.invoke(request).await?; + + info!("Show notification response: {}", response.into_inner().response); + + debug!("Completed the invoke request"); + sleep(Duration::from_secs(15)).await; + } +} + +/// Send a GET request to the digital twin provider and return the resulting value. +/// +/// # Arguments +/// `provider_uri` - The provider's URI. +/// `entity_id` - The entity id. +async fn send_get_request(provider_uri: &str, entity_id: &str) -> Result { + info!("Sending a get request to provider URI {provider_uri} for the value of {entity_id}"); + let mut client = DigitalTwinProviderTutorialClient::connect(provider_uri.to_string()) + .await + .map_err(|e| Status::internal(e.to_string()))?; + let request = tonic::Request::new(GetRequest { entity_id: entity_id.to_string() }); + let response = client.get(request).await?; + + Ok(response.into_inner().property_value) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logging. + Builder::new().filter(None, LevelFilter::Info).target(Target::Stdout).init(); + + info!("The digital twin consumer has started."); + + let settings = consumer_config::load_settings(); + + let invehicle_digital_twin_uri = retrieve_invehicle_digital_twin_uri( + settings.invehicle_digital_twin_uri, + settings.chariott_uri, + ) + .await?; + + // Acquire the provider's endpoint for the show notification command + let show_notification_command_provider_endpoint_info = + discover_digital_twin_provider_using_ibeji( + &invehicle_digital_twin_uri, + sdv::hmi::show_notification::ID, + digital_twin_protocol::GRPC, + &[digital_twin_operation::INVOKE.to_string()], + ) + .await + .unwrap(); + let show_notification_command_provider_uri = + show_notification_command_provider_endpoint_info.uri; + + // Acquire the provider's endpoint for the ambient air temperature signal + let mut provider_uri_map = HashMap::new(); + let ambient_air_temperature_property_provider_endpoint_info = + discover_digital_twin_provider_using_ibeji( + &invehicle_digital_twin_uri, + sdv::hvac::ambient_air_temperature::ID, + digital_twin_protocol::GRPC, + &[digital_twin_operation::GET.to_string()], + ) + .await + .unwrap(); + provider_uri_map.insert( + sdv::hvac::ambient_air_temperature::ID.to_string(), + ambient_air_temperature_property_provider_endpoint_info.uri, + ); + + // Acquire the provider's endpoint for the is air conditioning active signal + let is_air_conditioning_active_property_provider_endpoint_info = + discover_digital_twin_provider_using_ibeji( + &invehicle_digital_twin_uri, + sdv::hvac::is_air_conditioning_active::ID, + digital_twin_protocol::GRPC, + &[digital_twin_operation::GET.to_string()], + ) + .await + .unwrap(); + provider_uri_map.insert( + sdv::hvac::is_air_conditioning_active::ID.to_string(), + is_air_conditioning_active_property_provider_endpoint_info.uri, + ); + + tokio::select! { + _ = start_show_notification_repeater(show_notification_command_provider_uri.clone()) => {} + _ = start_get_signals_repeater(provider_uri_map) => {} + } + + debug!("The Consumer has completed."); + + Ok(()) +} diff --git a/samples/tutorial/provider/src/main.rs b/samples/tutorial/provider/src/main.rs new file mode 100644 index 00000000..51b031c2 --- /dev/null +++ b/samples/tutorial/provider/src/main.rs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod provider_impl; + +use crate::provider_impl::ProviderImpl; +use digital_twin_model::sdv_v1 as sdv; +use env_logger::{Builder, Target}; +use log::{info, LevelFilter}; +use samples_common::constants::{digital_twin_operation, digital_twin_protocol}; +use samples_common::provider_config; +use samples_common::utils::{retrieve_invehicle_digital_twin_uri, retry_async_based_on_status}; +use samples_protobuf_data_access::invehicle_digital_twin::v1::invehicle_digital_twin_client::InvehicleDigitalTwinClient; +use samples_protobuf_data_access::invehicle_digital_twin::v1::{ + EndpointInfo, EntityAccessInfo, RegisterRequest, +}; +use samples_protobuf_data_access::tutorial_grpc::v1::digital_twin_provider_tutorial_server::DigitalTwinProviderTutorialServer; +use std::net::SocketAddr; +use tokio::time::Duration; +use tonic::{transport::Server, Status}; + +/// Register the entities' endpoints with the In-Vehicle Digital Twin Service. +/// +/// # Arguments +/// * `invehicle_digital_twin_uri` - The In-Vehicle Digital Twin URI. +/// * `provider_uri` - The provider's URI. +async fn register_entities( + invehicle_digital_twin_uri: &str, + provider_uri: &str, +) -> Result<(), Status> { + // AmbientAirTemperature + let ambient_air_temperature_endpoint_info = EndpointInfo { + protocol: digital_twin_protocol::GRPC.to_string(), + operations: vec![digital_twin_operation::GET.to_string()], + uri: provider_uri.to_string(), + context: sdv::hvac::ambient_air_temperature::ID.to_string(), + }; + let ambient_air_temperature_access_info = EntityAccessInfo { + name: sdv::hvac::ambient_air_temperature::NAME.to_string(), + id: sdv::hvac::ambient_air_temperature::ID.to_string(), + description: sdv::hvac::ambient_air_temperature::DESCRIPTION.to_string(), + endpoint_info_list: vec![ambient_air_temperature_endpoint_info], + }; + + // IsAirConditioningActive + let is_air_conditioning_active_endpoint_info = EndpointInfo { + protocol: digital_twin_protocol::GRPC.to_string(), + operations: vec![digital_twin_operation::GET.to_string()], + uri: provider_uri.to_string(), + context: sdv::hvac::is_air_conditioning_active::ID.to_string(), + }; + let is_air_conditioning_active_access_info = EntityAccessInfo { + name: sdv::hvac::is_air_conditioning_active::NAME.to_string(), + id: sdv::hvac::is_air_conditioning_active::ID.to_string(), + description: sdv::hvac::is_air_conditioning_active::DESCRIPTION.to_string(), + endpoint_info_list: vec![is_air_conditioning_active_endpoint_info], + }; + + // ShowNotification + let show_notification_endpoint_info = EndpointInfo { + protocol: digital_twin_protocol::GRPC.to_string(), + operations: vec![digital_twin_operation::INVOKE.to_string()], + uri: provider_uri.to_string(), + context: sdv::hmi::show_notification::ID.to_string(), + }; + let show_notification_access_info = EntityAccessInfo { + name: sdv::hmi::show_notification::NAME.to_string(), + id: sdv::hmi::show_notification::ID.to_string(), + description: sdv::hmi::show_notification::DESCRIPTION.to_string(), + endpoint_info_list: vec![show_notification_endpoint_info], + }; + + let entity_access_info_list = vec![ + ambient_air_temperature_access_info, + is_air_conditioning_active_access_info, + show_notification_access_info, + ]; + + let mut client = InvehicleDigitalTwinClient::connect(invehicle_digital_twin_uri.to_string()) + .await + .map_err(|e| Status::internal(e.to_string()))?; + let request = tonic::Request::new(RegisterRequest { entity_access_info_list }); + let _response = client.register(request).await?; + + Ok(()) +} +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logging. + Builder::new().filter(None, LevelFilter::Info).target(Target::Stdout).init(); + + info!("The Digital Twin Provider has started."); + + let settings = provider_config::load_settings(); + + let provider_authority = settings.provider_authority; + + let invehicle_digital_twin_uri = retrieve_invehicle_digital_twin_uri( + settings.invehicle_digital_twin_uri, + settings.chariott_uri, + ) + .await?; + + // Construct the provider URI from the provider authority. + let provider_uri = format!("http://{provider_authority}"); // Devskim: ignore DS137138 + + // Setup the HTTP server. + let addr: SocketAddr = provider_authority.parse()?; + let provider_impl = ProviderImpl {}; + let server_future = Server::builder() + .add_service(DigitalTwinProviderTutorialServer::new(provider_impl)) + .serve(addr); + info!("The HTTP server is listening on address '{provider_authority}'"); + + info!("Sending a register request to the In-Vehicle Digital Twin Service URI {invehicle_digital_twin_uri}"); + retry_async_based_on_status(30, Duration::from_secs(1), || { + register_entities(&invehicle_digital_twin_uri, &provider_uri) + }) + .await?; + server_future.await?; + + info!("The Provider has completed."); + + Ok(()) +} diff --git a/samples/tutorial/provider/src/provider_impl.rs b/samples/tutorial/provider/src/provider_impl.rs new file mode 100644 index 00000000..7cbe2331 --- /dev/null +++ b/samples/tutorial/provider/src/provider_impl.rs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use digital_twin_model::sdv_v1 as sdv; +use log::{debug, info}; +use samples_protobuf_data_access::tutorial_grpc::v1::digital_twin_provider_tutorial_server::DigitalTwinProviderTutorial; +use samples_protobuf_data_access::tutorial_grpc::v1::{ + GetRequest, GetResponse, InvokeRequest, InvokeResponse, +}; +use tonic::{Request, Response, Status}; + +#[derive(Debug, Default)] +pub struct ProviderImpl {} + +#[tonic::async_trait] +impl DigitalTwinProviderTutorial for ProviderImpl { + /// Get implementation. + /// + /// # Arguments + /// * `request` - Get request. + async fn get(&self, request: Request) -> Result, Status> { + let request_inner = request.into_inner(); + let entity_id: String = request_inner.entity_id.clone(); + + let value = match entity_id.as_str() { + sdv::hvac::ambient_air_temperature::ID => "70", + sdv::hvac::is_air_conditioning_active::ID => "true", + _ => "NULL", + }; + + let get_response = GetResponse { property_value: String::from(value) }; + + Ok(Response::new(get_response)) + } + + /// Invoke implementation. + /// + /// # Arguments + /// * `request` - Invoke request. + async fn invoke( + &self, + request: Request, + ) -> Result, Status> { + debug!("Got an invoke request: {request:?}"); + + let request_inner = request.into_inner(); + let entity_id: String = request_inner.entity_id.clone(); + let payload: String = request_inner.payload; + + info!("Received an invoke request from for entity id {entity_id} with payload '{payload}'"); + + let response_message: String = if entity_id == sdv::hmi::show_notification::ID { + format!("Displaying notification '{payload}'") + } else { + format!("Error notification: The entity id {entity_id} is not recognized.") + }; + + info!("Sending an invoke response for entity id {entity_id}"); + + let response = InvokeResponse { response: response_message }; + + Ok(Response::new(response)) + } +}