sweet_love (auto-cultivo con Arduino) ¡¡NUEVA VERSIÓN!!

alejokaban

Crecimiento
17 Febrero 2013
92
66
23
Hola!

Decidí crear un nuevo tema pues hay mucha información y material nuevo. Primero un breve resumen: Este proyecto comenzó en el 2015, cuando @Zion Lion presentó a la comunidad un proyecto llamado EasyGrowWeed. @Zion Lion me envió desde España (hasta Colombia) todas las partes para que yo le ayudara en su desarrollo, desafortunadamente caí en un bloqueo creativo (perdón Zion!!) y pasaron unos años hasta que en el 2019 presenté un proyecto llamado sweet_love v0.11, el cual estaba inspirado en el trabajo de @Zion Lion pero estaba escrito desde cero, usando librerías más actualizadas. Se trata de un sistema embebido de control para el cuidado de cultivos diseñado para ser implementado en el Arduino Mega 2560. Luego, cuando estaba incluyendo el feature de acceso al sistema a través de una red, me di cuenta que dicha versión tenía muchas limitaciones, especialmente porque el flujo del programa estaba basado en una librería llamada SoftTimer.h. Entonces tomé la decisión de escribir nuevamente el programa desde cero y utilizar una técnica llamada "máquinas de estados", la cual permite realizar diferentes tareas de manera "simultanea".

En el camino y debido a fallas en el sistema tuve que reemplazar el Arduino Mega chino por un Arduino italiano original, el cual recomiendo 100%. También reemplacé el módulo LCD por uno I2C y uno de los relés mecánicos (el que controla la lámpara) por un módulo relé de estado sólido SSR (este cambio también es MUY importante). Omití la fuente switcheada + regulador de protoboard que alimentaban todo el sistema y los reemplacé por un adaptador de voltaje de 9v + un regulador LM7805 (con disipador de calor!). Otro cambio es que el módulo de relés mecánicos ahora lleva su propio adaptador de voltaje de 5v pues si se conecta a los 5v del LM7805 "jala" mucha corriente y se nota en el contraste del LCD. El Arduino lo alimento por el conector USB.

También tuve que reemplazar el módulo de relés y el sensor de humedad por partes nuevas pues las viejas se dañaron.

Entonces la lista de partes es la siguiente:

Arduino MEGA 2560 Rev3 Original de Italia
Arduino-MEGA-2560-Rev3-Original-de-Italia-1.jpg

Regulador de voltaje LM7805 (no encontré imagen con disipador!!)
lm7805-voltage-regulator.jpg

Módulo LCD 4x20 I2C
voltaat-4x20-lcd-with-i2c-module-14298842071142_1024x1024.jpg

Reloj de tiempo real RTC DS3232
real time clock DS3232.jpg

Sensor de humedad en suelo FC-28
Soil-Hygrometer-Detection-Module-Moisture-Sensor-FC-28-DC-3-3V-5V.jpg

Sensor de temperatura y humedad en aire DHT22
(Omití la imagen porque sólo puedo publicar 10 imágenes!!)

Módulo de relés mecánicos opto-aislado 5v 10A
Keyes MD-002 - Módulo de relé.jpg

Módulo de relé de estado solido SSR
rele_de_estado_solido_ssr_1_canal.jpg

Mini bomba de agua 12v 3.6W
(Omití la imagen porque sólo puedo publicar 10 imágenes!!)


Este es el diagrama eléctrico actualizado realizado en Fritzing. Ya incluye el LCD I2C, el relé de estado sólido, la alimentación independiente del relé mecánico, el bus de I2C con resistencias de 10k a +V y todas las conexiones al Arduino Mega para que funcione correctamente con el programa.

sweet_love v0.29 updated_bb.jpg

Aquí dejo un link con una imagen de mejor calidad.


Así luce el montaje de sweet_love v0.29. Foto tomada el mismo día de la publicación del tema:

montaje sweet_love 12DIC23.jpg

Recomendación:
Si piensan hacer el montaje de este proyecto usando cables viejos, hay que revisar que las puntas estén 100% limpias. En mi caso, el montaje presentaba intermitencia y fallas, así que opté por desarmar todo, armarlo de nuevo y limpiar las puntas de los cables con un bisturí pues tenían oxido o simplemente estaban opacas. La diferencia en el funcionamiento se nota MUCHO, por ejm en el contraste del LCD.

Este es el código:

C++:
/*  PROYECTO: Sistema de control para el cuidado de cultivos
    NOMBRE: sweet_love v0.2935
    CARACTERISTICAS:
      - Diseñado para el Board Arduino Mega 2560
      - Push Buttons en el Loop
      - LCD por I2C en state machine
      - Reloj RTC DS3232 en LCD
      - Sensores DHT22 y HL28 en ctrl state machine
      - Control manual de relays para lámpara y bomba
      - Control automático de bomba y lámpara (foto-periodo)
      - Configuración del foto-periodo: vegetativo o floración
      - Servidor web en Loop:
        - Lectura de sensores (boton para actualizar)
        - Acceso al control manual y automático
        - Configuración del foto-periodo

    Escrito hasta Diciembre de 2023
    Por Alejandro Bermúdez Ospina
    (alejandro.bermudez.ospina@gmail.com)*/
//========================================

// ----------LIBRARIES--------------
#include <ezButton.h>   // https://github.com/alexokaban/button
#include <Wire.h>
#include <hd44780.h>    // https://github.com/alexokaban/hd44780
#include <hd44780ioClass/hd44780_pinIO.h>   // Arduino pin i/o class header
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header
#include <DS3232RTC.h>    // https://github.com/alexokaban/DS3232RTC
#include <DHT.h>    // https://github.com/alexokaban/DHT-sensor-library
#include <DHT_U.h>
#include <Streaming.h>
#include <Ethernet.h>
#include <SPI.h>

// --------CONSTANTS (won't change)---------------

ezButton button1(2);  // create ezButton object that attach to pin 6;
ezButton button2(3);  // create ezButton object that attach to pin 7;
ezButton button3(5);  // create ezButton object that attach to pin 8;

#define HL28_pin A0    // analog pin to connect the soil moisture sensor
#define DHT_pin A1    // entrada pin análogo al sensor DHT22
#define SQW_pin 19     // señal de interrupción para las alarmas del RTC - // connect this pin to DS3231 INT/SQW pin.

