Michael On Everything Else

Arduino-based soil monitor

Project information

This is an Arduino-based data collector and emitter that uses a LoRa radio to transmit soil temperature and moisture data to a web-connected device.

This article is an aggregation of three WiKi pages still hosted in my Github account.


This was a field activity for a school project for a soil sciences class in which I gathered research about cover crops and their effects on soil. For the field activity, I built this monitoring station to compare the temperature and relative moisture levels of two areas of my lawn: one covered with a broadleaf turf grass and the other an exposed patch. The assignment didn’t include a field activity—it was just supposed to be a short, slide presentation for a subject of our choosing, researched and explained. I did the field activity as a way to continue figuring out how to get an environment monitor for coffee trees (my first attempt at an environment monitor). You can see the final assignment here.

The goal was to detect differences in temperature and/or moisture levels between the two patches of soil. Despite a few hardware failures (mostly with the FC-28 probes), I was able to get some meaningful data and see some good trends illustrated when the data was graphed.


I’ve been experimenting with how I share information about the initial build, using a ‘leave the garage door open’ philosophy (basically letting the neighbors see my work). I have a lengthy Twitter thread that I’ve updated as I go. It forks around at places as I have side comments about specific tweets. An unroll tool might be useful. But as you browse the thread, if you see a Tweet with multiple comments, it’s a fork in the thread, and I think at some point I was updating a fork instead of the main thread. Viewer be warned.


The data is all ultimately published to io.Adafruit where I have a publicly accessible dashboard created to view and analyze the data. If you don’t have an account on io.Adafruit, you’ll just see the most recent, static data. It won’t refresh (stream) live.

On that dashboard I have created two graphs; one for temp and one for moisture. That makes it easy to compare moisture A to moisture B and temperature A to temperature B

Initial Build

As I completed the initial build, I updated a Twitter thread and as I continue to work on the sensor I maintain that same thread.

There are photos there and you are welcome to follow along there and even comment if you’d like. That Twitter thread kinda forks around in a couple of places as I’ve had “side” conversations about certain tweets, so do explore around it a bit. This is as much an experiment with Twitter and “working with the garage door open” as it is testing soil.


The overall monitor is comprised of the following components:

2 X Waterproof DS18B20 Digital temperature sensor (more information on my usage of the DS18B20)

2 X FC-28 Soil Moisture Meter (more information on my usage of the FC-28)

Adafruit Feather M0 with LoRa Radio Module

6V 3.5W Solar panel (From Voltaics, resold by Adafruit)

USB / DC / Solar Lithium Ion/Polymer charger - v2

NCR18650B 3400mAh 3.6V 30A Li-ion Battery

Overall Function

The M0 counts milliseconds until the interval threshold is met and it then takes readings from the sensors one at a time and broadcasts the readings (one at a time) using the LoRa radio. There is 5s delay between each successive sensor reading+broadcast to make sure the web gateway has time to receive the transmission and pass it on to the Web.

The loop does the following:

  1. Has it been 5 minutes since the last reading?
  2. If it has, read each sensor, serially and broadcast the data via LoRa.
  3. Has it been 60 minutes since the last power report?
  4. If it has, read the power voltage and broadcast it via LoRa.

I already have a LoRa-to-Web gateway unit built that receives LoRa transmissions from other gadgets around the house. I added code for it to work with the soil monitor. The Web gateway is an ESP8266 unit with a LoRa radio.

The web gateway receives the LoRa broadcast and evaluates the following:

  1. Is the message addressed to me?
  2. Is the message from the soil monitor?
  3. Is the message payload 2 or 12 bytes?
    • If the message payload is 2 bytes, it contains moisture data or battery data.
    • If the message payload is 12 bytes, it contains temperature data.
  4. Decode and reformat the data into human-readable format
  5. Publish the data over MQTT to io.Adafruit where the data can be stored and analyzed

LoRa radio

I am using a minimally modified version of Sandeep Mistry’s LoRa duplex protocol. The ‘duplex protocol’ example from Sandeep simply creates a standard packet header of information that all LoRa devices in the network know and utilize. Each unit participating in the micro network expects the information to be formatted the same for every transmission. For example, each LoRa transmission payload consists of the following information in the following order (each piece of information is one byte) in the first five bytes of overall payload:

In LoRa terms, it’s all just payload. But each microcontroller using the duplex protocol expects the first five bytes of data to be formatted as above. And because it’s a radio transmission, all of the data is broadcasted and all units who can receive the radio transmission receive the full payload. But with the addressing scheme you can make conditional decisions for processing the payload received and you can do some error handling (checking payload size, etc).

Lessons learned

DS18B20 Lessons learned
FC-28 Lessons learned

Internal moisture

After 30 days of continuous deployment (to the day), the device stopped working due to water build-up inside the box. I believe the water got into the box through the cable gland. The device sat flat on the ground and was directly exposed to the sun all day, every day. With the heat, everything would expand and contract and while I sealed most holes with hotglue (flexible), I didn’t apply any directly to the hole where the cables pass into the box.

Since the box wasn’t oriented with the gland at the bottom, water could more easily enter the box.

Solution: Orient the box with all openings facing downward if possible and use an inexpensive moisture/temp sensor (i.e., DHT11) inside the box to detect problems before internal electronics get damaged.

water damage charger water damage

Power supply

The solar panel is a great unit from Voltaic, resold by Adafruit that can supply up to 6 volts at 3.5 watts. When I did the final protoboard and wired the connection from the LiPo charging unit, I did not include a regulator to drop the power down to an acceptable value for the M0 Feather and that resulted in squirrelly, inaccurate readings from all four sensors. At first I thought the squirrelly data could have been an overheating problem, as the entire monitor sits in the direct sun all day. I did a little bandana test to keep it cool and rule out heat as the problem.

Solution: Installed an LM4931 regulator. See Issue #4

See also: Twitter thread beginning here

over-voltage bandito test

DS18B20 digital temperature sensor


This was my first time using one of these and I was pleasantly surprised at how easy it was to deploy. Like the FC-28 sensors, I bought two (actually three with another project in mind) from the same vendor in Singapore.

Each of the temperature probes is buried 15cm. I used a 10mm X 250mm masonry drill bit to bore the holes. I marked 15cm on the drill bit with a sharpie to gauge hole-depth. The probes have 3.5cm of exposed surface at the tip, so in this deployment they are reporting the temperature at roughly 11.5cm to 15cm.


These are relatively straight-forward temperature probes with ±0.5°C precision. Each one has a unique, 64-bit ID burned in at the factory, allowing multiple units to be connected to one data pin on the microcontroller. I found this aspect to work right out of the box, without a hitch.

Adafruit has a little more information about the units but no tutorial yet but these things are relatively straight-forward to get deployed quickly and collecting data.

Working with the data

I worked with the example provided by OneWire and whittled the code down until I had the parts I needed (and I still have too much). The example code shows you how to poll devices, get addresses, determine device type, and ultimately retrieve and decode the data. I created a function and pass an address variable (the last byte of the devices hard-wired address) to differentiate the two probes. Here is the meat-and-potatoes of data retrieval (again, largely a cut-and-paste from the example):

ds.write(0x44, 1);

for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();

I then use those 9 bytes to build the LoRa transmission to be sent to the web gateway.

Decoding the raw data into human-readable format is relatively easy.

Note; you only need the first two bytes from the probes for a temperature reading. I was transmitting the full nine bytes unnecessarily.

int16_t raw = (data[1] << 8) | data[0];

celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;


None necessary. I did test the units in ice-water and was happy with the results. Again, these devices worked as advertised, right out of the box.

Lessons learned

They just worked

These temp probes were very easy to deploy. The OneWire feature meant that I could connect both probes together with a “Y” junction and consume fewer GPIO pins on the microcontroller and that also meant fewer wires to pass through the cable gland.

Post deployment, all that was necessary is an easy rinsing of the dirt on them and they’re ready to be redeployed with a new project.

FC-28 soil moisture sensor


I originally bought two identical probes from a shop in Singapore. I wanted them to be the same to eliminate as many variables as possible for the comparison between covered and exposed soil.

The FC-28 is really a crude way to measure the moisture level of a soil. There are a lot of variables in play such as soil-contact with the probe, mineral content of the soil and the water, etc. Anything that can impact electrical conductivity will affect the results.

So keep in mind that using the FC-28 in this way gives us a rough, indirect measure of water content in the soil. Given that, I’m hoping to simply see trends such as “more wet after a rain” and “more dry after days of no rain” and hopefully trends where the moisture of the covered soil differs in some way from the exposed soil.

Each of the moisture probes is buried to the black separation line of the probe allowing the device to be exposed to the top 4cm of soil.


The units can provide digital output (wet/dry) or analog output (a wetness range). The analog range is programmatically 0-1023, with 1023 being completely dry. The units measure electrical resistance between the two fork-prongs of the probe. When the prongs are buried in soil, the sensor measures the resistance in that soil. More water in the soil generally means less electrical resistance. Water conducts electricity, but variably, depending on its hardness, with distilled water being less conductive than a highly mineralized, say hard water.

Working with the data

Reading the analog value from the FC-28 is simply an analogRead() of the data pin. In the example below, I pass the parameter moisturePin to the function to identify which probe to poll since there are two.

int ValueRawMoistureSensor = analogRead(moisturePin);

I then transmit the data using a LoRa radio to a web gateway in the house. The web gateway receives the raw, analog value and using Arduino’s map() function converts the raw analog value to a percentage based on a range I set (see below).

When building the LoRa packet header for the moisture data, Sensor A is identified as 0x1 and Sensor B is identified as 0x2. The web gateway looks for those addresses and knows to publish to the appropriate MQTT feed for each set of data.


This isn’t really calibrating the sensor as much as it’s setting a ceiling for “full-wetness.” Remember the sensor can report a range between 0 - 1023 and that’s a measure of resistance, and water is comparatively a poor conductor. With the probe submerged in water, the sensor does not report an analog value of “0” because there is still some resistance.

Therefore to “calibrate” a rough saturation point, I put the sensor in water and record the value it reported (380). Then using map() I set that as the 100% value like this:

int MoistureMapped = map(MoistureRaw,1023,380,0,100);

That converts the analog range of 1023 - 380 to the percentage range of 0 - 100%. The mapped value is then sent to io.Adafruit.com using MQTT and is available for graphing and analyzing.

A better way of calibrating might be to soak the soil until it puddles around the probe and use that as the 100% saturation point…

Update April 22, 2020: I later did this form of ‘calibration’ and it came to the same results as a glass of water.


Lessons learned

Two of the sensor probes I used did not survive much longer than 30 days in the moist soil of Bali during the wet season. The first sensor failure I had was after about 10 days of deployment and the point of failure was the connection posts that I had left exposed to the elements.

corodded post

The second probe failure was also a corrosion/oxidation failure. I have no idea how long this degradation process was occurring and how it affected the data, which it undoubtedly did. My method of cleaning also didn’t help in the long run. I first used a nylon brush, which was okay, but that revealed some crusty bits that I took a metal brush to and that was a bad idea, as the brush took off more of the very corroded and oxidized strip.

corrosion more corrosion

Final corrosion pics

I dusted off the macro lens for the camera and took some pictures of the weathering that occurred on the probes. These are clearly not meant to sit in moist, slightly acidic (6.5pH) soils for long:

corroded prongs