Azure IoT DeviceClient SDK demonstration, the basics

The cloud gateway of Azure IoT offers multiple protocols to connect to:

Programming all D2C and C2D communication yourself is pretty hard. Microsoft has made it easy to communicate by providing SDKs, both for device communication and IoT Hub manipulation.

In this blog, we dive into what is offered by the Device SDKs.

Azure IoT supports many SDKs for programming languages to make it simple to communicate:

  • ANSI C C99
  • Java
  • NodeJS
  • Python
  • IOS
  • .Net

The capabilities offers should be fairly on par fo all languages.

Let’s check out the capabilities using a C# example.

Creating the device client instance

We start with the creation of a device registration in the IoT Hub:

As you can see, each device has it’s own security keys. Here I use symmetric keys for this demonstration.

Note: in production, I recommend to use x509 certificates or TPM for much stronger security if possible.

Create the C# application

Once the device is registered, we create a .Net Core C# application and connect a few Nuget packages:

Note: The Microsoft.Azure.Devices.Client should be enough to start communicating with the cloud. The other two are just needed for sending files over to the cloud. You can skip these two if not needed.

The main portion of this application looks like this:

private static void Main(string[] args)
{
    var connectionString = "[the connection string of your device]";

    using var deviceClient = DeviceClient.CreateFromConnectionString(connectionString);

    //// Set various callback messages: connection status, device twin, a single named method, multiple methods
    SetVariousCallbackMethods(deviceClient);

    //// open connection explicitly
    deviceClient.OpenAsync().Wait();

    //// Force the retrieval of the devicetwin
    ForceDeviceTwinRetrieval(deviceClient);

    //// send a single message
    SendSingleMessage(deviceClient);

    //// send multiple message in batch in one call
    SendBatchOfMessages(deviceClient);

    //// Send a file to the Blob storage configured in the IoT Hub
    SendFileToBlobStorage(deviceClient);

    //// Stall application closing
    Console.WriteLine("Press a key to exit...");
    Console.ReadKey();
}

This example falls apart in several steps:

  1. Create the device client
  2. Set up callback functionality for C2D communication
  3. Open the connection
  4. Retrieve the device twin by force
  5. Send a single message
  6. Send a batch of messages
  7. Send a blob file

The creation of the client is already shown. Just pass it the connection string and the client is ready to go.

You can also pass extra transport settings (to control the communication protocol) and client options (eg. to support Azure IoT Plug & Play) but I leave it for now like this.

The connection is not yet activated, the next step is to set up C2D communication.

Set up callback functionality for C2D communication

When talking about IoT usually it is referred as sending messages from a device to the cloud (D2C). But sending messages back from the cloud to the device (C2D) is equally important.

Here I have put in place all different ways you can react to information sent from the cloud to the device:

private static void SetVariousCallbackMethods(DeviceClient deviceClient)
{
    deviceClient.SetConnectionStatusChangesHandler(ConnectionStatusChangeHandler);

    deviceClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertyChanged, deviceClient).ConfigureAwait(false);

    deviceClient.SetMethodHandlerAsync("Ping", HandleMethodPing, deviceClient).ConfigureAwait(false);

    deviceClient.SetMethodDefaultHandlerAsync(HandleDefaultMethods, deviceClient).ConfigureAwait(false);

    //// Check for COMMAND messages (Warning: BLOCKING calls); Do not forget to return a 'Complete' or 'reject' or 'abandon'
    var thread = new Thread(() => ThreadBodyCommand(deviceClient));
    thread.Start();

    Console.WriteLine("Callback methods are set");
}

private static void ConnectionStatusChangeHandler(ConnectionStatus status, ConnectionStatusChangeReason reason)
{
    Console.WriteLine($"Connection Status Changed to {status} ({reason}) at {DateTime.Now}");
}

private static async Task OnDesiredPropertyChanged(TwinCollection desiredProperties, object userContext)
{
    Console.WriteLine("One or more device twin desired properties changed:");
    Console.WriteLine(JsonConvert.SerializeObject(desiredProperties));

    var client = userContext as DeviceClient;

    var reportedProperties = new TwinCollection
    {
        ["DateTimeLastDesiredPropertyChangeReceived"] = DateTime.Now
    };

    await client.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false);

    Console.WriteLine("Sent current time as reported property to device twin");
}

private static Task<MethodResponse> HandleMethodPing(MethodRequest methodRequest, object userContext)
{
    Console.WriteLine($"method {methodRequest.Name} handled with body {methodRequest.DataAsJson}");

    return Task.FromResult(new MethodResponse(new byte[0], 200));
}

private static Task<MethodResponse> HandleDefaultMethods(MethodRequest methodRequest, object userContext)
{
    Console.WriteLine($"Default method {methodRequest.Name} handled with body {methodRequest.DataAsJson}");

    return Task.FromResult(new MethodResponse(new byte[0], 200));
}

private static async void ThreadBodyCommand(object deviceClient)
{
    var client = deviceClient as DeviceClient;

    Console.WriteLine("Waiting for C2D messages (aka commands)");

    while (true)
    {
        // The following line is blocking until a timeout occurs
        using var message = await client.ReceiveAsync();

        if (message == null)
        {
            Console.WriteLine("Timeout. Command is null");
            continue;
        }

        string data = Encoding.UTF8.GetString(message.GetBytes());

        Console.WriteLine($"A message is received with body {data}");

        await client.CompleteAsync(message); // mark the message as handled
        //await client.RejectAsync(message); // drops the message as unhandled
        //await client.AbandonAsync(message); // puts message back on queue
    }
}

deviceClient.SetConnectionStatusChangesHandler

OK, this is not directly related to C2D communication but this method provides information about connecting and disconnecting the communication.

The client will not fire the ConnectionStatusChange event right away when the network is disconnected but it will fire the event if it tries to send/receive something on the wire.

Currently, there is a retry mechanism which will trigger the re-connection after 4 min. and that is when the client detects the network problem.

deviceClient.SetDesiredPropertyUpdateCallbackAsync

Each IoT Hub device registration has a device twin. In this twin we can find desired properties (information we desire to be sent to the device when a connection is made):

Here we see the ‘Interval’ and the ‘test’ properties.

This callback function is triggered when the device twin in changed. As we will see later on, we can force the client to check the latest situation.

In ‘OnDesiredPropertyChanged’ we see the properties and we send back a response. This is then part of the ‘reported properties’:

Here, the current date and time are sent.

deviceClient.SetMethodHandlerAsync

When a device is connected in real-time, it is possible to send a Direct Method.

In the example above, we are interested in some method called “Ping”:

Note: Direct method names are case-sensitive for this method.

We can pass a body JSON message to the device.

If the device is connected to the IoT Hub and a method is sent with the correct name, we see the arrival of the message body:

We have to pass a method response. A value ‘200’ means the message is handled well.

We also can respond with an extra JSON response message back:

In this case, the response body was left empty.

deviceClient.SetMethodDefaultHandlerAsync

If you go for the previous method handler, you have to specify a separate handler for all known method names. This is quite cumbersome.

If you want to be more flexible or you want to support case-insensitive direct method names, the ‘SetMethodDefaultHandler’ is the solution for you.

Each method name not handled by a specific ‘SetMethodHandler’ callback function is sent to this non-specific callback function.

In this direct method function, you have control over both the name and the body of the request.

You can also use it to respond to the cloud that some names are not handled or now obsolete (when the interface of the device is changed).

The previous two ways of communication are examples of direct methods. This is a common pattern for executing near-real-time actions performed on the device. The fact it can send back a response with a body is very powerful.

The only drawback is the fact the device has to be connected to the IoT Hub. If the device is not connected, the direct method will timeout and the action is lost in time.

What if we could send commands, even when the device is not connected yet?

deviceClient.ReceiveAsync

The IoTHub also supports sending messages to devices while the device is not connected (yet).

