169  Lab: Build a Multi-Layer IoT Architecture Demo

~45 min | Intermediate | P04.C18.LAB

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:

  1. How sensor data flows through layers
  2. Why layer separation makes code maintainable
  3. How real IoT systems structure their code
  4. 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

%%{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

Figure 169.1: Lab circuit connections: DHT22 sensor and button feed the Perception Layer, ESP32 processes data through Network Layer formatting, LED provides Application Layer feedback

169.5 Interactive Wokwi Simulator

Use the embedded simulator below to build and test the circuit. Click “Start Simulation” after entering the code.

NoteSimulator Tips
  • 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

  1. Open the Wokwi simulator above (or visit wokwi.com/projects/new/esp32)
  2. 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:

  1. DHT22 - Search “DHT22” and place it on the breadboard
  2. LED - Add a red or green LED
  3. Push Button - Add a momentary push button
  4. 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
TipWokwi Tip

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

  1. Copy the complete code above into the Wokwi code editor
  2. Click the green “Start Simulation” button
  3. Open the Serial Monitor (terminal icon at bottom)
  4. 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

Figure 169.2: How code functions map to IoT architecture layers: sensor reads in Perception, JSON formatting in Network, threshold logic and alerts in Application

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

CautionChallenge 1: Add a Second Sensor (Light Level)

Extend the Perception Layer by adding an LDR (Light Dependent Resistor):

  1. Add an LDR component in Wokwi connected to GPIO34 (analog input)
  2. Modify SensorData struct to include float lightLevel
  3. Add analogRead(34) to the perception layer function
  4. Add a light threshold (e.g., alert if light < 500)
  5. Update the JSON payload and dashboard to show light readings

Hint: Use map(analogRead(34), 0, 4095, 0, 100) to convert to percentage

CautionChallenge 2: Implement a State Machine

Add edge/fog computing behavior by detecting state transitions:

  1. Create an enum SystemState { NORMAL, WARNING, CRITICAL }
  2. Only send data when state changes (reducing network traffic)
  3. Add hysteresis: require 3 consecutive high readings before alerting
  4. Implement different LED blink patterns for WARNING vs CRITICAL

This simulates how edge computing reduces bandwidth by filtering data locally

CautionChallenge 3: Add Real Network Transmission

If you have physical hardware, extend the demo with actual network connectivity:

  1. Add Wi-Fi connection code (WiFi.begin(ssid, password))
  2. Install PubSubClient library for MQTT
  3. Transmit JSON payloads to a free MQTT broker (e.g., test.mosquitto.org)
  4. 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:

  1. Layer Separation: Each layer has distinct responsibilities - sensors don’t know about alerts, network formatting doesn’t know about thresholds

  2. Data Transformation: Raw analog/digital readings become structured JSON, which becomes actionable alerts - value increases at each layer

  3. Bidirectional Flow: Data flows UP from sensors to dashboard; Control flows DOWN from thresholds to LED actuator

  4. Abstraction: The Application Layer works with SensorData and AlertStatus abstractions, not raw GPIO pins - making business logic portable

  5. Scalability 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

NoteReflection Questions

After completing this lab, you should be able to answer:

  1. Why is sensor reading code separated from threshold checking code?
  2. What would change if you switched from MQTT to CoAP protocol?
  3. How would you add data storage (Layer 4) to this system?
  4. 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:

  1. Reference model layers map directly to code structure
  2. Layer separation enables independent development and testing
  3. Data transformation adds value at each layer
  4. 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: