Implementing a web server to run on an Arduino using the standard WiFi library involves dealing with all the details of the HTTP protocol (described here). This library class does most of the work for you. You just need to supply the HTML for the webpage, or pages the server is to serve and define functions to handle browser requests generated from those pages by submitting forms, etc.
Using this library, web servers are constructed using the "custom_server" class. The following program uses it to define a trivial "Test Server" given the identity (ssid) of a network access point and a password for authentication purposes. The server will identify itself on the network as "Test Server".
#include "custom_server.hpp"
String ssid = "some access point";
String password = "some password";
custom_server* server;
void setup()
{
Serial.begin(115200);
delay(2000);
server = new custom_server();
server->start("Test Server", ssid, password);
}
void loop()
{
}
The loop function is not used as the call to start the server does not terminate. If a serial terminal is connected to the server it will display something similar to the following when it starts running. This program is provided with the library examples as "test_server".
Connecting to Network ... Network connected Server URL: http://192.168.0.49
The number of dots following the "Connecting to Network" message will depend on how many attempts are required to connect to the access point. Pointing a browser at the URL provided for the server will produce a web page with the following HTML
<!DOCTYPE html>
<html>
<head>
<title>Test Server</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<h2>Test Server</h2>
</body>
</html>
This default web page can be used to check that access point connection details are correct and that the hardware is working, but is otherwise not very useful. The next stage is therefore to customise the default web server to generate a more useful page. The following example defines a web page which can be used to turn an LED connected to the server on and off. The facilities demonstrated in this example can easily be extended to controlling and displaying data from more complex systems.
The custom_server class defines a number of virtual functions (described here) which can be overridden in a class derived from it. One of these is "send_content" and it is called whenever the server needs to send an HTML response to a browser request. In the following class definition, the default send_content function is overridden to send a more complex page. Note: For those that have not come across it before, to make the HTML easier to read, this example uses the ability of all major C++ compilers to automatically concatenate adjacent string literals.
#include "custom_server.hpp"
class led_server: public custom_server
{
public:
led_server(): custom_server()
{
}
protected:
void send_content(WiFiClient& client)
{
send_standard_header(client);
client.println
(
"<!DOCTYPE html>"
"<html>"
" <head>"
" <title>LED Server</title>"
" <meta name='viewport' content='width=device-width, initial-scale=1'>"
" </head>"
" <body>"
" <h2>LED Server</h2>"
" <form name='on_off' method='post' action='/'>"
" <button type='submit' name='action' value='off'>Off</button>"
" <button type='submit' name='action' value='on'>On</button>"
" <button type='submit' name='action' value='toggle'>Toggle</button>"
" </form>"
" </body>"
"</html>"
);
}
};
The WiFiClient parameter defined by the standard WiFI library is used to send data back to the browser in response to an HTTP request. The call to send_standard_header prefixes the HTML with the standard response header required by the HTTP protocol. This derived class can be created in a new tab called "led_server.hpp" in the Arduino IDE alongside the original program shown above. The three lines in the original program shown on the left below should be changed to those shown on the right.
#include "custom_server.hpp" => #include "led_sever.hpp"
server = new custom_server(); => server = new led_server();
server->start("Test Server", ssid, password); => server->start("LED Server", ssid, password);
When the resulting server is run, a browser pointed at the server URL will display the page shown below.
As it stands, the buttons will not do anything, but appropriate operations can be implemented by overriding other virtual functions in the custom_server class, but first we need to add functions for controlling the LED and for keeping track of its state. The following definitions should be added to the protected section of the led_server class. Note: The "initialise" function is another virtual function defined by the custom_server class and is called once when the server starts, but before a connection is established to the access point.
const int led_pin = LED_BUILTIN;
bool led_on = false;
void initialise()
{
pinMode(led_pin, OUTPUT);
enable_led(false);
}
void toggle_led()
{
led_on = !led_on;
digitalWrite(led_pin, led_on ? HIGH : LOW);
}
void enable_led(bool on)
{
led_on = on;
digitalWrite(led_pin, led_on ? HIGH : LOW);
}
Having done this, we can override the handle_post function defined by the custom_server class as follows.
void handle_post(WiFiClient& client)
{
String action = get_content_parameter("action");
if (action == "on")
enable_led(true);
else if (action == "off")
enable_led(false);
else if (action == "toggle")
toggle_led();
send_content(client);
}
If one of the submit buttons contained in the form described by the HTML is clicked, a POST request to the server is generated by the browser. When the custom_server class receives this, it will process the entire POST request, including reading its content. In this case the content will contain the value of the action parameter associated with the corresponding submit button. The first line in the handle_post function above uses the get_content_parameter function, provided by the custom_server class, to retrieve the value of the action parameter. This will either be off, on or toggle according to which button was clicked. The function uses this value to perform the appropriate action. It then resends the page HTML to the browser. A small modification to the send_content function will allow the modified state of the LED to be reported. Immediately after the line which adds the "LED Server" heading, insert the following.
" <p>The LED is " + (led_on ? String("on") : String("off")) + ".</p>"
With these modifications the server will generate a page which looks like this:-
The buttons will perform the actions indicated by their captions, and the text will be updated to indicate the resulting state of the LED.
One further refinement can be made to suppress an annoying browser behaviour. If the user reloads the page displayed after a form is posted, most browsers will display a dialogue asking if the previous post data should be re-sent. For this application this implies that reloading the page will cause the last button click to be repeated, which is probably not what the user is expecting. In the case of the off and on buttons in this example this behaviour, apart from the annoying dialogue, would not make any difference as the off and on actions are idempotent. However, if the last button clicked was the toggle button, every subsequent reloading of the page would toggle the state of the LED. This undesirable browser behaviour can be suppressed with a simple script.
<script>
document.addEventListener('DOMContentLoaded', function()
{
if (window.history.replaceState)
window.history.replaceState({}, null, window.location.href);
);
</script>
For convenience, the custom_server class provides a function which can send this script to the client simply by inserting the following line in the send_content function immediately before the line which prints the closing </body> tag.
send_prevent_reposting_on_page_refresh_script(client);
This completes the design of a very simple web server to control the state of an LED. It demonstrates how the HTML for a single page is generated, how the state of a variable in the server can be reported to the user and how to handle buttons clicked by the user. This program is provided with the library examples as "led_server".
For more complex applications, different HTML pages can be generated according to the state of the server or parameters supplied to GET or POST requests. By adding JavaScript to the generated HTML, AJAX requests can be handled and the send_content function can be used to return content for parts of a page rather than an entire page.
These functions can be overridden to further customise the web server. To augment rather than replace the default implementations of any of these functions simply call the default version from the overridden version using the format custom_server::function_name.
void virtual initialise()
This is called once by the custom_server::start function before an attempt is made to connect to the specified access point. This can be used to initialise devices and variables used by the server, but not directly involved in HTTP communication, e.g the LED pin settings in the led_server example. The default version does nothing.
void virtual before_connecting()
This is called by the custom_server::start function before the first connection or reconnection attempt. The default version prints a message to the serial device indication that a connection attempt is being made.
void virtual connecting()
This is called by the custom_server::start function after each unsuccessful connection attempt. In general, it will take more than one attempt to establish a connection. The default version simple prints a "." character to the serial device.
void virtual connected(IPAddress ip)
This is called by the custom_server::start function after a connection has been made to the access point. The parameter will contain the IP address allocated to the server. The default version prints this information as a URL to the serial device.
void virtual parse_header_line(String header_line)
Following the initial HTTP command line sent from the client, GET, POST etc., will be a series of header lines containing further information about the request. Each of these lines are formatted as "description: value", e.g. "Content-Type: text/html". This function is called after each header line is read. The default version does nothing, but an overridden version can check the description and retrieve the value if it is of interest. The support functions get_header_line_description and get_header_line_value can be used to parse the header_line parameter (see below). This function will not be called if the header line description is "Content-Length". This case is automatically handled by the custom_server to allow any content following the header lines to be read and stored in the "content" variable. The length of the content will be stored in the content_length variable (see below).
void virtual handle_get(WiFiClient& client)
This is called after a GET request has been received and all of its header lines have been read. The default version simply calls the send_content function, which will usually have been overridden. The source part of the GET request will be stored in the “source” variable, and the get_parameter function can be used to access the values of individual query parameters if they are present (see below).
void virtual handle_post(WiFiClient& client)
This is called after a POST request has been received and all of its header lines and content have been read. The default version simply calls the send_content function, which will usually have been overridden. The source part of the POST request will be stored in the “source” variable, and the get_content_parameter function can be used to access the values of individual POST parameters if they are present in the content (see below).
void virtual handle_head(WiFiClient& client)
This is called after a HEAD request has been received and all of its header lines have been read. The default version simply calls the send_null_response function. The source part of the HEAD request will be stored in the “source” variable, and the get_parameter function can be used to access the values of individual query parameters if they are present (see below).
void virtual handle_put(WiFiClient& client)
This is called after a PUT request has been received and all of its header lines and content have been read. The default version simply calls the send_null_response function. The source part of the PUT request will be stored in the “source” variable, and the get_content_parameter function can be used to access the values of individual PUT parameters if they are present in the content (see below).
void virtual handle_delete(WiFiClient& client)
This is called after a DELETE request has been received and all of its header lines and content, if present, have been read. The default version simply calls the send_null_response function. The source part of the DELETE request will be stored in the “source” variable, and the get_parameter function can be used to access the values of individual DELETE parameters (see below).
void virtual send_content(WiFiClient& client)
This is called whenever the sever needs to send content back to the client, e.g. after a GET or POST request. It will usually be overridden to send HTML for a particular web page, but can be overridden to select different HTML according to the state of the server or the source and query parameters contained in the request. If the host system has access to files, e.g. on an SD card, the content might be read from a file.
String virtual get_content(WiFiClient& client, int content_length)
This will automatically be called when the request header contains an entry of the form “Content-Length: n”, where n is a value greater than zero. In this case, it will read the entire content into a String variable called “content” (see below). Where the content may be large, e.g. the content is to be used to update a file via a POST or PUT request, this function can be overridden to read and process the content incrementally, e.g. transferring to a file one line at a time.
void virtual show_command_information()
This function is provided for debugging purposes. The default version will print to the serial device the values of internal server variables including the command line of each request, the source and the raw query parameter strings. It can be overridden to display other information from a customised server. To augment the default information simply call the default version from an overridden version as shown in the following example.
void show_command_informaton()
{
custom_server::show_command_informaton();
Serial.println("The LED is " + String(led_on ? "on" : "off"));
}
These functions can be used in overriding any of the above virtual functions.
void send_standard_header(WiFiClient& client)
This sends a standard "200 OK" header to the client. Such a header is required before any HTML to be sent to the client by the send_content function.
void send_null_response(WiFiClient& client)
This sends a "204 No Content" response to the client. It can be used to respond to PUT, HEAD or DELETE requests where no content is required in the response.
void send_not_found_response(WiFiClient& client)
This sends a "404 Not Found" response to the client. It can be used to respond to GET requests where the source is not recognised.
void send_prevent_reposting_on_page_refresh_script(WiFiClient& client)
This sends a script that prevents reposting if the user refreshes the current page after a POST request. It should be inserted along with any other scripts before the final HTML </body> tag.
void send_ajax_request_script(WiFiClient& client)
This sends a script that simplifies AJAX requests (described here).
<script>
function send_ajax_request(source, action)
{
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function()
{
if (this.readyState == 4 && this.status == 200) action(this.responseText);
};
xhttp.open('GET', source, true);
xhttp.send();
}
</script>
It should be inserted along with any other scripts before the final HTML </body> tag. The "source" parameter, prefixed by a "/", will be sent to the server as the source component of a GET command. The "action" parameter should be a function which takes a single parameter which will receive the response text sent by the server. The server should override the handle_get function to check the source variable (described below) , or any query parameters, to decide what content to return.
<button type='button' onclick='send_ajax_request("test", do_test)'>Test</button>
If a button defined as above is clicked, a GET request with a source of "/test" will be sent to the server. The content generated by the server in response, via its handle_get function, will be passed to a call of the "do_test" function. A library example called "ajax_server" demonstrates this process in detail.
void send_timer_script(WiFiClient& client)
This function sends a script that allows AJAX requests to be send from a browser at regular intervals. It can be used to provide a continuous display of values available to the server, e.g. sensor readings. It should be inserted along with any other scripts before the final HTML </body> tag. Note: If this script is used, the AJAX request script described above must also be included in the HTML of the page.
<script>
var timer_source;
var timer_action;
var ajax_timer;
function start_timer(source, action, interval)
{
timer_source = source;
timer_action = action;
ajax_timer = window.setInterval(ajax_request, interval);
}
function stop_timer()
{
window.clearInterval(ajax_timer);
}
function ajax_request()
{
send_ajax_request(timer_source, timer_action);
}
</script>
The script defines three functions. The "start_timer" function takes the same parameters as "send_ajax_request" function described above, plus the interval in milliseconds at which the request should be generated. The "stop_timer" function allows the timer to be stopped after it has been started. The "ajax_request" function is used to generate an AJAX request every time the timer ticks.
<button type='button' onclick='start_timer("timer", tick, 1000)'>Start Timer</button>
If a button defined as above is clicked, GET requests with a source of "/timer" will be sent to the server at one second intervals. The content generated by the server in response, via its handle_get function, will be passed to a call of the "tick" function. A library example called "timer_server" demonstrates this process in detail.
<script>
start_timer("timer", tick, 1000);
</script>
The timer can be started when the web page is loaded, without any button being pressed, by including the above script with any others just before the final </body> tag.
String get_header_line_description(String header_line)
String get_header_line_value(String header_line)
These two functions will retrieve the description and value parts of a header line, e.g. if the header line contains "Content-Type: text/html", get_header_line_description will return "Content-Type" and get_header_line_value will return "text/html". They can be used when overriding the parse_header_line function described above to retrieve header information of interest.
String get_parameter(String name)
This will get the value of any query parameter contained in the first command line of a request, e.g. given the command "GET /?action=on&status=none HTTP/1.1" get_parameter("action") will return "on". If the parameter name passed to the function does not exist in the current request, an empty string is returned.
String get_content_parameter(String name)
This function is used to get parameter values when the parameters are part of the content rather than part of the command line, e.g. as is the case with POST requests. If the parameter name passed to the function does not exist in the current content, an empty string is returned. This function will only work when the encryption type associated with the content is enctype='application/x-www-form-urlencoded', which is the default in most cases. If, for example, the encryption type associated with POSTing a form was changed to enctype='text/plain', the result of applying this function would be undefined. For non-default encryption types parameter selection must be managed by overriding the get_content function described above or by accessing the "content" variable described below.
String decode_url(String encoded)
This function will decode a url encoded string into plain text, e.g. decode_url("The+answer+is+a+%2B+b") will produce "The answer is a + b". It can be used in conjunction with the two preceding functions when parameter values may contain special characters including spaces.
String encode_url(String plain_text)
This function is the inverse of decode_url. It will url encode a plain text string.
A number of variables are used to store request information by the custom_server class. These can be used in overriding any of the above virtual functions. Note: These are variables which should only be modified if you know what you are doing.
String ssid;
String password;
String server_name;
These three variables provide access to the parameters passed to the custom_server::start function.
String command;
String source;
String parameters;
String content;
These variables provide access to the components of the current request, e.g. if the current request is "GET /?action=on&status=none HTTP/1.1", command would contain "GET /?action=on&status=none HTTP/1.1", source would contain "/", parameters would contain "action=on&status=none" and content would contain the empty string.
The example programs included with the library demonstrate some of its capabilities, and the techniques demonstrated could be combined to form a basis for more complex servers.
test_server
This implements the default web server supported by the custom_server class without overriding any of its virtual methods. It is useful for checking that everything is working correctly with your hardware and network connection. All that is necessary to turn the example into a working server, is to fill in your network access point SSID and password. The URL you should enter into your browser will be printed to the serial device. If everything is working correctly, a web page containing the heading "Test Server" will be displayed.
led_server
This implements a server which allows an LED connected to the server system to be switched off or on, or to have its current state toggled. You need to fill in your network access point SSID and password. This demonstrates a basic technique for controlling network connected hardware. It uses a POST request with a form that contains three submit buttons. The ajax_server (see below) demonstrates how to use buttons to generate GET requests without a form.
form_server
This implements a server which displays a simple form. You need to fill in your network access point SSID and password. Entering data into the two fields on the form and then clicking the submit button will cause the server to print the entered data on the serial device. This demonstrates a basic technique for transferring data from browser to server by POSTing a form.
ajax_server
This implements a server which updates part of a web page without refreshing the entire page. You need to fill in your network access point SSID and password. This demonstrates basic AJAX (described here) used for allowing a web page to request data from a server. The returned data does not have to be used directly to update part of a page, It might be used in other ways by scripts defined on the page.
timer_server
This implements a server which updates part of a web page, at regular intervals, using data from the server. You need to fill in your network access point SSID and password. This demonstrates a basic technique for displaying changing server side data on a web page, e.g. sensor readings. To keep the example simple, the internal server data displayed is just the value of the Arduino "millis" function.
multi_page_server
This implements a server which shows a page containing four links. You need to fill in your network access point SSID and password. Clicking any of first three links on the page causes the server to display different pages. Clicking the fourth link causes the browser to display an external page. This demonstrates that a browser can follow an external link on a page generated by the server without the server being involved. This also allows other resources from external sources to be incorporated into pages generated by the server, e.g. images, without having to store them on the server.
It is assumed that the reader/user has a basic knowledge of HTML and perhaps a little JavaScript, although the latter is less important as the library can generate some useful scripts for you.
To install the library, download a zip file containing the library files from GitHub. Either expand the archive and copy the top-level folder into the Arduino IDE libraries folder, or let the IDE decompress and copy the library files for you.
This library has been tested on a number of ESP32 boards including Aduino Nano ESP32, ESP32 Dev Module and HelTec WiFi Kit 32(V3). It will not work on ESP8266 boards where the "standard" WiFi library uses different names and parameters for some functions, and using conditional compilation to overcome this has a bad smell in the 21st century. A separate version for older network capable systems could solve this.
The custom_server source is contained in a single .hpp C++ header file, because it contains a C++ class definition, not a .h C header file. As demonstrated in the examples, derived classes should also be stored in .hpp files.
Most Arduino libraries are not really "object orientated" but simply implement objects which can be manipulated via their public interfaces. Apart from its constructor, the custom_server class only has a single public function used to "start" the server. All of the magic happens as a result of overriding protected virtual methods.
The main program in all of the examples dynamically allocates an instance of the required server class. This is consistent with most other object oriented languages, where classes are reference types. Given that a custom_server, or a derived version, never terminates, dynamic allocation does not lead to heap fragmentation as the server is never deallocated. This is also why the custom_server class has a constructor but no destructor. In addition, ESP32 systems tend to have respectable amounts of memory so that the heap can be used as nature intended.
In addition to this document the custom_server class contains numerous helpful comments in the form of "meaningful identifiers".
Program layout follows the recommendations of psychologists, who have carried out scientific studies of the way the human brain absorbs information from formal textual descriptions, and not the current trend to follow mere personal preference or fashion.
(The GitHub project can be found here)