Build a sweet dispenser you control from Telegram

Wave your hand to get a sweet from a printed dispenser, with a daily limit. A parent can also give treats, check the count and change the limit from Telegram.
  • Level:Advanced
  • Duration: 2 h 30 min
  • Ages 13+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a sweet dispenser you control from Telegram – EEMENTOR STEM™ lesson

This dispenser gives out one sweet (or a dry pet treat, or a counter for a game) when you hold your hand in front of it, up to a daily limit. It also has a Telegram bot: send /treat and it drops a sweet from wherever you are, /count tells you how many it gave today, and /limit 3 changes the daily limit. You print the whole mechanism, so this capstone is as much about mechanical design as about code.

What you'll learn

  • How a rotating pocket wheel measures out one item at a time
  • How to make a servo do a short sequence of moves
  • How to combine a local sensor and remote commands in one sketch
  • How to remember a count across restarts with Preferences

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • Servo motor (ESP32 IoT Starter Kit)
  • IR obstacle avoidance sensor module (45-in-1 Sensor Kit)
  • Passive buzzer (ESP32 IoT Starter Kit)
  • Small round sweets or counters, 10 to 15 mm across
  • A phone with Telegram
  • A 2.4 GHz Wi-Fi network
  • The printed parts from the 3D printing box below (base, wheel and hopper), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a sweet dispenser you control from Telegram
Breadboard view. Check each connection against the wiring table below.
From To
Servo orange / yellow wire ESP32 GPIO 13
Servo red wire ESP32 5V
Servo brown / black wire GND
Obstacle sensor OUT ESP32 GPIO 27
Obstacle sensor + and GND 3V3 and GND
Passive buzzer + ESP32 GPIO 23
Passive buzzer − GND

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

Create your Telegram bot

  1. Install Telegram on your phone and sign in. Telegram is for ages 13 and up (16 in some countries), so younger makers should ask a parent or teacher to create the bot and run it on their account.
  2. Search for @BotFather (the official bot with a blue tick), send /newbot and follow the questions. Call it something like Sweet Dispenser; the username must end in bot.
  3. BotFather answers with a token, a long code such as 712345678:AAE…. It works like a password for your bot: put it in the sketch and never share it.
  4. Search for @userinfobot, start it and copy the number it shows as your Id. That is your chat ID. The sketch only obeys messages from this ID.
  5. Open a chat with your new bot and press Start, so it is allowed to send you messages.

The sketch

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).

In the Arduino IDE, open Library Manager and install ESP32Servo (by Kevin Harrington), UniversalTelegramBot (by Brian Lough) and ArduinoJson (by Benoit Blanchon).

Put your bot token and chat ID into the sketch.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
#include <Preferences.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* BOT_TOKEN = "YOUR_BOT_TOKEN";
const String CHAT_ID = "YOUR_CHAT_ID";

const int SERVO_PIN = 13;
const int SENSOR_PIN = 27;          // LOW when a hand is in front
const int BUZZER_PIN = 23;
const int LOAD_ANGLE = 10;          // pocket under the hopper
const int DROP_ANGLE = 170;         // pocket over the chute

WiFiClientSecure client;
UniversalTelegramBot bot(BOT_TOKEN, client);
Servo wheel;
Preferences memory;

int givenToday = 0;
int dailyLimit = 5;
unsigned long lastCheck = 0;
unsigned long lastDispense = 0;
unsigned long dayStart = 0;

void dispense() {
  wheel.write(LOAD_ANGLE - 5);      // wiggle so a sweet drops into the pocket
  delay(150);
  wheel.write(LOAD_ANGLE + 5);
  delay(150);
  wheel.write(LOAD_ANGLE);
  delay(300);
  for (int a = LOAD_ANGLE; a <= DROP_ANGLE; a += 2) {   // carry it over slowly
    wheel.write(a);
    delay(10);
  }
  delay(500);                       // let it fall down the chute
  wheel.write(LOAD_ANGLE);
  tone(BUZZER_PIN, 1319, 120);
  givenToday++;
  memory.putInt("given", givenToday);
  lastDispense = millis();
}

void handle(int count) {
  for (int i = 0; i < count; i++) {
    String chat = bot.messages[i].chat_id;
    String text = bot.messages[i].text;
    if (chat != CHAT_ID) {
      bot.sendMessage(chat, "Sorry, I only answer my owner.", "");
      continue;
    }
    if (text == "/start" || text == "/help") {
      bot.sendMessage(chat, "/treat - give one now\n/count - how many today\n/limit 5 - set the daily limit\n/reset - start counting again", "");
    } else if (text == "/treat") {
      dispense();
      bot.sendMessage(chat, "Done! That was number " + String(givenToday) + " today.", "");
    } else if (text == "/count") {
      bot.sendMessage(chat, String(givenToday) + " of " + String(dailyLimit) + " given today.", "");
    } else if (text.startsWith("/limit ")) {
      int limit = text.substring(7).toInt();
      if (limit >= 0 && limit <= 50) {
        dailyLimit = limit;
        memory.putInt("limit", dailyLimit);
        bot.sendMessage(chat, "The daily limit is now " + String(dailyLimit) + ".", "");
      }
    } else if (text == "/reset") {
      givenToday = 0;
      memory.putInt("given", 0);
      bot.sendMessage(chat, "Counter reset.", "");
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(SENSOR_PIN, INPUT);
  wheel.attach(SERVO_PIN);
  wheel.write(LOAD_ANGLE);
  memory.begin("dispenser", false);
  givenToday = memory.getInt("given", 0);
  dailyLimit = memory.getInt("limit", 5);

  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" connected");
  client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
  bot.sendMessage(CHAT_ID, "Sweet dispenser online. Send /help.", "");
  dayStart = millis();
}

void loop() {
  // a hand in front of the sensor, and at least 3 s since the last sweet
  if (digitalRead(SENSOR_PIN) == LOW && millis() - lastDispense > 3000) {
    if (givenToday < dailyLimit) {
      dispense();
      bot.sendMessage(CHAT_ID, "Someone took a sweet (" + String(givenToday) + " of " + String(dailyLimit) + " today).", "");
    } else {
      tone(BUZZER_PIN, 220, 400);    // a low "no" beep
      lastDispense = millis();
    }
  }

  if (millis() - dayStart > 24UL * 60 * 60 * 1000) {   // a simple day counter
    dayStart = millis();
    givenToday = 0;
    memory.putInt("given", 0);
  }

  if (millis() - lastCheck > 2000) {
    lastCheck = millis();
    int count = bot.getUpdates(bot.last_message_received + 1);
    while (count) {
      handle(count);
      count = bot.getUpdates(bot.last_message_received + 1);
    }
  }
}

Build it in stages

  1. Servo first: before printing, upload the sketch and send /treat. Watch the servo go from 10 to 170 degrees and back.
  2. Wheel and hopper: assemble the dispenser with five sweets in the hopper. Send /treat a few times. If two sweets drop at once, they are smaller than the pocket; use bigger ones.
  3. Sensor: adjust the obstacle sensor's screw so it reacts to a hand about 5 cm away, and test the daily limit.
  4. Finish: send /limit with the number you want and leave it running.

How it works

The wheel has one pocket, just big enough for one sweet. At the load angle the pocket is under the hopper and a sweet drops in; the little wiggle helps it settle. As the wheel turns to the drop angle, the solid part of the wheel closes the hopper's opening, so no more sweets follow, and the pocket arrives over the hole to the chute. This rotary feeder is the same idea that measures out pills, seeds and screws in factories.

The count and the limit are saved with Preferences in the ESP32's flash memory, so a power cut doesn't reset the day. The day itself is counted with millis() from when the dispenser was switched on; use internet time, as in the rain gauge, to reset it at midnight instead.

Try this next

  • Reset the count at midnight with internet time.
  • Ask a quiz question in Telegram and only give a sweet for the right answer.
  • Add a "please wait" light that glows while the 3-second pause runs.

Troubleshooting

  • Sweets jam between the wheel and the base: they are too big for the pocket, or not round. Use smooth, round sweets 10 to 15 mm across.
  • Nothing drops: the pocket isn't under the hopper at the load angle. Take the wheel off, upload, and put it back on in the right place, or change LOAD_ANGLE.
  • It gives sweets by itself: the obstacle sensor sees the cup or the table. Turn its screw anticlockwise.
  • The board restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.
  • It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit