Azure IoT Client SDK now supports IoT device modules

Silently, Microsoft introduced modules in IoT Devices.

No, I’m not talking about IoT Edge modules, these are modules for IoT Devices which can connect to the IoT Hub directly.

Before, we used the device client to communicate with clients and the IoTHub. And we used the Device Twin to configure the device with desired properties.

This approach is still valid. But in addition, we can also separate the client logic in multiple modules. And each module can send messages and receive a Module Twin configuration.

Let’s see how this works.

Why do we need modules?

I do not expect the usage of modules will be a common approach while building your IoT Device.

But modules can be very useful when your IoT Device has to represent multiple pieces of logic.

For example, a few years ago, I was working on a smart TV. A TV is build up by several components and all components can come from different vendors.

Although your smart TV is represented by one IoT Device, it would be nice to separate the D2C and C2D communication for each component. If your IoT Device has only one channel, a device client, separating the communication takes a lot of effort.

But with the new module client, this is a ‘piece of cake’.

Testing this new approach

Modules are supported in all kinds of SDKs: C#, C, Python, and NodeJS.

Microsoft has already excellent documentation which describes how to create a test app.

This documentation first shows how to register a device in an IoT Hub. After that, it shows how to register a module within the device and retrieve the connection string of that module.

Finally, you write code like this one (I made the message a bit more descriptive):

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;
 
namespace ConsoleModuleApp
{
    internal class Program
    {
        private const string ModuleConnectionString = "HostName=edgedemo-ih.azure-devices.net;DeviceId=iotdevice;ModuleId=moduleOne;SharedAccessKey=[primary key]";
        private static ModuleClient Client = null;
 
        private static void Main(string[] args)
        {
            Microsoft.Azure.Devices.Client.TransportType transport = Microsoft.Azure.Devices.Client.TransportType.Amqp;
 
            try
            {
                Client = ModuleClient.CreateFromConnectionString(ModuleConnectionString, transport);
                Client.SetConnectionStatusChangesHandler(ConnectionStatusChangeHandler);
                Client.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertyChanged, null).Wait();
 
                Console.WriteLine("Retrieving twin");
                var twinTask = Client.GetTwinAsync();
                twinTask.Wait();
                var twin = twinTask.Result;
                Console.WriteLine(JsonConvert.SerializeObject(twin));
 
                Console.WriteLine("Sending app start time as reported property");
                TwinCollection reportedProperties = new TwinCollection();
                reportedProperties["DateTimeLastAppLaunch"] = DateTime.Now;
 
                Client.UpdateReportedPropertiesAsync(reportedProperties);
            }
            catch (AggregateException ex)
            {
                Console.WriteLine("Error in sample: {0}", ex);
            }
 
            var telemetry = new Telemetry { counter = 42, timeStamp = DateTime.UtcNow };
            var messageString = JsonConvert.SerializeObject(telemetry);
            var message = new Message(Encoding.ASCII.GetBytes(messageString));
            var sendEventsTask = Client.SendEventAsync(message);
            sendEventsTask.Wait();
 
            Console.WriteLine("Event sent to IoT Hub.");
 
            Console.WriteLine("Waiting for Events.  Press enter to exit...");
            Console.ReadKey();
 
            Client.CloseAsync().Wait();
        }
 
        private static void ConnectionStatusChangeHandler(ConnectionStatus status, ConnectionStatusChangeReason reason)
        {
            Console.WriteLine($"Status {status} changed: {reason}");
        }
 
        private static async Task OnDesiredPropertyChanged(TwinCollection desiredProperties, object userContext)
        {
            Console.WriteLine("desired property change:");
            Console.WriteLine(JsonConvert.SerializeObject(desiredProperties));
            Console.WriteLine("Sending current time as reported property");
            TwinCollection reportedProperties = new TwinCollection
            {
                ["DateTimeLastDesiredPropertyChangeReceived"] = DateTime.Now
            };
 
            await Client.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false);
        }
    }
 
    internal class Telemetry
    {
        public int counter { get; set; }
 
        public DateTime timeStamp { get; set; }
    }
}

The most important thing you have to notice is the module in the connection string:

HostName=edgedemo-ih.azure-devices.net;DeviceId=iotdevice;ModuleId=moduleOne;SharedAccessKey=[primary key]

Note: It is possible to add multiple modules in the application, each has a different connection string (both the name of the module and another primary key).

Running this code

When we run the code, we will see how the module twin is received and how a message is sent successfully.

But I was especially interested in the messages. Are we able to separate messages from the different modules once these are arriving in the cloud?

Let’s look at different tooling, what information is shown regarding our module messages.

Device Explorer

The Device Explorer only shows the device name:

So this tool is not capable to show from which module this message is coming from.

Visual Studio Code

with the recent IoT Toolkit extensions for Visual Studio code, we can check incoming messages too:

Again, we are only shown the name of the device, not the name of the module.

So this is a bit disappointing.

Azure Function

I have one extra trick, I can dissect the message inside Azure Functions:

#r "Newtonsoft.Json"
#r "Microsoft.ServiceBus"

using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using System;

public static void Run(EventData myIoTHubMessage, TraceWriter log)
{
  log.Info($"C# IoT Hub trigger function processed a message: {myIoTHubMessage}");
  var bodyText = string.Empty;

  using(var stream = myIoTHubMessage.GetBodyStream())
  using(var streamReader = new StreamReader(stream))
  {
    bodyText = streamReader.ReadToEnd();
    log.Info($"body {bodyText}");
  }

  var systemProperties = string.Join(" - ", myIoTHubMessage.SystemProperties.Select(x => $"{x.Key} = {x.Value}"));
  log.Info($"System properties: {systemProperties}");

  var properties = string.Join(" - ", myIoTHubMessage.Properties.Select(x => $"{x.Key} = {x.Value}"));
  log.Info($"Properties: {properties}");

  if (myIoTHubMessage.Properties.Any(x => x.Key == "content-type"
                                      && x.Value.ToString() == "application/edge-modbus-json" ))
  {
    var list = JsonConvert.DeserializeObject<dynamic>(bodyText);

    foreach (var item in list)
    {
      log.Info($"{item.HwId} {item.DisplayName} {item.Address} {item.Value}");
    }
  }
}

This Azure Function is receiving a message from the IoT Hub as if this is an Event Hub message. And I already found out we can ‘cast’ our message and check out all properties:

2018-08-07T20:18:40.146 [Info] Function started (Id=7e4152d7-19f5-4144-89c8-0043fb3e734e)
2018-08-07T20:18:40.146 [Info] C# IoT Hub trigger function processed a message: Microsoft.ServiceBus.Messaging.EventData
2018-08-07T20:18:40.146 [Info] body {"counter":42,"timeStamp":"2018-08-07T20:18:43.218535Z"}
2018-08-07T20:18:40.146 [Info] System properties: 
                        iothub-connection-device-id = iotdevice - 
                        iothub-connection-module-id = moduleOne - 
                        iothub-connection-auth-method = {"scope":"module","type":"sas","issuer":"iothub","acceptingIpFilterRule":null} - 
                        iothub-connection-auth-generation-id = 636689299774845455 - 
                        iothub-enqueuedtime = 8/7/2018 8:18:40 PM - 
                        iothub-message-source = Telemetry - 
                        x-opt-sequence-number = 6 - 
                        x-opt-offset = 2560 - 
                        x-opt-enqueued-time = 8/7/2018 8:18:40 PM - 
                        EnqueuedTimeUtc = 8/7/2018 8:18:40 PM - 
                        SequenceNumber = 6 - 
                        Offset = 2560
2018-08-07T20:18:40.146 [Info] Properties:
2018-08-07T20:18:40.146 [Info] Function completed (Success, Id=7e4152d7-19f5-4144-89c8-0043fb3e734e, Duration=1ms)

And there it is, both the DeviceId and ModuleId are shown in the system properties. This means we can also split messages from different modules (from different devices but with the same ModuleID) in the IoTHub routing.

Conclusion

Although the use of modules is something to consider in special cases, the implementation is done in a very useful way.

If you have to split up your device in multiple, separated pieces of logic, modules will save from a lot of hassle.