CC Blog Projects Research & Design Hub

Build a Custom App with AWS IoT

Part 1: Embedded IoT Device

Using the Amazon Web Service (AWS) IoT EduKit as a starting point, I will show you how to build a custom system to monitor the air quality of an electronics workspace. This is the first of a two-part article series.

  • How can I build a custom app using AWS IoT?
  • What can I do with the AWS IoT EduKit?
  • How could I create a custom air quality monitoring system?
  • Core2 ESP32 Development Kit
  • Espressif Systems ESP32-D0WDQ6-V3
  • CJMCU-811 sensor module
  • SHARP/Socle Technology GP2Y1010AU0F

In a previous article, “Build IoT Secure Apps with AWS Services” (Circuit Cellar issue #383, June 2022) [1], I introduced Amazon Web Service’s (AWS) IoT EduKit learning program, advertised by its creators as an easy and cost-effective way to learn how to build secure Internet-of-Things (IoT) applications using the AWS IoT Core service. The EduKit learning program uses as a reference hardware the Core2 ESP32 IoT Development Kit, developed and manufactured by M5Stack. It offers a software framework and sample code in the form of learning tutorials, along with some community-developed projects for additional reference.

The EduKit learning program aims to help developers easily get into building secure IoT applications. Part of the idea behind it is to streamline the process of writing, compiling and testing embedded IoT firmware, to help developers focus on creating real-world applications. Secure communications at the embedded hardware level is not overlooked within the framework. This could help facilitate its adoption for commercial, industrial, medical and other safety-critical applications.

In my previous article [1], I discussed the EduKit learning program’s workflow, the reference hardware specifications, cloud services, libraries, and development tools. I also made a walk-through of the first three examples presented in the program, explaining key ideas regarding the hardware, software, and cloud services. 

In the present two-part article series, I take the EduKit learning program’s “Smart Thermostat” example as a starting point for building a custom system to monitor the air quality of an electronics workspace, where soldering fumes typically pollute the air. The system will monitor the room air quality and control an air extractor to automatically clean the air when needed. The solution I give is somewhat trivial, because the project is not intended for real-world use. Its main aim is to showcase the interfacing of off-board sensors not supported directly by the Software Development Kit (SDK) and the implementation of custom hardware driver code. I will also discuss the required embedded firmware project configurations to include the new hardware and the build of a custom, serverless application to support the system.

To follow the topics discussed here, you must be familiar with the AWS EduKit platform. If that’s not the case, please first read my previous article [1], or check the first three examples on the EduKit learning program’s website [2].

In Part 1 of this article series, I discuss the interfacing of the off-board sensors selected for this project, the development of custom hardware drivers for them, the required project configurations to include the new hardware for a successful compilation, and a basic testing with the AWS IoT Core server. In Part 2, I will discuss the inclusion of actuators, particularly an air extractor, and the build of a custom, serverless application to support the system on the AWS IoT Core Cloud.


Advertise Here

The block diagram for the Smart Air Cleaner system is shown in Figure 1. The Core2 ESP32 IoT Development Kit (from now on, “Core2 device” for short) is the embedded controller used for the IoT device in this project. It is based on the Espressif Systems ESP32-D0WDQ6-V3 microcontroller (MCU), featuring dual Xtensa 32-bit LX6 cores that run at 240MHz. The Core2 device comes with a Microchip ATECC608B Trust&GO pre-provisioned, secure element (SE) integrated circuit (IC) that facilitates network authentication and the use of secure connections.

Figure 1
Block diagram for the Smart Air Cleaner system
Figure 1
Block diagram for the Smart Air Cleaner system

The proposed system will use a carbon dioxide (CO2) sensor and a particle-density sensor to monitor air quality. An air extractor will clean the air if it becomes too polluted. Sensor measurements will be sent to the AWS IoT Core server via the Message Queueing Telemetry Transport (MQTT) protocol. A serverless application in the same server will generate and send back commands to the Core2 device to control the air extractor.

Amazon’s AWS IoT Core platform uses the MQTT protocol to exchange data with IoT devices. MQTT is great for interconnecting remote devices with small code footprints (such as MCUs) and low network bandwidth. MQTT is designed around the publish/subscribe messaging model, where message types are defined as “topics.” In this model, some devices publish to topics to send data, and others subscribe to topics to receive data. Data exchanged between publishers and subscribers can be text, numbers, binary data, JSON strings, and other data. JSON data format is recommended for interacting with AWS IoT Core platform. 

The EduKit learning program showcases the use of the “AWS IoT Device SDK for Embedded C” libraries for writing firmware for the Core2 device. We will be using the same SDK here. With this SDK it is possible to write embedded applications that securely connect to the AWS IoT Core platform via authenticated TLS connections [3]. It also greatly simplifies access to the platform’s MQTT broker to publish and subscribe to topics. The SDK was built with resource-constrained devices in mind—typically MCUs—and facilitates the interaction with the Core2 device’s SE for easy security authentication. 

The AWS IoT Core platform has a feature called “device shadows” that’s used to exchange and synchronize data between MQTT clients. A device shadow is just a JSON document that the platform stores in the cloud and contains current state information of the IoT device (for instance, the Core2). This JSON document is published on a special MQTT topic, and contains “key:value” pairs that store the latest state of the mirrored IoT device. Any system that has access to the device shadow can obtain real-time status updates from the mirrored device. Other systems can even push their own key:value pairs to the device shadow, so the mirrored device can receive data from them as well. 

For this project we will use a device shadow to mirror the state of a number of key:value pairs in the Core2 device. Some of them will be “reported” state values—for instance, the readings obtained from the CO2 and particle-density sensors. Some of them will be “desired” state values, such as commands coming from “topic rules” and the “detector model” (serverless application) running on the AWS IoT cloud platform.

Topic rules are an AWS IoT Core feature that allows reported state variables to be received from an MQTT client (for example, the Core2 device), via its shadow device. It then generates new, desired state values using SQL queries with conditional logic. Those generated state values are then inserted back into the shadow device, so the MQTT client can automatically receive them when a synchronization action is performed. These new, desired state values could be, for example, commands to control actuators.

A detector model is another AWS IoT feature that helps easily implement a serverless application. A detector model is just a Finite State Machine (FSM) with conditional logic capable of receiving input from topic rules, compute state changes, and publish them back to the device shadow as desired states. The behavior of the detector model is defined by using JSON syntax.


Figure 2 is the circuit diagram for the Smart Air Cleaner system without the air extractor. The system is composed of a CJMCU-811 sensor module carrying an ams OSRAM CCS811 ultra-low-power, digital gas sensor. This sensor uses a metal oxide (MOX) gas detector to sense a wide range of Volatile Organic Compounds (VOCs), and it is recommended for indoor air quality monitoring. The sensor uses an I2C port to interface with an application MCU.


Advertise Here

Figure 2
Circuit diagram for the Smart Air Cleaner system
Figure 2
Circuit diagram for the Smart Air Cleaner system

The CCS811 implements internally intelligent algorithms that process raw measurements and output values for the Total VOCs (TVOCs) and the equivalent CO2 (eCO2) detected in the air. The computed eCO2 value fairly represents the real CO2 concentration, as long as the detected VOCs come from humans. This sensor also implements multiple optimized measurement modes for low power consumption, and an “idle” mode for extending battery life in portable applications. The CCS811 is connected to the Core2 device’s Port A, which exposes pins GPIO32 (SDA) and GPIO33 (SCL) from the ESP32 processor (see Figure 2).

The SHARP/Socle Technology GP2Y1010AU0F is a compact optical dust sensor capable of detecting smoke and other very fine particles. It is an analog-output sensor composed of an infrared (IR) LED-phototransistor pair, diagonally arranged in the device. The sensor detects reflected light from dust or smoke particles in the air, and provides a voltage output level that corresponds to the particle density in µg/m3.

The GP2Y1010AU0F sensor interfaces with an MCU via two pins: a digital LED input pin and an analog Vo output pin. Through the LED input pin, the sensor receives a short pulse that turns on the internal infrared LED. After 280µs, the Vo output pin provides an analog voltage proportional to the air particle density. To read this sensor, the Core2 device must provide first the short digital pulse, and then sample the analog output voltage with an analog-to-digital converter (ADC) input. Next, by applying a transfer function provided by the device’s datasheet, the corresponding µg/m3 particle density value can be calculated.

I connected this sensor to the Core2 device’s Port B, which exposes pins GPIO26 (DAC) and GPIO36 (ADC) from the ESP32 MCU. GPIO26 is used as a digital output to generate the required digital pulse, and pin GPIO36 is configured as an ADC input to sample the output voltage. The hardware prototype for the system is shown in Figure 3.

Figure 3
Hardware prototype for the Smart Air Cleaner system
Figure 3
Hardware prototype for the Smart Air Cleaner system

In Part 2 of this article series, Port C from the Core2 device, which exposes pins GPIO13 (RXD2) and GPIO14 (TXD2), will be used to connect the air extractor.


I took the Smart Thermostat project from the EduKit learning program as a starting point for this project [4]. Then, I added and modified source code and configurations for the custom sensors and actuators. Because I couldn’t find any suitable CCS811 and GP2Y1010AU0F driver libraries readily available for the platform, I had to port custom ones myself, using libraries from other platforms as references. The AWS IoT EduKit development environment is based on the FreeRTOS real-time operating system. Any driver/library must be thread-safe in that context, to avoid race conditions and other synchronization issues.

The driver library I ported for the CCS811 is very simple with no-frills. It implements the minimum requirements to get readings from the sensor. It is composed of a ‘ccs811.c’ file located inside the project’s “…\Core2-for-AWS-IoT-EduKit\Smart-Air-Cleaner\main” folder and a ‘ccs811.h’ file inside the “…\Core2-for-AWS-IoT-EduKit\Smart-Air-Cleaner\main\includes” folder.

To compile the drivers without errors, the project’s ‘CMakeLists.txt’ file, also located inside the “main” folder, must be edited to include the new library modules. In that file, the first configuration line must be edited to include the libraries as follows: set(COMPONENT_SRCS “main.c” “ui.c” “fft.c” “wifi.c” “ccs811.c” “gp2y1010.c”). The last two parameters are the libraries for the two sensors in the system.

Source code from the CCS811 driver library is given in Listing 1. Lines 14-51 define the ccs811_Init() function that initializes the sensor. With line 21, a handle to the I2C communications port is created specifying the I2C address and baud rate. With line 24, a “software reset” is performed in the sensor by writing a reset code to the corresponding register in the device. Line 33 switches the sensor to “application mode” by writing to it a specific code. According to the datasheet, a destination register is not required for this transaction; hence, the ‘I2C_NO_REG’ value as the second argument. Line 43 sets the sensor’s default measurement mode as “mode 1s” (constant power mode, measurements every 1 second).

Here are lines 14 to 51 of the source code for the ams OSRAM CCS811 ultra-low-power, digital gas sensor’s driver library. (The complete listing is available on the Circuit Cellar article materials webpage.)

14 void ccs811_Init(void)
15 {
16     const static uint32_t sw_reset = 0x8a72e511; // Software reset code
17     static uint8_t app_start = CCS811_REG_APP_START; // App start register
18     static uint8_t mode_1s = CCS811_MODE_1S; // Mode 1S code
19     esp_err_t err; // ESP error type
21     my_port_A_peripheral = Core2ForAWS_Port_A_I2C_Begin(CCS811_I2C_ADDRESS_1, PORT_A_I2C_STANDARD_BAUD);
23     // Perform a software reset
24     err = Core2ForAWS_Port_A_I2C_Write(my_port_A_peripheral, CCS811_REG_SW_RESET, &sw_reset, 1);
25     if(!err){
26         ESP_LOGI(TAG, “-------------->> sw_reset OK”);    
27     } else {
28         ESP_LOGI(TAG, “-------------->> sw_reset Error”); 
29     }
30     vTaskDelay(100); // Wait 100 ms after the reset
32     // Switch to sensor’s application mode
33     err = Core2ForAWS_Port_A_I2C_Write(my_port_A_peripheral, I2C_NO_REG, &app_start, 1);
34     if(!err){
35         ESP_LOGI(TAG, “-------------->> app_start OK”);    
36     } else {
37         ESP_LOGI(TAG, “-------------->> app_start”); 
38     }
40     vTaskDelay(100); // Wait 100 ms after starting the app
42     // Set default measurement mode to “1s”
43     err = Core2ForAWS_Port_A_I2C_Write(my_port_A_peripheral, CCS811_REG_MEAS_MODE, &mode_1s, 1);
44     if(!err){
45         ESP_LOGI(TAG, “-------------->> CCS811_MODE_1S OK”);    
46     } else {
47         ESP_LOGI(TAG, “-------------->> CCS811_MODE_1S Error”); 
48     }
50     vTaskDelay(100); // Wait 100 ms after setting the meas. mode
51 }

Source code for the SHARP/Socle Technology GP2Y1010AU0F compact optical dust sensor's driver library

10 void gp2y_Init(void)
11 {
12     // Initialize GPIO pins for GP2Y1010AU0F particle sensor
13     Core2ForAWS_Port_PinMode(PORT_B_ADC_PIN, ADC); // ADC input
14     Core2ForAWS_Port_PinMode(GPIO_NUM_26, OUTPUT); // IR LED output
15 }
17 void gp2y_Read(float* part_con_val)
18 {
19     static double particle_concentration;
20     static int adc_reading_mv;
22     particle_concentration = 0;
23     adc_reading_mv = 0;
25     for (int x = 0; x < NUM_ADC_READS; x++)
26     {
27         // GP2Y1010AU0F reading sequence
28         Core2ForAWS_Port_Write(GPIO_NUM_26, false); // Turn on IR LED
29         ets_delay_us(280);
31         // Read ADC input
32         adc_reading_mv = Core2ForAWS_Port_B_ADC_ReadMilliVolts();
33         // ESP_LOGI(TAG, “>>>>>>>>>>> adc_reading_mv : %d”, adc_reading_mv); // Just for debugging
35         // Sum readings to calculate an average (convert mV to V)
36         particle_concentration = particle_concentration + adc_reading_mv/1000.0;
38         ets_delay_us(40); // Wait 40 us
39         Core2ForAWS_Port_Write(GPIO_NUM_26, true); // Turn off IR LED
40         ets_delay_us(9680); // Wait 9680 us (GP2Y1010AU0F’s reading period is >= 10 ms)
41     }
43     particle_concentration = particle_concentration/NUM_ADC_READS; // Compute average
44     // GP2Y1010AU0F runs with 5V. We are using a voltage divider to downscale the reading
45     // to an equivalent voltage between 0V-3.3V
46     float rdiv_ratio = 3.3 / (3.3 + 1.6); // Voltage divider resistor ratio
48     particle_concentration = particle_concentration/rdiv_ratio; // Upscale reading to a 0V-5V range
49     // ESP_LOGI(TAG, “>>>>>>>>>>> GP2Y10: Voltage [V]: %.2f”, particle_concentration); 
// Just for debugging
51     // Linear equation: to convert read voltage to ug/m^3
52     // Chris Nafis (c) 2012
53     particle_concentration = 170 * particle_concentration - 0.1;
55     // The equation is not adjusted for this particular sensor, so sometimes
56     // negative values of particle concentration are seen when it is near zero:
57     if(particle_concentration < 0) {
58         particle_concentration = 0;
59     }
61     *part_con_val = particle_concentration;
62     // ESP_LOGI(TAG, “>>>>>>>>>>> particleCon: %.2f”, particleCon); // Just for debugging
63 }

For the following code line references, please refer to the complete listing available on the Circuit Cellar article materials webpage. Lines 54-81 define the ccs811_ReadData() function that reads the eCO2 and TVOC values from the sensor. Lines 60-68 can be used to troubleshoot the I2C communications with the sensor, in case the obtained values appear to be incorrect. This code block reads the hardware ID from the sensor and prints it to the terminal window in hexadecimal format. According to the datasheet, the sensor’s ID is ‘0x81.’ Line 71 reads four bytes from the sensor. The first two bytes must be combined to get the eCO2 value, and the last two bytes to get the TVOC value (not used in this project). Lines 79-80 combine the corresponding bytes into 16-bit numbers by using bit-shift and “OR” logic operations.

Reading the GP2Y1010AU0F sensor is also straightforward. The basic procedure is to generate a digital pulse, sample an analog voltage, and apply a transfer function equation. Listing 2 shows the driver code for this sensor. Lines 10-15 define the gp2y_Init() function that initializes the required ADC input and digital output. Lines 17-63 define the gp2y_Read() function that samples the sensor’s analog voltage output and computes the particle concentration value. The ‘for’ loop in lines 25-41 samples the analog voltage ‘NUM_ADC_READS’ times, and computes an average. This helps to filter some high-frequency noise in the sensor data.

The suggested reading sequence in the datasheet is as follows: First, turn on the sensor’s internal IR LED. The IR LED circuit works with negative logic, hence the writing of a ‘false’ value in line 28. Wait 280µs (line 29), and then sample the sensor’s analog output voltage (lines 32-36). After waiting for 40µs (line 38), turn off the IR LED and wait for at least 9,680µs before repeating the reading procedure (lines 39-40). The datasheet recommends a sampling duty cycle of no less than 10ms. That’s the obtained period after adding the three delays.

The ESP32 ADC works with voltage inputs between 0V to 3.3V, but the sensor’s maximum output voltage is around 4V. To avoid overloading the ADC input, a voltage divider is implemented for the ADC input at GPIO36, with two resistors of 3.3kΩ and 1.6kΩ (see Figure 2). Line 46 computes the resistor divider ratio, and line 48 divides the averaged voltage by this ratio to obtain the voltage value in the sensor’s original output scale. I hard-coded the ratio calculation here for practicality, because my goal was to obtain a working driver as soon as possible. Line 53 applies the transfer function (a linear equation) to the sampled voltage to obtain the particle density value.


Listing 3 contains the most relevant source code I added in the project’s ‘main.c’ file for the custom sensors and actuators. For simplicity, I excluded the rest of the code from the original Smart Thermostat project, but full source code and schematics are available on the Circuit Cellar article material webpage. Please download the complete listing to check all code line references mentioned.

Source code added in the project’s ‘main.c’ file for the custom sensors and actuators 

31 void aws_iot_task(void *param) {
32     static double particle_concentration = 0;
33     static int adc_reading_mv = 0;
34     static uint16_t eco2 = 0;
35     static uint16_t  etvoc = 0;
36     // ...
38     // My custom sensor and actuators structs
39     jsonStruct_t eco2Handler;
40     eco2Handler.cb = NULL;
41     eco2Handler.pKey = “roomCo2”;
42     eco2Handler.pData = &roomCo2;
43     eco2Handler.type = SHADOW_JSON_UINT16;
44     eco2Handler.dataLength = sizeof(uint16_t);
46     // ...
48     jsonStruct_t airExhaustActuator;
49     airExhaustActuator.cb = airExhaust_Callback;
50     airExhaustActuator.pKey = “airExhaust”;
51     airExhaustActuator.pData = &airExhaust;
52     airExhaustActuator.type = SHADOW_JSON_BOOL;
53     airExhaustActuator.dataLength = sizeof(bool);
55     // ...
57     // register delta callback for airExhaust
58     rc = aws_iot_shadow_register_delta(&iotCoreClient, &airExhaustActuator);
59     if(SUCCESS != rc) {
60         ESP_LOGE(TAG, “Shadow Register Delta Error”);
61     }
61     }
63     // ...
65     // loop and publish changes
67         // ...
68     }

These are the details of aws_iot_task() infinite while loop from Listing 3. (The complete listing is available on the Circuit Cellar article materials webpage.)

2     rc = aws_iot_shadow_yield(&iotCoreClient, 200);
3     if(NETWORK_ATTEMPTING_RECONNECT == rc || shadowUpdateInProgress) {
4         rc = aws_iot_shadow_yield(&iotCoreClient, 1000);
5         // If the client is attempting to reconnect, or already waiting on a shadow update,
6         // we will skip the rest of the loop.
7         continue;
8     }
10     // START get sensor readings
11     // ...
13     // Read the CO2 sensor
14     ccs811_ReadData(&eco2, &etvoc); // read the data from sensor;
15     roomCo2 = eco2;
17     // Read the particle density sensor
18     gp2y_Read(&partcon);
19     particleCon = partcon;
21     // END get sensor readings
23     ESP_LOGI(TAG, “*********************************************************************”);
24     ESP_LOGI(TAG, “On Device: roomOccupancy %s”, roomOccupancy ? “true” : “false”);
25     ESP_LOGI(TAG, “On Device: hvacStatus %s”, hvacStatus);
26     ESP_LOGI(TAG, “On Device: temperature %f”, temperature);
27     ESP_LOGI(TAG, “On Device: sound %d”, reportedSound);
28     ESP_LOGI(TAG, “On Device: roomCo2 %d”, roomCo2);
29     ESP_LOGI(TAG, “On Device: particleCon %f”, particleCon);
30     ESP_LOGI(TAG, “On Device: airBlow %s”, airBlow ? “true” : “false”);
31     ESP_LOGI(TAG, “On Device: airExhaust %s”, airExhaust ? “true” : “false”);
33     rc = aws_iot_shadow_init_json_document(JsonDocumentBuffer, sizeOfJsonDocumentBuffer);
34     if(SUCCESS == rc) {
35         rc = aws_iot_shadow_add_reported(JsonDocumentBuffer, sizeOfJsonDocumentBuffer, 8, &temperatureHandler,
36             &soundHandler, &roomOccupancyActuator, &hvacStatusActuator, 
37             &eco2Handler, &particleConHandler, &airExhaustActuator, &airBlowActuator);

Lines 31-82 in this listing define the aws_iot_task FreeRTOS task function that handles all sensor readings and communications with the AWS IoT Core server. To include the custom sensors and actuators, structs of type ‘jsonStruct_t’ must be defined for each one of them. For instance, lines 39-53 define those structs for the CO2 sensor and the air extractor. The first line in each block defines a handler name for the device. The second line defines a callback function that will execute when a value change in the handler’s data member (‘eco2Handler.pData’) is detected—for example, after receiving an update from the shadow device on the AWS IoT Cloud. We don’t need a callback function for the CO2 sensor, because that data is being generated locally (see line 40). For the air extractor we do need a callback function that will execute commands received from the device shadow (see line 49). Those commands will arrive in its handler’s data member (‘airExhaustActuator.pData’)


Advertise Here

The third line in each code block (lines 41, 50) define key names for the key:value pairs in the shadow device containing state data for this device. Lines 42 and 51 set the local variables used to store values for the key:value pairs. For these handlers, those variables are defined in lines 26 and 27. ‘roomCo2’ is the integer variable that will store the CO2 readings in parts-per-million (PPM) from the CCS811 sensor. ‘airExhaust’ is the Boolean variable that will store “true/false” commands received from the AWS IoT serverless application, to activate/deactivate the air extractor. Lines 43, 44, 52 and 53 set the type and size for those variables. 

Next, for each actuator, you should register the corresponding callback function that will execute when a delta (a difference) in an incoming value is detected. Lines 58-61 register the air extractor callback function, and lines 7-14 define the callback function in question. For now, in this callback we are just printing the received command or state to the terminal window. 

For the particle sensor, a similar set of steps is repeated. I omitted those lines in the listing for simplicity. Open the ‘main.c’ file in the Smart-Air-Cleaner project folder to see the complete source code (on the Circuit Cellar Article Materials webpage).

Lines 66-68 show the place where the infinite while loop for this task is defined. Lines 70-79 show code for printing meaningful debugging messages, in case the infinite loop breaks due to any irrecoverable error—such as any persistent network communications error, or if some data buffer exceeds its capacity.

The contents of the while infinite loop from Listing 3 (available on the Circuit Cellar article materials webpage) is shown in detail in Listing 4. Lines 1-8 ensure that the main body will execute as long as the Core2 device is still connected to the AWS IoT Core server, and there’s no shadow device update currently in progress. If there’s aupdate in progress, the while loop will be skipped. If the device fails to reconnect to the server, the loop will be terminated. Line 14 reads the CO2 and TVOC values from the CCS811 sensor. Line 18 reads the particle concentration value from the GP2Y1010AU0F sensor. Lines 23-31 print the current sensors’ values and current actuator states to the terminal window. I’m keeping the Smart Thermostat’s original sensors and actuators in the project for a comparative reference with the implementation of the new ones. 

Line 33 initializes the JSON document that will contain the key:value pairs to be sent to the AWS IoT Core broker. Those values will then be stored in the corresponding device shadow. Particularly, in lines 35-37 we choose which values we want to send to the cloud. The third argument inside the function contains the number of values we are sending, and the rest of the arguments, from the fourth onwards, are the handles of the values themselves.


To test the system so far, we follow the same procedure outlined in the EduKit tutorials for testing the Smart-Thermostat project.

1. Log into your AWS account.

2. Navigate to the AWS IoT console.

3. Go to the “Test” section in the navigation pane and click the option “MQTT test client” (see Figure 4).

4. Confirm that the “Subscribe to a topic” tab is active in the “MQTT test client” window.

5. Enter the following topic filter: 


Replace <<CLIENT_ID>> with your device ID and choose “Subscribe.” After a few seconds you should see in the lower part of the window new messages arriving in JSON format (see Figure 5). 

6. Go to the “Publish to a topic” tab (see Figure 6).

7. Enter the following topic filter: 


Replace <<CLIENT_ID>> with your device ID.

8. Replace the “Message payload” window with the following JSON string:

{ “state”: { “desired”: { “hvacStatus”: “HEATING”, “roomOccupancy”: true, “airExhaust”: true } } }

Click on “Publish.” The Core2 device’s LED bars should go red for “HEATING,” blue for “COOLING,” and off for “STANDBY” values in “hvacStatus”. In the VS Code terminal window, you will also see the JSON string you just sent as part of a larger device shadow JSON string (see Figure 7). Please don’t mind the weird line endings in Figure 7, it’s just a configuration issue in my VS Code installation.

9. Change the JSON string published before to an “airExhaust“ value of “false,” and publish again. Verify that change appears in the terminal window after a few seconds. Change the other values as well, if you want.

Figure 4
MQTT test client—subscribe to topic
Figure 4
MQTT test client—subscribe to topic
Figure 6
MQTT test client—publish to topic
Figure 6
MQTT test client—publish to topic
Figure 7
VS Code terminal window update shadow
Figure 7
VS Code terminal window update shadow

One of the first challenges I faced with the implementation of my custom sensors was the lack of readily available drivers for them in the AWS IoT EduKit environment. Because this platform is relatively new, few driver libraries are available for sensors and actuators. Chances are you won’t find drivers available for a particular set of sensors and actuators. So, you will probably have to write or port your own. For people with little experience on embedded systems firmware, sure enough, that’s a downer—especially if the platform aims at helping people with little such experience. 

There’s an “EduKit Content Library” [5] webpage with some projects from the AWS team, and some others contributed by community developers. The last time I checked, there were only around 24 projects available. Nevertheless, they can provide an additional reference if you are interested.

In Part 2 of this article series, I will explain how to set up the cloud solution that completes the rest of the system. In particular, I will discuss the configuration of topic rules and a detector model suitable for this application. I will also elaborate further the use of the air extractor and the logic involved in controlling it. 

[1] Raul Alvarez Torrico, “Build IoT Secure Apps with AWS Services,” Circuit Cellar 383, June, 2022.
[2] AWS IoT EduKit workshop,
[3] AWS IoT Device SDK for Embedded C
[4] AWS IoT EduKit, Smart Thermostat
[5] AWS IoT EduKit Content Library

AWS IoT Core Developer Guide, “What is AWS IoT?”
Message Queueing Telemetry Transport (MQTT) home page
Sparkfun Tutorials, “Introduction to MQTT”
Core2 for AWS IoT EduKit BSP

M5Stack Core2 ESP32 IoT Development Kit for AWS IoT EduKit
CJMCU-811 CCS811 CO2 Gas Sensor Module
GP2Y1010AU0F Optical Dust Sensor
Visual Studio Code
PlatformIO IDE for VSCode Installation, Download Page
AWS Command Line Interface

Code and Supporting Files


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
| + posts

Raul Alvarez Torrico has a B.E. in electronics engineering and is the founder of TecBolivia, a company offering services in physical computing and educational robotics in Bolivia. In his spare time, he likes to experiment with wireless sensor networks, robotics and artificial intelligence. He also publishes articles and video tutorials about embedded systems and programming in his native language (Spanish), at his company’s web site You may contact him at

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2023 KCK Media Corp.

Build a Custom App with AWS IoT

by Raul Alvarez Torrico time to read: 20 min