bojler-esp/src/main.cpp
2026-09-07 15:32:57 +02:00

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