/////////////////////////////////////////////////////////////
// Author:         Gladyshev Dmitriy (2016-2017)
//
// Create Date:    06.11.2016
// Design Name:    Водонагреватель
// Target Devices: ESP8266
// Tool versions:  Arduino IDE 1.6.7 + ESP8266 2.3.0
// Description:    Контроллер для водонагревателя с управлением через WiFi
// Version:        1.0
// Link:           https://19dx.ru/2017/08/esp8266-termostat-dlya-vodonagrevatelya-s-udalyonnym-upravleniem/
/////////////////////////////////////////////////////////////
 
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <OneWire.h>
#include <EEPROM.h>
#include <WiFiUdp.h>
#include <TM1637Display.h>
#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager
 
//-----------------------  Настройка пинов -------------------------
 
#define PIN_RELAY     12       //реле
#define PIN_DS        2        //термодатчик
#define PIN_GREENLED  14       //светодиод "Питание"
#define PIN_BLUELED   16       //светодиод "Нагрев"
 
#define PIN_TM_CLK    4        //TM1637 CLK
#define PIN_TM_DIO    5        //TM1637 DIO
 
#define INTERVAL_UDP_SEND       60000 //интервал отправки данных по UDP в мс
#define EEPROM_INIT_VALUE       11    //Сменить для инициализации EEPROM первоначальными значениями
 
//Название точки доступа и пароль для режима настройки
const char* config_ssid     = "WaterHeater AP";
const char* config_password = "1234567890";
#define CONFIG_TIMEOUT      40    //время работы в режиме настройки при включении
 
//----------------------- адреса EEPROM -------------------------
#define ADDR_SETUP              0
#define ADDR_TEMP               1
#define ADDR_ENABLED            2
#define ADDR_GISTEREZIS         3
#define ADDR_PORT1              4
#define ADDR_PORT2              5
#define ADDR_IP1                6
#define ADDR_IP2                7
#define ADDR_IP3                8
#define ADDR_IP4                9
 
const uint8_t SEG_CONF[] = {
    SEG_A | SEG_F | SEG_E | SEG_D,                   // C
    SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,   // O
    SEG_C | SEG_E | SEG_G,                           // n
    SEG_A | SEG_E | SEG_F | SEG_G                    // F
};
 
const uint8_t SEG_ERR[] = {
    SEG_G,                                           // -
    SEG_A | SEG_E | SEG_F | SEG_G | SEG_D,           // E
    SEG_E | SEG_G,                                   // r
    SEG_E | SEG_G                                    // r
};
 
const uint8_t SEG_LINE[] = {
    SEG_G,                                           // -
    SEG_G,                                           // -
    SEG_G,                                           // -
    SEG_G,                                           // -
};
 
OneWire ds(PIN_DS);
ESP8266WebServer server(80);
WiFiUDP Udp;
TM1637Display display(PIN_TM_CLK, PIN_TM_DIO);
 
float celsius = 0;       //Текущая температура
int TempTarget;          //Целевая температура
bool Enabled;            //Включено/выключено
int Gisterezis;          //Гистерезис
int udpport;             //UDP порт для пересылки данных
byte udpip[4];           //IP для пересылки данных
bool HeaterState = 0;    //Текущее состояние нагревателя
 
bool NeedCommit = false; //Флаг необходимости сохранения настроек в EEPROM
 
bool SensorOK = false;   //Состояние сенсора
bool wifiOK = false;     //Состояние wifi
 
unsigned long ValidSensorTime = 0; //время последнего успешного чтения датчика
 
/*******************************************************************************
* Function Name  : EEPROM_update
* Description    : Запись в EEPROM с предварительной проверкой. Если данные
*                  совпадают, то повторно запись не производится, для экономии
*                  ресурса.
* Input          : address - адрес EEPROM
*                  value - записываемое значение
*******************************************************************************/
 
void EEPROM_update(int address, uint8_t value)
{
  if (EEPROM.read(address) != value)
  {
    EEPROM.write(address, value);
    NeedCommit = true;
  }
}
 
/*******************************************************************************
* Function Name  : EEPROM_commit
* Description    : Commit EEPROM, если это необходимо. ESP8266 не сразу делает
*                  запись в EEPROM, а только по вызову commit()
*******************************************************************************/
 