From the cloud perspective, a message can be prepared and put on the device ‘C2D message queue’. This is sometimes also referred to as commands:

Sending a message this way is simple. Even though the device is not connected at that time, the message is ‘sent’ successfully:

But the message, or command, is still in the queue and not yet received by the device already.

Once the device is connected to the internet, it can check if a message is available on the ‘C2D message queue’:

Here I send three messages already before the device was put online:

As you can see, once the device is connected, it starts reading the messages one by one.

There are a few things to keep in mind though:

  • Due to the async pattern, sending back an elaborate response (with a response body is not supported). This is a fire-and-forget action
  • Checking out if there is a message ready to be read from the IoT Hub is a blocking call. therefor I use the “var thread = new Thread(() => ThreadBodyCommand(deviceClient)); thread.Start();” so normal operation is not blocked
  • If there is no message to be read, the ReceiveAsync method will timeout and return a message being null. So check the content of the variable.
  • Just reading a message is not enough! You need to decide if you accept it, reject it, or abandon it. If you do not accept or reject it, the message will stay of the queue and you will be offered the same message over and over again until you finally accept or reject it.

Commands are a powerful solution to send C2D messages for eg. low powered devices that only connect once in a while.

Note: In the past, I used commands to loRaWan devices sitting behind a third-party cloud.

Commands also offer extra parameters, to give the message some context (eg. a priority). This is not offered by direct methods.

Open the connection

Until now we have seen how to set up the C2D callbacks. This has no meaning until we open the connection.

Just execute ‘deviceClient.OpenAsync’ and the device will try to contact the IoT Hub.

Once the connection is made, the ‘SetConnectionStatusChangesHandler’ function will execute:

From there, we are ready for D2C and C2D communication.

Retrieve the device twin by force

As seen during the setup of the callback function, we can detect changes to the device twin desired properties.

This is not foolproof. If the device is not able to store the desired properties or is not connected all the time, there is a chance we are missing the desired properties changes.

Luckily, it is possible to check for the current set of desired properties:

private static void ForceDeviceTwinRetrieval(DeviceClient deviceClient)
{
    //// Force the retrieval and handling of the desired twin properties

    var twin = deviceClient.GetTwinAsync().Result;

    OnDesiredPropertyChanged(twin.Properties.Desired, deviceClient).Wait();

    Console.WriteLine("The Devicetwin is forced retrieved");
}

This will result in a retrieval:

The first call was forced. Later on, I changed the desired properties on the cloud side.

This way, I am in full control of what the desired properties are offering.

It’s up to you if you want to use the callback function and respond directly to desired property changes or you want to be in control when you handle the changes (if there are any) by reading the values once in a while.

Send a single message

We finally reach the part where we are sending messages to the cloud.

Sending a single message is the most common pattern and simple to execute.

Just write a piece of JSON and send it over to the IoT Hub:

private static void SendSingleMessage(DeviceClient deviceClient)
{
    string jsonData = "{ \"single\":true }";
    
    var message = new Message(Encoding.ASCII.GetBytes(jsonData));

    message.Properties.Add("messagetype", "normal");

    deviceClient.SendEventAsync(message).Wait();

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

Once executed, we will see the arrival of the message in the cloud (using the Azure IoT Explorer):

I added an application property named ‘messagetype’. As you can see, the IoT Hub adds multiple system properties also.

Send a batch of messages

Keeping up the IoT Hub connection can bring some load to the device. If you prefer to send multiple messages in one go, as a batch, this is supported too:

private static void SendBatchOfMessages(DeviceClient deviceClient)
{
    string jsonDataOne = "{ \"number\": \"one\" }";
    var messageOne = new Message(Encoding.ASCII.GetBytes(jsonDataOne));
    messageOne.Properties.Add("batch", "true");

    string jsonDataTwo = "{ \"number\": \"two\" }";
    var messageTwo = new Message(Encoding.ASCII.GetBytes(jsonDataTwo));
    messageTwo.Properties.Add("batch", "true");

    deviceClient.SendEventBatchAsync(new List<Message> { messageOne, messageTwo }).Wait();

    Console.WriteLine("Messages are sent in batch");
}

Just create a list of multiple messages and send them out in one go:

As you can see, both messages are sent in one call.

Send a blob file

The Azure IoT Device SDK offers the ability to send files to the cloud too. This is available for IoT devices only. Azure IoT Edge devices must make use of a separate module to have the same abilities.

The file to be sent can be any file or format. You could for example upload photos or sound files to be analyzed in the cloud.

Note: Sending photos with persons on it to the cloud or with the voice of a person to the cloud could violate GDPR restrictions. Be aware of the privacy rules for individuals.

So this can also be the third option to send telemetry to the cloud. Just append messages in a blob and send them to some blob storage affiliated to the IoT Hub.

This has several implications:

  • You have to affiliate a blob storage container to the IoT Hub before this can work
  • Blobs are not messages. So any message in the blob will not be part of the message output stream of the IoT Hub
  • Because potentially thousands of messages being part of the blob only land in the blob storage, this will not impact the daily message count restriction of the IoT Hub. Uploading a file of several MBs only cost 2 messages on the IoT Hub.

So sending files can take the load from the IoT Hub:

File transfer to Azure Storage is not metered by IoT Hub. File transfer initiation and completion messages are charged as messaged metered in 4-KB increments. For example, transferring a 10-MB file is charged as two messages in addition to the Azure Storage cost.

Sending a file is simple:

private static void SendFileToBlobStorage(DeviceClient deviceClient)
{
    //// Send a file

    using var stream = File.OpenRead("upload.txt");

    var fileUploadSasUri = deviceClient.GetFileUploadSasUriAsync(
        new FileUploadSasUriRequest { 
            BlobName = $"upload-{DateTime.Now.Ticks}.txt" }).Result;

    var correlationId = fileUploadSasUri.CorrelationId;

    var isSuccess = false;

    try
    {
        var blobClient = new BlobClient(fileUploadSasUri.GetBlobUri());

        var response = blobClient.UploadAsync(stream).Result;

        isSuccess = (response.GetRawResponse().Status == 201);
    }
    catch
    {
        // ignore all exceptions
    }
    finally
    {
        // Be sure to complete the fileupload so we prevent : Number of active file upload requests exceeded limit

        var fileUploadCompletionNotification = new FileUploadCompletionNotification { CorrelationId = correlationId, IsSuccess = isSuccess };

        deviceClient.CompleteFileUploadAsync(fileUploadCompletionNotification).Wait();

        Console.WriteLine($"A file is sent; success={isSuccess}");
    }
}

In this case, I added a file to the project named upload.txt:

The context is just some text:

The only thing to do more is affiliating a blob storage container to the IoT Hub. This can be done in the Azure portal:

Once the file is uploaded, it is placed in the blob storage container:

I experimented with the filenames. On the device side, I made the device names unique at first. Each time the file was sent correctly:

So I also tried to upload the same file name ‘upload.txt’ multiple times. This only works one time, the second time the upload fails:

So uploading a file seems simple. Though, as seen in the code, there is a little flow to follow:

  1. Create a ‘fileUploadSasUri’ for the request to upload a file
  2. Check the ‘CorrelationId’ for later reference
  3. upload the file
  4. Check the response status which should be 201
  5. Construct a fileUploadCompletionNotification and complete the file upload with ‘deviceClient.CompleteFileUploadAsync’

If these last steps are not executed, at some point you are not allowed to upload files anymore due to the IoT Hub quota limitations:

Conclusion

In this blog, almost all features of the Device Client SDK are shown.

Both uploading single D2C messages, messages in a batch and even file upload is demonstrated. And we have seen how to react on C2D messages, and direct methods. Finally, the usage if device twin desired and reported properties are demonstrated.

The examples are shown in .Net C#. The same functionality is available in other programming languages too.

The complete code example is shown on GitHub Gist.