IoT Plug and Play support for Azure IoT Edge devices in IoT Central

In a previous blog, I already gave an introduction about the benefits of IoT Plug and Play (IPnP).

IoT Plug and Play enables solution builders to integrate IoT devices with their solutions without any manual configuration.

It introduces a model as a public description of a device. This device model advertises the capabilities of the logic on your IoT device like telemetry, properties, and commands. These elements are bundled as an interface. These interfaces are described using the Digital Twins Definition Language (DTDL).

Implementing IPnP in your solution is not that hard. The Azure IoT Device SDKs have you covered. Just interact with the cloud as usual. Only, when setting up the connection, the device exposes a ModelId. This id is compared with public models available in a repository so the actual model can be restored.

Azure IoT Central supports the capability models using the concept of device templates.

Next to direct-internet-connected devices, Azure IoT also support this concept of edge compute:

The way Azure IoT Edge devices are connected to the cloud differs quite a lot.

Azure IoT Edge devices lack the support for a ModelId. So there is no direct reference to any model is a model repository. But each type of edge device comes with a Deployment Manifest, describing the logic to be rolled out on the edge device and related settings.

These differences have an impact on how to implement IoT Plug and Play device templates.

Let’s see how to get started with IPnP for edge devices in IoT Central.

To demonstrate this feature, we use a new Azure IoT Central application together with an Edge device.

First, we take a look at a custom IoT Edge module so we can see what’s going on. Then, we will consume it in the IoT Central portal.

Test IoT Edge module

The solution we want to test will be sending telemetry messages to the cloud using an Azure IoT Edge device.

Note: Here you can find the official documentation about this topic.

For ease of demonstration, two different messages are sent using two direct methods in one custom module (source code is also available at GitHub).

We first focus on sending a TelemetryMessage to ‘output1’. That Direct method and response are just ceremony so we have full control when the message is generated and sent:

...
await ioTHubModuleClient.SetMethodHandlerAsync(
    "getCount",
    getCountMethodCallBack,
    ioTHubModuleClient);
...

static async Task<MethodResponse> getCountMethodCallBack(MethodRequest methodRequest, object userContext)        
{
    //// Send a message

    var telemetryMessage = new TelemetryMessage {
        id = 42,
        value = "telemetry",
    };

    var jsonMessage = JsonConvert.SerializeObject(telemetryMessage);

    using (var message = new Message(Encoding.UTF8.GetBytes(jsonMessage)))
    { 
        message.ContentEncoding = "utf-8";
        message.ContentType = "application/json";

        await ioTHubModuleClient.SendEventAsync("output1", message);
    }

    System.Console.WriteLine("Telemetry after 'getCount' sent.");

    //// Return a COUNT response

    var getCountResponse = new GetCountResponse {
        count = 42,            
    };

    var json = JsonConvert.SerializeObject(getCountResponse);
    var response = new MethodResponse(Encoding.UTF8.GetBytes(json), 200);

    await Task.Delay(TimeSpan.FromSeconds(0));

    return response;
}

public class GetCountResponse 
{
    public int count { get; set; }
}

public class TelemetryMessage
{
    public int id { get; set; }
    public string value { get; set; }
}

To make it even more interesting, a second Direct Method is added that sends another kind of message over output ‘output2’:

...
await ioTHubModuleClient.SetMethodHandlerAsync(
    "getError",
    getErrorMethodCallBack,
    ioTHubModuleClient);
...

static async Task<MethodResponse> getErrorMethodCallBack(MethodRequest methodRequest, object userContext)        
{
    //// Send a message

    var errorMessage = new ErrorMessage {
        code = 1234,
        message = "this is an error",
    };

    var jsonMessage = JsonConvert.SerializeObject(errorMessage);

    using (var message = new Message(Encoding.UTF8.GetBytes(jsonMessage)))
    { 
        message.ContentEncoding = "utf-8";
        message.ContentType = "application/json";

        await ioTHubModuleClient.SendEventAsync("output2", message);
    }

    System.Console.WriteLine("Error after 'getError' sent.");

    //// Return an ERROR response

    var getErrorResponse = new GetErrorResponse {
        code = 1234,            
    };

    var json = JsonConvert.SerializeObject(getErrorResponse);
    var response = new MethodResponse(Encoding.UTF8.GetBytes(json), 200);

    await Task.Delay(TimeSpan.FromSeconds(0));

    return response;
}

