/********************************************************************************* * * weather_station is a weatherstation build around the SparkFun weather meter * It can measure wind speed, wind gust , wind direction, rain fall, temperature, * humidity and air pressure and has an RS-485 ModBus interface for your convenience. * * LED on Arduino gives status: * * ON : Booting * BLINK : I2C ERROR * FLASH : Heartbeat * * Copyright (C) 2023, 2024 M.T. Konstapel https://meezenest.nl/mees * * This file is part of weather_station * * weather_station is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * weather_station is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with weather_station. If not, see . * **********************************************************************************/ #include #include "SparkFun_Weather_Meter_Kit_Arduino_Library.h" //I2C #include #include "i2c.h" //Temperature and humidity sensor #include "i2c_SI7021.h" SI7021 si7021; // Pressure sensor #include "i2c_BMP280.h" BMP280 bmp280; float PRESSURE_OFFSET = 210; // Calibration of BMP280: offset in Pascal /**************************/ /* Configurable variables */ /**************************/ // Sparkfun weather station int windDirectionPin = A0; int windSpeedPin = 2; int rainfallPin = 3; // RS485 driver #define RS485_RE 11 // Tight to RS485_DE and must be configured as an input to prevent a short circuit #define RS485_DE 12 // Used Pins const int TxenPin = RS485_DE; // -1 disables the feature, change that if you are using an RS485 driver, this pin would be connected to the DE and /RE pins of the driver. // ModBus address const byte SlaveId = 14; /* Modbus Registers Offsets (0-9999) * * 30000: Weater station ID (0x5758) * 30001: Wind direction (degrees) * 30002: Wind speed (average over 10 minutes in km/h) * 30003: Wind gust (peak wind speed in the last 10 minutes in km/h) * 30004: Temperature (degrees Celcius) * 30005: Rain last hour (l/m2) * 30006: Rain last 24 hours (l/m2) * 30007: Rain since midnight (l/m2) * 30008: Humidity (percent) * 30009: Barometric pressure (hPa) * */ const int SensorIDIreg = 0; const int SensorWindDirectionIreg = 1; const int SensorWindSpeedIreg = 2; const int SensorWindGustIreg = 3; const int SensorTemperatureIreg = 4; const int SensorRainIreg = 5; const int SensorRainLast24Ireg = 6; const int SensorRainSinceMidnightIreg = 7; const int SensorHumidityIreg = 8; const int SensorPressureIreg = 9; // RS-485 serial port #define MySerial Serial // define serial port used, Serial most of the time, or Serial1, Serial2 ... if available const unsigned long Baudrate = 9600; /******************************/ /* END Configurable variables */ /******************************/ // Create an instance of the weather meter kit SFEWeatherMeterKit weatherMeterKit(windDirectionPin, windSpeedPin, rainfallPin); // ModbusSerial object ModbusSerial mb (MySerial, SlaveId, TxenPin); unsigned long ts; unsigned long HourTimer; int WindGustData1[30]; unsigned char WindGustData1Counter=0; int WindGustData2[10]; unsigned char WindGustData2Counter=0; int WindAverageData1[30]; unsigned char WindAverageData1Counter=0; int WindAverageData2[10]; unsigned char WindAverageData2Counter=0; int RainPerHour[24]; unsigned char RainPerHourCounter=0; struct MeasuredData { int WindDirection; int WindSpeed; int WindGust; int Rain; int RainLast24; int SensorRainSinceMidnight; int Pressure; float Temperature; float Humidity; bool HeaterStatus = 0; } MeasuredData; // Read Si7021 sensor and process data void ReadSi7021 (void) { si7021.triggerMeasurement(); si7021.getHumidity(MeasuredData.Humidity); si7021.getTemperature(MeasuredData.Temperature); if (MeasuredData.Humidity>100 || MeasuredData.Humidity<0) MeasuredData.Humidity = 100; //If humidity is larger than 96% switch on heater to get more acurate measurement and prevent memory offset //Switch off when lower than 94% (hysteresis) if (MeasuredData.Humidity > 96 && !MeasuredData.HeaterStatus) { Serial.print(F("Heater on.")); MeasuredData.HeaterStatus = 1; si7021.setHeater(MeasuredData.HeaterStatus); } if (MeasuredData.Humidity < 94 && MeasuredData.HeaterStatus) { Serial.print(F("Heater off.")); MeasuredData.HeaterStatus = 0; si7021.setHeater(MeasuredData.HeaterStatus); } // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Humidity *= 100; MeasuredData.Temperature *= 100; } // Read BMP280 void ReadBMP280 (void) { // MeasuredData.Pressure=0; bmp280.awaitMeasurement(); float temperature; bmp280.getTemperature(temperature); float pascal; bmp280.getPressure(pascal); pascal = (pascal - PRESSURE_OFFSET) / 10; // Convert to hPa MeasuredData.Pressure = pascal; bmp280.triggerMeasurement(); // When humidity is high, the heater of the Si7021 is on. This causes the temperature sensor of the humidity sensor to heat up. // Use temperature sensor of BMP280 instead. if (MeasuredData.HeaterStatus) { bmp280.getTemperature(MeasuredData.Temperature); // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Temperature *= 100; } } int MaxOfArray (int array[], unsigned int length) { int maximum_value = 0; while (length--) { if (array[length] > maximum_value) maximum_value = array[length]; } return maximum_value; } int AverageOfArray (int array[], unsigned int length) { int tmp_value = 0; unsigned char tmp_length = length; int average_value = 0; while (length--) { tmp_value += array[length]; } average_value = tmp_value/tmp_length; return average_value; } // Call this function every 2 seconds void ReadSparkfunWeatherStation (void) { unsigned char cnt=0; float tmpRegister; MeasuredData.WindDirection = weatherMeterKit.getWindDirection(); tmpRegister = 100*(weatherMeterKit.getWindSpeed())/3.6; // Use float for conversion to m/s times 100, than put it in integer register for ModBus MeasuredData.WindSpeed = tmpRegister; tmpRegister = 100*weatherMeterKit.getTotalRainfall(); // Use float for conversion to l/m2 times 100, than put it in integer register for ModBus MeasuredData.Rain = tmpRegister; // FIFO for calculating wind gust of last 10 minutes // to preserve valuable RAM we caanot store all measurements of the last 10 minutes. // So we use a hack: store the last 30 values in a FIFO and every minute we store the maximum value from this FIFO in another FIFO. // This second FIFO is 10 deep: it stores the maximum values of the last 10 minutes. // The maximum value from this FIFO is the maximum wind gust of the last 10 minutes. if ( WindGustData1Counter < 29 ) { WindGustData1Counter++; } else { if ( WindGustData2Counter < 9 ) { WindGustData2Counter++; } else { WindGustData2Counter=0; } WindGustData2[WindGustData2Counter] = MaxOfArray(WindGustData1, 30); WindGustData1Counter=0; } WindGustData1[WindGustData1Counter] = MeasuredData.WindSpeed; MeasuredData.WindGust= MaxOfArray(WindGustData2, 10); // Smart FIFO, same as for Wind Gust, but now for average wind speed over 10 minutes if ( WindAverageData1Counter < 29 ) { WindAverageData1Counter++; } else { if ( WindAverageData2Counter < 9 ) { WindAverageData2Counter++; } else { WindAverageData2Counter=0; } WindAverageData2[WindAverageData2Counter] = AverageOfArray(WindAverageData1, 30); WindAverageData1Counter=0; WindAverageData1[WindAverageData1Counter] = MeasuredData.WindSpeed; } WindAverageData1[WindAverageData1Counter] = MeasuredData.WindSpeed; MeasuredData.WindSpeed = AverageOfArray(WindAverageData2, 10); // Record rainfall in one hour, save last 24 readings in FIFO if ( ( millis() - HourTimer) >= 3.6e+6) { HourTimer = millis(); if ( RainPerHourCounter < 23 ) { RainPerHourCounter++; } else { RainPerHourCounter=0; } RainPerHour[RainPerHourCounter] = MeasuredData.Rain; weatherMeterKit.resetTotalRainfall(); // Calculate rain fall in the last 24 hours MeasuredData.RainLast24=0; for (cnt=0; cnt<24;cnt++) { MeasuredData.RainLast24 += RainPerHour[cnt]; } } MeasuredData.Rain = RainPerHour[RainPerHourCounter]; } void setup() { MySerial.begin (Baudrate); // works on all boards but the configuration is 8N1 which is incompatible with the MODBUS standard // prefer the line below instead if possible // MySerial.begin (Baudrate, MB_PARITY_EVEN); // initialize digital pin LED_BUILTIN as an output and turn it on. pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); //Setup control lines for RS485 driver pinMode(RS485_RE,INPUT); // In hardware connected to RS485_DE. Should be input to prevent a short circuit! pinMode(RS485_DE,OUTPUT); digitalWrite(RS485_DE,LOW); mb.config (Baudrate); mb.setAdditionalServerData ("TEMP_SENSOR"); // for Report Server ID function (0x11) // Add SensorIreg registers - Use addIreg() for analog Inputs mb.addIreg (SensorIDIreg); mb.addIreg (SensorWindDirectionIreg); mb.addIreg (SensorWindSpeedIreg); mb.addIreg (SensorWindGustIreg); mb.addIreg (SensorTemperatureIreg); mb.addIreg (SensorRainIreg); mb.addIreg (SensorRainLast24Ireg); mb.addIreg (SensorRainSinceMidnightIreg); mb.addIreg (SensorHumidityIreg); mb.addIreg (SensorPressureIreg); // Set Weather station ID mb.Ireg (SensorIDIreg, 0x5758); // Set unused register to zero mb.Ireg (SensorRainSinceMidnightIreg, 0); Serial.println(F("Weather station")); //Initialize Si7021 sensor Serial.print(F("Humidity sensor SI7021 ")); if (si7021.initialize()) Serial.println(F("found")); else { Serial.println(F("missing")); while(1) { digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) delay(500); // wait for half a second digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW delay(500); } } // Initialize BMP280 pressure sensor Serial.print(F("Pressure sensor BMP280 ")); if (bmp280.initialize()) Serial.println(F("found")); else { Serial.println(F("missing")); while(1) { digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) delay(500); // wait for half a second digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW delay(500); } } // onetime-measure: bmp280.setEnabled(0); bmp280.triggerMeasurement(); // Expected ADC values have been defined for various platforms in the // library, however your platform may not be included. This code will check // if that's the case #ifdef SFE_WMK_PLAFTORM_UNKNOWN // The platform you're using hasn't been added to the library, so the // expected ADC values have been calculated assuming a 10k pullup resistor // and a perfectly linear 16-bit ADC. Your ADC likely has a different // resolution, so you'll need to specify it here: weatherMeterKit.setADCResolutionBits(10); #endif // Here we create a struct to hold all the calibration parameters SFEWeatherMeterKitCalibrationParams calibrationParams = weatherMeterKit.getCalibrationParams(); // The wind vane has 8 switches, but 2 could close at the same time, which // results in 16 possible positions. Each position has a resistor connected // to GND, so this library assumes a voltage divider is created by adding // another resistor to VCC. Some of the wind vane resistor values are // fairly close to each other, meaning an accurate ADC is required. However // some ADCs have a non-linear behavior that causes this measurement to be // inaccurate. To account for this, the vane resistor values can be manually // changed here to compensate for the non-linear behavior of the ADC calibrationParams.vaneADCValues[WMK_ANGLE_0_0] = 943; calibrationParams.vaneADCValues[WMK_ANGLE_22_5] = 828; calibrationParams.vaneADCValues[WMK_ANGLE_45_0] = 885; calibrationParams.vaneADCValues[WMK_ANGLE_67_5] = 702; calibrationParams.vaneADCValues[WMK_ANGLE_90_0] = 785; calibrationParams.vaneADCValues[WMK_ANGLE_112_5] = 404; calibrationParams.vaneADCValues[WMK_ANGLE_135_0] = 460; calibrationParams.vaneADCValues[WMK_ANGLE_157_5] = 82; calibrationParams.vaneADCValues[WMK_ANGLE_180_0] = 91; calibrationParams.vaneADCValues[WMK_ANGLE_202_5] = 64; calibrationParams.vaneADCValues[WMK_ANGLE_225_0] = 185; calibrationParams.vaneADCValues[WMK_ANGLE_247_5] = 125; calibrationParams.vaneADCValues[WMK_ANGLE_270_0] = 285; calibrationParams.vaneADCValues[WMK_ANGLE_292_5] = 242; calibrationParams.vaneADCValues[WMK_ANGLE_315_0] = 628; calibrationParams.vaneADCValues[WMK_ANGLE_337_5] = 598; // The rainfall detector contains a small cup that collects rain water. When // the cup fills, the water is dumped and the total rainfall is incremented // by some value. This value defaults to 0.2794mm of rain per count, as // specified by the datasheet calibrationParams.mmPerRainfallCount = 0.2794; // The rainfall detector switch can sometimes bounce, causing multiple extra // triggers. This input is debounced by ignoring extra triggers within a // time window, which defaults to 100ms calibrationParams.minMillisPerRainfall = 100; // The anemometer contains a switch that opens and closes as it spins. The // rate at which the switch closes depends on the wind speed. The datasheet // states that a wind of 2.4kph causes the switch to close once per second calibrationParams.kphPerCountPerSec = 2.4; // Because the anemometer generates discrete pulses as it rotates, it's not // possible to measure the wind speed exactly at any point in time. A filter // is implemented in the library that averages the wind speed over a certain // time period, which defaults to 1 second. Longer intervals result in more // accurate measurements, but cause delay in the measurement calibrationParams.windSpeedMeasurementPeriodMillis = 1000; // Now we can set all the calibration parameters at once weatherMeterKit.setCalibrationParams(calibrationParams); // Begin weather meter kit weatherMeterKit.begin(); ts = millis(); RainPerHourCounter = ts; } void loop() { // Call once inside loop() - all magic here mb.task(); // Read each two seconds if ( ( millis() - ts) >= 2000) { ts = millis(); digitalWrite(LED_BUILTIN, HIGH); // LED as heartbeat // Read temperature and humidity ReadSi7021(); // Read pressure and temperature ReadBMP280(); // Read Wind and rain ReadSparkfunWeatherStation(); // Setting Sparkfun weather station registers mb.Ireg (SensorWindDirectionIreg, MeasuredData.WindDirection); mb.Ireg (SensorWindSpeedIreg, MeasuredData.WindSpeed); mb.Ireg (SensorWindGustIreg, MeasuredData.WindGust); mb.Ireg (SensorRainIreg, MeasuredData.Rain); mb.Ireg (SensorRainLast24Ireg, MeasuredData.RainLast24); mb.Ireg (SensorTemperatureIreg, MeasuredData.Temperature); mb.Ireg (SensorHumidityIreg, MeasuredData.Humidity); mb.Ireg (SensorPressureIreg, MeasuredData.Pressure); // Debug wind vane //Serial.print(F("\n Measured ADC: ")); //Serial.print(analogRead(windDirectionPin)); digitalWrite(LED_BUILTIN, LOW); // LED as heartbeat } }