Azure IoT Central bridge for The Things Network

During the last The Thing Conference back in January in Amsterdam, The Netherlands, I spoke with the team of Tektelic. I got this smart room sensor from them to experiment with:


This sensor works with Lora and has some neat features. The sensor reads eg. temperature and humidity of the room it is placed in, but it also has a few other sensors. One of these is a magnetic switch.

It’s this sensor I am interested in. I want to see if a door is left open (and maybe putting a big, loud horn next to it…):

Today, I decided to connect this module to Azure IoT Central. For this, we use the Azure IoT Central Bridge.

I already blogged about this bridge where I connected to the Partical cloud. This time, I show how to connect to The Things Network cloud:

These are the steps we have to execute when connecting:

  1. Connect the Tektelic Room sensor to The Things Network
  2. Convert the byte array with data into a JSON message
  3. Setup an IoT Central App
  4. Setup the IoT Central Bridge
  5. Modify the bridge so it can handle TTN messages
  6. Setup a TTN webhook integration to the bridge
  7. Create a Device capability model for our room sensor
  8. See the influx of telemetry in IoT Central

Yes, there are a lot of small steps to perform. But I did the heavy lifting for you so it should be easy to follow.

Let’s see how to detect if a kitchen door is left open…

Connecting the room sensor

To connect the room sensor to The Things Network is not that hard. You need of course the security keys which I got from Tektelic:

After that, I got my first messages in the Azure portal which look like “01000008040001” or “036700B204683F00FF0129”.

Convert the byte array

Yes, this byte array representation (in hexadecimal) is not helpful right away. And the two messages even differ in length (and in format?). A solid, single JSON representation would be nice.

The Things Networks provides a solution for this step. Incoming messages can be picked up by Payload Format functions to decode, convert, and validate. These payload functions are written in JavaScript.

I do not have to write any JavaScript, a function for out sensor is already available.

It is advertized for both the Tektelic Smart Room Sensor Base (the one I show here) and the Tektelic Smart Room Sensor with PIR.

It feels like the function is not covering all functionality of the sensor but at least the most important one values are converted. So ‘036700B204683F00FF0129’ is converted into:

{
  "activity": null,
  "battery_voltage": 2.97,
  "bytes": "A2cAsgRoPwD/ASk=",
  "external_input": null,
  "humidity": 31.5,
  "reed_count": null,
  "reed_state": null,
  "temperature": 17.8
}

This message provides temperature, humidity and voltage. These values is sent ones every hour.

The message ‘01000008040001’ is converted into:

{
  "activity": null,
  "battery_voltage": null,
  "bytes": "AQAACAQAAQ==",
  "external_input": null,
  "humidity": null,
  "reed_count": 1,
  "reed_state": true,
  "temperature": null
}

Notice the ‘reed state’, this is the magnetic switch. This message is send every time the door opens of closes (which triggers the magnetic switch).

Also notice that all values are put next to each other, one level deep, these values are not nested. We need to know this exact format later on, this is what IoT Central ingests.

Setup an IoT Central App

For now, we leave the TTN portal and look at IoT Central.

Create a new custom IoT Central app using the wizard:

Once the app is created, we need to copy the Scope id and one of the two SAS keys (used in the next step):

We will connect our devices to IoT Central using an Azure Function. This function will perform the initial registration of devices in IoT Central. So, if new devices are added to The Things Network, we will not have to do the registration our selves.

Note: IoT Central is protected again being overwhelmed by new devices. There is still an action to perform by you, the administrator. We see that demonstrated in the last step of this blog.

Setup the IoT Central Bridge

Now it’s time to generate the IoT Central Bridge. You need an Azure subscription for that.

There is a good manual available on the GitHub page so I only highlight the most interesting parts.

First, you have to start the creation wizard which collects information needed for creating the Azure resources:

Note: here you have to fill in the Scope ID and the SAS key.

After creation, you are the proud owner of new resources like an Azure Function (based on a consumption plan) and a Key Vault.

The Azure function itself still needs some attention before it can be used.

First, you need to execute an ‘npm install’ once the function is created. After that, you need to restart the function so it will run smoothly. This is done in the ‘Console’ of the Azure function:

cd IoTCIntegration
npm install

Note: See also the error as described below.

Modify the bridge so it can handle TTN V2 messages

Note: the following paragraph refers to the TTN V2 message format. The updated V3 message is found below. V3 users can learn about the flow and use their own script.

Before we go back to the TTN portal, we also need to modify the JavaScript code in our Azure function.

The problem is this. We get a message from The Things Network webhook like this:

{
  "activity": null,
  "battery_voltage": null,
  "bytes": "AQD/CAQAAQ==",
  "external_input": null,
  "humidity": null,
  "reed_count": 1,
  "reed_state": false,
  "temperature": null
}

IoT Central needs a message like this:

{
    "device": {
        "deviceId": "my-cloud-device"
    },
    "measurements": {
        "activity": null,
        "battery_voltage": null,
        "bytes": "AQD/CAQAAQ==",
        "external_input": null,
        "humidity": null,
        "reed_count": 1,
        "reed_state": false,
        "temperature": null
    }
}

Yes, we need a Device ID. Unfortunately, this is something the TTN Payload functions can not offer in the message body.

Luckily, when the TTN portal is connected to this Azure Function, it exposes more information, other than the message body.

So change the code like described in GitHub. Add these extra lines (before the call to handleMessage in line 21):

req.body = {
    device: {
        deviceId: req.body.hardware_serial.toLowerCase()
    },
    measurements: req.body.payload_fields
};

Save the changes!

There is one comment to make before we can move on!

As you can see, the ‘hardware_serial’ is selected as device identification. This is, in fact, the Device EUI:

This is an awkward hexadecimal number.

If you want the human readable name, the Device ID, insert these lines:

        req.body = {
            device: {
                deviceId: req.body.dev_id.toLowerCase()  //req.body.hardware_serial.toLowerCase()
            },
            measurements: req.body.payload_fields
        };

With this, my own future IoT Central device will be named ‘kona01’ (all lower space, no spaces, may contain hyphens).

Note: This piece of code can be extended with other TTN values (like the GPS location of the Lora gateway which received the message first or the strength of the signal (which is a rough estimation for distance)).

The last step is to copy the unique (default function key ) URL of this Azure Function. We need to add it in the TTN portal:

Remember this key.

TTN V3 message format

With the TTN V3 portal, the message format is changed:

        // TTN V3 specific lines
        req.body = {
            device: {
                deviceId: req.body.end_device_ids.device_id.toLowerCase()  // req.body.dev_eui.toLowerCase()
            },
            measurements: req.body.uplink_message.decoded_payload
        };

Node-fetch exception

I got an exception regarding “node-fetch”:

Cannot find module 'node-fetch'

I found this solution. It’s related to that NPM command in the console.

In the Azure Portal, in the Azure Function, go to the console. There, browse to the function folder and update the NPM packages:

cd IoTCIntegration
npm install

Setup a TTN V3 webhook integration

In the TTN portal, within our application where our Room sensor messages are arriving, we add an Integration like this one here:

This is an HTTP integration AKA a webhook.

There are more types of integrations but this HTTP Integration is the one we use:

Just give it a nice name (in the ‘process id’ field) and fill in the URL of your Azure function in ‘URL’.

Finally, select you are interested in uplink messages and save the integration:

All incoming telemetry is now sent to our IoT Central app using the Azure Function.

But we still have to describe the capabilities of our devices, representing the incoming messages!

Now, the device are not bound to a certain Device Template.

Create a Device capability model/Device template for our room sensor

Start by adding a new Device template:

We customize an IoT Device:

Directly start the customization (Review):

And select Create in the next step:

Give your new device template a cunning name:

Now you can start defining the capabilities of your device, or import the ones I created for you:

Just import this model (first save it to a folder, please):

{
  "@id": "urn:dustyFieldOfLora:TektelicKonaLoras:1",
  "@type": "CapabilityModel",
  "implements": [
    {
      "@id": "urn:dustyFieldOfLora:TektelicKonaLoras:rqkqiuid9:1",
      "@type": "InterfaceInstance",
      "displayName": {
        "en": "Interface"
      },
      "name": "TektelicKona_6ji",
      "schema": {
        "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:1",
        "@type": "Interface",
        "displayName": {
          "en": "Interface"
        },
        "contents": [
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:activity:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "activity"
            },
            "name": "activity",
            "schema": "string"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:battery_voltage:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "battery_voltage"
            },
            "name": "battery_voltage",
            "displayUnit": {
              "en": "volt"
            },
            "schema": "double"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:bytes:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "bytes"
            },
            "name": "bytes",
            "schema": "string"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:external_input:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "external_input"
            },
            "name": "external_input",
            "schema": "boolean"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:humidity:1",
            "@type": [
              "Telemetry",
              "SemanticType/Humidity"
            ],
            "displayName": {
              "en": "humidity"
            },
            "name": "humidity",
            "schema": "double",
            "unit": "Units/Humidity/percent"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:reed_count:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "reed_count"
            },
            "name": "reed_count",
            "schema": "integer"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:reed_state:1",
            "@type": "Telemetry",
            "displayName": {
              "en": "reed_state"
            },
            "name": "reed_state",
            "schema": "boolean"
          },
          {
            "@id": "urn:dustyFieldOfLora:TektelicKona_6ji:temperature:1",
            "@type": [
              "Telemetry",
              "SemanticType/Temperature"
            ],
            "displayName": {
              "en": "temperature"
            },
            "name": "temperature",
            "schema": "double",
            "unit": "Units/Temperature/celsius"
          }
        ]
      }
    }
  ],
  "displayName": {
    "en": "Tektelic Kona"
  },
  "@context": [
    "http://azureiot.com/v1/contexts/IoTModel.json"
  ]
}

As you can see, the telemetry is described (eg. name, schema, semantic type). It’s not perfect but you now know how a device capability model and its device interface is described.

And notice also the values are indeed described only one level deep.

Note: This Device template format supports nesting of data values but I have not tested it in combination with the bridge.

This new template will look like this, once imported:

Because this model is imported, you are limited in editing this model. If you build it yourself, you have full control over all fields.

Notice the indication to the right that this template is still in draft mode. Once you are ready to use the template with actual (or simulated) devices, you have to publish it using the Publish button first.

I recommend to wait with publishing it right now and first add a View to visualize the device:

All telemetry items are shown for selection to build a beautiful dashboard. Select a telemetry value and select Add Tile. The tile added to the canvas can then be customized (size, position) or configured (range, etc.):

This is what I created:

When you are done, you have to Publish it now. You can alter your view afterwards and re-publish if needed.

See the influx of telemetry in IoT Central

So everything is configured now:

  • The telemetry of the sensor is sent to the TTN cloud
  • The byte array message is converted in a JSON format
  • The JSON message is sent to the Azure Function using the HTTP integration of the webhook
  • The Azure function creates the device in the IoT Central app

But wait, we have to perform one last step!

We have to migrate new TTN devices to the corresponding Device template. This is done on the ‘All devices’ page:

By now, you should see your device in this ‘All devices’ list. But in the image above, the device is already migrated using the Migrate button.

Select your new device and migrate it to the template you created.

Note: this is also a protection for the dashboard, unknown devices are not yet added to device sets.

Once the device is migrated, you can see the data arrive in the IoT Central portal:

As you can see, the data of the two partial messages (the message which is sent every one hour and the messages sent when a door is opened) are merged into one dashboard.

Somebody has left the door open!

What does it cost?

Is this all for free?

Well, Azure is not free. Microsoft has made a subset of Azure available for free, though.

This includes IoT Central apps with a maximum of two devices. And it includes one million requests and 400,000 GBs of resource consumption with Azure Function.

The only resources I see which will have some costs could be the Key vault (used to prevent hardcoded secrets) and the storage (used to store the JavaScript files).

If you put some effort (there is a 30 days trial with $200 of free Azure credits) in this solution, I suppose you could get rid of the Azure Key Vault.

Conclusion

That was fun, wasn’t it?

You have seen how the TTN portal is connected to the IoT Central portal and data from the TTN app is sent to the IoT Central app using a device template.

While you are connecting Lora devices to the IoT Central dashboard, look around and check out great other features like Dashboards, Analytics, Rules, and Actions.

You can now start building your own Lora dashboards using IoT Central!