Chapters

Network Performance Lab: Calculations and Experiments

networking-core
net
performance

This lab belongs to Network Performance Lab: Measurement Setup and Terms

Start With the Decision

This section: Start With the Decision — A protocol header is the control information placed before a message’s payload, the sensor data it carries.

A protocol header is the control information placed before a message’s payload, the sensor data it carries. On a small message, the header can take most of the bytes. Bandwidth is how much data a link can carry in a given time; latency is the wait for a message to arrive. A calculation and simulator run show where the time and bytes go.

Route Overview

This section: Route Overview — This is part 2 of 2.

This is part 2 of 2. Review Network Performance Lab: Measurement Setup and Terms for the preceding evidence.

Learning Objectives

This section: Learning Objectives

  • Calculate protocol overhead and transfer time.
  • Run performance experiments and compare expected output.

Chapter Roadmap

This section: Chapter Roadmap — Read Figure 1 before the calculator, because it names the three quantities the calculator moves between.

  • Putting Numbers to It
  • Try It: Protocol Overhead Calculator
  • Checkpoint: Metrics and Evidence
  • Test Before Hardware
  • Try It: Network Performance Workbench
  • Optional Hardware Build
  • Checkpoint: Simulate Before Wiring
  • Circuit Diagram
  • Optional ESP32 Implementation
  • Network Simulator Code Archive
  • Step-by-Step Instructions
  • Expected Output
  • Checkpoint: ESP32 Measurement Record
  • Try It: Latency Breakdown Explorer
  • Challenge Exercises
  • Measure Real Wi-Fi Performance
  • Add Adaptive Bitrate Simulation
  • Implement Congestion Control
  • Challenge 4: Multi-Sensor Competition
  • Troubleshooting
  • IoT Application: Why This Matters
  • Bandwidth Does Not Cut Latency
  • Checkpoint: Application Decision
  • Concept Review
  • Match: Metrics to Definitions
  • Order Performance Test Steps
  • Common Pitfalls
  • Measure Under Real Load
  • Report Percentiles, Not Averages
  • Control Background Traffic
  • Label the Diagram
  • Code Challenge
  • Summary
  • Knowledge Check
  • Quiz: Network Performance
  • What’s Next
  • Key Takeaway

For an illustrative LoRaWAN MAC frame carrying a 12-byte payload, assume four bytes of optional FOpts fields. Count the MAC frame only; radio preamble, coding, retries, and receive windows are outside this byte comparison:

The frame has a 1-byte MHDR, 7-byte minimum FHDR, 4-byte FOpts field, 1-byte FPort, and 4-byte Message Integrity Code (MIC):

Total packet size=12 (payload)+13 (MAC)+4 (MIC)=29 bytes\text{Total packet size} = 12 \text{ (payload)} + 13 \text{ (MAC)} + 4 \text{ (MIC)} = 29 \text{ bytes}

Protocol overhead ratio:

Overhead ratio=13+429=1729=58.6%\text{Overhead ratio} = \frac{13 + 4}{29} = \frac{17}{29} = 58.6\%

Efficiency (useful data / total transmission):

Efficiency=1229=41.4%\text{Efficiency} = \frac{12}{29} = 41.4\%

For a separate illustrative MQTT 3.1.1 QoS 0 PUBLISH to the three-byte topic a/b, count a 2-byte fixed header, 2-byte topic-length field, 3-byte topic, 20-byte TCP header, and 20-byte IPv4 header. This omits link framing, TCP options, security, and acknowledgements:

MQTT/IP bytes=12+2+2+3+20+20=59 bytes\text{MQTT/IP bytes} = 12 + 2 + 2 + 3 + 20 + 20 = 59 \text{ bytes} Efficiency=1259=20.3%\text{Efficiency} = \frac{12}{59} = 20.3\%

In these two specified byte-count examples, the LoRaWAN MAC frame’s application-payload fraction is about twice the MQTT/IPv4 packet’s. The gap widens for smaller payloads. They use different physical links, so this fraction alone does not compare airtime, energy, capacity, or end-to-end delivery.

Try It: Protocol Overhead Calculator

Read Figure 1 before the calculator, because it names the three quantities the calculator moves between.

Bandwidth is theoretical link capacity, throughput is delivered bits and goodput is received application data. Loss and retry reduce throughput; headers reduce useful payload.
Figure 1: Diagram showing the relationship between bandwidth, throughput, and goodput with typical loss factors

Start at bandwidth, the theoretical capacity of the link. The first arrow leads to throughput, the bits actually delivered, and the label on the way tells you what was taken: loss and retries. The second arrow leads to goodput, the application data that finally arrives, and there the cost is headers. So the chain in Figure 1 only ever runs downhill. Each step is a different kind of overhead, charged by a different part of the stack. When the calculator shows your payload shrinking, this diagram tells you which of the two steps to look at first.

Packet Pete, Networking and packet-journey domain expertCheckpoint: Metrics and Evidence

You now know:

  • A 1 Mbps link label is only a ceiling; the chapter examples show useful data can fall to 800 Kbps throughput, 600 Kbps goodput, or even 100 Kbps under overhead and congestion.
  • Goodput efficiency is useful payload divided by total transmitted bytes, such as 12 / 29 = 41.4% for the LoRaWAN example.
  • Latency, jitter, loss, and throughput are separate measurements, so a faster bandwidth number does not automatically fix a deadline miss.

The terms are now precise enough to test. The next step is to move from definitions into a controllable experiment where one slider at a time changes the result.

Test Before Hardware

This section: Test Before Hardware — Start here before building the ESP32 version.

Start here before building the ESP32 version. The controls below let you model the same performance ideas directly in the browser: bandwidth, offered load, latency, jitter, packet loss, and goodput. Begin with offered traffic comfortably below link capacity and record utilization, average latency, jitter range, loss, and delivered goodput. Then raise packet rate or packet size until utilization crosses the congestion region, observing which metric changes first. Finally vary base latency and loss independently so propagation, queueing, and delivery failure are not confused. Carry the same predicted thresholds into the hardware run and explain any difference with measured packet timing or implementation overhead.

Try It: Network Performance Workbench

Build this version only after you have tried the browser workbench. The extra hardware makes latency, congestion, and button-triggered tests visible on real LEDs.

ComponentQuantityPurpose
ESP32 DevKit1Microcontroller for network simulation
Red LED1High latency indicator (>200 ms)
Green LED1Low latency indicator (<50 ms)
Yellow LED1Medium latency/transmission active
Blue LED1Congestion detected indicator
RGB LED (optional)1Jitter visualization (color intensity)
220 ohm Resistors4-5Current limiting for LEDs
Push Button1Trigger network tests
10K ohm Resistor1Button pull-down
Breadboard1Circuit assembly
Jumper WiresSeveralConnections
Packet Pete, Networking and packet-journey domain expertCheckpoint: Simulate Before Wiring

You now know:

  • Offered load comes from packet size, packet rate, and link bandwidth, and the workbench makes queue delay visible when utilisation rises past the healthy region.
  • The optional circuit uses red, green, yellow, and blue LEDs to make latency and congestion observable instead of hidden in a log.
  • The hardware path should start after the browser model, so errors in wiring do not hide the performance idea being tested.

Circuit Diagram

This section: Circuit Diagram — The optional hardware exists to make queueing visible.

The optional hardware exists to make queueing visible. Figure 2 shows which part of the lab each component is there to expose.

An ESP32 network lab uses a button to inject traffic bursts, LEDs for packet movement, drops and acknowledgments, and USB serial for simulated statistics.
Figure 2: Circuit diagram for network performance lab showing ESP32 with LED indicators and button input

Everything in Figure 2 hangs off one ESP32 running the packet simulator, so the board is the model rather than a real network. The button is the only input; pressing it injects a burst of traffic, which is how you drive utilisation up on demand. The three LED groups then report what the burst did. The transmit and receive pair blinks as packets move, the drop indicator lights when the queue overflows, and the acknowledgement indicator lights when delivery succeeds. The serial link back to the computer carries the numbers. Lights give you the feel of congestion in real time; the printed statistics give you the evidence to quote afterwards.

Optional ESP32 Implementation

This section: Optional ESP32 Implementation — Use the ESP32 version after the browser workbench.

Use the ESP32 version after the browser workbench. Build it as a set of small behaviours so students can test each performance idea separately.

ESP32 pieceWhat to implementHow to verify it works
Bandwidth vs throughputCount total bytes sent and useful payload bytesThroughput is lower than raw bandwidth when overhead is included
Latency profileTimestamp send and receive eventsSerial output reports RTT and LED color follows the delay range
Jitter analysisRecord several RTT samplesThe result shows variation, not just one average
Congestion testIncrease packet rate or background loadBlue congestion LED turns on and queue delay increases
Packet loss simulationDrop a controlled percentage of packetsResults include retransmissions and goodput reduction
/*
 * =============================================================================
 * Network Performance Measurement Simulator for ESP32
 * =============================================================================
 *
 * This comprehensive lab demonstrates key network performance concepts:
 *
 * 1. BANDWIDTH vs THROUGHPUT
 *    - Bandwidth: Maximum theoretical capacity (simulated link speed)
 *    - Throughput: Actual achieved data rate (affected by overhead, errors)
 *
 * 2. LATENCY MEASUREMENT
 *    - Base latency: Propagation delay through the network
 *    - Processing latency: Time to encode/decode data
 *    - Queuing latency: Time waiting in buffers
 *    - Total Round-Trip Time (RTT): Complete send-receive cycle
 *
 * 3. JITTER ANALYSIS
 *    - Jitter: Variation in latency between packets
 *    - Critical for real-time applications (voice, video, sensor streams)
 *    - Measured as standard deviation of latency samples
 *
 * 4. CONGESTION EFFECTS
 *    - How competing traffic affects performance
 *    - Queue buildup and increased latency
 *    - Packet loss under heavy load
 *
 * 5. GOODPUT CALCULATION
 *    - Useful application data vs total transmitted data
 *    - Impact of protocol overhead and retransmissions
 *
 * LED Indicators:
 * - Green (GPIO 4):  Simulated latency under 50ms
 * - Yellow (GPIO 5): Simulated latency from 50-200ms / transmission
 * - Red (GPIO 2):    Simulated latency over 200ms
 * - Blue (GPIO 18):  Congestion detected - queue backup or packet loss
 *
 * Button (GPIO 15): Press to run a network performance test cycle
 *
 * Author: IoT Learning Platform
 * License: Educational Use
 */

#include <Arduino.h>
#include <math.h>

// =============================================================================
// PIN DEFINITIONS
// =============================================================================

#define LED_HIGH_LATENCY   2    // Red LED - latency > 200ms
#define LED_LOW_LATENCY    4    // Green LED - latency < 50ms
#define LED_TRANSMIT       5    // Yellow LED - medium latency / transmitting
#define LED_CONGESTION     18   // Blue LED - congestion detected
#define BUTTON_PIN         15   // Push button to trigger tests

// =============================================================================
// NETWORK SIMULATION PARAMETERS
// =============================================================================

// Simulated network characteristics (adjustable)
#define SIMULATED_BANDWIDTH_KBPS    1000    // 1 Mbps theoretical bandwidth
#define BASE_LATENCY_MS             25      // Base propagation delay
#define PROCESSING_LATENCY_MS       5       // Encoding/decoding time
#define MAX_QUEUE_SIZE              10      // Packets that can be queued

// Packet parameters
#define PACKET_SIZE_BYTES           100     // Data payload size
#define HEADER_OVERHEAD_BYTES       40      // TCP/IP header overhead
#define MAX_PACKETS_PER_TEST        20      // Packets to send per test

// Congestion simulation
#define CONGESTION_THRESHOLD        0.7     // Illustrative queue onset for this toy model
#define PACKET_LOSS_PROBABILITY     0.05    // 5% base packet loss rate

// Jitter parameters
#define MAX_JITTER_MS               30      // Maximum random jitter

// =============================================================================
// DATA STRUCTURES
// =============================================================================

// Statistics for a single packet transmission
struct PacketStats {
    uint32_t sequenceNumber;
    uint32_t sendTime;
    uint32_t receiveTime;
    uint32_t latency;
    bool wasLost;
    bool wasRetransmitted;
    uint16_t payloadSize;
    uint16_t totalSize;  // payload + overhead
};

// Aggregate statistics for a test run
struct TestResults {
    // Timing metrics
    float avgLatency;
    float minLatency;
    float maxLatency;
    float jitter;           // Standard deviation of latency

    // Throughput metrics
    float theoreticalBandwidthKbps;
    float achievedThroughputKbps;
    float goodputKbps;
    float efficiency;       // delivered application bits / delivered frame bits

    // Reliability metrics
    uint32_t packetsSent;
    uint32_t packetsReceived;
    uint32_t packetsLost;
    uint32_t packetsRetransmitted;
    float packetLossRate;

    // Congestion indicators
    bool congestionDetected;
    uint32_t maxQueueDepth;
    float avgQueueDepth;
};

// Network state simulation
struct NetworkState {
    float currentUtilization;   // 0.0 to 1.0
    uint32_t queueDepth;        // Current packets in queue
    bool isCongested;
    uint32_t backgroundTraffic; // Simulated competing traffic (bytes/sec)
};

// =============================================================================
// GLOBAL VARIABLES
// =============================================================================

PacketStats packetHistory[MAX_PACKETS_PER_TEST];
TestResults currentTest;
NetworkState network;

uint32_t testNumber = 0;
bool buttonPressed = false;
bool testInProgress = false;
uint32_t lastButtonDebounce = 0;

// Latency samples for jitter calculation
float latencySamples[MAX_PACKETS_PER_TEST];
int sampleCount = 0;

// =============================================================================
// FUNCTION PROTOTYPES
// =============================================================================

void initializeHardware();
void initializeNetwork();
void updateLEDs(float latency, bool congested);
void runPerformanceTest();
float simulateLatency(bool congested);
float simulateJitter();
bool simulatePacketLoss(float congestionLevel);
float calculateThroughput(uint32_t bytesTransmitted, uint32_t durationMs);
float calculateJitter(float* samples, int count);
float calculateStandardDeviation(float* samples, int count, float mean);
void printTestHeader(int testNum);
void printPacketDetails(PacketStats* pkt);
void printTestSummary(TestResults* results);
void printBandwidthVsThroughput();
void printLatencyBreakdown();
void printJitterAnalysis();
void printCongestionAnalysis();
void printPerformanceGrade();
void demonstrateBandwidthConcept();
void demonstrateLatencyConcept();
void demonstrateJitterConcept();
void demonstrateCongestionConcept();
void runAutomatedDemo();
void blinkLED(int pin, int times, int delayMs);
void setAllLEDs(bool state);

// =============================================================================
// SETUP
// =============================================================================

void setup() {
    Serial.begin(115200);
    delay(1000);  // Allow serial to initialize

    printWelcomeBanner();
    initializeHardware();
    initializeNetwork();

    Serial.println("\n[READY] Press the button to run a network performance test");
    Serial.println("        Or wait for automatic demonstration cycles\n");
}

// =============================================================================
// MAIN LOOP
// =============================================================================

void loop() {
    // Check for button press with debouncing
    if (digitalRead(BUTTON_PIN) == HIGH && !buttonPressed) {
        if (millis() - lastButtonDebounce > 200) {  // 200ms debounce
            buttonPressed = true;
            lastButtonDebounce = millis();

            if (!testInProgress) {
                Serial.println("\n[BUTTON] Manual test triggered!\n");
                runPerformanceTest();
            }
        }
    } else if (digitalRead(BUTTON_PIN) == LOW) {
        buttonPressed = false;
    }

    // Run automated demo every 15 seconds if not testing
    static uint32_t lastAutoDemo = 0;
    if (!testInProgress && millis() - lastAutoDemo > 15000) {
        lastAutoDemo = millis();
        runAutomatedDemo();
    }

    delay(10);  // Small delay for stability
}