void EEPROM_commit()
{
  if (NeedCommit)
  {
    EEPROM.commit();
    NeedCommit = false;
  }
}
 
/*******************************************************************************
* Function Name  : handleRoot
* Description    : Обработка GET-запроса /
*                  Информация об устройстве
*******************************************************************************/
 
void handleRoot() 
{
  String message = "WaterHeater v1.0<br>";
  message += "Author: Gladyshev Dmitriy<br>";
  message += "Hardware: ESP8266<br>";
  message += "<a href=\"http://19dx.ru\">http://19dx.ru</a><br>";
  server.send(200, "text/html", message);
}
 
/*******************************************************************************
* Function Name  : handleGet
* Description    : Обработка GET-запроса /get
*                  Получение данных о температурах и состоянии
*******************************************************************************/
 
void handleGet() 
{
  /*
  Строка ответа:
  CurrentTemp | TargetTemp | Enabled | HeaterState | SensorError
 
  где:
  CurrentTemp - текущая температура (NN.N)
  TargetTemp - установленная температура (NN)
  Enabled - включено поддержание температуры (0/1)
  HeaterState - состояние нагрева (нагрев/ожидание = 1/0)
  SensorError - ошибка датчика (0/1)
  */
  String message = String(celsius, 1);
  message += "|";
  message += String(TempTarget);
  message += "|";
  if (Enabled)
  {
    message += "1";
  }
  else
  {
    message += "0";
  }
  message += "|";
  if (HeaterState)
  {
    message += "1";
  }
  else
  {
    message += "0";
  }
  message += "|";
  if (SensorOK)
  {
    message += "0";
  }
  else
  {
    message += "1";
  }
  server.send(200, "text/plain", message);
}
 
/*******************************************************************************
* Function Name  : handleSet
* Description    : Обработка GET-запроса /set
*                  Установка текущей температуре и состоянии
*******************************************************************************/
 
void handleSet()
{
  if (server.args() != 2)
  {
    return;
  }
 
  for (int i=0; i<2; i++)
  {
    if (server.argName(i) == "t")
    {
      String temp = server.arg(i);
      int t = temp.toInt();
      if ((t>=30) && (t<=80))
      {
        TempTarget = t;
        EEPROM_update(ADDR_TEMP, TempTarget);
      }
 
    }
    if (server.argName(i) == "s")
    {
      if (server.arg(i) == "0")
      {
        Enabled = false;
      }
      else
      {
        Enabled = true;
      }
      EEPROM_update(ADDR_ENABLED, Enabled);
    }
  }
  EEPROM_commit();
  server.send(200, "text/plain", "OK");
}
 
/*******************************************************************************
* Function Name  : handleGetSettings
* Description    : Обработка GET-запроса /getset
*                  Получение настроек
*******************************************************************************/
 
void handleGetSettings() 
{
  String message = String(Gisterezis);
  message += "|";
  message += String(udpport);
  for (int i=0; i<4; i++)
  {
    message += "|";
    message += String(udpip[i]);
  }
  server.send(200, "text/plain", message);
}
 
/*******************************************************************************
* Function Name  : handleSetSettings
* Description    : Обработка GET-запроса /setset
*                  Сохранение настроек
*******************************************************************************/
 
void handleSetSettings()
{
  if (server.args() != 6)
  {
    return;
  }
 
  for (int i=0; i<6; i++)
  {
    if (server.argName(i) == "p")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=1) && (p<=65535))
      {
        udpport = p;
        byte t1 = p / 256;
        byte t2 = p % 256;
        EEPROM_update(ADDR_PORT1, t1);
        EEPROM_update(ADDR_PORT2, t2);
      }
 
    }
    if (server.argName(i) == "g")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=5) && (p<=20))
      {
        Gisterezis = p;
        EEPROM_update(ADDR_GISTEREZIS, p);
      }
    }
    if (server.argName(i) == "i1")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=0) && (p<=255))
      {
        udpip[0] = p;
        EEPROM_update(ADDR_IP1, p);
      }
    }
    if (server.argName(i) == "i2")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=0) && (p<=255))
      {
        udpip[1] = p;
        EEPROM_update(ADDR_IP2, p);
      }
    }
    if (server.argName(i) == "i3")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=0) && (p<=255))
      {
        udpip[2] = p;
        EEPROM_update(ADDR_IP3, p);
      }
    }
    if (server.argName(i) == "i4")
    {
      String temp = server.arg(i);
      int p = temp.toInt();
      if ((p>=0) && (p<=255))
      {
        udpip[3] = p;
        EEPROM_update(ADDR_IP4, p);
      }
    }
  }
  EEPROM_commit();
  server.send(200, "text/plain", "OK");
}
 
/*******************************************************************************
* Function Name  : handleNotFound
* Description    : Обработка ошибки 404
*******************************************************************************/
 
void handleNotFound()
{
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET)?"GET":"POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i=0; i<server.args(); i++){
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
 
/*******************************************************************************
* Function Name  : WiFiEvent
* Description    : Обработка изменений состояния Wi-Fi соединения.
*                  Вызывается автоматически.
*******************************************************************************/
 
void WiFiEvent(WiFiEvent_t event) 
{
  Serial.printf("[WiFi-event] event: %d\n", event);
 
  switch(event) {
    case WIFI_EVENT_STAMODE_GOT_IP:
      wifiOK = true;
      Serial.println(F("[WiFi-event] WiFi connected"));
      Serial.print(F("IP address: "));
      Serial.println(WiFi.localIP());
      break;
    case WIFI_EVENT_STAMODE_DISCONNECTED:
      wifiOK = false;
      Serial.println(F("[WiFi-event] WiFi lost connection"));
      break;
  }
}
 
/*******************************************************************************
* Function Name  : RelayControl
* Description    : Управление состоянием реле и светодиодом "Нагрев"
*******************************************************************************/
 
void RelayControl()
{
  static unsigned long TimeBlinkBlue = 0;
  static bool StateBlinkBlue = false;
 
  if (Enabled && SensorOK)
  {
    if (celsius < (TempTarget-Gisterezis))
    {
      digitalWrite(PIN_RELAY, HIGH);
      HeaterState = true;
    }
    else if (celsius > TempTarget)
    {
      digitalWrite(PIN_RELAY, LOW);
      HeaterState = false;
    }
  }
  else
  {
    digitalWrite(PIN_RELAY, LOW);
    HeaterState = false;
  }
 
  //Если включён нагрев, то индикатор "Нагрев" мигает. Иначе не горит.
  if (HeaterState)
  {
    unsigned long delta = millis() - TimeBlinkBlue;
    if (delta >= 500)
    {
      StateBlinkBlue = !StateBlinkBlue;
      digitalWrite(PIN_BLUELED, StateBlinkBlue);
      TimeBlinkBlue = millis();
    }
  }
  else
  {
    digitalWrite(PIN_BLUELED, LOW);
  }
}
 
/*******************************************************************************
* Function Name  : PowerLEDcontrol
* Description    : Управление светодиодом "Питание"
*******************************************************************************/
 
void PowerLEDcontrol()
{
  static unsigned long TimeBlinkGreen = 0;
  static bool StateBlinkGreen = false;
 
  if (SensorOK && wifiOK)
  {
    //в нормальном режиме светодиод просто горит
    digitalWrite(PIN_GREENLED, HIGH);
  }
  else if (!SensorOK)
  {
    //мигание раз в секунду, если ошибка датчика
    unsigned long delta = millis() - TimeBlinkGreen;
    if (delta >= 500)
    {
      StateBlinkGreen = !StateBlinkGreen;
      digitalWrite(PIN_GREENLED, StateBlinkGreen);
      TimeBlinkGreen = millis();
    }
  }
  else
  {
    //мигание раз в 300 мс, если ошибка подключения к Wi-Fi
    unsigned long delta = millis() - TimeBlinkGreen;
    if (delta >= 150)
    {
      StateBlinkGreen = !StateBlinkGreen;
      digitalWrite(PIN_GREENLED, StateBlinkGreen);
      TimeBlinkGreen = millis();
    }
  }
}
 
/*******************************************************************************
* Function Name  : SendUDPCurrentState
* Description    : Отправка температур и состояния по UDP
*******************************************************************************/
 
void SendUDPCurrentState()
{
  /*
  CurrentTemp | TargetTemp | Enabled | HeaterState | SensorError
  */
  static unsigned int TimeSend = 0;
  unsigned int delta = millis() - TimeSend;
  if ( (delta >= INTERVAL_UDP_SEND) && wifiOK )
  {
    String message = String(celsius, 1);
    message += "|";
    message += String(TempTarget);
    message += "|";
    if (Enabled)
    {
      message += "1";
    }
    else
    {
      message += "0";
    }
    message += "|";
    if (HeaterState)
    {
      message += "1";
    }
    else
    {
      message += "0";
    }
    message += "|";
    if (SensorOK)
    {
      message += "0";
    }
    else
    {
      message += "1";
    }
 
    IPAddress ip(udpip[0], udpip[1], udpip[2], udpip[3]);
 
    Udp.beginPacket(ip, udpport);
    Udp.print(message);
    Udp.endPacket();
    TimeSend = millis();
  }
}
 
/*******************************************************************************
* Function Name  : SensorRead
* Description    : Чтение данных с термодатчика
*******************************************************************************/
 
void SensorRead()
{
  static unsigned long TimeDS = 0;
  unsigned long delta = millis() - TimeDS;
  bool SensorFound = true;
 
  if (delta >= 3000)
  {
    byte present = 0;
    byte type_s;
    byte data[12];
    byte addr[8];
 
    if ( !ds.search(addr))
    {
      //Serial.println("1-wire scan ended.");
      ds.reset_search();
      delay(250);
      SensorFound = false;
      //return;
    }
 
    /*Serial.print("ROM =");
    for(int i = 0; i < 8; i++) {
      Serial.write(' ');
      Serial.print(addr[i], HEX);
    }*/
    if (SensorFound)
    {
      if (OneWire::crc8(addr, 7) != addr[7]) {
          Serial.println("CRC is not valid!");
          return;
      }
 
      // первый байт определяет чип
      switch (addr[0]) {
        case 0x10:
          //Serial.println("  Chip = DS18S20");  // or old DS1820
          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);        // start conversion, with parasite power on at the end
 
      //Задержка для конвертации данных датчиком
      unsigned long TimeT = millis();
      while (millis() - TimeT < 1000)
      {
        delay(100);
        RelayControl();
        PowerLEDcontrol();
      }
 
      present = ds.reset();
      ds.select(addr);    
      ds.write(0xBE);         // Read Scratchpad
 
      //Serial.print("  Data = ");
      //Serial.print(present, HEX);
      //Serial.print(" ");
      for (byte i = 0; i < 9; i++) {           // we need 9 bytes
        data[i] = ds.read();
        //Serial.print(data[i], HEX);
        //Serial.print(" ");
      }
      /*Serial.print(" CRC=");
       Serial.print(OneWire::crc8(data, 8), HEX);
       Serial.println();*/
 
      // Convert the data to actual temperature
      // because the result is a 16 bit signed integer, it should
      // be stored to an "int16_t" type, which is always 16 bits
      // even when compiled on a 32 bit processor.
      int16_t raw = (data[1] << 8) | data[0];
      if (type_s) {
        raw = raw << 3; // 9 bit resolution default
        if (data[7] == 0x10) {
          // "count remain" gives full 12 bit resolution
          raw = (raw & 0xFFF0) + 12 - data[6];
        }
      } else {
        byte cfg = (data[4] & 0x60);
        // at lower res, the low bits are undefined, so let's zero them
        if (cfg == 0x00) raw = raw & ~7;  // 9 bit resolution, 93.75 ms
        else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
        else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
        //// default is 12 bit resolution, 750 ms conversion time
      }
      celsius = (float)raw / 16.0;
 
      ValidSensorTime = millis();
    }
 
    unsigned long deltaT = millis() - ValidSensorTime;
    if (deltaT > 15000) //от датчика не было ответа больше 15 секунд
    {
      display.setSegments(SEG_ERR);
      SensorOK = false;
    }
    else
    {
      if (celsius > 5)
      {
        display.showNumberDec((int) celsius, false);
        SensorOK = true;
      }
      else
      {
        display.setSegments(SEG_ERR);
        SensorOK = false;
      }
    }
    TimeDS = millis();
  }
}
 
/*******************************************************************************
* Function Name  : debugOutput
* Description    : Вывод состояния для отладки
*******************************************************************************/
void debugOutput()
{
  static unsigned long dTime = millis();
  unsigned long delta = millis() - dTime;
  if (delta >= 5000)
  {
    Serial.print("Sensor ");
    Serial.print(SensorOK ? "OK ;" : "FAIL ;");
 
    Serial.print(" WiFi ");
    Serial.print(wifiOK ? "OK ;" : "FAIL ;");
 
    Serial.print(" Temp = ");
    Serial.println(celsius);
 
    dTime = millis();
  }
}
 
/*******************************************************************************
* Function Name  : Setup
* Description    : Инициализация устройств
*******************************************************************************/
 
void setup()
{
  Serial.begin(115200);
  delay(10);
  display.setBrightness(3);
 
  WiFiManager wifiManager;
 
  wifiManager.setTimeout(CONFIG_TIMEOUT);
 
  display.setSegments(SEG_CONF);
 
  //Запускаем точку доступа для настройки
  Serial.println();
  Serial.println();
  Serial.println(F("Starting config portal..."));
  wifiManager.startConfigPortal(config_ssid, config_password);
  display.setSegments(SEG_LINE);
  Serial.println(F("Stop config portal."));
  wifiManager.setTimeout(5);
  Serial.print(F("Connecting to Wi-Fi...  "));
  if(!wifiManager.autoConnect(config_ssid, config_password)) {
    Serial.println(F("Failed to connect."));
    wifiOK = false;
    delay(1000);
  }
  else
  {
    wifiOK = true;
    Serial.println(F("WiFi connected"));
    Serial.print(F("IP address: "));
    Serial.println(WiFi.localIP());
  }
  Serial.println();
 
  EEPROM.begin(16);
 
  pinMode(PIN_RELAY, OUTPUT);
  pinMode(PIN_GREENLED, OUTPUT);
  pinMode(PIN_BLUELED, OUTPUT);
  digitalWrite(PIN_RELAY, LOW);
  digitalWrite(PIN_GREENLED, LOW);
  digitalWrite(PIN_BLUELED, LOW);
 
  //Устанавливаем начальные значения в EEPROM
  if (EEPROM.read(ADDR_SETUP) != EEPROM_INIT_VALUE)
  {
    Serial.print(F("Init EEPROM..."));
    EEPROM_update(ADDR_TEMP, 60);
    EEPROM_update(ADDR_ENABLED, 1);
    EEPROM_update(ADDR_GISTEREZIS, 10);
    //10222
    EEPROM_update(ADDR_PORT1, 39); 
    EEPROM_update(ADDR_PORT2, 238);
 
    EEPROM_update(ADDR_IP1, 192);
    EEPROM_update(ADDR_IP2, 168);
    EEPROM_update(ADDR_IP3, 1);
    EEPROM_update(ADDR_IP4, 2);
 
    EEPROM.write(ADDR_SETUP, EEPROM_INIT_VALUE);
    EEPROM.commit();
    Serial.println(F("  completed."));
  }
 
  TempTarget = EEPROM.read(ADDR_TEMP);
  Enabled = EEPROM.read(ADDR_ENABLED);
  Gisterezis = EEPROM.read(ADDR_GISTEREZIS);
  udpport = EEPROM.read(ADDR_PORT1)*256 + EEPROM.read(ADDR_PORT2);
  udpip[0] = EEPROM.read(ADDR_IP1);
  udpip[1] = EEPROM.read(ADDR_IP2);
  udpip[2] = EEPROM.read(ADDR_IP3);
  udpip[3] = EEPROM.read(ADDR_IP4);
 
  WiFi.onEvent(WiFiEvent);
 
  delay(1000);
 
  server.on("/", handleRoot);
  server.on("/get", handleGet);
  server.on("/set", handleSet);
  server.on("/getset", handleGetSettings);
  server.on("/setset", handleSetSettings);
  server.onNotFound(handleNotFound);
  server.begin();
 
  ValidSensorTime = millis();
 
  Udp.begin(udpport);
}
 
void loop()
{
  //Обработка событий сервера
  server.handleClient();
  //Работа с реле и индикаторами
  RelayControl();
  PowerLEDcontrol();
  //Отправка данных по UDP
  SendUDPCurrentState();
  //Замер температуры
  SensorRead();
  //Вывод в консоль для отладки
  debugOutput();
}