const int lamp = 8;   // pin de salida digital al relé que controla la lampára
const int plump = 9;    // pin de salida al relé que controla la bomba de agua

#define DHT_type DHT22
DHT dht(DHT_pin, DHT_type);
hd44780_I2Cexp lcd;

// LCD geometry
const int LCD_COLS = 20;    // columnas
const int LCD_ROWS = 4;     // filas

uint8_t degree_char[8]  = {0x18, 0x18, 0x00, 0x07, 0x08, 0x08, 0x08, 0x07}; // caracter de grado
uint8_t arrow_char[8]  = {0x00, 0x00, 0x04, 0x06, 0x1F, 0x06, 0x04, 0x00}; // flecha
uint8_t mark_char[8] = {0x00, 0x00, 0x01, 0x02, 0x14, 0x08, 0x00, 0x00}; // marca

const int lcd_interval = 1000;     // tiempo de actualización
const int DHT_interval = 10000;    // tiempo entre lecturas

const int HL28_dry_soil = 950;    // constante de sustrato seco para activar la bomba de agua
const int HL28_wet_soil = 430;    // constante de sustrato humedo para detener la bomba de agua

const byte alarm1_hour = 6;     // hora de inicio del fotoperiodo (fija)
const byte alarm1_minute = 0;
const byte alarm1_second = 0;
const byte alarm1_day = 0;      //

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x03 };//dirección MAC de la Ethernet Shield que está con una etiqueta debajo la placa
IPAddress ip(192, 168, 1, 115); //Asignamos la IP privada al ethernet shield
EthernetServer server(80); //Creamos un servidor Web con el puerto 80 que es el puerto HTTP por defecto

//------------ VARIABLES (will change)---------------------

// máquina de estados para el LCD
typedef enum {  STATE_NONE,
                STATE_LCD_START,
                STATE_LCD_SENSORS,
                STATE_LCD_C_MANUAL_LAMP,
                STATE_LCD_C_MANUAL_PLUMP,
                STATE_LCD_C_AUTO_LAMP,
                STATE_LCD_C_AUTO_PLUMP,
                STATE_LCD_FOTO_P_VEG,
                STATE_LCD_FOTO_P_FLO,
                STATE_LCD_TELE_CONNECT,
                STATE_LCD_AUTO_TELE,
             } states_lcd;

states_lcd state_lcd = STATE_LCD_START;   // current state-machine state

// máquina de estados para los controles y sensores
typedef enum {  STATE_CTRL_NONE,
                STATE_CTRL_DHT_READ,
                STATE_CTRL_HL28_SREAD,
                STATE_CTRL_HL28_LREAD,
                STATE_CTRL_LAMP_OFF,
                STATE_CTRL_LAMP_ON,
                STATE_CTRL_PLUMP_OFF,
                STATE_CTRL_PLUMP_ON
             } states_ctrl;

// current state-machine state
states_ctrl state_ctrl = STATE_CTRL_NONE;

//máquina de estados para el último comando ejecutado
typedef enum {  LAST_CMD_NONE,
                LAST_CMD_PHOTO_STARTS,
                LAST_CMD_PHOTO_ENDS,
                LAST_CMD_HUM_LOW_PLUMP_ON,
                LAST_CMD_HUM_HIGH_PLUMP_OFF,
             } last_cmds;

volatile last_cmds last_cmd = LAST_CMD_NONE;    // current state-machine state
bool tele_lastcall_f = false;

// for temperature & humidity sensor DHT22
volatile float DHT_airhumidity;   // variables for saving sensors's values
volatile float DHT_temperature;

// for soil moisture sensor HL28
volatile bool HL28_get = false;   // flag to get the reading of moisture in the soil
volatile int HL28_maped_value;    // variable para guardar el valor mapeado
volatile int HL28_analog_value;   // variable para guardar el valor leído por el sensor HL-28
volatile bool HL28_pump_on = false;  // al prenderse la bomba cambia el valor de HL28_interval

// for controls and sensors
volatile bool c_manual_lamp_ON = false;   // bandera de estado para el control manual de la lampára
volatile bool c_manual_plump_ON = false;    // bandera de estado de para el control manual la bomba de agua
volatile bool c_auto_lamp_ON = false;   // bandera de estado para el control automático de la lámpara
volatile bool c_auto_plump_ON = false;    // bandera de estado para el control automático de la bomba de agua
volatile bool foto_p_veg = false;    // bandera que indica el foto-periodo true = vegetativo, false = florecimiento

// para los push buttons
int button_pressed = 0;

// for the timming
int msg_delay = 0;      // contador de segundos para mensaje en 4ta línea del LCD
volatile bool msg_delay_on = false;    // bandera que indica mensaje en la 4ta linea del LCD
int HL28_interval = 20000;   // tiempo entre lecturas largas del HL28
unsigned long currentMillis = 0;    // stores the value of millis() in each iteration of loop()
unsigned long previous_lcd_millis = 0;   // will store last times
unsigned long previous_HL28_millis = 0;
unsigned long previous_DHT_millis = 0;

// para el RTC DS3232
volatile boolean RTC_alarm_call_ON = false;

volatile byte alarm2_hour = 18;     // hora de finalización del fotoperiodo (variable), por defecto florecimiento
volatile byte alarm2_minute = 0;
volatile byte alarm2_second = 0;
volatile byte alarm2_day = 0;

