#include #include #include #include #include #include #include #include #include #include #include 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 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(); s_wifi_ssid = doc["ssid"].as(); s_wifi_password = doc["password"].as(); 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 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(); s_t1 = doc["t1"].as(); 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 obj = json.as(); if (obj.containsKey("t0") && obj.containsKey("t1")) { auto t0 = obj["t0"].as(); auto t1 = obj["t1"].as(); 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(); }