Objective
The objective was to build a simple end-to-end IoT pipeline:
DHT11 Sensor → ESP32 → Wi-Fi → MQTT/TLS → AWS IoT Core → MQTT Test Client
The ESP32 reads:
Temperature
Humidity
and publishes the values as JSON to AWS IoT Core.
I also wanted to verify the reverse communication path by publishing a message from AWS IoT Core and receiving it on the ESP32.
Prerequisites
AWS
- AWS account
- Access to AWS IoT Core
- Permission to create:
- Things
- Certificates
- IoT policies
AWS’s own IoT Core getting-started flow starts with an AWS account and then moves through creating IoT resources, configuring the device, and testing MQTT messages.
Hardware
- ESP32 development board
- DHT11 sensor
- Breadboard
- Jumper wires
Software
- Arduino IDE
- ESP32 board package
DHTespPubSubClientArduinoJson
For simulation
I used Wokwi first, so I could validate the application flow before moving to physical hardware, and then you can try with actual devices.
Step 1 — Connect the DHT11 to ESP32
For setup,
| DHT11 | ESP32 |
|---|---|
| VCC | 3V3 |
| DATA | D15 |
| GND | GND |
The DHT11 should also not be polled too frequently. I kept the reading interval at around 2 seconds or more.
Step 2 — Open AWS IoT Core
Go to:
AWS Console → IoT Core
AWS IoT Core represents connected devices using Thing objects in the IoT registry. A Thing can represent a physical device or sensor.
Before connecting the ESP32, I created the AWS-side resources required for the device.
The important pieces are:
Thing + Certificate + IoT Policy
The AWS documentation describes the same relationship:
- Thing → represents the device
- X.509 certificate → authenticates the device
- IoT policy → authorizes what the device can do
Step 3 — Create the IoT Policy
Go to:
AWS IoT Core → Security → Policies → Create policy
For this POC, I used a simple policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:Connect",
"iot:Publish",
"iot:Subscribe",
"iot:Receive"
],
"Resource": "*"
}
]
}
For example:
iot:Connect→ connect to AWS IoT Coreiot:Publish→ publish MQTT messagesiot:Subscribe→ subscribe to MQTT topicsiot:Receive→ receive messages
AWS documents that the certificate authenticates the device, while the policy attached to the certificate determines which IoT operations the device is permitted to perform.
Important
The Resource: "*" approach is convenient for a POC, but it should not be treated as a production policy.
AWS also recommends restricting resources instead of using the wildcard when stronger security is required.
Step 4 — Create the AWS IoT Thing
Go to:
AWS IoT Core → All devices → Things → Create things
Then:
- Choose Create single thing
- Click Next
- Enter a Thing name
- Keep the remaining Thing properties as required for this POC
- Choose Next
For my setup, I used a Thing name that was also used as the MQTT client ID.
AWS recommends choosing Thing names carefully because the name cannot simply be renamed later; a new Thing must be created if the name needs to change.
Step 5 — Generate the Device Certificate
During Thing creation:
Configure device certificate → Auto-generate a new certificate
Then attach the IoT policy created earlier.
The final relationship becomes:
ESP32 → Device Certificate → Thing
and
Device Certificate → IoT Policy
AWS specifically notes that attaching the certificate to a Thing establishes the device-to-Thing relationship, while attaching the policy authorizes actions such as connecting and publishing.
Step 6 — Download the Certificates
This is one of the most important steps.
Download and securely save:
1. Device Certificate
device.pem.crt
2. Private Key
private.pem.key
3. Amazon Root CA
Amazon-root-CA-1.pem
The AWS Developer Guide identifies these files as the certificate and key files required for device configuration.
Important
Save the files before leaving the certificate download screen.
AWS specifically warns that the certificate files generated during this process are not available again from that page after leaving it.
The private key is especially important.
Do not:
Commit it to Git
Push it to GitHub
Share it in a community post
Put it directly into publicly accessible source code
For my POC, I kept the certificates in a separate secrets.h file.
Step 7 — Get the AWS IoT Endpoint
Go to:
AWS IoT Core → Settings → Device data endpoint
You will get an endpoint similar to:
xxxx-ats.iot.<region>.amazonaws.com
This endpoint is used by the ESP32 to connect to AWS IoT Core over MQTT/TLS.
AWS’s troubleshooting guidance also recommends checking that the device is using the correct device data endpoint when diagnosing connectivity problems.
Step 8 — Add Certificates to secrets.h
I kept the sensitive configuration separate from the main Arduino code.
Example:
#include <pgmspace.h>
#define THINGNAME "your-thing-name"
const char WIFI_SSID[] = "Wokwi-GUEST";
const char WIFI_PASSWORD[] = "";
const char AWS_IOT_ENDPOINT[] =
"xxxx-ats.iot.<region>.amazonaws.com";
static const char AWS_CERT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...Amazon Root CA...
-----END CERTIFICATE-----
)EOF";
static const char AWS_CERT_CRT[] PROGMEM = R"KEY(
-----BEGIN CERTIFICATE-----
...device certificate...
-----END CERTIFICATE-----
)KEY";
static const char AWS_CERT_PRIVATE[] PROGMEM = R"KEY(
-----BEGIN RSA PRIVATE KEY-----
...private key...
-----END RSA PRIVATE KEY-----
)KEY";
For physical hardware, I changed:
Wokwi-GUEST
to actual Wi-Fi SSID and password.
Step 9 — Install Arduino Libraries
In Arduino IDE, install:
DHTesp
PubSubClient
ArduinoJson
The ESP32 code uses:
WiFi.h→ Wi-Fi connectivityWiFiClientSecure.h→ TLS connectionPubSubClient.h→ MQTTArduinoJson.h→ JSON payloadDHTesp.h→ DHT11 sensor reading
Step 10 — ESP32 Code
The basic flow in my code is:
Start ESP32
↓
Connect to Wi-Fi
↓
Synchronize time using NTP
↓
Load AWS certificates
↓
Connect to AWS IoT Core using MQTT
↓
Subscribe to MQTT topic
↓
Read DHT11
↓
Create JSON payload
↓
Publish to AWS IoT Core
↓
Listen for incoming messages
↓
Repeat
The key MQTT configuration was:
#define AWS_IOT_PUBLISH_TOPIC "iotwokwi/pub"
#define AWS_IOT_SUBSCRIBE_TOPIC "iotwokwi/sub"
The sensor data was published as JSON:
{
"humidity": "61.2",
"temperature": "27.45"
}
Step 12 — Connect to AWS IoT Core
The ESP32 connects to port:
8883
using:
- AWS IoT endpoint
- Root CA
- Device certificate
- Private key
Example:
net.setCACert(AWS_CERT_CA);
net.setCertificate(AWS_CERT_CRT);
net.setPrivateKey(AWS_CERT_PRIVATE);
client.setServer(AWS_IOT_ENDPOINT, 8883);
Then the MQTT client connects using the Thing Name as the client ID:
client.connect(THINGNAME);
AWS IoT Core uses the certificate presented by the device for authentication, and MQTT policies determine whether the device is authorized to perform operations such as publish and subscribe.
Step 13 — Publish Sensor Data
After reading the DHT22:
TempAndHumidity data = dhtSensor.getTempAndHumidity();
I created a JSON payload:
StaticJsonDocument<200> doc;
doc["humidity"] = humidity;
doc["temperature"] = temperature;
and published it:
client.publish(
AWS_IOT_PUBLISH_TOPIC,
jsonBuffer
);
The AWS IoT message broker then receives the MQTT message.
AWS describes the message broker as the service that allows devices and applications to publish and receive messages from one another.
Step 14 — Subscribe and Receive Messages
The ESP32 also subscribes to:
iotwokwi/sub
From the AWS IoT MQTT Test Client, I published:
{
"message": "hello device"
}
The ESP32 received the message through the MQTT callback and printed it in the Serial Monitor.
So the final communication was:
AWS IoT Core
/ \
/ \
ESP32 → Publish Subscribe ← ESP32
Sensor Data ↑
|
Command / Message
Step 15 — Test Using AWS MQTT Test Client
Go to:
AWS IoT Core → Test → MQTT test client
Test ESP32 → AWS
Subscribe to:
iotwokwi/pub
You should start seeing messages such as:
{
"humidity": "61.2",
"temperature": "27.45"
}
AWS also recommends using the MQTT test client to view MQTT messages as they pass through the message broker.
Test AWS → ESP32
Publish to:
iotwokwi/sub
Payload:
{
"message": "hello device"
}
The message should appear in the ESP32 Serial Monitor.
Step 16 — Verify in Serial Monitor
Open Arduino Serial Monitor at:
115200 baud
The expected flow is approximately:
[WiFi] Connecting...
[WiFi] Connected!
[Time] Syncing via NTP...
[Time] Synced!
[AWS] TLS certificates loaded
[AWS] Connecting to AWS IoT endpoint...
[AWS] IoT Connected!
[DHT] Temp: 27.45°C
[DHT] Humidity: 61.2%
[MQTT] Publishing payload...
[MQTT] Publish OK
The data should then appear in the AWS IoT MQTT Test Client.
Step 17 — Move from Wokwi to Physical Hardware
Once the Wokwi version worked, I moved to the physical ESP32.
The main changes were:
Wokwi
SSID = Wokwi-GUEST
Password = ""
Physical ESP32
SSID = <actual Wi-Fi>
Password = <actual password>
The AWS IoT configuration remained the same:
Thing → Certificate → Policy → Endpoint → MQTT Topics
Troubleshooting
client.state() = -2
Usually investigate TLS/network configuration:
- Certificate
- Private key
- Root CA
- Endpoint
- Device clock
- NTP synchronization
- TLS configuration
AWS’s connectivity troubleshooting flow starts by checking that the connection is valid, the certificate is valid and active, and the policy permits the required operation.
client.state() = 5
Check:
- Is the certificate active?
- Is the policy attached?
- Does the policy allow the requested action?
- Are the MQTT topic permissions correct?
AWS explicitly notes that a device connection can be refused even when the certificate is active if there is no appropriate policy attached.
client.state() = 5
Check:
- Is the certificate active?
- Is the policy attached?
- Does the policy allow the requested action?
- Are the MQTT topic permissions correct?
AWS explicitly notes that a device connection can be refused even when the certificate is active if there is no appropriate policy attached.