#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <avr/wdt.h> // Защита от зависаний
LiquidCrystal_I2C lcd(0x27, 16, 2);
const byte flowPin = 2; // Датчик Холла (JST желтый) -> пин D2
const byte buttonPin = 3; // Кнопка управления MODE -> пин D3
const byte ledPin = 13; // Встроенный светодиод на плате Nano -> пин D13
float calibrationFactor = 7.5; // Коэффициент датчика 1/2"
volatile byte pulseCount = 0;
volatile unsigned long lastPulseTime = 0; // Фильтр помех в трубах
float flowRate = 0.0;
unsigned int flowMilliLitres = 0;
float filterLitres = 0.0; // Режим 2 (Расход фильтра, сброс в Режиме 4)
float totalLitres = 0.0; // Режим 3 (Глобальный тотал, сброс в Режиме 5)
const float FILTER_LIMIT = 1.0; // Для тестов в Wokwi выставлено 1 литр (в железе ставьте 10000.0)
unsigned long oldTime = 0;
unsigned long lastFlowTime = 0;
bool needSave = false;
// Логика интерфейса
byte currentMode = 0;
bool lastButtonState = HIGH;
unsigned long buttonPressedTime = 0;
bool isHolding = false;
bool actionConfirmed = false;
// Логика двойного клика и сна
unsigned long lastButtonReleaseTime = 0;
bool singleClickPending = false;
bool isScreenOn = true;
unsigned long lastActivityTime = 0;
const unsigned long SLEEP_TIMEOUT = 30000; // Уход в сон через 5 минут простоя (300000 мс), для теста 30000 мс
const int ADDR_FILTER = 10;
const int ADDR_TOTAL = 20;
// Кастомные символы (наш дисплей помнит максимум 8 шт.):
// Массивы пикселей для создания бесшовного прогресс-бара
byte barLeft[] = {B11111, B10000, B10000, B10000, B10000, B10000, B10000, B11111}; // Левый край [
byte barRight[] = {B11111, B00001, B00001, B00001, B00001, B00001, B00001, B11111}; // Правый край ]
byte barEmpty[] = {B11111, B00000, B00000, B00000, B00000, B00000, B00000, B11111}; // Пустая рамка ═
byte barFull[] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111}; // Заполненный кубик █
// Массивы пикселей для создания...
void setup() {
wdt_disable(); // Защита от бесконечного ребута при старте
pinMode(flowPin, INPUT_PULLUP);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT); // Настройка пина встроенного светодиода
lcd.init();
lcd.backlight();
// Регистрация кастомных символов в памяти дисплея
lcd.createChar(1, barLeft);
lcd.createChar(2, barRight);
lcd.createChar(3, barEmpty);
lcd.createChar(4, barFull);
// Извлечение литров из памяти
EEPROM.get(ADDR_FILTER, filterLitres);
EEPROM.get(ADDR_TOTAL, totalLitres);
if (isnan(filterLitres) || filterLitres < 0) filterLitres = 0.0;
if (isnan(totalLitres) || totalLitres < 0) totalLitres = 0.0;
// --- БРЕНДИРОВАННЫЙ СТАРТОВЫЙ ЭКРАН ---
lcd.setCursor(0, 0); lcd.print("RD&P Laboratory ");
lcd.setCursor(0, 1); lcd.print(" with Gemini O_o ");
delay(2000); lcd.clear();
lcd.setCursor(0, 0); lcd.print("AQUA MONITOR 1.0");
//lcd.setCursor(0, 1); lcd.print(" "); // Стираем нижнюю строку под анимацию загрузки
//анимация загрузки прогресс-бар
for (int i = 0; i < 16; i++) {
lcd.setCursor(i, 1);
lcd.write(4);
delay(60);
}
delay(200); lcd.clear();
// --- КОНЕЦ СТАРТОВОГО ЭКРАНА ---
attachInterrupt(digitalPinToInterrupt(flowPin), pulseCounter, FALLING);
oldTime = millis();
lastActivityTime = millis();
wdt_enable(WDTO_2S); // Включение аппаратного сторожа на 2 сек.
}
void loop() {
wdt_reset();
unsigned long currentTime = millis();
bool currentButtonState = digitalRead(buttonPin);
// Автоматический уход в сон при простое
if (isScreenOn && (currentTime - lastActivityTime > SLEEP_TIMEOUT)) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("[O_o] (z-z-z) ");
lcd.setCursor(5, 1); lcd.print("sleeping... ");
delay(1800);
lcd.noBacklight();
lcd.clear();
isScreenOn = false;
}
// ОБРАБОТКА НАЖАТИЙ КНОПКИ "MODE"
if (lastButtonState == HIGH && currentButtonState == LOW) {
buttonPressedTime = currentTime;
isHolding = true;
actionConfirmed = false;
}
// Удержание кнопки в меню сбросов (Режимы 4 и 5)
if (currentButtonState == LOW && isHolding && isScreenOn) {
unsigned long holdDuration = currentTime - buttonPressedTime;
if (currentMode == 3 && !actionConfirmed) {
lcd.setCursor(0, 1);
if (holdDuration >= 3000) { // Сейф-таймер 3 секунды
filterLitres = 0.0;
EEPROM.put(ADDR_FILTER, filterLitres);
lcd.print("DONE! Reseted ");
actionConfirmed = true;
delay(1000);
currentMode = 0; lcd.clear();
lastActivityTime = millis();
} else {
int secondsLeft = 3 - (holdDuration / 1000);
lcd.print("Hold: "); lcd.print(secondsLeft); lcd.print(" sec... ");
}
}
if (currentMode == 4 && !actionConfirmed) {
lcd.setCursor(0, 1);
int secondsLeft = 8 - (holdDuration / 1000); // Сейф-таймер 8 секунд
if (secondsLeft <= 0 || holdDuration >= 8000) {
filterLitres = 0.0;
totalLitres = 0.0;
EEPROM.put(ADDR_FILTER, filterLitres);
EEPROM.put(ADDR_TOTAL, totalLitres);
lcd.print("ALL DATA ERASED!");
actionConfirmed = true;
delay(1500);
currentMode = 0; lcd.clear();
lastActivityTime = millis();
} else {
lcd.print("Hold: "); lcd.print(secondsLeft); lcd.print(" sec... ");
}
}
}
// Отпускание кнопки
if (lastButtonState == LOW && currentButtonState == HIGH) {
isHolding = false;
unsigned long holdDuration = currentTime - buttonPressedTime;
if (holdDuration >= 50 && holdDuration < 1000) {
if (!isScreenOn) {
lcd.backlight();
isScreenOn = true;
lastActivityTime = currentTime;
lcd.clear();
} else {
lastActivityTime = currentTime;
if (currentTime - lastButtonReleaseTime < 400) {
singleClickPending = false;
currentMode++;
if (currentMode > 4) currentMode = 0;
lcd.clear();
} else {
singleClickPending = true;
}
lastButtonReleaseTime = currentTime;
}
}
}
lastButtonState = currentButtonState;
if (singleClickPending && (currentTime - lastButtonReleaseTime >= 400)) {
singleClickPending = false;
}
// СЧЕТ РАСХОДА И ОБЪЕМА ВОДЫ (Раз в секунду)
if ((currentTime - oldTime) > 1000) {
detachInterrupt(digitalPinToInterrupt(flowPin));
flowRate = ((1000.0 / (currentTime - oldTime)) * pulseCount) / calibrationFactor;
oldTime = currentTime;
flowMilliLitres = (flowRate / 60) * 1000;
if (flowMilliLitres > 0) {
float litresPassed = flowMilliLitres / 1000.0;
filterLitres += litresPassed;
totalLitres += litresPassed;
lastFlowTime = currentTime;
needSave = true;
}
pulseCount = 0;
attachInterrupt(digitalPinToInterrupt(flowPin), pulseCounter, FALLING);
if (needSave && (currentTime - lastFlowTime > 3000) && !isHolding) {
EEPROM.put(ADDR_FILTER, filterLitres);
EEPROM.put(ADDR_TOTAL, totalLitres);
needSave = false;
}
if (isScreenOn && (!isHolding || (currentMode != 3 && currentMode != 4))) {
updateDisplay();
}
}
}
void updateDisplay() {
switch (currentMode) {
case 0:
lcd.setCursor(0, 0); lcd.print("Flow Rate: ");
if (flowRate <= 0.05) lcd.print("CLOSED ");
else if (flowRate < 1.8) lcd.print("Up! ");
else if (flowRate > 2.2) lcd.print("Down! ");
else lcd.print("Normal ");
lcd.setCursor(0, 1); lcd.print("Vol: "); lcd.print(flowRate, 2); lcd.print(" L/min ");
break;
case 1:
lcd.setCursor(0, 0); lcd.print("Unit "); drawProgressBar(filterLitres, FILTER_LIMIT);
lcd.setCursor(0, 1); lcd.print("");
if (filterLitres < 10000.00) lcd.print("0");
if (filterLitres < 1000.00) lcd.print("0");
if (filterLitres < 100.00) lcd.print("0");
if (filterLitres < 10.00) lcd.print("0"); // Зафиксировано под одометр
lcd.print(filterLitres, 2); lcd.print(" Litres ");
break;
case 2:
lcd.setCursor(0, 0); lcd.print("TOTAL Volume: ");
lcd.setCursor(0, 1);
//if (totalLitres < 10000000.0) lcd.print("0");
if (totalLitres < 1000000.0) lcd.print("0");
if (totalLitres < 100000.0) lcd.print("0");
if (totalLitres < 10000.0) lcd.print("0");
if (totalLitres < 1000.0) lcd.print("0");
if (totalLitres < 100.0) lcd.print("0");
if (totalLitres < 10.0) lcd.print("0");
lcd.print(totalLitres, 1); lcd.print(" Litres ");
break;
case 3:
lcd.setCursor(0, 0); lcd.print("(i) Unit RESET: ");
lcd.setCursor(0, 1); lcd.print("HOLD button 3 s. "); // Обновлено под 3 сек
break;
case 4:
lcd.setCursor(0, 0); lcd.print("(!) TOTAL RESET: ");
lcd.setCursor(0, 1); lcd.print("HOLD button 8 s. "); // Обновлено под 8 сек
break;
}
}
void drawProgressBar(float current, float maxVal) {
// Вычисляем процент ИЗРАСХОДОВАННОГО ресурса (от 0 до 100% и более)
float percentageUsed = (current / maxVal) * 100.0;
// Вычисляем ОСТАТОК ресурса для вывода на одометр (из 100% вычитаем израсходованное)
int intPercentRemaining = 100 - (int)percentageUsed;
// Шкала отображает остаток ресурса. Переводим его в количество сегментов (из "totalSegments = 5" делений)
int totalSegments = 5; //Длина шкалы
int filledSegments = ((float)intPercentRemaining / 100.0) * totalSegments;
// Ограничиваем шкалу от 0 до 50 кубиков (при минусе шкала будет просто пустой)
if (filledSegments > totalSegments) filledSegments = totalSegments;
if (filledSegments < 0) filledSegments = 0;
// 1. Отрисовка трансформируемой шкалы ("totalSegments = 5" символов)
for (int i = 0; i < totalSegments; i++) {
if (i == 0) {
if (filledSegments >= 1) lcd.write(4); // Залитый левый край █
else lcd.write(1); // Левый край [
}
else if (i == totalSegments - 1) {
if (filledSegments >= totalSegments) lcd.write(4); // Залитый правый край █
else lcd.write(2); // Правый край ]
}
else {
if (i < filledSegments) lcd.write(4); // Внутренний кубик █
else lcd.write(3); // Внутренняя рамка ═
}
}
// 2. Отрисовка жесткого одометра процентов с учетом знака МИНУС
lcd.print(" "); // Разделительный пробел (11-й символ строки)
if (intPercentRemaining < 0) {
// ЕСЛИ РЕСУРС В МИНУСЕ (Перелив):
lcd.print("-"); // Фиксируем минус в 12-й ячейке экрана
int absPercent = abs(intPercentRemaining); // Берем число без знака для одометра
// Дописываем нули для удержания позиций (теперь максимум до -99%)
if (absPercent < 10) lcd.print("0");
if (absPercent < 100) lcd.print("0");
lcd.print(absPercent);
}
else {
// ЕСЛИ РЕСУРС В ПЛЮСЕ (Норма):
lcd.print(" "); // Вместо минуса печатаем пробел, чтобы удержать позицию!
if (intPercentRemaining < 100) lcd.print("0");
if (intPercentRemaining < 10) lcd.print("0");
lcd.print(intPercentRemaining);
}
lcd.print("% "); // Процент встает строго в 16-ю (последнюю) ячейку экрана
}
// Прерывание с аппаратным морганием диода на D13
void pulseCounter() {
unsigned long pTime = micros();
if (pTime - lastPulseTime > 10000) {
pulseCount++;
lastPulseTime = pTime;
// Ошибка исправлена: стандартная команда digitalWrite
digitalWrite(ledPin, !digitalRead(ledPin));
}
}