An ESP32 learning kit about motion sensing: Start here, then 5 lessons - a button and OLED countdown, tilt angles from a GY-521 motion sensor, a live graph of jolts and drops, a buzzer alert, and finally a fall-detection experiment that starts a cancellable countdown when a jolt is followed by staying tipped over. Each lesson has a wiring diagram, code and troubleshooting. Code compiler-checked for the ESP32; the physical build is not bench-tested. A learning experiment only - not a medical, wheelchair-safety or emergency device.
1×ESP32U DevKitC Type-C
1×GY-521 MPU-6050 6DOF IMU Sensor Module
1×0.96" OLED Display Module 128x64 I2C (White)
1×Tactile Microswitch Kit 25pcs 12x12x7.3mm Momentary Push Button Switches with Assorted Coloured Caps
1×Jumper Wire Assorted Pack 120 Pieces
1×Transparent 830 Points Solderless Breadboard Clear Prototype Circuit Board MB-102 High Quality DIY Electronic Testing Experiment PCB Base with Power Plane
1×Active Buzzer Module 5V DC (optional)| Item | For | Note |
|---|---|---|
| A computer (Windows, macOS or Linux) where you can install the free Arduino IDE 2 and the ESP32 tools | All lessons | |
| A USB-C data cable - not a charge-only cable | All lessons | The ESP32U has a USB-C socket |
| An internet connection, for the IDE, the ESP32 tools and the libraries | Start here, Lesson 1 | |
| A small light box or book, and tape, as a test platform | Lessons 2, 3 and 5 | |
| A cushion or foam pad | Lessons 3 and 5 | Tests only on a padded, unoccupied platform |
| Soldering, by an adult | Only if the GY-521 or OLED pins arrive loose | Adult only |
| A multimeter | Optional | No lesson needs one. Useful for an adult checking 3.3 V and GND. |
| # | Step and what you should see | Done | Date |
|---|---|---|---|
| 1 | Start here: install the Arduino IDE and the ESP32 tools, select ESP32 Dev Module and your port, and upload GetChipID - the chip model appears in the Serial Monitor. | ||
| 2 | Lesson 1 - Button & OLED Countdown (40 min): install Adafruit SSD1306; a button starts and cancels a 20-second countdown on the OLED. | ||
| 3 | Lesson 2 - Tilt Angle from a Baseline (30 min): the GY-521 learns "upright"; the OLED shows the tilt angle and flags 55°. | ||
| 4 | Lesson 3 - Motion Data Lab (30 min): total g on the Serial Plotter; impacts above 2.5 g and free falls below 0.45 g counted. | ||
| 5 | Lesson 4 - Buzzer Alert (25 min): buzzer on GPIO27, powered from 3V3; an alert the button acknowledges, with a 15-second time-out. | ||
| 6 | Lesson 5 - Fall-Detection Learning Prototype (45-60 min): jolt + staying tipped over starts a beeping countdown; padded bench tests only. |
Every lesson uses this one pin plan, so parts wired in one lesson stay where they are for the next. The numbers are the GPIO labels printed on the ESP32 - their position along the edge depends on the board version, so go by the labels.
| ESP32 pin | Connects to | S | L1 | L2 | L3 | L4 | L5 | Note |
|---|---|---|---|---|---|---|---|---|
| 3V3 | + rail of the breadboard | ● | ● | ● | ● | ● | Powers the OLED, GY-521 and buzzer. Never use 5V or VIN for them. | |
| GND | − rail of the breadboard | ● | ● | ● | ● | ● | Shared ground. | |
| GPIO21 | SDA: OLED and GY-521 | ● | ● | ● | ● | ● | I2C data - both modules share it. | |
| GPIO22 | SCL: OLED and GY-521 | ● | ● | ● | ● | ● | I2C clock - both modules share it. | |
| GPIO26 | Push button (other leg to GND) | ● | ● | ● | ● | ● | INPUT_PULLUP - no resistor needed. | |
| GPIO27 | Buzzer I/O | ● | ● | Buzzer VCC goes to 3V3. | ||||
| GPIO2 | On-board LED, if your board has one | ● | ● | Nothing to wire. Some boards have no LED here. | ||||
| GPIO6-11 | Never use | Connected to the ESP32's flash memory. | ||||||
| GPIO0, GPIO12, GPIO15 | Avoid | They decide how the ESP32 starts up. | ||||||
| GPIO4, 5, 13, 14, 16-19, 23, 25, 32, 33 | Free | Spare pins for your own ideas. GPIO34-39 are inputs only. |
S = Start here · L1-L5 = Lessons. The same pins are in every lesson's diagram and code.
Follow these 7 steps before Lesson 1. They take about 45-60 minutes, most of it the one-time ESP32 download, and prove your computer, cable and ESP32 work - so if something goes wrong later, you know it is the wiring, not the setup.
Lessons 1-4 need only the kit, a computer and a USB-C data cable. Lesson 4 needs the buzzer module, an optional add-on - without it, Lesson 5 still works, silently. Lessons 3 and 5 also need a cushion and a small box or book from home. Lesson 5 is an experiment on a padded bench - never on a person or a wheelchair.
| Something is missing or broken | Send us a photo with your order number on WhatsApp. |
| The download or install is blocked | School and work computers often block installs. Ask an adult or use a home computer. |
| "esp32 by Espressif Systems" is not listed | Check the Preferences address is pasted exactly, then close and reopen the Boards Manager. |
| No light at all | Push the plug fully in. Try another USB port, then another cable. |
| The Port menu is empty | Many USB cables only charge. Use a data cable. |
| Windows shows an unknown device | Look at the small chip next to the USB socket: CP2102 or CH340. The computer needs the free driver for that chip - ask us on WhatsApp for the link. |
| Linux: "permission denied" on the port | Ask an adult to add your user to the dialout group (sudo usermod -a -G dialout $USER), then log out and back in. |
| The upload stops at "Connecting....____" | Hold down the BOOT button on the ESP32 when "Connecting" appears, and let go when the percentage starts. |
| "Failed to connect" or "port is busy" | Close the Serial Monitor and any other program using the port, choose the port again (step 5) and retry. |
| A red "Compilation error" | Check Tools → Board is ESP32 Dev Module. Close the example without saving and open it again. |
| The Serial Monitor shows strange symbols | Set the speed to 115200 baud. Press the EN (reset) button to start the sketch again. |
| ESP32U board + your USB-C data cable | 1 |
| 830-point breadboard | 1 |
| 0.96" OLED display | 1 |
| Push button (from the pack of 25) | 1 |
| Female-to-male jumper wires | about 6 |
| Male-to-male jumper wires | about 4 |
The OLED has its own controller chip. The Adafruit SSD1306 library talks to it over I2C on GPIO21 (SDA) and GPIO22 (SCL), so the sketch only says what to draw.
INPUT_PULLUP holds GPIO26 HIGH. Pressing the button connects it to GND, so the pin reads LOW. That is why the button goes between GPIO26 and GND, with no resistor.
Metal contacts bounce for a few milliseconds. pressedNow() only believes a change that lasts 35 ms, so one press counts once.
The countdown never waits: each time round loop() it works out the seconds left from millis(), so the button is read all the time.
| Part / pin | Connect to | Note |
|---|---|---|
| ESP32 3V3 | + rail of the breadboard | NOT 5V or VIN - every part in this kit runs on 3.3 V |
| ESP32 GND | − rail of the breadboard | Shared ground for everything |
| OLED VCC | + rail (3V3) | Go by the labels on the OLED - the pin order varies |
| OLED GND | − rail (GND) | |
| OLED SDA | GPIO21 | I2C data |
| OLED SCL | GPIO22 | I2C clock |
| Button leg | GPIO26 | |
| Button leg diagonally opposite | − rail (GND) | Diagonal legs are never joined inside, so the press always counts |
Easiest: scan the QR code above and press Copy or Download .ino on the lesson page, then paste it into the Arduino IDE and click Upload. The full listing is here for reading along.
/*
DIY KIT - ESP32 Fall-Detection Learning Prototype
Lesson 1 of 5: Button & OLED Countdown
WIRING (unplug the USB cable before you wire)
OLED VCC -> 3V3 GND -> GND SDA -> GPIO21 SCL -> GPIO22
Push button: one leg -> GPIO26, the leg diagonally opposite -> GND
(The ESP32's built-in pull-up keeps GPIO26 HIGH until you press.)
WHAT SHOULD HAPPEN
The OLED says "press to start". Press the button: a 20-second countdown
runs in big numbers. Press again to cancel. If it reaches 0 the screen
says EXPIRED - in Lesson 5 this is where the alert starts.
Serial Monitor at 115200 baud shows the same messages.
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SDA_PIN = 21;
const int SCL_PIN = 22;
const int BUTTON_PIN = 26;
const int LED_PIN = 2; // the small LED on many ESP32 boards; yours may not have one
const int COUNTDOWN_S = 20; // seconds - try 5 while testing
const unsigned long DEBOUNCE_MS = 35;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayOk = false;
bool counting = false;
unsigned long countdownStart = 0;
int lastShown = -1;
bool rawButton = HIGH, stableButton = HIGH;
unsigned long rawSince = 0;
// True once each time the button is pressed (not while it is held).
// A contact "bounces" for a few ms, so we wait DEBOUNCE_MS before believing it.
bool pressedNow() {
bool reading = digitalRead(BUTTON_PIN);
if (reading != rawButton) {
rawButton = reading;
rawSince = millis();
}
if (millis() - rawSince > DEBOUNCE_MS && reading != stableButton) {
stableButton = reading;
return stableButton == LOW; // pull-up: LOW means pressed
}
return false;
}
// Most of these displays answer at I2C address 0x3C, some at 0x3D. Try both.
bool startDisplay() {
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("OLED found at 0x3C"); return true; }
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { Serial.println("OLED found at 0x3D"); return true; }
Serial.println("OLED not found - unplug USB and check VCC, GND, SDA (GPIO21), SCL (GPIO22)");
return false;
}
// Two lines of small text and, if seconds >= 0, a big number.
void show(const char *line1, const char *line2, int seconds) {
if (!displayOk) return;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("COUNTDOWN LAB");
display.println(line1);
if (seconds >= 0) {
display.setTextSize(3);
display.setCursor(0, 22);
display.println(seconds);
}
display.setTextSize(1);
display.setCursor(0, 56);
display.println(line2);
display.display(); // nothing appears until display() is called
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Wire.begin(SDA_PIN, SCL_PIN);
displayOk = startDisplay();
show("ready", "press to start", -1);
Serial.println("Lesson 1: press the button to start the countdown, press again to cancel");
}
void loop() {
if (pressedNow()) {
if (counting) {
counting = false;
Serial.println("CANCELLED");
show("cancelled", "press to start", -1);
} else {
counting = true;
countdownStart = millis();
lastShown = -1;
Serial.println("COUNTDOWN started");
}
}
if (counting) {
unsigned long used = millis() - countdownStart;
int remaining = COUNTDOWN_S - (int)(used / 1000);
unsigned long quarterSeconds = millis() / 250;
digitalWrite(LED_PIN, quarterSeconds % 2 == 0 ? HIGH : LOW); // blink while counting
if (remaining <= 0) {
counting = false;
digitalWrite(LED_PIN, LOW);
Serial.println("EXPIRED");
show("EXPIRED", "press to start again", 0);
} else if (remaining != lastShown) {
lastShown = remaining;
Serial.print("seconds left: ");
Serial.println(remaining);
show("counting down", "press to cancel", remaining);
}
} else {
digitalWrite(LED_PIN, LOW);
}
delay(5);
}
| const int BUTTON_PIN = 26; | Names for the pins. If you move a wire, change only this number. |
| pinMode(BUTTON_PIN, INPUT_PULLUP); | Turns on the built-in pull-up: the pin reads HIGH until the button connects it to GND. |
| pressedNow() | Our own function: true once for each press, after the contact has been steady for 35 ms. |
| startDisplay() | Tries the OLED at address 0x3C, then 0x3D, and says which one answered. |
| COUNTDOWN_S - (used / 1000) | Seconds left, worked out from millis() - nothing waits. |
| "OLED not found" or a black screen | Unplug USB. Check OLED VCC to 3V3 (not 5V), GND, SDA to GPIO21 and SCL to GPIO22 - by the labels printed on the OLED. |
| The button does nothing | Unplug USB. Move one wire to the leg diagonally opposite: two legs on the same side can be joined inside the button. |
| The countdown starts by itself | GPIO26 is not wired to GND through the button, or a wire is loose. Check the button legs. |
| "Adafruit_SSD1306.h: No such file" | Install the library: step 1. |
| Nothing, or strange symbols, in the Serial Monitor | Set the speed in the corner of the Serial Monitor to 115200 baud. |
| The upload fails or stops at "Connecting..." | See Start here, step 6. |
Hold the button down. Does the countdown start when you press, or when you let go? Look at pressedNow() and explain why.
| Wiring from Lesson 1 | |
| GY-521 (MPU-6050) motion sensor | 1 |
| Male-to-male jumper wires | 4 |
Sitting still, the accelerometer feels only gravity: 1 g pointing down. Which of its x, y and z axes feel it tells us which way the sensor is facing.
For 3 seconds the sketch averages 150 readings while you keep it still. That direction becomes "upright", however the sensor is mounted.
After that it compares each new reading with "upright" and works out the angle between them (with acos of the dot product). 0° = as at the start, 90° = lying on its side.
The OLED and the GY-521 share SDA and SCL. They do not get mixed up because each has its own address.
| Part / pin | Connect to | Note |
|---|---|---|
| GY-521 VCC | + rail (3V3) | |
| GY-521 GND | − rail (GND) | |
| GY-521 SDA | GPIO21 | Same wire row as the OLED SDA |
| GY-521 SCL | GPIO22 | Same wire row as the OLED SCL |
| GY-521 XDA, XCL, AD0, INT | Nothing | Leave empty |
| OLED, button | As in Lesson 1 | GPIO21, GPIO22, GPIO26 |
Easiest: scan the QR code above and press Copy or Download .ino on the lesson page, then paste it into the Arduino IDE and click Upload. The full listing is here for reading along.
/*
DIY KIT - ESP32 Fall-Detection Learning Prototype
Lesson 2 of 5: Tilt Angle from a Baseline
WIRING (unplug the USB cable before you wire)
Keep the OLED and button from Lesson 1.
GY-521 (MPU-6050) VCC -> 3V3 GND -> GND SDA -> GPIO21 SCL -> GPIO22
The OLED and the GY-521 share the same two I2C wires (SDA and SCL).
Leave XDA, XCL, AD0 and INT on the GY-521 unconnected.
WHAT SHOULD HAPPEN
Keep the sensor still for 3 seconds while it learns "upright". Then the
OLED shows the angle you have tilted it from that position. Past 55
degrees it says TILTED. Press the button to learn a new "upright".
Serial Monitor (115200) or Serial Plotter shows angle_deg.
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SDA_PIN = 21;
const int SCL_PIN = 22;
const int BUTTON_PIN = 26;
const float TILT_DEG = 55.0; // the "lying down" angle used in Lesson 5
const int CALIBRATION_SAMPLES = 150; // 150 x 20 ms = 3 seconds
const unsigned long SAMPLE_MS = 20;
const unsigned long REPORT_MS = 250;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayOk = false;
int mpuAddress = 0; // 0x68 or 0x69, found at start
float baseX = 0, baseY = 0, baseZ = 1; // the "upright" direction of gravity
unsigned long lastSample = 0, lastReport = 0;
bool lastButton = HIGH;
bool answers(int address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
// Read the three accelerometer axes, in g. 1 g = the pull of gravity.
bool readAccel(float &x, float &y, float &z) {
Wire.beginTransmission(mpuAddress);
Wire.write(0x3B); // the first acceleration register
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(mpuAddress, 6) != 6) return false;
int16_t rx = (int16_t)((Wire.read() << 8) | Wire.read());
int16_t ry = (int16_t)((Wire.read() << 8) | Wire.read());
int16_t rz = (int16_t)((Wire.read() << 8) | Wire.read());
x = rx / 16384.0; // +-2 g range: 16384 counts per g
y = ry / 16384.0;
z = rz / 16384.0;
return true;
}
void show(const char *line1, int angle) {
if (!displayOk) return;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("TILT ANGLE LAB");
display.println(line1);
if (angle >= 0) {
display.setTextSize(3);
display.setCursor(0, 22);
display.print(angle);
display.setTextSize(2);
display.print(" deg");
}
display.setTextSize(1);
display.setCursor(0, 56);
display.print("limit ");
display.print((int)TILT_DEG);
display.print(" btn=reset"); // "limit 55 btn=reset": 21 characters fit on a line
display.display();
}
// Average 150 readings while the sensor is still: that is "upright".
bool calibrate() {
show("KEEP STILL: 3 s", -1);
Serial.println("Calibrating - keep the sensor still");
float sx = 0, sy = 0, sz = 0;
int good = 0;
for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
float x, y, z;
if (readAccel(x, y, z)) {
sx += x; sy += y; sz += z;
good++;
}
delay(SAMPLE_MS);
}
float length = sqrt(sx * sx + sy * sy + sz * sz);
if (good == 0 || length < 0.001) return false;
baseX = sx / length; // keep only the direction
baseY = sy / length;
baseZ = sz / length;
Serial.println("Calibrated");
return true;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Wire.begin(SDA_PIN, SCL_PIN);
displayOk = display.begin(SSD1306_SWITCHCAPVCC, 0x3C) || display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
if (!displayOk) Serial.println("OLED not found - see Lesson 1");
if (answers(0x68)) mpuAddress = 0x68;
else if (answers(0x69)) mpuAddress = 0x69;
if (mpuAddress == 0) {
Serial.println("GY-521 not found - unplug USB and check VCC 3V3, GND, SDA GPIO21, SCL GPIO22");
show("GY-521 NOT FOUND", -1);
while (true) delay(1000); // stop here
}
Serial.print("GY-521 found at 0x");
Serial.println(mpuAddress, HEX);
Wire.beginTransmission(mpuAddress); // wake the sensor up (it starts asleep)
Wire.write(0x6B);
Wire.write(0x00);
Wire.endTransmission();
delay(50);
if (!calibrate()) Serial.println("Calibration failed - is the sensor wired?");
}
void loop() {
bool button = digitalRead(BUTTON_PIN);
if (button == LOW && lastButton == HIGH) calibrate(); // press: learn a new "upright"
lastButton = button;
if (millis() - lastSample < SAMPLE_MS) return;
lastSample = millis();
float x, y, z;
if (!readAccel(x, y, z)) return;
float length = sqrt(x * x + y * y + z * z);
if (length < 0.001) return;
// The angle between "upright" and gravity now: the dot product of the two directions.
float dot = (x * baseX + y * baseY + z * baseZ) / length;
float angle = acos(constrain(dot, -1.0, 1.0)) * 180.0 / PI;
if (millis() - lastReport >= REPORT_MS) {
lastReport = millis();
Serial.print("angle_deg:");
Serial.print(angle, 1);
Serial.print(",limit:");
Serial.println(TILT_DEG, 0);
show(angle >= TILT_DEG ? "TILTED past the limit" : "upright", (int)angle);
}
}
| readAccel(x, y, z) | Reads the 6 bytes of acceleration from the GY-521 over I2C and turns them into g. |
| calibrate() | Averages 150 readings and keeps only the direction: the "upright" baseline. |
| acos(dot) * 180 / PI | The angle between "upright" and gravity now, in degrees. |
| answers(0x68) | Asks "is anyone at address 0x68?" - that is how the sketch finds the sensor. |
| "GY-521 not found" | Unplug USB. Check VCC to 3V3, GND, SDA to GPIO21 and SCL to GPIO22. Are the GY-521 header pins soldered? Loose pins pushed through the holes do not make contact. |
| The angle never comes back to 0 | It moved during the 3 seconds. Press the button and keep still. |
| The angle jumps about | Tape the sensor down firmly - a loose sensor wobbles. |
| "OLED not found" or a black screen | Unplug USB. Check OLED VCC to 3V3 (not 5V), GND, SDA to GPIO21 and SCL to GPIO22 - by the labels printed on the OLED. |
| Nothing, or strange symbols, in the Serial Monitor | Set the speed in the corner of the Serial Monitor to 115200 baud. |
Calibrate, then turn the book upside down. What angle does it show? Why is 180° the biggest angle you can get?
| Wiring from Lesson 2 - nothing new | |
| A book or card with the breadboard taped on, and a cushion (yours) |
The sketch combines x, y and z into one number, the total g: about 1.0 when still, whichever way the sensor faces.
It sets the sensor to ±8 g so a hard landing does not go off the scale (Lesson 2 used ±2 g for finer angles).
A crossing is counted once: only when total g goes above 2.5 g (or below 0.45 g) after being on the other side.
The Serial Plotter draws total_g with the two limits as flat lines, so you can see how close each movement gets.
| Part / pin | Connect to | Note |
|---|---|---|
| No changes | Same as Lesson 2 | GY-521 and OLED on GPIO21/22, button on GPIO26 |
Easiest: scan the QR code above and press Copy or Download .ino on the lesson page, then paste it into the Arduino IDE and click Upload. The full listing is here for reading along.
/*
DIY KIT - ESP32 Fall-Detection Learning Prototype
Lesson 3 of 5: Motion Data Lab
WIRING: the same as Lesson 2 (OLED and GY-521 on GPIO21/22, button on GPIO26).
WHAT SHOULD HAPPEN
Open Tools > Serial Plotter at 115200 baud. The total_g line sits at about
1.0 while the sensor is still. Lift and drop the test platform gently onto
foam: total_g dips towards 0 while it falls and spikes when it lands.
The OLED counts every IMPACT (above 2.5 g) and FREE FALL (below 0.45 g).
Press the button to set the counts back to 0.
Use a padded, unoccupied test platform - never a person.
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SDA_PIN = 21;
const int SCL_PIN = 22;
const int BUTTON_PIN = 26;
const float IMPACT_G = 2.5; // the same limits as Lesson 5
const float FREEFALL_G = 0.45;
const unsigned long SAMPLE_MS = 20; // 50 readings a second
const unsigned long REPORT_MS = 100; // 10 plotter points a second
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayOk = false;
int mpuAddress = 0;
int impacts = 0, freefalls = 0;
bool wasImpact = false, wasFreefall = false;
float peakG = 0, lowestG = 10;
unsigned long lastSample = 0, lastReport = 0;
bool lastButton = HIGH;
bool answers(int address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
bool writeRegister(int reg, int value) {
Wire.beginTransmission(mpuAddress);
Wire.write(reg);
Wire.write(value);
return Wire.endTransmission() == 0;
}
bool readAccel(float &x, float &y, float &z) {
Wire.beginTransmission(mpuAddress);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(mpuAddress, 6) != 6) return false;
int16_t rx = (int16_t)((Wire.read() << 8) | Wire.read());
int16_t ry = (int16_t)((Wire.read() << 8) | Wire.read());
int16_t rz = (int16_t)((Wire.read() << 8) | Wire.read());
x = rx / 4096.0; // +-8 g range: 4096 counts per g
y = ry / 4096.0;
z = rz / 4096.0;
return true;
}
void showCounts(float g) {
if (!displayOk) return;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("MOTION DATA LAB");
display.setTextSize(2);
display.setCursor(0, 12);
display.print(g, 2);
display.println(" g");
display.setTextSize(1);
display.setCursor(0, 34);
display.print("impacts "); display.println(impacts);
display.print("free falls "); display.println(freefalls);
display.print("peak "); display.print(peakG, 1); display.print(" low "); display.println(lowestG, 2);
display.display();
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Wire.begin(SDA_PIN, SCL_PIN);
displayOk = display.begin(SSD1306_SWITCHCAPVCC, 0x3C) || display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
if (answers(0x68)) mpuAddress = 0x68;
else if (answers(0x69)) mpuAddress = 0x69;
if (mpuAddress == 0 || !writeRegister(0x6B, 0x00) || !writeRegister(0x1C, 0x10)) { // wake up, +-8 g
Serial.println("GY-521 not found - see Lesson 2");
while (true) delay(1000);
}
Serial.println("Lesson 3: open the Serial Plotter");
}
void loop() {
bool button = digitalRead(BUTTON_PIN);
if (button == LOW && lastButton == HIGH) { // press: reset the counts
impacts = 0; freefalls = 0; peakG = 0; lowestG = 10;
}
lastButton = button;
if (millis() - lastSample < SAMPLE_MS) return;
lastSample = millis();
float x, y, z;
if (!readAccel(x, y, z)) return;
float g = sqrt(x * x + y * y + z * z); // total g-force, whichever way it points
if (g > peakG) peakG = g;
if (g < lowestG) lowestG = g;
// Count each crossing once, not every reading above the line.
bool impact = g > IMPACT_G;
if (impact && !wasImpact) impacts++;
wasImpact = impact;
bool freefall = g < FREEFALL_G;
if (freefall && !wasFreefall) freefalls++;
wasFreefall = freefall;
if (millis() - lastReport >= REPORT_MS) {
lastReport = millis();
Serial.print("total_g:");
Serial.print(g, 2);
Serial.print(",impact_line:");
Serial.print(IMPACT_G, 2);
Serial.print(",freefall_line:");
Serial.println(FREEFALL_G, 2);
showCounts(g);
}
}
| writeRegister(0x1C, 0x10) | Sets the sensor's range to ±8 g: 4096 counts per g. |
| sqrt(x*x + y*y + z*z) | The total g-force, whichever way the sensor points. |
| if (impact && !wasImpact) impacts++; | Counts a crossing only when it starts, not every reading above the line. |
| Serial.print("total_g:") | "name:value" pairs are what the Serial Plotter draws as labelled lines. |
| "GY-521 not found" | Unplug USB. Check VCC to 3V3, GND, SDA to GPIO21 and SCL to GPIO22. Are the GY-521 header pins soldered? Loose pins pushed through the holes do not make contact. |
| The Plotter shows nothing | Close the Serial Monitor first, then open the Plotter at 115200 baud. |
| Nothing ever crosses the impact line | A soft cushion slows the landing. Try a firmer pad, or lower IMPACT_G to see the data. |
| Counts go up while it sits still | A loose wire or loose sensor. Tape everything down and check SDA and SCL. |
Drop the platform 5 times from 10 cm and 5 times from 20 cm. Write down the peak g each time. Does a higher drop always give a bigger spike?
| Wiring from Lessons 1-3 | |
| Active buzzer module | 1 |
| Female-to-male jumper wires | 3 |
The module has a small transistor that switches the buzzer. It is sold as a 5 V module, but here its VCC goes to 3V3: then the ESP32's 3.3 V signal can switch it fully on and off. The beep is quieter than at 5 V.
Powering it from 5V could leave it beeping all the time, because a 3.3 V signal may not be enough to switch a 5 V module off. That is why this kit uses 3V3.
used % 500 says where we are in each half-second: the first 150 ms are on, the other 350 ms off. The pattern repeats without any delay().
An alert that cannot be stopped is a problem too: the button acknowledges it, and after 15 seconds it stops by itself.
| Part / pin | Connect to | Note |
|---|---|---|
| Buzzer I/O (signal) | GPIO27 | May be labelled S or IN |
| Buzzer VCC | + rail (3V3) | Not 5V - see "How it works" |
| Buzzer GND | − rail (GND) | |
| OLED, GY-521, button | As in Lessons 1-3 | GPIO21, GPIO22, GPIO26 |
Easiest: scan the QR code above and press Copy or Download .ino on the lesson page, then paste it into the Arduino IDE and click Upload. The full listing is here for reading along.
/*
DIY KIT - ESP32 Fall-Detection Learning Prototype
Lesson 4 of 5: Buzzer Alert
WIRING (unplug the USB cable before you wire)
Keep everything from Lessons 1-3.
Active buzzer module, with 3 female-to-male jumper wires:
I/O (signal, may be labelled S or IN) -> GPIO27
VCC -> 3V3 (NOT 5V - see the lesson)
GND -> GND
WHAT SHOULD HAPPEN
Three short test beeps. Then press the button to start an ALERT: the
buzzer beeps 150 ms on, 350 ms off and the OLED says ALERT. Press again
to acknowledge (stop) it. If nobody presses, it stops by itself after
15 seconds. Serial Monitor at 115200 baud.
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SDA_PIN = 21;
const int SCL_PIN = 22;
const int BUTTON_PIN = 26;
const int BUZZER_PIN = 27;
const unsigned long BEEP_ON_MS = 150;
const unsigned long BEEP_OFF_MS = 350;
const unsigned long MAX_ALERT_MS = 15000; // an alarm must not run for ever
const unsigned long DEBOUNCE_MS = 35;
// Most modules beep when the pin is HIGH. If yours beeps in the gaps or
// all the time, change false to true and upload again.
const bool BUZZER_ACTIVE_LOW = false;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayOk = false;
bool alerting = false;
unsigned long alertStart = 0;
bool rawButton = HIGH, stableButton = HIGH;
unsigned long rawSince = 0;
void buzzer(bool on) {
if (BUZZER_ACTIVE_LOW) digitalWrite(BUZZER_PIN, on ? LOW : HIGH);
else digitalWrite(BUZZER_PIN, on ? HIGH : LOW);
}
bool pressedNow() {
bool reading = digitalRead(BUTTON_PIN);
if (reading != rawButton) {
rawButton = reading;
rawSince = millis();
}
if (millis() - rawSince > DEBOUNCE_MS && reading != stableButton) {
stableButton = reading;
return stableButton == LOW;
}
return false;
}
void show(const char *line1, const char *line2) {
if (!displayOk) return;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BUZZER ALERT LAB");
display.setTextSize(2);
display.setCursor(0, 20);
display.println(line1);
display.setTextSize(1);
display.setCursor(0, 56);
display.println(line2);
display.display();
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
buzzer(false);
Wire.begin(SDA_PIN, SCL_PIN);
displayOk = display.begin(SSD1306_SWITCHCAPVCC, 0x3C) || display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
Serial.println("Lesson 4: three test beeps");
for (int i = 0; i < 3; i++) {
buzzer(true); delay(100);
buzzer(false); delay(200);
}
show("READY", "press to start alert");
Serial.println("Press the button to start an alert, press again to stop it");
}
void loop() {
bool pressed = pressedNow();
unsigned long now = millis();
if (!alerting) {
if (pressed) {
alerting = true;
alertStart = now;
Serial.println("ALERT started");
show("ALERT", "press to acknowledge");
}
} else {
unsigned long used = now - alertStart;
if (pressed || used >= MAX_ALERT_MS) {
alerting = false;
buzzer(false);
Serial.println(pressed ? "ACKNOWLEDGED" : "TIMED OUT after 15 s");
show(pressed ? "STOPPED" : "TIMED OUT", "press to start alert");
} else {
// Where are we in one 500 ms beep cycle? The first 150 ms are "on".
unsigned long phase = used % (BEEP_ON_MS + BEEP_OFF_MS);
buzzer(phase < BEEP_ON_MS);
}
}
delay(2);
}
| const int BUZZER_PIN = 27; | The buzzer's signal pin. |
| const bool BUZZER_ACTIVE_LOW = false; | Most modules beep when the pin is HIGH. If yours beeps in the gaps or all the time, change it to true. |
| used % (BEEP_ON_MS + BEEP_OFF_MS) | Where we are in each 500 ms cycle; under 150 means "beep". |
| MAX_ALERT_MS = 15000 | The time-out: the alert stops by itself after 15 seconds. |
| No sound at all | Unplug USB. Check I/O to GPIO27, VCC to 3V3 and GND, by the labels. Some 5 V modules are very quiet or silent at 3.3 V - tell us if yours is. |
| It beeps in the gaps, or all the time | Your module beeps when the pin is LOW. Set BUZZER_ACTIVE_LOW to true and upload again. |
| Only a click, no beep | It may be a passive buzzer. Check the label says active, or ask us. |
| "OLED not found" or a black screen | Unplug USB. Check OLED VCC to 3V3 (not 5V), GND, SDA to GPIO21 and SCL to GPIO22 - by the labels printed on the OLED. |
| The upload fails or stops at "Connecting..." | See Start here, step 6. |
Time how long the alert runs before TIMED OUT with a stopwatch. Then change MAX_ALERT_MS and check again.
| Everything from Lessons 1-4 (the buzzer only if you have it) | all |
| Light test platform and cushion (yours) |
At start it finds one OLED and one GY-521, then learns "upright" for 3 seconds (Lesson 2). If anything is missing it shows FAULT and stops, rather than pretending to work.
MONITORING reads the sensor 50 times a second. A total above 2.5 g or below 0.45 g (Lesson 3) makes a candidate.
Within 5 seconds the platform must stay tipped more than 55° from upright for 1.5 seconds. Only then does the 20-second countdown start, beeping once a second.
A press cancels the countdown. If nobody presses, LOCAL ALERT beeps (Lesson 4; silent without the buzzer) until the button acknowledges it. Nothing is sent anywhere: no Wi-Fi, no message, no call.
| Part / pin | Connect to | Note |
|---|---|---|
| OLED and GY-521 VCC / GND | + rail (3V3) / − rail | Lessons 1-2 |
| OLED and GY-521 SDA / SCL | GPIO21 / GPIO22 | Shared I2C |
| Button | GPIO26 and GND | Lesson 1 |
| Buzzer I/O / VCC / GND | GPIO27 / 3V3 / GND | Lesson 4 |
| On-board LED (if your board has one) | GPIO2 | Nothing to wire |
Easiest: scan the QR code above and press Copy or Download .ino on the lesson page, then paste it into the Arduino IDE and click Upload. The full listing is here for reading along.
/*
DIY KIT - ESP32 Fall-Detection Learning Prototype
Lesson 5 of 5: Fall-Detection Learning Prototype (the main build)
WIRING: everything from Lessons 1-4.
OLED and GY-521: VCC -> 3V3, GND -> GND, SDA -> GPIO21, SCL -> GPIO22
Button: GPIO26 and GND Buzzer: I/O -> GPIO27, VCC -> 3V3, GND -> GND
WHAT SHOULD HAPPEN
KEEP STILL AND LEVEL for 3 seconds (it learns "upright"), then MONITORING.
A jolt above 2.5 g or a drop below 0.45 g, followed by staying tipped more
than 55 degrees for 1.5 seconds, starts a 20-second countdown with a beep
every second. Press the button to cancel. If nobody does, LOCAL ALERT:
the buzzer beeps until you press the button. Nothing is sent anywhere.
Serial Monitor at 115200 baud.
Set FALL_DIAGNOSTIC to true to start the countdown with the button
instead of a movement - handy for testing the screen and buzzer.
An educational bench prototype. NOT a medical, wheelchair, emergency or
life-safety device. Test only on a padded, unoccupied platform.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#ifndef FALL_DIAGNOSTIC
#define FALL_DIAGNOSTIC false
#endif
constexpr uint8_t SDA_PIN = 21;
constexpr uint8_t SCL_PIN = 22;
constexpr uint8_t BUTTON_PIN = 26;
constexpr uint8_t BUZZER_PIN = 27; // Lesson 4
constexpr uint8_t LED_PIN = 2; // Common ESP32 Dev Module onboard LED; verify board.
// Most buzzer modules beep when the pin is HIGH. If yours beeps in the gaps
// or all the time, change false to true (see Lesson 4).
constexpr bool BUZZER_ACTIVE_LOW = false;
constexpr uint32_t SAMPLE_MS = 20;
constexpr uint32_t DEBOUNCE_MS = 35;
constexpr uint32_t CANDIDATE_MS = 5000;
constexpr uint32_t TILT_HOLD_MS = 1500;
constexpr uint32_t COUNTDOWN_MS = 20000;
constexpr uint32_t DISPLAY_MS = 250;
constexpr float IMPACT_G = 2.5f;
constexpr float FREEFALL_G = 0.45f;
constexpr float TILT_DEG = 55.0f;
Adafruit_SSD1306 display(128, 64, &Wire, -1, 100000, 100000);
enum class State { Monitoring, Candidate, Countdown, Alert, Fault };
State state = State::Fault;
uint8_t mpuAddress = 0, oledAddress = 0;
float baseX = 0, baseY = 0, baseZ = 1;
uint32_t stateSince = 0, tiltSince = 0, lastSample = 0, lastDisplay = 0;
uint32_t events = 0;
uint8_t readFailures = 0;
bool rawButton = HIGH, stableButton = HIGH, buttonArmed = false;
uint32_t rawSince = 0;
void buzzer(bool on) {
if (BUZZER_ACTIVE_LOW) digitalWrite(BUZZER_PIN, on ? LOW : HIGH);
else digitalWrite(BUZZER_PIN, on ? HIGH : LOW);
}
bool elapsed(uint32_t now, uint32_t since, uint32_t interval) {
return uint32_t(now - since) >= interval;
}
bool i2cAck(uint8_t address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
bool writeRegister(uint8_t reg, uint8_t value) {
Wire.beginTransmission(mpuAddress);
Wire.write(reg);
Wire.write(value);
return Wire.endTransmission() == 0;
}
bool readAccel(float &x, float &y, float &z) {
Wire.beginTransmission(mpuAddress);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(int(mpuAddress), 6) != 6) return false;
int16_t rx = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
int16_t ry = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
int16_t rz = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
x = rx / 4096.0f;
y = ry / 4096.0f;
z = rz / 4096.0f;
return true;
}
void show(const __FlashStringHelper *line1, const String &line2 = "") {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(F("FALL-DETECTION LAB"));
display.println(F("TRAINING ONLY"));
display.println();
display.println(line1);
display.println(line2);
display.display();
}
void fault(const __FlashStringHelper *reason) {
state = State::Fault;
buzzer(false);
digitalWrite(LED_PIN, HIGH);
Serial.print(F("FAULT: "));
Serial.println(reason);
if (oledAddress && i2cAck(oledAddress)) show(F("FAULT - POWER OFF"), String(reason));
}
bool initialiseDevices() {
bool oled3c = i2cAck(0x3C), oled3d = i2cAck(0x3D);
if (oled3c == oled3d) return false;
oledAddress = oled3c ? 0x3C : 0x3D;
if (!display.begin(SSD1306_SWITCHCAPVCC, oledAddress, true, false)) return false;
bool mpu68 = i2cAck(0x68), mpu69 = i2cAck(0x69);
if (mpu68 == mpu69) return false;
mpuAddress = mpu68 ? 0x68 : 0x69;
if (!writeRegister(0x6B, 0x01)) return false;
delay(50);
if (!writeRegister(0x1C, 0x10)) return false;
if (!writeRegister(0x1A, 0x04)) return false;
Serial.printf("OLED=0x%02X MPU=0x%02X\n", oledAddress, mpuAddress);
return true;
}
bool calibrateBaseline() {
show(F("KEEP STILL AND LEVEL"), F("Calibrating 3 seconds"));
float sx = 0, sy = 0, sz = 0;
constexpr int samples = 150;
for (int i = 0; i < samples; ++i) {
float x, y, z;
if (!readAccel(x, y, z)) return false;
float mag = sqrtf(x * x + y * y + z * z);
if (mag < 0.75f || mag > 1.25f) return false;
sx += x; sy += y; sz += z;
delay(20);
}
float mag = sqrtf(sx * sx + sy * sy + sz * sz);
if (mag < 0.001f) return false;
baseX = sx / mag; baseY = sy / mag; baseZ = sz / mag;
return true;
}
float angleFromBaseline(float x, float y, float z, float mag) {
if (mag < 0.001f) return 180.0f;
float dot = (x * baseX + y * baseY + z * baseZ) / mag;
return acosf(constrain(dot, -1.0f, 1.0f)) * 180.0f / PI;
}
bool pressedEdge(uint32_t now) {
bool reading = digitalRead(BUTTON_PIN);
if (reading != rawButton) {
rawButton = reading;
rawSince = now;
}
bool edge = false;
if (reading != stableButton && elapsed(now, rawSince, DEBOUNCE_MS)) {
stableButton = reading;
edge = stableButton == LOW;
Serial.println(edge ? F("Button pressed") : F("Button released"));
}
if (stableButton == HIGH && rawButton == HIGH && elapsed(now, rawSince, DEBOUNCE_MS))
buttonArmed = true;
if (edge && buttonArmed) {
buttonArmed = false;
return true;
}
return false;
}
void enterCountdown(uint32_t now, const __FlashStringHelper *source) {
state = State::Countdown;
stateSince = now;
++events;
Serial.print(F("Countdown started: "));
Serial.println(source);
show(F("EVENT? 20 SECONDS"), F("Button = cancel"));
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(BUZZER_PIN, OUTPUT);
buzzer(false);
rawButton = stableButton = digitalRead(BUTTON_PIN);
rawSince = millis();
if (!Wire.begin(SDA_PIN, SCL_PIN, 100000)) {
fault(F("I2C start failed"));
return;
}
Wire.setTimeOut(20);
if (!initialiseDevices()) {
fault(F("Need one OLED and one MPU"));
return;
}
if (!calibrateBaseline()) {
fault(F("Calibration failed"));
return;
}
state = State::Monitoring;
stateSince = millis();
show(F("MONITORING"), FALL_DIAGNOSTIC ? F("Button starts test") : F("Local demo only"));
Serial.println(F("Local demo only: no Wi-Fi, no messages, no emergency call."));
}
void loop() {
uint32_t now = millis();
bool button = pressedEdge(now);
if (state == State::Fault) return;
if (button && state == State::Countdown) {
state = State::Monitoring;
stateSince = now;
digitalWrite(LED_PIN, LOW);
buzzer(false);
show(F("CANCELLED"), F("Monitoring resumed"));
Serial.println(F("Countdown cancelled"));
return;
}
if (button && state == State::Alert) {
state = State::Monitoring;
stateSince = now;
digitalWrite(LED_PIN, LOW);
buzzer(false);
show(F("ACKNOWLEDGED"), F("Monitoring resumed"));
Serial.println(F("Local alert acknowledged"));
return;
}
if (button && FALL_DIAGNOSTIC && state == State::Monitoring) {
enterCountdown(now, F("diagnostic button"));
return;
}
if (state == State::Countdown) {
uint32_t used = now - stateSince;
if (used >= COUNTDOWN_MS) {
state = State::Alert;
stateSince = now;
buzzer(false);
show(F("LOCAL ALERT"), F("Press to acknowledge"));
Serial.println(F("LOCAL ALERT ONLY - no message was sent"));
return;
}
digitalWrite(LED_PIN, ((now / 300) & 1) ? HIGH : LOW);
buzzer((used % 1000) < 80); // a short beep at the start of every second
if (elapsed(now, lastDisplay, DISPLAY_MS)) {
lastDisplay = now;
uint32_t seconds = (COUNTDOWN_MS - used + 999) / 1000;
show(F("POSSIBLE EVENT"), String(seconds) + F(" s - button cancels"));
}
return;
}
if (state == State::Alert) {
digitalWrite(LED_PIN, ((now / 150) & 1) ? HIGH : LOW);
buzzer(((now - stateSince) % 500) < 150); // 150 ms on, 350 ms off - as in Lesson 4
return;
}
if (!elapsed(now, lastSample, SAMPLE_MS)) return;
lastSample = now;
float x, y, z;
if (!readAccel(x, y, z)) {
if (++readFailures >= 5) fault(F("MPU read failed"));
return;
}
readFailures = 0;
float mag = sqrtf(x * x + y * y + z * z);
float tilt = angleFromBaseline(x, y, z, mag);
if (state == State::Monitoring) {
if (mag > IMPACT_G || mag < FREEFALL_G) {
state = State::Candidate;
stateSince = now;
tiltSince = 0;
Serial.printf("Candidate: %.2f g, %.1f deg\n", mag, tilt);
} else if (elapsed(now, lastDisplay, 1000)) {
lastDisplay = now;
show(F("MONITORING"), String(mag, 2) + F(" g events ") + events);
}
return;
}
if (state == State::Candidate) {
if (elapsed(now, stateSince, CANDIDATE_MS)) {
state = State::Monitoring;
tiltSince = 0;
show(F("NO EVENT"), F("Monitoring resumed"));
return;
}
if (tilt >= TILT_DEG) {
if (tiltSince == 0) tiltSince = now;
if (elapsed(now, tiltSince, TILT_HOLD_MS)) enterCountdown(now, F("motion rule"));
} else {
tiltSince = 0;
}
}
}
| enum class State { Monitoring, Candidate, Countdown, Alert, Fault }; | The five states the prototype can be in. |
| if (mag > IMPACT_G || mag < FREEFALL_G) | Step 1 of the rule: a jolt or a drop makes a candidate. |
| if (tilt >= TILT_DEG) ... TILT_HOLD_MS | Step 2: it must stay tipped past 55° for 1.5 seconds within CANDIDATE_MS. |
| buzzer((used % 1000) < 80); | A short beep at the start of every second of the countdown. |
| fault(...) | Stops in a clear FAULT state when a device is missing or reads fail 5 times in a row. |
| #define FALL_DIAGNOSTIC false | true = the button starts a countdown, to test the screen and buzzer without moving anything. |
| "FAULT - POWER OFF" and "Need one OLED and one MPU" | One device did not answer. Unplug USB and check both on GPIO21/22 and 3V3 - see Lessons 1 and 2. |
| "FAULT - POWER OFF" and "Calibration failed" | It moved during the 3 seconds, or the sensor is loose. Keep it still and press the EN (reset) button on the ESP32. |
| It never starts a countdown | The knock must cross 2.5 g (or a drop 0.45 g) AND it must stay tipped past 55° for 1.5 s. Check with Lesson 3 what your knocks reach. |
| Countdowns start by themselves | Loose mounting or wires. Tape everything down. Record it - false alarms are part of the lesson. |
| It beeps in the gaps, or all the time | Set BUZZER_ACTIVE_LOW to true (see Lesson 4). |
| The upload fails or stops at "Connecting..." | See Start here, step 6. |
Run 10 knock-and-tip tests and 20 ordinary handling tests. Make a table: how many real events did it catch, how many did it miss, and how many false alarms? Change one threshold and repeat.
Unplug the USB cable first. Every problem below has its fix in the table of the lesson or step named on the right.
Every project in this kit uses the same parts, so you can combine them: add the display to a sensor project, or a buzzer to a measuring one. The project pages on diykit.ae have the code, the parts list and ideas to take each build further.
Most problems are one wire in the wrong hole. Check these first, then the lesson's "If it doesn't work" table:
Still stuck? Send us this message on WhatsApp with a photo of your wiring - scan the code to open WhatsApp with it already filled in: