Quality RTOS & Embedded Software

  Real time embedded FreeRTOS RSS feed  
NOTE: The AWS IoT Device Shadow library and documentation are in the FreeRTOS Labs.  The libraries in the FreeRTOS Labs download directory are fully functional, but undergoing optimizations or refactoring to improve memory usage, modularity, documentation, demo usability, or test coverage.  They are available as part of the main download.

AWS IoT Device Shadow Operations Demo

 

Introduction

This demo shows how to use the Shadow library to connect to the AWS Device Shadow Service. It uses an MQTT connection with TLS (Mutual Authentication) to the AWS IoT MQTT Broker. The demo showcases some basic shadow operations such as how to update a shadow document, how to delete a shadow document, a callback when receiving a Shadow Updated message, and a callback when receiving a Shadow Delta message. The demo uses a JSON parser to parse shadow documents received from the AWS Shadow service. This parser is optimized for a low memory footprint and does not check the JSON documents for correctness.  

This demo is intended as a learning exercise only because the request to update the shadow document (state) and the update response are done by the same application.  In a realistic production scenario, an external application requests an update of the state of the device remotely, even if the device is not currently connected. The device will acknowledge the update request when it is connected.  

The Shadow Device Operations demo project uses the FreeRTOS Windows port, so you can build and evaluate it with the free Community version of Visual Studio on Windows without the need for any MCU hardware. 

 

Source Code Organization

The Visual Studio solution for the Shadow Device Operations demo is named shadow_device_operations_demo.sln and is located in the \FreeRTOS-Plus\Demo\FreeRTOS_IoT_Libraries\shadow\shadow_device_operations directory of the FreeRTOS labs download.

Note: The project is not in the main FreeRTOS download, so it is provided as a separate zip file download.

The Shadow library utilizes the IoT MQTT library and a lightweight JSON parser library.

 

Configure the Demo Project

The demo uses the FreeRTOS+TCP TCP/IP stack, so follow the instructions provided for the TCP/IP starter project to:

  1. Install the pre-requisite components (such as WinPCap).
  2. Optionally set a static or dynamic IP address, gateway address and netmask.
  3. Optionally set a MAC address.
  4. Select an Ethernet network interface on your host machine.
  5. (Important!) Test your network connection before you attempt to run the Shadow demo.

All these settings should be performed in the Shadow demo project, not the TCP/IP starter project referenced from the same page. As delivered, the TCP/IP stack is configured to use a dynamic IP address.

 

Configure the AWS IoT MQTT Broker Connection

In this demo you use an MQTT connection to the AWS IoT MQTT broker. This connection is configured in the same way as the MQTT mutual authentication demo.

 

Build the Demo Project

The demo project uses the free community edition of Visual Studio

To build the demo:

  1. Open the \FreeRTOS-Plus\Demo\FreeRTOS_IoT_Libraries\shadow\shadow_device_operations\shadow_device_operations_demo.sln Visual Studio solution file from within the Visual Studio IDE.
  2. Select ‘build solution’ from the IDE’s ‘build’ menu.

 

Functionality

The demo creates a single application task that loops through a set of examples that demonstrate shadow updates and Shadow Delta callbacks to simulate toggling a remote device’s state. It sends a shadow update with the new desired state and waits for the device to change its reported state in response to the new desired state. In addition, a Shadow Updated callback is used to print the changing shadow states. This demo also uses a secure MQTT connection to the AWS IoT MQTT Broker. The structure of the demo is shown here:

static void prvShadowDemoTask( void *pvParameters )
{
uint32_t ulNotificationValue = 0;
const TickType_t xNoDelay = ( TickType_t ) 0;

  /* Remove compiler warnings about unused parameters. */
  ( void ) pvParameters;

  /* One time initialization of the libraries used by this demo. */
  prvInitialiseLibraries();

  for( ; ; )
  {
    /* Don't expect any notifications to be pending yet. */
    configASSERT( ulTaskNotifyTake( pdTRUE, xNoDelay ) == 0 );

    /****************************** Connect. ******************************/

    /* Establish a connection to the AWS IoT MQTT broker. This example connects to
     * the MQTT broker as specified in awsiotdemoprofileAWS_ENDPOINT and
     * awsiotdemoprofileAWS_MQTT_PORT at the top of this file.
     */
    configPRINTF( ( "Attempt to connect to %s\r\n", awsiotdemoprofileAWS_ENDPOINT ) );
    prvMQTTConnect();
    configPRINTF( ( "Connected to %s\r\n", awsiotdemoprofileAWS_ENDPOINT ) );

    /************************ Create a semaphore **************************/

    /* Creates a semaphore to synchronize between delta callback and
     * Shadow updates.
     */
    configPRINTF( ( "Creating delta semaphore\r\n" ) );
    configASSERT( xSemaphoreCreateCountingStatic( 1, 0, &xDeltaSemaphore.xSemaphore ) != NULL );

    /************************ Set shadow callbacks ************************/

    /* Sets the updated callback and delta callback */
    configPRINTF( ( "Setting the updated callback and  delta callback\r\n" ) );
    prvSetShadowCallbacks();

    /************************ Clear shadow document ***********************/

    /* Clears the Shadow document if it exists already */
    configPRINTF( ( "Clearing the Shadow document if it already exits\r\n" ) );
    prvClearShadowDocument();

    /*********************** Send Shadow updates **************************/

    /* Send Shadow updates for shadowexampleUPDATE_COUNT times.
     * For each Shadow update, it waits on xDeltaSemaphore. xDeltaSemaphore
     * will be posted by the delta callback.
     */
    configPRINTF( ( "Sending Shadow updates\r\n" ) );
    prvSendShadowUpdates();

    /************************ Clear shadow document ***********************/

    /* Clears the Shadow document at the end of the demo */
    configPRINTF( ( "Clearing the Shadow document\r\n" ) );
    prvClearShadowDocument();

    /************** Clear callbacks and Disconnect MQTT. ******************/

    /* Clear updated callback and delta callback */
    configPRINTF( ( "Clearing the Shadow updated callback and delta callback\r\n" ) );
    prvClearShadowCallbacks();

    /* Disconnect MQTT gracefully. */
    prvMQTTDisconnect();
    configPRINTF( ( "Disconnected from %s\r\n\r\n", awsiotdemoprofileAWS_ENDPOINT ) );

    /* Wait for the disconnect operation to complete which is informed to us
     * by the disconnect callback (prvExample_OnDisconnect)by setting
     * the shadowexampleDISCONNECTED_BIT in this task's notification value.
     * Note that the bit is cleared in the task's notification value to
     * ensure that it is ready for the next run. */
    xTaskNotifyWait( 0UL,                           /* Don't clear any bits on entry. */
             shadowexampleDISCONNECTED_BIT, /* Clear bit on exit. */
             &( ulNotificationValue ),      /* Obtain the notification value. */
             pdMS_TO_TICKS( shadowexampleMQTT_TIMEOUT_MS ) );
    configASSERT( ( ulNotificationValue & shadowexampleDISCONNECTED_BIT ) == shadowexampleDISCONNECTED_BIT );

    /* Destroy the delta semaphore*/
    vSemaphoreDelete( ( SemaphoreHandle_t ) &xDeltaSemaphore.xSemaphore );

    /* Clear the current reported shadow state to toggle the reported state. */
    lDevicePowerOnState = 0;

    /* Wait for some time between two iterations to ensure that we do not
     * bombard the broker. */
    configPRINTF( ( "prvShadowDemoTask() completed an iteration successfully. Total free heap is %u\r\n", xPortGetFreeHeapSize() ) );
    configPRINTF( ( "Demo completed successfully.\r\n" ) );
    configPRINTF( ( "Short delay before starting the next iteration... \r\n\r\n" ) );
    vTaskDelay( pdMS_TO_TICKS( shadowexampleLOOP_WAIT_PERIOD_MS ) );
  }
}
 

