151  Lab: ESP32 QoS Management

Lab execution time can be estimated before starting runs:

\[ T_{\text{total}} = N_{\text{runs}} \times (t_{\text{setup}} + t_{\text{run}} + t_{\text{review}}) \]

Worked example: With 5 runs and per-run times of 4 min setup, 6 min execution, and 3 min review, total lab time is \(5\times(4+6+3)=65\) minutes. This prevents under-scoping and helps schedule complete experimental cycles.

In 60 Seconds

This hands-on lab implements QoS management on an ESP32, covering traffic classification into 4 priority queues, weighted fair queuing for bandwidth allocation, and token bucket rate limiting. Critical alerts get guaranteed delivery while bulk telemetry is shaped to prevent network congestion.

151.1 Learning Objectives

By the end of this lab, you will be able to:

  • Implement multi-level priority queues with configurable scheduling on ESP32 hardware
  • Construct traffic shaping using the token bucket algorithm with differentiated costs
  • Configure sliding window rate limiters to protect system resources from burst overload
  • Monitor latency metrics and detect SLA violations in real-time using embedded dashboards
  • Design dynamic QoS policy engines that adapt enforcement based on system load state

Quality of Service (QoS) ensures that important IoT data gets priority treatment on the network. Think of an ambulance with its siren on – it gets priority over regular traffic because it carries urgent cargo. Similarly, QoS mechanisms give priority to critical IoT messages (like emergency alerts) over routine data (like periodic temperature readings).

151.2 What You Will Learn

In this hands-on lab, you will build and experiment with a comprehensive QoS management system on ESP32. The simulation demonstrates:

  1. Priority Queuing: Multi-level priority queues with configurable scheduling
  2. Traffic Shaping: Token bucket implementation for rate control
  3. Rate Limiting: Request throttling to protect system resources
  4. Service Level Monitoring: Real-time SLA tracking and violation detection
  5. QoS Policy Engine: Dynamic policy enforcement based on system state
  6. Metrics Dashboard: Live visualization of QoS performance

151.3 Lab Components

Component Purpose Simulation Role
ESP32 DevKit Main controller Runs QoS engine
Push Buttons (4) Traffic generators Simulate different priority messages
LEDs (4) Status indicators Show queue states and violations
Potentiometer Rate control Adjust traffic shaping rate
Serial Monitor Dashboard Real-time QoS metrics display

151.4 Wokwi Simulator Environment

About Wokwi

Wokwi is a free online simulator for Arduino, ESP32, and other microcontrollers. It allows you to build and test IoT projects entirely in your browser without purchasing hardware. This lab demonstrates QoS concepts using standard components.

Launch the simulator below and copy the provided code to explore QoS management interactively.

Simulator Tips
  • Click the + button to add components (search for “Push Button”, “LED”, “Potentiometer”)
  • Use the Serial Monitor to see QoS metrics (115200 baud)
  • Press buttons to generate messages of different priorities
  • Adjust the potentiometer to change the token bucket rate
  • Watch LEDs indicate queue states (green = healthy, red = overloaded)
  • The code demonstrates production-quality QoS patterns

151.5 Step-by-Step Instructions

151.5.1 Step 1: Set Up the Circuit

  1. Add 4 Push Buttons: Click + and search for “Push Button” - add 4 buttons
  2. Add 4 LEDs: Click + and search for “LED” - add Red, Yellow, Green, and Blue LEDs
  3. Add 1 Potentiometer: Click + and search for “Potentiometer”
  4. Wire the components to ESP32:
    • Button 1 (Emergency Traffic) -> GPIO 12
    • Button 2 (Critical Traffic) -> GPIO 13
    • Button 3 (Normal Traffic) -> GPIO 14
    • Button 4 (Background Traffic) -> GPIO 15
    • Red LED (Emergency Queue) -> GPIO 25
    • Yellow LED (Critical Queue) -> GPIO 26
    • Green LED (Normal Queue) -> GPIO 27
    • Blue LED (Background Queue) -> GPIO 32
    • Potentiometer signal -> GPIO 34 (ADC)
    • All button other pins -> GND
    • All LED cathodes -> GND (with 220 ohm resistors)

ESP32 circuit diagram for QoS lab showing wiring of 4 priority buttons (GPIO 12–15), 4 color-coded LEDs (GPIO 25–32) representing Emergency/Critical/Normal/Background queues, and a potentiometer (GPIO 34) for adjustable congestion simulation

151.5.2 Step 2: Understanding the QoS Architecture

This lab implements a complete QoS management system with the following components:

QoS architecture diagram showing four-layer priority system: input messages enter a classifier that assigns Emergency, Critical, Normal, or Background priority, routes them to corresponding queues, and a scheduler services queues in strict priority order with configurable bandwidth allocation per tier

151.5.3 Step 3: Copy the QoS Lab Code

Copy the following code into the Wokwi code editor. This comprehensive implementation demonstrates professional QoS patterns used in production IoT systems.

/*
 * QoS Management Lab - ESP32 Implementation
 *
 * Comprehensive demonstration of Quality of Service concepts for IoT:
 * - Priority Queuing with 4 priority levels
 * - Token Bucket Traffic Shaping
 * - Rate Limiting with sliding window
 * - Service Level Agreement (SLA) Monitoring
 * - Dynamic QoS Policy Enforcement
 * - Real-time Metrics Dashboard
 *
 * Hardware Configuration:
 * - Button 1 (GPIO 12): Generate EMERGENCY priority traffic
 * - Button 2 (GPIO 13): Generate CRITICAL priority traffic
 * - Button 3 (GPIO 14): Generate NORMAL priority traffic
 * - Button 4 (GPIO 15): Generate BACKGROUND priority traffic
 * - Red LED (GPIO 25): Emergency queue status
 * - Yellow LED (GPIO 26): Critical queue status
 * - Green LED (GPIO 27): Normal queue status
 * - Blue LED (GPIO 32): Background queue status
 * - Potentiometer (GPIO 34): Token bucket rate control
 *
 * Serial Monitor: 115200 baud for QoS metrics dashboard
 *
 * Key QoS Concepts Demonstrated:
 * 1. Priority-based message scheduling
 * 2. Traffic shaping with token bucket algorithm
 * 3. Rate limiting to prevent overload
 * 4. SLA violation detection and alerting
 * 5. Dynamic policy adjustment based on load
 * 6. Queue depth monitoring and overflow handling
 */

#include <Arduino.h>

// ============================================================
// CONFIGURATION CONSTANTS
// ============================================================

// Pin Definitions
#define BTN_EMERGENCY    12    // Emergency traffic generator
#define BTN_CRITICAL     13    // Critical traffic generator
#define BTN_NORMAL       14    // Normal traffic generator
#define BTN_BACKGROUND   15    // Background traffic generator

#define LED_EMERGENCY    25    // Emergency queue status LED
#define LED_CRITICAL     26    // Critical queue status LED
#define LED_NORMAL       27    // Normal queue status LED
#define LED_BACKGROUND   32    // Background queue status LED

#define POT_RATE         34    // Token rate control potentiometer

// Priority Levels (lower number = higher priority)
#define PRIORITY_EMERGENCY   0
#define PRIORITY_CRITICAL    1
#define PRIORITY_NORMAL      2
#define PRIORITY_BACKGROUND  3
#define NUM_PRIORITY_LEVELS  4

// Queue Configuration
#define MAX_QUEUE_SIZE       20   // Maximum messages per queue
#define MESSAGE_PAYLOAD_SIZE 64   // Bytes per message

// SLA Latency Requirements (milliseconds)
#define SLA_EMERGENCY_MS     50   // Emergency: max 50ms latency
#define SLA_CRITICAL_MS      200  // Critical: max 200ms latency
#define SLA_NORMAL_MS        1000 // Normal: max 1 second latency
#define SLA_BACKGROUND_MS    5000 // Background: max 5 seconds (best effort)

// Token Bucket Configuration
#define TOKEN_BUCKET_MAX     100  // Maximum tokens in bucket
#define TOKEN_COST_EMERGENCY 1    // Tokens consumed per emergency message
#define TOKEN_COST_CRITICAL  2    // Tokens consumed per critical message
#define TOKEN_COST_NORMAL    5    // Tokens consumed per normal message
#define TOKEN_COST_BACKGROUND 10  // Tokens consumed per background message

// Rate Limiting Configuration
#define RATE_LIMIT_WINDOW_MS 1000 // Sliding window duration
#define RATE_LIMIT_MAX_MSGS  50   // Maximum messages per window

// Timing
#define DEBOUNCE_MS          50   // Button debounce time
#define METRICS_INTERVAL_MS  2000 // Dashboard update interval
#define LED_BLINK_FAST_MS    100  // Fast blink for violations
#define LED_BLINK_SLOW_MS    500  // Slow blink for activity

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

/**
 * Message structure representing a unit of IoT data
 * Contains priority, payload, and timing information for SLA tracking
 */
struct Message {
    uint8_t priority;                      // 0=Emergency, 3=Background
    uint32_t enqueueTime;                  // When message entered queue
    uint32_t processTime;                  // When message was processed
    char payload[MESSAGE_PAYLOAD_SIZE];    // Message content
    uint16_t payloadSize;                  // Actual payload size
    uint32_t sequenceNumber;               // Unique message ID
    bool processed;                        // Processing status
};

/**
 * Priority Queue structure with SLA tracking
 * Each priority level has its own queue with configurable SLA requirements
 */
struct PriorityQueue {
    Message messages[MAX_QUEUE_SIZE];      // Message buffer
    uint8_t head;                          // Read position
    uint8_t tail;                          // Write position
    uint8_t count;                         // Current message count
    uint8_t maxSize;                       // Maximum capacity
    uint32_t slaLatencyMs;                 // Maximum allowed latency
    uint32_t totalEnqueued;                // Total messages received
    uint32_t totalProcessed;               // Total messages processed
    uint32_t totalDropped;                 // Messages dropped (overflow)
    uint32_t slaViolations;                // SLA violations count
    uint64_t totalLatencyMs;               // Cumulative latency for averaging
    const char* name;                      // Queue name for logging
};

/**
 * Token Bucket structure for traffic shaping
 * Controls the rate at which messages can be processed
 */
struct TokenBucket {
    float tokens;                          // Current token count
    float maxTokens;                       // Maximum token capacity
    float refillRate;                      // Tokens added per second
    uint32_t lastRefillTime;               // Last refill timestamp
};

/**
 * Rate Limiter structure using sliding window
 * Tracks request rates and enforces limits
 */
struct RateLimiter {
    uint32_t windowStart;                  // Current window start time
    uint32_t messageCount;                 // Messages in current window
    uint32_t windowDurationMs;             // Window duration
    uint32_t maxMessagesPerWindow;         // Maximum allowed messages
    uint32_t totalRejected;                // Total rejected due to limit
};

/**
 * QoS Metrics structure for dashboard reporting
 * Aggregates statistics across all queues and components
 */
struct QoSMetrics {
    uint32_t totalMessagesReceived;        // All messages received
    uint32_t totalMessagesProcessed;       // Successfully processed
    uint32_t totalMessagesDropped;         // Dropped due to overflow
    uint32_t totalMessagesThrottled;       // Rejected by rate limiter
    uint32_t totalSLAViolations;           // Total SLA breaches
    float avgLatencyMs[NUM_PRIORITY_LEVELS]; // Average latency per priority
    float throughputMsgsPerSec;            // Current throughput
    uint32_t lastThroughputCalcTime;       // Last throughput calculation
    uint32_t messagesInLastSecond;         // Messages processed in last second
    float tokenBucketLevel;                // Current token bucket fill level
    float systemLoad;                      // Estimated system load (0-1)
};

/**
 * QoS Policy structure for dynamic adjustment
 * Defines thresholds and actions for policy enforcement
 */
struct QoSPolicy {
    float highLoadThreshold;               // Load level triggering high-load policy
    float criticalLoadThreshold;           // Load level triggering critical policy
    bool emergencyOnlyMode;                // Only process emergency messages
    bool dropBackgroundTraffic;            // Drop background during high load
    uint32_t lastPolicyCheck;              // Last policy evaluation time
    uint32_t policyCheckIntervalMs;        // Policy check frequency
};

// ============================================================
// GLOBAL STATE
// ============================================================

// Priority Queues (one per priority level)
PriorityQueue queues[NUM_PRIORITY_LEVELS];

// Traffic Shaping
TokenBucket tokenBucket;
RateLimiter rateLimiter;

// Metrics and Policy
QoSMetrics metrics;
QoSPolicy policy;

// Message Sequencing
uint32_t nextSequenceNumber = 1;

// Button State Tracking
uint32_t lastButtonPress[4] = {0, 0, 0, 0};
bool buttonState[4] = {false, false, false, false};

// LED State for Blinking
uint32_t ledLastToggle[4] = {0, 0, 0, 0};
bool ledState[4] = {false, false, false, false};

// Dashboard Timing
uint32_t lastMetricsDisplay = 0;

// Auto-traffic generation for demonstration
uint32_t lastAutoTraffic = 0;
bool autoTrafficEnabled = true;
uint32_t autoTrafficInterval = 500; // Generate traffic every 500ms

// ============================================================
// QUEUE OPERATIONS
// ============================================================

/**
 * Initialize a priority queue with given parameters
 *
 * @param queue Pointer to the queue structure
 * @param name Human-readable queue name
 * @param slaLatencyMs Maximum allowed latency for SLA compliance
 */
void initQueue(PriorityQueue* queue, const char* name, uint32_t slaLatencyMs) {
    queue->head = 0;
    queue->tail = 0;
    queue->count = 0;
    queue->maxSize = MAX_QUEUE_SIZE;
    queue->slaLatencyMs = slaLatencyMs;
    queue->totalEnqueued = 0;
    queue->totalProcessed = 0;
    queue->totalDropped = 0;
    queue->slaViolations = 0;
    queue->totalLatencyMs = 0;
    queue->name = name;

    // Clear message buffer
    for (int i = 0; i < MAX_QUEUE_SIZE; i++) {
        queue->messages[i].processed = true;
        queue->messages[i].priority = 255; // Invalid
    }
}

/**
 * Check if a queue is full
 *
 * @param queue Pointer to the queue
 * @return true if queue is at capacity
 */
bool isQueueFull(PriorityQueue* queue) {
    return queue->count >= queue->maxSize;
}

/**
 * Check if a queue is empty
 *
 * @param queue Pointer to the queue
 * @return true if queue has no messages
 */
bool isQueueEmpty(PriorityQueue* queue) {
    return queue->count == 0;
}

/**
 * Enqueue a message into a priority queue
 * Handles overflow by dropping the message and tracking the drop
 *
 * @param queue Pointer to the target queue
 * @param priority Message priority level
 * @param payload Message content
 * @param payloadSize Size of payload in bytes
 * @return true if message was enqueued, false if dropped
 */
bool enqueueMessage(PriorityQueue* queue, uint8_t priority,
                    const char* payload, uint16_t payloadSize) {
    // Check for queue overflow
    if (isQueueFull(queue)) {
        queue->totalDropped++;
        metrics.totalMessagesDropped++;
        Serial.printf("[DROP] %s queue full, dropping message #%lu\n",
                     queue->name, nextSequenceNumber);
        return false;
    }

    // Create new message
    Message* msg = &queue->messages[queue->tail];
    msg->priority = priority;
    msg->enqueueTime = millis();
    msg->processTime = 0;
    msg->sequenceNumber = nextSequenceNumber++;
    msg->payloadSize = min(payloadSize, (uint16_t)(MESSAGE_PAYLOAD_SIZE - 1));
    strncpy(msg->payload, payload, msg->payloadSize);
    msg->payload[msg->payloadSize] = '\0';
    msg->processed = false;

    // Advance tail pointer (circular buffer)
    queue->tail = (queue->tail + 1) % queue->maxSize;
    queue->count++;
    queue->totalEnqueued++;
    metrics.totalMessagesReceived++;

    return true;
}

/**
 * Dequeue a message from a priority queue
 * Updates SLA tracking based on message latency
 *
 * @param queue Pointer to the source queue
 * @param outMessage Pointer to store dequeued message
 * @return true if message was dequeued, false if queue empty
 */
bool dequeueMessage(PriorityQueue* queue, Message* outMessage) {
    if (isQueueEmpty(queue)) {
        return false;
    }

    // Get message at head
    Message* msg = &queue->messages[queue->head];

    // Calculate latency
    uint32_t latency = millis() - msg->enqueueTime;
    msg->processTime = millis();

    // Check SLA compliance
    if (latency > queue->slaLatencyMs) {
        queue->slaViolations++;
        metrics.totalSLAViolations++;
        Serial.printf("[SLA VIOLATION] %s: Message #%lu latency %lums > %lums SLA\n",
                     queue->name, msg->sequenceNumber, latency, queue->slaLatencyMs);
    }

    // Update latency statistics
    queue->totalLatencyMs += latency;
    queue->totalProcessed++;
    metrics.totalMessagesProcessed++;

    // Copy message to output
    memcpy(outMessage, msg, sizeof(Message));
    msg->processed = true;

    // Advance head pointer
    queue->head = (queue->head + 1) % queue->maxSize;
    queue->count--;

    return true;
}

/**
 * Get the fill percentage of a queue
 *
 * @param queue Pointer to the queue
 * @return Fill percentage (0-100)
 */
float getQueueFillPercent(PriorityQueue* queue) {
    return (float)queue->count / queue->maxSize * 100.0f;
}

// ============================================================
// TOKEN BUCKET IMPLEMENTATION
// ============================================================

/**
 * Initialize the token bucket for traffic shaping
 *
 * @param maxTokens Maximum token capacity
 * @param refillRate Tokens added per second
 */
void initTokenBucket(float maxTokens, float refillRate) {
    tokenBucket.tokens = maxTokens;  // Start with full bucket
    tokenBucket.maxTokens = maxTokens;
    tokenBucket.refillRate = refillRate;
    tokenBucket.lastRefillTime = millis();
}

/**
 * Refill tokens based on elapsed time
 * Called before each consume attempt to add tokens proportional to time elapsed
 */
void refillTokens() {
    uint32_t now = millis();
    uint32_t elapsed = now - tokenBucket.lastRefillTime;

    if (elapsed > 0) {
        // Add tokens proportional to elapsed time
        float tokensToAdd = (elapsed / 1000.0f) * tokenBucket.refillRate;
        tokenBucket.tokens = min(tokenBucket.tokens + tokensToAdd,
                                  tokenBucket.maxTokens);
        tokenBucket.lastRefillTime = now;
    }
}

/**
 * Attempt to consume tokens for a message
 *
 * @param tokensRequired Number of tokens needed
 * @return true if tokens were consumed, false if insufficient tokens
 */
bool consumeTokens(float tokensRequired) {
    refillTokens();

    if (tokenBucket.tokens >= tokensRequired) {
        tokenBucket.tokens -= tokensRequired;
        return true;
    }

    return false;
}

/**
 * Get token cost for a message based on its priority
 * Higher priority messages cost fewer tokens (get more bandwidth)
 *
 * @param priority Message priority level
 * @return Token cost for the message
 */
float getTokenCost(uint8_t priority) {
    switch (priority) {
        case PRIORITY_EMERGENCY:  return TOKEN_COST_EMERGENCY;
        case PRIORITY_CRITICAL:   return TOKEN_COST_CRITICAL;
        case PRIORITY_NORMAL:     return TOKEN_COST_NORMAL;
        case PRIORITY_BACKGROUND: return TOKEN_COST_BACKGROUND;
        default:                  return TOKEN_COST_BACKGROUND;
    }
}

/**
 * Get current token bucket fill percentage
 *
 * @return Fill percentage (0-100)
 */
float getTokenBucketPercent() {
    refillTokens();
    return (tokenBucket.tokens / tokenBucket.maxTokens) * 100.0f;
}

// ============================================================
// RATE LIMITER IMPLEMENTATION
// ============================================================

/**
 * Initialize the rate limiter with sliding window parameters
 *
 * @param windowDurationMs Duration of the sliding window
 * @param maxMessagesPerWindow Maximum messages allowed per window
 */
void initRateLimiter(uint32_t windowDurationMs, uint32_t maxMessagesPerWindow) {
    rateLimiter.windowStart = millis();
    rateLimiter.messageCount = 0;
    rateLimiter.windowDurationMs = windowDurationMs;
    rateLimiter.maxMessagesPerWindow = maxMessagesPerWindow;
    rateLimiter.totalRejected = 0;
}

/**
 * Check if a message should be rate limited
 * Updates window and counts, returns whether message is allowed
 *
 * @return true if message is allowed, false if rate limited
 */
bool checkRateLimit() {
    uint32_t now = millis();

    // Check if window has expired
    if (now - rateLimiter.windowStart >= rateLimiter.windowDurationMs) {
        // Reset window
        rateLimiter.windowStart = now;
        rateLimiter.messageCount = 0;
    }

    // Check if within limit
    if (rateLimiter.messageCount < rateLimiter.maxMessagesPerWindow) {
        rateLimiter.messageCount++;
        return true;
    }

    // Rate limit exceeded
    rateLimiter.totalRejected++;
    metrics.totalMessagesThrottled++;
    return false;
}

/**
 * Get current rate (messages per second)
 *
 * @return Current message rate
 */
float getCurrentRate() {
    uint32_t elapsed = millis() - rateLimiter.windowStart;
    if (elapsed == 0) return 0;
    return (float)rateLimiter.messageCount / (elapsed / 1000.0f);
}

// ============================================================
// QoS POLICY ENGINE
// ============================================================

/**
 * Initialize QoS policy with default thresholds
 */
void initPolicy() {
    policy.highLoadThreshold = 0.7f;      // 70% load triggers high-load mode
    policy.criticalLoadThreshold = 0.9f;   // 90% load triggers critical mode
    policy.emergencyOnlyMode = false;
    policy.dropBackgroundTraffic = false;
    policy.lastPolicyCheck = millis();
    policy.policyCheckIntervalMs = 500;    // Check every 500ms
}

/**
 * Calculate current system load based on queue fill levels
 * Weighted average favoring high-priority queues
 *
 * @return Load factor (0.0 to 1.0)
 */
float calculateSystemLoad() {
    float weightedLoad = 0;
    float totalWeight = 0;

    // Weight high-priority queues more heavily
    float weights[] = {4.0f, 3.0f, 2.0f, 1.0f};

    for (int i = 0; i < NUM_PRIORITY_LEVELS; i++) {
        weightedLoad += getQueueFillPercent(&queues[i]) / 100.0f * weights[i];
        totalWeight += weights[i];
    }

    return weightedLoad / totalWeight;
}

/**
 * Evaluate and update QoS policy based on current system state
 * Adjusts policy flags for emergency-only mode and background dropping
 */
void evaluatePolicy() {
    uint32_t now = millis();

    // Only evaluate periodically
    if (now - policy.lastPolicyCheck < policy.policyCheckIntervalMs) {
        return;
    }
    policy.lastPolicyCheck = now;

    // Calculate current load
    metrics.systemLoad = calculateSystemLoad();

    // Update policy based on load
    bool wasEmergencyOnly = policy.emergencyOnlyMode;
    bool wasDropping = policy.dropBackgroundTraffic;

    if (metrics.systemLoad >= policy.criticalLoadThreshold) {
        // Critical load: emergency only
        policy.emergencyOnlyMode = true;
        policy.dropBackgroundTraffic = true;
    } else if (metrics.systemLoad >= policy.highLoadThreshold) {
        // High load: drop background
        policy.emergencyOnlyMode = false;
        policy.dropBackgroundTraffic = true;
    } else {
        // Normal load
        policy.emergencyOnlyMode = false;
        policy.dropBackgroundTraffic = false;
    }

    // Log policy changes
    if (policy.emergencyOnlyMode != wasEmergencyOnly) {
        Serial.printf("[POLICY] Emergency-only mode: %s\n",
                     policy.emergencyOnlyMode ? "ENABLED" : "DISABLED");
    }
    if (policy.dropBackgroundTraffic != wasDropping) {
        Serial.printf("[POLICY] Background traffic dropping: %s\n",
                     policy.dropBackgroundTraffic ? "ENABLED" : "DISABLED");
    }
}

/**
 * Check if a message should be accepted based on current policy
 *
 * @param priority Message priority level
 * @return true if message should be accepted, false if policy rejects it
 */
bool policyAllowsMessage(uint8_t priority) {
    // Emergency always allowed
    if (priority == PRIORITY_EMERGENCY) {
        return true;
    }

    // Emergency-only mode blocks all non-emergency
    if (policy.emergencyOnlyMode) {
        return false;
    }

    // High load mode drops background
    if (policy.dropBackgroundTraffic && priority == PRIORITY_BACKGROUND) {
        return false;
    }

    return true;
}

// ============================================================
// PRIORITY SCHEDULER
// ============================================================

/**
 * Process messages from queues in priority order
 * Implements strict priority scheduling with token bucket rate control
 *
 * @return Number of messages processed in this call
 */
uint8_t processQueues() {
    uint8_t processed = 0;
    Message msg;

    // Process in strict priority order
    for (int priority = 0; priority < NUM_PRIORITY_LEVELS; priority++) {
        PriorityQueue* queue = &queues[priority];

        // Process all messages in this priority level before moving to next
        while (!isQueueEmpty(queue)) {
            // Check rate limit
            if (!checkRateLimit()) {
                Serial.println("[RATE LIMIT] Rate limit exceeded, pausing processing");
                return processed;
            }

            // Check token bucket
            float cost = getTokenCost(priority);
            if (!consumeTokens(cost)) {
                // Not enough tokens - try lower priority or wait
                break;
            }

            // Dequeue and process
            if (dequeueMessage(queue, &msg)) {
                // Simulate processing (in real system, would send to cloud/actuator)
                processed++;

                // Update throughput tracking
                metrics.messagesInLastSecond++;
            }
        }
    }

    return processed;
}

// ============================================================
// TRAFFIC GENERATION
// ============================================================

/**
 * Generate a message with specified priority
 *
 * @param priority Message priority level
 */
void generateTraffic(uint8_t priority) {
    char payload[MESSAGE_PAYLOAD_SIZE];
    const char* priorityName;

    switch (priority) {
        case PRIORITY_EMERGENCY:
            priorityName = "EMERGENCY";
            snprintf(payload, MESSAGE_PAYLOAD_SIZE,
                    "ALERT: Critical sensor threshold exceeded at %lu", millis());
            break;
        case PRIORITY_CRITICAL:
            priorityName = "CRITICAL";
            snprintf(payload, MESSAGE_PAYLOAD_SIZE,
                    "WARNING: Actuator response required at %lu", millis());
            break;
        case PRIORITY_NORMAL:
            priorityName = "NORMAL";
            snprintf(payload, MESSAGE_PAYLOAD_SIZE,
                    "DATA: Sensor reading value=%.2f at %lu",
                    random(0, 10000) / 100.0f, millis());
            break;
        case PRIORITY_BACKGROUND:
            priorityName = "BACKGROUND";
            snprintf(payload, MESSAGE_PAYLOAD_SIZE,
                    "LOG: System diagnostic entry at %lu", millis());
            break;
        default:
            return;
    }

    // Check policy
    if (!policyAllowsMessage(priority)) {
        Serial.printf("[POLICY BLOCK] %s message rejected by policy\n", priorityName);
        return;
    }

    // Enqueue message
    PriorityQueue* queue = &queues[priority];
    if (enqueueMessage(queue, priority, payload, strlen(payload))) {
        Serial.printf("[ENQUEUE] %s: %s (queue depth: %d/%d)\n",
                     priorityName, payload, queue->count, queue->maxSize);
    }
}

/**
 * Generate automatic traffic for demonstration
 * Creates a realistic mix of different priority messages
 */
void generateAutoTraffic() {
    uint32_t now = millis();

    if (!autoTrafficEnabled) return;

    if (now - lastAutoTraffic >= autoTrafficInterval) {
        lastAutoTraffic = now;

        // Probability-based traffic generation
        int r = random(100);

        if (r < 2) {
            // 2% emergency
            generateTraffic(PRIORITY_EMERGENCY);
        } else if (r < 12) {
            // 10% critical
            generateTraffic(PRIORITY_CRITICAL);
        } else if (r < 52) {
            // 40% normal
            generateTraffic(PRIORITY_NORMAL);
        } else {
            // 48% background
            generateTraffic(PRIORITY_BACKGROUND);
        }
    }
}

// ============================================================
// LED STATUS DISPLAY
// ============================================================

/**
 * Update LED states to reflect queue status
 * - Solid: Queue has messages
 * - Blinking fast: SLA violations detected
 * - Off: Queue empty
 */
void updateLEDs() {
    uint32_t now = millis();

    for (int i = 0; i < NUM_PRIORITY_LEVELS; i++) {
        PriorityQueue* queue = &queues[i];
        uint8_t ledPin;

        switch (i) {
            case PRIORITY_EMERGENCY:  ledPin = LED_EMERGENCY; break;
            case PRIORITY_CRITICAL:   ledPin = LED_CRITICAL; break;
            case PRIORITY_NORMAL:     ledPin = LED_NORMAL; break;
            case PRIORITY_BACKGROUND: ledPin = LED_BACKGROUND; break;
            default: continue;
        }

        // Determine LED behavior based on queue state
        if (queue->slaViolations > 0 &&
            (now - ledLastToggle[i] >= LED_BLINK_FAST_MS)) {
            // SLA violations: fast blink
            ledState[i] = !ledState[i];
            ledLastToggle[i] = now;
            digitalWrite(ledPin, ledState[i] ? HIGH : LOW);
        } else if (queue->count > 0) {
            // Queue has messages: solid on
            digitalWrite(ledPin, HIGH);
            ledState[i] = true;
        } else {
            // Queue empty: off
            digitalWrite(ledPin, LOW);
            ledState[i] = false;
        }
    }
}

// ============================================================
// METRICS DASHBOARD
// ============================================================

