/* * PROJEKT: Huehnerautomat ************************* * VERSION: 1.2 * AUTOR: Daniel Menzel * DATE: 20141120 * DESC: Dieses Programm steuert als Daemmerungsschalter das automatische Oeffnen und Schliessen * mit einer einstellbaren Verzoegerung nach Erreichen des Helligkeitsschwellwerts * Weiterhin ist neben einem Kongfigurationsmodus ein manueller Modus mit Auf/Zu-Wechsel moeglich * Das Programm schreibt eine E-Mail ueber ein PHP-Skript auf meinem NAS-System bei Starten, Oeffnen, Schliessen und Fehler * * Steuerung von drei Zuständen ueber Poti * Off, Auto, Auf/Zu * * RAM Einsparungen: * - variablen auf byte reduziert wenn moeglich * - Alle Strings mit F("string") umgeben --> spart RAM */ // LAN #include static uint32_t timer; static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 }; // MAC Adresse Arduino static byte myip[] = { 172,16,17,200 }; // IP Adresse Arduino static byte gwip[] = { 172,16,17,1 }; // Gateway static byte hisip[] = { 172,16,17,2 }; // IP-Adresse des Servers static byte dnsip[] = { 172,16,17,1 }; // DNS-Adresse ACHTUNG Ohne die geht die statische Zuordnung der IP nicht ! Jede Menge falsche Beispiele im Internet ! byte Ethernet::buffer[400]; // kleiner Buffer sollte genuegen const char website[] PROGMEM = "172.16.17.2"; // Webseite die aufgerufen werden soll byte sendmail = 0; // Flag ob eine E-Mail geschrieben werden soll oder nicht byte action = 0; // Flag welche E-Mail geschrieben werden soll // Action-Parameter wird von PHP-Skript auf Synologie ausgewertet welche die eigentliche E-Mail schreibt: // 1 : Start // 2 : Offen // 3 : Zu // 4 : Fehler uint32_t emailrate = 120000; // Maximal alle x Millisekunden eine E-Mail schreiben // Timer // Ueber diese Funktion kann ein Ueberlaufsicherer Timer erstellt werden // Dieser sorgt hier dafuer, dass ein automatischer Tuer-Schaltvorgang erst nach dem zweiten verzoegerten Check durchgefuehrt wird // Reference: http://www.forward.com.au/pfod/ArduinoProgramming/TimingDelaysInArduino.html #include // I2C #include //EEPROM #include const byte EEPROM_MAGIC_NUMBER = 29; // Wenn diese Zahl im EEPROM steht hat dieses Programm schon geschrieben const byte EEPROM_MAGIC_ADRESS = 0; // Hier sollte die MAGIC_NUMBER stehen // DISPLAY #include #include //define I2C_ADDR 0x3F // I2C-Adresse des Displays 1 #define I2C_ADDR 0x27 // I2C-Adresse des Displays 2 #define BACKLIGHT_PIN 3 #define En_pin 2 #define Rw_pin 1 #define Rs_pin 0 #define D4_pin 4 #define D5_pin 5 #define D6_pin 6 #define D7_pin 7 LiquidCrystal_I2C lcd(I2C_ADDR,En_pin,Rw_pin,Rs_pin,D4_pin,D5_pin,D6_pin,D7_pin); // Variablen // int modus_status = 0; byte last_state = 0; // Speichert den letzten Status 0: Off 1: Auto 2: Auf/Zu byte verzoegerung_status = 0; // Wird 1 wenn zum ersten mal ein automatischer Tuerwechsel statt finden soll elapsedMillis timeElapsed; // Timer INIT // Ein- und Ausgänge // const int MODUS_EINGANG = A0; // Modus-Poti-Eingang const int LDR = A1; // Photozelle als Daemmerungsschalter const byte TUERSCHALTER = A3; // Schalter fuer die Tuer const byte MOTOR_RELAIS = 3; // PIN fuer Motor-Relais-Steuerung const byte KEY_ENTER = 8; // Tasten fuer Menuesteuerung const byte KEY_BACK = 9; // Optimierbar mit nur einem Eingang ueber Widerstandswerte const byte KEY_LEFT = 6; const byte KEY_RIGHT = 7; // Konfigurationswerte // (werden von EEPROM-Werten ueberschrieben) int dunkel; // Schwellwert der Photozelle ab der die Tuer geschlossen oder geoeffnet wird int zeit_motor_auf = 4000; // Wert in ms in der der Motor beim Oeffnen Strom bekommt int zeit_motor_zu = 7000; // Wert in ms in der der Motor beim Schliessen MAXIMAL Strom bekommt unabhaengig von Schalterposition. Falls danach noch der Schalter offen sein sollte --> ERROR unsigned long verzoegerung = 120000; // Wert in ms der beim ersten Eintreffen einer Hell/Dunkel-Aenderung gewartet wird. Ist es danach weiterhin dunkel/hell wird geschaltet. 1200000ms = 20 Minuten byte debug = 0; // Wenn debug = 1, dann gelten kuerzere Zeiten fuer die Verzoegerung // Konfig-Array // Menueeintraege fuer die Werte mit den Speicherzellen im EEPROM const int MAX_MENU_EINTRAEGE = 5; char* menu[MAX_MENU_EINTRAEGE] = { "Konfig Dunkel-Wert ?", "Konfig Motor-Auf ? ", "Konfig Motor-Zu ? ", "Verzoegerung ? ", "debug ? " }; // Parallel zu den Menuetexten hier die (EEPROM-Speicherzellen, DEFAULT-Werte, Erhoehungschritte) unsigned long menu_inhalt[MAX_MENU_EINTRAEGE][3] = { {1,0,1}, {5,3000,100}, {10,7000,100}, {15,1200000,6000}, {20,0,1} }; byte menu_position = 0; /*----------------------------------------------------------------------------------------------------*/ /* DESC: Setup-Aufruf beim Start des Arduino *----------------------------------------------------------------------------------------------------*/ void setup() { // INIT Serial.begin(9600); pinMode(LDR,INPUT); digitalWrite(LDR,LOW); pinMode(MOTOR_RELAIS,OUTPUT); pinMode(TUERSCHALTER,INPUT); // Tuerschalter-Eingang digitalWrite(TUERSCHALTER, HIGH); // setze pullup-widerstand pinMode(KEY_LEFT,INPUT); // KEY_LEFT-Eingang digitalWrite(KEY_LEFT, HIGH); // setze pullup-widerstand pinMode(KEY_RIGHT,INPUT); // KEY_RIGHT-Eingang digitalWrite(KEY_RIGHT, HIGH); // setze pullup-widerstand pinMode(KEY_ENTER,INPUT); // KEY_ENTER-Eingang digitalWrite(KEY_ENTER, HIGH); // setze pullup-widerstand pinMode(KEY_BACK,INPUT); // KEY_BACK-Eingang digitalWrite(KEY_BACK, HIGH); // setze pullup-widerstand digitalWrite(MOTOR_RELAIS, HIGH); // MOTOR-RELIAS aus // LAN INIT Serial.println(F("LAN-Init")); if (ether.begin(sizeof Ethernet::buffer, mymac, 10) == 0) Serial.println(F("Failed to access Ethernet controller")); if (!ether.staticSetup(myip, gwip, dnsip)) Serial.println(F("could not get a static IP")); ether.printIp("IP: ", ether.myip); ether.printIp("GW: ", ether.gwip); ether.printIp("DNS: ", ether.dnsip); ether.hisip[0] = 172; ether.hisip[1] = 16; ether.hisip[2] = 17; ether.hisip[3] = 2; ether.printIp("SRV: ", ether.hisip); // I2C INIT Serial.println(F("I2C-Init")); Wire.begin(); // DISPLAY INIT Serial.println(F("LCD-Init")); lcd.begin(20,4); // Display 20x4 lcd.setBacklightPin(BACKLIGHT_PIN,POSITIVE); // Hintergrundbeleuchtung an lcd.setBacklight(HIGH); lcd.home(); lcd.print(F("- Huehnerautomat V1-")); lcd.print(F(" von Daniel Menzel")); delay(1000); lcd.clear(); // EEPROM INIT byte MAGIC_TEST; MAGIC_TEST = EEPROM.read(EEPROM_MAGIC_ADRESS); Serial.print(F("EEPROM:")); Serial.println(MAGIC_TEST); lcd.home(); lcd.print(F("EEPROM-Check")); delay(1000); lcd.clear(); if (MAGIC_TEST != EEPROM_MAGIC_NUMBER) { Serial.println(F("Schreibe MAGIC_NUMBER und Parameter")); lcd.home(); lcd.print(F("1.Lauf: INIT_EEPROM")); delay(1000); lcd.clear(); // Beim INIT MAGIC-Number schreiben... EEPROM.write(EEPROM_MAGIC_ADRESS,EEPROM_MAGIC_NUMBER); // ...und jetzt die Default-Werte in das EEPROM-Schreiben for (byte x = 0; x < MAX_MENU_EINTRAEGE; x++) { EEPROMWriteLong(menu_inhalt[x][0],menu_inhalt[x][1]); } } else { Serial.println(F("Check OK")); lcd.print(F("OK")); delay(1000); lcd.clear(); } // Konfig-Werte aus EEPROM lesen dunkel = EEPROMReadLong(menu_inhalt[0][0]); // Schwellwert der Photozelle ab der die Tuer geschlossen oder geoeffnet wird zeit_motor_auf = EEPROMReadLong(menu_inhalt[1][0]); zeit_motor_zu = EEPROMReadLong(menu_inhalt[2][0]); verzoegerung = EEPROMReadLong(menu_inhalt[3][0]); debug = EEPROMReadLong(menu_inhalt[4][0]); // Wenn debug dann ist dei Verzoegerung nur 5 Sekunden if ( debug == 1) { verzoegerung = 5000; emailrate = 1000;} Serial.println(F("mem")); Serial.println(FreeRam()); //E-Mail ueber Start des Arduino schreiben WriteEmail(1); Serial.println(F("mem")); Serial.println(FreeRam()); } /*----------------------------------------------------------------------------------------------------*/ /* DESC: Hauptprogramm Loop *----------------------------------------------------------------------------------------------------*/ void loop() { // Da es mir offensichtlich unmoeglich ist in einer Subroutine eine URL aufzurufen, es aber // in der Mainloop gelingt, folgende Umgehung: // Es wird von der Sub WriteEmail lediglich ein Flag gesetzt, dass eine E-Mail geschrieben werden soll // und die Action-Variable gesetzt mit welchem Inhalt // In der Mainloop frage ich alle x Sekunden ab ob das Flag gesetzt ist und rufe die URL auf mit // gleichzeitigem Zuruecksetzen des Flags // ether.packetLoop(ether.packetReceive()); if (millis() > timer && sendmail == 1) { int chk = 1; char queryString[256] = {0}; sprintf(queryString, "?action=%d", (int)action); // Nur maximal alle x Sekunden eine E-Mailschreiben timer = millis() + emailrate; Serial.print(F("Neuer Timer : ")); Serial.println(timer); Serial.println(); Serial.println(F("Schreibe E-Mail")); ether.browseUrl(PSTR("/DeineURL/mail.php"), queryString, website, MyCallback); // Globale Variablen zuruecksetzen sendmail = 0; action = 0; } // ENDE LAN // Eigentliche Programmlogik Loop // modus_status = analogRead(MODUS_EINGANG); if ( modus_status <= 30 ) { // ---------------------------------------------------------------------------------------------------- AUTOmatik ist an if (last_state != 1 ) { lcd.clear(); Serial.println(F("AUTOMATIK")); } lcd.setCursor (0,0); lcd.print(F("AUTOMATIK-Betrieb")); int volts = analogRead(LDR); // Wert vom Sensor einlesen lcd.setCursor (0,1); lcd.print(F("LDR:")); lcd.print(volts); // Schwellenwert angeben lcd.print(F(" Dunkel:")); lcd.print(dunkel); lcd.print(" "); if (volts <= dunkel) { byte val = digitalRead(TUERSCHALTER); // lies Input vom Schalter if (val == HIGH) { // wenn der Wert von val gleich HIGH ist if ( verzoegerung_status == 1 && timeElapsed > verzoegerung ) { ClearDisplayLine(2); ClearDisplayLine(3); SchliesseTuer(); delay(1000); verzoegerung_status = 0; timeElapsed = 0; } else if (verzoegerung_status == 0){ // Schwellenwert zum ersten mal erreicht, Verzoegerung an verzoegerung_status = 1; timeElapsed = 0; lcd.setCursor (0,2); lcd.print(F("Verzoegerung ein")); } else if (verzoegerung_status == 1 && timeElapsed < verzoegerung ) { // Zeige restliche Wartezeit auf dem Display an lcd.setCursor (0,3); lcd.print((verzoegerung - timeElapsed)/1000); lcd.print(" "); } } else { // Tuer ist bereits zu delay(1000); verzoegerung_status = 0; timeElapsed = 0; ClearDisplayLine(2); ClearDisplayLine(3); } } else { byte val = digitalRead(TUERSCHALTER); // lies Input vom Schalter if (val == LOW) { // wenn der Wert von val gleich HIGH ist if ( verzoegerung_status == 1 && timeElapsed > verzoegerung ) { ClearDisplayLine(2); ClearDisplayLine(3); OeffneTuer(); delay(1000); verzoegerung_status = 0; timeElapsed = 0; } else if (verzoegerung_status == 0){ // Schwellenwert zum ersten mal erreicht, Verzoegerung an verzoegerung_status = 1; timeElapsed = 0; lcd.setCursor (0,2); lcd.print(F("Verzoegerung ein")); } else if (verzoegerung_status == 1 && timeElapsed < verzoegerung ) { // Zeige restliche Wartezeit auf dem Display an lcd.setCursor (0,3); lcd.print((verzoegerung - timeElapsed)/1000); lcd.print(" "); } } else { // Tuer ist bereits auf delay(1000); verzoegerung_status = 0; timeElapsed = 0; ClearDisplayLine(2); ClearDisplayLine(3); } } last_state = 1; } else if ( modus_status >= 70 && modus_status <= 630 ) { // ---------------------------------------------------------------------------------------------------- Off-Modus if (last_state != 0 ) { lcd.clear(); Serial.println(F("Off-Modus")); } lcd.setCursor (0,0); lcd.print(F("OFF-Modus ")); last_state = 0; verzoegerung_status = 0; // MOTOR-RELAIS aus digitalWrite(MOTOR_RELAIS, HIGH); // Konfig-Aenderungen ueber Taster lcd.setCursor(0,1); // MENUE-UEBERLAUF regeln (Am Ende wieder von Vorne/Hinten anfangen) if (menu_position > MAX_MENU_EINTRAEGE-1 ) { menu_position = 0; } if (menu_position < 0 ) { menu_position = MAX_MENU_EINTRAEGE-1; } // Menuepunkt anzeigen lcd.print(menu[menu_position]); lcd.setCursor(0,3); lcd.print("< ENTER >"); // Links-Knopf if ( digitalRead(KEY_LEFT) == LOW ) { Serial.println(F("Links-Knopf")); menu_position--; delay(100); while( digitalRead(KEY_LEFT) == LOW ) { delay(5);} } // Rechts-Knopf if ( digitalRead(KEY_RIGHT) == LOW ) { Serial.println(F("Rechts-Knopf")); menu_position++; delay(100); while( digitalRead(KEY_RIGHT) == LOW ) { delay(5);} } // Enter wurde gedrueckt, in Konfig-Loop springen if (digitalRead(KEY_ENTER) == LOW ) { Serial.println(F("Enter-Knopf")); KonfigParam(menu_position,menu_inhalt[menu_position][2]); delay (3000); ClearDisplayLine(3); } // Back-Knopf if ( digitalRead(KEY_BACK) == LOW ) { Serial.println(F("Back-Knopf")); } } else if ( modus_status >= 800 ) { if ( last_state != 2 ) // Nur einmal das Ganze hier machen { // ---------------------------------------------------------------------------------------------------- Auf / Zu-Wechselschaltung. Gibt dem Motor eine Zeit lang Strom Serial.println(F("Auf/Zu-Wechsel")); lcd.clear(); lcd.setCursor (0,0); lcd.print(F("AUF/ZU-Wechsel ")); byte val = digitalRead(TUERSCHALTER); // lies Input vom Schalter if (val == HIGH) { // wenn der Wert von val gleich HIGH ist SchliesseTuer(); delay(1000); } else { OeffneTuer(); delay(1000); } } last_state = 2; } } /*----------------------------------------------------------------------------------------------------*/ /* DESC: Schliesst die Stalltuer und initiiert E-Mail *----------------------------------------------------------------------------------------------------*/ void SchliesseTuer() { Serial.println(F("Schliesse Tuer")); lcd.setCursor (0,3); lcd.print(F("......Schliesse Tuer")); // RELAIS einschalten und nach Schalter zu oder längstens definierter Zeit wieder ausschalten digitalWrite(MOTOR_RELAIS, LOW); int x = 0; byte val = digitalRead(TUERSCHALTER); while ( val == HIGH && x < zeit_motor_zu ) { delay(1); x++; val = digitalRead(TUERSCHALTER); } digitalWrite(MOTOR_RELAIS, HIGH); // E-Mail schreiben ZU oder ERROR val = digitalRead(TUERSCHALTER); if ( val == LOW ) { WriteEmail(3); } else { WriteEmail(4); // ERROR E-Mail } ClearDisplayLine(4); } /*----------------------------------------------------------------------------------------------------*/ /* DESC: Oeffnet die Stalltuer und initiiert E-Mail *----------------------------------------------------------------------------------------------------*/ void OeffneTuer() { Serial.println(F("Oeffne Tuer")); lcd.setCursor (0,3); lcd.print(F(".........Oeffne Tuer")); // RELAIS einschalten und nach definierter Zeit wieder ausschalten digitalWrite(MOTOR_RELAIS, LOW); for (int x = 0; x < zeit_motor_auf; x++) { delay(1); } digitalWrite(MOTOR_RELAIS, HIGH); // E-Mail schreiben AUF oder ERROR byte val = digitalRead(TUERSCHALTER); if ( val == LOW ) { WriteEmail(4); // ERROR E-Mail } else { WriteEmail(2); } ClearDisplayLine(4); } /*----------------------------------------------------------------------------------------------------*/ /* DESC: Loescht eine Zeile des Displays. Uerbgeben wird die zu loeschende Zeile *----------------------------------------------------------------------------------------------------*/ void ClearDisplayLine( byte line ){ lcd.setCursor (0,line); lcd.print(" "); } /*----------------------------------------------------------------------------------------------------*/ /* DESC: Fuehrt einen Software-RESET durch / Neustart Arduino * REFERENCE: http://arduino.stackexchange.com/questions/1477/reset-the-an-arduino-uno-by-an-command *----------------------------------------------------------------------------------------------------*/ void(* ResetFunc) (void) = 0; // declare reset fuction at address 0 /*----------------------------------------------------------------------------------------------------*/ /* DESC: Konfig-Modus-Sub * Uebergeben werden der Parameter als Array-Index und die Schrittweite in der geaendert werden soll *----------------------------------------------------------------------------------------------------*/ void KonfigParam( int PARAM, int SCHRITT ) { // Erst wenn Enter-Key wieder losgelassen = entprellen while( digitalRead(KEY_ENTER) == LOW ) { delay(5);} // Wert aus EEPROM-Zelle lesen unsigned long TMP_PARAM_WERT = EEPROMReadLong(menu_inhalt[PARAM][0]); lcd.clear (); lcd.setCursor(0,0); lcd.print(F("KONFIGURATIONSMODUS")); lcd.setCursor(0,3); lcd.print(F("< BACK ENTER >")); lcd.setCursor(0,1); lcd.print(menu[PARAM]); lcd.setCursor(0,2); lcd.print(TMP_PARAM_WERT); do { delay(50); // Links-Knopf if ( digitalRead(KEY_LEFT) == LOW ) { TMP_PARAM_WERT = TMP_PARAM_WERT - SCHRITT; delay(100); while( digitalRead(KEY_LEFT) == LOW ) { delay(5);} ClearDisplayLine(2); lcd.setCursor(0,2); lcd.print(TMP_PARAM_WERT); } // Rechts-Knopf if ( digitalRead(KEY_RIGHT) == LOW ) { TMP_PARAM_WERT = TMP_PARAM_WERT + SCHRITT; delay(100); while( digitalRead(KEY_RIGHT) == LOW ) { delay(5);} ClearDisplayLine(2); lcd.setCursor(0,2); lcd.print(TMP_PARAM_WERT); } // ENTER-Knopf = Speichern if (digitalRead(KEY_ENTER) == LOW) { lcd.setCursor(0,3); lcd.print(F("...Speichere Wert...")); // Neuen Wert Speichern menu_inhalt[PARAM][1] = TMP_PARAM_WERT; // In EEPROM schreiben EEPROMWriteLong(menu_inhalt[PARAM][0],menu_inhalt[PARAM][1]); delay(2000); lcd.home (); // go home lcd.clear(); lcd.print(F(".......RESET........")); delay(5000); ResetFunc(); break; }; } while (digitalRead(KEY_BACK) != LOW); // Abbruch ohne speichern bei BACK-Knopf // Erzwinge Bildschirmneuaufbau last_state = 255; } /*---------------------------------------------------------------------------------*/ /* DESC: Schreibt Speicherwert aus EEPROM * REFERENCE: http://playground.arduino.cc//Code/EEPROMReadWriteLong *---------------------------------------------------------------------------------*/ void EEPROMWriteLong(int address, long value) { //Decomposition from a long to 4 bytes by using bitshift. //One = Most significant -> Four = Least significant byte byte four = (value & 0xFF); byte three = ((value >> 8) & 0xFF); byte two = ((value >> 16) & 0xFF); byte one = ((value >> 24) & 0xFF); //Write the 4 bytes into the eeprom memory. EEPROM.write(address, four); EEPROM.write(address + 1, three); EEPROM.write(address + 2, two); EEPROM.write(address + 3, one); } /*---------------------------------------------------------------------------------*/ /* DESC: Liest Speicherwert aus EEPROM * REFERENCE: http://playground.arduino.cc//Code/EEPROMReadWriteLong *---------------------------------------------------------------------------------*/ long EEPROMReadLong(long address) { //Read the 4 bytes from the eeprom memory. long four = EEPROM.read(address); long three = EEPROM.read(address + 1); long two = EEPROM.read(address + 2); long one = EEPROM.read(address + 3); //Return the recomposed long by using bitshift. return ((four << 0) & 0xFF) + ((three << 8) & 0xFFFF) + ((two << 16) & 0xFFFFFF) + ((one << 24) & 0xFFFFFFFF); } /*---------------------------------------------------------------------------------*/ /* DESC: Ermittelt freien RAM und gibt diesen als int zurueck * REFERENCE:http://jeelabs.org/2011/05/22/atmega-memory-use/ *---------------------------------------------------------------------------------*/ int FreeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); } /*---------------------------------------------------------------------------------*/ /* DESC: Wird aufgerufen wenn die Clientanfrage komplett ist und gibt Ergebnis aus * REFERENCE: http://devicehub.net/dev/tutorials/arduino_sensor_encj2860 *---------------------------------------------------------------------------------*/ static void MyCallback (byte status, word off, word len) { Serial.println(">>>"); Ethernet::buffer[off+300] = 0; Serial.print((const char*) Ethernet::buffer + off); Serial.println("..."); } /*---------------------------------------------------------------------------------*/ /* DESC: Wird aufgerufen wenn eine E-Mail geschrieben werden soll * Siehe Erlaeuterungen oben im Code. Ist nur noch zum Setzen des Flags *---------------------------------------------------------------------------------*/ void WriteEmail(byte param) { Serial.print(F("Setze Mail-Flag ")); Serial.println(param); sendmail = 1; action = param; Serial.println(F("mem")); Serial.println(FreeRam()); }