將裝置連線到遠端監視解決方案加速器 (Windows)

在此教學課程中,您會實作一個 Chiller 裝置,此裝置會將下列遙測資料傳送給遠端監視解決方案加速器

  • 溫度
  • 壓力
  • 溼度

為了簡單起見,程式碼會產生 Chiller 的範例遙測值。 您可以將實際感應器連線到您的裝置並傳送實際的遙測來擴充範例。

範例裝置也會:

  • 將中繼資料傳送至解決方案來描述其功能。
  • 回應從解決方案中的裝置頁面觸發的動作。
  • 回應從解決方案中的裝置頁面傳送的設定變更。

若要完成此教學課程,您需要一個有效的 Azure 帳戶。 如果您沒有帳戶,只需要幾分鐘的時間就可以建立免費試用帳戶。 如需詳細資料,請參閱 Azure 免費試用

開始之前

在您為裝置撰寫任何程式碼之前,請先部署遠端監視解決方案加速器,並將新的實體裝置新增至該解決方案。

部署遠端監視解決方案加速器

您在本教學課程中建立的 Chiller 裝置會將資料傳送給遠端監視解決方案加速器的執行個體。 如果您尚未在您的 Azure 帳戶中佈建遠端監視解決方案加速器,請參閱部署遠端監視解決方案加速器

當遠端監視解決方案的部署程序完成之後,請按一下 [啟動],以在瀏覽器中開啟解決方案儀表板。

解決方案儀表板

將您的裝置新增到遠端監視解決方案

注意

如果您已經在解決方案中新增裝置,則可以略過此步驟。 不過,下一個步驟需要您裝置的連接字串。 您可以從 Azure 入口網站或使用 az iot CLI 工具來擷取裝置的連線字串。

對於連線到解決方案加速器的裝置,該裝置必須使用有效的認證向 IoT 中樞識別自己。 當您將裝置新增至解決方案時,會有機會儲存包含這些認證的裝置連接字串。 稍後在本教學課程中,您會將裝置連接字串包含在您的用戶端應用程式中。

若要在遠端監視解決方案中新增裝置,請在解決方案的 [裝置總管] 頁面中完成下列步驟:

  1. 選擇 [+ 新增裝置],然後選擇 [Real] 作為 [裝置類型]:

    新增真實裝置

  2. 輸入 Physical-chiller 作為裝置識別碼。 選擇 [對稱金鑰] 和 [自動產生金鑰] 選項:

    選擇裝置選項

  3. 選擇 [套用]。 然後記下裝置識別碼主要金鑰連接字串主要金鑰值:

    擷取認證

您現在已將實體裝置新增至遠端監視解決方案加速器中,並將其裝置連接字串記下。 在下列章節中,您可以實作使用裝置連接字串來連線到您解決方案的用戶端應用程式。

用戶端應用程式會實作內建 Chiller 裝置型號。 解決方案加速器裝置型號會指定下列相關裝置資訊:

  • 裝置回報至解決方案的屬性。 例如,Chiller 裝置會報告其韌體和位置的相關資訊。
  • 裝置傳送至解決方案的遙測類型。 例如,Chiller 裝置傳會送溫度、溼度和壓力值。
  • 您可以從解決方案中排程以在裝置上執行的方法。 例如,Chiller 裝置必須實作 RebootFirmwareUpdateEmergencyValveReleaseIncreasePressure 方法。

本教學課程示範如何將真實裝置連線到遠端監視解決方案加速器。

如同大部分在受條件約束裝置上執行的內嵌應用程式,裝置應用程式的用戶端程式碼是以 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 資料結構的參考。 在 main 函式中設定回呼函式時,會設定 userContextCallback 的值:

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. 若要檢視有關您裝置的診斷資訊,請向下捲動至 [診斷]:

    檢視裝置診斷

在裝置上採取行動

若要在裝置上叫用方法,請使用遠端監視解決方案中的 [裝置總管] 頁面。 例如,在遠端監視解決方案中,Chiller 裝置會實作 Reboot 方法。

  1. 選擇 [裝置] 以巡覽至解決方案的 [裝置總管] 頁面。

  2. 在 [裝置總管] 頁面中選取裝置清單中已佈建的裝置:

    選取您的實體裝置

  3. 若要顯示可在裝置上呼叫的方法清單,請依序選擇 [作業] 和 [方法]。 若要排程可在多個裝置上執行的作業,您可以在清單中選取多個裝置。 [作業] 面板會顯示所有您選取之裝置的通用方法。

  4. 選擇 [重新開機],將作業名稱設定為 RebootPhysicalChiller,然後選擇 [套用]:

    排程韌體更新

  5. 當模擬裝置處理該方法時,會在執行裝置程式碼的主控台中顯示一系列訊息。

注意

若要追蹤解決方案中的作業狀態,請選擇 [檢視作業狀態]。

後續步驟

自訂遠端監視解決方案加速器一文中,說明了一些自訂解決方案加速器的方法。