// =============================================================================
// INITIALIZATION FUNCTIONS
// =============================================================================

void printWelcomeBanner() {
    Serial.println("\n");
    Serial.println("+=================================================================+");
    Serial.println("|     NETWORK PERFORMANCE MEASUREMENT SIMULATOR v2.0              |");
    Serial.println("|     ESP32 IoT Learning Lab - Understanding Network Metrics      |");
    Serial.println("+=================================================================+");
    Serial.println("|  This lab teaches:                                              |");
    Serial.println("|  - Bandwidth vs Throughput - Why they're different              |");
    Serial.println("|  - Latency Measurement - RTT and its components                 |");
    Serial.println("|  - Jitter Analysis - Variation affects real-time apps           |");
    Serial.println("|  - Congestion Effects - How traffic overload degrades networks  |");
    Serial.println("|  - Goodput Calculation - Useful data vs total transmitted       |");
    Serial.println("+=================================================================+");
    Serial.println();
}

void initializeHardware() {
    Serial.println("[INIT] Configuring GPIO pins...");

    // Configure LED pins as outputs
    pinMode(LED_HIGH_LATENCY, OUTPUT);
    pinMode(LED_LOW_LATENCY, OUTPUT);
    pinMode(LED_TRANSMIT, OUTPUT);
    pinMode(LED_CONGESTION, OUTPUT);

    // Configure button pin as input
    pinMode(BUTTON_PIN, INPUT);

    // Initial LED test - all off
    setAllLEDs(false);

    // LED self-test sequence
    Serial.println("[INIT] Running LED self-test...");

    Serial.println("       Testing: Green LED (Low Latency)");
    blinkLED(LED_LOW_LATENCY, 2, 200);

    Serial.println("       Testing: Yellow LED (Transmitting)");
    blinkLED(LED_TRANSMIT, 2, 200);

    Serial.println("       Testing: Red LED (High Latency)");
    blinkLED(LED_HIGH_LATENCY, 2, 200);

    Serial.println("       Testing: Blue LED (Congestion)");
    blinkLED(LED_CONGESTION, 2, 200);

    Serial.println("[INIT] Hardware initialization complete!\n");
}

void initializeNetwork() {
    Serial.println("[INIT] Initializing simulated network...");

    network.currentUtilization = 0.1;   // Start with 10% background utilization
    network.queueDepth = 0;
    network.isCongested = false;
    network.backgroundTraffic = SIMULATED_BANDWIDTH_KBPS * 1000 / 8 / 10;  // 10% of 1,000,000 bit/s = 12,500 bytes/s

    Serial.printf("       Simulated Bandwidth: %d Kbps (%.2f Mbps)\n",
                  SIMULATED_BANDWIDTH_KBPS, SIMULATED_BANDWIDTH_KBPS / 1000.0);
    Serial.printf("       Base Latency: %d ms\n", BASE_LATENCY_MS);
    Serial.printf("       Max Queue Size: %d packets\n", MAX_QUEUE_SIZE);
    Serial.printf("       Base Packet Loss: %.1f%%\n", PACKET_LOSS_PROBABILITY * 100);
    Serial.println("[INIT] Network simulation ready!\n");
}

// =============================================================================
// MAIN TEST FUNCTION
// =============================================================================

void runPerformanceTest() {
    testInProgress = true;
    testNumber++;

    printTestHeader(testNumber);

    // Reset test results
    memset(&currentTest, 0, sizeof(TestResults));
    memset(packetHistory, 0, sizeof(packetHistory));
    sampleCount = 0;

    currentTest.theoreticalBandwidthKbps = SIMULATED_BANDWIDTH_KBPS;
    currentTest.minLatency = 999999;

    // Simulate varying network conditions for this test
    float congestionLevel = (testNumber % 4) * 0.25;  // Four illustrative load steps
    network.currentUtilization = 0.2 + congestionLevel;  // 20%, 45%, 70%, 95%
    network.queueDepth = 0;
    network.isCongested = (network.currentUtilization > CONGESTION_THRESHOLD);

    Serial.printf("\n[CONFIG] Network Conditions for Test %lu:\n", testNumber);
    Serial.printf("         Utilization Level: %.0f%%\n", network.currentUtilization * 100);
    Serial.printf("         Congestion Status: %s\n", network.isCongested ? "CONGESTED" : "Normal");
    Serial.println();

    // Update congestion LED
    digitalWrite(LED_CONGESTION, network.isCongested);

    uint32_t totalBytesTransmitted = 0;
    uint32_t totalBytesDelivered = 0;
    uint32_t totalUsefulBytes = 0;
    uint32_t simulatedDurationMs = 0;

    // Transmit packets
    Serial.println("+----------------------------------------------------------------+");
    Serial.println("|                    PACKET TRANSMISSION LOG                      |");
    Serial.println("+----------------------------------------------------------------+");

    for (int i = 0; i < MAX_PACKETS_PER_TEST; i++) {
        PacketStats* pkt = &packetHistory[i];
        pkt->sequenceNumber = i + 1;
        pkt->payloadSize = PACKET_SIZE_BYTES;
        pkt->totalSize = PACKET_SIZE_BYTES + HEADER_OVERHEAD_BYTES;

        // Indicate transmission start
        digitalWrite(LED_TRANSMIT, HIGH);

        // Record send time
        pkt->sendTime = millis();

        // Simulate network latency
        float latency = simulateLatency(network.isCongested);
        pkt->latency = (uint32_t)latency;

        // Simulate potential packet loss
        bool initialAttemptLost = simulatePacketLoss(network.currentUtilization);
        pkt->wasLost = initialAttemptLost;

        if (initialAttemptLost) {
            // One retry is attempted; it can also fail.
            pkt->wasRetransmitted = true;
            latency += simulateLatency(network.isCongested);  // Add retransmission delay
            pkt->latency = (uint32_t)latency;
            pkt->wasLost = simulatePacketLoss(network.currentUtilization);
            currentTest.packetsRetransmitted++;
        }

        // Simulate the actual delay
        delay((uint32_t)(latency / 10));  // Scaled down for demo (divide by 10)

        pkt->receiveTime = pkt->sendTime + pkt->latency;

        // Update LED based on latency
        updateLEDs(latency, network.isCongested);

        // End transmission indication
        digitalWrite(LED_TRANSMIT, LOW);

        // Update statistics
        if (!pkt->wasLost) {
            currentTest.packetsReceived++;
            totalBytesDelivered += pkt->totalSize;
            totalUsefulBytes += pkt->payloadSize;
        } else {
            currentTest.packetsLost++;
        }

        totalBytesTransmitted += pkt->totalSize * (pkt->wasRetransmitted ? 2 : 1);
        currentTest.packetsSent++;
        simulatedDurationMs += (uint32_t)latency + 50;

        // Only delivered packets contribute to observed RTT and jitter.
        if (!pkt->wasLost) {
            latencySamples[sampleCount++] = latency;
            if (latency < currentTest.minLatency) currentTest.minLatency = latency;
            if (latency > currentTest.maxLatency) currentTest.maxLatency = latency;
        }

        // Print packet details
        printPacketDetails(pkt);

        // Small delay between packets
        delay(50);
    }

    Serial.println("+----------------------------------------------------------------+");

    // Calculate final statistics
    float sumLatency = 0;
    for (int i = 0; i < sampleCount; i++) {
        sumLatency += latencySamples[i];
    }
    currentTest.avgLatency = sampleCount ? sumLatency / sampleCount : 0;
    if (!sampleCount) currentTest.minLatency = 0;

    // Calculate jitter (standard deviation of latency)
    currentTest.jitter = calculateJitter(latencySamples, sampleCount);

    // Calculate throughput and goodput
    currentTest.achievedThroughputKbps = calculateThroughput(totalBytesDelivered, simulatedDurationMs);
    currentTest.goodputKbps = calculateThroughput(totalUsefulBytes, simulatedDurationMs);
    currentTest.efficiency = totalBytesDelivered ? (float)totalUsefulBytes / totalBytesDelivered : 0;

    // Calculate packet loss rate
    currentTest.packetLossRate = (float)currentTest.packetsLost / currentTest.packetsSent;
    currentTest.congestionDetected = network.isCongested;

    // Print comprehensive summary
    Serial.printf("[MODEL] %lu attempted frame bytes (including retries); %lu delivered frame bytes over %lu simulated ms. No physical network was measured.\n",
                  totalBytesTransmitted, totalBytesDelivered, simulatedDurationMs);
    printTestSummary(&currentTest);
    printBandwidthVsThroughput();
    printLatencyBreakdown();
    printJitterAnalysis();
    printCongestionAnalysis();
    printPerformanceGrade();

    // Turn off all LEDs
    setAllLEDs(false);

    Serial.println("\n[COMPLETE] Test finished. Press button for another test or wait for auto-demo.\n");

    testInProgress = false;
}

