将设备连接到远程监视解决方案加速器 (Windows)

本教程实施一个可将以下遥测数据发送到远程监视解决方案加速器的“冷却器”设备:

  • 温度
  • 压力
  • 湿度

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

示例设备还会:

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

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

开始之前

在为设备编写任何代码之前,部署远程监视解决方案加速器,并向该解决方案添加一个真实的新设备。

部署远程监视解决方案加速器

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

当远程监视解决方案的部署过程完成后,单击“启动”,以在浏览器中打开解决方案仪表板。

解决方案仪表板

将设备添加到远程监视解决方案

注意

如果已在解决方案中添加了设备,则可以跳过此步骤。 不过,下一步骤需要设备连接字符串。 可以从 Azure 门户或使用 az iot CLI 工具检索设备的连接字符串。

对于连接到解决方案加速器的设备,该设备必须使用有效的凭据将自身标识到 IoT 中心。 将设备添加到解决方案时,有机会保存包含这些凭据的设备连接字符串。 在本教程中,稍后会在客户端应用程序中添加设备连接字符串。

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

  1. 选择“+ 新建设备”,然后选择“真实”作为设备类型 :

    添加真实设备

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

    选择设备选项

  3. 选择“应用”。 然后记下设备 ID、主密钥和连接字符串主密钥值 :

    检索凭据

现在,你已向远程监视解决方案加速器添加了一个真实设备,并记下了其设备连接字符串。 在以下各部分中,你将实现使用设备连接字符串连接到解决方案的客户端应用程序。

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

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

本教程介绍如何将真实设备连接到远程监视解决方案加速器。

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

如果更喜欢模拟某个设备,请参阅创建和测试新的模拟设备

先决条件

若要完成本操作指南中的步骤,请按照设置 Windows 开发环境中的步骤将所需的开发工具和库添加到 Windows 计算机中。

查看代码

本指南中使用的示例代码可在 Azure IoT C SDK GitHub 存储库中找到。

下载源代码并准备项目

若要准备项目,请从 GitHub 克隆 Azure IoT C SDK 存储库

该示例位于 samples/solutions/remote_monitoring_client 文件夹中。

在文本编辑器中打开 samples/solutions/remote_monitoring_client 文件夹中的 remote_monitoring.c 文件。

代码演练

本部分介绍了示例代码的一些关键部分,并解释了它们如何与远程监视解决方案加速器相关。

以下代码片段展示了描述设备功能的报告属性是如何定义的。 这些属性包括:

  • 设备所在的位置(用于使解决方案加速器可以将该设备添加到地图)。
  • 当前的固件版本。
  • 设备支持的方法列表。
  • 设备发送的遥测消息的架构。
typedef struct MESSAGESCHEMA_TAG
{
    char* name;
    char* format;
    char* fields;
} MessageSchema;

typedef struct TELEMETRYSCHEMA_TAG
{
    MessageSchema messageSchema;
} TelemetrySchema;

typedef struct TELEMETRYPROPERTIES_TAG
{
    TelemetrySchema temperatureSchema;
    TelemetrySchema humiditySchema;
    TelemetrySchema pressureSchema;
} TelemetryProperties;

typedef struct CHILLER_TAG
{
    // Reported properties
    char* protocol;
    char* supportedMethods;
    char* type;
    char* firmware;
    FIRMWARE_UPDATE_STATUS firmwareUpdateStatus;
    char* location;
    double latitude;
    double longitude;
    TelemetryProperties telemetry;

    // Manage firmware update process
    char* new_firmware_version;
    char* new_firmware_URI;
} Chiller;

示例包括了一个 serializeToJson 函数,它使用 Parson 库对此数据结构进行序列化。

示例包括了多个回调函数,它们在客户端与解决方案加速器进行交互时将信息输出到控制台:

  • connection_status_callback
  • send_confirm_callback
  • reported_state_callback
  • device_method_callback

