In a previous article, we walked through a step by step process of how to get started with Amazon’s AWS IoT Core managed cloud service. To do this, we used the lightweight MQTT messaging protocol and a NetBurner development kit as the IoT device. While we covered all of the base requirements from start to finish, we also glossed over an awful lot concerning the actual application running on the NetBurner module. In this article, we’re going to revisit the program, crack it open, and take a peek at how it ticks. This walk-through will give you the jump-start you need to design even more robust applications around these core functions.
Program Overview
Our application is very straight forward, and implements four distinct pieces of functionality:
- Initial setup
- Publish an MQTT message ever 5 seconds
- Subscribe to an MQTT topic to receive incoming messages
- Provide a web interface to view the last message sent and received
We expect the messages to be published and received in the JSON format and use the ParsedJsonDataSet
class extensively in processing the MQTT payload data.
Source Code Review
Our project contains several source files, the AWS SDK, and an html folder that includes the index.html
we use in our web interface. In this article, we will concern ourselves mostly with the six .cpp
files directly in the src
folder (shown above in Figure 1), their associated header files, and a configuration header file. The following is a brief description of each of these:
main.cpp
– This is the starting point of the application. It defines the application name, AWS IoT Core Base, and provides the entry point for our program with UserMain()
. More on this bit later.
post-record-data.cpp
– This file contains the functions that initialize the AWS SDK, and handles processing both incoming and outgoing MQTT messages.
network_netburner.cpp
– This file contains the functions that are required to implement the networking interface for the AWS SDK API and ties it to our networking stack. These are all exclusively related to the TLS network connection.
timer_netburner.cpp
– This file contains additional functions that are required by the AWS IoT Core MQTT client, and are related to the timer functionality that is used to manage the MQTT connections.
record-data.cpp
– This contains a small function that serializes the JSON data used in the MQTT messages.
html-control.cpp
– This file contains two functions used to build the HTML page that displays the most recently sent and received MQTT messages, as shown below.
aws_iot_config.h
– This file has all of the config constant values that we need to connect to our service and publish/receive messages. As mentioned in the previous article, you will want to change both AWS_IOT_MQTT_HOST
and AWS_IOT_MQTT_CLIENT_ID
to values that are specific to your account and environment.
Initial Setup
Let’s turn our attention to the entry point of our application, UserMain()
, found in main.cpp
. Inside UserMain()
, you will see calls to the following functions, more or less in the given order, before we hit our main while()
loop:
init()
– This function sets up the network stack for the application, starts the configuration server, gives UserMain()
the correct task priority, and sets up a few debug details. If you happen to forget this in your application, you will know very quickly.
StartHttp()
– This kick-starts the webserver on the device. It couldn’t be easier.
InitializeAWSSDK()
– This does pretty much exactly what you would expect. It sets up the AWS SDK and takes care of the initial MQTT setup. We’ll dive into this a bit more below.
After everything is initialized, we will enter our main while loop, where you will see the following bit of code:
while (1) { // Post our record data PostRecordData(); // Wait 5 seconds before doing it again OSTimeDly(TICKS_PER_SECOND * 5); }
In true embedded systems fashion, we will continue to run our while() loop indefinitely. Every five seconds we will make a call to PostRecordData()
, which will post a message to our subscribed MQTT topic, defined as gTopicNameSend
in post-record-data.cpp
.
Initializing the AWS IoT Core SDK for MQTT
As mentioned above, initializing the AWS SDK for IoT Core services takes place in InitializeAWSSDK()
, found in post-record-data.cpp
.
bool InitializeAWSSDK() { IoT_Error_t rc = FAILURE; IoT_Client_Init_Params mqttInitParams = iotClientInitParamsDefault; IoT_Client_Connect_Params connectParams = iotClientConnectParamsDefault; iprintf("\nAWS IoT SDK Version %d.%d.%d-%s\n", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_TAG); // Set required MQTT parameters mqttInitParams.enableAutoReconnect = false; mqttInitParams.pHostURL = AWS_IOT_MQTT_HOST; mqttInitParams.port = AWS_IOT_MQTT_PORT; mqttInitParams.pRootCALocation = ""; mqttInitParams.pDeviceCertLocation = (char *) certificate; mqttInitParams.pDevicePrivateKeyLocation = (char *) privatekey; mqttInitParams.mqttCommandTimeout_ms = 30000; mqttInitParams.tlsHandshakeTimeout_ms = 10000; mqttInitParams.isSSLHostnameVerify = true; mqttInitParams.disconnectHandler = nullptr; mqttInitParams.disconnectHandlerData = nullptr; rc = aws_iot_mqtt_init(&client, &mqttInitParams); if (SUCCESS != rc) { iprintf("aws_iot_mqtt_init returned error : %d ", rc); return false; } connectParams.keepAliveIntervalInSec = 600; connectParams.isCleanSession = true; connectParams.MQTTVersion = MQTT_3_1_1; connectParams.pClientID = AWS_IOT_MQTT_CLIENT_ID; connectParams.clientIDLen = (uint16_t) strlen(AWS_IOT_MQTT_CLIENT_ID); connectParams.isWillMsgPresent = false; iprintf("Connecting...\n"); rc = aws_iot_mqtt_connect(&client, &connectParams); if (SUCCESS != rc) { iprintf("Error(%d) connecting to %s:%d\n", rc, mqttInitParams.pHostURL, mqttInitParams.port); return false; } rc = aws_iot_mqtt_autoreconnect_set_status(&client, true); if (SUCCESS != rc) { iprintf("Unable to set Auto Reconnect to true - %d\n", rc); return false; } iprintf("Subscribing to topic: %s, %d\n", gTopicNameRec, strlen(gTopicNameRec)); rc = aws_iot_mqtt_subscribe(&client, (char *) gTopicNameRec, strlen(gTopicNameRec), QOS1, IoTSubscribeCallbackHandler, nullptr); if (SUCCESS != rc) { iprintf("Error subscribing : %d\n", rc); return false; } return true; }
Near the top of the function, you will see the parameters getting set for the call to aws_iot_mqtt_init()
. You should be okay with sticking to the values we’ve provided. However, for the exceptionally curious, you can find out more about each one from the official AWS API documentation, here
.
Assuming the MQTT initialization function is successful, we will then set up our connection parameters and pass them into the function, aws_iot_mqtt_connet()
. As you might guess, establishes a connection to the AWS IoT Core service. Again, you should be okay with the values we set, but you can find more information on what these represent here. If we’re able to successfully set up a connection, we follow it up with a call to aws_iot_mqtt_autoreconnect_set_status()
which includes a parameter of true. This will make sure that we automatically reconnect if our connection fails for some reason.
Finally, we subscribe to our topic with a call to aws_iot_mqtt_subscribe()
. The topic that we will subscribe to is gTopicNameRec
, which is defined at the top of post-record-data.cpp
. We also pass a pointer to the function that is called when we receive a message, IoTSubscribeCallbackHandler()
.
Publishing MQTT Data
Now that we’ve established a connection to the AWS IoT Core service, we are ready to start publishing messages using the MQTT protocol. As we mentioned before, we will post a new message once every five seconds from our UserMain()
function with a call to PostRecordData()
, which is defined in post-record-data.cpp
.
void PostRecordData() { char buffer[200] = { 0 }; CreateOutMessage(gJsonOut); gJsonOut.PrintObjectToBuffer(buffer, 199, false); IoT_Publish_Message_Params paramsQOS1; paramsQOS1.qos = QOS1; paramsQOS1.payload = (void *) buffer; paramsQOS1.payloadLen = strlen(buffer); paramsQOS1.isRetained = 0; IoT_Error_t rc = aws_iot_mqtt_publish(&client, gTopicNameSend, strlen(gTopicNameSend), ¶msQOS1); if (SUCCESS != rc) { iprintf("Error publishing : %d\n", rc); } else { iprintf("Publish success:\r\n"); gJsonOut.PrintObject(true); iprintf("\r\n"); } }
The first thing you’ll notice we do is call CreateOutMessage()
. This builds our MQTT payload and stores it in gJsonOut
, which is a ParsedJsonDataSet
object. We save this globally so that it can be referenced and displayed via a webpage served up by our module. The contents of the record are a timestamp of when the message was created and a static id that is incremented whenever the function is called. The record is defined by the class PostRecord
and is serialized into gJsonOut
along with the device id, AWS_IOT_MQTT_CLIENT_ID
.
With our payload correctly structured, we dump it into a buffer before setting up our IoT publish parameters.
Receiving MQTT Messages
As mentioned before, our call to aws_iot_mqtt_subscribe()
during our initial setup subscribed us to the topic, gTopicNameRec
. One of the parameters to that function, IoTSubscribeCallbackHandler
, provided the handler to the function that will be used to process incoming messages.
void IoTSubscribeCallbackHandler(AWS_IoT_Client *pClient, char *topicName, uint16_t topicNameLen, IoT_Publish_Message_Params *params, void *pData) { iprintf("Received Message:\r\n"); ParsedJsonDataSet jsonIn = ParsedJsonDataSet((char *) params->payload, (int) params->payloadLen); jsonIn.PrintObject(true); iprintf("\r\n"); jsonIn.PrintObjectToBuffer( recJson, 256, true); }
We know what the signature of the function should be because Amazon is kind enough to give us a pretty good breakdown in their API documentation. The function itself is very short, as we simply take the payload data from the MQTT message, and use it to construct a ParsedJsonDataSet
object. We then stash the contents of that object into a buffer so that we can serve it up on the webpage and show what the last message received was.
The Web Interface
We’ve taken advantage of NetBurner’s ability to generate dynamic web content to display the most recently received and published MQTT messages. To do this, we defined two functions in html-control.cpp
that will print the data to the webpage before it served to the requesting browser from the device. These are PrintOutData()
and PrintInData()
.
Specially formatted comments in index.html trigger these functions, and they have the structure, . This makes it very easy to display any sort of application-level data through the web interface. We have extensive documentation on this system and include several fully functional examples with our NNDK.
Implementing the AWS SDK Dependencies
Of course, there has to be a way for the AWS SDK to tie into the NetBurner platforms and networking stack. There are two interfaces provided by the SDK that we use here: the network interface and the timer interface. As mentioned above, the functions for these interfaces are defined in network_netburner.cpp
and timer_netburner.cpp
respectively.
The network interface provides all of the TLS functionality for initiating and closing secure connections, as well as reading and writing data over those connections. The timer interface provides the timeout functionality required to keep the MQTT connections alive and kicking.
The functions for both of these interfaces were required before we could connect to the AWS IoT Core service and start sending and receiving MQTT messages. If you followed the instructions in the previous article on generating and compiling the certificate and private key, there shouldn’t be any need to modify the functions implemented here. That said, feel free to dig in and let us know if you have any specific questions or comments.
Closing
We’ve provided a solid foundation here, but this is truly just the beginning of where this particular project can go. In the coming months, we will start to explore incorporating other AWS services into our IoT application.
Don’t wait for that before you jump into your own projects, though! We’d love to hear what you’re up to, and how you’re using Amazon’s AWS IoT Core service or our development kits. Feel free to email us directly at sales@netburner.com or leave a comment below.
Also, if you want to learn more about IoT architectures and better understand how IoT devices, protocols, and cloud services all work together, click on through to our article on just that!
Cover photo credit: Pawel Czerwinski