Introduction to the IoT Edge SDK, part 3

In the previous blogs of this series, you have been introduced to the Module architecture of the IoT Edge SDK. In my last blog, we have sent data to the IoT Hub.

But the IoTHub has more capabilities for devices. Think of ‘device twins’, ‘direct methods’ and ‘message to device’. Are these supported too?

At this moment, the IoTHub module supports commands, messages to devices, coming from the IoT Hub.

Let’s see what we have to do to get this working.

This blog is the third part of a series:

  • Part one, how to use modules, gateway configuration and the broker
  • Part two, connecting to the IoT Hub
  • Part three, message to device
  • Part four, IoT Hub Routing

Strategy of receiving commands

We will start with the solution from part two. The following modules are used:

  • Custom Sensor module (which interacts with the ‘RealWorldDeviceClient’)
  • Identity Mapping module
  • IoT Hub module

Note: For clarity, some modules names have been updated subtly. The logic remains the same, though.

Both the Device Explorer tool and the Azure Portal IoT Hub Device Explorer (and of course your own programming skills) give the ability to send commands back to devices.

In general, an IoT Hub device is represented by a (stateful) Device Client which has an active MQTT or AMQP connection to the IoT Hub. And this client receives the commands and passes them on.

But in this case, in the gateway, IoT Hub devices are represented by the IoT Hub module, there are no clients (yet).

This is the same case as my Lora bridge. There, Lora devices deliver messages to a Lora provider like (The Things Network) and my bridge passes both messages and commands back and forth between the Lora Provider Portal and the Azure IoT Platform.

When that bridge receives a message from a Lora device, a new IoT Hub device client instance is activated for that specific device and added to a list of other instances. So the list contains multiple client instances, for each Lora device one client.

The same approach is used here!

Looking at the documentation of the IoT Hub module, we are told to register devices by sending messages to the IoT Hub containing specific properties. For each device, we have to send a properties combination of a IoT Hub Device name, a IoT Hub Device key and the ‘deviceFunction:register’.

And registering is done only once for each device.

Registering devices using a module

There is no default module for registering devices. You have to build one yourself.

We have to send device credentials to the IoT Hub combined with extra properties. This looks like we can call the mapper…

So this is my registration module:

using Microsoft.Azure.IoT.Gateway;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MyFirstEdge
{
  public class RegisterModule : IGatewayModule, IGatewayModuleStart
  {
    private Broker broker;
    private RegisterDevice[] _configDevices;

    public void Create(Broker broker, byte[] configuration)
    {
      this.broker = broker;
      var config = Encoding.UTF8.GetString(configuration);

      _configDevices = JsonConvert.DeserializeObject<RegisterDevice[]>(config);
    }

    public void Start()
    {
       var thread = new Thread(new ThreadStart(ThreadBody));
       thread.Start();
       Console.WriteLine($"{nameof(RegisterModule)} started");
    }

    public void Destroy()
    {
    }

    public void Receive(Message received_message)
    {
    }

    public void ThreadBody()
    {
      foreach (var device in _configDevices)
      {
        var data = new Dictionary<string, string>
        {
          { "source", "register" },
          { "macAddress", device.macAddress },
          { "deviceFunction", "register"}
        };

        var messageToPublish = new Message(string.Empty, data);

        broker.Publish(messageToPublish);
      }
    }
  }

  public class RegisterDevice
  {
    public string macAddress { get; set; }
  }
}

For each device in the argument (an array of MAC addresses) a register command is sent to the IoT Hub Module using the Mapping Module.

Note: This is done in the start event of the module. Technically, this logic could be combined with the Custom Sensor module.

So this is the configuration of the module:

...
{
  "name": "register_module",
  "loader": {
    "name": "dotnet",
    "entrypoint": {
      "assembly.name": "MyFirstEdge",
      "entry.type": "MyFirstEdge.RegisterModule"
    }
  },
  "args": [
    {
      "macAddress": "01:01:01:01:01:01"
    }
  ]
}
...

And also check out how to link this module:

...
{
  "source": "register_module",
  "sink": "identity_mapping_module"
}
...

If we run the gateway, in the Device Explorer tool we can see the arrival of the message:

Note: There is no other way to check the number of device clients in the IoT Hub module. The IoT Hub module does not confirm the creation of clients. And once registered, a device can not be deregistered.

Rework on the Real World Device Client class