/**
 * Calculate and update throughput metrics
 */
void updateThroughputMetrics() {
    uint32_t now = millis();
    uint32_t elapsed = now - metrics.lastThroughputCalcTime;

    if (elapsed >= 1000) {
        metrics.throughputMsgsPerSec = (float)metrics.messagesInLastSecond /
                                       (elapsed / 1000.0f);
        metrics.messagesInLastSecond = 0;
        metrics.lastThroughputCalcTime = now;
    }
}

/**
 * Calculate average latency for a queue
 *
 * @param queue Pointer to the queue
 * @return Average latency in milliseconds
 */
float calculateAvgLatency(PriorityQueue* queue) {
    if (queue->totalProcessed == 0) return 0;
    return (float)queue->totalLatencyMs / queue->totalProcessed;
}

/**
 * Display comprehensive QoS metrics dashboard
 */
void displayMetrics() {
    uint32_t now = millis();

    if (now - lastMetricsDisplay < METRICS_INTERVAL_MS) {
        return;
    }
    lastMetricsDisplay = now;

    // Update derived metrics
    updateThroughputMetrics();
    metrics.tokenBucketLevel = getTokenBucketPercent();

    // Clear screen and display header
    Serial.println("\n========================================");
    Serial.println("       QoS MANAGEMENT DASHBOARD");
    Serial.println("========================================");
    Serial.printf("Uptime: %lu seconds\n", millis() / 1000);
    Serial.println();

    // System Overview
    Serial.println("--- SYSTEM OVERVIEW ---");
    Serial.printf("System Load:      %.1f%%\n", metrics.systemLoad * 100);
    Serial.printf("Throughput:       %.1f msg/sec\n", metrics.throughputMsgsPerSec);
    Serial.printf("Token Bucket:     %.1f%%\n", metrics.tokenBucketLevel);
    Serial.printf("Current Rate:     %.1f msg/sec\n", getCurrentRate());
    Serial.println();

    // Policy Status
    Serial.println("--- POLICY STATUS ---");
    Serial.printf("Emergency Only:   %s\n", policy.emergencyOnlyMode ? "YES" : "no");
    Serial.printf("Drop Background:  %s\n", policy.dropBackgroundTraffic ? "YES" : "no");
    Serial.println();

    // Queue Statistics
    Serial.println("--- QUEUE STATISTICS ---");
    Serial.println("Priority    | Depth | Enqueued | Processed | Dropped | SLA Viol | Avg Lat");
    Serial.println("------------|-------|----------|-----------|---------|----------|--------");

    const char* names[] = {"EMERGENCY", "CRITICAL", "NORMAL", "BACKGROUND"};

    for (int i = 0; i < NUM_PRIORITY_LEVELS; i++) {
        PriorityQueue* q = &queues[i];
        Serial.printf("%-11s | %2d/%2d | %8lu | %9lu | %7lu | %8lu | %6.1fms\n",
                     names[i],
                     q->count, q->maxSize,
                     q->totalEnqueued,
                     q->totalProcessed,
                     q->totalDropped,
                     q->slaViolations,
                     calculateAvgLatency(q));
    }
    Serial.println();

    // Aggregate Metrics
    Serial.println("--- AGGREGATE METRICS ---");
    Serial.printf("Total Received:   %lu\n", metrics.totalMessagesReceived);
    Serial.printf("Total Processed:  %lu\n", metrics.totalMessagesProcessed);
    Serial.printf("Total Dropped:    %lu\n", metrics.totalMessagesDropped);
    Serial.printf("Total Throttled:  %lu\n", metrics.totalMessagesThrottled);
    Serial.printf("Total SLA Viols:  %lu\n", metrics.totalSLAViolations);
    Serial.println();

    // SLA Compliance
    Serial.println("--- SLA COMPLIANCE ---");
    for (int i = 0; i < NUM_PRIORITY_LEVELS; i++) {
        PriorityQueue* q = &queues[i];
        float compliance = 100.0f;
        if (q->totalProcessed > 0) {
            compliance = 100.0f * (1.0f - (float)q->slaViolations / q->totalProcessed);
        }
        Serial.printf("%s: %.2f%% (target: %lums)\n",
                     names[i], compliance, q->slaLatencyMs);
    }
    Serial.println();

    // Instructions
    Serial.println("--- CONTROLS ---");
    Serial.println("BTN1=Emergency, BTN2=Critical, BTN3=Normal, BTN4=Background");
    Serial.println("POT=Token Rate (turn to adjust traffic shaping)");
    Serial.println("========================================\n");
}

// ============================================================
// BUTTON HANDLING
// ============================================================

/**
 * Read buttons and generate traffic based on presses
 */
void handleButtons() {
    uint32_t now = millis();

    struct ButtonConfig {
        uint8_t pin;
        uint8_t priority;
    };

    ButtonConfig buttons[] = {
        {BTN_EMERGENCY, PRIORITY_EMERGENCY},
        {BTN_CRITICAL, PRIORITY_CRITICAL},
        {BTN_NORMAL, PRIORITY_NORMAL},
        {BTN_BACKGROUND, PRIORITY_BACKGROUND}
    };

    for (int i = 0; i < 4; i++) {
        bool currentState = (digitalRead(buttons[i].pin) == LOW);

        // Debounced press detection
        if (currentState && !buttonState[i] &&
            (now - lastButtonPress[i] >= DEBOUNCE_MS)) {
            // Button pressed
            generateTraffic(buttons[i].priority);
            lastButtonPress[i] = now;
        }

        buttonState[i] = currentState;
    }
}

/**
 * Read potentiometer and adjust token bucket rate
 */
void handlePotentiometer() {
    int potValue = analogRead(POT_RATE);

    // Map potentiometer to token refill rate (10 to 200 tokens/sec)
    float newRate = map(potValue, 0, 4095, 10, 200);

    // Only update if significantly changed
    if (abs(newRate - tokenBucket.refillRate) > 5) {
        tokenBucket.refillRate = newRate;
        Serial.printf("[CONFIG] Token refill rate adjusted to %.1f tokens/sec\n", newRate);
    }
}

// ============================================================
// INITIALIZATION
// ============================================================

/**
 * Initialize all QoS system components
 */
void initQoSSystem() {
    // Initialize priority queues
    initQueue(&queues[PRIORITY_EMERGENCY], "EMERGENCY", SLA_EMERGENCY_MS);
    initQueue(&queues[PRIORITY_CRITICAL], "CRITICAL", SLA_CRITICAL_MS);
    initQueue(&queues[PRIORITY_NORMAL], "NORMAL", SLA_NORMAL_MS);
    initQueue(&queues[PRIORITY_BACKGROUND], "BACKGROUND", SLA_BACKGROUND_MS);

    // Initialize token bucket (50 tokens, 50 tokens/sec refill)
    initTokenBucket(TOKEN_BUCKET_MAX, 50.0f);

    // Initialize rate limiter (50 messages per second)
    initRateLimiter(RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_MSGS);

    // Initialize policy engine
    initPolicy();

    // Initialize metrics
    memset(&metrics, 0, sizeof(metrics));
    metrics.lastThroughputCalcTime = millis();

    Serial.println("[INIT] QoS Management System initialized");
    Serial.println("[INIT] Priority Levels: EMERGENCY < CRITICAL < NORMAL < BACKGROUND");
    Serial.printf("[INIT] SLA Targets: %dms, %dms, %dms, %dms\n",
                 SLA_EMERGENCY_MS, SLA_CRITICAL_MS, SLA_NORMAL_MS, SLA_BACKGROUND_MS);
    Serial.printf("[INIT] Token Bucket: %d tokens max, %.1f tokens/sec refill\n",
                 TOKEN_BUCKET_MAX, tokenBucket.refillRate);
    Serial.printf("[INIT] Rate Limit: %d messages per %dms window\n",
                 RATE_LIMIT_MAX_MSGS, RATE_LIMIT_WINDOW_MS);
}

