将设备连接到远程监控预配置解决方案 (Node.js)

本教程实施一个可将以下遥测数据发送到远程监控预配置解决方案冷却器设备:

  • 温度
  • 压力
  • 湿度

为简单起见,代码会生成冷却器的示例遥测值。 可以通过将真实的传感器连接到设备并发送真实的遥测数据,在本示例的基础上融会贯通。

示例设备还会:

  • 将元数据发送到解决方案,以描述设备的功能。
  • 针对通过解决方案中的“设备”页触发的操作做出响应。
  • 针对通过解决方案中的“设备”页发送的配置更改做出响应。

要完成此教程,需要一个有效的 Azure 帐户。 如果没有帐户,可以创建一个试用帐户,只需几分钟即可完成。 有关详细信息,请参阅 Azure 试用

开始之前

在为设备编写任何代码之前,必须先预配远程监控预配置解决方案,并在该解决方案中预配新的自定义设备。

预配远程监控预配置解决方案

本教程中创建的冷却器设备会将数据发送到远程监控预配置解决方案的实例中。 如果尚未在 Azure 帐户中预配远程监控预配置解决方案,请参阅部署远程监控预配置解决方案

预配好远程监控解决方案后,单击“启动” ,在浏览器中打开解决方案仪表板。

解决方案仪表板

在远程监控方案中预配设备

Note

如果已在解决方案中预配了设备,则可以跳过此步骤。 创建客户端应用程序时需要设备凭据。

连接到预配置解决方案的设备必须能够使用有效凭据对 IoT 中心识别自身。 可以在解决方案的“设备”页中检索设备凭据。 本教程后文中的客户端应用程序要采用该设备凭据。

若要在远程监控解决方案中添加设备,请在解决方案中的“设备”页上完成以下步骤:

  1. 选择“预配”,并选择“物理”作为设备类型

    预配物理设备

  2. 输入 Physical-chiller 作为设备 ID。 选择“对称密钥”和“自动生成密钥”选项:

    选择设备选项

若要找到设备在连接到预配置解决方案时必须使用的凭据,请在浏览器中导航到 Azure 门户。 登录到订阅。

  1. 找到包含远程监控解决方案所用 Azure 服务的资源组。 该资源组与预配的远程监控解决方案同名。

  2. 在此资源组中导航到 IoT 中心。 然后选择“IoT 设备”:

    设备资源管理器

  3. 选择在远程监控解决方案中的“设备”页上创建的设备 ID

  4. 记下“设备 ID”和“设备密钥”值。 添加用于将设备连接到解决方案的代码时,将要使用这些值。

现已在远程监控预配置解决方案中预配了一个物理设备。 在以下部分中,我们将会实现使用设备凭据连接到解决方案的客户端应用程序。

客户端应用程序实现内置的冷却器设备模型。 预配置解决方案设备模型指定有关设备的以下信息:

  • 设备报告给解决方案的属性。 例如,冷却器设备报告有关其固件和位置的信息。
  • 由设备发送到解决方案的遥测数据类型。 例如,冷却器设备发送温度、湿度和压力值。
  • 可以在解决方案中计划的、要在设备上运行的方法。 例如,冷却器设备必须实现 RebootFirmwareUpdateEmergencyValveReleaseIncreasePressuree 方法。

本教程介绍如何将物理设备连接到远程监控预配置解决方案。 在本教程中,将使用 Node.js,它对于资源约束最少的环境是一个不错的选择。

创建 Node.js 解决方案

