Connect your device to the Remote Monitoring solution accelerator (Windows)

In this tutorial, you implement a Chiller device that sends the following telemetry to the Remote Monitoring solution accelerator:

  • Temperature
  • Pressure
  • Humidity

For simplicity, the code generates sample telemetry values for the Chiller. You could extend the sample by connecting real sensors to your device and sending real telemetry.

The sample device also:

  • Sends metadata to the solution to describe its capabilities.
  • Responds to actions triggered from the Devices page in the solution.
  • Responds to configuration changes send from the Devices page in the solution.

To complete this tutorial, you need an active Azure account. If you don't have an account, you can create a free trial account in just a couple of minutes. For details, see Azure Free Trial.

Before you start

Before you write any code for your device, deploy your Remote Monitoring solution accelerator and add a new physical device to the solution.

Deploy your Remote Monitoring solution accelerator

The Chiller device you create in this tutorial sends data to an instance of the Remote Monitoring solution accelerator. If you haven't already provisioned the Remote Monitoring solution accelerator in your Azure account, see Deploy the Remote Monitoring solution accelerator

When the deployment process for the Remote Monitoring solution finishes, click Launch to open the solution dashboard in your browser.

The solution dashboard

Add your device to the Remote Monitoring solution

Note

If you have already added a device in your solution, you can skip this step. However, the next step requires your device connection string. You can retrieve a device's connection string from the Azure portal or using the az iot CLI tool.

For a device to connect to the solution accelerator, it must identify itself to IoT Hub using valid credentials. You have the opportunity to save the device connection string that contains these credentials when you add the device the solution. You include the device connection string in your client application later in this tutorial.

To add a device to your Remote Monitoring solution, complete the following steps on the Devices page in the solution:

  1. Choose + New device, and then choose Physical as the Device type:

    Add a physical device

  2. Enter Physical-chiller as the Device ID. Choose the Symmetric Key and Auto generate keys options:

    Choose device options

  3. Choose Apply. Then make a note of the Device ID, Primary Key, and Connection string primary key values:

    Retrieve credentials

You've now added a physical device to the Remote Monitoring solution accelerator and noted its device connection string. In the following sections, you implement the client application that uses the device connection string to connect to your solution.

The client application implements the built-in Chiller device model. A solution accelerator device model specifies the following about a device:

  • The properties the device reports to the solution. For example, a Chiller device reports information about its firmware and location.
  • The types of telemetry the device sends to the solution. For example, a Chiller device sends temperature, humidity, and pressure values.
  • The methods you can schedule from the solution to run on the device. For example, a Chiller device must implement Reboot, FirmwareUpdate, EmergencyValveRelease, and IncreasePressure methods.

This tutorial shows you how to connect a physical device to the Remote Monitoring solution accelerator.

Create a C client solution on Windows

As with most embedded applications that run on constrained devices, the client code for the device application is written in C. In this tutorial, you build the application on a machine running Windows.

Create the starter project

Create a starter project in Visual Studio 2017 and add the IoT Hub device client NuGet packages:

  1. In Visual Studio, create a C console application using the Visual C++ Windows Console Application template. Name the project RMDevice.

    Create Visual C++ Windows Console Application

  2. In Solution Explorer, delete the files stdafx.h, targetver.h, and stdafx.cpp.

  3. In Solution Explorer, rename the file RMDevice.cpp to RMDevice.c.

    Solution Explorer showing renamed RMDevice.c file

  4. In Solution Explorer, right-click the RMDevice project and then click Manage NuGet packages. Choose Browse, then search for and install the following NuGet packages:

    • Microsoft.Azure.IoTHub.Serializer
    • Microsoft.Azure.IoTHub.IoTHubClient
    • Microsoft.Azure.IoTHub.MqttTransport

      NuGet package manager shows installed Microsoft.Azure.IoTHub packages

  5. In Solution Explorer, right-click on the RMDevice project and then choose Properties to open the project's Property Pages dialog box. For details, see Setting Visual C++ Project Properties.

  6. Choose the C/C++ folder, then choose the Precompiled Headers property page.

  7. Set Precompiled Header to Not Using Precompiled Headers. Then choose Apply.

    Project properties show project not using precompiled headers

  8. Choose the Linker folder, then choose the Input property page.

  9. Add crypt32.lib to the Additional Dependencies property. To save the project property values, choose OK and then OK again.

    Project properties show Linker including crypt32.lib

Add the Parson JSON library

