In an effort to counter my own forgetfulness to water the plant in my apartment and on my balcony, I tried myself on developing a soil moisture sensor device.
Since I wanted it to run on my balcony, where I don’t have the means to connect everything up to a power socket, it needs to be battery-powered device. And because it is supposed to integrate with my Home Assistant it needs wireless connectivity as well and not just a simple indicator light for example.
As sensors I came across these Capacitive Soil Moisture sensors, which are sold rather cheap, when sourced directly from China. They output an analog signal, which changes based on the capacitance of the surrounding material. The water contents of the soil gives enough of a change in it capacitance to change the senor’s reading.
Being battery-powered, continuous monitoring of an analog signal and the ESP32 might not be the first thing that spring to mind, but I think I fitted them well together nonetheless. For one of my plants, the ULPSM has been running for over 6 months now on a single battery charge.
Using LiFePO4 batteries & dropping the LDO
While LiFePO4 batteries have some disadvantages like lower power-density compared to Li-Ion batteries you normally see when battery powering an ESP32, they have the big advantage, that their voltage characteristics fit the required/allowed input voltage of the ESP32 much better, allowing to drop a voltage regulator.
Normally, you’d have a LDO for example, dropping the voltage of an fully charged Li-Ion battery from 4.2V, which is above the absolute maximum rating of the ESP32 down to 3.3V. However, the voltage of a fully charged LiFePO4 battery is “only” 3.6V, with a nominal voltage of 3.2V. It also has a rather flat discharge curve, around 3.3V to 3.1V, making at a great fit for the ESP32.
While there are LDOs with very low quiescent currents, it can be still a major contributor to power consumption and getting rid of it will help out a lot in the long run.
I opted for a 18650 cell on each ULPSM, which have a capacity of about 1100mAh.
The moisture sensors have an additional LDO each as well, regulating the input voltage to 3V as well. I de-soldered them and am directly powering them from the battery as well. The input voltage for the sensors doesn’t matter too much, as long as it is relatively stable between measurements.
ESP32’s ULP co-processor
Letting the power-hungry ESP32 run all the time is of course not possible running on the battery. It would be empty withing a several hours.
Knowing that, the ESP32 provides several power saving modes, where in one of the deepest sleep states, the power consumption is down to a couple of μA.
One way would be to periodically wake the ESP32 from its deep-sleep, run the measurements, send the data and go back to sleep again. Certainly a feasible way, but depending on the wake frequency, you could trigger way to often, since the moisture content might change very slowly.
So can we do better? Yes, we can. The ESP32 does feature a Ultra-Low Power co-processor (ULP), next to the main Xtensa dual-core processor. Compared to the main processor, it is very much reduced in its features and it has to be programed directly using its small instruction set and only four registers. But isn’t that half the fun working in such a constraint environment? 🙂
Most importantly however, there is an instruction to use the two ADCs in the ESP32, allowing to read their values even with the main processor turned off.
I programmed the ULP to measure all sensors periodically and only wake the main processor, if at least one sensor changed its value more than some threshold value, compared to the last value transmitted.
Using this method and a sensible threshold, I get the optimum wake-up frequency. Either sleeping for hours or days at end, when there is no change or waking more frequently as the soil dries more quickly.
At the time, that I started the project, I think there was only (unofficial) support for programming the ULP using the Arduino IDE, using duff2013’s ulptool set. PlatformIO has support for adding ULP programs to your firmware binary now as well, though in a quick test, I think I couldn’t make it work back then.
Connecting the Sensors
One of the down-sides of the ULP is, that not all GPIOs are accessible from it. Looking on the datasheet only pins that are labeled
RTC_GPIO_* can be used. But as I mentioned, this includes several pins of the built-in ADCs.
Being constrained by that, I was still able to use six channels as sensor inputs on ADC1, as well as an additional channel on ADC2 to measure the current battery voltage. This can both be used to compensate the sensor values, which are dependent on the input value, as well as monitor the battery voltage and warn when it runs out.
The ADC in the ESP32 normally only allows to measure voltages between 0V and about 1.1V. The measured voltage can be attenuated internally to provide a wider voltage range, at the expanse of a larger non-linearity and more noise. Instead I opted for simple voltage dividers on each of the sensors’ output, to regulate them to 1.1V at the maximum battery voltage of 3.6V.
Integration & Additional Periphery
As mentioned the ULPSM was supposed to integrate into my Home Assistant. And so it does using MQTT. Each device and sensor provide a topic the current moisture percentage is published to. Auto-discovery for Home-Assistant is implemented as well, meaning no additional configuration in required, but HASS will pick-up any new device on first boot.
The ULPSM has a built-in CP2102N as a Serial-to-USB interface, together with a reset circuit, it allows to program the ESP32 directly from the Arduino IDE or PlatformIO.
To charge the battery, there is also an MCP73123, a LiFePO4 Charger-IC, which can interface with the CP2102N to limit the maximum current it draws from USB, depending on what the port offers.
I haven’t used it that much yet, since I just change batteries instead. Also because I didn’t have to change them much, due to the overall low power consumption. 😉
There is also an unpopulated footprint for a BME280, in case you want to measure humidity & temperature of the surrounding area.
Results & Problems
As I mentioned earlier, I was able to have the ULPSM run on a single battery charge for about half a year now, while continuously monitoring a plant.
This is more or less a best case scenario, as there is only a single sensor connected to the ULPSM and the plant doesn’t consume water too quickly.
But even on the devices, where I do have all six sensors connected, which are in plant pots on my balcony, with sun shining on the in the summer, I got a couple of months of life time out of a single 18650 cell, which is good for me.
There are still a few problems to fix. But they are more related to the sensors, than the ULPSM hardware.
Water-proofness. The ULPSM, will read garbage values, in case the sensor connectors get wet. Who would have thought… I’ve seen a couple of people using shrink tube to protect the sensors. However, the sensor’s etch resist seems too peal off, after a while as well, probably changing the sensors characteristics over time. That’s what you get, if you go cheap from China. What I was thinking about, was coating both the sensors and the electronics with casting resin. But I haven’t gotten around to doing that yet.
Different Sensors. Depending on the seller you buy the sensors from or the batch you receive, the ADC values for low or high moisture can be completely different. In a newer version of the firmware I implemented a way to adjust each sensor. First the low or 0% moisture value is measured with the sensor in free air. Then measuring the 100% moisture value with the sensor dipped in water. Later the conversation from ADC value to moisture value is linearly interpolated between those values.
Even with adjusting the sensors, the measured values of two sensors next to each other in the same pot, often have an offset between each other. However their rate of change is very similar and the offset between them generally stays the same. I’ll have to look into that more systematically though.