/*********************************************************************************
*
* 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/>.
*
* 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 <ModbusSerial.h>
# include "SparkFun_Weather_Meter_Kit_Arduino_Library.h"
//I2C
# include <Wire.h>
# 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
}
}