Konstantin Kuyukov -

Aquarium Control

UPD. В ходе эксплуатации добавил отдельное питание для шагового двигателя, кормление по расписанию и ребут раз в сутки.

UPD2. Добавлен еще один двигатель для перемешивания корма. Как показала практика корм вокруг сверла слеживается и со временем в аквариум за единицу времени начинает сыпаться существено меньше корма.

-----

Некоторое время назад нам Андрей презентовал аквариум с гуппи и одним сомиком.
Через некоторое время население аквариума выросло, появились растения, в общем образовался новый чудесный минимир.
Всё бы хорошо, да вот системе крайне полезна регулярность: в одно время включать/выключать свет, кормить рыбок.
Тут и появилась идея автоматизировать аквариум.
Вот что из этого получилось:

Фукциональность:

- веб-интерфейс
- включение/выключение фильтра
- включение/выключение помпы
- автовключение фильтра и помпы
- включение/выключение освещения
- автовключение/автовыключение освещения по расписанию
- включение/выключение кормушки
- автовключение/автовыключение кормушки по расписанию
- контроль температуры
- автовключение/автовыключение охлаждения
- синхронизация времени
- LCD дисплей с тачскрином (не реализовано)

 

В качестве мозга системы взял Arduino Uno + ethernet shield.
Через некоторое время пришлось сменить на Ardiuno Mega 2560, так как скетч не помешался в памяти.

Итоговые комплектующие:
- Arduino Mega 2560 R3
- Ethernet Shield
- Цифровой температурный датчик DS18B20 c интерфейсом Dallas 1-Wire, влагозащищенный
- 2x Шаговый двигатель на 5В (28BYJ48) + драйвер (ULN2003)
- Четырехканальный 5В реле-модуль для Arduino
- 2x Блок питания 5V
- Блок питания 12V
- Вентилятор 80мм от системного блока
- Пластиковая коробка, куски пластика
- Сверло 8мм, 12см
- Дюбель 6мм

Корпус для кормушки я сделал основываясь на: http://rukobludov.net/%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F-%D0%BA%D0%BE%D1%80%D0%BC%D1%83%D1%88%D0%BA%D0%B0-%D0%B4%D0%BB%D1%8F-%D1%80%D1%8B%D0%B1/

Куски кода компоновал из исходников из разных источников. Прошу прощения у авторов, что не указал их здесь.

Пины 1-4 реле подключены к пинам 5-8
DS18B20: красный провод - +5В, черный - GND, желтый - пин 9. Между черным и желтым проводом резистор на 4,7кОм.
Пины 1-4 драйвера шагового двигателя подключены к пинам 40-43
Пины 1-4 драйвера второго шагового двигателя (перемешивание) подключены к пинам 44-47

В сетевой фильтр включено: блок питания 5В с usb разъемом, блок питания 5В, блок питания 12В, просто вилка с проводом.
В реле 1,2,4 подключен один провод 220В, он подключен в центральную клемму.
От розеток 1,2 провода в реле 1,2 подключены в нормальнозамкнутом состоянии, то есть в такие клеммы, чтобы питание устройств 220В проходило при выключенном управлении (отсутствии питания от arduino на реле).
В первую розетку подключается фильтр, во вторую - помпу.
В розетку 3 включаем освещение.
От розетки 3 в реле 4 провод подключен в клемму в нормальноразомкнутом состоянии, по умолчанию освещение выключено.
Через реле 3 идёт питание кулера. В него идёт провод от БП 12В опять же в нормальноразомкнутом состоянии.

Фотографии:

IMAG2006

IMAG2009

IMAG2019

IMAG2013

IMAG2014

IMAG2015

После UPD:
IMAG2499

IMAG2501

IMAG2502

ToDo
- Добавить возможность вводить время включения/выключения света
- Добавить возможность вводить время кормления
- Добавить сенсорный экран для вывода информации и управление

Скетч:

#include <Stepper.h>
#include <SD.h>
#include <OneWire.h>
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
//#include <EEPROM.h>

#define stepsPerMotorRevolution 32
#define stepsPerOutputRevolution 32 * 64  //2048  
#define motorSpeed 800
#define motor2Speed 200
#define motorPin1 40
#define motorPin2 41
#define motorPin3 42
#define motorPin4 43
#define motor2Pin1 44
#define motor2Pin2 45
#define motor2Pin3 46
#define motor2Pin4 47
#define dsPin 9 // DS18 Pin
#define ethPin 10
#define SSPin 53
#define SDPin 4
#define filterPin 5
#define airPin 6
#define coolerPin 7
#define lightPin 8

