I wanted to know what the temperature actually was at the bottom of the garden, which is the kind of question that should cost nothing and instead costs three weekends. The honest summary up front: it mostly works. It reports temperature, humidity and pressure to my MQTT broker every few minutes, it runs off a battery for weeks, and it has exactly one failure mode I haven't fully killed. For a thing held together with hot glue and optimism, I'll take it.
the parts, and why these ones
The build is deliberately boring, because boring is what survives outdoors.
- An ESP32 dev board (a WROOM module on the usual cheap carrier). Wi-Fi, deep sleep, and enough GPIO that I'm not counting pins.
- A BME280 on a breakout. One little sensor, I2C, and it gives you temperature, humidity and barometric pressure in a single part. Three readings, four wires, no analogue faff.
- An 18650 cell with a protection board, and a TP4056 module for charging. Nothing exotic.
The wiring is trivial: BME280 on SDA/SCL (GPIO 21 and 22 on this board), 3V3 and GND, and that's the sensor done. The interesting engineering isn't the circuit. It's the power, and the enclosure, in that order of difficulty.
the power problem is the whole project
An ESP32 with Wi-Fi up draws an alarming amount of current. Leave it running flat out and your "battery powered" sensor is a battery consumer, flat in a day or two. The entire trick to making this viable is deep sleep, and structuring the firmware so it's awake for as little time as humanly possible.
The pattern is: wake, read the sensor, connect to Wi-Fi, publish one MQTT message, go back to sleep. Crucially you want to be asleep for the overwhelming majority of every cycle. Deep sleep on the ESP32 drops the whole thing to microamps, so the maths only works if "awake" is measured in seconds and "asleep" in minutes.
#define uS_PER_SEC 1000000ULL
#define SLEEP_MINUTES 5
void setup() {
Wire.begin();
bme.begin(0x76);
float t = bme.readTemperature();
float h = bme.readHumidity();
float p = bme.readPressure() / 100.0F; // hPa
connectWifi();
publish(t, h, p);
esp_sleep_enable_timer_wakeup(SLEEP_MINUTES * 60 * uS_PER_SEC);
esp_deep_sleep_start();
}
void loop() {
// never runs; deep sleep restarts from setup()
}
The thing that catches everyone, and caught me, is that deep sleep is not "pause". It's closer to "power off and reboot". When the timer fires, the chip restarts from the top of setup() with RAM wiped. There is no loop() in any meaningful sense. Everything you want to persist across a sleep has to go in RTC memory (the RTC_DATA_ATTR attribute) or it's gone. Once that clicked, the firmware got simpler, not more complex: do one thing, publish it, sleep. No state machine, no long-running connection to babysit.
The other power win was being ruthless about Wi-Fi time. Connecting is the expensive bit, and DHCP plus association can take several seconds, which at full radio draw is most of your energy budget per cycle. Pinning a static IP and the channel shaved the awake time down considerably, because the board skips the slow parts of association. Every second awake is current you don't get back.
getting the readings out: MQTT
I went with MQTT because the broker was already there for the rest of the house, and because a single small publish is exactly the shape of message deep sleep wants. Connect, publish to home/garden/weather, disconnect, sleep. I send the three values as a small JSON blob so whatever's consuming it doesn't have to care about ordering.
The one wrinkle: with the deep-sleep model you reconnect to the broker every single cycle, which means a fresh MQTT session every few minutes. That's fine, brokers cope with it happily, but it does mean retained messages and a sensible last will matter more than usual, so a consumer can tell the difference between "no new reading yet" and "this node has gone quiet". I set a retained last-will so a dead node shows up as offline rather than just silently stale.
the part that mostly works
Now the honest bit, the "mostly" in the title.
The first problem was the temperature reading itself, and it's a classic. Mount a BME280 right next to an ESP32 that's just spent three seconds with its radio on, and the sensor reads warm. The chip heats its own immediate surroundings, and if the sensor is in that thermal shadow you're measuring the board, not the garden. The fix was partly physical (get the sensor away from the module on a short flying lead) and partly procedural (read the sensor first, before bringing Wi-Fi up, so you sample before the board warms itself). It's much better now. It is not perfect, and on a still warm day I think it still reads a fraction high.
The second problem was the enclosure, and this is the unglamorous heart of any outdoor sensor. You need the air to reach the sensor but the rain not to. My first enclosure was a sealed box, which gives you beautiful, stable, completely wrong humidity readings, because the air inside stops being the air outside. My current enclosure is, and I'm not proud of this, a jam jar mounted upside down with the lid drilled for the cable gland and the sensor poking out under the rim into a vented gap. It's a budget Stevenson screen made of preserve packaging. It breathes, it sheds water, and it has survived a month of British weather, which is the only test that counts.
The failure mode I haven't fully killed: roughly once a week, a wake cycle fails to get on Wi-Fi and the node skips a reading. I think it's the access point being slow to respond at exactly the wrong moment, and the firmware giving up after its timeout rather than burning battery retrying. I've left it that way on purpose, because a skipped reading every now and then is a far better outcome than a node that flattens its cell hammering a deaf AP. A gap in the graph is cheap. A dead battery at the bottom of the garden in November is a trek I'd rather not make.
would I do it again
Yes, and I'd change very little. The total cost was a handful of pounds in parts and a disproportionate amount of weekend, which is the correct ratio for this hobby. The BME280 was the right call: one I2C sensor for three measurements is exactly the simplicity you want when the hard problems are power and weatherproofing.
If you're tempted, the order of difficulty is the lesson. The circuit is an afternoon. Deep sleep done properly is an evening of realising loop() never runs. The enclosure is the part that defeats people, because "let air in, keep water out, don't let the electronics warm the sensor" is genuinely fiddly and no breakout board solves it for you. Get the jam jar right and the rest is just code. It mostly works, and out here, mostly is plenty.