// WLED-Compatible ESP32 LED Wall Controller
// 9x16 LED Matrix with Serpentine (Snaking) Layout
// Compatible with WLED mobile app via HTTP JSON API
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>
// LED Configuration
#define LED_PIN 3
#define LED_COUNT 16 // Testing with 16 LEDs first (will be 144 for full 9x16)
#define MATRIX_WIDTH 4 // columns (testing - will be 9)
#define MATRIX_HEIGHT 4 // rows (testing - will be 16)
// WiFi Configuration
const char* ap_ssid = "WLED-LED-Wall";
const char* ap_password = "wled1234";
// LED State
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
AsyncWebServer server(80);
// WLED State Variables
bool ledOn = true;
uint8_t brightness = 128;
uint8_t mainColor[3] = {255, 160, 0}; // Default orange
uint8_t effect = 0; // 0=solid, 1=rainbow, 2=chase, 3=fade
uint8_t effectSpeed = 128;
uint8_t effectIntensity = 128;
// Animation variables
unsigned long lastUpdate = 0;
uint16_t animationStep = 0;
// Matrix mapping: converts X,Y coordinates to LED index with serpentine layout
// Start: bottom-left, snake up column 0, down column 1, up column 2, etc.
uint16_t XY(uint8_t x, uint8_t y) {
if (x >= MATRIX_WIDTH || y >= MATRIX_HEIGHT) return 0;
uint16_t index;
if (x % 2 == 0) {
// Even columns: bottom to top (0 to 15)
index = x * MATRIX_HEIGHT + (MATRIX_HEIGHT - 1 - y);
} else {
// Odd columns: top to bottom (15 to 0)
index = x * MATRIX_HEIGHT + y;
}
return index;
}
void setup() {
Serial.begin(115200);
Serial.println("\n\nWLED-Compatible LED Wall Starting...");
// Initialize LED strip
strip.begin();
strip.setBrightness(brightness);
strip.show();
// Start WiFi Access Point
Serial.println("Starting WiFi Access Point...");
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
Serial.println("Connect to WiFi: " + String(ap_ssid));
Serial.println("Password: " + String(ap_password));
// Setup web server routes
setupWebServer();
server.begin();
Serial.println("HTTP server started on http://" + IP.toString());
Serial.println("\nReady! Open WLED app and connect to: " + IP.toString());
// Show startup pattern
startupAnimation();
}
void loop() {
// Update LED effects
updateLEDs();
delay(10);
}
void setupWebServer() {
// Root page - basic info
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = "<!DOCTYPE html><html><head><title>WLED LED Wall</title></head><body>";
html += "<h1>WLED-Compatible LED Wall</h1>";
html += "<p>9x16 LED Matrix (144 LEDs)</p>";
html += "<p>Use WLED app to control!</p>";
html += "<p><a href='/json/state'>View State (JSON)</a></p>";
html += "</body></html>";
request->send(200, "text/html", html);
});
// WLED JSON API - GET state
server.on("/json/state", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<512> doc;
doc["on"] = ledOn;
doc["bri"] = brightness;
JsonArray col = doc.createNestedArray("col");
JsonArray rgb = col.createNestedArray();
rgb.add(mainColor[0]);
rgb.add(mainColor[1]);
rgb.add(mainColor[2]);
doc["fx"] = effect;
doc["sx"] = effectSpeed;
doc["ix"] = effectIntensity;
JsonObject info = doc.createNestedObject("info");
info["leds"]["count"] = LED_COUNT;
info["name"] = "LED Wall";
info["ver"] = "1.0.0";
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// WLED JSON API - POST state (control)
server.on("/json/state", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL,
[](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
StaticJsonDocument<512> doc;
DeserializationError error = deserializeJson(doc, data);
if (!error) {
if (doc.containsKey("on")) ledOn = doc["on"];
if (doc.containsKey("bri")) {
brightness = doc["bri"];
strip.setBrightness(brightness);
}
if (doc.containsKey("col")) {
JsonArray col = doc["col"][0];
if (col.size() >= 3) {
mainColor[0] = col[0];
mainColor[1] = col[1];
mainColor[2] = col[2];
}
}
if (doc.containsKey("fx")) effect = doc["fx"];
if (doc.containsKey("sx")) effectSpeed = doc["sx"];
if (doc.containsKey("ix")) effectIntensity = doc["ix"];
Serial.printf("State updated: ON=%d BRI=%d RGB=(%d,%d,%d) FX=%d\n",
ledOn, brightness, mainColor[0], mainColor[1], mainColor[2], effect);
}
request->send(200, "application/json", "{\"success\":true}");
});
// WLED JSON API - GET info
server.on("/json/info", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<512> doc;
doc["ver"] = "1.0.0";
doc["name"] = "LED Wall";
JsonObject leds = doc.createNestedObject("leds");
leds["count"] = LED_COUNT;
leds["matrix"]["w"] = MATRIX_WIDTH;
leds["matrix"]["h"] = MATRIX_HEIGHT;
doc["wifi"]["ssid"] = ap_ssid;
doc["wifi"]["signal"] = 100;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// Simple control endpoints
server.on("/win", HTTP_GET, [](AsyncWebServerRequest *request) {
if (request->hasParam("T")) {
ledOn = request->getParam("T")->value() == "1";
}
if (request->hasParam("A")) {
brightness = request->getParam("A")->value().toInt();
strip.setBrightness(brightness);
}
if (request->hasParam("FX")) {
effect = request->getParam("FX")->value().toInt();
}
request->send(200, "text/plain", "OK");
});
}
void updateLEDs() {
if (!ledOn) {
strip.clear();
strip.show();
return;
}
unsigned long now = millis();
uint8_t speed = map(effectSpeed, 0, 255, 50, 1);
if (now - lastUpdate < speed) return;
lastUpdate = now;
switch (effect) {
case 0: // Solid color
solidColor();
break;
case 1: // Rainbow
rainbow();
break;
case 2: // Chase
chase();
break;
case 3: // Fade
fade();
break;
default:
solidColor();
}
strip.show();
animationStep++;
}
void solidColor() {
uint32_t color = strip.Color(mainColor[0], mainColor[1], mainColor[2]);
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, color);
}
}
void rainbow() {
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
uint16_t hue = (animationStep * 256 + (x * 65536 / MATRIX_WIDTH)) % 65536;
uint32_t color = strip.gamma32(strip.ColorHSV(hue));
strip.setPixelColor(XY(x, y), color);
}
}
}
void chase() {
strip.clear();
int pos = animationStep % LED_COUNT;
uint32_t color = strip.Color(mainColor[0], mainColor[1], mainColor[2]);
for (int i = 0; i < 3; i++) {
int idx = (pos + i * (LED_COUNT / 3)) % LED_COUNT;
strip.setPixelColor(idx, color);
}
}
void fade() {
uint8_t wave = (sin(animationStep * 0.1) + 1) * 127;
uint32_t color = strip.Color(
(mainColor[0] * wave) / 255,
(mainColor[1] * wave) / 255,
(mainColor[2] * wave) / 255
);
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, color);
}
}
void startupAnimation() {
Serial.println("Starting LED test animation...");
// Light up each LED one by one to verify wiring
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, strip.Color(0, 50, 255));
strip.show();
Serial.printf("LED %d lit\n", i);
delay(50);
}
delay(500);
// Quick column-by-column test
strip.clear();
strip.show();
for (int x = 0; x < MATRIX_WIDTH; x++) {
Serial.printf("Lighting column %d\n", x);
for (int y = 0; y < MATRIX_HEIGHT; y++) {
uint16_t idx = XY(x, y);
Serial.printf(" XY(%d,%d) = LED %d\n", x, y, idx);
strip.setPixelColor(idx, strip.Color(255, 0, 0));
}
strip.show();
delay(300);
}
delay(500);
strip.clear();
strip.show();
Serial.println("Startup animation complete!");
}