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).

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 a speed telemetry 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 the curl commands to POST sample telemetry and then execute as a shell script script.sh.
  • To complete the steps in this scenario, you can create and save the OCI CLI and the curl commands 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
    }
  }
}

This 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 call endpoint(3). The condition compares the returned value to metric-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 the velocity_kph field by 1.609 and 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-units for example, /telemetry/automotive/metric-units, and then converts velocity_kph to speed in mph, and floors the result.
  • Uses a default catch-all condition (*) to pass through speed unchanged for automobiles that use miles per hour.

Step 3: Create Two Digital Twin Instances

Create two digital twin instances that authenticate using a vault secret and share both digital twin instances share the same digital twin adapter. The endpoints are defined so that each digital twin instance can post data to a unique endpoint:
  • Endpoint for mph: https://device-host/telemetry/automotive/usa-standard-units
  • Endpoint for kph: https://device-host/telemetry/automotive/metric-units
Replace the digital twin adapter OCID with the OCID from the digital twin adapter created in previous step. Replace display names and external keys with the values for your environment.
Note

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-units
oci 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-key with the external-key from 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-host with 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"

Note

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 mph

    After 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 = 110speed = floor(110 / 1.609) = 68 mph
  • Standard data posts with speed pass 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 as 60. {"speed": "60.2"} is rejected unless mapping coerces to an integer (for example, with floor).
  • Metric route (kph → mph): {"velocity_kph": 110}68; {"velocity_kph": "110"}68 because the mapping floor emits an integer. Keep arithmetic inputs numeric to avoid expression errors; prefer 110 over "110" where possible.
  • Rounding remains explicit: Soft conversion does not auto-round 68.35 to 68. Use floor for integer schema, or switch the model to schema: "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.json or 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 via file:// arguments, so using camelCase JSON with CLI is expected and correct.
  • The referenceEndpoint in envelope.json should 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() and toInteger remain unsupported in route expressions; rely on arithmetic and floor, or adopt schema: "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–100 are 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 with schema: "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

Use the external key and device password from the digital twin instance to send data:
 -u "external-key:device-password" \
  • If the digital twin uses vault secret to authenticate, then use the base64-secret as the device password.
  • If the digital twin instance uses a mLTS certification then use the certificate-ocid as 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 your curl request must include the quotes, or a mismatch occurs and results in a 401 Unauthorized error. 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, apply FLOOR and the ROUND in SQL for example, SELECT FLOOR(speed) FROM …. With schema=integer, ensure the mapping emits integral values for example, by using floor to satisfy integer typing.
  • Expression support: The inbound-route accepts arithmetic and floor. Functions like toInteger or number were rejected and are not supported; use floor or adopt schema: "double" for fractional acceptance.