Kurz: Vizualizace dat zařízení IoT ze služby IoT Hub pomocí služby Azure Web PubSub a Azure Functions

V tomto kurzu se naučíte používat službu Azure Web PubSub a Azure Functions k vytvoření bezserverové aplikace s vizualizací dat v reálném čase ze služby IoT Hub.

V tomto kurzu se naučíte:

  • Vytvoření aplikace pro vizualizaci dat bez serveru
  • Spolupráce se vstupními a výstupními vazbami funkce Web PubSub a Azure IoT Hubem
  • Místní spuštění ukázkových funkcí

Požadavky

  • Editor kódu, například Visual Studio Code

  • Node.js, verze 18.x nebo vyšší.

    Poznámka:

    Další informace o podporovaných verzích Node.js najdete v dokumentaci k verzím modulu runtime služby Azure Functions.

  • Azure Functions Core Tools (upřednostňovaná verze 3 nebo vyšší) pro místní spouštění aplikací Funkcí Azure a jejich nasazení do Azure.

  • Azure CLI pro správu prostředků Azure.

Pokud ještě nemáte předplatné Azure, vytvořte si bezplatný účet Azure před tím, než začnete.

Vytvoření centra IoT

V této části použijete Azure CLI k vytvoření centra IoT a skupiny prostředků. Skupina prostředků Azure je logický kontejner, ve kterém se nasazují a spravují prostředky Azure. Centrum IoT funguje jako centrální centrum zpráv pro obousměrnou komunikaci mezi vaší aplikací IoT a zařízeními.

Pokud už ve svém předplatném Azure máte centrum IoT, můžete tuto část přeskočit.

Vytvoření centra IoT a skupiny prostředků:

  1. Spusťte aplikaci CLI. Pokud chcete spustit příkazy rozhraní příkazového řádku ve zbývající části tohoto článku, zkopírujte syntaxi příkazu, vložte ho do aplikace CLI, upravte hodnoty proměnných a stiskněte Enter.

    • Pokud používáte Cloud Shell, vyberte v příkazech rozhraní příkazového řádku tlačítko Vyzkoušet a spusťte Cloud Shell v rozděleném okně prohlížeče. Nebo můžete Cloud Shell otevřít na samostatné kartě prohlížeče.
    • Pokud používáte Azure CLI místně, spusťte konzolovou aplikaci CLI a přihlaste se k Azure CLI.
  2. Spuštěním příkazu az extension add nainstalujte nebo upgradujte rozšíření azure-iot na aktuální verzi.

    az extension add --upgrade --name azure-iot
    
  3. V aplikaci CLI spusťte příkaz az group create a vytvořte skupinu prostředků. Následující příkaz vytvoří skupinu prostředků MyResourceGroup v umístění eastus.

    Poznámka:

    Volitelně můžete nastavit jiné umístění. Pokud chcete zobrazit dostupná umístění, spusťte az account list-locationspříkaz . V tomto rychlém startu se používá eastus , jak je znázorněno v ukázkovém příkazu.

    az group create --name MyResourceGroup --location eastus
    
  4. Spuštěním příkazu az iot hub create vytvořte IoT Hub. Vytvoření centra IoT může trvat několik minut.

    YourIotHubName. Nahraďte tento zástupný symbol a okolní složené závorky v následujícím příkazu pomocí názvu, který jste zvolili pro centrum IoT. Název centra IoT musí být v Azure globálně jedinečný. Ve zbývající části tohoto rychlého startu použijte název centra IoT, ať se zobrazí zástupný symbol.

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

Vytvoření instance Web PubSub

Pokud už ve svém předplatném Azure máte instanci Web PubSub, můžete tuto část přeskočit.

Spuštěním příkazu az extension add nainstalujte nebo upgradujte rozšíření webpubsub na aktuální verzi.

az extension add --upgrade --name webpubsub

Pomocí příkazu az webpubsub az webpubsub vytvořte web pubSub ve skupině prostředků, kterou jste vytvořili. Následující příkaz vytvoří prostředek Free Web PubSub ve skupině prostředků myResourceGroup v eastUS:

Důležité

Každý prostředek Web PubSub musí mít jedinečný název. V následujících příkladech nahraďte <název_prostředku-unique-resource názvem> podsítě Web PubSub.

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

Výstup tohoto příkazu zobrazuje vlastnosti nově vytvořeného prostředku. Poznamenejte si hodnoty dvou vlastností uvedených níže:

  • Název prostředku: Název, který jste zadali výše uvedenému parametru --name .
  • hostName: V příkladu je <your-unique-resource-name>.webpubsub.azure.com/název hostitele .

V tuto chvíli je váš účet Azure jediným autorizovaným k provádění jakýchkoli operací s tímto novým prostředkem.

Místní vytvoření a spuštění funkcí

  1. Vytvořte pro projekt prázdnou složku a pak v nové složce spusťte následující příkaz.

    func init --worker-runtime javascript --model V4
    
  2. Vytvořte index funkci pro čtení a hostování statické webové stránky pro klienty.

    func new -n index -t HttpTrigger
    

    Aktualizujte src/functions/index.js následujícím kódem, který slouží obsahu HTML jako statický web.

    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. Vytvořte index.html soubor v kořenové složce.

    <!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. Vytvořte negotiate funkci, kterou klienti používají k získání adresy URL připojení služby a přístupového tokenu.

    func new -n negotiate -t HttpTrigger
    

    Aktualizujte src/functions/negotiate.js na použití WebPubSubConnection , která obsahuje vygenerovaný token.

    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. Vytvořte funkci pro messagehandler generování oznámení pomocí "IoT Hub (Event Hub)" šablony.

     func new --template "Azure Event Hub trigger" --name messagehandler
    
    • Aktualizujte src/functions/messagehandler.js a přidejte výstupní vazbu Web PubSub s následujícím kódem JSON. Jako název centra používáme proměnnou %hubName% pro ioT eventHubName i web PubSub Hub.

      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. Aktualizujte nastavení funkce.

    1. Přidejte nastavení a nahraďte hubName{YourIoTHubName} názvem centra, který jste použili při vytváření ioT Hubu.

      func settings add hubName "{YourIoTHubName}"
      
    2. Získejte řetězec Připojení ion služby pro IoT Hub.

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

    Nastavte IOTHubConnectionStringhodnotu a nahraďte <iot-connection-string> ji hodnotou.

    func settings add IOTHubConnectionString "<iot-connection-string>"
    
    1. Získejte řetězec Připojení ion pro web pubSub.
    az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
    

    Nastavte WebPubSubConnectionStringhodnotu a nahraďte <webpubsub-connection-string> ji hodnotou.

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

    Poznámka:

    Aktivační Azure Event Hub trigger událost funkce použitá v ukázce má závislost na službě Azure Storage, ale při místním spuštění funkce můžete použít emulátor místního úložiště. Pokud se zobrazí například chyba There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid., budete si muset stáhnout a povolit emulátor úložiště.

  7. Spusťte funkci místně.

    Teď můžete místní funkci spustit pomocí následujícího příkazu.

    func start
    

    Statickou stránku místního hostitele můžete navštívit: https://localhost:7071/api/index.

Spuštění zařízení pro odesílání dat

Registrace zařízení

Zařízení musí být zaregistrované ve vašem centru IoT Hub, aby se mohlo připojit. Pokud už máte zařízení zaregistrované ve službě IoT Hub, můžete tuto část přeskočit.

  1. Spuštěním příkazu az iot hub device-identity create v Azure Cloud Shellu vytvořte identitu zařízení.

    YourIoTHubName: Nahraďte tento zástupný symbol názvem, který jste zvolili pro centrum IoT.

    az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
    
  2. Spuštěním příkazu Show pro modul Az PowerShell hub device-identity connection-string v Azure Cloud Shellu získejte připojovací řetězec zařízení, které jste právě zaregistrovali:

    YourIoTHubName: Nahraďte tento zástupný symbol názvem, který jste zvolili pro centrum IoT.

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

    Poznamenejte si připojovací řetězec zařízení, které vypadá takto:

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

  • Pro nejrychlejší výsledky simulujte teplotní data pomocí simulátoru Raspberry Pi Azure IoT Online. Vložte zařízení připojovací řetězec a vyberte tlačítko Spustit.

  • Pokud máte fyzický senzor Raspberry Pi a BME280, můžete měřit a hlásit skutečné hodnoty teploty a vlhkosti podle kurzu Připojení Raspberry Pi do Azure IoT Hubu (Node.js).

Spuštění webu vizualizace

Otevření stránky indexu hostitele funkce: http://localhost:7071/api/index zobrazení řídicího panelu v reálném čase Zaregistrujte více zařízení a v reálném čase se zobrazí aktualizace více zařízení na řídicím panelu. Otevřete více prohlížečů a uvidíte, že se každá stránka aktualizuje v reálném čase.

Screenshot of multiple devices data visualization using Web PubSub service.

Vyčištění prostředků

Pokud chcete pokračovat v práci s dalšími rychlými starty a kurzy, možná budete chtít tyto prostředky zachovat.

Pokud už ji nepotřebujete, můžete k odebrání skupiny prostředků a všech souvisejících prostředků použít příkaz az group delete v Azure CLI:

az group delete --name "myResourceGroup"

Další kroky