Contents — 26 sections
Project Overview
A hyperlocal weather station that measures the conditions on your actual field — not the airport 30 km away — with correctly-sited sensors, solar power, and the derived agricultural metrics that generic forecasts never give you.
Farmers make weather-dependent decisions constantly — when to spray, irrigate, sow, harvest, protect against frost — and they usually make them on a forecast for a town or an airport that may be tens of kilometres away and hundreds of metres different in elevation. Weather is intensely local: a frost that settles in a valley bottom spares the slope above it, rainfall varies enormously over short distances, and wind on an exposed ridge bears no relation to the sheltered station in the valley. A weather station on your actual field measures what is actually happening where it matters.
This is, on the surface, a straightforward project — read some weather sensors — and the difficulty is entirely in doing it correctly. Weather measurement is a discipline with established siting and shielding standards, and ignoring them produces confident, wrong numbers. Temperature must be measured in a shaded, ventilated radiation shield at a standard height. Rainfall needs a properly-sized tipping-bucket gauge sited away from obstructions. Wind needs an anemometer clear of turbulence-causing structures at the standard 10 m (or a documented lower height). Get the siting wrong and the data is worse than the airport's, because at least the airport is sited properly.
Beyond the raw measurements, the station computes the derived agricultural metrics that generic weather services do not provide: growing degree days (which predict crop development and pest emergence), evapotranspiration (which drives irrigation, as in the drip project), leaf wetness duration and conditions favouring disease, chill hours (for fruit trees), and frost risk. These derived quantities are what actually inform farm decisions, and computing them from local data is the point.
The station is built for unattended field life: solar-powered, reporting over LoRa or cellular from a field with no Wi-Fi, rugged and sealed, and logging a continuous local record. Deployed correctly, it turns "the forecast said" into "my field is", which is a much better basis for a decision that depends on the actual conditions on the ground.
What this project does
- Measures temperature, humidity, pressure, rainfall, wind speed and direction, and solar radiation.
- Uses correctly-sited and shielded sensors per meteorological standards.
- Computes agricultural metrics: growing degree days, ET, leaf wetness, chill hours, frost risk.
- Runs on solar power, reporting over LoRa or cellular from remote fields.
- Logs a continuous local record independent of connectivity.
- Alerts on frost, high wind (spray decisions) and disease-favouring conditions.
- Provides the hyperlocal data that generic forecasts cannot.
Real-World Applications
| Setting | How it is used |
|---|---|
| Spray-timing decisions | Wind speed and rain determine whether and when to spray — from your field, not a distant forecast. |
| Frost protection | Local frost risk, especially in frost-pocket valleys the forecast misses, triggers protection in time. |
| Irrigation scheduling | Local ET drives demand-based irrigation (the drip project) far better than regional estimates. |
| Pest and disease forecasting | GDD predicts pest emergence; leaf wetness and conditions predict disease — both need local data. |
| Crop development tracking | Growing degree days predict growth stages and harvest timing. |
| Fruit-tree management | Chill-hour accumulation determines dormancy break and is highly local. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Correctly-sited, shielded sensing — the discipline that separates useful data from confident nonsense.
- Aspirated radiation shield for true air temperature.
- Tipping-bucket rain gauge with proper calibration.
- Anemometer and wind vane sited to avoid turbulence.
- Derived agricultural metrics: GDD, ET, leaf wetness, chill hours, frost.
- Solar power and long-range reporting for unattended field deployment.
- Continuous local logging independent of the network.
- Decision alerts tied to actual farm operations (spray, frost, disease).
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 14–20 hours |
| Indicative build cost | ₹5,800 – ₹7,500 |
| Primary discipline | Agriculture |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Arduino C++ with multi-sensor interfacing
- Pulse counting (rain, wind) and analogue reading (wind direction, radiation)
- Meteorological siting and shielding principles
- Agricultural metric computation (GDD, ET, etc.)
- Solar power and long-range communication
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 1 | ₹450 |
| BME280 pressure/humidity/temperature sensor Self-heating skews temperature by ~1 °C — read in forced mode, not continuous. | 300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz | 1 | ₹420 |
| BH1750 digital ambient light sensor Far more linear than an LDR — use it whenever you need real lux, not a relative value. | 1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye | 1 | ₹140 |
| Rain / water-level board (FC-37) Drive the electrode with AC or duty-cycle its power to slow electrolytic corrosion. | Interdigitated PCB electrode, analogue + digital comparator output | 1 | ₹90 |
| SX1278 LoRa 433 MHz module (Ra-02) Never power the radio without an antenna — the PA will destroy itself. | −148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF12 | 1 | ₹480 |
| 20 W 12 V polycrystalline solar panel Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs. | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame | 1 | ₹1,200 |
| CN3791 MPPT solar charge controller Set the MPPT point to ~80 % of panel Voc for polycrystalline modules. | 4.5–28 V in, MPPT set by resistor divider, 2 A charge to a 1S Li-ion pack | 1 | ₹320 |
| 18650 Li-ion cell 3400 mAh + holder Never charge below 0 °C; always use a protected cell or a BMS. | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 1 | ₹450 |
| Double-sided perfboard 7 × 9 cm + headers Solder female headers so the MCU can be swapped without desoldering. | FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid | 1 | ₹60 |
| IP65 ABS junction enclosure 158 × 90 × 60 mm Fit cable glands, not drilled holes, or the IP rating means nothing. | IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs | 1 | ₹260 |
| Anemometer + wind vane Site clear of turbulence; the standard height is 10 m, or document a lower height. | Cup anemometer (pulse) + potentiometer wind vane | 1 | ₹1,200 |
| Tipping-bucket rain gauge Calibrate the mm-per-tip; site away from overhanging obstructions. | 0.2 mm per tip, reed-switch output | 1 | ₹900 |
| Aspirated radiation shield Non-negotiable for accurate air temperature — an unshielded sensor reads the sun. | Multi-plate white shield + small fan for the temperature/humidity sensor | 1 | ₹600 |
| Mast and mounting hardware | Field mast for correct sensor heights | 1 | ₹800 |
Estimated total: ₹7,370, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter — continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 3.3 V logic / 5 V USB | UART, SPI, I²C, I²S, CAN, PWM | Datasheet |
| BME280 pressure/humidity/temperature sensor | 300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz | 1.7–3.6 V (module has 3.3 V LDO) | I²C (0x76/0x77) or SPI | Datasheet |
| BH1750 digital ambient light sensor | 1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye | 2.4–3.6 V | I²C (0x23/0x5C) | Datasheet |
| Rain / water-level board (FC-37) | Interdigitated PCB electrode, analogue + digital comparator output | 3.3–5 V | Analogue + digital | Datasheet |
| SX1278 LoRa 433 MHz module (Ra-02) | −148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF12 | 3.3 V | SPI | Datasheet |
| 20 W 12 V polycrystalline solar panel | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame | 12 V nominal | MC4 / screw terminals | Datasheet |
| CN3791 MPPT solar charge controller | 4.5–28 V in, MPPT set by resistor divider, 2 A charge to a 1S Li-ion pack | 4.5–28 V | Solder pads | Datasheet |
| 18650 Li-ion cell 3400 mAh + holder | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 3.0–4.2 V | Holder / spot-welded tabs | Datasheet |
| Double-sided perfboard 7 × 9 cm + headers | FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid | — | — | Datasheet |
| IP65 ABS junction enclosure 158 × 90 × 60 mm | IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs | — | — | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. |
| BME280 pressure/humidity/temperature sensor | 1.7–3.6 V (module has 3.3 V LDO) | 0.4 | Self-heating skews temperature by ~1 °C — read in forced mode, not continuous. |
| BH1750 digital ambient light sensor | 2.4–3.6 V | 0.19 | Far more linear than an LDR — use it whenever you need real lux, not a relative value. |
| Rain / water-level board (FC-37) | 3.3–5 V | 15 | Drive the electrode with AC or duty-cycle its power to slow electrolytic corrosion. |
| SX1278 LoRa 433 MHz module (Ra-02) | 3.3 V | 120 | Never power the radio without an antenna — the PA will destroy itself. |
| 20 W 12 V polycrystalline solar panel | 12 V nominal | 1140 | Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs. |
| CN3791 MPPT solar charge controller | 4.5–28 V | 2000 | Set the MPPT point to ~80 % of panel Voc for polycrystalline modules. |
Summed typical draw is 3435.59 mA. With a 1.5× design margin the supply should deliver at least 5200 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.
- Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager. - Set the correct port under Tools → Port. On Linux add yourself to the
dialoutgroup:sudo usermod -aG dialout $USERand log out and back in. - Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
- Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| Adafruit BME280 2.2.x | Compensation maths for the Bosch pressure/humidity/temperature sensor. | Library Manager → "Adafruit BME280 Library" |
| Adafruit Unified Sensor 1.1.x | Common sensor event abstraction; a dependency of most Adafruit drivers. | Library Manager → "Adafruit Unified Sensor" |
| BH1750 1.3.0 | Digital lux readings with selectable resolution modes. | Library Manager → "BH1750" by Christopher Laws |
| LoRa (sandeepmistry) 0.8.0 | SX127x radio configuration, packet TX/RX and callbacks. | Library Manager → "LoRa" by Sandeep Mistry |
| ArduinoJson 7.x | Zero-allocation JSON serialisation and parsing. | Library Manager → "ArduinoJson" by Benoit Blanchon |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
| NTPClient / configTime bundled | Wall-clock time from an NTP server for timestamping. | Bundled (`configTime()` on ESP32) |
Block Diagram
The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| BME280 (shielded) | SDA / SCL | GPIO 21 / 22 | T / RH / P, I²C |
| BH1750 solar radiation | SDA / SCL | GPIO 21 / 22 | Shared I²C, unshielded |
| Rain gauge | reed | GPIO 27 | Tip pulse, interrupt |
| Anemometer | reed | GPIO 26 | Rotation pulse, interrupt |
| Wind vane | wiper | GPIO 34 | Analogue direction |
| SX1278 LoRa | SPI + DIO0 | GPIO 5 18 19 23 / 25 | Uplink |
| Aspiration fan | MOSFET | GPIO 14 | Ventilates the shield |
| Battery/solar voltage | divider | GPIO 35 | Power monitoring |
Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- The temperature/humidity sensor goes in an aspirated radiation shield — a white multi-plate shield with a small fan drawing air through it. This is the single most important siting requirement: an unshielded sensor reads solar heating of its own body, easily several degrees high, and every derived metric built on it is then wrong.
- Mount the temperature sensor at the standard height (1.25–2 m) over short grass or representative ground, away from buildings, paving and heat sources.
- Site the anemometer clear of turbulence — the standard is 10 m height in open exposure. A lower height is acceptable if documented, but keep it well clear of the mast, buildings and trees that create turbulence and block wind.
- Site the rain gauge away from overhanging obstructions (a rule of thumb: no obstruction closer than twice its height), level, with the funnel clear. Calibrate the millimetres-per-tip against a known volume.
- The solar radiation sensor (BH1750 or a proper pyranometer) is unshielded and level, facing up with a clear sky view — the opposite of the temperature sensor.
- Solar-power the whole station and size the panel/battery for the aspiration fan and periodic transmissions with margin for cloudy spells. Seal everything for long unattended field life.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
The foundational principle is that weather measurement is a discipline with standards, and ignoring them produces data worse than useless. The World Meteorological Organization defines how each variable must be measured — sensor type, height, exposure, shielding — precisely because the measurement is so easily corrupted. The most important is temperature: a thermometer in sunlight absorbs solar radiation and reads its own heated body, not the air, giving errors of several degrees. The standard solution is a radiation shield (white, to reflect sunlight; louvred or multi-plate, to admit air while blocking radiation) with aspiration (a fan drawing air through, so the sensor equilibrates with the moving air, not the shield). This project uses an aspirated shield because without it, the temperature — and everything derived from it — is wrong.
Each variable has its own siting logic. Rainfall uses a tipping-bucket gauge — a funnel feeds a small seesaw bucket that tips and triggers a switch each time it fills with a fixed small volume (e.g. 0.2 mm), so counting tips gives rainfall. It must be level, away from obstructions that would block or funnel rain, and calibrated (the actual millimetres per tip drifts and must be verified). Wind uses a cup anemometer (rotation rate proportional to wind speed) and a vane (a potentiometer giving direction), sited high and clear of turbulence — turbulence from a nearby building or the mast itself corrupts both speed and direction. Solar radiation is measured level and unshielded, facing the sky — the exact opposite of the temperature sensor.
The derived agricultural metrics are where a farm weather station earns its keep over a generic forecast, because these quantities directly inform decisions and are highly local. Growing degree days (GDD) accumulate the daily temperature above a crop-specific base — crops and pests develop according to accumulated heat, not calendar days, so GDD predicts growth stages, harvest timing and pest emergence far better than the date. Evapotranspiration (as in the drip project) drives irrigation demand. Leaf wetness duration and the temperature/humidity combination predict fungal disease risk — many disease models are functions of how long leaves stay wet at what temperature. Chill hours (hours below a threshold) determine when fruit trees break dormancy. Frost risk — especially the radiative frost that settles in valley bottoms on clear calm nights — triggers protection.
The reason these must be local is that they depend on conditions that vary enormously over short distances. A frost pocket in a valley bottom can be several degrees colder than the slope above it on a still clear night — the forecast for the region gives no clue which is which, but a station in the pocket does. Rainfall from a summer thunderstorm can vary from nothing to a downpour within a kilometre. GDD accumulation differs between a warm south-facing slope and a cool north-facing one. The whole value proposition is measuring the actual field, not interpolating from a distant station.
The station is engineered for unattended field operation: solar-powered (with the aspiration fan being the notable continuous load to budget for), reporting over LoRa or cellular because fields lack Wi-Fi, ruggedly sealed against months of weather, and logging locally so a communication gap does not lose data. It samples frequently enough to catch the extremes that matter (a brief frost, a gust, a downpour) while managing power.
Finally, the station's job is to turn conditions into decisions. A high-wind alert says "do not spray now" (drift risk). A frost alert says "protect tonight". A disease-favouring-conditions alert says "consider a preventive fungicide". A GDD milestone says "the pest is about to emerge, scout now". These operationally-tied alerts, from local data, are the difference between a weather station and a farm weather station.
The maths behind it
Growing degree days
Daily GDD = max(0, (Tmax + Tmin)/2 − T_base)
T_base is crop-specific (e.g. 10 °C for maize).
Upper cap sometimes applied (max Tmax at ~30 °C).
Accumulate: GDD_total = Σ daily GDD from planting.
Development milestones occur at characteristic GDD:
maize silking ~ 1400 GDD, maturity ~ 2700 GDD.
Pest emergence also tracks GDD — a far better predictor
than calendar date.
Example: Tmax 28, Tmin 14, T_base 10:
GDD = (28+14)/2 − 10 = 11 GDD that day.
Wind speed from anemometer pulses
Cup anemometer: rotation rate ∝ wind speed.
wind (m/s) = pulses_per_second × K
K is the anemometer constant (from its datasheet or
calibration), e.g. 2.4 km/h per Hz for a common model.
Gust = maximum over a short window (e.g. 3 s peak).
Sustained = average over a longer window (e.g. 10 min).
Wind vane: analogue voltage → direction via a lookup
table of the vane's resistance-to-heading mapping.
Frost risk and leaf wetness
Radiative frost (clear, calm nights) — dew point matters:
dew point Td from T and RH (Magnus formula)
frost likely if T falling toward Td and Td < 0 °C
and wind low and sky clear (low incoming radiation)
Leaf wetness proxy (many disease models use this):
leaf wet when RH > ~90% or after rain, until it dries.
duration of wetness × temperature drives infection risk
(e.g. apple scab, downy mildew models).
Alert when accumulated wet-hours at favourable
temperature exceed a crop-specific disease threshold.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Complete Source Code
The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.
/* ═══════════════════════════════════════════════════════════════
Farm Weather Station — ESP32, correctly-sited, solar, LoRa
Measures weather with properly shielded/sited sensors and computes
the derived agricultural metrics (GDD, ET, leaf wetness, frost)
that generic forecasts do not provide.
══════════════════════════════════════════════════════════════════ */
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <BH1750.h>
#include <LoRa.h>
#include <SPI.h>
#include <Preferences.h>
#include <time.h>
#include <math.h>
#define PIN_RAIN 27
#define PIN_WIND 26
#define PIN_VANE 34
#define PIN_FAN 14
#define LORA_CS 5
#define LORA_RST 25
#define LORA_DIO0 26
#define RAIN_MM_PER_TIP 0.2f
#define WIND_K 2.4f // km/h per Hz — CALIBRATE for your anemometer
#define GDD_BASE 10.0f // crop base temperature
Adafruit_BME280 bme;
BH1750 lux;
Preferences prefs;
volatile uint32_t rainTips = 0, windPulses = 0;
float tempC, rh, pressure, radiation;
float tMinDay = 99, tMaxDay = -99;
float gddTotal = 0, wetHours = 0;
int lastDay = -1;
void IRAM_ATTR rainISR() { rainTips++; }
void IRAM_ATTR windISR() { windPulses++; }
/* ── derived metrics ────────────────────────────────────────── */
float dewPoint(float t, float relh) {
float a = 17.27f, b = 237.7f;
float g = (a * t) / (b + t) + logf(relh / 100.0f);
return (b * g) / (a - g);
}
float windSpeedKmh() {
static uint32_t lastWind = 0, lastPulses = 0;
uint32_t now = millis();
float dt = (now - lastWind) / 1000.0f;
if (dt < 1) return 0;
float hz = (windPulses - lastPulses) / dt;
lastWind = now; lastPulses = windPulses;
return hz * WIND_K;
}
int windDirection() {
// Vane potentiometer → 16-point compass via a lookup of its levels.
int adc = analogRead(PIN_VANE);
return (int)(adc / 4095.0f * 360.0f); // simplified; use the vane's real map
}
bool frostRisk() {
float td = dewPoint(tempC, rh);
return tempC < 3.0f && td < 0.5f && windSpeedKmh() < 5.0f; // radiative frost
}
bool diseaseFavourable() {
bool wet = rh > 90.0f;
bool warm = tempC > 12.0f && tempC < 25.0f; // many fungal optima
return wet && warm;
}
/* ── daily rollover ─────────────────────────────────────────── */
void endOfDay() {
float gdd = fmaxf(0, (tMaxDay + tMinDay) / 2.0f - GDD_BASE);
gddTotal += gdd;
prefs.putFloat("gdd", gddTotal);
Serial.printf("Daily GDD %.1f (total %.0f), rain %.1f mm\n",
gdd, gddTotal, rainTips * RAIN_MM_PER_TIP);
tMinDay = 99; tMaxDay = -99;
rainTips = 0; wetHours = 0;
}
/* ── LoRa ───────────────────────────────────────────────────── */
void transmit() {
bool frost = frostRisk(), disease = diseaseFavourable();
float wind = windSpeedKmh();
LoRa.beginPacket();
LoRa.printf("{\"t\":%.1f,\"rh\":%.0f,\"p\":%.0f,\"rad\":%.0f,"
"\"rain_mm\":%.1f,\"wind\":%.1f,\"dir\":%d,"
"\"gdd\":%.0f,\"frost\":%d,\"disease\":%d}",
tempC, rh, pressure, radiation, rainTips * RAIN_MM_PER_TIP,
wind, windDirection(), gddTotal, frost, disease);
LoRa.endPacket();
}
/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
Serial.begin(115200);
pinMode(PIN_RAIN, INPUT_PULLUP);
pinMode(PIN_WIND, INPUT_PULLUP);
pinMode(PIN_FAN, OUTPUT);
attachInterrupt(PIN_RAIN, rainISR, FALLING);
attachInterrupt(PIN_WIND, windISR, FALLING);
analogSetPinAttenuation(PIN_VANE, ADC_11db);
Wire.begin(21, 22);
bme.begin(0x76);
lux.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
SPI.begin();
LoRa.setPins(LORA_CS, LORA_RST, LORA_DIO0);
LoRa.begin(433E6);
LoRa.setSpreadingFactor(10);
prefs.begin("wx", false);
gddTotal = prefs.getFloat("gdd", 0);
lastDay = prefs.getInt("day", -1);
configTime(19800, 0, "pool.ntp.org");
Serial.println("Farm weather station running");
}
void loop() {
// Aspirate the shield before reading temperature.
digitalWrite(PIN_FAN, HIGH);
delay(20000); // ventilate 20 s (or run continuously)
tempC = bme.readTemperature();
rh = bme.readHumidity();
pressure = bme.readPressure() / 100.0f;
radiation = lux.readLightLevel() * 0.0079f; // lux → W/m² approx (site-cal)
digitalWrite(PIN_FAN, LOW);
tMinDay = fminf(tMinDay, tempC);
tMaxDay = fmaxf(tMaxDay, tempC);
if (rh > 90) wetHours += 1.0f / 6.0f; // 10-min sample → hours
time_t now = time(nullptr); struct tm tmv; localtime_r(&now, &tmv);
if (tmv.tm_yday != lastDay) {
lastDay = tmv.tm_yday; prefs.putInt("day", lastDay);
endOfDay();
}
Serial.printf("T %.1f RH %.0f%% P %.0f rad %.0f wind %.1f rain %.1f%s%s\n",
tempC, rh, pressure, radiation, windSpeedKmh(),
rainTips * RAIN_MM_PER_TIP,
frostRisk() ? " [FROST]" : "", diseaseFavourable() ? " [DISEASE]" : "");
transmit();
delay(600000 - 20000); // 10-min interval (minus fan time)
}
Configuration & Calibration
Configuration steps
- Site every sensor correctly — this is the whole project. Temperature in an aspirated shield at standard height; rain gauge level and clear of obstructions; anemometer high and clear of turbulence; radiation sensor level and unshielded.
- Calibrate the rain gauge (mm per tip) against a measured volume and the anemometer constant (K) against a known wind or its datasheet.
- Set the GDD base temperature for your crop, and configure the frost and disease thresholds for your situation.
- Set the reporting interval and reporting technology (LoRa or cellular) for your field's connectivity, and size the solar system for the aspiration fan load.
- Calibrate the lux-to-radiation conversion, or fit a proper pyranometer if solar radiation accuracy matters for ET.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Verify the shield and aspiration
Compare the shielded, aspirated temperature against a reference in shade — they should agree. Then compare against an unshielded sensor in sun to see the several-degree error the shield prevents. This confirms the most important part of the station works.
Calibrate the rain gauge
Pour a measured volume slowly into the funnel and count the tips. Compute the actual mm per tip and compare with the nominal. Repeat to check consistency — the tipping mechanism can drift.
Calibrate wind
Compare against a handheld anemometer in steady wind, or use the datasheet K constant. Verify the vane direction against a compass at known headings.
Cross-check against a reference station
Compare a day of data against the nearest official station, understanding that legitimate local differences (your frost pocket, your rainfall) are expected. Gross disagreement in a variable points to a siting or calibration problem.
Network Architecture & Connectivity
Dashboard setup
The value is a continuous local record and derived metrics no forecast provides: your field's GDD accumulation, rainfall, frost events and disease-condition hours. Compare against the regional forecast over a season and the local differences — the frost the forecast missed, the rain it over- or under-called — justify the station immediately.
Security considerations
- Weather data is low-sensitivity, but authenticate the uplink so alerts (frost, spray conditions) cannot be spoofed into wrong decisions.
- Keep the local log and alerting independent of the network — a frost alert must not depend on connectivity.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Compare shielded/aspirated temperature to a reference in sun | Close agreement — far better than an unshielded sensor, confirming the shield works. |
| Pour a measured volume into the rain gauge | The tip count matches the expected rainfall after calibration. |
| Spin the anemometer at a known rate | The computed wind speed matches; verify against a handheld anemometer in real wind. |
| Check the wind vane at known headings | The reported direction matches a compass. |
| Accumulate GDD over a few days | Plausible daily and total GDD for your temperatures and crop base. |
| Create frost conditions (cool, calm, dry) | The frost alert fires — especially valuable if your site is a frost pocket. |
| Create disease-favouring conditions (warm, humid) | The disease-condition alert fires. |
| Run on solar for several days | The battery holds through cloudy periods with the aspiration fan budgeted for. |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Sample frequently enough to catch the extremes that matter — a brief frost, a gust, a downpour — while managing solar power; 5–10 minute intervals are typical.
- Run the aspiration fan in bursts before temperature readings if power is tight, rather than continuously.
- Count rain and wind pulses in interrupts so none are missed during heavy rain or high wind.
- Replace every
delay()with amillis()comparison — blocking delays are the single most common cause of dropped readings. - Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
- Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
- Use
uint8_t/uint16_twhere the range allows; on an 8-bit AVR a 32-bit add costs four times as much. - Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
- Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
- For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
- Profile before optimising — print
micros()deltas around each stage and fix the slowest one first.
Safety Precautions
- Site sensors per meteorological standards — incorrectly-sited data is worse than useless and can drive wrong decisions (spraying in unsafe wind, missing a frost).
- A tall mast is a lightning risk in an open field — ground it properly and follow local guidance.
- Alerts drive real operations (spray, frost protection); ensure they are reliable and keep the alerting independent of the network.
- Follow safe practice around any operations the station informs, especially agrochemical spraying.
- Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
- Never power an RF module without its antenna fitted — the reflected power destroys the output stage. Check your local licence-free band and duty-cycle limits before transmitting.
- Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
- Clean the sensing element on a schedule. Optical and electrochemical sensors foul, and a fouled sensor reports plausible nonsense rather than failing outright.
- Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
- Wash the panel every few weeks in dusty conditions; a visible dust film costs 15–25 % of the harvest.
- Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Add a proper pyranometer for accurate solar radiation, improving the ET estimate.
- Add soil sensors (temperature, moisture) for a complete crop-environment picture combined with the drip and NPK projects.
- Add specific disease models (apple scab, downy mildew, etc.) computed from the local wetness and temperature record.
- Add a network of stations across a large or varied farm to map the local weather differences (frost pockets, rainfall gradients).
- Add forecast blending — combining the local measurements with a regional forecast for a corrected hyperlocal outlook.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add over-the-air firmware updates so you never have to physically reach a deployed node again.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.
- WMO Guide to Instruments and Methods of Observation (WMO-No. 8)World Meteorological Organization
- McMaster & Wilhelm, "Growing degree-days: one equation, two interpretations"Agricultural and Forest Meteorology, 1997
- FAO Irrigation and Drainage Paper 56 — evapotranspirationFAO
- Snyder & de Melo-Abreu, "Frost Protection: fundamentals, practice and economics"FAO
- BME280 environmental sensor — datasheetBosch Sensortec
- NOAA — siting standards for weather instrumentsNOAA / US National Weather Service