// =============================================================================
// SIMULATION FUNCTIONS
// =============================================================================

float simulateLatency(bool congested) {
    float baseLatency = BASE_LATENCY_MS + PROCESSING_LATENCY_MS;
    float jitter = simulateJitter();
    float congestionDelay = 0;

    if (congested) {
        // Toy queue-delay curve, not a general queueing law.
        congestionDelay = pow(2, network.queueDepth) * 5;  // Doubles for each queued packet
        if (congestionDelay > 500) congestionDelay = 500;  // Cap at 500ms

        // Simulate queue buildup
        if (random(100) < 30) {  // 30% chance to increase queue
            network.queueDepth = min(network.queueDepth + 1, (uint32_t)MAX_QUEUE_SIZE);
        } else if (network.queueDepth > 0 && random(100) < 50) {
            network.queueDepth--;
        }
    }

    return max(0.0f, baseLatency + jitter + congestionDelay);
}

float simulateJitter() {
    // Three uniform draws provide an illustrative centered variation.
    float jitter = 0;
    for (int i = 0; i < 3; i++) {
        jitter += random(-MAX_JITTER_MS, MAX_JITTER_MS);
    }
    jitter /= 3;

    return jitter;
}

bool simulatePacketLoss(float congestionLevel) {
    float lossProb = PACKET_LOSS_PROBABILITY;

    // Increase loss probability during congestion
    if (congestionLevel > CONGESTION_THRESHOLD) {
        lossProb += (congestionLevel - CONGESTION_THRESHOLD) * 0.3;  // Up to +9% loss at 100%
    }

    return (random(1000) / 1000.0) < lossProb;
}

// =============================================================================
// CALCULATION FUNCTIONS
// =============================================================================

float calculateThroughput(uint32_t bytesTransmitted, uint32_t durationMs) {
    if (durationMs == 0) return 0;

    // Convert to Kbps: (bytes * 8 bits/byte) / durationMs = Kbits/s
    return (float)(bytesTransmitted * 8) / durationMs;
}

float calculateJitter(float* samples, int count) {
    if (count < 2) return 0;

    float sum = 0;
    for (int i = 0; i < count; i++) {
        sum += samples[i];
    }
    float mean = sum / count;

    return calculateStandardDeviation(samples, count, mean);
}

float calculateStandardDeviation(float* samples, int count, float mean) {
    if (count < 2) return 0;

    float sumSquaredDiff = 0;
    for (int i = 0; i < count; i++) {
        float diff = samples[i] - mean;
        sumSquaredDiff += diff * diff;
    }

    return sqrt(sumSquaredDiff / (count - 1));
}

// =============================================================================
// LED CONTROL FUNCTIONS
// =============================================================================

void updateLEDs(float latency, bool congested) {
    // Turn off all status LEDs first
    digitalWrite(LED_LOW_LATENCY, LOW);
    digitalWrite(LED_TRANSMIT, LOW);
    digitalWrite(LED_HIGH_LATENCY, LOW);

    // Set appropriate LED based on latency
    if (latency < 50) {
        digitalWrite(LED_LOW_LATENCY, HIGH);   // Green - excellent
    } else if (latency < 200) {
        digitalWrite(LED_TRANSMIT, HIGH);       // Yellow - acceptable
    } else {
        digitalWrite(LED_HIGH_LATENCY, HIGH);   // Red - poor
    }

    // Congestion LED
    digitalWrite(LED_CONGESTION, congested);
}

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 setAllLEDs(bool state) {
    digitalWrite(LED_LOW_LATENCY, state);
    digitalWrite(LED_TRANSMIT, state);
    digitalWrite(LED_HIGH_LATENCY, state);
    digitalWrite(LED_CONGESTION, state);
}

// =============================================================================
// PRINTING FUNCTIONS
// =============================================================================

void printTestHeader(int testNum) {
    Serial.println("\n+=================================================================+");
    Serial.printf("|                    PERFORMANCE TEST #%d                          |\n", testNum);
    Serial.println("+=================================================================+");
    Serial.println("|  Simulating: Link rate, Throughput, Latency, Jitter, Goodput    |");
    Serial.println("+=================================================================+");
}

void printPacketDetails(PacketStats* pkt) {
    char status[20];
    char latencyBar[21];

    // Create visual latency bar
    int barLength = min((int)(pkt->latency / 25), 20);  // Scale to max 20 chars
    for (int i = 0; i < 20; i++) {
        latencyBar[i] = (i < barLength) ? '#' : '-';
    }
    latencyBar[20] = '\0';

    // Determine status
    if (pkt->wasLost) {
        strcpy(status, "LOST");
    } else if (pkt->wasRetransmitted) {
        strcpy(status, "RETX");
    } else if (pkt->latency < 50) {
        strcpy(status, "OK  ");
    } else if (pkt->latency < 200) {
        strcpy(status, "SLOW");
    } else {
        strcpy(status, "POOR");
    }

    Serial.printf("| PKT #%02lu | %4lu ms | [%s] | %s | %d+%d bytes |\n",
                  pkt->sequenceNumber,
                  pkt->latency,
                  latencyBar,
                  status,
                  pkt->payloadSize,
                  HEADER_OVERHEAD_BYTES);
}

void printTestSummary(TestResults* results) {
    Serial.println("\n+=================================================================+");
    Serial.println("|                       TEST RESULTS SUMMARY                       |");
    Serial.println("+=================================================================+");

    Serial.println("|                                                                 |");
    Serial.println("|  LATENCY METRICS:                                               |");
    Serial.printf("|    Average Latency:    %7.1f ms                                |\n", results->avgLatency);
    Serial.printf("|    Minimum Latency:    %7.1f ms                                |\n", results->minLatency);
    Serial.printf("|    Maximum Latency:    %7.1f ms                                |\n", results->maxLatency);
    Serial.printf("|    Jitter (StdDev):    %7.1f ms                                |\n", results->jitter);

    Serial.println("|                                                                 |");
    Serial.println("|  THROUGHPUT METRICS:                                            |");
    Serial.printf("|    Configured link rate:%7.1f Kbps                              |\n", results->theoreticalBandwidthKbps);
    Serial.printf("|    Modeled delivered rate:%7.1f Kbps                            |\n", results->achievedThroughputKbps);
    Serial.printf("|    Goodput (Useful):   %7.1f Kbps                              |\n", results->goodputKbps);
    Serial.printf("|    Payload fraction:   %7.1f%%                                 |\n", results->efficiency * 100);

    Serial.println("|                                                                 |");
    Serial.println("|  RELIABILITY METRICS:                                           |");
    Serial.printf("|    Packets Sent:       %7lu                                   |\n", results->packetsSent);
    Serial.printf("|    Packets Received:   %7lu                                   |\n", results->packetsReceived);
    Serial.printf("|    Packets Lost:       %7lu                                   |\n", results->packetsLost);
    Serial.printf("|    Retransmissions:    %7lu                                   |\n", results->packetsRetransmitted);
    Serial.printf("|    Packet Loss Rate:   %7.1f%%                                 |\n", results->packetLossRate * 100);

    Serial.println("|                                                                 |");
    Serial.println("+=================================================================+");
}