This screenshot shows the expected output when the demo executes correctly:

 

Connect to the AWS IoT MQTT Broker

To connect to the AWS IoT MQTT broker, use the same method as prvMQTTConnect() in the MQTT mutual authentication demo

Register a Shadow Delta Callback and Shadow Updated Callback

The function prvSetShadowCallbacks() registers both a Shadow Delta callback and a Shadow Updated callback.

The definition of the function is:

static void prvSetShadowCallbacks( void )
{
AwsIotShadowError_t xCallbackStatus = AWS_IOT_SHADOW_STATUS_PENDING;
AwsIotShadowCallbackInfo_t xDeltaCallback = AWS_IOT_SHADOW_CALLBACK_INFO_INITIALIZER,
               xUpdatedCallback = AWS_IOT_SHADOW_CALLBACK_INFO_INITIALIZER;

  /* Set the functions for callbacks. */
  xDeltaCallback.pCallbackContext = &xDeltaSemaphore;
  xDeltaCallback.function = prvShadowDeltaCallback;
  xUpdatedCallback.function = prvShadowUpdatedCallback;

  /************************ Set delta callbacks ****************************/

  /* Set the delta callback, which notifies of different desired and reported
   * Shadow states. */
  xCallbackStatus = AwsIotShadow_SetDeltaCallback( xMQTTConnection,
                           iotdemoCLIENT_IDENTIFIER,
                           shadowexampleCLIENT_IDENTIFIER_LENGTH,
                           0, &xDeltaCallback );
  configASSERT( xCallbackStatus == AWS_IOT_SHADOW_SUCCESS );

  /************************ Set updated callbacks **************************/

  /* Set the updated callback, which notifies when a Shadow document is
  * changed. */
  xCallbackStatus = AwsIotShadow_SetUpdatedCallback( xMQTTConnection,
                             iotdemoCLIENT_IDENTIFIER,
                             shadowexampleCLIENT_IDENTIFIER_LENGTH,
                             0, &xUpdatedCallback );

  configASSERT( xCallbackStatus == AWS_IOT_SHADOW_SUCCESS );
}

prvSetShadowCallbacks() uses a function pointer to prvShadowDeltaCallback() as the Shadow Delta callback. This function receives the delta state as a JSON document. It parses the JSON document to get the delta value for the state ‘powerOn’ and compares this against the current device state maintained locally. If those are different, the local device state is updated to the new value given by the delta ‘powerOn’ state. The reported section of the Device Shadow is also updated with the new state.

The definition of the function is:

