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 28 October, 2024.

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.

(October 2024)

The weather station now calls NTP on startup, and uses millis() to gauge the time when data is received, every 10 minutes. This allows it to better roll over rainfall counts at midnight, instead of relying on a counter which can drift if data is not transmitted, received, or uploaded successfully.

Reliability issues with the radio transmission system prompted me to implement failsafes — the transmitting module outside now tries every 5 seconds over a minute to transmit the gathered data, until receipt is acknowledged. Likewise, the receiving module will now try to upload data several times until it is successful. On startup, the NTP pool call (using the ESP8266 AT commands through the serial port) will try multiple times. I investigated the accuracy of the rain gauge, and lowered the sensitivity to 0.2 mm ‘buckets’ and deactivated the low power mode, which means the rain gauge can now make use of its small internal heater to reduce condensation. This seems to have brought the rainfall amounts in better alignment with those around it (though nearby rain gauges show considerable variability).

As an aside, I have to use AT commands directly for the ESP8266, because I assume the proprietary libraries are designed to make use of pins that I am using elsewhere, and nothing I’ve tried to be able to use the libraries has resolved this. AT commands are incredibly opaque, but they do seem to work as I have them (extensively borrowing from this Arduino forum thread). They are as mysterious as they are functional, and I hope that the receiver code I have below might help someone figure them out.

(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.2 // bucket size to trigger tip count (in mm) dip switches 87654321 = 00000100
#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)

bool txSuccess;
volatile unsigned int txTryCount;

// 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 10 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;

    txSuccess = false;
    while (!txSuccess &&  txTryCount < 25) {
      send();
      if (!txSuccess) {
        Serial.println("Transmission failed. Trying again");
        delay(5000);
        txTryCount++;
      } else {
        Serial.println("Transmission succeeded in main loop");
        break;
      }
    }
    txTryCount = 0;
  }
  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");
    txSuccess = true;
  }
  else {
    Serial.println(" Tx failed");
    txSuccess = false;
  }
}

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>
#include <time.h>

#define CE_PIN 7
#define CSN_PIN 8

// esp definitions, time related math
#define ESP_RX 2
#define ESP_TX 3
#define SECONDROUNDINGTHRESHOLD 115
#define SEVENTYYEARS 2208988800UL
#define NUMBEROFSECONDSPERHOUR 3600UL
#define NUMBEROFSECONDSPERMINUTE 60UL
#define NUMBEROFSECONDSPERDAY 86400UL
#define UTC_DELTA_HOURS -4
bool isDST = true; // true if it is daylight savings, false if not
long UTC_DELTA; // UTC -4 (Atlantic Time) in Seconds (-4*3600 sec/h)
unsigned long epochUnix;
unsigned long timeSentLastRequest;

char server [] = "rtupdate.wunderground.com";
char WEBPAGE [] = "GET /weatherstation/updateweatherstation.php?";
char ID [] = "INORTH1074";        //wunderground weather station ID
String PASSWORD  = "XXXXXX";   // 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;
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 = "XXXXXX";
String PASS = "XXXXXX";
int countTrueCommand;
int countTimeCommand;
boolean found = false;

static const unsigned long ntpFirstFourBytes = 0xEC0600E3; // first four bytes of a well-formed NTP request
const uint8_t NTP_PACKET_SIZE = 48;

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

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

void setup() {
  if (isDST) {
    UTC_DELTA = (UTC_DELTA_HOURS + 1) * NUMBEROFSECONDSPERHOUR;
  }
  else {
    UTC_DELTA = UTC_DELTA_HOURS * NUMBEROFSECONDSPERHOUR;
  }

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

  epochUnix = getTime();
  uint8_t offsetHours = (epochUnix  % NUMBEROFSECONDSPERDAY) / NUMBEROFSECONDSPERHOUR;
  uint8_t offsetMinutes = (epochUnix % NUMBEROFSECONDSPERHOUR) / NUMBEROFSECONDSPERMINUTE;
  Serial.println("Start Time: " + String(offsetHours) + ":" + String(offsetMinutes));
  timeSentLastRequest = 9999999UL;

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

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

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

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

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

void getData() {
  if ( radio.available() ) {
    radio.read(&data, sizeof(data));
    newData = true;
    unsigned long timeSinceLastRequest = millis() - timeSentLastRequest;
    if (newData == true && timeSinceLastRequest > 60000UL) { // if there has been more than a minute since the last data request
      convertData ();
      timeSentLastRequest = millis();
    }
  }
}

void showData() {
  if (newData == true) {
    Serial.println("Temp: " + String(data.temperature));
    Serial.println("Humd: " + String(data.humidity));
    Serial.println("Baro: " + String(data.pressure));
    Serial.println("Rain: " + String(data.rainfall));
    Serial.println("Wdir: " + String(data.vaneheading));
    Serial.println("Wspd: " + String(data.anemospeed));
    Serial.println("Wgus: " + String(data.windgust));
    Serial.println("");
    newData = false;
  }
}

void wunderground()
{
  unsigned int SendTryCount = 0;
  while (SendTryCount < 6) {
    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);
        break;
      }
      else {
        esp8266Module.println("AT+CIPCLOSE");
        Serial.println("Connection closed");
        Serial.println(" ");
      }
      SendTryCount++;
      delay(2500);
    }
  }
  SendTryCount = 0;
}

void convertData() {
  bool isMidnight = false;

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

  unsigned long timeSinceStart = millis();
  unsigned long curTimeSecs = epochUnix + (timeSinceStart / 1000); // number of milliseconds per second
  uint8_t trueHours = (curTimeSecs  % NUMBEROFSECONDSPERDAY) / NUMBEROFSECONDSPERHOUR;
  uint8_t trueMinutes = (curTimeSecs % NUMBEROFSECONDSPERHOUR) / NUMBEROFSECONDSPERMINUTE;
  Serial.println("Current Time: " + String(trueHours) + ":" + String(trueMinutes));

  if (trueHours == 0 && trueMinutes <= 10) {
    isMidnight = true;
  }
  else {
    isMidnight = false;
  }

  if (!isMidnight) {
    //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.0393701;
  }
  else {
    rain24hmm = 0;
    rain24h = 0;
  }
  rain1h = (rainfallnow + rainlast1 + rainlast2 + rainlast3 + rainlast4 + rainlast5) * 0.0393701;

  // 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("Command Processed OK");
    countTrueCommand++;
    countTimeCommand = 0;
  }

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

  found = false;
}

void emptyESP_RX(unsigned long duration)
{
  unsigned long currentTime;
  currentTime = millis();
  while (millis() - currentTime <= duration) {
    if (esp8266Module.available() > 0) esp8266Module.read();
  }
}

unsigned long getTime() {
  unsigned long secsSince1900 = 0UL;
  unsigned long epochUnix;
  bool NTPSuccess = false;
  unsigned int SendTryCount = 0;
  while (!NTPSuccess && SendTryCount < 6) {
    sendCommand("AT+CIPSTART=\"UDP\",\"ca.pool.ntp.org\",123", 20, "OK");
    sendCommand("AT+CIPSEND=48", 5, "OK");
    if (esp8266Module.find(">")) {
      emptyESP_RX(1000UL); // empty the buffer (we get a > character)
      esp8266Module.write((char*) &ntpFirstFourBytes, NTP_PACKET_SIZE); // the first 4 bytes matters, then we don't care, whatever is in the memory will do
      Serial.println("NTP send OK on try:");
      Serial.println(SendTryCount);
      NTPSuccess = true;
      break;
    }
    else {
      esp8266Module.println("AT+CIPCLOSE");
      Serial.println("NTP Connection closed");
      Serial.println(" ");
      sendCommand("AT+RST", 5, "OK");
      sendCommand("AT", 5, "OK");
      sendCommand("AT+CWMODE=1", 5, "OK");
      sendCommand("AT+CWJAP=\"" + AP + "\",\"" + PASS + "\"", 20, "OK");
      sendCommand("AT+CIPMUX=0", 5, "OK");
      NTPSuccess = false;
    }
    delay(5000);
    SendTryCount++;
  }
  if (!NTPSuccess) {
    return 88888888UL;
  }
  NTPSuccess = false;
  SendTryCount = 0;

  waitForString(":", 5000UL);
  for (int i = 0; i < NTP_PACKET_SIZE; i++) {
    while (!esp8266Module.available());
    int c = esp8266Module.read();
    if ((i >= 40) && (i < 44))  secsSince1900 = (secsSince1900 << 8) + (unsigned long) ((uint8_t) (c & (int) 0x00FF)); // Read the integer part of sending time
    else if (i == 44) secsSince1900 += (((uint8_t) c) > SECONDROUNDINGTHRESHOLD ? 1 : 0);
  }
  epochUnix = secsSince1900 - SEVENTYYEARS;
  Serial.println(epochUnix);
  sendCommand("AT+CIPCLOSE", 5, "OK"); // close connection
  return epochUnix + UTC_DELTA;
}

// from user J-M-L on Arduino Forums
// checks for a character in an AT command return (e.g. from AT+CWPSEND)
boolean waitForString(const char * endMarker, unsigned long duration)
{
  int localBufferSize = strlen(endMarker); // we won't need an \0 at the end
  char localBuffer[localBufferSize];
  int index = 0;
  boolean endMarkerFound = false;
  unsigned long currentTime;

  memset(localBuffer, '\0', localBufferSize); // clear buffer

  currentTime = millis();
  while (millis() - currentTime <= duration) {
    if (esp8266Module.available() > 0) {
      if (index == localBufferSize) index = 0;
      localBuffer[index] = (uint8_t) esp8266Module.read();
      endMarkerFound = true;
      for (int i = 0; i < localBufferSize; i++) {
        if (localBuffer[(index + 1 + i) % localBufferSize] != endMarker[i]) {
          endMarkerFound = false;
          break;
        }
      }
      index++;
    }
    if (endMarkerFound) break;
  }
  return endMarkerFound;
}