Beginner1 hour12+1 parts needed

Parent info

Cost: ~$12
Time: 1 hour
Age: 12+
Difficulty: ●●●
Soldering: No soldering needed
What they'll learn: Microcontroller programming, WiFi networking

Parts you need

Affiliate links — we may earn a small commission

ESP32-S3 Dev Board

One button. Unlimited actions.

Imagine a button on your desk. Press it once and a Telegram message arrives on your mom’s phone: “I’m leaving school — be home at 4.” Hold it for 2 seconds and your bedroom smart plug toggles, turning off everything at once.

The button is already on your ESP32 board. No soldering. No extra parts. Total hardware cost: $12. Just code.

That’s this project. 1 hour. Zero extra parts.


What you’ll need

Part What it does Price
ESP32-S3 Dev Board The brain AND the button — the BOOT button on the board is already wired to GPIO0 ~$12

No other hardware required! The ESP32 DevKit boards have a BOOT button (labeled BOOT or BOOT/GPIO0) already soldered on. You’re using that button directly.

You also need: home WiFi, a free Telegram account, and optionally a free IFTTT account.

Total: ~$12 | Time: ~1 hour | Difficulty: ●○○○○

Telegram bot setup (5 minutes):

  1. Open Telegram, message @BotFather
  2. Send /newbot, give it a name, copy the API token
  3. Message your new bot at least once
  4. Visit https://api.telegram.org/bot<TOKEN>/getUpdates and find "chat":{"id":123456} — that’s your chat ID

IFTTT setup (optional, 5 minutes):

  1. Go to ifttt.com → Create applet
  2. “If Webhooks” trigger → event name: button_held
  3. Connect any action (email, smart plug, Google Sheets, etc.)
  4. Go to ifttt.com/maker_webhooks → Documentation → copy your webhook key

How it works (60 seconds)

When you press the BOOT button, GPIO0 connects to GND — the pin reads LOW. When you release it, an internal resistor pulls it back to HIGH.

The code measures how long the button was held:

  • Released in under 2 seconds = short press → sends a Telegram message
  • Still held after 2 seconds = long press fires immediately → triggers IFTTT webhook

That’s a state machine — a bit of logic that tracks what’s happening over time. Three states: idle, pressing, and just released. One rule: how long was it held?

The ESP32 connects to WiFi and makes two kinds of HTTP calls: one to Telegram’s API (sends a message to your chat) and one to IFTTT’s Webhooks URL (fires any action you’ve connected).


Step 1: No wiring needed

The BOOT button on your ESP32 DevKit board is already connected to GPIO0. It’s the button closest to the USB connector on most boards, labeled “BOOT.”

Find it: Look at your board. One button is labeled “EN” or “RESET” — that restarts the board. The other is labeled “BOOT” — that’s the one you’re using.

That’s your hardware setup. Done.


Step 2: Flash the code

Time: ~10 minutes

Fill in your credentials, then upload:

The big picture first. This program turns the BOOT button on your ESP32 into a smart button with two actions:

  • A short press (under 2 seconds, then release) sends a Telegram message to your phone.
  • A long press (hold past 2 seconds) triggers an IFTTT webhook — which can control any smart device, log to a spreadsheet, or send an email.
  • The code uses a state machine: three states (idle → pressing → released) to figure out exactly what kind of press happened.
  • The BOOT button works because pressing it connects GPIO0 to GND — the pin goes LOW.
// ========== CHOOSE YOUR BOARD ==========
// Uncomment the line for YOUR board:
#define BOARD_S3    // ESP32-S3-DevKitC-1
//#define BOARD_C6  // ESP32-C6-DevKitC-1
// ========================================

#ifdef BOARD_S3
  #define PIN_BUTTON  0
#endif
#ifdef BOARD_C6
  #define PIN_BUTTON  9
#endif

#include <WiFi.h>
#include <HTTPClient.h>

const char* ssid     = "YourWiFiName";
const char* password = "YourWiFiPassword";

const char* BOT_TOKEN        = "your_telegram_bot_token";
const char* CHAT_ID          = "your_chat_id";
const char* TELEGRAM_MESSAGE = "Hey!+I+pressed+the+button.+Feed+the+cat.";

