491 lines
13 KiB
C++
491 lines
13 KiB
C++
#include <Arduino.h>
|
|
#include <DallasTemperature.h>
|
|
#include <ESP8266WiFi.h>
|
|
#include <ESPAsyncTCP.h>
|
|
#include <ESPAsyncWebServer.h>
|
|
#include <ArduinoJson.h>
|
|
#include <AsyncJson.h>
|
|
#include <OneWire.h>
|
|
#include <TaskScheduler.h>
|
|
#include <LittleFS.h>
|
|
#include <time.h>
|
|
|
|
static bool s_wifi_ap_mode = true;
|
|
static String s_wifi_ssid = "bojler";
|
|
static String s_wifi_password = "bojler123";
|
|
|
|
Scheduler scheduler;
|
|
|
|
constexpr int ONE_WIRE_BUS = D2;
|
|
constexpr int VALVE_PIN = D5;
|
|
constexpr int ALARM_PIN = LED_BUILTIN; // TODO: change to separate pin
|
|
|
|
OneWire one_wire(ONE_WIRE_BUS);
|
|
DallasTemperature sensors(&one_wire);
|
|
|
|
static float s_temp = 0.0;
|
|
static float s_t0 = 20.0; // Lower threshold
|
|
static float s_t1 = 30.0; // Upper threshold
|
|
static bool s_valve_open = false; // false = closed, true = open
|
|
|
|
static float s_temp_accum = 0.0f;
|
|
static int s_temp_count = 0;
|
|
constexpr int TEMP_INTERVAL = 10000; // 10 seconds
|
|
// constexpr int TEMP_INTERVAL = 1000;
|
|
constexpr int TEMP_AVERAGING_INTERVAL = 6*15; // 15 minutes (6 readings of 10 seconds each)
|
|
// constexpr int TEMP_AVERAGING_INTERVAL = 10;
|
|
|
|
constexpr size_t TEMP_HISTORY_CAPACITY = 4*24*5; // 5 days of 15-minute intervals
|
|
static float s_temp_history[TEMP_HISTORY_CAPACITY] = {0.0};
|
|
static size_t s_temp_history_index = 0;
|
|
static size_t s_temp_history_count = 0;
|
|
|
|
enum LogType : uint8_t
|
|
{
|
|
LOG_INFO = 0,
|
|
LOG_WARNING = 1,
|
|
LOG_ERROR = 2
|
|
};
|
|
|
|
struct LogEntry
|
|
{
|
|
long timestamp;
|
|
String message;
|
|
LogType type;
|
|
};
|
|
|
|
constexpr size_t LOG_CAPACITY = 32;
|
|
static LogEntry s_log[LOG_CAPACITY];
|
|
static size_t s_log_index = 0;
|
|
static size_t s_log_count = 0;
|
|
|
|
static void BlinkLed(uint32_t time)
|
|
{
|
|
digitalWrite(LED_BUILTIN, LOW);
|
|
delay(time);
|
|
digitalWrite(LED_BUILTIN, HIGH);
|
|
delay(time);
|
|
}
|
|
|
|
static void SetupSensorsAndPins()
|
|
{
|
|
// pinMode(ONE_WIRE_BUS, INPUT);
|
|
sensors.begin();
|
|
sensors.setWaitForConversion(true); // Non-blocking mode
|
|
sensors.setResolution(12);
|
|
|
|
// alarm output
|
|
pinMode(ALARM_PIN, OUTPUT);
|
|
digitalWrite(ALARM_PIN, HIGH);
|
|
|
|
// valve input
|
|
pinMode(VALVE_PIN, INPUT_PULLUP);
|
|
}
|
|
|
|
static void LogTempHistory(float temp)
|
|
{
|
|
s_temp_history[s_temp_history_index] = temp;
|
|
|
|
s_temp_history_index = (s_temp_history_index + 1) % TEMP_HISTORY_CAPACITY;
|
|
|
|
if (s_temp_history_count < TEMP_HISTORY_CAPACITY)
|
|
{
|
|
s_temp_history_count++;
|
|
}
|
|
}
|
|
|
|
static inline long GetCurrentTime()
|
|
{
|
|
return (long)time(nullptr);
|
|
}
|
|
|
|
static void LogMessage(LogType type, String message)
|
|
{
|
|
auto& log_entry = s_log[s_log_index];
|
|
log_entry.timestamp = GetCurrentTime();
|
|
log_entry.type = type;
|
|
log_entry.message = std::move(message);
|
|
|
|
s_log_index = (s_log_index + 1) % LOG_CAPACITY;
|
|
|
|
if (s_log_count < LOG_CAPACITY)
|
|
{
|
|
s_log_count++;
|
|
}
|
|
|
|
// #ifdef BOJLER_DEBUG
|
|
Serial.printf("[%s] %s\n",
|
|
(type == LOG_INFO) ? "INFO"
|
|
: (type == LOG_WARNING) ? "WARNING"
|
|
: "ERROR",
|
|
log_entry.message.c_str());
|
|
// #endif
|
|
}
|
|
|
|
static void LogTemp(float temp)
|
|
{
|
|
s_temp_accum += temp;
|
|
s_temp_count++;
|
|
|
|
if (s_temp_count >= TEMP_AVERAGING_INTERVAL)
|
|
{
|
|
float avg_temp = s_temp_accum / s_temp_count;
|
|
LogTempHistory(avg_temp);
|
|
s_temp_accum = 0.0f;
|
|
s_temp_count = 0;
|
|
}
|
|
}
|
|
|
|
static void ReadTemp()
|
|
{
|
|
#ifdef BOJLER_DEBUG
|
|
Serial.println("Measuring temperature...");
|
|
#endif // BOJLER_DEBUG
|
|
|
|
sensors.requestTemperatures();
|
|
|
|
float temp = sensors.getTempCByIndex(0);
|
|
|
|
#ifdef BOJLER_DEBUG
|
|
Serial.printf("Temperature: %.2f °C | %.2f °F\n", temp, DallasTemperature::toFahrenheit(temp));
|
|
#endif // BOJLER_DEBUG
|
|
|
|
if (temp == DEVICE_DISCONNECTED_C)
|
|
{
|
|
LogMessage(LOG_ERROR, "Nepodařilo se přečíst teplotu z čidla.");
|
|
}
|
|
else
|
|
{
|
|
s_temp = temp;
|
|
}
|
|
|
|
LogTemp(s_temp);
|
|
}
|
|
|
|
static void SignalAlarm()
|
|
{
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
digitalWrite(ALARM_PIN, LOW);
|
|
delay(100);
|
|
digitalWrite(ALARM_PIN, HIGH);
|
|
delay(100);
|
|
}
|
|
}
|
|
|
|
static void SignalAlarmIfNeeded()
|
|
{
|
|
if ((s_temp < s_t0 && s_valve_open) || (s_temp > s_t1 && !s_valve_open))
|
|
{
|
|
SignalAlarm();
|
|
}
|
|
}
|
|
|
|
static void ReadValve()
|
|
{
|
|
bool valve_open = digitalRead(VALVE_PIN) == LOW; // LOW = open
|
|
if (valve_open != s_valve_open)
|
|
{
|
|
s_valve_open = valve_open;
|
|
LogMessage(LOG_INFO, valve_open ? "Ventil otevřen." : "Ventil zavřen.");
|
|
}
|
|
|
|
SignalAlarmIfNeeded();
|
|
}
|
|
|
|
static void ReadSensors()
|
|
{
|
|
BlinkLed(100);
|
|
ReadTemp();
|
|
ReadValve();
|
|
}
|
|
|
|
Task temp_task(TEMP_INTERVAL, TASK_FOREVER, &ReadSensors);
|
|
|
|
static void SetupFS()
|
|
{
|
|
if (!LittleFS.begin())
|
|
{
|
|
Serial.println("Error: Failed to mount LittleFS filesystem.");
|
|
}
|
|
}
|
|
|
|
static void SetupLed()
|
|
{
|
|
pinMode(LED_BUILTIN, OUTPUT);
|
|
digitalWrite(LED_BUILTIN, HIGH);
|
|
}
|
|
|
|
static void LoadWiFiConfig()
|
|
{
|
|
if (!LittleFS.exists("/wifi.json"))
|
|
{
|
|
Serial.println("WiFi config file not found. Using default values.");
|
|
return;
|
|
}
|
|
|
|
File file = LittleFS.open("/wifi.json", "r");
|
|
if (!file)
|
|
{
|
|
Serial.println("Failed to open WiFi config file for reading. Using default values.");
|
|
return;
|
|
}
|
|
|
|
size_t size = file.size();
|
|
std::unique_ptr<char[]> buf(new char[size + 1]);
|
|
file.readBytes(buf.get(), size);
|
|
buf[size] = '\0';
|
|
|
|
DynamicJsonDocument doc(256);
|
|
DeserializationError error = deserializeJson(doc, buf.get());
|
|
if (error)
|
|
{
|
|
Serial.print("Failed to parse WiFi config JSON: ");
|
|
Serial.println(error.c_str());
|
|
return;
|
|
}
|
|
|
|
if (doc.containsKey("ap") && doc.containsKey("ssid") && doc.containsKey("password"))
|
|
{
|
|
s_wifi_ap_mode = doc["ap"].as<bool>();
|
|
s_wifi_ssid = doc["ssid"].as<String>();
|
|
s_wifi_password = doc["password"].as<String>();
|
|
Serial.printf("Loaded WiFi config: ap=%d, ssid=%s, password=%s\n", s_wifi_ap_mode, s_wifi_ssid.c_str(), s_wifi_password.c_str());
|
|
}
|
|
else
|
|
{
|
|
Serial.println("WiFi config JSON is missing required keys. Using default values.");
|
|
}
|
|
}
|
|
|
|
static void SetupWiFi()
|
|
{
|
|
LoadWiFiConfig();
|
|
|
|
static WiFiEventHandler s_wifi_connected_handler;
|
|
static WiFiEventHandler s_wifi_got_ip_handler;
|
|
static WiFiEventHandler s_wifi_disconnected_handler;
|
|
|
|
s_wifi_connected_handler = WiFi.onStationModeConnected([](const WiFiEventStationModeConnected& event) {
|
|
LogMessage(LOG_INFO, "Připojeno k síti " + String(s_wifi_ssid) + ", RSSI: " + String(WiFi.RSSI()) + " dBm");
|
|
});
|
|
|
|
s_wifi_got_ip_handler = WiFi.onStationModeGotIP([](const WiFiEventStationModeGotIP& event) {
|
|
LogMessage(LOG_INFO, "Získána IP adresa: " + WiFi.localIP().toString());
|
|
});
|
|
|
|
s_wifi_disconnected_handler = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& event) {
|
|
LogMessage(LOG_WARNING, "Připojení ztraceno. Důvod: " + String(event.reason));
|
|
});
|
|
|
|
if (s_wifi_ap_mode)
|
|
{
|
|
WiFi.mode(WIFI_AP);
|
|
WiFi.softAP(s_wifi_ssid.c_str(), s_wifi_password.c_str());
|
|
}
|
|
else
|
|
{
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(s_wifi_ssid.c_str(), s_wifi_password.c_str());
|
|
WiFi.setAutoReconnect(true);
|
|
|
|
while (WiFi.status() != WL_CONNECTED && WiFi.status() != WL_CONNECT_FAILED)
|
|
{
|
|
BlinkLed(250);
|
|
Serial.print(".");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void LoadThresholds()
|
|
{
|
|
if (!LittleFS.exists("/thresholds.json"))
|
|
{
|
|
Serial.println("Thresholds file not found. Using default values.");
|
|
return;
|
|
}
|
|
|
|
File file = LittleFS.open("/thresholds.json", "r");
|
|
if (!file)
|
|
{
|
|
Serial.println("Failed to open thresholds file for reading.");
|
|
return;
|
|
}
|
|
|
|
size_t size = file.size();
|
|
std::unique_ptr<char[]> buf(new char[size + 1]);
|
|
file.readBytes(buf.get(), size);
|
|
buf[size] = '\0';
|
|
|
|
DynamicJsonDocument doc(256);
|
|
DeserializationError error = deserializeJson(doc, buf.get());
|
|
if (error)
|
|
{
|
|
Serial.print("Failed to parse thresholds JSON: ");
|
|
Serial.println(error.c_str());
|
|
return;
|
|
}
|
|
|
|
if (doc.containsKey("t0") && doc.containsKey("t1"))
|
|
{
|
|
s_t0 = doc["t0"].as<float>();
|
|
s_t1 = doc["t1"].as<float>();
|
|
Serial.printf("Loaded thresholds: t0=%.2f, t1=%.2f\n", s_t0, s_t1);
|
|
}
|
|
}
|
|
|
|
static void SaveThresholds()
|
|
{
|
|
DynamicJsonDocument doc(256);
|
|
doc["t0"] = s_t0;
|
|
doc["t1"] = s_t1;
|
|
|
|
File file = LittleFS.open("/thresholds.json", "w");
|
|
if (!file)
|
|
{
|
|
Serial.println("Failed to open thresholds file for writing.");
|
|
return;
|
|
}
|
|
|
|
if (serializeJson(doc, file) == 0)
|
|
{
|
|
Serial.println("Failed to write thresholds to file.");
|
|
}
|
|
else
|
|
{
|
|
Serial.println("Thresholds saved successfully.");
|
|
}
|
|
|
|
file.flush();
|
|
}
|
|
|
|
inline bool IsValidTemperature(float temp)
|
|
{
|
|
return temp > -50.0f && temp < 150.0f;
|
|
}
|
|
|
|
static void SetThresholds(float t0, float t1)
|
|
{
|
|
if (t0 == s_t0 && t1 == s_t1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
s_t0 = t0;
|
|
s_t1 = t1;
|
|
SaveThresholds();
|
|
LogMessage(LOG_INFO, String("Mezní hodnoty byly nastaveny na: t0=") + String(t0) + ", t1=" + String(t1));
|
|
}
|
|
|
|
// Create AsyncWebServer object on port 80
|
|
AsyncWebServer server(80);
|
|
|
|
static void SetupServer()
|
|
{
|
|
Serial.print("Web Server URL: http://");
|
|
Serial.println(WiFi.localIP());
|
|
|
|
// frontend
|
|
server.on("/", HTTP_GET,
|
|
[](AsyncWebServerRequest* request) { request->send(LittleFS, "/www/index.html", "text/html"); });
|
|
|
|
// /api/status
|
|
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
String valve_state_str = s_valve_open ? "1" : "0";
|
|
String json_str = "{ \"temp\": " + String(s_temp, 2) + ", \"valve\": " + valve_state_str +
|
|
", \"t0\": " + String(s_t0, 2) + ", \"t1\": " + String(s_t1, 2) + " }";
|
|
request->send(200, "application/json", json_str);
|
|
});
|
|
|
|
// /api/history
|
|
server.on("/api/history", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
String json_str = "[";
|
|
for (size_t i = 0; i < s_temp_history_count; ++i)
|
|
{
|
|
size_t index = (s_temp_history_index + TEMP_HISTORY_CAPACITY - s_temp_history_count + i) % TEMP_HISTORY_CAPACITY;
|
|
json_str += String(s_temp_history[index], 2);
|
|
if (i < s_temp_history_count - 1)
|
|
{
|
|
json_str += ",";
|
|
}
|
|
}
|
|
json_str += "]";
|
|
request->send(200, "application/json", json_str);
|
|
});
|
|
|
|
// /api/log
|
|
server.on("/api/log", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
String json_str = "[";
|
|
for (size_t i = 0; i < s_log_count; ++i)
|
|
{
|
|
size_t index = (s_log_index + LOG_CAPACITY - s_log_count + i) % LOG_CAPACITY;
|
|
const LogEntry& entry = s_log[index];
|
|
json_str += "{\"ts\":" + String(entry.timestamp) + ",\"t\":" + String(entry.type) +
|
|
",\"m\":\"" + entry.message + "\"}";
|
|
if (i < s_log_count - 1)
|
|
{
|
|
json_str += ",";
|
|
}
|
|
}
|
|
json_str += "]";
|
|
request->send(200, "application/json", json_str);
|
|
});
|
|
|
|
// /api/thresholds
|
|
auto thresholds_handler = new AsyncCallbackJsonWebHandler("/api/thresholds", [](AsyncWebServerRequest* request, JsonVariant& json) {
|
|
if (json.is<JsonObject>())
|
|
{
|
|
JsonObject obj = json.as<JsonObject>();
|
|
if (obj.containsKey("t0") && obj.containsKey("t1"))
|
|
{
|
|
auto t0 = obj["t0"].as<float>();
|
|
auto t1 = obj["t1"].as<float>();
|
|
|
|
if (IsValidTemperature(t0) && IsValidTemperature(t1) && t0 < t1)
|
|
{
|
|
SetThresholds(t0, t1);
|
|
request->send(200, "application/json", "{ \"status\": \"success\" }");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
request->send(400, "application/json", "{ \"status\": \"error\", \"message\": \"Invalid JSON payload\" }");
|
|
}, 512);
|
|
|
|
server.addHandler(thresholds_handler);
|
|
|
|
server.begin();
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
Serial.begin(115200);
|
|
delay(500);
|
|
Serial.println("INIT...");
|
|
|
|
SetupFS();
|
|
LoadThresholds();
|
|
SetupLed();
|
|
SetupWiFi();
|
|
|
|
// Setup NTP time synchronization
|
|
constexpr int timezone = 3600; // UTC+1
|
|
constexpr int daylightOffset_sec = 3600; // 1 hour in seconds
|
|
configTime(timezone, daylightOffset_sec, "pool.ntp.org", "time.nist.gov");
|
|
|
|
SetupSensorsAndPins();
|
|
SetupServer();
|
|
|
|
// setup tasks
|
|
scheduler.init();
|
|
scheduler.addTask(temp_task);
|
|
temp_task.enable();
|
|
|
|
LogMessage(LOG_INFO, "Inicializace dokončena.");
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
scheduler.execute();
|
|
}
|