/*
ESP2_HMI_Gateway_AllInOne.ino
Purificadora Contactor - Dual ESP32 system
ESP#2 = HMI + Sensor T3 (AJ-SR04M crudo) + keypad 4x4 + LCD 20x4 I2C + UDP
Features included (hasta el momento):
- WiFi STA conectándose al AP PURIF_CTRL (sin internet)
- Lee T3 por AJ-SR04M y envía a ESP1 por UDP: T3=..;OK=..;SEQ=..;
- LCD20x4 I2C (SDA=21, SCL=22)
- Keypad 4x4 con menú:
HOME (A=Menu, D=Info, C=Diag)
Menu principal: Duración, Umbrales, Calibración, MQTT Config
Duración: AUTO/60/120/180/240 (envía CMD=DUR)
Umbrales: LOW/HIGH/RAW (edición + confirm, envía CMD=THR)
Calibración: T1/T2 (vía ESP1) y T3 local
MQTT Config: EN, IP, PORT (guarda en NVS local y manda CMD=MQTT a ESP1)
- INFO: muestra IP/RSSI local, estado ESP1, y rota: fail counters / uptime / MQTT short
- DIAG industrial: muestra STA/RSSI/IP, MQTT EN/CONN/host:port, MQF/MQERR/MQAGE, NTP/NTPAGE/HEAP/RST
- Acciones mantenimiento desde INFO/DIAG:
INFO: # reset fails (CMD=RSTFAIL)
DIAG: # reset mqtt fails (CMD=RSTMQ)
* reintento STA (CMD=STARETRY)
A forzar NTP (CMD=NTP)
D reiniciar ESP1 (confirmación)
*/
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>
#include <Preferences.h>
// ---------------- WIFI / UDP ----------------
const char* FW_VER = "ESP2-HMI v1.6";
const char* WIFI_SSID = "PURIF_CTRL";
const char* WIFI_PASS = "12345678";
IPAddress ESP1_IP(192,168,4,1);
const uint16_t UDP_PORT_TX_TO_ESP1 = 4210;
const uint16_t UDP_PORT_RX_FROM_ESP1 = 4211;
WiFiUDP udp;
// ---------------- LCD ----------------
LiquidCrystal_I2C lcd(0x27, 20, 4);
// ---------------- Keypad 4x4 ----------------
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// PINS keypad (ejemplo). Ajusta si tu cableado es diferente.
byte rowPins[ROWS] = {13, 14, 18, 19};
byte colPins[COLS] = {25, 26, 27, 33};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ---------------- Sensor T3 (AJ-SR04M) ----------------
const int PIN_T3_TRIG = 15;
const int PIN_T3_ECHO = 34; // input-only ok
float T3_DIST_EMPTY_CM = 180.0f;
float T3_DIST_FULL_CM = 25.0f;
// ---------------- NVS local ESP2 ----------------
Preferences prefs;
const char* PREF_NS2 = "purif2";
// MQTT config stored in ESP2 and sent to ESP1
bool mqtt_en = false;
String mqtt_ip = "192.168.4.2";
int mqtt_port = 1883;
// ---------------- State from ESP1 ----------------
float t1_pct = NAN, t2_pct = NAN, t3_pct_remote = NAN;
String modeStr = "OFF", whyStr = "OK";
String proc_state = "STBY", proc_cyc = "IDLE";
int rem_sec = -1, sessrem_sec = -1;
float thr_low = NAN, thr_high = NAN, thr_raw = NAN;
// Calibration info from ESP1
float t1_cm = NAN, t2_cm = NAN;
float t1_e = NAN, t1_f = NAN, t2_e = NAN, t2_f = NAN;
// Heartbeat/Uptime from ESP1
int esp1_hb = 0;
uint32_t esp1_up_s = 0;
int esp2ok_remote = -1;
uint32_t esp2age_remote_ms = 0;
// Fail counters from ESP1
uint32_t fc_comm = 0, fc_sens = 0, fc_raw = 0;
// STA/MQTT status from ESP1
int sta_on = 0;
String sta_ip = "0.0.0.0";
int mq_en_remote = 0;
int mq_conn = 0;
String mq_host = "";
int mq_port = 0;
// DIAG from ESP1
int rssi_sta = -127;
uint32_t mqf = 0;
int mqerr = 0;
uint32_t mqage = 0;
int ntp_ok = 0;
uint32_t ntpage = 0;
uint32_t heap_free = 0;
String rst = "";
// Watchdog ESP1
uint32_t lastEsp1RxMs = 0;
const uint32_t ESP1_OFFLINE_MS = 4000;
// ---------------- UI ----------------
enum ScreenMode {
HOME=0,
MENU_MAIN,
MENU_DUR,
CONFIRM_DUR,
MENU_THR,
EDIT_LOW,
EDIT_HIGH,
EDIT_RAW,
CONFIRM_THR,
MENU_CAL,
CAL_VIEW,
CAL_CONFIRM,
MENU_MQTT,
EDIT_MQTT_IP,
EDIT_MQTT_PORT,
CONFIRM_MQTT_SEND,
MAINT,
INFO,
DIAG,
CONFIRM_RST_ESP1
};
ScreenMode screen = HOME;
// --- Keypad diagnostics ---
char lastKey = 0;
uint32_t lastKeyMs = 0;
String lastKeyStr() {
if (!lastKey) return String("-");
return String(lastKey);
}
int selectedDurationMin = 60;
String editBuf;
int calTank = 1;
String calPendingAct = "";
// Home rotator
uint32_t homeRotateMs = 0;
int homeRotateIdx = 0;
const uint32_t HOME_ROTATE_PERIOD_MS = 3000;
// Quick message overlay (for short menu)
String quickMsg = "";
uint32_t quickMsgUntilMs = 0;
// DIAG rotator
int diagIdx = 0;
uint32_t diagMs = 0;
// ---------------- helpers ----------------
void lcdPrintLine(uint8_t row, const String &text) {
lcd.setCursor(0, row);
String t = text;
while (t.length() < 20) t += ' ';
lcd.print(t.substring(0, 20));
}
String fmtPct(float v) {
if (isnan(v) || v < 0) return "--.-";
char b[8]; snprintf(b, sizeof(b), "%4.1f", v);
return String(b);
}
String fmtMMSS(int sec) {
if (sec < 0) return "--:--";
int m = sec / 60, s = sec % 60;
char b[8]; snprintf(b, sizeof(b), "%02d:%02d", m, s);
return String(b);
}
String fmtVal(float v) {
if (isnan(v) || v < 0) return "--";
char b[10]; snprintf(b, sizeof(b), "%4.1f", v);
return String(b);
}
void editBufBackspace(){ if (editBuf.length()>0) editBuf.remove(editBuf.length()-1); }
bool editBufToFloat(float &out){ if (editBuf.length()==0) return false; out = editBuf.toFloat(); return true; }
bool esp1Online() { return (lastEsp1RxMs != 0) && (millis() - lastEsp1RxMs <= ESP1_OFFLINE_MS); }
uint32_t esp1AgeMs() { if (lastEsp1RxMs == 0) return 999999; return millis() - lastEsp1RxMs; }
String fmtAge(uint32_t s){
if (s > 99999) return "--";
if (s < 60) return String(s) + "s";
uint32_t m = s/60;
if (m < 60) return String(m) + "m";
uint32_t h = m/60;
return String(h) + "h";
}
// ---------------- T3 SR04M ----------------
float readDistanceCmOnceT3() {
digitalWrite(PIN_T3_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_T3_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_T3_TRIG, LOW);
uint32_t dur = pulseIn(PIN_T3_ECHO, HIGH, 30000);
if (dur == 0) return NAN;
return (float)dur / 58.2f;
}
float median5(float a[5]) {
for (int i=0;i<5;i++)
for (int j=i+1;j<5;j++)
if (a[j] < a[i]) { float t=a[i]; a[i]=a[j]; a[j]=t; }
return a[2];
}
bool readT3cm(float &cmOut) {
float vals[5]; int ok=0;
for (int i=0;i<5;i++){
float cm = readDistanceCmOnceT3();
if (!isnan(cm) && cm>2 && cm<400) vals[ok++] = cm;
delay(30);
}
if (ok < 3) return false;
while (ok < 5) { vals[ok] = vals[ok-1]; ok++; }
cmOut = median5(vals);
return true;
}
bool cmToPct(float cm, float distEmpty, float distFull, float &pctOut){
float denom = (distEmpty - distFull);
if (fabs(denom) < 1e-3) return false;
float pct = (distEmpty - cm) / denom * 100.0f;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
pctOut = pct;
return true;
}
bool readT3(float &cmOut, float &pctOut) {
if (!readT3cm(cmOut)) return false;
if (!cmToPct(cmOut, T3_DIST_EMPTY_CM, T3_DIST_FULL_CM, pctOut)) return false;
return true;
}
// ---------------- NVS local ----------------
void loadLocalConfig() {
prefs.begin(PREF_NS2, true);
T3_DIST_EMPTY_CM = prefs.getFloat("t3e", T3_DIST_EMPTY_CM);
T3_DIST_FULL_CM = prefs.getFloat("t3f", T3_DIST_FULL_CM);
mqtt_en = prefs.getBool("mq_en", mqtt_en);
mqtt_ip = prefs.getString("mq_ip", mqtt_ip.c_str());
mqtt_port = prefs.getInt("mq_port", mqtt_port);
prefs.end();
}
void saveLocalConfig() {
prefs.begin(PREF_NS2, false);
prefs.putFloat("t3e", T3_DIST_EMPTY_CM);
prefs.putFloat("t3f", T3_DIST_FULL_CM);
prefs.putBool("mq_en", mqtt_en);
prefs.putString("mq_ip", mqtt_ip);
prefs.putInt("mq_port", mqtt_port);
prefs.end();
}
// ---------------- UDP ----------------
void udpSendToEsp1(const String &payload) {
udp.beginPacket(ESP1_IP, UDP_PORT_TX_TO_ESP1);
udp.write((const uint8_t*)payload.c_str(), payload.length());
udp.endPacket();
}
// Commands to ESP1
void sendDurCmd(int minutes) { udpSendToEsp1("CMD=DUR;MIN=" + String(minutes) + ";"); }
void sendMaintStartFixed(int minutes) { udpSendToEsp1("CMD=MAINT;EN=1;CH=1;MIN=" + String(minutes) + ";"); }
void sendMaintStop() { udpSendToEsp1("CMD=MAINTSTOP;"); }
void sendMaintOff() { udpSendToEsp1("CMD=MAINT;EN=0;"); }
void quickSetDuration(int minutes) {
// For short menu: send without confirmation and show an overlay message on HOME.
sendDurCmd(minutes);
quickMsg = "DUR " + String(minutes) + "m enviada";
quickMsgUntilMs = millis() + 1200;
if (screen != HOME) {
screen = HOME;
renderScreen();
} else {
renderHome();
}
}
void sendThrCmd(float low, float high, float rawMin) {
String cmd = "CMD=THR;LOW=" + String((int)roundf(low)) +
";HIGH=" + String((int)roundf(high)) +
";RAW=" + String((int)roundf(rawMin)) + ";";
udpSendToEsp1(cmd);
}
void sendCalCmdEsp1(int tank, const String &act) { udpSendToEsp1("CMD=CAL;T=" + String(tank) + ";ACT=" + act + ";"); }
void sendMqttCfgToEsp1() {
String cmd = "CMD=MQTT;EN=" + String(mqtt_en ? 1 : 0) +
";HOST=" + mqtt_ip +
";PORT=" + String(mqtt_port) +
";USER=;PASS=;";
udpSendToEsp1(cmd);
}
// Maintenance commands
void sendRstFail() { udpSendToEsp1("CMD=RSTFAIL;"); }
void sendRstMq() { udpSendToEsp1("CMD=RSTMQ;"); }
void sendStaRetry(){ udpSendToEsp1("CMD=STARETRY;"); }
void sendForceNtp(){ udpSendToEsp1("CMD=NTP;"); }
void sendRestartEsp1(){ udpSendToEsp1("CMD=RST;"); }
// Parse ESP1 KV packet
void parseKeyValPacket(const String &pkt) {
lastEsp1RxMs = millis();
int i = 0;
while (i < (int)pkt.length()) {
int eq = pkt.indexOf('=', i);
if (eq < 0) break;
int sc = pkt.indexOf(';', eq);
if (sc < 0) sc = pkt.length();
String k = pkt.substring(i, eq); k.trim();
String v = pkt.substring(eq+1, sc); v.trim();
if (k == "HB") esp1_hb = v.toInt();
else if (k == "UP") esp1_up_s = (uint32_t)v.toInt();
else if (k == "ESP2OK") esp2ok_remote = v.toInt();
else if (k == "ESP2AGE") esp2age_remote_ms = (uint32_t)v.toInt();
else if (k == "FC_COMM") fc_comm = (uint32_t)v.toInt();
else if (k == "FC_SENS") fc_sens = (uint32_t)v.toInt();
else if (k == "FC_RAW") fc_raw = (uint32_t)v.toInt();
else if (k == "T1") t1_pct = v.toFloat();
else if (k == "T2") t2_pct = v.toFloat();
else if (k == "T3") t3_pct_remote = v.toFloat();
else if (k == "MODE") modeStr = v;
else if (k == "WHY") whyStr = v;
else if (k == "STATE") proc_state = v;
else if (k == "CYC") proc_cyc = v;
else if (k == "REM") rem_sec = v.toInt();
else if (k == "SESSREM") sessrem_sec = v.toInt();
else if (k == "LOW") thr_low = v.toFloat();
else if (k == "HIGH") thr_high = v.toFloat();
else if (k == "RAW") thr_raw = v.toFloat();
else if (k == "T1CM") t1_cm = v.toFloat();
else if (k == "T2CM") t2_cm = v.toFloat();
else if (k == "T1E") t1_e = v.toFloat();
else if (k == "T1F") t1_f = v.toFloat();
else if (k == "T2E") t2_e = v.toFloat();
else if (k == "T2F") t2_f = v.toFloat();
else if (k == "STA") sta_on = v.toInt();
else if (k == "STAIP") sta_ip = v;
else if (k == "MQEN") mq_en_remote = v.toInt();
else if (k == "MQCONN") mq_conn = v.toInt();
else if (k == "MQHOST") mq_host = v;
else if (k == "MQPORT") mq_port = v.toInt();
else if (k == "RSSI") rssi_sta = v.toInt();
else if (k == "MQF") mqf = (uint32_t)v.toInt();
else if (k == "MQERR") mqerr = v.toInt();
else if (k == "MQAGE") mqage = (uint32_t)v.toInt();
else if (k == "NTP") ntp_ok = v.toInt();
else if (k == "NTPAGE") ntpage = (uint32_t)v.toInt();
else if (k == "HEAP") heap_free = (uint32_t)v.toInt();
else if (k == "RST") rst = v;
i = sc + 1;
}
}
void udpPollRx() {
int p = udp.parsePacket();
if (p <= 0) return;
String pkt; pkt.reserve(1200);
while (p--) pkt += (char)udp.read();
parseKeyValPacket(pkt);
}
// ---------------- UI strings ----------------
String homeLine2() {
// Rotates information on line 2 while keeping line 3 for quick menu.
if (homeRotateIdx == 0) {
String m = modeStr; if (m.length()>5) m = m.substring(0,5);
String w = whyStr; if (w.length()>9) w = w.substring(0,9);
return "Modo:" + m + " Why:" + w;
}
if (homeRotateIdx == 1) {
return "Cyc:" + fmtMMSS(rem_sec) + " Ses:" + fmtMMSS(sessrem_sec);
}
if (homeRotateIdx == 2) {
int L = isnan(thr_low) ? 30 : (int)roundf(thr_low);
int H = isnan(thr_high) ? 85 : (int)roundf(thr_high);
int R = isnan(thr_raw) ? 20 : (int)roundf(thr_raw);
return "L/H/R:" + String(L) + "/" + String(H) + "/" + String(R);
}
if (homeRotateIdx == 3) {
String s = String("STA:") + (sta_on ? "ON " : "OFF");
if (sta_on) s += " " + sta_ip;
return s;
}
if (homeRotateIdx == 4) {
String s = "MQTT:";
s += (mq_en_remote ? "EN " : "OFF");
s += (mq_conn ? " CONN" : " DISC");
return s;
}
return "FC C/S/R:" + String(fc_comm) + "/" + String(fc_sens) + "/" + String(fc_raw);
}
// ---------------- UI render ----------------
void renderHome() {
lcdPrintLine(0, "T1 " + fmtPct(t1_pct) + "% T2 " + fmtPct(t2_pct) + "%");
lcdPrintLine(1, "T3 " + fmtPct(t3_pct_remote) + "% (crudo)");
if (!esp1Online()) {
lcdPrintLine(2, "ESP1:OFFLINE ");
lcdPrintLine(3, "Age:" + String(esp1AgeMs()/1000) + "s ");
return;
}
// Line 2 rotates (modo/diag). Line 3 is the short quick menu.
lcdPrintLine(2, homeLine2());
if (quickMsgUntilMs && millis() < quickMsgUntilMs && quickMsg.length()) {
lcdPrintLine(3, quickMsg);
} else {
lcdPrintLine(3, "1)60 2)120 3)180 B=M");
}
}
void renderMenuMain() {
lcdPrintLine(0, "MENU");
lcdPrintLine(1, "1) Duracion (min)");
lcdPrintLine(2, "2) Umbrales (%)");
lcdPrintLine(3, "3) Cal 4)MQTT B=Salir");
}
void renderMenuDur() {
lcdPrintLine(0, "TIEMPO PURIFICACION");
lcdPrintLine(1, "0)AUTO 1)60m");
lcdPrintLine(2, "2)120 3)180");
lcdPrintLine(3, "4)240 #=OK B=Back");
}
void renderConfirmDur() {
lcdPrintLine(0, "Enviar duracion?");
lcdPrintLine(1, (selectedDurationMin==0) ? "Seleccion: AUTO" : ("Seleccion: " + String(selectedDurationMin) + " min"));
lcdPrintLine(2, "#=Confirmar *=No");
lcdPrintLine(3, "B=Menu");
}
void renderMenuThr() {
lcdPrintLine(0, "UMBRALES ACTUALES");
lcdPrintLine(1, "1)LOW :" + fmtVal(thr_low) + "%");
lcdPrintLine(2, "2)HIGH :" + fmtVal(thr_high) + "%");
lcdPrintLine(3, "3)RAWmin:" + fmtVal(thr_raw) + "% #=Enviar");
}
void renderEdit(const String &title, float currentVal) {
lcdPrintLine(0, title);
lcdPrintLine(1, "Actual: " + fmtVal(currentVal) + "%");
lcdPrintLine(2, "Nuevo : " + (editBuf.length()? editBuf : String("_")));
lcdPrintLine(3, "#=OK *=Del B=Back");
}
void renderConfirmThr() {
float low = isnan(thr_low) ? 30.0f : thr_low;
float high = isnan(thr_high) ? 85.0f : thr_high;
float raw = isnan(thr_raw) ? 20.0f : thr_raw;
lcdPrintLine(0, "Enviar UMBRALES?");
lcdPrintLine(1, "LOW " + fmtVal(low) + " HIGH " + fmtVal(high));
lcdPrintLine(2, "RAW " + fmtVal(raw));
lcdPrintLine(3, "#=Enviar *=No B=Menu");
}
void renderMenuCal() {
lcdPrintLine(0, "CALIBRACION DIST");
lcdPrintLine(1, "1) Tinaco 1 (ESP1)");
lcdPrintLine(2, "2) Tinaco 2 (ESP1)");
lcdPrintLine(3, "3) Tinaco 3 (ESP2)");
}
void renderCalView() {
if (calTank == 3) {
float cm= NAN, pct=NAN;
readT3(cm, pct);
lcdPrintLine(0, "T3 CM:" + fmtVal(cm));
lcdPrintLine(1, "FULL:" + fmtVal(T3_DIST_FULL_CM) + " EMP:" + fmtVal(T3_DIST_EMPTY_CM));
lcdPrintLine(2, "#=FULL *=EMP A=Ref");
lcdPrintLine(3, "B=Back");
return;
}
float cm = (calTank==1) ? t1_cm : t2_cm;
float e = (calTank==1) ? t1_e : t2_e;
float f = (calTank==1) ? t1_f : t2_f;
lcdPrintLine(0, String("T") + String(calTank) + " CM:" + fmtVal(cm));
lcdPrintLine(1, "FULL:" + fmtVal(f) + " EMP:" + fmtVal(e));
lcdPrintLine(2, "#=FULL *=EMP A=Ref");
lcdPrintLine(3, "B=Back");
}
void renderCalConfirm() {
lcdPrintLine(0, "CONFIRMAR CAPTURA");
lcdPrintLine(1, "T" + String(calTank) + " " + calPendingAct + " ?");
lcdPrintLine(2, "#=SI *=NO");
lcdPrintLine(3, "B=Back");
}
// MQTT menu
void renderMenuMqtt() {
lcdPrintLine(0, "MQTT CONFIG (ESP2)");
lcdPrintLine(1, String("1)EN: ") + (mqtt_en ? "SI " : "NO "));
lcdPrintLine(2, "2)IP:" + mqtt_ip);
lcdPrintLine(3, "3)PORT:" + String(mqtt_port) + " #=SEND B=Back");
}
void renderEditMqttIp() {
lcdPrintLine(0, "EDIT MQTT IP");
lcdPrintLine(1, "Actual:" + mqtt_ip);
lcdPrintLine(2, "Nuevo :" + (editBuf.length()? editBuf : String("_")));
lcdPrintLine(3, "0-9 . #=OK *=Del B=Back");
}
void renderEditMqttPort() {
lcdPrintLine(0, "EDIT MQTT PORT");
lcdPrintLine(1, "Actual:" + String(mqtt_port));
lcdPrintLine(2, "Nuevo :" + (editBuf.length()? editBuf : String("_")));
lcdPrintLine(3, "0-9 #=OK *=Del B=Back");
}
void renderConfirmMqttSend() {
lcdPrintLine(0, "Enviar MQTT a ESP1?");
lcdPrintLine(1, String("EN:") + (mqtt_en?"1":"0") + " IP:" + mqtt_ip);
lcdPrintLine(2, "PORT:" + String(mqtt_port));
lcdPrintLine(3, "#=SI *=NO B=Back");
}
void renderMaint() {
lcdPrintLine(0, "MODO MANTENIMIENTO");
lcdPrintLine(1, "Bomba fija: CH1");
lcdPrintLine(2, "1)4min 2)7min");
if (quickMsgUntilMs > millis()) lcdPrintLine(3, quickMsg);
else lcdPrintLine(3, "D=OFF #STOP B=Salir");
}
void renderInfo() {
IPAddress ip = WiFi.localIP();
int rssi = WiFi.RSSI();
lcdPrintLine(0, String("INFO ") + FW_VER);
lcdPrintLine(1, "IP:" + ip.toString());
lcdPrintLine(2, "RSSI:" + String(rssi) + " ESP1:" + (esp1Online() ? "ON " : "OFF"));
static int infoIdx = 0;
static uint32_t infoMs = 0;
if (millis() - infoMs > 2500) { infoMs = millis(); infoIdx = (infoIdx + 1) % 3; }
if (infoIdx == 0) {
lcdPrintLine(3, "FC C/S/R:" + String(fc_comm) + "/" + String(fc_sens) + "/" + String(fc_raw));
} else if (infoIdx == 1) {
lcdPrintLine(3, "UP:" + String(esp1_up_s/60) + "m Age:" + String(esp1AgeMs()/1000) + "s");
} else {
String s = "MQ " + String(mq_en_remote ? "EN" : "OFF") + " " + String(mq_conn ? "OK" : "NO");
if (mq_host.length()) {
s += " " + mq_host;
if (mq_port > 0) s += ":" + String(mq_port);
}
lcdPrintLine(3, s);
}
}
void renderDiag() {
if (millis() - diagMs > 2500) { diagMs = millis(); diagIdx = (diagIdx + 1) % 4; }
lcdPrintLine(0, String("DIAG K:") + lastKeyStr() + " C>N B<");
if (diagIdx == 0) {
String l1 = String("STA:") + (sta_on ? "ON " : "OFF") + " RSSI:" + String(rssi_sta);
lcdPrintLine(1, l1);
lcdPrintLine(2, "IP:" + sta_ip);
lcdPrintLine(3, "ESP1Age:" + String(esp1AgeMs()/1000) + "s");
} else if (diagIdx == 1) {
String l1 = String("MQTT:") + (mq_en_remote ? "EN " : "OFF") + (mq_conn ? " CONN" : " DISC");
lcdPrintLine(1, l1);
String l2 = "H:" + (mq_host.length()? mq_host : String("-"));
if (mq_port > 0) l2 += ":" + String(mq_port);
lcdPrintLine(2, l2);
lcdPrintLine(3, "WHY:" + whyStr);
} else if (diagIdx == 2) {
lcdPrintLine(1, "MQF:" + String(mqf) + " ERR:" + String(mqerr));
lcdPrintLine(2, "MQAGE:" + fmtAge(mqage));
lcdPrintLine(3, "MODE:" + modeStr + " " + proc_state);
} else {
lcdPrintLine(1, String("NTP:") + (ntp_ok ? "OK" : "NO") + " AGE:" + fmtAge(ntpage));
lcdPrintLine(2, "HEAP:" + String(heap_free));
String rr = rst.length()? rst : String("-");
if (rr.length() > 20) rr = rr.substring(0,20);
lcdPrintLine(3, "RST:" + rr);
}
}
void renderConfirmRstEsp1() {
lcdPrintLine(0, "REINICIAR ESP1?");
lcdPrintLine(1, "#=SI *=NO");
lcdPrintLine(2, "B=Cancel");
lcdPrintLine(3, " ");
}
void renderScreen() {
lcd.clear();
switch(screen){
case HOME: renderHome(); break;
case MENU_MAIN: renderMenuMain(); break;
case MENU_DUR: renderMenuDur(); break;
case CONFIRM_DUR: renderConfirmDur(); break;
case MENU_THR: renderMenuThr(); break;
case EDIT_LOW: renderEdit("EDIT LOW (%)", thr_low); break;
case EDIT_HIGH: renderEdit("EDIT HIGH (%)", thr_high); break;
case EDIT_RAW: renderEdit("EDIT RAW_MIN (%)", thr_raw); break;
case CONFIRM_THR: renderConfirmThr(); break;
case MENU_CAL: renderMenuCal(); break;
case CAL_VIEW: renderCalView(); break;
case CAL_CONFIRM: renderCalConfirm(); break;
case MENU_MQTT: renderMenuMqtt(); break;
case MAINT: renderMaint(); break;
case EDIT_MQTT_IP: renderEditMqttIp(); break;
case EDIT_MQTT_PORT: renderEditMqttPort(); break;
case CONFIRM_MQTT_SEND: renderConfirmMqttSend(); break;
case INFO: renderInfo(); break;
case DIAG: renderDiag(); break;
case CONFIRM_RST_ESP1: renderConfirmRstEsp1(); break;
}
}
// ---------------- Key handler ----------------
void handleKey(char k) {
// store last key for diagnostics
lastKey = k;
lastKeyMs = millis();
if (screen == HOME) {
// Short menu (rápido): 1/2/3 envían duración sin confirmación
if (k == '1') { quickSetDuration(60); return; }
if (k == '2') { quickSetDuration(120); return; }
if (k == '3') { quickSetDuration(180); return; }
if (k == 'A') { screen = MENU_MAIN; renderScreen(); return; }
if (k == 'B') { screen = MAINT; renderScreen(); return; }
if (k == 'D') { screen = INFO; renderScreen(); return; }
if (k == 'C') { screen = DIAG; renderScreen(); return; }
return;
}
if (screen == MAINT) {
// Menú corto de mantenimiento: bomba fija CH1.
// 1 = 4 min, 2 = 7 min. No hay confirmación.
if (k == '1') { sendMaintStartFixed(4); quickMsg = "MAINT CH1 4m"; quickMsgUntilMs = millis() + 1200; renderMaint(); return; }
if (k == '2') { sendMaintStartFixed(7); quickMsg = "MAINT CH1 7m"; quickMsgUntilMs = millis() + 1200; renderMaint(); return; }
if (k == '#') { sendMaintStop(); quickMsg = "MAINT STOP"; quickMsgUntilMs = millis() + 1200; renderMaint(); return; }
// Tecla para SALIR de mantenimiento (deshabilita maint en ESP1 y regresa a HOME)
if (k == 'D') { sendMaintOff(); quickMsg = "MAINT OFF"; quickMsgUntilMs = millis() + 1200; screen = HOME; renderScreen(); return; }
if (k == 'B') { screen = HOME; renderScreen(); return; }
return;
}
if (screen == INFO) {
if (k == 'B') { screen = HOME; renderScreen(); return; }
if (k == 'C') { screen = DIAG; renderScreen(); return; }
if (k == '#') { sendRstFail(); lcdPrintLine(3,"Reset FAIL enviado "); delay(600); renderInfo(); return; }
return;
}
if (screen == DIAG) {
if (k == 'B') { screen = INFO; renderScreen(); return; }
if (k == 'C') { diagIdx = (diagIdx + 1) % 4; renderDiag(); return; }
// Maintenance actions
if (k == '#') { sendRstMq(); lcdPrintLine(3,"RSTMQ enviado "); delay(500); renderDiag(); return; }
if (k == '*') { sendStaRetry(); lcdPrintLine(3,"STA retry enviado "); delay(500); renderDiag(); return; }
if (k == 'A') { sendForceNtp(); lcdPrintLine(3,"NTP cmd enviado "); delay(500); renderDiag(); return; }
if (k == 'D') { screen = CONFIRM_RST_ESP1; renderScreen(); return; }
return;
}
if (screen == CONFIRM_RST_ESP1) {
if (k == 'B' || k == '*') { screen = DIAG; renderScreen(); return; }
if (k == '#') { sendRestartEsp1(); lcdPrintLine(3,"RST enviado "); delay(600); screen = DIAG; renderScreen(); return; }
return;
}
if (screen == MENU_MAIN) {
if (k == 'B') { screen = HOME; renderScreen(); return; }
if (k == '1') { screen = MENU_DUR; renderScreen(); return; }
if (k == '2') { screen = MENU_THR; renderScreen(); return; }
if (k == '3') { screen = MENU_CAL; renderScreen(); return; }
if (k == '4') { screen = MENU_MQTT; renderScreen(); return; }
return;
}
// DUR
if (screen == MENU_DUR) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '0') selectedDurationMin = 0;
else if (k == '1') selectedDurationMin = 60;
else if (k == '2') selectedDurationMin = 120;
else if (k == '3') selectedDurationMin = 180;
else if (k == '4') selectedDurationMin = 240;
else if (k == '#') { screen = CONFIRM_DUR; renderScreen(); return; }
return;
}
if (screen == CONFIRM_DUR) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '*') { screen = MENU_DUR; renderScreen(); return; }
if (k == '#') { sendDurCmd(selectedDurationMin); delay(350); screen = HOME; renderScreen(); return; }
return;
}
// THR
if (screen == MENU_THR) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '1') { editBuf=""; screen = EDIT_LOW; renderScreen(); return; }
if (k == '2') { editBuf=""; screen = EDIT_HIGH; renderScreen(); return; }
if (k == '3') { editBuf=""; screen = EDIT_RAW; renderScreen(); return; }
if (k == '#') { screen = CONFIRM_THR; renderScreen(); return; }
return;
}
if (screen == EDIT_LOW || screen == EDIT_HIGH || screen == EDIT_RAW) {
if (k == 'B') { screen = MENU_THR; renderScreen(); return; }
if (k == '*') editBufBackspace();
else if (k == '#') {
float v; if (editBufToFloat(v)) {
if (screen == EDIT_LOW) thr_low = v;
else if (screen == EDIT_HIGH) thr_high = v;
else thr_raw = v;
}
screen = MENU_THR; renderScreen(); return;
} else if (k >= '0' && k <= '9') {
if (editBuf.length() < 5) editBuf += k;
}
if (screen == EDIT_LOW) renderEdit("EDIT LOW (%)", thr_low);
else if (screen == EDIT_HIGH) renderEdit("EDIT HIGH (%)", thr_high);
else renderEdit("EDIT RAW_MIN (%)", thr_raw);
return;
}
if (screen == CONFIRM_THR) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '*') { screen = MENU_THR; renderScreen(); return; }
if (k == '#') {
float low = isnan(thr_low) ? 30.0f : thr_low;
float high = isnan(thr_high) ? 85.0f : thr_high;
float raw = isnan(thr_raw) ? 20.0f : thr_raw;
sendThrCmd(low, high, raw);
delay(350);
screen = HOME; renderScreen(); return;
}
return;
}
// CAL
if (screen == MENU_CAL) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '1') { calTank = 1; screen = CAL_VIEW; renderScreen(); sendCalCmdEsp1(1,"VIEW"); return; }
if (k == '2') { calTank = 2; screen = CAL_VIEW; renderScreen(); sendCalCmdEsp1(2,"VIEW"); return; }
if (k == '3') { calTank = 3; screen = CAL_VIEW; renderScreen(); return; }
return;
}
if (screen == CAL_VIEW) {
if (k == 'B') { screen = MENU_CAL; renderScreen(); return; }
if (k == 'A') { if (calTank==3) renderCalView(); else sendCalCmdEsp1(calTank,"VIEW"); return; }
if (k == '#') { calPendingAct="FULL"; screen = CAL_CONFIRM; renderScreen(); return; }
if (k == '*') { calPendingAct="EMPTY"; screen = CAL_CONFIRM; renderScreen(); return; }
return;
}
if (screen == CAL_CONFIRM) {
if (k == 'B') { screen = CAL_VIEW; renderScreen(); return; }
if (k == '*') { screen = CAL_VIEW; renderScreen(); return; }
if (k == '#') {
if (calTank == 3) {
float cm;
if (readT3cm(cm)) {
if (calPendingAct == "FULL") T3_DIST_FULL_CM = cm;
else T3_DIST_EMPTY_CM = cm;
saveLocalConfig();
}
} else {
sendCalCmdEsp1(calTank, calPendingAct);
}
screen = CAL_VIEW; renderScreen(); return;
}
return;
}
// MQTT MENU
if (screen == MENU_MQTT) {
if (k == 'B') { screen = MENU_MAIN; renderScreen(); return; }
if (k == '1') { mqtt_en = !mqtt_en; saveLocalConfig(); renderMenuMqtt(); return; }
if (k == '2') { editBuf=""; screen = EDIT_MQTT_IP; renderScreen(); return; }
if (k == '3') { editBuf=""; screen = EDIT_MQTT_PORT; renderScreen(); return; }
if (k == '#') { screen = CONFIRM_MQTT_SEND; renderScreen(); return; }
return;
}
if (screen == EDIT_MQTT_IP) {
if (k == 'B') { screen = MENU_MQTT; renderScreen(); return; }
if (k == '*') editBufBackspace();
else if (k == '#') {
if (editBuf.length() >= 7 && editBuf.indexOf('.') > 0) {
mqtt_ip = editBuf;
saveLocalConfig();
}
screen = MENU_MQTT; renderScreen(); return;
} else if ((k >= '0' && k <= '9') || k == '.') {
if (editBuf.length() < 15) editBuf += k;
}
renderEditMqttIp();
return;
}
if (screen == EDIT_MQTT_PORT) {
if (k == 'B') { screen = MENU_MQTT; renderScreen(); return; }
if (k == '*') editBufBackspace();
else if (k == '#') {
int p = editBuf.toInt();
if (p > 0 && p < 65536) {
mqtt_port = p;
saveLocalConfig();
}
screen = MENU_MQTT; renderScreen(); return;
} else if (k >= '0' && k <= '9') {
if (editBuf.length() < 5) editBuf += k;
}
renderEditMqttPort();
return;
}
if (screen == CONFIRM_MQTT_SEND) {
if (k == 'B') { screen = MENU_MQTT; renderScreen(); return; }
if (k == '*') { screen = MENU_MQTT; renderScreen(); return; }
if (k == '#') {
sendMqttCfgToEsp1();
lcdPrintLine(3, "Enviado a ESP1 ");
delay(700);
screen = MENU_MQTT;
renderScreen();
return;
}
return;
}
}
// ---------------- setup/loop ----------------
void setup() {
Serial.begin(115200);
pinMode(PIN_T3_TRIG, OUTPUT);
digitalWrite(PIN_T3_TRIG, LOW);
pinMode(PIN_T3_ECHO, INPUT);
loadLocalConfig();
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
// Keypad tuning for real hardware (debounce/hold)
keypad.setDebounceTime(30);
keypad.setHoldTime(500);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 12000) delay(250);
udp.begin(UDP_PORT_RX_FROM_ESP1);
homeRotateMs = millis();
homeRotateIdx = 0;
diagMs = millis();
diagIdx = 0;
screen = HOME;
renderScreen();
}
void loop() {
// keys
char k = keypad.getKey();
if (k) handleKey(k);
// rx from esp1
udpPollRx();
// home rotator
if (screen == HOME && esp1Online()) {
uint32_t now = millis();
if (now - homeRotateMs >= HOME_ROTATE_PERIOD_MS) {
homeRotateMs = now;
homeRotateIdx = (homeRotateIdx + 1) % 5;
lcdPrintLine(2, homeLine2());
}
}
// periodic UI refresh
static uint32_t lastUi = 0;
if (millis() - lastUi > 900) {
lastUi = millis();
if (screen == HOME) renderHome();
else if (screen == CAL_VIEW) renderCalView();
else if (screen == INFO) renderInfo();
else if (screen == MENU_MQTT) renderMenuMqtt();
else if (screen == MAINT) renderMaint();
else if (screen == DIAG) renderDiag();
}
// Send T3 to ESP1
static uint32_t lastMeasMs = 0;
static uint32_t seq = 0;
if (millis() - lastMeasMs > 1500) {
lastMeasMs = millis();
float cm, pct;
bool ok = readT3(cm, pct);
String msg = "T3=" + String(ok ? pct : -1.0f, 1) +
";OK=" + String(ok ? 1 : 0) +
";SEQ=" + String(seq++) + ";";
udpSendToEsp1(msg);
}
// WiFi reconnect
static uint32_t lastWifiChk = 0;
if (millis() - lastWifiChk > 3000) {
lastWifiChk = millis();
if (WiFi.status() != WL_CONNECTED) {
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
}
}