Arduino web server

Chapters

See content:

The first question could be “What is a web server?“. A web server is an application designed to receive HTTP requests from a client, process them and send a response to the client, using the HTTP protocol. Usually, the reponses are HTML pages, XML content, JSON objects, CSS stylesheets, etc. In this course we will serve an HTML page with buttons to control outputs, text boxes to send data through RS-485, texts with input values, and dynamic content using a little of Javascript.

TODO: test the Arduino device and the ethernet hardware.

The “Hello world” sketch is the base of all the examples and it replies an static HTML content to the browser when it requests for a web page.

    1. Create the sketch

Open the Arduino IDE application and select “File” in the menu bar and then click “New” to create a new sketch. A new Arduino IDE windows is opened with both setup and loop functions. You can click “File” and “Save as…” option to set a more descriptive sketch name, i.e. WebServer.

Notice that the Arduino IDE creates a folder called “WebServer” with the “WebServer.ino” file inside.

    1. Include the Ethernet library

First of all it is necessary to include the Ethernet library to use the Ethernet hardware as the communication port. The Ethernet library is included for WIZNET W5100 based hardware -the Ethernet shield- and the Ethernet2 library is included for WIZNET W5500 based hardware -the Ethernet2 shield-. It is also possible to use other libraries and hardware, like WIFI and WIFI101 libraries, but the code should be adapted to the specific library.

In this case, the Ethernet2 library is used.

#include <Ethernet2.h>
    1. Init the Ethernet hardware

To initialize the Ethernet hardware it is called the Ethernet.begin() function into the setup(). It requires, at least, the device MAC address as first argument. It also accepts an IP address, the DNS server address, the netmask and the gateway address.

When the IP address is not used, the library gets an IP address using the DHCP protocol, but in this case, as the sketch implements a web server, it is common to fix an static IP address. In this case the IP address is 192.168.1.2.

It is not necessary to set a DNS server due to the sketch is not trying to connect to another server by name. It is not necessary to set the netmask and the gateway either since they are defined automatically from the IP address.

byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
byte ip[] = {192, 168, 1, 2};

Ethernet.begin(mac, ip);
    1. Create an EthernetServer object

The component used to listen for HTTP requests is the EthernetServer object. As the server is an HTTP server it is used the port 80. It is possible to use another port, but the most common is this one. Add the next line just before the setup().

EthernetServer server(80);
    1. Start the server

After the server creation and the Ethernet hardware initialization it is important to remember the initialization of the server using the server.begin() function into the setup(). It prepares the server for listening for incoming connections from clients.

server.begin();
    1. Wait for client requests

Once everything is initialized in the setup(), the server should start accepting incomming HTTP requests and process them. To accept requests it is used the server.available() function. It doesn’t need any argument, but it returns an EthernetClient object. It is possible to check for a client request using the if (client) statement: it notifies that the client is ready.

EthernetClient client = server.available();
if (client) {
  // ...
}
    1. Parse the request as an HTTP request

It is supposed the server will receive and HTTP request from the connected client, so the sketch should understand that request. The principle is to read available data until an empty line is found, which indicates that the request is finished.

bool emptyLine = true;
while (client.connected()) {
  if (client.available()) {
    char c = client.read();
    if (emptyLine && (c == '\n')) {
      // Send response
    }

    // The request is finished when an empty line is received
    if (c == '\n') {
      emptyLine = true;
    } else if (c != '\r') {
      emptyLine = false;
    }
  }
}
    1. Send an static always-the-same HTTP response with HTML content

When the request is finished, the client waits for a response, so the sketch should send it while the connection is still alive. The response consists on an HTTP response status line, the response content type header, and many other optional headers. After the headers, and depending on the response content type, the content is sent. In this case the server always replies an static HTML page with a “Hello PLC!” text.

client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println(
    "<!doctype html>"
    "<html>"
    "<head>"
    "<title>Hello PLC!</title>"
    "</head>"
    "<body>"
    "<h1>Hello PLC!</h1>"
    "</body>"
    "</html>"
    );
client.flush();
    1. Close the connection

After sending the response to the client, it is necessary to close its connection. One connection contains just a request and its response, and nothing more.

client.stop();
    1. Final code review

#include <Ethernet2.h>

// Server creation
EthernetServer server(80);

void setup() {
  // Ethernet initialization
  byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
  byte ip[] = {10, 10, 10, 25};
  Ethernet.begin(mac, ip);

  // Server initialization
  server.begin();
}

void loop() {
  // Wait for clients
  EthernetClient client = server.available();
  if (client) {
    // Parse request
    bool emptyLine = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        // The request is finished when an empty line is received
        if (emptyLine && (c == '\n')) {
          // Send the response
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println();
          client.println(
              "<!doctype html>"
              "<html>"
              "<head>"
              "<title>Hello PLC!</title>"
              "</head>"
              "<body>"
              "<h1>Hello PLC!</h1>"
              "</body>"
              "</html>"
              );
          client.flush();

          // Close the connection
          client.stop();
          break;
        }

        // The request is finished when a blank line is received
        if (c == '\n') {
          emptyLine = true;
        } else if (c != '\r') {
          emptyLine = false;
        }
      }
    }
  }
}

This chapter adds a couple of links to set and clear an output.

      1. Add set and clear actions

The easiest way to add simple actions into the web page is to insert anchor HTML elements with associated action paths. The HTTP response from the server should contain these anchors:

client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println(
    "<!doctype html>"
    "<html>"
    "<head>"
    "<title>Hello PLC!</title>"
    "</head>"
    "<body>"
    "<h1>Hello PLC!</h1>"
    "<a href='/setOutput'>Set output</a><br>"
    "<a href='/clearOutput'>Clear output</a><br>"
    "</body>"
    "</html>"
    );
client.flush();
      1. Parse the request

Now the sketch should parse the request to set or clear the output depending on the path. To do this, it gets the route from the first line of the request using an String object called path and some of its functions.

Some variables are needed

String header;
bool firstLine = true;

And the character received from the client is appended to the header

if (firstLine) {
  header += c;
}

When the request is complete the sketch parses the header line to get the path

int pos = header.indexOf(' ');
String path = header.substring(pos + 1, header.indexOf(' ', pos + 1));

Remember to update the firstLine variable when the client sends a ‘new line’ (\n) character.

if (c == '\n') {
  emptyLine = true;
  firstLine = false;
} else if (c != '\r') {
  emptyLine = false;
}
      1. Set or clear output depending on the path

The server should set output to HIGH when the path is equal to ‘/setOutput’ and LOW when the path is equal to ‘/clearOutput’.

if (path.compareTo("/setOutput") == 0) {
  // Set output
  digitalWrite(Q0_0, HIGH);
} else if (path.compareTo("/clearOutput") == 0) {
  // Clear output
  digitalWrite(Q0_0, LOW);
}
      1. Final code review

Here is the whole code, with some improvements: send a 404 error for unknown requests, send a redirection response to the main page on set/clear command request and
add the sendMainPage() function to ease the reading and reuse it.

#include <Ethernet2.h>

// Server creation
EthernetServer server(80);

void setup() {
  Serial.begin(9600UL);
  Serial.println("WebServer started");

  // Ethernet initialization
  byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
  byte ip[] = {10, 10, 10, 25};
  Ethernet.begin(mac, ip);

  // Server initialization
  server.begin();
}

void loop() {
  // Wait for clients
  EthernetClient client = server.available();
  if (client) {
    // Parse request
    bool emptyLine = true;
    String header;
    bool firstLine = true;

    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        if (firstLine) {
          header += c;
        }

        // The request is finished when an empty line is received
        if (emptyLine && (c == '\n')) {
          // Get path from the header line
          int pos = header.indexOf(' ');
          String path = header.substring(pos + 1, header.indexOf(' ', pos + 1));

          // Act depending on path
          if (path.compareTo("/") == 0) {
            sendMainPage(client);
          } else if (path.compareTo("/setOutput") == 0) {
            // Set output
            digitalWrite(Q0_0, HIGH);

            // Redirect response
            redirectToMainPage(client);
          } else if (path.compareTo("/clearOutput") == 0) {
            // Clear output
            digitalWrite(Q0_0, LOW);

            // Redirect response
            redirectToMainPage(client);
          } else {
            // Unrecognized path
            sendNotFound(client);
          }

          client.flush();

          // Close connection
          client.stop();

          break;
        }

        // The request is finished when a blank line is received
        if (c == '\n') {
          emptyLine = true;
          firstLine = false;
        } else if (c != '\r') {
          emptyLine = false;
        }
      }
    }
  }
}

void sendNotFound(EthernetClient &client) {
  // Path not found
  client.println("HTTP/1.1 404 Not Found");
  client.println("Content-Type: text/plain");
  client.println();
  client.println("404 Not Found");
}

void redirectToMainPage(EthernetClient &client) {
  // Redirect to main page
  client.println("HTTP/1.1 303 See Other");
  client.println("Location: /");
  client.println();
}

void sendMainPage(EthernetClient &client) {
  // Send the response
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  client.println(
      "<!doctype html>"
      "<html>"
      "<head>"
      "<title>Hello PLC!</title>"
      "</head>"
      "<body>"
      "<h1>Hello PLC!</h1>"
      "<a href='/setOutput'>Set output</a><br>"
      "<a href='/clearOutput'>Clear output</a><br>"
      "</body>"
      "</html>"
      );
}

This chapter explains how to show an input value updated dynamically in the web page. Obviously it is also possible to show other dynamic values of variables o any other stuff.

      1. Show the input value

An HTML text element is added into the web page, to put the input value. To update it dynamically, it is necessary to add a little of Javascript into the web page. The Javascript updateInputValue() function sends a request to the server to obtain the input value and, when the response is received, it inserts the received text into the input-value HTML element using the getElementById() function and the innerHTML property.

client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println(
    "<!doctype html>"
    "<html>"
    "<head>"
    "<title>Hello PLC!</title>"
    "</head>"
    "<body>"
    "<h1>Hello PLC!</h1>"
    "<a href='/setOutput'>Set output</a><br>"
    "<a href='/clearOutput'>Clear output</a><br>"
    "<p>Input value: <span id='input-value'></span></p>"
    "<script>"
    "function updateInputValue() {"
    "  var xhttp = new XMLHttpRequest();"
    "  xhttp.onreadystatechange = function() {"
    "    if (this.readyState == 4 && this.status == 200) {"
    "      document.getElementById('input-value').innerHTML = this.responseText;"
    "    }"
    "  };"
    "  xhttp.open('GET', 'inputValue', true);"
    "  xhttp.send();"
    "}"
    "setInterval(updateInputValue, 1000);"
    "</script>"
    "</body>"
    "</html>"
    );
      1. Process the input value request

The sketch should recognize the /inputValue request, and get the input value using the digitalRead() function and send the response to the client.

// ...
} else if (path.compareTo("/inputValue") == 0) {
  // Get the input value
  int inputValue = digitalRead(I0_1);

  // Send input value
  sendText(client, inputValue == HIGH ? "HIGH" : "LOW");
} else {
//...

It would be pretty good to send a JSON text or any of these sophisticated structured text protocols, and it would be useful for more complex applications, but in this case it is sufficient to send a plain text response.

void sendText(EthernetClient &client, const char *text) {
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/plain");
  client.println();
  client.println(text);
}
      1. Final code review

#include <Ethernet2.h>

// Server creation
EthernetServer server(80);

void setup() {
  Serial.begin(9600UL);
  Serial.println("WebServer started");

  // Ethernet initialization
  byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
  byte ip[] = {10, 10, 10, 25};
  Ethernet.begin(mac, ip);

  // Server initialization
  server.begin();
}

void loop() {
  // Wait for clients
  EthernetClient client = server.available();
  if (client) {
    // Parse request
    bool emptyLine = true;
    String header;
    bool firstLine = true;

    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        if (firstLine) {
          header += c;
        }

        // The request is finished when an empty line is received
        if (emptyLine && (c == '\n')) {
          // Get path from the header line
          int pos = header.indexOf(' ');
          String path = header.substring(pos + 1, header.indexOf(' ', pos + 1));

          // Act depending on path
          if (path.compareTo("/") == 0) {
            sendMainPage(client);
          } else if (path.compareTo("/setOutput") == 0) {
            // Set output
            digitalWrite(Q0_0, HIGH);

            // Redirect response
            redirectToMainPage(client);
          } else if (path.compareTo("/clearOutput") == 0) {
            // Clear output
            digitalWrite(Q0_0, LOW);

            // Redirect response
            redirectToMainPage(client);
          } else if (path.compareTo("/inputValue") == 0) {
            // Get the input value
            int inputValue = digitalRead(I0_1);

            // Send input value
            sendText(client, inputValue == HIGH ? "HIGH" : "LOW");
          } else {
            // Unrecognized path
            sendNotFound(client);
          }

          client.flush();

          // Close connection
          client.stop();

          break;
        }

        // The request is finished when a blank line is received
        if (c == '\n') {
          emptyLine = true;
          firstLine = false;
        } else if (c != '\r') {
          emptyLine = false;
        }
      }
    }
  }
}

void sendNotFound(EthernetClient &client) {
  // Path not found
  client.println("HTTP/1.1 404 Not Found");
  client.println("Content-Type: text/plain");
  client.println();
  client.println("404 Not Found");
}

void redirectToMainPage(EthernetClient &client) {
  // Redirect to main page
  client.println("HTTP/1.1 303 See Other");
  client.println("Location: /");
  client.println();
}

void sendMainPage(EthernetClient &client) {
  // Send the response
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  client.println(
      "<!doctype html>"
      "<html>"
      "<head>"
      "<title>Hello PLC!</title>"
      "</head>"
      "<body>"
      "<h1>Hello PLC!</h1>"
      "<a href='/setOutput'>Set output</a><br>"
      "<a href='/clearOutput'>Clear output</a><br>"
      "<p>Input value: <span id='input-value'></span></p>"
      "<script>"
      "function updateInputValue() {"
      "  var xhttp = new XMLHttpRequest();"
      "  xhttp.onreadystatechange = function() {"
      "    if (this.readyState == 4 && this.status == 200) {"
      "      document.getElementById('input-value').innerHTML = this.responseText;"
      "    }"
      "  };"
      "  xhttp.open('GET', 'inputValue', true);"
      "  xhttp.send();"
      "}"
      "setInterval(updateInputValue, 1000);"
      "</script>"
      "</body>"
      "</html>"
      );
}

void sendText(EthernetClient &client, const char *text) {
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/plain");
  client.println();
  client.println(text);
}

Once the basic actions are implemented, it is also possible to create more complex ones, as the RS-232/RS-485/RS-422 communications are. This chapter will explain you how to send RS-485 data from the web application, but it is also possible to show received data from the RS-485.

      1. Add the RS-485 field

The first step could be to add an HTML form to send the RS-485 data to the PLC. In this case it is added an HTML input field to type alphanumeric data. When the send button is clicked, the sendRS485() function is called. This function still does not exist! It will be created in the next step.

<form action='javascript:void(0)' onsubmit='sendRS485()'>
  Data: <input type='text' pattern='[a-zA-Z0-9]+' title='Alphanumeric string' value='' id='rs485Data'>
  <input type='submit' value='Send'>
</form>
      1. Javascript send function

The sendRS485() Javascript function gets the data from the HTML input field and sends it integrated into the URL of the HTTP request: /rs485/<data>.

function sendRS485() {
  var el = document.getElementById('rs485Data');
  if (el) {
    var xhttp = new XMLHttpRequest();
    xhttp.open('GET', '/rs485/' + el.value, true);
    xhttp.send();
  }
}
      1. Parse the request

The RS-485 data is contained into the request URL. The sketch should parse it to obtain the data to send. When the URL starts with the ‘/rs485/’ string, it is considered to be an RS-485 data request to be sent through RS-485, so the sketch extracts the beginning of the URL to get the RS-485 data. As the client ingnores the response, only an OK text is sent.

  // ...
} else if (path.startsWith("/rs485/")) {
  // Get data from the request
  String data = path.substring(7);

  // Send a response
  sendText(client, "OK");
  // ...
      1. Set up RS-485

Before sending data through RS-485 it is mandatory to start the driver using the RS485.begin() function into the setup(). This function is available after including the RS485.h library.

#include <RS485.h>

To initializa the RS-485 you MUST set the serial rate. In this case the rate is set to 9600bps.

// ...
RS485.begin(9600);
// ...
      1. Send data through RS-485

The RS485.write() function is called to send data through the RS-485.

  // ...
} else if (path.startsWith("/rs485/")) {
  // Get data from the request
  String data = path.substring(7);

  // Send data through RS-485
  RS485.write(data.c_str());

  // Send a response
  sendText(client, "OK");
} else {
  // ...
      1. Final code review

#include <RS485.h>
#include <Ethernet2.h>

// Server creation
EthernetServer server(80);

void setup() {
  Serial.begin(9600UL);
  Serial.println("WebServer started");

  // RS-485 initialization
  RS485.begin(9600);

  // Ethernet initialization
  byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
  byte ip[] = {10, 10, 10, 25};
  Ethernet.begin(mac, ip);

  // Server initialization
  server.begin();
}

void loop() {
  // Wait for clients
  EthernetClient client = server.available();
  if (client) {
    // Parse request
    bool emptyLine = true;
    String header;
    bool firstLine = true;

    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        if (firstLine) {
          header += c;
        }

        // The request is finished when an empty line is received
        if (emptyLine && (c == '\n')) {
          // Get path from the header line
          int pos = header.indexOf(' ');
          String path = header.substring(pos + 1, header.indexOf(' ', pos + 1));

          // Act depending on path
          if (path.compareTo("/") == 0) {
            sendMainPage(client);
          } else if (path.compareTo("/setOutput") == 0) {
            // Set output
            digitalWrite(Q0_0, HIGH);

            // Redirect response
            redirectToMainPage(client);
          } else if (path.compareTo("/clearOutput") == 0) {
            // Clear output
            digitalWrite(Q0_0, LOW);

            // Redirect response
            redirectToMainPage(client);
          } else if (path.compareTo("/inputValue") == 0) {
            // Get the input value
            int inputValue = digitalRead(I0_1);

            // Send input value
            sendText(client, inputValue == HIGH ? "HIGH" : "LOW");
          } else if (path.startsWith("/rs485/")) {
            // Get data from the request
            String data = path.substring(7);

            // Send data through RS-485
            RS485.write(data.c_str());

            // Send a response
            sendText(client, "OK");
          } else {
            // Unrecognized path
            sendNotFound(client);
          }

          client.flush();

          // Close connection
          client.stop();

          break;
        }

        // The request is finished when a blank line is received
        if (c == '\n') {
          emptyLine = true;
          firstLine = false;
        } else if (c != '\r') {
          emptyLine = false;
        }
      }
    }
  }
}

void sendNotFound(EthernetClient &client) {
  // Path not found
  client.println("HTTP/1.1 404 Not Found");
  client.println("Content-Type: text/plain");
  client.println();
  client.println("404 Not Found");
}

void redirectToMainPage(EthernetClient &client) {
  // Redirect to main page
  client.println("HTTP/1.1 303 See Other");
  client.println("Location: /");
  client.println();
}

void sendMainPage(EthernetClient &client) {
  // Send the response
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  client.println(
      "<!doctype html>"
      "<html>"
      "<head>"
      "<title>Hello PLC!</title>"
      "</head>"
      "<body>"
      "<h1>Hello PLC!</h1>"
      "<a href='/setOutput'>Set output</a><br>"
      "<a href='/clearOutput'>Clear output</a><br>"
      "<p>Input value: <span id='input-value'></span></p>"
      "<form action='javascript:void(0)' onsubmit='sendRS485()'>"
      "Data: <input type='text' pattern='[a-zA-Z0-9]+' title='Alphanumeric string' value='' id='rs485Data'>"
      "<input type='submit' value='Send'>"
      "</form>"
      "<script>"
      "function updateInputValue() {"
      "  var xhttp = new XMLHttpRequest();"
      "  xhttp.onreadystatechange = function() {"
      "    if (this.readyState == 4 && this.status == 200) {"
      "      document.getElementById('input-value').innerHTML = this.responseText;"
      "    }"
      "  };"
      "  xhttp.open('GET', 'inputValue', true);"
      "  xhttp.send();"
      "}"
      "setInterval(updateInputValue, 1000);"
      "function sendRS485() {"
      "  var el = document.getElementById('rs485Data');"
      "  if (el) {"
      "    var xhttp = new XMLHttpRequest();"
      "    xhttp.open('GET', '/rs485/' + el.value, true);"
      "    xhttp.send();"
      "  }"
      "}"
      "</script>"
      "</body>"
      "</html>"
      );
}

void sendText(EthernetClient &client, const char *text) {
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/plain");
  client.println();
  client.println(text);
}

… You can do practices using one of our Ethernet PLC.