Compartir a través de


Tutorial: Visualización de datos de dispositivos IoT desde IoT Hub mediante el servicio Azure Web PubSub y Azure Functions

En este tutorial, aprenderá a usar el servicio Azure Web PubSub y Azure Functions para compilar una aplicación sin servidor con visualización en tiempo real desde IoT Hub.

En este tutorial, aprenderá a:

  • Compilar una aplicación de visualización de datos sin servidor
  • Trabajar junto con los enlaces de entrada y salida de funciones de Web PubSub y Azure IoT Hub
  • Ejecutar localmente funciones de ejemplo.

Importante

Las cadenas de conexión sin procesar solo aparecen en este artículo con fines de demostración.

Una cadena de conexión incluye la información de autorización necesaria para que la aplicación acceda al servicio Azure Web PubSub. La clave de acceso dentro de la cadena de conexión es similar a una contraseña raíz para el servicio. En entornos de producción, proteja siempre las claves de acceso. Use Azure Key Vault para administrar y rotar las claves de forma segura y proteja la conexión con WebPubSubServiceClient.

Evite distribuirlas a otros usuarios, codificarlas de forma rígida o guardarlas en un archivo de texto sin formato al que puedan acceder otros usuarios. Rote sus claves si cree que se han puesto en peligro.

Requisitos previos

Si no tiene una suscripción a Azure, cree una cuenta gratuita de Azure antes de empezar.

Crear un centro de IoT

En esta sección, usará la CLI de Azure para crear un centro de IoT y un grupo de recursos. Un grupo de recursos de Azure es un contenedor lógico en el que se implementan y se administran los recursos de Azure. Un centro de IoT funciona como centro de mensajes unificado para la comunicación bidireccional entre la aplicación de IoT y los dispositivos.

Si ya tiene un centro de IoT en su suscripción de Azure, puede omitir esta sección.

Para crear un centro de IoT y un grupo de recursos:

  1. Inicie la aplicación de la CLI. Para ejecutar los comandos de la CLI en el resto de este artículo, copie la sintaxis de comando, péguela en la aplicación de la CLI, edite los valores de las variables y presione Enter.

    • Si usa Cloud Shell, seleccione el botón Probar en los comandos de la CLI para iniciar Cloud Shell en una ventana dividida del explorador. O bien, puede abrir Cloud Shell en una pestaña del explorador independiente.
    • Si usa la CLI de Azure localmente, inicie la aplicación de consola de la CLI e inicie sesión en la CLI de Azure.
  2. Ejecute az extension add para instalar o actualizar la extensión azure-iot a la versión actual.

    az extension add --upgrade --name azure-iot
    
  3. En la aplicación de la CLI, ejecute el comando az group create para crear un grupo de recursos. El siguiente comando crea un grupo de recursos denominado MyResourceGroup en la ubicación eastus.

    Nota

    Opcionalmente, puede establecer otra ubicación. Para ver las ubicaciones disponibles, ejecute az account list-locations. En este inicio rápido se usa eastus, como se muestra en el comando de ejemplo.

    az group create --name MyResourceGroup --location eastus
    
  4. Ejecute el comando az iot hub create para crear una instancia de IoT Hub. La creación de una instancia de IoT Hub puede tardar unos minutos.

    YourIotHubName. Reemplace este marcador de posición y las llaves circundantes en el siguiente comando, con el nombre que eligió para el centro de IoT. Un nombre de centro de IoT debe ser único globalmente en Azure. Use el nombre del centro de IoT a lo largo de este inicio rápido cuando vea el marcador de posición.

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

Creación de una instancia de Web PubSub

Si ya tiene una instancia de Azure Web PubSub en su suscripción de Azure, puede omitir esta sección.

Ejecute az extension add para instalar o actualizar la extensión webpubsub en la versión actual.

az extension add --upgrade --name webpubsub

Use el comando az webpubsub create de la CLI de Azure para crear una instancia de Web PubSub en el grupo de recursos que ha creado. El comando siguiente crea un recurso Free de Web PubSub en el grupo de recursos myResourceGroup en EastUS:

Importante

Cada recurso de Web PubSub debe tener un nombre único. Reemplace <your-unique-resource-name> por el nombre de Web PubSub en los ejemplos siguientes.

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

La salida de este comando muestra las propiedades del recurso que acaba de crear. Tome nota de las dos propiedades siguientes:

  • Nombre del recurso: nombre que proporcionó al parámetro --name anterior.
  • hostName: en el ejemplo, el nombre de host es <your-unique-resource-name>.webpubsub.azure.com/.

En este momento, su cuenta de Azure es la única autorizada para realizar operaciones en este nuevo recurso.

Creación y ejecución locales de las funciones

  1. Cree una carpeta vacía para el proyecto y ejecute el siguiente comando en la nueva carpeta.

    func init --worker-runtime javascript --model V4
    
  2. Cree una función index para leer y hospedar una página web estática para los clientes.

    func new -n index -t HttpTrigger
    

    Actualice src/functions/index.js con el código siguiente, que sirve el contenido HTML como un sitio 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. Cree un archivo index.html en la carpeta raíz.

    <!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. Cree una función negotiate que los clientes usarán para obtener una dirección URL de conexión de servicio y un token de acceso.

    func new -n negotiate -t HttpTrigger
    

    Actualice src/functions/negotiate.js para usar WebPubSubConnection que contenga el token generado.

    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. Cree una función messagehandler para generar notificaciones con la plantilla "IoT Hub (Event Hub)".

    Las cadenas de conexión sin procesar solo aparecen en este artículo con fines de demostración. En entornos de producción, proteja siempre las claves de acceso. Use Azure Key Vault para administrar y rotar las claves de forma segura y proteja la conexión con WebPubSubServiceClient.

     func new --template "Azure Event Hub trigger" --name messagehandler
    
    • Actualice src/functions/messagehandler.js para agregar enlace de salida de Web PubSub con el código JSON siguiente. Usamos la variable %hubName% como nombre de centro para IoT eventHubName como para el centro de 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. Actualización de la configuración de la función.

    1. Agregue el valor hubName y reemplace {YourIoTHubName} por el nombre del centro que usó al crear el centro de IoT.

      func settings add hubName "{YourIoTHubName}"
      
    2. Obtenga la cadena de conexión del servicio del centro de IoT.

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

    Establezca IOTHubConnectionString, reemplazando <iot-connection-string> con el valor.

    func settings add IOTHubConnectionString "<iot-connection-string>"
    
    1. Obtenga la cadena de conexión para Web PubSub.
    az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
    

    Establezca WebPubSubConnectionString, reemplazando <webpubsub-connection-string> con el valor.

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

    Nota

    El desencadenador de funciones Azure Event Hub trigger usado en el ejemplo tiene dependencia de Azure Storage, pero puede usar un emulador de almacenamiento local cuando la función se ejecute localmente. Si se produce un error como There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid., tendrá que descargar y habilitar el emulador de Storage.

  7. Ejecute la función de forma local.

    Ya puede ejecutar la función local mediante el comando siguiente.

    func start
    

    Puede visitar la página estática del host local visitando: https://localhost:7071/api/index.

Ejecución del dispositivo para enviar datos

Registrar un dispositivo

Debe registrar un dispositivo con IoT Hub antes de poder conectarlo. Si ya tiene un dispositivo registrado en el centro de IoT, puede omitir esta sección.

  1. Ejecute el comando az iot hub device-identity create en Azure Cloud Shell para crear la identidad del dispositivo.

    YourIoTHubName: reemplace este marcador de posición por el nombre elegido para el centro de IoT.

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. Ejecute el comando Az PowerShell module iot hub device-identity connection-string show en Azure Cloud Shell para obtener la cadena de conexión del dispositivo que acaba de registrar:

    YourIoTHubName: reemplace este marcador de posición por el nombre elegido para el centro de IoT.

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

    Anote la cadena de conexión del dispositivo, que se parecerá a esta:

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

Ejecución del sitio web de visualización

Abra la página de índice del host de funciones: http://localhost:7071/api/index para ver el panel en tiempo real. Registre varios dispositivos y podrá ver que el panel actualiza varios dispositivos en tiempo real. Abra varios exploradores y podrá ver que todas las páginas se actualizan en tiempo real.

Captura de pantalla de la visualización de datos de varios dispositivos mediante el servicio Web PubSub.

Limpieza de recursos

Si tiene pensado seguir trabajando en otras guías de inicio rápido y tutoriales, considere la posibilidad de dejar estos recursos activos.

Cuando ya no se necesiten, puede usar el comando az group delete de la CLI de Azure para quitar el grupo de recursos y todos los recursos relacionados.

az group delete --name "myResourceGroup"

Pasos siguientes