Tutorial: Exibir dados do dispositivo IoT do Hub IoT usando o serviço 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 exibições de dados em tempo real do Hub IoT.

Neste tutorial, você aprenderá como:

  • Criar um aplicativo de exibição de dados sem servidor
  • Trabalhar em conjunto com associações de entrada e saída de função do Web PubSub e Hub IoT do Azure
  • Executar as funções de exemplo localmente

Pré-requisitos

Caso você não tenha uma assinatura do Azure, crie uma conta gratuita do Azure antes de começar.

Crie 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 contêiner lógico no qual os recursos do Azure são implantados e gerenciados. Um hub IoT atua como um hub central de mensagens para comunicação bidirecional entre o aplicativo IoT e os dispositivos.

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

Para criar um hub IoT e um grupo de recursos:

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

    • Se 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 estiver usando a CLI do Azure localmente, inicie o aplicativo de console da CLI e faça logon 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. No aplicativo da CLI, execute o comando az group create para criar um grupo de recursos. O comando a seguir cria um grupo de recursos chamado MyResourceGroup na localização eastus.

    Observação

    Opcionalmente, você pode definir um local diferente. Para ver os locais disponíveis, execute az account list-locations. Este início rápido usa eastus, conforme 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 o hub IoT. Um nome de Hub IoT deve ser exclusivo globalmente no Azure. Use o nome do hub IoT no restante deste guia de início rápido, sempre que ver 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, ignore 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 az webpubsub create da CLI do Azure para criar um Web PubSub no grupo de recursos criado. O seguinte comando cria um recurso gratuito do Web PubSub no grupo de recursos myResourceGroup no EastUS:

Importante

Cada recurso Web PubSub precisa ter um nome exclusivo. Substitua <nome-de recurso-exclusivo> pelo nome do 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. Anote as duas propriedades listadas abaixo:

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

Nesse ponto, sua conta do Azure é a única autorizada a executar qualquer operação nesse novo recurso.

Criar e executar as funções localmente

  1. Crie uma pasta vazia para o projeto e execute o comando a seguir na nova pasta.

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

    func new -n index -t HttpTrigger
    

    Atualize o src/functions/index.js com o código a seguir, que atende ao conteúdo HTML como 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 negotiate que os clientes usam para obter uma 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 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 função messagehandler para gerar notificações usando o modelo "IoT Hub (Event Hub)".

     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 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. Atualizar as configurações de função.

    1. Adicione a configuração hubName e substitua {YourIoTHubName} pelo nome do hub que você usou ao criar o 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>"
    

    Observação

    O gatilho de função Azure Event Hub trigger usado no exemplo tem dependência no 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 a função local com o comando abaixo.

    func start
    

    Você pode ir até a página estática do host local acessando: https://localhost:7071/api/index.

Executar o dispositivo para enviar dados

Registrar um dispositivo

Um dispositivo deve ser registrado no hub IoT antes de poder se conectar. Se você já tiver um dispositivo registrado em seu hub IoT, poderá ignorar essa seção.

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

    YourIoTHubName: substitua o espaço reservado pelo nome escolhido para o 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 o espaço reservado abaixo pelo nome escolhido para o hub IoT.

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

    Tome nota da cadeia de conexão do dispositivo, que se parece com o seguinte:

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

Executar o site de visualização

Abra a página de índice do host de funções: http://localhost:7071/api/index para exibir 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 você verá que todas as páginas são atualizadas em tempo real.

Screenshot of multiple devices data visualization using Web PubSub service.

Limpar os recursos

Se você planeja continuar a trabalhar com os tutoriais e inícios rápidos subsequentes, deixe esses recursos onde estão.

Quando não forem mais necessários, você poderá usar o comando az group delete da CLI do Azure para remover o grupo de recursos e todos os recursos relacionados:

az group delete --name "myResourceGroup"

Próximas etapas