Руководство. Визуализация данных устройства Интернета вещей из Центр Интернета вещей с помощью службы Azure Web PubSub и Функции Azure

В этом руководстве вы узнаете, как использовать службу Azure Web PubSub и Функции Azure для создания бессерверного приложения с визуализацией данных в режиме реального времени из Центр Интернета вещей.

В этом руководстве описано следующее:

  • Создание приложения визуализации бессерверных данных
  • Совместная работа с входными и выходными привязками функции Web PubSub и Центром Интернета вещей Azure
  • Локальный запуск примера функций

Необходимые компоненты

Если у вас еще нет подписки Azure, создайте бесплатную учетную запись Azure, прежде чем начинать работу.

Создание Центра Интернета вещей

В этом разделе описано, как использовать Azure CLI для создания центра Интернета вещей и группы ресурсов. Группа ресурсов Azure является логическим контейнером, в котором происходит развертывание ресурсов Azure и управление ими. Центр Интернета вещей действует в качестве центра сообщений для двусторонней связи между приложением Интернета вещей и устройствами.

Если у вас уже есть центр Интернета вещей в подписке Azure, этот раздел можно пропустить.

Чтобы создать центр Интернета вещей и группу ресурсов, выполните следующие действия:

  1. Запустите приложение CLI. Чтобы выполнить команды CLI в остальной части этой статьи, скопируйте синтаксис команды, вставьте его в приложение CLI, измените значения переменных и нажмите клавишу Enter.

    • При использовании Cloud Shell нажмите кнопку Попробовать в командах интерфейса командной строки, чтобы запустить 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, чтобы создать группу ресурсов. В следующей команде создается группа ресурсов с именем MyResourceGroup в расположении eastus.

    Примечание.

    При необходимости можно задать другое расположение. Чтобы отобразить доступные расположения, выполните команду az account list-locations. В рамках работы с этим кратким руководством используется eastus, как показано в примере команды.

    az group create --name MyResourceGroup --location eastus
    
  4. Создайте Центр Интернета вещей с помощью команды az iot hub create. Создание Центра Интернета вещей может занять несколько минут.

    YourIotHubName Замените этот заполнитель и окружающие фигурные скобки в указанной ниже команде именем своего центра Интернета вещей. Имя центра Интернета вещей должно быть уникальным по всему Azure. Используйте имя центра Интернета вещей при работе с оставшейся частью этого краткого руководства везде вместо заполнителя.

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

Создание экземпляра Web PubSub

Если у вас уже есть экземпляр Web PubSub в подписке Azure, можно пропустить этот раздел.

Запустите az extension add to install or upgrade the webpubsub extension to the current version.

az extension add --upgrade --name webpubsub

Используйте команду azure CLI az webpubsub create , чтобы создать web PubSub в созданной группе ресурсов. Следующая команда создает ресурс Free Web PubSub в группе ресурсов myResourceGroup в EastUS:

Важно!

Каждый ресурс 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. Создайте функцию, используемую negotiate клиентами для получения URL-адреса подключения службы и маркера доступа.

    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. messagehandler Создайте функцию для создания уведомлений с помощью "IoT Hub (Event Hub)" шаблона.

     func new --template "Azure Event Hub trigger" --name messagehandler
    
    • Обновите src/functions/messagehandler.js , чтобы добавить выходную привязку Web PubSub со следующим кодом json. Мы используем переменную %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} имя концентратора, которое вы использовали при создании Центр Интернета вещей.

      func settings add hubName "{YourIoTHubName}"
      
    2. Получите строку Подключение ion service для Центр Интернета вещей.

    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. Получите строку Подключение ion для 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

Запуск устройства для отправки данных

Регистрация устройства

Устройство должно быть зарегистрировано в центре Интернета вещей, прежде чем оно сможет подключиться. Если устройство уже зарегистрировано в центре Интернета вещей, этот раздел можно пропустить.

  1. Чтобы создать удостоверение устройства, выполните команду az iot hub device-identity create в Azure Cloud Shell.

    YourIoTHubName: замените этот заполнитель именем, выбранным для центра Интернета вещей.

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. Выполните команду az PowerShell module iot hub device-identity-identity-string show в Azure Cloud Shell, чтобы получить устройство строка подключения для только что зарегистрированного устройства:

    YourIoTHubName. Замените этот заполнитель именем вашего центра Интернета вещей.

    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 Online. Вставьте строка подключения устройства и нажмите кнопку "Выполнить".

  • Если у вас есть физический датчик Raspberry Pi и BME280, можно измерить и сообщить о реальных значениях температуры и влажности, следуя руководству по Подключение Raspberry Pi в Центр Интернета вещей Azure (Node.js).

Запуск веб-сайта визуализации

Откройте страницу индекса узла функции: 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"

Следующие шаги