14  Lab: Build & Compare Topologies

Hands-on ESP32 Simulation for Star, Mesh, and Tree Topologies

networking
topologies
lab
esp32
wokwi
hands-on
Keywords

network topology lab, ESP32 topology, star topology lab, mesh topology lab, tree topology lab, ESP-NOW, Wokwi simulation, IoT network lab

Key Concepts
  • Lab Objective: The specific measurable outcome the lab exercise is designed to produce, such as “measure end-to-end latency in a 10-node star vs mesh topology”
  • Control Variable: A parameter held constant between experimental conditions to ensure a fair comparison
  • Independent Variable: The parameter deliberately changed between experiments (e.g., topology type)
  • Dependent Variable: The measured outcome affected by changes to the independent variable (e.g., packet delivery ratio)
  • Baseline Measurement: An initial measurement of network performance before modifications, used as a reference for comparison
  • Fault Injection: Deliberately disabling a node or link to test the network’s failure response
  • Lab Report: A structured document recording hypothesis, procedure, observations, analysis, and conclusions from the exercise

14.1 In 60 Seconds

This hands-on lab uses a browser-based ESP32 simulator (Wokwi) to build and compare star, mesh, and tree network topologies. You will implement hub-and-spoke routing, peer-to-peer mesh forwarding, and hierarchical aggregation using ESP-NOW, then test how each topology handles node failures and measure differences in latency, hop count, and throughput.

14.2 Overview

This hands-on lab provides a browser-based ESP32 simulation where you can experiment with different network topologies without any hardware. You will build star, mesh, and tree topologies, compare how they handle communication patterns, and observe failure scenarios in each configuration.

14.3 Learning Objectives

By completing this lab, you will be able to:

  • Implement Star Topology Communication: Construct a hub-and-spoke pattern where all nodes route through a central coordinator
  • Build Mesh Topology with Peer-to-Peer Routing: Configure direct and multi-hop communication paths between any pair of nodes
  • Design Tree/Hierarchical Topology: Organise nodes in parent-child relationships with data aggregation at each tier
  • Contrast Message Routing Behaviour: Trace how messages traverse each topology type and identify differences
  • Test Failure Resilience: Simulate node failures and evaluate how each topology recovers or degrades
  • Measure Topology Performance: Quantify latency, hop count, and throughput differences across topologies

In this lab, you will build three different network topologies using ESP32 microcontrollers in a simulator. You will create a star network, a mesh network, and a tree network, then compare how they perform. It is like building three different road systems and testing which one handles traffic best.

14.4 Prerequisites

  • Basic understanding of C/C++ syntax
  • Familiarity with Arduino-style programming (setup/loop structure)
  • Completion of the topology fundamentals section

14.5 The Wokwi Simulator

Wokwi is a free, browser-based electronics simulator that supports ESP32, Arduino, and many sensors. No installation is required — all code runs in your browser with simulated Wi-Fi and ESP-NOW communication.

Interactive ESP32 Topology Simulator

Click inside the simulator below and press the green “Play” button to start. You can edit the code and see real-time serial output showing message flow through different topologies.

Note: The simulator provides a pre-configured ESP32. Copy the code below into the simulator to run the topology demonstrations.

14.6 Lab Components Overview

This lab simulates a 5-node IoT network that can operate in three different topologies:

Node Role Function LED Indicator
ESP32 #1 Hub/Root Central coordinator (star) or root node (tree/mesh) Blue LED
ESP32 #2 Node A Sensor node, router in mesh/tree Green LED
ESP32 #3 Node B Sensor node, router in mesh/tree Yellow LED
ESP32 #4 Node C Sensor node, relay in tree Red LED
ESP32 #5 Node D Sensor node, end device in all topologies White LED

14.7 Step 1: Understanding the Code Structure

The complete code below implements all three topologies in a single ESP32 program. You select the topology mode and node role through configuration constants.

Simulator Setup Instructions

Since Wokwi does not persist embedded projects, copy the complete topology code below into the simulator. The code demonstrates all three topology patterns with a single ESP32 program; change the TOPOLOGY_MODE and NODE_ROLE constants to switch between configurations.

14.8 Complete Network Topology Simulator Code

Copy this code into the Wokwi simulator. Change the TOPOLOGY_MODE and NODE_ROLE constants to experiment with different configurations.

Key configuration excerpt:

// Topology modes - change TOPOLOGY_MODE to compare
#define TOPOLOGY_STAR  1
#define TOPOLOGY_MESH  2
#define TOPOLOGY_TREE  3
#define TOPOLOGY_MODE TOPOLOGY_STAR  // Change to compare topologies

// Node roles - change NODE_ROLE for each ESP32
#define ROLE_HUB       0   // Central hub (star) / Root (tree/mesh)
#define ROLE_NODE_A    1   // Level 1 node
#define ROLE_NODE_B    2   // Level 1 node
#define ROLE_NODE_C    3   // Level 2 node (child of A in tree)
#define ROLE_NODE_D    4   // Level 2 node (child of B in tree)
#define NODE_ROLE ROLE_HUB  // Change this for each ESP32
// ============================================================
// Network Topology Comparison Lab
// Demonstrates: Star, Mesh, and Tree topologies
// Compare: Routing paths, failure handling, message flow
// ============================================================

