Share via


Tutorial: Visualize dados de dispositivos IoT do Hub IoT usando o serviço Azure Web PubSub e o Azure Functions

Neste tutorial, você aprenderá a usar o serviço Azure Web PubSub e o Azure Functions para criar um aplicativo sem servidor com visualização de dados em tempo real do Hub IoT.

Neste tutorial, irá aprender a:

  • Crie um aplicativo de visualização de dados sem servidor
  • Trabalhe em conjunto com as ligações de entrada e saída da função Web PubSub e o hub IoT do Azure
  • Execute as funções de exemplo localmente

Pré-requisitos

  • Um editor de código, como o Visual Studio Code

  • Node.js, versão 18.x ou superior.

    Nota

    Para obter mais informações sobre as versões suportadas do Node.js, consulte a documentação de versões de tempo de execução do Azure Functions.

  • Ferramentas Principais do Azure Functions (v3 ou superior preferencial) para executar aplicativos do Azure Function localmente e implantar no Azure.

  • A CLI do Azure para gerenciar recursos do Azure.

Se não tiver uma subscrição do Azure, crie uma conta gratuita do Azure antes de começar.

Criar um hub IoT

Nesta seção, você usa a CLI do Azure para criar um hub IoT e um grupo de recursos. Um grupo de recursos do Azure é um contentor lógico no qual os recursos do Azure são implementados e geridos. Um hub IoT atua como um hub de mensagens central para comunicação bidirecional entre seu aplicativo IoT e os dispositivos.

Se você já tiver um hub IoT em sua assinatura do Azure, poderá ignorar esta seção.

Para criar um hub IoT e um grupo de recursos:

  1. Inicie seu aplicativo CLI. Para executar os comandos da CLI no restante deste artigo, copie a sintaxe do comando, cole-a no aplicativo da CLI, edite valores de variáveis e pressione Enter.

    • Se você estiver usando o Cloud Shell, selecione o botão Experimentar nos comandos da CLI para iniciar o Cloud Shell em uma janela dividida do navegador. Ou você pode abrir o Cloud Shell em uma guia separada do navegador.
    • Se você estiver usando a CLI do Azure localmente, inicie seu aplicativo de console da CLI e entre na CLI do Azure.
  2. Execute az extension add para instalar ou atualizar a extensão azure-iot para a versão atual.

    az extension add --upgrade --name azure-iot
    
  3. Em seu aplicativo CLI, execute o comando az group create para criar um grupo de recursos. O comando a seguir cria um grupo de recursos chamado MyResourceGroup no local eastus .

    Nota

    Opcionalmente, você pode definir um local diferente. Para ver os locais disponíveis, execute az account list-locations. Este guia de início rápido usa eastus como mostrado no comando de exemplo.

    az group create --name MyResourceGroup --location eastus
    
  4. Execute o comando az iot hub create para criar um hub IoT. Pode levar alguns minutos para criar um hub IoT.

    YourIotHubName. Substitua esse espaço reservado e as chaves ao redor no comando a seguir, usando o nome escolhido para seu hub IoT. Um nome de hub IoT deve ser globalmente exclusivo no Azure. Use o nome do hub IoT no restante deste início rápido onde quer que você veja o espaço reservado.

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

Criar uma instância do Web PubSub

Se você já tiver uma instância do Web PubSub em sua assinatura do Azure, poderá ignorar esta seção.

Execute az extension add para instalar ou atualizar a extensão webpubsub para a versão atual.

az extension add --upgrade --name webpubsub

Use o comando Azure CLI az webpubsub create para criar um Web PubSub no grupo de recursos que você criou. O comando a seguir cria um recurso Free Web PubSub no grupo de recursos myResourceGroup em EastUS:

Importante

Cada recurso Web PubSub deve ter um nome exclusivo. Substitua <your-unique-resource-name> pelo nome do seu Web PubSub nos exemplos a seguir.

az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1

A saída deste comando mostra as propriedades do recurso recém-criado. Tome nota das duas propriedades listadas abaixo:

  • Nome do recurso: O nome que você forneceu para o --name parâmetro acima.
  • hostName: No exemplo, o nome do host é <your-unique-resource-name>.webpubsub.azure.com/.

Neste ponto, sua conta do Azure é a única autorizada a executar quaisquer operações neste novo recurso.

