Building a station is entirely feasible with an ESP32 and a weekend. Whether it produces good data comes down to a handful of details (gust windowing, vector-averaging wind direction, pressure reduction and radiation shielding) that are easy to get wrong and invisible when you do.
Anemometer
Cup anemometers with a reed or hall pulse output are the usual choice. Propeller-on-a-vane designs work. Ultrasonic sensors have no moving parts and no cut-in speed, which makes them the best option for a low-maintenance safety installation if the budget allows. Hot-wire is research equipment.
Pulses to speed
A reed switch closes some number of times per revolution. Convert with a linear
calibration, v = k·f + v0, where f is pulses per second. The slope and
offset come from a wind tunnel or a reference instrument, not from theory.
Two constants worth knowing, because so much hardware descends from them:
- Fine Offset, SparkFun and Argent cup assemblies: one switch closure per second equals 2.4 km/h (1.492 mph). The Fine Offset sensor gives two pulses per revolution, and its console counts over 2-second windows, giving 0.75 mph resolution steps. The empirical anemometer factor, wind speed divided by cup speed, is about 3.
- Davis 6410 and Vantage: one closure per 2.25 s equals 1 mph, so
v_mph = 2.25 · Ncounted over 2.25 s.
To derive your own, mount alongside a trusted station and regress, or do a calm-day car test at steady GPS speed on a genuinely windless day. Expect the best accuracy, around ±1%, between 5 and 15 m/s. Below 3 m/s the offset dominates, and above about 30 m/s cups flex and under-read.
Sustained and gust, done correctly
This is where most home-built firmware quietly diverges from every official station it will be compared against.
| Definition | |
|---|---|
| Sustained (NWS/METAR) | A 2-minute average |
| WMO synoptic mean | A 10-minute average |
| Gust | The peak 3-second average in the period |
A gust is not the single fastest pulse interval. It is the maximum of a 3-second sliding average. Implement it with a ring buffer of samples at 1 Hz or better, and debounce reed switches by roughly 1 to 5 ms, or use a hall sensor and skip the problem.
Wind vane
The classic Fine Offset and Argent vane is a resistor ladder: eight reed switches, each with a distinct resistor, and a magnet that closes one, or two adjacent ones, whose parallel resistance gives the value in between, for 16 discrete positions. Read it through a voltage divider into an ADC and map ranges to azimuth with a lookup table. There is a dead-band of about 10°.
A better choice for a new build is the AS5600: 12-bit, contactless, 0.1° resolution, no dead-band, nothing to wear out, I²C, and cheap.
Two details that ruin wind direction
Align to true north, not magnetic north. Look up your magnetic declination by latitude and longitude with NOAA NCEI’s calculator and correct for it. A declination of −4° means magnetic north sits 4° east of true.
Vector-average with sine and cosine components. Never average degrees. Averaging 350° and 10° numerically gives 180°, exactly backwards. The correct form is:
mean_dir = atan2(Σ sin θ, Σ cos θ)
This is not a refinement. A station that averages degrees reports garbage whenever the wind sits near north, which is most of the winter in much of the country.
Temperature and humidity
| Sensor | Accuracy | Verdict |
|---|---|---|
| Sensirion SHT31/35, SHT41/45 | ±0.2–0.3 °C, ±2% RH | The DIY gold standard, low drift |
| BME280 | ±1 °C | Adequate, and gives you pressure too |
| BME680 | as BME280 | Adds gas sensing you probably do not need |
| HDC302x, Si7021 | good | Fine alternatives |
| AM2302 / DHT22 | poor | Avoid: slow, drifty, high failure rate |
Watch for self-heating: mount the sensor away from the microcontroller, and never inside a sealed box in the sun.
Radiation shields
This is where citizen temperature data lives or dies.
A Stevenson screen is the classic wooden double-louvred reference, and it is bulky. A multi-plate Gill-style shield is the standard naturally ventilated AWS design, with radiation error typically around 0.5 K and occasionally 2–3 K in light wind and strong sun. An aspirated shield with a forced-draught fan is the accurate answer, around 0.1 °C, at the cost of power and a fan that can fail.
A stack of downturned plastic plant saucers with spacers makes a serviceable multi-plate shield for very little money, and adding a small 5 V fan gets you close to research quality. Mount over grass rather than pavement, 1.25 to 2 m up, well clear of walls.
Pressure
BMP390 and BMP581 have low drift; MS5611 is high resolution; DPS310 is cheap and good; BME280 is fine. Stability matters far more than absolute accuracy, because you are going to calibrate out an offset anyway.
Three pressures, and only one of them goes in the packet
| Station pressure (QFE) | The raw reading at your elevation |
| Altimeter setting | Reduced to sea level using the ISA standard atmosphere, ignoring actual temperature. What aviation uses |
| Sea-level pressure (MSLP) | Reduced using the actual mean column temperature. This is what CWOP’s b field wants |
The hypsometric reduction:
Psl = Pstn · exp( g·H / (Rd · Tv) )
g = 9.80665 m/s² H = elevation, metres
Rd = 287.05 J/kg·K Tv = mean virtual column temperature, kelvin
Worked, from the NWS reference implementation: at H = 600 m, surface temperature 11 °C and station pressure 942.1 hPa, you get 1013.25 hPa. Change the assumed column temperature to 2 °C and it becomes 1015.6; to 20 °C and it becomes 1011.0.
Rain
The tipping bucket is the standard: each tip is a fixed volume, sensed by a reed switch. Common calibrations are 0.01 inch in the US or 0.2 mm metric; Davis uses 0.2794 mm per tip. Debounce by around 10 ms. Calibrate by pouring a known volume slowly and counting tips.
Understand what it will miss. Undercatch at high wind is significant, and an Alter shield reduces it. Unheated gauges miss snow entirely; a heated gauge melts frozen precipitation so it can be counted. Weighing gauges and optical or acoustic disdrometers are the higher-end answers.
Solar radiation and UV
Apogee SP-series, the Davis pyranometer, or a cheap silicon photodiode.
Be careful converting lux to W/m². Lux is weighted to the human eye, and the common approximation of about 0.0079 W/m² per lux for daylight is rough. A real pyranometer measures broadband irradiance, which is what CWOP’s luminosity field expects. For UV, a VEML6075 or LTR390 calibrated against the EPA UV index.
Soil, optionally
Capacitive soil moisture probes (avoid resistive ones, they corrode) and buried DS18B20 temperature sensors are cheap agricultural extras. They have no place in the APRS weather format, but they make useful telemetry.
Parts by budget
| Tier | Cost | Wind | T/RH | Pressure | Rain | Compute | Shield |
|---|---|---|---|---|---|---|---|
| Minimal | under $100 | Fine Offset / SparkFun cup and vane kit | BME280 | in the BME280 | FO tipping bucket | ESP32 dev board | DIY saucer stack |
| Solid | ~$300 | Davis 6410, or FO cups with an AS5600 vane | SHT31 | BMP390 | Davis or Ecowitt bucket | ESP32-S3 or Pi Zero 2 W | Multi-plate Gill type |
| Near-research | ~$800 | Ultrasonic, or Davis Pro2 | SHT45, aspirated | MS5611 / BMP581 | Heated bucket with Alter shield | Pi with industrial encoder | Fan-aspirated |
Choosing the compute
| Board | Strengths | Weaknesses | Best for |
|---|---|---|---|
| ESP32 / S3 / C6 | WiFi and BLE, deep sleep, cheap, hardware pulse counters; C6 adds Thread and Zigbee | Noisy ADC, analog needs care | Battery WiFi stations, ESPHome |
| Pi Pico W | Cheap, PIO for precise pulse timing, MicroPython | WiFi only, no Linux | Tidy MicroPython builds |
| Pi Zero 2 W | Full Linux, so it runs WeeWX and Direwolf directly | 1–2 W, no deep sleep | Mains or solar bridge, and RF |
| Arduino (AVR) | Dead-simple interrupts | No WiFi, 8-bit | Sensor front end only |
Count wind pulses with hardware, the ESP32’s PCNT or the Pico’s PIO, and hardware interrupts. Never poll in a slow main loop; you will silently under-read in exactly the high-wind conditions that matter most.
Firmware
ESPHome is YAML rather than C, and integrates natively with Home Assistant:
esphome:
name: wxstation
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: pulse_counter # anemometer (Fine Offset)
pin:
number: GPIO14
mode: INPUT_PULLUP
name: "Wind Speed"
id: wind_mph
unit_of_measurement: "mph"
update_interval: 3s # 3 s aligns with the gust definition
count_mode:
rising_edge: INCREMENT
falling_edge: DISABLE
internal_filter: 13us # debounce
filters:
- multiply: 0.746 # counts/3s -> mph. CALIBRATE THIS
- platform: adc # resistor-ladder vane
pin: GPIO34
name: "Wind Dir Raw"
id: vane_v
update_interval: 3s
attenuation: 12db
- platform: bme280_i2c
address: 0x76
temperature: {name: "Temp"}
humidity: {name: "Humidity"}
pressure: {name: "Station Pressure", id: p_stn}
Map the vane’s ADC ranges to degrees in a lambda lookup, and do the MSLP
reduction and the sine/cosine direction averaging either on-device in a lambda
or downstream. ESPHome also has as3935_i2c and as3935_spi components for
lightning detection.
Tasmota works but is less flexible for custom sensors. PlatformIO or Arduino C++ gives full control and is the right choice for hard real-time pulse timing and power management. MicroPython and CircuitPython develop fast, but watch timing jitter on pulse counting; use the Pico’s PIO.
Meshtastic deserves a mention here: it is not an APRS transport, but it will carry sensor telemetry over a LoRa mesh where there is no WiFi or cell service, and you can bridge to CWOP at whichever node does have internet. If you already run a mesh, see MeshMonitor for the other direction: WxAlerts warnings onto that same mesh.
Enclosures and survival
Use an IP65 or better enclosure with cable glands on the bottom face, never open holes. Put desiccant inside and replace it periodically. Use UV-stabilised plastics; cheap ABS chalks and cracks within a season or two.
The failure nobody plans for is insects. Wasps nest in rain funnels and anemometer bodies, and spiders web across moving parts. Fine stainless mesh over openings solves most of it.
On the Gulf Coast or anywhere coastal, plan for salt corrosion: marine or stainless hardware, conformal-coated PCBs, and no dissimilar metals in contact.
Siting, and documenting it honestly
WMO No. 8, the CIMO Guide, defines siting classes 1 through 5 per variable, where 1 is ideal and 5 is severely compromised.
- Wind: anemometer at 10 m over open terrain, with obstacles more than 10 times their own height away, though some references relax that to 4 times.
- Temperature and humidity: 1.25 to 2 m over natural ground, away from pavement and buildings.
- Rain: level, and clear of obstacles by at least twice, ideally four times, their height.
Where to go next
- AS3935 lightning detection: the one sensor that cannot ride the APRS weather format.
- The APRS weather packet: what your firmware has to produce.
- Install and commissioning: proving the build works before you trust it.
