Zeitschaltuhr mit DCF77

Hallo Arduino Community,
da ich noch ziemlicher Neuling in Sachen Arduino bin und gerade mein ersten Projekt gestartet habe bräuchte ich eure Hilfe.

Zu meinem Projekt:
Ich benutze einen Arduino Mega 2560 an dem ich einen DCF77 Empfänger und ein 4 zeiliges Display angeschlossen habe.
Zeit wird synchronisiert und auf dem Display ausgegeben, das klappt auch schon super. Später kommen noch 2 Sensoren für Temperatur und Hygro dazu. Das ganze gibt dan eine Terrarium Steuerung.

Jetzt möchte ich Relais Zeitgesteuert schalten.
Sprich, wenn es z.B. 15:30 Uhr ist soll Relai 1 an gehen und um 16:00 Uhr ausgehen.
Es sollte auch auf Sekunden genau funktionieren. Also z.B. Relai 2 um 16:00:00 Uhr ein und um 16:00:30 aus.

Ich hab schon hier im Forum rumgeschaut aber was richtig passenden habe ich nicht gefunden
Hoffe Ihr könnt mir da weiterhelfen.

Hier mein aktueller Code:

// Libarys werden geladen
#include "DCF77.h"
#include "Time.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// Pins werden deklariert
#define DCF_PIN 18                   // Connection pin to DCF77 device
#define DCF_INTERRUPT 5              // Interrupt Nummer des Pins


LiquidCrystal_I2C lcd(0x3F, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); 

time_t time;
DCF77 DCF = DCF77(DCF_PIN,DCF_INTERRUPT,false); // *** pin/pin/(normal(true) or invert signal(false)


void setup() {
                  
  lcd.begin(20,4);                    
  lcd.print("  Frosch-Terrarium");    
  lcd.setCursor(0, 1);                
  lcd.print("--------------------");  

  Serial.begin(9600);                 
  DCF.Start();                        
  Serial.println("****************************************** ");
  Serial.println("> DCF77-Test , Lunalander.de ");
  Serial.println("> Warte auf DCF77-Sync ... ");
  Serial.println("> Der erste Update dauert mind. 2 Minuten. ");
  Serial.println("****************************************** ");
                  
  createCustomChars();                
  lcd.setCursor(12, 4);               
  lcd.write(byte(2));                  
  
  lcd.setCursor(2, 2);                
  lcd.write(byte(0));                 
  
  lcd.setCursor(4, 2);
  lcd.print ("25,3");
  
  lcd.setCursor(8, 2);                
  lcd.write(byte(3));                 
  lcd.print ("C");                    
  
  lcd.setCursor(2, 3);                
  lcd.write(byte(0));                 
  
  lcd.setCursor(4, 3);
  lcd.print ("25,3");
  
  lcd.setCursor(8, 3);                 
  lcd.write(byte(3));                
  lcd.print ("C");                    
  
  lcd.setCursor(12, 2);               
  lcd.write(byte(1));                
  
  lcd.setCursor(14, 2);
  lcd.print ("75");
  lcd.print ("%");
}

void loop() {
  delay(1000);                          
  time_t DCFtime = DCF.getTime();       
  if (DCFtime!=0)                       
  {
    Serial.println("> DCF77 OK! , Zeit wurde syncronisiert!");             
    setTime(DCFtime);                   
  }    
  digitalClockDisplay();                            
              
}


void digitalClockDisplay()
{
  lcd.setCursor(14, 4);
  Serial.print(hour());                
  printHour(hour());                   
  printDigits(minute());               

}


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


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


void createCustomChars()
{
  uint8_t temperatur[8]     = {4, 10, 10, 10, 14, 31, 31, 14};
  uint8_t wassertropfen[8]  = {4, 4, 10, 10, 17, 17, 17, 14};
  uint8_t clock[8]          = {0x00,0x0E,0x15,0x17,0x11,0x0E,0x00,0x00};
  uint8_t grad[8]           = {28, 20, 28, 0, 0, 0, 0, 0};

  
  lcd.createChar(0, temperatur);
  lcd.createChar(1, wassertropfen);
  lcd.createChar(2, clock);
  lcd.createChar(3, grad);

}

G3cko:
Sprich, wenn es z.B. 15:30 Uhr ist soll Relai 1 an gehen und um 16:00 Uhr ausgehen.
Es sollte auch auf Sekunden genau funktionieren. Also z.B. Relai 2 um 16:00:00 Uhr ein und um 16:00:30 aus.

Wenn es sich um Tagesschaltzeiten handelt, rechnest Du am besten stets mit "Sekunden seit Mitternacht" zur Auswertung der Schaltbedingung. Da der Wert über den Wertebereich bei 86400 Sekunden täglich über den Wertebereich eines "unsigned int" hinausgeht, müßten die Variablen "long" sein.

long Einschaltzeit=16*3600L + 10 *60 + 5;  // Einschaltzeit 16:10:05 Uhr
long Ausschaltzeit=16*3600L + 10 *60 + 15;  // Ausschaltzeit 16:10:15 Uhr

Und zur Auswertung dann:

long jetzt=stunden*3600L + minuten*60 + sekunden;
if (jetzt>=Einschaltzeit && jetzt<Ausschaltzeit) digitalWrite(ausgang,HIGH);
else digitalWrite(ausgang,LOW);

Hab deinen Code mal eingebaut aber jetzt bekomme ich folgenden Fehler:

Terrarium.ino: In function 'void loop()':
Terrarium:86: error: invalid operands of types '' and 'long int' to binary 'operator*'
Terrarium:86: error: invalid operands of types '' and 'int' to binary 'operator*'

Hi jurs,
habs hinbekommen zum laufen, Vielen Dank für die schnelle Hilfe.
Nur noch ein paar Verständnisfragen zu dem Code.
was macht dieses L bei 3600L?

Hier mal der Code fals jemand anderes den auch braucht:
Habs mit der LED am Mega (ledPin 13) ausprobiert.

long Einschaltzeit=16*3600L + 10 60 + 5; // Einschaltzeit 16:10:05 Uhr
long Ausschaltzeit=16
3600L + 10 *60 + 15; // Ausschaltzeit 16:10:15 Uhr

long jetzt=(hour()*3600L) + (minute()*60) + (second());
if (jetzt>=Einschaltzeit && jetzt<Ausschaltzeit) digitalWrite(ledPin,HIGH);
else digitalWrite(ledPin,LOW);

G3cko:
was macht dieses L bei 3600L?

Das macht den Faktor 3600 "long" und damit die gesamte Multiplikation zu einer "long" Multiplikation, auch wenn der zweite Faktor nur eine "int" Variable ist. Standard-Variablentyp für Ganzzahlen in Arduino ist "int".

Beispiel: hour()*3600L

Wenn Deine "hour()" Funktion mal angenommen ein "unsigned int" zurückliefert, dann wäre es ohne das "L" eine Multiplikation zwischen zwei unsigned int Werten, maximales Ergebnis für unsigned int ist 65535. Um 23 Uhr würdest Du eigentlich erhalten:
23*3600 = 82800
Aber da bei unsigned int bei 65336 ein Überlauf stattfindet, käme als Ergebnis nur 82800-65336 = 17464 heraus.
Dies würde also zur Berechnung falscher Schaltzeiten führen.

Also erzwingst Du durch das nachgestellt "L" bei der Konstanten, dass diese Konstante eine "long" Konstante sein soll. Und bei einer Multiplikation mit "long" ist auch die gesamte Rechnung und das Ergebnis "long", selbst wenn der zweite Faktor nur ein "int" oder "unsigned int" sein sollte. Und schon ist das Ergebnis im Wertebereich von "long", womit Zahlen bis über 2 Milliarden darstellbar sind.

Wenn Deine Zeitschaltuhr nur Minuten schalten sollte, bestünde diese Problematik nicht: Der Tag hat nur 1440 Minuten, und wenn Du nur "Minuten nach Mitternacht" berechnen müßtest, wäre das auch mit "int" Wertebereich möglich.

Danke für die Erlärung.
Jetzt hab ich noch ein Problem. Wenn ich mehrmals am Tag den gleichen Pin schalten möchte dann macht er das immer nur 1mal aber bei der zweiten Zeit nicht mehr. Bzw. in dem Code Beispiel macht er nur die Einschaltzeit2 und Ausschaltzeit2. Woran liegt den das?

Habe den Code so eingefügt:

long Einschaltzeit=13*3600L + 41 60 + 0;
long Ausschaltzeit=13
3600L + 41 *60 + 30;

long Einschaltzeit2=13*3600L + 42 60 + 0;
long Ausschaltzeit2=13
3600L + 43 *60 + 0;

long jetzt=(hour()*3600L) + (minute()*60) + (second());
if (jetzt>=Einschaltzeit && jetzt<Ausschaltzeit) digitalWrite(ledPin,HIGH);
else digitalWrite(ledPin,LOW);     

if (jetzt>=Einschaltzeit2 && jetzt<Ausschaltzeit2) digitalWrite(ledPin,HIGH);
else digitalWrite(ledPin,LOW);

G3cko:
Woran liegt den das?

An Deiner Schaltlogik.

Wenn Du viele Schaltzeiten verarbeiten möchtest und die if-Abfragen nicht zu komplex werden sollen, machst Du es am besten so in der Art:

long jetzt=(hour()*3600L) + (minute()*60) + (second());
boolean eingeschaltet=false;
if (jetzt>=Einschaltzeit && jetzt<Ausschaltzeit) eingeschaltet=true;
if (jetzt>=Einschaltzeit2 && jetzt<Ausschaltzeit2) eingeschaltet=true;
if (eingeschaltet) digitalWrite(ledPin,HIGH);
else digitalWrite(ledPin,LOW);

Hallo,
ich sehe die "geheimnisvollen" Zeitschaltuhren hier ja öffters. Und frage mich-
"was- macht ihr da eigentlich?"

Das mit den Tagesschaltzeiten habe ich jetzt dank diesem:
"Wertebereich bei 86400" begriffen.

Die Routine von Jurs ist ja extrem Speicherarm gestaltet. Sie aber in den
tiefen eines Sketch zu finden- und fehlerfrei zu ändern, eher zweifelhaft.

Warum setzt man bei Schaltzeiten die nicht direkt mit Tastern gesetzt werden,
Variablen an den Anfang des Sketch? z.B.

long einStd = 16;
long einMin = 10;
long einSek = 05;

long ausStd = 16;
long ausMin = 10;
long ausSek = 15;

so könnte man die Schaltzeiten "fehlerfrei" angeben und müßte sich nicht durch
den ganzen Sketch wühlen- ist also nicht so umständlich.
Im Sketch würde es dann einmalig so aussehen:

long Einschaltzeit=einStd*3600L + einMin 60 + einSek;
long Ausschaltzeit=ausStd
3600L + ausMin *60 + ausSek;

Das ist jetzt die Geschichte mit den Tagesschaltzeiten, und was mache ich wenn
ich tatsächlich mal über Nacht schalten will/muß? Alles wieder ändern?

ich mache es z.B. so, das ich aus der gesetzten Schaltzeit eine String bilde:
"16:10:05" und diesen einfach mit dem Uhrzeit String vergleiche.
Es gibt also kein Problem damit die Geschiche um "00:57:23" wieder schalten
zu lassen.

Das ist doch wesentlich eleganter zu bedienen und handzuhaben als diese
Tagesschaltzeiten.
Ich meine wir haben es doch in der Hand, die Sache so "angenehm" wie möglich zu
programmieren.
Wenn uns auf dem iPhone eine Taste fehlt, dann schreien wir "ScheißApple" aber
wenn wir selber etwas programmieren, dann wohl nach dem Motto:
"Es muß doch noch umständlicher gehen"
Ich spreche hier von Bedienung, nicht vom programmieren!

Wenn man aber nur einen BabyArduino mit chronischem Speichemangel zur
Verfügung hat, dann ist die Methode von jurs schon allererstesahne…
Gruß und Spaß
Andreas

SkobyMobil:
Das ist jetzt die Geschichte mit den Tagesschaltzeiten, und was mache ich wenn
ich tatsächlich mal über Nacht schalten will/muß? Alles wieder ändern?

Nein. Wenn Du sowohl Schaltzeiten innerhalb desselben Tages verarbeiten möchtest, aber auch Schaltzeiten, bei denen gestern eingeschaltet und heute ausgeschaltet wird, ist es nur eine kleine Erweiterung der Schaltlogik, damit auch Schaltzeiten über Mitternacht hinweg korrekt geschaltet werden.

SkobyMobil:
ich mache es z.B. so, das ich aus der gesetzten Schaltzeit eine String bilde:
"16:10:05" und diesen einfach mit dem Uhrzeit String vergleiche.
Es gibt also kein Problem damit die Geschiche um "00:57:23" wieder schalten
zu lassen.

Das ist doch wesentlich eleganter zu bedienen und handzuhaben als diese
Tagesschaltzeiten.

Nein, ist es nicht.

Denn Dein Code funktioniert nur unter bestimmten Randbedingungen, die erfüllt sein müssen.

Zum Beispiel die Randbedingung, dass Dein Schaltcode auch exakt in derselben Sekunde laufen muß, in der die Soll-Schaltzeit liegt.

Wenn jemand nun beispielsweise aber ein Steuergerät baut, das 10 Temperaturen mißt und jede Temperaturmessung braucht 750 Millisekunden, dann brauchen 10 Temperaturmessungen an 10 Sensoren 7,5 Sekunden. Wenn er diese Messungen nacheinander ausführt, ist nicht mehr sichergestellt, dass Deine Schaltung überhaupt ausgeführt wird, wenn nämlich die richtige Schaltsekunde bei der Logikauswertung verpaßt wird.

Der Code mit den Tagesschaltzeiten würde allenfalls dazu führen, dass bei so einer 7,5 Sekunden Verzögerung im Programmablauf auch die Schaltzeit um maximal 7,5 Sekunden verzögert werden, aber die Schaltung fällt nicht komplett aus.

Darüber hinaus kommt bei einer Zeitsynchronisierung per DCF noch dazu: Wenn der Arduino etwas "nachgeht" und er irgendwann wieder ein Zeitsignal bekommt, dann kann es sein, dass die Zeit beim Synchronisieren um deutlich mehr als eine Sekunde vorgestellt werden muss. Das bedeutet: Manche Sekunden im Ablauf gibt es einfach nicht. Beispielsweise die Uhr geht nach und zeigt "00:57:20", und nun synchronisiert sich die DCF-Zeit auf ""00:58:00", dann kannst Du lange auf Erreichen der Schaltzeit bei "00:57:23" warten, die kommt dann erst wieder fast 24 Stunden später.

Auch fällt mir gerade ein: Woher bekommt Dein Programm dann eigentlich den ersten Schaltzustand beim Starten des Controllers? Wenn Strings nur auf Gleichheit mit Schaltzeiten getestet werden? Mal angenommen, es ist eine Einschaltzeit von 10:00:00 bis 22:00:00 eingestellt und Du startest den Controller um 15:08:23 Uhr, woher weiß die Stringvergleich-Programmlogik dann, dass gerade "ein" geschaltet sein soll?

Und wenn Du die Strings nicht auf Gleichheit testest, sondern mit entsprechenden Stringfunktionen doch auf ">=" und "<", dann bist Du logisch genau bei denselben Vergleichen von Tagesschaltzeiten wie ich sie oben gezeigt habe, allerdings

  • mit größerem Verbrauch an RAM-Speicher
  • mit größerem Verbrauch an Flash-Speicher
  • mit langsamerer Programmausführung

SkobyMobil:
Ich meine wir haben es doch in der Hand, die Sache so "angenehm" wie möglich zu
programmieren.

Ich meine, wir haben es vor allem in der Hand, die Sache so "fail safe" wie möglich zu programmieren, dass sie zuverlässig funktioniert. Und dass eine Programmlogik nicht einfach schon dadurch komplett wie ein Kartenhaus zusammenfällt, nur weil man an anderer Stelle im Programm eine kleine Zeitverzögerung von wenigen Sekunden programmiert oder weil die DCF-Uhrzeit um ein paar Sekunden anders synchronisiert wird als es die Programmlogik noch ausfallfrei verkraftet.

Hallo,
"exakt in derselben Sekunde laufen muß, in der die Soll-Schaltzeit liegt"

Genau, da bin ich schon einmal drauf reingefallen.
Mein Code ist Uhrzeit abhängig- funktioniert also nur, wenn er die Sekunde
auch zu fassen bekommt.
Dein Code zählt einfach sturr weiter, schaltet dann um die Verzögerung
später- aber schaltet! Meiner nicht!

Ich bin mir ziemlich sicher, das mein Code die Sekunde erwischt, weil ich das
prüfe (der Code durchläuft die Sekunde mehrmals)
Das ganze nützt mir aber nichts, wenn die bestimmte Sekunde nicht "da" ist.
grübel, grübel… ich weiß noch nicht, wie ich das finden soll…

Ich glaube das hast Du ziemlich gut erklärt, da kann ich etwas mit anfangen.
Gruß und Dank
Andreas

Wenn dir Speicher egal ist und du das etwas abstrahieren willst, kannst du dir mal die TimeAlarms Lib ansehen:
https://www.pjrc.com/teensy/td_libs_TimeAlarms.html
Braucht noch die Time Lib

Die arbeitet intern mit Unix Zeit und löst die Alarme auch aus wenn man sie nicht direkt erwischt

Hallo,
die TimeAlarm kenne ich. Ich hab´s aber nicht so mit Lib´s.
Keine Ahnung wie ich aus der Nummer raus komme. Es besteht keine Notwendigkeit
"Sekunden geanu" zu schalten. Ich habe die nur in den String genommen, weil
man es später mal gebrauchen könnte.
"lieber eine Sekunde haben und keine brauchen, als eine brauchen und keine haben"

Ich glaube das muss man erst einmal auseinander klabüstern.

Wenn man auf die Sekunde schalten will, dann taugt jurs seine Routine auch
nur etwas, wenn es nicht zu Verzögerungen kommt.
Wenn es zu Verzögerungen kommt, dann schaltet jurs seine Routine- mehr auch
nicht. Sie schaltet nicht mehr auf die vorgegebene Sekunde.
Wenn nicht auf die vorgegebene Sekunde geschaltet wird, dann liegt ein Fehler
im System vor!

Sie schaltet NACH der vorgegebenen Sekunde.
Sie verbraucht keinen Speicher und sie braucht "keine" Fehlerabfrage- weil-
sie schaltet ja. Alles schön...

Ich habe mich dazu entschlossen an meiner Schaltuhr nichts zu ändern. Das
werde ich über eine Fehlerabfrage (Kontrolle des Pin-Zustand) abfragen.
Wenn ich dem Ding sage, schalte um "16:10:15"- dann will ich, das zu dieser
Zeit geschaltet wird- nicht vorher und nicht nachher.
Und wenn sie das nicht macht, dann muß ich sie in einen bestimmten Zustand
bringen.

Da können wir uns ja noch bis übermorgen drüber unterhalten.
Da versaut mir jurs seine "Sekunden(Erbsen)Zählerei doch meine schöne
Schaltuhr. Nicht zu fassen.
Gruß und Spaß
Andreas

SkobyMobil:
Da versaut mir jurs seine "Sekunden(Erbsen)Zählerei doch meine schöne
Schaltuhr. Nicht zu fassen.

Mach Dir nichts draus, eine logisch völlig einwandfreie Schaltfunktion zu realisieren, die stets und unter allen Umständen korrekt schaltet, scheint wirklich gar nicht so einfach zu sein.

Selbst die Firma Apple ist da in den letzten Jahren schon mehrfach kräftig reingefallen und hat sich bis auf die Knochen blamiert, indem die Weckfunktion auf dem iPhone versagt hat - wegen Fehlern bei der Programmierung/Synchronisierung/Zeitumstellung.

Januar 2011 sind die iPhones mit der Weckfunktion schlecht über den Jahreswechsel gekommen:

Januar 2013 hat eine neue Do-Not-Disturb Funktion den Wecker nach einer Ruhephase nicht wieder scharfgeschaltet:

Und das waren nicht die einzigen Vorfälle, denn schon bei der Zeitumstellung zurück auf Winterzeit im Jahre 2011 gab es Probleme:

Also kann man sich damit trösten, wenn es mal mit der Zeitschaltung ein Problem gibt:
Selbst Weltfirmen wie Apple haben Probleme damit, ein Gerät stets und unter allen Umständen fehlerfrei zur beabsichten Zeit zu schalten.

Interessant sind auch die korrekte Behandlung von Schaltsekunden sowie Sommer-/Winterzeit Umstellung . Soll die Schaltuhr in UTC schalten oder in CET/CEST? Und immer dran denken, es ist NICHT richtig, daß jeder Tag 86400 Sekunden lang ist, denn es gibt Minuten die 61s haben (und theoretisch auch welche mit 59s).

Was passiert wenn die Uhr lange nicht synchronisiert hat? (Wobei das bei meiner Library: DCF77 Library | Blinkenlight eher selten bis gar nicht vorkommen sollte.)

Mein aktuelles Uhren Projekt mit WS2812 Stripes und Weckfunktion nach einem definierten Schichtplan basiert auf der Unix Zeit.
Also einfach die Sekunden seit dem 1. Januar 1970 00:00 Uhr mit der Weckzeit/Alarmzeit vergleichen.
In der Time.h sind die entsprechenden Funktionen zum Umrechnen enthalten.

Wenn die Uhr regelmässig via DCF abgeglichen wird funktioniert dies ohne Probleme.

@Udo:
Deine Methode das DCF Signal via FFT zu analysieren ist mit Sicherheit deutlich besser als die herkommlichen Methoden.
Ich habe sogar versucht dies zu verstehen, und mich erstmal mit Faltungen von Funktionen befasst.
Aber dann aufgegeben deinen Code zu verstehen, ich glaube aber das ist keine Schande :grin:

Der Nachteil ist, das z.b der Beispielcode 25.126 Byte groß ist.
Kommt jetzt drauf an auf welchen Arduino es laufen soll, beim Nano wird der freie Flash dann schon sehr knapp.