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

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

  • 温度
  • 压力
  • 湿度

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

示例设备还会:

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

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

开始之前

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

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

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

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

解决方案仪表板

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

Note

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

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

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

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

    预配物理设备

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

    选择设备选项

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

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

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

    设备资源管理器

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

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

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

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

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

本教程介绍如何将物理设备连接到远程监控预配置解决方案。

在 Linux 上创建 C 客户端项目

与受约束设备上运行的大多数嵌入式应用程序一样,设备应用程序的客户端代码是用 C 语言编写的。在本教程中,将在运行 Ubuntu (Linux) 的计算机上生成应用程序。

若要完成这些步骤,需要一个运行 Ubuntu 版本 15.04 或更高版本的设备。 继续操作之前,请使用以下命令在 Ubuntu 设备上安装必备组件包:

sudo apt-get install cmake gcc g++

在设备上安装客户端库

Azure IoT 中心客户端库以包的形式提供,可以使用 apt get 命令在 Ubuntu 设备上安装该包。 完成以下步骤,在 Ubuntu 计算机上安装包含 IoT 中心客户端库和标头文件的包:

  1. 在外壳程序中,向计算机添加 AzureIoT 存储库:

    sudo add-apt-repository ppa:aziotsdklinux/ppa-azureiot
    sudo apt-get update
    
  2. 安装 azure-iot-sdk-c-dev 包

    sudo apt-get install -y azure-iot-sdk-c-dev
    

安装 Parson JSON 分析器

IoT 中心客户端库使用 Parson JSON 分析器分析消息有效负载。 在计算机上的适当文件夹中,使用以下命令克隆 Parson GitHub 存储库:

git clone https://github.com/kgabis/parson.git

准备项目

在 Ubuntu 计算机上,创建名为 remote_monitoring 的文件夹。 在 remote_monitoring 文件夹中:

  • 创建四个文件:main.cremote_monitoring.cremote_monitoring.hCMakeLists.txt
  • 创建名为 parson 的文件夹。

将文件 parson.cparson.h 从 Parson 存储库的本地副本复制到 remote_monitoring/parson 文件夹。

在文本编辑器中打开 remote_monitoring.c 文件。 添加以下 #include 语句:

#include "iothubtransportmqtt.h"
#include "schemalib.h"
#include "iothub_client.h"
#include "serializer_devicetwin.h"
#include "schemaserializer.h"
#include "azure_c_shared_utility/threadapi.h"
#include "azure_c_shared_utility/platform.h"
#include "parson.h"

指定 IoT 中心设备的行为

IoT 中心序列化程序客户端库使用模型来指定设备与 IoT 中心交换的消息的格式。

  1. #include 语句之后添加以下变量声明。 将占位符值 [Device Id][Device Key] 替换为在远程监控解决方案仪表板中记下的设备值。 使用解决方案仪表板中的 IoT 中心主机名替换 [IoTHub Name]。 例如,如果 IoT 中心主机名是 contoso.azure-devices.cn,则将 [IoTHub Name] 替换为 contoso

    static const char* deviceId = "[Device Id]";
    static const char* connectionString = "HostName=[IoTHub Name].azure-devices.cn;DeviceId=[Device Id];SharedAccessKey=[Device Key]";
    static char propText[1024];
    
  2. 添加以下代码以定义使设备可以与 IoT 中心通信的模型。 此模型指定设备可执行以下操作:

    • 可以将温度、压力和湿度作为遥测数据发送。
    • 可以向 IoT 中心中的设备孪生发送已报告的属性。 这些报告属性包括有关遥测架构和受支持方法的信息。
    • 可以接收和处理在 IoT 中心的设备孪生中设置的所需属性。
    • 可响应从 UI 调用的 RebootFirmwareUpdateEmergencyValveReleaseIncreasePressure 直接方法。 设备使用报告的属性发送有关其支持的直接方法的信息。

      // Define the Model
      BEGIN_NAMESPACE(Contoso);
      
      DECLARE_STRUCT(MessageSchema,
      ascii_char_ptr, Name,
      ascii_char_ptr, Format,
      ascii_char_ptr_no_quotes, Fields
      )
      
      DECLARE_STRUCT(TelemetrySchema,
      ascii_char_ptr, Interval,
      ascii_char_ptr, MessageTemplate,
      MessageSchema, MessageSchema
      )
      
      DECLARE_STRUCT(TelemetryProperties,
      TelemetrySchema, TemperatureSchema,
      TelemetrySchema, HumiditySchema,
      TelemetrySchema, PressureSchema
      )
      
      DECLARE_DEVICETWIN_MODEL(Chiller,
      /* Telemetry (temperature, external temperature and humidity) */
      WITH_DATA(double, temperature),
      WITH_DATA(ascii_char_ptr, temperature_unit),
      WITH_DATA(double, pressure),
      WITH_DATA(ascii_char_ptr, pressure_unit),
      WITH_DATA(double, humidity),
      WITH_DATA(ascii_char_ptr, humidity_unit),
      
      /* Device twin properties */
      WITH_REPORTED_PROPERTY(ascii_char_ptr, Protocol),
      WITH_REPORTED_PROPERTY(ascii_char_ptr, SupportedMethods),
      WITH_REPORTED_PROPERTY(TelemetryProperties, Telemetry),
      WITH_REPORTED_PROPERTY(ascii_char_ptr, Type),
      WITH_REPORTED_PROPERTY(ascii_char_ptr, Firmware),
      WITH_REPORTED_PROPERTY(ascii_char_ptr, FirmwareUpdateStatus),
      WITH_REPORTED_PROPERTY(ascii_char_ptr, Location),
      WITH_REPORTED_PROPERTY(double, Latitiude),
      WITH_REPORTED_PROPERTY(double, Longitude),
      
      WITH_DESIRED_PROPERTY(ascii_char_ptr, Interval, onDesiredInterval),
      
      /* Direct methods implemented by the device */
      WITH_METHOD(Reboot),
      WITH_METHOD(FirmwareUpdate),
      WITH_METHOD(EmergencyValveRelease),
      WITH_METHOD(IncreasePressure)
      );
      
      END_NAMESPACE(Contoso);
      

