htmaa '22, by jakin

week 12: interfaces

Prior Experience: 2/5

I have prior experience with web development in the form of web.lab over IAP.

Interfaces: haptic metronome

I made a website to control the metronome for my final project using the ESP32. I can turn it on and off, and type in the beats per minute to change the frequency of the metronome pulses (of the vibration motor).

Website via Wifi

Apparently the ESP32 is can communicate with a website by connecting to wifi.

I added Neil's hello world code (http://academy.cba.mit.edu/classes/networking_ocmmunications/ESP32/hello.ESP32-WROOM.WebClient.ino) and went to 10.31.85.41 (which is printed out in the Arduino serial monitor). This number changes sometimes, and I think it is just based on what server is available. The hello world code works!

The SSID is "MIT" and the password is "", since I want it to connect to MIT wifi.

//
// hello.ESP32-WROOM.WebServer.ino
//
// ESP32 Web server hello-world
//
// Neil Gershenfeld 11/12/19
//
// This work may be reproduced, modified, distributed,
// performed, and displayed for any purpose, but must
// acknowledge this project. Copyright is retained and
// must be preserved. The work is provided as is; no
// warranty is provided, and users accept all liability.
//

#include <WiFi.h>

const char* ssid = "MIT";
const char* password = "";
WiFiServer server(80);

void setup() {
  Serial.begin(115200);
  printf("\nConnecting ");
  WiFi.begin(ssid,password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    printf(".");
    }
  printf("\nConnected with address %s\n",WiFi.localIP().toString().c_str());
  server.begin();
  }

void loop() {
  char cold,cnew;
  WiFiClient client = server.available();
  if (client) {
    printf("\nReceived connection from %s\n\n",client.remoteIP().toString().c_str());
    while (client.connected()) {
      if (client.available()) {
        cnew = client.read();
        printf("%c",cnew);
        if ((cold == '\n') && (cnew == '\r')) { // check for blank line at end of request
          client.printf("HTTP/1.1 200 OK\n");
          client.printf("Content-type:text/html\n");
          client.printf("\n");
          client.printf("Hello %s from ESP32-WROOM!<br>\n",client.remoteIP().toString().c_str());
          client.stop();
          break;
          }
        cold = cnew;
        }
      }
    }
  }

Here is the website tutorial that I first followed. https://randomnerdtutorials.com/esp32-web-server-arduino-ide/

Then, I followed this tutorial: https://randomnerdtutorials.com/esp32-async-web-server-espasyncwebserver-library/ to make the website asynchronous, only responding when I click the toggle, so that I can run the code for the vibration motor inside the loop. I used it to make the motor vibrate when I click the button.

Here is the code for the asynchronous website:

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-input-data-html-form/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

#include <Arduino.h>
#ifdef ESP32
  #include <WiFi.h>
  #include <AsyncTCP.h>
#else
  #include <ESP8266WiFi.h>
  #include <ESPAsyncTCP.h>
#endif

#include <ESPAsyncWebServer.h>

AsyncWebServer server(80);

// REPLACE WITH YOUR NETWORK CREDENTIALS
const char* ssid = "MIT";
const char* password = "";

const char* PARAM_INPUT_1 = "input1";
bool metronome_on = false; 

const char* PARAM_INPUT_OUTPUT = "output";
const char* PARAM_INPUT_STATE = "state";


// HTML web page to handle 3 input fields (input1, input2, input3)
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
  <title>ESP Input Form</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 3.0rem;}
    p {font-size: 3.0rem;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px} 
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
    input:checked+.slider {background-color: #b30000}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
  </style>
  </head>
  <body>
  <form action="/get">
    input1: <input type="text" name="input1">
    <input type="submit" value="Submit">
  </form><br>
  <h2>ESP Web Server</h2>
  %BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ xhr.open("GET", "/update?output="+element.id+"&state=1", true); }
  else { xhr.open("GET", "/update?output="+element.id+"&state=0", true); }
  xhr.send();
}
</script>
</body></html>)rawliteral";

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons = "";
    buttons += "<h4>Output - GPIO 2</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"2\" " + outputState(2) + "><span class=\"slider\"></span></label>";
    buttons += "<h4>Output - GPIO 4</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"4\" " + outputState(4) + "><span class=\"slider\"></span></label>";
    buttons += "<h4>Output - GPIO 33</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"33\" " + outputState(33) + "><span class=\"slider\"></span></label>";
    return buttons;
  }
  return String();
}

