Skip to content

15: Personal Assignment - Smart Fantasy Lantern Web Control Interface Design and Implementation

Discussing Requirements with AI

Given the high complexity of this week's assignment, I attempted to use AI tools to determine the project approach from the very beginning. I regularly use Claude.

First, I provided Claude 3.7 with detailed background information and described my initial requirements.

I provided Claude with an introduction to my final project, the personal assignments from weeks 10 and 11 in md format, and the text of my week 15 course handout to ensure Claude had the necessary background information. Then I used the following prompt:

"I've provided you with my Fab Academy Week 15 course handout, where you can see the personal assignment requirements. I've also provided documentation for my Final Project in its current version, as well as my Week 10 and Week 11 personal assignment documents. I'd like to build upon the Week 10 and 11 assignments, adding software control functionality that could become part of my Final Project. Please first thoroughly understand this background material, then use Chinese to create a separate markdown document outlining system goals, system architecture (explaining hardware and software architecture separately, using text-based diagram language), technical choices, and implementation plans. Let's not get into the programming details yet; let's first discuss the key project issues."

Claude subsequently provided a very lengthy and detailed plan outline with three options (A, B, and C) of varying complexity. I realized that AI's perfectionist tendencies would turn a simple requirement into a complex project requiring weeks of work. I saved one of the plan documents, started a new chat, and asked Claude to simplify the project.

My prompt to simplify the requirements was:

"Week 15 contains my assignment requirements, and the Week 10 and 11 documents are for reference. Please first thoroughly understand the Week 15 document. I think the current Week 15 plan is too complex, and we only have 1 day. Please keep it as simple as possible, meeting the requirements without unnecessarily expanding functionality. Please help me modify the Week 15 document, removing the 4-stage plan and directly completing the full project documentation and code. I hope to be able to deploy and test the entire project according to your document."

The main body of the following document, including code sections, was basically output by Claude in one go. During actual implementation, I made minor modifications such as adding illustrations, modifying or completing explanations, etc.


Project Introduction

The main goal of this week's personal project is to build an intuitive, fully functional software interface based on existing hardware design to remotely control and monitor the Smart Fantasy Lantern. This system will integrate the Week 10 gesture control system, providing a richer interactive experience through a Web application, laying the software foundation for the final project.

Core project objectives:

  1. Design and implement a Web-based user interface to control the Smart Fantasy Lantern
  2. Integrate existing gesture control functionality
  3. Provide LED status visualization and feedback
  4. Implement system status monitoring

Technology Selection and Rationale

Core Technology Choices

Technology LayerSelected SolutionSelection Rationale
Server-sideESP32C3 + Arduino + ESPAsyncWebServer1. Fully compatible with existing hardware 2. Small resource footprint, suitable for embedded devices 3. ESP32C3's networking capabilities verified in Week 11
Frontend FrameworkHTML + CSS + Minimal JavaScript1. Lightweight, suitable for ESP32C3's limited storage capacity 2. Simple and intuitive, high development efficiency 3. Good cross-platform compatibility
Communication ProtocolRESTful API1. Simple to use, suitable for basic control functions 2. Complies with Web standards, easy to extend 3. Simple development and debugging
Data StorageSPIFFS1. Built into ESP32C3, no additional hardware needed 2. Supports storing web page files 3. Well supported in Arduino ecosystem

Detailed Technology Selection Rationale

Server-side Technology

Reasons for implementing the Web server directly on the ESP32C3 rather than using an external server or cloud platform:

  1. Independence: The system can operate without an internet connection, requiring only a local Wi-Fi network.
  2. Low latency: Control commands are transmitted directly on the local network without going through the internet.
  3. Cost-effectiveness: No additional hardware devices or cloud service subscriptions required.
  4. Integration with existing systems: Can directly extend the network functionality implemented in Week 11.

Although the ESP32C3 has limited resources (only 320KB SRAM), a carefully optimized Web interface and resource compression can fully realize the required functionality. The ESPAsyncWebServer library supports asynchronous request handling, allowing it to respond to the gesture sensor while processing Web requests.

Frontend Technology

Reasons for developing the frontend interface with HTML + CSS + minimal JavaScript:

  1. Lightweight: Minimizes resource usage, suitable for ESP32C3's storage capacity
  2. Development efficiency: Simple, intuitive technology stack for quick implementation and debugging
  3. Cross-platform compatibility: The same interface works on phones, tablets, and computers
  4. Easy maintenance: Simple code structure, easy to understand and modify

Complex frameworks like React or Vue are avoided because, although powerful, they would occupy more storage space and are overly complex for a simple control interface.

System Architecture Design

Overall Architecture

The system adopts a typical client-server architecture, divided into three main levels:

This sketch was created by asking Claude to use Mermaid diagram language

Detailed Software Module Design

Server-side Modules (ESP32C3)

ModuleFunction DescriptionTechnical Implementation
Web ServerProvide static file service Handle RESTful API requestsESPAsyncWebServer SPIFFS file system
Control Logic LayerProcess LED control commands Respond to gesture recognition events Manage system statusArduino C++ State management logic
Data ManagementStore LED status Record system statusMemory variables Optional SPIFFS persistence
Hardware Abstraction LayerLED control Gesture sensor interfaceDirect GPIO control I2C communication

Frontend Modules (Browser)

ModuleFunction DescriptionTechnical Implementation
Control PanelLED light quantity control LED all on/all off controlHTML form elements JavaScript event handling
Status DisplayLED status visualization Device connection statusCSS styles JavaScript DOM updates
Communication ServiceRESTful API calls Status pollingFetch API JavaScript timers

Interface Design

RESTful API Design

LED Control Interface

Interface PathMethodFunction DescriptionRequest ParametersResponse Data
/api/led/statusGETGet LED statusNoneNumber of lit LEDs (0-6)
/api/led/controlPOSTControl LEDscount: Number of LEDs to light action: "all_on"/"all_off"Success/failure status

Example requests and responses:

GET /api/led/status

json
// Response
{
  "count": 3
}

POST /api/led/control

plain
// Request - Set number of lit LEDs
count=4

// Or

// Request - Turn all on
action=all_on

// Response
{
  "success": true,
  "message": "LED count updated"
}

System Status Interface

Interface PathMethodFunction DescriptionRequest ParametersResponse Data
/api/system/infoGETGet system informationNoneSystem name, version, IP address, connection status

Example response:

GET /api/system/info

json
{
  "name": "SmartLantern",
  "version": "1.0.0",
  "ip": "192.168.1.100",
  "connected": true
}

User Interface Design

Main Interface Layout

Based on a simplified interface design, focusing on core functionality and intuitive operation:

  1. Device status area:
    • Shows current device connection status (color-coded: green for connected, red for disconnected).
    • Displays device IP address.
    • Located at the top of the interface for quick viewing.
  2. LED control area:
    • 6 independent LED status indicators (corresponding to 6 physical LEDs).
    • Uses color to distinguish on/off states (green for on, gray for off).
    • "Increase" and "Decrease" buttons directly control the number of lit LEDs.
    • "All on" and "All off" buttons provide shortcut operations.
    • Consistent with Week 10 gesture control operation logic (swipe up to increase, swipe down to decrease).

Control Panel Functionality

Intuitive, concise control function design:

  1. LED control:
    • Intuitive LED indicators show current lighting status.
    • "Increase" and "Decrease" buttons control the number of lit LEDs.
    • "All on" and "All off" buttons provide one-touch operation.
    • Visual feedback for each operation.
  2. Status feedback:
    • Connection status indicator provides network connection feedback.
    • IP address display for easy identification in multi-device environments.
    • Regular automatic status refresh to keep the interface synchronized.

Interface Wireframe

This sketch was created by asking Claude to use Mermaid diagram language

Implementation Code

Main Project Folder

plain
/smart_lantern_control/
├── smart_lantern_control.ino     # Arduino main program file
├── data/                         # SPIFFS data directory (web files)
│   └── index.html                # Web interface HTML file
└── libraries/                    # Third-party libraries (automatically managed by Arduino IDE)

Main Program Code (smart_lantern_control.ino)

The main program is responsible for initializing hardware, establishing Wi-Fi connections, setting up the Web server, and handling gesture control and LED status updates. Key features include:

  • Initializing the SPIFFS file system and gesture sensor
  • Setting up Web server routes, including static file service and API interfaces
  • Implementing gesture recognition logic and LED control
  • Adding file management functionality, solving the problem of unavailable ESP32FS tools

The added file management functionality provides an interface when accessing the /files path, allowing uploading and managing Web files, solving the problem of unavailable ESP32FS tools. This is an important solution, ensuring that Web interface files can be uploaded even without specialized tools.

cpp
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <Wire.h>
#include <ArduinoJson.h>
#include "XLOT_APDS9960AD.h"

// WiFi settings
const char* ssid = "xiao";
const char* password = "12345678";

// Server and WebSocket
AsyncWebServer server(80);

// File upload
File fsUploadFile;

// Pin redefinition - adapted for custom expansion board
#define SDA_PIN 20      // RX/D7 (GPIO20)
#define SCL_PIN 21      // TX/D6 (GPIO21)

// LED pin definitions
const int LED_PINS[] = {D5, D4, D3, D2, D1, D0};
const int LED_COUNT = 6;

// Gesture sensor
XLOT_APDS9960AD apds;

// Control variables
int ledCount = 0;              // Number of lit LEDs (0-6)
unsigned long lastGestureTime = 0;  // Last gesture timestamp
const int gestureDelay = 500;  // Gesture recognition interval (ms)

// System information
String deviceName = "SmartLantern";
String firmwareVersion = "1.0.0";

// Initialize SPIFFS
void initSPIFFS() {
  if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS mount failed");
    return;
  }
  Serial.println("SPIFFS mount successful");
  
  // List files in SPIFFS
  File root = SPIFFS.open("/");
  File file = root.openNextFile();
  
  Serial.println("SPIFFS file list:");
  while(file) {
    Serial.print("- ");
    Serial.print(file.name());
    Serial.print(" (");
    Serial.print(file.size());
    Serial.println(" bytes)");
    file = root.openNextFile();
  }
}

