Building StressCheck: An AI-Powered Stress Level Analyzer with ESP32 & Hugging Face

Introduction: IoT Meets Generative AI

In the world of Embedded Systems, we typically work with sensors, actuators, and straightforward control logic. But what happens when we connect a microcontroller directly to a Large Language Model (LLM)?

Before diving into the code, let’s explore the user experience. This system runs as a standalone local server and provides a seamless, interactive workflow from start to finish.

In this tutorial, we will walk through StressCheck, a project I developed that bridges the gap between IoT and AI. StressCheck is an ESP32-based system designed to conduct a short psychological-style interview with the user to estimate their stress level.

Unlike static, predefined quizzes, this device uses the Hugging Face API to generate dynamic, real-time questions. It analyzes the user’s text responses and produces both a written assessment and a numerical stress score. The result is also reflected physically: the onboard LED blinks faster as the detected stress level increases.


Part 1 — Required Hardware

We keep the project modular, dividing the system into clear components as needed.
For the hardware, you only need one ESP32 development board (ESP32 DevKit V1). The button and LED pins in the code are mapped according to this board’s default GPIO layout.

To operate the system, you’ll need a stable internet connection, since the ESP32 communicates with the Hugging Face API over Wi-Fi.

The development environment used in this project is PlatformIO.

In the following code sections, placeholder values are used for client_id, client_secret, and redirect_uri. You must create your own Connected App on Hugging Face, then replace these placeholder values with your actual credentials so that the OAuth flow works correctly.


Part 2 — Wi-Fi Configuration

Before the ESP32 can interact with the API, it needs a reliable internet connection. To make this user-friendly, we designed a configuration flow that allows the user to set up Wi-Fi credentials without recompiling or re-uploading code.

When the device powers on for the first time, it starts in Access Point (AP) mode. The user connects to the “StressCheck” Wi-Fi network, which automatically opens a Captive Portal. In this interface, the user enters their home router’s Wi-Fi credentials. The ESP32 stores this information in LittleFS and then attempts to connect to the internet.

On every subsequent boot, the device reads the saved configuration and automatically connects to the previously configured router.

To reset the device to factory mode, the user can press and hold the BOOT button (GPIO 0) during the first second after startup. This clears the stored configuration and returns the device to Access Point mode.

This entire initialization and configuration process is encapsulated in a dedicated class to keep the code clean and modular.

				
					// wifi_config.cpp

/**
 * @file wifi_config.cpp
 * @brief Implementation of the WiFiConfig class for managing Wi-Fi connection and configuration.
 */
#include "wifi_config.h"

/// @cond Doxygen_Skipped
// Internal Definitions
#define CONFIG_FILE "/config.json"
#define AP_SSID "StressCheck"
#define AP_PASS "11111111"
#define AP_IP IPAddress(192,168,4,1)
#define AP_NETMASK IPAddress(255,255,255,0)
#define LED_PIN 2
/// @endcond

/**
 * @brief Constructor for the WiFiConfig class.
 * Initializes member variables and loads HTML templates.
 * @param resetPin The GPIO pin used to force the configuration portal when held low at boot.
 */
WiFiConfig::WiFiConfig(uint8_t resetPin)
: server(80), resetButtonPin(resetPin),
  restartRequested(false), restartAt(0),
  ledMode(0),
  ledTaskHandle(NULL), restartTaskHandle(NULL)
{
    memset(&cfg, 0, sizeof(cfg));

/** @brief HTML content for the Wi-Fi configuration portal index page (SSID and password input). */
htmlIndex = R"rawliteral(
<title>Wi-Fi Settings</title>
body{font-family:Tahoma,Arial;margin:20px;padding:20px;max-width:420px;margin-left:auto;margin-right:auto;background:#f8f9fa;}
.container{padding:12px;background:white;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.06);}
input{width:100%;padding:8px;margin-top:6px;box-sizing:border-box;border:1px solid #ddd;border-radius:4px;}
label{display:block;margin-top:10px;font-size:14px;color:#333;}
.row{display:flex;gap:8px;margin-top:8px;}
.row &gt; div{flex:1;}
button{width:100%;margin-top:14px;padding:10px;background:#1e88e5;color:#fff;border:0;border-radius:4px;cursor:pointer;}
h2{text-align:center;margin:0 0 8px 0;}
.note{font-size:13px;color:#555;margin-top:12px;}
<div class="container">
  <h2>Wi-Fi Settings</h2>
    <label>SSID</label>
    <label>Password</label>
    <div class="row">
      <div>
        <label>IP Address</label>
      </div>
      <div>
        <label>Gateway</label>
      </div>
    </div>
    <button type="submit">Save Settings</button>
  <p class="note">After saving, please restart the device to connect to the new network.</p>
</div>
)rawliteral";

/** @brief HTML content for the success page after configuration is saved. */
htmlSuccess = R"rawliteral(
<title>Saved</title>
body{font-family:Tahoma,Arial;margin:20px;padding:20px;max-width:420px;margin-left:auto;margin-right:auto;background:#f8f9fa;}
.container{padding:12px;background:white;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.06);text-align:center;}
h2{margin:0 0 8px 0;color:#2e7d32;}
p{color:#333;margin-top:8px;}
a.button-link{display:inline-block;padding:10px 18px;margin-top:14px;background:#1e88e5;color:#fff;text-decoration:none;border-radius:4px;}
<div class="container">
  <h2>Settings Saved</h2>
  <p>The microcontroller is restarting and connecting to the configured network.</p>
  <a class="button-link" href="http://%%IP_ADDRESS%%/">Access Device (New IP)</a>
</div>)rawliteral";

/** @brief HTML content for the error page in the configuration portal. */
htmlError = R"rawliteral(<title>Error</title>
body{font-family:Tahoma,Arial;margin:20px;padding:20px;max-width:420px;margin-left:auto;margin-right:auto;background:#f8f9fa;}
.container{padding:12px;background:white;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.06);text-align:center;}
h2{margin:0 0 8px 0;color:#c62828;}
.error-message{margin-top:15px;padding:10px;border:1px solid #D32F2F;background:#FFEBEE;text-align:right;border-radius:4px;}
a.retry{display:inline-block;padding:10px 18px;margin-top:14px;background:#757575;color:#fff;text-decoration:none;border-radius:4px;}
<div class="container">
  <h2>Error Saving Settings</h2>
  <div class="error-message">
    <p>%%ERROR_MESSAGE%%</p>
  </div>
  <a class="retry" href="/">Try Again</a>
</div>
)rawliteral";
}

/**
 * @brief FreeRTOS task to control the LED mode.
 * Blinks the LED based on the current `ledMode` state.
 * @param param Pointer to the WiFiConfig class instance.
 */
void WiFiConfig::ledTask(void* param){
    WiFiConfig* self = (WiFiConfig*)param;
    const TickType_t fast = pdMS_TO_TICKS(100);
    const TickType_t slow = pdMS_TO_TICKS(500);
    for(;;){
        uint8_t mode = self-&gt;ledMode;
        if(mode == 0){ // Fast blink (Initial/Connection attempt)
            digitalWrite(LED_PIN, HIGH);
            vTaskDelay(fast);
            digitalWrite(LED_PIN, LOW);
            vTaskDelay(fast);
        } else if(mode == 1){ // Solid ON (AP Mode)
            digitalWrite(LED_PIN, HIGH);
            vTaskDelay(pdMS_TO_TICKS(200));
        } else if(mode == 2){ // Slow blink (STA connection attempt)
            digitalWrite(LED_PIN, HIGH);
            vTaskDelay(slow);
            digitalWrite(LED_PIN, LOW);
            vTaskDelay(slow);
        } else { // OFF (Connection successful)
            digitalWrite(LED_PIN, LOW);
            vTaskDelay(pdMS_TO_TICKS(5000));
        }
    }
}

/**
 * @brief FreeRTOS task to check for and execute a device restart.
 * Waits a short period after configuration save before restarting the ESP.
 * @param param Pointer to the WiFiConfig class instance.
 */
void WiFiConfig::restartCheckTask(void* param){
    WiFiConfig* self = (WiFiConfig*)param;
    const TickType_t period = pdMS_TO_TICKS(200);
    for(;;){
        if(self-&gt;restartRequested){
            unsigned long now = millis();
            // Check if at least 2000ms have passed since restart was requested.
            if(now &gt;= self-&gt;restartAt &amp;&amp; (now - self-&gt;restartAt) &gt;= 2000){
                Serial.println("RestartCheckTask: conditions met -&gt; restarting now.");
                vTaskDelay(pdMS_TO_TICKS(50));
                ESP.restart();
            }
        }
        vTaskDelay(period);
    }
}

/**
 * @brief Initiates the Wi-Fi configuration process.
 * Checks the reset pin, loads config, and decides whether to start STA or AP mode.
 */
void WiFiConfig::begin(){
    pinMode(resetButtonPin, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);

    // Start LED Task with default mode (fast blink).
    ledMode = 0;
    xTaskCreatePinnedToCore(WiFiConfig::ledTask, "ledTask", 2048, this, 1, &amp;ledTaskHandle, 1);

    // Initialize LittleFS.
    if(!LittleFS.begin()){
        Serial.println("LittleFS mount failed, formatting...");
        LittleFS.format();
        if(!LittleFS.begin()){
            Serial.println("LittleFS re-mount failed!");
        } else {
            Serial.println("LittleFS mounted after format.");
        }
    } else {
        Serial.println("LittleFS mounted.");
    }

    // Load saved configuration.
    loadConfig();
    vTaskDelay(pdMS_TO_TICKS(900));

    // Check if reset pin is held to force config portal.
    bool forceConfig = (digitalRead(resetButtonPin) == LOW);
    if(forceConfig){
        Serial.println("Reset pin held: forcing config portal (reset config file).");
        resetConfigFile();
    }

    // If no config is available or force config is requested, enter AP Mode.
    if(!isConfigAvailable() || forceConfig){
        Serial.println("Entering Config Portal mode (AP).");
        startConfigPortal();

        ledMode = 1; // Change LED mode to Solid ON (AP Mode).

        // Start the restart check task if not running.
        if(restartTaskHandle == NULL){
            xTaskCreatePinnedToCore(WiFiConfig::restartCheckTask, "restartChk", 2048, this, 1, &amp;restartTaskHandle, 1);
        }

        // Block execution in AP Mode until restart happens.
        while(true){
            vTaskDelay(pdMS_TO_TICKS(200));
        }
    }

    // Config found, attempt to connect as Station (STA).
    Serial.println("Config found: attempting to connect as station.");
    ledMode = 2; // Change LED mode to Slow blink (STA attempt).
    if(!connectSTA()){
        // STA connection failed, fall back to AP Mode.
        Serial.println("Could not connect as station; starting Config Portal as fallback.");
        startConfigPortal();
        ledMode = 1; // Change LED mode to Solid ON (AP Mode).
        if(restartTaskHandle == NULL){
            xTaskCreatePinnedToCore(WiFiConfig::restartCheckTask, "restartChk", 2048, this, 1, &amp;restartTaskHandle, 1);
        }
        while(true){
            vTaskDelay(pdMS_TO_TICKS(200));
        }
    }

    // STA connection successful, delete LED Task and turn off LED.
    if(ledTaskHandle != NULL){
        vTaskDelete(ledTaskHandle);
        ledTaskHandle = NULL;
    }
    digitalWrite(LED_PIN, LOW);
    ledMode = 3; // Set LED mode to OFF.
    Serial.println("Wi-Fi ready; continuing with main application.");
}

/**
 * @brief Checks if a valid Wi-Fi configuration (SSID and Password) is stored.
 * @retval true If SSID and Password fields have non-zero length.
 * @retval false Otherwise.
 */
bool WiFiConfig::isConfigAvailable(){
    return (strlen(cfg.ssid) &gt; 0 &amp;&amp; strlen(cfg.password) &gt; 0);
}

/**
 * @brief Resets the Wi-Fi configuration file to empty defaults in LittleFS.
 */
void WiFiConfig::resetConfigFile(){
    Serial.println("Resetting config file to empty defaults.");
    memset(&amp;cfg, 0, sizeof(cfg));
    StaticJsonDocument doc;
    doc["ssid"] = "";
    doc["password"] = "";
    doc["ip"] = "";
    doc["gateway"] = "";
    File f = LittleFS.open(CONFIG_FILE, "w");
    if(f){
        serializeJson(doc, f);
        f.close();
        Serial.println("Empty config written to LittleFS.");
    } else {
        Serial.println("Failed to write empty config to LittleFS.");
    }
}

/**
 * @brief Loads the Wi-Fi configuration from the JSON file in LittleFS.
 * Resets the config if the file is not found or fails to parse.
 */
void WiFiConfig::loadConfig(){
    if(!LittleFS.exists(CONFIG_FILE)){
        Serial.println("Config file not found, creating default.");
        resetConfigFile();
        return;
    }

    File f = LittleFS.open(CONFIG_FILE, "r");
    if(!f){
        Serial.println("Failed to open config file for read, resetting.");
        resetConfigFile();
        return;
    }

    StaticJsonDocument doc;
    DeserializationError err = deserializeJson(doc, f);
    f.close();
    if(err){
        Serial.print("Config parse error: ");
        Serial.println(err.c_str());
        resetConfigFile();
        return;
    }

    // Load values with fallback to empty strings.
    strlcpy(cfg.ssid, doc["ssid"] | "", sizeof(cfg.ssid));
    strlcpy(cfg.password, doc["password"] | "", sizeof(cfg.password));
    strlcpy(cfg.ip, doc["ip"] | "", sizeof(cfg.ip));
    strlcpy(cfg.gateway, doc["gateway"] | "", sizeof(cfg.gateway));

    Serial.printf("Loaded Wi-Fi config: SSID=%s, IP=%s, Gateway=%s\n", cfg.ssid, cfg.ip, cfg.gateway);
}

/**
 * @brief Saves the current Wi-Fi configuration to a JSON file in LittleFS.
 */
void WiFiConfig::saveConfig(){
    StaticJsonDocument doc;
    doc["ssid"] = cfg.ssid;
    doc["password"] = cfg.password;
    doc["ip"] = cfg.ip;
    doc["gateway"] = cfg.gateway;

    File f = LittleFS.open(CONFIG_FILE, "w");
    if(f){
        serializeJson(doc, f);
        f.close();
        Serial.println("Wi-Fi config saved to LittleFS.");
    } else {
        Serial.println("Failed to open config file for write!");
    }
}

/**
 * @brief Attempts to connect to the configured Wi-Fi network as a Station (STA).
 * Supports both DHCP and static IP configuration.
 * @retval true If connection is successful.
 * @retval false If connection times out after 15 seconds or no SSID is configured.
 */
bool WiFiConfig::connectSTA(){
    if(strlen(cfg.ssid) == 0){
        Serial.println("No SSID configured.");
        return false;
    }

    Serial.printf("Attempting to connect to SSID: %s\n", cfg.ssid);

    WiFi.mode(WIFI_STA);

    IPAddress localIP, gw;
    IPAddress dns1(8,8,8,8);
    IPAddress dns2(1,1,1,1);

    // Check for valid static IP configuration.
    bool useStaticIP = (strlen(cfg.ip) &gt; 0 &amp;&amp; strlen(cfg.gateway) &gt; 0 &amp;&amp;
                         localIP.fromString(cfg.ip) &amp;&amp; gw.fromString(cfg.gateway));

    if(useStaticIP){
        IPAddress subnet(255,255,255,0);
        bool ok = WiFi.config(localIP, gw, subnet, dns1, dns2);
        if(ok){
            Serial.println("Static IP and DNS configured.");
            Serial.printf("Static IP: %s, Gateway: %s, DNS1: %s, DNS2: %s\n",
                localIP.toString().c_str(), gw.toString().c_str(), dns1.toString().c_str(), dns2.toString().c_str());
        } else {
            Serial.println("WiFi.config failed; will try DHCP.");
        }
    } else {
        Serial.println("No static IP provided; using DHCP (DNS via DHCP).");
    }

    WiFi.begin(cfg.ssid, cfg.password);

    unsigned long start = millis();
    // Wait loop for Wi-Fi connection.
    while(WiFi.status() != WL_CONNECTED){
        vTaskDelay(pdMS_TO_TICKS(500));
        Serial.print(".");
        if(millis() - start &gt; 15000){
            Serial.println("\nConnection timed out (15s).");
            return false;
        }
    }

    Serial.println("\nConnected to Wi-Fi.");
    Serial.printf("SSID: %s\n", cfg.ssid);
    Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("Gateway: %s\n", WiFi.gatewayIP().toString().c_str());

    if(useStaticIP){
        Serial.println("DNS1: 8.8.8.8");
        Serial.println("DNS2: 1.1.1.1");
    } else {
        Serial.println("DNS set by DHCP (if DHCP provided DNS).");
    }

    return true;
}

/**
 * @brief Starts the Wi-Fi configuration portal in Access Point (AP) mode.
 * Configures the AP with predefined SSID/Password and sets up the web server routes.
 */
void WiFiConfig::startConfigPortal(){
    Serial.println("Starting Config Portal (AP mode)...");
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(AP_IP, AP_IP, AP_NETMASK);
    WiFi.softAP(AP_SSID, AP_PASS);
    Serial.printf("AP started. SSID: %s, PASS: %s, IP: %s\n", AP_SSID, AP_PASS, AP_IP.toString().c_str());

    setupServerRoutes();
    server.begin();
    Serial.println("HTTP server started for portal on port 80.");
}

/**
 * @brief Sets up the HTTP routes for the configuration portal.
 * Handles the index page ("/") and the configuration submission page ("/save").
 */
void WiFiConfig::setupServerRoutes(){
    // Root route: serve the configuration form.
    server.on("/", HTTP_GET, [this](AsyncWebServerRequest* req){
        req-&gt;send(200, "text/html; charset=utf-8", htmlIndex);
    });

    // Save route: process submitted configuration data.
    server.on("/save", HTTP_POST, [this](AsyncWebServerRequest* req){
        String ssid = req-&gt;arg("ssid");
        String pass = req-&gt;arg("password");
        String ip_str = req-&gt;arg("ip");
        String gateway_str = req-&gt;arg("gateway");

        // Validate SSID.
        if(ssid.length() == 0){
            String tmp = htmlError;
            tmp.replace("%%ERROR_MESSAGE%%", "نام شبکه (SSID) نمی‌تواند خالی باشد.");
            req-&gt;send(400, "text/html; charset=utf-8", tmp);
            return;
        }
        
        // Validate IP Address format.
        IPAddress test;
        if(ip_str.length() &gt; 0 &amp;&amp; !test.fromString(ip_str)){
            String tmp = htmlError;
            tmp.replace("%%ERROR_MESSAGE%%", "آدرس IP وارد شده نامعتبر است.");
            req-&gt;send(400, "text/html; charset=utf-8", tmp);
            return;
        }
        // Validate Gateway format.
        if(gateway_str.length() &gt; 0 &amp;&amp; !test.fromString(gateway_str)){
            String tmp = htmlError;
            tmp.replace("%%ERROR_MESSAGE%%", "آدرس Gateway وارد شده نامعتبر است.");
            req-&gt;send(400, "text/html; charset=utf-8", tmp);
            return;
        }

        // Copy valid data to the configuration structure.
        strlcpy(cfg.ssid, ssid.c_str(), sizeof(cfg.ssid));
        strlcpy(cfg.password, pass.c_str(), sizeof(cfg.password));
        strlcpy(cfg.ip, ip_str.c_str(), sizeof(cfg.ip));
        strlcpy(cfg.gateway, gateway_str.c_str(), sizeof(cfg.gateway));

        saveConfig();

        // Determine the target IP for the success link.
        String targetIp = ip_str;
        if(targetIp.length() == 0){
            if(gateway_str.length() &gt; 0) targetIp = gateway_str;
            else targetIp = String("192.168.1.1"); // Default assumption if no static IP/Gateway.
        }
        
        // Send success page and set restart flags.
        String out = htmlSuccess;
        out.replace("%%IP_ADDRESS%%", targetIp);
        req-&gt;send(200, "text/html; charset=utf-8", out);

        restartAt = millis();
        restartRequested = true;
        Serial.println("Config saved. restartRequested set = true; restartCheckTask will perform restart after 2s.");
    });
    
    // Not Found handler: redirect to the configuration form.
    server.onNotFound([](AsyncWebServerRequest* req){
        req-&gt;redirect("/");
    });
}
				
			

Part 3 — Tasks

All the initialization and Wi-Fi configuration logic described earlier is handled inside a dedicated module located in the wifi_config directory of the project. Once the device connects successfully, we move on to building the core functionality of the system.

The main application is organized around four primary FreeRTOS tasks, each responsible for a key part of the workflow.

The first task manages the behavior of the onboard LED. It continuously adjusts the blink rate based on the stress score returned by the AI.

The remaining three tasks handle the Hugging Face API interactions:

  1. OAuth Login Task — Authenticates the user and retrieves a JWT token.

  2. Question Generation Task — Sends a request to the LLM and receives dynamically generated interview questions.

  3. Analysis Task — Processes the user’s responses and retrieves the final stress evaluation and score.

Each of these tasks is implemented as a modular component, keeping the project clean, organized, and easy to maintain.

				
					// blink.h
#pragma once
#include "config.h" // Access to LED_PIN, blink_status, blink_time
#include "Arduino.h"

/**
 * @brief FreeRTOS task that implements the LED blinking logic.
 * Controls the LED_PIN based on the global variables `blink_status` and `blink_time`.
 * @param pvParameters Parameters passed to the task (not used).
 */
void blinkTask(void *pvParameters) {
    pinMode(LED_PIN, OUTPUT);
    while (1) {
        if (blink_status) {
            // Blink the LED with the rate defined by blink_time.
            digitalWrite(LED_PIN, HIGH);
            vTaskDelay(blink_time / portTICK_PERIOD_MS);
            digitalWrite(LED_PIN, LOW);
            vTaskDelay(blink_time / portTICK_PERIOD_MS);
        } else {
            // When inactive, yield the CPU periodically.
            vTaskDelay(100 / portTICK_PERIOD_MS);
        }
    }
}

// huggingface.cpp
/**
 * @file huggingface.cpp
 * @brief Implementation of API functions and FreeRTOS tasks for Hugging Face interactions.
 * This file handles authentication, fetching questions, and retrieving the final conclusion/analysis.
 */
#include "config.h"
#include "user_data.h"
#include "Arduino.h"
#include "ArduinoJson.h"
#include "AsyncTCP.h"
#include "HTTPClient.h"
#include "string.h"
#include "tasks/huggingface.h"
#include "utils/blink.h"
#include "WiFi.h"

/**
 * @brief Attempts to fetch the user's access token from the Hugging Face API using OAuth.
 * Sends an authorization code grant request to exchange the user's code for an access token.
 * @param code The user code/identifier received from the client.
 * @param clientId The ID of the WebSocket client to send success/failure messages back to.
 * @retval true If the token is successfully retrieved and stored.
 * @retval false Otherwise, and a "login-fail" message is sent.
 */
bool get_user_access_token(String code, uint32_t clientId) {
    WsQueueMessage msg{};
    msg.clientId = clientId;

    // Check if user data (code) already exists.
    bool exists = strlen(user.code) &gt; 0;

    const char *url = "https://huggingface.co/oauth/token";
    // Base64 encoded Client ID and Secret (Placeholder/Example in the original code)
    const char *token = "Basic "
                        "Y2E0MjAxNjgtYzY2MC00NWE1LTgxMzEtNWMzM2YxNmQ1MDU1OmI4NjIx"
                        "Y2Q0LWM5NDQtNDRjZS1hMzdhLTA2MWI3YmMwODI2ZQ==";

    HTTPClient http;
    http.begin(url);
    http.setTimeout(30000); // 30 second timeout
    http.addHeader("Authorization", token);
    http.addHeader("Content-Type", "application/json");

    // Build the request body for token exchange.
    DynamicJsonDocument requestDoc(1024);
    requestDoc["grant_type"] = "authorization_code";
    code.trim();
    requestDoc["code"] = code;
    // Redirect URI must match the registered OAuth application settings (using local IP).
    requestDoc["redirect_uri"] = "http://" + WiFi.localIP().toString() + "/";
    requestDoc["client_id"] = "ca420168-c660-45a5-8131-5c33f16d5055";

    String requestBody;
    serializeJson(requestDoc, requestBody);

    int httpCode = -1;

    // Retry POST request up to 3 times.
    for (int retry = 0; retry  0)
            break;
        Serial.println("POST failed. Retrying...");
        vTaskDelay(pdMS_TO_TICKS(1200));
    }

    if (httpCode code, params-&gt;clientId);
    delete params;
    xSemaphoreGive(loginSem);
    vTaskDelete(NULL);
}

/**
 * @brief Fetches five personalized questions from the Hugging Face Chat Completion API.
 * Uses a fixed system prompt to instruct the model to generate five Persian questions separated by '@'.
 * @param code The user code (used for verification against stored code).
 * @param clientId The ID of the WebSocket client.
 * @retval true If questions are successfully retrieved and stored.
 * @retval false Otherwise, and a "question-fail" message is sent.
 */
bool get_questions(String code, uint32_t clientId) {
    WsQueueMessage msg{};
    msg.clientId = clientId;

    // Check if the code matches the stored user code and an access token exists.
    if (!code.equals(user.code) || strlen(user.accessToken) == 0) {
        Serial.println(user.code);
        Serial.println(user.accessToken);
        Serial.println("Fail at 1");

        strlcpy(msg.text, "question-fail", sizeof(msg.text));
        xQueueSend(wsQueue, &amp;msg, 0);
        return false;
    }

    const char *url = "https://router.huggingface.co/v1/chat/completions";

    String auth = "Bearer " + String(user.accessToken);

    const char *system_prompt =
        "You must generate exactly 5 questions in Persian.\n"
        "Topic: These questions MUST help assess how stressed the person is.\n"
        "They should explore stress level, emotional pressure, coping style, daily tension,\n"
        "and how the user reacts to stressful situations.\n"
        "\n"
        "Output format MUST be:\n"
        "@@@@\n"
        "\n"
        "Strict rules:\n"
        "- ONLY Persian text. No English. No translations.\n"
        "- No numbering, no bullets, no bold, no markdown.\n"
        "- No extra text before or after the questions.\n"
        "- Questions must be meaningful, friendly, and related ONLY to stress assessment.\n"
        "- Use @ ONLY as the separator, nowhere else.\n"
        "\n"
        "Example of correct format:\n"
        "سوال اول؟@سوال دوم؟@سوال سوم؟@سوال چهارم؟@سوال پنجم؟\n";

    DynamicJsonDocument reqDoc(8192);
    reqDoc["model"] = "deepseek-ai/DeepSeek-V3-0324";
    JsonArray messages = reqDoc.createNestedArray("messages");

    JsonObject sys_msg = messages.createNestedObject();
    sys_msg["role"] = "system";
    sys_msg["content"] = system_prompt;

    JsonObject userMsg = messages.createNestedObject();
    userMsg["role"] = "user";
    userMsg["content"] = "Generate 5 Persian questions.";

    String body;
    serializeJson(reqDoc, body);

    HTTPClient http;
    int status = -1;
    String payload;

    // Retry POST request up to 4 times.
    for (int retry = 0; retry  0) {
            payload = http.getString();
            http.end();
            break;
        }

        Serial.printf("Question request failed (%d). Retry...\n", retry + 1);
        http.end();
        vTaskDelay(pdMS_TO_TICKS(1500));
    }

    if (status &lt;= 0) {
        Serial.println(status);
        Serial.println(&quot;Fail at 2&quot;);
        strlcpy(msg.text, &quot;question-fail&quot;, sizeof(msg.text));
        xQueueSend(wsQueue, &amp;msg, 0);
        return false;
    }

    // Parse the API response.
    DynamicJsonDocument respDoc(8192);
    if (deserializeJson(respDoc, payload)) {
        Serial.println(&quot;Question JSON parse failed.&quot;);
        strlcpy(msg.text, &quot;question-fail&quot;, sizeof(msg.text));
        xQueueSend(wsQueue, &amp;msg, 0);
        return false;
    }

    const char *content = respDoc[&quot;choices&quot;][0][&quot;message&quot;][&quot;content&quot;];
    if (!content) {
        Serial.println(&quot;No content returned.&quot;);
        strlcpy(msg.text, &quot;question-fail&quot;, sizeof(msg.text));
        xQueueSend(wsQueue, &amp;msg, 0);
        return false;
    }

    // Split the received string by the &#039;@&#039; separator.
    String raw = String(content);
    String questions[5];
    int idx = 0, last = 0;

    while (idx &lt; 5) {
        int pos = raw.indexOf(&#039;@&#039;, last);
        if (pos code, params-&gt;clientId);
    delete params;
    vTaskDelete(NULL);
}

/**
 * @brief Sends the user's answers to the API to retrieve a conclusion and an emotional score.
 * The system prompt strictly enforces the output format: `@`.
 * @param code The user code (used for verification).
 * @param answers The user's combined answers, separated by newlines.
 * @param clientId The ID of the WebSocket client.
 * @retval true If the conclusion and score are successfully retrieved.
 * @retval false Otherwise, and a "conclusion-fail" message is sent.
 */
bool get_conclusion(String code, String answers, uint32_t clientId) {
    WsQueueMessage msg{};
    msg.clientId = clientId;

    answers.replace("\r", "");
    answers.trim();

    String result_test;

    // Split the combined answers by newline to associate them with questions.
    String answerLines[5];
    int start = 0;
    int index = 0;

    while (index &lt; 5) {
        int end = answers.indexOf(&#039;\n&#039;, start);
        if (end == -1)
            end = answers.length();
        answerLines[index] = answers.substring(start, end);
        start = end + 1;
        index++;
    }

    // Format the Q&amp;A pairs for the API prompt.
    String questions[5] = {user.question1, user.question2, user.question3,
                           user.question4, user.question5};
    for (int i = 0; i &lt; 5; i++) {
        result_test += questions[i] + &quot;\n&quot;;
        result_test += answerLines[i] + &quot;\n\n&quot;;
    }

    // Check verification.
    if (!code.equals(user.code) || strlen(user.accessToken) == 0) {
        strlcpy(msg.text, &quot;conclusion-fail&quot;, sizeof(msg.text));
        xQueueSend(wsQueue, &amp;msg, 0);
        return false;
    }

    const char *url = &quot;https://router.huggingface.co/v1/chat/completions&quot;;
    String auth = &quot;Bearer &quot; + String(user.accessToken);

    // System prompt defining the analysis task, score range (0=rational, 100=emotional), and output format.
    const char *system_prompt =
        &quot;You are a stress-analysis assistant. The user provides 5 Persian Q&amp;A pairs.\n&quot;
        &quot;Input format: each question on one line, the answer on the next line, then one empty line.\n&quot;
        &quot;Your task: produce exactly ONE integer score (0..100) representing how STRESSED the person is.\n&quot;
        &quot;Output format MUST be:\n&quot;
        &quot;@\n"
        "\n"
        "Important strict rules:\n"
        "- The score must be an integer between 0 and 100 (inclusive).\n"
        "- The analysis MUST be entirely in Persian, friendly, addressing the user, and no longer than 10 lines.\n"
        "- The first part before '@' must be only the integer score. No extra characters, no percent sign here.\n"
        "- After the '@' provide the Persian textual analysis (you may also repeat the score in text if you want, see example).\n"
        "- You MUST also state the complement (100-score) in text, using the same integer, e.g. \"35%% استرس، 65%% غیر استرسی\".\n"
        "- Use ONLY that integer and its complement for numeric mentions; do NOT include other numbers.\n"
        "- No English, no extra numbers, no JSON, no markdown, no additional metadata.\n"
        "\n"
        "Example of correct output:\n"
        "35@کاربر عزیز، سطح استرس شما 35%% است و 65%% غیر استرسی. توضیح کوتاه و دوستانه درباره یافته‌ها...\n";

    DynamicJsonDocument reqDoc(8192);
    reqDoc["model"] = "deepseek-ai/DeepSeek-V3-0324";
    JsonArray messages = reqDoc.createNestedArray("messages");

    JsonObject sys_msg = messages.createNestedObject();
    sys_msg["role"] = "system";
    sys_msg["content"] = system_prompt;

    JsonObject user_msg = messages.createNestedObject();
    user_msg["role"] = "user";

    result_test.replace("\r", "");
    result_test.trim();

    user_msg["content"] = result_test;

    String body;
    serializeJson(reqDoc, body);

    HTTPClient http;
    int status = -1;
    String payload;

    Serial.println("Payload length: " + String(body.length()));

    // Retry POST request up to 10 times due to complexity/size.
    for (int retry = 0; retry  0) {
            payload = http.getString();
            http.end();
            break;
        }

        Serial.printf("Conclusion request failed (%d). Retry...\n", retry + 1);
        http.end();
        vTaskDelay(pdMS_TO_TICKS(1500));
    }

    if (status  0) {
        String scoreStr = rawResult.substring(0, sep);
        summary = rawResult.substring(sep + 1);
        summary.trim();
        percentage_emotional = scoreStr.toInt();
    }

    Serial.printf("Emotional tendency score: %d/100\n", percentage_emotional);

    // Set the LED blink rate based on the emotional score.
    SetBlinkOutput(percentage_emotional);

    // Prepare and send the conclusion to the WebSocket client.
    DynamicJsonDocument outDoc(8192);
    JsonArray arr = outDoc.createNestedArray("conclusion");
    arr.add(summary);

    String output;
    serializeJson(outDoc, output);
    strlcpy(msg.text, output.c_str(), sizeof(msg.text));
    xQueueSend(wsQueue, &amp;msg, 0);

    user.state_number = 4; // Move to state 4: Conclusion received/Process complete.
    return true;
}

/**
 * @brief FreeRTOS task wrapper for fetching the conclusion.
 * Frees parameters and deletes the task upon completion.
 * @param pvParameters Pointer to the ConclusionTaskParams structure.
 */
void conclusionTask(void *pvParameters) {
    ConclusionTaskParams *params = (ConclusionTaskParams *)pvParameters;
    get_conclusion(params-&gt;code, params-&gt;answers, params-&gt;clientId);
    delete params;
    vTaskDelete(NULL);
}
				
			

Part 4 — Web Application

Next, we move on to the Web Application layer of the project.

The web interface is built with a modular structure to keep the system clean and easy to extend. This layer is divided into three primary components, each placed in the web directory and responsible for a distinct part of the workflow:

  1. Route Registration Module — Defines all HTTP routes and maps them to their corresponding handlers.

  2. WebSocket Handler Module — Manages real-time communication between the browser and the ESP32, including sending questions, receiving responses, and triggering API requests.

  3. Static File Server Module — Serves the front-end files (HTML, CSS, JavaScript) stored in LittleFS, ensuring the entire UI is delivered directly from the microcontroller.

By separating these components, the web system remains fully modular, making it easier to update, debug, or scale additional functionalities as the project grows.

				
					// routes.h
/**
 * @file routes.h
 * @brief Utility functions for registering web server and WebSocket routes.
 */
#pragma once
#include "AsyncTCP.h"
#include "config.h" // Access to ws, httpServer, wsServer
#include "serve_files.h"
#include "views.h"

/**
 * @brief Registers the WebSocket handler.
 * Attaches the global `ws` object to the `wsServer` and sets the event handler.
 */
inline void registerWebSocket() {
    /** @brief WebSocket events are dispatched to the onWsEvent function. */
    ws.onEvent(onWsEvent);
    wsServer.addHandler(&amp;ws);
}

/**
 * @brief Registers the main HTTP web server routes.
 * Includes the root route ("/") and asset routes ("/assets/*").
 */
inline void registerWebRoutes() {
    /** @brief Root route ("/") serves the main index.html file. */
    httpServer.on("/", HTTP_GET, serveIndex);
    
    /** @brief Asset route ("/assets/*") serves static files (CSS, JS, images, etc.). */
    httpServer.on("/assets/*", HTTP_GET, serveAsset);

    /**
     * @brief Default handler for Not Found (404) requests.
     * If the URL starts with "/api/", sends a JSON 404 response; otherwise, serves index.html (SPA fallback).
     */
    httpServer.onNotFound([](AsyncWebServerRequest *request) {
        if (request-&gt;url().startsWith("/api/")) {
            request-&gt;send(404, "application/json", "{\"error\":\"API not found\"}");
        } else {
            serveIndex(request);
        }
    });
}
				
			

Within this module, we define two main functions responsible for initializing the web layer:

  1. WebSocket Handler Registration — This function sets up the WebSocket endpoint and links it to the real-time event handler used by the chat interface.

  2. Static File Route Registration — This function maps all static file paths (HTML, CSS, JS) to the ESP32’s internal file server, allowing the browser to load the front-end directly from LittleFS.

These two registration steps complete the setup of the web application and ensure that both the UI and real-time communication channel are properly initialized.

				
					// serve_files.h

/**
 * @file serve_files.h
 * @brief Functions for serving static files from LittleFS.
 */
#pragma once
#include "config.h" // Access to AsyncWebServerRequest
#include "LittleFS.h"

/**
 * @brief Serves the main index.html file from LittleFS.
 * @param request Pointer to the web server request object.
 */
void serveIndex(AsyncWebServerRequest *request) {
    request-&gt;send(LittleFS, "/index.html", "text/html");
}

/**
 * @brief Serves asset files from LittleFS based on the requested URL path.
 * Determines the Content-Type based on the file extension.
 * @param request Pointer to the web server request object.
 */
void serveAsset(AsyncWebServerRequest *request) {
    String path = request-&gt;url();
    if (LittleFS.exists(path)) {
        String contentType = "text/plain";
        // Determine Content Type based on file extension
        if (path.endsWith(".css"))
            contentType = "text/css";
        else if (path.endsWith(".js"))
            contentType = "application/javascript";
        else if (path.endsWith(".png"))
            contentType = "image/png";
        else if (path.endsWith(".jpg") || path.endsWith(".jpeg"))
            contentType = "image/jpeg";
        else if (path.endsWith(".svg"))
            contentType = "image/svg+xml";
        else if (path.endsWith(".ico"))
            contentType = "image/x-icon";
        else if (path.endsWith(".json"))
            contentType = "application/json";

        request-&gt;send(LittleFS, path, contentType);
    } else {
        request-&gt;send(404, "text/plain", "File not found");
    }
}
				
			

In the static file serving module, we implement two key functions:

  1. Index File Handler — This function simply returns the index.html file whenever the root of the web server is accessed. It ensures that the main interface loads correctly on every request.

  2. Dynamic Asset Handler — This function serves any file located inside the assets directory. It determines the correct Content-Type based on the file extension (such as .js, .css, .png, .svg, etc.) and delivers the file to the browser.
    If the requested file does not exist, the handler automatically returns a 404 Not Found response.

This approach keeps the web server lightweight while still allowing the ESP32 to host a structured front-end with dynamic asset loading.

				
					// views.cpp

/**
 * @file views.cpp
 * @brief Implementation of the WebSocket event handler.
 * Responsible for managing connection/disconnection and processing inbound data from WebSocket clients.
 */
#include "views.h"
#include "config.h"
#include "ArduinoJson.h"
#include "tasks/huggingface.h"
#include "utils/blink.h"
#include "user_data.h"
#include "utils/funcion.h"

/**
 * @brief Main handler for all WebSocket events.
 * Performs different actions based on the event type (connect, disconnect, data).
 * @param server Pointer to the AsyncWebSocket object.
 * @param client Pointer to the client that triggered the event.
 * @param type The type of WebSocket event (WS_EVT_CONNECT, WS_EVT_DISCONNECT, WS_EVT_DATA, ...).
 * @param arg Additional parameter (not used).
 * @param data Buffer containing received data (only for WS_EVT_DATA).
 * @param len Length of the data buffer (only for WS_EVT_DATA).
 */
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
               AwsEventType type, void *arg, uint8_t *data, size_t len)
{
    if (type == WS_EVT_CONNECT)
    {
        Serial.printf("Client connected: %u\n", client-&gt;id());
        // Set initial user state to waiting for login code.
        user.state_number = 1; 
    }
    else if (type == WS_EVT_DISCONNECT)
    {
        // On disconnection, reset user data and blink status.
        memset(&amp;user, 0, sizeof(user));
        ResetBlinkOutput();
        Serial.printf("Client disconnected: %u\n", client-&gt;id());
    }
    else if (type == WS_EVT_DATA)
    {
        // Convert received data buffer to a String.
        String message = "";
        for (size_t i = 0; i id()};
                    xTaskCreate(tokenTask, "TokenTask", 12000, params, 1, NULL);
                    client-&gt;text("login-process"); // Respond to client.
                }
                else
                {
                    client-&gt;text("login-busy"); // Respond if another login process is running.
                }
            }
            return;
        }

        if (user.state_number == 2)
        {
            // State 2: Waiting for 'CQ' (create question) request.
            if (msg == "CQ")
            {
                // Create task to fetch questions.
                QuestionTaskParams *params = new QuestionTaskParams{code, client-&gt;id()};
                xTaskCreate(questionTask, "QuestionTask", 12000, params, 1, NULL);
                client-&gt;text("question-process"); // Respond to client.
            }

            return;
        }

        if (user.state_number == 3)
        {
            // State 3: Waiting for user answer for conclusion.
            // Create task to process conclusion based on the answer.
            ConclusionTaskParams *params = new ConclusionTaskParams{code, client-&gt;id(), msg};
            xTaskCreate(conclusionTask, "ConclusionTask", 16000, params, 1, NULL);
            client-&gt;text("conclusion-process"); // Respond to client.
            return;
        }
        
        if (user.state_number == 4)
        {
            // State 4: Process finished, waiting for 'again' request.
            if (msg == "again")
            {
                // Reset question answers and blink status.
                memset(user.question1, 0, sizeof(user.question1));
                memset(user.question2, 0, sizeof(user.question2));
                memset(user.question3, 0, sizeof(user.question3));
                memset(user.question4, 0, sizeof(user.question4));
                memset(user.question5, 0, sizeof(user.question5));
                ResetBlinkOutput();
                user.state_number = 2; // Return to waiting for questions.
                Serial.println("User requested to start again, ready for questions.");
                client-&gt;text("restart-success"); // Respond to client.
            }
            return;
        }
        
        // Fallback return.
        return;
    }
}
				
			

In addition to routing and static file handling, we also have a views module, which contains the logic for each page of the web interface.
Since the entire interaction model relies on WebSockets, this module primarily focuses on a single WebSocket handler function that manages all real-time communication between the user and the ESP32.

The handler is responsible for several key operations:

  1. Client Connection — When a user connects, the system initializes the session and transitions into the starting state.

  2. Client Disconnection — When a user disconnects, any temporary user data stored in memory is cleared to ensure a clean session on the next connection.

  3. Message Handling — Incoming messages from the browser are processed based on the current internal state of the system.
    Depending on the message type, the handler triggers one of several operations, including:

    • OAuth login

    • Fetching the list of generated interview questions

    • Sending the user’s answers for analysis

    • Returning the final stress evaluation

    • Restarting the entire flow for a new test

These operations are executed by interacting with the FreeRTOS tasks defined earlier, ensuring clean separation between UI logic, AI processing, and hardware control.

Part 5 — Main File

Finally, we arrive at the main application file: main.cpp.
Here, the entire system comes together through just two standard Arduino functions — setup() and loop().

Thanks to the modular architecture we built throughout the project, the main.cpp file remains remarkably clean and easy to understand. Each subsystem—Wi-Fi configuration, tasks, WebSocket handling, and the web server—is initialized through well-structured function calls.

  • setup() handles all initialization steps, such as mounting the file system, starting Wi-Fi configuration, creating FreeRTOS tasks, and launching the web server.

  • loop() remains almost empty, only containing minimal background operations if needed, because the primary logic runs inside our modular tasks and WebSocket-based event handlers.

This structure keeps the entry point of the program simple, readable, and highly maintainable—allowing anyone reading the code to quickly grasp the overall workflow without digging through implementation details.

				
					#include "config.h"
#include "tasks/blink.h"
#include "user_data.h"
#include "web/routes.h"
#include "wifi_config/wifi_config.h"

/**
 * @brief Instance of the WiFiConfig class.
 * Responsible for managing Wi-Fi configuration and connection.
 * @param 0 The Wi-Fi reset pin (refer to WIFI_RESET_PIN in wifi_config.h).
 */
WiFiConfig wifiConfig(0);

/**
 * @brief FreeRTOS QueueHandle for WebSocket messages.
 * Used to pass messages from various tasks to the loop() function for WebSocket transmission.
 */
QueueHandle_t wsQueue;

/**
 * @brief Asynchronous HTTP Web Server running on port 80.
 * Used for serving static files and handling API routes.
 */
AsyncWebServer httpServer(80);

/**
 * @brief Asynchronous WebSocket Server running on port 81.
 * Used for real-time, bidirectional communication with clients.
 */
AsyncWebServer wsServer(81); // WebSocket server on port 81

/**
 * @brief The WebSocket object handling the "/ws" route.
 */
AsyncWebSocket ws("/ws");

/**
 * @brief FreeRTOS binary semaphore for managing the login process.
 * Ensures that only one login process (TokenTask) runs at a time.
 */
SemaphoreHandle_t loginSem;

/**
 * @brief Global status of the LED blinking feature.
 * If `true`, the internal LED will be controlled by blinkTask.
 */
bool blink_status = false;

/**
 * @brief Delay time in milliseconds for the LED blink rate.
 * The value is inversely proportional to the user's emotional state.
 */
int blink_time = 500;

/**
 * @brief Simple counter used for debugging purposes in printUserTask.
 */
int i = 0;

/**
 * @brief FreeRTOS task to periodically print the current UserData structure to Serial.
 * Used primarily for monitoring and debugging the user's state and data contents.
 * @param pvParameters Parameters passed to the task (not used).
 */
void printUserTask(void *pvParameters) {
    while (1) {
        i++;
        Serial.print(i);
        Serial.println(" ----- User Data -----");
        Serial.print("Code: ");
        Serial.println(user.code);
        Serial.print("AccessToken: ");
        Serial.println(user.accessToken);
        Serial.print("Status: ");
        Serial.println(user.state_number);
        Serial.print("Q1: ");
        Serial.println(user.question1);
        Serial.print("Q2: ");
        Serial.println(user.question2);
        Serial.print("Q3: ");
        Serial.println(user.question3);
        Serial.print("Q4: ");
        Serial.println(user.question4);
        Serial.print("Q5: ");
        Serial.println(user.question5);
        Serial.println("--------------------\n");

        // Delay for 30 seconds
        vTaskDelay(30000 / portTICK_PERIOD_MS);
    }
}

/**
 * @brief Arduino setup function.
 * Executed once upon device boot. Initializes serial, Wi-Fi, file system,
 * time, FreeRTOS tasks, servers, and routes.
 */
void setup() {
    Serial.println("Initlize setup");
    Serial.begin(115200);

    // Starts Wi-Fi configuration (STA connection or AP portal).
    wifiConfig.begin();
    
    // Check placeholder for WS queue creation (actual creation is below).
    if (!wsQueue) {
        Serial.println("Failed to create WS queue");
    }

    // Configures system time using NTP.
    configTime(0, 0, "pool.ntp.org", "time.nist.gov");
    struct tm timeinfo;
    if (!getLocalTime(&amp;timeinfo)) {
        Serial.println("Failed to obtain time");
    }

    // Create FreeRTOS tasks.
    // LED blinking task.
    xTaskCreate(blinkTask, "LED Blink", 1024, NULL, 1, NULL);
    // User data printing task for debugging.
    xTaskCreate(printUserTask, "PrintUser", 4096, NULL, 1, NULL);

    // Create queue and semaphore.
    wsQueue = xQueueCreate(10, sizeof(WsQueueMessage));
    loginSem = xSemaphoreCreateBinary();
    xSemaphoreGive(loginSem); // Give the semaphore for initial use.
    
    // Register web and WebSocket routes.
    registerWebRoutes();
    registerWebSocket();

    // Start the web servers.
    httpServer.begin();
    wsServer.begin();

    Serial.println("HTTP Server started on port 80");
    Serial.println("WebSocket Server started on port 81");
}

/**
 * @brief Arduino loop function.
 * Executes continuously. Responsible for managing the WebSocket message queue
 * and cleaning up disconnected WebSocket clients.
 */
void loop() {
    WsQueueMessage msg;
    // Receive and send messages from the WebSocket queue.
    while (xQueueReceive(wsQueue, &amp;msg, 0)) {
        Serial.println("In Queue");
        AsyncWebSocketClient *client = ws.client(msg.clientId);
        if (client) {
            client-&gt;text(msg.text);
        }
    }
    // Cleanup disconnected WebSocket clients.
    ws.cleanupClients();
}
				
			

Part 6 — Other Files

In this section, we focus on the supplementary files that implement the core project functionality and support the main application. These files provide modularity and reusability, keeping the overall codebase organized and maintainable.


1. config.h

Throughout the project, several global variables and data structures are used across multiple modules. To ensure a clean architecture and centralized access, all shared resources are declared in a dedicated header file: config.h.

This file contains:

  • Globally shared structures for storing session data, API responses, and system states.

  • Common variables required by multiple modules.

  • Extern declarations for variables instantiated in main.cpp but accessed elsewhere.

  • Definitions for constants, pin mappings, and configuration flags used throughout the project.

By centralizing these resources, each module can focus on its own functionality without redundant declarations, improving maintainability and scalability.

2. utils/blink.cpp

This file implements functions to control the LED blinking parameters, reflecting the user’s emotional score.

				
					/**
 * @file config.h
 * @brief Global variables, constants, and structure definitions for the application.
 * This file centralizes global access to server instances, FreeRTOS handles,
 * and data structures used for inter-task communication.
 */
#pragma once
#include "Arduino.h"
#include "AsyncTCP.h"
#include "ESPAsyncWebServer.h"
#include "WiFi.h"

/** @brief Maximum allowed length for a WebSocket message in the queue. */
#define MAX_WS_MSG_LEN 4096

/** @brief The GPIO pin connected to the internal LED (used for status blinking). */
#define LED_PIN 2

// --- Global External Declarations ---

/** @brief External declaration of the main HTTP server instance (port 80). */
extern AsyncWebServer httpServer;
/** @brief External declaration of the WebSocket server instance (port 81). */
extern AsyncWebServer wsServer;
/** @brief External declaration of the global WebSocket handler object. */
extern AsyncWebSocket ws;

/** @brief External declaration of the FreeRTOS binary semaphore for login process control. */
extern SemaphoreHandle_t loginSem;
/** @brief External declaration of the flag controlling the LED blinking feature. */
extern bool blink_status;
/** @brief External declaration of the LED blink rate delay in milliseconds. */
extern int blink_time;

/** @brief External declaration of the FreeRTOS queue for WebSocket messages. */
extern QueueHandle_t wsQueue;

// --- Structure Definitions ---

/**
 * @brief Structure for messages placed in the WebSocket queue.
 * Used to send messages from FreeRTOS tasks to the loop() function for transmission.
 */
struct WsQueueMessage {
    /** @brief The ID of the target WebSocket client. */
    uint32_t clientId;
    /** @brief The text content of the message to be sent (max MAX_WS_MSG_LEN). */
    char text[MAX_WS_MSG_LEN];
};

/**
 * @brief Structure for parameters passed to the TokenTask (login process).
 */
struct TokenTaskParams {
    /** @brief The user code/identifier. */
    String code;
    /** @brief The ID of the WebSocket client. */
    uint32_t clientId;
};

/**
 * @brief Structure for parameters passed to the QuestionTask (question generation).
 */
struct QuestionTaskParams {
    /** @brief The user code/identifier. */
    String code;
    /** @brief The ID of the WebSocket client. */
    uint32_t clientId;
};

/**
 * @brief Structure for parameters passed to the ConclusionTask (analysis generation).
 */
struct ConclusionTaskParams {
    /** @brief The user code/identifier. */
    String code;
    /** @brief The ID of the WebSocket client. */
    uint32_t clientId;
    /** @brief The user's combined answers/text input. */
    String answers;
};
				
			
				
					/**
 * @file blink.cpp
 * @brief Functions to control the LED blinking parameters.
 * These functions modify the global variables `blink_status` and `blink_time` used by `blinkTask`.
 */
#include "config.h" // Access to blink_status and blink_time

/**
 * @brief Sets the LED blink rate based on an emotional percentage score.
 * A higher emotional percentage results in a faster blink rate (shorter `blink_time`).
 * @param persentage_emotional The emotional percentage (0 to 100).
 */
void SetBlinkOutput(int persentage_emotional)
{
    blink_status = true;
    // Calculate delay: (101 - percentage) * 10. 
    // E.g., 100% -&gt; 10ms, 1% -&gt; 1000ms.
    blink_time = (101 - persentage_emotional) * 10; 
}

/**
 * @brief Resets the LED blinking state to inactive/default.
 * Sets `blink_status` to false and `blink_time` to 500ms (inactive state).
 */
void ResetBlinkOutput()
{
    blink_status = false;
    blink_time = 500; 
}
				
			

3. utils/funcion.h

General utility functions are implemented here. Currently, it provides a helper to verify if all questions have been answered, following DRY principles.

				
					/**
 * @file funcion.h
 * @brief General utility functions.
 */
#include "user_data.h"

/**
 * @brief Checks if all question fields in the UserData structure are filled (non-empty).
 * @param user The UserData structure to check.
 * @retval true If all 5 questions (question1 through question5) contain non-null characters.
 * @retval false Otherwise.
 */
bool allQuestionsFilled(const UserData &amp;user) {
    return user.question1[0] != '\0' &amp;&amp; user.question2[0] != '\0' &amp;&amp;
           user.question3[0] != '\0' &amp;&amp; user.question4[0] != '\0' &amp;&amp;
           user.question5[0] != '\0';
}
				
			

4. user_data.h / user_data.cpp

The UserData structure stores all information related to the user session, including answers, tokens, and state tracking.

				
					// user_data.h

/**
 * @file user_data.h
 * @brief Definition of the UserData structure and global declaration.
 */
#pragma once
#include "Arduino.h"

/**
 * @brief Structure for storing user information and session state.
 */
struct UserData {
    /**
     * @brief Response to the first question.
     */
    char question1[256];
    /**
     * @brief Response to the second question.
     */
    char question2[256];
    /**
     * @brief Response to the third question.
     */
    char question3[256];
    /**
     * @brief Response to the fourth question.
     */
    char question4[256];
    /**
     * @brief Response to the fifth question.
     */
    char question5[256];
    /**
     * @brief User's access token (likely for an external API).
     */
    char accessToken[1024];
    /**
     * @brief User code/identifier.
     */
    char code[30];
    /**
     * @brief Current state number or step of the user flow.
     * * - 1: Waiting for login code
     * - 2: Login successful / Waiting for question request
     * - 3: Answering questions / Waiting for conclusion
     * - 4: Conclusion received / Process complete
     */
    uint8_t state_number;
};

/**
 * @brief Global declaration of the UserData instance.
 * Provides access to user data from other files.
 */
extern UserData user;

// user_data.cpp
/**
 * @file user_data.cpp
 * @brief Definition of the global User Data structure.
 */
#include "user_data.h"

/**
 * @brief Global instance of the UserData structure.
 * This object holds information about the current user's session, access token, and survey responses.
 */
UserData user = {};


				
			

Conclusion

In this tutorial, we explored StressCheck, an ESP32-based embedded system that bridges the gap between IoT and AI. By combining real-time interaction with a Large Language Model (via Hugging Face) and physical feedback through an LED, the project demonstrates how microcontrollers can be used for innovative human-centric applications.

The modular architecture of the project ensures clean separation of concerns: Wi-Fi configuration, FreeRTOS tasks, WebSocket handling, and front-end serving are all encapsulated in distinct modules. This not only keeps the main file concise but also improves maintainability and scalability.

Through StressCheck, we see the potential of embedding intelligent, adaptive logic in low-power devices, providing a foundation for future projects that combine AI, embedded systems, and interactive user experiences.


References

  1. Hugging Face. (2023). Hugging Face API documentation. Retrieved from https://huggingface.co/docs

  2. ESP32. (2023). Espressif Systems ESP32 Technical Reference Manual. Espressif Systems. https://www.espressif.com/en/products/socs/esp32

  3. Arduino. (2023). Arduino Programming Guide. Arduino.cc. https://www.arduino.cc/en/Guide/HomePage

  4. FreeRTOS. (2023). FreeRTOS Kernel Documentation. Amazon Web Services. https://www.freertos.org

دیدگاه‌ خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

پیمایش به بالا