/********************************************************************************* * * 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) * * 2-24-05-02: - Removed cope for si7021 * - Added code for HYT221 humidity sensor * * See CHANGELOG.md * **********************************************************************************/ #include #include "SparkFun_Weather_Meter_Kit_Arduino_Library.h" //I2C #include #include "i2c.h" //Temperature and humidity sensor #define HYT_ADDR 0x28 // I2C address of the HYT 221, 271, 371 and most likely the rest of the family // 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; float HUMIDITY_THRESHOLD = 92.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; void ReadHYT221 (void) { double humidity; double temperature; Wire.beginTransmission(HYT_ADDR); // Begin transmission with given device on I2C bus Wire.requestFrom(HYT_ADDR, 4); // Request 4 bytes // Read the bytes if they are available // The first two bytes are humidity the last two are temperature if(Wire.available() == 4) { int b1 = Wire.read(); int b2 = Wire.read(); int b3 = Wire.read(); int b4 = Wire.read(); Wire.endTransmission(); // End transmission and release I2C bus // combine humidity bytes and calculate humidity int rawHumidity = b1 << 8 | b2; // compound bitwise to get 14 bit measurement first two bits // are status/stall bit (see intro text) rawHumidity = (rawHumidity &= 0x3FFF); humidity = 100.0 / pow(2,14) * rawHumidity; // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Humidity = 100 * humidity; // combine temperature bytes and calculate temperature b4 = (b4 >> 2); // Mask away 2 least significant bits see HYT 221 doc int rawTemperature = b3 << 6 | b4; temperature = 165.0 / pow(2,14) * rawTemperature - 40; // Scale for more decimal positions when converted to integer value for ModBus MeasuredData.Temperature = 100 * temperature; //Serial.print(MeasuredData.Humidity); //Serial.print("% - Temperature: "); //Serial.println(MeasuredData.Temperature); } else { Serial.println("Not enough bytes available on wire."); } } // 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) { // decrement length, because 0/1 problem: if lenght = n, than last position off array is n-1 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) { // decrement length, because 0/1 problem: if lenght = n, than last position off array is n-1 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.3.0")); 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/")); // 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 ReadHYT221(); // 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 } }