#include <Arduino.h>
#include <Wire.h>
#include <EEPROM.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <avr/wdt.h>
#include <math.h>
// =====================================================
// TIPI
// =====================================================
// Aggiunte: DRY2 e END
enum class Mode : uint8_t { STANDBY = 0, APPASSIMENTO, DRY, DRY2, END };
enum class StandbyMode : uint8_t {
STBY1_MATS_ONLY = 0,
STBY2_MATS_PLUS_L2,
STBY3_MATS_PLUS_L1_L2
};
struct Hyst { float onOffset; float offOffset; };
struct Settings {
float drySetTempC;
uint16_t dryMinutes;
float standbyTempC;
StandbyMode stbyMode;
float appTempC;
uint16_t appMinutes;
Hyst lamp1;
Hyst lamp2;
Hyst mats;
// AIR isteresi su set corrente (delta rispetto al set della modalità)
float airOnDelta; // ON se temp >= set + airOnDelta
float airOffDelta; // OFF se temp <= set + airOffDelta
// Toggle manuali (solo APP/DRY/DRY2)
bool appManL1;
bool appManL2;
bool appManMAT;
bool dryManL1;
bool dryManL2;
bool dryManMAT;
bool appManAIR;
bool dryManAIR;
uint16_t magic;
uint16_t crc;
};
static const uint16_t EEPROM_MAGIC = 0xCB06;
// =====================================================
// PINOUT (MEGA)
// =====================================================
static const uint8_t PIN_DS18B20 = 2;
static const uint8_t PIN_BTN_MINUS = 3;
static const uint8_t PIN_BTN_PLUS = 4;
static const uint8_t PIN_BTN_START = 5; // short: start/stop, long 3s: save
static const uint8_t PIN_BTN_MENU_DN = 6; // menu GIU
static const uint8_t PIN_BTN_RESET = 7; // long reset MCU
static const uint8_t PIN_BTN_MENU_UP = 10; // menu SU
// 4 bottoni toggle (solo in APP/DRY/DRY2)
static const uint8_t PIN_BTN_TOG_L1 = 22;
static const uint8_t PIN_BTN_TOG_L2 = 23;
static const uint8_t PIN_BTN_TOG_MAT = 24;
static const uint8_t PIN_BTN_TOG_AIR = 28;
// 4 LED toggle (HIGH=acceso) separati da STBY
static const uint8_t PIN_LED_L1 = 25;
static const uint8_t PIN_LED_L2 = 26;
static const uint8_t PIN_LED_MAT = 27;
static const uint8_t PIN_LED_AIR = 29;
// NUOVI: bottone + led DRY2
static const uint8_t PIN_BTN_DRY2 = 30; // pulsante a GND (INPUT_PULLUP)
static const uint8_t PIN_LED_DRY2 = 31; // LED (pin -> 220ohm -> LED -> GND)
// Relè (ACTIVE LOW)
static const uint8_t PIN_RELAY_L1 = 8;
static const uint8_t PIN_RELAY_L2 = 9;
static const uint8_t PIN_RELAY_MAT = 11;
static const uint8_t PIN_RELAY_AIR = 12;
// =====================================================
// OLED SSD1306 128x64
// =====================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
static const uint8_t OLED_ADDR = 0x3C;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// =====================================================
// DS18B20
// =====================================================
OneWire oneWire(PIN_DS18B20);
DallasTemperature sensors(&oneWire);
// =====================================================
// TIMING
// =====================================================
static const uint32_t SENSOR_PERIOD_MS = 800;
static const uint32_t UI_PERIOD_MS = 160;
static const uint16_t DEBOUNCE_MS = 0; // come nel tuo ultimo codice
static const uint16_t LONGPRESS_MS = 950;
static const uint16_t REPEAT_MS = 150;
static const uint16_t START_SAVE_MS = 3000;
// =====================================================
// BUTTON (click/long esclusivi + repeat)
// =====================================================
struct Button {
uint8_t pin = 0;
bool stable = HIGH;
bool lastReading = HIGH;
uint32_t lastChangeMs = 0;
bool clickEvent = false;
bool longPressEvent = false;
bool repeatEvent = false;
uint32_t pressStartMs = 0;
bool pressedLatch = false;
bool longLatched = false;
uint32_t lastRepeatMs = 0;
void begin(uint8_t p) {
pin = p;
pinMode(pin, INPUT_PULLUP);
stable = digitalRead(pin);
lastReading = stable;
lastChangeMs = millis();
}
bool isPressed() const { return stable == LOW; }
void update() {
clickEvent = false;
longPressEvent = false;
repeatEvent = false;
bool reading = digitalRead(pin);
if (reading != lastReading) {
lastReading = reading;
lastChangeMs = millis();
Serial.print(".");
}
if ((millis() - lastChangeMs) > DEBOUNCE_MS) {
if (stable != reading) {
Serial.print(reading);
Serial.print(" ");
Serial.println(lastChangeMs);
stable = reading;
if (stable == LOW) {
pressedLatch = true;
longLatched = false;
pressStartMs = millis();
lastRepeatMs = millis();
} else {
if (pressedLatch) {
uint32_t held = millis() - pressStartMs;
if (!longLatched && held < LONGPRESS_MS) clickEvent = true;
}
pressedLatch = false;
longLatched = false;
}
}
}
if (isPressed() && pressedLatch && !longLatched) {
if (millis() - pressStartMs >= LONGPRESS_MS) {
longPressEvent = true;
longLatched = true;
}
}
if (isPressed() && pressedLatch) {
if (millis() - lastRepeatMs >= REPEAT_MS) {
repeatEvent = true;
lastRepeatMs = millis();
}
}
}
};
Button bMinus, bPlus, bStart, bMenuDn, bMenuUp, bReset;
Button bTogL1, bTogL2, bTogMat, bTogAir;
Button bDry2;
// =====================================================
// RELAY ACTIVE LOW
// =====================================================
inline void relayWrite(uint8_t pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); }
// =====================================================
// GLOBALS
// =====================================================
Settings S;
Mode mode = Mode::STANDBY;
float tempC = NAN;
bool sensorOk = false;
uint32_t lastSensorMs = 0;
uint32_t lastUiMs = 0;
uint32_t phaseStartMs = 0;
uint32_t phaseDurationMs = 0;
struct Outputs { bool L1=false, L2=false, MAT=false, AIR=false; } out;
static const uint8_t ROW_COUNT = 14;
uint8_t selectedRow = 0;
bool startSaveLatched = false;
uint32_t saveToastUntilMs = 0;
// DRY2 “armato” (manuale). Ogni ciclo parte con questo OFF.
bool dry2Armed = false;
// Splash START non bloccante
bool splashActive = false;
uint32_t splashStartMs = 0;
uint32_t splashLastFrameMs = 0;
// =====================================================
// PROTOTIPI
// =====================================================
uint16_t crc16(const uint8_t* data, size_t len);
void setDefaults();
void saveSettingsNow();
bool loadSettings();
bool applyHyst(bool currentOn, float t, float set, const Hyst& h);
void enterMode(Mode m);
bool phaseDone();
uint32_t remainingMs();
float currentSetC();
void applyOutputs();
void allOff();
void computeControl();
void updatePhases();
void updateSensor();
void updateRelayLeds();
const __FlashStringHelper* rowName(uint8_t r);
void printRowVal(uint8_t r, int x, int y);
void drawUI();
void updateUI();
void adjustSelected(int dir);
void handleButtons();
void startStartSplash();
void updateStartSplash();
// =====================================================
// EEPROM / CRC
// =====================================================
uint16_t crc16(const uint8_t* data, size_t len) {
uint16_t c = 0xFFFF;
for (size_t i = 0; i < len; i++) {
c ^= data[i];
for (uint8_t b = 0; b < 8; b++) {
if (c & 1) c = (c >> 1) ^ 0xA001;
else c >>= 1;
}
}
return c;
}
void setDefaults() {
S.drySetTempC = 55.0f;
S.dryMinutes = 190;
S.standbyTempC = 30.0f;
S.stbyMode = StandbyMode::STBY1_MATS_ONLY;
S.appTempC = 30.0f;
S.appMinutes = 15;
S.lamp1 = { -0.1f, 0.0f };
S.lamp2 = { -0.4f, -0.2f };
S.mats = { 0.0f, 1.0f };
S.airOnDelta = 3.0f;
S.airOffDelta = 2.0f;
// manual toggle default: tutti ON
S.appManL1 = true; S.appManL2 = true; S.appManMAT = true; S.appManAIR = true;
S.dryManL1 = true; S.dryManL2 = true; S.dryManMAT = true; S.dryManAIR = true;
S.magic = EEPROM_MAGIC;
}
void saveSettingsNow() {
S.magic = EEPROM_MAGIC;
S.crc = 0;
S.crc = crc16((uint8_t*)&S, sizeof(Settings));
EEPROM.put(0, S);
}
bool loadSettings() {
Settings tmp;
EEPROM.get(0, tmp);
if (tmp.magic != EEPROM_MAGIC) return false;
uint16_t stored = tmp.crc;
tmp.crc = 0;
uint16_t c = crc16((uint8_t*)&tmp, sizeof(Settings));
if (c != stored) return false;
tmp.crc = stored;
S = tmp;
return true;
}
// =====================================================
// LED toggle separati da STBY + LED DRY2
// =====================================================
void updateRelayLeds() {
bool l1=false, l2=false, mat=false, air=false;
if (mode == Mode::APPASSIMENTO) {
l1 = S.appManL1;
l2 = S.appManL2;
mat = S.appManMAT;
air = S.appManAIR;
} else if (mode == Mode::DRY || mode == Mode::DRY2) {
l1 = S.dryManL1;
l2 = S.dryManL2;
mat = S.dryManMAT;
air = S.dryManAIR;
} else {
l1 = l2 = mat = air = false; // STBY/END: spenti
}
digitalWrite(PIN_LED_L1, l1 ? HIGH : LOW);
digitalWrite(PIN_LED_L2, l2 ? HIGH : LOW);
digitalWrite(PIN_LED_MAT, mat ? HIGH : LOW);
digitalWrite(PIN_LED_AIR, air ? HIGH : LOW);
// LED DRY2 mostra solo “armato” (manuale)
digitalWrite(PIN_LED_DRY2, dry2Armed ? HIGH : LOW);
}
// =====================================================
// CONTROL
// =====================================================
void applyOutputs() {
relayWrite(PIN_RELAY_L1, out.L1);
relayWrite(PIN_RELAY_L2, out.L2);
relayWrite(PIN_RELAY_MAT, out.MAT);
relayWrite(PIN_RELAY_AIR, out.AIR);
updateRelayLeds();
}
void allOff() {
out.L1 = out.L2 = out.MAT = out.AIR = false;
applyOutputs();
}
bool applyHyst(bool currentOn, float t, float set, const Hyst& h) {
if (!isfinite(t)) return false;
if (!currentOn) return (t <= set + h.onOffset);
return !(t >= set + h.offOffset);
}
void enterMode(Mode m) {
mode = m;
phaseStartMs = millis();
if (mode == Mode::APPASSIMENTO) phaseDurationMs = (uint32_t)S.appMinutes * 60000UL;
else if (mode == Mode::DRY || mode == Mode::DRY2) phaseDurationMs = (uint32_t)S.dryMinutes * 60000UL;
else phaseDurationMs = 0;
}
bool phaseDone() {
if (phaseDurationMs == 0) return false;
return (millis() - phaseStartMs) >= phaseDurationMs;
}
uint32_t remainingMs() {
if (phaseDurationMs == 0) return 0;
uint32_t e = millis() - phaseStartMs;
if (e >= phaseDurationMs) return 0;
return phaseDurationMs - e;
}
float currentSetC() {
if (mode == Mode::APPASSIMENTO) return S.appTempC;
if (mode == Mode::DRY || mode == Mode::DRY2) return S.drySetTempC;
return S.standbyTempC;
}
void computeControl() {
// END: tutto spento sempre
if (mode == Mode::END) { allOff(); return; }
if (!sensorOk) { allOff(); return; }
float set = currentSetC();
// AIR enable (toggle in APP/DRY/DRY2), in STBY sempre abilitato
bool airEnabled = true;
if (mode == Mode::APPASSIMENTO) airEnabled = S.appManAIR;
else if (mode == Mode::DRY || mode == Mode::DRY2) airEnabled = S.dryManAIR;
if (!airEnabled) {
out.AIR = false;
} else {
if (!out.AIR) out.AIR = (tempC >= set + S.airOnDelta);
else out.AIR = !(tempC <= set + S.airOffDelta);
}
// se AIR ON -> spegne tutto il resto
if (out.AIR) {
out.L1 = false;
out.L2 = false;
out.MAT = false;
applyOutputs();
return;
}
if (mode == Mode::STANDBY) {
out.MAT = applyHyst(out.MAT, tempC, set, S.mats);
if (S.stbyMode == StandbyMode::STBY3_MATS_PLUS_L1_L2) {
out.L1 = applyHyst(out.L1, tempC, set, S.lamp1);
out.L2 = applyHyst(out.L2, tempC, set, S.lamp2);
} else if (S.stbyMode == StandbyMode::STBY2_MATS_PLUS_L2) {
out.L1 = false;
out.L2 = applyHyst(out.L2, tempC, set, S.lamp2);
} else {
out.L1 = false;
out.L2 = false;
}
} else if (mode == Mode::APPASSIMENTO) {
out.L1 = S.appManL1 ? applyHyst(out.L1, tempC, set, S.lamp1) : false;
out.L2 = S.appManL2 ? applyHyst(out.L2, tempC, set, S.lamp2) : false;
out.MAT = S.appManMAT ? applyHyst(out.MAT, tempC, set, S.mats) : false;
} else { // DRY o DRY2
out.L1 = S.dryManL1 ? applyHyst(out.L1, tempC, set, S.lamp1) : false;
out.L2 = S.dryManL2 ? applyHyst(out.L2, tempC, set, S.lamp2) : false;
out.MAT = S.dryManMAT ? applyHyst(out.MAT, tempC, set, S.mats) : false;
}
applyOutputs();
}
void updatePhases() {
if (mode == Mode::APPASSIMENTO && phaseDone()) {
enterMode(dry2Armed ? Mode::DRY2 : Mode::DRY);
}
else if (mode == Mode::DRY && phaseDone()) {
enterMode(Mode::STANDBY);
}
else if (mode == Mode::DRY2 && phaseDone()) {
enterMode(Mode::END);
allOff();
}
}
// =====================================================
// SENSOR
// =====================================================
void updateSensor() {
if (millis() - lastSensorMs < SENSOR_PERIOD_MS) return;
lastSensorMs = millis();
sensors.requestTemperatures();
float t = sensors.getTempCByIndex(0);
if (t <= -100.0f || t >= 125.0f || t == 85.0f) {
sensorOk = false;
tempC = NAN;
} else {
sensorOk = true;
tempC = t;
}
}
// =====================================================
// UI rows
// =====================================================
const __FlashStringHelper* rowName(uint8_t r) {
switch (r) {
case 0: return F("DRY SET");
case 1: return F("DRY TIM");
case 2: return F("STBY T");
case 3: return F("STBY MOD");
case 4: return F("APP T");
case 5: return F("APP TIM");
case 6: return F("L1 ON");
case 7: return F("L1 OFF");
case 8: return F("L2 ON");
case 9: return F("L2 OFF");
case 10: return F("MAT ON");
case 11: return F("MAT OFF");
case 12: return F("AIR ON");
case 13: return F("AIR OFF");
}
return F("?");
}
static inline void trimLeft(char* s) {
char* p = s;
while (*p == ' ') p++;
if (p != s) memmove(s, p, strlen(p) + 1);
}
void printRowVal(uint8_t r, int x, int y) {
char buf[16];
buf[0] = '\0';
switch (r) {
case 0: dtostrf(S.drySetTempC, 0, 1, buf); strcat(buf, "C"); break;
case 1: itoa(S.dryMinutes, buf, 10); strcat(buf, "m"); break;
case 2: dtostrf(S.standbyTempC, 0, 1, buf); strcat(buf, "C"); break;
case 3:
if (S.stbyMode == StandbyMode::STBY1_MATS_ONLY) strcpy(buf, "1");
else if (S.stbyMode == StandbyMode::STBY2_MATS_PLUS_L2) strcpy(buf, "2");
else strcpy(buf, "3");
break;
case 4: dtostrf(S.appTempC, 0, 1, buf); strcat(buf, "C"); break;
case 5: itoa(S.appMinutes, buf, 10); strcat(buf, "m"); break;
case 6: dtostrf(S.lamp1.onOffset, 0, 1, buf); break;
case 7: dtostrf(S.lamp1.offOffset, 0, 1, buf); break;
case 8: dtostrf(S.lamp2.onOffset, 0, 1, buf); break;
case 9: dtostrf(S.lamp2.offOffset, 0, 1, buf); break;
case 10: dtostrf(S.mats.onOffset, 0, 1, buf); break;
case 11: dtostrf(S.mats.offOffset, 0, 1, buf); break;
case 12: dtostrf(S.airOnDelta, 0, 1, buf); break;
case 13: dtostrf(S.airOffDelta, 0, 1, buf); break;
default: strcpy(buf, "?"); break;
}
trimLeft(buf);
display.setCursor(x, y);
display.print(buf);
}
static inline void printModeLabel(int x, int y) {
display.setCursor(x, y);
if (out.AIR) { display.print("COOL"); return; }
if (mode == Mode::STANDBY) {
if (S.stbyMode == StandbyMode::STBY1_MATS_ONLY) display.print("ST1");
else if (S.stbyMode == StandbyMode::STBY2_MATS_PLUS_L2) display.print("ST2");
else display.print("ST3");
return;
}
if (mode == Mode::APPASSIMENTO) { display.print("APP"); return; }
if (mode == Mode::DRY) { display.print("DRY"); return; }
if (mode == Mode::DRY2) { display.print("DRY2"); return; }
display.print("END");
}
// =====================================================
// UI DRAW
// =====================================================
void drawUI() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
// HEADER riga 0
display.setTextSize(1);
display.setCursor(0, 0);
display.print("M:");
printModeLabel(14, 0);
// toast SALV
bool toastOn = (saveToastUntilMs != 0) && ((int32_t)(millis() - saveToastUntilMs) < 0);
if (toastOn) {
display.setCursor(40, 0);
display.print("SALV");
}
// stati uscite (reali)
display.setCursor(66, 0);
display.print(out.L1 ? "L1" : "-");
display.print(out.L2 ? ".L2" : ".-");
display.print(out.MAT ? ".M" : ".-");
display.print(out.AIR ? ".F" : ".-");
// TEMP grande
display.setTextSize(2,4);
display.setCursor(0, 12);
if (sensorOk) {
char tbuf[10];
dtostrf(tempC, 0, 1, tbuf);
trimLeft(tbuf);
display.print(tbuf);
display.print((char)247);
display.print("C");
} else {
display.setTextSize(2);
display.setCursor(0, 12);
display.print("SENS");
display.setCursor(0, 28);
display.print("ERR");
}
// TIMER (destra)
display.setTextSize(1,2);
display.setCursor(96, 10);
if (mode == Mode::APPASSIMENTO || mode == Mode::DRY || mode == Mode::DRY2) {
uint32_t r = remainingMs();
uint16_t rm = (uint16_t)(r / 60000UL);
uint8_t rs = (uint8_t)((r % 60000UL) / 1000UL);
display.print(rm); display.print("m");
display.setCursor(96, 26);
if (rs < 10) display.print("0");
display.print(rs); display.print("s");
} else {
display.print("--m");
display.setCursor(96, 26);
display.print("--s");
}
// SETTINGS window (2 righe)
const uint8_t WINDOW_ROWS = 2;
uint8_t winStart = (selectedRow / WINDOW_ROWS) * WINDOW_ROWS;
if (winStart > ROW_COUNT - WINDOW_ROWS) winStart = ROW_COUNT - WINDOW_ROWS;
const int y0 = 44;
const int dy = 10;
const int xVal = 78;
for (uint8_t i = 0; i < WINDOW_ROWS; i++) {
uint8_t r = winStart + i;
int y = y0 + i * dy;
bool sel = (r == selectedRow);
if (sel) {
display.fillRect(0, y - 1, 128, 10, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
} else {
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
}
display.setTextSize(1);
display.setCursor(0, y);
display.print(rowName(r));
display.setCursor(xVal, y);
printRowVal(r, xVal, y);
}
display.display();
}
void updateUI() {
if (millis() - lastUiMs < UI_PERIOD_MS) return;
lastUiMs = millis();
drawUI();
}
// =====================================================
// Splash START non bloccante (OLED)
// =====================================================
void startStartSplash() {
splashActive = true;
splashStartMs = millis();
splashLastFrameMs = 0;
display.clearDisplay();
display.display();
}
void updateStartSplash() {
if (!splashActive) return;
const uint32_t now = millis();
if (now - splashLastFrameMs < 70) return;
splashLastFrameMs = now;
const uint32_t t = now - splashStartMs;
const uint32_t TOTAL_MS = 3000;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
const int cx = 64;
const int cy = 28;
if (t < 1000) {
// bomba
display.fillCircle(cx, cy, 10, SSD1306_WHITE);
display.fillCircle(cx, cy, 9, SSD1306_BLACK);
display.drawCircle(cx, cy, 10, SSD1306_WHITE);
display.drawLine(cx + 7, cy - 7, cx + 18, cy - 16, SSD1306_WHITE);
display.drawLine(cx + 18, cy - 16, cx + 22, cy - 12, SSD1306_WHITE);
if ((t / 120) % 2 == 0) {
display.drawCircle(cx + 22, cy - 12, 2, SSD1306_WHITE);
display.drawLine(cx + 20, cy - 12, cx + 24, cy - 12, SSD1306_WHITE);
display.drawLine(cx + 22, cy - 14, cx + 22, cy - 10, SSD1306_WHITE);
}
display.setTextSize(1,2);
display.setCursor(38, 45);
display.print("STARTING...");
} else {
uint32_t et = t - 1000;
uint8_t k = (uint8_t)(et / 260);
int r1 = 6 + k * 6;
int r2 = 12 + k * 7;
int r3 = 18 + k * 8;
display.drawCircle(cx, cy, r1, SSD1306_WHITE);
display.drawCircle(cx, cy, r2, SSD1306_WHITE);
display.drawCircle(cx, cy, r3, SSD1306_WHITE);
display.drawLine(cx - r2, cy, cx + r2, cy, SSD1306_WHITE);
display.drawLine(cx, cy - r2, cx, cy + r2, SSD1306_WHITE);
display.drawLine(cx - r1, cy - r1, cx + r1, cy + r1, SSD1306_WHITE);
display.drawLine(cx - r1, cy + r1, cx + r1, cy - r1, SSD1306_WHITE);
display.setTextSize(3);
display.setCursor(30, 8);
display.print("BOOM");
}
display.display();
if (t >= TOTAL_MS) {
splashActive = false;
enterMode(Mode::APPASSIMENTO);
allOff(); // parti pulito
updateRelayLeds();
drawUI();
}
}
// =====================================================
// ADJUST
// =====================================================
void adjustSelected(int dir) {
const float stepTemp = 1.0f;
const float stepHys = 0.1f;
switch (selectedRow) {
case 0: S.drySetTempC = constrain(S.drySetTempC + dir * stepTemp, 20.0f, 80.0f); break;
case 1: { int v = (int)S.dryMinutes + dir * 10; v = constrain(v, 10, 24 * 60); S.dryMinutes = (uint16_t)v; } break;
case 2: S.standbyTempC = constrain(S.standbyTempC + dir * stepTemp, 10.0f, 60.0f); break;
case 3:
if (S.stbyMode == StandbyMode::STBY1_MATS_ONLY) S.stbyMode = StandbyMode::STBY2_MATS_PLUS_L2;
else if (S.stbyMode == StandbyMode::STBY2_MATS_PLUS_L2) S.stbyMode = StandbyMode::STBY3_MATS_PLUS_L1_L2;
else S.stbyMode = StandbyMode::STBY1_MATS_ONLY;
break;
case 4: S.appTempC = constrain(S.appTempC + dir * stepTemp, 10.0f, 60.0f); break;
case 5: { int v = (int)S.appMinutes + dir; v = constrain(v, 1, 180); S.appMinutes = (uint16_t)v; } break;
case 6: S.lamp1.onOffset = constrain(S.lamp1.onOffset + dir * stepHys, -10.0f, 10.0f); break;
case 7: S.lamp1.offOffset = constrain(S.lamp1.offOffset + dir * stepHys, -10.0f, 10.0f); break;
case 8: S.lamp2.onOffset = constrain(S.lamp2.onOffset + dir * stepHys, -10.0f, 10.0f); break;
case 9: S.lamp2.offOffset = constrain(S.lamp2.offOffset + dir * stepHys, -10.0f, 10.0f); break;
case 10: S.mats.onOffset = constrain(S.mats.onOffset + dir * stepHys, -10.0f, 10.0f); break;
case 11: S.mats.offOffset = constrain(S.mats.offOffset + dir * stepHys, -10.0f, 10.0f); break;
case 12: S.airOnDelta = constrain(S.airOnDelta + dir * stepHys, 0.0f, 30.0f); break;
case 13: S.airOffDelta = constrain(S.airOffDelta + dir * stepHys, 0.0f, 30.0f); break;
default: break;
}
}
// =====================================================
// INPUT
// =====================================================
void handleButtons() {
bMinus.update();
bPlus.update();
bStart.update();
bMenuDn.update();
bMenuUp.update();
bReset.update();
bTogL1.update();
bTogL2.update();
bTogMat.update();
bTogAir.update();
bDry2.update();
// reset long
if (bReset.longPressEvent) {
wdt_enable(WDTO_15MS);
while (true) {}
}
// menu up/down
bool dn = bMenuDn.clickEvent;
bool up = bMenuUp.clickEvent;
if (dn && !up) selectedRow = (selectedRow + 1) % ROW_COUNT;
if (up && !dn) selectedRow = (selectedRow + ROW_COUNT - 1) % ROW_COUNT;
if (bMenuDn.longPressEvent || bMenuUp.longPressEvent) selectedRow = 0;
// +/- (click + repeat)
if (bPlus.clickEvent || bPlus.repeatEvent) adjustSelected(+1);
if (bMinus.clickEvent || bMinus.repeatEvent) adjustSelected(-1);
// toggle manuali L1/L2/MAT/AIR: solo APP/DRY/DRY2
if (mode == Mode::APPASSIMENTO) {
if (bTogL1.clickEvent) S.appManL1 = !S.appManL1;
if (bTogL2.clickEvent) S.appManL2 = !S.appManL2;
if (bTogMat.clickEvent) S.appManMAT = !S.appManMAT;
if (bTogAir.clickEvent) S.appManAIR = !S.appManAIR;
} else if (mode == Mode::DRY || mode == Mode::DRY2) {
if (bTogL1.clickEvent) S.dryManL1 = !S.dryManL1;
if (bTogL2.clickEvent) S.dryManL2 = !S.dryManL2;
if (bTogMat.clickEvent) S.dryManMAT = !S.dryManMAT;
if (bTogAir.clickEvent) S.dryManAIR = !S.dryManAIR;
}
// DRY2 toggle: utilizzabile in APP/DRY/DRY2
if (mode == Mode::APPASSIMENTO || mode == Mode::DRY || mode == Mode::DRY2) {
if (bDry2.clickEvent) {
dry2Armed = !dry2Armed;
}
}
updateRelayLeds();
// START long 3s: salva (non blocca, non spegne)
if (bStart.isPressed() && bStart.pressedLatch && !startSaveLatched) {
if (millis() - bStart.pressStartMs >= START_SAVE_MS) {
saveSettingsNow();
startSaveLatched = true;
saveToastUntilMs = millis() + 1200;
}
}
if (!bStart.isPressed()) startSaveLatched = false;
// START click: start/stop
if (bStart.clickEvent) {
if (mode == Mode::STANDBY) {
// ULTIMA MODIFICA: ogni ciclo parte sempre APP->DRY (DRY2 disarmato di default)
dry2Armed = false;
allOff();
startStartSplash(); // splash non bloccante poi entra in APP
}
else if (mode == Mode::END) {
// da END: START -> STANDBY (tutto spento)
dry2Armed = false;
enterMode(Mode::STANDBY);
allOff();
}
else {
// da APP/DRY/DRY2: STOP -> STANDBY
enterMode(Mode::STANDBY);
allOff();
dry2Armed = false;
}
}
}
// =====================================================
// SETUP / LOOP
// =====================================================
void setup() {
// relè
pinMode(PIN_RELAY_L1, OUTPUT);
pinMode(PIN_RELAY_L2, OUTPUT);
pinMode(PIN_RELAY_MAT, OUTPUT);
pinMode(PIN_RELAY_AIR, OUTPUT);
// led
pinMode(PIN_LED_L1, OUTPUT);
pinMode(PIN_LED_L2, OUTPUT);
pinMode(PIN_LED_MAT, OUTPUT);
pinMode(PIN_LED_AIR, OUTPUT);
pinMode(PIN_LED_DRY2, OUTPUT);
// off iniziale
dry2Armed = false;
allOff();
// bottoni
bMinus.begin(PIN_BTN_MINUS);
bPlus.begin(PIN_BTN_PLUS);
bStart.begin(PIN_BTN_START);
bMenuDn.begin(PIN_BTN_MENU_DN);
bMenuUp.begin(PIN_BTN_MENU_UP);
bReset.begin(PIN_BTN_RESET);
bTogL1.begin(PIN_BTN_TOG_L1);
bTogL2.begin(PIN_BTN_TOG_L2);
bTogMat.begin(PIN_BTN_TOG_MAT);
bTogAir.begin(PIN_BTN_TOG_AIR);
bDry2.begin(PIN_BTN_DRY2);
// sensore
sensors.begin();
// OLED
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.display();
Serial.begin(2000000);
Serial.println("Boot");
// settings
if (!loadSettings()) {
setDefaults();
saveSettingsNow();
}
enterMode(Mode::STANDBY);
updateRelayLeds();
drawUI();
}
void loop() {
uint64_t t1 = micros();
handleButtons();
uint64_t t2 = micros();
if (splashActive) {
updateStartSplash();
return;
}
uint64_t t3 = micros();
updateSensor();
uint64_t t4 = micros();
updatePhases();
uint64_t t5 = micros();
computeControl();
uint64_t t6 = micros();
updateUI();
uint64_t t7 = micros();
Serial.print(uint32_t(t2-t1));
Serial.print("\t");
Serial.print(uint32_t(t3-t2));
Serial.print("\t");
Serial.print(uint32_t(t4-t3));
Serial.print("\t");
Serial.print(uint32_t(t5-t4));
Serial.print("\t");
Serial.print(uint32_t(t6-t5));
Serial.print("\t");
Serial.println(uint32_t(t7-t6));
}