// ============================================================
// ARDUINO SETUP AND LOOP
// ============================================================

void setup() {
    // Initialize serial communication
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n\n");
    Serial.println("================================================");
    Serial.println("    QoS MANAGEMENT LAB - ESP32 IMPLEMENTATION");
    Serial.println("================================================");
    Serial.println();

    // Configure GPIO pins
    pinMode(BTN_EMERGENCY, INPUT_PULLUP);
    pinMode(BTN_CRITICAL, INPUT_PULLUP);
    pinMode(BTN_NORMAL, INPUT_PULLUP);
    pinMode(BTN_BACKGROUND, INPUT_PULLUP);

    pinMode(LED_EMERGENCY, OUTPUT);
    pinMode(LED_CRITICAL, OUTPUT);
    pinMode(LED_NORMAL, OUTPUT);
    pinMode(LED_BACKGROUND, OUTPUT);

    // Initialize all LEDs off
    digitalWrite(LED_EMERGENCY, LOW);
    digitalWrite(LED_CRITICAL, LOW);
    digitalWrite(LED_NORMAL, LOW);
    digitalWrite(LED_BACKGROUND, LOW);

    // Initialize QoS system
    initQoSSystem();

    // Startup LED test
    Serial.println("[INIT] LED test sequence...");
    for (int i = 0; i < 4; i++) {
        uint8_t pins[] = {LED_EMERGENCY, LED_CRITICAL, LED_NORMAL, LED_BACKGROUND};
        digitalWrite(pins[i], HIGH);
        delay(200);
        digitalWrite(pins[i], LOW);
    }

    Serial.println("[INIT] System ready!");
    Serial.println("[INIT] Press buttons to generate traffic or wait for auto-generation");
    Serial.println();
}

void loop() {
    // Handle user input
    handleButtons();
    handlePotentiometer();

    // Generate automatic traffic for demonstration
    generateAutoTraffic();

    // Evaluate and apply QoS policy
    evaluatePolicy();

    // Process messages from priority queues
    processQueues();

    // Update LED status indicators
    updateLEDs();

    // Display metrics dashboard
    displayMetrics();

    // Small delay to prevent busy-waiting
    delay(10);
}

151.6 Expected Outcomes

After running the lab, you should observe:

  1. Priority Processing: Emergency messages (Button 1) are always processed first, even when other queues are full
  2. Traffic Shaping: Adjusting the potentiometer changes how quickly messages are processed
  3. Rate Limiting: Rapid button pressing eventually triggers rate limiting
  4. SLA Violations: If queue processing is too slow, SLA violations are logged
  5. Policy Enforcement: Under high load, background traffic is automatically dropped
  6. LED Indicators: LEDs reflect queue states (solid = messages waiting, blinking = SLA violations)

151.7 Challenge Exercises

Challenge 1: Implement Weighted Fair Queuing

Modify the processQueues() function to implement weighted fair queuing instead of strict priority. Each priority level should get a proportional share of bandwidth: - Emergency: 40% of bandwidth - Critical: 30% of bandwidth - Normal: 20% of bandwidth - Background: 10% of bandwidth

Hint: Track credits per queue and serve queues in round-robin fashion based on their credits.

Challenge 2: Add Priority Aging

Implement a priority aging mechanism where messages waiting too long in lower-priority queues get their effective priority increased. This prevents starvation while still respecting priorities.

Implementation ideas:

  • Track time in queue for each message
  • Calculate effective priority as base priority minus (wait_time / AGING_FACTOR)
  • Messages should never exceed emergency priority through aging
Challenge 3: Implement Leaky Bucket

Replace the token bucket with a leaky bucket implementation: - Messages enter a queue (bucket) - Messages leave at a constant rate (leak) - If bucket overflows, messages are dropped

Compare the behavior differences between token bucket (allows bursts) and leaky bucket (constant rate).

Challenge 4: Add Network Condition Simulation

Simulate variable network conditions by: - Adding random processing delays - Randomly dropping messages (packet loss) - Varying the token refill rate automatically

Observe how the QoS system adapts to changing conditions.

Challenge 5: Implement Hierarchical Queuing

Create a two-level hierarchy: 1. Tier 1: Emergency and Critical share one queue group 2. Tier 2: Normal and Background share another queue group

Apply weighted fair queuing between tiers (70% Tier 1, 30% Tier 2), then strict priority within each tier.

Scenario: An ESP32-based smart factory gateway manages 50 sensors with different priority levels. Design a token bucket to ensure emergency alerts get through while preventing sensor floods.

Traffic Profile:

  • Emergency sensors (5 devices): 1 msg/hour each = 5 msg/hour = 0.0014 msg/sec
  • Critical sensors (15 devices): 1 msg/min each = 15 msg/min = 0.25 msg/sec
  • Normal sensors (20 devices): 1 msg/10 sec each = 2 msg/sec
  • Background logs (10 devices): 1 msg/30 sec each = 0.33 msg/sec
  • Total average: 2.58 msg/sec

Token Costs (from lab code):

  • Emergency: 1 token/msg
  • Critical: 2 tokens/msg
  • Normal: 5 tokens/msg
  • Background: 10 tokens/msg

Step 1: Calculate Average Token Consumption

  • Emergency: 0.0014 msg/s × 1 token = 0.0014 tokens/s
  • Critical: 0.25 msg/s × 2 tokens = 0.50 tokens/s
  • Normal: 2.0 msg/s × 5 tokens = 10.0 tokens/s
  • Background: 0.33 msg/s × 10 tokens = 3.3 tokens/s
  • Total: 13.8 tokens/s average consumption

Step 2: Add Safety Margin for Bursts

  • Peak burst scenario: All 50 sensors send within 1 second (rare but possible during power restoration)
  • Peak consumption: (5×1) + (15×2) + (20×5) + (10×10) = 5 + 30 + 100 + 100 = 235 tokens
  • Bucket capacity should handle 10-second burst: 235 tokens × 10 = 2,350 tokens minimum

Step 3: Design Token Bucket Parameters

#define TOKEN_BUCKET_MAX 100    // Lab default - TOO SMALL for this scenario!
float refillRate = 50.0;        // Lab default

// CORRECTED design for 50-sensor factory:
#define TOKEN_BUCKET_MAX 250    // Handles 1-second burst from all sensors
float refillRate = 15.0;        // 15 tokens/sec = 109% of average consumption

Verification:

  • Refill rate: 15 tokens/s (slightly above 13.8 average → bucket slowly refills)
  • Burst capacity: 250 tokens ÷ 5 tokens/normal msg = 50 normal messages (enough for brief spike)
  • Emergency guarantee: 1 token/msg means emergencies pass even when bucket at 1% (2.5 tokens)

Step 4: Simulate 60-Second Traffic Pattern

Time Event Tokens Consumed Tokens Refilled Bucket Level Pass/Drop
0s Bucket full - - 250 -
1s 2 normal msgs 10 15 255 (capped at 250) PASS
5s 20-sensor burst! 100 60 210 PASS (all 20)
6s 1 emergency 1 15 224 PASS
10s Normal traffic 50 60 234 PASS
30s Sustained high rate 400 300 134 18 PASS, 2 DROP
60s Normal traffic 828 450 0 (empty!) ALL DROP

Key Observations:

  1. Bucket drains under sustained overload (30-60s): Consumed 828 tokens, refilled only 450 → deficit of 378
  2. Emergency messages survive: Cost only 1 token, so pass unless bucket completely empty
  3. Refill rate determines steady-state: 15 tokens/s supports ~13.8 tokens/s average with 8% margin

Optimal Design for THIS scenario:

  • Bucket size: 250 tokens (10-second burst absorption)
  • Refill rate: 15 tokens/s (8% above average consumption)
  • Result: Handles normal traffic + brief bursts, degrades gracefully under sustained overload

Comparison with Lab Defaults (100 tokens, 50 tokens/s):

  • Lab defaults: Bucket empties in 2 seconds under full load, then refills immediately (high jitter)
  • Optimized design: Bucket lasts 18 seconds under overload (smoother degradation), but takes 17 seconds to refill (slower recovery)

Rule of Thumb: Token bucket capacity = Peak rate × Burst duration tolerance. Refill rate = Average consumption × 1.1 (10% headroom).

QoS Requirement Mechanism When to Use ESP32 Implementation
Prevent one device from flooding network Rate limiting (sliding window) Always - basic protection if (rateLimiter.messageCount >= MAX_MSGS) reject()
Ensure critical messages arrive first Priority queuing Mixed-criticality traffic (emergency + routine) 4-level queues (0=emergency, 3=background)
Smooth bursty traffic Token bucket shaping Irregular traffic patterns (event-driven sensors) if (!consumeTokens(cost)) buffer()
Guarantee minimum bandwidth Weighted fair queuing Multiple traffic classes with service guarantees Process 40% from Queue 0, 30% from Queue 1, etc.
Prevent starvation of low-priority traffic Priority aging Long-running systems with rare background tasks Increase priority if wait time >threshold

Selection Decision Tree:

  1. Is one device capable of overwhelming the system?
    • YES → Implement rate limiting first (50 msg/sec/device)
    • NO → Continue to step 2
  2. Do you have critical vs non-critical traffic?
    • YES → Add priority queuing (2-4 priority levels)
    • NO → Continue to step 3 (single queue sufficient)
  3. Is traffic bursty (spikes followed by quiet periods)?
    • YES → Add token bucket shaping (smooth bursts, allow temporary overrun)
    • NO → Priority queuing alone sufficient
  4. Must low-priority traffic eventually get through?
    • YES → Implement priority aging OR weighted fair queuing
    • NO → Strict priority OK (low-priority can starve)

Real-World Mapping (Smart Building with 500 Sensors):

Sensor Type Count Rate Priority QoS Mechanism Why
Fire alarm 20 1/hour (rare) 0 (emergency) Priority queue only Low rate, highest priority → no shaping needed
HVAC control 50 1/minute 1 (critical) Priority queue + token bucket Bursty (all ACs sync), needs smoothing
Occupancy 300 1/10 sec 2 (normal) Rate limiting + WFQ High volume, must prevent flood
Diagnostics 130 1/30 sec 3 (background) Aging + rate limiting Low priority but must not starve

Specific Configuration for Building:

// Priority Queues
initQueue(&queues[PRIORITY_EMERGENCY], "FIRE", 50);  // 50ms SLA
initQueue(&queues[PRIORITY_CRITICAL], "HVAC", 200);  // 200ms SLA
initQueue(&queues[PRIORITY_NORMAL], "OCCUPANCY", 1000);  // 1s SLA
initQueue(&queues[PRIORITY_BACKGROUND], "DIAG", 5000);  // 5s SLA

// Token Bucket (for HVAC smoothing)
initTokenBucket(500, 20.0);  // 500 tokens, 20 tokens/sec
// Allows 50 HVAC msgs (10 tokens each) burst, then throttles to 20/sec

// Rate Limiter (per-device)
initRateLimiter(1000, 100);  // 100 msgs per second total (all devices)

// Weighted Fair Queuing (prevents starvation)
float queueWeights[4] = {0.4, 0.3, 0.2, 0.1};  // 40% emergency, 30% critical, etc.

Performance Expectations:

Configuration Emergency Latency HVAC Burst Handled Occupancy Throughput Background Starvation
Priority only <10ms ALL (but floods network) Limited by flood Common (>50% starved)
Priority + Rate Limit <10ms First 100, then drops Capped at 100/sec Still common (30%)
Priority + Token + WFQ <10ms 50 burst, then shaped 80/sec sustained Rare (<5%)

Key Insight: Combine mechanisms for robust QoS. Priority queuing alone allows starvation. Rate limiting alone doesn’t prioritize. Token bucket alone doesn’t differentiate traffic. The lab’s multi-layered approach (priority + token + rate + policy) mirrors production SDN controllers (ONOS, OpenDaylight).

Common Mistake: Confusing Rate Limiting and Traffic Shaping

The Error: Developers treat rate limiting (hard cap) and traffic shaping (token bucket) as interchangeable, leading to either dropped critical messages or uncontrolled bursts.

Scenario: A smart home hub receives sensor data from 30 devices. Developer implements “rate limiting” to prevent overload.

Broken Approach (Hard Rate Limit):

// WRONG: Drops everything beyond 50 msg/sec, even emergency alerts
if (messageCount >= 50) {
    drop(packet);  // Critical fire alarm dropped during routine burst!
}

What Happens:

  • Normal operation: 20 sensors × 1 msg/sec = 20 msg/sec (under limit, OK)
  • Burst event: Power restored, all 30 sensors send at once = 30 msg/sec (under limit, OK)
  • Emergency during burst: 30 sensors + 1 fire alarm = 31 msg/sec → Fire alarm DROPPED (waited 1 second before retry)

Measured Impact:

  • Without emergency: Rate limiter works fine, drops only background logs
  • During brief burst (5 seconds): 37% of emergency alerts dropped (11/30 test fires)
  • Fire detection delay increased from <100ms to 2.3 seconds average (some retries took 8+ seconds)

The Fix: Token Bucket Shaping with Priority

// CORRECT: Shape using tokens, but differentiate by priority
float cost = getTokenCost(priority);  // Emergency=1, Normal=5, Background=10

if (consumeTokens(cost)) {
    enqueue(packet);  // Accepted into priority queue
} else {
    // Token bucket empty - check if EMERGENCY (life-critical)
    if (priority == PRIORITY_EMERGENCY && tokenBucket.tokens >= 1) {
        // Emergency bypass: Reserve 1% of bucket for life-safety
        consumeTokens(1);
        enqueue(packet);
        logEmergencyBypass();
    } else {
        drop(packet);  // Drop non-critical when overloaded
    }
}

Comparison:

Mechanism Normal Burst (30 msgs) Emergency During Burst Sustained Overload (100 msg/sec)
Hard rate limit (50 msg/sec) All pass Fire alarm dropped (31>50) All >50 dropped (no prioritization)
Token bucket (50 tokens/sec) All pass (bucket absorbs burst) All pass (1-token cost) Background dropped first, emergency last
Lab implementation (both) All pass All pass (priority bypass) Emergency: 100% pass, Background: 15% pass

Why Token Bucket + Priority Works:

  1. Token bucket smooths bursts:
    • 30-device burst: Bucket has 100 tokens, devices consume 5 each = 150 tokens needed
    • First 20 devices (100 tokens) pass immediately
    • Remaining 10 wait ~2 seconds as bucket refills (50 tokens/sec)
    • Result: Burst spread over 2 seconds instead of dropped
  2. Priority ensures critical messages bypass during congestion:
    • Fire alarm costs 1 token (cheapest)
    • Even when bucket at 1% (1 token remaining), emergency passes
    • Background logs cost 10 tokens → dropped when bucket <10

Real-World Numbers (from lab testing):

Test Scenario Hard Rate Limit Token Bucket Only Token + Priority
Fire alarm latency (normal) 50ms 45ms 40ms
Fire alarm latency (during burst) 2.3s (dropped, retried) 80ms (delayed in queue) 55ms (priority bypass)
Background log delivery (overload) 0% (all dropped) 15% (random drop) 10% (lowest priority, graceful degradation)
Network utilization 65% (bursts dropped) 92% (bursts smoothed) 88% (smoothed + prioritized)

Rule of Thumb:

  • Rate limiter: Hard limit to prevent system crash (100 msg/sec cap for 10 msg/sec system = 10× headroom)
  • Token bucket: Smooth bursts, allow temporary overrun (bucket size = 2× peak burst)
  • Priority queuing: Ensure critical messages bypass during congestion

Key Lesson: The lab combines all three mechanisms (rate limit + token + priority) because each solves a different problem. Rate limiting prevents catastrophic overload. Token bucket smooths bursts. Priority ensures critical messages get through. Using only one creates failure modes.

Key Concepts

  • ESP32 FreeRTOS Task Priority: FreeRTOS priority levels (0–configMAX_PRIORITIES-1) assigned to tasks, where higher numbers indicate higher priority — used to ensure time-critical IoT tasks (sensor reading, actuation) preempt background tasks (telemetry upload, OTA update check)
  • Message Queue (FreeRTOS): A thread-safe FIFO buffer enabling producer tasks (sensor sampling) to enqueue data for consumer tasks (MQTT uploader) without shared variable race conditions or blocking — the primary IPC mechanism in ESP32 IoT firmware
  • Priority Inversion: A scheduling anomaly where a high-priority task is blocked waiting for a resource held by a low-priority task that is itself preempted by medium-priority tasks — resolved by priority inheritance or priority ceiling protocol in FreeRTOS mutexes
  • MQTT Will Message: A broker-stored message published on behalf of a device when the device disconnects abnormally (without sending DISCONNECT) — used to alert subscribers that a device has gone offline unexpectedly
  • Retain Flag: An MQTT feature causing the broker to store the last message on a topic and deliver it immediately to new subscribers — useful for IoT device state topics so new dashboard clients see the current state without waiting for the next publish
  • ESP32 Wi-Fi Power Save Mode: An IEEE 802.11 power management feature where the ESP32 radio sleeps between beacon intervals, waking periodically to receive buffered messages — trades latency (up to 100 ms delay) for 10× reduction in Wi-Fi idle current

Common Pitfalls

Running sensor sampling and MQTT publishing in the same FreeRTOS task. When the network is congested and MQTT publish blocks, sensor samples are missed. Use separate tasks: a high-priority sensor task queues readings; a lower-priority network task drains the queue.

Using vTaskDelay(100ms) for 10 Hz sensor sampling. vTaskDelay delays from the end of execution, causing sampling rate to drift when task execution time varies. Use xTaskDelayUntil() to maintain precise fixed-rate execution regardless of variable processing time.

Reconnecting to MQTT broker after disconnection without re-subscribing to topics. The broker clears subscriptions when a client disconnects (for clean sessions). Implement resubscription in the reconnect callback for all required topics.

Assigning 2048-byte stack to tasks that use printf, JSON serialization, and deep function call chains. Stack overflows on ESP32 cause silent heap corruption. Use uxTaskGetStackHighWaterMark() to measure actual stack usage and provision 50% headroom.

151.8 Summary

In this lab, you implemented a complete QoS management system on ESP32 including:

  • Priority Queues: Four-level priority system with SLA tracking
  • Token Bucket: Traffic shaping with configurable refill rate
  • Rate Limiter: Sliding window protection against overload
  • Policy Engine: Dynamic load-based policy adjustment
  • Metrics Dashboard: Real-time visibility into QoS performance
Key Takeaway

A complete QoS management system requires four integrated mechanisms: priority queuing (to ensure critical messages are processed first), traffic shaping via token bucket (to control burst rates while allowing flexibility), rate limiting (to protect backend systems from overload), and a dynamic policy engine (to automatically adapt behavior under load). Implementing these on constrained ESP32 hardware demonstrates that effective QoS does not require powerful infrastructure – it requires smart design.

Building a QoS system is like being a lifeguard at the busiest swimming pool ever!

151.8.1 The Sensor Squad Adventure: Lifeguard Lila’s Pool Rules

Lila the LED was the new lifeguard at Sensor City’s massive swimming pool. THOUSANDS of messages wanted to swim at the same time!

“HELP! EMERGENCY!” screamed a fire alarm message, but it was stuck behind 500 temperature readings all splashing around slowly.

“This is chaos!” said Lila. “We need POOL RULES!”

She set up four lanes:

  • Lane 1 (Red - EMERGENCY): Only for fire alarms and safety alerts. These ALWAYS swim first! No waiting!
  • Lane 2 (Yellow - IMPORTANT): Door locks, motion detectors. They go next!
  • Lane 3 (Green - NORMAL): Regular temperature and humidity readings. Wait your turn politely!
  • Lane 4 (Blue - BACKGROUND): System logs and diagnostics. Swim whenever there is room!

Max the Microcontroller added a TOKEN MACHINE at the entrance: “Each swimmer needs tokens to enter. Emergency messages need just 1 token, but background logs need 10 tokens. This way, important messages always get through!”

Bella the Battery was the RATE LIMITER: “Only 50 swimmers per minute allowed in! If too many show up at once, some have to wait in the parking lot.”

Sammy the Sensor watched the dashboard: “Lane 1 is empty – good, no emergencies! Lane 3 has 15 waiting – normal. Lane 4 is FULL – time to drop some old log messages to make room!”

One day, the system got SUPER busy. Lila’s POLICY ENGINE kicked in: “HIGH LOAD DETECTED! Closing Lane 4 completely – all resources to Lanes 1 and 2!” The emergency messages zoomed through while background logs waited patiently.

“THAT’S QoS!” cheered the Sensor Squad. “Making sure the most important messages ALWAYS get through, even when things get busy!”

151.8.2 Try This at Home!

Build a Priority Queue with Cards!

  1. Get a deck of cards. Assign priorities: Aces = EMERGENCY, Face cards = IMPORTANT, Number cards = NORMAL
  2. Shuffle and place them in a pile face-down
  3. Flip cards one at a time into 3 separate piles (queues) by priority
  4. “Process” by always taking from the EMERGENCY pile first, then IMPORTANT, then NORMAL
  5. Notice how Aces always get processed first, even if the NORMAL pile is huge!

151.9 Concept Relationships

Core Concept Builds On Enables Contrasts With
Priority Queues (3-tier) Queue data structure, priority comparison Emergency messages processed first FIFO queue, equal treatment
Token Bucket Shaping Token refill rate, differentiated costs Burst smoothing, rate limiting Unlimited transmission, no flow control
Dynamic Policy Adjustment System load monitoring, threshold detection Graceful degradation under overload Static policies, fail-stop behavior
Message Classification Message type inspection, priority mapping Automated traffic segregation Manual tagging, application-layer routing
Queue Starvation Prevention Weighted fair queuing, minimum guarantees Low-priority messages eventually processed Strict priority (can starve low queues)

151.10 See Also

Prerequisites:

Next Steps:

Related Topics:

151.11 What’s Next

  • QoS in Real-World IoT: Apply these concepts to industrial IoT, smart buildings, and learn about protocol-level QoS
  • SDN Fundamentals: Learn how Software-Defined Networking enables programmable QoS policies
Previous Up Next
QoS Fundamentals QoS Service Management QoS Real-World Patterns