void setup() {

  delay(100);
  // inicializa puerto serial, push buttons y pin 13 (built in LED)
  Serial.begin(9600);
  printDateTime(RTC.get());
  Serial << " --> Current RTC time\n";

  button1.setDebounceTime(50); // set debounce time to 50 milliseconds
  button2.setDebounceTime(50); // set debounce time to 50 milliseconds
  button3.setDebounceTime(50); // set debounce time to 50 milliseconds

  pinMode(13, OUTPUT);     //nn
  digitalWrite(13, LOW);

  // inicializa el LCD
  Wire.begin();
  lcd.begin(LCD_COLS, LCD_ROWS);
  lcd.backlight();    // turn on backlight
  lcd.createChar(0, degree_char);   // crea caracter de grado
  lcd.createChar(1, arrow_char);    // .. flecha
  lcd.createChar(2, mark_char);    // .. marca

  // inicializa controles
  pinMode(lamp, OUTPUT);     // initialize digital pin 50 as an relay output.
  digitalWrite(lamp, HIGH);    // inicializa la lámpara apagada, -> LÓGICA INVERSA

  pinMode(plump, OUTPUT);     // initialize digital pin 52 as an relay output.
  digitalWrite(plump, LOW);   // inicializa la bomba apagada

  // inicializa sensores
  pinMode(HL28_pin, INPUT);
  pinMode(DHT_pin, INPUT);
  dht.begin();      // inicializa el objeto dht
  sensor_update();

  // inicializa RTC y alarmas
  //RTC.set(compileTime()); // ejecutar una sola vez para cargar el tiempo en el RTC

  // setSyncProvider() causes the Time library to synchronize with the
  // external RTC by calling RTC.get() every five minutes by default.
  setSyncProvider(RTC.get);   // the function to get the time from the RTC

  RTC.squareWave(SQWAVE_NONE);      // desactiva la onda cuadrada del RTC

  // las alarmas 1 y 2 del RTC se utilizan para controlar el fotoperiodo. ALARM_1 marca el inicio del ciclo y ALARM_2 el final
  // sintaxis: setAlarm(ALARM_TYPES_t alarmType, byte seconds, byte minutes, byte hours, byte daydate)

  RTC.setAlarm(ALM1_MATCH_HOURS, alarm1_second, alarm1_minute, alarm1_hour, alarm1_day);
  RTC.alarm(ALARM_1);
  RTC.alarmInterrupt(ALARM_1, true);

  RTC.setAlarm(ALM2_MATCH_HOURS, alarm2_second, alarm2_minute, alarm2_hour, alarm2_day);
  RTC.alarm(ALARM_2);
  RTC.alarmInterrupt(ALARM_2, true);

  pinMode(SQW_pin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SQW_pin), RTC_alarm_call, FALLING);

  process_lcd_machine();

  Ethernet.begin(mac, ip);// Inicializamos la comunicación Ethernet y el servidor
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());//Mostramos en el monitor serie la ip del ethernet shield
}

void loop() {

  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  read_buttons();   //
  process_buttons();    //

  currentMillis = millis();

  lcd_check();    //
  HL28_check();   //
  DHT_check();    //
  auto_lamp_check();    //
  lcd_last_cmd(last_cmd);   //
  internet_task();    //
}

void sensor_update() {  // función para actualizar los sensores de húmedad y temperatura
  HL28_analog_value = analogRead(HL28_pin);     // realiza una lectura análoga del pin conectado al sensor HL-28
  HL28_maped_value = map(HL28_analog_value, HL28_wet_soil, HL28_dry_soil, 100, 0);     // ubica este valor leído entre un mínimo y un máximo
  DHT_airhumidity = dht.readHumidity();     //  lee y almacena el valor de la humedad en el aire
  DHT_temperature = dht.readTemperature();      // ... de la temperatura
}

void read_buttons() {

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  if (button1.isPressed()) {
    button_pressed = 1;
    last_cmd = LAST_CMD_NONE;
  }

  if (button2.isPressed()) {
    button_pressed = 2;
    last_cmd = LAST_CMD_NONE;
  }

  if (button3.isPressed()) {
    button_pressed = 3;
    last_cmd = LAST_CMD_NONE;
  }
}

void process_buttons() {
  switch (button_pressed) {
    case 1:   // pantalla

      button_pressed = 0;

      lcd.setCursor(0, 3);
      lcd.print("> BUTTON PRESSED: 1");

      switch (state_lcd)  {
        case STATE_LCD_START:
          state_lcd = STATE_LCD_SENSORS;
          break;

        case STATE_LCD_SENSORS:
          state_lcd = STATE_LCD_C_MANUAL_LAMP;
          break;

        case STATE_LCD_C_MANUAL_LAMP:
          state_lcd = STATE_LCD_C_MANUAL_PLUMP;
          break;

        case STATE_LCD_C_MANUAL_PLUMP:
          state_lcd = STATE_LCD_C_AUTO_LAMP;
          break;

        case STATE_LCD_C_AUTO_LAMP:
          state_lcd = STATE_LCD_C_AUTO_PLUMP;
          break;

        case STATE_LCD_C_AUTO_PLUMP:
          state_lcd = STATE_LCD_FOTO_P_VEG;
          break;

        case STATE_LCD_FOTO_P_VEG:
          state_lcd = STATE_LCD_FOTO_P_FLO;
          break;

        case STATE_LCD_FOTO_P_FLO:
          state_lcd = STATE_LCD_START;
          break;
      }
      break;

    case 2:

      button_pressed = 0;

      lcd.setCursor(0, 3);
      lcd.print("> BUTTON PRESSED: 2");

      switch (state_lcd)  {
        case STATE_LCD_START:
          state_lcd = STATE_LCD_FOTO_P_FLO;
          break;

        case STATE_LCD_SENSORS:
          state_lcd = STATE_LCD_START;
          break;

        case STATE_LCD_C_MANUAL_LAMP:
          state_lcd = STATE_LCD_SENSORS;
          break;

        case STATE_LCD_C_MANUAL_PLUMP:
          state_lcd = STATE_LCD_C_MANUAL_LAMP;
          break;

        case STATE_LCD_C_AUTO_LAMP:
          state_lcd = STATE_LCD_C_MANUAL_PLUMP;
          break;

        case STATE_LCD_C_AUTO_PLUMP:
          state_lcd = STATE_LCD_C_AUTO_LAMP;
          break;

        case STATE_LCD_FOTO_P_VEG:
          state_lcd = STATE_LCD_C_AUTO_PLUMP;
          break;

        case STATE_LCD_FOTO_P_FLO:
          state_lcd = STATE_LCD_FOTO_P_VEG;
          break;
      }
      break;

    case 3:

      button_pressed = 0;

      lcd.setCursor(0, 3);
      lcd.print("> BUTTON PRESSED: 3");

      switch (state_lcd)  {
        case STATE_LCD_C_MANUAL_LAMP:
          if (c_manual_lamp_ON == false)  {
            digitalWrite(lamp, LOW);
            c_manual_lamp_ON = true;
          }
          else {
            digitalWrite(lamp, HIGH);
            c_manual_lamp_ON = false;
          }
          break;

        case STATE_LCD_C_MANUAL_PLUMP:
          if (c_manual_plump_ON == false)  {
            digitalWrite(plump, HIGH);
            c_manual_plump_ON = true;
            HL28_interval = 1000;
          }
          else {
            digitalWrite(plump, LOW);
            c_manual_plump_ON = false;
            HL28_interval = 20000;
          }
          break;

        case STATE_LCD_C_AUTO_LAMP:
          if (c_auto_lamp_ON == true)  {
            c_auto_lamp_ON = false;
          }
          else {
            c_auto_lamp_ON = true;
            auto_lamp_first();
          }
          break;

        case STATE_LCD_C_AUTO_PLUMP:
          if (c_auto_plump_ON == true)  {
            c_auto_plump_ON = false;
          }
          else {
            c_auto_plump_ON = true;
            state_ctrl = STATE_CTRL_HL28_SREAD;
            process_ctrl_machine();
          }
          break;

        case STATE_LCD_FOTO_P_VEG:
          foto_p_veg = true;
          alarm2_hour = 24;
          if (c_auto_lamp_ON == true) {
            auto_lamp_first();
          }
          break;

        case STATE_LCD_FOTO_P_FLO:
          foto_p_veg = false;
          alarm2_hour = 18;
          if (c_auto_lamp_ON == true) {
            auto_lamp_first();
          }
          break;
      }
      break;
  }
}

void lcd_check()  {
  if (currentMillis - previous_lcd_millis >= lcd_interval) {
    previous_lcd_millis = currentMillis;
    process_lcd_machine();
    msg_delay_check();
  }
}

void process_lcd_machine() {
  switch (state_lcd) {
    case STATE_LCD_START:
      lcd.clear();

      lcd.setCursor(0, 0);
      lcd.print("  sweet_love v0.29  ");

      lcd.setCursor(5, 1);
      print_Digits(day());
      lcd.print('/');
      print_Digits(month());
      lcd.print('/');
      lcd.print(year());

      lcd.setCursor(6, 2);
      print_Digits(hour());
      lcd.print(':');
      print_Digits(minute());
      lcd.print(':');
      print_Digits(second());

      break;

    case STATE_LCD_SENSORS:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Temperatura");
      lcd.setCursor(12, 0);
      lcd.print(DHT_temperature);
      lcd.write(0);

      lcd.setCursor(0, 1);
      lcd.print("Humedad aire");
      lcd.setCursor(13, 1);
      lcd.print(DHT_airhumidity);
      lcd.print("%");

      lcd.setCursor(0, 2);
      lcd.print("Hum. sustrato");
      lcd.setCursor(14, 2);
      lcd.print(HL28_maped_value);
      lcd.print("%");
      break;

    case STATE_LCD_C_MANUAL_LAMP:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("   CONTROL MANUAL   ");

      lcd.setCursor(0, 1);
      lcd.write(1);

      lcd.setCursor(2, 1);
      if (c_manual_lamp_ON == false) {
        //if (lamp == LOW) {
        lcd.print("Prender lampara   ");
      }
      if (c_manual_lamp_ON == true) {
        //if (lamp == HIGH) {
        lcd.print("Apagar lampara   ");
      }

      lcd.setCursor(2, 2);
      if (c_manual_plump_ON == false) {
        //if (plump == LOW) {
        lcd.print("Prender bomba   ");
      }
      if (c_manual_plump_ON == true) {
        //if (plump == HIGH) {
        lcd.print("Apagar bomba   ");
      }
      break;

    case STATE_LCD_C_MANUAL_PLUMP:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("   CONTROL MANUAL   ");

      lcd.setCursor(2, 1);
      if (c_manual_lamp_ON == false) {
        lcd.print("Prender lampara   ");
      }
      if (c_manual_lamp_ON == true) {
        lcd.print("Apagar lampara   ");
      }

      lcd.setCursor(0, 2);
      lcd.write(1);

      lcd.setCursor(2, 2);
      if (c_manual_plump_ON == false) {
        lcd.print("Prender bomba   ");
      }
      if (c_manual_plump_ON == true) {
        lcd.print("Apagar bomba   ");
      }
      break;

    case STATE_LCD_C_AUTO_LAMP:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(" CONTROL AUTOMATICO ");

      lcd.setCursor(0, 1);
      lcd.write(1);

      lcd.setCursor(2, 1);
      if (c_auto_lamp_ON == true) {
        lcd.print("Auto-lampara ON   ");
      }
      if (c_auto_lamp_ON == false) {
        lcd.print("Auto-lampara OFF   ");
      }

      lcd.setCursor(2, 2);
      if (c_auto_plump_ON == true) {
        lcd.print("Auto-bomba ON   ");
      }
      if (c_auto_plump_ON == false) {
        lcd.print("Auto-bomba OFF   ");
      }
      break;

    case STATE_LCD_C_AUTO_PLUMP:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(" CONTROL AUTOMATICO ");

      lcd.setCursor(0, 2);
      lcd.write(1);

      lcd.setCursor(2, 1);
      if (c_auto_lamp_ON == true) {
        lcd.print("Auto-lampara ON   ");
      }
      if (c_auto_lamp_ON == false) {
        lcd.print("Auto-lampara OFF   ");
      }

      lcd.setCursor(2, 2);
      if (c_auto_plump_ON == true) {
        lcd.print("Auto-bomba ON   ");
      }
      if (c_auto_plump_ON == false) {
        lcd.print("Auto-bomba OFF   ");
      }
      break;

    case STATE_LCD_FOTO_P_VEG:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("    FOTO-PERIODO    ");

      lcd.setCursor(0, 1);
      lcd.write(1);

      lcd.setCursor(2, 1);
      lcd.print("Vegetativo 18/6   ");
      lcd.setCursor(2, 2);
      lcd.print("Florecim. 12/12   ");

      if (foto_p_veg == true) {
        lcd.setCursor(18, 1);
      }
      else {
        lcd.setCursor(18, 2);
      }
      lcd.write(2);
      break;

    case STATE_LCD_FOTO_P_FLO:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("    FOTO-PERIODO    ");

      lcd.setCursor(0, 2);
      lcd.write(1);

      lcd.setCursor(2, 1);
      lcd.print("Vegetativo 18/6   ");
      lcd.setCursor(2, 2);
      lcd.print("Florecim. 12/12   ");

      if (foto_p_veg == true) {
        lcd.setCursor(18, 1);
      }
      else {
        lcd.setCursor(18, 2);
      }
      lcd.write(2);
      break;
  }
}