请确保已在开发计算机上安装 Node.js 版本 4.0.0 或更高版本。 若要检查版本,可在命令行中运行 node --version

  1. 在开发计算机上,创建名为 RemoteMonitoring 的文件夹。 在命令行环境中导航到此文件夹。

  2. 若要下载并安装完成示例应用所需的包,请运行以下命令:

    npm init
    npm install async azure-iot-device azure-iot-device-mqtt --save
    
  3. RemoteMonitoring 文件夹中,创建名为 remote_monitoring.js 的文件。 在文本编辑器中打开此文件。

  4. remote_monitoring.js 文件中,添加以下 require 语句:

    var Protocol = require('azure-iot-device-mqtt').Mqtt;
    var Client = require('azure-iot-device').Client;
    var ConnectionString = require('azure-iot-device').ConnectionString;
    var Message = require('azure-iot-device').Message;
    var async = require('async');
    
  5. require 语句之后添加以下变量声明。 将占位符值 {device connection string} 替换为针对远程监控解决方案中预配的设备记下的值:

    var connectionString = '{device connection string}';
    var deviceId = ConnectionString.parse(connectionString).DeviceId;
    
  6. 若要定义一些基本遥测数据,请添加以下变量:

    var temperature = 50;
    var temperatureUnit = 'F';
    var humidity = 50;
    var humidityUnit = '%';
    var pressure = 55;
    var pressureUnit = 'psig';
    
  7. 若要定义一些属性值,请添加以下变量:

    var temperatureSchema = 'chiller-temperature;v1';
    var humiditySchema = 'chiller-humidity;v1';
    var pressureSchema = 'chiller-pressure;v1';
    var interval = "00:00:05";
    var deviceType = "Chiller";
    var deviceFirmware = "1.0.0";
    var deviceFirmwareUpdateStatus = "";
    var deviceLocation = "Building 44";
    var deviceLatitude = 47.638928;
    var deviceLongitude = -122.13476;
    var deviceOnline = true;
    
  8. 添加以下变量以定义要发送到解决方案的报告属性。 这些属性包括用于说明设备使用的方法和遥测的元数据:

    var reportedProperties = {
      "Protocol": "MQTT",
      "SupportedMethods": "Reboot,FirmwareUpdate,EmergencyValveRelease,IncreasePressure",
      "Telemetry": {
        "TemperatureSchema": {
          "Interval": interval,
          "MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
          "MessageSchema": {
            "Name": temperatureSchema,
            "Format": "JSON",
            "Fields": {
              "temperature": "Double",
              "temperature_unit": "Text"
            }
          }
        },
        "HumiditySchema": {
          "Interval": interval,
          "MessageTemplate": "{\"humidity\":${humidity},\"humidity_unit\":\"${humidity_unit}\"}",
          "MessageSchema": {
            "Name": humiditySchema,
            "Format": "JSON",
            "Fields": {
              "humidity": "Double",
              "humidity_unit": "Text"
            }
          }
        },
        "PressureSchema": {
          "Interval": interval,
          "MessageTemplate": "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}",
          "MessageSchema": {
            "Name": pressureSchema,
            "Format": "JSON",
            "Fields": {
              "pressure": "Double",
              "pressure_unit": "Text"
            }
          }
        }
      },
      "Type": deviceType,
      "Firmware": deviceFirmware,
      "FirmwareUpdateStatus": deviceFirmwareUpdateStatus,
      "Location": deviceLocation,
      "Latitude": deviceLatitude,
      "Longitude": deviceLongitude
    }
    
  9. 若要输出操作结果,请添加以下帮助程序函数:

    function printErrorFor(op) {
        return function printError(err) {
            if (err) console.log(op + ' error: ' + err.toString());
        };
    }
    
  10. 添加以下帮助器函数用于随机化遥测值:

    function generateRandomIncrement() {
        return ((Math.random() * 2) - 1);
    }
    
  11. 添加以下泛型函数以处理解决方案中的直接方法调用。 该函数显示调用的直接方法的相关信息,但在此示例中不以任何方式修改设备。 解决方案使用直接方法对设备进行操作:

    function onDirectMethod(request, response) {
      // Implement logic asynchronously here.
      console.log('Simulated ' + request.methodName);
    
      // Complete the response
      response.send(200, request.methodName + ' was called on the device', function (err) {
        if (err) console.error('Error sending method response :\n' + err.toString());
        else console.log('200 Response to method \'' + request.methodName + '\' sent successfully.');
      });
    }
    
  12. 添加以下函数以处理解决方案中的 FirmwareUpdate 直接方法调用。 该函数验证直接方法负载中传入的参数,并以异步方式运行固件更新模拟:

    function onFirmwareUpdate(request, response) {
      // Get the requested firmware version from the JSON request body
      var firmwareVersion = request.payload.Firmware;
      var firmwareUri = request.payload.FirmwareUri;
    
      // Ensure we got a firmware values
      if (!firmwareVersion || !firmwareUri) {
        response.send(400, 'Missing firmware value', function(err) {
          if (err) console.error('Error sending method response :\n' + err.toString());
          else console.log('400 Response to method \'' + request.methodName + '\' sent successfully.');
        });
      } else {
        // Respond the cloud app for the device method
        response.send(200, 'Firmware update started.', function(err) {
          if (err) console.error('Error sending method response :\n' + err.toString());
          else {
            console.log('200 Response to method \'' + request.methodName + '\' sent successfully.');
    
            // Run the simulated firmware update flow
            runFirmwareUpdateFlow(firmwareVersion, firmwareUri);
          }
        });
      }
    }
    
  13. 添加以下函数以模拟长时间运行的固件更新流(将进度报告回解决方案):

    // Simulated firmwareUpdate flow
    function runFirmwareUpdateFlow(firmwareVersion, firmwareUri) {
      console.log('Simulating firmware update flow...');
      console.log('> Firmware version passed: ' + firmwareVersion);
      console.log('> Firmware URI passed: ' + firmwareUri);
      async.waterfall([
        function (callback) {
          console.log("Image downloading from " + firmwareUri);
          var patch = {
            FirmwareUpdateStatus: 'Downloading image..'
          };
          reportUpdateThroughTwin(patch, callback);
          sleep(10000, callback);
        },
        function (callback) {
          console.log("Downloaded, applying firmware " + firmwareVersion);
          deviceOnline = false;
          var patch = {
            FirmwareUpdateStatus: 'Applying firmware..',
            Online: false
          };
          reportUpdateThroughTwin(patch, callback);
          sleep(8000, callback);
        },
        function (callback) {
          console.log("Rebooting");
          var patch = {
            FirmwareUpdateStatus: 'Rebooting..'
          };
          reportUpdateThroughTwin(patch, callback);
          sleep(10000, callback);
        },
        function (callback) {
          console.log("Firmware updated to " + firmwareVersion);
          deviceOnline = true;
          var patch = {
            FirmwareUpdateStatus: 'Firmware updated',
            Online: true,
            Firmware: firmwareVersion
          };
          reportUpdateThroughTwin(patch, callback);
          callback(null);
        }
      ], function(err) {
        if (err) {
          console.error('Error in simulated firmware update flow: ' + err.message);
        } else {
          console.log("Completed simulated firmware update flow");
        }
      });
    
      // Helper function to update the twin reported properties.
      function reportUpdateThroughTwin(patch, callback) {
        console.log("Sending...");
        console.log(JSON.stringify(patch, null, 2));
        client.getTwin(function(err, twin) {
          if (!err) {
            twin.properties.reported.update(patch, function(err) {
              if (err) callback(err);
            });      
          } else {
            if (err) callback(err);
          }
        });
      }
    
      function sleep(milliseconds, callback) {
        console.log("Simulate a delay (milleseconds): " + milliseconds);
        setTimeout(function () {
          callback(null);
        }, milliseconds);
      }
    }
    
  14. 添加以下代码将遥测数据发送到解决方案。 客户端应用将属性添加到消息,以确定消息架构:

    function sendTelemetry(data, schema) {
      if (deviceOnline) {
        var d = new Date();
        var payload = JSON.stringify(data);
        var message = new Message(payload);
        message.properties.add('$$CreationTimeUtc', d.toISOString());
        message.properties.add('$$MessageSchema', schema);
        message.properties.add('$$ContentType', 'JSON');
    
        console.log('Sending device message data:\n' + payload);
        client.sendEvent(message, printErrorFor('send event'));
      } else {
        console.log('Offline, not sending telemetry');
      }
    }
    
  15. 添加以下代码用于创建客户端实例:

    var client = Client.fromConnectionString(connectionString, Protocol);
    
  16. 添加以下代码来执行下述操作:

    • 打开连接。
    • 设置所需属性的处理程序。
    • 发送报告的属性。
    • 为直接方法注册处理程序。 此示例对固件更新直接方法使用单独的处理程序。
    • 开始发送遥测数据。

      client.open(function (err) {
      if (err) {
        printErrorFor('open')(err);
      } else {
        // Create device Twin
        client.getTwin(function (err, twin) {
          if (err) {
            console.error('Could not get device twin');
          } else {
            console.log('Device twin created');
      
            twin.on('properties.desired', function (delta) {
              // Handle desired properties set by solution
              console.log('Received new desired properties:');
              console.log(JSON.stringify(delta));
            });
      
            // Send reported properties
            twin.properties.reported.update(reportedProperties, function (err) {
              if (err) throw err;
              console.log('twin state reported');
            });
      
            // Register handlers for all the method names we are interested in.
            // Consider separate handlers for each method.
            client.onDeviceMethod('Reboot', onDirectMethod);
            client.onDeviceMethod('FirmwareUpdate', onFirmwareUpdate);
            client.onDeviceMethod('EmergencyValveRelease', onDirectMethod);
            client.onDeviceMethod('IncreasePressure', onDirectMethod);
          }
        });
      
        // Start sending telemetry
        var sendTemperatureInterval = setInterval(function () {
          temperature += generateRandomIncrement();
          var data = {
            'temperature': temperature,
            'temperature_unit': temperatureUnit
          };
          sendTelemetry(data, temperatureSchema)
        }, 5000);
      
        var sendHumidityInterval = setInterval(function () {
          humidity += generateRandomIncrement();
          var data = {
            'humidity': humidity,
            'humidity_unit': humidityUnit
          };
          sendTelemetry(data, humiditySchema)
        }, 5000);
      
        var sendPressureInterval = setInterval(function () {
          pressure += generateRandomIncrement();
          var data = {
            'pressure': pressure,
            'pressure_unit': pressureUnit
          };
          sendTelemetry(data, pressureSchema)
        }, 5000);
      
        client.on('error', function (err) {
          printErrorFor('client')(err);
          if (sendTemperatureInterval) clearInterval(sendTemperatureInterval);
          if (sendHumidityInterval) clearInterval(sendHumidityInterval);
          if (sendPressureInterval) clearInterval(sendPressureInterval);
          client.close(printErrorFor('client.close'));
        });
      }
      });
      
  17. 保存对 remote_monitoring.js 文件的更改。

  18. 若要启动示例应用程序,请在命令提示符处运行以下命令:

    node remote_monitoring.js
    

