Visualizing Azure IoT Edge using local dashboard

In my last series of blogs, we first looked at how to deploy a non-IoT Edge module using Azure IoT Edge.

For this example, I used a NodeJS website running SocketIO. It was possible to access this website with a default SocketIO chat application.

After that, we looked at how to add some charts in the HTML page offered by the NodeJS server.

Let’s see how we can combine this all into one solution. Let’s build a local for raw Azure IoT Edge telemetry.

This is the last part of a series of blogs:

  1. Deploying a NodeJS server with SocketIO to Docker using Azure IoT Edge
  2. Show telemetry in NodeJS using SocketIO and HighCharts
  3. Visualizing Azure IoT Edge using local dashboard

<UPDATE 2018-06-29>: This code is based on the Public Preview of Azure IoT Edge. With the recent General Availability (GA), Microsoft broke (a little) the C# code. So if you want to use this, I leave it to you to update the code.

But with GA, the C# intermediate module has become obsolete. It is now possible to create first-class NodeJS modules. And these have access to the routing. </ UPDATE 2018-06-29>

Prerequisites

First, let’s look what at what we have already:

  • NodeJS website deployed as a Docker Image
  • The website contains an HTML page with SocketIO on it and a High Charts chart
  • The chat application tries to convert incoming messages into values like the temperature and shows it on the chart
  • The NodeJS module is deployed using Azure IoT Edge
  • The image is running with an exposed port 80
  • The website is accessible by both local web browsers and a .Net Core C# application

This results in something like this:

I multiplied the Chart into four separate lines, temperature, pressure, etc.

And the chart app can now handle messages coming in the same format as the standard Microsoft simulated Temperature sensor exposes:

{"machine":{"temperature":102.69594449559025,"pressure":10.307132917219143},"ambient":{"temperature":20.804696279254134,"humidity":24},"timeCreated":"2018-04-17T10:46:19.2253596Z"}

Let’s try to connect this to a running “microsoft/azureiotedge-simulated-temperature-sensor” module.

Building and deploying the intermediate module

So we have something like this (built in the previous blogs):

But we need something like:

We generate messages on the routing by deploying the Temperature simulation of Microsoft.

This will generate the messages we can represent (Module T generates messages).

But these messages can never arrive at the NodeJS server (Module B), simply because of the fact this NodeJS server is not attached to the routing.

So we need a module A which speaks both worlds: it must retrieve messages from the routing and put them on the Socket IO chat service.

So I build a C# module based on this tutorial. I transformed the .Net Core application from my previous blog into an Azure IoT Edge C# module:

namespace SocketIoModule
{
  using ...

  using Quobject.EngineIoClientDotNet.ComponentEmitter;
  using Quobject.SocketIoClientDotNet.Client;

  class Program
  {
    static void Main(string[] args)
    {
      ...
    }

    ///

    /// Handles cleanup operations when app is cancelled or unloads
    /// 

    public static Task WhenCancelled(CancellationToken cancellationToken)
    {
      ...
    }

    ///

    /// Add certificate in local cert store for use by client for secure connection to IoT Edge runtime
    /// 

    static void InstallCert()
    {
      ...
    }

    ///

    /// Initializes the DeviceClient and sets up the callback to receive
    /// messages containing temperature information
    /// 

    static async Task Init(string connectionString, bool bypassCertVerification = false)
    {
      Console.WriteLine("Connection String {0}", connectionString);

      MqttTransportSettings mqttSetting = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
      // During dev you might want to bypass the cert verification. It is highly recommended to verify certs systematically in production
      if (bypassCertVerification)
      {
      mqttSetting.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
      }

      ITransportSettings[] settings = { mqttSetting };

      // Open a connection to the Edge runtime
      DeviceClient ioTHubModuleClient = DeviceClient.CreateFromConnectionString(connectionString, settings);

      // Execute callback function during Init for Twin desired properties
      var twin = await ioTHubModuleClient.GetTwinAsync();
      await onDesiredPropertiesUpdate(twin.Properties.Desired, ioTHubModuleClient);

      // Attach callback for Twin desired properties updates
      await ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(onDesiredPropertiesUpdate, ioTHubModuleClient);

      await ioTHubModuleClient.OpenAsync();
      Console.WriteLine("IoT Hub module client initialized.");

      // Register callback to be called when a message is received by the module
      await ioTHubModuleClient.SetInputMessageHandlerAsync("input1", PipeMessage, ioTHubModuleClient);
    }

    private static string _address = "http://localhost:80";

    static Task onDesiredPropertiesUpdate(TwinCollection desiredProperties, object userContext)
    {
      if (desiredProperties.Count == 0)
      {
        return Task.CompletedTask;
      }

      try
      {
        Console.WriteLine("Desired property change:");
        Console.WriteLine(JsonConvert.SerializeObject(desiredProperties));

        var deviceClient = userContext as DeviceClient;

        if (deviceClient == null)
        {
          throw new InvalidOperationException("UserContext doesn't contain " + "expected values");
        }

        var reportedProperties = new TwinCollection();

        if (desiredProperties["address"] != null)
        {
          _address = desiredProperties["address"];
          reportedProperties["address"] = _address;

          _socket = null;

          System.Console.WriteLine($"Socket reset to {_address}");
        }

        if (reportedProperties.Count > 0)
        {
          deviceClient.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false);
        }
      }
      catch (AggregateException ex)
      {
        foreach (Exception exception in ex.InnerExceptions)
        {
          Console.WriteLine();
          Console.WriteLine("Error when receiving desired property: {0}", exception);
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine();
        Console.WriteLine("Error when receiving desired property: {0}", ex.Message);
      }

      return Task.CompletedTask;
    }

    private static Emitter _socket = null;

    private static bool _eventsConnected = false;

    ///

    /// This method is called whenever the module is sent a message from the EdgeHub.
    /// It just pipe the messages without any change.
    /// It prints all the incoming messages.
    /// 

    static async Task PipeMessage(Message message, object userContext)
    {
      var deviceClient = userContext as DeviceClient;

      if (deviceClient == null)
      {
        throw new InvalidOperationException("UserContext doesn't contain " + "expected values");
      }

      byte[] messageBytes = message.GetBytes();
      string messageString = Encoding.UTF8.GetString(messageBytes);
      Console.WriteLine($"Received message to broadcast: [{messageString}]");

      if (!string.IsNullOrEmpty(messageString))
      {
        //// Do SocketIo stuff

        if (_socket == null)
        {
          System.Console.WriteLine($"Connecting to {_address} after reset");

          _socket = IO.Socket(_address);

          if (!_eventsConnected)
          {
            _eventsConnected = true;

            _socket.On(Socket.EVENT_CONNECT, () =>
            {
              Console.WriteLine(Socket.EVENT_CONNECT.ToString());
            });

            _socket.On("chat message", (data) =>
            {
              Console.WriteLine($"Chat: {data}");
            });
          }
        }

        //// SocketIo Server is connected

        _socket.Emit("chat message", messageString);

        //// Do original stuff

        var pipeMessage = new Message(messageBytes);
        foreach (var prop in message.Properties)
        {
          pipeMessage.Properties.Add(prop.Key, prop.Value);
        }

        await deviceClient.SendEventAsync("output1", pipeMessage);
        Console.WriteLine("Received message sent");
      }
      return MessageResponse.Completed;
    }
  }
}

So what we do basically, we deploy a module which retrieves routed message at input1 and sends them out to “address” using SocketIO.

Build and deploy this module to the IoT Edge with the following routing:

"temp2socket": "FROM /messages/modules/tempSensor/* INTO BrokeredEndpoint(\"/modules/socketiomodule/inputs/input1\")"

Now see… nothing is happening.

The messages are arriving at our new SocketIO module but the website is not receiving them, why?

Virtual networks in Docker

The problem lays in the fact that Docker modules are not aware of each other by default. In fact, all traffic, inbound and outbound, is dropped.

We have to give the docker modules special rights to communicate with each other, here we have to grand the SocketIO module access to the NodeJS module.

For that, we use a Docker network!

The new picture will look something like this:

Creating this network needs some command line magic:

docker network create --attachable --subnet=172.20.0.1/16 --gateway=172.20.0.1 testiobridge
docker network connect --ip 172.20.0.42 testiobridge ldb
docker network connect --ip 172.20.0.43 testiobridge socketio

So first we create a virtual network in Docker, complete with a subnet and a gateway.

After that, we attach the two modules: SocketIO and the LDB (local dashboard).

So now we have a network running on top of our Docker images but we are not using it yet!

go back to the module twin of our SocketIO module:

The SocketIO module tries to push SocketIO messages to the address ‘http://localhost:80&#8217;. And although this is a correct address for the LDB module and our browsers, this address is not reachable/forbidden to the SocketIO module.

The SocketIO module has to use the virtual network address: http://172.20.0.42 .

If you inspect the virtual network with:

docker network inspect testiobridge

you will get a message like:

[
  {
    "Name": "testiobridge",
    "Id": "e1939d844cdf180505594511305c557fb3e5566916fdc6ab1e4f11cbee8f742e",
    "Created": "2018-04-17T09:24:12.5892709Z",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
    "Driver": "default",
    "Options": {},
    "Config": [
       {
         "Subnet": "172.20.0.1/16",
         "Gateway": "172.20.0.1"
       }
     ]
  },
  "Internal": false,
  "Attachable": true,
  "Ingress": false,
  "ConfigFrom": {
    "Network": ""
  },
  "ConfigOnly": false,
  "Containers": {
    "5e7c1d73efe9efc0151aed73765a1d2a1508343efbd9b3ec57d72b335877f74a": {
      "Name": "ldb",
      "EndpointID": "4b9e4c0f7fb6c1427e5991b8bf86064f17ba6cd36474ec877b52cd0550999c82",
      "MacAddress": "02:42:ac:14:00:2a",
      "IPv4Address": "172.20.0.42/16",
      "IPv6Address": ""
    },
    "9d12fe3d4e19d09b73630cb738ce4f14f33093c5199fb2b4c0499ac842caac41": {
      "Name": "socketio",
      "EndpointID": "9d527bdb29317e65bfd76da8d0964b8bdc619d6647b2b19557713376a1aab53c",
      "MacAddress": "02:42:ac:14:00:2b",
      "IPv4Address": "172.20.0.43/16",
      "IPv6Address": ""
    }
  },
  "Options": {},
  "Labels": {}
  }
]

Both modules are shown, and these are related to the network.

Be careful. If you alter a module (newer version etc.), chances are high the connection between that module and the network is dropped.

Note: I tried to use the “Container Create Options”. But I did not get it right to let a module automatically connected to an (existing) network.

So in the end, after we correct the address, we get our local dashboard filled with telemetry:

Conclusion

So now we have running a local dashboard for our Azure IoT Edge gateway. This is a great way to represent technical data, running on the same machine.

As you have seen, we had to add a C# module to do the migration of the messages coming from the routing. If Microsoft starts providing an Azure IoT Edge native NodeJS module, we can skip that step.

It should be possible to send commands back from the front-end towards the routing too.

I still am looking for a way to both deploy the Docker Network and register the modules using the IoT Edge container create options. This will make the solution more stable for zero-touch deployment.

So in the end, we have an interesting extension to the original IoT Edge gateway.

 

Advertenties

Reacties zijn gesloten.