Stepper stepper(stepsPerMotorRevolution, motorPin1, motorPin2, motorPin3, motorPin4);
Stepper stepper2(stepsPerMotorRevolution, motor2Pin1, motor2Pin2, motor2Pin3, motor2Pin4);
OneWire  ds(dsPin);  //
float celsius, fahrenheit;
int randNumber;

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x16 }; // the media access control (ethernet hardware) address for the shield
byte ip[] = { 192, 168, 18, 116 }; //the IP address for the shield
byte gateway[] = { 192, 168, 18, 1 };
byte subnet[] = { 255, 255, 255, 0 };
EthernetServer server(80);

//IPAddress timeServer1(88, 147, 254, 235);
//IPAddress timeServer2(91, 226, 136, 155);
//IPAddress timeServer3(88, 147, 254, 234);
IPAddress timeServer(192, 168, 18, 1);

const long timeZoneOffset = 10800L;
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
// A UDP instance to let us send and receive packets over UDP
EthernetUDP Udp;
unsigned int localPort = 8888;      // local port to listen for UDP packets
boolean NTPSync = false;

const long NTPIntv = 3600000;
const long NTPRepIntv = 60000;
const long filterIntv = 600000;    // Max delay in Filter
const long airIntv = 600000;    // Max delay in Air
const long lightIntv = 60000;    // interval to checking light
const long del = 100000;

long currentTime = 0;
long NTPSyncTime = 0;
long epoch = 0;
long prvLight = 0; // Last check time. Light
long prvNTPSync = 0; // Last NTP sync time
//long prvNTPsend = 0; // Last NTP sync time
long filterSwOffTime = 0; // When filter switch off
long airSwOffTime = 0; // When air switch off
const long softResetIntv = 86400000;

Sd2Card card;
SdVolume volume;
SdFile root;
File myFile;
String buttonsNames[9] = {};
char Filename[15] = "epoch.txt";

int upLightTime = 8;      // switch on (hour)
int downLightTime = 18;     // switch off (hour)
int isNight = 0;    // in switch on at night
boolean lightTimer = true;

boolean feederTimer = true;
boolean feederMustStop = true;
boolean feederON = false;
boolean feeder2Timer = true;
boolean feeder2MustStop = true;
boolean feeder2ON = false;

int feederOnTime[2] = {9, 12};    // time to switch on feeder
int feederOnIntv = 5; // period in minutes
int feeder2OnTime[2] = {8, 11};    // time to switch on feeder
int feeder2OnIntv = 10; // period in minutes

int minTemp = 23; // Switch off Cooler
int maxTemp = 26; // Switch on Cooler

//The number of outputs going to be switched.
int outputQuantity = 4;  //when added to outputLowest result should not exceed 10

//The lowest output pin we are starting from
int outputLowest = 5;    //Should be between 2 to 9

// Variable declaration
int outp = 0;
boolean initialPrint = true;
boolean reading = false;
boolean readInput[10]; //Create a boolean array for the maximum ammount.

void setup()
{
  randomSeed(analogRead(0));
  //Set pins as Outputs
  for (int var = outputLowest; var < outputLowest + outputQuantity; var++)  {
    pinMode(var, OUTPUT);
    digitalWrite(var, 1);
  }
  pinMode(ethPin, OUTPUT);
  pinMode(SSPin, OUTPUT);
  digitalWrite(ethPin, HIGH);
  //digitalWrite(SDPin, HIGH);
  //digitalWrite(ethPin, LOW);

  Serial.begin(9600);
    
  //SD.begin(SDPin);
  if (!SD.begin(SDPin)) {
    Serial.println("SD initialization failed!");
    //return;
  }
  if (!volume.init(card)) {
    Serial.println("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");
    //return;
  }  
  Serial.println("\nFiles found on the card (name, date and size in bytes): ");
  root.openRoot(volume);
  root.ls(LS_R | LS_DATE | LS_SIZE);  
  //myFile = SD.open("epoch.txt", FILE_WRITE);

  // print the type and size of the first FAT-type volume
  //uint32_t volumesize;
  //Serial.print("\nVolume type is FAT");
  //Serial.println(volume.fatType(), DEC);
  //Serial.println();

  //volumesize = volume.blocksPerCluster();    // clusters are collections of blocks
  //volumesize *= volume.clusterCount();       // we'll have a lot of clusters
  //volumesize *= 512;                            // SD card blocks are always 512 bytes
  //Serial.print("Volume size (bytes): ");
  //Serial.println(volumesize);
  //Serial.print("Volume size (Kbytes): ");
  //volumesize /= 1024;
  //Serial.println(volumesize);
  //Serial.print("Volume size (Mbytes): ");
  //volumesize /= 1024;
  //Serial.println(volumesize);

  buttonsNames[5] = "Filter";
  buttonsNames[6] = "Air";
  buttonsNames[7] = "Cooler";
  buttonsNames[8] = "Lamp";

  currentTime = millis();
  if (SD.exists(Filename)) {
    myFile = SD.open(Filename);
    if (myFile) {
      while (myFile.available()) {
        epoch = myFile.read();
        Serial.print("Epoch: ");
        Serial.println(epoch);
      }      
      myFile.close();
    }
  } else {
    myFile = SD.open(Filename,FILE_WRITE);
    myFile.close();
    if (SD.exists(Filename)) {
      Serial.print(Filename);
      Serial.println(" created");
    } else {
      Serial.print(Filename);
      Serial.println(" did not create");      
    }
  }
  //epoch = EEPROM.read(301);

  Ethernet.begin(mac, ip, gateway, subnet);
  server.begin();  
  Udp.begin(localPort);

  EthernetReset();

  stepper.setSpeed(motorSpeed); // set the speed of the motor
  stepper2.setSpeed(motor2Speed); // set the speed of the motor

  Serial.println(F("Setup ready!"));
}

#define BUFSIZ 100 // Buffer for filename
void loop()
{
  currentTime = millis();

  if (currentTime % del == 0 ) {
    Serial.print("currentTime: ");
    Serial.print(currentTime);
    Serial.println(" ");         
  }
 
  if (currentTime > softResetIntv) softReset();

  if (currentTime < prvNTPSync ) prvNTPSync = currentTime; // 00:00:00
  if (currentTime < prvLight ) prvLight = currentTime; // 00:00:00

  if ((currentTime - prvNTPSync >= NTPRepIntv) && (NTPSync == false || currentTime - NTPSyncTime >= NTPIntv)) {
    //if (NTPSync == false && currentTime - NTPSyncTime >= NTPIntv) {
    //EthernetReset();
    //}
    Serial.print("currentTime: ");
    Serial.print(currentTime);
    Serial.println(" ");            
    Serial.print("prvNTPSync: ");
    Serial.print(prvNTPSync);
    Serial.println(" ");        
    //Serial.print("currentTime - prvNTPSync: ");
    //Serial.print((currentTime - prvNTPSync));
    //Serial.println(" ");        
    //Serial.print("NTPSync: ");
    //Serial.print(NTPSync);
    //Serial.println(" ");        
    Serial.print("NTPSyncTime: ");
    Serial.print(NTPSyncTime);
    Serial.println(" ");        
    //Serial.print("currentTime - NTPSyncTime: ");
    //Serial.print((currentTime - NTPSyncTime));    
    //Serial.println(" ");    
    //Serial.print("NTPIntv: ");    
    //Serial.print(NTPRepIntv);    
    //Serial.println(" ");            
    //randNumber = random(1,4);
    //Serial.println(randNumber);
    //if(randNumber == 1){sendNTPpacket(timeServer1);} // send an NTP packet to a time server
    //if(randNumber == 2){sendNTPpacket(timeServer2);} // send an NTP packet to a time server
    //if(randNumber == 3){sendNTPpacket(timeServer3);} // send an NTP packet to a time server
    prvNTPSync = currentTime;    
    sendNTPpacket(timeServer);
  }

  readOutputStatuses(); //Refresh the reading of outputs
  syncTime();
  checkTemp();
  checkForClient(); // listen for incoming clients, and process requests.
  switchCooler();
  switchFeeder(1);
  if (lightTimer == true) {
    if (currentTime - prvLight > lightIntv) {
      prvLight = currentTime;
      switchLight();
    }
  }
}

void checkTemp() {

  byte i;
  byte type_s;
  byte data[12];
  byte addr[8];
  if ( !ds.search(addr)) {
    //Serial.println("No more addresses.");
    //Serial.println();
    ds.reset_search();
    //delay(250);
    return;
  }

  if (OneWire::crc8(addr, 7) != addr[7]) {
    //Serial.println("CRC is not valid!");
    return;
  }
  Serial.println();

  switch (addr[0]) {
    case 0x10:
      //Serial.println("  Chip = DS18S20");
      type_s = 1;
      break;
    case 0x28:
      //Serial.println("  Chip = DS18B20");
      type_s = 0;
      break;
    case 0x22:
      //Serial.println("  Chip = DS1822");
      type_s = 0;
      break;
    default:
      //Serial.println("Device is not a DS18x20 family device.");
      return;
  }

  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1);
  //delay(1000);

  ds.reset();
  ds.select(addr);
  ds.write(0xBE);

  for ( i = 0; i < 9; i++) {
    data[i] = ds.read();
  }

  int16_t raw = (data[1] << 8) | data[0];
  if (type_s) {
    raw = raw << 3;
    if (data[7] == 0x10) {
      raw = (raw & 0xFFF0) + 12 - data[6];
    }
  }
  else {
    byte cfg = (data[4] & 0x60);
    if (cfg == 0x00) raw = raw & ~7;
    else if (cfg == 0x20) raw = raw & ~3;
    else if (cfg == 0x40) raw = raw & ~1;
  }
  celsius =  (float)raw / 16.0;
  fahrenheit = celsius * 1.8 + 32.0;
}

void checkForClient() {

  char clientline[BUFSIZ];
  char *filename;
  int index = 0;

  EthernetClient client = server.available();
  if (client) {

    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    boolean sentHeader = false;

    while (client.connected()) {
      if (client.available()) {

        if (!sentHeader) {

          // send a standard http response header
          client.println(F("HTTP/1.1 200 OK"));
          client.println(F("Content-Type: text/html"));
          client.println(F("Connnection: close"));
          client.println();

          client.println(F("<!DOCTYPE HTML>"));
          client.println(F("<head>"));

          // add page title
          client.println(F("<title>Aquarium's Control</title>"));
          client.println(F("<meta name=\"description\" content=\"Aquarium's Control\"/>"));
          client.println(F("<meta content=\"text/html; charset=UTF-8\" http-equiv=\"content-type\">"));

          // add a meta refresh tag, so the browser pulls again every 10 seconds:
          client.println(F("<meta http-equiv=\"refresh\" content=\"20; url=/\">"));

          // add other browser configuration
          client.println(F("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">"));
          client.println(F("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">"));
          client.println(F("<meta name=\"viewport\" content=\"width=device-width, user-scalable=no\"/>"));

          //inserting the styles data, usually found in CSS files.
          client.println(F("<style type=\"text/css\">"));
          client.println(F(""));

          //This will set how the page will look graphically
          client.println(F("html { height:100%; }"));
          client.println(F("body {"));
          client.println(F("    height: 100%;"));
          client.println(F("    margin: 0;"));
          client.println(F("    font-family: helvetica, sans-serif;"));
          client.println(F("    -webkit-text-size-adjust: none;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F("body {"));
          client.println(F("    -webkit-background-size: 100% 21px;"));
          client.println(F("    background-color: #c5ccd3;"));
          client.println(F("    background-image:"));
          client.println(F("    -webkit-gradient(linear, left top, right top,"));
          client.println(F("    color-stop(.75, transparent),"));
          client.println(F("    color-stop(.75, rgba(255,255,255,.1)) );"));
          client.println(F("    -webkit-background-size: 7px;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".button { width:150px; }"));
          client.println(F(""));
          client.println(F(".view {"));
          client.println(F("    min-height: 100%;"));
          client.println(F("    overflow: auto;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".header-wrapper {"));
          client.println(F("    height: 44px;"));
          client.println(F("    font-weight: bold;"));
          client.println(F("    text-shadow: rgba(0,0,0,0.7) 0 -1px 0;"));
          client.println(F("    border-top: solid 1px rgba(255,255,255,0.6);"));
          client.println(F("    border-bottom: solid 1px rgba(0,0,0,0.6);"));
          client.println(F("    color: #fff;"));
          client.println(F("    background-color: #8195af;"));
          client.println(F("    background-image:"));
          client.println(F("    -webkit-gradient(linear, left top, left bottom,"));
          client.println(F("    from(rgba(255,255,255,.4)),"));
          client.println(F("    to(rgba(255,255,255,.05)) ),"));
          client.println(F("    -webkit-gradient(linear, left top, left bottom,"));
          client.println(F("    from(transparent),"));
          client.println(F("    to(rgba(0,0,64,.1)) );"));
          client.println(F("    background-repeat: no-repeat;"));
          client.println(F("    background-position: top left, bottom left;"));
          client.println(F("    -webkit-background-size: 100% 21px, 100% 22px;"));
          client.println(F("    -webkit-box-sizing: border-box;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".header-wrapper h1 {"));
          client.println(F("    text-align: center;"));
          client.println(F("    font-size: 20px;"));
          client.println(F("    line-height: 44px;"));
          client.println(F("    margin: 0;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".group-wrapper {"));
          client.println(F("    margin: 9px;"));
          client.println(F("    }"));
          client.println(F(""));
          client.println(F(".group-wrapper h2 {"));
          client.println(F("    text-align: center;"));
          client.println(F("    color: #4c566c;"));
          client.println(F("    font-size: 17px;"));
          client.println(F("    line-height: 0.8;"));
          client.println(F("    font-weight: bold;"));
          client.println(F("    text-shadow: #fff 0 1px 0;"));
          client.println(F("    margin: 20px 10px 12px;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".group-wrapper h3 {"));
          client.println(F("    color: #4c566c;"));
          client.println(F("    font-size: 12px;"));
          client.println(F("    line-height: 1;"));
          client.println(F("    font-weight: bold;"));
          client.println(F("    text-shadow: #fff 0 1px 0;"));
          client.println(F("    margin: 20px 10px 12px;"));
          client.println(F("   }"));
          client.println(F(""));
          client.println(F(".group-wrapper table {"));
          client.println(F("    background-color: #fff;"));
          client.println(F("    -webkit-border-radius: 10px;"));

          client.println(F("    -moz-border-radius: 10px;"));
          client.println(F("    -khtml-border-radius: 10px;"));
          client.println(F("    border-radius: 10px;"));

          client.println(F("    font-size: 17px;"));
          client.println(F("    line-height: 20px;"));
          client.println(F("    margin: 9px 0 20px;"));
          client.println(F("    border: solid 1px #a9abae;"));
          client.println(F("    padding: 11px 3px 12px 3px;"));
          client.println(F("    margin-left:auto;"));
          client.println(F("    margin-right:auto;"));

          client.println(F("    -moz-transform :scale(1);")); //Code for Mozilla Firefox
          client.println(F("    -moz-transform-origin: 0 0;"));



          client.println(F("   }"));
          client.println(F(""));


          //how the green (ON) LED will look
          client.println(F(".green-circle {"));
          client.println(F("    display: block;"));
          client.println(F("    height: 23px;"));
          client.println(F("    width: 23px;"));
          client.println(F("    background-color: #0f0;"));
          //client.println(F("    background-color: rgba(60, 132, 198, 0.8);"));
          client.println(F("    -moz-border-radius: 11px;"));
          client.println(F("    -webkit-border-radius: 11px;"));
          client.println(F("    -khtml-border-radius: 11px;"));
          client.println(F("    border-radius: 11px;"));
          client.println(F("    margin-left: 1px;"));

          client.println(F("    background-image: -webkit-gradient(linear, 0% 0%, 0% 90%, from(rgba(46, 184, 0, 0.8)), to(rgba(148, 255, 112, .9)));@"));
          client.println(F("    border: 2px solid #ccc;"));
          client.println(F("    -webkit-box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px;"));
          client.println(F("    -moz-box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px; /* FF 3.5+ */"));
          client.println(F("    box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px; /* FF 3.5+ */"));

          client.println(F("    }"));
          client.println(F(""));

          //how the black (off)LED will look
          client.println(F(".black-circle {"));
          client.println(F("    display: block;"));
          client.println(F("    height: 23px;"));
          client.println(F("    width: 23px;"));
          client.println(F("    background-color: #040;"));
          client.println(F("    -moz-border-radius: 11px;"));
          client.println(F("    -webkit-border-radius: 11px;"));
          client.println(F("    -khtml-border-radius: 11px;"));
          client.println(F("    border-radius: 11px;"));
          client.println(F("    margin-left: 1px;"));
          client.println(F("    -webkit-box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px;"));
          client.println(F("    -moz-box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px; /* FF 3.5+ */"));
          client.println(F("    box-shadow: rgba(11, 140, 27, 0.5) 0px 10px 16px; /* FF 3.5+ */"));
          client.println(F("    }"));
          client.println(F(""));

          //this will add the glare to both of the LEDs
          client.println(F("   .glare {"));
          client.println(F("      position: relative;"));
          client.println(F("      top: 1;"));
          client.println(F("      left: 5px;"));
          client.println(F("      -webkit-border-radius: 10px;"));
          client.println(F("      -moz-border-radius: 10px;"));
          client.println(F("      -khtml-border-radius: 10px;"));
          client.println(F("      border-radius: 10px;"));
          client.println(F("      height: 1px;"));
          client.println(F("      width: 13px;"));
          client.println(F("      padding: 5px 0;"));
          client.println(F("      background-color: rgba(200, 200, 200, 0.25);"));
          client.println(F("      background-image: -webkit-gradient(linear, 0% 0%, 0% 95%, from(rgba(255, 255, 255, 0.7)), to(rgba(255, 255, 255, 0)));"));
          client.println(F("    }"));
          client.println(F(""));


          //and finally this is the end of the style data and header
          client.println(F("</style>"));
          client.println(F("</head>"));

          //now printing the page itself
          client.println(F("<body>"));
          client.println(F("<div class=\"view\">"));
          client.println(F("    <div class=\"header-wrapper\">"));
          client.println(F("      <h1>Aquarium's Control</h1>"));
          client.println(F("    </div>"));
          client.println(F("<div class=\"group-wrapper\">"));
          client.print(F("<h2>Time: "));
          client.print(((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 86400L) / 3600);
          client.print(F(":"));
          client.print(((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 3600) / 60);
          client.print(F(":"));
          client.print((epoch + currentTime / 1000 - NTPSyncTime / 1000) % 60);
          client.println(F("</h2>"));
          client.print(F("<h2>NTP last sync: "));
          client.print(((epoch + NTPSyncTime / 1000)  % 86400L) / 3600);
          client.print(F(":"));
          client.print(((epoch + NTPSyncTime / 1000)  % 3600) / 60);
          client.print(F(":"));
          client.print((epoch + NTPSyncTime / 1000) % 60);
          client.println(F("</h2>"));
          client.print(F("<h2>Temperature: "));
          client.print(celsius);
          client.print(F("C</h2>"));
          client.println(F("    </div>"));
          client.println();
          //This is for the arduino to construct the page on the fly.
          sentHeader = true;
        }

        char c = client.read();

        if (reading && c == ' ') {
          reading = false;
        }

        if (c == '?') {
          reading = true; //found the ?, begin reading the info
        }

        if (reading) {
          if (c == 'A') {
            lightTimer = true;
          }
          if (c == 'B') {
            lightTimer = false;
            //Serial.println("false");
          }
          if (c == 'C') {
            feederTimer = true;
          }
          if (c == 'D') {
            feederTimer = false;
          }
          if (c == 'E') {
            feederMustStop = false;
            feederON = true;
          }
          if (c == 'F') {
            feederMustStop = true;
            feederON = false;
          }
          if (c == 'G') {
            outp = 0;
          }
          if (c == 'H') {
            outp = 1;
          }
          if (c == 'J') {
            feeder2Timer = true;
          }
          if (c == 'K') {
            feeder2Timer = false;
          }
          if (c == 'L') {
            feeder2MustStop = false;
            feeder2ON = true;
          }
          if (c == 'M') {
            feeder2MustStop = true;
            feeder2ON = false;
          }
          Serial.print(c);   //print the value of c to serial communication
          //Serial.print(outp);
          //Serial.print('\n');

          switch (c) {
            case '2':
              //add code here to trigger on 2
              triggerPin(2, client, outp);
              break;
            case '3':
              //add code here to trigger on 3
              triggerPin(3, client, outp);
              break;
            case '4':
              //add code here to trigger on 4
              triggerPin(4, client, outp);
              break;
            case '5':
              //add code here to trigger on 5
              triggerPin(5, client, outp);
              //printHtml(client);
              break;
            case '6':
              //add code here to trigger on 6
              triggerPin(6, client, outp);
              break;
            case '7':
              //add code here to trigger on 7
              triggerPin(7, client, outp);
              break;
            case '8':
              //add code here to trigger on 8
              triggerPin(8, client, outp);
              break;
            case '9':
              //add code here to trigger on 9
              triggerPin(9, client, outp);
              break;
          }
        }
        if (c == '\n' && currentLineIsBlank) {
          readOutputStatuses(); //Refresh the reading of outputs
          printHtmlButtons(client);
          Serial.println("");
          break;
        }
      }
    }

    client.println(F("</div>\n</div>\n</body>\n</html>"));

    delay(1); // give the web browser time to receive the data
    client.stop(); // close the connection:

  }

}



void triggerPin(int pin, EthernetClient client, int outp) {
  //Switching on or off outputs
  if (outp == 1) {
    digitalWrite(pin, HIGH);
  }
  if (outp == 0) {
    digitalWrite(pin, LOW);
  }
  if (pin == lightPin) {
    lightTimer = false;
    Serial.println("false");
  }
  if (pin == filterPin) {
    filterSwOffTime = currentTime;
  }
  if (pin == airPin) {
    airSwOffTime = currentTime;
  }
  //Serial.println(pin);
}


//print the html buttons to switch on/off channels
void printHtmlButtons(EthernetClient client) {

  //Start to create the html table
  client.println("");
  client.println(F("<FORM>"));
  client.println(F("<table border=\"0\" align=\"center\">"));

  //Start printing button by button
  for (int var = outputLowest; var < outputLowest + outputQuantity; var++)  {

    //Print begining of row
    client.print("<tr>\n");

    //Prints the ON Buttons
    client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - "));
    client.print(buttonsNames[var]);
    if (var == filterPin || var == airPin) {
      client.print(F("\" onClick=\"parent.location='/?H"));
    } else {
      client.print(F("\" onClick=\"parent.location='/?G"));
    }
    client.print(var);
    client.print(F("'\"></td>\n"));

    //Prints the OFF Buttons
    client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - "));
    client.print(buttonsNames[var]);
    if (var == filterPin || var == airPin) {
      client.print(F("\" onClick=\"parent.location='/?G"));
    } else {
      client.print(F("\" onClick=\"parent.location='/?H"));
    }
    client.print(var);
    client.print(F("'\"></td>\n"));

    if (var == filterPin || var == airPin) {
      //Print first part of the Circles or the LEDs
      if (readInput[var] == true) {
        client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
      } else {
        client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
      }
    } else {
      //Print first part of the Circles or the LEDs
      if (readInput[var] == false) {
        client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
      } else {
        client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
      }
    }

    //Print end of row
    client.print("</tr>\n");

  }

  //Print begining of row
  client.print("<tr>\n");

  //Prints the ON Buttons
  client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - Light Timer"));
  client.print(F("\" onClick=\"parent.location='/?A"));
  client.print(F("'\"></td>\n"));

  //Prints the OFF Buttons
  client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - Light Timer"));
  client.print(F("\" onClick=\"parent.location='/?B"));
  client.print(F("'\"></td>\n"));

  //Print first part of the Circles or the LEDs
  if (lightTimer == true) {
    client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
  } else {
    client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
  }

  //Print end of row
  client.print("</tr>\n");

  //Print begining of row
  client.print("<tr>\n");

  //Prints the ON Buttons
  client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - Feeder"));
  client.print(F("\" onClick=\"parent.location='/?E"));
  client.print(F("'\"></td>\n"));

  //Prints the OFF Buttons
  client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - Feeder"));
  client.print(F("\" onClick=\"parent.location='/?F"));
  client.print(F("'\"></td>\n"));

  //Print first part of the Circles or the LEDs
  if (feederON == true) {
    client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
  } else {
    client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
  }

  //Print end of row
  client.print("</tr>\n");

  //Print begining of row
  client.print("<tr>\n");

  //Prints the ON Buttons
  client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - Feeder Timer"));
  client.print(F("\" onClick=\"parent.location='/?C"));
  client.print(F("'\"></td>\n"));

  //Prints the OFF Buttons
  client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - Feeder Timer"));
  client.print(F("\" onClick=\"parent.location='/?D"));
  client.print(F("'\"></td>\n"));

  //Print first part of the Circles or the LEDs
  if (feederTimer == true) {
    client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
  } else {
    client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
  }

  //Print end of row
  client.print("</tr>\n");

  //Print begining of row
  client.print("<tr>\n");

  //Prints the ON Buttons
  client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - Feeder2"));
  client.print(F("\" onClick=\"parent.location='/?L"));
  client.print(F("'\"></td>\n"));

  //Prints the OFF Buttons
  client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - Feeder2"));
  client.print(F("\" onClick=\"parent.location='/?M"));
  client.print(F("'\"></td>\n"));

  //Print first part of the Circles or the LEDs
  if (feeder2ON == true) {
    client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
  } else {
    client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
  }

  //Print end of row
  client.print("</tr>\n");

  //Print begining of row
  client.print("<tr>\n");

  //Prints the ON Buttons
  client.print(F(" <td><INPUT class='button' id='on' TYPE=\"button\" VALUE=\"ON - Feeder 2 Timer"));
  client.print(F("\" onClick=\"parent.location='/?J"));
  client.print(F("'\"></td>\n"));

  //Prints the OFF Buttons
  client.print(F(" <td><INPUT class='button' id='off' TYPE=\"button\" VALUE=\"OFF - Feeder 2 Timer"));
  client.print(F("\" onClick=\"parent.location='/?K"));
  client.print(F("'\"></td>\n"));

  //Print first part of the Circles or the LEDs
  if (feeder2Timer == true) {
    client.print(F(" <td><div class='green-circle'><div class='glare'></div></div></td>\n"));
  } else {
    client.print(F(" <td><div class='black-circle'><div class='glare'></div></div></td>\n"));
  }

  //Print end of row
  client.print("</tr>\n");  

  //Closing the table and form
  client.println(F("</table>"));
  client.println(F("</FORM>"));

}

//Reading the Output Statuses
void readOutputStatuses() {
  for (int var = outputLowest; var < outputLowest + outputQuantity; var++)  {
    readInput[var] = digitalRead(var);
  }

}

// send an NTP request to the time server at the given address
unsigned long sendNTPpacket(IPAddress& address)
{
  Serial.println(F("NTPpacket sending"));
  //Ethernet.maintain();
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
  delay(1);
  //Udp.stop();
  Serial.println(F("NTPpacket sended"));
}

void syncTime() {
  if ( Udp.parsePacket() ) {
    Serial.println(F("UDP packet received"));
    // We've received a packet, read the data from it
    Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

    //the timestamp starts at byte 40 of the received packet and is four bytes,
    // or two words, long. First, esxtract the two words:

    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    // combine the four bytes (two words) into a long integer
    // this is NTP time (seconds since Jan 1 1900):
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    // now convert NTP time into everyday time:
    // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    const unsigned long seventyYears = 2208988800UL;
    // subtract seventy years:
    epoch = secsSince1900 - seventyYears + timeZoneOffset;
    //Serial.println(epoch);    
    NTPSyncTime = currentTime;
    NTPSync = true;    
    Serial.print(F("Time: "));
    Serial.print(((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 86400L) / 3600);
    Serial.print(F(":"));
    Serial.print(((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 3600) / 60);
    Serial.print(F(":"));
    Serial.println((epoch + currentTime / 1000 - NTPSyncTime / 1000) % 60);
    EthernetReset();
  }
}

void EthernetReset() {
    //Udp.stop();
    //Ethernet.begin(mac, ip, gateway, subnet);
    //server.begin();    
    //Udp.begin(localPort);  
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    // Initialize values needed to form NTP request
    // (see URL above for details on the packets)
    packetBuffer[0] = 0b11100011;   // LI, Version, Mode
    packetBuffer[1] = 0;     // Stratum, or type of clock
    packetBuffer[2] = 6;     // Polling Interval
    packetBuffer[3] = 0xEC;  // Peer Clock Precision
    // 8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]  = 49;
    packetBuffer[13]  = 0x4E;
    packetBuffer[14]  = 49;
    packetBuffer[15]  = 52;    
}

void switchFilter () {    // Switch ON Filter
  if (currentTime > filterIntv + filterSwOffTime) {
    digitalWrite(filterPin, HIGH);
  }
}

void switchAir () {    // Switch ON Air
  if (currentTime > airIntv + airSwOffTime) {
    digitalWrite(airPin, HIGH);
  }
}

void switchCooler () {    // Switch ON/OFF Cooler
  if (celsius > maxTemp) {
    digitalWrite(coolerPin, LOW);
  }
  if (celsius < minTemp) {
    digitalWrite(coolerPin, HIGH);
  }
}

void switchFeeder (float num) {    // Switch ON/OFF Feeder
  int hr = (((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 86400L) / 3600);
  int m = (((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 3600) / 60);
  int stepsNum = stepsPerOutputRevolution  * num;  
  if ((hr == feederOnTime[0] || hr == feederOnTime[1]) && m >= 0 && m < feederOnIntv) {   // check interval
    feederON = true;
  } else {
    if (feederMustStop == true) {
      feederON = false;
    }
  }
  if (feederON == true) {
    //Serial.println(F("Feeder ON"));
    stepper.step(-stepsNum);
  }
  if ((hr == feeder2OnTime[0] || hr == feeder2OnTime[1]) && m >= 0 && m < feeder2OnIntv) {   // check interval
    feeder2ON = true;
  } else {
    if (feeder2MustStop == true) {
      feeder2ON = false;
    }
  }
  if (feeder2ON == true) {
    //Serial.println(F("Feeder ON"));
    stepper2.step(-stepsNum);
  }  
}


void switchLight () {    // Switch ON/OFF light
  //Serial.println(F("switchLight"));
  int hr = (((epoch + currentTime / 1000 - NTPSyncTime / 1000)  % 86400L) / 3600);
  //Serial.println(hr);
  int isLght = 0;
  if (isNight == 0) {  // if day
    if (hr >= upLightTime && hr < downLightTime) {   // check interval
      isLght = 1;
    } else {
      isLght = 0;
    }
  } else {    // if night
    if (hr - upLightTime >= 0) {
      isLght = 1;
    } else {
      if (hr < downLightTime) {
        isLght = 1;
      } else {
        isLght = 0;
      }
    }
  }
  if ((isLght == 1) && (readInput[lightPin] == 1)) {
    digitalWrite(lightPin, LOW);
  }
  if (isLght == 0 && readInput[lightPin] == 0) {
    digitalWrite(lightPin, HIGH);
  }
}

void softReset() {
  //Serial.print("currentTime: ");
  //Serial.print(currentTime);
  //Serial.println(" ");    
  //Serial.print("softResetIntv: ");
  //Serial.print(softResetIntv);
  //Serial.println(" ");      
  //Serial.println(F("softReset"));
  //EEPROM.write(301, epoch + currentTime / 1000 - NTPSyncTime / 1000);
  //SD.remove(Filename);
  myFile = SD.open(Filename, FILE_WRITE);
  if (myFile) {
    myFile.println(epoch);
    myFile.close();
  } else {
    Serial.println(F("Cannot open file"));
  }
  delay(100);
  asm volatile ("  jmp 0");
}

Konstantin Kuyukov, 2012-2018