/**
* @file Arduino_Placas.ino
* @author Arturo Olivares Martos
*
* Informaremos, cada FREC_MEDICION segundos:
* - Dirección del viento.
* - Velocidad del viento.
* - Temperatura exterior.
* - Temperatura en el cuadro de placas.
* - Llueve o no. Consideraremos que llueve si, en esos FREC_MEDICION segundos, ha habido al menos un click en el pluviómetro (0.2794mm).
* - Del acumulado de lluvia en la última hora.
*
*
* @see https://learn.sparkfun.com/tutorials/weather-meter-hookup-guide (guía de montaje)
* @see Documento de documentación
*
* @note *
* ================================================
* Pluviómetro
* ================================================
* Para la conexión, ver la documentación.
* Cada click del pluviómetro cierra un circuito. Se puede detectar empleando una interrupción en FALLING.
* Cada click equivale a 0.2794mm. *
*
*
* ================================================
* Anemómetro
* ================================================
* Para la conexión, ver la documentación. Misma que el pluviómetro.
* Cada revolución del anemómetro cierra un circuito. Se puede detectar empleando una interrupción en FALLING.
* Cada revolución equivale a 2.4 km/h.
* *Observación:* No es cada revolución, creo que es cada 1/3 de rev. Pero da igual, cada interrupción es un click.
*
*
* ================================================
* Veleta
* ================================================
* Para la conexión, ver la documentación.
* Usaremos un divisor de tensión para medir el voltaje en el pin analógico, que variará en función de la resistencia de la veleta.
*
* Usando una resistencia de 10K, nuestra lectura en el sensor analógico será:
* 1023 * (R/(10000+R))
* donde R es la resistencia desconocida de la veleta.
*
+5V
|
<
> 10K
< Resistor
<
>
|
Analog Pin ------|
|
-----------| To weather vane
| (mystery resistance)
-----------|
|
|
-----
---
-
*
* ================================================
* Sensor Temperatura Cuadro de Placas (LM35)
* ================================================
* Lee 10mV/ºC. Por tanto, para obtener la temperatura en ºC:
* temp = (voltage * 100)
* Como Arduino solo puede leer de 0 a 5V, la temperatura estará limitada a temperaturas positivas.
*
*/
#include <DHT.h>
#include <SPI.h>
#include <Ethernet.h>
#include <ArduinoModbus.h>
#define PIN_ANEMOMETRO 2
#define PIN_PLUVIOMETRO 3
#define PIN_VELETA A0
#define PIN_TEMP_CUADRO_PLACAS A1
#define PIN_DHT22_EXTERIOR 5
#define PIN_RELAY_ABEJAS 6
#define PIN_VOLATE_BATERIA A2
// Configuración ethernet y modbus
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEF };
IPAddress ip(192, 168, 0, 152);
EthernetServer ethernetServer(502); // Puerto Modbus TCP
ModbusTCPServer modbusTCPServer; // Servidor Modbus
// Sensor DHT22
#define DHT_EXT_TYPE DHT22
DHT dht_exterior(PIN_DHT22_EXTERIOR, DHT_EXT_TYPE);
float humExterior = 0;
float tempExterior = 0;
// Sensor LM35
#define GRADOS_POR_VOLTIO 100
float tempCuadroPlacas = 0;
// Frecuencia de las mediciones (segundos)
#define FREC_MEDICION 3
unsigned long nextMedicion = 0;
unsigned long tiempoActual = 0;
// Variables para el pluviómetro y el anemómetro. Modificadas por las interrupciones.
volatile unsigned long numClicksPluviometro = 0;
volatile unsigned long numClicksAnemometro = 0;
#define DEBOUNCE_TIME_PLUVIOMETRO 100000UL // Debounce time en microsegundos para contar sólo un click. Para evitar ruido.
#define DEBOUNCE_TIME_ANEMOMETRO 30
volatile unsigned long last_pluviometro = 0; // Último tiempo en el que se contó un click.
volatile unsigned long last_anemometro = 0;
#define MM_POR_CLICK 0.2794
#define KMH_POR_CLICK 2.4
float velViento = 0;
bool lluviaDetectada = false;
// Variables para la veleta
// Correspondencia de dirección del viento con ángulo.
#define NUM_DIR_POSIBLES 16
const float dirAng[NUM_DIR_POSIBLES] = {112.5,67.5,90,157.5,135,202.5,180,22.5,45,247.5,225,337.5,0,292.5,315,270};
const char* dirCard[NUM_DIR_POSIBLES] = {"ESE","ENE","E","SSE","SE","SSW","S","NNE","NE","WSW","SW","NNW","N","WNW","NW","W"};
const float correccion = 200.0;
// Cada una de las 16 direcciones tiene una resistencia asociada (ver documentación).
// Usando un divisor de tensión, para cada valor de resistencia, tenemos un valor de tensión asociado.
// En nuestro caso (Vin=Vout=5V, R1=10K), la tensión en el pin analógico se puede ver en la documentación.
// Convertiendo dichas lecturas de V a valores de 0 a 1023 (que es lo que lee el Arduino), tenemos los siguientes valores.
const int sensorVal[NUM_DIR_POSIBLES] = {66,84,93,126,184,244,287,406,461,599,630,702,785,827,886,945};
// Dándole un rango a cada dirección, podemos saber la dirección del viento.
const int sensorMin[NUM_DIR_POSIBLES] = {63,80,89,120,175,232,273,385,438,569,613,667,746,812,869,931};
const int sensorMax[NUM_DIR_POSIBLES] = {69,88,98,133,194,257,301,426,484,612,661,737,811,868,930,993};
// Valores de la veleta
float angulo = 0;
const char* dir = "TMP";
// Variable para hacer medias
#define NUM_LECTURAS 20
// Acumulaciones horarias de clicks.
// Usa un array circular que almacena el acumulado de clicks en la última hora.
// Un índice apunta al valor más antiguo, que se va sobreescribiendo.
const int NUM_ACTUALIZACIONES_HORA = 60*60/FREC_MEDICION;
#define VENTANA_AISLADO 3
unsigned int clicks_por_medicion[NUM_ACTUALIZACIONES_HORA] = {0};
unsigned long acumulado_clicks_hora = 0; // Aunque es redundante, evitamos tener que sumar todo el array cada vez
unsigned int indice_actual = 0;
float lluvia_ultima_hora = 0;
// Variables para control de las abejas
const unsigned long MINUTOS_ENCENDIDO = 1;
const unsigned long MINUTOS_TRAS_ENCENDIDO = 2*60; // Apagado tras encendido
#define VOLTAJE_MINIMO_PLACA 12.0
#define TEMP_MINIMA_RELE 2.0 // Temperatura mínima para encender el relé. Para evitar congelación
unsigned long nextActivacionRele = 0; // Tiempo en el que se activará el relé
unsigned long nextApagadoRele = 0; // Tiempo en el que se apagará el relé. Se configura al encenderlo
float voltajeBateria = 0; // Voltaje de la batería. Se usa para comprobar si está baja
bool releEncendido = false; // Indica si el relé está encendido
bool bateriaBaja = false; // Indica si la batería está baja
bool tempMuyBaja = false; // Indica si la temperatura es muy baja para encender el relé
bool ordenEnciendoRele = false; // Indica si se ha dado la orden de encender el relé
bool ordenApagoRele = false; // Indica si se ha dado la orden de apagar el relé
float restanteEncendido = 0; // Segundos que aún queda encendido
float hastaSigMedicion = 0; // Segundos que aún quedan hasta la siguiente medición
/**
* @brief Función para controlar los desbordamientos de las variables de tiempo.
*
* Millis se desborda cada 50 días. Micros cada 70 minutos.
*
*/
void overflows(){
if (tiempoActual > millis()){ //Millis ha desbordado
tiempoActual = millis();
Serial.println("MILLIS se ha desbordado");
// Descartamos las mediciones anteriores
numClicksPluviometro = 0;
numClicksAnemometro = 0;
nextMedicion = 0;
nextActivacionRele = 0;
nextApagadoRele = 0;
}
if ((last_anemometro > micros()) || (last_pluviometro > micros())){ //Micros se ha desbordado
Serial.println("MICROS se ha desbordado");
// Este será el nuevo tiempo de la última medición. Si llueve mucho/hay mucho viento, podrá haber pequeños errores.
last_anemometro = 0;
last_pluviometro = 0;
}
}
/**
* @brief Interrupción para el pluviómetro. Llamada cada vez que hay un click.
*/
void intrPluviometro(){
if((micros() - last_pluviometro) >= DEBOUNCE_TIME_PLUVIOMETRO) {
numClicksPluviometro++;
last_pluviometro = micros();
}
}
/**
* @brief Interrupción para el anemómetro. Llamada cada vez que hay un click.
*/
void intrAnemometro(){
if((micros() - last_anemometro) >= DEBOUNCE_TIME_ANEMOMETRO) {
numClicksAnemometro++;
last_anemometro = micros();
}
}
/**
* @brief Obtiene la velocidad del viento, reseteando el contador de clicks.
*
* @return float Velocidad del viento en km/h.
*/
float getVelocidadViento(){
float revs_por_segundo = ((float)numClicksAnemometro) / FREC_MEDICION;
numClicksAnemometro = 0;
return revs_por_segundo * KMH_POR_CLICK;
}
/**
* @brief Obtiene la cantidad de lluvia en la última hora.
*
* @return float Cantidad de lluvia en mm en la última hora.
*
* @post El contador de clicks de la interrupción del pluviómetro se resetea.
*/
float actualizaLluviaHoraria(){
// x x x o x x x [ ] Ahora estoy en [ ]. Si todas las x son 0 y o es positivo, han sido aislados; los descartamos
int centro_ventana = (indice_actual - VENTANA_AISLADO - 1 + NUM_ACTUALIZACIONES_HORA) % NUM_ACTUALIZACIONES_HORA; // Índice del centro de la ventana de aislamiento
if (centro_ventana > 0){
// Comprobamos si los valores de la ventana de aislamiento son todos 0
bool aislado = true;
int izq = centro_ventana;
int der = centro_ventana;
for (int i=0; i<VENTANA_AISLADO && aislado; i++){
izq = (izq - 1 + NUM_ACTUALIZACIONES_HORA) % NUM_ACTUALIZACIONES_HORA; // Índice a la izquierda
der = (der + 1) % NUM_ACTUALIZACIONES_HORA;
aislado &= (clicks_por_medicion[izq] == 0) && (clicks_por_medicion[der] == 0);
}
if (aislado){
acumulado_clicks_hora -= clicks_por_medicion[centro_ventana]; // Si todos los valores de la ventana son 0, descartamos el valor del centro
clicks_por_medicion[centro_ventana] = 0; // Reseteamos el valor del centro
}
}
// Actualizamos el acumulado. Sumamos los nuevos clicks y restamos los más antiguos.
acumulado_clicks_hora += numClicksPluviometro;
acumulado_clicks_hora -= clicks_por_medicion[indice_actual];
// Actualizamos el array y el índice
clicks_por_medicion[indice_actual] = numClicksPluviometro;
numClicksPluviometro = 0;
indice_actual = (indice_actual + 1) % NUM_ACTUALIZACIONES_HORA;
return acumulado_clicks_hora * MM_POR_CLICK;
}
/**
* @brief Informa si ha llovido en el último intervalo de tiempo.
*
* @note Se considera que ha llovido si ha habido al menos un click en el pluviómetro.
*
* @return true Ha llovido.
* @return false No ha llovido.
*/
bool haLlovido(){
return numClicksPluviometro > 0;
}
/**
* @brief Obtiene la dirección del viento.
*
* @return float Angulo del viento en grados (0-360).
*/
float getDireccionViento(){
float angulo = 0;
for (int i=0; i<NUM_LECTURAS; i++) {
int value = analogRead(PIN_VELETA);
for (int j=0; j<NUM_DIR_POSIBLES; j++) {
if(sensorMin[j] <= value && value <= sensorMax[j]) {
angulo += dirAng[j];
break;
}
}
}
angulo /= NUM_LECTURAS; // Media de las lecturas
angulo = (int)(angulo + correccion) % 360; // Ajustamos
return angulo;
}
/**
* @brief Obtiene la temperatura del interior del Cuadro de Placas.
*
* @return float Temperatura en ºC.
*/
float getTemperaturaCuadroPlacas(){
int sensorValue = 0;
for (int i=0; i<NUM_LECTURAS; ++i){
sensorValue += analogRead(PIN_TEMP_CUADRO_PLACAS);
}
sensorValue /= NUM_LECTURAS;
float voltage = sensorValue * (5.0 / 1023.0);
float temp = voltage * GRADOS_POR_VOLTIO;
return temp;
}
#define VOLTAJE_MAX 25.0
/**
* @brief Obtiene el voltaje de la batería.
*
* @return float Voltaje de la batería en V.
*
* @note Se calcula como media de 20 lecturas del pin analógico.
*/
float getVoltajeBateria(){
float valor = 0; // Calculamos la media de 20 lecturas
for (int i=0; i<NUM_LECTURAS; i++)
valor += analogRead(PIN_VOLATE_BATERIA);
valor /= NUM_LECTURAS;
float voltaje = valor * (VOLTAJE_MAX / 1023.0);
#ifdef DEBUG_VOLTAJE_PLACA
Serial.print("Voltaje Placa: ");
Serial.println(voltaje);
#endif
return voltaje;
}
/**
* @brief Indica si la batería está baja.
*
* @return true La batería está baja.
* @return false La batería no está baja.
*/
bool bateriaEstaBaja(){
return voltajeBateria < VOLTAJE_MINIMO_PLACA;
}
/**
* @brief Indica si la temperatura exterior es muy baja.
*
* @return true La temperatura exterior es muy baja.
* @return false La temperatura exterior no es muy baja.
*/
bool temperaturaEstaMuyBaja(){
return tempExterior < TEMP_MINIMA_RELE;
}
//=======================================================
// Setup
//=======================================================
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
dht_exterior.begin();
pinMode(PIN_ANEMOMETRO, INPUT);
pinMode(PIN_PLUVIOMETRO, INPUT);
attachInterrupt(digitalPinToInterrupt(PIN_ANEMOMETRO), intrAnemometro, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_PLUVIOMETRO), intrPluviometro, FALLING);
pinMode(PIN_RELAY_ABEJAS, OUTPUT);
// Aseguramos que clicks_por_medicion está inicializado a 0
for(int i=0; i<NUM_ACTUALIZACIONES_HORA; i++)
clicks_por_medicion[i] = 0;
// Inicializamos el Ethernet
Ethernet.begin(mac, ip);
if (Ethernet.hardwareStatus() == EthernetNoHardware)
Serial.println("Ethernet shield was not found.");
else if (Ethernet.hardwareStatus() == EthernetW5100)
Serial.println("W5100 Ethernet controller detected.");
else if (Ethernet.hardwareStatus() == EthernetW5200)
Serial.println("W5200 Ethernet controller detected.");
else if (Ethernet.hardwareStatus() == EthernetW5500)
Serial.println("W5500 Ethernet controller detected.");
ethernetServer.begin(); // start listening for clients
modbusTCPServer.begin(); // start listening for clients
// Configuramos los registros del servidor Modbus
modbusTCPServer.configureHoldingRegisters(0, 100); // Desde la dirección 0, ocupa 100 registros
modbusTCPServer.configureInputRegisters(0, 100);
modbusTCPServer.configureCoils(0, 100);
}
/**
* @brief Funcionalidad del Arduino de las placas.
*
*/
void funcionalidad(){
delay(50); // Para evitar errores en el overflow
overflows(); // Control de desbordamientos
tiempoActual = millis(); // Tiempo común para toda la medición
// Comprobación para la medición
if(tiempoActual > nextMedicion) { // Medimos cada FREC_MEDICION segundos
nextMedicion = tiempoActual + (FREC_MEDICION*1000UL);
velViento = getVelocidadViento();
lluviaDetectada = haLlovido();
angulo = getDireccionViento();
tempCuadroPlacas = getTemperaturaCuadroPlacas();
tempExterior = dht_exterior.readTemperature();
humExterior = dht_exterior.readHumidity();
lluvia_ultima_hora = actualizaLluviaHoraria();
}
// Comprobación del estado de la batería
voltajeBateria = getVoltajeBateria();
bateriaBaja = bateriaEstaBaja();
tempMuyBaja = temperaturaEstaMuyBaja();
bool hayPeligroRele = bateriaBaja || tempMuyBaja; // Si la batería está baja o la temperatura es muy baja, apagamos el relé
// Control del relé de las abejas
if (!releEncendido && !hayPeligroRele && (ordenEnciendoRele || (tiempoActual > nextActivacionRele))) {
digitalWrite(PIN_RELAY_ABEJAS, HIGH);
releEncendido = true;
nextApagadoRele = tiempoActual + (MINUTOS_ENCENDIDO * 60 * 1000UL);
}
else if (releEncendido && (hayPeligroRele || ordenApagoRele || (tiempoActual > nextApagadoRele))) {
digitalWrite(PIN_RELAY_ABEJAS, LOW);
releEncendido = false;
nextActivacionRele = tiempoActual + (MINUTOS_TRAS_ENCENDIDO * 60 * 1000UL);
}
if (releEncendido){
restanteEncendido = (nextApagadoRele - tiempoActual)/1e3;
hastaSigMedicion = 0;
}else{
restanteEncendido = 0;
hastaSigMedicion = (nextActivacionRele - tiempoActual)/1e3;
}
}
/**
* @brief Comunicación por Serial. Muestra los datos de la estación meteorológica.
*
* @note Esta función se usa para depuración y no es necesaria para el funcionamiento del sistema.
*/
void comunicacionSerial(){
Serial.print("Velocidad viento: ");
Serial.print(velViento);
Serial.println(" km/h");
Serial.print("Temperatura exterior: ");
Serial.print(tempExterior);
Serial.println(" ºC");
Serial.print("Humedad exterior: ");
Serial.print(humExterior);
Serial.println(" %");
Serial.print("Temperatura cuadro placas: ");
Serial.print(tempCuadroPlacas);
Serial.println(" ºC");
Serial.print("Lluvia última hora: ");
Serial.print(lluvia_ultima_hora);
Serial.println(" mm");
Serial.print("Lluvia detectada: ");
if (lluviaDetectada)
Serial.println("Sí");
else
Serial.println("No");
getDireccionViento();
// Mostramos la dirección del viento
if (angulo >= 0) {
Serial.print("Dirección del viento: ");
Serial.println(angulo);
}
else {
Serial.println("Dirección del viento no disponible");
}
if (releEncendido)
Serial.println("Rele encendido");
else{
Serial.println("Rele apagado");
}
Serial.print("Voltaje batería: ");
Serial.print(voltajeBateria);
Serial.println(" V");
Serial.print("Batería baja: ");
if (bateriaBaja)
Serial.println("Sí");
else
Serial.println("No");
Serial.print("Temperatura muy baja: ");
if (tempMuyBaja)
Serial.println("Sí");
else
Serial.println("No");
Serial.print("Restante Encendido: ");
Serial.print(restanteEncendido);
Serial.println(" s");
Serial.print("HastaSigMedicion: ");
Serial.print(hastaSigMedicion);
Serial.println(" s");
Serial.println("------------------------------------------------\n");
}
#define FACTOR 1e2 // Número de decimales. Por Modbus, solo se envían enteros. La pantalla ajustará los decimales.
/**
* @brief Comunicación Modbus con la pantalla.
*/
void comunicacionModbus(){
modbusTCPServer.holdingRegisterWrite(0x00, velViento * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x01, tempExterior * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x02, humExterior * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x03, tempCuadroPlacas * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x04, lluvia_ultima_hora * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x05, angulo * FACTOR);
modbusTCPServer.holdingRegisterWrite(0x06, restanteEncendido * FACTOR); // Tiempo restante para el apagado
modbusTCPServer.holdingRegisterWrite(0x07, hastaSigMedicion * FACTOR); // Tiempo restante para la siguiente medición
modbusTCPServer.holdingRegisterWrite(0x08, voltajeBateria * FACTOR);
modbusTCPServer.coilWrite(0x00, lluviaDetectada);
modbusTCPServer.coilWrite(0x01, bateriaBaja);
modbusTCPServer.coilWrite(0x02, releEncendido);
modbusTCPServer.coilWrite(0x03, tempMuyBaja);
ordenEnciendoRele = modbusTCPServer.coilRead(0x04);
ordenApagoRele = modbusTCPServer.coilRead(0x05);
// Actualizamos los valores de las órdenes de encendido o apagado del relé
if (releEncendido && ordenEnciendoRele) {
ordenEnciendoRele = false;
modbusTCPServer.coilWrite(0x04, false);
}
else if (!releEncendido && ordenApagoRele) {
ordenApagoRele = false;
modbusTCPServer.coilWrite(0x05, false);
}
}
//=======================================================
// Main loop
//=======================================================
void loop() {
funcionalidad();
#ifdef DEBUG_SERIAL
comunicacionSerial();
#endif
comunicacionModbus();
// Esperamos a un cliente
EthernetClient client = ethernetServer.available();
// Cuando se conecta un cliente, comenzamos
if (client) {
Serial.println("Nuevo cliente");
// Aceptamos la conexión
modbusTCPServer.accept(client);
while (client.connected()) {
if (client.available()) {
// Procesamos la petición
modbusTCPServer.poll();
}
// Actualizamos los datos
funcionalidad();
#ifdef DEBUG_SERIAL
comunicacionSerial();
#endif
comunicacionModbus();
}
Serial.println("Cliente desconectado");
}
}
I know the code is quite big, but I hope it his clear and organized in different functions.