Community project

Smart Agro Monitoring Station

Photo of Smart Agro Monitoring Station
Generated with AI

Asilbek Panjiyev

Published November 14, 2026

The Smart Agro Monitoring Station is a comprehensive environmental data logger built around the ESP32 that captures soil moisture, temperature, humidity, atmospheric pressure, light levels, and rainfall in a single integrated system. It combines capacitive soil sensing with a full weather station suite (BME280, BH1750, tipping bucket rain gauge) and transmits collected data via 4G cellular modem, making it ideal for remote agricultural monitoring, research stations, or environmental studies where grid connectivity is unavailable.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building and deploying the station in field conditions. The included firmware handles sensor acquisition, data validation, cellular transmission, and graceful error recovery. Builders will learn how to calibrate soil moisture sensors, configure the SIM7600G-H modem, manage power distribution across multiple sensor buses, and structure JSON telemetry for cloud ingestion.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

Gravity: IP65 Capacitive Soil Moisture Sensor

Capacitive soil moisture sensor with IP65 waterproof and corrosion-resistant construction. Compatible with Arduino, ESP32, and Raspberry Pi.

1

DS18B20

Digital temperature sensor using OneWire protocol

1

Adafruit BME280 I2C or SPI Temperature Humidity Pressure Sensor

Adafruit breakout for the Bosch BME280 sensor, measuring temperature, relative humidity and barometric pressure. It talks over I2C (address 0x77, or 0x76 with SDO to ground) or SPI, runs from 3 to 5 V through an on-board regulator with level shifting, and connects through STEMMA QT/Qwiic connectors or a 7-pin 0.1 in header.

1

BH1750

Rohm BH1750FVI 16-bit digital ambient light sensor with I2C interface. Measures illuminance from 1 to 65535 lux. Two selectable I2C addresses (0x23 / 0x5C) via ADDR pin.

1

Resistor

4.7 kohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kohm

Through-hole resistor (current-limiting in series with an LED)

1

SparkFun SEN-28867 tipping bucket rain gauge

Passive reed contact on center RJ11 conductors; manufacturer nominal 0.2794 mm per closure. No simulator model.

1

Waveshare SIM7600G-H 4G HAT new carrier with SIM7600G-H R2

External module; 5V input, onboard conversion and 330uF filtering. Set VCCIO to 3.3V; PWR-3V3 auto-start jumper. Verify R2 modem supplied. No simulator model.

1

Regulated isolated 5V 4A DC adapter

Certified enclosed SELV supply, screw-terminal DC output; no exposed mains wiring.

1

Adafruit LM73100 23V 5A Max Ideal Diode Breakout

Compact ideal-diode power breakout based on the LM73100, with terminal blocks for inline 3–22 V DC power paths up to 5 A and monitor/control pins broken out for breadboard use.

Assemble it in 6 steps

1. Prepare the power branches

Disconnect all power. Supply +5V goes to protection IN (VIN); protection OUT (VOUT) splits to ESP32 5V and modem 5V input. Supply negative, protection GND, ESP32 GND and modem GND join together. Use short screw-terminal power leads; do not pass modem power through the breadboard.

  • Never connect 5V to bare modem VBAT or ESP32 3V3. Disconnect external ESP32 5V when plugging USB into the ESP32 to avoid backfeeding. Keep mains adapter indoors or in an appropriately rated installation.

2. Configure the modem before power

Use the new Waveshare carrier with its onboard 330uF capacitor. Set VCCIO solder selection to 3.3V and PWR jumper to 3V3 for automatic startup. Leave flight-mode jumper disconnected. Connect TXD to GPIO16 (receive), RXD to GPIO17 (transmit). Do not connect a USB-UART adapter that drives these same pins simultaneously.

  • Verify SIM7600G-H R2 is supplied; carrier revision is not the modem revision. Install an activated data SIM of the size fitting the carrier slot while powered off. Attach the supplied LTE antenna to MAIN; fit the supplied diversity antenna to AUX if included. GNSS connector is not LTE. Wait at least 12 seconds after power-up.

3. Connect the soil probes

SEN0308 red VCC to 3V3, black GND to GND, yellow AOUT to GPIO34 (analog input). DS18B20 VCC to 3V3, GND to GND, DATA to GPIO4. Fit the 4.7 kohm resistor between DATA and 3V3. Check the probe supplier wire labels rather than assuming its colors.

  • Breadboard rails may be split: check continuity. GPIO34 cannot drive an output. Use three-wire powered DS18B20, not parasite power.

4. Connect the shared environmental bus

BME280 VIN and BH1750 VCC to 3V3; both grounds to GND. BME280 physical SDI and BH1750 SDA to GPIO21 (data). BME280 physical SCK and BH1750 SCL to GPIO22 (clock). BH1750 SDA/SCL join the BME280 SDI/SCK wiring junctions on the same GPIO21/22 bus. Leave BME280 CS and SDO unconnected. BH1750 ADDR to GND.

  • BME280 address 0x77 and BH1750 0x23 do not conflict. Adafruit breakout includes pull-ups; inspect the BH1750 breakout for additional pull-ups and measure the combined bus resistance before adding any. Keep I2C leads short, inside the enclosure.

5. Connect the rainfall contact

Use the two center conductors of the SparkFun SEN-28867 RJ11 cable. One goes to GPIO27 (pulse input); the other to GND. Connect 10 kohm from GPIO27 to 3V3. There is no separate gauge supply. Mount the collector level and clear of splash and obstructions.

  • Each falling edge is counted with 50ms software debounce. Verify one count per manual tip before installing. Nominal 0.2794 mm per tip applies only to this gauge.

6. Mount for protected field trials

Use an IP65 or IP66 enclosure, matching rated cable glands, UV-resistant cable and strain relief. Place BME280 in a ventilated rain-protected radiation shield away from electronics heat. Mount BH1750 under an optically clear weather cover, then compare readings after fitting it. Keep the LTE antenna outside a metal enclosure and above nearby obstructions.

  • The assembly is not certified waterproof. Do not coat sensing surfaces or bury the soil probe above its specified insertion limit. Long field cables require engineered surge/ESD protection before unattended use. Do not use a breadboard outdoors.

Review all connections

1. Connections between "soil" and "ESP32"

FunctionsoilESP32
powerVCC3V3
groundGNDGND
analogAOUTGPIO 34

2. Connections between "temp" and "ESP32"

FunctiontempESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 4

3. Connections between "pullTemp" and "ESP32"

FunctionpullTempESP32
digitalP1 → DS18B20 DATAEXT
powerP23V3

4. Connections between "air" and "ESP32"

FunctionairESP32
powerVIN3V3
groundGNDGND
i2cSDIGPIO 21
i2cSCKGPIO 22

5. Connections between "light" and "ESP32"

FunctionlightESP32
powerVCC3V3
groundGNDGND
i2cSDA → Adafruit BME280 I2C or SPI Temperature Humidity Pressure Sensor SDIEXT
i2cSCL → Adafruit BME280 I2C or SPI Temperature Humidity Pressure Sensor SCKEXT
groundADDRGND

6. Connections between "rain" and "ESP32"

FunctionrainESP32
digitalSIGNALGPIO 27
groundGNDGND

7. Connections between "pullRain" and "ESP32"

FunctionpullRainESP32
digitalP1 → SparkFun SEN-28867 tipping bucket rain gauge SIGNALEXT
powerP23V3

8. Connections between "modem" and "ESP32"

FunctionmodemESP32
uartTXDGPIO 16
uartRXDGPIO 17
groundGNDGND

9. Connections between "supply" and "ESP32"

FunctionsupplyESP32
powerPOS → Adafruit LM73100 23V 5A Max Ideal Diode Breakout VINEXT
groundGNDGND

10. Connections between "protection" and "ESP32"

FunctionprotectionESP32
groundGNDGND
powerVOUT5V
powerVOUT → Waveshare SIM7600G-H 4G HAT new carrier with SIM7600G-H R2 VCCEXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <ArduinoJson.h>
#include <sys/time.h>
#include "config.h"
#include "data_models.h"
#include "sensors.h"
String deviceId;uint32_t bootId; volatile time_t lastSuccess=0;
#include "modem_manager.h"
#include "api_client.h"
QueueHandle_t packets;volatile uint32_t dropped=0;
uint32_t sampleStart=0,nextSample=0,lastSend=0,lastUrgent=0;bool converting=false,wasCritical=false;
void measurement(JsonDocument &j,const char* key,float v,const char* unit,float lo,float hi,const char* bad="unavailable"){auto x=j["measurements"][key];x["unit"]=unit;if(isfinite(v)&&v>=lo&&v<=hi){x["value"]=v;x["status"]="ok";}else{x["value"]=nullptr;x["status"]=bad;}}
void acquire(){
 if(!bmeOK)bmeOK=bme.begin(0x77);if(!luxOK)luxOK=lux.begin(BH1750::CONTINUOUS_HIGH_RES_MODE,0x23,&Wire);
 uint32_t sum=0;for(int i=0;i<32;i++)sum+=analogRead(34);float raw=sum/32.f;
 float moisture=NAN;if(SOIL_DRY>=0&&SOIL_WET>=0&&SOIL_DRY!=SOIL_WET&&raw>5&&raw<4090)moisture=constrain(100.f*(raw-SOIL_DRY)/(SOIL_WET-SOIL_DRY),0.f,100.f);
 float st=ds.getTempCByIndex(0),at=NAN,ah=NAN,ap=NAN,ll=NAN;
 if(bmeOK&&present(0x77)){at=bme.readTemperature();ah=bme.readHumidity();ap=bme.readPressure()/100.f;}else bmeOK=false;
 if(luxOK&&present(0x23)){ll=lux.readLightLevel();}else luxOK=false;
 uint32_t count;portENTER_CRITICAL(&rainMux);count=tips;portEXIT_CRITICAL(&rainMux);
 JsonDocument j;j["device_id"]=deviceId;j["boot_id"]=bootId;j["uptime_ms"]=millis();time_t now=time(nullptr);bool timed=now>1794000000;
 if(timed){tm t;gmtime_r(&now,&t);char b[24];strftime(b,sizeof(b),"%Y-%m-%dT%H:%M:%SZ",&t);j["timestamp"]=b;}else j["timestamp"]=nullptr;
 j["time_quality"]=timed?"cellular_clock":"unsynchronized";
 measurement(j,"soil_moisture",moisture,"%",0,100,"uncalibrated_or_invalid_adc");j["soil_adc_raw"]=raw;j["soil_quality"]="relative_soil_specific_not_VWC; unplugged_analog_not_reliably_detectable";
 measurement(j,"soil_temperature",st,"C",-55,125);measurement(j,"air_temperature",at,"C",-40,85);measurement(j,"air_humidity",ah,"%",0,100);measurement(j,"air_pressure",ap,"hPa",300,1100);measurement(j,"light_intensity",ll,"lux",0,65000);
 measurement(j,"rainfall",count*MM_PER_TIP,"mm",0,1e9);j["rain_tips"]=count;j["rainfall_basis"]="cumulative_since_boot";j["rain_status"]="passive_contact_not_self_diagnosable";
 j["status"]["cellular"]=dataConnected;j["status"]["csq"]=signalQuality;j["status"]["last_http_or_error"]=lastError;j["status"]["last_success_epoch"]=lastSuccess;j["status"]["queue_dropped"]=dropped;
 bool critical=(isfinite(moisture)&&moisture<CRITICAL_SOIL)||(isfinite(at)&&at>CRITICAL_AIR);uint32_t ms=millis();bool urgent=critical&&!wasCritical&&(lastUrgent==0||ms-lastUrgent>=URGENT_MS);wasCritical=critical;
 Packet p={};p.uptime=ms;p.epoch=timed?now:0;serializeJson(j,p.json,sizeof(p.json));Serial.println(p.json);
 if(xQueueSend(packets,&p,0)!=pdTRUE){Packet old;xQueueReceive(packets,&old,0);xQueueSend(packets,&p,0);++dropped;Serial.println("QUEUE_FULL: oldest waiting sample dropped");}
 if(urgent){lastUrgent=ms;xTaskNotifyGive(xTaskGetHandle("uplink"));}
}
void networkTask(void*){
 modemPort.begin(115200,SERIAL_8N1,16,17);tls.setClient(&tcp);tls.setCACert(ROOT_CA);tls.setTimeout(15);vTaskDelay(pdMS_TO_TICKS(12000));bool initialized=false;Packet pending={};bool held=false;uint32_t retry=10000,clockCheck=0;
 for(;;){
  if(!held)ulTaskNotifyTake(pdTRUE,pdMS_TO_TICKS(SEND_MS));
  if(!initialized)initialized=initModem();dataConnected=initialized&&connectData();
  if(!dataConnected){vTaskDelay(pdMS_TO_TICKS(retry));retry=min<uint32_t>(retry*2,300000);continue;}
  if(time(nullptr)<1794000000||millis()-clockCheck>21600000){syncClock();clockCheck=millis();}
  if(!held)held=xQueueReceive(packets,&pending,0)==pdTRUE;
  while(held){int code=sendPacket(pending);lastError=code;Serial.printf("HTTP/error=%d queue=%u CSQ=%d\n",code,(unsigned)uxQueueMessagesWaiting(packets),(int)signalQuality);
   if(code>=200&&code<300){lastSuccess=time(nullptr);held=xQueueReceive(packets,&pending,0)==pdTRUE;retry=10000;vTaskDelay(pdMS_TO_TICKS(25));}
   else{if(code>=400&&code<500&&code!=408&&code!=429){Serial.println("Permanent request failure: fix backend/auth; retaining pending packet");}vTaskDelay(pdMS_TO_TICKS(retry));retry=min<uint32_t>(retry*2,300000);break;}
  }
 }
}
void setup(){Serial.begin(115200);setenv("TZ","UTC0",1);tzset();bootId=esp_random();uint64_t mac=ESP.getEfuseMac();char id[40];snprintf(id,sizeof(id),"SMARTAGRO-%04X%08X",(unsigned)(mac>>32),(unsigned)mac);deviceId=strlen(DEVICE_ID)?DEVICE_ID:id;host=API_BASE_URL;host.replace("https://","");while(host.endsWith("/"))host.remove(host.length()-1);packets=xQueueCreate(24,sizeof(Packet));if(!packets){Serial.println("QUEUE_ALLOC_FAILED");while(true)delay(1000);}initSensors();xTaskCreatePinnedToCore(networkTask,"uplink",16384,nullptr,1,nullptr,0);Serial.println("SMART AGRO AI: configure APN, backend, CA, secret and soil calibration; no simulated measurements.");}
void loop(){uint32_t now=millis();if(!converting&&(int32_t)(now-nextSample)>=0){ds.begin();ds.setWaitForConversion(false);ds.requestTemperatures();sampleStart=now;converting=true;nextSample=now+READ_MS;}if(converting&&now-sampleStart>=800){converting=false;acquire();}delay(10);}
// Wiring reviewed: BME SDI/SCK on GPIO21/22; BH1750 joins those same nets.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik