#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#define LCD_ADDR 0x27
#define LCD_COLS 20
#define LCD_ROWS 4
LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS);
// Pines (tu diagram.json)
const int PIN_ENJ_BTN = 23;
const int PIN_5L = 5;
const int PIN_10L = 26;
const int PIN_20L = 25;
const int PIN_STOP = 19;
const int PIN_COIN = 18;
const int PIN_RELE_ENJ = 32;
const int PIN_RELE_FILL = 33;
// Config
const unsigned long DEBOUNCE_MS = 35;
const int MONEDA_PESOS = 5;
const unsigned long ENJ_TIME_MS = 4000;
const unsigned long MSG_MS = 1200;
const unsigned long SALDO_SMALL_MS = 650;
const unsigned long FILL_STEP_MS = 180;
const int PRECIO_5L = 5;
const int PRECIO_10L = 10;
const int PRECIO_20L = 20;
// Estado
enum State { ST_MENU, ST_BIG_SALDO, ST_RINSE, ST_MSG, ST_SALDO_SMALL, ST_FILLING, ST_DONE };
State st = ST_MENU;
unsigned long tState = 0;
unsigned long tFill = 0;
int saldoPesos = 0;
int litrosObjetivo = 0;
int precioObjetivo = 0;
int fillPercent = 0;
String msg0="", msg1="", msg2="", msg3="";
bool cobroHecho = false;
// Debounce
struct Btn { int pin; int lastRaw; int stable; unsigned long tDeb; };
Btn bEnj = { PIN_ENJ_BTN, HIGH, HIGH, 0 };
Btn b5L = { PIN_5L, HIGH, HIGH, 0 };
Btn b10L = { PIN_10L, HIGH, HIGH, 0 };
Btn b20L = { PIN_20L, HIGH, HIGH, 0 };
Btn bStop = { PIN_STOP, HIGH, HIGH, 0 };
Btn bCoin = { PIN_COIN, HIGH, HIGH, 0 };
bool pressedEdge(Btn &b) {
int raw = digitalRead(b.pin);
if (raw != b.lastRaw) { b.lastRaw = raw; b.tDeb = millis(); }
if (millis() - b.tDeb > DEBOUNCE_MS) {
if (b.stable != raw) { b.stable = raw; if (b.stable == LOW) return true; }
}
return false;
}
void printRow(uint8_t row, const String &s) {
lcd.setCursor(0, row);
String out = s;
if (out.length() > 20) out = out.substring(0, 20);
while (out.length() < 20) out += ' ';
lcd.print(out);
}
// ================== BIG DIGITS ==================
// 0=vLeft, 1=vRight, 2=Top, 3=dashShortUp(4px), 4=Bottom, 5=vCenter
byte vLeftThick[8] = { B11000,B11000,B11000,B11000,B11000,B11000,B11000,B11000 };
byte vRightThick[8] = { B00011,B00011,B00011,B00011,B00011,B00011,B00011,B00011 };
byte vCenterThick[8] = { B00110,B00110,B00110,B00110,B00110,B00110,B00110,B00110 };
byte hTopThick[8] = { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 };
byte hBotThick[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 };
// ✅ guion corto ARRIBA, corrido a la derecha, 4 pixeles (01111)
byte dashShortUp[8] = { B01111,B01111,B00000,B00000,B00000,B00000,B00000,B00000 };
void loadBigChars() {
lcd.createChar(0, vLeftThick);
lcd.createChar(1, vRightThick);
lcd.createChar(2, hTopThick);
lcd.createChar(3, dashShortUp);
lcd.createChar(4, hBotThick);
lcd.createChar(5, vCenterThick);
}
static const uint8_t SP = 255;
static const uint8_t L = 0;
static const uint8_t R = 1;
static const uint8_t T = 2;
static const uint8_t DS = 3;
static const uint8_t B = 4;
static const uint8_t C = 5;
// filas: 0=top, 1=vertical arriba, 2=vertical abajo, 3=bottom
const uint8_t DIG[10][4][3] = {
{ {T,T,T},
{L,SP,R},
{L,SP,R},
{B,B,B} }, //0
{ {R,L,SP},
{R,L,SP},
{R,L,SP},
{R,L,SP} }, //1
{ {T,T,R},
{B,B,R},
{L,T,SP},
{L,B,B} }, //2
{ {T,T,R},
{SP,B,R},
{SP,T,R},
{B,B,B} }, //3
{ {L,SP,R},
{L,B,R},
{SP,T,R},
{SP,SP,R} }, //4
{ {T,T,T},
{L,DS,SP},
{SP,SP,R},
{B,B,B} }, //5 ✅ DS arriba 4px
{ {T,T,T},
{L,SP,SP},
{L,SP,R},
{B,B,B} }, //6
{ {T,T,T},
{SP,SP,R},
{SP,SP,R},
{SP,SP,R} }, //7
{ {T,T,T},
{L,SP,R},
{L,SP,R},
{B,B,B} }, //8
{ {T,T,T},
{L,SP,R},
{SP,SP,R},
{B,B,B} } //9
};
void drawBigDigit3x4(int digit, int col0) {
digit = constrain(digit, 0, 9);
for (int r=0;r<4;r++) {
lcd.setCursor(col0, r);
for (int c=0;c<3;c++) {
uint8_t code = DIG[digit][r][c];
if (code == SP) lcd.print(' ');
else lcd.write(code);
}
}
}
void renderBigSaldo() {
lcd.clear();
loadBigChars();
lcd.setCursor(0, 1); lcd.print('$');
lcd.setCursor(0, 2); lcd.print('$');
int val = saldoPesos;
if (val < 0) val = 0;
if (val > 99) val = 99;
int tens = val / 10;
int ones = val % 10;
if (tens == 0) {
drawBigDigit3x4(ones, 6);
} else {
drawBigDigit3x4(tens, 2);
drawBigDigit3x4(ones, 6);
}
lcd.setCursor(12, 0); lcd.print("SALDO");
lcd.setCursor(12, 3); lcd.print("5/10/20L");
}
// ================== Barra ==================
byte bar0[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000 };
byte bar1[8] = { B10000,B10000,B10000,B10000,B10000,B10000,B10000,B10000 };
byte bar2[8] = { B11000,B11000,B11000,B11000,B11000,B11000,B11000,B11000 };
byte bar3[8] = { B11100,B11100,B11100,B11100,B11100,B11100,B11100,B11100 };
byte bar4[8] = { B11110,B11110,B11110,B11110,B11110,B11110,B11110,B11110 };
byte bar5[8] = { B11111,B11111,B11111,B11111,B11111,B11111,B11111,B11111 };
void loadBarChars() {
lcd.createChar(0, bar0);
lcd.createChar(1, bar1);
lcd.createChar(2, bar2);
lcd.createChar(3, bar3);
lcd.createChar(4, bar4);
lcd.createChar(5, bar5);
}
void drawBar20(int percent) {
percent = constrain(percent, 0, 100);
int fullCells = percent / 5;
int rem = percent % 5;
lcd.setCursor(0, 1);
for (int i=0;i<fullCells;i++) lcd.write((uint8_t)5);
if (fullCells < 20) { lcd.write((uint8_t)rem); fullCells++; }
for (int i=fullCells;i<20;i++) lcd.write((uint8_t)0);
}
void renderMenu() {
lcd.clear();
printRow(0, "INSERTE MONEDA");
printRow(1, "ENJ / 5L /10L/20L");
printRow(2, "Saldo:$" + String(saldoPesos));
printRow(3, "MONEDA=$5 STOP");
}
void renderSaldoSmall() {
lcd.clear();
printRow(0, "LISTO " + String(litrosObjetivo) + "L");
printRow(1, "COBRADO: $" + String(precioObjetivo));
printRow(2, "Iniciando llenado");
printRow(3, "STOP=PARO");
}
void renderFillingHeader() {
lcd.clear();
loadBarChars();
printRow(0, "PORCENTAJE");
printRow(2, "0.0 litros surtidos");
printRow(3, "SURTIENDO... STOP");
}
void renderLitros(float litros) {
char buf[21];
snprintf(buf, sizeof(buf), "%.1f litros surtidos", litros);
printRow(2, String(buf));
}
void setState(State ns) {
st = ns;
tState = millis();
switch (st) {
case ST_MENU:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
renderMenu();
break;
case ST_BIG_SALDO:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
renderBigSaldo();
break;
case ST_RINSE:
digitalWrite(PIN_RELE_FILL, LOW);
digitalWrite(PIN_RELE_ENJ, HIGH);
lcd.clear();
printRow(0, "ENJUAGUANDO...");
printRow(1, "RELÉ ON 4 SEG");
printRow(2, "Saldo:$" + String(saldoPesos));
printRow(3, "STOP=PARO");
break;
case ST_MSG:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
lcd.clear();
printRow(0, msg0);
printRow(1, msg1);
printRow(2, msg2);
printRow(3, msg3);
break;
case ST_SALDO_SMALL:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
renderSaldoSmall();
break;
case ST_FILLING:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, HIGH);
fillPercent = 0;
tFill = millis();
renderFillingHeader();
drawBar20(fillPercent);
renderLitros(0.0f);
break;
case ST_DONE:
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
lcd.clear();
printRow(1, "FIN");
printRow(2, "SALDO RESTANTE:");
printRow(3, "$" + String(saldoPesos));
break;
}
}
void showMsg(const String& a, const String& b, const String& c="", const String& d="") {
msg0=a; msg1=b; msg2=c; msg3=d;
setState(ST_MSG);
}
void startEnjuague() {
if (saldoPesos <= 0) { showMsg("SIN SALDO", "INSERTE MONEDA"); return; }
setState(ST_RINSE);
}
// Cobro inmediato
void startFillIfPossible(int litros, int precio) {
litrosObjetivo = litros;
precioObjetivo = precio;
if (saldoPesos < precioObjetivo) {
showMsg("SALDO INSUFICIENTE", "Falta: $" + String(precioObjetivo - saldoPesos));
return;
}
if (!cobroHecho) {
saldoPesos -= precioObjetivo;
if (saldoPesos < 0) saldoPesos = 0;
cobroHecho = true;
}
setState(ST_SALDO_SMALL);
}
void setup() {
pinMode(PIN_ENJ_BTN, INPUT_PULLUP);
pinMode(PIN_5L, INPUT_PULLUP);
pinMode(PIN_10L, INPUT_PULLUP);
pinMode(PIN_20L, INPUT_PULLUP);
pinMode(PIN_STOP, INPUT_PULLUP);
pinMode(PIN_COIN, INPUT_PULLUP);
pinMode(PIN_RELE_ENJ, OUTPUT);
pinMode(PIN_RELE_FILL, OUTPUT);
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
setState(ST_MENU);
}
void loop() {
bool coin = pressedEdge(bCoin);
bool stop = pressedEdge(bStop);
bool s5 = pressedEdge(b5L);
bool s10 = pressedEdge(b10L);
bool s20 = pressedEdge(b20L);
bool enj = pressedEdge(bEnj);
if (stop) {
digitalWrite(PIN_RELE_ENJ, LOW);
digitalWrite(PIN_RELE_FILL, LOW);
if (st == ST_FILLING || st == ST_SALDO_SMALL) cobroHecho = false;
if (saldoPesos > 0) setState(ST_BIG_SALDO);
else setState(ST_MENU);
return;
}
if (coin) { saldoPesos += MONEDA_PESOS; setState(ST_BIG_SALDO); }
if (enj) startEnjuague();
if (s5) { cobroHecho = false; startFillIfPossible(5, PRECIO_5L); }
if (s10) { cobroHecho = false; startFillIfPossible(10, PRECIO_10L); }
if (s20) { cobroHecho = false; startFillIfPossible(20, PRECIO_20L); }
switch (st) {
case ST_MENU:
case ST_BIG_SALDO:
break;
case ST_MSG:
if (millis() - tState >= MSG_MS) {
if (saldoPesos > 0) setState(ST_BIG_SALDO);
else setState(ST_MENU);
}
break;
case ST_RINSE:
if (millis() - tState >= ENJ_TIME_MS) {
digitalWrite(PIN_RELE_ENJ, LOW);
if (saldoPesos > 0) setState(ST_BIG_SALDO);
else setState(ST_MENU);
}
break;
case ST_SALDO_SMALL:
if (millis() - tState >= SALDO_SMALL_MS) setState(ST_FILLING);
break;
case ST_FILLING:
if (millis() - tFill >= FILL_STEP_MS) {
tFill = millis();
fillPercent = min(100, fillPercent + 1);
drawBar20(fillPercent);
float litros = (fillPercent / 100.0f) * (float)litrosObjetivo;
renderLitros(litros);
if (fillPercent >= 100) { cobroHecho = false; setState(ST_DONE); }
}
break;
case ST_DONE:
if (millis() - tState >= 1400) {
if (saldoPesos > 0) setState(ST_BIG_SALDO);
else setState(ST_MENU);
}
break;
}
}