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:
- Play single notes
- Send whole Morse messages
- Trigger preset melodies (Darth Vader theme or a playful “Will you marry me?”)
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!)
#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:
# 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:
#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
}