At the end of this blog, you will have experienced how to receive a command. This will be done by yet another module with access to the same ‘Real World Device Client class’.

But just before that, we have to extend our ‘proxy’, which represents a real device:

  • We have to add some state which can be changed by a command. A simple property ‘IsStarted’
  • [optional] because the class is loaded by two modules, we have to transform the class into a Singleton so both know about the same state

This is how the class looks now:

using System;

namespace MyFirstEdge
{
  public class RealWorldDeviceClient : IDisposable
  {
    private static RealWorldDeviceClient _realWorldDeviceClientSingleton = null;
    private Random _random = new Random();
    private bool _started;

    public double Humidity { get; set; }
    public double Temperature { get; set; }
    public bool IsStarted { get; set; }

    public static RealWorldDeviceClient GetInstance()
    {
      if (_realWorldDeviceClientSingleton == null)
      {
        _realWorldDeviceClientSingleton = new RealWorldDeviceClient();
      }

      return _realWorldDeviceClientSingleton;
    }

    public void ReadSensors(string ipAddress, string name, string password)
    {
      Humidity = Math.Round(_random.NextDouble() * 100, 3, MidpointRounding.AwayFromZero);
      Temperature = Math.Round(_random.NextDouble() * 100, 3, MidpointRounding.AwayFromZero);
    }

    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
      if (!disposedValue)
      {
        if (disposing)
        {
          // HOLD: dispose managed state (managed objects). 
        }

        disposedValue = true;
      }
    }

    public void Dispose()
    {
      Dispose(true);
    }
  }
}

Now we have a ‘real device’ which both generates data and has a state.

Rework on the Custom Sensor module

This is the module which ingests the telemetry. We have built it last time. But we want to expose the state in the data send to the IoT Platform. So we alter the message to send a bit. And we consume the real device as a singleton:

using System;
using System.Collections.Generic;
using Microsoft.Azure.IoT.Gateway;
using System.Threading;
using Newtonsoft.Json;

namespace MyFirstEdge
{
  public class CustomSensorModule : IGatewayModule, IGatewayModuleStart
  {
    private Broker broker;
    private ConfigDevice[] _configDevices; 
    private RealWorldDeviceClient _client;

    public void Create(Broker broker, byte[] configuration)
    {
      this.broker = broker;
      var config = System.Text.Encoding.UTF8.GetString(configuration);
      _configDevices = JsonConvert.DeserializeObject<ConfigDevice[]>(config);
      _client = RealWorldDeviceClient.GetInstance(); // <- Use singleton
    }

    public void Start()
    {
      var thread = new Thread(new ThreadStart(ThreadBody));
      thread.Start();
      Console.WriteLine("CustomSensorModule started");
    }

    public void Destroy()
    {
      _client.Dispose();
    }

    public void Receive(Message received_message)
    {
    }

    public void ThreadBody()
    {
      while (true)
      {
        foreach (var device in _configDevices)
        {
          _client.ReadSensors(device.ipAddress, device.name, device.password);

          var data = new Dictionary<string, string>
          {
            { "source", "sensor" },
            { "macAddress", device.macAddress }
          };

          var dataMessage = FillDataMessage(); // <- exposing state
          var message = JsonConvert.SerializeObject(dataMessage);
          var messageToPublish = new Message(message, data);
          this.broker.Publish(messageToPublish);
          Thread.Sleep(5000);
        }
      }
    }

    private DataMessage FillDataMessage()
    {
      return new DataMessage
      {
        humidity = _client.Humidity,
        temperature = _client.Temperature,
        isStarted = _client.IsStarted ? 1 : 0,
      };
    }
  }

  public class DataMessage
  {
    public double humidity { get; set; }
    public double temperature { get; set; }
    public int isStarted { get; set; }
  }
}

Now, sending data from the real device is updated. This brings us to the last part, retrieving commands.

Custom Command module

We had to write a new Registration module. We will also write a new custom Command module.

Note: technically, both the sensor module and command module could be combined. I have chosen to split these two blocks of functionality for clarity.

How do we receive commands?

If we look at the IoT Hub module documentation, we are told that the command is accompanied by a source called ‘iothub’ and the IoT Hub device name.

And we have to pass these command messages to the Identity Mapper module which will map the device name to the real world MAC address.

So we create this new Custom Command module which listens for messages coming from the Identity Mapper module.

This is a bit tricky, the mapper module exposes multiple kinds of messages. We are not interested in messages passed from the mapper to the IoT Hub (sent by the sensor module). When receiving a message we first check if the properties list contains a MAC address. Then we know it is an actual command:

using Microsoft.Azure.IoT.Gateway;
using Newtonsoft.Json;
using System;
using System.Linq;

namespace MyFirstEdge
{
  public class CustomCommandModule : IGatewayModule, IGatewayModuleStart
  {
    private Broker broker;
    private ConfigDevice[] _configDevices;
    private RealWorldDeviceClient _client;

    public void Create(Broker broker, byte[] configuration)
    {
      this.broker = broker;
      var config = System.Text.Encoding.UTF8.GetString(configuration);

      _configDevices = JsonConvert.DeserializeObject<ConfigDevice[]>(config);
      _client = RealWorldDeviceClient.GetInstance();
    }

    public void Start()
    {
      Console.WriteLine("CustomCommandModule started");
    }

    public void Destroy()
    {
      _client.Dispose();
    }

    public void Receive(Message received_message)
    {
      var content = System.Text.Encoding.UTF8.GetString(received_message.Content, 0, received_message.Content.Length);

      if (received_message.Properties["source"] == "mapping"
          && received_message.Properties.ContainsKey("macAddress"))
      {
        Console.WriteLine($"CustomCommandModule: device {received_message.Properties["macAddress"]} received {content}");

        var device = _configDevices.FirstOrDefault(x => x.macAddress == received_message.Properties["macAddress"]);

        if (device != null)
        {
          _client.ReadSensors(device.ipAddress, device.name, device.password);

          if (content.ToLower().Contains("start"))
          {
            _client.IsStarted = !_client.IsStarted;
          }
        }
      }
    }
  }
}

Once a message with a MAC address is received, we toggle the state of the actual device.

This is the configuration of this module:

...
{
  "name": "custom_command_module",
  "loader": {
    "name": "dotnet",
    "entrypoint": {
      "assembly.name": "MyFirstEdge",
      "entry.type": "MyFirstEdge.CustomCommandModule"
    }
  },
  "args": [
    {
      "ipAddress": "192.168.1.2",
      "macAddress": "01:01:01:01:01:01",
      "name": "name",
      "password": "password"
    }
  ]
}
...

So we act on retrieving commands.

Putting everything together

This is the complete module linking configuration I use:

"links": [
  {
    "source": "register_module",
    "sink": "identity_mapping_module"
  },
  {
    "source": "custom_sensor_module",
    "sink": "identity_mapping_module"
  },
  {
    "source": "identity_mapping_module",
    "sink": "iothub_module"
  },
  {
    "source": "iothub_module",
    "sink": "identity_mapping_module"
  },
  {
    "source": "identity_mapping_module",
    "sink": "custom_command_module"
  },
  {
    "source": "identity_mapping_module",
    "sink": "message_printer_module"
  }
]

So when I run the gateway, I can see how the device is registered and the first telemetry including the state is sent:

CustomSensorModule started
CustomCommandModule started
MessagePrinterModule started
RegisterModule started
Gateway is running. Press return to quit.
Info: Retry policy set (5, timeout = 0)
Info: Transport state changed from AMQP_TRANSPORT_STATE_NOT_CONNECTED to AMQP_TRANSPORT_STATE_CONNECTING
Info: Transport state changed from AMQP_TRANSPORT_STATE_CONNECTING to AMQP_TRANSPORT_STATE_CONNECTED
Printer message:
Printer properties: Key 'source' / Value 'mapping'; Key 'deviceFunction' / Value 'register'; Key 'deviceName' / Value 'IoTEdgeSdkDevice'; Key 'deviceKey' / Value '[key]';
Printer message: {"humidity":72.278,"temperature":79.808,"isStarted":0}
Printer properties: Key 'source' / Value 'mapping'; Key 'deviceName' / Value 'IoTEdgeSdkDevice'; Key 'deviceKey' / Value '[key]';

Now let’s send a command. You can use the Device Explorer tool:

Or you can use the Azure portal IoT Hub Device Explorer:

Either way, we will see the arrival of the command in the console and the toggle of the state:

In blue, the arrival of the commands is shown. In red, the toggle of the state is shown.

Conclusion

In this blog, we have seen how the process of registering devices and the retrieval of commands are handled.

It is possible to combine the logic in these modules but I leave that to the reader’s discretion.

 

Advertenties