Positioning GPS devices on a map using Azure Functions, Azure SignalR Service and Azure Maps

Last year, I bought this RAK7200 Lora Tracker with the idea to track my bicycle in the neighborhood.

This month, I finally found some time to have this device connected to the cloud and map its position.

This tracker from RAK Wireless, running on a rechargeable battery for multiple days, has several sensors aboard and is connected to a Lora network:

RAK7200 LoRa® Tracker | The Things Network

Here you see the payload as presented in The Things Network console:

Potentially, I could even do some alerting based on the movement of the device, even if there is no GPS fix (acceleration, magnetometer).

An uplink payload formatter can be found here. I changed it a bit so the latitude, longitude, etc. are decimals, not strings:

The same goes for the battery power.

Showing a generic location in Azure Maps is not that hard, there are many samples available. But I wanted to have the map updated IN REAL-TIME!

The TTN portal now supports the Azure IoT Hub natively so I was looking for a way to represent the ingested location in Azure Maps.

Azure Maps tiles live inside the browser. I also needed something like Websockets to update the page representing the map. For this, I wanted to use Azure SignalR Service.

Last but not least, I was looking for a lightweight website because I need to host the pages somewhere.

This is the solution I came up with:

Let’s check out how this is done.

I could have tried to work with an Azure Web App running eg. a .Net Core website (I’m in love with Blazor).

Or I could have tried something with Azure static web apps.

Both are interesting and modern concepts.

Still, because I get the position information from an IoT Hub and running this Azure Function is a sure thing to do (we have to forward the messages in some way to SignalR), I checked out the combination of SignalR and Azure Functions.

Azure Functions had this SignalR output binding already.

The twist is that I want to run a ‘whole’ website in Azure Functions too: just one webpage with a map in this case 🙂

This combination is actually possible!

There is even a sample code project available on GitHub as a good starting point.

So, I started with the available guide to set up an Azure Function showing a webpage while being connected to the SignalR service. I then enhanced it to work with IoT GPS messages.

Prerequisites

I already connected the GPS tracker to the Lora TTN backend and forwarded uplink messages to an IoT Hub.

Here, we see incoming RAK7200 messages shown in the IoT Explorer:

I created a separate IoT Hub consumer group for Azure Functions named ‘afa’.

Azure Maps account

We want to make use of a map drawn by Azure Maps tiles. So we need to create this Azure Maps resource.

Notice the primary key which we need to fill in into the properties of the Azure Maps HTML object:

Take a note of that primary key. Later on, we will see that key in plain text in the HTML later on:

Note: This is just to simplify the example code.

Check out this documentation to see how you can secure your Azure Maps instance.

Azure SignalR Service

We now have an IoT Hub with a device registration that generates GPS location and we have a map. We also have an Azure Maps instance. Let’s add all the stuff in between.

As seen in the original guide, We need a SignalR Service that is created with the ‘serverless’ server mode:

If the service is already created, you can correct this afterward.

We are using Azure Functions to connect, therefore we need this specific setting.

Once the service is created, take a note of the SignalR service connection string (access key):

Creating The Azure Function App

All settings and services are available, we only need the create an Azure Function App and execute functions within it.

I cloned the original source code and replaced the original timer trigger (which did some non-IoT stuff) with an Eventhub trigger. This trigger is then connected to the IoT Hub using the event-hub compatible endpoint.

download this updated project source code and publish it to your own Azure Function App.

The Function App application settings

We need to add two application settings that contain the private connection strings of both the IoT Hub and the SignalR service:

  1. AzureSignalRConnectionString : The connection string of your SignalR service
  2. ttn-ih-test-weu-ih_events_IOTHUB : This is the eventhub-compatible endpoint of your IoT Hub

Note: the ‘AzureSignalRConnectionString‘ parameter name can not be changed. It feels like a magic string…

The ‘ttn-ih-test-weu-ih_events_IOTHUB‘ can be changed into another name if you want to. Just check out that same property value in the Function.cs. It can also be added into the local.settings.json if you want to test the eventhub trigger locally.

The Azure Functions source code

You will need these three (Azure Function app) functions.

First, we need ‘some administration for SignalR:

[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
    [SignalRConnectionInfo(HubName = "serverless")] SignalRConnectionInfo connectionInfo)
{
    return connectionInfo;
}

You do not need to change anything to it. This is just to meet the needs of the SignalR service.

Then, we ingest IoT Hub message with this second function:

[FunctionName("broadcast")]
public static async Task Broadcast(
    [EventHubTrigger("dummy", ConsumerGroup = "afa", Connection = "ttn-ih-test-weu-ih_events_IOTHUB")] EventData message,
    ILogger log,
    [SignalR(HubName = "serverless")] IAsyncCollector<SignalRMessage> signalRMessages)
{
    dynamic data = JObject.Parse(Encoding.UTF8.GetString(message.Body.Array));

    var webMessage = new WebMessage();
    webMessage.deviceId = (string)data.end_device_ids.device_id;
    webMessage.timestamp = DateTime.Parse((string)data.received_at).ToUniversalTime();

    if (data != null
            && data.uplink_message != null
            && data.uplink_message.decoded_payload != null
            && data.uplink_message.decoded_payload.latitude != null
            && data.uplink_message.decoded_payload.longitude != null
            && data.uplink_message.decoded_payload.battery != null)
    {
        webMessage.battery = (string)data.uplink_message.decoded_payload.battery;
        webMessage.latitude = (decimal)data.uplink_message.decoded_payload.latitude;
        webMessage.longitude = (decimal)data.uplink_message.decoded_payload.longitude;

        log.LogInformation($"Message sent to webpage: DeviceId: {webMessage.deviceId} - timestamp: {webMessage.timestamp} - lat: {webMessage.latitude} - lon: {webMessage.longitude} ");

        var json = JsonConvert.SerializeObject(webMessage);

        await signalRMessages.AddAsync(
            new SignalRMessage
            {
                Target = "gpsMessage",
                Arguments = new[] { json }
            });
    }
}

public class WebMessage
{
    public string deviceId { get; set; }
    public DateTime timestamp { get; set; }
    public decimal latitude { get; set; }
    public decimal longitude { get; set; }
    public string battery { get; set; }
}

This is the eventhub trigger that transforms incoming IoT Hub GPS tracker messages into JSON messages and sends them to the SignalR service.

This is done using the arbitrary SignalR target ‘gpsMessage’. This target can have any name but it has to match with the consumer code later on (in the Javascript running on the HTML page).

Note: see how the connection string and consumer group are configured. That ‘dummy’ value is just a string to ignore (We connect to an IoT Hub, not an actual Event Hub).

Last but not least, we expose a webpage using a third function:

[FunctionName("index")]
public static IActionResult GetHomePage([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req, ExecutionContext context)
{
    var path = Path.Combine(context.FunctionAppDirectory, "content", "index.html");
    return new ContentResult
    {
        Content = File.ReadAllText(path),
        ContentType = "text/html",
    };
}

We just read an HTML page from the context folder and expose it as HTML text. Browsers do understand this.

The magic is done inside that HTML.

Map tiles in HTML

Take a look at the HTML page.

These HTML lines link to certain script libraries to support executing Azure Maps and SignalR related logic:

Notice I added libraries for Azure Maps (atlas.min.js) and SignalR (signalr.min.js). I also added JQuery to demonstrate the usage.

When the page loads, the ‘InitMapsAndSignalR’ function is executed:

As you can see, the page body has placeholder DIVs for the message string and the map also.

The InitMapsAndSignalR functionality (part of HEAD) is pretty straight forward:

<script>
    var map;
    var mapsDataSource;

    function InitMapAndSignalR() {
        map = new atlas.Map('myMap', {
            center: [5.55, 51.5],
            zoom: 10,
            view: "Auto",
            language: 'en-US',
            authOptions: {
                authType: 'subscriptionKey',
                subscriptionKey: 'the secret key in plain sight'
            }
        });

        //Construct a zoom control and add it to the map.
        map.controls.add(new atlas.control.ZoomControl(), {
            position: 'bottom-right'
        });

        map.events.add('ready', function () {

            // Create a data source and add it to the map.
            mapsDataSource = new atlas.source.DataSource();
            map.sources.add(mapsDataSource);

            // Create a symbol layer to render icons and/or text at points on the map.
            var layer = new atlas.layer.SymbolLayer(mapsDataSource);

            // Add the layer to the map.
            map.layers.add(layer);

            const apiBaseUrl = window.location.origin;

            const connection = new signalR.HubConnectionBuilder()
                .withUrl(apiBaseUrl + '/api')
                .configureLogging(signalR.LogLevel.Information)
                .build();

            connection.on('gpsMessage', (message) => {
                document.getElementById("messages").innerHTML = message;

                const obj = JSON.parse(message);

                // Replace the pin so only latest position is shown.
                mapsDataSource.clear();
                mapsDataSource.add(new atlas.data.Point([obj.longitude, obj.latitude]));

            });

            connection.start()
                .catch(console.error);
        });
    }
</script>

Notice the two variables which are created in a stateful manner.

First, a map control (the area representing the Azure Maps tiles, pins, etc.) is created (hence the private primary key of the Azure Maps services hidden in plain sight).

Once that map control is ready, It gets some default settings. Also, a connection to the SignalR hub is built in this phase.

Finally, every incoming SignalR message on the ‘gpsMessage’ hub will replace the previous pin with a new pin on the map (it’s added to the data source after that array is cleared, over and over again).

Note: because you know the unique name of each device (it’s part of the message) you could leave other pins of other devices on the map untouched and only update your own map.

The full HTML code is found here.

Testing the setup

By now you have should have deployed the functions. Three functions should be available:

Everything is set and connected, we can finally test the webpage.

The ‘index’ function has its own public endpoint. This is the starting point:

Open that index page and see how a pin is set at the right location once the GPS tracker sends a message to the cloud:

If you open multiple browsers, all will be updated at the same time. Each page has its own SignalR subscription.

Conclusion

This is just a demonstration of how to represent just one device in Azure Maps. It’s the bare minimum to represent a GPS location using simple Azure resources.

It exposes your Azure Maps private key (see this link about securing it).

Though, the whole solution is sound and scalable. Check out the code and see how everything is connected.

Most of the Azure resources used have free tiers so this solution can run with minimal costs if you keep the numbers small:

  • 5000 free Base Map Tiles
  • Monthly free grant of 1 million Azure Function requests and 400,000 GB-s of resource consumption per month per subscription in pay-as-you-go pricing
  • 20,000 SignalR service Messages / Unit / Day
  • 8,000 free F1 IoT Hub messages/day per IoT (0.5 KB Message meter size)

I recommend this Azure Function approach only for simple web pages. Moving over to a website in a Web App is a smart thing to do once things get complicated.

The source code seen here has been open-sourced on GitHub. I got confirmed the Python equivalent of the C# repository is working too.

It yo are interested in more Azure Maps examples, check out this part of the IoT for beginners workshop.

Advertentie