AWS IoT Device Shadow Operations Demo
Introduction
This demo shows how to use the AWS IoT Device Shadow library to connect to the AWS Device Shadow Service. It uses the
coreMQTT library to
establish an MQTT connection with TLS (Mutual Authentication) to the AWS IoT MQTT Broker and the
coreJSON parser to parse
shadow documents it receives from the AWS Shadow service. The demo showcases some basic
shadow operations, such as how to update a shadow document and how to delete a shadow document. The demo also show how to
register a callback function with the MQTT library to handle messages like the shadow /update
and
/update/delta
messages that are sent from the AWS Device Shadow service.
This demo is intended only as a learning exercise 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 would request an
update of the state of the IoT device remotely, even if the IoT device is not currently connected. The device will
acknowledge the update request when it is connected.
This 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 demo project is called shadow_device_operations_demo.sln
and can be found on
Github in
the following directory:
FreeRTOS-Plus\Demo\AWS\Device_Shadow_Windows_Simulator\Device_Shadow_Demo
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:
- Install the pre-requisite
components (such as WinPCap).
- Optionally set a static
or dynamic IP address, gateway address and netmask.
- Optionally set a MAC
address.
- Select an Ethernet network
interface on your host machine.
- (Important!) Test
your network connection before you attempt to run the Shadow demo.
All of these settings should be set in the Shadow demo project.
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:
- Open the Visual Studio solution file
FreeRTOS-Plus\Demo\AWS\Device_Shadow_Windows_Simulator\Device_Shadow_Demo\shadow_main_demo.sln
from within the Visual Studio IDE.
- 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 /update
and /update/delta
callbacks to simulate toggling a remote IoT device’s state. It sends a shadow update with the new
desired
state and waits for the IoT device to change its reported
state in response to the new
desired
state. In addition, a shadow /update
callback is used to print the changing shadow states.
This demo also uses a secure MQTT connection to the AWS IoT MQTT Broker, and assumes there is a powerOn
state in
the device shadow.
The demo performs the following operations:
- Establish a MQTT connection by using the helper functions in
shadow_demo_helpers.c
.
- Assemble MQTT topic strings for IoT device shadow operations, using macros defined by the Device Shadow library.
- Publish to the MQTT topic used for deleting a device shadow to delete any existing device shadow.
- Subscribe to the MQTT topics for
/update/delta
, /update/accepted
and
/update/rejected
using helper functions in shadow_demo_helpers.c
.
- Publish a desired state of
powerOn
using helper functions in shadow_demo_helpers.c
. This will
cause an /update/delta
message to be sent to the IoT device.
- Handle incoming MQTT messages in
prvEventCallback
, and determine whether the message is related to the
device shadow by using a function defined by the Device Shadow library (Shadow_MatchTopic
). If the
message is a device shadow /update/delta
message, then the main demo function will publish a second
message to update the reported state to powerOn
. If an /update/accepted
message is received,
verify that it has the same clientToken as previously published in the update message. That will mark the end of
the demo.
The structure of the demo is shown here:
void prvShadowDemoTask( void * pvParameters )
{
BaseType_t demoStatus = pdPASS;
static char pcUpdateDocument[ SHADOW_REPORTED_JSON_LENGTH + 1 ] = { 0 };
demoStatus = xEstablishMqttSession( prvEventCallback );
if( pdFAIL == demoStatus )
{
LogError( ( “Failed to connect to MQTT broker.” ) );
}
else
{
demoStatus = xPublishToTopic( SHADOW_TOPIC_STRING_DELETE( THING_NAME ),
SHADOW_TOPIC_LENGTH_DELETE( THING_NAME_LENGTH ),
pcUpdateDocument,
0U );
if( demoStatus == pdPASS )
{
demoStatus = xSubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_DELTA( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_DELTA( THING_NAME_LENGTH ) );
}
if( demoStatus == pdPASS )
{
demoStatus = xSubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_ACCEPTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_ACCEPTED( THING_NAME_LENGTH ) );
}
if( demoStatus == pdPASS )
{
demoStatus = xSubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_REJECTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_REJECTED( THING_NAME_LENGTH ) );
}
if( demoStatus == pdPASS )
{
LogInfo( ( “Send desired power state with 1.” ) );
( void ) memset( pcUpdateDocument,
0x00,
sizeof( pcUpdateDocument ) );
snprintf( pcUpdateDocument,
SHADOW_DESIRED_JSON_LENGTH + 1,
SHADOW_DESIRED_JSON,
( int ) 1,
( long unsigned ) ( xTaskGetTickCount() % 1000000 ) );
demoStatus = xPublishToTopic( SHADOW_TOPIC_STRING_UPDATE( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE( THING_NAME_LENGTH ),
pcUpdateDocument,
( SHADOW_DESIRED_JSON_LENGTH + 1 ) );
}
if( demoStatus == pdPASS )
{
if( stateChanged == true )
{
LogInfo( ( “Report to the state change: %d”, ulCurrentPowerOnState ) );
( void ) memset( pcUpdateDocument,
0x00,
sizeof( pcUpdateDocument ) );
ulClientToken = ( xTaskGetTickCount() % 1000000 );
snprintf( pcUpdateDocument,
SHADOW_REPORTED_JSON_LENGTH + 1,
SHADOW_REPORTED_JSON,
( int ) ulCurrentPowerOnState,
( long unsigned ) ulClientToken );
demoStatus = xPublishToTopic( SHADOW_TOPIC_STRING_UPDATE( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE( THING_NAME_LENGTH ),
pcUpdateDocument,
( SHADOW_DESIRED_JSON_LENGTH + 1 ) );
}
else
{
LogInfo( ( “No change from /update/delta, unsubscribe all shadow topics and disconnect from MQTT.\r\n” ) );
}
}
if( demoStatus == pdPASS )
{
LogInfo( ( “Start to unsubscribe shadow topics and disconnect from MQTT. \r\n” ) );
demoStatus = xUnsubscribeFromTopic( SHADOW_TOPIC_STRING_UPDATE_DELTA( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_DELTA( THING_NAME_LENGTH ) );
if( demoStatus != pdPASS )
{
LogError( ( “Failed to unsubscribe the topic %s”,
SHADOW_TOPIC_STRING_UPDATE_DELTA( THING_NAME ) ) );
}
}
if( demoStatus == pdPASS )
{
demoStatus = xUnsubscribeFromTopic( SHADOW_TOPIC_STRING_UPDATE_ACCEPTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_ACCEPTED( THING_NAME_LENGTH ) );
if( demoStatus != pdPASS )
{
LogError( ( “Failed to unsubscribe the topic %s”,
SHADOW_TOPIC_STRING_UPDATE_ACCEPTED( THING_NAME ) ) );
}
}
if( demoStatus == pdPASS )
{
demoStatus = xUnsubscribeFromTopic( SHADOW_TOPIC_STRING_UPDATE_REJECTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_REJECTED( THING_NAME_LENGTH ) );
if( demoStatus != pdPASS )
{
LogError( ( “Failed to unsubscribe the topic %s”,
SHADOW_TOPIC_STRING_UPDATE_REJECTED( THING_NAME ) ) );
}
}
demoStatus = xDisconnectMqttSession();
if( ( xUpdateAcceptedReturn != pdPASS ) || ( xUpdateDeltaReturn != pdPASS ) )
{
LogError( ( “Callback function failed.” ) );
}
if( demoStatus == pdPASS )
{
LogInfo( ( “Demo completed successfully.” ) );
}
else
{
LogError( ( “Shadow Demo failed.” ) );
}
}
LogInfo( ( “Deleting Shadow Demo task.” ) );
vTaskDelete( NULL );
}
This screenshot shows the expected output when the demo executes correctly:
Click to enlarge
Connect to the AWS IoT MQTT Broker
To connect to the AWS IoT MQTT broker, we use the same method as MQTTConnect()
in the
MQTT mutual
authentication demo.
Delete the Shadow Document
To delete the shadow document, call xPublishToTopic
with an empty message, using macros defined by the Device
Shadow library. This uses MQTT_Publish
to publish to the /delete
topic. The following code
section shows how this is done in the function prvShadowDemoTask
.
returnStatus = PublishToTopic( SHADOW_TOPIC_STRING_DELETE( THING_NAME ),
SHADOW_TOPIC_LENGTH_DELETE( THING_NAME_LENGTH ),
pcUpdateDocument,
0U );
Subscribe to Shadow Topics
Subscribe to the Device Shadow topics to receive notifications from the AWS IoT broker about shadow changes. The Device
Shadow topics are assembled by macros defined in the Device Shadow library. The following code section shows how this is
done in the prvShadowDemoTask
function:
if( returnStatus == EXIT_SUCCESS )
{
returnStatus = SubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_DELTA( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_DELTA( THING_NAME_LENGTH ) );
}
if( returnStatus == EXIT_SUCCESS )
{
returnStatus = SubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_ACCEPTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_ACCEPTED( THING_NAME_LENGTH ) );
}
if( returnStatus == EXIT_SUCCESS )
{
returnStatus = SubscribeToTopic( SHADOW_TOPIC_STRING_UPDATE_REJECTED( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE_REJECTED( THING_NAME_LENGTH ) );
}
Send Shadow Updates
To send a shadow update, the demo calls xPublishToTopic
with a message in JSON format, using macros defined
by the Device Shadow library. This uses MQTT_Publish
to publish to the /delete
topic. The
following code section shows how this is done in the prvShadowDemoTask
function:
#define SHADOW_REPORTED_JSON \
“{” \
“\”state\”:{” \
“\”reported\”:{” \
“\”powerOn\”:%01d” \
“}” \
“},” \
“\”clientToken\”:\”%06lu\”” \
“}”
snprintf( pcUpdateDocument,
SHADOW_REPORTED_JSON_LENGTH + 1,
SHADOW_REPORTED_JSON,
( int ) ulCurrentPowerOnState,
( long unsigned ) ulClientToken );
xPublishToTopic( SHADOW_TOPIC_STRING_UPDATE( THING_NAME ),
SHADOW_TOPIC_LENGTH_UPDATE( THING_NAME_LENGTH ),
pcUpdateDocument,
( SHADOW_DESIRED_JSON_LENGTH + 1 ) );
Handle Shadow Delta Messages and Shadow Update Messages
The user callback function, that was registered to the
coreMQTT Client library
using the function MQTT_Init()
, will notify us about an incoming packet event. Here’s the callback
function:
static void prvEventCallback( MQTTContext_t * pxMqttContext,
MQTTPacketInfo_t * pxPacketInfo,
MQTTDeserializedInfo_t * pxDeserializedInfo )
{
ShadowMessageType_t messageType = ShadowMessageTypeMaxNum;
const char * pcThingName = NULL;
uint16_t usThingNameLength = 0U;
uint16_t usPacketIdentifier;
( void ) pxMqttContext;
configASSERT( pxDeserializedInfo != NULL );
configASSERT( pxMqttContext != NULL );
configASSERT( pxPacketInfo != NULL );
usPacketIdentifier = pxDeserializedInfo->packetIdentifier;
if( ( pxPacketInfo->type & 0xF0U ) == MQTT_PACKET_TYPE_PUBLISH )
{
configASSERT( pxDeserializedInfo->pPublishInfo != NULL );
LogInfo( ( “pPublishInfo->pTopicName:%s.”, pxDeserializedInfo->pPublishInfo->pTopicName ) );
if( SHADOW_SUCCESS == Shadow_MatchTopic( pxDeserializedInfo->pPublishInfo->pTopicName,
pxDeserializedInfo->pPublishInfo->topicNameLength,
&messageType,
&pcThingName,
&usThingNameLength ) )
{
if( messageType == ShadowMessageTypeUpdateDelta )
{
prvUpdateDeltaHandler( pxDeserializedInfo->pPublishInfo );
}
else if( messageType == ShadowMessageTypeUpdateAccepted )
{
prvUpdateAcceptedHandler( pxDeserializedInfo->pPublishInfo );
}
else if( messageType == ShadowMessageTypeUpdateDocuments )
{
LogInfo( ( “/update/documents json payload:%s.”, ( const char * ) pxDeserializedInfo->pPublishInfo->pPayload ) );
}
else if( messageType == ShadowMessageTypeUpdateRejected )
{
LogInfo( ( “/update/rejected json payload:%s.”, ( const char * ) pxDeserializedInfo->pPublishInfo->pPayload ) );
}
else
{
LogInfo( ( “Other message type:%d !!”, messageType ) );
}
}
else
{
LogError( ( “Shadow_MatchTopic parse failed:%s !!”, ( const char * ) pxDeserializedInfo->pPublishInfo->pTopicName ) );
}
}
else
{
vHandleOtherIncomingPacket( pxPacketInfo, usPacketIdentifier );
}
}
The callback function confirms the incoming packet is of type MQTT_PACKET_TYPE_PUBLISH
, and uses the Device
Shadow Library API Shadow_MatchTopic
to confirm that the incoming message is a shadow message.
If the incoming message is a shadow message with type ShadowMessageTypeUpdateDelta
, then we call
prvUpdateDeltaHandler
to handle this message. The handler prvUpdateDeltaHandler
uses the
coreJSON library
to parse the message to get the delta value for the state powerOn
and compares this against the current IoT device
state maintained locally. If those are different, the local IoT device state is updated to reflect the new value of the
powerOn
state from the shadow document.
static void prvUpdateDeltaHandler( MQTTPublishInfo_t * pxPublishInfo )
{
static uint32_t ulCurrentVersion = 0;
uint32_t ulVersion = 0U;
uint32_t ulNewState = 0U;
char * pcOutValue = NULL;
uint32_t ulOutValueLength = 0U;
JSONStatus_t result = JSONSuccess;
configASSERT( pxPublishInfo != NULL );
configASSERT( pxPublishInfo->pPayload != NULL );
LogInfo( ( “/update/delta json payload:%s.”, ( const char * ) pxPublishInfo->pPayload ) );
result = JSON_Validate( pxPublishInfo->pPayload,
pxPublishInfo->payloadLength );
if( result == JSONSuccess )
{
result = JSON_Search( ( char * ) pxPublishInfo->pPayload,
pxPublishInfo->payloadLength,
“version”,
sizeof( “version” ) – 1,
‘.’,
&pcOutValue,
( size_t * ) &ulOutValueLength );
}
else
{
LogError( ( “The json document is invalid!!” ) );
}
if( result == JSONSuccess )
{
LogInfo( ( “version: %.*s”,
ulOutValueLength,
pcOutValue ) );
ulVersion = ( uint32_t ) strtoul( pcOutValue, NULL, 10 );
}
else
{
LogError( ( “No version in json document!!” ) );
}
LogInfo( ( “version:%d, ulCurrentVersion:%d \r\n”, ulVersion, ulCurrentVersion ) );
if( ulVersion > ulCurrentVersion )
{
ulCurrentVersion = ulVersion;
result = JSON_Search( ( char * ) pxPublishInfo->pPayload,
pxPublishInfo->payloadLength,
“state.powerOn”,
sizeof( “state.powerOn” ) – 1,
‘.’,
&pcOutValue,
( size_t * ) &ulOutValueLength );
}
else
{
LogWarn( ( “The received version is smaller than current one!!” ) );
}
if( result == JSONSuccess )
{
ulNewState = ( uint32_t ) strtoul( pcOutValue, NULL, 10 );
LogInfo( ( “The new power on state newState:%d, ulCurrentPowerOnState:%d \r\n”,
ulNewState, ulCurrentPowerOnState ) );
if( ulNewState != ulCurrentPowerOnState )
{
ulCurrentPowerOnState = ulNewState;
stateChanged = true;
}
}
else
{
LogError( ( “No powerOn in json document!!” ) );
xUpdateDeltaReturn = pdFAIL;
}
}
If the incoming message is a shadow message with type ShadowMessageTypeUpdateAccepted
, then
prvUpdateAcceptedHandler
is called to handle this message. The handler prvUpdateAcceptedHandler
parses the message using the
coreJSON library to get the clientToken from the message. This handler function checks that the client token from the
JSON message matches the client token used by the application. If it doesn’t match, the function logs a warning message.
static void prvUpdateAcceptedHandler( MQTTPublishInfo_t * pxPublishInfo )
{
char * pcOutValue = NULL;
uint32_t ulOutValueLength = 0U;
uint32_t ulReceivedToken = 0U;
JSONStatus_t result = JSONSuccess;
assert( pxPublishInfo != NULL );
assert( pxPublishInfo->pPayload != NULL );
LogInfo( ( “/update/accepted json payload:%s.”, ( const char * ) pxPublishInfo->pPayload ) );
result = JSON_Validate( pxPublishInfo->pPayload,
pxPublishInfo->payloadLength );
if( result == JSONSuccess )
{
result = JSON_Search( ( char * ) pxPublishInfo->pPayload,
pxPublishInfo->payloadLength,
“clientToken”,
sizeof( “clientToken” ) – 1,
‘.’,
&pcOutValue,
( size_t * ) &ulOutValueLength );
}
else
{
LogError( ( “Invalid json documents !!” ) );
}
if( result == JSONSuccess )
{
LogInfo( ( “clientToken: %.*s”, ulOutValueLength,
pcOutValue ) );
ulReceivedToken = ( uint32_t ) strtoul( pcOutValue, NULL, 10 );
LogInfo( ( “receivedToken:%d, clientToken:%u \r\n”, ulReceivedToken, ulClientToken ) );
if( ulReceivedToken == ulClientToken )
{
LogInfo( ( “Received response from the device shadow. Previously published ”
“update with clientToken=%u has been accepted. “, ulClientToken ) );
}
else
{
LogWarn( ( “The received clientToken=%u is not identical with the one=%u we sent “,
ulReceivedToken, ulClientToken ) );
}
}
else
{
LogError( ( “No clientToken in json document!!” ) );
lUpdateAcceptedReturn = EXIT_FAILURE;
}
}
Copyright (C) Amazon Web Services, Inc. or its affiliates. All rights reserved.