查看设备遥测数据

可以在解决方案中的“设备”页上查看从设备发送的遥测数据。

  1. 在“设备”页上的设备列表中选择已预配的设备。 一个面板将显示有关设备的信息,其中包括设备遥测绘图:

    查看设备详细信息

  2. 选择“压力”可更改遥测显示:

    查看压力遥测

  3. 若要查看有关设备的诊断信息,请向下滚动到“诊断”:

    查看设备诊断

对设备执行操作

若要对设备调用方法,请使用远程监控解决方案中的“设备”页。 例如,在远程监控解决方案中,冷却器设备实现了FirmwareUpdate方法。

  1. 选择“设备”可导航到解决方案中的“设备”页。

  2. 在“设备”页上的设备列表中选择已预配的设备:

    选择物理设备

  3. 若要显示可在设备上调用的方法列表,请选择“作业”,然后选择“运行方法”。 若要计划在多个设备上运行的作业,可以在列表中选择多个设备。 “作业”面板会显示普遍适用于所有选定设备的方法类型。

  4. 选择 FirmwareUpdate,将作业名称设置为 UpdatePhysicalChiller。 将“固件版本”设置为 2.0.0,将“固件 URI”设置为 http://contoso.com/updates/firmware.bin,然后选择“应用”:

    计划固件更新

  5. 模拟设备处理该方法时,一系列消息将显示在运行设备代码的控制台中。

  6. 更新完成时,“设备”页上将显示新的固件版本:

    更新已完成

Note

若要跟踪解决方案中作业的状态,请选择“查看”。

后续步骤

自定义远程监控预配置的解决方案一文中介绍了自定义预配置的解决方案的一些方法。