Scenario: Normalizing Units of Measurement using a Digital Twin Adapter
This scenario explains how to use a digital twin model, a digital twin adapter with envelope and route mappings to normalize specific telemetry into a common schema, and how to validate the flow by posting sample device payloads.
This scenario demonstrates how to post automotive telemetry data with different units of measurement miles per hour and kilometers per hour using different endpoints, two digital twin instances, and two different external keys. For more information about the JQ patterns referenced in this scenario, see Reference: JQ Expressions for Digital Twin Models and Adapters
A digital twin adapter routes requests by endpoint and maps payloads to a single model. For example, metric model sends velocity in kilometers per hour (velocity_kph) while other standard digital twin instance sends miles per hour (speed).
Tasks
Before you begin
Make sure you have the required permissions and your OCI CLI is configured. For more information, see Prerequisites, Policy Details for the Internet of Things (IoT) Platform, and Using a JSON File for Complex Input.
Understand the files in this scenario:
Code snippets you can use in your digital twin model referenced in the steps below:
model.json— Digital twin model based on DTDL v3 specifications with aspeedtelemetry property in miles per hour that uses a validation extension that applies limits on the value range from 0–100.envelope.json— Envelope configuration that declares a reference endpoint and an example payload shape.routes.json— Route conditions and payload mappings that convert kilometres per hour to miles per hour.script.sh— In this example, you can save all the OCI CLI commands listed below to create a digital twin model, adapter, and instances, plus thecurlcommands to POST sample telemetry and then execute as a shell scriptscript.sh.To complete the steps in this scenario, you can create and save the OCI CLI and the
curlcommands listed in the steps below and the execute this scenario as a shell script:script.sh
This example digital twin model model.json code snippet uses a custom extension dtmi:com:oracle:dtdl:extension:validation;1 that applies validation rules the JSON schema for the "Telemetry", "Historized", "Validated", "Velocity" elements. If the data does not match the expected values defined in this validation then the data is rejected.
For a complete list of supported validation property rules, see DTMI Validation Extension Reference.
model.json
{
"@context":[
"dtmi:dtdl:context;3",
"dtmi:dtdl:extension:historization;1",
"dtmi:com:oracle:dtdl:extension:validation;1",
"dtmi:dtdl:extension:quantitativeTypes;1"
],
"@id":"dtmi:com:oracle:iot:poc:testmodel;1",
"@type":"Interface",
"contents":[
{
"@type":[ "Telemetry", "Historized", "Validated", "Velocity" ],
"displayName":"Speed",
"name":"speed",
"schema":"integer",
"unit":"milePerHour",
"minimum":0,
"maximum":100
}
]
}envelope.json
{
"referenceEndpoint": "telemetry/automotive/usa-standard-units",
"referencePayload": {
"dataFormat": "JSON",
"data": {
"speed": 65
}
}
}
routes.json file listed below contains 3 expressions that transforms the kilometers and normalizes the data payload into one unit of measurement, miles per hour:- A condition expression that evaluates the data from the endpoint:
"condition" : "${endpoint(3) == \"metric-units\"}"The
${ ... }syntax indicates an expression that evaluates the value of the third endpoint parameter or path element in an API callendpoint(3). The condition compares the returned value tometric-units. If true, then it applies these rules. - Payload mapping expression:
"payloadMapping" : { "$.speed": "${(.velocity_kph / 1.609) | floor}"}The
${ ... }syntax indicates an expression, this expression evaluates and performs an arithmetic calculation to convert the speed or velocity from kilometers per hour to miles per hour(.velocity_kph / 1.609)this divides thevelocity_kphfield by1.609and then applies the floor function, rounding down to the nearest integer. This value comes from the conversion of kilometers to miles which is kilometers = miles × 1.60934, "payloadMapping": {"$.speed": "$.speed"}This is a direct value mapping expression, passing through the value of speed as is.
[
{
"description" : "Automotive data using metric units (kilometers) that's converted to miles, with a different external key in the digital twin instance",
"condition" : "${endpoint(3) == \"metric-units\"}",
"payloadMapping" : {
"$.speed": "${(.velocity_kph / 1.609) | floor}"
},
"referencePayload" : {
"dataFormat" : "JSON",
"data" : { "velocity_kph": 104 }
}
},
{
"description" : "Auto 1 and Auto 2 use USA standard units, shows speed as is.",
"condition" : "*",
"payloadMapping" : { "$.speed": "$.speed" }
}
]
Step 1: Create a Digital Twin Model
Use this oci iot digital-twin-model create CLI command to create a digital twin model using the model.json file. This digital twin model standardizes speed in miles per hour.
This command registers the digital twin model with this DTMI URI dtmi:com:oracle:iot:poc:testmodel;1 as defined in model.json mention above.
oci iot digital-twin-model create \
--iot-domain-id <iot-domain-ocid> \
--display-name "Test Digital Twin Model" \
--description "Model for testing automotive telemetry routing and unit normalization" \
--spec file://~/model.json
Step 2: Create a Digital Twin Adapter with an Envelope and Routes
Create an adapter that references the digital twin model specification DTMI and that uses the inbound envelope and routes to normalize incoming telemetry.
oci iot digital-twin-adapter create \
--iot-domain-id <iot-domain-ocid> \
--display-name "automotive-speed-adapter" \
--description "Routes by units" \
--digital-twin-model-spec-uri "dtmi:com:oracle:iot:poc:testmodel;1" \
--inbound-envelope file://~/envelope.json \
--inbound-routes file://~/routes.json
The referenceEndpoint in envelope.json is telemetry/automotive/usa-standard-units. The routes.json file:
- Routes requests to the third path segment that equals
metric-unitsfor example,/telemetry/automotive/metric-units, and then convertsvelocity_kphtospeedin mph, and floors the result. - Uses a default catch-all condition (
*) to pass throughspeedunchanged for automobiles that use miles per hour.
Step 3: Create Two Digital Twin Instances
- Endpoint for mph:
https://device-host/telemetry/automotive/usa-standard-units - Endpoint for kph:
https://device-host/telemetry/automotive/metric-units
When you create a digital twin instance with authentication, you can use either a vault secret or an mTLS certificate to authenticate. For security, it's a best practice to create a unique vault secret or mTLS certificate for each digital twin instance. All resources must be in the same region and tenancy as any other related IoT resources.
If you use a mTLS certificate to authenticate then you must use the certificate's common name as the external key: --external-key <common-name-from-certificate-details>, see Scenario: Create a Digital Twin Instance that uses a mTLS Certificate.
An administrator must add the policy for creating secrets or certificates, see Step 3 in Prerequisites.
Digital twin instance for the USA standard units, miles per hour (mph), notice the external key:
american-auto-standard-unitsoci iot digital-twin-instance create \
--iot-domain-id <IoT-domain-ocid> \
--display-name "auto using miles per hour" \
--external-key american-auto-standard-units \
--digital-twin-adapter-id <same-digital-twin-adapter-ocid> \
--auth-id <secret-ocid-or-certificate-ocid>Digital twin instance for the European metric units, kilometers per hour (kph), notice the external key: european-auto-metric-units
oci iot digital-twin-instance create \
--iot-domain-id <IoT-domain-ocid> \
--display-name "auto using kilometers per hour" \
--external-key european-auto-metric-units \
--digital-twin-adapter-id <same-digital-twin-adapter-ocid> \
--auth-id <secret-ocid-or-certificate-ocid>
Step 4: Send sample telemetry to validate routing and mapping
To send telemetry you need the external key from the digital twin instance response from Step 3, the device password, and device host endpoint.
If the digital twin instance uses the vault secret to authenticate then you must use as the base 64 encoded secret value as the device password.
- External key: Replace the
external-keywith theexternal-keyfrom the digital twin instance you want to work with. To avoid quoting issues, it's a best practice to not use quotes in the external key value. - Device password: Replace the device password with either the vault secret contents or mTLS certificate OCID. If you use basic authentication, use the base 64 vault secret contents for the device password.
- Device host: Replace
device-hostwith your environment's device host from your IoT domain. To get the device host endpoint URL for the IoT domain, see Getting an IoT Domain's Details.
-u "external-key:device-password-vault-secret-contents-or-certificate-OCID"
Depending on your operating system or your application, Some applications or code editors may add unwanted quotes to your values, this can cause an error.
curl -i -X POST \
-u "european-auto-metric-units:device-password-vault-secret-base-64-or-certificate-OCID" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/metric-units" \
-d '{ "velocity_kph": 0 }'curl -i -X POST \
-u "european-auto-metric-units:device-password-vault-secret-base-64-or-certificate-OCID" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/metric-units" \
-d '{ "velocity_kph": 110 }'
curl -i -X POST \
-u "american-auto-standard-units:device-password-vault-secret-base-64-or-certificate-OCID" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/usa-standard-units" \
-d '{ "speed": 0 }'
curl -i -X POST \
-u "american-auto-standard-units:device-password-vault-secret-base-64-or-certificate-OCID" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/usa-standard-units" \
-d '{ "speed": 60 }'
Step 5: Verify normalization behavior
The route condition ${endpoint(3) == "metric-units"} evaluates the data and applies the following payload mapping to the metric units data endpoint:
"$.speed": "${(.velocity_kph / 1.609) | floor}"
Expected result:
- The adapter’s mapping converts kilometers per hour (kph) to miles per hour (mph), then applies floor to satisfy an integer schema:
speed_mph = floor(velocity_kph / 1.609) - In this example,
velocity_kph = 0: speed_mph = floor(0 / 1.609) = floor(0) = 0 mphAfter floor indicates the rounding step that forces the result to a whole number, rounding down toward negative infinity. This is required when your DTDL model declares the speed telemetry as
schema: "integer"so the value is an integer, not a float or string. velocity_kph = 110→speed = floor(110 / 1.609) = 68mph- Standard data posts with
speedpass through unchanged for example the values from this example:0,60
If your model validation is enabled minimum: 0, maximum: 100, out-of-range values are rejected according to the validation rules.
Uses soft type conversion:
- Pass-through (mph, schema "integer"):
{"speed": "60"}and{"speed": 60.0}are stored as60.{"speed": "60.2"}is rejected unless mapping coerces to an integer (for example, withfloor). - Metric route (kph → mph):
{"velocity_kph": 110}→68;{"velocity_kph": "110"}→68because the mappingflooremits an integer. Keep arithmetic inputs numeric to avoid expression errors; prefer110over"110"where possible. - Rounding remains explicit: Soft conversion does not auto-round
68.35to68. Usefloorfor integer schema, or switch the model toschema: "double"to preserve fractions.
Best Practices
- Reference JSON files for digital twin model specifications and adapters: When you upload an adapter using the CLI you can use JSON files specify data mapping. In CLI commands, you can reference files as
file://~/name.jsonor provide an absolute or relative path depending on your shell environment. Depending on your operating system, you may have slightly different syntax with quotes, slashes, or location of the file by default. See Managing CLI Input and Output and Using a JSON File for Complex Input. - JSON config files (envelope, routes) use API field names in
camelCase(for example,referenceEndpoint). The OCI CLI passes these files through unchanged viafile://arguments, so using camelCase JSON with CLI is expected and correct. - The
referenceEndpointinenvelope.jsonshould reflect a typical endpoint for your adapter. - Wildcard route condition (
*) is evaluated after specific conditions; order your route definitions accordingly. - Soft conversion scope: Numeric-like strings and whole-number doubles are accepted when they match the model type (for example, integer). Casting helpers like
number()andtoIntegerremain unsupported in route expressions; rely on arithmetic andfloor, or adoptschema: "double"to preserve fractions.
Variation: Using schema="double" instead of floor
This variation shows how setting the model property schema to double affects adapter mapping and the values recorded. With double, the validator accepts any numeric integral or fractional value that meets the range constraints, without auto-rounding. You can choose to preserve fractional precision (raw) or coerce to whole numbers by using floor. Both pass validation as long as values remain within the defined range.
What schema=double validation does
- Type acceptance: Accepts JSON numbers with or without fractional parts for example: 60, 68.35. Strings like
"68"remain invalid. - Range: Minimum and maximum for example, in this example
0–100are enforced. - No auto-rounding: The IoT platform does not round values; you control rounding in your digital twin adapter mapping or downstream using APEX or SQL depending on the systems configured to view your data.
Files used in this variation:
model_double.json— DTDL model withschema: "double".{ "@context": [ "dtmi:dtdl:context;3", "dtmi:dtdl:extension:historization;1", "dtmi:com:oracle:dtdl:extension:validation;1", "dtmi:dtdl:extension:quantitativeTypes;1" ], "@id": "dtmi:com:oracle:iot:poc:testmodeldouble;1", "@type": "Interface", "contents": [ { "@type": [ "Telemetry", "Historized", "Validated", "Velocity" ], "displayName": "Speed", "name": "speed", "schema": "double", "unit": "milePerHour", "minimum": 0, "maximum": 100 } ] }routes_double_raw.json— Mapping preserves fractional precision:"$.speed": "${.velocity_kph / 1.609}".[ { "description": "Double model: European metric units to miles per hour (mph); preserving fractional precision (no floor).", "condition": "${endpoint(3) == \"metric-units\"}", "payloadMapping": { "$.speed": "${.velocity_kph / 1.609}" }, "referencePayload": { "dataFormat": "JSON", "data": { "velocity_kph": 110 } } }, { "description": "Double model: USA standard units passthrough.", "condition": "*", "payloadMapping": { "$.speed": "$.speed" } } ]routes_double_floor.json— Mapping coerces to whole-number mph:"$.speed": "${(.velocity_kph / 1.609) | floor}"stored as a double.[ { "description": "Double model: European metric units to miles per hour (mph); floor to whole number (stored as double).", "condition": "${endpoint(3) == \"metric-units\"}", "payloadMapping": { "$.speed": "${(.velocity_kph / 1.609) | floor}" }, "referencePayload": { "dataFormat": "JSON", "data": { "velocity_kph": 110 } } }, { "description": "Double model: USA standard units passthrough.", "condition": "*", "payloadMapping": { "$.speed": "$.speed" } } ]
Step A: Create the digital twin model using double
oci iot digital-twin-model create \
--iot-domain-id iot-domain-ocid \
--display-name "TestModelSpeedDouble" \
--spec file://model_double.json
Step B: Create two adapters associated to the double model
Use raw values to preserve fractional precision:
oci iot digital-twin-adapter create \
--iot-domain-id iot-domain-ocid \
--display-name "auto-adapter-double-raw" \
--digital-twin-model-id double-model-ocid \
--inbound-envelope file://envelope.json \
--inbound-routes file://routes_double_raw.json
Floor uses a whole number as the mph, that's a double:
oci iot digital-twin-adapter create \
--iot-domain-id iot-domain-ocid \
--display-name "auto-adapter-double-floor" \
--digital-twin-model-id double-model-ocid \
--inbound-envelope file://envelope.json \
--inbound-routes file://routes_double_floor.json
Step C: Create digital twin instances for each adapter
oci iot digital-twin-instance create \
--iot-domain-id iot-domain-ocid \
--display-name "american-auto-raw" \
--external-key american-auto-raw \
--digital-twin-adapter-id adapter-double-raw-ocid \
--auth-id vault-secret-ocid
oci iot digital-twin-instance create \
--iot-domain-id iot-domain-ocid \
--display-name "european-auto-raw" \
--external-key european-auto-raw \
--digital-twin-adapter-id adapter-double-raw-ocid \
--auth-id vault-secret-ocid
oci iot digital-twin-instance create \
--iot-domain-id iot-domain-ocid \
--display-name "american-auto-dfloor" \
--external-key american-auto-dfloor \
--digital-twin-adapter-id adapter-double-floor-ocid \
--auth-id vault-secret-ocid
oci iot digital-twin-instance create \
--iot-domain-id iot-domain-ocid \
--display-name "european-auto-dfloor" \
--external-key european-auto-dfloor \
--digital-twin-adapter-id adapter-double-floor-ocid \
--auth-id vault-secret-ocid
Step D: Post sample telemetry and compare outcomes
-u "external-key:device-password" \- If the digital twin uses vault secret to authenticate, then use the
base64-secretas the device password. - If the digital twin instance uses a mLTS certification then use the
certificate-ocidas the device password.
Raw values (double, no floor):
curl -i -X POST \
-u "american-auto-raw:device-password" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/usa-standard-units" \
-d '{ "speed": 60 }'
curl -i -X POST \
-u "european-auto-raw:device-password" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/metric-units" \
-d '{ "velocity_kph": 110 }'
Expected Result: Second post produces approx. 68.35… mph (fractional) and is accepted because schema=double accepts fractional numbers within range.
Floor (double, with floor):
curl -i -X POST \
-u "american-auto-dfloor:device-password" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/usa-standard-units" \
-d '{ "speed": 60 }'
curl -i -X POST \
-u "european-auto-dfloor:device-password" \
-H "Content-Type: application/json" \
"https://device-host/telemetry/automotive/metric-units" \
-d '{ "velocity_kph": 110 }'
Expected Result: Second post produces 68 a whole-number and is accepted. Value is stored as a double for example 68.0 even though it is a whole number.
Notes on username quotes and downstream impact
- External key equals authentication username: If a digital twin instance is created using quotes in the external key value for example,
"\"american-auto-standard-units\"", the basic authentication username in yourcurlrequest must include the quotes, or a mismatch occurs and results in a401 Unauthorizederror. To avoid quoting issues, it's a best practice to not use quotes in your external key value as in the examples in this scenario. - Downstream in APEX or using SQL: With
schema=double, fractional mph values are preserved. If you need whole numbers in reports, applyFLOORand theROUNDin SQL for example,SELECT FLOOR(speed) FROM …. Withschema=integer, ensure the mapping emits integral values for example, by usingfloorto satisfy integer typing. - Expression support: The
inbound-routeaccepts arithmetic andfloor. Functions liketoIntegerornumberwere rejected and are not supported; useflooror adoptschema: "double"for fractional acceptance.