/*
* =====================================================================
* Nove cerpani vody - ridici jednotka cerpadla (v4)
* =====================================================================
* Platforma : Arduino Nano (ATmega328P)
* Zadani : "Nove cerpani vody - predpis pro naprogramovani" (Pepa)
* + "Nove cerpani vody Pepa Doplneni.pdf" (zmeny v2)
* + "Doplneni - reakce.pdf" (opravy v3)
* Podklady : voda/souhrn-nove-cerpani-vody.md
* voda/Souhrn-zmen-Doplneni.pdf
*
* Zmeny v4 oproti v3 (upravy funkcnosti + displej, 2026-08-17):
* 1. Displej: radky s dvojteckou zarovnany pod sebe (dvojtecka na
* sloupci 9), za kazdou hodnotou v % se zobrazuje znacka %
* 2. "ALARM :OK" misto "ALARM : OFF"
* 3. Napis ZASTAVENO blika kmitoctem 1 Hz
* 4. 5. stisk Zalevani i uplynuti casu -> primy prechod do
* AUTOMATICKY/MANUALNI PROVOZ (v AUT rele D6 rovnou sepne,
* bez nutnosti Reset + Start)
* 5. Behem kalibrace se nevyhodnocuje porucha 2 (SPOTREBA VODY);
* po ulozeni kalibrace primy prechod do AUT/MAN provozu
* 6. Klouzavy prumer hladiny (Moving Average 16 vzorku po 250 ms,
* okno ~4 s) pouzity pro VSE vc. spinani rele D6; zobrazena
* hodnota hladiny: obnova 1x za 2 s, prepis az pri zmene o
* +-1 %, litry zaokrouhlene na desitky
*
* Zmeny v3 oproti v2 (dle reakce na souhrn zmen):
* - SW6 kratky stisk: 5 s se zobrazi AKTUALNI objem v litrech
* (dlouhy stisk >3 s = ulozit 100 % beze zmeny)
* - Spotreba 24 h: KLOUZAVE okno; kazdou 1 minutu se pricte pokles
* hladiny (pritok se ignoruje) a displej se hned obnovi; do 24 h
* od Startu se zobrazuje prubezna hodnota, po 24 h klouzava
* hodnota za poslednich 24 h (obnova po 1 min). Okno = 288 kosu
* po 5 min ulozenych v interni EEPROM (kruhovy buffer s markerem)
* -> data prezijou vypadek napajeni. MicroSD modul NENI potreba.
* - Spotreba zalevani: pocita se jen pri ukonceni casem nebo
* 5. stiskem SW3; tlacitko Reset spotrebu NEzaznamenava
* - Displej AUT, radek 4: pri poruse 2 text "SPOTREBA VODY"
* (misto "ROZDIL VYSKY")
*
* Zmeny v2 oproti v1 (dle doplneni):
* - SW6 (D2): kratky/dlouhy stisk (viz vyse)
* - Nova kalibracni procedura SW5 (D3) + pot. A2 (+-50 %):
* 1. stisk = zapamatovat hladinu, odcerpat vodu, potenciometrem
* nastavit skutecne odcerpane mnozstvi, 2. stisk = ulozit novou
* konstantu litry/cm do EEPROM
* - Porucha 3 (max. hladina) ZRUSENA v celem rozsahu
* - Spotreba v rezimu Zalevani: rozdil hladin zacatek/konec
* - Definitivni konstanta 66,7 l/cm
* - Displej: definitivni rozvrzeni 4 obrazovek (LCD 20x4)
*
* Zapojeni:
* A0 - pot.: max. rozdil hladiny pro poruchu 2 (0V=100 l, 5V=500 l)
* A1 - pot.: delka plovouciho casoveho okna (0V=30 min, 5V=120 min)
* A2 - pot.: kalibrace - skutecne odcerpane mnozstvi, +-50 %
* A3 - pot.: rezervovano (indikace 100 % - bez alarmove funkce)
* A4 - I2C SDA - displej LCD 20x4 (adresa 0x27)
* A5 - I2C SCL - displej
* A7 - cidlo hladiny: 0,094V = 0 cm (pracovni nula), 5V = 800 cm
* D2 - SW6: kratce = zobrazit aktualni objem, dlouze = ulozit 100 %
* D3 - SW5: kalibracni procedura (2 stisky)
* D5 - SW4: prepinac AUT (log.0) / MAN (log.1)
* D7 - SW2: Reset (aktivni log.1)
* D8 - SW1: Start - realizuje se 3 s po pusteni (aktivni log.1)
* D9 - SW3: Zalevani 30/60/90/120 min, 5. stisk = zrusit
* D6 - vystup: rele stykace cerpadla (log.1 = seple)
* D10 - vystup: indikace poruchy 1/2 (log.1 = porucha)
* D4, D11, D12, D13 - rezervovano (externi SPI pamet se nepouziva)
*
* Poruchy:
* 1 - nizka hladina (A7 < 0,094 V) -> rele VYP, D10=1, latch
* 2 - pokles hladiny > limit A0 v okne A1 -> rele VYP, D10=1, latch
* Latch poruchy 1/2 se rusi POUZE tlacitkem Reset (po odezneni priciny).
*
* Knihovny: Wire, EEPROM (soucast IDE), LiquidCrystal_I2C
* (Library Manager: "LiquidCrystal I2C")
* =====================================================================
*/
#include <Wire.h>
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
/* ===================== KONFIGURACE (upravitelne) ================== */
// Definitivni kalibracni konstanta (doplneni): 66,7 litru na 1 cm.
// Slouzi jako vychozi hodnota; kalibracni procedurou lze zmenit.
const float DEFAULT_LITERS_PER_CM = 66.7;
// Vychozi 100% vyska hladiny [cm], nez ji uzivatel ulozi tlacitkem SW6.
const float DEFAULT_LEVEL100_CM = 700.0;
// Prevod cidla hladiny: 0,094 V = 0 cm (pracovni nula), 5 V = 800 cm.
const int ADC_ZERO_RAW = 19; // 0,094 V na 10bit ADC (5V ref.)
const int ADC_FULL_RAW = 1023; // 5 V
const float LEVEL_FULL_CM = 800.0; // vyska hladiny pri 5 V
// Hystereze poruchy 1: porucha odezni az +2 cm nad pracovni nulou.
const float FAULT1_CLEAR_CM = 2.0;
// Start: cerpadlo se rozbehne 3 s po pusteni tlacitka Start.
const unsigned long START_DELAY_MS = 3000UL;
// SW6: prah dlouheho stisku a doba docasneho zobrazeni objemu.
const unsigned long SW6_LONG_MS = 3000UL; // dlouhy stisk > 3 s
const unsigned long SW6_SHOW_MS = 5000UL; // 5 s zobrazeni litru
// Zalevani: potvrzeni poctu stisku po 2 s bez stisku.
const unsigned long WATER_CONFIRM_MS = 2000UL;
const unsigned int WATER_MINUTES[4] = {30, 60, 90, 120};
// Debounce tlacitek.
const unsigned long DEBOUNCE_MS = 50UL;
// Porucha 2 - vzorkovani hladiny do klouzaveho okna.
const unsigned long F2_SAMPLE_MS = 60000UL; // 1 vzorek za minutu
const int F2_BUF_SIZE = 121; // max. okno 120 min + rezerva
// Spotreba za 24 h (v3): minutove pricitani poklesu, klouzave okno
// 24 h slozene z 5min kosu v EEPROM (288 kosu x 2 B = 576 B).
const unsigned long CONS_MINUTE_MS = 60000UL; // 1 minuta
const uint8_t CONS_BUCKET_MINUTES = 5; // 1 kos = 5 minut
const uint16_t CONS_BUCKETS = 288; // 24 h / 5 min
const uint16_t CONS_MARKER = 0xFFFF; // znacka aktualni pozice
const uint16_t CONS_VALUE_MAX = 0xFFFE; // max. litru v kosi
// Displej: LCD 20x4 pres I2C.
const uint8_t LCD_ADDR = 0x27;
const uint8_t LCD_COLS = 20;
const uint8_t LCD_ROWS = 4;
const unsigned long LCD_REFRESH_MS = 250UL; // v4: 250 ms kvuli presnemu
// blikani ZASTAVENO (1 Hz)
// Obrazovka kalibrace: mizi 3 s po poslednim pohybu potenciometru A2
// (behem rozpracovane kalibrace zustava trvale).
const unsigned long CAL_SCREEN_MS = 3000UL;
const int POT_DEADBAND = 8;
// v4: klouzavy prumer hladiny (Moving Average) - 16 vzorku po 250 ms.
const unsigned long MA_SAMPLE_MS = 250UL;
const uint8_t MA_SIZE = 16;
// v4: stabilizace zobrazeni hladiny - obnova 1x za 2 s, prepis az pri
// zmene o +-1 %, litry zaokrouhlene na desitky.
const unsigned long DISP_LEVEL_MS = 2000UL;
// v4: blikani napisu ZASTAVENO (1 Hz = 500 ms svit / 500 ms tma).
const unsigned long BLINK_HALF_MS = 500UL;
// EEPROM - ulozene hodnoty (interni pamet ATmega328P).
const int EE_ADDR = 0; // struct Persist (nastaveni)
const int EE_CONS_ADDR = 32; // kose spotreby 24 h (576 B)
const uint16_t EE_MAGIC = 0xC0DB; // v2 - zmena struktury dat
/* ========================= PINY =================================== */
const uint8_t PIN_POT_MAXDIFF = A0;
const uint8_t PIN_POT_WINDOW = A1;
const uint8_t PIN_POT_CAL = A2;
const uint8_t PIN_POT_IND100 = A3; // rezervovano (bez alarmove funkce)
const uint8_t PIN_SENSOR = A7;
const uint8_t PIN_BTN_SAVE100 = 2; // SW6
const uint8_t PIN_BTN_SAVECAL = 3; // SW5
const uint8_t PIN_SW_AUTMAN = 5; // SW4 (0=AUT, 1=MAN)
const uint8_t PIN_BTN_RESET = 7; // SW2
const uint8_t PIN_BTN_START = 8; // SW1
const uint8_t PIN_BTN_WATER = 9; // SW3
const uint8_t PIN_RELAY = 6; // rele cerpadla
const uint8_t PIN_FAULT = 10; // indikace poruchy
/* ====================== DATOVE TYPY =============================== */
// Stavovy automat rizeni cerpadla.
enum State : uint8_t {
ST_STOPPED, // po Resetu / poruse - rele vypnuto, ceka na Start
ST_START_WAIT, // Start stisknut a pusten - bezi 3s odpocet
ST_RUNNING, // normalni automaticky provoz - rele seplo
ST_WATERING // rezim zalevani - rele seplo na navoleny cas
};
// Stav kalibracni procedury (doplneni, bod 2).
enum CalState : uint8_t {
CAL_IDLE, // klid - ceka na 1. stisk SW5
CAL_ARMED // hladina zapamatovana - odcerpava se, ceka na 2. stisk
};
// Data ukladana do interni EEPROM (s kontrolnim souctem).
struct Persist {
uint16_t magic; // EE_MAGIC = data platna
float level100Cm; // ulozena 100% vyska hladiny [cm]
float litersPerCm; // kalibracni konstanta [l/cm]
uint8_t crc; // XOR kontrolni soucet
};
// Stav jednoho tlacitka s debouncingem (aktivni v log.1, pull-down).
struct Button {
uint8_t pin;
bool stable; // ustaleny stav
bool lastRead; // posledni ctena uroven
unsigned long lastChangeMs;
unsigned long pressBeganMs; // cas stisku (pro dlouhy stisk)
bool pressedEvent; // hrana 0->1 (stisk)
bool releasedEvent; // hrana 1->0 (pusteni)
};
/* ==================== GLOBALNI PROMENNE =========================== */
LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS);
State state = ST_STOPPED;
// Mereni hladiny
float levelCm = 0.0; // aktualni vyska hladiny [cm] (filtr. hodnota)
float liters = 0.0; // prepocet na litry
float percent = 0.0; // % naplneni vuci 100% hladine
int sensorRaw = 0; // syrova hodnota ADC z A7
// Perzistentni nastaveni
float level100Cm = DEFAULT_LEVEL100_CM;
float litersPerCm = DEFAULT_LITERS_PER_CM;
// Poruchy (porucha 3 zrusena dle doplneni)
bool fault1 = false; // nizka hladina (latch)
bool fault2 = false; // prekroceni rozdilu hladiny (latch)
// Start s 3s prodlevou
unsigned long startWaitBeganMs = 0;
// SW6 - kratky/dlouhy stisk, docasne zobrazeni aktualniho objemu
// (v3: kratky i dlouhy stisk zobrazuji AKTUALNI objem v litrech)
bool sw6LongDone = false; // dlouhy stisk jiz obslouzen
unsigned long volShowUntilMs = 0; // do kdy zobrazit objem misto %
// Zalevani
uint8_t waterPressCount = 0;
unsigned long waterLastPressMs = 0;
bool waterSelecting = false;
unsigned long waterEndMs = 0;
unsigned int waterMinutes = 0;
float waterStartLiters = 0.0; // hladina pri 1. stisku SW3
float waterConsumption = 0.0; // spotreba posledniho zalevani [l]
// Kalibracni procedura
CalState calState = CAL_IDLE;
float calStartCm = 0.0; // vyska hladiny pri 1. stisku SW5
// Spotreba za 24 h (v3 - klouzave okno s persistenci v EEPROM)
bool consRunning = false; // mereni bezi (od Startu / obnovy)
unsigned long consMinuteBeganMs = 0; // zacatek aktualni minuty
float consMinuteStartLiters = 0.0; // objem na zacatku minuty
float consBucketAccum = 0.0; // litru v rozpracovanem 5min kosi
uint8_t consMinutesInBucket = 0;// pocet minut v rozpracovanem kosi
float consTotal = 0.0; // soucet uzavrenych kosu v okne [l]
uint16_t consWriteIdx = 0; // pozice markeru v kruhovem bufferu
// Porucha 2 - kruhovy buffer minutovych vzorku hladiny [l]
int16_t f2Buf[F2_BUF_SIZE];
uint8_t f2Head = 0;
uint8_t f2Count = 0;
unsigned long f2LastSampleMs = 0;
// Tlacitka
Button btnSave100 = {PIN_BTN_SAVE100, false, false, 0, 0, false, false};
Button btnSaveCal = {PIN_BTN_SAVECAL, false, false, 0, 0, false, false};
Button btnReset = {PIN_BTN_RESET, false, false, 0, 0, false, false};
Button btnStart = {PIN_BTN_START, false, false, 0, 0, false, false};
Button btnWater = {PIN_BTN_WATER, false, false, 0, 0, false, false};
// v4: Moving Average buffer hladiny [cm]
float maBuf[MA_SIZE];
uint8_t maIdx = 0;
unsigned long maLastSampleMs = 0;
// v4: stabilizovane hodnoty hladiny pro displej
long dispLiters = 0; // litry zaokrouhlene na desitky
int dispPct = 0; // procenta
unsigned long dispLastUpdateMs = 0; // posledni obnova (max 1x za 2 s)
// Displej
unsigned long lcdLastRefreshMs = 0;
unsigned long calPotLastMoveMs = 0;
int calPotLastRaw = -1000;
unsigned long a0LastMoveMs = 0;
unsigned long a1LastMoveMs = 0;
int a0LastRaw = -1000;
int a1LastRaw = -1000;
/* ======================== SETUP =================================== */
void setup() {
// Tlacitka maji externi pull-down -> prosty INPUT.
pinMode(PIN_BTN_SAVE100, INPUT);
pinMode(PIN_BTN_SAVECAL, INPUT);
pinMode(PIN_SW_AUTMAN, INPUT);
pinMode(PIN_BTN_RESET, INPUT);
pinMode(PIN_BTN_START, INPUT);
pinMode(PIN_BTN_WATER, INPUT);
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_FAULT, OUTPUT);
digitalWrite(PIN_RELAY, LOW); // po zapnuti rele rozeple
digitalWrite(PIN_FAULT, LOW);
loadPersist(); // nacteni EEPROM (100 %, konstanta l/cm)
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Cerpani vody v4.0"));
lcd.setCursor(0, 1);
lcd.print(F("Start systemu..."));
// Inicializacni zmereni hladiny (naplneni filtru realnou hodnotou).
sensorRaw = readSensorRaw();
levelCm = rawToCm(sensorRaw);
liters = levelCm * litersPerCm;
for (uint8_t i = 0; i < MA_SIZE; i++) maBuf[i] = levelCm; // v4: MA
// v4: inicializace stabilizovanych hodnot pro displej
dispLiters = ((long)(liters + 5.0) / 10) * 10;
dispPct = (int)((level100Cm > 1.0)
? (levelCm / level100Cm) * 100.0 + 0.5 : 0.0);
f2Reset();
consRestore(); // obnova okna spotreby 24 h z EEPROM
delay(1000); // jedina blokujici prodleva - uvodni obrazovka
lcd.clear();
}
/* ========================= LOOP =================================== */
void loop() {
const unsigned long now = millis();
readInputs(now); // tlacitka + potenciometry + cidlo
updateFaults(now); // vyhodnoceni poruch 1/2
handleButtons(now); // Reset / Start / Zalevani / SW5 / SW6
updateStateMachine(now);// prechody stavu vc. 3s startu a konce zalevani
updateConsumption(now); // spotreba za 24 h
driveOutputs(); // rele D6 + porucha D10
updateDisplay(now); // prekresleni LCD
}
/* ============== CTENI VSTUPU + PREVODY HLADINY ==================== */
// Precte cidlo hladiny s prumerovanim (potlaceni sumu ADC).
int readSensorRaw() {
long sum = 0;
for (uint8_t i = 0; i < 8; i++) {
sum += analogRead(PIN_SENSOR);
}
return (int)(sum / 8);
}
// Prevod ADC -> vyska hladiny [cm]. 0,094 V = 0 cm (pracovni nula).
float rawToCm(int raw) {
if (raw <= ADC_ZERO_RAW) return 0.0;
return (float)(raw - ADC_ZERO_RAW) * LEVEL_FULL_CM
/ (float)(ADC_FULL_RAW - ADC_ZERO_RAW);
}
// Limit poruchy 2 [l] z potenciometru A0 (0V=100 l, 5V=500 l).
float f2LimitLiters() {
int a0 = analogRead(PIN_POT_MAXDIFF);
return 100.0 + ((float)a0 / 1023.0) * 400.0;
}
// Delka klouzaveho okna [min] z potenciometru A1 (0V=30, 5V=120 min).
unsigned int f2WindowMinutes() {
int a1 = analogRead(PIN_POT_WINDOW);
return (unsigned int)(30.0 + ((float)a1 / 1023.0) * 90.0 + 0.5);
}
// Kalibrace: skutecne odcerpane mnozstvi [l] dle potenciometru A2.
// Rozsah +-50 % kolem hodnoty vypoctene dle stavajici konstanty.
float calActualLiters() {
float measured = (calStartCm - levelCm) * litersPerCm; // dle stare konst.
if (measured < 0.0) measured = 0.0;
int a2 = analogRead(PIN_POT_CAL);
float factor = 0.5 + ((float)a2 / 1023.0); // 0,5 .. 1,5 (=+-50 %)
return measured * factor;
}
// Kalibrace: nova konstanta [l/cm] ze skutecneho mnozstvi a rozdilu cm.
float calNewConstant() {
float diffCm = calStartCm - levelCm;
if (diffCm < 0.5) return litersPerCm; // prilis maly rozdil - drz starou
return calActualLiters() / diffCm;
}
// Nacte vsechny vstupy, aktualizuje hladinu/litry/procenta a detekuje
// pohyb potenciometru (kvuli docasnym obrazovkam nastaveni).
void readInputs(unsigned long now) {
// --- cidlo hladiny s klouzavym prumerem (v4: MA 16 x 250 ms) ---
// Vyhlazena hodnota se pouziva vsude vc. poruch a spinani rele D6.
sensorRaw = readSensorRaw();
if (now - maLastSampleMs >= MA_SAMPLE_MS) {
maLastSampleMs = now;
maBuf[maIdx] = rawToCm(sensorRaw);
maIdx = (maIdx + 1) % MA_SIZE;
float sum = 0.0;
for (uint8_t i = 0; i < MA_SIZE; i++) sum += maBuf[i];
levelCm = sum / MA_SIZE;
}
liters = levelCm * litersPerCm;
float liters100 = level100Cm * litersPerCm;
percent = (liters100 > 1.0) ? (liters / liters100) * 100.0 : 0.0;
// --- tlacitka (debounce) ---
updateButton(btnSave100, now);
updateButton(btnSaveCal, now);
updateButton(btnReset, now);
updateButton(btnStart, now);
updateButton(btnWater, now);
// --- detekce pohybu potenciometru A2 (obrazovka kalibrace) ---
int a2 = analogRead(PIN_POT_CAL);
if (calPotLastRaw < -999) calPotLastRaw = a2;
if (abs(a2 - calPotLastRaw) > POT_DEADBAND) {
calPotLastRaw = a2;
calPotLastMoveMs = now;
}
// --- detekce pohybu A0 / A1 (zobrazeni nastavovane hodnoty) ---
int a0 = analogRead(PIN_POT_MAXDIFF);
if (a0LastRaw < -999) a0LastRaw = a0;
if (abs(a0 - a0LastRaw) > POT_DEADBAND) { a0LastRaw = a0; a0LastMoveMs = now; }
int a1 = analogRead(PIN_POT_WINDOW);
if (a1LastRaw < -999) a1LastRaw = a1;
if (abs(a1 - a1LastRaw) > POT_DEADBAND) { a1LastRaw = a1; a1LastMoveMs = now; }
}
// Debounce jednoho tlacitka; generuje udalosti stisk/pusteni a eviduje
// cas stisku pro rozliseni dlouheho stisku (SW6).
void updateButton(Button &b, unsigned long now) {
b.pressedEvent = false;
b.releasedEvent = false;
bool raw = (digitalRead(b.pin) == HIGH);
if (raw != b.lastRead) {
b.lastRead = raw;
b.lastChangeMs = now;
}
if ((now - b.lastChangeMs) >= DEBOUNCE_MS && raw != b.stable) {
b.stable = raw;
if (raw) { b.pressedEvent = true; b.pressBeganMs = now; }
else b.releasedEvent = true;
}
}
// Vraci true, je-li prepinac SW4 v poloze MAN (log.1).
bool isManualMode() {
return digitalRead(PIN_SW_AUTMAN) == HIGH;
}
/* ====================== PORUCHY 1 / 2 ============================= */
// Reset klouzaveho okna poruchy 2 (naplni buffer aktualni hodnotou).
void f2Reset() {
int16_t cur = (int16_t)liters;
for (int i = 0; i < F2_BUF_SIZE; i++) f2Buf[i] = cur;
f2Head = 0;
f2Count = 1;
f2LastSampleMs = millis();
}
// Prida minutovy vzorek litru do kruhoveho bufferu (jen za behu cerpadla).
void f2AddSample(unsigned long now) {
if (now - f2LastSampleMs < F2_SAMPLE_MS) return;
f2LastSampleMs = now;
f2Head = (f2Head + 1) % F2_BUF_SIZE;
f2Buf[f2Head] = (int16_t)liters;
if (f2Count < F2_BUF_SIZE) f2Count++;
}
// Porucha 2: pokles objemu v okne delky dle A1 (30-120 min) vetsi nez
// limit dle A0 (100-500 l) -> porucha (unik/prasknute potrubi).
bool f2Check() {
unsigned int windowMin = f2WindowMinutes();
if (f2Count < 2) return false;
uint8_t depth = min((uint8_t)windowMin, (uint8_t)(f2Count - 1));
// Nejvyssi hodnota v okne minus aktualni = maximalni pokles.
int16_t maxInWindow = f2Buf[f2Head];
for (uint8_t i = 1; i <= depth; i++) {
uint8_t idx = (f2Head + F2_BUF_SIZE - i) % F2_BUF_SIZE;
if (f2Buf[idx] > maxInWindow) maxInWindow = f2Buf[idx];
}
float drop = (float)maxInWindow - liters;
return drop > f2LimitLiters();
}
// Vyhodnoceni poruch 1 a 2 (porucha 3 zrusena dle doplneni).
// Poruchy jsou latch - drzi az do Resetu.
void updateFaults(unsigned long now) {
// Porucha 1 - nizka hladina; latch drzi do Resetu.
if (sensorRaw < ADC_ZERO_RAW) {
fault1 = true;
}
// Porucha 2 - vyhodnocuje se jen za behu cerpadla (RUNNING/WATERING).
// v4: behem kalibracni procedury (CAL_ARMED) se porucha 2 NEhlida
// (voda se manualne odcerpava - pokles je zamerny).
if ((state == ST_RUNNING || state == ST_WATERING)
&& calState != CAL_ARMED) {
f2AddSample(now);
if (f2Check()) fault2 = true;
}
}
// Podminka pro zruseni latchu poruchy 1 pri Resetu: hladina musi byt
// alespon FAULT1_CLEAR_CM nad pracovni nulou (hystereze).
bool fault1ConditionCleared() {
return levelCm >= FAULT1_CLEAR_CM;
}
/* =============== OBSLUHA TLACITEK A REZIMU ======================== */
// Ukonceni zalevani: vypocet spotreby z rozdilu hladin (doplneni, bod 6).
void finishWatering() {
waterConsumption = waterStartLiters - liters;
if (waterConsumption < 0.0) waterConsumption = 0.0;
waterMinutes = 0;
}
void handleButtons(unsigned long now) {
// ---- Reset (SW2): vypnout rele; smazat poruchy, pokud pricina ustala ----
// v3: Reset spotrebu zalevani NEzaznamenava (jen ukonci rezim).
if (btnReset.pressedEvent) {
state = ST_STOPPED;
waterMinutes = 0;
waterSelecting = false;
waterPressCount = 0;
calState = CAL_IDLE; // rozpracovana kalibrace se rusi
if (fault1 && fault1ConditionCleared()) fault1 = false;
fault2 = false; // pricina pominula zastavenim cerpadla
f2Reset();
}
// ---- Start (SW1): jen v AUT rezimu, jen bez trvajici poruchy 1/2 ----
// Start se realizuje 3 s PO PUSTENI tlacitka (prechod 1 -> 0).
if (btnStart.releasedEvent) {
if (!isManualMode() && !fault1 && !fault2 && state == ST_STOPPED) {
state = ST_START_WAIT;
startWaitBeganMs = now;
consStart(now); // spotreba 24 h se meri od Startu
}
}
// ---- Zalevani (SW3): pocet stisku voli cas, 5. stisk = zrusit ----
if (btnWater.pressedEvent && !fault1 && !fault2) {
if (!waterSelecting && state != ST_WATERING) {
// 1. stisk: registrace hladiny pro vypocet spotreby (doplneni).
waterStartLiters = liters;
}
waterSelecting = true;
waterLastPressMs = now;
waterPressCount++;
if (waterPressCount >= 5) {
// 5. stisk = zruseni funkce; v4: primy prechod do provozu
// dle prepinace D5 (AUT -> rovnou bezi, bez Reset + Start).
waterSelecting = false;
waterPressCount = 0;
if (state == ST_WATERING) {
finishWatering();
enterDirectRun();
}
}
}
// Potvrzeni volby zalevani po 2 s bez dalsiho stisku.
if (waterSelecting && (now - waterLastPressMs >= WATER_CONFIRM_MS)) {
waterSelecting = false;
if (waterPressCount >= 1 && waterPressCount <= 4) {
waterMinutes = WATER_MINUTES[waterPressCount - 1];
waterEndMs = now + (unsigned long)waterMinutes * 60000UL;
state = ST_WATERING;
f2Reset();
}
waterPressCount = 0;
}
// ---- SW6 (D2): kratky vs. dlouhy stisk (v3, reakce bod 1) ----
// Dlouhy stisk (>3 s v drzeni): ulozit aktualni hladinu jako 100 %.
if (btnSave100.stable && !sw6LongDone
&& (now - btnSave100.pressBeganMs >= SW6_LONG_MS)) {
sw6LongDone = true;
level100Cm = levelCm;
savePersist();
volShowUntilMs = now + SW6_SHOW_MS; // zobrazit aktualni objem
}
// Kratky stisk (pusteni pred prahem): 5 s zobrazit AKTUALNI objem.
if (btnSave100.releasedEvent) {
if (!sw6LongDone) {
volShowUntilMs = now + SW6_SHOW_MS; // zobrazit aktualni objem
}
sw6LongDone = false;
}
// ---- SW5 (D3): dvoukrokova kalibracni procedura (doplneni, bod 2) ----
if (btnSaveCal.pressedEvent) {
if (calState == CAL_IDLE) {
// 1. stisk: zapamatovat vychozi hladinu, zacina odcerpavani.
calStartCm = levelCm;
calState = CAL_ARMED;
} else {
// 2. stisk: ulozit novou konstantu misto stavajici.
float newConst = calNewConstant();
if (newConst > 1.0 && newConst < 1000.0) { // ochrana proti nesmyslu
litersPerCm = newConst;
savePersist();
}
calState = CAL_IDLE;
// v4: odcerpana voda nesmi vyvolat poruchu 2 + primy prechod
// do AUT/MAN provozu bez nutnosti Reset + Start.
f2Reset();
enterDirectRun();
}
}
}
// v4: primy prechod do provozu dle prepinace D5 - v AUT rezimu rovnou
// ST_RUNNING (rele D6 sepne bez Reset + Start), v MAN rezimu STOPPED
// (zobrazi se MANUALNI PROVOZ, rele v MAN nebezi).
void enterDirectRun() {
if (!isManualMode() && !fault1 && !fault2) {
state = ST_RUNNING;
f2Reset();
} else {
state = ST_STOPPED;
}
}
/* ==================== STAVOVY AUTOMAT ============================= */
void updateStateMachine(unsigned long now) {
// Trvajici porucha 1/2 okamzite zastavi cerpadlo (latch drzi do Resetu).
if (fault1 || fault2) {
if (state == ST_WATERING) finishWatering();
if (state != ST_STOPPED) state = ST_STOPPED;
return;
}
switch (state) {
case ST_START_WAIT:
// MAN rezim behem cekani rusi start (Start funguje jen v AUT).
if (isManualMode()) {
state = ST_STOPPED;
} else if (now - startWaitBeganMs >= START_DELAY_MS) {
state = ST_RUNNING;
f2Reset();
}
break;
case ST_RUNNING:
// Prepnuti do MAN za behu -> zastavit automatiku (v MAN jen indikace).
if (isManualMode()) state = ST_STOPPED;
break;
case ST_WATERING:
// Konec navolene doby -> spotreba + v4: primy prechod do
// AUT/MAN provozu (v AUT rele bezi dal bez Reset + Start).
if ((long)(now - waterEndMs) >= 0) {
finishWatering();
enterDirectRun();
}
break;
case ST_STOPPED:
default:
break;
}
}
/* ================= SPOTREBA ZA 24 HODIN (v3) ====================== */
/*
* Reakce, bod 4: klouzave okno 24 h. Kazdou 1 minutu se pricte pokles
* hladiny (pritok se ignoruje) a displej se hned obnovi. Do 24 h od
* Startu se zobrazuje prubezna hodnota; po naplneni okna klouzava
* hodnota za poslednich 24 h (obnova rovnez po 1 minute).
*
* Persistence: okno = 288 kosu po 5 minutach v interni EEPROM jako
* kruhovy buffer. Pozice zapisu je oznacena markerem 0xFFFF (netreba
* samostatny index -> rovnomerne opotrebeni, kazda bunka se prepise
* jen ~2x za 24 h). Po vypadku napajeni se soucet obnovi z EEPROM;
* ztratit se muze jen rozpracovany 5min kos.
*/
// Adresa kose v EEPROM.
int consAddr(uint16_t idx) { return EE_CONS_ADDR + (int)idx * 2; }
uint16_t consReadBucket(uint16_t idx) {
uint16_t v;
EEPROM.get(consAddr(idx), v);
return v;
}
void consWriteBucket(uint16_t idx, uint16_t v) {
EEPROM.put(consAddr(idx), v); // put = update, zapis jen pri zmene
}
// Obnova okna spotreby z EEPROM po zapnuti napajeni.
// Najde marker (0xFFFF) a secte vsechny platne kose.
void consRestore() {
consTotal = 0.0;
consWriteIdx = 0;
bool markerFound = false;
for (uint16_t i = 0; i < CONS_BUCKETS; i++) {
uint16_t v = consReadBucket(i);
if (v == CONS_MARKER) {
if (!markerFound) { consWriteIdx = i; markerFound = true; }
} else if (v <= CONS_VALUE_MAX) {
consTotal += (float)v;
}
}
if (!markerFound) {
// EEPROM neinicializovana (prvni spusteni) - vynulovat cele okno.
for (uint16_t i = 0; i < CONS_BUCKETS; i++) consWriteBucket(i, 0);
consWriteBucket(0, CONS_MARKER);
consWriteIdx = 0;
consTotal = 0.0;
}
// Mereni pokracuje ihned po obnove (i bez stisku Startu po vypadku).
consRunning = (consTotal > 0.0);
consMinuteBeganMs = millis();
consMinuteStartLiters = liters;
consBucketAccum = 0.0;
consMinutesInBucket = 0;
}
// Vynulovani okna spotreby a spusteni mereni (volano pri Startu).
void consStart(unsigned long now) {
for (uint16_t i = 0; i < CONS_BUCKETS; i++) consWriteBucket(i, 0);
consWriteBucket(0, CONS_MARKER);
consWriteIdx = 0;
consTotal = 0.0;
consRunning = true;
consMinuteBeganMs = now;
consMinuteStartLiters = liters;
consBucketAccum = 0.0;
consMinutesInBucket = 0;
}
// Aktualni hodnota spotreby 24 h pro displej (uzavrene kose
// + rozpracovany kos) - obnovuje se kazdou minutu.
float cons24hValue() {
return consTotal + consBucketAccum;
}
void updateConsumption(unsigned long now) {
if (!consRunning) return;
if (now - consMinuteBeganMs >= CONS_MINUTE_MS) {
// Konec minuty: pricist pokles hladiny (pritok se nezaznamenava).
float drop = consMinuteStartLiters - liters;
if (drop > 0.0) consBucketAccum += drop;
consMinuteBeganMs = now;
consMinuteStartLiters = liters;
consMinutesInBucket++;
if (consMinutesInBucket >= CONS_BUCKET_MINUTES) {
// Uzavreni 5min kose: zapis do EEPROM a posun markeru.
uint16_t v = (consBucketAccum > (float)CONS_VALUE_MAX)
? CONS_VALUE_MAX : (uint16_t)(consBucketAccum + 0.5);
uint16_t nextIdx = (consWriteIdx + 1) % CONS_BUCKETS;
// Nejstarsi kos (na nove pozici markeru) vypadava z okna.
uint16_t oldest = consReadBucket(nextIdx);
if (oldest != CONS_MARKER && oldest <= CONS_VALUE_MAX)
consTotal -= (float)oldest;
if (consTotal < 0.0) consTotal = 0.0;
// Poradi zapisu: nejdriv marker na novou pozici, pak hodnota
// (pri vypadku behem zapisu zustane okno konzistentni).
consWriteBucket(nextIdx, CONS_MARKER);
consWriteBucket(consWriteIdx, v);
consTotal += (float)v;
consWriteIdx = nextIdx;
consBucketAccum = 0.0;
consMinutesInBucket = 0;
}
}
}
/* ======================= VYSTUPY ================================== */
void driveOutputs() {
// Rele D6: seple pri RUNNING nebo WATERING.
bool relayOn = (state == ST_RUNNING || state == ST_WATERING)
&& !fault1 && !fault2;
digitalWrite(PIN_RELAY, relayOn ? HIGH : LOW);
// Porucha D10: log.1 pri poruse 1 nebo 2 (porucha 3 zrusena).
digitalWrite(PIN_FAULT, (fault1 || fault2) ? HIGH : LOW);
}
/* ================== EEPROM (interni pamet) ======================== */
// XOR kontrolni soucet struktury Persist (bez pole crc).
uint8_t persistCrc(const Persist &p) {
const uint8_t *b = (const uint8_t *)&p;
uint8_t crc = 0x5A;
for (size_t i = 0; i < sizeof(Persist) - sizeof(p.crc); i++) crc ^= b[i];
return crc;
}
// Nacte ulozene hodnoty; pri nevalidnich datech pouzije vychozi.
void loadPersist() {
Persist p;
EEPROM.get(EE_ADDR, p);
if (p.magic == EE_MAGIC && p.crc == persistCrc(p)
&& p.level100Cm > 0.0 && p.level100Cm <= LEVEL_FULL_CM
&& p.litersPerCm > 1.0 && p.litersPerCm < 1000.0) {
level100Cm = p.level100Cm;
litersPerCm = p.litersPerCm;
} else {
level100Cm = DEFAULT_LEVEL100_CM;
litersPerCm = DEFAULT_LITERS_PER_CM;
}
}
// Ulozi hodnoty do EEPROM. EEPROM.put pouziva update() -> zapisuje se
// jen pri zmene, tim se setri zivotnost pameti (~100 000 cyklu).
void savePersist() {
Persist p;
p.magic = EE_MAGIC;
p.level100Cm = level100Cm;
p.litersPerCm = litersPerCm;
p.crc = persistCrc(p);
EEPROM.put(EE_ADDR, p);
}
/* ======================== DISPLEJ ================================= */
/*
* v4: radky s dvojteckou zarovnany pod sebe (dvojtecka na sloupci 9),
* za kazdou hodnotou v % se zobrazuje znacka %:
*
* AUT: 1: NADRZ :4260 L 85% MAN: 3: MANUALNI PROVOZ
* 2: SPOT.24H :1.234 L 4: (neobsazen)
* 3: AUTOMATICKY PROVOZ / ZASTAVENO (blika 1 Hz)
* 4: ALARM :OK / NIZKA HLAD / SPOTR.VODY
*
* Zalevani: 3: ZALEVANI :XXX MIN (zbyva)
* 4: SPOTREBA :XXX L (rozdil hladin od zacatku)
*
* Kalibrace: 1: KALIBRACE CIDLA 3: SKUTECNE :XXX L (pot. A2)
* 2: ODCERPANO:XXX L 4: KONSTANTA:XX.X (nova l/cm)
*/
// Vypis radku doplneny mezerami na sirku LCD (prepis bez blikani).
void lcdPrintLine(uint8_t row, const char *text) {
lcd.setCursor(0, row);
uint8_t i = 0;
for (; text[i] != '\0' && i < LCD_COLS; i++) lcd.print(text[i]);
for (; i < LCD_COLS; i++) lcd.print(' ');
}
// v4: stabilizace zobrazene hodnoty hladiny - obnova max 1x za 2 s,
// prepis az pri zmene o +-1 % (procentniho bodu nebo 1 % litru),
// litry zaokrouhlene na desitky. Interni mereni bezi dal presne.
void updateDispLevel(unsigned long now) {
if (now - dispLastUpdateMs < DISP_LEVEL_MS) return;
long newLiters = ((long)(liters + 5.0) / 10) * 10; // na desitky
int newPct = (int)(percent + 0.5);
if (newPct > 999) newPct = 999;
long thrLiters = labs(dispLiters) / 100; // 1 % litru
if (thrLiters < 10) thrLiters = 10;
if (labs(newLiters - dispLiters) >= thrLiters
|| abs(newPct - dispPct) >= 1) {
dispLiters = newLiters;
dispPct = newPct;
dispLastUpdateMs = now;
}
}
// Radek 1 (spolecny): NADRZ :XXXX L XX% (dvojtecka na sloupci 9).
// Pri pohybu A0/A1 nebo po stisku SW6 se zobrazi nastavovana hodnota.
void displayRow1(unsigned long now, char *line, size_t n) {
if (volShowUntilMs != 0 && (long)(volShowUntilMs - now) > 0) {
// SW6: 5 s docasne zobrazeni AKTUALNIHO objemu v litrech.
snprintf(line, n, "NADRZ :AKT %ld L", ((long)(liters + 5.0) / 10) * 10);
} else if (a0LastMoveMs != 0 && (now - a0LastMoveMs) < CAL_SCREEN_MS) {
snprintf(line, n, "MAX.DIF :%ld L", (long)f2LimitLiters());
} else if (a1LastMoveMs != 0 && (now - a1LastMoveMs) < CAL_SCREEN_MS) {
snprintf(line, n, "PLOV.CAS :%u MIN", f2WindowMinutes());
} else {
snprintf(line, n, "NADRZ :%ld L %d%%", dispLiters, dispPct);
}
}
// Radek 2 (spolecny): SPOT.24H :X.XXX L (dvojtecka na sloupci 9).
// Klouzava hodnota spotreby 24 h, obnovovana kazdou 1 minutu.
void displayRow2(char *line, size_t n) {
long c = (long)(cons24hValue() + 0.5);
if (c >= 1000)
snprintf(line, n, "SPOT.24H :%ld.%03ld L", c / 1000, c % 1000);
else
snprintf(line, n, "SPOT.24H :%ld L", c);
}
void updateDisplay(unsigned long now) {
updateDispLevel(now); // v4: stabilizovana hodnota hladiny
if (now - lcdLastRefreshMs < LCD_REFRESH_MS) return;
lcdLastRefreshMs = now;
char line[21];
// ---- Obrazovka Kalibrace cidla ----
// Zobrazi se pri pohybu A2 (mizi po 3 s) nebo po celou dobu
// rozpracovane kalibracni procedury (CAL_ARMED).
bool calScreen = (calState == CAL_ARMED)
|| (calPotLastMoveMs != 0 && (now - calPotLastMoveMs) < CAL_SCREEN_MS);
if (calScreen) {
lcdPrintLine(0, "KALIBRACE CIDLA");
if (calState == CAL_ARMED) {
// r.2: odcerpano dle stavajici konstanty (dvojtecka na sl. 9)
float pumped = (calStartCm - levelCm) * litersPerCm;
if (pumped < 0.0) pumped = 0.0;
//snprintf(line, sizeof(line), "ODCERPANO:%ld L", (long)pumped);
snprintf(line, sizeof(line), "NADRZ :4260 L 80%%", (long)pumped);
lcdPrintLine(1, line);
// r.3: skutecne mnozstvi dle potenciometru A2 (+-50 %)
snprintf(line, sizeof(line), "SKUTECNE :%ld L", (long)calActualLiters());
lcdPrintLine(2, line);
// r.4: nova konstanta l/cm (na desetiny)
float k = calNewConstant();
snprintf(line, sizeof(line), "KONSTANTA:%ld.%d",
(long)k, (int)(k * 10) % 10);
lcdPrintLine(3, line);
} else {
// Kalibrace nezahajena - zobraz aktualni konstantu a napovedu.
snprintf(line, sizeof(line), "KONSTANTA:%ld.%d L/CM",
(long)litersPerCm, (int)(litersPerCm * 10) % 10);
lcdPrintLine(1, line);
lcdPrintLine(2, "STISKNI SW5 A");
lcdPrintLine(3, "ODCERPEJ VODU");
}
return;
}
// ---- Radky 1 a 2 (spolecne pro AUT/MAN/Zalevani) ----
displayRow1(now, line, sizeof(line));
lcdPrintLine(0, line);
displayRow2(line, sizeof(line));
lcdPrintLine(1, line);
// ---- Radky 3 a 4 dle rezimu ----
if (state == ST_WATERING || waterSelecting) {
// Obrazovka Zalevani (dvojtecky na sloupci 9)
if (waterSelecting) {
if (waterPressCount >= 1 && waterPressCount <= 4)
snprintf(line, sizeof(line), "ZALEVANI :%u MIN ?",
WATER_MINUTES[waterPressCount - 1]);
else
snprintf(line, sizeof(line), "ZALEVANI :ZRUSIT ?");
} else {
unsigned long remainMin = (waterEndMs > now)
? (waterEndMs - now + 59999UL) / 60000UL : 0;
snprintf(line, sizeof(line), "ZALEVANI :%lu MIN", remainMin);
}
lcdPrintLine(2, line);
// r.4: prubezna spotreba zalevani (rozdil hladin od zacatku)
float cons = (state == ST_WATERING) ? (waterStartLiters - liters)
: waterConsumption;
if (cons < 0.0) cons = 0.0;
snprintf(line, sizeof(line), "SPOTREBA :%ld L", (long)cons);
lcdPrintLine(3, line);
}
else if (isManualMode()) {
// Obrazovka MAN provoz
lcdPrintLine(2, "MANUALNI PROVOZ");
// r.4 neobsazen dle zadani; pri poruse presto zobraz alarm
if (fault1) lcdPrintLine(3, "ALARM :NIZKA HLAD");
else if (fault2) lcdPrintLine(3, "ALARM :SPOTR.VODY");
else lcdPrintLine(3, "");
}
else {
// Obrazovka AUT provoz
if (state == ST_RUNNING) lcdPrintLine(2, "AUTOMATICKY PROVOZ");
else if (state == ST_START_WAIT) lcdPrintLine(2, "START ZA 3 S...");
else {
// v4: napis ZASTAVENO blika kmitoctem 1 Hz (500 ms / 500 ms)
bool blinkOn = ((now / BLINK_HALF_MS) % 2) == 0;
lcdPrintLine(2, blinkOn ? "ZASTAVENO" : "");
}
// v4: "ALARM :OK" misto OFF; porucha 2 = SPOTR.VODY
if (fault1) lcdPrintLine(3, "ALARM :NIZKA HLAD");
else if (fault2) lcdPrintLine(3, "ALARM :SPOTR.VODY");
else lcdPrintLine(3, "ALARM :OK");
}
}
Garaz
Min Mez
Hystereze