So I gave it a quick try after dinner, here is something you can start with and expand on
This is built on top of ESPAsyncWebserver so you need to install that library
test.ino
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
#include "htmlConfig.h"
const char* ssid = "********";
const char* password = "********";
void notFound(AsyncWebServerRequest *request) {
Serial.printf("notFound: %s\n", request->url().c_str());
request->send(404, "text/plain", "Not found");
}
void handleConfig(AsyncWebServerRequest *request) {
int args = request->args();
for (int i = 0; i < args; i++) {
Serial.printf("%s -> %s\n", request->argName(i).c_str(), request->arg(i).c_str());
}
request->send(200);
}
void configPage(AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("text/html");
startResponse(response);
addRange(response, "Range", "range0", 20, 120, 1, 40);
addNumber(response, "Number", "number0", -20, 20, 0);
addCheckbox(response, "Checkbox 0", "ckbox0", true);
addCheckbox(response, "Checkbox 1", "ckbox1", false);
addButton(response, "Button", "btn0", "Click Me!");
addNiceButton(response, "Nicer Button", "btn1", "Arduino");
addRadioButtons(response, "Radios", "radio0", 1, 3, "radio 0", "radio 1", "radio 2");
endResponse(response);
request->send(response);
}
void setup() {
Serial.begin(115200); Serial.println();
WiFi.disconnect();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, configPage);
server.on("/update", HTTP_GET, handleConfig);
server.onNotFound(notFound);
server.begin();
}
void loop() {}
htmlConfig.h
#ifndef _HTMLCONFIG_
#define _HTMLCONFIG_
#include <ESPAsyncWebServer.h>
void startResponse(AsyncResponseStream * response);
void endResponse(AsyncResponseStream * response);
void addRange(AsyncResponseStream * response, const char * label, const char * name, int minV, int maxV, int step, int value);
void addNumber(AsyncResponseStream * response, const char * label, const char * name, int minV, int maxV, int value);
void addCheckbox(AsyncResponseStream * response, const char * label, const char * name, bool checked);
void addButton(AsyncResponseStream * response, const char * label, const char * name, const char * text);
void addNiceButton(AsyncResponseStream * response, const char * label, const char * name, const char * text);
void addRadioButtons(AsyncResponseStream * response, const char * label, const char * name, uint8_t selected, uint8_t count, ... );
#endif
htmlConfig.cpp
#include "htmlConfig.h"
#include <cstdarg>
const char configStart[] PROGMEM = R"--(
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>Configuration</title>
<style>
input,output{display: inline-block; vertical-align: middle;}
th, td{text-align: left; padding: 5px; font-weight: normal;}
.button {
background-color: white;
color: black;
border: 2px solid #377F83;
border-radius: 10px;
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
transition-duration: 0.4s;
cursor: pointer;
}
.button:hover {
background-color: #377F83;
}
</style>
</head>
<body>
<table>
)--";
const char configEnd[] PROGMEM = R"--(
</table>
<script>
function updateValue(N, val) {document.getElementsByName(N)[0].value=val;}
function callback(N, val) {
var req = new XMLHttpRequest();
url = "/update?" + N
if (val != null) url += "=" + val
req.open("GET", url , false);
req.send(null);
}
</script>
)--";
void startResponse(AsyncResponseStream * response) {
response->printf(configStart);
}
void endResponse(AsyncResponseStream * response) {
response->printf(configEnd);
}
void addRange(AsyncResponseStream * response, const char * label, const char * name, int minV, int maxV, int step, int value) {
response->printf("<tr><th>%s</th><th><input type=\"range\" name=\"%s\" min=\"%d\" max=\"%d\" step=\"%d\" value=\"%d\" data-show-value=\"true\" oninput=\"updateValue(this.name + 'V', this.value);\" onchange=\"callback(this.name, this.value);\"><output name=\"%sV\">%d</output></th></tr>",
label, name, minV, maxV, step, value, name, value);
}
void addNumber(AsyncResponseStream * response, const char * label, const char * name, int minV, int maxV, int value) {
response->printf("<tr><th>%s</th><th><input type=\"number\" name=\"%s\" min=\"%d\" max=\"%d\" value=\"%d\" onchange=\"callback(this.name,this.value);\"></th></tr>",
label, name, minV, maxV, value);
}
void addCheckbox(AsyncResponseStream * response, const char * label, const char * name, bool checked) {
response->printf("<tr><th>%s</th><th><input type=\"checkbox\" name=\"%s\" %s onchange=\"callback(this.name, this.checked ? '1' : '0');\"></th></tr>",
label, name, checked ? "checked" : "");
}
void addButton(AsyncResponseStream * response, const char * label, const char * name, const char * text) {
response->printf("<tr><th>%s</th><th><input type=\"button\" name=\"%s\" value=\"%s\" onclick=\"callback(this.name, null);\"></th></tr>",
label, name, text);
}
void addNiceButton(AsyncResponseStream * response, const char * label, const char * name, const char * text) {
response->printf("<tr><th>%s</th><th><button class=\"button\" name=\"%s\" onclick=\"callback(this.name, null);\">%s</button></th></tr>",
label, name, text);
}
void addRadioButtons(AsyncResponseStream * response, const char * label, const char * name, uint8_t selected, uint8_t count, ... ) { // would be better with template and recursive variadic function
va_list vl;
va_start(vl, count);
response->printf("<tr><th>%s</th><th>", label);
for (uint8_t i = 0; i < count; i++) {
const char * radioItem = va_arg(vl, const char*);
response->printf("<div><input type=\"radio\" name=\"%s\" value=\"%u\" %s onchange=\"callback(this.name,this.value);\">%s</div>\n",
name, i, (i == selected) ? "checked" : "", radioItem);
}
response->printf("</th></tr>");
}
You don't have to do anything with the .cpp and .h file, just make sure the .h is included in your .ino ➜ They are utility functions (of course both files needs to be in your sketch’s directory. feel free to make these utility functions into a library then a simple #include <htmlConfig.h> would bring you the capability).
The way it works is that you implement a page call back that will paint a new page in your web browser, the standard way for the ESPAsyncWebserver library.
here for example you'll see that I'm saying
server.on("/", HTTP_GET, configPage);
➜ if a browser request the home page (/) then the configPage() function is called. It's the responsibility of that function to provide an HTML answer that will be used to draw the page.
In the implementation of that function you have access to helper functions
the body of that function needs to look like this
void configPage(AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("text/html");
startResponse(response);
// HERE YOU ADD YOUR ELEMENTS
endResponse(response);
request->send(response);
}
so what elements can you add ?
I've implemented:
- range (a slider with min, max, step) ➜
addRange()
- number (a chooser for a number between min and max) ➜ `addNumber()
- check boxe (with the option to be checked or unchecked at start) ➜ `addCheckbox()
- two types of buttons (one very plain and one that looks better) ➜
addButton() and addNiceButton()
- radio buttons (as a column of choices) ➜
addRadioButtons()
hopefully the parameters of those functions are self explanatory and entering this description
In the code I have
void configPage(AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("text/html");
startResponse(response);
addRange(response, "Range", "range0", 20, 120, 1, 40);
addNumber(response, "Number", "number0", -20, 20, 0);
addCheckbox(response, "Checkbox 0", "ckbox0", true);
addCheckbox(response, "Checkbox 1", "ckbox1", false);
addButton(response, "Button", "btn0", "Click Me!");
addNiceButton(response, "Nicer Button", "btn1", "Arduino");
addRadioButtons(response, "Radios", "radio0", 1, 3, "radio 0", "radio 1", "radio 2");
endResponse(response);
request->send(response);
}
Which draws that page:
So that's the "page design" phase. How does the user interaction work ?
All items you add on the page will trigger an HTTP GET upon change for /update with a parameter ➜ so if you declare a call back for /update like this
server.on("/update", HTTP_GET, handleConfig);
then the handleConfig() function is called.
That's the function where you need to implement you business logic ( based on the parameters you get for /update you decide what to do).
you can see I provided a standard function for this
void handleConfig(AsyncWebServerRequest *request) {
int args = request->args();
for (int i = 0; i < args; i++) {
Serial.printf("%s -> %s\n", request->argName(i).c_str(), request->arg(i).c_str());
}
request->send(200);
}
this only prints the parameters you get when the function is called. what you get is the name of your widget and if there is a value attached you get the selected value as well.
for example:
For the range, if you move the cursor to 76 the function will print range0 -> 76
For the number, if you go up one notch, the function will print number0 -> 1
For the first checkbox, if you unselect it, the function will print ckbox0 -> 0
For the second checkbox, if you select it, the function will print ckbox1 -> 1
for the plain button, if you click it, the function will print btn0 -> (there is no parameter)
for the nice button, if you click it, the function will print btn1 -> (there is no parameter)
last but not least, for the radio buttons, when you select one you get its position in the list (starting at 0). for example if you select "radio 1" you'll see radio0 -> 1
so, whilst I did not push it all the way to have a structure describing the page with the call backs, you have the tools to build that yourself if you want. At the moment to get the call back you would just have to do (as there is only one arg)
void handleConfig2(AsyncWebServerRequest *request) {
if (strcmp(request->argName(0).c_str(), "range0") == 0) {
// the new value is in string you can access with request->arg(i).c_str()
} else if (strcmp(request->argName(0).c_str(), "number0") == 0) {
// the new value is in string you can access with request->arg(i).c_str()
} else if (strcmp(request->argName(0).c_str(), "ckbox0") == 0) {
// the status is in string you can access with request->arg(i).c_str(). "1" is checked, "0" is uncheked
} else if (strcmp(request->argName(0).c_str(), "ckbox1") == 0) {
// the status is in string you can access with request->arg(i).c_str(). "1" is checked, "0" is uncheked
} else if (strcmp(request->argName(0).c_str(), "btn0") == 0) {
// there is no parameter, the button has been pressed
} else if (strcmp(request->argName(0).c_str(), "btn1") == 0) {
// there is no parameter, the button has been pressed
} else if (strcmp(request->argName(0).c_str(), "radio0") == 0) {
// the selected item index (starting at 0) is in string you can access with request->arg(i).c_str().
} else {
// unknown
}
request->send(200); // don't forget to return 200 to the web browser to say all went well
}
hope this make sense and helps a bit.
PS/ make sure to use different names for all the widgets.