Azure IoT PnP from a device client perspective

In the past, this blog has produced a number of posts on the Azure IoT Plug-and-Play (IPNP) topic.

The idea with IPNP is that incoming messages, coming from devices, provide context regarding the format of that message.

If messages follow a certain template, an interface or contract is offered. This makes it easier to exchange messages in the longer term.

For an overview of IoT Plug-and-Play check out this previous post.

Today, I want to look at IPNP from a device client perspective.

Notice this post is based on C# code using the Azure Device Client SDK for C#.

The same logic can also be provided in other programming languages, either by using one of the SDKs or using native MQTT / AMQP protocols.

ModelId

In the previous post, we have seen that devices can store the Azure IoT Plug-and-Play modelId (the id of the model describing the message exchange format) in the DeviceTwin:

You can put it there yourself in the device twin but it’s recommended to let the device share their model ID when they connect to the cloud!

In C#, the modelId can be provided together with the connection:

ComponentId

The definition language of IPNP also knows the concept of components. Think about reusable parts or complete models.

So, a model can have multiple components.

This is an interesting concept that looks a bit like the Azure IoT Hub device module identities:

As you can see a module identity named ‘moduleOne’ is created and a ‘device’ is connected using this module identity.

Azure IoT Devices can logically be split up into separate modules, each with its own identity/security keys and its own C2D communication.

Incoming D2C messages are still received as device messages but the message is enriched with the id of the module:

This makes it possible to eg. have multiple groups wording on smaller parts of a big device, each sending their own messages.

How does this match up with the IPNP components?

At first, it does not!

If we send a message from a module using the new ModuleClient, the only thing that differs is the extra ModuleId section in the connection string:

Notice, this is of course the module identity connection string, not from the device the module is part of!

As we will see in the next paragraph, we need to do one more thing to honor the IPNP model-component relationship.

Mapping modules and components

Regarding the IPNP model-component relationship, we have the following situation:

a. The device communication is described in one model

b. That model is split up into multiple components

c. Our Azure IoT Device has an equal amount of module identities

d. Each module identity implements a component

At this moment, both the device client and the module client only expose the moduleId.

Note: in a real situation, devices only communicate using the module identities. The Device credentials are probably never used because it now is a logical/abstract device.

As I already mentioned, there is no direct relationship. No component reference can be stored in eg. the Azure IoT module identity module twin.

No, it’s quite simple.

Each message sent can hold the component identification:

Note: you need to combine the componentID with every message you send. That identification is not persisted in the module twin or device twin. So, the message size will increase a little and communication costs could increase too.

If we go for that one module one component approach, you probably want to keep the module identity name on par with the component name.

This can be a little awkward due to the naming convention of components:

But you are allowed to enter a module identity name based on this component name so if this helps you:

So technically it’s also possible to have the same component-related message being sent by multiple modules. A module is allowed to send multiple component-related messages too.

This is ok regarding uniqueness. The name of the module identity must only be unique within the ‘parent’ device:

It’s up to your use cases on how you use this.

dt-dataschema and dt-subject

How are the modelId and componentId exposed within Azure?

We know the modelId is part of the DeviceTwin / ModuleTwin. But it’s not efficient to retrieve the ID from the twin for every incoming message. This consumes a lot of messages from the IoT Hub daily message quota.

We know the componentId is not persisted.

Is it sent along with the message?

Yes.

Actually, both IDs are made available for each message in-flight:

It’s set in the dt-dataschema and the dt-subject fields!

The code behind this message looks like this:

static void Main(string[] args)
{
    Console.WriteLine("Hello, IoT PnP World!");

    var connectionString = "HostName=model-test-weu-ih.azure-devices.net;DeviceId=csharpdevice;ModuleId=dtmi:com:example:Thermostat;1;SharedAccessKey=[SAS]";

    var options = new ClientOptions { ModelId = "dtmi:com:example:NonExistingModuleController;1" };

    using var client = ModuleClient.CreateFromConnectionString(connectionString, options);

    while (true)
    {
        SendMessage(client);

        Thread.Sleep(1000);
    }
}

private static void SendMessage(ModuleClient client)
{
    var messageBody = new ModuleOne { doorOpen = true };

    string jsonData = JsonSerializer.Serialize(messageBody);
    var message = new Message(Encoding.ASCII.GetBytes(jsonData));
    message.ComponentName = "dtmi:com:example:Thermostat;1";
    message.Properties.Add("messagetype", "normal");

    client.SendEventAsync(message).Wait();

    Console.WriteLine("A single message is sent");
}

public class ModuleOne
{
    public bool doorOpen { get; set; }
}

So we see, each message has the IoT PnP identification stored in the system properties.

Validation of incoming messages

Once messages arrive in the cloud, advertised as being part of a DTDL model, do you trust the format to be what you expected?

If you want to validate incoming messages against the model itself, check out this GitHub repo.

This recently added DTDLParser for .NET is provided by the Digital Twin Consortium and available as a NuGet package.

Consuming in other Azure resources

The system properties are part of the official EventData format.

So, each Azure resource capable of receiving Azure IoT messages is capable of reading the system properties too.

Azure Functions, Azure Storage, and Azure Stream Analytics are a few of all Azure services capable of extracting the model and optionally component name.

Even Azure Data Explorer supports these system properties on incoming messages using the IoT Hub data connection:

Note: these properties are lost using the EventHub data connection.

This way, the format of incoming messages could be checked against the model or a user interface could dynamically adjust to the format of the message based on the model and component.

Conclusion

In this post, the code is shown to expose Azure IoT Plug and Play model and component identifications.

The post also shows how to integrate the model and component relationship in the IoT Hub device and module relationship.

The relationship is attached to every message flowing though the system so other services can use it to their advantage.