15. Interface and Application Programming¶
Overview¶
This week, I created a web-based controller for my 4×4 LED matrix from my outputs week connected to a Xiao ESP32-C3. The goal was to build a webpage that displays a grid of 16 buttons that correspond directly to the LEDs on my hardware. When a user presses one of the buttons, the matching LED on the matrix lights up and all others turn off.
UI Design¶
The user interface is built in plain HTML, CSS, and JavaScript and is served directly from the ESP32-C3’s internal web server.
Layout¶
- The page contains a 4×4 grid of buttons arranged to match the physical LED matrix on the PCB.
- Each button starts red (off state) and turns green (active state) when clicked.
- Only one button can be green at a time — representing that only one LED can be lit.
-
Two control buttons sit above the grid:
-
Turn All On – lights every LED on the board.
- Turn All Off – clears the entire matrix.
How It Was Made¶
- The grid is generated dynamically in the
handleRoot()function on the ESP32-C3:
```cpp
``` * Each button calls a small JavaScript function when clicked:
js
function send(x, y) {
fetch('/press?row=' + x + '&col=' + y).then(() => location.reload());
}
This function sends a request to the ESP32-C3 with the coordinates of the pressed button. * CSS is used to color and arrange the buttons:
css
.grid {
display: grid;
grid-template-columns: repeat(4, 60px);
gap: 10px;
}
.btn { background-color: red; }
.btn.active { background-color: green; }
Result¶
The webpage is simple, relativley responsive, and easy to use.

Communication Between the Web App and the Microcontroller¶
1. Web Server Role¶
The ESP32-C3 acts as both server and controller:
- It hosts the HTML interface.
- It listens for HTTP requests when a user interacts with the webpage.
2. Data Exchange¶
When a user clicks a button:
- JavaScript sends a
GETrequest such as
/press?row=2&col=3
to the ESP32-C3’s local IP address.
2. The ESP32-C3 receives the request through the WebServer library.
3. The handler function handlePress() extracts the row and column numbers:
cpp
int row = server.arg("row").toInt();
int col = server.arg("col").toInt();
lightLED(row, col);
4. The lightLED() function sets the appropriate row pin LOW (cathode active)
and column pin HIGH (anode active) to light the selected LED.
5. The webpage reloads, updating the grid to show the selected button as green.
3. “Turn All On/Off” Commands¶
The control buttons send similar requests:
/control?action=on→ turns on all LEDs/control?action=off→ clears all LEDs
4. Feedback Cycle¶
The webpage reflects the physical state of the matrix through CSS updates — meaning that what you see on the browser always matches the actual LED pattern on the board.
Code¶
#include <WiFi.h>
#include <WebServer.h>
// Replace with your network credentials
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
// Define pins for rows (cathodes) and columns (anodes)
const int rows[4] = {D1, D2, D3, D4};
const int cols[4] = {D7, D8, D9, D10};
// Current LED state
int activeRow = -1;
int activeCol = -1;
bool allOn = false;
// Web server on port 80
WebServer server(80);
void setupMatrixPins() {
for (int i = 0; i < 4; i++) {
pinMode(rows[i], OUTPUT);
pinMode(cols[i], OUTPUT);
digitalWrite(rows[i], HIGH); // Rows HIGH = OFF
digitalWrite(cols[i], LOW); // Columns LOW = OFF
}
}
void clearMatrix() {
for (int i = 0; i < 4; i++) {
digitalWrite(rows[i], HIGH);
digitalWrite(cols[i], LOW);
}
activeRow = -1;
activeCol = -1;
}
void lightLED(int row, int col) {
clearMatrix();
digitalWrite(cols[col], HIGH);
digitalWrite(rows[row], LOW);
activeRow = row;
activeCol = col;
}
void lightAll() {
clearMatrix();
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
digitalWrite(cols[c], HIGH);
digitalWrite(rows[r], LOW);
delay(1); // brief delay to allow all LEDs to blink visibly
digitalWrite(cols[c], LOW);
digitalWrite(rows[r], HIGH);
}
}
allOn = true;
}
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html><html><head>
<style>
body { font-family: Arial; text-align: center; }
.grid { display: grid; grid-template-columns: repeat(4, 60px); gap: 10px; justify-content: center; margin-top: 20px; }
.btn { width: 60px; height: 60px; background-color: red; border: none; color: white; font-size: 20px; }
.btn.active { background-color: green; }
.control { margin: 20px; }
</style>
<script>
function send(x, y) {
fetch('/press?row=' + x + '&col=' + y).then(() => location.reload());
}
function control(action) {
fetch('/control?action=' + action).then(() => location.reload());
}
</script></head><body>
<h1>4x4 LED Matrix Controller</h1>
<div class="control">
<button onclick="control('on')">Turn All On</button>
<button onclick="control('off')">Turn All Off</button>
</div>
<div class="grid">
)rawliteral";
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
bool isActive = (r == activeRow && c == activeCol);
html += "<button class='btn" + String(isActive ? " active" : "") + "' onclick='send(" + r + "," + c + ")'></button>";
}
}
html += R"rawliteral(
</div></body></html>
)rawliteral";
server.send(200, "text/html", html);
}
void handlePress() {
if (server.hasArg("row") && server.hasArg("col")) {
int row = server.arg("row").toInt();
int col = server.arg("col").toInt();
lightLED(row, col);
allOn = false;
}
server.sendHeader("Location", "/");
server.send(303);
}
void handleControl() {
if (server.hasArg("action")) {
String action = server.arg("action");
if (action == "on") {
lightAll();
} else if (action == "off") {
clearMatrix();
allOn = false;
}
}
server.sendHeader("Location", "/");
server.send(303);
}
void setup() {
Serial.begin(115200);
setupMatrixPins();
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.println(" connected!");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/press", handlePress);
server.on("/control", handleControl);
server.begin();
}
void loop() {
server.handleClient();
// Keep "all on" visually blinking
if (allOn) {
lightAll();
}
}