#include <WiFi.h>
#include <esp_now.h>

// ============ TOPOLOGY CONFIGURATION ============
// Select ONE topology mode:
#define TOPOLOGY_STAR  1
#define TOPOLOGY_MESH  2
#define TOPOLOGY_TREE  3

#define TOPOLOGY_MODE TOPOLOGY_STAR  // Change to compare topologies

// Select ONE node role (determines behavior):
#define ROLE_HUB       0   // Central hub (star) / Root (tree/mesh)
#define ROLE_NODE_A    1   // Level 1 node
#define ROLE_NODE_B    2   // Level 1 node
#define ROLE_NODE_C    3   // Level 2 node (child of A in tree)
#define ROLE_NODE_D    4   // Level 2 node (child of B in tree)

#define NODE_ROLE ROLE_HUB  // Change this for each ESP32

// LED Configuration
#define STATUS_LED 2        // Built-in LED for status
#define MSG_LED 4           // External LED for message activity

// ============ NETWORK CONSTANTS ============
#define MAX_NODES 10
#define HEARTBEAT_INTERVAL 3000
#define DATA_SEND_INTERVAL 5000
#define NODE_TIMEOUT 10000
#define MAX_HOPS 5

// Broadcast address for ESP-NOW
uint8_t broadcastMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// ============ MESSAGE STRUCTURES ============
enum MessageType {
  MSG_HEARTBEAT = 0,
  MSG_DATA = 1,
  MSG_ACK = 2,
  MSG_ROUTE_REQ = 3,
  MSG_ROUTE_REPLY = 4
};

typedef struct __attribute__((packed)) {
  uint8_t msgType;
  uint8_t srcNode;         // Original source node
  uint8_t dstNode;         // Final destination
  uint8_t senderNode;      // Immediate sender (for routing)
  uint8_t hopCount;
  uint8_t maxHops;
  uint32_t seqNum;
  uint8_t topology;        // Which topology is active
  char payload[48];
  uint8_t payloadLen;
} TopologyMessage;

// ============ NODE STATE ============
typedef struct {
  uint8_t nodeId;
  uint8_t mac[6];
  bool isActive;
  uint32_t lastSeen;
  int8_t rssi;
  uint8_t parentNode;      // For tree topology
  uint8_t hopDistance;     // Hops from hub/root
} NodeInfo;

NodeInfo nodes[MAX_NODES];
int nodeCount = 0;

uint8_t myMAC[6];
uint32_t messageSeq = 0;
uint32_t lastHeartbeat = 0;
uint32_t lastDataSend = 0;
uint32_t messagesRouted = 0;
uint32_t messagesSent = 0;
uint32_t messagesReceived = 0;

// Statistics for comparison
uint32_t totalHops = 0;
uint32_t deliveryCount = 0;
uint32_t failureCount = 0;

// ============ HELPER FUNCTIONS ============
const char* getTopologyName() {
  switch (TOPOLOGY_MODE) {
    case TOPOLOGY_STAR: return "STAR";
    case TOPOLOGY_MESH: return "MESH";
    case TOPOLOGY_TREE: return "TREE";
    default: return "UNKNOWN";
  }
}

const char* getRoleName() {
  switch (NODE_ROLE) {
    case ROLE_HUB: return "HUB/ROOT";
    case ROLE_NODE_A: return "NODE_A";
    case ROLE_NODE_B: return "NODE_B";
    case ROLE_NODE_C: return "NODE_C";
    case ROLE_NODE_D: return "NODE_D";
    default: return "UNKNOWN";
  }
}

void blinkLED(int pin, int times, int delayMs) {
  for (int i = 0; i < times; i++) {
    digitalWrite(pin, HIGH);
    delay(delayMs);
    digitalWrite(pin, LOW);
    delay(delayMs);
  }
}

void flashMessageLED() {
  digitalWrite(MSG_LED, HIGH);
  delay(30);
  digitalWrite(MSG_LED, LOW);
}

// ============ NODE TABLE MANAGEMENT ============
int findNode(uint8_t nodeId) {
  for (int i = 0; i < nodeCount; i++) {
    if (nodes[i].nodeId == nodeId) return i;
  }
  return -1;
}

void updateNode(uint8_t nodeId, const uint8_t* mac, int8_t rssi) {
  int idx = findNode(nodeId);

  if (idx >= 0) {
    nodes[idx].isActive = true;
    nodes[idx].lastSeen = millis();
    nodes[idx].rssi = rssi;
  } else if (nodeCount < MAX_NODES) {
    nodes[nodeCount].nodeId = nodeId;
    memcpy(nodes[nodeCount].mac, mac, 6);
    nodes[nodeCount].isActive = true;
    nodes[nodeCount].lastSeen = millis();
    nodes[nodeCount].rssi = rssi;
    nodes[nodeCount].parentNode = 0xFF;  // Unknown
    nodes[nodeCount].hopDistance = 0xFF;
    nodeCount++;

    Serial.printf("[TOPOLOGY] New node discovered: Node_%d (RSSI: %d)\n",
                  nodeId, rssi);
  }
}

void checkNodeTimeouts() {
  for (int i = 0; i < nodeCount; i++) {
    if (nodes[i].isActive && (millis() - nodes[i].lastSeen > NODE_TIMEOUT)) {
      nodes[i].isActive = false;
      Serial.printf("[TOPOLOGY] Node_%d OFFLINE - triggering topology event\n",
                    nodes[i].nodeId);

      // Topology-specific failure handling
      handleNodeFailure(nodes[i].nodeId);
    }
  }
}

// ============ TOPOLOGY-SPECIFIC ROUTING ============

// STAR TOPOLOGY: All messages go through hub
uint8_t getNextHopStar(uint8_t destNode) {
  if (NODE_ROLE == ROLE_HUB) {
    // Hub sends directly to destination
    return destNode;
  } else {
    // All other nodes send to hub first
    return ROLE_HUB;
  }
}

// MESH TOPOLOGY: Direct peer-to-peer or multi-hop
uint8_t getNextHopMesh(uint8_t destNode) {
  // Check if destination is a direct neighbor
  int idx = findNode(destNode);
  if (idx >= 0 && nodes[idx].isActive) {
    return destNode;  // Direct delivery
  }

  // Find best relay (highest RSSI active node)
  int bestRelay = -1;
  int8_t bestRSSI = -127;

  for (int i = 0; i < nodeCount; i++) {
    if (nodes[i].isActive && nodes[i].nodeId != NODE_ROLE) {
      if (nodes[i].rssi > bestRSSI) {
        bestRSSI = nodes[i].rssi;
        bestRelay = i;
      }
    }
  }

  if (bestRelay >= 0) {
    Serial.printf("[MESH] Routing via Node_%d (RSSI: %d)\n",
                  nodes[bestRelay].nodeId, bestRSSI);
    return nodes[bestRelay].nodeId;
  }

  return 0xFF;  // No route
}

// TREE TOPOLOGY: Follow parent-child hierarchy
uint8_t getNextHopTree(uint8_t destNode) {
  // Define tree structure:
  // Hub (root) -> Node_A, Node_B (level 1)
  // Node_A -> Node_C (level 2)
  // Node_B -> Node_D (level 2)

  switch (NODE_ROLE) {
    case ROLE_HUB:
      // Hub routes to level 1 children or through them
      if (destNode == ROLE_NODE_A || destNode == ROLE_NODE_C) return ROLE_NODE_A;
      if (destNode == ROLE_NODE_B || destNode == ROLE_NODE_D) return ROLE_NODE_B;
      break;

    case ROLE_NODE_A:
      if (destNode == ROLE_NODE_C) return ROLE_NODE_C;  // Direct child
      return ROLE_HUB;  // Everything else via parent

    case ROLE_NODE_B:
      if (destNode == ROLE_NODE_D) return ROLE_NODE_D;  // Direct child
      return ROLE_HUB;  // Everything else via parent

    case ROLE_NODE_C:
      return ROLE_NODE_A;  // Always go through parent

    case ROLE_NODE_D:
      return ROLE_NODE_B;  // Always go through parent
  }

  return ROLE_HUB;  // Default: send to root
}

uint8_t getNextHop(uint8_t destNode) {
  switch (TOPOLOGY_MODE) {
    case TOPOLOGY_STAR: return getNextHopStar(destNode);
    case TOPOLOGY_MESH: return getNextHopMesh(destNode);
    case TOPOLOGY_TREE: return getNextHopTree(destNode);
    default: return 0xFF;
  }
}

// ============ FAILURE HANDLING ============
void handleNodeFailure(uint8_t failedNode) {
  Serial.printf("\n!!! NODE FAILURE: Node_%d !!!\n", failedNode);

  switch (TOPOLOGY_MODE) {
    case TOPOLOGY_STAR:
      if (failedNode == ROLE_HUB) {
        Serial.println("[STAR] CRITICAL: Hub failure - network DOWN");
        Serial.println("[STAR] All communication impossible without hub");
        Serial.println("[STAR] Recovery: Wait for hub restart or deploy backup");
        failureCount++;
      } else {
        Serial.printf("[STAR] Node_%d offline - other nodes unaffected\n", failedNode);
        Serial.println("[STAR] Hub can still communicate with remaining nodes");
      }
      break;

    case TOPOLOGY_MESH:
      Serial.printf("[MESH] Node_%d offline - finding alternate routes\n", failedNode);
      Serial.println("[MESH] Self-healing: Messages will route around failure");
      Serial.println("[MESH] Network remains operational via peer relays");
      // Mesh automatically finds alternate paths
      break;

    case TOPOLOGY_TREE:
      if (failedNode == ROLE_HUB) {
        Serial.println("[TREE] CRITICAL: Root failure - subtrees isolated");
        Serial.println("[TREE] Children of root cannot reach each other");
        failureCount++;
      } else if (failedNode == ROLE_NODE_A || failedNode == ROLE_NODE_B) {
        Serial.printf("[TREE] Level 1 node failed - subtree isolated\n");
        Serial.println("[TREE] Children of this node cannot reach root");
        failureCount++;
      } else {
        Serial.printf("[TREE] Leaf node_%d offline - tree structure intact\n", failedNode);
      }
      break;
  }
  Serial.println();
}

