Skip to content

Week 14 Assignments - Interface and Application Programming

Two-Way Browser / Board Control of XIAO Onboard LED Using Web Socket Connection

Group Assignment

The group assignment for this week was to:

  • Compare as many tool options as possible.
  • Document your work on the group work page and reflect on your individual page what you learned.

Outcomes

The group assignment page for this week is on the 2025 Charlotte Super Fab Lab group site for Week 14 - Interface and Application Programming.

What Was Learned

In the group assignment, we considered a variety of platforms and languages to support interface and application programming. This included languages such as HTML, JavaScript, and C, interfaces such as Web Sockets, and platforms such MIT App Inventor, Bluefruit Connect, Adafruit IO, Blynk, and Losant.

Discussion on the different options provided us a sense of the tradeoffs on capability and complexity in selecting tools to support interface and application programming.

Individual Assignment

The individual assignment for this week was to:

  • Write an application for the embedded board that you made. that interfaces a user with an input and/or output device(s)

Outcomes

I tried a web services approach for basic interface and application development. The microcontroller board would act as a web server to provide web content / services connected to board functionality. The connection would be over WiFi and support web browser interaction.

WiFi Connectivity - Scanning

As a foundation for the setup, I tested the WiFi connectivity for the XIAO ESP32C3 microcontroller development board I created in Week 8.

The first step was to check basic WiFi capability for the XIAO ESP32C3 board by scanning for available WiFi networks. I used the Arduino IDE environment. The scanning setup followed the Seeed Studio WiFi example documentation on how to Scan WiFi networks.

Basic Wi-Fi Scan
#include "WiFi.h"

void setup() {
  Serial.begin(115200);

  // Set WiFi to station mode and disconnect from an AP if it was previously connected
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  Serial.println("Setup done");
}

void loop() {
  Serial.println("scan start");

  // WiFi.scanNetworks will return the number of networks found
  int n = WiFi.scanNetworks();
  Serial.println("scan done");
  if (n == 0) {
    Serial.println("no networks found");
  } else {
    Serial.print(n);
    Serial.println(" networks found");
    for (int i = 0; i < n; ++i) {
      // Print SSID and RSSI for each network found
      Serial.print(i + 1);
      Serial.print(": ");
      Serial.print(WiFi.SSID(i));
      Serial.print(" (");
      Serial.print(WiFi.RSSI(i));
      Serial.print(")");
      Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " " : "*");
      delay(10);
    }
  }
  Serial.println("");

  // Wait a bit before scanning again
  delay(5000);
}

The scanning code worked well, and the serial monitor showed that the XIAO ESP32C3 was able to identify nearby WiFi networks.

Setup done
scan start
scan done
5 networks found
1: lakehouse89 (-69)*
2: FiOS-MGSU8 (-86)*
3: mesh (-88)*
4: mesh (-90)*
5: wanda (-90)*

...

scan start
scan done
6 networks found
1: lakehouse89 (-69)*
2: FiOS-MGSU8 (-86)*
3: wanda (-87)*
4: mesh (-89)*
5: Thomas Ring (-89)*
6: Rutiger 2.4 (-90)*

WiFi Connectivity - Connecting

The second step was to check whether the XIAO ESP32C3 board could connect to our local WiFi network. The connection setup followed the Seeed Studio WiFi example documentation on how to Connect to a WiFi network.

Basic Wi-Fi Connect
#include <WiFi.h>

const char* ssid = "your-ssid";
const char* password = "your-password";

void setup() {
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}
void loop() {}

The WiFi connecting code worked well, and the serial monitor showed that the XIAO ESP32C3 was able to connect with our local WiFi network.

Connecting to lakehouse89
...
WiFi connected
IP address: 
192.168.1.121

Web Server

With the WiFi connection in place, I tried to create a web server that would run on the XIAO board and provide board-specific web services. This involved setting up a web server to run on the XIAO board.

I followed the tutorial on creating a ESP32 Web Server – Arduino IDE. The tutorial used a setup with 2 LEDs. I adjusted the steps of the tutorial for the single LED setup with my development board.

The web server application uses structured URLs as end points for LED on/off actions. When a request is made to a web server, the URL for that request has a number of parts. The first part is the protocol, typically http:// or https://, which indicates the kind of message content expected. The second part is the domain address of the server (at a particular domain name / IP address), such as 192.168.1.121. The third part describes the path to the resource that is being requested from the server, such as /26/on. In the application below, a URL for turning on the LED is structured as:

  • http://192.168.1.121/26/on

And a URL for turning off the LED is structured as:

  • http://192.168.1.121/26/off

For hypertext transfer protocol requests (http or https), a method is also specified as part of the communication, such as GET or POST, that can be used to indicate different kinds of actions.

When the server receives the URL request, it checks what resource path is being requested. If it sees a request for /26/on using the GET method, it can direct to the code for turning the LED on. If it sees a request for /26/off using the GET method, it can direct to the part of the code for turning the LED off. In the complete application code (later), the part that does this is:

LED Action Based on URL Request
// turns the GPIOs on and off
if (header.indexOf("GET /26/on") >= 0) {
  Serial.println("GPIO 26 on");
  output26State = "on";
  digitalWrite(output26, HIGH);
} else if (header.indexOf("GET /26/off") >= 0) {
  Serial.println("GPIO 26 off");
  output26State = "off";
  digitalWrite(output26, LOW);
}

If the server is set up to respond to a URL request, then a web page can use standard HTML links as a way to communicate with the server - and to send commands. An HTML anchor link has 2 basic components:

  • The text to be displayed in the web page
  • The URL address of the destination - the page / content to go to when the link is activated / clicked

And HTML anchor link template is structured as

  • <a href=""> </a>
    • The <a> </a> structure indicates that it is an HTML link
    • href="" is where the destination URL goes - between the quotes
    • The space between the start <a href=""> and end </a> is where the link text for display goes

So, an HTML page can send a structured message to the server using the previously described URL in an HTML link, such as:

  • <a href="http://192.168.1.121/26/on">Turn LED On</a>

When clicked, the link will make a http://192.168.1.121/26/on request to the server, which will trigger the LED on action in the code.

In the application below, the server provides an initial web page on first request. That web page contains an HTML link that can be used in this way to send commands to the server for LED actions. Part of the code that does this is:

Generate HTML Link for LED Action
client.println("<p><a href=\"/26/on\"><button class=\"button\">ON</button></a></p>");

There are extra control characters needed as part of the code, but this creates an HTML link as part of the web page:

  • <a href="/26/on"><button class="button">ON</button></a>
    • this uses some shorthand - because the web page came from the same web server being used, it presumes the same server address, so does not include the http://192.168.1.121 part
    • this uses a fancier link display - instead of just text, the space between the start <a href=""> and end </a> displays an HTML button
  • When the displayed button is clicked, the link is activated, the control message is sent to the server, and the LED action is taken

The full code for the Web Server application follows.

ESP32 Web Server
/*********
  Rui Santos
  Complete project details at http://randomnerdtutorials.com  
*********/

// Load Wi-Fi library
#include <WiFi.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Auxiliar variables to store the current output state
String output26State = "off";

// Assign output variables to GPIO pins
const int output26 = D6;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;

void setup() {
  Serial.begin(115200);
  // Initialize the output variables as outputs
  pinMode(output26, OUTPUT);
  // Set outputs to LOW
  digitalWrite(output26, LOW);

  // Connect to 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("GPIO 26 on");
              output26State = "on";
              digitalWrite(output26, HIGH);
            } else if (header.indexOf("GET /26/off") >= 0) {
              Serial.println("GPIO 26 off");
              output26State = "off";
              digitalWrite(output26, LOW);
            }

            // 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: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}</style></head>");

            // Web Page Heading
            client.println("<body><h1>ESP32 Web Server</h1>");

            // Display current state, and ON/OFF buttons for GPIO 26  
            client.println("<p>GPIO 26 - State " + output26State + "</p>");
            // If the output26State is off, it displays the ON button       
            if (output26State=="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>");
            }                
            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("");
  }
}

When the code is uploaded and run, the microcontroller connects to WiFi and starts the web server. The IP address assigned to the development board is printed on the serial monitor. This is the URL address that can be used in a web browser on the same local network, in order to connect to the development board web services.

Connecting to lakehouse89
....
WiFi connected.
IP address: 
192.168.1.121

Entering the IP address into a web browser on the same local network results in the web page being loaded from the web server on the development board.

Web page served from XIAO

Button interaction on the web page makes a request to the server to turn the LED on / off and receives a corresponding web page update as a response.

Browser Control of XIAO Onboard LED Using Web Server

The web server shows details on the HTTP request using the serial monitor.

New Client.
GET /26/on HTTP/1.1
Host: 192.168.1.121
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Referer: http://192.168.1.121/26/off
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Accept-Encoding: gzip, deflate
Connection: keep-alive

GPIO 26 on
Client disconnected.

...

New Client.
GET /26/off HTTP/1.1
Host: 192.168.1.121
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Referer: http://192.168.1.121/26/on
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Accept-Encoding: gzip, deflate
Connection: keep-alive

GPIO 26 off
Client disconnected.

Web Socket

In order to enable two-way communication between a web interface and the board running a web server, web sockets can be used. When a client connects with the web server, a web socket connection is established using JavaScript and the web socket protocol. Event-based processing is used to trigger communication and response.

I followed a tutorial on ESP32 WebSocket Server: Control Outputs (Arduino IDE). I modified the code to include a debounced button control for the onboard development board button. The button acts as a physical control for the LED alongside the web service based control.

ESP32 WebSocket Server
/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-websocket-server-arduino/
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
const uint8_t DEBOUNCE_DELAY = 5; // in milliseconds

bool ledState = 0;
const int ledPin = D6;

struct Button {
    // state variables
    uint8_t  pin;
    bool     levelForPressed;
    bool     lastReading;
    uint32_t lastDebounceTime;
    uint16_t state;

    // methods determining the logical state of the button
    bool pressed()                { return state == 1; }
    bool released()               { return state == 0xffff; }
    bool held(uint16_t count = 0) { return state > 1 + count && state < 0xffff; }

    // method for reading the physical state of the button
    void read() {
        // reads the voltage on the pin connected to the button
        bool reading = digitalRead(pin);

        // if the logic level has changed since the last reading,
        // we reset the timer which counts down the necessary time
        // beyond which we can consider that the bouncing effect
        // has passed.
        if (reading != lastReading) {
            lastDebounceTime = millis();
        }

        // from the moment we're out of the bouncing phase
        // the actual status of the button can be determined
        if (millis() - lastDebounceTime > DEBOUNCE_DELAY) {
            // don't forget that the read pin is pulled-up
            bool pressed = reading == levelForPressed;
            if (pressed) 
            {
                if (state  < 0xfffe) state++;
                else if (state == 0xfffe) state = 2;
            } 
            else if (state) {
                state = state == 0xffff ? 0 : 0xffff;
            }
        }

        // finally, each new reading is saved
        lastReading = reading;
    }
};

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   /*.button:hover {background-color: #0f8b8d}*/
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>XIAO ESP32C3 WebSocket Server</h1>
  </div>
  <div class="content">
    <div class="card">
      <h2>Onboard LED</h2>
      <p class="state">state: <span id="state">%STATE%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = event.data;
  }
  function onLoad(event) {
    initWebSocket();
    initButton();
  }
  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>
)rawliteral";

void notifyClients() {
  ws.textAll(String(ledState));
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
  return String();
}


Button button = { D7, LOW, LOW, 0, 0 };

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(button.pin, INPUT);

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

  initWebSocket();

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", index_html, processor);
  });

  // Start server
  server.begin();
}

void loop() {
  ws.cleanupClients();
  digitalWrite(ledPin, ledState);
  button.read();
  if (button.pressed()) {
    ledState = !ledState;
    notifyClients();
  }

}

The web socket setup enables two-way communication. So, the web page button can turn the LED on and off, with status update on the web page. But also, the physical button on the development board can control the LED and push the status update to the web page connected via the web socket.

The web socket application uses several core libraries for network connectivity and web services.

  • #include <WiFi.h> - provides WiFi connectivity functions
  • #include <AsyncTCP.h> - enables multiple network connections needed for simultaneous web socket connections
  • #include <ESPAsyncWebServer.h> - provides core customizable WebSocket server

Several global parameters are defined for use in the application, including WiFi credentials, debouncing delay, LED status, and LED pin detail.

Application Parameters
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
const uint8_t DEBOUNCE_DELAY = 5; // in milliseconds

bool ledState = 0;
const int ledPin = D6;

The WebSocket library is used to create a web server (on port 80) and associated web socket service (on /ws path), which can then be customized for the application. This defines the server and ws variables used in other places in the code.

Create Web Server and Web Socket Service on Port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

The overall web socket application has several main parts. The application centers on knowing the state of the LED - whether it is on or off. This is handled by the boolean LED state variable ledState, which is initialized to 0 (false) at the start.

LED State Variable
bool ledState = 0;

The core of the application is the standard Arduino loop(). Here, the loop is reasonably short and does the following:

  • ws.cleanupClients(); - library function to close stale web socket connections
  • digitalWrite(ledPin, ledState); - standard Arduino pin function - set the LED pin on/off to correspond with the value of the ledState variable
    • the ledState variable may be changed either by the physical button or by web socket interaction - either way the LED will follow suit
  • button.read(); - uses a helper function to read whether the physical button is pressed
  • if (button.pressed()) - checks if the button was read as being pressed, if so
    • ledState = !ledState; - toggle the LED state - LED will respond in the next loop iteration
    • notifyClients(); - sends a message to all connected web pages that a change has happened, so the web pages can update accordingly

The rest of the application is essentially setup and helper functions. The standard Arduino setup() is used for the server application. The first part is typical Arduinio initialization, setting up the serial monitor, button pin as input, LED pin as output, and initializing the LED pin (LOW).

Setup Serial Monitor and Initialize Pins
Serial.begin(115200);

pinMode(button.pin, INPUT);

pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

The rest of the setup() is for connecting the board to WiFi and starting the web socket server. For connecting to WiFi:

  • WiFi.begin(ssid, password); - uses WiFi library to make a WiFi connection using defined credentials for network name and password
  • while (WiFi.status() != WL_CONNECTED) { - wait until a WiFi connection has been established
    • delay(1000); - give some time before checking WiFi connection again
    • Serial.println("Connecting to WiFi.."); - print WiFi status message on serial monitor
  • Serial.println(WiFi.localIP()); - once WiFi is connected, print IP address as status message on serial monitor

Once WiFi is connected, setup() can initialize and start the web server and web socket service. - initWebSocket(); - helper function to initialize the web socket service with customizations - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - tells the server how to respond to an inital browser request for the home page * request->send(200, "text/html", index_html, processor); - provides the web page content - server.begin(); - starts the web server

The web socket service is customized to provide straightforward logging data and to respond to a specific kind of LED control message. Customized responses are defined in the onEvent function. There are 3 basic customizations:

  • On Connect (WS_EVT_CONNECT)- when a web page connects to the web socket service, the connection is logged with the serial monitor
  • On Disconnect (WS_EVT_DISCONNECT) - when a web page disconnects from the web socket service, the disconnection is logged with the serial monitor
  • On Message (WS_EVT_DATA) - when a web socket message is sent from a connected web page, the service processes the message with the handleWebSocketMessage function
Web Socket Service Customized Responses
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

If the web socket service receives a message, it is handled with the handleWebSocketMessage function. This checks the messaage and responds as follows:

  • If the message is the string "toggle"
    • toggle the value of the ledState variable - this will get picked up in the next iteration of the loop() function and turn the LED on/off
    • notify all web paged connected to the web socket service with a message that there has been an update, so the pages can respond accordingly
Web Socket Service Message Handler
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}

When an initial browser request is made to the web server, it provides the web page content for the application. The web page content is a large literal string representing an HTML document. It is defined in the application between the rawliteral constructs.

  • const char index_html[] PROGMEM = R"rawliteral(
  • ... long string of HTML web page content ...
  • )rawliteral";

The HTML page content includes CSS styling and site structure to show a basic web page with a panel for information about the onboard LED state. It also includes a button for toggling the LED state. The page design can be seen in the video. In terms of application functionality, the web page includes embedded JavaScript functions inside the <script> </script> HTML element that are used for web socket communication between the web page and the web server / web socket service.

The initWebSocket() function is used to establish a connection with the web server's web socket service and set up handler functions for different events.

Connect to Web Socket Service
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage;
  }

The onOpen(event) function listens for when a web socket connection to the service is successfully made and logs that to the developer console in the web browser.

On Connection to Web Socket Service
function onOpen(event) {
    console.log('Connection opened');
}

The onClose(event) function listens for when a web socket connection to the service is closed. It logs that to the to the developer console in the web browser and then tries to reopen a new connection.

On Disconnect from the Web Socket Service
function onClose(event) {
  console.log('Connection closed');
  setTimeout(initWebSocket, 2000);
}

The onMessage(event) listens for when a message is received from the server application through the web socket. If the message content is a "1", it changes the web page content to show the LED state as ON. If the message content is anything else, it changes the web page content to show the LED state as OFF. This keeps the displayed content of the web page synchronized with the application server.

On Disconnect from the Web Socket Service
function onMessage(event) {
  var state;
  if (event.data == "1"){
    state = "ON";
  }
  else{
    state = "OFF";
  }
  document.getElementById('state').innerHTML = event.data;
}

Three functions ae used to set up all of the other functions once the web page loads. The onLoad(event) function is called when the web page is done loading. It first initializes the web socket connection, as noted earlier. It then defnines a response for the button on the web page - when clicked call the toggle() function. The toggle() function uses the web socket to send a "toggle" message to the server through the web socket connection. The server will respond to the "toggle" message as noted earlier - changing the state of the LED and messaging all connected web pages about the update.

When Web Page Loading is Complete, Initialize Web Socket Connection and Button
function onLoad(event) {
  initWebSocket();
  initButton();
}
function initButton() {
  document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
  websocket.send('toggle');
}

The Web Socket Applicaton can be seen in action below.

Two-Way Browser / Board Control of XIAO Onboard LED Using Web Socket Connection