Criar e executar as funções localmente

  1. Crie uma pasta vazia para o projeto e, em seguida, execute o seguinte comando na nova pasta.

    func init --worker-runtime javascript --model V4
    
  2. Crie uma função para ler e hospedar uma index página da Web estática para clientes.

    func new -n index -t HttpTrigger
    

    Atualize src/functions/index.js com o código a seguir, que serve o conteúdo HTML como um site estático.

    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. Crie um index.html arquivo na pasta raiz.

    <!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. Crie uma função que os clientes usam para obter uma negotiate URL de conexão de serviço e um token de acesso.

    func new -n negotiate -t HttpTrigger
    

    Atualize src/functions/negotiate.js para usar WebPubSubConnection que contém o token gerado.

    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. Crie uma messagehandler função para gerar notificações usando o "IoT Hub (Event Hub)" modelo.

     func new --template "Azure Event Hub trigger" --name messagehandler
    
    • Atualize src/functions/messagehandler.js para adicionar a ligação de saída Web PubSub com o seguinte código json. Usamos a variável %hubName% como o nome do hub para o hub IoT eventHubName e 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. Atualize as configurações da função.

    1. Adicione hubName a configuração e substitua {YourIoTHubName} pelo nome do hub que você usou ao criar seu Hub IoT.

      func settings add hubName "{YourIoTHubName}"
      
    2. Obtenha a cadeia de conexão de serviço para o Hub IoT.

    az iot hub connection-string show --policy-name service --hub-name {YourIoTHubName} --output table --default-eventhub
    

    Defina IOTHubConnectionString, substituindo <iot-connection-string> pelo valor.

    func settings add IOTHubConnectionString "<iot-connection-string>"
    
    1. Obtenha a cadeia de conexão para Web PubSub.
    az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
    

    Defina WebPubSubConnectionString, substituindo <webpubsub-connection-string> pelo valor.

    func settings add WebPubSubConnectionString "<webpubsub-connection-string>"
    

    Nota

    O Azure Event Hub trigger gatilho de função usado no exemplo tem dependência do Armazenamento do Azure, mas você pode usar um emulador de armazenamento local quando a função estiver sendo executada localmente. Se você receber um erro como There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid., precisará baixar e habilitar o Emulador de Armazenamento.

  7. Execute a função localmente.

    Agora você pode executar sua função local pelo comando abaixo.

    func start
    

    Você pode visitar sua página estática de host local visitando: https://localhost:7071/api/index.

Execute o dispositivo para enviar dados

Registar um dispositivo

É necessário registar um dispositivo no hub IoT antes de o mesmo se poder ligar. Se já tiver um dispositivo registado no seu hub IoT, pode ignorar esta secção.

  1. Execute o comando az iot hub device-identity create no Azure Cloud Shell para criar a identidade do dispositivo.

    YourIoTHubName: substitua este espaço reservado pelo nome que você escolheu para seu hub IoT.

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. Execute o comando Az PowerShell module iot hub device-identity connection-string show no Azure Cloud Shell para obter a cadeia de conexão do dispositivo para o dispositivo que você acabou de registrar:

    YourIoTHubName: substitua este espaço reservado abaixo pelo nome que você escolheu para seu hub IoT.

    az iot hub device-identity connection-string show --hub-name {YourIoTHubName} --device-id simDevice --output table
    

    Anote a cadeia de conexão do dispositivo, que tem esta aparência:

    HostName={YourIoTHubName}.azure-devices.net;DeviceId=simDevice;SharedAccessKey={YourSharedAccessKey}

  • Para obter resultados mais rápidos, simule dados de temperatura usando o Simulador do Raspberry Pi Azure IoT Online. Cole na cadeia de conexão do dispositivo e selecione o botão Executar.

  • Se você tiver um sensor físico do Raspberry Pi e BME280, poderá medir e relatar valores reais de temperatura e umidade seguindo o tutorial Conectar o Raspberry Pi ao Hub IoT do Azure (Node.js ).

Executar o site de visualização

Abra a página de índice do host da função: http://localhost:7071/api/index para visualizar o painel em tempo real. Registre vários dispositivos e você verá que o painel atualiza vários dispositivos em tempo real. Abra vários navegadores e verá que todas as páginas são atualizadas em tempo real.

Screenshot of multiple devices data visualization using Web PubSub service.

Clean up resources (Limpar recursos)

Se quiser continuar a trabalhar com os inícios rápidos e tutoriais subsequentes, pode manter estes recursos.

Quando não for mais necessário, você pode usar o comando azur CLI az group delete para remover o grupo de recursos e todos os recursos relacionados:

az group delete --name "myResourceGroup"

Próximos passos