Skocz do zawartości

Sterownik akwarium zarządzany przez stronę WWW


rziomber

Pomocna odpowiedź

Kolega Robert (SP9-10126-KR) poprosił mnie o wykonanie sterownika do akwarium.

IMG_20171126_125636.thumb.jpg.9b98676c67c149cb203e5156350a86f2.jpgIMG_20171209_193118.thumb.jpg.70eaafd5a43f608d135077db00643ac7.jpg

Założeniem była "symulacja dnia" (płynne rozjaśnianie i ściemnianie oświetlenia LED o świcie i zmierzchu) oraz monitorowanie temperatury wody z alarmem (w razie awarii fabrycznej grzałki z termostatem do akwariów).

Aquarium_controller.thumb.jpg.df524f52cbc89f47b604e53036f1bd8f.jpg

Pracę urządzenia można doglądać za pomocą strony WWW. Parametry odświeżają się na żywo dzięki zastosowaniu technologii AJAX.

akwarium.thumb.jpg.955a07c16ce898973c471d768a34687f.jpg

Jeżeli temperatura nie znajdzie się w ustawionym przedziale zostanie włączony alarm.

IMG_20171222_181001.thumb.jpg.e1f0e55a2142edbc1c95b346edefcde6.jpg

Czas synchronizowany jest automatycznie korzystając z serwera NTP. Ciekowostką jest możliwość konfiguracji nazywy SSID sieci WiFi i hasła do niej przez samego użytkownika poprzez stronę sterująca. Standardowo urządzenie łączy się z siecią WiFi. W przypadku, gdy zapamiętana konfiguracja jest niezgodna z parametrami sieci domowej, wystarczy nacisnąć i przytrzymać przycisk przed podłączeniem do prądu. ESP8266 uruchomi się w trybie Access Pointa, z którym będziemy mogli się połączyć i na stronie http://192.168.1.1 dokonamy poprawek.

Aquarium_controller_webpage.thumb.png.98b98cb3706d49335561a28c077c5f46.png

Po podłączeniu do sieci domowej WiFi możemy też nacisnąć wspomniany przycisk w trakcie pracy urządzenia, co pozwoli na wyświetlenie uzyskanego z DHCP IP. Ułatwi to wejście na stronę konfiguracyjną.

Przy uruchomieniu następuje odczyt ustawień sieci

#include <EEPROM.h>
String ssid = "", pass = "";
[...]
setup(){
   if (ssid == "")
  {
    int i;
    for (i = 100; i < 170; i++)
    {
      if (EEPROM.read(i) == NULL) {
        break;
      }
      ssid += char(EEPROM.read(i));
    }
    for (int ii = i + 1; ii < 250; ii++)
    {
      if (EEPROM.read(ii) == NULL) {
        break;
      }
      pass += char(EEPROM.read(ii));
    }
  } 
  
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid.c_str(), pass.c_str());
  [...]
  
  server.on("/", handleRoot);
  server.on("/sensors", handleSensors);
  server.begin();
}

Za odbieranie danych z formularza WWW i zapisywanie ich w pamięci odpowiada

 void handleRoot() {
   [...]
  if (server.hasArg("SSID") && server.arg("SSID") != "" && server.hasArg("wifipass") && server.arg("wifipass") != "")
  {
    for (i = 0; i < server.arg("SSID").length(); i++)
    {
      EEPROM.write(i + 100, server.arg("SSID")[i]);
    }
    i += 100;
    EEPROM.write(i, NULL);

    for (i = 0; i < server.arg("wifipass").length(); i++)
    {
      EEPROM.write(i + 101 + server.arg("SSID").length(), server.arg("wifipass")[i]);
    }
    EEPROM.write(server.arg("SSID").length() + server.arg("wifipass").length() + 101, NULL);
    EEPROM.commit();
    ESP.restart();
  }
}

Cały kod wygląda następująco:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <WiFiUdp.h>
#include <Time.h>  // http://www.pjrc.com/teensy/td_libs_Time.html
#include <ESP8266mDNS.h>
#include <OneWire.h> //https://github.com/adafruit/ESP8266-Arduino/tree/esp8266/libraries/OneWire
#include <DallasTemperature.h> //https://github.com/milesburton/Arduino-Temperature-Control-Library
#include <Wire.h>
#include <LiquidCrystal_I2C.h> //https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

#define PWMpin 15  //(D8)
#define WiFimodeButton 12 //D6
#define PumpPin 13  //(D7)
#define TempAlarmPin 14  //(D5)
#define ONE_WIRE_BUS 4 //D2

/*char ssid[] = "";  //  your network SSID (name)
  char pass[] = "";       // your network password */
String ssid = "", pass = "";
const char* WiFiHostname = "aquarium";
unsigned char screenCount = 0, tmBegin, tmEnd, dusk, pumpBegin, pumpEnd, tempMin, tempMax;
bool lastTimeLCD, tempAlarm, lastButtonStatus = HIGH;
unsigned int localPort = 2390;      // local port to listen for UDP packets
int PWM = 0;
float temp = 0;
/* Don't hardwire the IP address or we won't get the benefits of the pool.
    Lookup the IP address for the host name instead */
//IPAddress timeServer(129, 6, 15, 28); // time.nist.gov NTP server
IPAddress timeServerIP; // time.nist.gov NTP server address
const char* ntpServerName = "time.nist.gov";

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
WiFiUDP udp;
ESP8266WebServer server(80);
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
LiquidCrystal_I2C lcd(0x27, 16, 2);
tmElements_t tm;
time_t getNtpTime();

unsigned long last_time_sync = 0, last_NTP_check = 0;
void setup()
{
  pinMode(PWMpin, OUTPUT);
  pinMode(WiFimodeButton, INPUT_PULLUP);
  pinMode(TempAlarmPin, OUTPUT);
  Serial.begin(9600);
  EEPROM.begin(512);
  Wire.begin(0, 2); //D2, D4
  Wire.setClock(100000);
  sensors.begin();
  lcd.begin();
  lcd.backlight();
  tmBegin = EEPROM.read(0);
  tmEnd = EEPROM.read(1);
  dusk = EEPROM.read(2);
  pumpBegin = EEPROM.read(3);
  pumpEnd = EEPROM.read(4);
  tempAlarm = EEPROM.read(5);
  tempMin = EEPROM.read(6);
  tempMax = EEPROM.read(7);
  // We start by connecting to a WiFi network
  //  WiFi.mode(WIFI_AP_STA);
  WiFi.softAPdisconnect();
  WiFi.disconnect();

  if (ssid == "")
  {
    int i;
    for (i = 100; i < 170; i++)
    {
      if (EEPROM.read(i) == NULL) {
        break;
      }
      ssid += char(EEPROM.read(i));
    }
    for (int ii = i + 1; ii < 250; ii++)
    {
      if (EEPROM.read(ii) == NULL) {
        break;
      }
      pass += char(EEPROM.read(ii));
    }
  }

  if (digitalRead(WiFimodeButton) == LOW) {
    //access point part
    Serial.println("Creating Accesspoint");
    IPAddress apIP(192, 168, 1, 1);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
    WiFi.softAP("aquarium", "aquarium123");
    Serial.print("IP address:\t");
    Serial.println(WiFi.softAPIP());
  }
  else
  {
    //station part

    Serial.print("connecting to...");
    Serial.println(ssid);
    WiFi.hostname(WiFiHostname);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid.c_str(), pass.c_str());
    /*
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }
    */
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
  }
  /* MDNS.begin("aquarium");
    MDNS.addService("http", "tcp", 80);*/
  Serial.println("Starting UDP");
  udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(udp.localPort());
  server.on("/", handleRoot);
  server.on("/sensors", handleSensors);
  server.begin();
}

