15. Interface & Application Programming¶
Controlling something through an interface. Per Andri’s suggestion I decided to keep things simple for now. I’m using a Xiao ESP32S3 and the board I made in Assignment 9. Electronics Production.
After meeting with Steven I also overhauled it and made a different version using NodeJS running a server on my laptop rather than running the webserver directly on the ESP.
Group Assignment¶
Second Hero Shot¶
Hero Shot¶
Re-Do¶
I had to re-do this assignment as running the web server on the ESP itself wasn’t enough for the assignment. So I switched it over to running on my laptop through Node.JS. I used ChatGPT for most of the coding here but it proved surprisingly unhelpful. I’ve never had an argument with a language model before but in the end it worked flawlessly. The rough idea is using one of the boards from Networking & Communications and an ESP32C3. The board has a button and an LED. I want to control the state of the LED through a web interface and I want to control the interface using the board. The second hero shot above shows the function well.
Here is the chatGPT prompt history. It’s long and convoluted.
There’s three important pieces of code:
- index.html displays a big button that controls the LED and displays the status of the button. Uses a websocket connection to send the LED state and receive button states.
⌨️ show / hide index.html
<!DOCTYPE html>
<html><head>
<meta charset="utf-8" />
<title>ESP32C3 WS Dashboard</title>
<style>
body{font-family:Consolas;text-align:center;margin-top:3rem}
button{padding:1rem 3rem;font-size:1.2rem;border:0;border-radius:.5rem;background:#333;color:#fff;cursor:pointer}
button.on{background:#20d11a}
</style>
</head><body>
<h1>Wi-Fi Blinky (WS)</h1>
<p><button id="ledBtn">LED OFF</button></p>
<p>Button on board is: <span id="boardBtn">--</span></p>
<script>
const ws = new WebSocket(`ws://${location.host}`);
let ledState = 0;
ws.onmessage = ev => {
const msg = ev.data;
console.log('‹', msg);
if (msg.startsWith('led:')) {
ledState = msg.split(':')[1] === '1' ? 1 : 0;
const btn = document.getElementById('ledBtn');
btn.textContent = ledState ? 'LED ON' : 'LED OFF';
btn.classList.toggle('on', !!ledState);
}
if (msg.startsWith('btnState:')) {
const state = msg.split(':')[1].toUpperCase();
document.getElementById('boardBtn').textContent = state;
}
};
// local click also drives the board
document.getElementById('ledBtn').onclick = () => {
ledState ^= 1;
ws.send('led:' + ledState);
};
</script>
</body></html>
- server.js Runs an HTTP server for the web page and a WebSocket server that forwards the LED and button messages between the browser and the ESP32C3.
⌨️ show / hide server.js
// server.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
let boardSocket = null;
let ledState = 0;
wss.on('connection', ws => {
ws.on('message', data => {
const msg = data.toString();
console.log('‹', msg);
// Board identification
if (msg === 'identify:board') {
boardSocket = ws;
console.log('✅ Board registered');
return;
}
// From board → broadcast to browsers
if (ws === boardSocket && msg.startsWith('led:')) {
ledState = msg.split(':')[1] === '1' ? 1 : 0;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
return;
}
// From browser → forward to board and broadcast to all dashboards
if (msg.startsWith('led:')) {
// update internal state
ledState = msg.split(':')[1] === '1' ? 1 : 0;
// forward to the board
if (boardSocket && boardSocket.readyState === WebSocket.OPEN) {
boardSocket.send(msg);
}
// broadcast to every dashboard (including the one that clicked)
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
return;
}
// From board button press → browser update
if (ws === boardSocket && msg.startsWith('btnState:')) {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
return;
}
});
ws.on('close', () => {
if (ws === boardSocket) {
console.log('⚠️ Board disconnected');
boardSocket = null;
}
});
});
// serve static UI from ./public
app.use(express.static('public'));
server.listen(3000, () => {
console.log('HTTP+WS server on http://localhost:3000');
});
- new.ino Connects the ESP32C3 to Wi-Fi. Opens a websocket to the server, Turns the led on or off when it gets inputs from the server and outputs the button state when button is pressed.
⌨️ show / hide new.ino
#include <WiFi.h>
#include <ArduinoWebsockets.h>
using namespace websockets;
// — your Wi-Fi credentials —
const char* WIFI_SSID = "SSID";
const char* WIFI_PASSWORD = "PASSWORD";
// — your WS server —
const char* WS_HOST = "Host IP Address";
const uint16_t WS_PORT = 3000;
WebsocketsClient ws;
// — XIAO_ESP32_C3 pins —
const uint8_t LED_PIN = 8; // onboard LED
const uint8_t BUTTON_PIN = 10; // user button
void setup(){
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
digitalWrite(LED_PIN, LOW);
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED){
Serial.print('.');
delay(500);
}
Serial.println("\nWi-Fi connected!");
ws.onMessage([](WebsocketsMessage msg){
// handle incoming led: commands from server/browser
String s = msg.data();
if (s.startsWith("led:")){
bool on = s.substring(4).toInt() == 1;
digitalWrite(LED_PIN, on ? HIGH : LOW);
Serial.printf("← led set to %d\n", on);
}
});
String url = String("ws://") + WS_HOST + ":" + WS_PORT;
bool ok = ws.connect(url);
Serial.printf("WS connect %s\n", ok ? "OK" : "FAIL");
if (ok) {
ws.send("identify:board");
}
}
void loop(){
// keep WS alive
if (!ws.available()){
static uint32_t last = 0;
if (millis() - last > 5000){
last = millis();
Serial.println("Reconnecting WS...");
if (ws.connect(String("ws://") + WS_HOST + ":" + WS_PORT)){
Serial.println(" → reconnected");
ws.send("identify:board");
}
}
}
ws.poll();
// button handling
static bool last = HIGH;
bool curr = digitalRead(BUTTON_PIN);
if (curr != last){
last = curr;
if (curr == LOW){
// pressed: toggle LED and notify
bool ledOn = digitalRead(LED_PIN) == HIGH;
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
// send both button and led
ws.send(String("btnState:") + 1);
ws.send(String("led:") + (ledOn ? 1 : 0));
Serial.printf("→ btnState:1, led:%d\n", ledOn);
} else {
// released
ws.send(String("btnState:") + 0);
Serial.println("→ btnState:0");
}
delay(50); // debounce
}
}
All of this could be transferred to a more “desktop” UI using Python or by turning this into a web-app using something like Electron. But that’d be complete overkill.
First Attempt¶
Research¶
Andri suggested following this Guide which shows how to create a standalone webserver on an ESP32 that controls two LEDs. This fits my use case exactly. I already have the board made and the example code only needed slight modifications to function.
I initially wanted to add more to the code. But I’ve been extremely busy this week. I was working at my second job over the weekend (Never work retail on a payday weekend) so I ended up having to do this week’s assignment while working on-call at the lab.
Here’s some of the things I read up on and considered adding.
- Adding a hostname and custom DNS address
- Using the ESP32S3 Sense Camera
- Using the button I included on the original board to control something in the interface.
- Adding Over-The-Air firmware updates
- Adding a captive portal to input WiFi credentials
-
PWM Brightness control with a slider.
-
Controlling a stepper motor
All of these were interesting rabbit holes to read into but I had to rush and get it done.
The Code¶
I ended up re-writing the example code from the website with minimal changes. I changed the pin variable names and adjusted the colors of the buttons and the font for the page. It took me some time to wrap my head around the idea of printing the HTML/CSS using Client.println
and calling if (header.indexOf"(GET 26/on")
which uses the directories to trigger events on the board.
The only thing I added was a Wi-Fi Signal strength printout using Arduino WiFi.RSSI. Fairly simple stuff. Before displaying the HTML I set an integer equal to Wifi.RSSI then later print the integer whenever one of the buttons is pressed.
I also rounded the buttons. But I did that after I recorded the Hero Shot.
⌨️ show / hide code
/*********
Rui Santos
Complete project details at https://randomnerdtutorials.com
*********/
// Include Wi-Fi Library
#include <WiFi.h>
const char* ssid = "SSID";
const char* password = "PASSWORD";
// Set to standard HTTP port "80"
WiFiServer server(80);
// Variable to store the HTTP request
String header;
//Auxiliary variables to store the current output state
String LED1State = "off";
String LED2State = "off";
String BTNState = "up";
// Assign output variables to GPIO pins
const int LED1 = D0;
const int LED2 = D2;
const int BUTT_PIN = D4;
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds
const long timeoutTime = 2000;
void setup() {
Serial.begin(115200);
//Initialize the LED pins as outputs and the button as an input
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(BUTT_PIN, INPUT);
// set starting outputs to LOW
digitalWrite(LED1, LOW);
digitalWrite(LED2, LOW);
// Connect to a Wi-Fi Network with SSID and password
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() !=WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Print local IP address and start web server
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
void loop(){
WiFiClient client = server.available(); // Listen for incoming clients
if (client) { // If a new client connects,
currentTime = millis();
previousTime = currentTime;
Serial.println("New Client."); // print a message out in the serial port
String currentLine = ""; // make a String to hold incoming data from the client
while (client.connected() && currentTime - previousTime <= timeoutTime) { // loop while the client's connected
currentTime = millis();
if (client.available()) { // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write(c); // print it out the serial monitor
header += c;
if (c == '\n') { // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// turns the GPIOs on and off
if (header.indexOf("GET /26/on") >= 0) {
Serial.println("BLUE LED on");
LED1State = "on";
digitalWrite(LED1, HIGH);
} else if (header.indexOf("GET /26/off") >= 0) {
Serial.println("BLUE LED off");
LED1State = "off";
digitalWrite(LED1, LOW);
} else if (header.indexOf("GET /27/on") >= 0) {
Serial.println("RED LED on");
LED2State = "on";
digitalWrite(LED2, HIGH);
} else if (header.indexOf("GET /27/off") >= 0) {
Serial.println("GPIO 27 off");
LED2State = "off";
digitalWrite(LED2, LOW);
}
// Get Wi-Fi Signal Strength
long signalStrength = WiFi.RSSI();
// Display the HTML web page
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// CSS to style the on/off buttons
// Feel free to change the background-color and font-size attributes to fit your preferences
client.println("<style>html { font-family: Consolas; display: inline-block; margin: 0px auto; text-align: center;}");
client.println(".button { background-color:rgb(51, 51, 51); border: none; border-radius: 5px; color: white; padding: 16px 40px;");
client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
client.println(".button2 {background-color:rgb(32, 209, 26);}</style></head>");
// Web Page Heading
client.println("<body><h1>Blinky Thingy. Network edition!</h1>");
// Display current state, and ON/OFF buttons for BLUE LED
client.println("<p>Blue LED is: " + LED1State + "</p>");
// If the LED1State is off, it displays the ON button
if (LED1State=="off") {
client.println("<p><a href=\"/26/on\"><button class=\"button\">ON</button></a></p>");
} else {
client.println("<p><a href=\"/26/off\"><button class=\"button button2\">OFF</button></a></p>");
}
// Display current state, and ON/OFF buttons for RED LED
client.println("<p>Red LED is: " + LED2State + "</p>");
// If the LED2State is off, it displays the ON button
if (LED2State=="off") {
client.println("<p><a href=\"/27/on\"><button class=\"button\">ON</button></a></p>");
} else {
client.println("<p><a href=\"/27/off\"><button class=\"button button2\">OFF</button></a></p>");
}
// Display Wi-Fi signal strength
client.println("<div class=\"signal\">Signal Strength: " + String(signalStrength) + " dBm</div>");
client.println("</body></html>");
// The HTTP response ends with another blank line
client.println();
// Break out of the while loop
break;
} else { // if you got a newline, then clear currentLine
currentLine = "";
}
} else if (c != '\r') { // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
}
}
// Clear the header variable
header = "";
// Close the connection
client.stop();
Serial.println("Client disconnected.");
Serial.println("");
}
}
Not a lot to write about this week as it’s mostly code. It was good to get a rough refresh on HTML and I particularly liked playing around with the Wi-Fi Strength readout. I have two Access points at home and I was able to compare the signal between them using my laptop positioned in different rooms. I was surprised that the AP that I thought was more blocked to the living room turned out to have a stronger signal than the other. Fun stuff.