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.

In 60 Seconds

IoT architecture layers map directly to code: sensor reading functions = Perception Layer, JSON formatting = Network Layer, threshold logic = Application Layer. Layer separation makes code maintainable – changing the alert threshold does not require touching sensor code, and switching MQTT to CoAP changes only the network function.

Minimum Viable Understanding
  • IoT layers map directly to code: sensor reading functions = Perception Layer, JSON formatting = Network Layer, threshold logic = Application Layer.
  • Layer separation makes code maintainable: changing the alert threshold does not require touching sensor code, and switching from MQTT to CoAP changes only the network function.
  • The same architecture scales from prototype to production: the JSON payload format and threshold logic you write on an ESP32 demo can be reused in a factory-scale system.

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!”

~45 min | Intermediate | P04.C18.LAB

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:

  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

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

Breadboard circuit diagram showing ESP32 development board connected to DHT22 temperature and humidity sensor (GPIO4 with 10k pull-up resistor to 3.3V), push button (GPIO5 with pull-up), and LED indicator (GPIO2 through 220 ohm current-limiting resistor to ground), illustrating the Perception Layer hardware for the 3-layer IoT architecture lab
Figure 15.1: Lab circuit connections: DHT22 sensor and button feed the Perception Layer, ESP32 processes data through Network Layer formatting, LED provides Application Layer feedback

15.5 Interactive Wokwi Simulator

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

Simulator 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

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

  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

15.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)

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
Wokwi Tip

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

  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

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

Diagram mapping lab code functions to IoT architecture layers: readPerceptionLayer function and SensorData struct in Perception Layer (reading DHT22 temperature, humidity, and button state), formatNetworkPayload and transmitData functions in Network Layer (creating JSON payload and simulating MQTT transmission), and processApplicationLayer, executeActuators, and displayDashboard functions in Application Layer (threshold logic, LED control, and serial dashboard output)
Figure 15.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
Common Mistake: Mixing Layer Responsibilities in Code

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:

  1. 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.

  2. 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.

  3. 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 a SensorData struct as input and can be tested with mock data.

  4. 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:

  1. Your loop() function is >50 lines and does multiple unrelated things
  2. You use comments like // Read sensor // Check threshold // Send data in one function → each comment should be a separate function
  3. Changing one feature (e.g., alert threshold) requires scrolling through code that handles sensors, network, and display
  4. 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

Challenge 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

Challenge 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

Challenge 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

15.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

15.11 Knowledge Check

Reflection 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?

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:

  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

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