772  Lab: Build and Compare Network 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

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

772.2 Learning Objectives

By completing this lab, you will be able to:

  • Implement star topology communication: Create a hub-and-spoke pattern where all nodes communicate through a central coordinator
  • Build mesh topology with peer-to-peer routing: Enable direct and multi-hop communication between any nodes
  • Design tree/hierarchical topology: Organize nodes in parent-child relationships with traffic aggregation
  • Compare message routing behavior: Observe how messages traverse different topologies
  • Test failure resilience: See how each topology responds when a node fails
  • Measure topology performance: Understand latency, hop count, and throughput differences

772.3 Prerequisites

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

772.4 The Wokwi Simulator

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

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

772.5 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

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

NoteSimulator 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 showing each role’s behavior.

772.7 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:

// ============================================================
// 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) {
    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);
}

772.8 Step 2: Running the Experiments

772.8.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
*******************************************

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

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

772.9 Step 3: Testing Failure Scenarios

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

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

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

772.10 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-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

772.11 Challenge Exercises

NoteChallenge 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?
NoteChallenge 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
NoteChallenge 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.

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

772.12 Key Topology Concepts Demonstrated

This lab illustrates these fundamental topology principles:

TipStar 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
TipMesh 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
TipTree Topology Key Points
  1. Hierarchical Structure: Clear parent-child relationships
  2. Traffic Aggregation: Data flows up toward root
  3. Partial Failure Impact: Subtrees isolated, siblings unaffected
  4. Predictable Paths: Route determined by tree position
  5. Scalability: Easy to add leaves, harder to add intermediate nodes

772.13 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

772.14 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

772.15 What’s Next

Now that you have hands-on experience with network topologies, continue exploring: