Demystifying Azure IoT Hub Device SDKs

ridomin

Rido

Posted on May 2, 2022

Demystifying Azure IoT Hub Device SDKs

This post covers how to access main IoTHub features using the MQTT protocol without using the official Device SDKs (as described in the docs), instead it uses a generic MQTT library for dotnet capable of basic MQTT primitives: Connect, Publish and Subscribe to access the Telemetry, Commands and DeviceTwin features.

Choosing an MQTT Library

We are going to use MQTTNet as the foundation for this article, although the same concepts can be applied with any other MQTT client.

Establishing the connection

Azure IoT Hub offers two different authentication method for devices: Shared Security Access (SAS) Tokens and X509 Certificates. Let's start with SAS Tokens.

The MQTT protocol allow clients to connect by specifying a UserName and Password, along with the ClientId. IoTHub requires a specific format for the UserName based on the HubName, DeviceId and the SasToken, and the Password is computed with an HMAC signature adding an expiry string.

The next gist shows how to obtain the UserName and Password from the device connection string based on SAS tokens:

using System.Text;

namespace demistify_iothub
{
    public class SasAuth
    {
        private const string apiversion_2020_09_30 = "2020-09-30";
        public static string GetUserName(string hostName, string deviceId, string modelId = "") =>
            $"{hostName}/{deviceId}/?api-version={apiversion_2020_09_30}&model-id={modelId}";

        private static string Sign(string requestString, string key)
        {
            using (var algorithm = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(key)))
            {
                return Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(requestString)));
            }
        }

        public static string CreateSasToken(string resource, string sasKey, int minutes)
        {
            var expiry = DateTimeOffset.UtcNow.AddMinutes(minutes).ToUnixTimeMilliseconds().ToString();
            var sig = System.Net.WebUtility.UrlEncode(Sign($"{resource}\n{expiry}", sasKey));
            return $"SharedAccessSignature sr={resource}&sig={sig}&se={expiry}";
        }

        public static (string username, string password) GenerateHubSasCredentials(string hostName, string deviceId, string sasKey, string modelId, int minutes = 60) =>
            (GetUserName(hostName, deviceId, modelId), CreateSasToken($"{hostName}/devices/{deviceId}", sasKey, minutes));
    }
}
Enter fullscreen mode Exit fullscreen mode

IoTHub requires all MQTT connections to be protected with TLS 1.2, and can be configured using the MQTTNetConnectionOptionsBuilder with the extension method below:

using MQTTnet.Client.Options;
using System.Text;