const char* IFTTT_KEY   = "your_ifttt_webhook_key";
const char* IFTTT_EVENT = "button_held";

const int LONG_PRESS_MS = 2000;

unsigned long pressStart = 0;
bool          pressing   = false;
bool          longFired  = false;

void sendTelegram(const String& msg) {
  HTTPClient http;
  String url = "https://api.telegram.org/bot";
  url += BOT_TOKEN;
  url += "/sendMessage?chat_id=";
  url += CHAT_ID;
  url += "&text=";
  url += msg;
  http.begin(url);
  int code = http.GET();
  Serial.printf("Telegram response: %d\n", code);
  http.end();
}

void triggerIFTTT() {
  HTTPClient http;
  String url = "https://maker.ifttt.com/trigger/";
  url += IFTTT_EVENT;
  url += "/with/key/";
  url += IFTTT_KEY;
  http.begin(url);
  http.GET();
  Serial.println("IFTTT triggered");
  http.end();
}

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

  pinMode(PIN_BUTTON, INPUT_PULLUP);

  WiFi.begin(ssid, password);
  Serial.print("Connecting");
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.printf("\nConnected! IP: %s\n", WiFi.localIP().toString().c_str());
  Serial.println("Short press = Telegram. Hold 2 seconds = IFTTT.");
}

void loop() {
  bool buttonDown = (digitalRead(PIN_BUTTON) == LOW);

  if (buttonDown && !pressing) {
    pressStart = millis();
    pressing   = true;
    longFired  = false;
    Serial.println("Button pressed...");
  }

  if (pressing && buttonDown) {
    if (!longFired && (millis() - pressStart >= LONG_PRESS_MS)) {
      longFired = true;
      Serial.println("LONG PRESS — IFTTT triggered");
      triggerIFTTT();
    }
  }

  if (pressing && !buttonDown) {
    unsigned long held = millis() - pressStart;
    pressing = false;
    if (!longFired && held > 50) {
      Serial.println("SHORT PRESS — Telegram message sent");
      sendTelegram(TELEGRAM_MESSAGE);
    }
  }

  delay(10);
}

Line-by-line: what every line does and why

Lines 1–2: Borrowing ready-made tools

#include <WiFi.h>
#include <HTTPClient.h>

#include grabs instruction books. WiFi connects to your network. HTTPClient makes outgoing web requests — used to call Telegram’s API and IFTTT’s webhook URL. Both services work by receiving an HTTPS GET request to a special URL.


Lines 4–14: Settings

const char* TELEGRAM_MESSAGE = "Hey!+I+pressed+the+button.+Feed+the+cat.";
const char* IFTTT_EVENT = "button_held";
const int BUTTON_PIN    = 0;
const int LONG_PRESS_MS = 2000;

const means “locked — never changes.”

TELEGRAM_MESSAGE — the text that arrives on your phone. Spaces are written as + because this text goes inside a URL, and URLs can’t contain spaces.

BUTTON_PIN = 0 — the BOOT button on the ESP32 board is already connected to GPIO 0. No extra wiring needed.

LONG_PRESS_MS = 2000 — 2,000 milliseconds = 2 seconds. Hold longer than this = long press. Change to 3000 for 3 seconds if you accidentally trigger it.


Lines 16–19: State machine variables

unsigned long pressStart = 0;
bool          pressing   = false;
bool          longFired  = false;

These three variables are the memory of the state machine — they remember what’s happening between loop() calls.

pressStart — “what time did the button go down?” Used to calculate how long it’s been held.

pressing — are we currently in the middle of a press? false = idle, true = button is currently down.

longFired — “have we already fired the long-press action during this hold?” Prevents firing IFTTT over and over while the button is held past 2 seconds.


Lines 21–35: sendTelegram() and triggerIFTTT()

void sendTelegram(const String& msg) {
  HTTPClient http;
  String url = "https://api.telegram.org/bot" + BOT_TOKEN + "/sendMessage?chat_id=" + CHAT_ID + "&text=" + msg;
  http.begin(url);
  http.GET();
  http.end();
}

