A Weather Station Made of Arduinos

When a derecho passed through eastern Ontario in late May of 2022, I decided that I would like to build my own weather station, so that I would be able to monitor the local weather, and do it accurately.

Updated 4 December, 2023.

The weather station is finished (or somewhere close to that)! More photos have been added below, and the code for the transmitting and receiving modules can be found below. Apologies for the messy and mostly uncommented code — It doesn’t help that Squarespace’s code blocks aren’t the best for legibility. But it works, and the data uploads every 10 minutes to Weather Underground.

(December 2023) The weather station is now outside. Below are pictures of how it is set up on my deck, and the internal wiring of the junction box (it’s a mess, I know). I have noticed that the rain gauge seems to be over-reporting compared to other stations in my area, but I think it’s due to the optical rain gauge being more susceptible to detecting ‘mist’ as rain. There may be a problem in the way it counts the total rainfall over 24 hours, but I don’t have a clue as to what it is at the moment; it just seems to act funny.

The weather station uses a Davis Anemometer, Hydreon RG-11 optical rain gauge, a BME280 chip (used here only for barometric pressure and humidity), and a DS18B20 temperature probe that is outside the junction box, shielded and ventilated in the grey piece of PVC. Every 10 minutes, moderated by a DS3231 RTC clock module, the data is sent over radio by the Nrf24l01 radio module. Everything is run through an Arduino Uno.

The receiver module above picks up the radio signals with another Nrf24l01 connected to an Arduino Uno and uploads them with the ESP8266 module to Weather Underground.

(December 2022) I am as far as having all the sensors hooked up to the Arduinos, and they are transmitting data well. I am having to wait a bit for a junction box to put everything in, so that I can put it all outside. Hopefully I will be able to update this section once I have done that! Fingers crossed that nothing catches fire.

Much of the code for the weather station has been taken from this other project (website no longer up), although with a few modifications. Credit should also go to this project and this project by Christopher Grant for dealing with the ESP8366 WiFi module, and this project for dealing with the Nrf24l01 radio modules. If I can figure out how to make the ESP8266 call UTC time properly, I can use it to count the proper rainfall amount in 24 hours — for now, I have just set a counter to cycle every 24 hours, but it needs to be reset every time the power goes out.

I have chosen to use radio transmission instead of a long ethernet cable, so I have two Nrf24l01 radio transceiver modules: one for transmitting the data, and one for receiving it. This means that there is also no external power coming through the ethernet cable, so I planned to have a 12V battery pack supplying power. This ended up not working for very long, and I would have needed a sizeable battery to keep it running for any substantial length of time, so I switched it out for wiring the station directly into a 12V adapter. If I choose to put the station out in the yard, I’ll bury the power cable.

The radio modules proved a little challenging to get working properly, but I eventually managed to do so. You can find tutorials on how to use the modules and troubleshooting them all over the internet. I notice that they do not work well with breadboards, and the length of wire used to power them has a large impact on how they function, if you are powering them from the Arduino Uno’s 3.3V pin. The Uno seems to have a harder time maintaining that stable voltage across longer wires.

I have also used pin A3 on my Arduino Uno as the analogue pin for the Davis Anemometer, because using pin A4 as an analogue pin at the same time as an IRC pin seemed to be breaking things.

Transmitter Code

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Adafruit_BME280.h>
#include <ds3231.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include "TimerOne.h"
#include <math.h>

Adafruit_BME280 bme;

struct ts t;

// local time variables
int year;
int month;
int day;
int hour;
int minute;
int sec;

#define ONE_WIRE_BUS 9
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

#define CE_PIN 7
#define CSN_PIN 8

const byte rxAddress[5] = {'R', 'x', 'A', 'A', 'A'};

RF24 radio(CE_PIN, CSN_PIN);

// RG-11 definitions
#define Bucket_Size 0.01 // bucket size to trigger tip count
#define RG11_Pin 3 // digital pin RG11 connected to

// RG-11 variables
volatile unsigned long tipCount; // bucket tip counter used in interrupt routine
volatile unsigned long contactTime; // Timer to manage any contact bounce in interrupt routine
volatile float totalRainfall; // total amount of rainfall detected

// Davis stuff
int VaneValue;// raw analog value from wind vane
int Direction;// translated 0 - 360 direction
int CalDirection;// converted value with offset applied
int LastValue;

#define Offset 0;

#define WindSensorPin (2) // The pin location of the anemometer sensor

volatile bool IsSampleRequired;
volatile unsigned int TimerCount;
volatile unsigned long Rotations; // cup rotation counter used in interrupt routine
volatile unsigned long ContactBounceTime; // Timer to avoid contact bounce in interrupt routine

float WindSpeed; // speed miles per hour
float WindAvg = 0; // average wind speed
float WindGust = 0; // highest wind speed
volatile unsigned int WindDataCount = 0; // number of wind measurements (3 seconds each)

// define the struct
struct package {
  float temperature;
  float humidity;
  float pressure;
  float rainfall;
  int vaneheading;
  float anemospeed;
  float windgust;
};

struct package data;

void setup() {
  Serial.begin(9600);
  while (!Serial) {

  }

  Wire.begin();
  // Clock
  DS3231_init(DS3231_CONTROL_INTCN);

  t.hour = 1;
  t.min = 1;
  t.sec = 1;
  t.mday = 1;
  t.mon = 1;
  t.year = 1;

  DS3231_set(t);

  // Temperature Probe
  sensors.begin();

  // Transceiver
  radio.begin();
  radio.setPALevel(RF24_PA_MAX);
  radio.setRetries(3, 5);
  radio.setDataRate(RF24_250KBPS);
  radio.openWritingPipe(rxAddress);
  delay(1000);

  //RG-11
  tipCount = 0;
  totalRainfall = 0;

  pinMode(RG11_Pin, INPUT);
  attachInterrupt(digitalPinToInterrupt(RG11_Pin), isr_rg, FALLING);
  sei();// Enable Interrupts

  //Davis
  LastValue = 0;

  IsSampleRequired = false;

  TimerCount = 0;
  Rotations = 0;

  pinMode(WindSensorPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(WindSensorPin), isr_rotation, FALLING);

  Timer1.initialize(500000);
  Timer1.attachInterrupt(isr_timer);

  // BME280 stuff
  unsigned status;

  status = bme.begin();

  if (!status)
  {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  else {
    Serial.println("BME OK");
  }
}

void loop() {
  
  DS3231_get(&t);
  timedef();
  
  if (minute % 10 == 0 && sec == 0)
  {

    readSensors();

    data.rainfall = totalRainfall;
    tipCount = 0;   // reset the rain gauge after 30 mins
    totalRainfall = 0;

    davisvane();
    if (abs(CalDirection - LastValue) > 5) {
      LastValue = CalDirection;
    }
    data.vaneheading = LastValue;
    
    data.anemospeed = (WindAvg / WindDataCount) * 1.60934;
    data.windgust = WindGust * 1.60934;
    WindAvg = 0;
    WindGust = 0;
    WindDataCount = 0;

    send();
  }
  delay(1000);
}

void readSensors() {
  data.pressure = bme.readPressure() / 100;
  data.humidity = bme.readHumidity();

  sensors.requestTemperatures();
  float tempC = sensors.getTempCByIndex(0);
  if (tempC != DEVICE_DISCONNECTED_C)
  {
    data.temperature = tempC;
  }
  else
  {
    Serial.println("Error: Could not read temperature data");
  }
}

void timedef() {
  year = t.year;
  month = t.mon;
  day = t.mday;
  hour = t.hour;
  minute = t.min;
  sec = t.sec;
}

void davisvane() {
  VaneValue = analogRead(A3);
  Direction = map(VaneValue, 0, 1023, 0, 360);
  CalDirection = Direction + Offset;

  if (CalDirection > 360)
    CalDirection = CalDirection - 360;

  if (CalDirection < 0)
    CalDirection = CalDirection + 360;
}

void send() {
  bool rslt;
  rslt = radio.write(&data, sizeof(data));

  Serial.print("Data Sent,");
  if (rslt) {
    Serial.println(" Acknowledge received");
  }
  else {
    Serial.println(" Tx failed");
  }
}

void isr_rg() {

  if ((millis() - contactTime) > 15 ) { // debounce of sensor signal
    tipCount++;
    totalRainfall = tipCount * Bucket_Size;
    contactTime = millis();
  }
}

void isr_rotation () {

  if ((millis() - ContactBounceTime) > 15 ) { // debounce the switch contact.
    Rotations++;
    ContactBounceTime = millis();
  }

}

void isr_timer() {

  TimerCount++;

  if (TimerCount == 6)
  {
    // convert to mp/h using the formula V=P(2.25/T)
    // V = P(2.25/2.5) = P * 0.9
    WindSpeed = Rotations * 0.9;
    Rotations = 0; // Reset count for next sample
    TimerCount = 0;

    WindDataCount++;
    WindAvg = WindAvg + WindSpeed;
    if (WindSpeed > WindGust) {
      WindGust = WindSpeed;
    }
  }
}

Receiver Code

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <SoftwareSerial.h>

#define CE_PIN 7
#define CSN_PIN 8

#define ESP_RX 2
#define ESP_TX 3

char server [] = "rtupdate.wunderground.com";
char WEBPAGE [] = "GET /weatherstation/updateweatherstation.php?";
char ID [] = "INORTH1074";        //wunderground weather station ID
String PASSWORD  = "XXXXX";   // wunderground weather station password

const byte txAddress[5] = {'R', 'x', 'A', 'A', 'A'};

RF24 radio(CE_PIN, CSN_PIN);

struct package {
  float temperature ;
  float humidity ;
  float pressure ;
  float rainfall;
  int vaneheading;
  float anemospeed;
  float windgust;
};

struct package data;

bool newData = false;

float tempf = 0;
float tempc = 0;
float humidityraw = 0;
float dewpointf = 0;

float rainfallnow = 0;
float rainlast1 = 0;
float rainlast2 = 0;
float rainlast3 = 0;
float rainlast4 = 0;
float rainlast5 = 0;

float rain1h = 0;
float rain24h = 0;
float rain24hmm = 0;
int raincount24h = 128; // instead of calling UTC time, which is complicated, set this to the hour*6 + #10 minutes when the sketch was uploaded
float windspeedmph = 0;
float baromin = 0;
int winddir = 0;
float windgustmph = 0;

long startTime = 0;
unsigned char check_connection = 0;
unsigned char times_check = 0;

String AP = "XXXXX";
String PASS = "XXXXX";
int countTrueCommand;
int countTimeCommand;
boolean found = false;

SoftwareSerial esp8266Module(ESP_RX, ESP_TX); // RX, TX ESP8266

//===========

void setup() {

  Serial.begin(9600);
  esp8266Module.begin(9600);
  sendCommand("AT", 5, "OK");
  sendCommand("AT+CWMODE=1", 5, "OK");
  sendCommand("AT+CWJAP=\"" + AP + "\",\"" + PASS + "\"", 20, "OK");

  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.openReadingPipe(1, txAddress);
  radio.setPALevel(RF24_PA_HIGH);

  radio.startListening();
  Serial.println("Receiver Starting Listening");

}

//=============

void loop() {
  getData();
  showData();
}

//==============

void getData() {
  if ( radio.available() ) {
    radio.read(&data, sizeof(data));
    newData = true;
    if (newData == true) {
      convertData ();
    }
  }
}

void showData() {
  if (newData == true) {
    Serial.println(data.temperature);
    Serial.println(data.humidity);
    Serial.println(data.pressure);
    Serial.println(data.rainfall);
    Serial.println(data.vaneheading);
    Serial.println(data.anemospeed);
    Serial.println(data.windgust);
    Serial.println("");
    newData = false;
  }
}

void wunderground()
{
  String cmd1 = "AT+CIPSTART=\"TCP\",\"";
  cmd1 += "rtupdate.wunderground.com"; // wunderground
  cmd1 += "\",80";
  esp8266Module.println(cmd1);

  if (esp8266Module.find("Error")) {
    Serial.println("AT+CIPSTART error");
    return;
  }

  if (newData == true) {
    String cmd = "GET /weatherstation/updateweatherstation.php?ID=";
    cmd += ID;
    cmd += "&PASSWORD=";
    cmd += PASSWORD;
    cmd += "&dateutc=now&winddir=";
    cmd += winddir;
    cmd += "&windspeedmph=";
    cmd += windspeedmph;
    cmd += "&windgustmph=";
    cmd += windgustmph;
    cmd += "&tempf=";
    cmd += tempf;
    cmd += "&dewptf=";
    cmd += dewpointf;
    cmd += "&humidity=";
    cmd += humidityraw;
    cmd += "&baromin=";
    cmd += baromin;
    cmd += "&rainin=";
    cmd += rain1h;
    cmd += "&dailyrainin=";
    cmd += rain24h;
    cmd += "&softwaretype=Arduino-ESP8266&action=updateraw&realtime=1&rtfreq=30";   // &softwaretype=Arduino%20UNO%20version1
    cmd += "/ HTTP/1.1\r\nHost: rtupdate.wunderground.com:80\r\nConnection: close\r\n\r\n";

    cmd1 = "AT+CIPSEND=";
    cmd1 += String(cmd.length());
    esp8266Module.println(cmd1);
    if (esp8266Module.find(">")) {
      esp8266Module.print(cmd);
      Serial.println("Data send OK");
      delay(1000);
    }
    else {
      esp8266Module.println("AT+CIPCLOSE");
      Serial.println("Connection closed");
      Serial.println(" ");
    }
    delay(2500);
  }
}

void convertData() {
  tempc = data.temperature;
  tempf = (tempc * 9.0) / 5.0 + 32.0;
  humidityraw = data.humidity;
  dewpointf = (dewPoint(tempf, data.humidity));

  raincount24h++;
  if (raincount24h < 143) {
    //have to account for the fact that wunderground only counts precip > 0.25mm (0.01 in). the rg-11 is very sensitive, and the graphs look off otherwise
    if (data.rainfall > 0.25) {
      rainfallnow = data.rainfall;
    }
    else {
      rainfallnow = 0;
    }
    rain24hmm = rain24hmm + rainfallnow;
    rain24h = rain24hmm * 0.00393701;
    // theoretically this should be 0.039... but I think the RG-11 is putting out 10mm chunks. This could be fixed in the transmitter, but easier here.
  }
  else {
    raincount24h = 0;
    rain24h = 0;
  }
  rain1h = (rainfallnow + rainlast1 + rainlast2 + rainlast3 + rainlast4 + rainlast5) * 0.00393701;
  // theoretically this should be 0.039... but I think the RG-11 is putting out 10mm chunks. This could be fixed in the transmitter, but easier here.

  // move each rainfall amount forward by 1
  rainlast5 = rainlast4;
  rainlast4 = rainlast3;
  rainlast3 = rainlast2;
  rainlast2 = rainlast1;
  rainlast1 = rainfallnow;

  windspeedmph = data.anemospeed / 1.60934;
  windgustmph = data.windgust / 1.60934;
  winddir = data.vaneheading;
  baromin = (data.pressure) / 33.86;

  wunderground();
}

double dewPoint(double tempf, double humidityraw)
{
  double RATIO = 373.15 / (273.15 + tempf);  // RATIO was originally named A0, possibly confusing in Arduino context
  double SUM = -7.90298 * (RATIO - 1);
  SUM += 5.02808 * log10(RATIO);
  SUM += -1.3816e-7 * (pow(10, (11.344 * (1 - 1 / RATIO ))) - 1) ;
  SUM += 8.1328e-3 * (pow(10, (-3.49149 * (RATIO - 1))) - 1) ;
  SUM += log10(1013.246);
  double VP = pow(10, SUM - 3) * humidityraw;
  double T = log(VP / 0.61078); // temp var
  return (241.88 * T) / (17.558 - T);
}

void sendCommand(String command, int maxTime, char readReplay[]) {
  Serial.print(countTrueCommand);
  Serial.print(". at command => ");
  Serial.print(command);
  Serial.print(" ");
  while (countTimeCommand < (maxTime * 1))
  {
    esp8266Module.println(command);//at+cipsend
    if (esp8266Module.find(readReplay)) //ok
    {
      found = true;
      break;
    }

    countTimeCommand++;
  }

  if (found == true)
  {
    Serial.println("OYI");
    countTrueCommand++;
    countTimeCommand = 0;
  }

  if (found == false)
  {
    Serial.println("Fail");
    countTrueCommand = 0;
    countTimeCommand = 0;
  }

  found = false;
}