教學課程:使用 Azure Web PubSub 服務和 Azure Functions 將 IoT 中樞的 IoT 裝置資料視覺化
在本教學課程中,您將會了解如何使用 Azure Web PubSub 服務和 Azure Functions,從 IoT 中樞建置具有即時資料視覺效果的無伺服器應用程式。
在本教學課程中,您會了解如何:
- 建置無伺服器資料視覺效果應用程式
- 一起使用 Web PubSub 函式輸入和輸出繫結與 Azure IoT 中樞
- 在本機執行範例函式
重要
原始 連接字串 只會出現在本文中,僅供示範之用。
連接字串包含應用程式存取 Azure Web PubSub 服務所需的授權資訊。 連接字串內的存取金鑰類似於服務的根密碼。 在生產環境中,請一律保護您的存取金鑰。 使用 Azure 金鑰保存庫,安全地管理和輪替密鑰,並使用保護連線WebPubSubServiceClient
。
避免將存取金鑰散發給其他使用者、寫入程式碼,或將其以純文字儲存在他人可以存取的位置。 如果您認為金鑰可能已遭盜用,請輪替金鑰。
必要條件
程式碼編輯器,例如 Visual Studio Code
Node.js18.x 版或更新版本。
注意
如需支援的 Node.js 版本詳細資訊,請參閱 Azure Functions 執行階段版本文件。
Azure Functions Core Tools (最好是 v3 或更高版本) 以在本機執行 Azure Function 應用程式,並部署至 Azure。
Azure CLI 以管理 Azure 資源。
如果您沒有 Azure 訂用帳戶,請在開始之前先建立 Azure 免費帳戶。
建立 IoT 中樞
在本節中,您會使用 Azure CLI 來建立 IoT 中樞和資源群組。 Azure 資源群組是在其中部署與管理 Azure 資源的邏輯容器。 IoT 中樞可作為 IoT 應用程式與裝置之間雙向通訊的中央訊息中樞。
如果您的 Azure 訂用帳戶中已有 IoT 中樞,則您可以略過本節。
若要建立 IoT 中樞和資源群組:
啟動 CLI 應用程式。 若要在此文其餘部分執行 CLI 命令,請複製命令語法,並將其貼入 CLI 應用程式,然後編輯變數值,再按
Enter
。- 如果您要使用 Cloud Shell,則請選取 CLI 命令上的 [試用] 按鈕,以在分割的瀏覽器視窗中啟動 Cloud Shell。 或者,您可以在不同的瀏覽器索引標籤中開啟 Cloud Shell。
- 如果您要在本機使用 Azure CLI,則請啟動 CLI 主控台應用程式,並登入 Azure CLI。
執行 az extension add 以將 azure-iot 延伸模組安裝或升級至目前版本。
az extension add --upgrade --name azure-iot
在 CLI 應用程式中,執行 az group create 命令以建立資源群組。 下列命令會在 eastus 位置中建立名為 MyResourceGroup 的資源群組。
注意
您可以選擇性地設定不同的位置。 若要查看可用位置,請執行
az account list-locations
。 本快速入門使用 eastus,如範例命令所示。az group create --name MyResourceGroup --location eastus
使用 az iot hub create 命令建立 IoT 中樞。 建立 IoT 中樞可能需要幾分鐘的時間。
YourIotHubName。 使用您為 IoT 中樞選擇的名稱,取代下列命令中的這個預留位置和前後的大括弧。 IoT 中樞名稱在 Azure 中必須是全域唯一的。 只要您看到預留位置,就請在本快速入門的其餘部分使用您的 IoT 中樞名稱。
az iot hub create --resource-group MyResourceGroup --name {your_iot_hub_name}
建立 Web PubSub 執行個體
如果您的 Azure 訂用帳戶中已有 Web PubSub 執行個體,則您可以略過本節。
執行 az extension add 以將 webpubsub 延伸模組安裝或升級至目前版本。
az extension add --upgrade --name webpubsub
使用 Azure CLI az webpubsub create 命令,在您已建立的資源群組中建立 Web PubSub。 下列命令會在 EastUS 的資源群組 myResourceGroup 下建立免費 Web PubSub 資源:
重要
每個 Web PubSub 資源都必須有唯一的名稱。 使用下列範例中的 Web PubSub 名稱取代 <your-unique-resource-name>。
az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1
此命令的輸出顯示新建資源的屬性。 請記下下列兩個屬性:
- 資源名稱:您提供給上述
--name
參數的名稱。 - hostName:在此範例中,主機名稱為
<your-unique-resource-name>.webpubsub.azure.com/
。
此時,您的 Azure 帳戶是唯一獲得授權在此新資源上執行任何作業的帳戶。
在本機建立及執行函式
建立專案的空白資料夾,然後在新資料夾中執行下列命令。
func init --worker-runtime javascript --model V4
建立
index
函式,以讀取和裝載用戶端的靜態網頁。func new -n index -t HttpTrigger
使用下列程式碼更新
src/functions/index.js
,將 HTML 內容作為靜態網站。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, }; } });
在根資料夾下建立
index.html
檔案。<!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>
建立用戶端用來取得服務連線 URL 和存取權杖的
negotiate
函式。func new -n negotiate -t HttpTrigger
更新
src/functions/negotiate.js
以使用包含所產生權杖的WebPubSubConnection
。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')) }; }, });
使用
"IoT Hub (Event Hub)"
範本建立messagehandler
函式來產生通知。原始 連接字串 只針對示範目的出現在本文中。 在生產環境中,請一律保護您的存取金鑰。 使用 Azure 金鑰保存庫,安全地管理和輪替密鑰,並使用保護連線
WebPubSubServiceClient
。func new --template "Azure Event Hub trigger" --name messagehandler
使用下列 JSON 程式碼更新
src/functions/messagehandler.js
以新增 Web PubSub 輸出繫結。 我們使用變數%hubName%
作為 IoT eventHubName 和 Web PubSub 中樞的中樞名稱。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); } });
更新函式設定。
新增
hubName
設定,並且將{YourIoTHubName}
取代為建立 IoT 中樞時使用的中樞名稱。func settings add hubName "{YourIoTHubName}"
取得 IoT 中樞的服務連接字串。
az iot hub connection-string show --policy-name service --hub-name {YourIoTHubName} --output table --default-eventhub
設定
IOTHubConnectionString
,以值取代<iot-connection-string>
。func settings add IOTHubConnectionString "<iot-connection-string>"
- 取得 Web PubSub 的連接字串。
az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
設定
WebPubSubConnectionString
,以值取代<webpubsub-connection-string>
。func settings add WebPubSubConnectionString "<webpubsub-connection-string>"
注意
範例中使用的
Azure Event Hub trigger
函式觸發程序相依於 Azure 儲存體,但您可以在函式於本機執行時使用本機儲存體模擬器。 如果您收到There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid.
之類的錯誤,您必須下載並啟用儲存體模擬器。在本機執行函式。
現在您可以透過下列命令執行本機函式。
func start
您可以藉由造訪下列項目,造訪本機主機靜態頁面:
https://localhost:7071/api/index
。
執行裝置以傳送資料
註冊裝置
裝置必須向您的 IoT 中樞註冊,才能進行連線。 如果您已在 IoT 中樞內註冊裝置,則可以略過本節。
在 Azure Cloud Shell 中執行 az iot hub device-identity create 命令,以建立裝置身分識別。
YourIoTHubName:以您為 IoT 中樞選擇的名稱取代此預留位置。
az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
在 Azure Cloud Shell 中執行 Az PowerShell module iot hub device-identity connection-string show 命令,以針對您剛註冊的裝置取得「裝置連接字串」:
YourIoTHubName:以您為 IoT 中樞選擇的名稱取代此預留位置。
az iot hub device-identity connection-string show --hub-name {YourIoTHubName} --device-id simDevice --output table
記下裝置連接字串,看起來如下:
HostName={YourIoTHubName}.azure-devices.net;DeviceId=simDevice;SharedAccessKey={YourSharedAccessKey}
如需最快的結果,請使用 Raspberry Pi Azure IoT 線上模擬器來模擬溫度資料。 貼上「裝置連接字串」,然後選取 [執行] 按鈕。
如果您有實體 Raspberry Pi 和 BME280 感應器,可以藉由遵循將 Raspberry Pi 連線至 Azure IoT 中樞 (Node.js) 教學課程,測量及報告實際溫度和濕度值。
執行視覺效果網站
開啟函式主機索引頁面:http://localhost:7071/api/index
以檢視即時儀表板。 註冊多個裝置,您會看到儀表板即時更新多個裝置。 開啟多個瀏覽器,您會看到每個頁面都會即時更新。
清除資源
如果您打算繼續進行後續的快速入門和教學課程,您可以讓這些資源留在原處。
若不再需要,您可以使用 Azure CLI az group delete 命令來移除資源群組和所有相關資源:
az group delete --name "myResourceGroup"