IoT Plug and Play, modeling IoT Central devices

If you have built an IoT Device yourself and are finally able to send telemetry to the cloud, you should be familiar with the scenario where you have to repeat the hard work of describing the messages all again on ingestion.

IoT Devices expose D2C telemetry and it can also support C2D communication. This interface is most of the time unique for that device. To be able to get insights from a device you have to be able to react to its interface.

Wouldn’t it be nice if a device was able to provide metadata about its interface once it connects to the cloud? This way, the incoming D2C telemetry could automatically result in e.g. a full user interface. And all C2D output could be represented by pre-configured input controls.

With IoT Plug and Play this is all possible:

IoT Plug and Play enables solution builders to integrate smart devices with their solutions without any manual configuration. At the core of IoT Plug and Play, is a device model that a device uses to advertise its capabilities to an IoT Plug and Play-enabled application

https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play

Microsoft provides a way to describe the interface of a device and annotate every feature.

To experience how this works, we will look at the best example: Azure IoT Central.

This SaaS IoT Dashboard makes perfect use of the ability how devices can expose their meta-model.

Let’s see how.

It all begins with adding a device template within an IoT Central app.

Instead of creating one, we can start adding a new device template selected from the list of existing device templates:

All these devices are certified for IoT Plug&Play.

For example, this RuuviTag device is one of them:

As you can see, the device exposes a list of capabilities. Here, we see a part of it with Telemetry and Properties. There are also Command capabilities.

Next to the Capabilities description, the device template also contains Forms. These forms are user interfaces for a selected number of capabilities.

Let’s take a closer look at this device. The Device model is available publicly:

Follow the “Device model” link. This brings us to the actual model:

Here, you see Temperature, RelativeHumidity, etc. This model is in fact written in the Digital Twin Design Language:

If you want to browse through all models of currently certified devices, check out this GitHub page:

Here you find all models from multiple vendors.

You can take a look at how the interface of these devices looks like. This makes it even possible to provide a simulation of that device. We will see how later on.

Instead of selecting an existing model, it’s time to create a device model ourselves and connect a device exposing this model.

Creating our own DTDL model

Let’s create an IoT Central app first before we create the model.

Azure IoT Central apps are living inside an Azure subscription. It is also possible to create a free app first which lives for seven days. That is time enough for a demonstration or some tests.

Notice that each app exposes a unique URL for the web interface. So the part you fill is unique too:

Remember that part…

Once the app is created, add a new device template:

That list of IoT Plug&Play certified devices is shown. This time, we create a custom device template:

First, we have to customize the template with a stellar name. Here, we use the name ‘TestDevice’:

Next, review this:

Now, we create a device template. This results in an empty device template.

It is possible to import an existing model. Here, we create a new custom model:

This results in a model named ‘TestDevice’. It does not contain any capabilities yet. Let’s add some:

So, select the TestDevice within Model. Here you can add the capabilities:

Add two Telemetry capabilities:

  1. Display name: count Name: count Capability type: Telemetry Schema: Integer
  2. Display name: state Name: state Capability type: Telemetry Schema: Boolean

This will look like this:

Do not forget to save this. Press Save.

We skip Cloud properties. these are properties that tell something about the device. Think of properties like Make, Usage, Version, Building, etc. This is great if you want to make subsets of all your devices and see or change all of them.

Note: Within Azure, these cloud properties are also known as Tags.

We now visualize the incoming data. So we need a form. Select Views:

Select “Visualizing the device”.

It opens a View template. Here, we can add tiles for both telemetry items. Select the telemetry capability and add the tile. Do this both for count and state:

By default, a tile with a line chart representation is added.

Change the type of both tiles into “Last Known Value” by selecting the most left customize icon on each tile:

Now save the view by selecting Save.

Finally, select Publish at the top of the page. This makes the device template available within the app so devices can be connected.

If you select Export, you can see how our own DTDL model looks like:

Here, the model is exported to Visual Studio Code.

As you can see, the device get’s its unique name. This is the first version of the model (hence the 1 at the end). If it is changed, the version must be incremented:

See that the id of the DTMI is a combination of both the unique name as filled in in the App URL and the template name.

Note: We have to store the id for later use below…

This model is now stored in IoT Central.

Note: It is possible to certify your own device. In that case, the model you see now is made available globally. We have this seen above with the predefined devices.

We have created a device template, let’s consume it now. Let’s create a device.

Register a device

First, we register a device. We need to give it the unique name and credentials of a new device:

Select New. This shows a dialog where the device name, id, etc. can be inserted.

It is even possible to mark the device as a simulation. This will create an actual simulation that generates random telemetry values.

It is also possible to create devices without a device template (unassigned). We will look at that later on. Here, we select our own template.

We fill in the properties for an actual device:

After you have hit Create, the device is registered:

If you open the device, it has the Registered Status. No telemetry is received yet:

We are looking for the device secrets. Select Connect and a new dialog appears:

This shows the ID Scope, Device ID, and two keys. We only need the two IDs and one key, copy.

Note: For those who are familiar with the Azure Device Provisioning Service, this looks like an individual enrollment with a symmetric key.

We have the credentials. The only thing we need now is an actual device.

Creating a client device

Our device template is unique and there is no certified device yet which implements it.

So we create one. For this, I took the example C# code of this Quickstart which lives in GitHub and I modified the Thermostat project.

I stripped the code to the bare minimum so it’s easy to see how the registration goes.

First, notice we have to provide those two IDs and the secret key. Also, we enter the ModelId.

The code falls apart into three parts:

  1. Enroll the DPS registration of the device (SetupDeviceClientAsync)
  2. Connect to the IoT Hub inside IoT Central (InitializeDeviceClient) as a DeviceClient
  3. Sending messages in a loop

This is the complete code:

using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Provisioning.Client;
using Microsoft.Azure.Devices.Provisioning.Client.Transport;
using Microsoft.Azure.Devices.Shared;
using System;
using System.Text;
using System.Threading.Tasks;

namespace PnP.Devices.Client.Sample
{
    public class Program
    {
        private static string DpsIdScope = "0ne0xxxx";
        private static string DeviceId = "TestDevice01reg";
        private static string DeviceSymmetricKey = "fillinprimarykey";

        private const string ModelId = "dtmi:pnptestapp:TestDevicef4;1";

        private static DeviceClient DeviceClient;

        private static int i;

        public static async Task Main(string[] args)
        {
            Console.WriteLine($"Set up the device client.");

            var dpsRegistrationResult = await SetupDeviceClientAsync();

            InitializeDeviceClient(dpsRegistrationResult);

            while (true)
            {
                i++;
                var state = (i % 2 == 0) ? "true" : "false";
                string telemetryPayload = $"{{ \"count\": {i}, \"state\": {state} }}";
                using var message = new Message(Encoding.UTF8.GetBytes(telemetryPayload));

                await DeviceClient.SendEventAsync(message);

                Console.WriteLine($"Telemetry: Sent - {telemetryPayload}.");

                await Task.Delay(5000);
            }
        }

        private static async Task<DeviceRegistrationResult> SetupDeviceClientAsync()
        {
            Console.WriteLine($"Initializing via DPS");

            var symmetricKeyProvider = new SecurityProviderSymmetricKey(DeviceId, DeviceSymmetricKey, null);
            var mqttTransportHandler = new ProvisioningTransportHandlerMqtt();
            var pdc = ProvisioningDeviceClient.Create("global.azure-devices-provisioning.net", DpsIdScope, symmetricKeyProvider, mqttTransportHandler);

            var pnpPayload = new ProvisioningRegistrationAdditionalData
            {
                JsonData = $"{{ \"modelId\": \"{ModelId}\" }}",
            };

            return await pdc.RegisterAsync(pnpPayload);
        }

        private static void InitializeDeviceClient(DeviceRegistrationResult dpsRegistrationResult)
        {
            string hostname = dpsRegistrationResult.AssignedHub;

            var authenticationMethod = new DeviceAuthenticationWithRegistrySymmetricKey(dpsRegistrationResult.DeviceId, DeviceSymmetricKey);

            var options = new ClientOptions
            {
                ModelId = ModelId
            };

            DeviceClient = DeviceClient.Create(hostname, authenticationMethod, TransportType.Mqtt, options);
            DeviceClient.SetConnectionStatusChangesHandler((status, reason) =>
            {
                Console.WriteLine($"Connection status change registered - status={status}, reason={reason}.");
            });
        }
    }
}

Notice the ModelId is provided both during the enrollment and during connecting.

This code needs some Nuget packages. This is part of the Csproj project file:

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.31.0" />
    <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Client" Version="1.16.0" />
    <PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Mqtt" Version="1.13.0" />
  </ItemGroup>

If you fill in the secrets, the code should work as advertised.

Let’s enroll, connect and send some telemetry.

Connecting the device

Before we start the app, notice the device in IoT Central is still in a registered state:

Start the application and see the device is enrolled and the connection is made. You even see the first telemetry being sent:

In the Azure IoT Central portal, the status is now Provisioned:

And, on refreshing the page, you see incoming data.

Notice the count and state columns are filled. So, the incoming telemetry is mapped on the device template. This is also clear because the “Unmodified data” column stays empty:

Now data arrives, check out the View. The last known values are show and update once in a while:

So, we have seen how a device model works within IoT Central and how a device exposes that ModelId.

Again, now with an unassigned device

There is just one thing.

We still had to add knowledge about the interface to both ends.

The device knows about the model ID which is fair.

But the registration was told which interface or moduleId (template assignment) to expect from the device.

Let’s repeat the same procedure with one difference.

Let’s register a device with the ‘unassigned’ template:

Once registered, we see the device is not yet assigned to any template:

The portal has no clue what to expect. There is not even a view assigned yet:

Now, start the same device code with these new credentials and see what happens.

Again, the connection is made:

The unassigned device is provisioned:

But, if you refresh the screen, the device is assigned to our own template:

The view is available and we see the tiles being updated.

This is exactly the same process as seen with certified devices!

Simulating certified devices

We know now how we can write code for a device that exposes a model, this opens the possibility to simulate certified devices.

Because we can get access to the DTDL model and the accompanying id, we can build something that can simulate each certified device.

Conclusion

We have seen a demonstration of IoT Plug and Play in action.

And it’s now clear how we have to ‘advertise’ the Model Id.

The Device Twin Definition Language is also shared with Azure Digital Twins. Just as shown in IoT Central, devices can be added automatically to the Azure Digital Twin environment.

The device client returned by the Connection code can update twin properties, it handles commands, and it can send telemetry, identical to the code for a device that doesn’t use the IoT Plug and Play conventions.