A weighing rain gauge on ESP32 — a bottle hanging from a load cell measures rainfall by weight and reports it over MQTT.
A collecting bottle hangs from a bar load cell (HX711). As rain raises the fill level, the weight rises; from the weight gain over time the evaluation side (Node-RED) computes rainfall amount and intensity. The device runs on battery/solar in the garden, spends most of its time in deep sleep and wakes adaptively more often while it is raining.
Language convention:
README.de.mdis the primary, authoritative version; this EnglishREADME.mdis kept in sync from it (GitHub default). Code, commit and PR language: English.Hosting: primary on
gitlab.brokenpipe.de(groupplatformio), mirrored to GitHub. Full concept/decision record in MemPalace: wingregenmesser-rain-gauge(roomsdecisions+technical).
Each wake-up runs a linear cycle, then goes back to deep sleep:
- Power the load cell (HX711) via a gate, let it settle, take a smoothed reading
- Read the environment sensor AHT20 + BMP280 (temperature/humidity/pressure)
- Measure the battery voltage
- Only when needed (weight changed · heartbeat due · first boot):
read the environment sensor, WiFi (static IP + cached BSSID/channel → fast
connect) + MQTT: apply retained config +
cmd, publish state, OTA if requested - Pick the next interval adaptively and go to sleep
Adaptive: while the weight barely changes (dry) it measures rarely
(dry_interval). Once the change exceeds a threshold (rain) the device switches to
a fine interval (wet_interval) for a few cycles — fine resolution during rain,
frugal when dry. The last value survives deep sleep in RTC memory.
Energy: WiFi is the most expensive part. Dry wakes without a weight change are
therefore measure-only wakes without WiFi; the device transmits only on a
change (rain) or at the latest via heartbeat (heartbeat_s), which also keeps
the OTA window open. The weight is the median of several samples → robust against
the bottle swinging in the wind.
- MCU: ESP32-WROOM-32 (bare module) on an AI-Thinker adapter board
- Weighing: HX711 module (XFW-HX711) + bar load cell (~5 kg)
- Environment: AHT20 + BMP280 (I²C combo module)
- Power: 1× LiIon 3.7 V + solar (planned); low-Iq LDO concept (two LDOs in parallel: main LDO for the steady load, the second only for the WiFi current spikes, ~1 µA quiescent)
- HX711 and sensor are powered from GPIOs only while measuring (power gating against quiescent draw)
- Brownout detector disabled (
RTC_CNTL_BROWN_OUT_REG): the WiFi current spike briefly pulls the dual-LDO voltage below the threshold — not a real undervoltage, but it otherwise caused resets mid-connect.
| Function | GPIO | Note |
|---|---|---|
| Battery voltage | IO34 | 1M/1M divider, ADC1 |
| HX711 DOUT | IO35 | input-only (ESP reads) |
| HX711 SCK | IO32 | output |
| HX711 power gate | IO33 | powers HX711 + bridge |
| Sensor power gate | IO26 | powers AHT20+BMP280 |
| I²C SDA / SCL | IO21 / IO22 | AHT20 @0x38, BMP280 @0x77 |
The device sends raw grams (source of truth); mm/intensity is computed on the evaluation side (Node-RED → InfluxDB/Grafana), so swapping the funnel does not require a reflash:
mm = 10 × grams / funnel_area[cm²]
A siphon drain (planned, a "Pythagorean cup" at the bottle bottom) produces a large negative weight step → treat it server-side as a reset, not as negative rain. The known empty weight after draining also enables an auto-tare to compensate load-cell drift.
Topic prefix: ohm/garten/regenmesser
…/state— readings as JSON:{"grams":…,"temp_c":…,"humidity":…,"pressure_hpa":…,"vbat":…,"rssi":…,"boot":…,"version":…}…/config(retained) — applied on connect and cached in RTC memory, i.e. tunable live without reflashing:dry_interval_s,wet_interval_s,heartbeat_s,change_threshold_g,wet_hold_cycles,cal_factor, plusfw_version+fw_url(see OTA)…/cmd(publish retained) — plain-text commands:tare(tare the empty cell) orcalibrate:<grams>(put a known mass on it). The device runs the command on its next connect, clears the topic afterwards (retained"") and reports the result retained on…/cmd/result. Calibration is two-step: firsttare, then in the next cycle, with the mass on the cell,calibrate:<grams>.
HTTP-pull OTA, triggered via the retained config: set fw_version (≠ running
version) + fw_url there, and on the next wake-up the device pulls the .bin over
HTTP, flashes it and reboots. No BLE, no MQTT chunking.
Latency: OTA is only checked while WiFi is up. When it's dry that's not until the next heartbeat (
heartbeat_s, default 1 h); during rain it's sooner. To update faster, lowerheartbeat_svia config for a while.
The ota_deploy.sh script automates the whole flow:
./ota_deploy.sh --bump # VERSION +1, build, serve firmware.bin,
# push retained config, confirm the updateCALIBRATION_MODE 1 in src/config.h starts an interactive serial loop
(115200 baud): t = tare (empty), c = calibrate with a known mass,
p = show cal factor. Cal factor and tare offset are stored in NVS. In normal
operation (CALIBRATION_MODE 0) the deep-sleep measurement cycle runs.
- Toolchain: PlatformIO with the pioarduino platform (arduino-esp32 3.x on
ESP-IDF 5.5) — see
platformio.ini - Credentials: copy
secrets.h.exampletosecrets.hand fill it in (WiFi, static IP, MQTT).secrets.his excluded via.gitignore.
pio run # build
pio run -t upload # flash (FTDI)Flashing without auto-reset: an FTDI Basic only has DTR (no RTS), so enter the
bootloader manually: hold IO0 → tap Reset → release IO0, then upload; after
flashing tap Reset once to start the app. (After the first OTA-capable flash,
further updates are wireless.)
Working (verified): HX711 measurement + calibration, AHT20, BMP280, battery
voltage, adaptive deep sleep, WiFi (static IP), MQTT state, retained-config
uptake, HTTP-pull OTA, ota_deploy.sh.
Implemented (still to verify on the mounted device): cmd tare/calibrate via
MQTT · energy-frugal send logic (WiFi only on change/heartbeat) · HX711 median
against wind noise · BSSID/channel caching in RTC for faster reconnect.
Open: siphon auto-tare (needs the mechanics mounted) · Node-RED evaluation (mm/intensity, siphon reset) → InfluxDB/Grafana · final tare with the mechanics mounted.
MIT — see LICENSE.