15 IoT Architecture Lab
Lab execution time can be estimated before starting runs:
\[ T_{\text{total}} = N_{\text{runs}} \times (t_{\text{setup}} + t_{\text{run}} + t_{\text{review}}) \]
Worked example: With 5 runs and per-run times of 4 min setup, 6 min execution, and 3 min review, total lab time is \(5\times(4+6+3)=65\) minutes. This prevents under-scoping and helps schedule complete experimental cycles.
Today the Sensor Squad is building something real!
Sammy the Sensor (DHT22) has the first job: “I measure temperature and humidity – that is the Perception Layer. I just read the physical world.”
Max the Microcontroller (ESP32) is the brains: “I take Sammy’s numbers and package them neatly into JSON format – that is the Network Layer. It is like putting a letter in an envelope with a proper address.”
Lila the LED gets excited: “And I am the Application Layer! When the temperature gets too high, I light up red to warn everyone. I do not care HOW Sammy measured the temperature – I just care about the number.”
Bella the Battery reminds everyone: “Notice how each of us has ONE job? That is layer separation. If we replace Sammy with a different sensor, Max and Lila do not need to change at all!”
15.1 Learning Objectives
By completing this hands-on lab, you will be able to:
- Construct a 3-Layer IoT Architecture: Build a working system that demonstrates Perception, Network, and Application layers
- Wire Sensors to Microcontrollers: Connect and program a DHT22 temperature/humidity sensor with ESP32
- Structure and Transmit IoT Data: Format sensor readings as JSON payloads ready for network transmission
- Implement Application-Layer Logic: Develop threshold-based alerts and a serial dashboard that visualizes data flow through layers
15.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
15.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 |
15.4 Circuit Diagram
15.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
15.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);
}15.7 Step-by-Step Instructions
15.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
15.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)
15.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.
15.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
15.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
15.8 Understanding the Layer Mapping
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 |
The Scenario: A beginner implements the lab but writes a single loop() function that reads sensors, formats JSON, checks thresholds, and controls the LED all in one 150-line block of code.
// BAD: Monolithic code mixing all layers
void loop() {
// Read sensor
float temp = dht.readTemperature();
float hum = dht.readHumidity();
// Format JSON (should be Layer 2)
String json = "{\"temp\":" + String(temp) + ",\"hum\":" + String(hum) + "}";
// Check threshold (should be Layer 3)
if (temp > 30) {
digitalWrite(LED_PIN, HIGH); // Control actuator (should be separate)
Serial.println(json); // Transmit (should be separate)
Serial.println("ALERT: High temperature!"); // Dashboard (should be separate)
} else {
digitalWrite(LED_PIN, LOW);
}
delay(3000);
}What Goes Wrong:
Cannot change data format without rewriting threshold logic: If you switch from JSON to CBOR binary format, you must modify the entire function. In a layered design, only
formatNetworkPayload()changes.Cannot add a new sensor without touching alert logic: Adding a CO2 sensor requires editing the 150-line monolithic function. Risk of breaking working temperature alerts while adding new features.
Cannot test layers independently: You cannot unit-test “does JSON formatting work?” without also running sensor hardware and threshold checks. In layered code,
formatNetworkPayload()takes aSensorDatastruct as input and can be tested with mock data.Duplication across devices: If you build 5 different IoT devices (weather station, greenhouse, aquarium, HVAC, factory), you copy-paste the monolithic code and modify each one. Bugs in threshold logic must be fixed 5 times.
The Layered Alternative (from the lab code):
void loop() {
// === LAYER 1: PERCEPTION ===
SensorData data = readPerceptionLayer(); // One function, one job
// === LAYER 2: NETWORK ===
String payload = formatNetworkPayload(data); // Format independent of sensors
transmitData(payload); // Transmission independent of format
// === LAYER 3: APPLICATION ===
AlertStatus alerts = processApplicationLayer(data); // Logic independent of format
executeActuators(alerts); // Actuators independent of logic
displayDashboard(data, alerts); // UI independent of all above
delay(3000);
}Why This Matters at Scale:
| Task | Monolithic Code | Layered Code | Time Savings |
|---|---|---|---|
| Change JSON to CBOR | Edit 150-line function, risk breaking thresholds | Edit only formatNetworkPayload() |
4 hours → 30 min |
| Add CO2 sensor | Insert code into monolithic block, test everything | Add CO2 to SensorData struct, extend perception function |
6 hours → 2 hours |
| Fix alert threshold bug | Edit monolithic code, retest sensors + network + UI | Edit only processApplicationLayer() |
3 hours → 45 min |
| Reuse code in new project | Copy-paste + heavily modify | Import sensor/network/app modules, configure thresholds | 20 hours → 4 hours |
| Unit test threshold logic | Cannot test in isolation | Pass mock SensorData, verify AlertStatus |
Impossible → 15 min |
How to Recognize This Mistake:
- Your
loop()function is >50 lines and does multiple unrelated things - You use comments like
// Read sensor// Check threshold// Send datain one function → each comment should be a separate function - Changing one feature (e.g., alert threshold) requires scrolling through code that handles sensors, network, and display
- You cannot answer “where is the JSON formatting code?” in 5 seconds
How to Fix It:
Step 1: Identify layer boundaries by asking “what is the PRIMARY job of this code block?”
| Code Block | Primary Job | Layer |
|---|---|---|
dht.readTemperature(), digitalRead(BUTTON) |
Sense physical world | Perception (L1) |
String json = "{...}", Serial.println(json) |
Format and transmit | Network (L2) |
if (temp > 30), AlertStatus struct |
Business logic | Application (L3) |
digitalWrite(LED), Serial.println("Dashboard...") |
Control output, UI | Application (L3) |
Step 2: Create one function per layer
// Perception Layer - only knows about hardware
SensorData readPerceptionLayer() {
SensorData data;
data.temperature = dht.readTemperature();
data.humidity = dht.readHumidity();
data.buttonPressed = digitalRead(BUTTON_PIN) == LOW;
return data; // Return structured data, no JSON/alerts/display here
}
// Network Layer - only knows about data formats
String formatNetworkPayload(SensorData data) {
// Formatting logic only, no threshold checks
return "{\"temp\":" + String(data.temperature) + "}";
}
// Application Layer - only knows about business rules
AlertStatus processApplicationLayer(SensorData data) {
AlertStatus alerts;
alerts.tempHighAlert = data.temperature > 30; // Threshold logic isolated
return alerts;
}Step 3: Wire layers together in loop() - each layer is one function call
Real-World Validation: Production IoT codebases (AWS IoT Device SDK, Arduino IoT Cloud, Google IoT Core client libraries) all follow this pattern. They provide separate modules/classes for: - Sensor drivers (Perception) - Protocol handlers (MQTT, CoAP - Network) - Application callbacks (Business logic - Application)
If professional IoT libraries separate layers, your code should too.
Lesson: The 7-layer reference model is not just for system architecture diagrams — it directly maps to how you structure code. One function per layer, one layer per function. Violating this principle creates “big ball of mud” code that’s impossible to maintain at scale.
15.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
15.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
15.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?
Common Pitfalls
Beginning hardware setup without reading all requirements first, then discovering a later requirement (specific library version, second sensor, output format) requires redoing work. Read the complete specification before touching hardware.
Embedding server IP addresses directly in firmware so device breaks silently when infrastructure changes. Store connection parameters in NVS (non-volatile storage) or configuration, not source code constants.
Writing lab firmware that hangs indefinitely when the MQTT broker is unreachable. Add 10-second connection timeouts and 3-attempt retry with exponential backoff to all network operations.
Declaring a lab non-functional without examining serial output. The serial monitor reveals initialization errors and connection failures that diagnose 90% of lab issues in under 60 seconds. Always open at 115200 baud before concluding hardware is faulty.
15.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
15.13 Knowledge Check
15.14 What’s Next
| If you want to… | Read this |
|---|---|
| Explore production IoT architecture patterns | Production Architecture Management |
| Learn edge computing concepts | Edge and Fog Computing |
| Study data storage options | IoT Data Storage |
| Review interview preparation topics | Architecture Interview Prep |
| Test your architecture knowledge | IoT Architecture Quiz |