Skip to content

Commit

Permalink
Add Digital Twin Provider Tutorial (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
jorchiu authored Nov 15, 2023
1 parent 56ddbaf commit 6cb95ae
Show file tree
Hide file tree
Showing 12 changed files with 673 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ members = [
"samples/property",
"samples/seat_massager",
"samples/streaming",
"samples/tutorial"
]

[workspace.dependencies]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ through an extensible, open and dynamic architecture that provides access to the

## <a name="high-level-design">High-level Design</a>

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.

## <a name="prerequisites">Prerequisites</a>
Expand Down
11 changes: 8 additions & 3 deletions docs/tutorials/in_vehicle_model/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# 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)
- [1.4 DTDL Properties](#14-dtdl-properties)
- [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

Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
208 changes: 208 additions & 0 deletions docs/tutorials/provider/README.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions samples/interfaces/tutorial/digital_twin_provider.proto
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions samples/mixed/provider/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ async fn publish(subscription_map: Arc<Mutex<SubscriptionMap>>, 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<Mutex<SubscriptionMap>>,
vehicle: Arc<Mutex<Vehicle>>,
Expand Down
1 change: 1 addition & 0 deletions samples/protobuf_data_access/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
&["../../external/chariott/service_discovery/proto/core/v1/"],
)?;

tonic_build::compile_protos("../interfaces/tutorial/digital_twin_provider.proto")?;
Ok(())
}
6 changes: 6 additions & 0 deletions samples/protobuf_data_access/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ pub mod sample_grpc {
}
}
}

pub mod tutorial_grpc {
pub mod v1 {
tonic::include_proto!("digital_twin_provider_tutorial");
}
}
33 changes: 33 additions & 0 deletions samples/tutorial/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
180 changes: 180 additions & 0 deletions samples/tutorial/consumer/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
) -> 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<String, Status> {
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<dyn std::error::Error>> {
// 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(())
}
Loading

0 comments on commit 6cb95ae

Please sign in to comment.