public class GetErrorResponse 
{
public int code { get; set; }
}

public class ErrorMessage
{
    public int code { get; set; }
    public string message { get; set; }
}

This is actually a common scenario where one module sends multiply types of messages (here we see both a telemetry message and an error message).

The existence of one or more outputs is not part of the module interface from a Device model concern, there are just messages being sent (and Direct Methods to be called).

Note: In this case, there is no overlap of message properties for the two messages shown. This could be possible though.

The logic (the modules, containers) rolled out to the edge device itself is captured in this Azure IoT Edge Deployment Manifest:

{
  "modulesContent": {
    "$edgeAgent": {
      "properties.desired": {
        "modules": {
          "testmodule": {
            "settings": {
              "image": "docker.io/svelde/iot-edge-test-iotcentral:0.0.1-amd64",
              "createOptions": ""
            },
            "type": "docker",
            "version": "1.0",
            "status": "running",
            "restartPolicy": "always"
          }
        },
        "runtime": {
          "settings": {
            "minDockerVersion": "v1.25"
          },
          "type": "docker"
        },
        "schemaVersion": "1.1",
        "systemModules": {
          "edgeAgent": {
            "settings": {
              "image": "mcr.microsoft.com/azureiotedge-agent:1.1",
              "createOptions": ""
            },
            "type": "docker"
          },
          "edgeHub": {
            "settings": {
              "image": "mcr.microsoft.com/azureiotedge-hub:1.1",
              "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"443/tcp\":[{\"HostPort\":\"443\"}],\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}]}}}"
            },
            "type": "docker",
            "status": "running",
            "restartPolicy": "always"
          }
        }
      }
    },
    "$edgeHub": {
      "properties.desired": {
        "routes": {
          "SimulatedTemperatureSensorToIoTHub": "FROM /messages/modules/testmodule/* INTO $upstream"
        },
        "schemaVersion": "1.1",
        "storeAndForwardConfiguration": {
          "timeToLiveSecs": 7200
        }
      }
    },
    "testmodule": {
      "properties.desired": {
        "dummyValue": 43
      }
    }
  }
}

This document shows we roll out three modules: edgeAgent, edgeHub, and the custom module for this test. The first two are just system modules. These are out of scope for our story today.

Note: we deploy version 0.0.1 of the custom module. We will update that version number later on.

There is no ModelID of any capability model related to this deployment manifest. We have to construct a Device template for this device ourselves within IoT Central.

Note: just for demonstration, we see the registration of this dummyValue. This is only used to demonstrate which logic from the deployment Manifest is referenced in a Device template.

Finally, in IoT Central, we can roll out this logic to an edge device using the Deployment manifest provided in combination with a device template.

Set up a first Device template

In a new IoT Central custom application, we add this new device template for an Azure IoT Edge device:

We supply the Deployment Manifest as seen above which is valid:

After a review and acceptance, the Device template is set up and ready for more additions. Here is the editor where both the Device template and Deployment manifest can be edited:

We see this Device Template is actually a Capability Model (ignoring the predefined awkward unique name, it can be fixed if needed) and it already knows about this testmodule (taken over from the deployment manifest):

This dialog is smart enough to add a ‘Management’ interface to the test module because this desired property named ‘dummyValue’ is found.

The desired property is represented as a writable property in the interface.

We now could add the outputted telemetry messages to this same interface…

Instead, we add a separate Device Template interface for each of the two messages:

You could experiment with first adding a Component and then adding an Interface.

A device model may include components in addition to the root component to describe device capabilities. Each component has an interface that describes the component’s capabilities. Component interfaces may be reused in other device models. For example, several phone device models could use the same camera interface.

Here is the complete interface for the Telemetry message (and related Direct Method):

This is the complete interface for the error message (and related Direct Method):

See how we add both the properties of the telemetry message (Telemetry) and the direct method (Command) in separate interfaces. The ‘dummyValue’ is of capability type Property.

Note: Do not forget to save each added or changed interface.

The device template (or: capability model) is still in draft so publish it now so devices can be related to it:

Note: For simplicity, we ignore cloud properties (Azure IoT device twin tags) and extra views.

We now have a ‘Version 1’ of this template. Related devices will be part of a device group (the version tag is omitted):

We can add a specific device by adding a new registration:

Let’s add one:

This dialog provides is used to register a device with a certain name and registration Id.

Once created the credentials are available which have to be filled in into the specific Azure IoT Edge runtime configuration on the device configuration file (here is a reference using symmetric keys):

We take these settings, save them in the configuration file, and spin up the edge device:

The deployment manifest is automatically sent to this device and the edgeAgent downloads and starts the modules, including that test module:

The device is now provisioned and connected:

See how the module information is shown (in a read-only manor).

The module-related deployment manifest parameters like the container create options or environment variables are not part of this display. We also cannot alter them. Only by deploying a new version of the deployment manifest makes it possible to manipulate it, update it on the associated devices.

That dummy value is picked up too and made available for editing in the UI (no edge side logic added for this one though):

The two commands, each defined in its own interface, are shown together on the same page:

On the raw data page, we see there is some exchange of Device twins going on, as expected when a device connects for the first time:

Testing the messages

We execute both Direct Methods. Each time, a specific message is sent:

We see the Direct Methods being executed on the edge (in the logging of the test module):

Note: that ‘Error after…’ is just a non-specific text explaining the execution went well. There is no actual error occurring.

This successful execution is shown in the portal too, in the history related to the Direct Methods.

The response of both the direct methods looks like this:

Finally, what we really care about is the ‘raw data’, the incoming messages:

Yes, we see both messages are successfully mapped to the right values in different columns.

To check if both messages are untouched while flowing through IoT Central, I added this export option, to an Event hub, in IoT Central:

In this Event Hub, we can monitor incoming messages (coming from IoT Central):

As you can see, we see two separate messages, equal to the format as seen in the source code:

We see these are related to a certain module in a certain device.

And, a template registration is added too. So in the Azure portal, custom logic can act on that template name too if it understands what it means.

At this moment, we have an IoT Central application that understands the IoT Edge device capability model related to a certain Deployment Manifest.

Versions

All devices that can use/support that same Deployment Manifest can be added to the same device group in IoT Central.

This means all devices share the same modules, routing, environment variables, container create options, etc. These are not configurable within IoT Central. Device twin related values will be part of the interface though.

Still, there are a lot of modules that use eg. the environment variables to define device-specific IP addresses, user names, and passwords. If a device has to implement such a different configuration, apart from the module set and versions, a new deployment manifest has to be created. This also means you probably want to have a separate Device Template!

You can either start creating a new device template or generate a new version.

A new template will probably mean a new capability model.

A new version of an existing device template will probably have changes to either the deployment model or the capability model or both.

Here we have a V2 of the mode we used before:

I expect different versions (V1, V2, V3) are most interesting when devices in an existing device group are migrated due to eg. a fix in a container, a new version of a container including an interface change (the output messages change), new routing options.

This is because the underlying capability model just gets a new version number, not a completely new name.

In this example, I just update the deployment manifest because there is a new module version (with a fictitious bug fix):

This second version must be published too (it also starts as a draft version).

Updating a deployment manifest could even be needed because there are other modules changed that do not have any telemetry, properties, or methods in their interface. Only their deployment manifest settings are changed. This still ends up in a new device template version.

Devices related to an (older) device template are not automatically migrated to a new one. Each device has the option to change its device template function:

You have to assign a new version by hand. Or automate this using the IoT Central Rest API.

Module Interfaces

In a new device template, it is possible to reuse ‘module templates’, by importing the related interface from a previous device template.

This is because both at the module level and at the interface level, the model can be exported:

Here, we see the exported models of our test module referencing both the Telemetry Message interface and the Error Message interface.

Conclusion

We have seen how IoT Central supports Azure IoT Edge devices using Azure Plug and Play.

The steps shown can feel a bit overwhelming but this is because we only scratched the surface.

It’s important to understand IoT Central just tries to match the incoming messages to the Device template. The incoming messages are not touched or altered.

In the Event Hub references to device, module, and template are part of the exported messages for additional rules to be composed in Azure, next to IoT Central.

If you want to try this out yourself, on MS Learn a learning module “Connect an IoT Edge device to your IoT Central application” is available. Here, you can create an IoT Central application and connect an IoT Edge device in a sandboxed Azure subscription.

The source code seen in this blog post is available at GitHub.

Advertentie