Share via


教學課程:使用 Azure Web PubSub 服務和 Azure Functions 將 IoT 中樞的 IoT 裝置資料視覺化

在本教學課程中,您將會了解如何使用 Azure Web PubSub 服務和 Azure Functions,從 IoT 中樞建置具有即時資料視覺效果的無伺服器應用程式。

在本教學課程中,您會了解如何:

  • 建置無伺服器資料視覺效果應用程式
  • 一起使用 Web PubSub 函式輸入和輸出繫結與 Azure IoT 中樞
  • 在本機執行範例函式

必要條件

如果您沒有 Azure 訂用帳戶,請在開始之前先建立 Azure 免費帳戶

建立 IoT 中樞

在本節中,您會使用 Azure CLI 來建立 IoT 中樞和資源群組。 Azure 資源群組是在其中部署與管理 Azure 資源的邏輯容器。 IoT 中樞可作為 IoT 應用程式與裝置之間雙向通訊的中央訊息中樞。

如果您的 Azure 訂用帳戶中已有 IoT 中樞,則您可以略過本節。

若要建立 IoT 中樞和資源群組:

  1. 啟動 CLI 應用程式。 若要在此文其餘部分執行 CLI 命令,請複製命令語法,並將其貼入 CLI 應用程式,然後編輯變數值,再按 Enter

    • 如果您要使用 Cloud Shell,則請選取 CLI 命令上的 [試用] 按鈕,以在分割的瀏覽器視窗中啟動 Cloud Shell。 或者,您可以在不同的瀏覽器索引標籤中開啟 Cloud Shell
    • 如果您要在本機使用 Azure CLI,則請啟動 CLI 主控台應用程式,並登入 Azure CLI。
  2. 執行 az extension add 以將 azure-iot 延伸模組安裝或升級至目前版本。

    az extension add --upgrade --name azure-iot
    
  3. 在 CLI 應用程式中,執行 az group create 命令以建立資源群組。 下列命令會在 eastus 位置中建立名為 MyResourceGroup 的資源群組。

    注意

    您可以選擇性地設定不同的位置。 若要查看可用位置,請執行 az account list-locations。 本快速入門使用 eastus,如範例命令所示。

    az group create --name MyResourceGroup --location eastus
    
  4. 使用 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 資源都必須有唯一的名稱。 在下列範例中,將 <your-unique-resource-name> 取代為您的 Web PubSub 名稱。

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 帳戶是唯一獲得授權在此新資源上執行任何作業的帳戶。

在本機建立及執行函式

  1. 建立專案的空白資料夾,然後在新資料夾中執行下列命令。

    func init --worker-runtime javascript --model V4
    
  2. 建立 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, 
            };
        }
    });
    
  3. 在根資料夾下建立 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>
    
  4. 建立用戶端用來取得服務連線 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')) };
        },
    });
    
  5. 使用 "IoT Hub (Event Hub)" 範本建立 messagehandler 函式來產生通知。

     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);
          }
      });
      
  6. 更新函式設定。

    1. 新增 hubName 設定,並且將 {YourIoTHubName} 取代為建立 IoT 中樞時使用的中樞名稱。

      func settings add hubName "{YourIoTHubName}"
      
    2. 取得 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>"
    
    1. 取得 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. 之類的錯誤,您必須下載並啟用儲存體模擬器

  7. 在本機執行函式。

    現在您可以透過下列命令執行本機函式。

    func start
    

    您可以藉由造訪下列項目,造訪本機主機靜態頁面:https://localhost:7071/api/index

執行裝置以傳送資料

註冊裝置

裝置必須向您的 IoT 中樞註冊,才能進行連線。 如果您已在 IoT 中樞內註冊裝置,則可以略過本節。

  1. 在 Azure Cloud Shell 中執行 az iot hub device-identity create 命令,以建立裝置身分識別。

    YourIoTHubName:以您為 IoT 中樞選擇的名稱取代此預留位置。

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. 在 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}

執行視覺效果網站

開啟函式主機索引頁面:http://localhost:7071/api/index 以檢視即時儀表板。 註冊多個裝置,您會看到儀表板即時更新多個裝置。 開啟多個瀏覽器,您會看到每個頁面都會即時更新。

Screenshot of multiple devices data visualization using Web PubSub service.

清除資源

如果您打算繼續進行後續的快速入門和教學課程,您可以讓這些資源留在原處。

若不再需要,您可以使用 Azure CLI az group delete 命令來移除資源群組和所有相關資源:

az group delete --name "myResourceGroup"

下一步