Add the Parson JSON library to the RMDevice project and add the required #include statements:

  1. In a suitable folder on your computer, clone the Parson GitHub repository using the following command:

    git clone https://github.com/kgabis/parson.git
    
  2. Copy the parson.h and parson.c files from the local copy of the Parson repository to your RMDevice project folder.

  3. In Visual Studio, right-click the RMDevice project, choose Add, and then choose Existing Item.

  4. In the Add Existing Item dialog, select the parson.h and parson.c files in the RMDevice project folder. To add these two files to your project, choose Add.

    Solution Explorer shows parson.h and parson.c files

  5. In Visual Studio, open the RMDevice.c file. Replace the existing #include statements with the following code:

    #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 <string.h>
    

    Note

    Now you can verify that your project has the correct dependencies set up by building the solution.

Specify the behavior of the IoT device

The IoT Hub serializer client library uses a model to specify the format of the messages the device exchanges with IoT Hub.

  1. Add the following variable declarations after the #include statements. Replace the placeholder values [Device Id] and [Device connection string] with the values you noted for the physical device you added to the Remote Monitoring solution:

    static const char* deviceId = "[Device Id]";
    static const char* connectionString = "[Device connection string]";
    
  2. Add the following code to define the model that enables the device to communicate with IoT Hub. This model specifies that the device:

    • Can send temperature, pressure, and humidity as telemetry.
    • Can send reported properties, to the device twin in IoT Hub. These reported properties include information about the telemetry schema and supported methods.
    • Can receive and act on desired properties set in the device twin in IoT Hub.
    • Can respond to the Reboot, FirmwareUpdate, EmergencyValveRelease, and IncreasePressure direct methods invoked from the UI. The device sends information about the direct methods it supports using reported properties.

      // 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),
      
      /* Manage firmware update process */
      WITH_DATA(ascii_char_ptr, new_firmware_URI),
      WITH_DATA(ascii_char_ptr, new_firmware_version),
      
      /* 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, ascii_char_ptr, Firmware, ascii_char_ptr, FirmwareUri),
      WITH_METHOD(EmergencyValveRelease),
      WITH_METHOD(IncreasePressure)
      );
      
      END_NAMESPACE(Contoso);
      

Implement the behavior of the device

Now add code that implements the behavior defined in the model.

  1. Add the following callback handler that runs when the device has sent new reported property values to the solution accelerator:

    /* 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);
    }
    
  2. Add the following function that simulates a firmware update process:

    static int do_firmware_update(void *param)
    {
      Chiller *chiller = (Chiller *)param;
      printf("do_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";
      /* Send reported properties to IoT Hub */
      if (IoTHubDeviceTwin_SendReportedStateChiller(chiller, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
      {
        printf("Failed sending serialized reported state\r\n");
      }
      ThreadAPI_Sleep(5000);
    
      printf("Simulating applying phase...\r\n");
      chiller->FirmwareUpdateStatus = "applying";
      /* Send reported properties to IoT Hub */
      if (IoTHubDeviceTwin_SendReportedStateChiller(chiller, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
      {
        printf("Failed sending serialized reported state\r\n");
      }
      ThreadAPI_Sleep(5000);
    
      printf("Simulating reboot phase...\r\n");
      chiller->FirmwareUpdateStatus = "rebooting";
      /* Send reported properties to IoT Hub */
      if (IoTHubDeviceTwin_SendReportedStateChiller(chiller, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
      {
        printf("Failed sending serialized reported state\r\n");
      }
      ThreadAPI_Sleep(5000);
    
    #pragma warning(suppress : 4996)
      chiller->Firmware = strdup(chiller->new_firmware_version);
      chiller->FirmwareUpdateStatus = "waiting";
      /* Send reported properties to IoT Hub */
      if (IoTHubDeviceTwin_SendReportedStateChiller(chiller, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
      {
        printf("Failed sending serialized reported state\r\n");
      }
    
      return 0;
    }
    
  3. Add the following function that handles the desired properties set in the solution dashboard. These desired properties are defined in the model:

    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);
    }
    
  4. Add the following functions that handle the direct methods invoked through the IoT hub. These direct methods are defined in the model:

    /* 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, ascii_char_ptr Firmware, ascii_char_ptr FirmwareUri)
    {
      printf("Recieved firmware update request request\r\n");
      METHODRETURN_HANDLE result = NULL;
      if (chiller->FirmwareUpdateStatus != "waiting")
      {
        LogError("Attempting to initiate a firmware update out of order");
        result = MethodReturn_Create(400, "\"Attempting to initiate a firmware update out of order\"");
      }
      else
      {
    #pragma warning(suppress : 4996)
        chiller->new_firmware_version = strdup(Firmware);
    #pragma warning(suppress : 4996)
        chiller->new_firmware_URI = strdup(FirmwareUri);
        THREAD_HANDLE thread_apply;
        THREADAPI_RESULT t_result = ThreadAPI_Create(&thread_apply, do_firmware_update, chiller);
        if (t_result == THREADAPI_OK)
        {
          result = MethodReturn_Create(201, "\"Starting firmware update thread\"");
        }
        else
        {
          LogError("Failed to start firmware update thread");
          result = MethodReturn_Create(500, "\"Failed to start firmware update thread\"");
        }
      }
    
      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;
    }
    
  5. Add the following function that adds a property to a device-to-cloud message:

    /* 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);
      }
    }
    
  6. Add the following function that sends a message with properties to the solution accelerator:

    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);
    }
    
  7. Add the following function to connect your device to the solution accelerator in the cloud, and exchange data. This function performs the following steps:

    • Initializes the platform.
    • Registers the Contoso namespace with the serialization library.
    • Initializes the client with the device connection string.
    • Create an instance of the Chiller model.
    • Creates and sends reported property values.
    • Creates a loop to send telemetry every five seconds while the firmware update status is waiting.
    • Deinitializes all resources.

      void remote_monitoring_run(void)
      {
      if (platform_init() != 0)
      {
        printf("Failed to initialize the platform.\r\n");
      }
      else
      {
        if (SERIALIZER_REGISTER_NAMESPACE(Contoso) == NULL)
        {
          printf("Unable to SERIALIZER_REGISTER_NAMESPACE\r\n");
        }
        else
        {
          IOTHUB_CLIENT_HANDLE iotHubClientHandle = IoTHubClient_CreateFromConnectionString(connectionString, MQTT_Protocol);
          if (iotHubClientHandle == NULL)
          {
            printf("Failure in IoTHubClient_CreateFromConnectionString\r\n");
          }
          else
          {
            Chiller* chiller = IoTHubDeviceTwin_CreateChiller(iotHubClientHandle);
            if (chiller == NULL)
            {
              printf("Failure in IoTHubDeviceTwin_CreateChiller\r\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 = "waiting";
              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\r\n");
              }
              else
              {
                /* Send telemetry */
                chiller->temperature_unit = "F";
                chiller->pressure_unit = "psig";
                chiller->humidity_unit = "%";
      
                srand((unsigned int)time(NULL));
                while (1)
                {
                  chiller->temperature = 50 + ((rand() % 10) - 5);
                  chiller->pressure = 55 + ((rand() % 10) - 5);
                  chiller->humidity = 30 + ((rand() % 10) - 5);
                  unsigned char*buffer;
                  size_t bufferSize;
      
                  if (chiller->FirmwareUpdateStatus == "waiting")
                  {
                    (void)printf("Sending sensor value Temperature = %f %s,\r\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,\r\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,\r\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();
      }
      

      For reference, here is a sample Telemetry message sent to the solution accelerator:

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

Build and run the sample

Add code to invoke the remote_monitoring_run function, then build and run the device application:

  1. To invoke the remote_monitoring_run function, replace the main function with following code:

    int main()
    {
      remote_monitoring_run();
      return 0;
    }
    
  2. Choose Build and then Build Solution to build the device application.

  3. In Solution Explorer, right-click the RMDevice project, choose Debug, and then choose Start new instance to run the sample. The console displays messages as:

    • The application sends sample telemetry to the solution accelerator.
    • Receives desired property values set in the solution dashboard.
    • Responds to methods invoked from the solution dashboard.

View device telemetry

You can view the telemetry sent from your device on the Devices page in the solution.

  1. Select the device you provisioned in the list of devices on the Devices page. A panel displays information about your device including a plot of the device telemetry:

    See device detail

  2. Choose Pressure to change the telemetry display:

    View pressure telemetry

  3. To view diagnostic information about your device, scroll down to Diagnostics:

    View device diagnostics

Act on your device

To invoke methods on your devices, use the Devices page in the Remote Monitoring solution. For example, in the Remote Monitoring solution Chiller devices implement a FirmwareUpdate method.

  1. Choose Devices to navigate to the Devices page in the solution.

  2. Select the device you provisioned in the list of devices on the Devices page:

    Select your physical device

  3. To display a list of the methods you can call on your device, choose Jobs, then Run method. To schedule a job to run on multiple devices, you can select multiple devices in the list. The Jobs panel shows the types of method common to all the devices you selected.

  4. Choose FirmwareUpdate, set the job name to UpdatePhysicalChiller. Set Firmware Version to 2.0.0, set Firmware URI to http://contoso.com/updates/firmware.bin, and then choose Apply:

    Schedule the firmware update

  5. A sequence of messages displays in the console running your device code while the simulated device handles the method.

  6. When the update is complete, the new firmware version displays on the Devices page:

    Update completed

Note

To track the status of the job in the solution, choose View.

Next steps

The article Customize the Remote Monitoring solution accelerator describes some ways to customize the solution accelerator.