Week 9

Goal of the week: Control a circuit over a local Wi‑Fi network and fetch data from the Internet.

Wireless Tone Generator

As my first project, I made a buzzer that can be controlled through a local Wi‑Fi network via a web interface hosted on the ESP32. It can:

Watch the video below to see how it works in real life:

Below is the breadboard wiring drawn in Fritzing:

Before running the sketch, I prepared the Arduino IDE. I installed the esp32 by Espressif Systems (stable version 2.0.11), selected the board type as ESP32 Dev Module, and set the port to COM6. Then I pasted in the code below. (Make sure to replace the Wi‑Fi name and password with your own values!)

Copied!
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ====== CONFIG ======
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const int buzzerPin = 25;

// ====== OLED SETUP ======
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ====== SERVER ======
WebServer server(80);

// ====== COMMAND STATE ======
enum CommandType { NONE, TONE, MORSE, MELODY_DARTH, MELODY_MARRY };
volatile CommandType currentCommand = NONE;

int toneFreq = 0;
String morseCode = "";

// ====== TIMING ======
unsigned long toneStart = 0;
unsigned long toneDuration = 0;
bool toneActive = false;
unsigned long nextNoteDelayUntil = 0;

// ====== FUNCTION: NON-BLOCKING TONE ======
void startTone(int freq, int duration) {
  ledcSetup(0, freq, 8);
  ledcAttachPin(buzzerPin, 0);
  ledcWrite(0, 127);
  toneStart = millis();
  toneDuration = duration;
  toneActive = true;
}

void stopTone() {
  ledcWrite(0, 0);
  toneActive = false;
  nextNoteDelayUntil = millis() + 100;  // pause between tones
}

// ====== MORSE STATE ======
int morseIndex = 0;

// ====== MELODY STATE ======
int melodyIndex = 0;
int marryMelody[] = {262, 392, 392, 440, 392, 523, 494};
int marryDuration[] = {500, 500, 500, 500, 500, 700, 700};

int darthMelody[] = {440, 440, 440, 349, 523, 440, 349, 523, 440};
int darthDuration[] = {500, 500, 500, 350, 150, 500, 350, 150, 650};

// ====== ROUTES ======
void handleRoot() {
  String html = R"rawliteral(
    <!DOCTYPE html>
    <html>
    <head>
      <title>ESP32 Buzzer Control</title>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>
        body { font-family: sans-serif; text-align: center; margin-top: 30px; }
        button, input { font-size: 18px; padding: 10px 15px; margin: 5px; }
      </style>
    </head>
    <body>
      <h2>💡 Tone Player</h2>
      <div>
        <button onclick="playTone(262)">C4</button>
        <button onclick="playTone(294)">D4</button>
        <button onclick="playTone(330)">E4</button>
        <button onclick="playTone(349)">F4</button>
        <button onclick="playTone(392)">G4</button>
        <button onclick="playTone(440)">A4</button>
        <button onclick="playTone(494)">B4</button>
        <button onclick="playTone(523)">C5</button>
      </div>

      <h2>🎶 Special Melodies</h2>
      <button onclick="fetch('/melody?type=marry')">💍 Will You Marry Me?</button>
      <button onclick="fetch('/melody?type=darth')">🎵 Darth Vader Theme</button>

      <h2>📡 Morse Code</h2>
      <input id="morseInput" placeholder="-.-. ...- ..- -" size="40">
      <button onclick="sendMorse()">Send</button>
      <button onclick="sendLove()">❤️ Love Message</button>

      <script>
        function playTone(freq) {
          fetch("/play?freq=" + freq);
        }

        function sendMorse() {
          let code = document.getElementById("morseInput").value;
          fetch("/morse?code=" + encodeURIComponent(code));
        }

        function sendLove() {
          let message = ".. / .-.. --- ...- . / -.-- --- ..- / -.-. .- .-. --- .-.. .. -. . .-.-.- / -.-- --- ..- / .- .-. . / -- -.-- / .... --- -- . .-.-.-";
          fetch("/morse?code=" + encodeURIComponent(message));
        }
      </script>
    </body>
    </html>
  )rawliteral";

  server.send(200, "text/html", html);
}

void handlePlayTone() {
  if (server.hasArg("freq")) {
    toneFreq = server.arg("freq").toInt();
    currentCommand = TONE;
    server.send(200, "text/plain", "Tone started");
  } else {
    server.send(400, "text/plain", "Missing frequency");
  }
}

void handleMorse() {
  if (server.hasArg("code")) {
    morseCode = server.arg("code");
    morseIndex = 0;
    currentCommand = MORSE;
    server.send(200, "text/plain", "Morse received");
  } else {
    server.send(400, "text/plain", "Missing code");
  }
}

void handleMelody() {
  if (server.hasArg("type")) {
    String type = server.arg("type");
    melodyIndex = 0;
    if (type == "marry") currentCommand = MELODY_MARRY;
    else if (type == "darth") currentCommand = MELODY_DARTH;
    server.send(200, "text/plain", "Melody started");
  } else {
    server.send(400, "text/plain", "Missing melody type");
  }
}

// ====== SETUP ======
void setup() {
  Serial.begin(115200);
  pinMode(buzzerPin, OUTPUT);

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

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OLED failed");
    while (true);
  }

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Wi‑Fi Connected:");
  display.println(ssid);
  display.println();
  display.println("IP Address:");
  display.println(WiFi.localIP());
  display.display();

  server.on("/", handleRoot);
  server.on("/play", handlePlayTone);
  server.on("/morse", handleMorse);
  server.on("/melody", handleMelody);
  server.begin();
}

// ====== LOOP (REAL‑TIME CONTROL) ======
void loop() {
  server.handleClient();

  if (toneActive && millis() - toneStart >= toneDuration) {
    stopTone();
    return;
  }

  if (!toneActive && millis() >= nextNoteDelayUntil) {
    switch (currentCommand) {
      case TONE:
        startTone(toneFreq, 400);
        currentCommand = NONE;
        break;

      case MORSE:
        if (morseIndex < morseCode.length()) {
          char c = morseCode.charAt(morseIndex++);
          if (c == '.') startTone(800, 150);
          else if (c == '-') startTone(800, 400);
          else if (c == ' ') nextNoteDelayUntil = millis() + 250;
          else if (c == '/') nextNoteDelayUntil = millis() + 600;
        } else {
          currentCommand = NONE;
        }
        break;

      case MELODY_MARRY:
        if (melodyIndex < 7) {
          startTone(marryMelody[melodyIndex], marryDuration[melodyIndex]);
          melodyIndex++;
        } else {
          currentCommand = NONE;
        }
        break;

      case MELODY_DARTH:
        if (melodyIndex < 9) {
          startTone(darthMelody[melodyIndex], darthDuration[melodyIndex]);
          melodyIndex++;
        } else {
          currentCommand = NONE;
        }
        break;

      case NONE:
      default:
        break;
    }
  }
}

Handy tip: my ESP32 board requires pressing its BOOT button at exactly the right moment. To avoid recompiling every time, I switched to the Arduino CLI. The workflow is:

Copied!
# 1) Find all ESP32 boards you have
arduino-cli board listall

# These work for me:
DOIT ESP32 DEVKIT V1    esp32:esp32:esp32doit-devkit-v1
ESP32 Dev Module        esp32:esp32:esp32

# 2) Make sure the IDE’s Serial Monitor is closed, otherwise COM6 will stay locked.

# 3) Check which port your board is on
arduino-cli board list

# 4) Compile (build folder and sketch path are placeholders)
arduino-cli compile `
  --fqbn esp32:esp32:esp32 `
  --build-path "C:\PATH\TO\project\build" `
  "C:\PATH\TO\project"

# 5) Upload the already‑compiled binary
arduino-cli upload `
  -p COM6 `
  -b esp32:esp32:esp32 `
  --input-file "C:\PATH\TO\project\build\project.ino.bin"

Random Number Facts

The second demo pulls trivia from numbersapi.com every ten seconds and shows it on the OLED display:

Wiring diagram (Fritzing again):

Complete sketch:

Copied!
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// === Wi‑Fi credentials ===
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

// === OLED setup ===
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// === Useless fact API ===
const char* apiURL = "http://numbersapi.com/random/trivia";

void showText(String text) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0, 0);

  const int maxWidth = 21;   // characters per line
  const int maxLines = 7;    // number of lines available

  String line = "";
  int lineCount = 0;

  text += " "; // ensure trailing space

  while (text.length() > 0 && lineCount < maxLines) {
    int spaceIndex = text.indexOf(' ');
    if (spaceIndex == -1) break;

    String word = text.substring(0, spaceIndex + 1);
    text = text.substring(spaceIndex + 1);

    if (line.length() + word.length() > maxWidth) {
      display.println(line);
      line = word;
      lineCount++;
    } else {
      line += word;
    }
  }

  if (lineCount < maxLines && line.length() > 0) {
    display.println(line);
  }

  display.display();
}

void fetchAndDisplayFact() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(apiURL);
    int httpCode = http.GET();

    if (httpCode > 0) {
      String fact = http.getString();
      Serial.println(fact);
      showText(fact);
    } else {
      Serial.printf("HTTP error: %d\n", httpCode);
      showText("HTTP error " + String(httpCode));
    }

    http.end();
  } else {
    showText("WiFi not connected");
  }
}

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

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextWrap(true);
  showText("Connecting WiFi...");

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

  Serial.println("\nWiFi connected!");
  showText("WiFi connected!");
  delay(2000);
}

void loop() {
  fetchAndDisplayFact();
  delay(10000); // Update every 10 seconds
}