Connect your device to the remote monitoring preconfigured solution (Linux)

Scenario overview

In this scenario, you create a device that sends the following telemetry to the remote monitoring preconfigured solution:

  • External temperature
  • Internal temperature
  • Humidity

For simplicity, the code on the device generates sample values, but we encourage you to extend the sample by connecting real sensors to your device and sending real telemetry.

The device is also able to respond to methods invoked from the solution dashboard and desired property values set in the solution dashboard.

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, you must provision your remote monitoring preconfigured solution and provision a new custom device in that solution.

Provision your remote monitoring preconfigured solution

The device you create in this tutorial sends data to an instance of the remote monitoring preconfigured solution. If you haven't already provisioned the remote monitoring preconfigured solution in your Azure account, use the following steps:

  1. On the https://www.azureiotsolutions.com/ page, click + to create a solution.
  2. Click Select on the Remote monitoring panel to create your solution.
  3. On the Create Remote monitoring solution page, enter a Solution name of your choice, select the Region you want to deploy to, and select the Azure subscription to want to use. Then click Create solution.
  4. Wait until the provisioning process completes.

Warning

The preconfigured solutions use billable Azure services. Be sure to remove the preconfigured solution from your subscription when you are done with it to avoid any unnecessary charges. You can completely remove a preconfigured solution from your subscription by visiting the https://www.azureiotsolutions.com/ page.

When the provisioning process for the remote monitoring solution finishes, click Launch to open the solution dashboard in your browser.

Solution dashboard

Provision your device in the remote monitoring solution

Note

If you have already provisioned a device in your solution, you can skip this step. You need to know the device credentials when you create the client application.

For a device to connect to the preconfigured solution, it must identify itself to IoT Hub using valid credentials. You can retrieve the device credentials from the solution dashboard. You include the device credentials in your client application later in this tutorial.

To add a device to your remote monitoring solution, complete the following steps in the solution dashboard:

  1. In the lower left-hand corner of the dashboard, click Add a device.

    Add a device

  2. In the Custom Device panel, click Add new.

    Add a custom device

  3. Choose Let me define my own Device ID. Enter a Device ID such as mydevice, click Check ID to verify that name isn't already in use, and then click Create to provision the device.

    Add device ID

  4. Make a note the device credentials (Device ID, IoT Hub Hostname, and Device Key). Your client application needs these values to connect to the remote monitoring solution. Then click Done.

    View device credentials

  5. Select your device in the device list in the solution dashboard. Then, in the Device Details panel, click Enable Device. The status of your device is now Running. The remote monitoring solution can now receive telemetry from your device and invoke methods on the device.

Build and run a sample C client Linux

The following steps show you how to create a client application that communicates with the remote monitoring preconfigured solution. This application is written in C and built and run on Ubuntu Linux.

To complete these steps, you need a device running Ubuntu version 15.04 or 15.10. Before proceeding, install the prerequisite packages on your Ubuntu device using the following command:

sudo apt-get install cmake gcc g++

Install the client libraries on your device

The Azure IoT Hub client libraries are available as a package you can install on your Ubuntu device using the apt-get command. Complete the following steps to install the package that contains the IoT Hub client library and header files on your Ubuntu computer:

  1. In a shell, add the AzureIoT repository to your computer:

    sudo add-apt-repository ppa:aziotsdklinux/ppa-azureiot
    sudo apt-get update
    
  2. Install the azure-iot-sdk-c-dev package

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

Install the Parson JSON parser

The IoT Hub client libraries use the Parson JSON parser to parse message payloads. In a suitable folder on your computer, clone the Parson GitHub repository using the following command:

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

Prepare your project

On your Ubuntu machine, create a folder called remote_monitoring. In the remote_monitoring folder:

  • Create the four files main.c, remote_monitoring.c, remote_monitoring.h, and CMakeLists.txt.
  • Create folder called parson.

Copy the files parson.c and parson.h from your local copy of the Parson repository into the remote_monitoring/parson folder.

In a text editor, open the remote_monitoring.c file. Add the following #include statements:

#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"

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 Key] with values you noted for your device in the remote monitoring solution dashboard. Use the IoT Hub Hostname from the solution dashboard to replace [IoTHub Name]. For example, if your IoT Hub Hostname is contoso.azure-devices.net, replace [IoTHub Name] with contoso:

    static const char* deviceId = "[Device Id]";
    static const char* connectionString = "HostName=[IoTHub Name].azure-devices.net;DeviceId=[Device Id];SharedAccessKey=[Device Key]";
    
  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, external temperature, humidity, and a device ID as telemetry.
    • Can send metadata about the device to IoT Hub. The device sends basic metadata in a DeviceInfo object at startup.
    • Can send reported properties, to the device twin in IoT Hub. These reported properties are grouped into configuration, device, and system properties.
    • Can receive and act on desired properties set in the device twin in IoT Hub.
    • Can respond to the Reboot and InitiateFirmwareUpdate direct methods invoked through the solution portal. The device sends information about the direct methods it supports using reported properties.
    // Define the Model
    BEGIN_NAMESPACE(Contoso);
    
    /* Reported properties */
    DECLARE_STRUCT(SystemProperties,
      ascii_char_ptr, Manufacturer,
      ascii_char_ptr, FirmwareVersion,
      ascii_char_ptr, InstalledRAM,
      ascii_char_ptr, ModelNumber,
      ascii_char_ptr, Platform,
      ascii_char_ptr, Processor,
      ascii_char_ptr, SerialNumber
    );
    
    DECLARE_STRUCT(LocationProperties,
      double, Latitude,
      double, Longitude
    );
    
    DECLARE_STRUCT(ReportedDeviceProperties,
      ascii_char_ptr, DeviceState,
      LocationProperties, Location
    );
    
    DECLARE_MODEL(ConfigProperties,
      WITH_REPORTED_PROPERTY(double, TemperatureMeanValue),
      WITH_REPORTED_PROPERTY(uint8_t, TelemetryInterval)
    );
    
    /* Part of DeviceInfo */
    DECLARE_STRUCT(DeviceProperties,
      ascii_char_ptr, DeviceID,
      _Bool, HubEnabledState
    );
    
    DECLARE_DEVICETWIN_MODEL(Thermostat,
      /* Telemetry (temperature, external temperature and humidity) */
      WITH_DATA(double, Temperature),
      WITH_DATA(double, ExternalTemperature),
      WITH_DATA(double, Humidity),
      WITH_DATA(ascii_char_ptr, DeviceId),
    
      /* DeviceInfo */
      WITH_DATA(ascii_char_ptr, ObjectType),
      WITH_DATA(_Bool, IsSimulatedDevice),
      WITH_DATA(ascii_char_ptr, Version),
      WITH_DATA(DeviceProperties, DeviceProperties),
    
      /* Device twin properties */
      WITH_REPORTED_PROPERTY(ReportedDeviceProperties, Device),
      WITH_REPORTED_PROPERTY(ConfigProperties, Config),
      WITH_REPORTED_PROPERTY(SystemProperties, System),
    
      WITH_DESIRED_PROPERTY(double, TemperatureMeanValue, onDesiredTemperatureMeanValue),
      WITH_DESIRED_PROPERTY(uint8_t, TelemetryInterval, onDesiredTelemetryInterval),
    
      /* Direct methods implemented by the device */
      WITH_METHOD(Reboot),
      WITH_METHOD(InitiateFirmwareUpdate, ascii_char_ptr, FwPackageURI),
    
      /* Register direct methods with solution portal */
      WITH_REPORTED_PROPERTY(ascii_char_ptr_no_quotes, SupportedMethods)
    );
    
    END_NAMESPACE(Contoso);
    

Implement the behavior of the device

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

  1. Add the following functions that handle the desired properties set in the solution dashboard. These desired properties are defined in the model:

    void onDesiredTemperatureMeanValue(void* argument)
    {
      /* By convention 'argument' is of the type of the MODEL */
      Thermostat* thermostat = argument;
      printf("Received a new desired_TemperatureMeanValue = %f\r\n", thermostat->TemperatureMeanValue);
    
    }
    
    void onDesiredTelemetryInterval(void* argument)
    {
      /* By convention 'argument' is of the type of the MODEL */
      Thermostat* thermostat = argument;
      printf("Received a new desired_TelemetryInterval = %d\r\n", thermostat->TelemetryInterval);
    }
    
  2. 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(Thermostat* thermostat)
    {
      (void)(thermostat);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Rebooting\"");
      printf("Received reboot request\r\n");
      return result;
    }
    
    METHODRETURN_HANDLE InitiateFirmwareUpdate(Thermostat* thermostat, ascii_char_ptr FwPackageURI)
    {
      (void)(thermostat);
    
      METHODRETURN_HANDLE result = MethodReturn_Create(201, "\"Initiating Firmware Update\"");
      printf("Recieved firmware update request. Use package at: %s\r\n", FwPackageURI);
      return result;
    }
    
  3. Add the following function that sends a message to the preconfigured solution:

    /* Send data to IoT Hub */
    static void sendMessage(IOTHUB_CLIENT_HANDLE iotHubClientHandle, const unsigned char* buffer, size_t size)
    {
      IOTHUB_MESSAGE_HANDLE messageHandle = IoTHubMessage_CreateFromByteArray(buffer, size);
      if (messageHandle == NULL)
      {
        printf("unable to create a new IoTHubMessage\r\n");
      }
      else
      {
        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);
    }
    
  4. Add the following callback handler that runs when the device has sent new reported property values to the preconfigured solution:

    /* 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);
    }
    
  5. Add the following function to connect your device to the preconfigured solution 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 Thermostat model.
    • Creates and sends reported property values.
    • Sends a DeviceInfo object.
    • Creates a loop to send telemetry every second.
    • Deinitializes all resources.
    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
          {
    #ifdef MBED_BUILD_TIMESTAMP
            // For mbed add the certificate information
            if (IoTHubClient_SetOption(iotHubClientHandle, "TrustedCerts", certificates) != IOTHUB_CLIENT_OK)
            {
                printf("Failed to set option \"TrustedCerts\"\n");
            }
    #endif // MBED_BUILD_TIMESTAMP
            Thermostat* thermostat = IoTHubDeviceTwin_CreateThermostat(iotHubClientHandle);
            if (thermostat == NULL)
            {
              printf("Failure in IoTHubDeviceTwin_CreateThermostat\n");
            }
            else
            {
              /* Set values for reported properties */
              thermostat->Config.TemperatureMeanValue = 55.5;
              thermostat->Config.TelemetryInterval = 3;
              thermostat->Device.DeviceState = "normal";
              thermostat->Device.Location.Latitude = 47.642877;
              thermostat->Device.Location.Longitude = -122.125497;
              thermostat->System.Manufacturer = "Contoso Inc.";
              thermostat->System.FirmwareVersion = "2.22";
              thermostat->System.InstalledRAM = "8 MB";
              thermostat->System.ModelNumber = "DB-14";
              thermostat->System.Platform = "Plat 9.75";
              thermostat->System.Processor = "i3-7";
              thermostat->System.SerialNumber = "SER21";
              /* Specify the signatures of the supported direct methods */
              thermostat->SupportedMethods = "{\"Reboot\": \"Reboot the device\", \"InitiateFirmwareUpdate--FwPackageURI-string\": \"Updates device Firmware. Use parameter FwPackageURI to specify the URI of the firmware file\"}";
    
              /* Send reported properties to IoT Hub */
              if (IoTHubDeviceTwin_SendReportedStateThermostat(thermostat, deviceTwinCallback, NULL) != IOTHUB_CLIENT_OK)
              {
                printf("Failed sending serialized reported state\n");
              }
              else
              {
                printf("Send DeviceInfo object to IoT Hub at startup\n");
    
                thermostat->ObjectType = "DeviceInfo";
                thermostat->IsSimulatedDevice = 0;
                thermostat->Version = "1.0";
                thermostat->DeviceProperties.HubEnabledState = 1;
                thermostat->DeviceProperties.DeviceID = (char*)deviceId;
    
                unsigned char* buffer;
                size_t bufferSize;
    
                if (SERIALIZE(&buffer, &bufferSize, thermostat->ObjectType, thermostat->Version, thermostat->IsSimulatedDevice, thermostat->DeviceProperties) != CODEFIRST_OK)
                {
                  (void)printf("Failed serializing DeviceInfo\n");
                }
                else
                {
                  sendMessage(iotHubClientHandle, buffer, bufferSize);
                }
    
                /* Send telemetry */
                thermostat->Temperature = 50;
                thermostat->ExternalTemperature = 55;
                thermostat->Humidity = 50;
                thermostat->DeviceId = (char*)deviceId;
    
                while (1)
                {
                  unsigned char*buffer;
                  size_t bufferSize;
    
                  (void)printf("Sending sensor value Temperature = %f, Humidity = %f\n", thermostat->Temperature, thermostat->Humidity);
    
                  if (SERIALIZE(&buffer, &bufferSize, thermostat->DeviceId, thermostat->Temperature, thermostat->Humidity, thermostat->ExternalTemperature) != CODEFIRST_OK)
                  {
                    (void)printf("Failed sending sensor value\r\n");
                  }
                  else
                  {
                    sendMessage(iotHubClientHandle, buffer, bufferSize);
                  }
    
                  ThreadAPI_Sleep(1000);
                }
    
                IoTHubDeviceTwin_DestroyThermostat(thermostat);
              }
            }
            IoTHubClient_Destroy(iotHubClientHandle);
          }
          serializer_deinit();
        }
      }
      platform_deinit();
    }
    

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

    {"DeviceId":"mydevice01", "Temperature":50, "Humidity":50, "ExternalTemperature":55}
    