void printBandwidthVsThroughput() {
    Serial.println("\n+----------------------------------------------------------------+");
    Serial.println("|           CONCEPT: BANDWIDTH vs THROUGHPUT vs GOODPUT          |");
    Serial.println("+----------------------------------------------------------------+");
    Serial.println("|                                                                |");
    Serial.println("|  Bandwidth (Theoretical Maximum):                              |");
    Serial.printf("|  [##################################################] %4.0f Kbps |\n",
                  currentTest.theoreticalBandwidthKbps);
    Serial.println("|                                                                |");

    // Calculate bar lengths
    int throughputBar = (int)(currentTest.achievedThroughputKbps / currentTest.theoreticalBandwidthKbps * 50);
    int goodputBar = (int)(currentTest.goodputKbps / currentTest.theoreticalBandwidthKbps * 50);

    Serial.println("|  Throughput (modeled delivered rate):                          |");
    Serial.print("|  [");
    for (int i = 0; i < 50; i++) Serial.print(i < throughputBar ? "#" : "-");
    Serial.printf("] %4.0f Kbps |\n", currentTest.achievedThroughputKbps);

    Serial.println("|                                                                |");
    Serial.println("|  Goodput (Useful Application Data):                            |");
    Serial.print("|  [");
    for (int i = 0; i < 50; i++) Serial.print(i < goodputBar ? "#" : "-");
    Serial.printf("] %4.0f Kbps |\n", currentTest.goodputKbps);

    Serial.println("|                                                                |");
    Serial.println("|  WHY THE DIFFERENCE?                                           |");
    Serial.printf("|  - Protocol Overhead: %d bytes per %d-byte packet (%.0f%%)         |\n",
                  HEADER_OVERHEAD_BYTES, PACKET_SIZE_BYTES,
                  (float)HEADER_OVERHEAD_BYTES / (PACKET_SIZE_BYTES + HEADER_OVERHEAD_BYTES) * 100);
    Serial.printf("|  - Retransmissions: %lu packets resent                           |\n",
                  currentTest.packetsRetransmitted);
    Serial.printf("|  - Delivered payload fraction: %.1f%% of delivered frame bytes   |\n",
                  currentTest.efficiency * 100);
    Serial.println("|                                                                |");
    Serial.println("+----------------------------------------------------------------+");
}

void printLatencyBreakdown() {
    Serial.println("\n+----------------------------------------------------------------+");
    Serial.println("|                    LATENCY BREAKDOWN                            |");
    Serial.println("+----------------------------------------------------------------+");
    Serial.println("|                                                                |");
    Serial.println("|  Total RTT (Round-Trip Time) Components:                       |");
    Serial.println("|                                                                |");
    Serial.printf("|  +- Propagation Delay:  %3d ms (signal travel time)            |\n", BASE_LATENCY_MS);
    Serial.printf("|  +- Processing Delay:   %3d ms (encode/decode)                 |\n", PROCESSING_LATENCY_MS);
    Serial.printf("|  +- Queuing Delay:      %3.0f ms (buffer waiting)                |\n",
                  currentTest.avgLatency - BASE_LATENCY_MS - PROCESSING_LATENCY_MS - (currentTest.jitter / 2));
    Serial.printf("|  +- Jitter Variation:  +/-%3.0f ms (timing variance)             |\n", currentTest.jitter);
    Serial.println("|  ----------------------------------------------------------    |");
    Serial.printf("|  = Modeled Avg RTT:    %3.0f ms                                  |\n", currentTest.avgLatency);
    Serial.println("|                                                                |");
    Serial.println("|  LATENCY RATINGS:                                              |");
    Serial.println("|  Typical guideline: <50 ms excellent for interactive apps    |");
    Serial.println("|  Typical guideline: 50-150 ms good for most IoT apps          |");
    Serial.println("|  Typical guideline: 150-300 ms acceptable for monitoring     |");
    Serial.println("|  Typical guideline: >300 ms poor for interaction             |");
    Serial.println("|                                                                |");
    Serial.printf("|  Your Result: %.0fms = %s                                       |\n",
                  currentTest.avgLatency,
                  currentTest.avgLatency < 50 ? "EXCELLENT" :
                  currentTest.avgLatency < 150 ? "GOOD" :
                  currentTest.avgLatency < 300 ? "ACCEPTABLE" : "POOR");
    Serial.println("|                                                                |");
    Serial.println("+----------------------------------------------------------------+");
}

void printJitterAnalysis() {
    Serial.println("\n+----------------------------------------------------------------+");
    Serial.println("|                      JITTER ANALYSIS                            |");
    Serial.println("+----------------------------------------------------------------+");
    Serial.println("|                                                                |");
    Serial.println("|  What is Jitter?                                               |");
    Serial.println("|  Jitter = Variation in packet latency over time                |");
    Serial.println("|  Measured as standard deviation of latency samples             |");
    Serial.println("|                                                                |");
    Serial.println("|  Latency Distribution:                                         |");
    Serial.println("|                                                                |");

    // Create ASCII histogram of latency distribution
    int histogram[5] = {0, 0, 0, 0, 0};  // 0-50, 50-100, 100-150, 150-200, 200+
    for (int i = 0; i < sampleCount; i++) {
        int bucket = min((int)(latencySamples[i] / 50), 4);
        histogram[bucket]++;
    }

    const char* labels[] = {"  0-50ms", " 50-100ms", "100-150ms", "150-200ms", "200+ms"};
    for (int b = 0; b < 5; b++) {
        int barLen = (histogram[b] * 30) / MAX_PACKETS_PER_TEST;
        Serial.printf("|  %s: ", labels[b]);
        for (int j = 0; j < 30; j++) {
            Serial.print(j < barLen ? "#" : ".");
        }
        Serial.printf(" (%d)     |\n", histogram[b]);
    }

    Serial.println("|                                                                |");
    Serial.printf("|  Jitter (StdDev): %.1f ms                                       |\n", currentTest.jitter);
    Serial.println("|                                                                |");
    Serial.println("|  JITTER IMPACT BY APPLICATION (typical guideline):           |");
    Serial.println("|  - VoIP/Video:    <30 ms (buffer to smooth)                   |");
    Serial.println("|  - Real-time IoT: <50 ms preferred (sensor fusion)            |");
    Serial.println("|  - Monitoring:    <200 ms acceptable (dashboard display)     |");
    Serial.println("|                                                                |");
    Serial.printf("|  Modeled RTT standard deviation: %.1f ms (demo band: %s)      |\n",
                  currentTest.jitter,
                  currentTest.jitter < 30 ? "<30 ms" :
                  currentTest.jitter < 50 ? "30-49 ms" :
                  currentTest.jitter < 100 ? "50-99 ms" : ">=100 ms");
    Serial.println("|                                                                |");
    Serial.println("+----------------------------------------------------------------+");
}

void printCongestionAnalysis() {
    Serial.println("\n+----------------------------------------------------------------+");
    Serial.println("|                   CONGESTION ANALYSIS                           |");
    Serial.println("+----------------------------------------------------------------+");
    Serial.println("|                                                                |");

    if (currentTest.congestionDetected) {
        Serial.println("|  [!] CONGESTION DETECTED                                       |");
        Serial.println("|                                                                |");
        Serial.printf("|  Network Utilization: %.0f%% (threshold: %.0f%%)                  |\n",
                      network.currentUtilization * 100, CONGESTION_THRESHOLD * 100);
        Serial.println("|                                                                |");
        Serial.println("|  Congestion Effects Observed:                                  |");
        Serial.printf("|  - Increased Latency: +%.0fms above baseline                    |\n",
                      currentTest.avgLatency - BASE_LATENCY_MS - PROCESSING_LATENCY_MS);
        Serial.printf("|  - Packet Loss Rate: %.1f%%                                     |\n",
                      currentTest.packetLossRate * 100);
        Serial.printf("|  - Retransmissions Required: %lu                                |\n",
                      currentTest.packetsRetransmitted);
        Serial.println("|                                                                |");
        Serial.println("|  CONGESTION MITIGATION STRATEGIES:                             |");
        Serial.println("|  1. Traffic shaping: Rate-limit non-critical traffic           |");
        Serial.println("|  2. QoS: Prioritize critical IoT sensor data                   |");
        Serial.println("|  3. Load balancing: Distribute across multiple paths           |");
        Serial.println("|  4. Reduce payload: Compress or aggregate sensor readings      |");
    } else {
        Serial.println("|  [OK] NO CONGESTION - Network operating normally               |");
        Serial.println("|                                                                |");
        Serial.printf("|  Network Utilization: %.0f%% (below threshold: %.0f%%)            |\n",
                      network.currentUtilization * 100, CONGESTION_THRESHOLD * 100);
        Serial.println("|                                                                |");
        Serial.println("|  Network Health Indicators:                                    |");
        Serial.println("|  - Queue depth: Minimal                                        |");
        Serial.printf("|  - Packet loss: %.1f%% (within normal range)                    |\n",
                      currentTest.packetLossRate * 100);
        Serial.println("|  - Latency: Stable and predictable                             |");
    }

    Serial.println("|                                                                |");
    Serial.println("+----------------------------------------------------------------+");
}

void printPerformanceGrade() {
    Serial.println("\n+=================================================================+");
    Serial.println("|                    OVERALL PERFORMANCE GRADE                     |");
    Serial.println("+=================================================================+");

    // Calculate overall score (0-100)
    int score = 100;

    // Deduct for latency
    if (currentTest.avgLatency > 50) score -= 10;
    if (currentTest.avgLatency > 150) score -= 15;
    if (currentTest.avgLatency > 300) score -= 20;

    // Deduct for jitter
    if (currentTest.jitter > 30) score -= 10;
    if (currentTest.jitter > 50) score -= 10;
    if (currentTest.jitter > 100) score -= 15;

    // Deduct for packet loss
    score -= (int)(currentTest.packetLossRate * 100);

    // Deduct for low efficiency
    if (currentTest.efficiency < 0.7) score -= 10;
    if (currentTest.efficiency < 0.5) score -= 15;

    score = max(0, score);

    char grade;
    const char* gradeDesc;
    if (score >= 90) { grade = 'A'; gradeDesc = "Top demo band - compare with your requirements"; }
    else if (score >= 80) { grade = 'B'; gradeDesc = "Second demo band - inspect each metric"; }
    else if (score >= 70) { grade = 'C'; gradeDesc = "Middle demo band - inspect each metric"; }
    else if (score >= 60) { grade = 'D'; gradeDesc = "Low demo band - inspect each metric"; }
    else { grade = 'F'; gradeDesc = "Bottom demo band - inspect each metric"; }

    Serial.println("|                                                                 |");
    Serial.printf("|                      SCORE: %d/100                               |\n", score);
    Serial.printf("|                      GRADE: %c                                   |\n", grade);
    Serial.printf("|                                                                 |\n");
    Serial.printf("|  %s               |\n", gradeDesc);
    Serial.println("|                                                                 |");
    Serial.println("|  Toy score deducts for demo latency, jitter, loss and         |");
    Serial.println("|  payload-fraction bands. It is not a deployment pass/fail.     |");
    Serial.println("|                                                                 |");
    Serial.println("+=================================================================+");
}

// =============================================================================
// AUTOMATED DEMONSTRATION
// =============================================================================

void runAutomatedDemo() {
    static int demoStep = 0;

    switch (demoStep) {
        case 0:
            demonstrateBandwidthConcept();
            break;
        case 1:
            demonstrateLatencyConcept();
            break;
        case 2:
            demonstrateJitterConcept();
            break;
        case 3:
            demonstrateCongestionConcept();
            break;
        case 4:
            runPerformanceTest();
            break;
    }

    demoStep = (demoStep + 1) % 5;
}

void demonstrateBandwidthConcept() {
    Serial.println("\n+=================================================================+");
    Serial.println("|           MINI-LESSON: BANDWIDTH vs THROUGHPUT                  |");
    Serial.println("+=================================================================+");
    Serial.println();
    Serial.println("Think of BANDWIDTH as the SIZE of a water pipe:");
    Serial.println();
    Serial.println("  Bandwidth = Maximum theoretical capacity");
    Serial.println("  +======================================+");
    Serial.println("  |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ->   |  1 Mbps pipe");
    Serial.println("  +======================================+");
    Serial.println();
    Serial.println("Think of THROUGHPUT as how much water ACTUALLY flows:");
    Serial.println();
    Serial.println("  +======================================+");
    Serial.println("  |  ~~~~~~~~~~~[X][X]~~~~~~~~~ ->      |  800 Kbps actual");
    Serial.println("  +======================================+");
    Serial.println("              ^   ^");
    Serial.println("          Overhead & Losses");
    Serial.println();
    Serial.println("WHY THE DIFFERENCE?");
    Serial.println("- Protocol headers consume space (TCP/IP = 40+ bytes/packet)");
    Serial.println("- Retransmissions waste capacity");
    Serial.println("- Congestion causes queuing delays");
    Serial.println("- Physical layer overhead (preambles, gaps)");
    Serial.println();

    // Visual LED demo
    digitalWrite(LED_LOW_LATENCY, HIGH);
    delay(500);
    digitalWrite(LED_TRANSMIT, HIGH);
    delay(500);
    setAllLEDs(false);
}

void demonstrateLatencyConcept() {
    Serial.println("\n+=================================================================+");
    Serial.println("|               MINI-LESSON: UNDERSTANDING LATENCY                |");
    Serial.println("+=================================================================+");
    Serial.println();
    Serial.println("LATENCY = Time for data to travel from source to destination");
    Serial.println();
    Serial.println("  [Sensor] ----------------------------------------> [Cloud]");
    Serial.println("           <--------- RTT (Round-Trip Time) ------->");
    Serial.println();
    Serial.println("Components of Latency:");
    Serial.println();
    Serial.println("  1. Propagation Delay (25ms)");
    Serial.println("     +- Speed of light through fiber/copper");
    Serial.println();
    Serial.println("  2. Processing Delay (5ms)");
    Serial.println("     +- CPU time to encode/decode packets");
    Serial.println();
    Serial.println("  3. Queuing Delay (0-500ms)");
    Serial.println("     +- Time waiting in router buffers");
    Serial.println();
    Serial.println("  4. Transmission Delay");
    Serial.println("     +- Time to push bits onto the wire");
    Serial.println();
    Serial.println("IMPORTANT: Bandwidth does NOT reduce latency!");
    Serial.println("A 10 Gbps link has the same propagation delay as 1 Mbps.");
    Serial.println();

    // Demo: Show latency progression
    Serial.println("Simulating latency levels...");

    digitalWrite(LED_LOW_LATENCY, HIGH);
    Serial.println("  [GREEN]  Demo RTT under 50ms");
    delay(1000);
    digitalWrite(LED_LOW_LATENCY, LOW);

    digitalWrite(LED_TRANSMIT, HIGH);
    Serial.println("  [YELLOW] Demo RTT from 50-200ms");
    delay(1000);
    digitalWrite(LED_TRANSMIT, LOW);

    digitalWrite(LED_HIGH_LATENCY, HIGH);
    Serial.println("  [RED]    Demo RTT over 200ms");
    delay(1000);
    digitalWrite(LED_HIGH_LATENCY, LOW);
}

