ESP32-S3 Programming Development Guide

ESP32-S3 Programming Development Guide

Complete ESP32-S3 programming guide based on XiaoZhi AI voice robot project, covering basic GPIO operations, network communication, audio processing, AI feature integration and practical project development.

I. Development Environment Setup

1.1 Arduino IDE Development

Beginner Friendly: Arduino IDE is suitable for rapid prototyping and basic function development, with a simple interface and rich library ecosystem

Installation Steps

# 1. Install Arduino IDE 2.x
# Download: https://www.arduino.cc/en/software

# 2. Add ESP32 development board support
File → Preferences → Additional Boards Manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json

# 3. Install ESP32 board package
Tools → Board → Boards Manager → Search "ESP32" → Install

XiaoZhi AI Arduino Library Configuration

// libraries.txt - Recommended Arduino libraries
#include <WiFi.h>           // Wi-Fi connectivity
#include <WebSocketsClient.h> // WebSocket communication
#include <ArduinoJson.h>    // JSON data parsing
#include <I2S.h>            // Audio I2S interface
#include <driver/i2s.h>     // Low-level I2S driver
#include <HTTPClient.h>     // HTTP requests
#include <esp_task_wdt.h>   // Watchdog timer

1.2 ESP-IDF Professional Development

Environment Setup

# Install ESP-IDF (Linux/macOS)
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
git checkout v5.3.2
./install.sh esp32s3
source export.sh

# Windows: Use ESP-IDF installer
# Download: https://dl.espressif.com/dl/esp-idf-installer/

Project Initialization

# Create XiaoZhi AI project
idf.py create-project xiaozhi_ai
cd xiaozhi_ai

# Configure target chip
idf.py set-target esp32s3

# Configure project
idf.py menuconfig

Key Configuration Options

ESP-IDF Configuration Recommendations:
├── Component Config
│   ├── ESP32S3-Specific → PSRAM: Enable
│   ├── ESP32S3-Specific → Flash Size: 16MB  
│   ├── FreeRTOS → Tick Rate: 1000Hz
│   ├── WiFi → Max WiFi Static RX: 10
│   └── Bluetooth → Enable
├── Audio Configuration
│   ├── I2S → Enable
│   └── I2S → Sample Rate: 16000Hz
└── Serial Flasher Config
    └── Flash Size: 16MB

II. Basic GPIO Programming

2.1 Digital I/O Operations

LED Control Example

// LED control - Arduino style
#define LED_PIN 48  // WS2812 RGB LED

void setup() {
    pinMode(LED_PIN, OUTPUT);
    Serial.begin(115200);
}

void loop() {
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
    delay(1000);
}

Button Reading with Interrupt

// Button interrupt - Arduino style
#define BUTTON_PIN 0    // Boot button
volatile bool buttonPressed = false;

void IRAM_ATTR buttonISR() {
    buttonPressed = true;
}

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
    Serial.begin(115200);
}

void loop() {
    if (buttonPressed) {
        Serial.println("Button pressed!");
        buttonPressed = false;
    }
    delay(100);
}

2.2 Analog Input Processing

ADC Reading and Filtering

// ADC reading with moving average filter
#define ADC_PIN A0
#define FILTER_SIZE 10

int adcBuffer[FILTER_SIZE];
int bufferIndex = 0;

int readFilteredADC() {
    adcBuffer[bufferIndex] = analogRead(ADC_PIN);
    bufferIndex = (bufferIndex + 1) % FILTER_SIZE;
    
    int sum = 0;
    for (int i = 0; i < FILTER_SIZE; i++) {
        sum += adcBuffer[i];
    }
    return sum / FILTER_SIZE;
}

void setup() {
    Serial.begin(115200);
    // Initialize buffer
    for (int i = 0; i < FILTER_SIZE; i++) {
        adcBuffer[i] = analogRead(ADC_PIN);
    }
}

void loop() {
    int filteredValue = readFilteredADC();
    float voltage = filteredValue * 3.3 / 4095.0;
    Serial.printf("ADC: %d, Voltage: %.2fV\n", filteredValue, voltage);
    delay(500);
}

2.3 PWM Control Applications

RGB LED Color Control

// RGB LED PWM control
#define RED_PIN 15
#define GREEN_PIN 16  
#define BLUE_PIN 17

void setRGBColor(int red, int green, int blue) {
    analogWrite(RED_PIN, red);
    analogWrite(GREEN_PIN, green);
    analogWrite(BLUE_PIN, blue);
}

void breathingEffect() {
    for (int brightness = 0; brightness <= 255; brightness += 5) {
        setRGBColor(brightness, 0, 0);  // Red breathing
        delay(50);
    }
    for (int brightness = 255; brightness >= 0; brightness -= 5) {
        setRGBColor(brightness, 0, 0);
        delay(50);
    }
}

III. Network Communication Programming

3.1 Wi-Fi Connection Management

Smart Wi-Fi Connection

#include <WiFi.h>
#include <Preferences.h>

Preferences preferences;

class WiFiManager {
private:
    String ssid, password;
    bool autoReconnect = true;
    
public:
    bool connectToWiFi(const char* ssid, const char* password) {
        WiFi.begin(ssid, password);
        
        int attempts = 0;
        while (WiFi.status() != WL_CONNECTED && attempts < 20) {
            delay(500);
            Serial.print(".");
            attempts++;
        }
        
        if (WiFi.status() == WL_CONNECTED) {
            Serial.printf("\nConnected to %s\n", ssid);
            Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
            
            // Save credentials
            preferences.begin("wifi", false);
            preferences.putString("ssid", ssid);
            preferences.putString("password", password);
            preferences.end();
            
            return true;
        }
        return false;
    }
    
    bool autoConnect() {
        preferences.begin("wifi", true);
        String savedSSID = preferences.getString("ssid", "");
        String savedPassword = preferences.getString("password", "");
        preferences.end();
        
        if (savedSSID.length() > 0) {
            return connectToWiFi(savedSSID.c_str(), savedPassword.c_str());
        }
        return false;
    }
    
    void enableAutoReconnect() {
        WiFi.setAutoReconnect(true);
        WiFi.persistent(true);
    }
};

WiFiManager wifiManager;

void setup() {
    Serial.begin(115200);
    
    if (!wifiManager.autoConnect()) {
        Serial.println("Auto-connect failed, starting AP mode...");
        // Start AP mode for configuration
        WiFi.softAP("XiaoZhi-Setup", "12345678");
        Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
    }
}

3.2 WebSocket Real-time Communication

WebSocket Client Implementation

#include <WebSocketsClient.h>
#include <ArduinoJson.h>

WebSocketsClient webSocket;

void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
    switch(type) {
        case WStype_DISCONNECTED:
            Serial.println("WebSocket Disconnected");
            break;
            
        case WStype_CONNECTED:
            Serial.printf("WebSocket Connected to: %s\n", payload);
            // Send initial message
            webSocket.sendTXT("{\"type\":\"device_online\",\"device\":\"xiaozhi_esp32\"}");
            break;
            
        case WStype_TEXT:
            Serial.printf("Received: %s\n", payload);
            handleWebSocketMessage((char*)payload);
            break;
            
        default:
            break;
    }
}

void handleWebSocketMessage(const char* message) {
    DynamicJsonDocument doc(1024);
    deserializeJson(doc, message);
    
    String command = doc["command"];
    if (command == "play_audio") {
        String audioUrl = doc["url"];
        // Handle audio playback
        playAudioFromURL(audioUrl);
    } else if (command == "set_volume") {
        int volume = doc["volume"];
        setVolumeLevel(volume);
    }
}

void setup() {
    Serial.begin(115200);
    
    // Connect to WebSocket server
    webSocket.begin("your-server.com", 8080, "/");
    webSocket.onEvent(webSocketEvent);
    webSocket.setReconnectInterval(5000);
}

void loop() {
    webSocket.loop();
}

3.3 HTTP Client Development

RESTful API Communication

#include <HTTPClient.h>
#include <ArduinoJson.h>

class APIClient {
private:
    String baseURL;
    String apiKey;
    
public:
    APIClient(String url, String key) : baseURL(url), apiKey(key) {}
    
    String sendTextToLLM(String text) {
        HTTPClient http;
        http.begin(baseURL + "/chat/completions");
        http.addHeader("Content-Type", "application/json");
        http.addHeader("Authorization", "Bearer " + apiKey);
        
        DynamicJsonDocument request(1024);
        request["model"] = "deepseek-chat";
        request["messages"][0]["role"] = "user";
        request["messages"][0]["content"] = text;
        
        String requestString;
        serializeJson(request, requestString);
        
        int httpResponseCode = http.POST(requestString);
        String response = "";
        
        if (httpResponseCode == 200) {
            response = http.getString();
            DynamicJsonDocument responseDoc(2048);
            deserializeJson(responseDoc, response);
            response = responseDoc["choices"][0]["message"]["content"];
        }
        
        http.end();
        return response;
    }
    
    bool uploadSensorData(float temperature, float humidity) {
        HTTPClient http;
        http.begin(baseURL + "/sensor/data");
        http.addHeader("Content-Type", "application/json");
        
        DynamicJsonDocument data(512);
        data["device_id"] = "xiaozhi_001";
        data["timestamp"] = millis();
        data["temperature"] = temperature;
        data["humidity"] = humidity;
        
        String dataString;
        serializeJson(data, dataString);
        
        int httpResponseCode = http.POST(dataString);
        http.end();
        
        return httpResponseCode == 200;
    }
};

IV. Audio System Programming

4.1 I2S Audio Interface

I2S Configuration and Initialization

#include "driver/i2s.h"

#define I2S_SAMPLE_RATE 16000
#define I2S_SAMPLE_BITS 32
#define I2S_CHANNEL_NUM 1

// Microphone pins (INMP441)
#define I2S_MIC_WS_PIN  4
#define I2S_MIC_SCK_PIN 5  
#define I2S_MIC_SD_PIN  6

// Speaker pins (MAX98357A)
#define I2S_SPK_BCLK_PIN 15
#define I2S_SPK_LRC_PIN  16
#define I2S_SPK_DIN_PIN  7

bool initMicrophone() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
        .sample_rate = I2S_SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 1024,
        .use_apll = false
    };
    
    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_MIC_SCK_PIN,
        .ws_io_num = I2S_MIC_WS_PIN,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_MIC_SD_PIN
    };
    
    esp_err_t result = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    if (result != ESP_OK) return false;
    
    result = i2s_set_pin(I2S_NUM_0, &pin_config);
    return result == ESP_OK;
}

bool initSpeaker() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = I2S_SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 1024,
        .use_apll = false
    };
    
    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_SPK_BCLK_PIN,
        .ws_io_num = I2S_SPK_LRC_PIN,
        .data_out_num = I2S_SPK_DIN_PIN,
        .data_in_num = I2S_PIN_NO_CHANGE
    };
    
    esp_err_t result = i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
    if (result != ESP_OK) return false;
    
    result = i2s_set_pin(I2S_NUM_1, &pin_config);
    return result == ESP_OK;
}

Audio Recording and Playback

// Audio recording task
void audioRecordTask(void *parameter) {
    int32_t *i2s_read_buffer = (int32_t*)malloc(1024 * sizeof(int32_t));
    int16_t *samples = (int16_t*)malloc(1024 * sizeof(int16_t));
    
    while (true) {
        size_t bytes_read;
        i2s_read(I2S_NUM_0, i2s_read_buffer, 1024 * sizeof(int32_t), &bytes_read, portMAX_DELAY);
        
        // Convert 32-bit to 16-bit samples
        for (int i = 0; i < bytes_read / sizeof(int32_t); i++) {
            samples[i] = (i2s_read_buffer[i] >> 16) & 0xFFFF;
        }
        
        // Process audio samples (e.g., send to voice recognition)
        processAudioSamples(samples, bytes_read / sizeof(int32_t));
    }
    
    free(i2s_read_buffer);
    free(samples);
}

// Audio playback task
void audioPlayTask(void *parameter) {
    while (true) {
        if (audioQueue.available()) {
            AudioData data = audioQueue.pop();
            
            size_t bytes_written;
            i2s_write(I2S_NUM_1, data.buffer, data.length, &bytes_written, portMAX_DELAY);
            
            free(data.buffer);
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void startAudioTasks() {
    xTaskCreatePinnedToCore(audioRecordTask, "AudioRecord", 4096, NULL, 5, NULL, 0);
    xTaskCreatePinnedToCore(audioPlayTask, "AudioPlay", 4096, NULL, 5, NULL, 1);
}

4.2 Voice Processing Algorithms

Simple Voice Activity Detection (VAD)

class VoiceActivityDetector {
private:
    float energyThreshold = 1000.0;
    int silentFrames = 0;
    int maxSilentFrames = 10;
    bool isSpeaking = false;
    
public:
    bool detectVoiceActivity(int16_t* samples, int length) {
        // Calculate energy
        float energy = 0;
        for (int i = 0; i < length; i++) {
            energy += samples[i] * samples[i];
        }
        energy = energy / length;
        
        if (energy > energyThreshold) {
            silentFrames = 0;
            if (!isSpeaking) {
                isSpeaking = true;
                Serial.println("Voice detected - Start speaking");
                return true;  // Speaking started
            }
        } else {
            silentFrames++;
            if (isSpeaking && silentFrames > maxSilentFrames) {
                isSpeaking = false;
                Serial.println("Voice ended - Stop speaking");
                return false; // Speaking ended
            }
        }
        
        return isSpeaking;
    }
    
    void setThreshold(float threshold) {
        energyThreshold = threshold;
    }
};

V. Sensor Interface Programming

5.1 I2C Communication

SSD1306 OLED Display Control

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

#define I2C_SDA 41
#define I2C_SCL 42

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

class DisplayManager {
public:
    bool init() {
        Wire.begin(I2C_SDA, I2C_SCL);
        
        if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
            Serial.println("SSD1306 allocation failed");
            return false;
        }
        
        display.clearDisplay();
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.println("XiaoZhi AI Ready");
        display.display();
        
        return true;
    }
    
    void showVoiceStatus(String status) {
        display.clearDisplay();
        display.setCursor(0, 0);
        display.setTextSize(2);
        display.println("Voice:");
        display.setTextSize(1);
        display.println(status);
        display.display();
    }
    
    void showNetworkInfo(String ip, String ssid) {
        display.clearDisplay();
        display.setCursor(0, 0);
        display.setTextSize(1);
        display.println("Network:");
        display.printf("SSID: %s\n", ssid.c_str());
        display.printf("IP: %s\n", ip.c_str());
        display.display();
    }
};

5.2 SPI Communication

SD Card Storage Management

#include "SD.h"
#include "SPI.h"

#define SD_CS_PIN 10

class StorageManager {
private:
    File audioFile;
    
public:
    bool init() {
        if (!SD.begin(SD_CS_PIN)) {
            Serial.println("SD Card initialization failed");
            return false;
        }
        
        Serial.printf("SD Card Size: %lluMB\n", SD.cardSize() / (1024 * 1024));
        return true;
    }
    
    bool saveAudioData(const char* filename, uint8_t* data, size_t length) {
        audioFile = SD.open(filename, FILE_WRITE);
        if (!audioFile) {
            Serial.println("Failed to open file for writing");
            return false;
        }
        
        size_t written = audioFile.write(data, length);
        audioFile.close();
        
        return written == length;
    }
    
    bool loadAudioData(const char* filename, uint8_t* buffer, size_t maxLength) {
        audioFile = SD.open(filename);
        if (!audioFile) {
            Serial.println("Failed to open file for reading");
            return false;
        }
        
        size_t fileSize = audioFile.size();
        size_t readSize = min(fileSize, maxLength);
        
        audioFile.read(buffer, readSize);
        audioFile.close();
        
        return true;
    }
};

VI. Multitasking Programming

6.1 FreeRTOS Task Management

Task Priority Design

// XiaoZhi AI Task Priority Definition
#define PRIORITY_AUDIO_RECORD    5  // Highest priority
#define PRIORITY_AUDIO_PLAY      5  // Highest priority  
#define PRIORITY_VOICE_PROCESS   4  // High priority
#define PRIORITY_NETWORK_COMM    3  // Medium priority
#define PRIORITY_DISPLAY_UPDATE  2  // Low priority
#define PRIORITY_LED_CONTROL     1  // Lowest priority

TaskHandle_t audioRecordHandle;
TaskHandle_t audioPlayHandle;
TaskHandle_t voiceProcessHandle;
TaskHandle_t networkCommHandle;

void createXiaoZhiTasks() {
    // Audio recording task (Core 0)
    xTaskCreatePinnedToCore(
        audioRecordTask,
        "AudioRecord",
        4096,
        NULL,
        PRIORITY_AUDIO_RECORD,
        &audioRecordHandle,
        0
    );
    
    // Audio playback task (Core 1)
    xTaskCreatePinnedToCore(
        audioPlayTask,
        "AudioPlay", 
        4096,
        NULL,
        PRIORITY_AUDIO_PLAY,
        &audioPlayHandle,
        1
    );
    
    // Voice processing task (Core 1)
    xTaskCreatePinnedToCore(
        voiceProcessTask,
        "VoiceProcess",
        8192,
        NULL,
        PRIORITY_VOICE_PROCESS,
        &voiceProcessHandle,
        1
    );
    
    // Network communication task (Core 0)
    xTaskCreatePinnedToCore(
        networkCommTask,
        "NetworkComm",
        4096,
        NULL,
        PRIORITY_NETWORK_COMM,
        &networkCommHandle,
        0
    );
}

6.2 Inter-task Communication

Queue and Semaphore Usage

// Global communication objects
QueueHandle_t audioQueue;
QueueHandle_t commandQueue;
SemaphoreHandle_t i2sMutex;
EventGroupHandle_t systemEvents;

// Event definitions
#define EVENT_WIFI_CONNECTED    (1 << 0)
#define EVENT_VOICE_DETECTED    (1 << 1)
#define EVENT_AI_RESPONSE       (1 << 2)

void initCommunication() {
    // Create queues
    audioQueue = xQueueCreate(10, sizeof(AudioData));
    commandQueue = xQueueCreate(5, sizeof(Command));
    
    // Create semaphore
    i2sMutex = xSemaphoreCreateMutex();
    
    // Create event group
    systemEvents = xEventGroupCreate();
}

// Voice processing task
void voiceProcessTask(void *parameter) {
    AudioData audioData;
    
    while (true) {
        if (xQueueReceive(audioQueue, &audioData, portMAX_DELAY)) {
            // Process audio data
            if (detectWakeWord(audioData.buffer, audioData.length)) {
                xEventGroupSetBits(systemEvents, EVENT_VOICE_DETECTED);
                
                // Send command to network task
                Command cmd = {CMD_SEND_AUDIO, audioData};
                xQueueSend(commandQueue, &cmd, 0);
            }
        }
    }
}

// Network communication task
void networkCommTask(void *parameter) {
    Command cmd;
    
    while (true) {
        // Wait for network events
        EventBits_t events = xEventGroupWaitBits(
            systemEvents,
            EVENT_WIFI_CONNECTED | EVENT_VOICE_DETECTED,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY
        );
        
        if (events & EVENT_VOICE_DETECTED) {
            if (xQueueReceive(commandQueue, &cmd, 0)) {
                // Send audio to cloud for recognition
                String response = sendAudioToCloud(cmd.audioData);
                
                if (response.length() > 0) {
                    xEventGroupSetBits(systemEvents, EVENT_AI_RESPONSE);
                }
            }
        }
    }
}

VII. Debugging and Optimization

7.1 Serial Debugging

Advanced Debug Output

// Multi-level debug system
enum DebugLevel {
    DEBUG_ERROR = 0,
    DEBUG_WARN = 1,
    DEBUG_INFO = 2,
    DEBUG_VERBOSE = 3
};

#define DEBUG_LEVEL DEBUG_INFO

#define DEBUG_PRINT(level, format, ...) \
    if (level <= DEBUG_LEVEL) { \
        Serial.printf("[%s] %s:%d - " format "\n", \
            getDebugLevelString(level), __FILE__, __LINE__, ##__VA_ARGS__); \
    }

const char* getDebugLevelString(DebugLevel level) {
    switch (level) {
        case DEBUG_ERROR: return "ERROR";
        case DEBUG_WARN: return "WARN";
        case DEBUG_INFO: return "INFO";
        case DEBUG_VERBOSE: return "VERBOSE";
        default: return "UNKNOWN";
    }
}

// Usage examples
void someFunction() {
    DEBUG_PRINT(DEBUG_INFO, "Initializing audio system...");
    
    if (!initMicrophone()) {
        DEBUG_PRINT(DEBUG_ERROR, "Failed to initialize microphone");
        return;
    }
    
    DEBUG_PRINT(DEBUG_VERBOSE, "Microphone initialized successfully");
}

7.2 Performance Monitoring

System Resource Monitoring

void printSystemStatus() {
    // Memory usage
    size_t freeHeap = esp_get_free_heap_size();
    size_t minFreeHeap = esp_get_minimum_free_heap_size();
    
    Serial.printf("=== System Status ===\n");
    Serial.printf("Free Heap: %u bytes\n", freeHeap);
    Serial.printf("Min Free Heap: %u bytes\n", minFreeHeap);
    Serial.printf("Heap Usage: %.1f%%\n", 
        (1.0 - (float)freeHeap / (float)(freeHeap + minFreeHeap)) * 100);
    
    // Task information
    Serial.printf("\n=== Task Status ===\n");
    char* taskListBuffer = (char*)malloc(2048);
    vTaskList(taskListBuffer);
    Serial.print(taskListBuffer);
    free(taskListBuffer);
    
    // Wi-Fi status
    Serial.printf("\n=== Network Status ===\n");
    Serial.printf("WiFi Status: %s\n", 
        WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected");
    if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
        Serial.printf("Signal Strength: %d dBm\n", WiFi.RSSI());
    }
}

// Periodic monitoring task
void monitorTask(void *parameter) {
    while (true) {
        printSystemStatus();
        vTaskDelay(pdMS_TO_TICKS(30000)); // Print every 30 seconds
    }
}

Next Steps: