/*********************************************************************************
*
* 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 < https : //www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
# include <ModbusSerial.h>
# include "SparkFun_Weather_Meter_Kit_Arduino_Library.h"
//I2C
# include <Wire.h>
# 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 ;
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 ;
int Luminosity ;
int StatusBits = 0 ;
unsigned int 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 )
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 ) {
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 10 minutes
case 2 :
if ( ( millis ( ) - StatemachineTimer ) > = 600000 ) {
StatemachineTimer = millis ( ) ;
Heater = 0 ;
state = 3 ;
} else {
Heater = 1 ;
}
TempValid = 0 ;
break ;
// Heater is now off, let the sensor cool for 10 minutes
case 3 :
if ( ( millis ( ) - StatemachineTimer ) > = 600000 ) {
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 ;
si7021 . triggerMeasurement ( ) ;
si7021 . getHumidity ( MeasuredData . Humidity ) ;
if ( MeasuredData . Humidity > 100 | | MeasuredData . Humidity < 0 )
MeasuredData . Humidity = 100 ;
//If humidity is larger than 95% switch on heater to get more acurate measurement and prevent memory offset
result = HeaterSi7021 ( MeasuredData . 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
// Scale for more decimal positions when converted to integer value for ModBus
MeasuredData . Humidity * = 100 ;
// Temperture 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"));
}
// 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 [ ] , 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 ;
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 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 ;
// 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.1 " ) ) ;
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
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 ) ;
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
}
}