void demonstrateJitterConcept() {
    Serial.println("\n+=================================================================+");
    Serial.println("|                  MINI-LESSON: WHAT IS JITTER?                   |");
    Serial.println("+=================================================================+");
    Serial.println();
    Serial.println("JITTER = Variation in latency between packets");
    Serial.println();
    Serial.println("Low Jitter (Good - Consistent timing):");
    Serial.println("  Pkt1: 50ms  .....#");
    Serial.println("  Pkt2: 52ms  .....#");
    Serial.println("  Pkt3: 48ms  .....#");
    Serial.println("  Pkt4: 51ms  .....#");
    Serial.println("  Jitter (peak-to-peak delay variation) = 52 - 48 = 4ms");
    Serial.println();
    Serial.println("High Jitter (Bad - Unpredictable timing):");
    Serial.println("  Pkt1: 50ms  .....#");
    Serial.println("  Pkt2: 120ms ............#");
    Serial.println("  Pkt3: 30ms  ...#");
    Serial.println("  Pkt4: 200ms ....................#");
    Serial.println("  Jitter (peak-to-peak delay variation) = 200 - 30 = 170ms");
    Serial.println();
    Serial.println("WHY JITTER MATTERS:");
    Serial.println("- VoIP calls: High jitter causes choppy audio");
    Serial.println("- Video streaming: Causes buffering/stutter");
    Serial.println("- Sensor fusion: Difficult to correlate readings");
    Serial.println("- Industrial control: Timing-sensitive operations fail");
    Serial.println();

    // Visual jitter demo with LEDs
    Serial.println("Demonstrating jitter with LEDs...");

    // Low jitter - consistent timing
    Serial.println("  Low jitter (consistent blinks):");
    for (int i = 0; i < 5; i++) {
        digitalWrite(LED_LOW_LATENCY, HIGH);
        delay(100);
        digitalWrite(LED_LOW_LATENCY, LOW);
        delay(100);
    }

    delay(500);

    // High jitter - inconsistent timing
    Serial.println("  High jitter (erratic blinks):");
    int jitterDelays[] = {50, 200, 30, 300, 80};
    for (int i = 0; i < 5; i++) {
        digitalWrite(LED_HIGH_LATENCY, HIGH);
        delay(jitterDelays[i]);
        digitalWrite(LED_HIGH_LATENCY, LOW);
        delay(jitterDelays[i]);
    }

    setAllLEDs(false);
}

void demonstrateCongestionConcept() {
    Serial.println("\n+=================================================================+");
    Serial.println("|                MINI-LESSON: NETWORK CONGESTION                  |");
    Serial.println("+=================================================================+");
    Serial.println();
    Serial.println("Congestion occurs when traffic exceeds network capacity");
    Serial.println();
    Serial.println("Normal Operation (30% utilization):");
    Serial.println("  ========================================");
    Serial.println("  ==> ==> ==>    ==>    ==>   ==>       ");
    Serial.println("  ========================================");
    Serial.println("  Packets flow smoothly, low latency");
    Serial.println();
    Serial.println("Congested (90% utilization):");
    Serial.println("  ========================================");
    Serial.println("  ==>==>==>==>==>==>==>==>==>==>==>==>==>");
    Serial.println("  ========================================");
    Serial.println("  ^ Packets queue up, latency increases!");
    Serial.println();
    Serial.println("CONGESTION EFFECTS:");
    Serial.println("- Latency rises sharply (non-linearly) as utilisation approaches capacity");
    Serial.println("- Packet loss increases (buffers overflow)");
    Serial.println("- Jitter becomes unpredictable");
    Serial.println("- Throughput may actually DECREASE");
    Serial.println();
    Serial.println("TCP CONGESTION COLLAPSE:");
    Serial.println("When packet loss triggers retransmissions,");
    Serial.println("which cause more congestion, which causes");
    Serial.println("more loss... throughput can drop to near zero!");
    Serial.println();

    // Visual demo - congestion building
    Serial.println("Simulating congestion buildup...");

    digitalWrite(LED_LOW_LATENCY, HIGH);
    Serial.println("  Network normal... [GREEN]");
    delay(1000);

    digitalWrite(LED_LOW_LATENCY, LOW);
    digitalWrite(LED_TRANSMIT, HIGH);
    Serial.println("  Traffic increasing... [YELLOW]");
    delay(1000);

    digitalWrite(LED_TRANSMIT, LOW);
    digitalWrite(LED_HIGH_LATENCY, HIGH);
    Serial.println("  Latency rising... [RED]");
    delay(1000);

    digitalWrite(LED_CONGESTION, HIGH);
    Serial.println("  CONGESTION DETECTED! [BLUE]");
    delay(1500);

    setAllLEDs(false);
    Serial.println("  Traffic cleared, network recovering...");
}

Step-by-Step Instructions

This section: Step-by-Step Instructions

Step 1: Set Up the Circuit

  1. Open your preferred ESP32 development environment
  2. Add an ESP32 DevKit to your workspace or breadboard
  3. Add 4 LEDs (red, green, yellow, blue) to the breadboard
  4. Add 4 x 220 ohm resistors for current limiting
  5. Add 1 push button and 1 x 10K ohm resistor for pull-down
  6. Connect each LED through its resistor to the specified GPIO pins:
    • Red LED: GPIO 2 (high latency indicator)
    • Green LED: GPIO 4 (low latency indicator)
    • Yellow LED: GPIO 5 (transmission active)
    • Blue LED: GPIO 18 (congestion detected)
  7. Connect button: 3.3V to one leg, other leg to GPIO 15 with 10K pull-down to GND
  8. Connect all LED cathodes (short legs) to GND

Step 2: Upload and Run

  1. Start from the ESP32 behaviour map above rather than pasting one large program.
  2. Implement one performance feature at a time: latency LED, throughput counter, jitter samples, congestion flag, then packet loss.
  3. Compile and run after each feature so errors stay easy to isolate.
  4. Open the Serial Monitor at 115200 baud and compare each result with the browser workbench.

Step 3: Observe the Demonstrations

The simulator automatically cycles through educational demonstrations:

  1. Bandwidth vs Throughput: Explains why modeled delivered rate differs from configured link rate
  2. Latency Concepts: Shows components of round-trip time
  3. Jitter Analysis: Demonstrates timing variation effects
  4. Congestion Effects: Simulates network overload
  5. Full Performance Test: Runs a complete simulation cycle

Step 4: Run Manual Tests

Press the button to trigger a full network performance test at any time. Each test:

  • Sends 20 simulated packets
  • Measures latency for each packet
  • Calculates jitter (standard deviation)
  • Simulates varying congestion levels
  • Reports comprehensive statistics

Expected Output

This section: Expected Output

When running the simulation, the Serial Monitor should report a readable modeled sequence:

StageSerial monitor should showWhat the LEDs should do
StartupLab title, GPIO setup, and LED self-test statusEach LED blinks during self-test
Network setupSimulated bandwidth, base latency, queue size, and packet lossLEDs turn off after setup
ReadyPrompt to press the button or wait for automatic cyclesAll LEDs idle
Demo cycleThe current topic: bandwidth, latency, jitter, congestion, or full testLED color matches the simulated condition
Manual testPacket count, RTT samples, goodput, and packet-loss summaryBlue LED turns on if congestion is detected
ResultPerformance rating and recommended tuning actionLEDs reset before the next cycle

This section:

Packet Pete, Networking and packet-journey domain expertCheckpoint: ESP32 Measurement Record

You now know:

  • A complete test sends 20 simulated packets, records RTT samples, counts retransmissions, and reports packet loss before grading the run.
  • The chapter’s code separates propagation delay, processing delay, queueing delay, and jitter instead of hiding them inside one average.
  • Build the ESP32 lab one feature at a time: latency LED, throughput counter, jitter samples, congestion flag, then packet loss.

This section: — With a local simulation loop in place, the remaining question is where the delay should be removed.

With a local simulation loop in place, the remaining question is where the delay should be removed. The next explorer connects this same RTT breakdown to edge, fog, and cloud placement.

Try It: Latency Breakdown Explorer

