// ════════════════════════════════════════════════════════════════════
// P5 – TERAPIA FÍSICA: Modo Isotónico + Isométrico
// ESP32-S3 | Arduino Core 2.x | No-bloqueante + HW Timer
// ════════════════════════════════════════════════════════════════════
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
// ════════════════════════════════════════════════════════════════════
// DEFINICIÓN DE PINES (ajustar según montaje físico)
// ════════════════════════════════════════════════════════════════════
// ── OLED (I²C) ──────────────────────────────────────────────────────
#define SDA_OLED 2
#define SCL_OLED 1
// ── Botones pulsadores (INPUT_PULLUP → LOW al presionar) ────────────
#define BTN1_PIN 15
#define BTN2_PIN 16
#define BTN3_PIN 17
#define BTN4_PIN 18
// ── Switches de palanca (INPUT_PULLUP) ──────────────────────────────
// SW1: LOW = pausa | HIGH = activo
// SW2: LOW = mostrar reps en 7-seg | HIGH = mostrar tiempo (solo isotónico)
#define SW1_PIN 8
#define SW2_PIN 9
// ── LEDs indicadores individuales (NO son el RGB) ──────────────────
#define LED1_PIN 48 // Indicador de pausa (SW1 activo)
#define LED2_PIN 45 // Indicador de modo reps en 7-seg (SW2=0, solo isotónico)
// ── Buzzer pasivo ───────────────────────────────────────────────────
#define BUZZER_PIN 47
// ── NeoPixel RGB integrado en ESP32-S3 ─────────────────────────────
#define NEOPIXEL_PIN 3
#define NEOPIXEL_COUNT 1
// ── Display 7 segmentos ─────────────────────────────────────────────
// Orden: segmentos a, b, c, d, e, f, g
const uint8_t SEG_PINS[7] = {10, 11, 12, 13, 14, 21, 20};
// Pines de dígito: D1 (izquierda) … D4 (derecha)
const uint8_t DIG_PINS[4] = {4, 5, 6, 7};
// Tipo de display: true = ánodo común | false = cátodo común
#define COMMON_ANODE false
// Pin del punto decimal (-1 si no está disponible)
#define SEG_DP_PIN 46
// ════════════════════════════════════════════════════════════════════
// CONSTANTES DE SISTEMA
// ════════════════════════════════════════════════════════════════════
// ── Isotónico ────────────────────────────────────────────────────────
#define TIME_MIN_SEC 60 // 1 min mínimo
#define TIME_MAX_SEC 300 // 5 min máximo
#define TIME_DEFAULT 180 // 3 min por defecto
#define TIME_STEP 10 // paso de 10 s
#define REPS_MIN 3
#define REPS_MAX 12
#define REPS_DEFAULT 5
// ── Isométrico ───────────────────────────────────────────────────────
#define ISO_TIME_MIN_SEC 30 // 30 s mínimo
#define ISO_TIME_MAX_SEC 180 // 3 min máximo
#define ISO_TIME_DEFAULT 60 // 1 min por defecto
// (comparte TIME_STEP = 10 s)
// ── Temporización general ────────────────────────────────────────────
#define DEBOUNCE_MS 50
#define DOUBLE_CLICK_MS 500
#define OLED_REFRESH_MS 150
#define SEG_MUX_MS 8
// ── Señales ──────────────────────────────────────────────────────────
#define WARN_THRESHOLD_MS 20000UL // advertencia a 20 s restantes
#define WARN_BLINK_HZ_MS 833UL // 1.2 Hz → ~833 ms
#define WARN_DURATION_MS 5000UL // parpadeo durante 5 s
#define SIGNAL_REP_MS 500UL // azul + buzzer por rep completada
#define SIGNAL_LIMIT_MS 1000UL // rojo + buzzer por límite alcanzado
#define SIGNAL_END_MS 2000UL // buzzer fin de sesión / ciclo
// ════════════════════════════════════════════════════════════════════
// OBJETOS DE LIBRERÍA
// ════════════════════════════════════════════════════════════════════
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_NeoPixel rgb(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// ════════════════════════════════════════════════════════════════════
// MÁQUINA DE ESTADOS PRINCIPAL
// ════════════════════════════════════════════════════════════════════
enum State : uint8_t {
// ── Compartido ──────────────────────────────────────────────────
S_MODE_SELECT, // Selección Isotónico / Isométrico
S_END, // Resumen de fin de sesión (ambos modos)
// ── Isotónico ───────────────────────────────────────────────────
S_TIME_CONFIG, // Configurar duración (1-5 min)
S_REPS_CONFIG, // Configurar repeticiones (3-12)
S_SUMMARY, // Resumen + doble clic para iniciar
S_THERAPY, // Terapia isotónica en curso
// ── Isométrico ──────────────────────────────────────────────────
S_ISO_TIME_CONFIG, // Configurar duración (30 s - 3 min)
S_ISO_SUMMARY, // Resumen isométrico + doble clic para iniciar
S_ISO_THERAPY, // Terapia isométrica en curso (HW timer)
S_ISO_SIDE_QUERY // Preguntar si se hace el otro lado
};
State sysState = S_MODE_SELECT;
// ════════════════════════════════════════════════════════════════════
// VARIABLES DE SESIÓN – Isotónico
// ════════════════════════════════════════════════════════════════════
bool modeIsotonic = true;
int cfgTimeSec = TIME_DEFAULT;
int cfgReps = REPS_DEFAULT;
int repsOK = 0;
String endReason = "";
unsigned long therapyStartMs = 0;
unsigned long accPausedMs = 0;
unsigned long pauseBeginMs = 0;
bool therapyPaused = false;
// ════════════════════════════════════════════════════════════════════
// VARIABLES DE SESIÓN – Isométrico
// ════════════════════════════════════════════════════════════════════
int isoTimeSec = ISO_TIME_DEFAULT;
volatile int isoCountdown = 0; // decrementado por ISR del HW timer
volatile bool isoTimerDone = false;
bool isoPaused = false;
int isoCycle = 1; // 1 = primer lado, 2 = segundo lado
hw_timer_t *isoHwTimer = NULL;
// ════════════════════════════════════════════════════════════════════
// HARDWARE TIMER – ISR e inicialización
// ════════════════════════════════════════════════════════════════════
// ISR: se ejecuta cada 1 segundo, decrementa el contador isométrico.
// Reside en IRAM para evitar latencias de caché de flash.
void IRAM_ATTR onIsoTimerISR() {
if (isoCountdown > 0) {
isoCountdown--;
if (isoCountdown == 0) isoTimerDone = true;
}
}
// Crea (si no existe) y configura el timer de hardware.
// Timer 1, prescaler 80 → 80 MHz / 80 = 1 MHz. Alarma cada 1 000 000 ticks = 1 s.
void isoTimerInit() {
if (isoHwTimer == NULL) {
// Arduino Core 3.x API: timerBegin(frequency_en_Hz)
isoHwTimer = timerBegin(1000000);
timerAttachInterrupt(isoHwTimer, &onIsoTimerISR);
}
}
// Arranca la cuenta regresiva desde `seconds` segundos.
void isoTimerStart(int seconds) {
isoTimerInit();
isoCountdown = seconds;
isoTimerDone = false;
timerWrite(isoHwTimer, 0); // resetea el contador hardware a 0
// Configura y habilita la alarma en un solo paso
timerAlarm(isoHwTimer, 1000000UL, true, 0);
timerStart(isoHwTimer);
}
// Congela el contador hardware (pausa total, no solo alarma).
void isoTimerPause() {
if (isoHwTimer) timerStop(isoHwTimer);
}
// Reanuda desde donde se pausó; resetea el contador para que el
// próximo tick tarde exactamente 1 segundo.
void isoTimerResume() {
if (isoHwTimer) {
timerWrite(isoHwTimer, 0);
timerStart(isoHwTimer);
}
}
// Deshabilita el contador.
void isoTimerStop() {
if (isoHwTimer) {
timerStop(isoHwTimer);
}
}
// Libera completamente el timer (se llama en returnToStart).
void isoTimerDestroy() {
if (isoHwTimer) {
timerStop(isoHwTimer);
timerDetachInterrupt(isoHwTimer);
timerEnd(isoHwTimer);
isoHwTimer = NULL;
}
}
// ════════════════════════════════════════════════════════════════════
// MANEJO DE BOTONES (antirebote + flanco descendente)
// ════════════════════════════════════════════════════════════════════
struct BtnCtx {
uint8_t pin;
bool stable;
bool rawPrev;
unsigned long debounceAt;
bool fired;
};
BtnCtx btns[4];
void initButtons() {
const uint8_t PINS[4] = {BTN1_PIN, BTN2_PIN, BTN3_PIN, BTN4_PIN};
for (int i = 0; i < 4; i++) {
pinMode(PINS[i], INPUT_PULLUP);
btns[i] = {PINS[i], HIGH, HIGH, 0UL, false};
}
}
void readButtons() {
for (int i = 0; i < 4; i++) {
btns[i].fired = false;
bool reading = digitalRead(btns[i].pin);
if (reading != btns[i].rawPrev) {
btns[i].rawPrev = reading;
btns[i].debounceAt = millis();
}
if ((millis() - btns[i].debounceAt) >= DEBOUNCE_MS) {
if (reading != btns[i].stable) {
btns[i].stable = reading;
if (reading == LOW) btns[i].fired = true;
}
}
}
}
// ── Doble clic sobre BTN1 ────────────────────────────────────────────
unsigned long lastBtn1ClickMs = 0;
bool pendingClick = false;
bool checkDoubleClick() {
if (pendingClick && (millis() - lastBtn1ClickMs) > DOUBLE_CLICK_MS) {
pendingClick = false;
}
if (btns[0].fired) {
unsigned long now = millis();
if (pendingClick && (now - lastBtn1ClickMs) <= DOUBLE_CLICK_MS) {
pendingClick = false;
return true;
}
pendingClick = true;
lastBtn1ClickMs = now;
}
return false;
}
// ════════════════════════════════════════════════════════════════════
// DISPLAY DE 7 SEGMENTOS (multiplexado por software, sin librería)
// ════════════════════════════════════════════════════════════════════
const uint8_t SEG_ENC[10] = {
0b0111111, // 0
0b0000110, // 1
0b1011011, // 2
0b1001111, // 3
0b1100110, // 4
0b1101101, // 5
0b1111101, // 6
0b0000111, // 7
0b1111111, // 8
0b1101111, // 9
};
#define SEG_BLANK 0b0000000u
#define SEG_DASH 0b1000000u
uint8_t segBuf[4] = {SEG_BLANK, SEG_BLANK, SEG_BLANK, SEG_BLANK};
bool segDP[4] = {false, false, false, false};
uint8_t muxDigit = 0;
unsigned long muxLast = 0;
void initSeg() {
for (int i = 0; i < 7; i++) {
pinMode(SEG_PINS[i], OUTPUT);
digitalWrite(SEG_PINS[i], COMMON_ANODE ? HIGH : LOW);
}
for (int i = 0; i < 4; i++) {
pinMode(DIG_PINS[i], OUTPUT);
digitalWrite(DIG_PINS[i], COMMON_ANODE ? LOW : HIGH);
}
if (SEG_DP_PIN >= 0) {
pinMode(SEG_DP_PIN, OUTPUT);
digitalWrite(SEG_DP_PIN, COMMON_ANODE ? HIGH : LOW);
}
}
void muxSeg() {
if ((millis() - muxLast) < SEG_MUX_MS) return;
muxLast = millis();
digitalWrite(DIG_PINS[muxDigit], COMMON_ANODE ? LOW : HIGH);
muxDigit = (muxDigit + 1) % 4;
uint8_t enc = segBuf[muxDigit];
for (int seg = 0; seg < 7; seg++) {
bool on = (enc >> seg) & 1;
if (COMMON_ANODE) on = !on;
digitalWrite(SEG_PINS[seg], on ? HIGH : LOW);
}
if (SEG_DP_PIN >= 0) {
bool dp = segDP[muxDigit];
if (COMMON_ANODE) dp = !dp;
digitalWrite(SEG_DP_PIN, dp ? HIGH : LOW);
}
digitalWrite(DIG_PINS[muxDigit], COMMON_ANODE ? HIGH : LOW);
}
void segBlank() {
for (int i = 0; i < 4; i++) { segBuf[i] = SEG_BLANK; segDP[i] = false; }
}
// " XX" – repeticiones completadas
void segShowReps(int reps) {
reps = constrain(reps, 0, 99);
segBuf[0] = SEG_BLANK;
segBuf[1] = SEG_BLANK;
segBuf[2] = (reps / 10 > 0) ? SEG_ENC[reps / 10] : SEG_BLANK;
segBuf[3] = SEG_ENC[reps % 10];
for (int i = 0; i < 4; i++) segDP[i] = false;
}
// "M.SS" – tiempo restante
void segShowTime(int totalSec) {
totalSec = constrain(totalSec, 0, 599);
int m = totalSec / 60;
int s = totalSec % 60;
segBuf[0] = SEG_BLANK;
segBuf[1] = SEG_ENC[m];
segBuf[2] = SEG_ENC[s / 10];
segBuf[3] = SEG_ENC[s % 10];
for (int i = 0; i < 4; i++) segDP[i] = false;
segDP[1] = true; // punto decimal → M.SS
}
// ════════════════════════════════════════════════════════════════════
// BUZZER (no-bloqueante)
// ════════════════════════════════════════════════════════════════════
unsigned long buzzEndMs = 0;
bool buzzActive = false;
void buzzStart(unsigned long durationMs) {
tone(BUZZER_PIN, 1000, durationMs);
buzzActive = true;
buzzEndMs = millis() + durationMs;
}
void buzzStop() {
noTone(BUZZER_PIN);
digitalWrite(BUZZER_PIN, LOW);
buzzActive = false;
}
void buzzUpdate() {
if (buzzActive && millis() >= buzzEndMs) buzzStop();
}
// ════════════════════════════════════════════════════════════════════
// LED RGB (no-bloqueante)
// ════════════════════════════════════════════════════════════════════
unsigned long rgbEndMs = 0;
bool rgbActive = false;
void rgbSet(uint8_t r, uint8_t g, uint8_t b, unsigned long durationMs) {
rgb.setPixelColor(0, rgb.Color(r, g, b));
rgb.show();
rgbActive = true;
rgbEndMs = millis() + durationMs;
}
void rgbOff() {
rgb.clear(); rgb.show();
rgbActive = false;
}
void rgbUpdate() {
if (rgbActive && millis() >= rgbEndMs) rgbOff();
}
// ── Señales compuestas ───────────────────────────────────────────────
void signalLimit() {
buzzStart(SIGNAL_LIMIT_MS);
rgbSet(255, 0, 0, SIGNAL_LIMIT_MS); // rojo
}
void signalRep() {
buzzStart(SIGNAL_REP_MS);
rgbSet(0, 0, 255, SIGNAL_REP_MS); // azul
}
void signalEnd() {
buzzStart(SIGNAL_END_MS);
rgbOff();
}
void signalCycleEnd() {
buzzStart(SIGNAL_END_MS);
rgbSet(0, 200, 0, SIGNAL_END_MS); // verde al completar ciclo
}
// ════════════════════════════════════════════════════════════════════
// ADVERTENCIA 20 SEGUNDOS (parpadeo amarillo 1.2 Hz durante 5 s)
// ════════════════════════════════════════════════════════════════════
bool warningFired = false;
bool warnBlinking = false;
unsigned long warnBlinkStart = 0;
unsigned long warnBlinkLast = 0;
bool warnBlinkOn = false;
void startWarning() {
warningFired = true;
warnBlinking = true;
warnBlinkStart = millis();
warnBlinkLast = millis();
warnBlinkOn = false;
}
void warnUpdate() {
if (!warnBlinking) return;
unsigned long now = millis();
if (now - warnBlinkStart >= WARN_DURATION_MS) {
warnBlinking = false;
rgbOff();
buzzStop();
return;
}
if (now - warnBlinkLast >= WARN_BLINK_HZ_MS) {
warnBlinkLast = now;
warnBlinkOn = !warnBlinkOn;
if (warnBlinkOn) {
rgbSet(255, 200, 0, WARN_BLINK_HZ_MS);
buzzStart(WARN_BLINK_HZ_MS / 2);
} else {
rgbOff();
buzzStop();
}
}
}
// ════════════════════════════════════════════════════════════════════
// OLED – Pantallas del modo isotónico
// ════════════════════════════════════════════════════════════════════
void printCentered(const char *txt, int y, int textSize = 1) {
display.setTextSize(textSize);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(txt, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - (int16_t)w) / 2, y);
display.print(txt);
}
void oledModeSelect() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Selecciona Modo:");
display.setCursor(0, 16); display.print("[1] Isotonico");
display.setCursor(0, 28); display.print("[2] Isometrico");
display.display();
}
void oledTimeConfig(int sec) {
char buf[8];
snprintf(buf, sizeof(buf), "%d:%02d", sec / 60, sec % 60);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Tiempo (min:seg):");
printCentered(buf, 18, 3);
display.setTextSize(1);
display.setCursor(0, 56); display.print("-10s");
display.setCursor(96, 56); display.print("+10s");
display.display();
}
void oledRepsConfig(int reps) {
char buf[4];
snprintf(buf, sizeof(buf), "%d", reps);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Num. repeticiones:");
printCentered(buf, 18, 3);
display.setTextSize(1);
display.setCursor(0, 56); display.print("-");
display.setCursor(SCREEN_WIDTH-6, 56); display.print("+");
display.display();
}
void oledSummary() {
char timeBuf[10], repsBuf[12];
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d", cfgTimeSec / 60, cfgTimeSec % 60);
snprintf(repsBuf, sizeof(repsBuf), "Reps: %d", cfgReps);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Isotonico");
display.setCursor(0, 12); display.print(repsBuf);
display.setCursor(0, 22); display.print("Tiempo: "); display.print(timeBuf);
display.setCursor(0, 40); display.print("Doble clic [1]");
display.setCursor(0, 50); display.print("para iniciar");
display.display();
}
void oledTherapy(long msLeft, int done, int total, bool warn20, bool paused) {
char timeBuf[8], fracBuf[10];
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d",
(int)(msLeft / 60000), (int)((msLeft % 60000) / 1000));
snprintf(fracBuf, sizeof(fracBuf), "%d / %d", done, total);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
printCentered(timeBuf, 0, 2);
display.setTextSize(1);
display.setCursor(0, 20); display.print(fracBuf);
if (paused) { display.setCursor(80, 20); display.print("PAUSA"); }
display.setCursor(0, 32);
display.print("OK:"); display.print(done);
display.print(" Falt:"); display.print(total - done);
if (warn20) {
display.setCursor(0, 46);
display.print("!!! Faltan 20s !!!");
}
display.display();
}
void oledEnd(const String &reason, int done, int total) {
char repBuf[16];
snprintf(repBuf, sizeof(repBuf), "Reps: %d / %d", done, total);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("FIN : "); display.print(reason);
display.setCursor(0, 14); display.print(repBuf);
display.setCursor(0, 42); display.print("[4] Volver a inicio");
display.display();
}
// ════════════════════════════════════════════════════════════════════
// OLED – Pantallas del modo isométrico
// ════════════════════════════════════════════════════════════════════
void oledIsoTimeConfig(int sec) {
char buf[8];
snprintf(buf, sizeof(buf), "%d:%02d", sec / 60, sec % 60);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Isometrico - t(s):");
printCentered(buf, 18, 3);
display.setTextSize(1);
display.setCursor(0, 56); display.print("-10s");
display.setCursor(96, 56); display.print("+10s");
display.display();
}
void oledIsoSummary() {
char timeBuf[10];
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d",
isoTimeSec / 60, isoTimeSec % 60);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Isometrico");
display.setCursor(0, 12); display.print("Tiempo: "); display.print(timeBuf);
display.setCursor(0, 30); display.print("Doble clic [1]");
display.setCursor(0, 40); display.print("para iniciar");
display.display();
}
// Pantalla de terapia isométrica: muestra el tiempo restante (del HW timer),
// ciclo actual y estado de pausa.
void oledIsoTherapy(int countdown, int cycle, bool paused) {
char timeBuf[8], cycleBuf[12];
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d", countdown / 60, countdown % 60);
snprintf(cycleBuf, sizeof(cycleBuf), "Ciclo %d / 2", cycle);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
printCentered(timeBuf, 0, 2);
display.setTextSize(1);
display.setCursor(0, 22); display.print(cycleBuf);
if (paused) {
display.setCursor(70, 22); display.print("PAUSA");
}
display.setCursor(0, 36); display.print("[3] Terminar sesion");
display.display();
}
// Pregunta si el usuario requiere hacer el otro lado.
void oledIsoSideQuery() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("Ciclo 1 terminado!");
display.setCursor(0, 16); display.print("Hacer otro lado?");
display.setCursor(0, 36); display.print("[1] Si - otro lado");
display.setCursor(0, 48); display.print("[2] No - finalizar");
display.display();
}
// Resumen final de la sesión isométrica.
// cyclesDone = cuántos ciclos se completaron (0, 1 o 2).
// manual = true si la sesión fue interrumpida con BTN3.
void oledIsoEnd(int cyclesDone, bool manual) {
char buf[22];
snprintf(buf, sizeof(buf), "Ciclos: %d / 2", cyclesDone);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.print("FIN: Isometrico");
display.setCursor(0, 12); display.print(buf);
if (manual) {
display.setCursor(0, 24); display.print("(Interrupcion manual)");
}
display.setCursor(0, 42); display.print("[4] Volver a inicio");
display.display();
}
// ════════════════════════════════════════════════════════════════════
// HELPERS DE TEMPORIZACIÓN (isotónico)
// ════════════════════════════════════════════════════════════════════
long getElapsedMs() {
if (therapyPaused) {
return (long)(pauseBeginMs - therapyStartMs) - (long)accPausedMs;
}
return (long)(millis() - therapyStartMs) - (long)accPausedMs;
}
long getRemainingMs() {
long rem = (long)cfgTimeSec * 1000L - getElapsedMs();
return rem > 0L ? rem : 0L;
}
// ════════════════════════════════════════════════════════════════════
// TRANSICIONES DE ESTADO
// ════════════════════════════════════════════════════════════════════
// ── Isotónico ────────────────────────────────────────────────────────
void startTherapy() {
repsOK = 0;
therapyPaused = false;
accPausedMs = 0;
warningFired = false;
warnBlinking = false;
pendingClick = false;
therapyStartMs = millis();
sysState = S_THERAPY;
oledTherapy(getRemainingMs(), repsOK, cfgReps, false, false);
}
void endTherapy(const String &reason) {
endReason = reason;
sysState = S_END;
signalEnd();
segShowReps(repsOK);
oledEnd(endReason, repsOK, cfgReps);
}
// ── Isométrico ───────────────────────────────────────────────────────
// Arranca (o reinicia para ciclo 2) la terapia isométrica.
void startIsoTherapy() {
isoPaused = false;
isoTimerDone = false;
pendingClick = false;
isoTimerStart(isoTimeSec);
sysState = S_ISO_THERAPY;
oledIsoTherapy(isoTimeSec, isoCycle, false);
segShowTime(isoTimeSec);
}
// Cierra la sesión isométrica (natural o manual).
// cyclesDone = número de ciclos completados; manual = interrupción BTN3.
void endIsoSession(int cyclesDone, bool manual) {
isoTimerStop();
digitalWrite(LED1_PIN, LOW);
if (manual) signalEnd();
// si no es manual, signalCycleEnd ya fue llamado desde S_ISO_THERAPY
oledIsoEnd(cyclesDone, manual);
sysState = S_END;
}
// ── Retorno al inicio (común a ambos modos) ──────────────────────────
void returnToStart() {
// Detener y liberar timer isométrico si está activo
isoTimerDestroy();
isoPaused = false;
isoTimeSec = ISO_TIME_DEFAULT;
isoCycle = 1;
isoCountdown = 0;
isoTimerDone = false;
// Reset isotónico
cfgTimeSec = TIME_DEFAULT;
cfgReps = REPS_DEFAULT;
repsOK = 0;
pendingClick = false;
warnBlinking = false;
warningFired = false;
// Apagar periféricos
buzzStop();
rgbOff();
digitalWrite(LED1_PIN, LOW);
digitalWrite(LED2_PIN, LOW);
segBlank();
sysState = S_MODE_SELECT;
oledModeSelect();
}
// ════════════════════════════════════════════════════════════════════
// SETUP
// ════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
// OLED
Wire.begin(SDA_OLED, SCL_OLED);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("ERROR: OLED no encontrada");
while (true);
}
display.setTextColor(SSD1306_WHITE);
display.clearDisplay();
// NeoPixel RGB
rgb.begin();
rgb.setBrightness(80);
rgb.clear();
rgb.show();
// LEDs indicadores
pinMode(LED1_PIN, OUTPUT); digitalWrite(LED1_PIN, LOW);
pinMode(LED2_PIN, OUTPUT); digitalWrite(LED2_PIN, LOW);
// Buzzer
pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW);
// Switches
pinMode(SW1_PIN, INPUT_PULLUP);
pinMode(SW2_PIN, INPUT_PULLUP);
// Botones
initButtons();
// Display 7 segmentos
initSeg();
// Pantalla inicial
oledModeSelect();
}
// ════════════════════════════════════════════════════════════════════
// LOOP – Máquina de estados completamente no-bloqueante
// ════════════════════════════════════════════════════════════════════
unsigned long lastOledRefresh = 0;
void loop() {
// ── 1. Leer entradas ─────────────────────────────────────────────
readButtons();
bool sw1Running = (digitalRead(SW1_PIN) == HIGH); // HIGH → activo
bool sw2Time = (digitalRead(SW2_PIN) == HIGH); // HIGH → mostrar tiempo (iso.)
// ── 2. Tareas de fondo (no-bloqueantes) ──────────────────────────
muxSeg();
buzzUpdate();
rgbUpdate();
if (sysState == S_THERAPY) warnUpdate();
// ── 3. BTN4: retorno al inicio (habilitado fuera de S_MODE_SELECT) ──
if (sysState != S_MODE_SELECT && btns[3].fired) {
returnToStart();
return;
}
// ── 4. Máquina de estados ─────────────────────────────────────────
switch (sysState) {
// ════════════════════════════════════════════════════════════════
// SELECCIÓN DE MODO
// ════════════════════════════════════════════════════════════════
case S_MODE_SELECT:
if (btns[0].fired) { // BTN1 → Isotónico
modeIsotonic = true;
cfgTimeSec = TIME_DEFAULT;
cfgReps = REPS_DEFAULT;
sysState = S_TIME_CONFIG;
oledTimeConfig(cfgTimeSec);
}
if (btns[1].fired) { // BTN2 → Isométrico
modeIsotonic = false;
isoTimeSec = ISO_TIME_DEFAULT;
isoCycle = 1;
sysState = S_ISO_TIME_CONFIG;
oledIsoTimeConfig(isoTimeSec);
}
break;
// ════════════════════════════════════════════════════════════════
// ISOTÓNICO – CONFIGURACIÓN
// ════════════════════════════════════════════════════════════════
case S_TIME_CONFIG: {
bool changed = false;
if (btns[0].fired) {
if (cfgTimeSec <= TIME_MIN_SEC) { signalLimit(); }
else { cfgTimeSec -= TIME_STEP; changed = true; }
}
if (btns[1].fired) {
if (cfgTimeSec >= TIME_MAX_SEC) { signalLimit(); }
else { cfgTimeSec += TIME_STEP; changed = true; }
}
if (changed) oledTimeConfig(cfgTimeSec);
if (btns[2].fired) {
sysState = S_REPS_CONFIG;
oledRepsConfig(cfgReps);
}
break;
}
case S_REPS_CONFIG: {
bool changed = false;
if (btns[0].fired) {
if (cfgReps <= REPS_MIN) { signalLimit(); }
else { cfgReps--; changed = true; }
}
if (btns[1].fired) {
if (cfgReps >= REPS_MAX) { signalLimit(); }
else { cfgReps++; changed = true; }
}
if (changed) oledRepsConfig(cfgReps);
if (btns[2].fired) {
pendingClick = false;
sysState = S_SUMMARY;
oledSummary();
}
break;
}
case S_SUMMARY:
if (checkDoubleClick()) startTherapy();
break;
// ════════════════════════════════════════════════════════════════
// ISOTÓNICO – TERAPIA EN CURSO
// ════════════════════════════════════════════════════════════════
case S_THERAPY: {
bool pauseNow = !sw1Running;
if (pauseNow && !therapyPaused) {
therapyPaused = true;
pauseBeginMs = millis();
digitalWrite(LED1_PIN, HIGH);
} else if (!pauseNow && therapyPaused) {
accPausedMs += millis() - pauseBeginMs;
therapyPaused = false;
digitalWrite(LED1_PIN, LOW);
}
// LED2: encendido cuando SW2=0 (muestra reps, no tiempo)
digitalWrite(LED2_PIN, sw2Time ? LOW : HIGH);
if (!therapyPaused) {
long msLeft = getRemainingMs();
if (msLeft <= 0) { endTherapy("Tiempo"); break; }
if (!warningFired && msLeft <= (long)WARN_THRESHOLD_MS) {
startWarning();
}
if (checkDoubleClick()) {
repsOK++;
signalRep();
if (repsOK >= cfgReps) { endTherapy("Reps OK"); break; }
}
if (btns[2].fired) { endTherapy("Manual"); break; }
if (sw2Time) segShowTime((int)(msLeft / 1000));
else segShowReps(repsOK);
unsigned long now = millis();
if (now - lastOledRefresh >= OLED_REFRESH_MS) {
lastOledRefresh = now;
oledTherapy(msLeft, repsOK, cfgReps, warningFired, false);
}
} else {
unsigned long now = millis();
if (now - lastOledRefresh >= OLED_REFRESH_MS) {
lastOledRefresh = now;
oledTherapy(getRemainingMs(), repsOK, cfgReps, warningFired, true);
}
}
break;
}
// ════════════════════════════════════════════════════════════════
// ISOMÉTRICO – CONFIGURACIÓN DE TIEMPO
// ════════════════════════════════════════════════════════════════
case S_ISO_TIME_CONFIG: {
bool changed = false;
if (btns[0].fired) {
if (isoTimeSec <= ISO_TIME_MIN_SEC) { signalLimit(); }
else { isoTimeSec -= TIME_STEP; changed = true; }
}
if (btns[1].fired) {
if (isoTimeSec >= ISO_TIME_MAX_SEC) { signalLimit(); }
else { isoTimeSec += TIME_STEP; changed = true; }
}
if (changed) oledIsoTimeConfig(isoTimeSec);
if (btns[2].fired) {
pendingClick = false;
sysState = S_ISO_SUMMARY;
oledIsoSummary();
}
break;
}
// ════════════════════════════════════════════════════════════════
// ISOMÉTRICO – RESUMEN ANTES DE INICIAR
// ════════════════════════════════════════════════════════════════
case S_ISO_SUMMARY:
if (checkDoubleClick()) {
isoCycle = 1;
startIsoTherapy();
}
break;
// ════════════════════════════════════════════════════════════════
// ISOMÉTRICO – TERAPIA EN CURSO (usa hardware timer)
// ════════════════════════════════════════════════════════════════
case S_ISO_THERAPY: {
// ── Pausa / reanudación con SW1 ────────────────────────────────
bool pauseNow = !sw1Running;
if (pauseNow && !isoPaused) {
isoPaused = true;
isoTimerPause(); // detiene el contador HW
digitalWrite(LED1_PIN, HIGH);
} else if (!pauseNow && isoPaused) {
isoPaused = false;
isoTimerResume(); // reanuda desde 0 del contador HW
digitalWrite(LED1_PIN, LOW);
}
// ── BTN3 → interrumpir sesión ──────────────────────────────────
if (btns[2].fired) {
// cyclesDone = ciclos completados antes de la interrupción
endIsoSession(isoCycle - 1, true);
break;
}
if (!isoPaused) {
// Lee isoCountdown de forma segura (int de 32-bit es atómico en ESP32)
int remaining = isoCountdown;
// ── Fin de ciclo por timer ──────────────────────────────────
if (isoTimerDone) {
isoTimerDone = false;
isoTimerStop();
signalCycleEnd(); // Buzzer + RGB verde al finalizar ciclo
if (isoCycle >= 2) {
// Segundo ciclo completado → fin de sesión
oledIsoEnd(2, false);
sysState = S_END;
} else {
// Primer ciclo completado → preguntar por otro lado
sysState = S_ISO_SIDE_QUERY;
oledIsoSideQuery();
}
break;
}
// ── Actualizar display de 7 segmentos con el tiempo restante ──
segShowTime(remaining);
// ── Refresco OLED no-bloqueante ────────────────────────────
unsigned long now = millis();
if (now - lastOledRefresh >= OLED_REFRESH_MS) {
lastOledRefresh = now;
oledIsoTherapy(remaining, isoCycle, false);
}
} else {
// Modo pausa: refrescar OLED con indicador de PAUSA
unsigned long now = millis();
if (now - lastOledRefresh >= OLED_REFRESH_MS) {
lastOledRefresh = now;
oledIsoTherapy(isoCountdown, isoCycle, true);
}
}
break;
}
// ════════════════════════════════════════════════════════════════
// ISOMÉTRICO – ¿HACER OTRO LADO?
// ════════════════════════════════════════════════════════════════
case S_ISO_SIDE_QUERY:
if (btns[0].fired) { // BTN1 → Sí, hacer el otro lado
isoCycle = 2;
startIsoTherapy();
}
if (btns[1].fired) { // BTN2 → No, finalizar sesión
signalEnd();
oledIsoEnd(1, false);
sysState = S_END;
}
break;
// ════════════════════════════════════════════════════════════════
// FIN DE SESIÓN (ambos modos – espera BTN4 manejado arriba)
// ════════════════════════════════════════════════════════════════
case S_END:
break;
}
}Loading
ssd1306
ssd1306