void process_ctrl_machine() {   // procesa la máquina de estados que controla la lectura de sensores
  switch (state_ctrl) {
    case STATE_CTRL_HL28_SREAD:   // lectura con intervalo corto de humedad en sustrato
      HL28_analog_value = analogRead(HL28_pin);     // realiza una lectura análoga del pin conectado al sensor HL-28
      HL28_maped_value = map(HL28_analog_value, HL28_wet_soil, HL28_dry_soil, 100, 0);     // ubica este valor leído entre un mínimo y un máximo
      state_ctrl = STATE_CTRL_NONE;

      // para pruebas
      /*Serial.println(HL28_analog_value); //lectura analógica
        Serial.print(HL28_maped_value); //lectura mapeada
        Serial.println("%");
        Serial.print("\n");*/

      // función para comparar el valor analogo obtenido con las constantes de sustrato definidas al inicio
      if (c_auto_plump_ON == true) {    // sólo realiza la comparación si el control automático de la bomba de agua está activado
        if ((HL28_analog_value + 32) > HL28_dry_soil) {    // si el valor (+5% para evitar el caso de que nunca llegue a 0% de humedad) es mayor que sustrato seco, entonces prender bomba de agua
          //if (HL28_analog_value > HL28_dry_soil) {    // si el valor es mayor que sustrato seco, entonces prender bomba de agua
          digitalWrite(plump, HIGH);
          c_manual_plump_ON = true;
          last_cmd = LAST_CMD_HUM_LOW_PLUMP_ON;
          //if (HL28_pump_on == false ) {
          HL28_interval = 1000;
        }
        else if ((HL28_analog_value - 32) < HL28_wet_soil) {    // si el valor (-5% para evitar el caso de que nunca llegue a 100% de humedad) es mayor que sustrato seco, entonces prender bomba de agua
          //if (HL28_analog_value < HL28_wet_soil) {    // si el valor es menor que sustrato humedo, entonces apagar bomba de agua
          digitalWrite(plump, LOW);
          c_manual_plump_ON = false;
          last_cmd = LAST_CMD_HUM_HIGH_PLUMP_OFF;
          HL28_interval = 20000;
        }
      }
      break;

    case STATE_CTRL_DHT_READ:   // lectura de humedad en el aire y temperatura
      DHT_airhumidity = dht.readHumidity();     //  lee y almacena el valor de la humedad en el aire
      DHT_temperature = dht.readTemperature();      // ... de la temperatura
      state_ctrl = STATE_CTRL_NONE;
      break;
  }
}

void HL28_check()  {
  if (currentMillis - previous_HL28_millis >= HL28_interval) {
    previous_HL28_millis = currentMillis;
    state_ctrl = STATE_CTRL_HL28_SREAD;
    process_ctrl_machine();
  }
}

void DHT_check()  {
  if (currentMillis - previous_DHT_millis >= DHT_interval) {
    previous_DHT_millis = currentMillis;
    state_ctrl = STATE_CTRL_DHT_READ;
    process_ctrl_machine();
  }
}

void auto_lamp_check()  {

  if (RTC_alarm_call_ON == true && c_auto_lamp_ON == true ) {
    if (RTC.alarm(ALARM_1)) {     // si la ALARM_1 se activa indica el inicio del ciclo
      printDateTime( RTC.get() );
      Serial << " --> Alarm 1\n";
      digitalWrite(lamp, LOW);     // ... entonces prende la lámpara
      c_manual_lamp_ON = true;
      last_cmd = LAST_CMD_PHOTO_STARTS;

    }
    if (RTC.alarm(ALARM_2)) {     // si la ALARM_2 se activa indica el final del ciclo
      printDateTime( RTC.get() );
      Serial << " --> Alarm 2\n";
      digitalWrite(lamp, HIGH);     // ... entonces apaga la lámpara
      c_manual_lamp_ON = false;
      last_cmd = LAST_CMD_PHOTO_ENDS;
    }
    RTC_alarm_call_ON = false;      // limpia la bandera de llamado por interrupción de las alarmas del RTC
  }
}

void lcd_last_cmd(last_cmds last_cmd) {

  lcd.setCursor(0, 3);
  switch (last_cmd) {
    case LAST_CMD_PHOTO_STARTS:
      lcd.print("> PHOTOPERIOD STARTS");
      msg_delay_on = true;
      break;
    case LAST_CMD_PHOTO_ENDS:
      lcd.print("> PHOTOPERIOD ENDS  ");
      msg_delay_on = true;
      break;
    case LAST_CMD_HUM_HIGH_PLUMP_OFF:
      lcd.print("> HUM HIGH PLUMP OFF");
      break;
    case LAST_CMD_HUM_LOW_PLUMP_ON:
      lcd.print("> HUM LOW PLUMP ON ");
      break;
  }
  last_cmd = LAST_CMD_NONE;
}

