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
A code editor, such as Visual Studio Code
Node.js, version 18.x or above.
Note
For more information about the supported versions of Node.js, see Azure Functions runtime versions documentation.
Azure Functions Core Tools (v3 or higher preferred) to run Azure Function apps locally and deploy to Azure.
The Azure CLI to manage Azure resources.
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:
Launch your CLI app. To run the CLI commands in the rest of this article, 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.
Run az extension add to install or upgrade the azure-iot extension to the current version.
az extension add --upgrade --name azure-iot
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
. This quickstart uses eastus as shown in the example command.az group create --name MyResourceGroup --location eastus
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 {your_iot_hub_name}
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
Create an empty folder for the project, and then run the following command in the new folder.
func init --worker-runtime javascript --model V4
Create an
index
function to read and host a static web page for clients.func new -n index -t HttpTrigger
Update
src/functions/index.js
with following code, which serves the HTML content as a static site.const { app } = require('@azure/functions'); const { readFile } = require('fs/promises'); app.http('index', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: async (context) => { const content = await readFile('index.html', 'utf8', (err, data) => { if (err) { context.err(err) return } }); return { status: 200, headers: { 'Content-Type': 'text/html' }, body: content, }; } });
Create an
index.html
file under the root folder.<!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>
Create a
negotiate
function that clients use to get a service connection URL and access token.func new -n negotiate -t HttpTrigger
Update
src/functions/negotiate.js
to useWebPubSubConnection
that contains the generated token.const { app, input } = require('@azure/functions'); const connection = input.generic({ type: 'webPubSubConnection', name: 'connection', hub: '%hubName%' }); app.http('negotiate', { methods: ['GET', 'POST'], authLevel: 'anonymous', extraInputs: [connection], handler: async (request, context) => { return { body: JSON.stringify(context.extraInputs.get('connection')) }; }, });
Create a
messagehandler
function to generate notifications by using the"IoT Hub (Event Hub)"
template.func new --template "Azure Event Hub trigger" --name messagehandler
Update
src/functions/messagehandler.js
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.const { app, output } = require('@azure/functions'); const wpsAction = output.generic({ type: 'webPubSub', name: 'action', hub: '%hubName%' }); app.eventHub('messagehandler', { connection: 'IOTHUBConnectionString', eventHubName: '%hubName%', cardinality: 'many', extraOutputs: [wpsAction], handler: (messages, context) => { var actions = []; if (Array.isArray(messages)) { context.log(`Event hub function processed ${messages.length} messages`); for (const message of messages) { context.log('Event hub message:', message); actions.push({ actionName: "sendToAll", data: JSON.stringify({ IotData: message, MessageDate: message.date || new Date().toISOString(), DeviceId: message.deviceId, })}); } } else { context.log('Event hub function processed message:', messages); actions.push({ actionName: "sendToAll", data: JSON.stringify({ IotData: message, MessageDate: message.date || new Date().toISOString(), DeviceId: message.deviceId, })}); } context.extraOutputs.set(wpsAction, actions); } });
Update the Function settings.
Add
hubName
setting and replace{YourIoTHubName}
with the hub name you used when creating your IoT Hub.func settings add hubName "{YourIoTHubName}"
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>"
- 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
Azure Event Hub trigger
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 asThere 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.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.
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
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}
For quickest results, simulate temperature data using the Raspberry Pi Azure IoT Online Simulator. Paste in the device connection string, and select the Run button.
If you have a physical Raspberry Pi and BME280 sensor, you can measure and report real temperature and humidity values by following the Connect Raspberry Pi to Azure IoT Hub (Node.js) tutorial.
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.
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"