void loop()
{
  Serial.println(ssid);
  Serial.println(pass);
  Serial.println(WiFi.status());
  if (millis() - last_time_sync > 10 * 24 * 60 * 60 * 1000 || last_time_sync == 0) {
    unsigned long unixt = unix_time();
    if (unixt > 1512876158) {
      setTime(unixt);
      last_time_sync = millis();
    }
  }
  Serial.print("Time library ");
  Serial.print(current_time(0));
  breakTime(now(), tm);
  if (now() < 1512876158) {
    PWM = 0;
  }

  else if (tmBegin < tmEnd && tm.Hour >= tmBegin && tm.Hour < tmEnd || tmBegin > tmEnd && (tm.Hour >= tmBegin || tm.Hour < tmEnd))
  {
    if (tm.Hour * 60 + tm.Minute < tmBegin * 60 + dusk || tm.Hour * 60 + tm.Minute < tmEnd * 60 - dusk) {
      PWM = 1023 - map(tmBegin * 60 * 60 + dusk * 60 - (tm.Hour * 60 * 60 + tm.Minute * 60  + tm.Second), 0, dusk * 60, 0, 1023);
    }
    if (tm.Hour * 60 + tm.Minute > tmEnd * 60 - dusk) {
      if (tmBegin < tmEnd)
      {
        PWM = 1023 - map(tm.Hour * 60 * 60 + tm.Minute * 60 + tm.Second - (tmEnd * 60 * 60 - dusk * 60), 0, dusk * 60, 0, 1023);
      } else
      {
        PWM = map(tmEnd * 60 * 60 - (tm.Hour * 60 * 60 + tm.Minute * 60 + tm.Second), 0, dusk * 60, 0, 1023);
      }
    }
    if (tm.Hour * 60 + tm.Minute >= tmBegin * 60 + dusk && tm.Hour * 60 + tm.Minute <= tmEnd * 60 - dusk || tmBegin > tmEnd && (tm.Hour * 60 + tm.Minute > tmBegin * 60 + dusk || tm.Hour * 60 + tm.Minute < tmEnd * 60 - dusk))
    {
      PWM = 1023;
    }
  }

  /*else if (tmBegin < tmEnd && tm.Hour > tmBegin && tm.Hour < tmEnd || tmBegin > tmEnd && (tm.Hour > tmBegin || tm.Hour < tmEnd))
    {
    PWM = 1023;
    }
    else if (tm.Hour == tmBegin)
    {
    PWM = map(tm.Minute, 0, 59, 0, 1023);
    }
    else if (tm.Hour == tmEnd)
    {
    PWM = map(59 - tm.Minute, 0, 59, 0, 1023);
    }*/
  else
  {
    PWM = 0;
  }
  analogWrite(PWMpin, PWM);
  Serial.print(" ");
  Serial.println(PWM);
  server.handleClient();

  if (now() % 2 != lastTimeLCD)
  {
    lastTimeLCD = now() % 2;

    sensors.requestTemperatures();

    if (digitalRead(WiFimodeButton) == LOW && lastButtonStatus == HIGH && WiFi.status() == WL_CONNECTED) {
      screenCount++;
    }
    lastButtonStatus = digitalRead(WiFimodeButton);

    temp = sensors.getTempCByIndex(0);
    if (tempAlarm && (temp < (float)tempMin || temp > (float)tempMax)) {
      digitalWrite(TempAlarmPin, HIGH);
    } else {
      digitalWrite(TempAlarmPin, LOW);
    }

    if (tm.Hour >= pumpBegin && tm.Hour < pumpEnd) {
      digitalWrite(PumpPin, HIGH);
    } else {
      digitalWrite(PumpPin, LOW);
    }

    lcd.clear();
    switch (screenCount % 2)
    {
      case 0:
        lcd.print(current_time(1));
        lcd.setCursor(0, 1);
        lcd.print(temp, 1);
        lcd.print("C Day");
        lcd.print(tmBegin);
        lcd.print("-");
        lcd.print(tmEnd);
        break;

      case 1:
        lcd.print(WiFi.localIP().toString());
        break;
    }
  }
}

void handleRoot() {
  int i;
  if (server.hasArg("begin") && server.arg("begin").toInt() >= 0 && server.arg("begin").toInt() <= 24 && server.hasArg("end") && server.arg("end").toInt() >= 0 && server.arg("end").toInt() <= 24 && server.hasArg("dusk") && server.arg("dusk").toInt() >= 0 && server.arg("dusk").toInt() <= 255)
  {
    tmBegin = server.arg("begin").toInt();
    tmEnd = server.arg("end").toInt();
    dusk = server.arg("dusk").toInt();
    EEPROM.write(0, tmBegin);
    EEPROM.write(1, tmEnd);
    EEPROM.write(2, dusk);
    if (server.hasArg("pumpBegin") && server.arg("pumpBegin").toInt() >= 0 && server.arg("pumpBegin").toInt() <= 24 && server.hasArg("pumpEnd") && server.arg("pumpEnd").toInt() >= 0 && server.arg("pumpEnd").toInt() <= 24)
    {
      pumpBegin = server.arg("pumpBegin").toInt();
      pumpEnd = server.arg("pumpEnd").toInt();
      EEPROM.write(3, pumpBegin);
      EEPROM.write(4, pumpEnd);
    }

    //  if (server.arg("tempMin").toInt() >= 0 && server.arg("tempMin").toInt() <= 100 && server.hasArg("tempMax") && server.arg("tempMax").toInt() >= 0 && server.arg("tempMax").toInt() <= 100 && server.arg("tempMax").toInt() > server.arg("tempMin").toInt())
    if (server.arg("tempMin").toInt() >= 0 && server.arg("tempMin").toInt() <= 100 && server.arg("tempMax").toInt() >= 0 && server.arg("tempMax").toInt() <= 100 && server.arg("tempMax").toInt() > server.arg("tempMin").toInt())
    {
      tempAlarm = server.arg("tempAlarm").toInt();
      tempMin = server.arg("tempMin").toInt();
      tempMax = server.arg("tempMax").toInt();
      EEPROM.write(5, tempAlarm);
      EEPROM.write(6, tempMin);
      EEPROM.write(7, tempMax);
    }
    EEPROM.commit();
  }

  if (server.hasArg("SSID") && server.arg("SSID") != "" && server.hasArg("wifipass") && server.arg("wifipass") != "")
  {
    for (i = 0; i < server.arg("SSID").length(); i++)
    {
      EEPROM.write(i + 100, server.arg("SSID")[i]);
    }
    i += 100;
    EEPROM.write(i, NULL);

    for (i = 0; i < server.arg("wifipass").length(); i++)
    {
      EEPROM.write(i + 101 + server.arg("SSID").length(), server.arg("wifipass")[i]);
    }
    //i += 3 + server.arg("wifipass").length();
    EEPROM.write(server.arg("SSID").length() + server.arg("wifipass").length() + 101, NULL);
    EEPROM.commit();
    ESP.restart();
  }
  String content = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"><title>Aquarium controller</title></head><body>";
  /*
    for (i = 2; i < 100; i++)
    {
    if (EEPROM.read(i) == 5) {
      break;
    }
    content += char(EEPROM.read(i));
    }
    content += ";";
    for (int ii = i + 1; ii < 150; ii++)
    {
    if (EEPROM.read(ii) == 5) {
      break;
    }
    content += char(EEPROM.read(ii));
    }
  */
  content += "<DIV style=\"display:table; font-size: large; border-style: solid;\">Current time: <span id=\"time\"></span><BR>Temperature: <span id=\"temp\"></span>°C<BR>PWM: <span id=\"PWM\"></span>%<br>";
  // content += "<div id=\"refresh\"> </div><script>var myVar = setInterval(myTimer, 1000); function myTimer() {var xhttp = new XMLHttpRequest();  xhttp.onreadystatechange = function() {  if (this.readyState == 4 && this.status == 200) {  document.getElementById(\"refresh\").innerHTML =     this.responseText;}  };  xhttp.open(\"GET\", \"sensors\", true);  xhttp.send();}</script>";
  content += "\"Artificial day\" from " + String((int)tmBegin) + ":00 to " + String((int)tmEnd) + ":00. Dusk duration: " + String((int)dusk) + " minutes.</DIV>";
  content += "<script>myTimer();var myVar = setInterval(myTimer, 1000);function myTimer() {var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var str = this.responseText; var values = str.split(\";\"); document.getElementById(\"time\").innerHTML = values[0]; document.getElementById(\"temp\").innerHTML = values[1]; document.getElementById(\"PWM\").innerHTML = values[2]; } }; xhttp.open(\"GET\", \"sensors\", true); xhttp.send();}  var i = 0;</script>";
  content += "<form action='/' method='POST'>Begin: <input type='number' pattern='[0-9]' maxlength='2' size='2' name='begin' value='" + String((int)tmBegin) + "'> UTC<br>";
  content += "End: <input type='number' pattern='[0-9]' maxlength='2' size='2' name='end' value='" + String((int)tmEnd) + "'> UTC<br>";
  content += "Dusk: <input type='number' pattern='[0-9]' maxlength='2' size='2' name='dusk' value='" + String((int)dusk) + "'> minutes<br>";
  content += "Air pump operates between <input type='number' pattern='[0-9]' maxlength='2' size='2' name='pumpBegin' value='" + String((int)pumpBegin) + "'> and <input type='number' pattern='[0-9]' maxlength='2' size='2' name='pumpEnd' value='" + String((int)pumpEnd) + "'><br>";
  content += "<input type=\"checkbox\" name=\"tempAlarm\" value=\"1\"";
  if (tempAlarm) {
    content += " checked";
  }
  content += ">TempAlarm <input type='number' pattern='[0-9]' maxlength='2' size='2' name='tempMin' value='" + String((int)tempMin) + "'> - <input type='number' pattern='[0-9]' maxlength='2' size='2' name='tempMax' value='" + String((int)tempMax) + "'><br>";
  content += "<a href=\"\" onclick=\"i = i+1; if(i % 2 == 0) {wifi.style.display = 'none';} else  {wifi.style.display = 'block';} return false;\">Change WiFi settings</a>";
  content += "<div  id=\"wifi\" style=\"display:none;\">Home WiFi Network Name (SSID): <input type='text' name='SSID' value='" + String(ssid) + "'><br>";
  content += "Password: <input type='password' name='wifipass'></div><br>";
  content += "<input type='submit' value='Submit'></form> <br>";
  content += "</body></html>";
  server.send(200, "text/html", content);
}

void handleSensors() {
  String content = current_time(0) + ";" + String(temp) + ";" + String(100.0 * PWM / 1023.0);
  server.send(200, "text/html", content);
}

String current_time(bool shortstring) {
  breakTime(now(), tm);
  String timeStr;
  if (!shortstring) {
    if (tm.Year > 0) {
      tm.Year -= 30;
    } else {
      tm.Year = 0;
    }
  }
  timeStr = String(tm.Day) + "." + String(tm.Month);
  if (!shortstring) {
    timeStr += "." + String(tm.Year);
  }
  timeStr += " " + String(tm.Hour) + ":";
  if (tm.Minute < 10) {
    timeStr += "0";
  }
  timeStr += String(tm.Minute) + ":";
  if (tm.Second < 10) {
    timeStr += "0";
  }
  timeStr += String(tm.Second) + "UTC";

  return timeStr;
}
// send an NTP request to the time server at the given address
//https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/examples/NTPClient/NTPClient.ino
unsigned long sendNTPpacket(IPAddress& address)
{
  //Serial.println("sending NTP packet...");
  // 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;

  // 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();
}

time_t unix_time() {
  if (WiFi.status() == WL_CONNECTED && ((unsigned long)(millis() - last_NTP_check) > 15000 || last_time_sync == 0)) {
    last_NTP_check = millis();
    //get a random server from the pool
    WiFi.hostByName(ntpServerName, timeServerIP);

    sendNTPpacket(timeServerIP); // send an NTP packet to a time server
    // wait to see if a reply is available
    delay(1000);

    int cb = udp.parsePacket();
    if (!cb) {
      //Serial.println("no packet yet");
    }
    else {
      //Serial.print("packet received, length=");
      //Serial.println(cb);
      // 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;
      //Serial.print("Seconds since Jan 1 1900 = " );
      //Serial.println(secsSince1900);

      // now convert NTP time into everyday time:
      Serial.println("Unix time sync ");
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;
      // subtract seventy years:
      unsigned long epoch = secsSince1900 - seventyYears;
      // print Unix time:
      if (epoch > 1512876158) {
        return epoch + 2;
      } else
      {
        return 0;
      }
    }
  }
}

Składniki:

IMG_20171214_202914.thumb.jpg.a8bd2f2749c68d55c8b6cb63c8b33559.jpg

Edytowano przez rziomber
  • Lubię! 1
Link do komentarza
Share on other sites

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.