namespace demistify_iothub
{
    public static class MqttNetExtensions
    {
        public static MqttClientOptionsBuilder WithAzureIoTHubCredentialsSas(this MqttClientOptionsBuilder builder, string hostName, string deviceId, string sasKey, string modelId = "", int sasMinutes = 60)
        {
            (string username, string password) = SasAuth.GenerateHubSasCredentials(hostName, deviceId, sasKey, modelId, sasMinutes);
            builder
                .WithTcpServer(hostName, 8883)
                .WithTls()
                .WithClientId(deviceId)
                .WithCredentials(username, password);
            return builder;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once you have your IoT Hub, and your device , grab the device credentials from the portal or CLI, and replace it in the code below, and once you run it you are connected. See this instructions to create your IoT Hub and register a device.

using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Options;

IMqttClient mqttClient = new MqttFactory().CreateMqttClient();
var connAck = await mqttClient.ConnectAsync(new MqttClientOptionsBuilder()
    .WithAzureIoTHubCredentialsSas(
        hostName: "<your-hub>.azure-devices.net",
        deviceId : "<your-device>",
        sasKey: "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
    )
    .Build());
System.Console.WriteLine(Azs$"connAck resaon: {connAck.ResultCode} IsConnected: {mqttClient.IsConnected}");

Enter fullscreen mode Exit fullscreen mode

Sending Telemetry

The first feature to explore is device telemetry, telemetry messages can be routed to other Azure services such as Storage, EventHub or EventGrid.

To send a telemetry message we just need to publish a message to a well known MQTT topic:

var pubAck = await mqttClient.PublishAsync(
        $"devices/{mqttClient.Options.ClientId}/messages/events/",
        JsonSerializer.Serialize(new { Environment.WorkingSet }) );

Console.WriteLine($"Telemetry sent with pubAck: {pubAck.ReasonCode}" );
Enter fullscreen mode Exit fullscreen mode

To verify the telemetry has been received by hub use the Azure CLI:

az iot hub monitor-events -n <your-hub>

Implement Commands

Devices connected to IoT Hub can receive command invocations by following the next pattern.

  • Subscribe to the topic $iothub/methods/POST/#
  • Process incoming messages and parse the commandName requestIdentifier and commandPayload
  • Publish the commadn response to topic $iothub/methods/res/{response.Status}/?$rid={rid}

The following code implements this pattern

await mqttClient.SubscribeAsync("$iothub/methods/POST/#");
mqttClient.UseApplicationMessageReceivedHandler(async m =>
{
    var topic = m.ApplicationMessage.Topic;
    if (topic.StartsWith("$iothub/methods/POST/"))
    {
        var segments = topic.Split('/');
        var qs = HttpUtility.ParseQueryString(segments[^1]);
        _ = int.TryParse(qs["$rid"], out int rid);
        var cmdName = segments[3];
        var cmdPayload = Encoding.UTF8.GetString(m.ApplicationMessage.Payload);

        Console.WriteLine($"Executing command {cmdName} with rid {rid} and payload {cmdPayload}");
        await mqttClient.PublishAsync($"$iothub/methods/res/200/?$rid={rid}", cmdPayload);
    }
});
Enter fullscreen mode Exit fullscreen mode

To invoke the command, and see the response from the CLI:

az iot hub invoke-device-method -n yourhub -d yourdevice --method-name myCommand --method-paylaod "Hello"

Properties (aka Device Twins)

Device Twins allows to manage the device state by using the reported and desired properties pattern.

Read DeviceTwin

To read the device twin devices can send a request by publish a message to $iothub/twin/GET/?$rid={rid} and will get the response subscribing to $iothub/twin/res/200.

The code below implements this pattern using the same mqttClient:

await mqttClient.SubscribeAsync("$iothub/twin/res/#");
mqttClient.UseApplicationMessageReceivedHandler(m => {
    var topic = m.ApplicationMessage.Topic;
    if (topic.StartsWith("$iothub/twin/res/200"))
    {
        var segments = topic.Split('/');
        var qs = HttpUtility.ParseQueryString(segments[^1]);
        _ = int.TryParse(qs["$rid"], out int rid);
        var twin = Encoding.UTF8.GetString(m.ApplicationMessage.Payload);
        Console.WriteLine(twin);
    }
});
await mqttClient.PublishAsync($"$iothub/twin/GET/?$rid=1");
Enter fullscreen mode Exit fullscreen mode

Update Device Twin

Device properties can be updated from the device and the values will be stored in the reported section of the twin. To make the update devices must send a message to the topic $iothub/twin/PATCH/properties/reported/?$rid=, as a result of this request devices can subscribe to the topic $iothub/twin/res/204 to get the updated reported version. Here is the flow:

  • Device subscribes to $iothub/twin/res/204
  • Device publish a message to $iothub/twin/PATCH/properties/reported/?$rid=
  • Device process the incoming message and extracts the updated version from the response topic

The code below implements this flow:

await mqttClient.SubscribeAsync("$iothub/twin/res/#");
mqttClient.UseApplicationMessageReceivedHandler(m =>
{
    var topic = m.ApplicationMessage.Topic;
    if (topic.StartsWith("$iothub/twin/res/204"))
    {
        var segments = topic.Split('/');
        var qs = HttpUtility.ParseQueryString(segments[^1]);
        var twinVersion = Convert.ToInt32(qs["$version"]);
        System.Console.WriteLine(twinVersion);
    }
});
await mqttClient.PublishAsync(
    "$iothub/twin/PATCH/properties/reported/?$rid=2",
    JsonSerializer.Serialize(new { myProperty = "myValue" }));
Enter fullscreen mode Exit fullscreen mode

To check the twin value with the CLI:

az iot hub device-twin show -n yourhub -d yourdevice

Handle desired property updates

Every time the desired section of the twin gets updated, IoT Hub makes the update available to connected devices who are subscribed to the topic $iothub/twin/PATCH/properties/desired/#, this flow is similar to the one implemented above for commands:

  • Device subscribe to $iothub/twin/PATCH/properties/desired/#
  • When new message is published to this topic the device reads the payload
  • Optionally the device reports back a reported property to indicate the property was accepted or rejected
await mqttClient.SubscribeAsync("$iothub/twin/PATCH/properties/desired/#");
mqttClient.UseApplicationMessageReceivedHandler(async m =>
{
    var topic = m.ApplicationMessage.Topic;
    if (topic.StartsWith("$iothub/twin/PATCH/properties/desired"))
    {
        string msg = Encoding.UTF8.GetString(m.ApplicationMessage.Payload);
        System.Console.WriteLine(msg);
    }
});
Enter fullscreen mode Exit fullscreen mode

To trigger a device update from the CLI:

az iot hub device-twin update -n yourhub -d yourdevice --desired "{'propName':value}"

Conclusion

You can access main IoTHub features with any MQTT client by connecting with the approprate crendentials and pub/sub to the predefined topics to implement device-to-cloud and cloud-to-device patterns.

Considerations

When using SaS tokens, keep in mind that tere is a default expiry interval - in this sample is 60 minutes, after that period the client will be disconnected. You can use a timer to manually re-connect before the client gets disconnected.

In a future port I will cover how to connect with X509 Certificates that dont have any expiration limt.

The UseApplicationMessageReceivedHandler method, when called multiple times, will remove the previous handlers, in a full exmaple you must parse all incoming messages from the same handler, or... (spoiler alert) use a multicast delegate as will show in a future post.

Enjoy Azure IoT Hub without an official SDK.

The complete code for this sample can be found in this gist

💖 💪 🙅 🚩
ridomin
Rido

Posted on May 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related