Weather station with ModBus over RS-485
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 lines
10 KiB

11 months ago
/*********************************************************************************
*
* 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.
*
* (C)2023 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 humidit sensor
#include "i2c_SI7021.h"
SI7021 si7021;
/**************************/
/* 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);
// Scale for more decimal positions when converted to integer value for ModBus
MeasuredData.Humidity *= 100;
MeasuredData.Temperature *= 100;
//If humidity is larger than 80% switch on heater to get more acurate measurement
//Switch off when lower than 78% (hysteresis)
if (MeasuredData.Humidity > 80 && !MeasuredData.HeaterStatus) {
Serial.print(F("Heater on."));
MeasuredData.HeaterStatus = 1;
si7021.setHeater(MeasuredData.HeaterStatus);
}
if (MeasuredData.Humidity < 78 && MeasuredData.HeaterStatus) {
Serial.print(F("Heater off."));
MeasuredData.HeaterStatus = 0;
si7021.setHeater(MeasuredData.HeaterStatus);
}
}
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);
//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);
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) {};
}
// 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
// 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();
// Read temperature and humidity
ReadSi7021();
// 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);
}
}