#include <WiFi.h>
#include <WiFiUdp.h>
#include <EEPROM.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <SSD1306Wire.h>
#include <WiFiClientSecure.h>
#include <ElegantOTA.h>
#include <ArduinoJson.h>
// OLED Display setup
SSD1306Wire display(0x3c, SDA, SCL);
String botToken, chatID, AlamatIP, ssid, password, Home;
// Access Point credentials
const char* ap_ssid = "REPOPIN";
const char* ap_password = NULL;
// Pin definitions
const int PUMP_PIN = 23;
const int BUZZER_PIN = 4;
const int MUTE_BUTTON_PIN = 33;
// Web server for CSV API only
WebServer server(80);
// UDP Configuration
WiFiUDP udp;
unsigned int localUdpPort = 4210;
const unsigned long DATA_TIMEOUT = 60000; // 60 seconds timeout
// EEPROM addresses
#define EEPROM_SIZE 256
#define ADDR_MAX_LEVEL 0
#define ADDR_MIN_LEVEL 6
#define ADDR_CRITICAL_LEVEL 12
#define ADDR_MAX_PUMP_TIME 18
#define ADDR_BUZZER_MUTED 24 // NEW: EEPROM address for buzzer mute state
#define BOT_TOKEN_ADDRESS 38
#define CHAT_ID_ADDRESS 100
#define SSID_ADDR 118
#define PASS_ADDR 152
#define MODE_ADDR 186
// SYSTEM CONSTRAINTS
#define MAX_CRITICAL_LEVEL 120.0f // Maximum allowed critical level in cm (float)
#define MAX_DISTANCE_READING 125.0f // Maximum distance reading - beyond critical level
#define TELEGRAM_REPEAT_INTERVAL 1800000 // 30 minutes in milliseconds (fixed)
#define TELEGRAM_CHECK_INTERVAL 5000 // Check for new messages every 5 seconds
#define STARTUP_ALARM_DELAY 10000 // NEW: 60 seconds (1 minute) delay before alarms can trigger
// Alarm type enum - MUST be declared before variables that use it
enum AlarmType {
NO_ALARM,
CRITICAL_LEVEL,
PUMP_TIMEOUT,
SENSOR_ERROR
};
// System variables - NOW IN SECONDS for easier configuration
float maxWaterLevel = 22.0; // cm from sensor
float minWaterLevel = 80.0; // cm from sensor
float criticalLevel = 120.0; // cm from sensor - LIMITED TO MAX 120cm
unsigned long maxPumpTimeSeconds = 300; // 5 minutes in SECONDS (was 300000 ms)
// Pump control variables - 3 consecutive readings logic
float currentDistance = -1.0;
float lastDistances[3] = { -1.0, -1.0, -1.0 };
int distanceIndex = 0;
int consecutiveReadings = 0;
bool pumpState = false;
bool manualMode = false;
bool alarmState = false;
bool buzzerMuted = false;
bool lastMuteButtonState = HIGH;
bool sensorConnected = false;
// NEW: Track previous alarm state to detect alarm clearing and re-triggering
bool previousAlarmState = false;
AlarmType previousAlarmType = NO_ALARM;
// NEW: System startup time tracking for alarm delay
unsigned long systemStartupTime = 0;
bool startupAlarmDelayActive = true;
unsigned long pumpStartTime = 0;
unsigned long lastTelegramTime = 0;
unsigned long lastDisplayUpdate = 0;
unsigned long pumpBlockedUntil = 0;
unsigned long lastDataReceived = 0;
unsigned long lastTelegramCheck = 0; // New variable for checking Telegram messages
int displayMode = 0;
AlarmType currentAlarm = NO_ALARM;
String alarmMessages[] = {
"System Normal",
"Air Habis",
"Waktu Habis",
"Sensor Error"
};
// NEW: Function to check if startup alarm delay is still active
bool isStartupAlarmDelayActive() {
if (startupAlarmDelayActive && (millis() - systemStartupTime >= STARTUP_ALARM_DELAY)) {
startupAlarmDelayActive = false;
Serial.println("Startup alarm delay period ended - alarms now enabled");
// Send Telegram notification when alarm system becomes active
if (WiFi.status() == WL_CONNECTED && botToken.length() > 0 && chatID.length() > 0) {
sendTelegramNotification("š¢ REPOPIN Alarm System Aktif");
}
}
return startupAlarmDelayActive;
}
// Function to enforce critical level limit
float limitCriticalLevel(float value) {
if (value > MAX_CRITICAL_LEVEL) {
Serial.println("Critical level limited from " + String(value) + " to " + String(MAX_CRITICAL_LEVEL) + " cm");
return MAX_CRITICAL_LEVEL;
}
return value;
}
// Function to enforce distance reading limit
float limitDistanceReading(float distance) {
if (distance > MAX_CRITICAL_LEVEL) {
Serial.println("Distance reading " + String(distance) + " cm limited to " + String(MAX_DISTANCE_READING) + " cm");
return MAX_DISTANCE_READING;
}
return distance;
}
// UDP data receiving function
void handleUDPData() {
int packetSize = udp.parsePacket();
if (packetSize) {
char packetBuffer[10];
int len = udp.read(packetBuffer, sizeof(packetBuffer) - 1);
if (len > 0) {
packetBuffer[len] = '\0';
// Convert received string to float
float receivedDistance = String(packetBuffer).toFloat();
// Validate distance reading
if (receivedDistance >= 0 && receivedDistance <= 500) {
currentDistance = limitDistanceReading(receivedDistance); // Apply distance limit
lastDataReceived = millis();
sensorConnected = true;
// Store distance in consecutive readings array
lastDistances[distanceIndex] = currentDistance;
distanceIndex = (distanceIndex + 1) % 3;
if (consecutiveReadings < 3) {
consecutiveReadings++;
}
Serial.println("UDP received - Distance: " + String(receivedDistance, 1) + " cm, Applied: " + String(currentDistance, 1) + " cm (" + String(consecutiveReadings) + "/3)");
} else if (receivedDistance == -1) {
// Error value received from sender
Serial.println("Sensor error received from UDP");
sensorConnected = false;
currentDistance = -1.0;
} else {
Serial.println("Invalid UDP distance value: " + String(receivedDistance));
}
}
}
}
// Fixed Telegram notification function
void sendTelegramNotification(String message) {
if (WiFi.status() == WL_CONNECTED && botToken.length() > 0 && chatID.length() > 0) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient https;
// Properly encode the message for URL
message.replace(" ", "%20");
message.replace("\n", "%0A");
message.replace("!", "%21");
message.replace("šØ", "%F0%9F%9A%A8"); // Alarm emoji
message.replace("ā
", "%E2%9C%85"); // Check mark emoji
message.replace("ā¹ļø", "%E2%84%B9%EF%B8%8F"); // Info emoji
message.replace("āļø", "%E2%9A%99%EF%B8%8F"); // Settings emoji
message.replace("š", "%F0%9F%94%87"); // Muted speaker emoji
message.replace("š", "%F0%9F%94%8A"); // Speaker emoji
message.replace("š¢", "%F0%9F%9F%A2"); // Green circle emoji
String url = "https://api.telegram.org/bot" + botToken + "/sendMessage?chat_id=" + chatID + "&text=" + message;
https.begin(client, url);
int httpCode = https.GET();
https.end();
}
}
// NEW: Function to check for Telegram commands
void checkTelegramCommands() {
if (WiFi.status() != WL_CONNECTED || botToken.length() == 0 || chatID.length() == 0) {
return;
}
// Only check every 5 seconds to avoid hitting API limits
if (millis() - lastTelegramCheck < TELEGRAM_CHECK_INTERVAL) {
return;
}
lastTelegramCheck = millis();
WiFiClientSecure client;
client.setInsecure();
HTTPClient https;
String url = "https://api.telegram.org/bot" + botToken + "/getUpdates?limit=1&timeout=1";
https.begin(client, url);
int httpCode = https.GET();
if (httpCode == 200) {
String response = https.getString();
// Parse JSON response
DynamicJsonDocument doc(2048);
deserializeJson(doc, response);
if (doc["ok"] && doc["result"].size() > 0) {
JsonObject result = doc["result"][0];
String messageText = result["message"]["text"];
String fromChatId = String((long long)result["message"]["chat"]["id"]);
int updateId = result["update_id"];
// Only process if message is from the configured chat ID
if (fromChatId == chatID && messageText.startsWith("/")) {
processTelegramCommand(messageText);
// Mark message as processed by getting updates with offset
String offsetUrl = "https://api.telegram.org/bot" + botToken + "/getUpdates?offset=" + String(updateId + 1);
HTTPClient offsetHttp;
offsetHttp.begin(client, offsetUrl);
offsetHttp.GET();
offsetHttp.end();
}
}
}
https.end();
}
// ENHANCED: Function to process Telegram commands with mute/unmute support
void processTelegramCommand(String command) {
Serial.println("Processing Telegram command: " + command);
command.toLowerCase();
if (command == "/info") {
String infoMsg = "ā¹ļø Informasi System REPOPIN:\n\n";
infoMsg += "š§ Level Air: " + String(currentDistance >= 0 ? String(currentDistance, 1) + " cm" : "Error") + "\n";
infoMsg += "š§ Mode: " + String(manualMode ? "Manual" : "Auto") + "\n";
infoMsg += "ā” Pompa: " + String(pumpState ? "ON" : "OFF") + "\n";
infoMsg += "š” Sensor: " + String(sensorConnected ? "Connected" : "Offline") + "\n";
infoMsg += "šØ Alarm: " + alarmMessages[currentAlarm] + "\n";
infoMsg += String(buzzerMuted ? "š" : "š") + " Buzzer: " + String(buzzerMuted ? "OFF" : "ON") + "\n";
// NEW: Show startup alarm delay status
if (isStartupAlarmDelayActive()) {
unsigned long remaining = (STARTUP_ALARM_DELAY - (millis() - systemStartupTime)) / 1000;
infoMsg += "ā³ Alarm Delay: " + String(remaining) + " detik\n";
}
infoMsg += "\nāļø Data Pengaturan:\n";
infoMsg += "⢠Level Air Max: " + String(maxWaterLevel, 1) + " cm\n";
infoMsg += "⢠Level Air Min: " + String(minWaterLevel, 1) + " cm\n";
infoMsg += "⢠Level Air Habis: " + String(criticalLevel, 1) + " cm\n";
infoMsg += "⢠Waktu On Pompa: " + String(maxPumpTimeSeconds) + " detik\n";
if (millis() < pumpBlockedUntil) {
unsigned long remaining = (pumpBlockedUntil - millis()) / 60000;
infoMsg += "ā° Pump Di-Stop: " + String(remaining) + " min\n";
}
sendTelegramNotification(infoMsg);
}
else if (command == "/man") {
manualMode = true;
EEPROM.put(MODE_ADDR, manualMode);
EEPROM.commit();
sendTelegramNotification("āļø Mode Pompa Manual");
Serial.println("Manual mode activated via Telegram");
}
else if (command == "/auto") {
manualMode = false;
EEPROM.put(MODE_ADDR, manualMode);
EEPROM.commit();
sendTelegramNotification("āļø Mode Pompa Auto");
Serial.println("Auto mode activated via Telegram");
}
// NEW: Mute command
else if (command == "/mute") {
buzzerMuted = true;
EEPROM.put(ADDR_BUZZER_MUTED, buzzerMuted);
EEPROM.commit();
sendTelegramNotification("š Buzzer Di-OFF");
Serial.println("Buzzer muted via Telegram");
}
// NEW: Unmute command
else if (command == "/unmute") {
buzzerMuted = false;
EEPROM.put(ADDR_BUZZER_MUTED, buzzerMuted);
EEPROM.commit();
sendTelegramNotification("š Buzzer Di-ON");
Serial.println("Buzzer unmuted via Telegram");
}
else if (command == "/on") {
if (manualMode) {
pumpState = true;
pumpStartTime = millis();
digitalWrite(PUMP_PIN, HIGH);
sendTelegramNotification("ā” Pompa ON Manual");
Serial.println("Manual pump turned ON via Telegram");
} else {
sendTelegramNotification("ā Tidak Bisa Kontrol On. Pindah Ke Mode Manual Dulu , Perintah : /man");
}
}
else if (command == "/off") {
if (manualMode) {
pumpState = false;
pumpStartTime = 0;
digitalWrite(PUMP_PIN, LOW);
sendTelegramNotification("ā” Pompa OFF Manual");
Serial.println("Manual pump turned OFF via Telegram");
} else {
sendTelegramNotification("ā Tidak Bisa Kontrol Off. Pindah Ke Mode Manual Dulu , Perintah : /man");
}
}
else if (command.startsWith("/setatas ")) {
String valueStr = command.substring(9);
float newValue = valueStr.toFloat();
// LIMITATION: /setAtas cannot be lower than 22 cm
if (newValue >= 22.0 && newValue < 60) {
maxWaterLevel = newValue;
EEPROM.put(ADDR_MAX_LEVEL, maxWaterLevel);
EEPROM.commit();
sendTelegramNotification("āļø Level Air Max: " + String(maxWaterLevel, 1) + " cm");
Serial.println("Max water level set to " + String(maxWaterLevel) + " cm via Telegram");
} else {
sendTelegramNotification("ā Nilai isian salah. Gunakan: /setAtas <jarak>\nMinimum: 22 cm, Maximum: 60 cm\nContoh: /setAtas 25");
}
}
else if (command.startsWith("/setbawah ")) {
String valueStr = command.substring(10);
float newValue = valueStr.toFloat();
// LIMITATION: /setBawah cannot be higher than 80 cm
if (newValue > 50 && newValue <= 80.0) {
minWaterLevel = newValue;
EEPROM.put(ADDR_MIN_LEVEL, minWaterLevel);
EEPROM.commit();
sendTelegramNotification("āļø Level Air Min: " + String(minWaterLevel, 1) + " cm");
Serial.println("Min water level set to " + String(minWaterLevel) + " cm via Telegram");
} else {
sendTelegramNotification("ā Nilai isian salah. Gunakan: /setBawah <jarak>\nMinimum: 50 cm, Maximum: 80 cm\nContoh: /setBawah 60");
}
}
else if (command.startsWith("/sethabis ")) {
String valueStr = command.substring(11);
float newValue = valueStr.toFloat();
if (newValue > 80 && newValue <= MAX_CRITICAL_LEVEL) {
criticalLevel = limitCriticalLevel(newValue);
EEPROM.put(ADDR_CRITICAL_LEVEL, criticalLevel);
EEPROM.commit();
sendTelegramNotification("āļø Level Air Habis: " + String(criticalLevel, 1) + " cm");
Serial.println("Critical level set to " + String(criticalLevel) + " cm via Telegram");
} else {
sendTelegramNotification("ā Nilai isian salah. Gunakan: /setHabis <jarak>\nMinimum: 50 cm, Maximum: " + String(MAX_CRITICAL_LEVEL) + " cm\nContoh: /setHabis 100");
}
}
else if (command.startsWith("/setwaktu ")) {
String valueStr = command.substring(10);
int newValue = valueStr.toInt();
// LIMITATION: /setWaktu cannot be higher than 1800 seconds (30 minutes)
if (newValue > 0 && newValue <= 1800) {
maxPumpTimeSeconds = newValue;
EEPROM.put(ADDR_MAX_PUMP_TIME, maxPumpTimeSeconds);
EEPROM.commit();
sendTelegramNotification("āļø Batas Waktu On Pompa: " + String(maxPumpTimeSeconds) + " Detik (" + String(maxPumpTimeSeconds/60) + " min)");
Serial.println("Max pump time set to " + String(maxPumpTimeSeconds) + " seconds via Telegram");
} else {
sendTelegramNotification("ā Nilai isian salah. Gunakan: /setWaktu <detik>\nMinimum: 60, Maximum: 1800 (30 min)\nContoh: /setWaktu 300");
}
}
else {
String helpMsg = "š¤ Daftar Perintah REPOPIN :\n\n";
helpMsg += "š Informasi REPOPIN:\n";
helpMsg += "/info - Info System\n\n";
helpMsg += "š§ Mode Pengontrol Pompa:\n";
helpMsg += "/man - Set Pompa Manual mode\n";
helpMsg += "/auto - Set Pompa Auto\n\n";
helpMsg += "ā” Kontrol Pompa Manual:\n";
helpMsg += "/on - Pompa ON Manual\n";
helpMsg += "/off - Pompa OFF Manual\n\n";
helpMsg += "š Kontrol Buzzer:\n";
helpMsg += "/mute - Buzzer OFF\n";
helpMsg += "/unmute - Buzzer ON\n\n";
helpMsg += "āļø Pengaturan:\n";
helpMsg += "/setAtas <jarak> - Set Batas Atas Air (22-" + String(minWaterLevel, 0) + " cm)\n";
helpMsg += "/setBawah <jarak> - Set Batas Bawah Air (" + String(maxWaterLevel, 0) + "-80 cm)\n";
helpMsg += "/setHabis <jarak> - Set Batas Air Habis (max: 120 cm)\n";
helpMsg += "/setWaktu <detik> - Set Batas Waktu Pompa (60-1800 detik)\n\n";
helpMsg += "š Contoh Perintah:\n";
helpMsg += "/setAtas 25\n";
helpMsg += "/setBawah 60\n";
helpMsg += "/setHabis 100\n";
helpMsg += "/setWaktu 300\n";
helpMsg += "/mute\n";
helpMsg += "/unmute";
sendTelegramNotification(helpMsg);
}
}
// Function to send telegram alert for ongoing alarms
void sendTelegramAlert() {
String message = "šØ Alarm REPOPIN :\n";
message += "Status: " + alarmMessages[currentAlarm] + "\n";
message += "Level Air: " + String(currentDistance, 1) + " cm\n";
message += "Pompa: " + String(pumpState ? "ON" : "OFF") + "\n";
message += "Sensor: " + String(sensorConnected ? "Online" : "Offline") + "\n";
message += String(buzzerMuted ? "š" : "š") + " Buzzer: " + String(buzzerMuted ? "OFF" : "ON");
// Send the message
sendTelegramNotification(message);
}
// Function to handle telegram repeat messages for ongoing alarms
void handleTelegramRepeat() {
// Only send repeat messages if alarm is active and 30 minutes have passed
if (alarmState && (millis() - lastTelegramTime > TELEGRAM_REPEAT_INTERVAL)) {
Serial.println("Sending 30-minute repeat Telegram alert");
sendTelegramAlert();
lastTelegramTime = millis();
}
}
void handleRoot() {
if (alarmState) {
Home = String(currentAlarm);
} else {
Home = "OK";
}
server.send(200, "text/plain", Home);
}
void handleJarak() {
String csvData = String(currentDistance) + "," + String(pumpState) + "," + String(alarmState) + "," + String(currentAlarm) + ",end" ;
server.send(200, "text/plain", csvData );
}
void handleApp() {
String csvData = "RPP," + String(maxWaterLevel) + "," + String(minWaterLevel) + "," + String(criticalLevel);
csvData += "," + String(currentDistance) + "," + String(maxPumpTimeSeconds) + "," + String(pumpState) + "," + String(manualMode);
csvData += "," + String(alarmState) + "," + String(currentAlarm) + "," + String(buzzerMuted) + "," + String(sensorConnected) + ",end";
server.send(200, "text/csv", csvData);
}
void handleSet() {
if (server.hasArg("mode")) {
manualMode = server.arg("mode") == "manual";
EEPROM.put(MODE_ADDR, manualMode);
EEPROM.commit();
server.send(200, "text/plain", String(manualMode));
}
if (server.hasArg("buzzer")) {
buzzerMuted = server.arg("buzzer") == "mute";
EEPROM.put(ADDR_BUZZER_MUTED, buzzerMuted); // Save to EEPROM
EEPROM.commit();
server.send(200, "text/plain", String(buzzerMuted));
}
if (server.hasArg("atas")) {
maxWaterLevel = server.arg("atas").toFloat();
EEPROM.put(ADDR_MAX_LEVEL, maxWaterLevel);
EEPROM.commit();
server.send(200, "text/plain", String(maxWaterLevel));
}
if (server.hasArg("bawah")) {
minWaterLevel = server.arg("bawah").toFloat();
EEPROM.put(ADDR_MIN_LEVEL, minWaterLevel);
EEPROM.commit();
server.send(200, "text/plain", String(minWaterLevel));
}
if (server.hasArg("kritis")) {
float requestedCriticalLevel = server.arg("kritis").toFloat();
criticalLevel = limitCriticalLevel(requestedCriticalLevel); // Apply limit
EEPROM.put(ADDR_CRITICAL_LEVEL, criticalLevel);
EEPROM.commit();
server.send(200, "text/plain", String(criticalLevel));
}
if (server.hasArg("waktu")) {
maxPumpTimeSeconds = server.arg("waktu").toInt(); // Now accepts seconds directly
EEPROM.put(ADDR_MAX_PUMP_TIME, maxPumpTimeSeconds);
EEPROM.commit();
server.send(200, "text/plain", String(maxPumpTimeSeconds));
}
if (server.hasArg("token")) {
botToken = server.arg("token");
saveStringToEEPROM(BOT_TOKEN_ADDRESS, botToken, 60);
server.send(200, "text/plain", String(botToken));
}
if (server.hasArg("chat")) {
chatID = server.arg("chat");
saveStringToEEPROM(CHAT_ID_ADDRESS, chatID, 16);
server.send(200, "text/plain", String (chatID));
}
if (server.hasArg("pompa")) {
pumpState = server.arg("pompa") == "on";
server.send(200, "text/plain", String(pumpState));
if (pumpState) {
digitalWrite(PUMP_PIN, HIGH);
} else {
digitalWrite(PUMP_PIN, LOW);
}
}
}
void handleTele() {
String csvData = String(botToken) + "," + String(chatID) + String(botToken) + "," + ",end" ;
server.send(200, "text/csv", csvData);
}
String readStringNewFromEEPROM(int startAddress, int maxLength) {
String value = "";
for (int i = 0; i < maxLength; ++i) {
char c = EEPROM.read(startAddress + i);
if (c == 0 || c == 255) {
break;
}
value += c;
}
return value;
}
void saveStringToEEPROM(int address, const String& str, int maxLength) {
int len = str.length();
if (len >= maxLength) len = maxLength - 1;
for (int i = 0; i < len; i++) {
EEPROM.write(address + i, str[i]);
}
EEPROM.write(address + len, '\0');
EEPROM.commit();
Serial.println("Data stored to EEPROM!");
}
void loadSettings() {
EEPROM.get(ADDR_MAX_LEVEL, maxWaterLevel);
if (isnan(maxWaterLevel)) maxWaterLevel = 25.0;
EEPROM.get(ADDR_MIN_LEVEL, minWaterLevel);
if (isnan(minWaterLevel)) minWaterLevel = 50.0;
EEPROM.get(ADDR_CRITICAL_LEVEL, criticalLevel);
if (isnan(criticalLevel)) {
criticalLevel = 100.0;
} else {
criticalLevel = limitCriticalLevel(criticalLevel); // Apply limit when loading
}
// Load pump time in seconds and set default to 300 seconds (5 minutes)
EEPROM.get(ADDR_MAX_PUMP_TIME, maxPumpTimeSeconds);
if (isnan(maxPumpTimeSeconds) || maxPumpTimeSeconds > 3601) maxPumpTimeSeconds = 300; // 5 minutes in seconds
EEPROM.get(MODE_ADDR, manualMode);
// NEW: Load buzzer mute state from EEPROM
EEPROM.get(ADDR_BUZZER_MUTED, buzzerMuted);
// Load Telegram credentials
botToken = readStringNewFromEEPROM(BOT_TOKEN_ADDRESS, 60);
chatID = readStringNewFromEEPROM(CHAT_ID_ADDRESS, 16);
}
void checkUDPTimeout() {
if (lastDataReceived > 0 && (millis() - lastDataReceived > DATA_TIMEOUT)) {
if (sensorConnected) {
Serial.println("UDP timeout - sensor disconnected");
sensorConnected = false;
currentDistance = -1.0;
consecutiveReadings = 0;
for (int i = 0; i < 3; i++) {
lastDistances[i] = -1.0;
}
}
}
}
void handlePumpControl() {
// Manual pump timeout safety check - prevent pump from running too long in manual mode
if (manualMode && pumpState && pumpStartTime > 0) {
if (millis() - pumpStartTime > (maxPumpTimeSeconds * 1000)) {
digitalWrite(PUMP_PIN, LOW);
pumpState = false;
pumpStartTime = 0;
Serial.println("Manual pump timeout after " + String(maxPumpTimeSeconds) + " seconds");
// Send Telegram notification about manual pump timeout
if (WiFi.status() == WL_CONNECTED && botToken.length() > 0 && chatID.length() > 0) {
sendTelegramNotification("ā ļø Pompa Di-Stop Sementara Sampai " + String(maxPumpTimeSeconds/60) + " Menit");
}
}
}
if (!manualMode) {
// Check if pump is blocked due to timeout
if (millis() < pumpBlockedUntil) {
if (pumpState) {
digitalWrite(PUMP_PIN, LOW);
pumpState = false;
pumpStartTime = 0;
Serial.println("Pump OFF - timeout block active");
}
return;
}
// Skip control if sensor error or insufficient readings
if (currentDistance < 0 || !sensorConnected || consecutiveReadings < 3) {
if (pumpState) {
digitalWrite(PUMP_PIN, LOW);
pumpState = false;
pumpStartTime = 0;
Serial.println("Pump OFF - sensor error");
}
return;
}
// Check if all 3 readings indicate pump should run
bool allReadingsSayPumpOn = true;
bool allReadingsSayPumpOff = true;
for (int i = 0; i < 3; i++) {
if (lastDistances[i] < 0) return; // Invalid reading
if (lastDistances[i] <= minWaterLevel) {
allReadingsSayPumpOn = false;
}
if (lastDistances[i] >= maxWaterLevel) {
allReadingsSayPumpOff = false;
}
}
// Start pump if all 3 readings confirm low water
if (!pumpState && allReadingsSayPumpOn) {
digitalWrite(PUMP_PIN, HIGH);
pumpState = true;
pumpStartTime = millis();
Serial.println("Pump ON - low water confirmed by 3 readings");
}
// Stop pump if all 3 readings confirm high water
else if (pumpState && allReadingsSayPumpOff) {
digitalWrite(PUMP_PIN, LOW);
pumpState = false;
pumpStartTime = 0;
Serial.println("Pump OFF - high water confirmed by 3 readings");
}
// Check pump timeout - Convert seconds to milliseconds for comparison
if (pumpState && pumpStartTime > 0 && (millis() - pumpStartTime > (maxPumpTimeSeconds * 1000))) {
digitalWrite(PUMP_PIN, LOW);
pumpState = false;
pumpStartTime = 0;
pumpBlockedUntil = millis() + 1800000; // Block for 30 minutes (keep in ms for internal timing)
Serial.println("Pump timeout after " + String(maxPumpTimeSeconds) + " seconds - blocked for 30 minutes");
}
}
}
// ENHANCED: Mute button handling with EEPROM persistence
void handleMuteButton() {
bool currentButtonState = digitalRead(MUTE_BUTTON_PIN);
// Detect button press (HIGH to LOW transition)
if (lastMuteButtonState == HIGH && currentButtonState == LOW) {
buzzerMuted = !buzzerMuted;
// Save mute state to EEPROM when changed via button
EEPROM.put(ADDR_BUZZER_MUTED, buzzerMuted);
EEPROM.commit();
Serial.println("Buzzer mute state changed via button: " + String(buzzerMuted ? "MUTED" : "UNMUTED"));
delay(200); // Debounce delay
}
lastMuteButtonState = currentButtonState;
}
void displayWaterLevel() {
if (manualMode){
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "MODE :");
display.drawString(0, 25,"MANUAL");
} else {
// Draw tank representation - enlarged and positioned left for distance display
int tankHeight = 55;
int tankWidth = 50;
int tankX = 10; // Positioned left to make room for distance
int tankY = 5;
// Draw tank outline with double border for better visibility
display.drawRect(tankX, tankY, tankWidth, tankHeight);
display.drawRect(tankX-1, tankY-1, tankWidth+2, tankHeight+2);
// Draw tank base (thicker bottom)
display.drawHorizontalLine(tankX-1, tankY+tankHeight+1, tankWidth+2);
if (currentDistance < 0 || !sensorConnected) {
// Show error message beside tank
display.setFont(ArialMT_Plain_16);
display.drawString(65, 17, "Sensor");
display.drawString(70, 35, "OFF");
// Draw X pattern in tank to indicate error
display.drawLine(tankX+3, tankY+3, tankX+tankWidth-3, tankY+tankHeight-3);
display.drawLine(tankX+tankWidth-3, tankY+3, tankX+3, tankY+tankHeight-3);
return;
}
// Enhanced water level visualization with sensor offset and critical level limit
int waterHeight = 0;
int availableHeight = tankHeight - 4; // Available pixels for water (minus borders)
// Create a visual offset at the top of the tank to represent sensor position
// The sensor is positioned some distance from the tank top
int sensorOffset = 8; // 8 pixels from tank top represents sensor position
int waterDisplayHeight = availableHeight - sensorOffset; // Actual area for water display
// Use the limited critical level (max 120cm) for calculations
float effectiveCriticalLevel = (criticalLevel < MAX_CRITICAL_LEVEL) ? criticalLevel : MAX_CRITICAL_LEVEL;
// Map distance to water level with proper sensor positioning
if (currentDistance <= maxWaterLevel) {
// Water at maximum level - reaches the sensor position (not tank top)
waterHeight = waterDisplayHeight;
} else if (currentDistance >= effectiveCriticalLevel) {
// Tank is empty - no water pixels (uses actual critical level, not the 125cm limit)
waterHeight = 0;
} else {
// Calculate water height based on distance
// The range from maxWaterLevel to effectiveCriticalLevel maps to full water display area
float totalRange = effectiveCriticalLevel - maxWaterLevel; // Total distance range
float waterRange = effectiveCriticalLevel - currentDistance; // Distance from critical
// Map water range to available display pixels
waterHeight = (int)((waterRange / totalRange) * waterDisplayHeight);
// Ensure valid range
waterHeight = constrain(waterHeight, 0, waterDisplayHeight);
}
// Draw water level with sensor offset consideration
if (waterHeight > 0) {
// Calculate water position - start from tank bottom, account for sensor offset
int waterBottomY = tankY + tankHeight - 2; // Tank bottom minus border
int waterTopY = waterBottomY - waterHeight;
// Draw water as filled rectangle from bottom up
display.fillRect(tankX + 2, waterTopY, tankWidth - 4, waterHeight);
// Draw horizontal lines to make water more visible
for (int i = 0; i < waterHeight; i += 3) {
display.drawHorizontalLine(tankX + 2, waterTopY + i, tankWidth - 4);
}
// Add water surface line (if not at maximum)
if (waterHeight < waterDisplayHeight) {
display.drawHorizontalLine(tankX + 1, waterTopY - 1, tankWidth - 2);
}
}
// Display "Level" label and distance value clearly beside the tank
display.setFont(ArialMT_Plain_16);
display.drawString(70, 0, "Level :");
display.setFont(ArialMT_Plain_24);
display.drawString(70, 22, String(currentDistance, 0));
display.setFont(ArialMT_Plain_16);
display.drawString(70, 44, "cm");}
}
void displayPumpStatus() {
display.setFont(ArialMT_Plain_24);
display.drawString(12, 0, "POMPA :");
display.drawString(30, 32, pumpState ? "ON" : "OFF");
if (!manualMode){
if (millis() < pumpBlockedUntil) {
display.clear();
display.setFont(ArialMT_Plain_24);
display.drawString(12, 0, "POMPA :");
unsigned long remaining = (pumpBlockedUntil - millis()) / 60000;
display.setFont(ArialMT_Plain_16);
display.drawString(0, 32, "Stop: " + String(remaining) + " Menit");}
}
}
// ENHANCED: Display system status with buzzer mute indicator and startup delay
void displaySystemStatus() {
display.setFont(ArialMT_Plain_16);
// NEW: Show startup alarm delay if still active
if (isStartupAlarmDelayActive()) {
display.drawString(0, 0, "STARTUP:");
unsigned long remaining = (STARTUP_ALARM_DELAY - (millis() - systemStartupTime)) / 1000;
display.setFont(ArialMT_Plain_16);
display.drawString(0, 18, "Delay: " + String(remaining) + "s");
display.drawString(0, 36, "Alarm Off");
return;
}
if (manualMode){
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "MODE :");
display.drawString(0, 25,"MANUAL");
} else {
if (alarmState) {
// Display "ALARM" instead of "STATUS" when alarm is active
display.drawString(0, 0, "ALARM :");
// Display specific alarm message clearly
display.setFont(ArialMT_Plain_16);
display.drawString(0, 18, alarmMessages[currentAlarm]);
// Enhanced buzzer status display with icon
if (buzzerMuted) {
display.drawString(0, 40, "Buzzer: OFF");
} else {
display.drawString(0, 40, "Buzzer: ON");
}
} else {
// Display "STATUS" when no alarm
display.drawString(0, 0, "STATUS :");
// Normal status - centered and clear
display.setFont(ArialMT_Plain_24);
display.drawString(15, 25, "NORMAL");}
}
}
void displayIP() {
if (WiFi.status() == WL_CONNECTED) {
display.setFont(ArialMT_Plain_16);
display.drawString(0, 0, "Alamat IP :");
display.drawString(0, 18, WiFi.localIP().toString());
display.drawString(0, 42, String(WiFi.SSID()));
} else {
display.setFont(ArialMT_Plain_24);
display.drawString(8, 15, "REPOPIN");
display.drawString(8, 42, "..Offline..");
}
}
void updateDisplay() {
if (millis() - lastDisplayUpdate < 5000) return;
display.clear();
switch (displayMode) {
case 0:
displayWaterLevel();
break;
case 1:
displayPumpStatus();
break;
case 2:
displaySystemStatus();
break;
case 3:
displayIP();
break;
}
display.display();
displayMode = (displayMode + 1) % 4;
lastDisplayUpdate = millis();
}
// ENHANCED: Complete alarm handling function with startup delay protection
void handleAlarms() {
// NEW: Skip all alarm processing during startup delay period
if (isStartupAlarmDelayActive()) {
// During startup delay, ensure no alarms are active
if (alarmState) {
currentAlarm = NO_ALARM;
alarmState = false;
digitalWrite(BUZZER_PIN, LOW);
Serial.println("Clearing any existing alarms during startup delay");
}
return; // Exit early - no alarm processing during startup delay
}
AlarmType newAlarm = NO_ALARM;
// Determine current alarm condition using limited critical level
if (currentDistance < 0 || !sensorConnected) {
newAlarm = SENSOR_ERROR;
} else if (currentDistance > ((criticalLevel < MAX_CRITICAL_LEVEL) ? criticalLevel : MAX_CRITICAL_LEVEL)) {
newAlarm = CRITICAL_LEVEL;
} else if (millis() < pumpBlockedUntil) {
newAlarm = PUMP_TIMEOUT;
}
// Check for alarm state changes
bool alarmCleared = (previousAlarmState == true && newAlarm == NO_ALARM);
bool firstAlarmEver = (!previousAlarmState && !alarmState && newAlarm != NO_ALARM);
bool sameAlarmReoccurred = (newAlarm != NO_ALARM && newAlarm == previousAlarmType && alarmCleared);
bool differentAlarmTriggered = (newAlarm != NO_ALARM && newAlarm != currentAlarm && !alarmCleared);
bool newAlarmTriggered = firstAlarmEver || sameAlarmReoccurred || differentAlarmTriggered;
// Handle alarm clearing
if (alarmCleared) {
currentAlarm = NO_ALARM;
alarmState = false;
digitalWrite(BUZZER_PIN, LOW);
// Send "alarm cleared" notification
sendTelegramNotification("ā
Alarm REPOPIN : Normal");
lastTelegramTime = millis(); // Reset telegram timer
}
// ENHANCED: Handle new alarm or re-triggered alarm with improved mute behavior
else if (newAlarmTriggered) {
currentAlarm = newAlarm;
alarmState = true;
// IMPORTANT: Only unmute buzzer for NEW alarms if it was previously muted via Telegram/Web
// Physical button mute state is preserved across alarm cycles
if (firstAlarmEver || differentAlarmTriggered) {
// For completely new alarms or different alarm types, respect current mute setting
Serial.println("New/Different alarm - respecting current mute setting: " + String(buzzerMuted ? "MUTED" : "UNMUTED"));
}
lastTelegramTime = 0; // Reset to send immediate telegram
if (firstAlarmEver) {
Serial.println("FIRST ALARM AFTER STARTUP DELAY: " + alarmMessages[currentAlarm]);
} else if (sameAlarmReoccurred) {
Serial.println("SAME ALARM RE-OCCURRED: " + alarmMessages[currentAlarm]);
} else {
Serial.println("DIFFERENT ALARM TRIGGERED: " + alarmMessages[currentAlarm]);
}
// Send immediate Telegram alert for new alarms
sendTelegramAlert();
lastTelegramTime = millis();
}
// Update previous state tracking
previousAlarmState = alarmState;
if (alarmState) {
previousAlarmType = currentAlarm;
}
// Handle buzzer with respect to mute setting
if (alarmState && !buzzerMuted) {
static unsigned long lastBuzzerToggle = 0;
static bool buzzerOn = false;
if (millis() - lastBuzzerToggle > 500) {
buzzerOn = !buzzerOn;
digitalWrite(BUZZER_PIN, buzzerOn ? HIGH : LOW);
lastBuzzerToggle = millis();
}
} else {
digitalWrite(BUZZER_PIN, LOW);
}
}
void setup() {
Serial.begin(115200);
delay(100);
// NEW: Record system startup time for alarm delay
systemStartupTime = millis();
startupAlarmDelayActive = true;
Serial.println("System startup - alarm delay active for " + String(STARTUP_ALARM_DELAY/1000) + " seconds");
// Initialize OLED
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_24);
display.drawString(8, 15, "REPOPIN");
display.drawString(8, 42, "************");
display.display();
// Start Access Point
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(ap_ssid, ap_password);
// Initialize pins
pinMode(PUMP_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(MUTE_BUTTON_PIN, INPUT_PULLUP);
digitalWrite(PUMP_PIN, LOW);
digitalWrite(BUZZER_PIN, HIGH);
// Initialize EEPROM and load settings
EEPROM.begin(EEPROM_SIZE);
loadSettings();
// Set up server routes for AP mode
server.on("/", handleRoot);
server.on("/jarak", handleJarak);
server.on("/app", handleApp);
server.on("/set", handleSet);
server.on("/tele", handleTele);
server.on("/submit", []() {
// Retrieve Wi-Fi credentials from GET parameters
ssid = server.arg("ssid");
password = server.arg("password");
// Save new credentials to EEPROM
saveStringToEEPROM(SSID_ADDR, ssid, 32);
saveStringToEEPROM(PASS_ADDR, password, 32);
server.send(200, "text/csv", ssid + "," + password);
Serial.println("New WiFi credentials saved: " + ssid);
delay(2000);
ESP.restart();
});
// Retrieve Wi-Fi credentials from EEPROM
ssid = readStringNewFromEEPROM(SSID_ADDR, 32);
password = readStringNewFromEEPROM(PASS_ADDR, 32);
// Connect to WiFi if credentials exist
if (ssid.length() > 0) {
Serial.println("Connecting to WiFi: " + ssid);
WiFi.begin(ssid, password);
// Wait for connection with timeout
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!");
Serial.println("IP address: " + WiFi.localIP().toString());
// Send startup notification if Telegram is configured
if (botToken.length() > 0 && chatID.length() > 0) {
delay(2000); // Wait for connection to stabilize
String startupMsg = "ā
REPOPIN Siap Sedia\n";
startupMsg += "Alamat IP: " + WiFi.localIP().toString() + "\n";
startupMsg += String(buzzerMuted ? "š" : "š") + " Buzzer: " + String(buzzerMuted ? "OFF" : "ON") + "\n";
startupMsg += "ā³ Alarm Delay: " + String(STARTUP_ALARM_DELAY/1000) + " detik\n";
startupMsg += "š¤ Kendalikan dengan pesan\n";
startupMsg += "Ketik: /info untuk informasi\n";
startupMsg += "Ketik: /start untuk melihat daftar perintah\n";
sendTelegramNotification(startupMsg);
}
} else {
Serial.println("\nWiFi connection failed!");
}
}
ElegantOTA.begin(&server);
server.begin();
// Initialize UDP
udp.begin(localUdpPort);
// Update display to show system ready
display.clear();
display.setFont(ArialMT_Plain_24);
display.drawString(8, 15, "REPOPIN");
display.drawString(8, 42, ".....OK.....");
display.display();
digitalWrite(BUZZER_PIN, LOW);
}
void loop() {
// Update IP address for display
if (WiFi.status() == WL_CONNECTED) {
AlamatIP = WiFi.localIP().toString();
}
ElegantOTA.loop();
// Handle all system functions
server.handleClient();
handleUDPData(); // Process incoming UDP sensor data
checkUDPTimeout(); // Check if sensor is still connected
handleMuteButton(); // Handle mute button presses with EEPROM persistence
handlePumpControl(); // Control pump based on water levels
handleAlarms(); // Process alarms and send Telegram notifications (with startup delay)
handleTelegramRepeat(); // Handle 30-minute repeat messages for ongoing alarms
checkTelegramCommands(); // Check for incoming Telegram commands including /mute and /unmute
updateDisplay(); // Update OLED display
// Small delay to prevent overwhelming the system
delay(100);
}
Loading
esp32-s2-devkitm-1
esp32-s2-devkitm-1