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