Irrigazione automatica

zoomx:
Oltre alla memoria contenuta nell'RTC sul modulo TinyRTC c'è montata una memoria I2C.

... quella mi sembra un'inutile complicazione ! ::slight_smile:

Considera che lui ha già (a gratis) le chiamate per leggere e scrivere nella memoria del DS1307, difatti la libreria RTClib, che lui usa, include i seguenti metodi:

uint8_t readnvram(uint8_t address);
void readnvram(uint8_t* buf, uint8_t size, uint8_t address);
void writenvram(uint8_t address, uint8_t data);
void writenvram(uint8_t address, uint8_t* buf, uint8_t size);

Guglielmo

maubarzi:
La EEPROM ha senso per salvare informazioni che resistano al riavvio, si rileggono nel setup e poi si lavora in memoria, a meno che le informazioni non siano proprio tante da non stare in memoria e allora si usa come appoggio per le cose meno usate.
Io la avevo intesa così quando ti ho incoraggiato sulla EEPROM.

A me serve solo ritardare una fase di 24 ore, tutto qui. Siccome la fase RIPOSO sarà quella che deciderà quando perchè irrigare ho bisogno che questo delay di 24ore sia isolato, altrimenti ci sarà sempre un ritardo di 24 ore prima di ogni irrigazione.
Per questo motivo ho inserito una fase PRIMA_ACCENSIONE con lo scopo di creare un ritardo di 24 ore solo alla prima accensione, e mai più.

switch (fase)
    {
      case RIEMPIMENTO_START:
        // riempie il contenitore e l'acqua dovrà decantare 24 ore
        fase = PRIMA_ACCENSIONE;  

      case PRIMA_ACCENSIONE:
        // aspetta 24 ore (decantazione)
        break;
        
      case RIPOSO:
        // attende la condizione su base mensile ed oraria
        break;

      case CONCIMAZIONE:
        // se il mese rientra nella condizione inserisce il concime nel contenitore altrimenti passa alla fase IRRIGAZIONE
        break;

      case IRRIGAZIONE:
      // Irriga e a fine operazione ritorna alla fase RIPOSO
      break;

Come ho detto precedentemente un delay di 86400000 millesimi basterebbe, ma avendo il TinyRTC stavo cercando altre vie. Mi è venuta in mente l'EEPROM ma io ignoro il 99% delle vie possibili quindi può essere una stupidaggine bella e buona usarla

gpb01:
... quella mi sembra un'inutile complicazione ! ::slight_smile:

Ricorda che quei 56 byte non sono in flash ma alimentati a batteria, quella dell'RTC.

zoomx:
Ricorda che quei 56 byte non sono in flash ma alimentati a batteria, quella dell'RTC.

Che si spera sia sempre carica (durano anni) e che comunque serve SOLO se manca l'alimentazione al modulo.
Considera che se gli si scarica ... addio anche a data/ora ... e quindi :wink:

Guglielmo

gpb01:
Considera che se gli si scarica ... addio anche a data/ora ... e quindi :wink:

E questo è anche vero!

Però fatemi capire la differenza perché ora sto facendo delle prove... sul mio RTC c'è scritto "TinyRTC I2C Module" quindi con cosa sto lavorando con RTC o con I2C? Nello sketch precedente ho impostato l'ora dell'RTC con

RTC.adjust(DateTime(2019, 05, 28, 23, 10, 00)); // decommentare per regolare ora e data (yyyy,mm,dd,hh,mm,ss)

Ora provando a scrivere sulla ram dell'RTC richiamando la libreria WIRE noto che i due orari sono diversi quindi sono un po confuso... questo è l'esempio che mi ha confuso:

#include <Wire.h> //include la libreria I2C

void setup()
{
Serial.begin(9600); //inizializza la seriale

Wire.begin(); //inizializza libreria

Wire.beginTransmission(0x68); //attiva la comunicazione con il DS1307 all'indirizzo RTC 0x68
Wire.write((byte)0x00); //il primo byte stabilisce il registro iniziale da scrivere i SECONDI da 0x00 a 0x59
Wire.write((byte)0x10); //il secondo byte specifica il tempo  MINUTI da 0x00 a 0x59
Wire.write((byte)0x80 | 0x10); //il terzo byte specifica il tempo  da 0x00 a 0x24
Wire.endTransmission();
}

void loop()
{
Wire.beginTransmission(0x68); //inizializza la trasmissione
Wire.write((byte)0x00); // partendo dall'indirizzo 0x00
Wire.endTransmission();

Wire.requestFrom(0x68, 3); //richiedo 3 byte dal dispositivo con indirizzo 0x68
byte secondi = Wire.read(); //recupero 1 byte dei secondi
byte minuti = Wire.read(); //recupero 1 byte dei minuti
byte ora = Wire.read(); //recupero i 1 byte delle ore

Serial.print("Orario corrente: ");
Serial.print(ora, HEX);
Serial.print(":");
Serial.print(minuti, HEX);
Serial.print(":");
Serial.println(secondi, HEX);
Serial.println();

delay(1000);
}

O usi la libreria o accedi direttamente al modulo ...
... inutile fare entrambe le cose (senza, per di più, sapere esattamente quello che si sta facendo). La libreria accede al modulo attraverso il bus I2C, ma quello è un problema di collegamento "fisico", non di sofware.

Guglielmo

gpb01:
O usi la libreria o accedi direttamente al modulo ...
... inutile fare entrambe le cose (senza, per di più, sapere esattamente quello che si sta facendo). La libreria accede al modulo attraverso il bus I2C, ma quello è un problema di collegamento "fisico", non di sofware.

Guglielmo

E se sapevo esattamente quello che stavo facendo ti pare che ero qui ad elemosinare aiuto? senza offesa.

karnhack:
E se sapevo esattamente quello che stavo facendo ti pare che ero qui ad elemosinare aiuto? senza offesa.

... ma appunto t'ho detto di usare la libreria e, almeno per il moento, lasciar stare altri metodi :wink:

La libreria ti mette a disposizione tutto quello che ti serve (inclusi i metodi per salvare/leggere dati nella ram del DS1307).

Guglielmo

Quindi se ho capito bene per dialogare con l'RTC uso la libreria RTClib... il fatto che nella maggior parte degli sketch venga inclusa anche la wire è per dialogare col monitor seriale arduino che usa l'I2C
Ok ci riprovo, grazie

Se non vi dispiace, mentre continuate le discussioni sugli aspetti di dettaglio, vorrei tornare sulla struttura di base per cercare di fare un po' di ordine, in primis, nella mia testa.
Fatto questo, si potrebbero iniziare ad inserire nel posto giusto i vari dettagli così da veder concretizzarsi qualcosa e mantenere l'entusiasmo.

Personalmente partirei dal diagramma del post 6 che riporto qui:
karnhackfasi.png
Anche io metterei al primo posto la fase di riempimento, per essere sicuri di non fare un giro a vuoto a causa di una qualche anomalia, ad es. un riavvio imprevisto o il primo avvio di stagione con bidone ancora vuoto.
Poi, visto che hai già in mente di aggiungere successivamente rilevamento di umidità, temperatura e pioggia, scarterei l'ipotesi 1 di programma cablato e puramente sequenziale e passerei direttamente alla opzione 2 che è più flessibile e se fatta bene non è poi tanto più complicata. Parlo delle due opzioni numerate in rosso nel post 6.

Da questo schema passerei a quello qui sotto che dettaglia ulteriormente alcune cose e si presta meglio ad una mappatura 1:1 con gli stati della possibile macchina a stati finiti.
Alcune cose le ho schematizzate come fasi (in particolare le due fasi stand-by...) appunto per una migliore traducibilità finale, poi è una questione di scelte, per cui se non piace discutiamone o pattumiamo pure :wink:


Da qui si potrebbe già scrivere lo scheletro del programma che verrebbe tipo quello qui sotto scritto in pseudo C misto ad italiano.
Come si vede ho creato lo switch case e mappato tutto in funzioni che dovranno contenere il codice di dettaglio.
In questo modo ogni funzione isola un aspetto del problema che può essere trattato e approfondito da solo.

#define START 0 // Prima fase, che abbiamo anche all'avvio di Arduino.
#define RIEMPIMENTO 1
#define RIPOSO 2
#define STAND_BY_CONCIMAZIONE 3
#define CONCIMAZIONE 4
#define AGITAZIONE 5
#define STAND_BY_IRRIGAZIONE 6
#define IRRIGAZIONE 7

byte fase = START;
void setup() {
  inizializzazioni varie.
}

void loop() {
  // PREMESSA: il loop dovrà girare al massimo della velocità!!!

  // Qui raccogliamo dati che useremo in seguito come l'ora, i valori dei pulsanti delle sonde, dei troppo pieno, della temperatura, umidità ecc.
  letturaComandiESensoriVari();

  // Inizziamo con la gestione della macchina a stati finiti.
  switch(fase) {
    case START:
      faseStart();
      break;
    case RIEMPIMENTP:
      faseRiempimento();
      break;
    case RIPOSO:
      faseRiposo();
      break;
    case STAND_BY_CONCIMAZIONE:
      verificaNecessitaConcimazione();
      break;
    case CONCIMAZIONE:
      faseConcimazione();
      break;
    case AGITAZIONE:
      faseAgitazione();
      break;
    case STAND_BY_IRRIGAZIONE:
      verificaNecessitaIrrigazione();
      break;
    case IRRIGAZIONE:
      faseIrrigazione();
      break;
  }
  visualizzazioneDatiSuDisplay();
}

void letturaComandiESensoriVari() {
  // Come già indicato sopra, qui ci andranno tutte le rilevazioni del caso opportunamente temporizzate per non farle proprio ad ogni giro di loop.
}

void faseStart() {
  if (verificaTroppoPieno()) {
    // Passiamo alla fase successiva.
    fase++;
  } else {
    // Saltiamo alla fase specifica.
    fase = STAND_BY_CONCIMAZIONE;
  }
}

boolean verificaTroppoPieno() {
  // Leggiamo il sensore o i sensori per verificare se il serbatoio è pieno.
  if (il serbatoio è pieno) {
    return true;
  } else {
    return false;
  }
}

void faseRiempimento() {
  // Attiviamo pompe e ciapini vari per iniziare il riempimento tenendo sempre sott'occhio il sensore del troppo pieno per bloccare tutto appena la cisterna è piena.
  if (cisterna piena) {
    if (pompa accesa) {
      // Il test rende più tollerante il codice da eventuali malfunzionamenti che fanno arrivare qui a pompa ancora spenta, ad es. è fallito il test della fase precedente. Meglio non dare queste cose per scontato, anche se dovrebbero esserlo ;)
      spegni pommpa se accesa
    }
    fase++
  } else if (pompa spenta) {
    accendiamo la pompa
  } else {
    if (timeout pompa accesa) {
      spegni pompa
      salva errore per notifica
      fase++;
    }
  }
}

void faseRiposo() {
  // Attendiamo le 24h.
  if (sono passate le 24h) {
    fase++;
  }
}

void verificaNecessitaConcimazione() {
  // Analizziamo i dati dell'RTC letti a inizio loop per vedere se è ora di concimare.
  if (è ora di concimare) {
    // Passiamo alla fase successiva che è quella di concimazione.
    fase++;
  } else {
    // Passiamo alla fase di verifica dell'orario di irrigazione.
    fase = STAND_BY_IRRIGAZIONE;
  }
}

void faseConcimazione() {
  // Attiviamo tutte le cose che servono per la concimazione.
  // Sulla falsariga della faseRiempimento.
  // E' da verificare che ci sia stata almeno una irrigazione dall'ultima concimazione. Per evitare di farla doppia ad es. a causa di un mese piovoso che non fa irrigare mai.
  ...
  if (tutto finito e fatto bene) {
    fase++;
  }
}

void faseAgitazione() {
  // Attiviamo tutte le cose che servono per l'agitazione.
  // Sulla falsariga della faseRiempimento e faseConcimazione.
  ...
  if (tutto finito e fatto bene) {
    fase++;
  }
}

void verificaNecessitaIrrigazione() {
  // E' arrivata l'ora di irrigare?
  // Possiamo anche verificare altre condizioni per evitare di irrigare, ad es. se sta piovendo.
  // In base alle condizioni rilevate, possiamo impostare un nuovo tempo di attesa prima di irrigare nuovamente.
  if (è ora di irrigare) {
    fase++;
  } else {
    // Cicliamo tra queste due condizioni per poter far partire quella che scade per prima anche in base ad eventuali contrattempi.
    fase = STAND_BY_CONCIMAZIONE;
  }
}

void faseIrrigazione() {
  // Attiviamo tutte le cose che servono per l'irrigazione.
  // Sulla falsariga della faseRiempimento, faseConcimazione e faseAgitazione.
  ...
  if (tutto finito e fatto bene) {
    // Ripartiamo dalla prima fase in modo da permettere il riempimento della cisterna.
    fase = START;
  }
}

void visualizzazioneDatiSuDisplay() {
  // Operazione da temporizzare opportunamente per non fare aggiornamenti ad ogni giro di loop ma solo quando serve.
  // Si potrebbe usare una variabile booleana da mettere a true dentro le altre funzioni se ci sono modifiche ai dati da visualizzare.
  if (c'è qualche novità da visualizzare) {
    // scrivi su display tutti i dati da visualizzare.
  }
}

karnhack:
Quindi se ho capito bene per dialogare con l'RTC uso la libreria RTClib...

La Wire la devi lasciare, viene usata anche dalle altre librerie (... che spesso se la includono per conto loro) per parlare con qualunque cosa sia collegato al bus I2C (sensori, RTC, ecc. ecc.). Semplificado possiamo dire che ha il compito di interfacciare il bus fisico ed i suoi segnali con il software.

Se vai a vedere, la tua RTClib, nel file RTClib.cpp la prima cosa che fa è proprio:

#include <Wire.h>
#include "RTClib.h"

... perché la usa per parlare con il RTC :wink:

Guglielmo

karnhack:
Ora provando a scrivere sulla ram dell'RTC richiamando la libreria WIRE....

....

Wire.beginTransmission(0x68); //attiva la comunicazione con il DS1307 all'indirizzo RTC 0x68
Wire.write((byte)0x00); //il primo byte stabilisce il registro iniziale da scrivere i SECONDI da 0x00 a 0x59
Wire.write((byte)0x10); //il secondo byte specifica il tempo  MINUTI da 0x00 a 0x59
Wire.write((byte)0x80 | 0x10); //il terzo byte specifica il tempo  da 0x00 a 0x24
Wire.endTransmission();

Manca il byte di indirizzamento:

Wire.beginTransmission(0x68);  //attiva la comunicazione con il DS1307 all'indirizzo RTC 0x68
Wire.write((byte)0x00);        //il primo byte stabilisce il registro iniziale su cui scrivere
Wire.write((byte)0x00);        //il secondo specifica i SECONDI da 0x00 a 0x59 (scritti nel registro 0)
Wire.write((byte)0x10);        //il terzo byte specifica i MINUTI da 0x00 a 0x59 (scritti nel registro 1)
Wire.write((byte)0x80 | 0x10); //il quarto byte specifica le ORE da 0x00 a 0x24 (scritte nel registro 2)
Wire.endTransmission();

La libreria RTClib accede all'RTC usando la WIRE esattamente come stai provando a fare, ma permette un accesso a livello più alto (leggi comodo) senza doversi occupare dei registri dell'RTC e della conversione BCD dei valori. In altre parole i valori nell'RTC sono memorizzati in formato BCD, e se non si effettua una conversione alle 20 leggi 32, alle 12 leggi 18 ecc.

Mi è venuta in mente l'EEPROM ma io ignoro il 99% delle vie possibili quindi può essere una stupidaggine bella e buona usarla

Se serve mantenere un'informazione anche in caso di mancanza alimentazione/reset non solo è una buona idea ma anche indispensabile. Poi come emerso successivamente l'RTC ha anche lui uno spazio utile (56 byte di RAM) per salvare qualche dato utente. E a seconda del modulino RTC può esserci anche un'ulteriore EEPROM esterna, quindi posti dove salvare le cose ce ne sono in esubero :smiley:

Ma allora oltre a un semplice orario va salvata anche qualche informazione sulla fase in corso, in modo che all'accensione/reset la si possa rileggere e riconoscere, per scegliere il comportamento opportuno.

maubarzi:
vorrei tornare sulla struttura di base per cercare di fare un po' di ordine, in primis, nella mia testa.

Mi sono un po' perso anch'io... nel senso che non mi è più totalmente chiara la sequenza (se non sbaglio le 24 ore si dovrebbero aspettare solo al primissimo giro), e non è ancora chiaro come gestire il momento di concimazione più o meno in corrispondenza dell'irrigazione, avevo pensato a una cosa del genere... che prevede anche dei rabbocchi se il bidone perde un po'...

case RIPOSO:
    if      (tempo_di_concimare()) { fase = CONCIMAZIONE; }
    else if (ora_di_irrigare())    { fase = IRRIGAZIONE;  }
    else if (!troppo_pieno())      { fase = RIEMPIMENTO;  }
break;

Poi per contare i tempi delle fasi avevo pensato a una funzione 'trascorso'...

bool trascorso(unsigned long secondi)
{
    return (millis() - inizio) >= (secondi * 1000);
}

...e "ricaricare" il tempo in 'inizio' ogni volta che la fase cambia, quindi appena dopo lo switch:

// ricarica tempo se fase cambiata
if (fase != fase_prec) { fase_prec = fase;  inizio = millis(); }

Dopo di che nelle fasi si può controllare il tempo semplicemente con:

if (trascorso(TEMPO_CONCIMAZIONE)) ...

Da notare poi che ragionando per fasi non serve neppure scrivere le varie digitalWrite nei case o nelle funzioni, perché le fasi corrispondono già ai relé da accendere.... nella fase irrigazione acceso solo il relé pompa, in quella riposo nessuno ecc. Quindi le digitalWrite si possono mettere tutte alla fine... se si usa il bistrattato ternario sono quattro righe di numero.

maubarzi:
Se non vi dispiace, mentre continuate le discussioni sugli aspetti di dettaglio, vorrei tornare sulla struttura di base per cercare di fare un po' di ordine, in primis, nella mia testa.

Claudio_FF:
Mi sono un po' perso anch'io... nel senso che non mi è più totalmente chiara la sequenza (se non sbaglio le 24 ore si dovrebbero aspettare solo al primissimo giro), e non è ancora chiaro come gestire il momento di concimazione più o meno in corrispondenza dell'irrigazione ... ...

Occhio che stiamo perdendo pezzi e cioè il post #70. Lo schema è questo sempre se va bene

Nel codice ho inserito questo:

/*********IMPOSTAZIONE TEMPI Ora Irrigazione E CONCIMAZIONE*********/

int OraIrrigazione[] = {18, 22, 18, 23, 0, 0, 0, 0}; // Ora inizio estivo, ora fine estivo, Ora inizio invernale, ora fine invernale

/***************************************************************/

Praticamente pensavo ad una cosa del genere: è inverno se Mese è Gennaio, Febraio ecc altrimenti è estate.
Se è estate l'orario è inizio hh[ 0 ] mm[ 1 ] fine hh[ 2 ] mm[ 3 ]
Se è inverno l'orario è inizio hh[ 4 ] mm[ 5 ] fine hh[ 6 ] mm[ 7 ]

Prova.ino (4.65 KB)

Al momento ho fatto così per ricavare l'orario e la stagione per quanto riguarda la fase RIPOSO (inserisco solo il codice utile a questa fase)

int orario_start = 2;
int orario_stop = 3;
int stagione = 4;
int INVERNALE = 5;
int ESTIVO = 6;

...

int OraIrrigazione[] = {18, 22, 18, 23, 0, 0, 0, 0}; // Ora inizio estivo, ora fine estivo, Ora inizio invernale, ora fine invernale
...

int MESE = now.month();
int INIZIO_ESTIVO = OraIrrigazione[0] * 60 + OraIrrigazione[1];
int FINE_ESTIVO = OraIrrigazione[2] * 60 + OraIrrigazione[3];
int INIZIO_INVERNALE = OraIrrigazione[4] * 60 + OraIrrigazione[5];
int FINE_INVERNALE = OraIrrigazione[6] * 60 + OraIrrigazione[7];

...

    int GENNAIO = now.month() == 1;
    int FEBBRAIO = now.month() == 2;
    int MARZO = now.month() == 3;
    int APRILE = now.month() == 4;
    int MAGGIO = now.month() == 5;
    int GIUGNO = now.month() == 6;
    int LUGLIO = now.month() == 7;
    int AGOSTO = now.month() == 8;
    int SETTEMBRE = now.month() == 9;
    int OTTOBRE = now.month() == 10;
    int NOVEMBRE = now.month() == 11;
    int DICEMBRE = now.month() == 12;

...

 case RIPOSO:
      if ((MESE=GENNAIO) || (MESE=FEBBRAIO) || (MESE=NOVEMBRE) || (MESE=DICEMBRE)) 
      {
        stagione = INVERNALE;
        orario_start = INIZIO_INVERNALE;
        orario_stop = FINE_INVERNALE;
      }
      else 
      {
        stagione = ESTIVO;
        orario_start = INIZIO_ESTIVO;
        orario_stop = FINE_ESTIVO;
      }
        break;

gpb01:
Considera che lui ha già (a gratis) le chiamate per leggere e scrivere nella memoria del DS1307, difatti la libreria RTClib, che lui usa, include i seguenti metodi:

uint8_t readnvram(uint8_t address);

void readnvram(uint8_t* buf, uint8_t size, uint8_t address);
void writenvram(uint8_t address, uint8_t data);
void writenvram(uint8_t address, uint8_t* buf, uint8_t size);



Guglielmo

@gpb01 Hai un link con esempi? non saprei come usare questo suggerimento

ed ecco che è arrivato il momento della domanda di @Maurotec :slight_smile:

Maurotec:
PS: lo so, la domanda è; da dove spuntano fuori uint8_t, uint16_t ecc?

karnhack:
Occhio che stiamo perdendo pezzi e cioè il post #70. Lo schema è questo sempre se va bene...

Hai distinto solo l'orario di partenza in base alla stagione, giusto?
Questa cosa non necessariamente va schematizzata come uno stato specifico.
Si può fattorizzare dentro una funzione che ti ritorna l'orario di irrigazione giusto in base alla stagione.
Il flusso resta inalterato e dentro questa funzione isoli la logica che restituisce l'orario giusto.

Isoli il problema e lo confini un un posto ben preciso che puoi modificare agilmente in momenti successii mantenendo inalterata la complessità di tutto il resto. Ho spiegato meglio cosa intendo con il termine "fattorizzare".

Invece si potrebbe inglobare il suggerimento di @Claudio_FF che potrebbe semplificare lo schema, intendo questo suggerimento:

Claudio_FF:

case RIPOSO:

if      (tempo_di_concimare()) { fase = CONCIMAZIONE; }
   else if (ora_di_irrigare())    { fase = IRRIGAZIONE;  }
   else if (!troppo_pieno())      { fase = RIEMPIMENTO;  }
break;

maubarzi:
Invece si potrebbe inglobare il suggerimento di @Claudio_FF che potrebbe semplificare lo schema, intendo questo suggerimento.

@Maurotec scusa mi stai dicendo questa cosa perchè hai visto i post #82 e #83 ?

Perche altrimenti non ho capito... io intendevo inserire il suggerimento che tu hai citato subito dopo aver ricavato stagione e orari

case RIPOSO:
      if ((MESE=GENNAIO) || (MESE=FEBBRAIO) || (MESE=NOVEMBRE) || (MESE=DICEMBRE))
      {
        stagione = INVERNALE;
        orario_start = INIZIO_INVERNALE;
        orario_stop = FINE_INVERNALE;
      }
      else
      {
        stagione = ESTIVO;
        orario_start = INIZIO_ESTIVO;
        orario_stop = FINE_ESTIVO;
      }

// Va ancora avviato !
        break;

Si potrebbe anche ricavare l'orario in base al giorno dell'anno di cui si conosce l'ora di alba e tramonto tramite formule.

Non entro nel merito perché non mi intendo di coltivazione, però a me il codice sembra sempre più confuso.

Puoi spiegare nuovamente tutte le fasi manuali, cioè come se le compissi tu manualmente.

if ((MESE=GENNAIO) || (MESE=FEBBRAIO) || (MESE=NOVEMBRE) || (MESE=DICEMBRE))

Non usare '=' perché è assegnazione e non eguaglianza '==', quindi,

if   (  (MESE==GENNAIO) 
     || (MESE==FEBBRAIO) 
     || (MESE==NOVEMBRE) 
     || (MESE==DICEMBRE) )
{
    // eseguito solo a gennaio, febbraio, novembre e dicembre 
}

PS: l'ho scritto cosi per comodità e inoltre non crea problemi al compilatore.