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