String outputState(int output){
  if(digitalRead(output)){
    return "checked";
  }
  else {
    return "";
  }
}

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

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

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("WiFi Failed!");
    return;
  }
  Serial.println();
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Send web page with input fields to client
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Send a GET request to <ESP_IP>/update?output=<inputMessage1>&state=<inputMessage2>
  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage1;
    String inputMessage2;
    // GET input1 value on <ESP_IP>/update?output=<inputMessage1>&state=<inputMessage2>
    if (request->hasParam(PARAM_INPUT_OUTPUT) && request->hasParam(PARAM_INPUT_STATE)) {
      inputMessage1 = request->getParam(PARAM_INPUT_OUTPUT)->value();
      inputMessage2 = request->getParam(PARAM_INPUT_STATE)->value();
      digitalWrite(inputMessage1.toInt(), inputMessage2.toInt());
    }
    else {
      inputMessage1 = "No message sent";
      inputMessage2 = "No message sent";
    }
    Serial.print("GPIO: ");
    Serial.print(inputMessage1);
    Serial.print(" - Set to: ");
    Serial.println(inputMessage2);
    request->send(200, "text/plain", "OK");
  });

  // Send a GET request to <ESP_IP>/get?input1=<inputMessage>
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    // GET input1 value on <ESP_IP>/get?input1=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = PARAM_INPUT_1;
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    request->send(200, "text/html", "HTTP GET request sent to your ESP on input field (" 
                                     + inputParam + ") with value: " + inputMessage +
                                     "<br><a href=\"/\">Return to Home Page</a>");
  });
  server.onNotFound(notFound);
  server.begin();
}

void loop() {

}

I followed this to add an input box for the speed of the metronome: https://randomnerdtutorials.com/esp32-esp8266-input-data-html-form/, combining it with the on off button to make the vibration motor pulse at the bpm that the user inputs. (I had a really annoying error where I did not realize I had to use String.equals instead of == for a long time, but it ended up working.)

Bluetooth + Website

Then, I just merged the code for the metronome website with the code for the Bluetooth sensing from before. I followed the structure of LED blinkWithoutDelay so that the metronome loop runs based on the bpm and the distance sensor runs every 1 second (and responds based on that reading every second).

The sketch ended up being too big for the ESP32 so I did this: https://thecustomizewindows.com/2020/03/solve-esp32-sketch-too-big-error-on-arduino-ide/ to get more space for the code.

Here is the final website:

When the toggle is clicked, the metronome turns on or off, and when a bpm is typed into input1 field and the submit button is clicked, the metronome switches to the new bpm. The same code also runs a distance sensor and if the distance is below a cutoff (I set it to be 70), turns the page left and if it is above a cutoff (I set 1000) or out of range, it turns the page right.

Here is the final code:

    /*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-input-data-html-form/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/
//wifi
#include <Arduino.h>
#ifdef ESP32
  #include <WiFi.h>
  #include <AsyncTCP.h>
#else
  #include <ESP8266WiFi.h>
  #include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

//distance sensor
#include "Adafruit_VL53L0X.h" 
//bluetooth keyboard
#include <BleKeyboard.h>
#define ledPin 14  // The pin the LED is connected to

int bpm = 72;

unsigned long previousMillis = 0;  // will store last time LED was updated
int ledState = LOW;  // ledState used to set the LED
unsigned long previousMillisDistance = 0;  // will store last time distance sensor was updated

Adafruit_VL53L0X lox = Adafruit_VL53L0X();
BleKeyboard bleKeyboard;


AsyncWebServer server(80);

// REPLACE WITH YOUR NETWORK CREDENTIALS
const char* ssid = "MIT";
const char* password = "";

const char* PARAM_INPUT_1 = "input1";
String metronome_on = "0"; 

const char* PARAM_INPUT_OUTPUT = "output";
const char* PARAM_INPUT_STATE = "state";


// HTML web page to handle 3 input fields (input1, input2, input3)
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
  <title>ESP Input Form</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 3.0rem;}
    p {font-size: 3.0rem;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px} 
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
    input:checked+.slider {background-color: #1a7d2e}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
  </style>
  </head>
  <body>
  <form action="/get">
    input1: <input type="text" name="input1">
    <input type="submit" value="Submit">
  </form><br>
  <h2>Jakin's Metronome</h2>
  %BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ xhr.open("GET", "/update?output="+element.id+"&state=1", true); }
  else { xhr.open("GET", "/update?output="+element.id+"&state=0", true); }
  xhr.send();
}
</script>
</body></html>)rawliteral";

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons = "";
    buttons += "<h4>Metronome Toggle</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"metronome_on_off\" " + outputState(2) + "><span class=\"slider\"></span></label>";
    return buttons;
  }
  return String();
}

String outputState(int output){
  if(digitalRead(output)){
    return "checked";
  }
  else {
    return "";
  }
}

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

  // wait until serial port opens for native USB devices
  while (! Serial) {
    delay(1);
  }

  Serial.println("Adafruit VL53L0X test");
  if (!lox.begin()) {
    Serial.println(F("Failed to boot VL53L0X"));
    while(1);
  }
  // power 
  Serial.println(F("VL53L0X API Simple Ranging example\n\n")); 
  Serial.println("Starting BLE work!");
  bleKeyboard.begin();

  pinMode(ledPin, OUTPUT); // Declare the LED as an output
  digitalWrite(ledPin, LOW);


  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("WiFi Failed!");
    return;
  }
  Serial.println();
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Send web page with input fields to client
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Send a GET request to <ESP_IP>/update?output=<inputMessage1>&state=<inputMessage2>
  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage1;
    String inputMessage2;
    // GET input1 value on <ESP_IP>/update?output=<inputMessage1>&state=<inputMessage2>
    if (request->hasParam(PARAM_INPUT_OUTPUT) && request->hasParam(PARAM_INPUT_STATE)) {
      inputMessage1 = request->getParam(PARAM_INPUT_OUTPUT)->value();
      inputMessage2 = request->getParam(PARAM_INPUT_STATE)->value();
      digitalWrite(inputMessage1.toInt(), inputMessage2.toInt());
    }
    else {
      inputMessage1 = "No message sent";
      inputMessage2 = "No message sent";
    }
    Serial.print("GPIO: ");
    Serial.print(inputMessage1);
    Serial.print(" - Set to: ");
    Serial.println(inputMessage2);
//    if (inputMessage2.toInt() == 1) {
//      metronome_on = false;
//    } else {
//      metronome_on = true;
//    }
//    Serial.println("metronome on set to: ");
//    Serial.println(metronome_on);
    metronome_on = inputMessage2;
    request->send(200, "text/plain", "OK");
  });

  // Send a GET request to <ESP_IP>/get?input1=<inputMessage>
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    // GET input1 value on <ESP_IP>/get?input1=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = PARAM_INPUT_1;
      bpm = (request->getParam(PARAM_INPUT_1)->value()).toInt();
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    Serial.print("BPM: ");
    Serial.println(bpm);
//    request->send(200, "text/plain", "OK");
    request->send(200, "text/html", "Metronome BPM set to " 
                                     + String(bpm) +
                                     "<br><a href=\"/\">Return to Home Page</a>");
  });
  server.onNotFound(notFound);
  server.begin();
}

void loop() {

  VL53L0X_RangingMeasurementData_t measure;

  unsigned long currentMillis = millis();
  if (metronome_on.equals("1")) {
    if (ledState == LOW) {
      if (currentMillis - previousMillis >= 1000 * 60 / bpm - 100) {
        // save the last time you blinked the LED
        previousMillis = currentMillis;
        ledState = HIGH;
        // set the LED with the ledState of the variable:
        digitalWrite(ledPin, ledState);
      }
    } else {
      if (currentMillis - previousMillis >= 100) {
        // save the last time you blinked the LED
        previousMillis = currentMillis;
        ledState = LOW;
        // set the LED with the ledState of the variable:
        digitalWrite(ledPin, ledState);
      }
    }
  } 

  unsigned long currentMillisDistance = millis();
  if (bleKeyboard.isConnected()) {
    if (currentMillisDistance - previousMillisDistance >= 1000) {
        previousMillisDistance = currentMillisDistance;
        // hold and release right arrow key to swipe forward
        Serial.print("Reading a measurement... ");
        lox.rangingTest(&measure, false); // pass in 'true' to get debug data printout!

        if (measure.RangeStatus != 4) {  // phase failures have incorrect data
          Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter);
          if (measure.RangeMilliMeter <= 70 and measure.RangeMilliMeter > 0) {
            Serial.println(metronome_on);
            Serial.println("left arrow!");
            Serial.println(String(bpm));
            Serial.println(metronome_on);
            bleKeyboard.press(KEY_LEFT_ARROW);
            delay(50);
            bleKeyboard.releaseAll();
          } else if (measure.RangeMilliMeter >= 7000) {
            Serial.println("right arrow!");
            bleKeyboard.press(KEY_RIGHT_ARROW);
            delay(50);
            bleKeyboard.releaseAll();
          }
        } else {
          Serial.println(" out of range ");
          Serial.println("right arrow!");
          bleKeyboard.press(KEY_RIGHT_ARROW);
          delay(50);
          bleKeyboard.releaseAll();
        }
    }
  }
//  
//
//  delay(500);
}