Siddhant Kumar
Project 032 · Agriculture

Farm Weather Station.

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.

Intermediate 14–20 hours 38 min read WeatherSensorsCloud
Jump to source Bill of materials
Farm Weather Station — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
14–20 hours
Indicative cost
₹5,800 – ₹7,500
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Agriculture
Last updated
28 July 2026
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

SettingHow it is used
Spray-timing decisionsWind speed and rain determine whether and when to spray — from your field, not a distant forecast.
Frost protectionLocal frost risk, especially in frost-pocket valleys the forecast misses, triggers protection in time.
Irrigation schedulingLocal ET drives demand-based irrigation (the drip project) far better than regional estimates.
Pest and disease forecastingGDD predicts pest emergence; leaf wetness and conditions predict disease — both need local data.
Crop development trackingGrowing degree days predict growth stages and harvest timing.
Fruit-tree managementChill-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

AttributeValue
Difficulty levelIntermediate
Estimated completion time14–20 hours
Indicative build cost₹5,800 – ₹7,500
Primary disciplineAgriculture
Reference platformESP32 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.

ComponentKey specificationQtyApprox. 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 DAC1₹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 Hz1₹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 eye1₹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 output1₹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–SF121₹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 frame1₹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 pack1₹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 discharge1₹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 grid1₹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 lugs1₹260
Anemometer + wind vane
Site clear of turbulence; the standard height is 10 m, or document a lower height.
Cup anemometer (pulse) + potentiometer wind vane1₹1,200
Tipping-bucket rain gauge
Calibrate the mm-per-tip; site away from overhanging obstructions.
0.2 mm per tip, reed-switch output1₹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 sensor1₹600
Mast and mounting hardwareField mast for correct sensor heights1₹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

PartSpecificationSupplyInterfaceReference
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 DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
BME280 pressure/humidity/temperature sensor300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz1.7–3.6 V (module has 3.3 V LDO)I²C (0x76/0x77) or SPIDatasheet
BH1750 digital ambient light sensor1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye2.4–3.6 VI²C (0x23/0x5C)Datasheet
Rain / water-level board (FC-37)Interdigitated PCB electrode, analogue + digital comparator output3.3–5 VAnalogue + digitalDatasheet
SX1278 LoRa 433 MHz module (Ra-02)−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF123.3 VSPIDatasheet
20 W 12 V polycrystalline solar panelVmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame12 V nominalMC4 / screw terminalsDatasheet
CN3791 MPPT solar charge controller4.5–28 V in, MPPT set by resistor divider, 2 A charge to a 1S Li-ion pack4.5–28 VSolder padsDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet
IP65 ABS junction enclosure 158 × 90 × 60 mmIP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugsDatasheet

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.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
BME280 pressure/humidity/temperature sensor1.7–3.6 V (module has 3.3 V LDO)0.4Self-heating skews temperature by ~1 °C — read in forced mode, not continuous.
BH1750 digital ambient light sensor2.4–3.6 V0.19Far 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 V15Drive the electrode with AC or duty-cycle its power to slow electrolytic corrosion.
SX1278 LoRa 433 MHz module (Ra-02)3.3 V120Never power the radio without an antenna — the PA will destroy itself.
20 W 12 V polycrystalline solar panel12 V nominal1140Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
CN3791 MPPT solar charge controller4.5–28 V2000Set 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.json under 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 dialout group: sudo usermod -aG dialout $USER and 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

LibraryWhy it is neededInstall
Adafruit BME280 2.2.xCompensation maths for the Bosch pressure/humidity/temperature sensor.Library Manager → "Adafruit BME280 Library"
Adafruit Unified Sensor 1.1.xCommon sensor event abstraction; a dependency of most Adafruit drivers.Library Manager → "Adafruit Unified Sensor"
BH1750 1.3.0Digital lux readings with selectable resolution modes.Library Manager → "BH1750" by Christopher Laws
LoRa (sandeepmistry) 0.8.0SX127x radio configuration, packet TX/RX and callbacks.Library Manager → "LoRa" by Sandeep Mistry
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core
NTPClient / configTime bundledWall-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.

Farm Weather Station — system block diagramFunctional block diagram of the Farm Weather Station system. MeasureBME280 shieldedT/RH/PRain + windpulseRadiationunshieldedDeriveGDD, ETcrop dev, waterLeaf wetness, frostdisease, protectDecideAlert conditionsspray/frost/diseaseLocal logcontinuousReportLoRa/cellularfrom the fieldDashboardhyperlocalraw weathermetricsdata + alerts
Farm Weather Station — system block diagram

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.

Farm Weather Station — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBBME280 (shielded)GPIO 21 / 22T / RH / P, I²CBH1750 solar radiationGPIO 21 / 22Shared I²C,unshieldedRain gaugeGPIO 27Tip pulse, interruptAnemometerGPIO 26Rotation pulse,interruptWind vaneGPIO 34Analogue directionSX1278 LoRaGPIO 5 18 19 23 / 25UplinkAspiration fanGPIO 14Ventilates theshieldBattery/solar voltageGPIO 35Power monitoring
Farm Weather Station — wiring schematic
PeripheralPeripheral pinController pinSignal
BME280 (shielded)SDA / SCLGPIO 21 / 22T / RH / P, I²C
BH1750 solar radiationSDA / SCLGPIO 21 / 22Shared I²C, unshielded
Rain gaugereedGPIO 27Tip pulse, interrupt
AnemometerreedGPIO 26Rotation pulse, interrupt
Wind vanewiperGPIO 34Analogue direction
SX1278 LoRaSPI + DIO0GPIO 5 18 19 23 / 25Uplink
Aspiration fanMOSFETGPIO 14Ventilates the shield
Battery/solar voltagedividerGPIO 35Power 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.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 development board reading the sensors, computing the agricultural metrics, and reporting from a field over LoRa. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

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.

Farm Weather Station — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · BME280 pressure/humidity/temperaturesensor · BH1750 digital ambient light sensorDriver layerbme · unified · bh1750lib · lorolibApplication logicsampling loop · filtering · thresholds · state machineTransport layerLoRa or cellular · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Farm Weather Station — architecture stack

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

plainGrowing 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

plainWind 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

plainFrost 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.

Farm Weather Station — firmware flowchartControl flow through the main program loop. Read all sensors (aspirateshield first)Accumulate rain tips, windpulses, min/max TCompute GDD, ET, leaf wetness,frost riskFrost / high wind /disease condition?alertnormalLog locallyReporting intervalreached?transmitwaitTransmit over LoRa/cellularLow-power wait to next reading
Farm Weather Station — firmware flowchart

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.

cppfarm-weather-station.ino
/* ═══════════════════════════════════════════════════════════════
   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)
}
Aspirate the shield before reading temperatureThe fan ventilates the radiation shield so the sensor reads the moving air, not the sun-warmed shield. This is the single most important line for accuracy — an unaspirated temperature reading is several degrees high in sun, and every derived metric inherits that error.
Rain and wind counted in interruptsA tipping-bucket gauge and a cup anemometer both output pulses (a tip per fixed rainfall, a pulse per rotation). Counting them in interrupts ensures no tip or rotation is missed, which matters during heavy rain or high wind when the pulse rate is high.
frostRisk() — radiative frost logicThe dangerous frost for many crops is radiative frost on clear, calm nights, when the ground radiates heat to a clear sky and cold air pools in low spots. The logic checks for low temperature approaching a sub-zero dew point with low wind — the conditions that produce it — which a regional forecast often misses in a frost pocket.
diseaseFavourable() from wetness and temperatureMany fungal diseases need prolonged leaf wetness at a favourable temperature. Flagging high humidity in the fungal temperature optimum is a simple disease-condition alert; a fuller version implements a specific crop's disease model from accumulated wet-hours.
GDD accumulated dailyGrowing degree days accumulate the daily mean temperature above the crop base. Crops and pests develop by accumulated heat, not calendar days, so GDD predicts development stages and pest emergence — a genuinely useful local metric a forecast does not provide.
WIND_K commented CALIBRATEThe anemometer constant relating pulse rate to wind speed varies by model and must be calibrated or taken from the datasheet. A wrong constant makes every wind speed wrong, which directly affects spray-timing decisions.
Local logging + LoRa transmitThe station logs locally (via the persisted GDD and a fuller log) so a communication gap does not lose data, and transmits over LoRa because fields lack Wi-Fi. The 10-minute interval catches meaningful variation while managing solar power.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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

Farm Weather Station — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsWeather stationon the fieldLoRa / cellularFarm gatewayor direct cellularLoRa 433/868 or LTEBroker + Grafanahyperlocal recordWeather dashboardthe fieldDecision alertsspray/frost
Farm Weather Station — network topology

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.

TestWhat you should see
Compare shielded/aspirated temperature to a reference in sunClose agreement — far better than an unshielded sensor, confirming the shield works.
Pour a measured volume into the rain gaugeThe tip count matches the expected rainfall after calibration.
Spin the anemometer at a known rateThe computed wind speed matches; verify against a handheld anemometer in real wind.
Check the wind vane at known headingsThe reported direction matches a compass.
Accumulate GDD over a few daysPlausible 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 daysThe 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.

A field irrigation system watering crops
Farmland. A station on your actual field measures the frost pocket and the local rainfall that a regional forecast cannot. Photograph sourced from Wikimedia Commons — Irrigation system.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Temperature reads too high

Likely cause. Inadequate shielding or aspiration — the sensor reads the sun.

Fix. This is the most common and most damaging error. Use a proper radiation shield WITH aspiration. An unshielded or unventilated sensor in a greenhouse or in the field reads its own solar heating, several degrees high, corrupting every derived metric. This is non-negotiable.

Rainfall reads wrong

Likely cause. Uncalibrated mm-per-tip, un-level gauge, or obstructions.

Fix. Calibrate against a measured volume. Level the gauge. Site it away from overhanging obstructions that block or funnel rain. Check the tipping mechanism moves freely and is not blocked by debris.

Wind readings are erratic or too low

Likely cause. Turbulence from nearby structures, or wrong anemometer constant.

Fix. Site the anemometer high and clear of the mast, buildings and trees that create turbulence and block wind. Calibrate the K constant. The standard 10 m height exists precisely to get above local turbulence.

Data differs from the regional forecast

Likely cause. Often legitimate — weather is local.

Fix. This is frequently the point, not a fault. Your frost pocket really is colder; your field really did get more rain. Legitimate local differences are the value proposition. Only investigate if a variable grossly disagrees in a way that indicates a siting or calibration error rather than real local weather.

Battery drains despite solar

Likely cause. The aspiration fan running continuously, or the panel undersized.

Fix. The aspiration fan is a notable continuous load. Run it in bursts before temperature readings rather than continuously if power is tight, or size the solar system to support it. Budget for the fan plus periodic transmissions with cloudy-day margin.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

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 a millis() 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_t where 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

Why not just use a weather forecast?

Because weather is intensely local and a forecast is regional. The forecast is for a town or airport that may be tens of kilometres away and at a different elevation. A frost that settles in your valley bottom spares the slope above and is invisible to the regional forecast; summer rainfall varies from nothing to a downpour within a kilometre; wind on your exposed ridge bears no relation to the sheltered valley station. For decisions that depend on the actual conditions on your field — when to spray, whether it will frost tonight — measuring your field beats interpolating from a distant station.

What is the single most important thing to get right?

The temperature shielding. A thermometer in sunlight reads its own solar-heated body, not the air, and can be several degrees high. Since temperature feeds every derived metric — GDD, ET, frost, disease — an unshielded sensor makes the whole station wrong. A proper white radiation shield with aspiration (a fan drawing air through it) is non-negotiable. Get this wrong and your data is worse than the airport's, because at least the airport shields its sensors properly.

What are growing degree days and why do they matter?

They are the accumulated daily temperature above a crop-specific base — a measure of accumulated heat rather than elapsed time. Crops and pests develop according to how much warmth they have received, not the calendar, so GDD predicts growth stages, harvest timing and — importantly — pest emergence far better than the date. A pest that emerges at a characteristic GDD can be scouted for at exactly the right time, and GDD is highly local (a warm south-facing slope accumulates faster than a cool north-facing one), so a local station gives you the number that actually applies to your field.

How is this different from a hobby weather station?

Correct siting and the agricultural metrics. A hobby station stuck on a wall in the sun gives pretty numbers that are meteorologically wrong. This project treats siting as the discipline it is — aspirated shield, correct heights, clear exposure — so the data is actually valid, and then computes the derived metrics (GDD, ET, leaf wetness, frost risk, chill hours) that inform farm decisions. The difference is between a gadget and an instrument.

Do the local readings really differ from the forecast that much?

For the variables that matter to farming, often dramatically. Frost is the clearest case: on a clear calm night, cold air pools in low spots, and a frost pocket can be several degrees below the surrounding land and below what the regional forecast predicts — the difference between a killed crop and an untouched one. Rainfall from convective storms varies enormously over short distances. Wind on an exposed site far exceeds the sheltered forecast station. These are not measurement errors; they are real local weather that only a local station captures.

Why does it need to run on solar and LoRa?

Because it lives in a field, which has neither mains power nor Wi-Fi. Solar with a battery makes it self-powered indefinitely; LoRa (or cellular) reports its data kilometres to the farm on very little power. This unattended, infrastructure-free operation is what lets you site the station where the weather actually matters — in the crop, in the frost pocket — rather than only where you can run a cable. The aspiration fan is the notable power load to budget for, but a modest solar panel handles it.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. WMO Guide to Instruments and Methods of Observation (WMO-No. 8)World Meteorological Organization
  2. McMaster & Wilhelm, "Growing degree-days: one equation, two interpretations"Agricultural and Forest Meteorology, 1997
  3. FAO Irrigation and Drainage Paper 56 — evapotranspirationFAO
  4. Snyder & de Melo-Abreu, "Frost Protection: fundamentals, practice and economics"FAO
  5. BME280 environmental sensor — datasheetBosch Sensortec
  6. NOAA — siting standards for weather instrumentsNOAA / US National Weather Service