static void prvShadowDeltaCallback( void * pCallbackContext,
                  AwsIotShadowCallbackParam_t * pxCallbackParam )
{
BaseType_t xDeltaFound = pdFALSE;
const char * pcDelta = NULL;
size_t xDeltaLength = 0;
IotSemaphore_t * pxDeltaSemaphore = pCallbackContext;
uint32_t ulUpdateDocumentLength = 0;
AwsIotShadowError_t xShadowStatus = AWS_IOT_SHADOW_STATUS_PENDING;
AwsIotShadowDocumentInfo_t xUpdateDocument = AWS_IOT_SHADOW_DOCUMENT_INFO_INITIALIZER;
uint8_t ucNewState = 0;

  configASSERT( pxDeltaSemaphore != NULL );
  configASSERT( pxCallbackParam != NULL );

  /* A buffer containing the update document. It has static duration to prevent
   * it from being placed on the call stack. */
  static char cUpdateDocument[ shadowexampleREPORTED_JSON_SIZE + 1 ] = { 0 };

  /****************** Get delta state from Shadow document *****************/
  /* Check if there is a different "powerOn" state in the Shadow. */
  xDeltaFound = prvGetDelta( pxCallbackParam->u.callback.pDocument,
                 pxCallbackParam->u.callback.documentLength,
                 "powerOn",
                 &pcDelta,
                 &xDeltaLength );

  configASSERT( xDeltaFound == pdTRUE );

  /* Change the current state based on the value in the delta document. */
  if( *pcDelta == '0' )
  {
    ucNewState = 0;
  }
  else if( *pcDelta == '1' )
  {
    ucNewState = 1;
  }
  else
  {
    configPRINTF( ( "Unknown powerOn state parsed from delta document.\r\n" ) );

    /* Set new state to current state to ignore the delta document. */
    ucNewState = lDevicePowerOnState;
  }

  if( ucNewState != lDevicePowerOnState )
  {
    /* Toggle state. */
    configPRINTF( ( "%.*s changing state from %d to %d.\r\n",
            pxCallbackParam->thingNameLength,
            pxCallbackParam->pThingName,
            lDevicePowerOnState,
            ucNewState ) );

    lDevicePowerOnState = ucNewState;

    /* Set the common members to report the new state. */
    xUpdateDocument.pThingName = pxCallbackParam->pThingName;
    xUpdateDocument.thingNameLength = pxCallbackParam->thingNameLength;
    xUpdateDocument.u.update.pUpdateDocument = cUpdateDocument;
    xUpdateDocument.u.update.updateDocumentLength = shadowexampleREPORTED_JSON_SIZE;

    /* Generate a Shadow document for the reported state. To keep the client
    * token within 6 characters, it is modded by 1000000. */
    ulUpdateDocumentLength = snprintf( cUpdateDocument,
                       shadowexampleREPORTED_JSON_SIZE + 1,
                       shadowexampleREPORTED_JSON,
                       ( int ) lDevicePowerOnState,
                       ( long unsigned ) ( IotClock_GetTimeMs() % 1000000 ) );

    /* Check if the reported state document is generated for Shadow update*/
    configASSERT( ( size_t ) ulUpdateDocumentLength == shadowexampleREPORTED_JSON_SIZE );

    /* Send the Shadow update. Its result is not checked, as the Shadow updated
     * callback will report if the Shadow was successfully updated. Because the
     * Shadow is constantly updated in this demo, the "Keep Subscriptions" flag
     * is passed to this function. */
    xShadowStatus = AwsIotShadow_UpdateAsync( pxCallbackParam->mqttConnection,
                          &xUpdateDocument,
                          AWS_IOT_SHADOW_FLAG_KEEP_SUBSCRIPTIONS,
                          NULL,
                          NULL );

    configASSERT( xShadowStatus == AWS_IOT_SHADOW_STATUS_PENDING );
    configPRINTF( ( "%.*s sent new state report: %.*s\r\n",
            pxCallbackParam->thingNameLength,
            pxCallbackParam->pThingName,
            shadowexampleREPORTED_JSON_SIZE,
            cUpdateDocument ) );

    /* Post to the delta semaphore to unblock the thread sending Shadow updates. */
    xSemaphoreGive( ( SemaphoreHandle_t ) &pxDeltaSemaphore->xSemaphore );
  }
}

prvSetShadowCallbacks() uses a function pointer to prvShadowUpdatedCallback() as the Shadow Updated callback. This function receives the previous and current states in JSON format, then it parses the states and logs them.

The definition of the function is:

static void prvShadowUpdatedCallback( void * pCallbackContext,
                    AwsIotShadowCallbackParam_t * pxCallbackParam )
{
BaseType_t xPreviousFound = pdFALSE, xCurrentFound = pdFALSE;
const char * pcPrevious = NULL, * pcCurrent = NULL;
size_t xPreviousLength = 0, xCurrentLength = 0;

  /* Silence warnings about unused parameters. */
  ( void ) pCallbackContext;

  configASSERT( pxCallbackParam != NULL );

  /****************** Get previous state from Shadow document **************/
  /* Find the previous Shadow document. */
  xPreviousFound = prvGetUpdatedState( pxCallbackParam->u.callback.pDocument,
                     pxCallbackParam->u.callback.documentLength,
                     "previous",
                     &pcPrevious,
                     &xPreviousLength );

  /****************** Get current state from Shadow document **************/
  /* Find the current Shadow document. */
  xCurrentFound = prvGetUpdatedState( pxCallbackParam->u.callback.pDocument,
                    pxCallbackParam->u.callback.documentLength,
                    "current",
                    &pcCurrent,
                    &xCurrentLength );

  configASSERT( ( xPreviousFound == pdTRUE ) || ( xCurrentFound == pdTRUE ) );

  /* Log the previous and current states. */
  configPRINTF( ( "Shadow was updated!\r\n"
          "Previous: {\"state\":%.*s}\r\n"
          "Current:  {\"state\":%.*s}\r\n",
          xPreviousLength,
          pcPrevious,
          xCurrentLength,
          pcCurrent ) );
}

 

Delete the Shadow Document

The function prvClearShadowDocument() is used to delete a Shadow Document at the beginning and end of each loop of the demo. 

The definition of the function is:

static void prvClearShadowDocument( void )
{
AwsIotShadowError_t xDeleteStatus = AWS_IOT_SHADOW_STATUS_PENDING;

  /************************* Delete Shadow document ************************/
  xDeleteStatus = AwsIotShadow_DeleteSync( xMQTTConnection,
                       iotdemoCLIENT_IDENTIFIER,
                       shadowexampleCLIENT_IDENTIFIER_LENGTH,
                       0, shadowexampleMQTT_TIMEOUT_MS );
  configASSERT( ( xDeleteStatus == AWS_IOT_SHADOW_SUCCESS ) || ( xDeleteStatus == AWS_IOT_SHADOW_NOT_FOUND ) );

  configPRINTF( ( "Successfully cleared Shadow of %.*s.\r\n",
          shadowexampleCLIENT_IDENTIFIER_LENGTH,
          iotdemoCLIENT_IDENTIFIER ) );
}

 

Send Shadow Updates

The function prvSendShadowUpdates() is used to send updates to the Shadow Document a given number of times (configured by shadowexampleUPDATE_COUNT). At each update, the value of the ‘powerOn’ state in the desired section of the shadow document is toggled between 0 and 1. Each update will wait for the Shadow Delta callback to process the delta before it proceeds with the next update. When it updates the Device Shadow Document, it also updates a ‘clientToken’ that is used to associate responses with requests in an MQTT environment. Refer to AWS Shadow documentation for more details.

Also note that the ‘Keep Subscriptions’ flag is used in all shadow updates by passing the flag AWS_IOT_SHADOW_FLAG_KEEP_SUBSCRIPTIONS. This keeps the underlying MQTT subscriptions until all the updates to the Shadow are done.  Keeping subscriptions helps reduce the number of subscription requests that would be needed for each Shadow update by reusing the subscriptions.

The definition of the function is:

static void prvSendShadowUpdates( void )
{
int32_t lIndex = 0, lDesiredState = 0, lStatus = 0;
AwsIotShadowError_t xShadowStatus = AWS_IOT_SHADOW_STATUS_PENDING;
AwsIotShadowDocumentInfo_t xUpdateDocument = AWS_IOT_SHADOW_DOCUMENT_INFO_INITIALIZER;

  /* A buffer containing the update document. It has static duration to prevent
   * it from being placed on the call stack. */
  static char cUpdateDocument[ shadowexampleDESIRED_JSON_SIZE + 1 ] = { 0 };

  /********** Set the common members of Shadow update document *************/
  xUpdateDocument.pThingName = iotdemoCLIENT_IDENTIFIER;
  xUpdateDocument.thingNameLength = shadowexampleCLIENT_IDENTIFIER_LENGTH;
  xUpdateDocument.u.update.pUpdateDocument = cUpdateDocument;
  xUpdateDocument.u.update.updateDocumentLength = shadowexampleDESIRED_JSON_SIZE;

  /*************** Publish Shadow updates at a set period. *****************/
  for( lIndex = 1; lIndex <= shadowexampleUPDATE_COUNT; lIndex++ )
  {
    /* Toggle the desired state. */
    lDesiredState = !( lDesiredState );

    /* Generate a Shadow desired state document, using a timestamp for the client
     * token. To keep the client token within 6 characters, it is modded by 1000000. */
    lStatus = snprintf( cUpdateDocument,
              shadowexampleDESIRED_JSON_SIZE + 1,
              shadowexampleDESIRED_JSON,
              ( int ) lDesiredState,
              ( long unsigned ) ( IotClock_GetTimeMs() % 1000000 ) );

    /* Check for errors from snprintf. The expected value is the length of
     * the desired JSON document less the format specifier for the state. */
    configASSERT( lStatus == shadowexampleDESIRED_JSON_SIZE );

    configPRINTF( ( "Sending Shadow update %d of %d: %s\r\n",
            lIndex,
            shadowexampleUPDATE_COUNT,
            cUpdateDocument ) );

    /* Send the Shadow update. Because the Shadow is constantly updated in
     * this demo, the "Keep Subscriptions" flag is passed to this function.
     * Note that this flag only needs to be passed on the first call, but
     * passing it for subsequent calls is fine.
     */
    xShadowStatus = AwsIotShadow_UpdateSync( xMQTTConnection,
                         &xUpdateDocument,
                         AWS_IOT_SHADOW_FLAG_KEEP_SUBSCRIPTIONS,
                         shadowexampleMQTT_TIMEOUT_MS );

    configASSERT( xShadowStatus == AWS_IOT_SHADOW_SUCCESS );

    configPRINTF( ( "Successfully sent Shadow update %d of %d.\r\n",
            lIndex,
            shadowexampleUPDATE_COUNT ) );

    /* Wait for the delta callback to change its state before continuing. */
    configASSERT( xSemaphoreTake( ( SemaphoreHandle_t ) &xDeltaSemaphore.xSemaphore,
      pdMS_TO_TICKS( shadowexampleWAIT_PERIOD_FOR_DELTA_MS ) ) == pdTRUE );

    IotClock_SleepMs( shadowexampleUPDATE_PERIOD_MS );
  }

  /* Remove persistent subscriptions. In the AwsIotShadow_UpdateSync call, we have used the */
  xShadowStatus = AwsIotShadow_RemovePersistentSubscriptions( xMQTTConnection,
                                iotdemoCLIENT_IDENTIFIER,
                                shadowexampleCLIENT_IDENTIFIER_LENGTH,
                                AWS_IOT_SHADOW_FLAG_REMOVE_UPDATE_SUBSCRIPTIONS );

  configASSERT( xShadowStatus == AWS_IOT_SHADOW_SUCCESS );
}

 

Clear Shadow Callbacks

The function prvClearShadowCallbacks() clears registered callbacks for Shadow Delta and Shadow Updated by passing NULL for the function pointers in the API calls for registering callbacks.

The definition of the function is:

static void prvClearShadowCallbacks( void )
{
AwsIotShadowError_t xCallbackStatus = AWS_IOT_SHADOW_STATUS_PENDING;

  /************************ Clear delta callbacks **************************/
  xCallbackStatus = AwsIotShadow_SetDeltaCallback( xMQTTConnection,
                           iotdemoCLIENT_IDENTIFIER,
                           shadowexampleCLIENT_IDENTIFIER_LENGTH,
                           0, NULL );
  configASSERT( xCallbackStatus == AWS_IOT_SHADOW_SUCCESS );

  /************************ Clear updated callbacks ************************/
  xCallbackStatus = AwsIotShadow_SetUpdatedCallback( xMQTTConnection,
                             iotdemoCLIENT_IDENTIFIER,
                             shadowexampleCLIENT_IDENTIFIER_LENGTH,
                             0, NULL );
  configASSERT( xCallbackStatus == AWS_IOT_SHADOW_SUCCESS );
}
Copyright (C) Amazon Web Services, Inc. or its affiliates. All rights reserved.