HTTPClient http creates an HTTP client object named http. Then http.begin(url) opens a connection to that URL, http.GET() sends the GET request, http.end() closes the connection.

Telegram’s Bot API is just a web API — sending a message is as simple as opening this URL in a browser: https://api.telegram.org/botTOKEN/sendMessage?chat_id=ID&text=Hello

Your ESP32 does the same thing in code.

IFTTT works the same way — a specific URL triggers the webhook.


Lines 37–52: setup() — morning routine

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
}

pinMode(BUTTON_PIN, INPUT_PULLUP) — this line does two things at once:

  1. Sets GPIO0 as an input (reading signals coming in).
  2. Enables the internal pull-up resistor (a small resistor built into the ESP32 chip that keeps the pin at HIGH when nothing is connected).

When the button is pressed, it connects GPIO0 to GND — pulling it LOW. When released, the pull-up resistor pulls it back to HIGH.

This is called active-LOW logic: the signal is LOW when the button IS pressed. It seems backwards — “pressed = LOW” not “pressed = HIGH” — but it’s the standard pattern. Without the pull-up, the pin would float between HIGH and LOW randomly when unpressed (like a loose wire that sometimes picks up interference).


Lines 54–82: loop() — the state machine

void loop() {
  bool buttonDown = (digitalRead(BUTTON_PIN) == LOW);

  if (buttonDown && !pressing) { ... }      // State 1: button just went down
  if (pressing && buttonDown) { ... }       // State 2: button is held
  if (pressing && !buttonDown) { ... }      // State 3: button just released

  delay(10);
}

digitalRead(BUTTON_PIN) == LOW — read the pin. LOW means button is pressed (active-LOW logic). The parentheses produce true or false directly.

State 1: button just went down (buttonDown && !pressing):

  • !pressing means “we were NOT already tracking a press.” This is the moment the button transitions from up to down.
  • Record pressStart = millis() — the start time.
  • Set pressing = true and longFired = false (fresh press, no long-press fired yet).

State 2: button held down (pressing && buttonDown):

  • We’re currently holding the button. Check if we’ve crossed 2 seconds.
  • millis() - pressStart >= LONG_PRESS_MS — “has 2 seconds passed since pressing started?”
  • If yes AND !longFired (we haven’t fired yet): set longFired = true, call triggerIFTTT().
  • The long press fires immediately at 2 seconds — you don’t have to release first.

State 3: button just released (pressing && !buttonDown):

  • !buttonDown means the button is now up. We were pressing (state 2), now released.
  • unsigned long held = millis() - pressStart — how long was it held?
  • If !longFired (long press didn’t fire) AND held > 50 (at least 50ms — longer than electrical bounce): it was a short press → send Telegram.
  • held > 50 filters out bouncing: when a button is released, the metal contacts spring back and briefly bounce between HIGH and LOW. This takes 5–20ms. Ignoring anything under 50ms filters that noise.

delay(10) — check button state 100 times per second. Fast enough to catch transitions.


The whole thing in one sentence

loop() checks the button state 100 times per second and runs a three-state machine: on press-start it records the time, while held it checks for 2-second long press, and on release it decides short press vs long press and fires the appropriate action.

First thing to try: press the BOOT button briefly. Watch Serial Monitor — “SHORT PRESS” should appear. Your Telegram should get a message within 5 seconds. Then hold the button for 3 seconds — “LONG PRESS” should fire. If only one action fires and not the other, double-check LONG_PRESS_MS — it must match how long you’re holding.

Check: Open Serial Monitor (115200 baud). Connected message appears. Press the BOOT button briefly — Serial shows “SHORT PRESS.” Hold it 2 seconds — “LONG PRESS.”


Step 3: Test it

  1. Short press the BOOT button → Serial says “SHORT PRESS” → Telegram message arrives on your phone within 5 seconds
  2. Hold the BOOT button for 2 seconds → Serial says “LONG PRESS” → IFTTT action fires (if you set it up)

Check: Both actions fire correctly. If Telegram message doesn’t arrive, check the API token and chat ID — they’re easy to get wrong. Re-read the bot setup steps and verify by visiting the getUpdates URL in a browser.


Step 4: Use it!

This is where it gets fun. Customize the message and actions for your actual life:

Short press ideas (Telegram messages):

  • “Leaving school now — home at 4”
  • “Feed the dog please”
  • “I need help — respond ASAP”
  • “About to start homework — DND for 1 hour”

Long press ideas (via IFTTT):

  • Toggle a smart plug (turn off your monitor, lamp, etc.)
  • Log a timestamped note to a Google Sheet (“I took my medication at 3:15pm”)
  • Send an email to someone
  • Add a reminder to Google Calendar
  • Ring your own phone when you can’t find it

To use an external button instead of BOOT: Wire any normally-open pushbutton between GPIO0 and GND. The INPUT_PULLUP in the code handles everything. No resistor needed. You could put the button in a nice enclosure — a box with one big arcade button sticking out.


What just happened

  • Active LOW button logic: When a button connects a GPIO pin to GND, pressing it makes the pin read LOW. This seems backwards — “pressed” reads LOW not HIGH. But it’s correct: the internal pull-up holds the pin at HIGH when open. Without a pull-up, the pin would float between values randomly. INPUT_PULLUP uses the ESP32’s internal 47kΩ resistor. One line of code, no external resistor needed.

  • Button debounce: Mechanical buttons aren’t electrically clean. The metal contacts bounce for 5–20 milliseconds when pressed — one press looks like dozens of rapid transitions. The held > 50 check filters this: only register a short press if the button was held for at least 50ms (longer than any bounce, shorter than any real press). The delay(10) samples the button at 100Hz — slow enough to smooth out bounces.

  • State machine for input detection: The press detection is a finite state machine — a design pattern for tracking things that change over time. Three states: idle (not pressing), pressing (held down), done (just released). Each state checks different conditions. This pattern scales to triple-press detection, double-tap, hold-for-5-seconds — any press pattern you can describe as a sequence of states.

  • IFTTT Webhooks: IFTTT (If This Then That) is an automation platform connecting 700+ services. The Webhook trigger receives an HTTP GET and fires any connected action — Google Sheets, Philips Hue, Gmail, Google Calendar, phone notifications, anything. Your ESP32 sends one HTTP request; IFTTT does the rest. You never have to integrate with those APIs directly.


Level Up

Add triple-press for emergencies. Count rapid presses within a 500ms window. After each release, wait 500ms for another press. If 3 presses happen within that window, send an emergency Telegram message to a different chat ID. This is how Apple’s SOS feature works (5 side button presses).

Add LED feedback. Wire an LED to GPIO2 (many ESP32 boards have one built in). Flash it once on short press, twice on long press — so you get visual confirmation that the action was registered, before the Telegram message arrives.

Add WiFi reconnect logic. Right now, if the router restarts, the button won’t send messages until you reboot the ESP32. Add a check at the top of sendTelegram(): if WiFi.status() != WL_CONNECTED, call WiFi.reconnect() and wait up to 10 seconds. The button will work reliably even after network blips.


Troubleshooting

Problem Fix
Short press triggers twice Increase debounce: change held > 50 to held > 100. Some buttons are bouncier than others.
Long press fires immediately without waiting The LONG_PRESS_MS constant is in milliseconds. 2000 = 2 seconds. Check that you’re holding the button past the threshold.
Telegram message never arrives Check that you messaged your bot at least once (bots can’t initiate conversations). Verify the BOT_TOKEN has no extra spaces. Visit the getUpdates URL to confirm the chat ID is correct.
IFTTT never fires Verify the event name in IFTTT matches exactly: button_held. Check your webhook key has no spaces. Test the URL in a browser: https://maker.ifttt.com/trigger/button_held/with/key/YOUR_KEY — you should see “Congratulations!” if it works.
WiFi never connects The ESP32 only connects to 2.4GHz WiFi (not 5GHz). Make sure SSID and password are correct (both case-sensitive).
Serial shows messages but phone gets nothing Telegram delivery depends on Telegram servers. Wait 10 seconds. If still nothing, open https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<ID>&text=test in a browser — if this works, the issue is in the code.
Affiliate disclosure: Some links on this page are affiliate links. If you buy through them, we may earn a small commission at no extra cost to you.