/********************************************************************************* * * 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 . * * 2023-01-21: - Buffer overflow when calculating average wind speed in AverageOfArray() * Fix: use 32 bit register for average_value. * - Changed some variables to the propper standard (uint8_t, uint16_t, etc.) * - SparkFun wind interrupt now calculates over 3 seconds in stead of 1 second (KNMI standard) * **********************************************************************************/ #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) [NOT IMPLEMENTED, always 0] * 30008: Humidity (percent) * 30009: Barometric pressure (hPa) * 30010: Luminosity (W/m2) * 30011: Snow fall [NOT IMPLEMENTED, always 0] * 30012: Raw rainfall counter (mm) * 30013: Temperature pressure sensor (degrees Celsius) * 30014: Status bits 0=heater, 1-15: reserved * */ 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; const int SensorLuminosityIreg = 10; const int SensorSnowFallIreg = 11; const int SensorRainfallRawIreg = 12; const int SensorTemperatureBackupIreg = 13; const int SensorStatusBitsIreg = 14; /* Modbus Registers Offsets (0-9999) * Coils * 0 = Heater algorithm (0 = disable, 1 = enable) */ const int HeaterCoil = 0; // 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; uint16_t WindGustData1[30]; uint8_t WindGustData1Counter=0; uint16_t WindGustData2[10]; uint8_t WindGustData2Counter=0; uint16_t WindAverageData1[30]; uint8_t WindAverageData1Counter=0; uint16_t WindAverageData2[10]; uint8_t WindAverageData2Counter=0; uint16_t RainPerHour[24]; uint8_t RainPerHourCounter=0; struct MeasuredData { uint16_t WindDirection; uint16_t WindSpeed; uint16_t WindGust; uint16_t Rain; uint16_t RainLast24; uint16_t SensorRainSinceMidnight; uint16_t Pressure; uint16_t Luminosity; uint16_t StatusBits = 0; uint16_t RainfallCounter = 0; float Temperature; float Humidity; float TemperatureBackup; bool HeaterStatus = 0; } MeasuredData; // State machine implementing smart heater to prevent saturation of the sensor char HeaterSi7021 (float humidity) { static int state=0; static unsigned long StatemachineTimer=0; bool TempValid=1; bool Heater=0; // If Smart heater algorithm is disabled, reset the statemachine forever. // TempValid bit is also forced to 1, but it could be that we just came out of a heater period. // We assume that the client on the other side of the ModBus is smart enough to understand. if ( (MeasuredData.StatusBits & 0x04) == 0) state = 0; switch (state) { // Default state: humidity is below 95% case 0: Heater = 0; if (humidity >= 95) { StatemachineTimer = millis(); state = 1; } break; // Humidity went above 95%. See if humidity stays above 95% for more than an hour, if so turn on heater case 1: if (humidity >= 95) { if ( (millis() - StatemachineTimer) >= 3.6e+6 ) { //if ( (millis() - StatemachineTimer) >= 300000 ) { // short delay for testing Heater = 1; TempValid = 0; StatemachineTimer = millis(); state = 2; } else { Heater = 0; } } else { Heater = 0; state = 0; } break; // Heater is now on, let the sensor cook for 5 minutes case 2: if ( (millis() - StatemachineTimer) >= 300000 ) { StatemachineTimer = millis(); Heater = 0; state = 3; } else { Heater = 1; } TempValid = 0; break; // Heater is now off, let the sensor cool for 15 minutes case 3: if ( (millis() - StatemachineTimer) >= 900000 ) { TempValid = 1; // Sensor cooled, so we can take a valid temperature reading if (humidity >= 95) { // Humidity still above 95%, repeat heating/cooling Heater = 1; StatemachineTimer = millis(); state = 2; } else { // Humidity below 95%, reset statemachine Heater = 0; state = 0; } } else { Heater = 0; TempValid = 0; } break; default: Heater = 0; state = 0; break; } return TempValid<<1 | Heater; } // Read Si7021 sensor and process data void ReadSi7021 (void) { char result=0x2; float humidity; si7021.triggerMeasurement(); si7021.getHumidity(humidity); if (humidity>100 || humidity<0) humidity = 100; //If humidity is larger than 95% switch on heater to get more acurate measurement and prevent memory offset result = HeaterSi7021(humidity); MeasuredData.StatusBits &= 0xFFFC; // Reset heater status bits to zero MeasuredData.StatusBits |= result; // And set the proper bits to one if there are any. The result is we copied the status bits to the register // Temperture and humidity readings are valid (as the sensor is not heated) if (result & 0x2) { si7021.getTemperature(MeasuredData.Temperature); // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Temperature *= 100; //Serial.print(F("Valid temp")); si7021.getHumidity(MeasuredData.Humidity); if (MeasuredData.Humidity>100 || MeasuredData.Humidity<0) MeasuredData.Humidity = 100; // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Humidity *= 100; } // Statemachine thinks it is time to switch on the heater if (result & 0x1) { //Serial.print(F("Heater on.")); MeasuredData.HeaterStatus = 1; } else { //Serial.print(F("Heater off.")); MeasuredData.HeaterStatus = 0; } si7021.setHeater(MeasuredData.HeaterStatus); } // Read BMP280 void ReadBMP280 (void) { bmp280.awaitMeasurement(); float pascal; bmp280.getPressure(pascal); pascal = (pascal - PRESSURE_OFFSET) / 10; // Convert to hPa MeasuredData.Pressure = pascal; bmp280.getTemperature(MeasuredData.TemperatureBackup); // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.TemperatureBackup *= 100; bmp280.triggerMeasurement(); } int MaxOfArray (int array[], uint16_t length) { int maximum_value = 0; while (length--) { if (array[length] > maximum_value) maximum_value = array[length]; } return maximum_value; } uint16_t AverageOfArray (uint16_t array[], uint16_t length) { uint32_t tmp_value = 0; uint8_t tmp_length = length; uint16_t 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; tmpRegister = 10*weatherMeterKit.getWindDirection(); // Use float for conversion to degrees times 10, than put it in integer register for ModBus MeasuredData.WindDirection = tmpRegister; 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 cannot 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; // Every time before we reset the TotalRainCounter we add the amount to the RawRainCounter. // This 16 bit register will eventually overflow, but 655.35mm of rain fall is a lot! MeasuredData.RainfallCounter += MeasuredData.Rain; // We don't care about the rounding error due to the convertion from float to int 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); mb.addIreg (SensorLuminosityIreg); mb.addIreg (SensorSnowFallIreg); mb.addIreg (SensorRainfallRawIreg); mb.addIreg (SensorTemperatureBackupIreg); mb.addIreg (SensorStatusBitsIreg); // Add HeaterCoil register mb.addCoil (HeaterCoil); // Set Weather station ID mb.Ireg (SensorIDIreg, 0x5758); // Set unused register to zero mb.Ireg (SensorRainSinceMidnightIreg, 0); mb.Ireg (SensorSnowFallIreg, 0); Serial.println(F("Weather station v0.2.2")); Serial.println(F("(C)2024 M.T. Konstapel")); Serial.println(F("This project is free and open source")); Serial.println(F("More details: https://meezenest.nl/mees/")); //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); } } // The standard library of the Si7021 sets the heater element to the default 3.1mA, but we want the full power // We could alter the library, but than we break compatibility. So for this one time we do a raw-write to the // heater register. const uint8_t SI7021_I2C_ADDRESS =(0x40); const uint8_t SI7021_CMD_WRITE_HEATER_CONTROL_REG =(0x51); const uint8_t SI7021_HEATER_FULL_BLAST =(0x0F); // Set heater to 94mA i2c.writeByte(SI7021_I2C_ADDRESS, SI7021_CMD_WRITE_HEATER_CONTROL_REG, SI7021_HEATER_FULL_BLAST); // 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 // Dutch metrology institute (KNMI) defines that the windspeed and gust should // be calculated from 3 seconds measurements. calibrationParams.windSpeedMeasurementPeriodMillis = 3000; // 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); mb.Ireg (SensorTemperatureBackupIreg, MeasuredData.TemperatureBackup); mb.Ireg (SensorLuminosityIreg, MeasuredData.Luminosity); mb.Ireg (SensorRainfallRawIreg, MeasuredData.RainfallCounter); mb.Ireg (SensorStatusBitsIreg, MeasuredData.StatusBits); // Debug wind vane //Serial.print(F("\n Measured ADC: ")); //Serial.print(analogRead(windDirectionPin)); // enable or disable smart heater if (mb.Coil (HeaterCoil)) { MeasuredData.StatusBits |= 0x04; // Set bit } else { MeasuredData.StatusBits &= 0x0B; // Reset bit } digitalWrite(LED_BUILTIN, LOW); // LED as heartbeat } }