#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
// --- Точные настройки пинов ---
const int FLOW_SENSOR_PIN = 2; // Синяя кнопка / Датчик Холла (D2 -> GND)
const int BUTTON_MODE_PIN = 3; // Черная кнопка (D3 -> GND)
const int BUTTON_RESET_PIN = 4; // КРАСНАЯ КНОПКА СБРОСА (D4 -> GND)
const int BUTTON_UP_PIN = 5; // НОВАЯ: Кнопка ВВЕРХ (D5 -> GND)
const int BUTTON_DOWN_PIN = 6; // НОВАЯ: Кнопка ВНИЗ (D6 -> GND)
const int LED_PIN = 13; // Встроенный светодиод паники/потока
// --- Калибровка расхода ---
float calibrationFactor = 450.0; // Теперь это переменная (по умолчанию 450)
const float TOTAL_RESOURCE_LITERS = 10000.0; // Ресурс, в железе 8000-10000 л.
// --- Адреса in EEPROM ---
const int EEPROM_ADDR_LITERS = 0;
const int EEPROM_ADDR_TOTAL = 4;
const int EEPROM_ADDR_INIT = 10;
const int EEPROM_ADDR_CALIB = 14; // НОВЫЙ АДРЕС: для калибровки
const int EEPROM_INIT_KEY = 42;
// --- Переменные для прерываний ---
volatile unsigned long pulseCount = 0;
volatile bool isFlowing = false;
volatile unsigned long lastPulseTime = 0;
volatile unsigned long flowPulsesSec = 0;
unsigned long lastFlowMillis = 0;
float currentFlowRate = 0.0;
// --- Переменные основного цикла ---
float litersPassed = 0.0;
float totalLitersPassed = 0.0;
float currentResource = TOTAL_RESOURCE_LITERS;
unsigned long lastEepromWriteTime = 0;
bool needEepromWrite = false;
// --- Состояния Черной кнопки (Mode) ---
bool isModePressed = false;
unsigned long lastModeReleaseTime = 0;
const unsigned long DEBOUNCE_TIME = 200;
// --- Стейт-машина 8-секундного протокола Красной кнопки (Reset) ---
enum ResetState { STATE_NORMAL, STATE_COUNTDOWN, STATE_ABORTED };
ResetState currentResetState = STATE_NORMAL;
unsigned long resetStartTime = 0;
int lastSecondsRemaining = -1;
unsigned long lastLedToggleTime = 0;
bool currentLedState = false;
// --- ТРЕКЕР ФИЗИЧЕСКОГО СОСТОЯНИЯ КНОПКИ RESET (ДЛЯ ОТЛАДКИ) ---
bool lastPhysicalResetState = HIGH;
// --- Логика режима сна (Энергосбережение) ---
bool isSleeping = false;
bool screenToldToSleep = false;
unsigned long sleepTransitionTimer = 0;
unsigned long lastActivityTime = 0;
const unsigned long SLEEP_TIMEOUT = 300000; // 5 минут до сна
// --- Состояния экрана ---
int displayMode = 0; // Теперь 0-5 (основные) и 6 (скрытый)
unsigned long lastBlinkTime = 0;
bool blinkState = true;
unsigned long lastDisplayUpdateTime = 0;
const unsigned long DISPLAY_UPDATE_INTERVAL = 250;
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- Массив символов шкалы ---
byte barChars[4][8] = {
{B11111, B10000, B10000, B10000, B10000, B10000, B10000, B11111}, // 0: Левый пустой край
{B11111, B00001, B00001, B00001, B00001, B00001, B00001, B11111}, // 1: Правый пустой край
{B11111, B00000, B00000, B00000, B00000, B00000, B00000, B11111}, // 2: Внутреннее пустое тело
{B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111} // 3: Универсальный полный блок
};
// --- НОВЫЙ: Символ для меню калибровки [<> ] ---
byte selectorChar[8] = {
B00100, B01110, B11111, B00000, B00000, B11111, B01110, B00100
};
void flowPulseISR() {
pulseCount++;
flowPulsesSec++;
lastPulseTime = millis();
isFlowing = true;
}
void setup() {
// === ИНИЦИАЛИЗАЦИЯ МОНИТОРА ПОРТА ===
Serial.begin(9600);
Serial.println(F("--- AQUA MONITOR 3.5 SYSTEM START ---"));
pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
pinMode(BUTTON_MODE_PIN, INPUT_PULLUP);
pinMode(BUTTON_RESET_PIN, INPUT_PULLUP);
pinMode(BUTTON_UP_PIN, INPUT_PULLUP); // Инициализация новых кнопок
pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
lcd.init();
lcd.backlight();
for (int i = 0; i < 4; i++) {
lcd.createChar(i, barChars[i]);
}
lcd.createChar(4, selectorChar); // Загружаем символ стрелочек
// === ЭКРАНЫ ПРИВЕТСТВИЯ ===
lcd.setCursor(0, 0); lcd.print("RD&P Laboratory ");
lcd.setCursor(0, 1); lcd.print(" with Gemini O_o ");
delay(1500);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("AQUA MONITOR 3.5");
lcd.setCursor(0, 1);
for (int i = 0; i < 16; i++) {
lcd.write(3);
delay(30);
}
delay(1000);
lcd.clear();
// Инициализация памяти EEPROM
if (EEPROM.read(EEPROM_ADDR_INIT) != EEPROM_INIT_KEY) {
Serial.println(F("EEPROM Key not found. Initializing memory..."));
EEPROM.write(EEPROM_ADDR_INIT, EEPROM_INIT_KEY);
EEPROM.put(EEPROM_ADDR_LITERS, 0.0f);
EEPROM.put(EEPROM_ADDR_TOTAL, 0.0f);
EEPROM.put(EEPROM_ADDR_CALIB, 450.0f); // Базовая калибровка при первом запуске
litersPassed = 0.0;
totalLitersPassed = 0.0;
calibrationFactor = 450.0;
} else {
EEPROM.get(EEPROM_ADDR_LITERS, litersPassed);
EEPROM.get(EEPROM_ADDR_TOTAL, totalLitersPassed);
EEPROM.get(EEPROM_ADDR_CALIB, calibrationFactor);
// Защита от мусора в памяти для калибровки
if (isnan(calibrationFactor) || calibrationFactor < 100 || calibrationFactor > 1000) {
calibrationFactor = 450.0;
}
Serial.print(F("Loaded Unit Liters: ")); Serial.println(litersPassed, 3);
Serial.print(F("Loaded Total Liters: ")); Serial.println(totalLitersPassed, 3);
Serial.print(F("Loaded Calib Factor: ")); Serial.println(calibrationFactor);
}
currentResource = TOTAL_RESOURCE_LITERS - litersPassed;
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseISR, FALLING);
unsigned long startupTime = millis();
lastFlowMillis = startupTime;
lastActivityTime = startupTime;
Serial.println(F("System Ready."));
}
void loop() {
unsigned long currentTime = millis();
// Единая метка времени для всего цикла loop!
// Чтение состояния всех кнопок
bool modeState = (digitalRead(BUTTON_MODE_PIN) == LOW);
bool upState = (digitalRead(BUTTON_UP_PIN) == LOW);
bool downState = (digitalRead(BUTTON_DOWN_PIN) == LOW);
bool currentPhysicalResetState = digitalRead(BUTTON_RESET_PIN);
bool resetState = (currentPhysicalResetState == LOW);
// Расчет скорости потока (раз в секунду)
if (currentTime - lastFlowMillis >= 1000) {
noInterrupts();
unsigned long pulsesLastSec = flowPulsesSec;
flowPulsesSec = 0;
interrupts();
// ИСПОЛЬЗУЕМ calibrationFactor ИЗ EEPROM
float targetFlowRate = ((float)pulsesLastSec * 60.0) / calibrationFactor;
currentFlowRate = (currentFlowRate * 0.4) + (targetFlowRate * 0.6);
if (pulsesLastSec == 0 && currentTime - lastPulseTime > 1200) {
currentFlowRate = 0.0;
}
lastFlowMillis = currentTime;
}
// Подсчет накопленных литров
if (isFlowing) {
if (currentResetState == STATE_NORMAL) {
noInterrupts();
unsigned long pulses = pulseCount;
pulseCount = 0;
isFlowing = false;
interrupts();
if (pulses > 0) {
float delta = (float)pulses / calibrationFactor; // ИСПОЛЬЗУЕМ ПЕРЕМЕННУЮ
litersPassed += delta;
totalLitersPassed += delta;
currentResource = TOTAL_RESOURCE_LITERS - litersPassed;
needEepromWrite = true;
lastEepromWriteTime = currentTime;
lastActivityTime = currentTime;
digitalWrite(LED_PIN, HIGH);
}
}
}
if (currentResetState == STATE_NORMAL && currentTime - lastPulseTime > 200) {
digitalWrite(LED_PIN, LOW);
}
// Запись in EEPROM через 3 секунды после остановки потока
if (needEepromWrite && (currentTime - lastEepromWriteTime > 3000)) {
EEPROM.put(EEPROM_ADDR_LITERS, litersPassed);
EEPROM.put(EEPROM_ADDR_TOTAL, totalLitersPassed);
needEepromWrite = false;
Serial.println(F("[EEPROM] Data saved successfully."));
}
// 1. ОБРАБОТКА ЧЕРНОЙ КНОПКИ MODE
if (modeState && !isModePressed) {
if (currentTime - lastModeReleaseTime > DEBOUNCE_TIME) {
isModePressed = true;
lastActivityTime = currentTime;
if (isSleeping || screenToldToSleep) {
isSleeping = false;
screenToldToSleep = false;
lcd.backlight();
lcd.clear();
Serial.println(F("[SYSTEM] Woke up from sleep."));
} else {
if (currentResetState == STATE_NORMAL) {
if (displayMode == 6) {
// Если мы в меню 6 и жмем Mode - выходим без сохранения
displayMode = 5;
EEPROM.get(EEPROM_ADDR_CALIB, calibrationFactor); // Откатываем значение
} else {
displayMode++;
if (displayMode > 5) displayMode = 0; // Теперь листаем от 0 до 5
}
lcd.clear();
Serial.print(F("[UI] Display mode changed to: ")); Serial.println(displayMode);
}
}
}
} else if (!modeState && isModePressed) {
isModePressed = false;
lastModeReleaseTime = currentTime;
lastActivityTime = currentTime;
}
// --- ЛОГИКА ВХОДА В МЕНЮ 6 (UP + DOWN) ---
if (displayMode == 5 && currentResetState == STATE_NORMAL && !isSleeping) {
if (upState && downState) {
displayMode = 6;
lastActivityTime = currentTime;
lcd.clear();
delay(500); // Чтобы отпустить кнопки
}
}
// --- ЛОГИКА НАСТРОЙКИ (МЕНЮ 6) ---
if (displayMode == 6 && currentResetState == STATE_NORMAL && !isSleeping) {
if (upState && !downState && calibrationFactor < 600.0) {
calibrationFactor += 5.0;
lastActivityTime = currentTime;
delay(200);
}
if (downState && !upState && calibrationFactor > 300.0) {
calibrationFactor -= 5.0;
lastActivityTime = currentTime;
delay(200);
}
}
// --- ОТЛАДКА ФИЗИЧЕСКОГО СОСТОЯНИЯ КНОПКИ RESET ---
if (currentPhysicalResetState != lastPhysicalResetState) {
lastPhysicalResetState = currentPhysicalResetState;
}
// 2. ОБРАБОТКА КРАСНОЙ КНОПКИ RESET (Удержание 8 сек)
if (!isSleeping && !screenToldToSleep) {
if (resetState) {
lastActivityTime = currentTime;
}
if (currentResetState == STATE_NORMAL) {
// Добавляем displayMode == 6 как триггер для удержания кнопки
if (resetState && (displayMode == 3 || displayMode == 4 || displayMode == 6)) {
currentResetState = STATE_COUNTDOWN;
resetStartTime = currentTime;
lastSecondsRemaining = -1;
detachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN));
digitalWrite(LED_PIN, LOW);
lcd.clear();
Serial.println(F("[PROTOCOL] Countdown Started! Holding RESET button..."));
}
}
else if (currentResetState == STATE_COUNTDOWN) {
if (!resetState) { // Отпустили кнопку раньше времени - ОТМЕНА
currentResetState = STATE_ABORTED;
resetStartTime = currentTime;
digitalWrite(LED_PIN, LOW);
lcd.clear();
lcd.setCursor(0, 0); lcd.print("Process ABORTED! ");
if (displayMode == 6) {
lcd.setCursor(0, 1); lcd.print("Settings Reverted");
EEPROM.get(EEPROM_ADDR_CALIB, calibrationFactor); // Откат изменений
} else {
lcd.setCursor(0, 1); lcd.print("Data NOT Deleted ");
}
Serial.println(F("[PROTOCOL] ABORTED by user! Data is safe."));
}
else {
unsigned long elapsed = currentTime - resetStartTime;
int secondsRemaining = 8 - (elapsed / 1000);
if (secondsRemaining < 0) secondsRemaining = 0;
if (secondsRemaining != lastSecondsRemaining) {
lastSecondsRemaining = secondsRemaining;
lcd.setCursor(0, 0);
if (displayMode == 3) lcd.print("UPDATE the Unit? ");
else if (displayMode == 4) lcd.print("ERASE Total Vol? ");
else if (displayMode == 6) lcd.print("Apply NEW Value? ");
lcd.setCursor(0, 1); lcd.print("Passed "); lcd.print(secondsRemaining); lcd.print(" sec..... ");
Serial.print(F("[PROTOCOL] Time left: ")); Serial.print(secondsRemaining); Serial.println(F("s..."));
}
unsigned long toggleInterval = (secondsRemaining * 50) + 30;
if (currentTime - lastLedToggleTime > toggleInterval) {
currentLedState = !currentLedState;
digitalWrite(LED_PIN, currentLedState ? HIGH : LOW);
lastLedToggleTime = currentTime;
}
// ТОЧКА НЕВОЗВРАТА: 8 СЕКУНД ПРОШЛИ
if (elapsed >= 8000) {
Serial.println(F("[PROTOCOL] 8 SECONDS REACHED. EXECUTING..."));
digitalWrite(LED_PIN, HIGH);
lcd.clear();
if (displayMode == 6) lcd.print("SAVING DATA... ");
else lcd.print("ERASING DATA... ");
// Вывод графики прогресс-бара
lcd.setCursor(0, 1);
for (int i = 0; i < 16; i++) {
lcd.write(3);
delay(30);
}
delay(1000);
lcd.clear();
if (displayMode == 3) {
litersPassed = 0.0;
currentResource = TOTAL_RESOURCE_LITERS;
EEPROM.put(EEPROM_ADDR_LITERS, litersPassed);
Serial.println(F("[EEPROM] Unit Data Updated."));
}
else if (displayMode == 4) {
litersPassed = 0.0;
totalLitersPassed = 0.0;
currentResource = TOTAL_RESOURCE_LITERS;
EEPROM.put(EEPROM_ADDR_LITERS, litersPassed);
EEPROM.put(EEPROM_ADDR_TOTAL, totalLitersPassed);
Serial.println(F("[EEPROM] Total System FULL ERASE completed."));
}
else if (displayMode == 6) { // СОХРАНЯЕМ КАЛИБРОВКУ
EEPROM.put(EEPROM_ADDR_CALIB, calibrationFactor);
Serial.println(F("[EEPROM] Calibration Factor Saved."));
}
needEepromWrite = false;
pulseCount = 0;
currentFlowRate = 0.0;
displayMode = 0; // После сохранения всегда выкидываем на стартовый экран
digitalWrite(LED_PIN, LOW);
lcd.clear();
currentResetState = STATE_NORMAL;
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseISR, FALLING);
lastActivityTime = currentTime;
Serial.println(F("[SYSTEM] Returned to normal operation."));
}
}
}
else if (currentResetState == STATE_ABORTED) {
lastActivityTime = currentTime;
if (currentTime - resetStartTime > 1500) {
lcd.clear();
currentResetState = STATE_NORMAL;
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseISR, FALLING);
lastActivityTime = currentTime;
}
}
}
// 3. УМНЫЙ ДВУХЭТАПНЫЙ ПЕРЕХОД В СОН
if (currentResetState == STATE_NORMAL) {
if (!isSleeping && !screenToldToSleep && (currentTime - lastActivityTime >= SLEEP_TIMEOUT)) {
screenToldToSleep = true;
sleepTransitionTimer = currentTime;
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0); lcd.print("[O_o] (z-z-z) ");
lcd.setCursor(5, 1); lcd.print("sleeping... ");
Serial.println(F("[SYSTEM] Preparing to sleep..."));
}
if (!isSleeping && screenToldToSleep && (currentTime - sleepTransitionTimer >= 2000)) {
isSleeping = true;
screenToldToSleep = false;
lcd.noBacklight();
lcd.clear();
Serial.println(F("[SYSTEM] Device is now sleeping."));
}
}
// 4. ТАЙМЕР МИГАНИЯ СИМВОЛОВ (Для пустой шкалы ресурса)
if (currentTime - lastBlinkTime > 500) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
}
// 5. ОТРИСОВКА ЭКРАНОВ
if (currentResetState == STATE_NORMAL && !isSleeping && !screenToldToSleep) {
if (currentTime - lastDisplayUpdateTime >= DISPLAY_UPDATE_INTERVAL) {
drawDisplay();
lastDisplayUpdateTime = currentTime;
}
}
}
void drawDisplay() {
float percent = (currentResource / TOTAL_RESOURCE_LITERS) * 100.0;
bool isExpired = (currentResource <= 0);
if (displayMode == 0) {
lcd.setCursor(0, 0);
lcd.print("Flow Rate: ");
if (currentFlowRate <= 0.05) lcd.print("CLOSE");
else if (currentFlowRate < 1.800) lcd.print("Rise!");
else if (currentFlowRate > 2.200) lcd.print("Down!");
else lcd.print("Norm.");
lcd.print(" ");
lcd.setCursor(0, 1);
if (currentFlowRate < 10.0) lcd.print("0");
lcd.print(currentFlowRate, 3);
lcd.print(" L/min ");
}
else if (displayMode == 1) {
lcd.setCursor(0, 0);
lcd.print("Unit ");
if (isExpired && !blinkState) {
lcd.print(" ");
} else {
if (isExpired) {
lcd.write(0); lcd.write(2);
lcd.write(2); lcd.write(2); lcd.write(2); lcd.write(1);
} else {
int activeBlocks = (int)((percent / 100.0) * 6.0 + 0.5);
activeBlocks = constrain(activeBlocks, 1, 6);
for (int i = 0; i < 6; i++) {
if (i < activeBlocks) lcd.write(3);
else {
if (i == 0) lcd.write(0);
else if (i == 5) lcd.write(1);
else lcd.write(2);
}
}
}
}
lcd.print(" ");
if (percent >= 100.0) {
lcd.print((int)percent); lcd.print("%");
}
else if (percent >= 0.0) {
int p = (int)percent;
if (p < 10) lcd.print("00");
else if (p < 100) lcd.print("0");
lcd.print(p); lcd.print("%");
}
else {
int p = (int)percent;
if (p < -99) p = -99;
if (p > -10) {
lcd.print("-0");
lcd.print(abs(p));
}
else {
lcd.print(p);
}
lcd.print("%");
}
lcd.print(" ");
lcd.setCursor(0, 1);
unsigned long wholeLitres = (unsigned long)litersPassed;
if (wholeLitres < 10) lcd.print("0000");
else if (wholeLitres < 100) lcd.print("000");
else if (wholeLitres < 1000) lcd.print("00");
else if (wholeLitres < 10000) lcd.print("0");
lcd.print(litersPassed, 3);
lcd.print(" Litres ");
}
else if (displayMode == 2) {
lcd.setCursor(0, 0); lcd.print("TOTAL Volume: ");
lcd.setCursor(0, 1);
unsigned long wholeTotal = (unsigned long)totalLitersPassed;
if (wholeTotal < 10) lcd.print("000000");
else if (wholeTotal < 100) lcd.print("00000");
else if (wholeTotal < 1000) lcd.print("0000");
else if (wholeTotal < 10000) lcd.print("000");
else if (wholeTotal < 100000) lcd.print("00");
else if (wholeTotal < 1000000) lcd.print("0");
lcd.print(totalLitersPassed, 1);
lcd.print(" Litres ");
}
else if (displayMode == 3) {
lcd.setCursor(0, 0); lcd.print("(i) Unit UPDATE: ");
lcd.setCursor(0, 1); lcd.print("HOLD RED Button! ");
}
else if (displayMode == 4) {
lcd.setCursor(0, 0); lcd.print("(!) Total ERASE:");
lcd.setCursor(0, 1); lcd.print("HOLD RED Button! ");
}
else if (displayMode == 5) {
// --- ИНФОРМАЦИОННЫЙ ЭКРАН КАЛИБРОВКИ ---
lcd.setCursor(0, 0); lcd.print("(!) SETUP Sensor");
lcd.setCursor(0, 1); lcd.print("Up+Down > BEGIN!");
}
else if (displayMode == 6) {
// --- СКРЫТОЕ МЕНЮ НАСТРОЙКИ ---
lcd.setCursor(0, 0);
lcd.print("Curr.Value "); lcd.print((int)calibrationFactor); lcd.print(" "); lcd.write(4); // Кастомный символ [<>]
lcd.print(" ");
lcd.print(" "); // Затираем старые символы
lcd.setCursor(0, 1); lcd.print("HOLD > Save&Exit ");
}
}