实现设备的行为

现在添加实现模型中定义的行为的代码。

  1. 添加以下函数,用于处理在解决方案仪表板中设置的所需属性。 模型中定义了以下所需属性:

    void onDesiredInterval(void* argument)
    {
      /* By convention 'argument' is of the type of the MODEL */
      Chiller* chiller = argument;
      printf("Received a new desired Interval value: %s \r\n", chiller->Interval);
    }
    
  2. 添加以下函数,用于处理通过 IoT 中心调用的直接方法。模型中定义了以下直接方法:

    /* Handlers for direct methods */
    METHODRETURN_HANDLE Reboot(Chiller* chiller)
    {
      (void)(chiller);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Rebooting\"");
      printf("Received reboot request\r\n");
      return result;
    }
    
    METHODRETURN_HANDLE FirmwareUpdate(Chiller* chiller)
    {
      (void)(chiller);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Updating Firmware\"");
      printf("Recieved firmware update request\r\n");
      return result;
    }
    
    METHODRETURN_HANDLE EmergencyValveRelease(Chiller* chiller)
    {
      (void)(chiller);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Releasing Emergency Valve\"");
      printf("Recieved emergency valve release request\r\n");
      return result;
    }
    
    METHODRETURN_HANDLE IncreasePressure(Chiller* chiller)
    {
      (void)(chiller);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Increasing Pressure\"");
      printf("Received increase pressure request\r\n");
      return result;
    }
    
  3. 添加以下函数以便将属性添加到设备到云消息:

    /* Add message property */
    static void addProperty(MAP_HANDLE propMap, char* propName, char* propValue)
    {
      if (Map_AddOrUpdate(propMap, propName, propValue) != MAP_OK)
      {
        (void)printf("ERROR: Map_AddOrUpdate Failed on %s!\r\n", propName);
      }
    }
    
  4. 添加以下函数以便使用属性向预配置解决方案发送消息:

    static void sendMessage(IOTHUB_CLIENT_HANDLE iotHubClientHandle, const unsigned char* buffer, size_t size, char* schema)
    {
      IOTHUB_MESSAGE_HANDLE messageHandle = IoTHubMessage_CreateFromByteArray(buffer, size);
      if (messageHandle == NULL)
      {
        printf("unable to create a new IoTHubMessage\r\n");
      }
      else
      {
        // Add properties
        MAP_HANDLE propMap = IoTHubMessage_Properties(messageHandle);
        addProperty(propMap, "$$MessageSchema", schema);
        addProperty(propMap, "$$ContentType", "JSON");
        time_t now = time(0);
        struct tm* timeinfo;
        #pragma warning(disable: 4996)
        timeinfo = gmtime(&now);
        char timebuff[50];
        strftime(timebuff, 50, "%Y-%m-%dT%H:%M:%SZ", timeinfo);
        addProperty(propMap, "$$CreationTimeUtc", timebuff);
    
        if (IoTHubClient_SendEventAsync(iotHubClientHandle, messageHandle, NULL, NULL) != IOTHUB_CLIENT_OK)
        {
          printf("failed to hand over the message to IoTHubClient");
        }
        else
        {
          printf("IoTHubClient accepted the message for delivery\r\n");
        }
    
        IoTHubMessage_Destroy(messageHandle);
      }
      free((void*)buffer);
    }
    
  5. 添加以下回调处理程序,设备向预配置解决方案发送新的报告属性值后,会运行该处理程序:

    /* Callback after sending reported properties */
    void deviceTwinCallback(int status_code, void* userContextCallback)
    {
      (void)(userContextCallback);
      printf("IoTHub: reported properties delivered with status_code = %u\n", status_code);
    }
    
  6. 添加以下函数,用于将设备连接到云中的预配置解决方案并交换数据。 此函数执行以下步骤:

    • 初始化平台。
    • 向序列化库注册 Contoso 命名空间。
    • 使用设备连接字符串初始化客户端。
    • 创建冷却器模型的实例。
    • 创建并发送报告属性值。
    • 创建循环以便每 5 秒发送一次遥测数据。
    • 取消初始化所有资源。

      void remote_monitoring_run(void)
      {
      if (platform_init() != 0)
      {
        printf("Failed to initialize the platform.\n");
      }
      else
      {
        if (SERIALIZER_REGISTER_NAMESPACE(Contoso) == NULL)
        {
          printf("Unable to SERIALIZER_REGISTER_NAMESPACE\n");
        }
        else
        {
          IOTHUB_CLIENT_HANDLE iotHubClientHandle = IoTHubClient_CreateFromConnectionString(connectionString, MQTT_Protocol);
          if (iotHubClientHandle == NULL)
          {
            printf("Failure in IoTHubClient_CreateFromConnectionString\n");
          }
          else
          {
            Chiller* chiller = IoTHubDeviceTwin_CreateChiller(iotHubClientHandle);
            if (chiller == NULL)
            {
              printf("Failure in IoTHubDeviceTwin_CreateChiller\n");
            }
            else
            {
              /* Set values for reported properties */
              chiller->Protocol = "MQTT";
              chiller->SupportedMethods = "Reboot,FirmwareUpdate,EmergencyValveRelease,IncreasePressure";
              chiller->Telemetry.TemperatureSchema.Interval = "00:00:05";
              chiller->Telemetry.TemperatureSchema.MessageTemplate = "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}";
              chiller->Telemetry.TemperatureSchema.MessageSchema.Name = "chiller-temperature;v1";
              chiller->Telemetry.TemperatureSchema.MessageSchema.Format = "JSON";
              chiller->Telemetry.TemperatureSchema.MessageSchema.Fields = "{\"temperature\":\"Double\",\"temperature_unit\":\"Text\"}";
              chiller->Telemetry.HumiditySchema.Interval = "00:00:05";
              chiller->Telemetry.HumiditySchema.MessageTemplate = "{\"humidity\":${humidity},\"humidity_unit\":\"${humidity_unit}\"}";
              chiller->Telemetry.HumiditySchema.MessageSchema.Name = "chiller-humidity;v1";
              chiller->Telemetry.HumiditySchema.MessageSchema.Format = "JSON";
              chiller->Telemetry.HumiditySchema.MessageSchema.Fields = "{\"humidity\":\"Double\",\"humidity_unit\":\"Text\"}";
              chiller->Telemetry.PressureSchema.Interval = "00:00:05";
              chiller->Telemetry.PressureSchema.MessageTemplate = "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}";
              chiller->Telemetry.PressureSchema.MessageSchema.Name = "chiller-pressure;v1";
              chiller->Telemetry.PressureSchema.MessageSchema.Format = "JSON";
              chiller->Telemetry.PressureSchema.MessageSchema.Fields = "{\"pressure\":\"Double\",\"pressure_unit\":\"Text\"}";
              chiller->Type = "Chiller";
              chiller->Firmware = "1.0.0";
              chiller->FirmwareUpdateStatus = "";
              chiller->Location = "Building 44";
              chiller->Latitiude = 47.638928;
              chiller->Longitude = -122.13476;
      
              /* Send reported properties to IoT Hub */
              if (IoTHubDeviceTwin_SendReportedStateChiller(chiller, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
              {
                printf("Failed sending serialized reported state\n");
              }
              else
              {
                /* Send telemetry */
                chiller->temperature = 50;
                chiller->temperature_unit = "F";
                chiller->pressure= 55;
                chiller->pressure_unit = "psig";
                chiller->humidity = 50;
                chiller->humidity_unit = "%";
      
                while (1)
                {
                  unsigned char*buffer;
                  size_t bufferSize;
      
                  (void)printf("Sending sensor value Temperature = %f %s,\n", chiller->temperature, chiller->temperature_unit);
      
                  if (SERIALIZE(&buffer, &bufferSize, chiller->temperature, chiller->temperature_unit) != CODEFIRST_OK)
                  {
                    (void)printf("Failed sending sensor value\r\n");
                  }
                  else
                  {
                    sendMessage(iotHubClientHandle, buffer, bufferSize, chiller->Telemetry.TemperatureSchema.MessageSchema.Name);
                  }
      
                  (void)printf("Sending sensor value Humidity = %f %s,\n", chiller->humidity, chiller->humidity_unit);
      
                  if (SERIALIZE(&buffer, &bufferSize, chiller->humidity, chiller->humidity_unit) != CODEFIRST_OK)
                  {
                    (void)printf("Failed sending sensor value\r\n");
                  }
                  else
                  {
                    sendMessage(iotHubClientHandle, buffer, bufferSize, chiller->Telemetry.HumiditySchema.MessageSchema.Name);
                  }
      
                  (void)printf("Sending sensor value Pressure = %f %s,\n", chiller->pressure, chiller->pressure_unit);
      
                  if (SERIALIZE(&buffer, &bufferSize, chiller->pressure, chiller->pressure_unit) != CODEFIRST_OK)
                  {
                    (void)printf("Failed sending sensor value\r\n");
                  }
                  else
                  {
                    sendMessage(iotHubClientHandle, buffer, bufferSize, chiller->Telemetry.PressureSchema.MessageSchema.Name);
                  }
      
                  ThreadAPI_Sleep(5000);
                }
      
                IoTHubDeviceTwin_DestroyChiller(chiller);
              }
            }
            IoTHubClient_Destroy(iotHubClientHandle);
          }
          serializer_deinit();
        }
      }
      platform_deinit();
      }
      

      下面提供了发送到预配置解决方案的示例 遥测 消息供参考:

      Device: [myCDevice],
      Data:[{"humidity":50.000000000000000, "humidity_unit":"%"}]
      Properties:
      '$$MessageSchema': 'chiller-humidity;v1'
      '$$ContentType': 'JSON'
      '$$CreationTimeUtc': '2017-09-12T09:17:13Z'
      

添加代码以运行应用

在文本编辑器中打开 remote_monitoring.h 文件。 添加以下代码:

void remote_monitoring_run(void);

在文本编辑器中打开 main.c 文件。 添加以下代码:

#include "remote_monitoring.h"

int main(void)
{
    remote_monitoring_run();

    return 0;
}

生成并运行应用程序

以下步骤描述如何使用 CMake 生成客户端应用程序。

  1. 在文本编辑器中,打开 remote_monitoring 文件夹中的 CMakeLists.txt 文件。

  2. 添加以下指令,以定义如何生成客户端应用程序:

    macro(compileAsC99)
      if (CMAKE_VERSION VERSION_LESS "3.1")
        if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
          set (CMAKE_C_FLAGS "--std=c99 ${CMAKE_C_FLAGS}")
          set (CMAKE_CXX_FLAGS "--std=c++11 ${CMAKE_CXX_FLAGS}")
        endif()
      else()
        set (CMAKE_C_STANDARD 99)
        set (CMAKE_CXX_STANDARD 11)
      endif()
    endmacro(compileAsC99)
    
    cmake_minimum_required(VERSION 2.8.11)
    compileAsC99()
    
    set(AZUREIOT_INC_FOLDER "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/parson" "/usr/include/azureiot" "/usr/include/azureiot/inc")
    
    include_directories(${AZUREIOT_INC_FOLDER})
    
    set(sample_application_c_files
        ./parson/parson.c
        ./remote_monitoring.c
        ./main.c
    )
    
    set(sample_application_h_files
        ./parson/parson.h
        ./remote_monitoring.h
    )
    
    add_executable(sample_app ${sample_application_c_files} ${sample_application_h_files})
    
    target_link_libraries(sample_app
        serializer
        iothub_client
        iothub_client_mqtt_transport
        aziotsharedutil
        umqtt
        pthread
        curl
        ssl
        crypto
        m
    )
    
  3. remote_monitoring 文件夹中,创建一个文件夹以存储 CMake 生成的 make 文件。 然后运行 cmakemake 命令,如下所示:

    mkdir cmake
    cd cmake
    cmake ../
    make
    
  4. 运行客户端应用程序,并将遥测数据发送到 IoT 中心:

    ./sample_app
    

查看设备遥测数据

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

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

    查看设备详细信息

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

    查看压力遥测

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

    查看设备诊断

对设备执行操作

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

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

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

    选择物理设备

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

  4. 选择“重新启动”,将作业名称设置为 RebootPhysicalChiller,然后选择“应用”:

    计划重新启动

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

Note

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

后续步骤

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