// ============ MESSAGE SENDING ============
void sendTopologyMessage(TopologyMessage* msg, uint8_t nextHopNode) {
  // Find MAC address for next hop
  int idx = findNode(nextHopNode);
  const uint8_t* destMAC;

  if (idx >= 0) {
    // NOTE: In production code, call esp_now_add_peer() here if not already registered
    // before calling esp_now_send(). This lab uses broadcast for discovery so all peers
    // are reachable via the broadcast address registered in setup().
    destMAC = nodes[idx].mac;
  } else if (nextHopNode == NODE_ROLE) {
    // Sending to self (shouldn't happen)
    return;
  } else {
    // Node not in table, broadcast
    destMAC = broadcastMAC;
  }

  msg->senderNode = NODE_ROLE;
  msg->topology = TOPOLOGY_MODE;

  esp_err_t result = esp_now_send(destMAC, (uint8_t*)msg, sizeof(TopologyMessage));

  flashMessageLED();
  messagesSent++;

  Serial.printf("[TX] %s: %s msg to Node_%d via Node_%d (hops: %d)\n",
                getTopologyName(),
                msg->msgType == MSG_DATA ? "DATA" :
                msg->msgType == MSG_HEARTBEAT ? "HEARTBEAT" :
                msg->msgType == MSG_ACK ? "ACK" : "ROUTE",
                msg->dstNode, nextHopNode, msg->hopCount);
}

void sendHeartbeat() {
  TopologyMessage hb;
  hb.msgType = MSG_HEARTBEAT;
  hb.srcNode = NODE_ROLE;
  hb.dstNode = 0xFF;  // Broadcast
  hb.senderNode = NODE_ROLE;
  hb.hopCount = 0;
  hb.maxHops = 1;
  hb.seqNum = ++messageSeq;
  hb.topology = TOPOLOGY_MODE;
  snprintf(hb.payload, sizeof(hb.payload), "%s:%s",
           getTopologyName(), getRoleName());
  hb.payloadLen = strlen(hb.payload);

  esp_now_send(broadcastMAC, (uint8_t*)&hb, sizeof(TopologyMessage));
  Serial.printf("[HEARTBEAT] Beacon: %s in %s topology\n",
                getRoleName(), getTopologyName());
}

void sendSensorData() {
  // Only non-hub nodes send sensor data
  if (NODE_ROLE == ROLE_HUB) return;

  TopologyMessage data;
  data.msgType = MSG_DATA;
  data.srcNode = NODE_ROLE;
  data.dstNode = ROLE_HUB;  // Data always goes to hub/root
  data.hopCount = 0;
  data.maxHops = MAX_HOPS;
  data.seqNum = ++messageSeq;

  // Simulated sensor data
  float temp = 20.0 + (random(0, 100) / 10.0);
  int light = 200 + random(0, 600);
  snprintf(data.payload, sizeof(data.payload),
           "T:%.1fC,L:%d", temp, light);
  data.payloadLen = strlen(data.payload);

  Serial.println("\n====== SENDING SENSOR DATA ======");
  Serial.printf("Topology: %s\n", getTopologyName());
  Serial.printf("Source: %s -> Destination: HUB\n", getRoleName());
  Serial.printf("Data: %s\n", data.payload);

  uint8_t nextHop = getNextHop(ROLE_HUB);

  if (nextHop != 0xFF) {
    Serial.printf("Next hop: Node_%d\n", nextHop);
    Serial.printf("Expected path: ");

    // Show expected routing path
    switch (TOPOLOGY_MODE) {
      case TOPOLOGY_STAR:
        Serial.printf("%s -> HUB (1 hop)\n", getRoleName());
        break;
      case TOPOLOGY_MESH:
        Serial.printf("%s -> [best RSSI relay] -> HUB\n", getRoleName());
        break;
      case TOPOLOGY_TREE:
        switch (NODE_ROLE) {
          case ROLE_NODE_A:
          case ROLE_NODE_B:
            Serial.printf("%s -> HUB (1 hop)\n", getRoleName());
            break;
          case ROLE_NODE_C:
            Serial.printf("NODE_C -> NODE_A -> HUB (2 hops)\n");
            break;
          case ROLE_NODE_D:
            Serial.printf("NODE_D -> NODE_B -> HUB (2 hops)\n");
            break;
        }
        break;
    }

    sendTopologyMessage(&data, nextHop);
  } else {
    Serial.println("ERROR: No route to HUB!");
    failureCount++;
  }
  Serial.println("==================================\n");
}

// ============ MESSAGE RECEIVING ============
void onDataSent(const uint8_t* mac, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) {
    Serial.println("[DELIVERY] Success");
    deliveryCount++;
  } else {
    Serial.println("[DELIVERY] Failed");
  }
}

void onDataReceived(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
  TopologyMessage msg;
  memcpy(&msg, data, sizeof(TopologyMessage));

  messagesReceived++;
  flashMessageLED();

  // Update node table
  updateNode(msg.senderNode, info->src_addr, info->rx_ctrl->rssi);

  Serial.printf("[RX] From Node_%d: %s (seq: %lu, hops: %d)\n",
                msg.senderNode,
                msg.msgType == MSG_DATA ? "DATA" :
                msg.msgType == MSG_HEARTBEAT ? "HEARTBEAT" :
                msg.msgType == MSG_ACK ? "ACK" : "ROUTE",
                msg.seqNum, msg.hopCount);

  switch (msg.msgType) {
    case MSG_HEARTBEAT:
      // Already updated node table
      break;

    case MSG_DATA:
      handleDataMessage(&msg);
      break;

    case MSG_ACK:
      Serial.printf("[ACK] Message %lu acknowledged\n", msg.seqNum);
      break;
  }
}

void handleDataMessage(TopologyMessage* msg) {
  msg->hopCount++;
  totalHops++;

  if (msg->dstNode == NODE_ROLE) {
    // We are the destination
    Serial.println("\n****** DATA RECEIVED AT DESTINATION ******");
    Serial.printf("Topology: %s\n", getTopologyName());
    Serial.printf("Original source: Node_%d\n", msg->srcNode);
    Serial.printf("Total hops: %d\n", msg->hopCount);
    Serial.printf("Payload: %s\n", msg->payload);
    Serial.println("*******************************************\n");

    // Send ACK back
    TopologyMessage ack;
    ack.msgType = MSG_ACK;
    ack.srcNode = NODE_ROLE;
    ack.dstNode = msg->srcNode;
    ack.hopCount = 0;
    ack.maxHops = MAX_HOPS;
    ack.seqNum = msg->seqNum;

    uint8_t nextHop = getNextHop(msg->srcNode);
    if (nextHop != 0xFF) {
      sendTopologyMessage(&ack, nextHop);
    }

  } else if (msg->hopCount < msg->maxHops) {
    // Relay message (routing)
    messagesRouted++;

    Serial.printf("[ROUTING] Relaying from Node_%d to Node_%d\n",
                  msg->srcNode, msg->dstNode);
    Serial.printf("[ROUTING] Hop %d of %d\n", msg->hopCount, msg->maxHops);

    uint8_t nextHop = getNextHop(msg->dstNode);

    if (nextHop != 0xFF && nextHop != msg->senderNode) {
      sendTopologyMessage(msg, nextHop);
    } else {
      Serial.println("[ROUTING] No valid next hop - message dropped");
      failureCount++;
    }
  } else {
    Serial.println("[ROUTING] Max hops exceeded - message dropped");
    failureCount++;
  }
}

// ============ STATISTICS DISPLAY ============
void printStatistics() {
  Serial.println("\n========== TOPOLOGY STATISTICS ==========");
  Serial.printf("Topology Mode: %s\n", getTopologyName());
  Serial.printf("Node Role: %s\n", getRoleName());
  Serial.printf("Active Nodes: %d\n", nodeCount);
  Serial.println("-----------------------------------------");
  Serial.printf("Messages Sent: %lu\n", messagesSent);
  Serial.printf("Messages Received: %lu\n", messagesReceived);
  Serial.printf("Messages Routed: %lu\n", messagesRouted);
  Serial.printf("Total Hops: %lu\n", totalHops);
  Serial.printf("Delivery Success: %lu\n", deliveryCount);
  Serial.printf("Failures: %lu\n", failureCount);

  if (deliveryCount > 0) {
    Serial.printf("Avg Hops per Message: %.2f\n",
                  (float)totalHops / deliveryCount);
  }
  Serial.println("=========================================\n");
}

// ============ SETUP AND LOOP ============
void setup() {
  Serial.begin(115200);
  delay(100);

  pinMode(STATUS_LED, OUTPUT);
  pinMode(MSG_LED, OUTPUT);

  Serial.println("\n================================================");
  Serial.println("   Network Topology Comparison Lab");
  Serial.printf("   Topology: %s\n", getTopologyName());
  Serial.printf("   Role: %s\n", getRoleName());
  Serial.println("================================================\n");

  // Initialize Wi-Fi in station mode for ESP-NOW
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  // Get MAC address
  WiFi.macAddress(myMAC);
  Serial.printf("MAC Address: %02X:%02X:%02X:%02X:%02X:%02X\n",
                myMAC[0], myMAC[1], myMAC[2],
                myMAC[3], myMAC[4], myMAC[5]);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW initialization failed!");
    return;
  }

  esp_now_register_send_cb(onDataSent);
  esp_now_register_recv_cb(onDataReceived);

  // Add broadcast peer for discovery
  esp_now_peer_info_t peerInfo;
  memset(&peerInfo, 0, sizeof(peerInfo));
  memcpy(peerInfo.peer_addr, broadcastMAC, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  esp_now_add_peer(&peerInfo);

  // Show topology-specific information
  Serial.println("\n--- Topology Characteristics ---");
  switch (TOPOLOGY_MODE) {
    case TOPOLOGY_STAR:
      Serial.println("STAR Topology:");
      Serial.println("  - All nodes connect to central HUB");
      Serial.println("  - Single point of failure at HUB");
      Serial.println("  - Maximum 1 hop for any message");
      Serial.println("  - Simple routing, easy management");
      break;
    case TOPOLOGY_MESH:
      Serial.println("MESH Topology:");
      Serial.println("  - All nodes can communicate directly");
      Serial.println("  - Self-healing: routes around failures");
      Serial.println("  - Variable hop count based on RSSI");
      Serial.println("  - Complex routing, high resilience");
      break;
    case TOPOLOGY_TREE:
      Serial.println("TREE Topology:");
      Serial.println("  - Hierarchical parent-child structure");
      Serial.println("  - Root failure isolates entire network");
      Serial.println("  - Hop count = depth in tree");
      Serial.println("  - Structured routing, moderate resilience");
      break;
  }
  Serial.println("--------------------------------\n");

  // Indicate ready with LED pattern
  blinkLED(STATUS_LED, 3, 200);
  digitalWrite(STATUS_LED, HIGH);

  randomSeed(analogRead(0) + myMAC[5]);

  Serial.println("Waiting for network formation...\n");
}

void loop() {
  static uint32_t lastStats = 0;

  // Send periodic heartbeats
  if (millis() - lastHeartbeat > HEARTBEAT_INTERVAL) {
    lastHeartbeat = millis();
    sendHeartbeat();
  }

  // Send sensor data periodically (non-hub nodes)
  if (millis() - lastDataSend > DATA_SEND_INTERVAL) {
    lastDataSend = millis();
    sendSensorData();
  }

  // Check for timed-out nodes
  checkNodeTimeouts();

  // Print statistics every 30 seconds
  if (millis() - lastStats > 30000) {
    lastStats = millis();
    printStatistics();
  }

  delay(10);
}

14.9 Step 2: Running the Experiments

14.9.1 Experiment 1: Star Topology

  1. Set TOPOLOGY_MODE TOPOLOGY_STAR and NODE_ROLE ROLE_HUB
  2. Start the simulation and observe heartbeat broadcasts
  3. Notice all data messages route directly through the hub

What to observe:

  • Messages always take exactly 1 hop to reach the hub
  • Hub failure (stop simulation) breaks all communication
  • Simple and predictable routing behavior

Serial output example:

[HEARTBEAT] Beacon: HUB/ROOT in STAR topology
[TX] STAR: DATA msg to Node_0 via Node_0 (hops: 0)
[DELIVERY] Success

****** DATA RECEIVED AT DESTINATION ******
Topology: STAR
Original source: Node_1
Total hops: 1
Payload: T:24.5C,L:512
*******************************************

14.9.2 Experiment 2: Mesh Topology

  1. Change to TOPOLOGY_MODE TOPOLOGY_MESH
  2. Run with multiple nodes (change NODE_ROLE for each instance)
  3. Observe peer-to-peer message routing

What to observe:

  • Messages may take multiple hops based on RSSI
  • If a node fails, messages automatically reroute
  • More complex routing decisions in serial output

Serial output example:

[MESH] Routing via Node_2 (RSSI: -45)
[TX] MESH: DATA msg to Node_0 via Node_2 (hops: 0)
[ROUTING] Relaying from Node_1 to Node_0
[ROUTING] Hop 1 of 5

14.9.3 Experiment 3: Tree Topology

  1. Change to TOPOLOGY_MODE TOPOLOGY_TREE
  2. Note the fixed parent-child relationships in the code
  3. Observe hierarchical routing

What to observe:

  • Node_C routes through Node_A to reach Hub
  • Node_D routes through Node_B to reach Hub
  • Intermediate node failure isolates subtrees

Serial output example:

Expected path: NODE_C -> NODE_A -> HUB (2 hops)
[TX] TREE: DATA msg to Node_0 via Node_1 (hops: 0)
[ROUTING] Relaying from Node_3 to Node_0

14.10 Step 3: Testing Failure Scenarios

14.10.1 Star Topology Failure Test

  1. Run two ESP32 instances: one as HUB, one as NODE_A
  2. While running, stop the HUB instance
  3. Observe NODE_A cannot deliver messages

Expected behavior:

[TOPOLOGY] Node_0 OFFLINE - triggering topology event

!!! NODE FAILURE: Node_0 !!!
[STAR] CRITICAL: Hub failure - network DOWN
[STAR] All communication impossible without hub

14.10.2 Mesh Topology Failure Test

  1. Run three nodes: HUB, NODE_A, NODE_B
  2. NODE_A routes through NODE_B
  3. Stop NODE_B - observe automatic rerouting

Expected behavior:

!!! NODE FAILURE: Node_2 !!!
[MESH] Node_2 offline - finding alternate routes
[MESH] Self-healing: Messages will route around failure

14.10.3 Tree Topology Failure Test

  1. Run HUB, NODE_A, NODE_C (Node_C is child of Node_A)
  2. Stop NODE_A - observe NODE_C is isolated

Expected behavior:

!!! NODE FAILURE: Node_1 !!!
[TREE] Level 1 node failed - subtree isolated
[TREE] Children of this node cannot reach root

14.11 Step 4: Comparing Topology Metrics

After running each topology for several minutes, compare the statistics:

Metric Star Mesh Tree
Avg Hops 1 1–3 1–tree depth
Hub Failure Impact Total None Total
Node Failure Impact Single node Self-heal Subtree
Routing Complexity Simple Complex Moderate
Scalability Hub-limited Good Good
Try It: Topology Hop-Count Explorer

Use the controls below to model the routing path and hop count for your 5-node network in each topology. This mirrors exactly what the ESP32 code computes at runtime.

14.12 Challenge Exercises

Challenge 1: Add a Fifth Node

Modify the code to add ROLE_NODE_E as a child of NODE_C in the tree topology. Update the routing logic in getNextHopTree() to handle this deeper hierarchy.

Questions to answer:

  • How does the additional depth affect average hop count?
  • What happens when NODE_A fails now?
Challenge 2: Implement Redundant Hub

Modify the star topology to support two hubs (ROLE_HUB and ROLE_HUB_BACKUP). When the primary hub fails, nodes should automatically switch to the backup.

Hints:

  • Add a hubPrimary and hubBackup variable
  • Modify getNextHopStar() to check hub availability
  • Track which hub responded most recently
Challenge 3: Hybrid Topology

Create a hybrid topology that uses:

  • Star for nodes close to the hub (1 hop away)
  • Mesh for nodes farther away (automatic relay)

Modify the code to detect RSSI and choose the appropriate routing strategy.

Challenge 4: Measure Latency

Add timestamps to messages to measure end-to-end latency. Compare:

  • Star: Hub-to-node latency
  • Mesh: Multi-hop latency vs direct
  • Tree: Deep node latency

Add fields to TopologyMessage for sendTime and calculate receiveTime - sendTime.

14.13 Key Topology Concepts Demonstrated

This lab illustrates these fundamental topology principles:

Star Topology Key Points
  1. Central Point of Control: The hub manages all routing decisions
  2. Single Point of Failure: Hub failure = complete network failure
  3. Predictable Performance: Exactly 1 hop for any communication
  4. Easy Troubleshooting: All traffic visible at hub
  5. Bandwidth Concentration: Hub must handle all traffic
Mesh Topology Key Points
  1. Self-Healing: Automatic rerouting around failed nodes
  2. Distributed Routing: Every node makes routing decisions
  3. Variable Hop Count: Path length depends on network state
  4. Resilience: No single point of failure (unless all neighbors fail)
  5. Complexity Trade-off: More sophisticated routing protocols required
Tree Topology Key Points
  1. Hierarchical Structure: Clear parent-child relationships
  2. Traffic Aggregation: Data flows up toward root
  3. Partial Failure Impact: Intermediate node failure isolates its entire subtree; sibling branches remain operational
  4. Predictable Paths: Route determined by tree position
  5. Scalability: Easy to add leaves, harder to add intermediate nodes

Common Pitfalls

Labs that skip fault injection miss the most insightful measurements. Fix: allocate the last 20–30% of lab time to removing the most critical node and measuring recovery time and packet loss.

Changing both topology type and node count simultaneously makes it impossible to attribute performance differences to either variable. Fix: change only one variable at a time (control experiment design).

A single latency measurement is not representative; network variability requires multiple samples. Fix: measure at least 100 packets per configuration and report mean, standard deviation, and 95th percentile.

Residual routing tables or channel reservations from a previous experiment contaminate the next run. Fix: restart the simulation environment completely between topology configurations.

14.14 What You Learned

After completing this lab, you should understand:

  1. Routing Differences: How star, mesh, and tree topologies route messages differently
  2. Failure Modes: The specific vulnerabilities of each topology
  3. Performance Trade-offs: Hop count, latency, and complexity considerations
  4. Selection Criteria: When to choose each topology for IoT deployments
  5. Self-Healing: How mesh networks automatically recover from failures

14.15 Further Exploration

  • Zigbee Networks: Use mesh topology with ZCL clusters
  • LoRaWAN: Star topology with gateways as hubs
  • Thread/Matter: Tree topology with border routers
  • Wi-Fi Mesh: Hybrid approach with AP backhaul

14.16 Knowledge Check

Mesh network power consumption varies drastically by node role—relay nodes drain batteries orders of magnitude faster than edge nodes because they must keep their radio in receive mode continuously.

Battery Life formulas (inputs in mAh, mA, seconds, messages/hr):

\[L_{\text{edge}} = \frac{C_{\text{batt}}}{I_{\text{TX}} \times \frac{T_{\text{TX}}}{3600} \times N_{\text{own}}}\]

\[L_{\text{relay}} = \frac{C_{\text{batt}}}{I_{\text{TX}} \times \frac{T_{\text{TX}}}{3600} \times (N_{\text{own}} + N_{\text{relay}}) + I_{\text{RX}}}\]

Quick result (2400 mAh battery, 50 mA TX, 0.5 s per message, 6 msg/hr, 15 mA RX-on):

Node Type Hourly Draw Battery Life
Edge (TX only, sleeps) 0.052 mAh/hr ~5.3 years
Core relay (5 downstream nodes, RX always-on) 15.25 mAh/hr ~6.6 days

See the Common Mistake callout below for full calculation details and design guidance.

Try It: Mesh Node Battery Life Calculator

Adjust the parameters to see how node role dramatically changes battery life. The model matches the power analysis in the ESP32 lab statistics output.

Common Mistake: Treating All Mesh Nodes Equally in Power Budget

The Mistake: Students running the ESP32 lab often assume that battery life estimates apply equally to all nodes in a mesh network. They calculate “if relay nodes forward 100 packets/day and consume 50 mA per transmission, battery life is X years” without considering node role differences.

Why This Is Wrong: In mesh networks, some nodes are relay bottlenecks that forward far more traffic than others. These nodes drain batteries orders of magnitude faster than edge nodes, primarily because relay nodes must keep their radio in receive mode continuously.

Lab Observation from Code:

In the ESP32 topology lab, look at the handleDataMessage() function:

void handleDataMessage(TopologyMessage* msg) {
  msg->hopCount++;
  totalHops++;

  if (msg->dstNode == NODE_ROLE) {
    // We are destination - no forwarding needed
  } else if (msg->hopCount < msg->maxHops) {
    // RELAY MESSAGE - this costs energy!
    messagesRouted++;
    uint8_t nextHop = getNextHop(msg->dstNode);
    if (nextHop != 0xFF) {
      sendTopologyMessage(msg, nextHop);
    }
  }
}

Real-World Power Analysis:

Assume 20-node Zigbee mesh, battery-powered, sending data every 10 minutes:

Edge Node (End Device):

  • Role: Only sends own data, never relays
  • Traffic: 6 transmissions/hour (one every 10 minutes)
  • Energy per TX: 50 mA × 0.5 s = 25 mA·s = 0.00694 mAh per message
  • TX draw: 6 msg/hr × 0.00694 mAh = 0.042 mAh/hr
  • Sleep draw: 10 µA × (3600 − 3) s/hr ÷ 3600 = 0.010 mAh/hr
  • Total hourly consumption: 0.052 mAh/hr → Daily: 1.25 mAh/day
  • Battery life (2400 mAh battery): 2400 / 1.25 = ~5.3 years

Core Relay Node (Router):

  • Role: Sends own data + forwards for 5 downstream nodes
  • Own traffic: 6 transmissions/hour
  • Relay traffic: 5 nodes × 6 transmissions = 30 relays/hour; total: 36 msg/hr
  • TX draw: 36 × 0.00694 mAh = 0.25 mAh/hr
  • Cannot sleep (must listen for incoming packets at all times)
  • RX always-on draw: 15 mA × 1 hr = 15 mAh/hr
  • Total hourly consumption: 15.25 mAh/hr → Daily: 366 mAh/day
  • Battery life (2400 mAh battery): 2400 / 366 = ~6.6 days

Shocking Result: Core relay node battery drains ~290× faster than edge node (~6.6 days vs ~5.3 years)!

Why The Lab Code Highlights This:

The messagesRouted counter in the lab tracks relay activity:

Serial.printf("Messages Routed: %lu\n", messagesRouted);

Run the lab with NODE_A as relay between NODE_C and HUB. After 30 minutes: - NODE_A messagesRouted: 18 (relayed for NODE_C) - NODE_A messagesSent: 6 (own traffic) - Ratio: 3:1 (relay traffic vs own traffic)

In a real network with 5 downstream nodes: ratio becomes 5:1. With 10 nodes: 10:1.

Correct Design Approach:

  1. Identify relay bottlenecks: Use topology visualization to find nodes with high betweenness centrality (many paths go through them)

  2. Deploy hierarchical mesh: Mains-powered routers form mesh backbone, battery sensors connect as end devices (never relay)

  3. Power budget by role:

    • Edge end devices: multi-year battery life feasible (5+ years at 6 msg/hr on 2400 mAh)
    • Core routers: Must be mains-powered or have large batteries/solar
  4. Load balancing: Distribute relay load across multiple routers, don’t route all traffic through single bottleneck

Lab Exercise: Modify the ESP32 code to track messagesRoutedByNode for each node. After 1 hour of simulation, identify which nodes relayed the most traffic. These are your battery bottlenecks.

Key Insight: Mesh topology does not have uniform power consumption. Central routers consume 100-300× more power than edge nodes when RX-on time is factored in. Always design with role-based power budgets; never assume average values apply to all nodes.

14.17 Concept Relationships

Foundation Concepts:

  • Topology Types - This lab implements the star, mesh, and tree topologies described there
  • Routing Fundamentals - The getNextHop() functions demonstrate routing decisions

Lab Skills Applied:

Real-World Protocols:

  • Zigbee Architecture - Uses mesh routing similar to this lab’s mesh implementation
  • Thread Operation - Thread uses a hierarchical tree-like structure (DODAG) that shares the parent-child routing concept demonstrated in this lab
  • RPL Fundamentals - RPL implements tree routing like getNextHopTree()

Advanced Topics:

14.18 See Also

Related Labs:

Design Practice:

Troubleshooting:

14.19 What’s Next

If you want to… Read this
Review the analysis techniques used in the lab Topology Analysis
Understand hybrid topologies based on lab findings Hybrid Design
Try the interactive topology simulator Interactive Topology
Study failure modes in more depth Topology Failures
See the comprehensive review Comprehensive Review