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
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
For Beginners: Topology Lab
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
Full Simulator Source Code (click to expand)
// ============================================================
// 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
- Set
TOPOLOGY_MODE TOPOLOGY_STARandNODE_ROLE ROLE_HUB - Start the simulation and observe heartbeat broadcasts
- 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
- Change to
TOPOLOGY_MODE TOPOLOGY_MESH - Run with multiple nodes (change
NODE_ROLEfor each instance) - 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
- Change to
TOPOLOGY_MODE TOPOLOGY_TREE - Note the fixed parent-child relationships in the code
- 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
- Run two ESP32 instances: one as HUB, one as NODE_A
- While running, stop the HUB instance
- 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
- Run three nodes: HUB, NODE_A, NODE_B
- NODE_A routes through NODE_B
- 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
- Run HUB, NODE_A, NODE_C (Node_C is child of Node_A)
- 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 |
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
hubPrimaryandhubBackupvariable - 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
- Central Point of Control: The hub manages all routing decisions
- Single Point of Failure: Hub failure = complete network failure
- Predictable Performance: Exactly 1 hop for any communication
- Easy Troubleshooting: All traffic visible at hub
- Bandwidth Concentration: Hub must handle all traffic
Mesh Topology Key Points
- Self-Healing: Automatic rerouting around failed nodes
- Distributed Routing: Every node makes routing decisions
- Variable Hop Count: Path length depends on network state
- Resilience: No single point of failure (unless all neighbors fail)
- Complexity Trade-off: More sophisticated routing protocols required
Tree Topology Key Points
- Hierarchical Structure: Clear parent-child relationships
- Traffic Aggregation: Data flows up toward root
- Partial Failure Impact: Intermediate node failure isolates its entire subtree; sibling branches remain operational
- Predictable Paths: Route determined by tree position
- Scalability: Easy to add leaves, harder to add intermediate nodes
Common Pitfalls
1. Running Only the Happy-Path Scenario
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.
2. Changing Multiple Variables Between Experiments
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).
3. Not Taking Enough Samples for Statistical Validity
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.
4. Not Cleaning Up Simulations Between Experiments
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:
- Routing Differences: How star, mesh, and tree topologies route messages differently
- Failure Modes: The specific vulnerabilities of each topology
- Performance Trade-offs: Hop count, latency, and complexity considerations
- Selection Criteria: When to choose each topology for IoT deployments
- 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
Putting Numbers to It
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.
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:
Identify relay bottlenecks: Use topology visualization to find nodes with high betweenness centrality (many paths go through them)
Deploy hierarchical mesh: Mains-powered routers form mesh backbone, battery sensors connect as end devices (never relay)
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
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:
- ESP-NOW Protocol - This lab uses ESP-NOW for peer-to-peer communication
- Network Programming - Code structure demonstrates network simulation patterns
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:
- Wireless Sensor Networks - WSN protocols use these same topology patterns at scale
- Self-Healing Networks - Mesh self-healing demonstrated in this lab
14.18 See Also
Related Labs:
- Routing Labs - Build on topology knowledge with routing protocol implementation
- Wokwi Simulator Collection - More ESP32 IoT simulations
Design Practice:
- Topology Selection - Apply lab insights to choose topologies for projects
- Network Design - Design methodology using topology principles from lab
Troubleshooting:
- Topology Failures - Understand failure modes demonstrated in lab experiments
- Network Traffic Analysis - Analyze traffic patterns observed in lab
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 |