// Connect to WiFi
void connectToWiFi() {
  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("WiFi connection successful!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// Update LED display
void updateLEDs() {
  for(int i = 0; i < LED_COUNT; i++) {
    // If i is less than ledCount, turn on LED, otherwise turn off
    digitalWrite(LED_PINS[i], (i < ledCount) ? HIGH : LOW);
  }
}

// Process gesture
void processGesture(uint8_t gesture) {
  switch(gesture) {
    case APDS9960_RIGHT:
      // Swipe right - Turn on all LEDs
      if(ledCount < LED_COUNT) {
        ledCount = LED_COUNT;
        updateLEDs();
        Serial.println("All LEDs on");
      }
      break;
      
    case APDS9960_LEFT:
      // Swipe left - Turn off all LEDs
      if(ledCount > 0) {
        ledCount = 0;
        updateLEDs();
        Serial.println("All LEDs off");
      }
      break;
      
    case APDS9960_UP:
      // Swipe up - Increase LED count
      if(ledCount < LED_COUNT) {
        ledCount++;
        updateLEDs();
        Serial.print("LED count: ");
        Serial.println(ledCount);
      }
      break;
      
    case APDS9960_DOWN:
      // Swipe down - Decrease LED count
      if(ledCount > 0) {
        ledCount--;
        updateLEDs();
        Serial.print("LED count: ");
        Serial.println(ledCount);
      }
      break;
  }
}

// Set up Web server routes
void setupServer() {
  // Static file handling
  server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
  
  // API route: Get LED status
  server.on("/api/led/status", HTTP_GET, [](AsyncWebServerRequest *request) {
    DynamicJsonDocument doc(128);
    doc["count"] = ledCount;
    
    String response;
    serializeJson(doc, response);
    
    request->send(200, "application/json", response);
  });
  
  // API route: Control LEDs
  server.on("/api/led/control", HTTP_POST, [](AsyncWebServerRequest *request) {
    // Default response
    DynamicJsonDocument responseDoc(128);
    responseDoc["success"] = false;
    responseDoc["message"] = "Invalid request";
    
    if (request->hasParam("count", true)) {
      int newCount = request->getParam("count", true)->value().toInt();
      if (newCount >= 0 && newCount <= LED_COUNT) {
        ledCount = newCount;
        updateLEDs();
        
        responseDoc["success"] = true;
        responseDoc["message"] = "LED count updated";
      } else {
        responseDoc["message"] = "LED count out of range (0-6)";
      }
    } else if (request->hasParam("action", true)) {
      String action = request->getParam("action", true)->value();
      
      if (action == "all_on") {
        ledCount = LED_COUNT;
        updateLEDs();
        responseDoc["success"] = true;
        responseDoc["message"] = "All LEDs turned on";
      } else if (action == "all_off") {
        ledCount = 0;
        updateLEDs();
        responseDoc["success"] = true;
        responseDoc["message"] = "All LEDs turned off";
      } else {
        responseDoc["message"] = "Unknown action";
      }
    }
    
    String response;
    serializeJson(responseDoc, response);
    
    request->send(200, "application/json", response);
  });
  
  // API route: Get system information
  server.on("/api/system/info", HTTP_GET, [](AsyncWebServerRequest *request) {
    DynamicJsonDocument doc(256);
    doc["name"] = deviceName;
    doc["version"] = firmwareVersion;
    doc["ip"] = WiFi.localIP().toString();
    doc["connected"] = WiFi.status() == WL_CONNECTED;
    
    String response;
    serializeJson(doc, response);
    
    request->send(200, "application/json", response);
  });
  
  // File upload related routes
  // File management interface - Fix UTF-8 encoding issue
  server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) {
    String content = "<html><head>";
    content += "<title>Smart Fantasy Lantern - File Management</title>";
    content += "<meta charset='UTF-8'>"; // Add UTF-8 character set declaration
    content += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
    content += "<style>";
    content += "body { font-family: Arial, sans-serif; margin: 20px; }";
    content += "h1 { color: #2196F3; }";
    content += ".file-list { margin: 20px 0; }";
    content += ".file-item { padding: 8px; border-bottom: 1px solid #eee; }";
    content += ".btn { padding: 8px 15px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; text-decoration: none; display: inline-block; }";
    content += ".btn-danger { background-color: #F44336; }";
    content += ".upload-form { margin: 20px 0; padding: 15px; background-color: #f5f5f5; border-radius: 5px; }";
    content += "</style>";
    content += "</head><body>";
    content += "<h1>Smart Fantasy Lantern - File Management</h1>";
    content += "<div class='upload-form'>";
    content += "<h2>Upload File</h2>";
    content += "<form method='POST' action='/upload' enctype='multipart/form-data'>";
    content += "<input type='file' name='upload'>";
    content += "<button class='btn' type='submit'>Upload</button>";
    content += "</form>";
    content += "</div>";
    content += "<h2>File List</h2>";
    content += "<div class='file-list'>";
    
    File root = SPIFFS.open("/");
    File file = root.openNextFile();
    while(file) {
      String fileName = String(file.name());
      content += "<div class='file-item'>";
      content += fileName + " (" + String(file.size()) + " bytes) ";
      content += "<a class='btn' href='" + fileName + "'>View</a> ";
      content += "<a class='btn btn-danger' href='/delete?file=" + fileName + "'>Delete</a>";
      content += "</div>";
      file = root.openNextFile();
    }
    
    content += "</div>";
    content += "<p><a class='btn' href='/'>Back to Control Panel</a></p>";
    content += "</body></html>";
    request->send(200, "text/html; charset=utf-8", content); // Specify character set as UTF-8
  });
  
  // File upload handling
  server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) {
    request->redirect("/files");
  }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
    if (!index) {
      Serial.printf("Receiving upload: %s\n", filename.c_str());
      // Open file for writing
      if (!filename.startsWith("/")) {
        filename = "/" + filename;
      }
      fsUploadFile = SPIFFS.open(filename, "w");
    }
    
    if (fsUploadFile) {
      // Write to file
      fsUploadFile.write(data, len);
    }
    
    if (final) {
      if (fsUploadFile) {
        // Close file
        fsUploadFile.close();
      }
      Serial.printf("Upload complete: %s, %u bytes\n", filename.c_str(), index + len);
    }
  });
  
  // File deletion
  server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("file")) {
      String fileName = request->getParam("file")->value();
      if (SPIFFS.remove(fileName)) {
        request->redirect("/files?msg=Delete successful");
      } else {
        request->send(500, "text/plain", "Delete failed");
      }
    } else {
      request->send(400, "text/plain", "Missing file parameter");
    }
  });
  
  // 404 handling
  server.onNotFound([](AsyncWebServerRequest *request) {
    request->send(404, "text/plain", "Page not found");
  });
  
  // Start server
  server.begin();
  Serial.println("HTTP server started");
}

void setup() {
  Serial.begin(115200);
  delay(3000);  // Add delay to ensure enough time to upload new code
  Serial.println("\nSmart Fantasy Lantern Web Control System starting...");
  
  // Initialize I2C with redefined pins
  Wire.begin(SDA_PIN, SCL_PIN);
  
  // Initialize all LED pins
  for(int i = 0; i < LED_COUNT; i++) {
    pinMode(LED_PINS[i], OUTPUT);
    digitalWrite(LED_PINS[i], LOW);  // Initial state all off
  }
  
  // Initialize gesture sensor
  if(!apds.begin()){
    Serial.println("Gesture sensor initialization failed! Please check wiring.");
    // Error indication - flash first LED
    while(1) {
      digitalWrite(LED_PINS[0], HIGH);
      delay(100);
      digitalWrite(LED_PINS[0], LOW);
      delay(100);
    }
  } else {
    Serial.println("Gesture sensor initialization successful!");
    apds.enableProximity(true);
    apds.enableGesture(true);
    apds.setProxGain(APDS9960_PGAIN_8X);
    apds.setGestureGain(APDS9960_PGAIN_8X);
    apds.setGestureGain(APDS9960_AGAIN_64X);
    apds.setGestureGain(APDS9960_GGAIN_8);
    
    // Success indication - all LEDs light up sequentially then turn off
    for(int i = 0; i < LED_COUNT; i++) {
      digitalWrite(LED_PINS[i], HIGH);
      delay(200);
    }
    delay(500);
    for(int i = 0; i < LED_COUNT; i++) {
      digitalWrite(LED_PINS[i], LOW);
      delay(200);
    }
  }
  
  // Initialize SPIFFS
  initSPIFFS();
  
  // Connect to WiFi
  connectToWiFi();
  
  // Set up server
  setupServer();
  
  Serial.println("System initialization complete!");
}

void loop() {
  // Read gesture
  uint8_t gesture = apds.readGesture();
  
  // Process gesture (add delay to prevent too rapid response)
  if(gesture != 0 && millis() - lastGestureTime > gestureDelay) {
    lastGestureTime = millis();
    processGesture(gesture);
  }
  
  // Check WiFi connection
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi connection lost, attempting to reconnect...");
    WiFi.reconnect();
  }
  
  // Add short delay to reduce CPU usage
  delay(10);
}

HTML Interface Code (index.html)

The HTML file implements a responsive design Web interface:

  • Status indicator area shows device connection status and IP
  • LED control area provides visual feedback and control buttons
  • JavaScript portion handles API calls and status updates
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Smart Fantasy Lantern Control</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 600px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f5f5f5;
    }
    
    .header {
      text-align: center;
      margin-bottom: 20px;
    }
    
    .status-bar {
      background-color: #fff;
      padding: 10px;
      border-radius: 5px;
      margin-bottom: 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .status-indicator {
      display: flex;
      align-items: center;
    }
    
    .status-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      margin-right: 8px;
    }
    
    .connected {
      background-color: #4CAF50;
    }
    
    .disconnected {
      background-color: #F44336;
    }
    
    .control-card {
      background-color: #fff;
      padding: 20px;
      border-radius: 5px;
      margin-bottom: 20px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    
    .led-container {
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
    }
    
    .led {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background-color: #ddd;
      margin: 0 5px;
    }
    
    .led.active {
      background-color: #4CAF50;
      box-shadow: 0 0 10px #4CAF50;
    }
    
    .control-buttons {
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
    }
    
    .button-group {
      display: flex;
    }
    
    button {
      padding: 10px 15px;
      margin: 0 5px;
      border: none;
      border-radius: 4px;
      background-color: #2196F3;
      color: white;
      cursor: pointer;
      font-size: 14px;
    }
    
    button:hover {
      background-color: #0b7dda;
    }
    
    .footer {
      text-align: center;
      font-size: 12px;
      color: #666;
      margin-top: 30px;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>Smart Fantasy Lantern Control</h1>
  </div>
  
  <div class="status-bar">
    <div class="status-indicator">
      <div id="status-dot" class="status-dot disconnected"></div>
      <span id="status-text">Not connected</span>
    </div>
    <div id="ip-address">IP: -</div>
  </div>
  
  <div class="control-card">
    <h2>LED Control</h2>
    <div class="led-container" id="led-display">
      <div class="led" id="led-0"></div>
      <div class="led" id="led-1"></div>
      <div class="led" id="led-2"></div>
      <div class="led" id="led-3"></div>
      <div class="led" id="led-4"></div>
      <div class="led" id="led-5"></div>
    </div>
    
    <div class="control-buttons">
      <div class="button-group">
        <button id="btn-decrease">Decrease ⬇️</button>
        <button id="btn-increase">Increase ⬆️</button>
      </div>
      <div class="button-group">
        <button id="btn-all-off">All Off ◀️</button>
        <button id="btn-all-on">All On ▶️</button>
      </div>
    </div>
  </div>
  
  <div class="footer">
    <p>Smart Fantasy Lantern Control System v1.0.0</p>
    <p>Based on ESP32C3 + Web technology</p>
  </div>
  <script>
    // Status variables
    let ledCount = 0;
    let isConnected = false;
    
    // DOM elements
    const statusDot = document.getElementById('status-dot');
    const statusText = document.getElementById('status-text');
    const ipAddress = document.getElementById('ip-address');
    const ledElements = Array.from(document.querySelectorAll('.led'));
    
    // Button elements
    const btnIncrease = document.getElementById('btn-increase');
    const btnDecrease = document.getElementById('btn-decrease');
    const btnAllOn = document.getElementById('btn-all-on');
    const btnAllOff = document.getElementById('btn-all-off');
    
    // Update LED display
    function updateLedDisplay() {
      ledElements.forEach((led, index) => {
        if (index < ledCount) {
          led.classList.add('active');
        } else {
          led.classList.remove('active');
        }
      });
    }
    
    // Update connection status
    function updateConnectionStatus(connected, ip) {
      isConnected = connected;
      
      if (connected) {
        statusDot.classList.remove('disconnected');
        statusDot.classList.add('connected');
        statusText.textContent = 'Connected';
        ipAddress.textContent = `IP: ${ip}`;
      } else {
        statusDot.classList.remove('connected');
        statusDot.classList.add('disconnected');
        statusText.textContent = 'Not connected';
        ipAddress.textContent = 'IP: -';
      }
    }
    
    // Get LED status
    async function getLedStatus() {
      try {
        const response = await fetch('/api/led/status');
        const data = await response.json();
        
        ledCount = data.count;
        updateLedDisplay();
      } catch (error) {
        console.error('Failed to get LED status:', error);
      }
    }
    
    // Control LED
    async function controlLed(params) {
      try {
        const formData = new URLSearchParams(params);
        const response = await fetch('/api/led/control', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          body: formData
        });
        
        const data = await response.json();
        
        if (data.success) {
          await getLedStatus();
        } else {
          console.error('Failed to control LED:', data.message);
        }
      } catch (error) {
        console.error('Failed to send control request:', error);
      }
    }
    
    // Get system information
    async function getSystemInfo() {
      try {
        const response = await fetch('/api/system/info');
        const data = await response.json();
        
        updateConnectionStatus(data.connected, data.ip);
      } catch (error) {
        console.error('Failed to get system information:', error);
        updateConnectionStatus(false, '-');
      }
    }
    
    // Initialize
    async function initialize() {
      await getSystemInfo();
      await getLedStatus();
      
      // Set up periodic refresh
      setInterval(getSystemInfo, 5000);
      setInterval(getLedStatus, 2000);
    }
    
    // Event listeners
    btnIncrease.addEventListener('click', () => {
      if (ledCount < 6) {
        controlLed({ count: ledCount + 1 });
      }
    });
    
    btnDecrease.addEventListener('click', () => {
      if (ledCount > 0) {
        controlLed({ count: ledCount - 1 });
      }
    });
    
    btnAllOn.addEventListener('click', () => {
      controlLed({ action: 'all_on' });
    });
    
    btnAllOff.addEventListener('click', () => {
      controlLed({ action: 'all_off' });
    });
    
    // Start the application
    document.addEventListener('DOMContentLoaded', initialize);
  </script>
</body>
</html>

The HTML file above was previewed in Windsurf IDE to see its actual appearance in the browser.

Preview of the index.html file in Windsurf IDE, looks good

Project Implementation Steps

Preparation

First, prepare the following materials and tools:

  1. Hardware devices:
    • XIAO ESP32C3 development board (self-made)
    • APDS-9960 gesture sensor
    • Connection wires/jumper wires
    • USB data cable
  2. Software tools:

Installing Necessary Libraries

Install necessary libraries in Arduino IDE:

  1. Arduino built-in library manager:
    • Open Arduino IDE
    • Select "Tools" -> "Manage Libraries"
    • Search for and install ArduinoJson library
  2. Manually install third-party libraries:
    • ESPAsyncWebServer and AsyncTCP libraries need manual installation
    • Download library files from GitHub (ZIP format)
    • In Arduino IDE, select "Sketch" -> "Include Library" -> "Add .ZIP Library"
    • Select the downloaded ZIP file
  3. Confirm that XLOT_APDS9960AD library (used in Week 10) is installed

Hardware Connection

Connect the gesture sensor according to Week 10 assignment:

APDS-9960 PinConnection to Expansion Board PinXIAO ESP32C3 PinFunction
VCC (red wire)J1-2 (3.3V)3.3VPower positive
GND (black wire)J1-1 (GND)GNDGround
SDA (yellow wire)J1-3 (RX/D7)GPIO20I2C data line (software implementation)
SCL (green wire)J1-4 (TX/D6)GPIO21I2C clock line (software implementation)

Connection diagram showing use of Y-type splitter to share power and ground connections between two devices

Installation Steps

  1. Prepare and install necessary libraries (ESPAsyncWebServer, AsyncTCP, ArduinoJson)
  2. Create project directory structure, including main program and data folder
  3. Compile and upload the main program to XIAO ESP32C3, open serial monitor to see an IP address prompt.

After successful upload, the serial monitor provides status information and displays an IP address

  1. Use a computer to access http://[ESP32IP address]/files via web browser to upload index.html. Note that the computer needs to be connected to the same Wi-Fi as the XIAO ESP32C3 to access it successfully.

File management interface displayed by XIAO ESP32C3, through which we can upload the index.html file

  1. Now you can test the Web control interface by accessing http://[ESP32IP address]/ through a computer or mobile phone browser.

Using mouse click to turn on all LEDs

All LEDs on the development board are lit.

Now you can fully test both gesture and Web control effects.

Challenges and Solutions

ESP32FS Tool Unavailable

Problem: ESP32FS tool could not be found in Arduino IDE, making direct upload of HTML files impossible.

Solution: Added Web file management functionality, providing a file upload interface through the /files path, allowing users to upload HTML files directly through a Web browser, eliminating dependency on the ESP32FS tool.

Page Encoding Issues

Problem: Chinese characters appeared garbled when initially accessing the file management interface.

Solution: Although there was garbling, the functionality worked normally, and a correctly encoded index.html file was successfully uploaded through the file management interface, solving the problem. Then informed Claude about the encoding issue, and Claude provided an updated Arduino program which resolved the problem.

Cross-device Access

Problem: Computer could not access the ESP32C3's Web interface.

Solution: Confirmed that all devices need to be connected to the Wi-Fi network created by ESP32C3 to access the Web interface.

Test Results

System test results show that the project functionality is complete:

  • Wi-Fi connection successfully established, IP address correctly displayed
  • File upload function works normally, index.html successfully uploaded
  • Web interface loads correctly, showing LED status and connection information
  • Button control functions work normally, able to increase/decrease LED counts and turn all on/off
  • Gesture control function works normally, synchronized with Web control

Conclusion and Future Development

This project successfully implemented the Web control interface for the Smart Fantasy Lantern, integrating gesture control functionality and providing an intuitive user interface. The system adopted a lightweight design suitable for ESP32C3's resource constraints while maintaining a good user experience.

By adding Web file management functionality, the problem of unavailable ESP32FS tools was solved, providing a more flexible deployment method. The project laid the software foundation for the "Smart Fantasy Lantern" final system, demonstrating ESP32C3's ability to handle both sensor input and Web services simultaneously.

Future considerations could include adding LED brightness control, motor control functionality, multi-device synchronization, and other extensions to further enhance system functionality and user experience.