以下代码片段显示了 device_method_callback 函数。 此函数确定当从解决方案加速器收到方法调用时要执行的操作。 此函数在 userContextCallback 参数中接收对 Chiller 数据结构的引用。 userContextCallback 的值是在 main 函数中配置回调函数时设置的:

static int device_method_callback(const char* method_name, const unsigned char* payload, size_t size, unsigned char** response, size_t* response_size, void* userContextCallback)
{
    Chiller *chiller = (Chiller *)userContextCallback;

    int result;

    (void)printf("Direct method name:    %s\r\n", method_name);

    (void)printf("Direct method payload: %.*s\r\n", (int)size, (const char*)payload);

    if (strcmp("Reboot", method_name) == 0)
    {
        MESSAGERESPONSE(201, "{ \"Response\": \"Rebooting\" }")
    }
    else if (strcmp("EmergencyValveRelease", method_name) == 0)
    {
        MESSAGERESPONSE(201, "{ \"Response\": \"Releasing emergency valve\" }")
    }
    else if (strcmp("IncreasePressure", method_name) == 0)
    {
        MESSAGERESPONSE(201, "{ \"Response\": \"Increasing pressure\" }")
    }
    else if (strcmp("FirmwareUpdate", method_name) == 0)
    {
        if (chiller->firmwareUpdateStatus != IDLE)
        {
            (void)printf("Attempt to invoke firmware update out of order\r\n");
            MESSAGERESPONSE(400, "{ \"Response\": \"Attempting to initiate a firmware update out of order\" }")
        }
        else
        {
            getFirmwareUpdateValues(chiller, payload);

            if (chiller->new_firmware_version != NULL && chiller->new_firmware_URI != NULL)
            {
                // Create a thread for the long-running firmware update process.
                THREAD_HANDLE thread_apply;
                THREADAPI_RESULT t_result = ThreadAPI_Create(&thread_apply, do_firmware_update, chiller);
                if (t_result == THREADAPI_OK)
                {
                    (void)printf("Starting firmware update thread\r\n");
                    MESSAGERESPONSE(201, "{ \"Response\": \"Starting firmware update thread\" }")
                }
                else
                {
                    (void)printf("Failed to start firmware update thread\r\n");
                    MESSAGERESPONSE(500, "{ \"Response\": \"Failed to start firmware update thread\" }")
                }
            }
            else
            {
                (void)printf("Invalid method payload\r\n");
                MESSAGERESPONSE(400, "{ \"Response\": \"Invalid payload\" }")
            }
        }
    }
    else
    {
        // All other entries are ignored.
        (void)printf("Method not recognized\r\n");
        MESSAGERESPONSE(400, "{ \"Response\": \"Method not recognized\" }")
    }

    return result;
}

当解决方案加速器调用固件更新方法时,示例会对 JSON 有效负载进行反序列化,并启动一个后台线程来完成更新过程。 以下代码片段显示了在该线程上运行的 do_firmware_update

/*
 This is a thread allocated to process a long-running device method call.
 It uses device twin reported properties to communicate status values
 to the Remote Monitoring solution accelerator.
*/
static int do_firmware_update(void *param)
{
    Chiller *chiller = (Chiller *)param;
    printf("Running simulated firmware update: URI: %s, Version: %s\r\n", chiller->new_firmware_URI, chiller->new_firmware_version);

    printf("Simulating download phase...\r\n");
    chiller->firmwareUpdateStatus = DOWNLOADING;
    sendChillerReportedProperties(chiller);

    ThreadAPI_Sleep(5000);

    printf("Simulating apply phase...\r\n");
    chiller->firmwareUpdateStatus = APPLYING;
    sendChillerReportedProperties(chiller);

    ThreadAPI_Sleep(5000);

    printf("Simulating reboot phase...\r\n");
    chiller->firmwareUpdateStatus = REBOOTING;
    sendChillerReportedProperties(chiller);

    ThreadAPI_Sleep(5000);

    size_t size = strlen(chiller->new_firmware_version) + 1;
    (void)memcpy(chiller->firmware, chiller->new_firmware_version, size);

    chiller->firmwareUpdateStatus = IDLE;
    sendChillerReportedProperties(chiller);

    return 0;
}

以下代码片段展示了客户端如何将遥测消息发送给解决方案加速器。 消息属性包括了消息架构,以帮助解决方案加速器在仪表板上显示遥测数据:

static void send_message(IOTHUB_DEVICE_CLIENT_HANDLE handle, char* message, char* schema)
{
    IOTHUB_MESSAGE_HANDLE message_handle = IoTHubMessage_CreateFromString(message);
    if (message_handle != NULL)
    {
        // Set system properties
        (void)IoTHubMessage_SetMessageId(message_handle, "MSG_ID");
        (void)IoTHubMessage_SetCorrelationId(message_handle, "CORE_ID");
        (void)IoTHubMessage_SetContentTypeSystemProperty(message_handle, "application%2fjson");
        (void)IoTHubMessage_SetContentEncodingSystemProperty(message_handle, "utf-8");

        // Set application properties
        MAP_HANDLE propMap = IoTHubMessage_Properties(message_handle);
        (void)Map_AddOrUpdate(propMap, "$$MessageSchema", schema);
        (void)Map_AddOrUpdate(propMap, "$$ContentType", "JSON");

        time_t now = time(0);
        struct tm* timeinfo;
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4996) /* Suppress warning about possible unsafe function in Visual Studio */
#endif
        timeinfo = gmtime(&now);
#ifdef _MSC_VER
#pragma warning(pop)
#endif
        char timebuff[50];
        strftime(timebuff, 50, "%Y-%m-%dT%H:%M:%SZ", timeinfo);
        (void)Map_AddOrUpdate(propMap, "$$CreationTimeUtc", timebuff);

        IoTHubDeviceClient_SendEventAsync(handle, message_handle, send_confirm_callback, NULL);

        IoTHubMessage_Destroy(message_handle);
    }
}

示例中的 main 函数:

  • 初始化和关闭 SDK 子系统。
  • 初始化 Chiller 数据结构。
  • 将报告的属性发送给解决方案加速器。
  • 配置设备方法回调函数。
  • 将模拟的遥测数据值发送给解决方案加速器。
int main(void)
{
    srand((unsigned int)time(NULL));
    double minTemperature = 50.0;
    double minPressure = 55.0;
    double minHumidity = 30.0;
    double temperature = 0;
    double pressure = 0;
    double humidity = 0;

    (void)printf("This sample simulates a Chiller device connected to the Remote Monitoring solution accelerator\r\n\r\n");

    // Used to initialize sdk subsystem
    (void)IoTHub_Init();

    (void)printf("Creating IoTHub handle\r\n");
    // Create the iothub handle here
    device_handle = IoTHubDeviceClient_CreateFromConnectionString(connectionString, MQTT_Protocol);
    if (device_handle == NULL)
    {
        (void)printf("Failure creating IotHub device. Hint: Check your connection string.\r\n");
    }
    else
    {
        // Setting connection status callback to get indication of connection to iothub
        (void)IoTHubDeviceClient_SetConnectionStatusCallback(device_handle, connection_status_callback, NULL);

        Chiller chiller;
        memset(&chiller, 0, sizeof(Chiller));
        chiller.protocol = "MQTT";
        chiller.supportedMethods = "Reboot,FirmwareUpdate,EmergencyValveRelease,IncreasePressure";
        chiller.type = "Chiller";
        size_t size = strlen(initialFirmwareVersion) + 1;
        chiller.firmware = malloc(size);
        if (chiller.firmware == NULL)
        {
            (void)printf("Chiller Firmware failed to allocate memory.\r\n");
        }
        else
        {
            memcpy(chiller.firmware, initialFirmwareVersion, size);
            chiller.firmwareUpdateStatus = IDLE;
            chiller.location = "Building 44";
            chiller.latitude = 47.638928;
            chiller.longitude = -122.13476;
            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.messageSchema.name = "chiller-humidity;v1";
            chiller.telemetry.humiditySchema.messageSchema.format = "JSON";
            chiller.telemetry.humiditySchema.messageSchema.fields = "{\"humidity\":\"Double\",\"humidity_unit\":\"Text\"}";
            chiller.telemetry.pressureSchema.messageSchema.name = "chiller-pressure;v1";
            chiller.telemetry.pressureSchema.messageSchema.format = "JSON";
            chiller.telemetry.pressureSchema.messageSchema.fields = "{\"pressure\":\"Double\",\"pressure_unit\":\"Text\"}";

            sendChillerReportedProperties(&chiller);

            (void)IoTHubDeviceClient_SetDeviceMethodCallback(device_handle, device_method_callback, &chiller);

            while (1)
            {
                temperature = minTemperature + ((double)(rand() % 10) + 5);
                pressure = minPressure + ((double)(rand() % 10) + 5);
                humidity = minHumidity + ((double)(rand() % 20) + 5);

                if (chiller.firmwareUpdateStatus == IDLE)
                {
                    (void)printf("Sending sensor value Temperature = %f %s,\r\n", temperature, "F");
                    (void)sprintf_s(msgText, sizeof(msgText), "{\"temperature\":%.2f,\"temperature_unit\":\"F\"}", temperature);
                    send_message(device_handle, msgText, chiller.telemetry.temperatureSchema.messageSchema.name);


                    (void)printf("Sending sensor value Pressure = %f %s,\r\n", pressure, "psig");
                    (void)sprintf_s(msgText, sizeof(msgText), "{\"pressure\":%.2f,\"pressure_unit\":\"psig\"}", pressure);
                    send_message(device_handle, msgText, chiller.telemetry.pressureSchema.messageSchema.name);


                    (void)printf("Sending sensor value Humidity = %f %s,\r\n", humidity, "%");
                    (void)sprintf_s(msgText, sizeof(msgText), "{\"humidity\":%.2f,\"humidity_unit\":\"%%\"}", humidity);
                    send_message(device_handle, msgText, chiller.telemetry.humiditySchema.messageSchema.name);
                }

                ThreadAPI_Sleep(5000);
            }

            (void)printf("\r\nShutting down\r\n");

            // Clean up the iothub sdk handle and free resources
            IoTHubDeviceClient_Destroy(device_handle);
            free(chiller.firmware);
            free(chiller.new_firmware_URI);
            free(chiller.new_firmware_version);
        }
    }
    // Shutdown the sdk subsystem
    IoTHub_Deinit();

    return 0;
}

生成并运行示例

  1. 编辑 remote_monitoring.c 文件,使用本操作指南开头在将设备添加到解决方案加速器时记下的设备连接字符串替换 <connectionstring>

  2. 按照在 Windows 中构建 C SDK 中的步骤构建 SDK 和远程监视客户端应用程序。

  3. 在用于构建解决方案的命令提示符下,运行:

    samples\solutions\remote_monitoring_client\Release\remote_monitoring_client.exe
    

    控制台中的消息显示为:

    • 应用程序将示例遥测数据发送到解决方案加速器。
    • 响应从解决方案仪表板调用的方法。

查看设备遥测数据

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

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

    查看设备详细信息

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

    查看压力遥测

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

    查看设备诊断

对设备执行操作

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

  1. 选择“设备”,导航到解决方案中的“设备资源管理器”页 。

  2. 在“设备资源管理器”页上的设备列表中选择已预配的设备:

    选择真实设备

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

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

    计划固件更新

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

注意

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

后续步骤

自定义远程监视解决方案加速器一文中介绍了自定义解决方案加速器的一些方法。