void internet_task() {    // crea un cliente web accesible a tráves de la red. muestra la página HTML de sweet_love, lee y ejecuta peticiones web
  EthernetClient client = server.available(); //Creamos un cliente Web
  //Cuando detecte un cliente a través de una petición HTTP
  if (client) {
    Serial.println("new client");
    boolean currentLineIsBlank = true; //Una petición HTTP acaba con una línea en blanco
    String comando = ""; //cadena de caracteres vacía para guardar el estado del la lampara
    while (client.connected()) {//mientras el cliente este conectado y disponible
      if (client.available()) {
        char c = client.read();//Leemos la petición HTTP cada caracter individualmente
        Serial.write(c);
        comando.concat(c);//vamos agregando cada caracter de cada peticion para obtener la orden completa

        int poscomando = comando.indexOf("CMD="); //buscamos en el texto donde empieza el comando a ejecutar palabra LAM

        if (comando.substring(poscomando) == "CMD=LAMP_ON") //Si en la posicion poscomando hay "LAMP=ON"
        {
          digitalWrite(lamp, LOW); //encendemos la bombilla
          c_manual_lamp_ON = true;
        }
        if (comando.substring(poscomando) == "CMD=LAMP_OFF") //Si en la posicion poscomando hay "LAMP=OFF"
        {
          digitalWrite(lamp, HIGH); //apagamos bombilla
          c_manual_lamp_ON = false;
        }
        if (comando.substring(poscomando) == "CMD=PLUMP_ON") //Si en la posicion poscomando hay "PLUMP=ON"
        {
          digitalWrite(plump, HIGH); //encendemos la bomba rele activo
          c_manual_plump_ON = true;
          HL28_interval = 1000;
        }
        if (comando.substring(poscomando) == "CMD=PLUMP_OFF") //Si en la posicion poscomando hay "PLUMP=OFF"
        {
          digitalWrite(plump, LOW); //apagamos bomba rele desactivo
          c_manual_plump_ON = false;
          HL28_interval = 20000;
        }
        if (comando.substring(poscomando) == "CMD=AUTO_LAMP_ON")
        {
          c_auto_lamp_ON = true;
          auto_lamp_first();
        }
        if (comando.substring(poscomando) == "CMD=AUTO_LAMP_OFF")
        {
          c_auto_lamp_ON = false;
        }
        if (comando.substring(poscomando) == "CMD=AUTO_PLUMP_ON")
        {
          c_auto_plump_ON = true;
          state_ctrl = STATE_CTRL_HL28_SREAD;
          process_ctrl_machine();
        }
        if (comando.substring(poscomando) == "CMD=AUTO_PLUMP_OFF")
        {
          c_auto_plump_ON = false;
        }

        if (comando.substring(poscomando) == "CMD=FOTO_P_VEG")
        {
          foto_p_veg = true;
          if (c_auto_lamp_ON == true) {
            auto_lamp_first();
          }
        }

        if (comando.substring(poscomando) == "CMD=FOTO_P_FLO")
        {
          foto_p_veg = false;
          if (c_auto_lamp_ON == true) {
            auto_lamp_first();
          }
        }

        if (comando.substring(poscomando) == "CMD=SENSOR_UPDATE") //Si en la posicion poscomando hay "LAM=OFF"
        {
          sensor_update();
        }

        if (c == '\n' && currentLineIsBlank) {//comprobamos que ha acabado la petición con una linea en blanco

          client.println("HTTP/1.1 200 OK");//Enviamos la respuesta de la peticion al cliente
          client.println("Content-Type: text/html");
          client.println();

          if (digitalRead(lamp) == 1) {
            c_manual_lamp_ON = false;
          }

          if (digitalRead(lamp) == 0) {
            c_manual_lamp_ON = true;
          }

          if (digitalRead(plump) == 1) {
            c_manual_plump_ON = true;
          }

          if (digitalRead(plump) == 0) {
            c_manual_plump_ON = false;
          }

          sensor_update();

          client.println("<html>");//servimos la web a mostrar en HTML
          client.println("<head>");
          String html_1 = "<meta name=\"HandheldFriendly\" content=\"True\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">";
          client.println(html_1);

          client.println("</head>");
          client.println("<body>");

          String html_2 = "<p style=\"text-align:center\"><strong><span style=\"font-family:Courier New,Courier,monospace\"><em><span style=\"font-size:28px\">sweet_love v0.29</span></em></span></strong></p> <p style=\"text-align:center\"><strong><span style=\"font-family:Courier New,Courier,monospace\"><em>Sistema de control para cultivos</em></span></strong></p> <p>&nbsp;</p> ";
          client.print(html_2);

          client.print("<p>SENSORES <button type=\"button\" onclick=\"location.href='./?CMD=SENSOR_UPDATE\'\">ACTUALIZAR</button> </p> <ul> <li> ");
          client.print(DHT_temperature);
          client.print("&ordm;C &rArr; TEMPERATURA</li> <li>");
          client.print(DHT_airhumidity);
          client.print("%&nbsp; &rArr; H&Uacute;MEDAD EN EL AIRE</li> <li>");
          client.print(HL28_maped_value);
          client.print("%&nbsp; &rArr; H&Uacute;MEDAD EN EL SUSTRATO</li> </ul> <p>&nbsp;</p>");

          client.println("<p>CONTROL MANUAL</p> <ul>");

          if (c_manual_lamp_ON == false) { //si la lampara esta apagada
            client.println("<li> <radio onClick=location.href='./?CMD=LAMP_ON\'>");//cuando se pinche envia el comando lam=on
            client.print("<input type=\"radio\"name=\"c_manual_lamp\"value=\"1\">&nbsp;ENCENDER LAMPARA </li>");//muestra el texto encender
          }
          if (c_manual_lamp_ON == true) { //si esta encendida muestra texto apagar
            client.println("<li> <radio onClick=location.href='./?CMD=LAMP_OFF\'>");//cuando se pinche da orden de apagar
            client.print("<input type=\"radio\"name=\"c_manual_lamp\"value=\"1\">&nbsp;APAGAR LAMPARA </li>");//muestra texto apagar
          }
          if (c_manual_plump_ON == false) { //si la bomba esta apagada
            client.println("<li> <radio onClick=location.href='./?CMD=PLUMP_ON\'>");//cuando se pinche envia el comando lam=on
            //client.print("<input type=\"radio\"name=\"c_manual_lamp\"value=\"1\">&nbsp;ENCENDER BOMBA </li> </ul> <p>&nbsp;</p>");//muestra el texto encender
            client.print("<input type=\"radio\"name=\"c_manual_plump\"value=\"1\">&nbsp;ENCENDER BOMBA </li> </ul> <p>&nbsp;</p>");//muestra el texto encender
          }
          if (c_manual_plump_ON == true) { //si esta encendida muestra texto apagar
            client.println("<li> <radio onClick=location.href='./?CMD=PLUMP_OFF\'>");//cuando se pinche da orden de apagar
            //client.print("<input type=\"radio\"name=\"c_manual_lamp\"value=\"1\">&nbsp;APAGAR BOMBA </li> </ul> <p>&nbsp;</p>");//muestra texto apagar
            client.print("<input type=\"radio\"name=\"c_manual_plump\"value=\"1\">&nbsp;APAGAR BOMBA </li> </ul> <p>&nbsp;</p>");//muestra texto apagar
          }

          client.println("<p>CONTROL AUTOM&Aacute;TICO</p> <ul>");

          if (c_auto_lamp_ON == false) { //si auto lamp esta apagada
            client.println("<li> <radio onClick=location.href='./?CMD=AUTO_LAMP_ON\'>");
            client.print("<input type=\"radio\"name=\"c_auto_lamp\"value=\"1\">&nbsp;ACTIVAR AUTO L&Aacute;MPARA</li>");//muestra el texto encender
          }
          if (c_auto_lamp_ON == true) {
            client.println("<li> <radio onClick=location.href='./?CMD=AUTO_LAMP_OFF\'>");//cuando se pinche da orden de apagar
            client.print("<input type=\"radio\"name=\"c_auto_lamp\"value=\"1\">&nbsp;DESACTIVAR AUTO L&Aacute;MPARA</li>");//muestra texto apagar
          }
          if (c_auto_plump_ON == false) { //si auto bomba esta apagada
            client.println("<li> <radio onClick=location.href='./?CMD=AUTO_PLUMP_ON\'>");
            client.print("<input type=\"radio\"name=\"c_auto_plump\"value=\"1\">&nbsp;ACTIVAR AUTO BOMBA </li> </ul> <p>&nbsp;</p>");//muestra el texto encender
          }
          if (c_auto_plump_ON == true) { //si esta activada muestra texto desactivar
            client.println("<li> <radio onClick=location.href='./?CMD=AUTO_PLUMP_OFF\'>");
            client.print("<input type=\"radio\"name=\"c_auto_plump\"value=\"1\">&nbsp;DESACTIVAR AUTO BOMBA </li> </ul> <p>&nbsp;</p>");//muestra texto apagar
          }

          client.println("<p>FOTO PERIODO</p> <ul>");

          if (foto_p_veg == true) {
            client.print("<li> <input checked=\"checked\" name=\"web_foto_p_veg\" type=\"checkbox\" />&nbsp;VEGETATIVO 18/6 HORAS</li>");
            client.println("<li> <radio onClick=location.href='./?CMD=FOTO_P_FLO\'>");
            client.print("<input name=\"web_foto_p_veg\" type=\"checkbox\" />&nbsp;FLORECIMIENTO&nbsp;12/12 HORAS</li>");
          }

          else {
            client.println("<li> <radio onClick=location.href='./?CMD=FOTO_P_VEG\'>");
            client.print("<input name=\"web_foto_p_veg\" type=\"checkbox\" />&nbsp;VEGETATIVO 18/6 HORAS</li>");
            client.print("<li> <input checked=\"checked\" name=\"web_foto_p_flo\" type=\"checkbox\" />&nbsp;FLORECIMIENTO&nbsp;12/12 HORAS</li> </ul> <p>&nbsp;</p>");
          }

          String html_3 = "<p style=\"text-align:right\"><strong><em>Creado en Diciembre de 2023 </em></strong></p> <p style=\"text-align:right\"><strong><em>Por: Alejandro Bermudez Ospina</em></strong></p> <p style=\"text-align:right\"><strong><em>(alejandro.bermudez.ospinan@gmail.com)</em></strong></p>";
          client.print(html_3);

          client.println("</body>");
          client.println("</html>");
          break;
        }
        //final de respuesta
        if (c == '\n') {
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }

    delay(1);//Esperamos a que reciba la respuesta
    client.stop();//Termina la conexión
  }
}

void auto_lamp_first () {   //compara el tiempo actual y retorna 1 de 3 posibles valores
  //si es de 00 a 06 apaga la lamp(1), si es 06 a 18 la prende(2), de 18 a 24 si es veg prendida(3) y si es flo apagada

  time_t t_now = RTC.get();
  time_t t_0 = tmConvert_t(year(t_now), month(t_now), day(t_now), 0, 0, 0);
  time_t t_6 = tmConvert_t(year(t_now), month(t_now), day(t_now), 6, 0, 0);
  time_t t_18 = tmConvert_t(year(t_now), month(t_now), day(t_now), 18, 0, 0);
  time_t t_24 = tmConvert_t(year(t_now), month(t_now), day(t_now), 23, 59, 59);

  if (t_now >= t_0 && t_now < t_6) {
    digitalWrite(lamp, HIGH);
    c_manual_lamp_ON = false;
  }
  if (t_now >= t_6 && t_now < t_18) {
    digitalWrite(lamp, LOW);
    c_manual_lamp_ON = true;
  }
  if (t_now >= t_18 && t_now <= t_24) {
    if (foto_p_veg == true) {
      digitalWrite(lamp, LOW);
      c_manual_lamp_ON = true;
    }
    else  {
      digitalWrite(lamp, HIGH);
      c_manual_lamp_ON = false;
    }
  }
}

time_t tmConvert_t(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss)
{
  tmElements_t tmSet;
  tmSet.Year = YYYY - 1970;
  tmSet.Month = MM;
  tmSet.Day = DD;
  tmSet.Hour = hh;
  tmSet.Minute = mm;
  tmSet.Second = ss;
  return makeTime(tmSet);         //convert to time_t
}

void RTC_alarm_call(void) {
  RTC_alarm_call_ON = true;
  Serial.println("RTC alarm called");
}

void print_Digits(int digits) {
  if (digits < 10)
    lcd.print('0');
  lcd.print(digits);
}

void printDateTime(time_t t) {  // funciones de manejo de fecha y hora
  Serial << ((day(t) < 10) ? "0" : "") << _DEC(day(t));
  Serial << monthShortStr(month(t)) << _DEC(year(t)) << ' ';
  Serial << ((hour(t) < 10) ? "0" : "") << _DEC(hour(t)) << ':';
  Serial << ((minute(t) < 10) ? "0" : "") << _DEC(minute(t)) << ':';
  Serial << ((second(t) < 10) ? "0" : "") << _DEC(second(t));
}

time_t compileTime() {       // function to return the compile date and time as a time_t value
  const time_t FUDGE(10);    //fudge factor to allow for upload time, etc. (seconds, YMMV)
  const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
  char compMon[3], *m;

  strncpy(compMon, compDate, 3);
  compMon[3] = '\0';
  m = strstr(months, compMon);

  tmElements_t tm;
  tm.Month = ((m - months) / 3 + 1);
  tm.Day = atoi(compDate + 4);
  tm.Year = atoi(compDate + 7) - 1970;
  tm.Hour = atoi(compTime);
  tm.Minute = atoi(compTime + 3);
  tm.Second = atoi(compTime + 6);

  time_t t = makeTime(tm);
  return t + FUDGE;        //add fudge factor to allow for compile time
}

void msg_delay_check() {
  if (msg_delay_on == true) {
    msg_delay++;
    if (msg_delay == 60) {
      last_cmd = LAST_CMD_NONE;
      lcd.setCursor(0, 3);
      lcd.print(">                   ");
      msg_delay = 0;
      msg_delay_on = false;
    }
  }
}
NOTA: Para cargar la fecha y hora en el RTC por primera vez, se debe programar el Arduino quitando el comando de comentario "//"de la línea que dice: //RTC.set(compileTime()); y luego se debe programar de nuevo el Arduino incluyendo el comando "//".

Resumen de funcionamiento (copy/paste del tema anterior!):

El sistema controla el riego y la iluminación del cultivo. Muestra en la pantalla del LCD la siguiente información: tiempo, fecha, temperatura, humedad en aire y en sustrato, estado de los controles manuales y automáticos, así como el fotoperiodo. Puede ser configurado para realizar control manual y automático. Para el control automático del riego, utiliza la medida del sensor de humedad en el sustrato y activa el riego mediante una bomba de agua. Para el control automático de la luz, configura las alarmas del reloj de tiempo real con el foto-periodo especificado sea de crecimiento 18/6 (por defecto) o 12/12 de maduración. Los controles automáticos vienen desactivados por defecto.

Reemplacé la sonda del sensor de humedad que viene de fabrica por una artesanal hecha con tornillos gruesos y piezas de ferretearía, de modo que si se daña por corrosión podría ser reemplazada fácilmente. De todas maneras en esta parte tuve en cuenta las dificultades presentadas anteriormente (a Zion y otros usuarios) debido a la corrosión en la sonda, entonces para este código implementé un algoritmo que usa dicha sonda y realiza una lectura de humedad sólo una vez cada minuto pero con una mayor frecuencia al momento de usar la bomba de agua para lograr detener su uso a tiempo. La corrosión la causa la lectura como tal así que reducir la frecuencia con que se hace debería extender la vida de la sonda..


Riego y lámpara

El riego lo implementé usando una sonda plástica de las que venden en ferretería. Ubicándola en forma de círculo alrededor del tallo de la planta, perforando pequeños agujeros hacia abajo y poniendo un tapón al final de la sonda. La lámpara que usé para las pruebas fue una lámpara fluorescente de 110v. No sirve mucho para cultivos de cannabis pero creo que sirve para probar el sistema. El relé de estado solido soportaría hasta 240VAC @2A.

¡ACTUALIZACIÓN!

Como lo prometí, en esta nueva versión es posible acceder al sistema utilizando un Ethernet Shield (donación de @Zion_Lion, gracias!!).

Así luce el servidor web accediendo desde un celular:

servidor web desde celular.jpg

La dirección IP por defecto del servidor web es 192.168.1.115. Esta dirección debe ser acorde a la subred que haya en el lugar, si hay una subred distinta entonces hay que cambiar la dirección IP desde el código (en la línea: IPAddress ip(192, 168, 1, 115);). Para acceder directamente el Ethernet Shield simplemente se conecta al router con un cable UTP, para acceder a través de Internet se tendría que abrir el puerto 80 del router!! (esto aún no lo he probado).

Vídeos en Youtube!

El año pasado grabé material para una futura publicación. Aquí dejo los links!




Saludos!!
 
Última edición:

ariete63

Semilla
14 Septiembre 2023
3
0
1
61
hondarribia
Kaixo alejokaban alucinante y bonito proyecto gracias por compartir, me gustaria hacerlo pero tengo dudas en las conexiones con las nuevas modificaciones que has hecho y el video no esta completo ¿prodrias subir una foto con las nuevas modificaciones que has hecho?
Mas que nada para saber si me pongo a comprar mas cachibaches😁😁😁
Gracias
Saludos
 

alejokaban

Crecimiento
17 Febrero 2013
92
66
23
Kaixo alejokaban alucinante y bonito proyecto gracias por compartir, me gustaria hacerlo pero tengo dudas en las conexiones con las nuevas modificaciones que has hecho y el video no esta completo ¿prodrias subir una foto con las nuevas modificaciones que has hecho?
Mas que nada para saber si me pongo a comprar mas cachibaches😁😁😁
Gracias
Saludos
Hola ariete. Ya arreglé el vídeo incompleto. Pero es cierto, ese diagrama sólo da una idea porque algunas conexiones cambian y no corresponde al actual que es donde correría el código de manera limpia. Dale ya instalé el Fritzing en esta máquina y en los próximos días estaré actualizando las conexiones para subir un nuevo vídeo / imagen del diagrama actualizado que te pueda servir para tu montaje. Saludos.
 
  • 👍
Reacciones: Sr de la luz

alejokaban

Crecimiento
17 Febrero 2013
92
66
23
Hola @ariete63 . Es con mucho gusto. Ya terminé el diagrama eléctrico actualizado. Aquí te dejo la imagen. Me dices si así está bien o si necesitas que suba el proyecto en Fritzing para que puedas ver más de cerca y analizar las conexiones. Saludos!.

sweet_love v0.29 updated_bb.jpg

Aquí te dejo un link de la imagen con un poco más de bitrate.
 
Última edición:
  • 👍
Reacciones: Pakidermo

ariete63

Semilla
14 Septiembre 2023
3
0
1
61
hondarribia
Gracias asi esta perfecto ,me pongo a ello ,ya te ire contando que tal va ,me encanta el proyecto:cool:
Saludos
Por cierto FELIZ AÑO
 
-