Digital Twin Usage Examples

This section shows a few usage examples that can occur in common digital twin usage scenarios and cover handling of the modules DigitalTwin and Container. The examples in here are build around the management of data for heavy construction machines and cover common events link storing data, sharing data, etc.

  • manufacturer: original manufacturer of the heavy machine
  • customer: a user, that has bought the physical heavy machine
  • service-technician: hired by the customer to perform maintenance on the heavy machine

The examples in this section use these variables for aforementioned users:

const manufacturer = '0x0000000000000000000000000000000000000001';
const customer = '0x0000000000000000000000000000000000000002';
const serviceTechnician = '0x0000000000000000000000000000000000000003';

Create a Digital Twin

Digital Identities are collections of data related to a “thing”. A “thing” can be basically anything, a bird, a plane or someone from the planet Krypton. Jokes aside, it most commonly describes a physical object - like in our example here a heavy machine.

So let’s create a digital twin four our heavy machine “Big Crane 250”, which is done with the DigitalTwin.create function:

const bigCrane250 = await DigitalTwin.create(runtime, { accountId: manufacturer });

This creates a new digital twin for the account manufacturer, which can now add containers to it or set other properties.

The DigitalTwin.create config argument function supports more properties than just accountId.

You can and should give your digital twin a DBCP description. To do this pass it to the new digital twin in the config property.

const description = {
  name: 'Big Crane 250',
  description: 'Digital Twin for my heavy machine "Big Crane 250"',
  author: 'Manufacturer',
  version: '0.1.0',
  dbcpVersion: 2,
};
const bigCrane250 = await DigitalTwin.create(
  runtime, { accountId: manufacturer, description });

If you do not set a description, at creation time, a default description is set. This description is available at the DigitalTwin.defaultDescription and can be used as a starting point for your own description. A description can be updated later on as well, see digitalTwin.setDescription.

So let’s say, we have created a digital twin four our heavy machine with the setup from the last code example. We now have the following setup:

manufacturer created a digital twin

manufacturer created a digital twin


Add Containers to Digital Twin

Continuing with the digital twin from the last section we add a container, that holds manufacturers private data with information about the production process and a link to a manual file. This can be done with the digitalTwin.createContainers function:

const { data } = await bigCrane250.createContainers({
  data: {},
});

The manufacturer account now has created a Container instance called data. This can be customized as described at Container.create.

manufacturer added a container to the twin

manufacturer added a container to the twin


Add Data to the Container

Continuing the example, the manufacturer adds data to the container.

await data.setEntry(
  'productionProfile',
  {
    id: 'BC250-4711',
    dateOfManufacturing: '1554458858126',
    category: 'hem-c',
  },
);
await data.setEntry('manual', 'https://a-link-the-manual...');

As these properties are new, container.setEntry adds a role for each property and the owner of the digital twin joins this role. During this role 0 to 63 are skipped as they are system reserved and can be used for more complex contract role setups. So the roles 64 (for productionProfile) and 65 (for manual) are created.

For each new property a new encryption key is generated and stored in the contracts Sharings. When new properties are added, this key is only shared for the owner of the digital twin, so only the owner can access the data stored in the contract.

Data can be read from the containers with container.getEntry:

const productionProfile = await data.getEntry('productionProfile');
manufacturer added entries to the container

manufacturer added entries to the container


Share Container Properties

As already said, the manufacturer wants to keep production data for own usage and share a link to the manual to the account customer. When not explicitly shared, properties are kept private, so nothing to do for the field productionProfile. To allow other accounts to access manual, encryption keys have to be shared, which can be done with container.shareProperties:

await data.shareProperties([
  { accountId: customer, read: ['manual'] }
]);

With this call, the account customer is added to the role 1 (member), which allows basic contract interaction but not necessarily access to the data. And because manual has be specified as a read (-only) field, this account receives an encryption key for the property manual, so it is now able to read data from this field.

To load data from the twins, customer can now fetch the container from the digital twin and load its data. Let’s assume manufacturer has communicated the address of the digital twin (e.g. 0x00000000000000000000000000000000000000c1) to customer and the customer can access the link to the manual with:

const bigCrane250LoadedFromCustomer = new DigitalTwin(
  runtime, { accountId: customer, address: '0x00000000000000000000000000000000000000c1' });
const dataLoadedFromCustomer = await bigCrane250LoadedFromCustomer.getEntry('data');
const link = await dataLoadedFromCustomer.getEntry('manual');
customer can read entry "manual"

customer can read entry “manual”


Cloning Containers

If customer wants to re-use data from a data container or an entire data container but have ownership over it, it can clone it and use it in an own digital twin contract. This can be done with Container.clone:

const dataClone = await Container.clone(
  runtime, { accountId: customer }, dataLoadedFromCustomer);

This clone can be linked to a digital twin owner by customer. So let’s create a new one and add the clone to it:

const customersDescription = {
  name: 'My own Big Crane 250',
  description: 'I bought a Big Crane 250 and this is my collection of data for it',
  author: 'Customer',
  version: '0.1.0',
  dbcpVersion: 2,
};
const customersBigCrane250 = await DigitalTwin.create(
  runtime, { accountId: customer, description: customersDescription });

await customersBigCrane250.setEntry(
  'machine-data',
  dataClone,
  DigitalTwinEntryType.Container,
);

Note that the container is not named data like in the original twin but called machine-data here. Names can be reassigned as desired.

customer cloned data container

customer cloned data container


Granting Write Access

Properties at Containers can be “entries” as used in the last examples or “list entries”. To add data to lists call container.addListEntries:

await dataClone.addListEntries(
  'usagelog',
  [ 'I started using my new Big Crane 250' ]
);

Now customer wants to invite serviceTechnician and allow this account to add entries to the list usagelog as well. To do this, the list is shared the same way as in the previous example, but the field is shared as readWrite:

await dataClone.shareProperties([
  { accountId: customer, readWrite: ['usagelog'] }
]);

serviceTechnician can now write to the list usagelog and we now have the following setup:

customer invited service technician

customer invited service technician


Handling Files

Containers can hold files as well. File handling follows a few simple principles:

  • files are stored encrypted (as everything in containers is stored encrypted)
  • files are always stored as an array of files (think of it like a folder with files)
  • files are encrypted, uploaded and a reference is stored as a file at the contract (sounds like the default Hybrid Storage) approach, but is a reapplication to itself, as encrypted additional files with references to the original encrypted files are stored at the contract

Okay, let’s add some files to a container (taken from our tests).

A file needs to be provided as a buffer. In NodeJs, this can be done with fs.readFile

import { promisify } from 'util';
import { readFile } from 'fs';

const file = await promisify(readFile)(
`${__dirname}/testfiles/animal-animal-photography-cat-96938.jpg`);

The file is expected to be wrapped in a specific container format, which is defined in the ContainerFile interface. So let’s build such a file object and store it in an object including a property called files, as files are always provided as arrays of ContainerFile instances to the API:

const sampleFiles = {
  files:[{
    name: 'animal-animal-photography-cat-96938.jpg',
    fileType: 'image/jpeg',
    file,
  }]
};

If not already done, create (or load) a container:

const container = await Container.create(runtime, config);

If not already done, add a field for files to our container, for this the static property Container.defaultTemplates can be useful:

await container.ensureProperty('sampleFiles', Container.defaultSchemas.filesEntry);

So now everything is set up and we can store our file:

await container.setEntry('sampleFiles', sampleFiles);

And later on we can retrieve our file with:

await container.getEntry('sampleFiles');

That’s it for the simple case. If you want to get fancy, you can have a look at the more complex examples in the tests. With the build in file handling you can:


Handling Templates and Plugins

Container definitions can be saved as plugins, so they can easy be shared and the structure can be imported by anyone else. These plugins can be combined, including a dbcp description, that represents a whole twin structure, a so called Twin Template.

Have a look at several example twin templates in our separated Twin Templates repository:

Steps to your own twin template:

  1. Define a short description of your twin template:
{
  "description": {
    "name": "Heavy Machine",
    "description": "Basic Heavy Machine twin structure."
  },
  "plugins": { ... }
}
  1. Add default plugins to your template:
{
  "description": { ... },
  "plugins": {
    "data": {
      "description": {
        ...
      },
      "template": {
        "properties": {
          "productionProfile": {
            "dataSchema": {
              "properties": {
                "id": {
                  "type": "string"
                },
                "dateOfManufacturing": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "permissions": {
              "0": [
                "set"
              ]
            },
            "type": "entry"
          }
        },
        "type": "heavyMachineData"
      }
    },
    "plugin2": { ... },
    "pluginX": { ... },
  }
}
  1. You can also add translations for names and descriptions to your twin template and plugins. Also labels and placeholders can be defined for fields within data sets. You can simply apply a i18n property within the description:
{
  "description": {
    "name": "Heavy Machine",
    "description": "Basic Heavy Machine twin structure.",
    "i18n": {
      "de": {
        "description": "Beispiel für eine Vorlage eines Digitalen Zwillings.",
        "name": "Beispiel Vorlage"
      },
      "en": {
        "description": "Example of a template for a digital twin.",
        "name": "Sample Twin Template"
      }
    }
  },
  "plugins": {
    "data": {
      "description": {
        "i18n": {
          "de": {
            "productionProfile": {
              "description": "Beschreibung Datenset",
              "name": "Datenset 1",
              "properties": {
                "id": {
                  "label": "TWIN ID",
                  "placeholder": "Bitte geben Sie eine Produktions-ID ein."
                },
                ...
              }
            },
            "description": "Generelle Daten einer Baumaschine.",
            "name": "Produktdaten"
          },
          "en": {
            "productionProfile": {
              "description": "description data set",
              "name": "dataset 1",
              "properties": {
                "id": {
                  "label": "TWIN ID.",
                  "placeholder": "Please insert production id."
                },
                ...
              }
            },
            "description": "General information about a heavy machine.",
            "name": "Product data"
          }
        }
      },
    }
  }
}
  1. Create a new Digital Twin using with a twin template
const twinTemplate = {
  "description": { ... },
  "plugins": { ... }
};
const bigCrane250 = await DigitalTwin.create(runtime, {
  accountId: manufacturer,
  ...twinTemplate
});
  1. Export your existing twin structure to a twin template
const bigCraneTemplate = await bigCrane250.exportAsTemplate(true);

console.log(bigCraneTemplate);

// {
//   "description": { ... },
//   "plugins": {
//     "data": {
//       "description": {
//         ...
//       },
//       "template": {
//         ...
//       }
//     },
//     "pluginX": {
//       ...
//     }
//   }
// }

Setup plugin data structure

Data structure of the plugins is defined within the plugins template part. Each plugin will result in one container, that have several data sets, which are defined under the properties parameter. All data in this parameter, is defined using the ajv validator. You can also have a look at the Container.create documentation. Read the following points for a short conclusion about dataSchema and it’s structure.

  1. dataset1
  1. dataSchema - AJV data schema definition for the entry / listentry. Could be e.g. type of string, number, files, nested objects
  2. permissions - Initial permission setup, that is directly rolled out by container initialization with this plugin. You can directly define, which role is allowed to “set” or “remove” data of this entry.
  3. type - type of the datacontract entry (“entry” / “list”)
  4. value - initial value that should be passed to the entry / added as listentries to a list

Sample AJV data type configurations are listed below and pleae keep in mind, that JSON structures can be nested using a array or a object type:

  • string
{
  "type": "string"
}
  • number
{
  "type": "number"
}
  • files (it’s evan specific)
{
  "type": "object",
  "$comment": "{\"isEncryptedFile\": true}",
  "properties": {
    "additionalProperties": false,
    "files": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": [
    "files"
  ],
  "default": {
    "files": []
  }
}
  • objects
{
  "properties": {
    "prop1": {
      "type": "string"
    },
    "prop2": {
      "type": "string"
    }
  },
  "type": "object"
}
  • lists including objects
{
  "items": {
    "properties": {
      "prop1": {
        "type": "string"
      },
      "prop2": {
        "type": "string"
      }
    },
    "type": "object"
  },
  "type": "array"
}

Default values

Default values can be specified for all entries (except list entries) next to the dataSchema. The API will directly call setEntry for each specified default.

Example:

{
  "pluginXYZ": {
    "entry1": {
      "dataSchema": {
        "properties": {
          "prop1": {
            "type": "string"
          },
          "prop2": {
            "type": "string"
          }
        },
        "type": "object"
      },
      ...,
      "value": {
        "prop1": "default value for prop1"
      }
    }
  }
}