%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
graph LR
subgraph "ESP32 Connections"
ESP32["ESP32<br/>Dev Board"]
end
subgraph "Perception Layer"
DHT22["DHT22<br/>Sensor"]
BTN["Push<br/>Button"]
end
subgraph "Application Layer Output"
LED["Alert<br/>LED"]
end
DHT22 -->|"Data: GPIO4<br/>VCC: 3.3V<br/>GND: GND"| ESP32
BTN -->|"GPIO5<br/>(with 10k pull-up)"| ESP32
ESP32 -->|"GPIO2<br/>(with 220 Ohm)"| LED
style ESP32 fill:#2C3E50,stroke:#16A085,color:#fff
style DHT22 fill:#16A085,stroke:#2C3E50,color:#fff
style BTN fill:#16A085,stroke:#2C3E50,color:#fff
style LED fill:#E67E22,stroke:#2C3E50,color:#fff
169 Lab: Build a Multi-Layer IoT Architecture Demo
169.1 Learning Objectives
By completing this hands-on lab, you will be able to:
- Implement the 3-Layer IoT Architecture: Build a working system that demonstrates Perception, Network, and Application layers
- Connect Sensors to Microcontrollers: Wire and program a DHT22 temperature/humidity sensor with ESP32
- Format and Transmit IoT Data: Structure sensor readings as JSON payloads ready for network transmission
- Create Application-Layer Logic: Implement threshold-based alerts and a serial dashboard that visualizes data flow through layers
169.2 Introduction
This lab provides hands-on experience building a working IoT system that demonstrates the reference model layers in code. You will create a system where:
- Layer 1 (Perception): DHT22 sensor collects temperature and humidity
- Layer 2 (Network): Data is formatted as JSON for transmission
- Layer 3 (Application): Threshold logic triggers LED alerts
Reading about IoT layers is one thing; building them is another. This lab shows you:
- How sensor data flows through layers
- Why layer separation makes code maintainable
- How real IoT systems structure their code
- The difference between perception, network, and application concerns
169.3 Components Required
| Component | Purpose | Quantity |
|---|---|---|
| ESP32 Dev Board | Microcontroller (all layers) | 1 |
| DHT22 Sensor | Temperature/humidity sensing (Perception Layer) | 1 |
| LED (any color) | Alert indicator (Application Layer) | 1 |
| Push Button | Manual trigger (Perception Layer) | 1 |
| 10k Ohm Resistor | Pull-up for DHT22 data line | 1 |
| 220 Ohm Resistor | Current limiting for LED | 1 |
| Breadboard | Circuit assembly | 1 |
| Jumper Wires | Connections | Several |
169.4 Circuit Diagram
169.5 Interactive Wokwi Simulator
Use the embedded simulator below to build and test the circuit. Click “Start Simulation” after entering the code.
- Add components: Click the blue “+” button to add DHT22, LED, button, and resistors
- Wire connections: Click on pins to create wires between components
- Upload code: Paste the code below into the editor panel
- Serial Monitor: Click the terminal icon to see output after starting simulation
- Save project: Create a free Wokwi account to save and share your work
169.6 Complete Code: 3-Layer IoT Architecture Demo
/*
* IoT 3-Layer Architecture Demo
* Demonstrates: Perception Layer -> Network Layer -> Application Layer
*
* Hardware: ESP32 + DHT22 + LED + Button
*
* This code shows how data flows through the IoT reference model:
* - PERCEPTION LAYER: Sensors collect raw environmental data
* - NETWORK LAYER: Data is formatted as JSON for transmission
* - APPLICATION LAYER: Business logic triggers alerts based on thresholds
*/
#include "DHT.h"
// ============================================
// PERCEPTION LAYER - Hardware Pin Definitions
// ============================================
#define DHT_PIN 4 // DHT22 data pin
#define DHT_TYPE DHT22 // Sensor type
#define BUTTON_PIN 5 // Manual trigger button
#define LED_PIN 2 // Alert LED (built-in on many ESP32 boards)
DHT dht(DHT_PIN, DHT_TYPE);
// ============================================
// APPLICATION LAYER - Threshold Configuration
// ============================================
const float TEMP_HIGH_THRESHOLD = 30.0; // Celsius - trigger alert above this
const float TEMP_LOW_THRESHOLD = 15.0; // Celsius - trigger alert below this
const float HUMIDITY_HIGH_THRESHOLD = 80.0; // % - trigger alert above this
// ============================================
// NETWORK LAYER - Data Structures
// ============================================
struct SensorData {
float temperature;
float humidity;
bool buttonPressed;
unsigned long timestamp;
bool isValid;
};
struct AlertStatus {
bool tempHighAlert;
bool tempLowAlert;
bool humidityHighAlert;
bool manualTrigger;
};
// ============================================
// LAYER 1: PERCEPTION LAYER
// Collects raw data from physical sensors
// ============================================
SensorData readPerceptionLayer() {
SensorData data;
// Read environmental sensors
data.temperature = dht.readTemperature();
data.humidity = dht.readHumidity();
// Read button state (inverted - LOW when pressed with pull-up)
data.buttonPressed = (digitalRead(BUTTON_PIN) == LOW);
// Timestamp for data freshness
data.timestamp = millis();
// Validate sensor readings
data.isValid = !isnan(data.temperature) && !isnan(data.humidity);
return data;
}
// ============================================
// LAYER 2: NETWORK LAYER
// Formats data for transmission (JSON)
// In a full system, this would transmit via MQTT/CoAP/HTTP
// ============================================
String formatNetworkPayload(SensorData data) {
String json = "{";
json += "\"layer\": \"network\",";
json += "\"device_id\": \"esp32-demo-001\",";
json += "\"timestamp\": " + String(data.timestamp) + ",";
json += "\"payload\": {";
if (data.isValid) {
json += "\"temperature\": " + String(data.temperature, 2) + ",";
json += "\"humidity\": " + String(data.humidity, 2) + ",";
json += "\"unit_temp\": \"celsius\",";
json += "\"unit_humidity\": \"percent\"";
} else {
json += "\"error\": \"sensor_read_failed\"";
}
json += "},";
json += "\"button_state\": " + String(data.buttonPressed ? "true" : "false");
json += "}";
return json;
}
// Simulate network transmission (in real system: MQTT publish, HTTP POST, etc.)
void transmitData(String payload) {
Serial.println("\n--- NETWORK LAYER: Transmitting ---");
Serial.println(payload);
// In a real IoT system, you would:
// - MQTT: client.publish("sensors/temp", payload);
// - CoAP: coap.put("/sensors", payload);
// - HTTP: http.POST(serverUrl, payload);
}
// ============================================
// LAYER 3: APPLICATION LAYER
// Business logic, alerts, and user interface
// ============================================
AlertStatus processApplicationLayer(SensorData data) {
AlertStatus alerts;
alerts.tempHighAlert = data.isValid && (data.temperature > TEMP_HIGH_THRESHOLD);
alerts.tempLowAlert = data.isValid && (data.temperature < TEMP_LOW_THRESHOLD);
alerts.humidityHighAlert = data.isValid && (data.humidity > HUMIDITY_HIGH_THRESHOLD);
alerts.manualTrigger = data.buttonPressed;
return alerts;
}
// Control actuators based on application decisions
void executeActuators(AlertStatus alerts) {
bool shouldAlert = alerts.tempHighAlert || alerts.tempLowAlert ||
alerts.humidityHighAlert || alerts.manualTrigger;
digitalWrite(LED_PIN, shouldAlert ? HIGH : LOW);
}
// Display dashboard on Serial Monitor
void displayDashboard(SensorData data, AlertStatus alerts) {
Serial.println("\n========================================");
Serial.println(" IoT 3-LAYER ARCHITECTURE DASHBOARD ");
Serial.println("========================================");
// Perception Layer Status
Serial.println(" LAYER 1: PERCEPTION (Sensors)");
Serial.print(" Temperature: ");
if (data.isValid) {
Serial.print(data.temperature, 1);
Serial.println(" C");
Serial.print(" Humidity: ");
Serial.print(data.humidity, 1);
Serial.println(" %");
} else {
Serial.println("SENSOR ERROR");
}
Serial.print(" Button: ");
Serial.println(data.buttonPressed ? "PRESSED" : "Released");
// Network Layer Status
Serial.println("----------------------------------------");
Serial.println(" LAYER 2: NETWORK (JSON Payload Ready)");
Serial.print(" Status: ");
Serial.println(data.isValid ? "Payload formatted, ready to transmit" :
"Error - invalid sensor data");
// Application Layer Status
Serial.println("----------------------------------------");
Serial.println(" LAYER 3: APPLICATION (Business Logic)");
Serial.print(" Temp Alert: ");
if (alerts.tempHighAlert) Serial.println("HIGH TEMPERATURE WARNING!");
else if (alerts.tempLowAlert) Serial.println("LOW TEMPERATURE WARNING!");
else Serial.println("Normal");
Serial.print(" Humidity: ");
Serial.println(alerts.humidityHighAlert ? "HIGH HUMIDITY WARNING!" : "Normal");
Serial.print(" LED Status: ");
bool ledOn = alerts.tempHighAlert || alerts.tempLowAlert ||
alerts.humidityHighAlert || alerts.manualTrigger;
Serial.println(ledOn ? "ON (Alert Active)" : "OFF (All Normal)");
Serial.println("========================================");
}
// ============================================
// MAIN SETUP AND LOOP
// ============================================
void setup() {
Serial.begin(115200);
// Initialize Perception Layer hardware
dht.begin();
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Initialize Application Layer hardware
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.println("\n========================================");
Serial.println(" IoT 3-Layer Architecture Demo");
Serial.println(" ESP32 + DHT22 + LED + Button");
Serial.println("========================================");
Serial.println("Thresholds:");
Serial.print(" Temp High: "); Serial.print(TEMP_HIGH_THRESHOLD); Serial.println(" C");
Serial.print(" Temp Low: "); Serial.print(TEMP_LOW_THRESHOLD); Serial.println(" C");
Serial.print(" Humidity: "); Serial.print(HUMIDITY_HIGH_THRESHOLD); Serial.println(" %");
Serial.println("========================================\n");
delay(2000); // Allow DHT22 to stabilize
}
void loop() {
// === PERCEPTION LAYER ===
// Collect raw sensor data from the physical world
SensorData sensorData = readPerceptionLayer();
// === NETWORK LAYER ===
// Format data as JSON payload for transmission
String networkPayload = formatNetworkPayload(sensorData);
transmitData(networkPayload);
// === APPLICATION LAYER ===
// Apply business logic and determine alerts
AlertStatus alerts = processApplicationLayer(sensorData);
// Execute actuator commands based on application decisions
executeActuators(alerts);
// Display human-readable dashboard
displayDashboard(sensorData, alerts);
// Wait before next reading cycle
delay(3000);
}169.7 Step-by-Step Instructions
169.7.1 Step 1: Set Up the Wokwi Project
- Open the Wokwi simulator above (or visit wokwi.com/projects/new/esp32)
- You’ll see an empty ESP32 project with a code editor
169.7.2 Step 2: Add Components
Click the blue “+” button and add these components:
- DHT22 - Search “DHT22” and place it on the breadboard
- LED - Add a red or green LED
- Push Button - Add a momentary push button
- Resistors - Add one 10k Ohm (for DHT22) and one 220 Ohm (for LED)
169.7.3 Step 3: Wire the Circuit
Make these connections (click pins to create wires):
| Component | Component Pin | ESP32 Pin |
|---|---|---|
| DHT22 | VCC | 3.3V |
| DHT22 | GND | GND |
| DHT22 | DATA | GPIO4 |
| Button | One side | GPIO5 |
| Button | Other side | GND |
| LED | Anode (+, longer leg) | GPIO2 (through 220 Ohm) |
| LED | Cathode (-, shorter leg) | GND |
In Wokwi, the button has an internal pull-up option. Right-click the button and check “Use internal pull-up” to simplify wiring.
169.7.4 Step 4: Upload and Test the Code
- Copy the complete code above into the Wokwi code editor
- Click the green “Start Simulation” button
- Open the Serial Monitor (terminal icon at bottom)
- Observe the dashboard updating every 3 seconds
169.7.5 Step 5: Experiment with the Layers
Test the Perception Layer:
- Click on the DHT22 sensor in Wokwi
- Adjust the temperature slider above 30C - watch the LED turn on!
- Adjust humidity above 80% - another alert condition
Test the Application Layer:
- Click the button - the LED should light up immediately
- Watch the Serial Monitor show the button state change
Observe the Network Layer:
- Look for the JSON payload in the Serial output
- Notice how raw sensor data is transformed into a structured format
169.8 Understanding the Layer Mapping
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
flowchart TB
subgraph perception["PERCEPTION LAYER (Physical)"]
direction LR
DHT["DHT22 Sensor<br/>readTemperature()<br/>readHumidity()"]
BTN["Button<br/>digitalRead()"]
STRUCT["SensorData struct<br/>Raw values + timestamp"]
end
subgraph network["NETWORK LAYER (Transport)"]
direction LR
JSON["formatNetworkPayload()<br/>Converts to JSON"]
TRANSMIT["transmitData()<br/>Serial output<br/>(simulates MQTT/CoAP)"]
end
subgraph application["APPLICATION LAYER (Business Logic)"]
direction LR
LOGIC["processApplicationLayer()<br/>Threshold checking"]
ALERT["AlertStatus struct<br/>Boolean flags"]
ACT["executeActuators()<br/>LED control"]
DASH["displayDashboard()<br/>Human interface"]
end
DHT --> STRUCT
BTN --> STRUCT
STRUCT --> JSON
JSON --> TRANSMIT
STRUCT --> LOGIC
LOGIC --> ALERT
ALERT --> ACT
ALERT --> DASH
style perception fill:#16A085,stroke:#2C3E50,color:#fff
style network fill:#E67E22,stroke:#2C3E50,color:#fff
style application fill:#2C3E50,stroke:#16A085,color:#fff
Code-to-Layer Mapping:
| Architecture Layer | Code Component | Function |
|---|---|---|
| Perception Layer | readPerceptionLayer() |
Reads DHT22 temperature/humidity, button state |
| Perception Layer | SensorData struct |
Holds raw sensor values with validation |
| Network Layer | formatNetworkPayload() |
Converts sensor data to JSON format |
| Network Layer | transmitData() |
Simulates MQTT/CoAP transmission |
| Application Layer | processApplicationLayer() |
Applies threshold-based business logic |
| Application Layer | executeActuators() |
Controls LED based on alert conditions |
| Application Layer | displayDashboard() |
Provides human-readable serial interface |
169.9 Challenge Exercises
Extend the Perception Layer by adding an LDR (Light Dependent Resistor):
- Add an LDR component in Wokwi connected to GPIO34 (analog input)
- Modify
SensorDatastruct to includefloat lightLevel - Add
analogRead(34)to the perception layer function - Add a light threshold (e.g., alert if light < 500)
- Update the JSON payload and dashboard to show light readings
Hint: Use map(analogRead(34), 0, 4095, 0, 100) to convert to percentage
Add edge/fog computing behavior by detecting state transitions:
- Create an
enum SystemState { NORMAL, WARNING, CRITICAL } - Only send data when state changes (reducing network traffic)
- Add hysteresis: require 3 consecutive high readings before alerting
- Implement different LED blink patterns for WARNING vs CRITICAL
This simulates how edge computing reduces bandwidth by filtering data locally
If you have physical hardware, extend the demo with actual network connectivity:
- Add Wi-Fi connection code (
WiFi.begin(ssid, password)) - Install PubSubClient library for MQTT
- Transmit JSON payloads to a free MQTT broker (e.g.,
test.mosquitto.org) - Use MQTT Explorer or Node-RED to visualize received data
This completes the transformation from simulation to real IoT system
169.10 Key Concepts Demonstrated
This lab demonstrates core IoT architecture principles:
Layer Separation: Each layer has distinct responsibilities - sensors don’t know about alerts, network formatting doesn’t know about thresholds
Data Transformation: Raw analog/digital readings become structured JSON, which becomes actionable alerts - value increases at each layer
Bidirectional Flow: Data flows UP from sensors to dashboard; Control flows DOWN from thresholds to LED actuator
Abstraction: The Application Layer works with
SensorDataandAlertStatusabstractions, not raw GPIO pins - making business logic portableScalability Pattern: The JSON format could transmit to cloud (Layers 4-7 in Cisco model); the threshold logic could run on edge gateway - same code structure scales to production
169.11 Knowledge Check
After completing this lab, you should be able to answer:
- Why is sensor reading code separated from threshold checking code?
- What would change if you switched from MQTT to CoAP protocol?
- How would you add data storage (Layer 4) to this system?
- Why does the button trigger work instantly while temperature alerts have a 3-second delay?
169.12 Summary
In this hands-on lab, you built a working 3-layer IoT system:
What You Built:
- ESP32-based IoT device with DHT22 sensor and LED actuator
- Layer 1 (Perception): Sensor reading and data collection
- Layer 2 (Network): JSON payload formatting for transmission
- Layer 3 (Application): Threshold-based alerting and dashboard
Key Takeaways:
- Reference model layers map directly to code structure
- Layer separation enables independent development and testing
- Data transformation adds value at each layer
- The same architecture scales from prototype to production
What’s Different in Production:
- Layer 2 would use actual MQTT/CoAP transmission
- Layer 4 (Storage) would persist data in databases
- Layer 5 (Abstraction) would provide REST APIs
- Layer 6-7 would include dashboards and business workflows
169.13 What’s Next
Continue your learning journey:
- Edge Computing Patterns - Layer 3 implementation strategies
- Data Storage and Databases - Layer 4 storage options
- IoT Protocols Overview - Layer 2 protocol selection
- Architectural Enablers - Technologies supporting the reference model layers