Most automatic weather stations measure rain with a tipping bucket: the rain runs down a funnel into a tiny seesaw with a cup on each side. When one cup is full it tips over, empties itself and brings the other cup under the funnel. Each tip means the same small amount of rain, so counting tips measures the rainfall. In this capstone you print the whole mechanism, count the tips with the photo-interrupter, work out millimetres of rain and send them to ThingSpeak, where you can follow every shower on a chart.
What you'll learn
- How a tipping-bucket rain gauge turns rain into pulses
- How to calibrate a sensor with a measured amount of water
- How to count events with an interrupt and send totals to the cloud
- How to reset a daily total at midnight with internet time
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Photo-interrupter module (45-in-1 Sensor Kit)
- 3 long female-to-male jumper wires (or extend them with female-to-female and male-to-male wires)
- A 3 mm rod, 65 mm long: a bamboo skewer or an M3 screw with a nut
- A syringe or a kitchen measuring jug, to calibrate
- A free ThingSpeak account
- A 2.4 GHz Wi-Fi network
- A plastic food box to keep the ESP32 dry
- The printed parts from the 3D printing box below (frame, bucket and funnel), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Photo-interrupter S | ESP32 GPIO 27 |
| Photo-interrupter + | ESP32 3V3 |
| Photo-interrupter − | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Set up ThingSpeak
- Sign in at thingspeak.com (it uses a MathWorks account; if you are under the age required by their terms, ask a parent or teacher to create it).
- Create a new channel called Rain gauge with two fields: Rain last 15 min (mm) and Rain today (mm).
- Copy the Write API Key from the API Keys tab.
Calibrate the bucket
First you find out how much water one tip holds. Upload the sketch with ML_PER_TIP left at 2.0, open the Serial Monitor at 115200 baud, and then:
- Fill a syringe or measuring jug with exactly 50 ml of water.
- Pour it into the funnel very slowly, a few drops at a time, over about five minutes. Real rain is slow, and pouring fast makes the bucket overflow before it tips.
- Read the number of tips in the Serial Monitor. Work out
ML_PER_TIP= 50 ÷ tips. For example, 50 ml and 22 tips gives 2.27. - Put your value into the sketch and upload it again. Repeat once to check.
The funnel opening is 100 mm across, so its area is π × 50 × 50 = 7854 mm². One millimetre of rain over that area is 7.85 ml of water, which is why the sketch divides the volume by 7.85.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Put your Write API Key into WRITE_API_KEY, your calibrated value into ML_PER_TIP, and your time zone into TIME_ZONE (the one in the sketch is for Estonia and Finland; search for "POSIX time zone" plus your city to find yours).
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* WRITE_API_KEY = "YOUR_WRITE_API_KEY";
const char* TIME_ZONE = "EET-2EEST,M3.5.0/3,M10.5.0/4";
const int SENSOR_PIN = 27;
const float ML_PER_TIP = 2.0; // set this when you calibrate
const float ML_PER_MM = 7.85; // 1 mm of rain in a 100 mm funnel
const unsigned long EVERY = 15UL * 60 * 1000; // send every 15 minutes
volatile unsigned long tips = 0;
volatile unsigned long lastTipMs = 0;
unsigned long tipsToday = 0;
unsigned long lastSend = 0;
int today = -1;
void IRAM_ATTR onTip() {
unsigned long now = millis();
if (now - lastTipMs > 300) { // one tip, not the bucket bouncing
tips++;
lastTipMs = now;
}
}
int dayOfMonth() {
struct tm t;
if (!getLocalTime(&t, 10)) return -1;
return t.tm_mday;
}
void send(float mmPeriod, float mmToday) {
HTTPClient http;
String url = String("http://api.thingspeak.com/update?api_key=") + WRITE_API_KEY +
"&field1=" + String(mmPeriod, 2) + "&field2=" + String(mmToday, 2);
http.begin(url);
int code = http.GET();
http.end();
Serial.printf("Sent %.2f mm (today %.2f mm), ThingSpeak answered %d\n", mmPeriod, mmToday, code);
}
void setup() {
Serial.begin(115200);
pinMode(SENSOR_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onTip, CHANGE); // both ways count
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" connected");
configTzTime(TIME_ZONE, "pool.ntp.org");
}
void loop() {
static unsigned long shown = 0;
if (tips != shown) { // show every tip while you calibrate
shown = tips;
Serial.printf("Tips: %lu\n", tipsToday + shown);
}
if (millis() - lastSend >= EVERY) {
lastSend = millis();
noInterrupts();
unsigned long period = tips;
tips = 0;
interrupts();
shown = 0;
int day = dayOfMonth();
if (day != -1 && today != -1 && day != today) tipsToday = 0; // a new day
if (day != -1) today = day;
tipsToday += period;
float mmPeriod = period * ML_PER_TIP / ML_PER_MM;
float mmToday = tipsToday * ML_PER_TIP / ML_PER_MM;
if (WiFi.status() == WL_CONNECTED) send(mmPeriod, mmToday);
else WiFi.reconnect();
}
}
Put it outside
- Place the gauge on a flat, open spot, away from walls, trees and roofs that shelter it or drip onto it. Check it is level, or the bucket tips unevenly.
- Put the ESP32 in a plastic food box and run only the sensor wires out to the gauge, or keep the ESP32 indoors and use long wires through a window gap.
- Weigh the frame down with a stone, or screw it to a post, so the wind can't blow it over.
- Open your ThingSpeak channel. After the next shower you see the rain in 15-minute steps and the total for the day.
How it works
The flag under the bucket is in the sensor's slot when the bucket leans one way and out of it when it leans the other way. So every tip changes the sensor's output, from LOW to HIGH or from HIGH to LOW. That is why the interrupt uses CHANGE: each change is one tip. The 300 ms check stops one tip from counting twice when the bucket bounces.
The interrupt only counts. Every 15 minutes loop() takes the count, resets it, adds it to today's total and converts both to millimetres. The internet time from configTzTime() tells the sketch when a new day starts, so the daily total goes back to zero at midnight, just like the rainfall figures in weather reports.
Try this next
- Use ThingSpeak's MATLAB Visualization to draw a bar chart of rain per day.
- Send an ntfy alert, as in the motion alert lesson, when more than 5 mm falls in an hour.
- Combine it with the anemometer and the weather logger into a complete weather station.
Troubleshooting
- The tips don't count: watch the module's LED while you rock the bucket by hand. It must change every time; if not, move the module so the flag goes deeper into the slot.
-
Each tip counts twice: make the 300 in
onTip()bigger. - The bucket overflows instead of tipping: it sticks on its rod. Make the holes in the bucket slightly bigger with a 3.5 mm drill, and check the frame is level.
- "ThingSpeak answered 0": the free plan accepts one update every 15 seconds; check the Write API Key.
- 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.