Run it: The explorer below splits one RTT into propagation, processing, and queuing; the edge-fog latency simulator underneath shows how moving that processing changes the total. Pick a Network Profile such as Cellular WAN or Congested and a Workload, then switch the Focus Tier between Edge, Fog, and Cloud and read the Request-Response Timeline and Latency Results. That makes this lab’s “local edge processing” remedy concrete: the same job that misses a control deadline over the WAN can meet it when the compute sits at the edge.

Challenge Exercises

This section: Challenge Exercises

Measure Real Wi-Fi Performance

Modify the code to measure actual network performance instead of simulation:

  1. Connect the ESP32 to your Wi-Fi network
  2. Send HTTP requests to a known server (like httpbin.org)
  3. Measure actual round-trip time using millis()
  4. Compare simulated results to real-world measurements

Hint: Use WiFi.h and HTTPClient.h libraries. Real measurements can be more or less variable than a chosen simulation profile; repeat under stated load and path conditions.

Add Adaptive Bitrate Simulation

Implement adaptive transmission similar to video streaming:

  1. Monitor current network conditions (latency, loss rate)
  2. If conditions degrade, reduce payload size (lower “quality”)
  3. If conditions improve, increase payload size
  4. Display quality level changes on Serial Monitor

Hint: Create quality levels like “High (500 bytes)”, “Medium (200 bytes)”, “Low (50 bytes)” and switch based on measured performance.

Implement Congestion Control

Add TCP-like congestion control to the simulator:

  1. Implement slow start: Begin with small transmission rate
  2. Implement congestion avoidance: Gradually increase rate
  3. On packet loss: Cut rate in half (multiplicative decrease)
  4. Visualize the congestion window (CWND) changes

Hint: Research TCP Reno’s AIMD (Additive Increase Multiplicative Decrease) algorithm.

Challenge 4: Multi-Sensor Competition

Simulate multiple IoT sensors competing for network resources:

  1. Create 5 virtual sensors, each trying to send data
  2. Implement a simple TDMA (time division) scheduler
  3. Compare performance with and without scheduling
  4. Show whether fair queuing improves per-sensor delay and access under the chosen load; total throughput may stay the same or fall slightly from scheduling overhead

Hint: Use an array of sensor structures and cycle through them with different scheduling algorithms.

Troubleshooting

This section: Troubleshooting

ProblemSolution
LEDs not lightingCheck resistor values (220 ohm) and GPIO pin assignments
No Serial outputEnsure baud rate is 115200 in Serial Monitor
Button not respondingVerify 10K pull-down resistor and button wiring
Simulation freezesReduce delay times in code; ESP32 may need reset
Results seem randomThis is intentional — jitter simulation includes randomness

IoT Application: Why This Matters

This section: IoT Application: Why This Matters — The table shows illustrative targets, not universal requirements.

Understanding these metrics is critical for real IoT deployments:

ApplicationKey MetricTypical thresholdWhy
Industrial sensorsEnd-to-end latency<100 msFreshness requirement
Video surveillanceDelivered throughput>2 Mbps for 1080p H.264Image quality depends on encoding
Timed measurementDelay variation<20 msTiming consistency
Smart metersSuccessful report delivery>90%Delivery rate is distinct from goodput efficiency
Voice assistantsEnd-to-end response time<150 msPerceived responsiveness
Fleet trackersLost-update rate<1%Position freshness

The table shows illustrative targets, not universal requirements. The warning below explains why buying more bandwidth may still leave an application deadline missed.

Bandwidth Alone Does Not Cut Latency

The Mistake: “We upgraded from 100 Mbps to 1 Gbps, so our IoT sensor latency should decrease 10x.”

Why It’s Wrong: A faster link reduces serialization time and may relieve queues, but it does not reduce propagation distance or every processing delay. The outcome depends on packet size, load, and the full path.

The Numbers:

  • 100-byte packet at 100 Mbps: transmission time = 8 microseconds

  • 100-byte packet at 1 Gbps: transmission time = 0.8 microseconds

  • Savings: 7.2 microseconds

  • Propagation delay (100 m cable at an assumed 2 × 10^8 m/s): 500 nanoseconds one way

  • Processing delay (illustrative router): 50-500 microseconds, dependent on device and workload

  • Queuing delay (illustrative loaded path): 1-100 milliseconds, dependent on offered load and scheduling

Total latency: ≈1–100 ms from the listed components, so the 7.2 µs saved in serialization is negligible.

The Fix: Measure each delay component. Depending on the bottleneck, reduce queue load, improve scheduling, shorten the path, move processing nearer the sensor, or increase constrained-link capacity. A transport change from TCP to UDP also changes reliability and must be evaluated for the application.

Packet Pete, Networking and packet-journey domain expertCheckpoint: Application Decision

You now know:

  • Typically, industrial sensors need latency under 100 ms, video surveillance more than 2 Mb/s, and timed systems low jitter.
  • A 100-byte packet saves only 7.2 microseconds when moving from 100 Mbps to 1 Gbps, so queueing and processing usually dominate the IoT delay budget.
  • A strong recommendation names the measured bottleneck — loss, jitter, throughput, retransmissions, or queueing — and ties the fix to that evidence.

Concept Review

This section: Concept Review

Match: Metrics to Definitions
Order Performance Test Steps

Common Pitfalls

This section: Common Pitfalls

Networks often perform well at light load and typically degrade sharply above about 70–80% utilisation, because queueing delay grows as 1/(1−ρ). Measure at multiple load levels (for example 10%, 30%, 50%, 70%, 90%) to locate this path’s saturation point.

An average latency of 50 ms looks acceptable, but a 99th-percentile latency of 5 seconds is unacceptable for real-time control. Fix: always report p50, p95, and p99 latency alongside the mean.

Other devices sharing the channel during measurements add variability. Run a controlled baseline where possible, then test representative background traffic and report it separately. Subtracting a background bit rate does not undo its contention, queueing, or retry effects.

Label the Diagram
Code Challenge

Summary

This section: Summary

This lab demonstrated the critical network performance metrics through hands-on simulation:

  • Bandwidth vs Throughput: Maximum capacity differs from actual achieved data rate due to protocol overhead, contention, and retransmissions
  • Latency Components: Propagation, processing, queuing, and transmission delays combine to produce the round-trip time
  • Jitter Impact: Variation in latency affects real-time applications more than average latency alone
  • Congestion Effects: Overloaded queues can grow sharply in delay, drop packets, and in some transport conditions contribute to congestion collapse
  • Efficiency Metrics: Goodput is useful application data delivered per second; the example’s payload fraction compares useful bytes with delivered frame bytes

Knowledge Check

This section: Knowledge Check

Quiz: Network Performance

What’s Next

This section: What’s Next

TopicChapterDescription
Packet Simulator LabPacket Simulator LabBuild and inspect packet structure hands-on, calculate checksums, and observe error detection in action
TCP FundamentalsTCP FundamentalsUnderstand how TCP congestion control (slow start, AIMD) manages throughput and prevents congestion collapse
Transport Protocol ComparisonTransport Protocol ComparisonCompare TCP vs UDP tradeoffs for latency-sensitive vs reliability-sensitive IoT applications
MQTT QoS and SessionsMQTT QoS and SessionsApply QoS levels 0, 1, and 2 to choose message-delivery behavior; QoS is not a network priority setting
Network Topology DesignTopologies FundamentalsEvaluate how mesh, star, and tree topologies affect latency, jitter, and fault tolerance in IoT deployments
Edge and Fog ComputingEdge/Fog IntroductionReduce network latency by processing sensor data closer to the source with edge computing patterns

Key Takeaway

This section: Key Takeaway — Performance labs should measure throughput, latency, loss, jitter, retries, and bottlenecks under changed conditions.

Performance labs should measure throughput, latency, loss, jitter, retries, and bottlenecks under changed conditions. A single successful transfer does not prove network capacity.

Continue Your Route

This section: Continue Your Route — This is the last part of this chapter.

This is the last part of this chapter. Return to Network Performance Lab: Measurement Setup and Terms or continue from the networking-core module index.