#include #include #include #include // Wi-Fi // ESP32-C3 / ESP32-S3 only supports 2.4 GHz Wi-Fi. // Replace these with your router's 2.4 GHz SSID and password. const char* wifiSsid = "YOUR_2G_WIFI_NAME"; const char* wifiPassword = "YOUR_WIFI_PASSWORD"; // fallback AP const char* apSsid = "ESP32C3-Light-Test"; const char* apPassword = "12345678"; // LED strip #ifndef D1 #define D1 3 #endif const int LED_PIN = D1; const int NUM_LEDS = 30; // Match the actual WS2812 strip length Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); WebServer server(80); bool stationConnected = false; bool apStarted = false; bool lightOn = false; unsigned long lastWifiRetryAt = 0; constexpr unsigned long WIFI_RETRY_INTERVAL_MS = 30000; int brightnessValue = 100; // 0-180,先不要开到255 String colorName = "yellow"; uint8_t currentR = 255; uint8_t currentG = 180; uint8_t currentB = 60; String currentScene = "breathing"; float breathRate = 1.6f; int breathDepth = 68; unsigned long lastAnimationFrameAt = 0; constexpr float MIN_BREATH_RATE = 0.8f; constexpr float MAX_BREATH_RATE = 3.2f; constexpr int MIN_BREATH_DEPTH = 20; constexpr int MAX_BREATH_DEPTH = 100; constexpr unsigned long ANIMATION_FRAME_MS = 20; void addCorsHeaders() { server.sendHeader("Access-Control-Allow-Origin", "*"); server.sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); server.sendHeader("Access-Control-Allow-Headers", "*"); } void handleOptions() { addCorsHeaders(); server.send(204); } // ===================================================== // LED functions // ===================================================== void applyColorFromName(String name) { name.toLowerCase(); if (name == "blue") { currentR = 80; currentG = 130; currentB = 255; colorName = "blue"; } else if (name == "red") { currentR = 255; currentG = 70; currentB = 50; colorName = "red"; } else { currentR = 255; currentG = 180; currentB = 60; colorName = "yellow"; } } String normalizeSceneName(String name) { name.trim(); name.toLowerCase(); if (name == "breath" || name == "breathing" || name == "breathe") { return "breathing"; } if (name == "pulse" || name == "pulsing") { return "pulse"; } return "static"; } bool lightNeedsAnimation() { return lightOn && brightnessValue > 0 && (currentScene == "breathing" || currentScene == "pulse"); } void applySceneFromName(String name) { currentScene = normalizeSceneName(name); } void applyBreathSettingsFromRequest() { if (server.hasArg("rate")) { breathRate = server.arg("rate").toFloat(); } if (breathRate < MIN_BREATH_RATE) { breathRate = MIN_BREATH_RATE; } else if (breathRate > MAX_BREATH_RATE) { breathRate = MAX_BREATH_RATE; } if (server.hasArg("depth")) { breathDepth = server.arg("depth").toInt(); } if (breathDepth < MIN_BREATH_DEPTH) { breathDepth = MIN_BREATH_DEPTH; } else if (breathDepth > MAX_BREATH_DEPTH) { breathDepth = MAX_BREATH_DEPTH; } if (server.hasArg("scene")) { applySceneFromName(server.arg("scene")); } } float getSceneScale(unsigned long nowMs) { if (currentScene != "breathing" && currentScene != "pulse") { return 1.0f; } float depth = breathDepth / 100.0f; float cycleSeconds = breathRate; if (currentScene == "pulse") { cycleSeconds *= 0.7f; } if (cycleSeconds < MIN_BREATH_RATE) { cycleSeconds = MIN_BREATH_RATE; } unsigned long cycleMs = (unsigned long)(cycleSeconds * 1000.0f); if (cycleMs < 1) { cycleMs = 1; } float progress = (float)(nowMs % cycleMs) / (float)cycleMs; float wave = (sin(progress * TWO_PI) + 1.0f) * 0.5f; if (currentScene == "pulse") { wave = wave * wave; } return (1.0f - depth) + (depth * wave); } void renderLight() { if (!lightOn || brightnessValue <= 0) { strip.clear(); strip.show(); return; } float scale = (brightnessValue / 180.0f) * getSceneScale(millis()); int r = (int)(currentR * scale); int g = (int)(currentG * scale); int b = (int)(currentB * scale); if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0; if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; for (int i = 0; i < NUM_LEDS; i++) { strip.setPixelColor(i, strip.Color((uint8_t)r, (uint8_t)g, (uint8_t)b)); } strip.show(); } // ===================================================== // API responses // ===================================================== String makeStateJson() { const bool wifiConnected = WiFi.status() == WL_CONNECTED; String ipText = wifiConnected ? WiFi.localIP().toString() : WiFi.softAPIP().toString(); String json = "{"; json += "\"connected\":" + String(wifiConnected ? "true" : "false") + ","; json += "\"apActive\":" + String(apStarted ? "true" : "false") + ","; json += "\"ip\":\"" + ipText + "\","; json += "\"mode\":\"" + String(wifiConnected ? "STA" : "AP") + "\","; json += "\"lightOn\":" + String(lightOn ? "true" : "false") + ","; json += "\"brightness\":" + String(brightnessValue) + ","; json += "\"color\":\"" + colorName + "\","; json += "\"scene\":\"" + currentScene + "\","; json += "\"breath_rate\":" + String(breathRate, 1) + ","; json += "\"breathRate\":" + String(breathRate, 1) + ","; json += "\"breath_depth\":" + String(breathDepth) + ","; json += "\"breathDepth\":" + String(breathDepth) + ","; json += "\"numLeds\":" + String(NUM_LEDS); json += "}"; return json; } void sendJson(String json) { addCorsHeaders(); server.send(200, "application/json", json); } // ===================================================== // API routes // ===================================================== void handleRoot() { addCorsHeaders(); String html = ""; html += ""; html += ""; html += ""; html += "ESP32-C3 Light API"; html += ""; html += "
"; html += "

ESP32-C3 Light API

"; html += "

This page is hosted by ESP32-C3. Your local designed webpage can call the API below.

"; html += "

API:

"; html += "

/api/on / /api/off / /api/toggle / /api/state

"; html += "

/api/brightness?value=120

"; html += "

/api/color?name=yellow | blue | red

"; html += "

/api/scene?name=static | breathing | pulse

"; html += "

/api/breath?rate=1.6&depth=68&scene=breathing

"; html += "ON"; html += "OFF"; html += "TOGGLE"; html += "YELLOW"; html += "BLUE"; html += "RED"; html += "LOW"; html += "MID"; html += "HIGH"; html += "STATIC"; html += "BREATH"; html += "PULSE"; html += "
" + makeStateJson() + "
"; html += "
"; server.send(200, "text/html", html); } void handleState() { Serial.println("[API] STATE requested"); sendJson(makeStateJson()); } void handleOn() { lightOn = true; renderLight(); Serial.println("[API] Light ON"); sendJson(makeStateJson()); } void handleOff() { lightOn = false; renderLight(); Serial.println("[API] Light OFF"); sendJson(makeStateJson()); } void handleToggle() { lightOn = !lightOn; renderLight(); Serial.print("[API] Light TOGGLE -> "); Serial.println(lightOn ? "ON" : "OFF"); sendJson(makeStateJson()); } void handleBrightness() { if (server.hasArg("value")) { brightnessValue = server.arg("value").toInt(); brightnessValue = constrain(brightnessValue, 0, 180); } renderLight(); Serial.print("[API] Brightness -> "); Serial.println(brightnessValue); sendJson(makeStateJson()); } void handleColor() { if (server.hasArg("name")) { applyColorFromName(server.arg("name")); } renderLight(); Serial.print("[API] Color -> "); Serial.println(colorName); sendJson(makeStateJson()); } void handleScene() { if (server.hasArg("name")) { applySceneFromName(server.arg("name")); } renderLight(); Serial.print("[API] Scene -> "); Serial.println(currentScene); sendJson(makeStateJson()); } void handleBreath() { applyBreathSettingsFromRequest(); renderLight(); Serial.print("[API] Breath -> scene="); Serial.print(currentScene); Serial.print(", rate="); Serial.print(breathRate, 1); Serial.print("s, depth="); Serial.println(breathDepth); sendJson(makeStateJson()); } void handleNotFound() { addCorsHeaders(); server.send(404, "application/json", "{\"error\":\"not_found\"}"); } // ===================================================== // Wi-Fi // ===================================================== void startAP() { if (apStarted) { return; } WiFi.softAP(apSsid, apPassword); apStarted = true; Serial.println(); Serial.println("Fallback AP started."); Serial.print("AP SSID: "); Serial.println(apSsid); Serial.print("AP Password: "); Serial.println(apPassword); Serial.print("Open browser: http://"); Serial.println(WiFi.softAPIP()); } void connectWiFi() { WiFi.persistent(false); WiFi.setAutoReconnect(true); WiFi.mode(WIFI_AP_STA); startAP(); WiFi.begin(wifiSsid, wifiPassword); lastWifiRetryAt = millis(); Serial.println(); Serial.print("Connecting to Wi-Fi: "); Serial.println(wifiSsid); } void monitorWiFi() { if (WiFi.status() == WL_CONNECTED) { if (!stationConnected) { stationConnected = true; Serial.println(); Serial.println("Wi-Fi connected."); Serial.print("Open browser: http://"); Serial.println(WiFi.localIP()); } return; } if (stationConnected) { stationConnected = false; Serial.println(); Serial.println("Wi-Fi disconnected. AP fallback stays available."); } if (millis() - lastWifiRetryAt >= WIFI_RETRY_INTERVAL_MS) { lastWifiRetryAt = millis(); Serial.println("Retrying Wi-Fi..."); WiFi.begin(wifiSsid, wifiPassword); } } // ===================================================== // Setup / Loop // ===================================================== void setup() { Serial.begin(115200); delay(500); Serial.println(); Serial.println("===================================="); Serial.println("XIAO ESP32-C3 Light API Test"); Serial.println("Local webpage -> ESP32-C3 -> LED strip"); Serial.println("No WebSocket yet."); Serial.println("===================================="); strip.begin(); strip.clear(); strip.setBrightness(255); strip.show(); applyColorFromName(colorName); renderLight(); connectWiFi(); server.on("/", HTTP_GET, handleRoot); server.on("/api/state", HTTP_GET, handleState); server.on("/api/on", HTTP_GET, handleOn); server.on("/api/off", HTTP_GET, handleOff); server.on("/api/toggle", HTTP_GET, handleToggle); server.on("/api/brightness", HTTP_GET, handleBrightness); server.on("/api/color", HTTP_GET, handleColor); server.on("/api/scene", HTTP_GET, handleScene); server.on("/api/breath", HTTP_GET, handleBreath); server.on("/api/state", HTTP_OPTIONS, handleOptions); server.on("/api/on", HTTP_OPTIONS, handleOptions); server.on("/api/off", HTTP_OPTIONS, handleOptions); server.on("/api/toggle", HTTP_OPTIONS, handleOptions); server.on("/api/brightness", HTTP_OPTIONS, handleOptions); server.on("/api/color", HTTP_OPTIONS, handleOptions); server.on("/api/scene", HTTP_OPTIONS, handleOptions); server.on("/api/breath", HTTP_OPTIONS, handleOptions); server.onNotFound(handleNotFound); server.begin(); Serial.println("HTTP API server started."); } void loop() { monitorWiFi(); server.handleClient(); if (lightNeedsAnimation() && millis() - lastAnimationFrameAt >= ANIMATION_FRAME_MS) { lastAnimationFrameAt = millis(); renderLight(); } }