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.
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
For Beginners: Lab: ESP32 QoS Management
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:
Priority Queuing: Multi-level priority queues with configurable scheduling
Traffic Shaping: Token bucket implementation for rate control
Rate Limiting: Request throttling to protect system resources
Service Level Monitoring: Real-time SLA tracking and violation detection
QoS Policy Engine: Dynamic policy enforcement based on system state
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
Add 4 Push Buttons: Click + and search for “Push Button” - add 4 buttons
Add 4 LEDs: Click + and search for “LED” - add Red, Yellow, Green, and Blue LEDs
Add 1 Potentiometer: Click + and search for “Potentiometer”
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)
151.5.2 Step 2: Understanding the QoS Architecture
This lab implements a complete QoS management system with the following components:
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.
Full Lab Code (click to expand)
/* * 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=Backgrounduint32_t enqueueTime;// When message entered queueuint32_t processTime;// When message was processedchar payload[MESSAGE_PAYLOAD_SIZE];// Message contentuint16_t payloadSize;// Actual payload sizeuint32_t sequenceNumber;// Unique message IDbool 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 bufferuint8_t head;// Read positionuint8_t tail;// Write positionuint8_t count;// Current message countuint8_t maxSize;// Maximum capacityuint32_t slaLatencyMs;// Maximum allowed latencyuint32_t totalEnqueued;// Total messages receiveduint32_t totalProcessed;// Total messages processeduint32_t totalDropped;// Messages dropped (overflow)uint32_t slaViolations;// SLA violations countuint64_t totalLatencyMs;// Cumulative latency for averagingconstchar* 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 countfloat maxTokens;// Maximum token capacityfloat refillRate;// Tokens added per seconduint32_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 timeuint32_t messageCount;// Messages in current windowuint32_t windowDurationMs;// Window durationuint32_t maxMessagesPerWindow;// Maximum allowed messagesuint32_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 receiveduint32_t totalMessagesProcessed;// Successfully processeduint32_t totalMessagesDropped;// Dropped due to overflowuint32_t totalMessagesThrottled;// Rejected by rate limiteruint32_t totalSLAViolations;// Total SLA breachesfloat avgLatencyMs[NUM_PRIORITY_LEVELS];// Average latency per priorityfloat throughputMsgsPerSec;// Current throughputuint32_t lastThroughputCalcTime;// Last throughput calculationuint32_t messagesInLastSecond;// Messages processed in last secondfloat tokenBucketLevel;// Current token bucket fill levelfloat 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 policyfloat criticalLoadThreshold;// Load level triggering critical policybool emergencyOnlyMode;// Only process emergency messagesbool dropBackgroundTraffic;// Drop background during high loaduint32_t lastPolicyCheck;// Last policy evaluation timeuint32_t policyCheckIntervalMs;// Policy check frequency};// ============================================================// GLOBAL STATE// ============================================================// Priority Queues (one per priority level)PriorityQueue queues[NUM_PRIORITY_LEVELS];// Traffic ShapingTokenBucket tokenBucket;RateLimiter rateLimiter;// Metrics and PolicyQoSMetrics metrics;QoSPolicy policy;// Message Sequencinguint32_t nextSequenceNumber =1;// Button State Trackinguint32_t lastButtonPress[4]={0,0,0,0};bool buttonState[4]={false,false,false,false};// LED State for Blinkinguint32_t ledLastToggle[4]={0,0,0,0};bool ledState[4]={false,false,false,false};// Dashboard Timinguint32_t lastMetricsDisplay =0;// Auto-traffic generation for demonstrationuint32_t lastAutoTraffic =0;bool autoTrafficEnabled =true;uint32_t autoTrafficInterval =500;// Generate traffic every 500ms// ============================================================// QUEUE OPERATIONS// ============================================================/** * Initialize a priority queue with given parameters * * @paramqueue Pointer to the queue structure * @paramname Human-readable queue name * @paramslaLatencyMs Maximum allowed latency for SLA compliance */void initQueue(PriorityQueue* queue,constchar* 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 bufferfor(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 * * @paramqueue 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 * * @paramqueue 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 * * @paramqueue Pointer to the target queue * @parampriority Message priority level * @parampayload Message content * @parampayloadSize Size of payload in bytes * @return true if message was enqueued, false if dropped */bool enqueueMessage(PriorityQueue* queue,uint8_t priority,constchar* payload,uint16_t payloadSize){// Check for queue overflowif(isQueueFull(queue)){ queue->totalDropped++; metrics.totalMessagesDropped++; Serial.printf("[DROP] %s queue full, dropping message #%lu\n", queue->name, nextSequenceNumber);returnfalse;}// 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++;returntrue;}/** * Dequeue a message from a priority queue * Updates SLA tracking based on message latency * * @paramqueue Pointer to the source queue * @paramoutMessage Pointer to store dequeued message * @return true if message was dequeued, false if queue empty */bool dequeueMessage(PriorityQueue* queue, Message* outMessage){if(isQueueEmpty(queue)){returnfalse;}// Get message at head Message* msg =&queue->messages[queue->head];// Calculate latencyuint32_t latency = millis()- msg->enqueueTime; msg->processTime = millis();// Check SLA complianceif(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--;returntrue;}/** * Get the fill percentage of a queue * * @paramqueue 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 * * @parammaxTokens Maximum token capacity * @paramrefillRate 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 timefloat tokensToAdd =(elapsed /1000.0f)* tokenBucket.refillRate; tokenBucket.tokens = min(tokenBucket.tokens + tokensToAdd, tokenBucket.maxTokens); tokenBucket.lastRefillTime = now;}}/** * Attempt to consume tokens for a message * * @paramtokensRequired 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;returntrue;}returnfalse;}/** * Get token cost for a message based on its priority * Higher priority messages cost fewer tokens (get more bandwidth) * * @parampriority 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 * * @paramwindowDurationMs Duration of the sliding window * @parammaxMessagesPerWindow 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 expiredif(now - rateLimiter.windowStart >= rateLimiter.windowDurationMs){// Reset window rateLimiter.windowStart = now; rateLimiter.messageCount =0;}// Check if within limitif(rateLimiter.messageCount < rateLimiter.maxMessagesPerWindow){ rateLimiter.messageCount++;returntrue;}// Rate limit exceeded rateLimiter.totalRejected++; metrics.totalMessagesThrottled++;returnfalse;}/** * Get current rate (messages per second) * * @return Current message rate */float getCurrentRate(){uint32_t elapsed = millis()- rateLimiter.windowStart;if(elapsed ==0)return0;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 heavilyfloat 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 periodicallyif(now - policy.lastPolicyCheck < policy.policyCheckIntervalMs){return;} policy.lastPolicyCheck = now;// Calculate current load metrics.systemLoad = calculateSystemLoad();// Update policy based on loadbool wasEmergencyOnly = policy.emergencyOnlyMode;bool wasDropping = policy.dropBackgroundTraffic;if(metrics.systemLoad >= policy.criticalLoadThreshold){// Critical load: emergency only policy.emergencyOnlyMode =true; policy.dropBackgroundTraffic =true;}elseif(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 changesif(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 * * @parampriority Message priority level * @return true if message should be accepted, false if policy rejects it */bool policyAllowsMessage(uint8_t priority){// Emergency always allowedif(priority == PRIORITY_EMERGENCY){returntrue;}// Emergency-only mode blocks all non-emergencyif(policy.emergencyOnlyMode){returnfalse;}// High load mode drops backgroundif(policy.dropBackgroundTraffic && priority == PRIORITY_BACKGROUND){returnfalse;}returntrue;}// ============================================================// 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 orderfor(int priority =0; priority < NUM_PRIORITY_LEVELS; priority++){ PriorityQueue* queue =&queues[priority];// Process all messages in this priority level before moving to nextwhile(!isQueueEmpty(queue)){// Check rate limitif(!checkRateLimit()){ Serial.println("[RATE LIMIT] Rate limit exceeded, pausing processing");return processed;}// Check token bucketfloat cost = getTokenCost(priority);if(!consumeTokens(cost)){// Not enough tokens - try lower priority or waitbreak;}// Dequeue and processif(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 * * @parampriority Message priority level */void generateTraffic(uint8_t priority){char payload[MESSAGE_PAYLOAD_SIZE];constchar* 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 policyif(!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 generationint r = random(100);if(r <2){// 2% emergency generateTraffic(PRIORITY_EMERGENCY);}elseif(r <12){// 10% critical generateTraffic(PRIORITY_CRITICAL);}elseif(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 stateif(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);}elseif(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 * * @paramqueue Pointer to the queue * @return Average latency in milliseconds */float calculateAvgLatency(PriorityQueue* queue){if(queue->totalProcessed ==0)return0;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("------------|-------|----------|-----------|---------|----------|--------");constchar* 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 detectionif(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 changedif(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);}
Quick Check: Token Bucket Design
151.6 Expected Outcomes
After running the lab, you should observe:
Priority Processing: Emergency messages (Button 1) are always processed first, even when other queues are full
Traffic Shaping: Adjusting the potentiometer changes how quickly messages are processed
Rate Limiting: Rapid button pressing eventually triggers rate limiting
SLA Violations: If queue processing is too slow, SLA violations are logged
Policy Enforcement: Under high load, background traffic is automatically dropped
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).
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.
Worked Example: Calculating Token Bucket Rate for IoT QoS
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.
#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 sensorsfloat refillRate =15.0;// 15 tokens/sec = 109% of average consumption
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 QueuesinitQueue(&queues[PRIORITY_EMERGENCY],"FIRE",50);// 50ms SLAinitQueue(&queues[PRIORITY_CRITICAL],"HVAC",200);// 200ms SLAinitQueue(&queues[PRIORITY_NORMAL],"OCCUPANCY",1000);// 1s SLAinitQueue(&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 alertsif(messageCount >=50){ drop(packet);// Critical fire alarm dropped during routine 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.
Match: QoS Lab Mechanisms
Order: Building a QoS System on ESP32
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
1. Not Separating Sensor and Network Tasks in FreeRTOS
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.
2. Using vTaskDelay Instead of xTaskDelayUntil for Fixed-Rate Sampling
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.
3. Not Handling MQTT Reconnect Without Resubscribing
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.
4. Ignoring ESP32 Task Stack Overflow in Complex QoS Logic
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.
Label the Diagram
💻 Code Challenge
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
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.
For Kids: Meet the Sensor Squad!
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!
Get a deck of cards. Assign priorities: Aces = EMERGENCY, Face cards = IMPORTANT, Number cards = NORMAL
Shuffle and place them in a pile face-down
Flip cards one at a time into 3 separate piles (queues) by priority
“Process” by always taking from the EMERGENCY pile first, then IMPORTANT, then NORMAL
Notice how Aces always get processed first, even if the NORMAL pile is huge!