你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:使用 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 参数提供的名称。
  • 主机名:在本例中,主机名为 <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
    
    • 更新 src/functions/messagehandler.js 以使用以下 json 代码添加 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"

后续步骤