Community project
Remote Weather Monitoring Station
This remote weather monitoring station captures comprehensive environmental data from a field location and transmits it via cellular network. Built around an ESP32 microcontroller, the station measures temperature at seven locations using DS18B20 probes, reads humidity and air temperature from a DHT21 sensor, monitors barometric pressure with a BMP280, detects rainfall, measures soil moisture across 16 channels, and tracks battery voltage—then queues and sends all measurements over GPRS using a SIM900 module.
This guide provides a complete wiring diagram, full parts list with resistor values for power conditioning and level shifting, and step-by-step assembly instructions covering power supply protection, sensor wiring, the multiplexed soil-moisture circuit, and battery monitoring. The included Arduino firmware handles sensor reading, local data queuing during connectivity gaps, and reliable transmission to a remote server, with the station entering deep sleep between measurement cycles to conserve power.
Wiring diagram

Gather all the parts
Assemble it in 7 steps
1. Build the protected battery supply
Connect the 18650 cell positive terminal to charger_tp4056 B+ and its negative terminal to B-. Connect charger_tp4056 OUT+ to esp_regulator VIN and OUT- to esp_regulator GND. Set and verify the regulator output is 3.3 V before connecting it to the ESP32 3V3 and GND pins.
- Charge through the TP4056 USB input; use only a protected single 18650 cell.
- Use short, thicker wires for the battery and regulator connections.
- Never connect the 18650 cell directly to the ESP32 3V3 pin — an overcharged cell can damage the board.
- Do not reverse the battery leads; reversed battery wiring can damage the charger and cause the cell to overheat.
2. Give the GSM module its own strong supply wires
Connect sim900_1 VCC to charger_tp4056 OUT+ and sim900_1 GND to charger_tp4056 OUT-. Put sim_bulk_capacitor directly across the same two SIM900 power points: its + leg goes to VCC and its marked - leg goes to GND. Connect SIM900 TX to ESP32 GPIO16. Make the receive voltage divider: ESP32 GPIO17 to sim_rx_series End 1, sim_rx_series End 2 to SIM900 RX, and sim_rx_shunt End 1 also to SIM900 RX with its End 2 to GND.
- Place the 470 µF capacitor physically close to the SIM900 VCC and GND pins.
- The two resistors reduce the ESP32’s 3.3 V transmit signal for the SIM900 receive pin.
- Do not power the SIM900 from the ESP32 3V3 pin; its short radio bursts can pull far more current than the board supply can safely provide.
- Make sure the capacitor stripe marking is on the GND side — reversed electrolytic capacitors can fail violently.
3. Add the switched sensor supply
Connect sensor_power_switch VIN to the ESP32 3V3 pin and GND to ESP32 GND. Connect its EN pin to GPIO14. Its VOUT pin is the switched 3.3 V supply used by every sensor in the following steps.
- Keep all sensor ground wires connected to the ESP32 GND rail even though their positive supply is switched off during sleep.
- Make sure VIN and VOUT are not swapped — swapped connections can leave the sensors permanently off or damage a module.
4. Wire the air, pressure, light, and rain sensors
Connect dht21_1 VCC to switched VOUT (power), GND to GND (ground), and DATA to GPIO4 (signal). Connect bmp280_1 VCC to switched VOUT (power), GND to GND (ground), SCL to GPIO22 (clock), SDA to GPIO21 (data), CSB to GND (selects the two-wire I²C mode), and SDO to GND (selects address 0x76). For the light divider, connect ldr_1 Terminal 1 to switched VOUT; join its Terminal 2 with ldr_fixed_resistor End 1 and GPIO34; connect ldr_fixed_resistor End 2 to GND. Connect rain_sensor VCC to switched VOUT (power), GND to GND (ground), and AO to GPIO36 (signal).
- All sensor modules must run at 3.3 V so their output voltage stays safe for the ESP32.
- Keep the BMP280 dry inside the enclosure but give it small air vents so it can measure air pressure.
- Do not connect a 5 V rain or soil module analog output to an ESP32 pin; voltage above 3.3 V can damage it.
- Make sure VCC and GND are not swapped on the BMP280 — swapped power can damage the sensor.
5. Wire the seven temperature probes
Connect every DS18B20 probe in parallel: all VCC wires to switched VOUT, all GND wires to GND, and all DATA wires together on GPIO27. Connect onewire_pullup between switched VOUT and the same GPIO27 data junction; this is the 4.7 kΩ pull-up resistor.
- Label each probe by depth before burying it: 0, 15, 30, 45, 60, 75, and 90 cm.
- Waterproof probes are needed for direct soil placement.
- Do not omit the 4.7 kΩ resistor; without it the shared temperature wire will give unreliable or missing readings.
6. Wire the soil-moisture multiplexer
Connect soil_mux VCC to switched VOUT, GND to GND, and EN to GND. Connect S0 to GPIO25, S1 to GPIO26, S2 to GPIO32, S3 to GPIO33, and SIG to GPIO39. For each soil sensor, connect VCC to switched VOUT and GND to GND. Connect their AO wires in order to Y0, Y1, Y2, Y3, Y4, Y5, and Y6 on the multiplexer.
- Use the same depth labels as the matching DS18B20 probes so the dashboard values remain meaningful.
- Capacitive soil probes last much longer in wet ground than exposed-metal FC-28 style probes.
- Do not power soil-sensor modules from 5 V; their analog output may rise above the ESP32-safe 3.3 V limit.
7. Add the battery measurement divider and inspect the build
Connect battery_divider_top End 1 to charger_tp4056 OUT+. Join battery_divider_top End 2, battery_divider_bottom End 1, and ESP32 GPIO35. Connect battery_divider_bottom End 2 to GND. Recheck that every module has a ground return, then connect the regulator’s 3.3 V output to ESP32 3V3 and its ground to ESP32 GND.
- The resistor divider makes the battery voltage safe for GPIO35.
- Keep the ESP32 USB port available; it is used for the first firmware deployment.
- Do not connect the charger OUT+ point directly to GPIO35 — the divider is required to keep battery voltage away from the ESP32 input.
Review all connections
1. Connections between "battery_18650" and "ESP32"
2. Connections between "charger_tp4056" and "ESP32"
3. Connections between "esp_regulator" and "ESP32"
4. Connections between "sensor_power_switch" and "ESP32"
5. Connections between "dht21_1" and "ESP32"
6. Connections between "ds18b20_soil_array" and "ESP32"
7. Connections between "onewire_pullup" and "ESP32"
8. Connections between "bmp280_1" and "ESP32"
9. Connections between "soil_mux" and "ESP32"
10. Connections between "rain_sensor" and "ESP32"
11. Connections between "ldr_1" and "ESP32"
12. Connections between "ldr_fixed_resistor" and "ESP32"
13. Connections between "soil_sensor_0" and "ESP32"
14. Connections between "soil_sensor_15" and "ESP32"
15. Connections between "soil_sensor_30" and "ESP32"
16. Connections between "soil_sensor_45" and "ESP32"
17. Connections between "soil_sensor_60" and "ESP32"
18. Connections between "soil_sensor_75" and "ESP32"
19. Connections between "soil_sensor_90" and "ESP32"
20. Connections between "sim900_1" and "ESP32"
21. Connections between "sim_rx_series" and "ESP32"
22. Connections between "sim_rx_shunt" and "ESP32"
23. Connections between "sim_bulk_capacitor" and "ESP32"
24. Connections between "battery_divider_top" and "ESP32"
25. Connections between "battery_divider_bottom" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_BMP280.h>
#include <LittleFS.h>
#include "esp_sleep.h"
// Field weather station: reads sensors, queues measurements locally, then posts by SIM900 GPRS.
// Forward declarations
void selectMuxChannel(uint8_t channel);
int averageAnalog(int pin, int samples);
float batteryVoltage();
int batteryPercent(float volts);
void clearModem();
bool waitFor(const char *text, uint32_t timeout);
bool at(const String &command, const char *expect, uint32_t timeout);
bool modemReady();
bool startGprs();
bool postMeasurement(const String &body);
bool beginQueue();
void queueMeasurement(const String &body);
bool firstQueued(String &line);
bool removeFirstQueued();
void sendQueue();
constexpr int STATION_ID = 22;
constexpr uint64_t SLEEP_US = 30ULL * 60ULL * 1000000ULL;
constexpr char APN[] = "izi.me";
constexpr char POST_URL[] = "http://37.151.253.210:2409/dashboard/index.php";
constexpr char QUEUE_FILE[] = "/post_queue.txt";
constexpr int SENSOR_POWER_PIN = 14;
constexpr int DHT_PIN = 4;
constexpr int ONE_WIRE_PIN = 27;
constexpr int BMP_SDA_PIN = 21;
constexpr int BMP_SCL_PIN = 22;
constexpr int PHOTO_PIN = 34;
constexpr int RAIN_PIN = 36;
constexpr int SOIL_MUX_PIN = 39;
constexpr int MUX_S0_PIN = 25;
constexpr int MUX_S1_PIN = 26;
constexpr int MUX_S2_PIN = 32;
constexpr int MUX_S3_PIN = 33;
constexpr int BATTERY_PIN = 35;
constexpr int SIM_RX_PIN = 16; // ESP32 RX receives SIM900 TX
constexpr int SIM_TX_PIN = 17; // ESP32 TX, through the resistor divider, drives SIM900 RX
constexpr float BATTERY_R_TOP_KOHM = 99.6f;
constexpr float BATTERY_R_BOTTOM_KOHM = 50.0f;
constexpr float ADC_REFERENCE = 3.3f;
constexpr float ADC_CORRECTION = 1.107f;
constexpr int SOIL_DRY = 4095;
constexpr int SOIL_WET = 600;
constexpr int RAIN_DRY = 4095;
constexpr int RAIN_WET = 0;
DHT dht(DHT_PIN, DHT21);
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature soilTemps(&oneWire);
Adafruit_BMP280 bmp;
HardwareSerial sim900(2);
DeviceAddress temperatureAddresses[7] = {
{0x28,0x33,0x59,0x34,0x00,0x00,0x00,0x8A},
{0x28,0x0D,0x23,0x34,0x00,0x00,0x00,0xA4},
{0x28,0xBF,0x36,0x34,0x00,0x00,0x00,0x63},
{0x28,0x8B,0x1F,0x36,0x00,0x00,0x00,0x5E},
{0x28,0xA2,0x7A,0x38,0x00,0x00,0x00,0xA8},
{0x28,0xE9,0x27,0x36,0x00,0x00,0x00,0x49},
{0x28,0xF0,0x48,0x38,0x00,0x00,0x00,0xEF}
};
void selectMuxChannel(uint8_t channel) {
digitalWrite(MUX_S0_PIN, channel & 1);
digitalWrite(MUX_S1_PIN, (channel >> 1) & 1);
digitalWrite(MUX_S2_PIN, (channel >> 2) & 1);
digitalWrite(MUX_S3_PIN, (channel >> 3) & 1);
}
int averageAnalog(int pin, int samples = 8) {
uint32_t total = 0;
for (int i = 0; i < samples; ++i) { total += analogRead(pin); delay(5); }
return total / samples;
}
float batteryVoltage() {
float adc = averageAnalog(BATTERY_PIN, 20);
return (adc / 4095.0f) * ADC_REFERENCE * ADC_CORRECTION *
((BATTERY_R_TOP_KOHM + BATTERY_R_BOTTOM_KOHM) / BATTERY_R_BOTTOM_KOHM);
}
int batteryPercent(float volts) {
return constrain((int)((volts - 3.0f) * 100.0f / 1.2f), 0, 100);
}
void clearModem() { while (sim900.available()) sim900.read(); }
bool waitFor(const char *text, uint32_t timeout) {
String reply;
uint32_t started = millis();
while (millis() - started < timeout) {
while (sim900.available()) {
char c = (char)sim900.read();
Serial.write(c);
reply += c;
if (reply.indexOf(text) >= 0) return true;
if (reply.indexOf("ERROR") >= 0) return false;
if (reply.length() > 400) reply.remove(0, 200);
}
delay(1);
}
return false;
}
bool at(const String &command, const char *expect, uint32_t timeout) {
clearModem();
sim900.println(command);
return waitFor(expect, timeout);
}
bool modemReady() {
for (int i = 0; i < 3; ++i) if (at("AT", "OK", 2000)) return true;
return false;
}
bool startGprs() {
if (!at("AT+SAPBR=3,1,\"CONTYPE\",\"GPRS\"", "OK", 5000)) return false;
if (!at(String("AT+SAPBR=3,1,\"APN\",\"") + APN + "\"", "OK", 5000)) return false;
if (!at("AT+SAPBR=1,1", "OK", 60000)) {
if (!at("AT+SAPBR=2,1", "+SAPBR: 1,1", 5000)) return false;
}
return true;
}
bool postMeasurement(const String &body) {
if (!modemReady() || !startGprs()) return false;
at("AT+HTTPTERM", "OK", 2000);
bool ok = at("AT+HTTPINIT", "OK", 5000) &&
at("AT+HTTPPARA=\"CID\",1", "OK", 5000) &&
at(String("AT+HTTPPARA=\"URL\",\"") + POST_URL + "\"", "OK", 10000) &&
at("AT+HTTPPARA=\"CONTENT\",\"application/x-www-form-urlencoded\"", "OK", 5000);
if (ok) {
clearModem();
sim900.print("AT+HTTPDATA="); sim900.print(body.length()); sim900.println(",10000");
ok = waitFor("DOWNLOAD", 5000);
if (ok) { sim900.print(body); ok = waitFor("OK", 10000); }
if (ok) { clearModem(); sim900.println("AT+HTTPACTION=1"); ok = waitFor("+HTTPACTION: 1,200", 60000); }
}
at("AT+HTTPTERM", "OK", 5000);
at("AT+SAPBR=0,1", "OK", 10000);
return ok;
}
bool beginQueue() {
if (!LittleFS.begin(true)) return false;
if (!LittleFS.exists(QUEUE_FILE)) { File f = LittleFS.open(QUEUE_FILE, FILE_WRITE); if (!f) return false; f.close(); }
return true;
}
void queueMeasurement(const String &body) {
File f = LittleFS.open(QUEUE_FILE, FILE_APPEND);
if (f) { f.println(body); f.close(); }
}
bool firstQueued(String &line) {
File f = LittleFS.open(QUEUE_FILE, FILE_READ);
if (!f || !f.available()) { if (f) f.close(); return false; }
line = f.readStringUntil('\n'); f.close(); line.trim(); return line.length() > 0;
}
bool removeFirstQueued() {
File input = LittleFS.open(QUEUE_FILE, FILE_READ);
File output = LittleFS.open("/queue_tmp.txt", FILE_WRITE);
if (!input || !output) return false;
input.readStringUntil('\n');
while (input.available()) output.println(input.readStringUntil('\n'));
input.close(); output.close();
LittleFS.remove(QUEUE_FILE);
return LittleFS.rename("/queue_tmp.txt", QUEUE_FILE);
}
void sendQueue() {
String body;
while (firstQueued(body)) {
if (!postMeasurement(body)) break;
if (!removeFirstQueued()) break;
}
}
void setup() {
Serial.begin(115200);
pinMode(SENSOR_POWER_PIN, OUTPUT);
digitalWrite(SENSOR_POWER_PIN, HIGH);
pinMode(MUX_S0_PIN, OUTPUT); pinMode(MUX_S1_PIN, OUTPUT);
pinMode(MUX_S2_PIN, OUTPUT); pinMode(MUX_S3_PIN, OUTPUT);
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
delay(1500); // sensors need time to receive stable 3.3 V
dht.begin();
soilTemps.begin();
Wire.begin(BMP_SDA_PIN, BMP_SCL_PIN);
bool bmpFound = bmp.begin(0x76, &Wire);
float airHumidity = dht.readHumidity();
float airTemp = dht.readTemperature();
float pressureMmHg = NAN;
if (bmpFound) { bmp.setSampling(Adafruit_BMP280::MODE_FORCED); bmp.takeForcedMeasurement(); pressureMmHg = bmp.readPressure() / 100.0f * 0.750062f; }
float soilTemperature[7];
soilTemps.requestTemperatures();
delay(750);
for (int i = 0; i < 7; ++i) soilTemperature[i] = soilTemps.getTempC(temperatureAddresses[i]);
int soilHumidity[7];
for (int i = 0; i < 7; ++i) {
selectMuxChannel(i); delay(100);
soilHumidity[i] = constrain(map(averageAnalog(SOIL_MUX_PIN, 5), SOIL_DRY, SOIL_WET, 0, 100), 0, 100);
}
int light = constrain(map(averageAnalog(PHOTO_PIN), 4095, 0, 0, 100), 0, 100);
int rain = constrain(map(averageAnalog(RAIN_PIN), RAIN_DRY, RAIN_WET, 0, 100), 0, 100);
float volts = batteryVoltage();
String body = "station_id=" + String(STATION_ID);
body += "&temperature_air=" + String(airTemp, 1);
body += "&humidity_air=" + String(airHumidity, 1);
body += "&pressure=" + String(pressureMmHg, 1);
body += "&illumination=" + String(light);
const int depth[] = {0,15,30,45,60,75,90};
for (int i = 0; i < 7; ++i) {
body += "&humidity_soil_" + String(depth[i]) + "=" + String(soilHumidity[i]);
body += "&temperature_soil_" + String(depth[i]) + "=" + String(soilTemperature[i], 1);
}
body += "&precipitation=" + String(rain);
body += "&battery=" + String(batteryPercent(volts));
body += "&version_firmware=1.0.0";
Serial.println(body);
sim900.begin(9600, SERIAL_8N1, SIM_RX_PIN, SIM_TX_PIN);
if (beginQueue()) { queueMeasurement(body); sendQueue(); }
digitalWrite(SENSOR_POWER_PIN, LOW);
esp_sleep_enable_timer_wakeup(SLEEP_US);
delay(100);
esp_deep_sleep_start();
}
void loop() {}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.




