Tutorial: Visualize IoT device data from IoT Hub using Azure Web PubSub service and Azure Functions

In this tutorial, you'll learn how to use Azure Web PubSub service and Azure Functions to build a serverless application with real-time data visualization from IoT Hub.

In this tutorial, you learn how to:

  • Build a serverless data visualization app
  • Work together with Web PubSub function input and output bindings and Azure IoT hub
  • Run the sample functions locally

Prerequisites

If you don't have an Azure subscription, create an Azure free account before you begin.

Create an IoT hub

In this section, you use Azure CLI to create an IoT hub and a resource group. An Azure resource group is a logical container into which Azure resources are deployed and managed. An IoT hub acts as a central message hub for bi-directional communication between your IoT application and the devices.

If you already have an IoT hub in your Azure subscription, you can skip this section.

To create an IoT hub and a resource group:

  1. Launch your CLI app. To run the CLI commands in the rest of this quickstart, copy the command syntax, paste it into your CLI app, edit variable values, and press Enter.

    • If you're using Cloud Shell, select the Try It button on the CLI commands to launch Cloud Shell in a split browser window. Or you can open the Cloud Shell in a separate browser tab.
    • If you're using Azure CLI locally, start your CLI console app and sign in to Azure CLI.
  2. Run az extension add to install or upgrade the azure-iot extension to the current version.

    az extension add --upgrade --name azure-iot
    
  3. In your CLI app, run the az group create command to create a resource group. The following command creates a resource group named MyResourceGroup in the eastus location.

    Note

    Optionally, you can set a different location. To see available locations, run az account list-locations.

    az group create --name MyResourceGroup --location eastus
    
  4. Run the az iot hub create command to create an IoT hub. It might take a few minutes to create an IoT hub.

    YourIotHubName. Replace this placeholder and the surrounding braces in the following command, using the name you chose for your IoT hub. An IoT hub name must be globally unique in Azure. Use your IoT hub name in the rest of this quickstart wherever you see the placeholder.

    az iot hub create --resource-group MyResourceGroup --name {YourIoTHubName}
    

Create a Web PubSub instance

If you already have a Web PubSub instance in your Azure subscription, you can skip this section.

Run az extension add to install or upgrade the webpubsub extension to the current version.

az extension add --upgrade --name webpubsub

Use the Azure CLI az webpubsub create command to create a Web PubSub in the resource group you've created. The following command creates a Free Web PubSub resource under resource group myResourceGroup in EastUS:

Important

Each Web PubSub resource must have a unique name. Replace <your-unique-resource-name> with the name of your Web PubSub in the following examples.

az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1

The output of this command shows properties of the newly created resource. Take note of the two properties listed below:

  • Resource Name: The name you provided to the --name parameter above.
  • hostName: In the example, the host name is <your-unique-resource-name>.webpubsub.azure.com/.

At this point, your Azure account is the only one authorized to perform any operations on this new resource.

Create and run the functions locally

  1. Create an empty folder for the project, and then run the following command in the new folder.

    func init --worker-runtime javascript
    
  2. Update host.json's extensionBundle to version 3.3.0 or later to get Web PubSub support.

{
    "version": "2.0",
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[3.3.*, 4.0.0)"
    }
}
  1. Create an index function to read and host a static web page for clients.

    func new -n index -t HttpTrigger
    
    • Update index/index.js with following code, which serves the HTML content as a static site.
      var fs = require("fs");
      var path = require("path");
      
      module.exports = function (context, req) {
      let index = path.join(
          context.executionContext.functionDirectory,
          "index.html"
      );
      fs.readFile(index, "utf8", function (err, data) {
          if (err) {
              console.log(err);
              context.done(err);
              return;
          }
          context.res = {
              status: 200,
              headers: {
                  "Content-Type": "text/html",
              },
              body: data,
          };
          context.done();
          });
      };
      
      
  2. Create an index.html file under the same folder as file index.js.

    <!doctype html>
    
    <html lang="en">
    
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0/dist/Chart.min.js" type="text/javascript"
            charset="utf-8"></script>
        <script>
            document.addEventListener("DOMContentLoaded", async function (event) {
                const res = await fetch(`/api/negotiate?id=${1}`);
                const data = await res.json();
                const webSocket = new WebSocket(data.url);
    
                class TrackedDevices {
                    constructor() {
                        // key as the deviceId, value as the temperature array
                        this.devices = new Map();
                        this.maxLen = 50;
                        this.timeData = new Array(this.maxLen);
                    }
    
                    // Find a device temperature based on its Id
                    findDevice(deviceId) {
                        return this.devices.get(deviceId);
                    }
    
                    addData(time, temperature, deviceId, dataSet, options) {
                        let containsDeviceId = false;
                        this.timeData.push(time);
                        for (const [key, value] of this.devices) {
                            if (key === deviceId) {
                                containsDeviceId = true;
                                value.push(temperature);
                            } else {
                                value.push(null);
                            }
                        }
    
                        if (!containsDeviceId) {
                            const data = getRandomDataSet(deviceId, 0);
                            let temperatures = new Array(this.maxLen);
                            temperatures.push(temperature);
                            this.devices.set(deviceId, temperatures);
                            data.data = temperatures;
                            dataSet.push(data);
                        }
    
                        if (this.timeData.length > this.maxLen) {
                            this.timeData.shift();
                            this.devices.forEach((value, key) => {
                                value.shift();
                            })
                        }
                    }
    
                    getDevicesCount() {
                        return this.devices.size;
                    }
                }
    
                const trackedDevices = new TrackedDevices();
                function getRandom(max) {
                    return Math.floor((Math.random() * max) + 1)
                }
                function getRandomDataSet(id, axisId) {
                    return getDataSet(id, axisId, getRandom(255), getRandom(255), getRandom(255));
                }
                function getDataSet(id, axisId, r, g, b) {
                    return {
                        fill: false,
                        label: id,
                        yAxisID: axisId,
                        borderColor: `rgba(${r}, ${g}, ${b}, 1)`,
                        pointBoarderColor: `rgba(${r}, ${g}, ${b}, 1)`,
                        backgroundColor: `rgba(${r}, ${g}, ${b}, 0.4)`,
                        pointHoverBackgroundColor: `rgba(${r}, ${g}, ${b}, 1)`,
                        pointHoverBorderColor: `rgba(${r}, ${g}, ${b}, 1)`,
                        spanGaps: true,
                    };
                }
    
                function getYAxy(id, display) {
                    return {
                        id: id,
                        type: "linear",
                        scaleLabel: {
                            labelString: display || id,
                            display: true,
                        },
                        position: "left",
                    };
                }
    
                // Define the chart axes
                const chartData = { datasets: [], };
    
                // Temperature (ºC), id as 0
                const chartOptions = {
                    responsive: true,
                    animation: {
                        duration: 250 * 1.5,
                        easing: 'linear'
                    },
                    scales: {
                        yAxes: [
                            getYAxy(0, "Temperature (ºC)"),
                        ],
                    },
                };
                // Get the context of the canvas element we want to select
                const ctx = document.getElementById("chart").getContext("2d");
    
                chartData.labels = trackedDevices.timeData;
                const chart = new Chart(ctx, {
                    type: "line",
                    data: chartData,
                    options: chartOptions,
                });
    
                webSocket.onmessage = function onMessage(message) {
                    try {
                        const messageData = JSON.parse(message.data);
                        console.log(messageData);
    
                        // time and either temperature or humidity are required
                        if (!messageData.MessageDate ||
                            !messageData.IotData.temperature) {
                            return;
                        }
                        trackedDevices.addData(messageData.MessageDate, messageData.IotData.temperature, messageData.DeviceId, chartData.datasets, chartOptions.scales);
                        const numDevices = trackedDevices.getDevicesCount();
                        document.getElementById("deviceCount").innerText =
                            numDevices === 1 ? `${numDevices} device` : `${numDevices} devices`;
                        chart.update();
                    } catch (err) {
                        console.error(err);
                    }
                };
            });
        </script>
        <style>
            body {
                font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
                padding: 50px;
                margin: 0;
                text-align: center;
            }
    
            .flexHeader {
                display: flex;
                flex-direction: row;
                flex-wrap: nowrap;
                justify-content: space-between;
            }
    
            #charts {
                display: flex;
                flex-direction: row;
                flex-wrap: wrap;
                justify-content: space-around;
                align-content: stretch;
            }
    
            .chartContainer {
                flex: 1;
                flex-basis: 40%;
                min-width: 30%;
                max-width: 100%;
            }
    
            a {
                color: #00B7FF;
            }
        </style>
    
        <title>Temperature Real-time Data</title>
    </head>
    
    <body>
        <h1 class="flexHeader">
            <span>Temperature Real-time Data</span>
            <span id="deviceCount">0 devices</span>
        </h1>
        <div id="charts">
            <canvas id="chart"></canvas>
        </div>
    </body>
    
    </html>
    
  3. Create a negotiate function that clients use to get a service connection URL and access token.

    func new -n negotiate -t HttpTrigger
    
    • Update negotiate/function.json to include an input binding WebPubSubConnection, with the following json code.
      {
          "bindings": [
              {
                  "authLevel": "anonymous",
                  "type": "httpTrigger",
                  "direction": "in",
                  "name": "req"
              },
              {
                  "type": "http",
                  "direction": "out",
                  "name": "res"
              },
              {
                  "type": "webPubSubConnection",
                  "name": "connection",
                  "hub": "%hubName%",
                  "direction": "in"
              }
          ]
      }
      
    • Update negotiate/index.js to return the connection binding that contains the generated token.
      module.exports = function (context, req, connection) {
          // Add your own auth logic here
          context.res = { body: connection };
          context.done();
      };
      
  4. Create a messagehandler function to generate notifications by using the "IoT Hub (Event Hub)" template.

     func new --template "IoT Hub (Event Hub)" --name messagehandler
    
    • Update messagehandler/function.json to add Web PubSub output binding with the following json code. We use variable %hubName% as the hub name for both IoT eventHubName and Web PubSub hub.
      {
          "bindings": [
              {
                  "type": "eventHubTrigger",
                  "name": "IoTHubMessages",
                  "direction": "in",
                  "eventHubName": "%hubName%",
                  "connection": "IOTHUBConnectionString",
                  "cardinality": "many",
                  "consumerGroup": "$Default",
                  "dataType": "string"
              },
              {
                  "type": "webPubSub",
                  "name": "actions",
                  "hub": "%hubName%",
                  "direction": "out"
              }
          ]
      }
      
    • Update messagehandler/index.js with the following code. It sends every message from IoT hub to every client connected to Web PubSub service using the Web PubSub output bindings.
      module.exports = function (context, IoTHubMessages) {
      IoTHubMessages.forEach((message) => {
          const deviceMessage = JSON.parse(message);
          context.log(`Processed message: ${message}`);
          context.bindings.actions = {
          actionName: "sendToAll",
          data: JSON.stringify({
              IotData: deviceMessage,
              MessageDate: deviceMessage.date || new Date().toISOString(),
              DeviceId: deviceMessage.deviceId,
          }),
          };
      });
      
      context.done();
      };
      
  5. Update the Function settings.

    1. Add hubName setting and replace {YourIoTHubName} with the hub name you used when creating your IoT Hub.

      func settings add hubName "{YourIoTHubName}"
      
    2. Get the Service Connection String for IoT Hub.

    az iot hub connection-string show --policy-name service --hub-name {YourIoTHubName} --output table --default-eventhub
    

    Set IOTHubConnectionString, replacing <iot-connection-string> with the value.

    func settings add IOTHubConnectionString "<iot-connection-string>"
    
    1. Get the Connection String for Web PubSub.
    az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
    

    Set WebPubSubConnectionString, replacing <webpubsub-connection-string> with the value.

    func settings add WebPubSubConnectionString "<webpubsub-connection-string>"
    

    Note

    The IoT Hub (Event Hub) function trigger used in the sample has dependency on Azure Storage, but you can use a local storage emulator when the function is running locally. If you get an error such as There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid., you'll need to download and enable Storage Emulator.

  6. Run the function locally.

    Now you're able to run your local function by command below.

    func start
    

    You can visit your local host static page by visiting: https://localhost:7071/api/index.

Run the device to send data

Register a device

A device must be registered with your IoT hub before it can connect. If you already have a device registered in your IoT hub, you can skip this section.

  1. Run the az iot hub device-identity create command in Azure Cloud Shell to create the device identity.

    YourIoTHubName: Replace this placeholder with the name you chose for your IoT hub.

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. Run the Az PowerShell module iot hub device-identity connection-string show command in Azure Cloud Shell to get the device connection string for the device you just registered:

    YourIoTHubName: Replace this placeholder below with the name you chose for your IoT hub.

    az iot hub device-identity connection-string show --hub-name {YourIoTHubName} --device-id simDevice --output table
    

    Make a note of the device connection string, which looks like this:

    HostName={YourIoTHubName}.azure-devices.net;DeviceId=simDevice;SharedAccessKey={YourSharedAccessKey}

Run the visualization website

Open function host index page: http://localhost:7071/api/index to view the real-time dashboard. Register multiple devices and you'll see the dashboard updates multiple devices in real-time. Open multiple browsers and you'll see every page is updated in real-time.

Screenshot of multiple devices data visualization using Web PubSub service.

Clean up resources

If you plan to continue on to work with subsequent quickstarts and tutorials, you may wish to leave these resources in place.

When no longer needed, you can use the Azure CLI az group delete command to remove the resource group and all related resources:

az group delete --name "myResourceGroup"

Next steps