Call the remote_monitoring_run function

In a text editor, open the remote_monitoring.h file. Add the following code:

void remote_monitoring_run(void);

In a text editor, open the main.c file. Add the following code:

#include "remote_monitoring.h"

int main(void)
{
    remote_monitoring_run();

    return 0;
}

Build and run the application

The following steps describe how to use CMake to build your client application.

  1. In a text editor, open the CMakeLists.txt file in the remote_monitoring folder.

  2. Add the following instructions to define how to build your client application:

    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. In the remote_monitoring folder, create a folder to store the make files that CMake generates and then run the cmake and make commands as follows:

    mkdir cmake
    cd cmake
    cmake ../
    make
    
  4. Run the client application and send telemetry to IoT Hub:

    ./sample_app
    

View device telemetry in the dashboard

The dashboard in the remote monitoring solution enables you to view the telemetry your devices send to IoT Hub.

  1. In your browser, return to the remote monitoring solution dashboard, click Devices in the left-hand panel to navigate to the Devices list.

  2. In the Devices list, you should see that the status of your device is Running. If not, click Enable Device in the Device Details panel.

    View device status

  3. Click Dashboard to return to the dashboard, select your device in the Device to View drop-down to view its telemetry. The telemetry from the sample application is 50 units for internal temperature, 55 units for external temperature, and 50 units for humidity.

    View device telemetry

Invoke a method on your device

The dashboard in the remote monitoring solution enables you to invoke methods on your devices through IoT Hub. For example, in the remote monitoring solution you can invoke a method to simulate rebooting a device.

  1. In the remote monitoring solution dashboard, click Devices in the left-hand panel to navigate to the Devices list.

  2. Click Device ID for your device in the Devices list.

  3. In the Device details panel, click Methods.

    Device methods

  4. In the Method drop-down, select InitiateFirmwareUpdate, and then in FWPACKAGEURI enter a dummy URL. Click Invoke Method to call the method on the device.

    Invoke a device method

  5. You see a message in the console running your device code when the device handles the method. The results of the method are added to the history in the solution portal:

    View method history

Next steps

The article Customizing preconfigured solutions describes some ways you can extend this sample. Possible extensions include using real sensors and implementing additional commands.

You can learn more about the permissions on the azureiotsuite.com site.