/* 1-Minute Timer + Scoreboard (Clean rendering, fixed) Temporizador 1 minuto + Marcador (render limpio, corregido) Board / Placa: Seeed XIAO RP2040 Display / Pantalla: SSD1306 128x64 I2C @ 0x3C Libraries / Librerías: Wire, Adafruit_GFX, Adafruit_SSD1306 Serial: 115200 */ #include #include #include // ------------------------- Display / Pantalla ------------------------- #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define OLED_ADDR 0x3C Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // ------------------------- Fast IO fallback ------------------------- #ifndef digitalReadFast #define digitalReadFast digitalRead #endif #ifndef digitalWriteFast #define digitalWriteFast digitalWrite #endif #ifndef pinModeFast #define pinModeFast pinMode #endif // ------------------------- RGB LED (active LOW) / LED RGB (activo LOW) ------------------------- const uint8_t LED_R = 17; const uint8_t LED_G = 16; const uint8_t LED_B = 25; inline void ledOn(uint8_t pin) { digitalWriteFast(pin, LOW); } // active LOW inline void ledOff(uint8_t pin) { digitalWriteFast(pin, HIGH); } void ledsAllOff() { ledOff(LED_R); ledOff(LED_G); ledOff(LED_B); } // ------------------------- Touch pins / Pines táctiles ------------------------- const uint8_t TOUCH_PINS[6] = { 3, 4, 2, 27, 1, 26 }; const uint16_t THRESHOLD = 6; const uint16_t TOUCH_MAX_COUNT = 2000; const uint32_t TOUCH_LOCKOUT_MS = 180; uint32_t lastTouchTriggerMs[6] = {0,0,0,0,0,0}; uint16_t readTouchCount(uint8_t pin) { // Discharge / Descargar pinModeFast(pin, OUTPUT); digitalWriteFast(pin, LOW); delayMicroseconds(5); // Measure rise / Medir subida pinModeFast(pin, INPUT_PULLUP); uint16_t count = 0; while (digitalReadFast(pin) == LOW && count < TOUCH_MAX_COUNT) { count++; } return count; } bool touchPressed(uint8_t idx) { uint16_t v = readTouchCount(TOUCH_PINS[idx]); if (v <= THRESHOLD) return false; uint32_t now = millis(); if (now - lastTouchTriggerMs[idx] < TOUCH_LOCKOUT_MS) return false; lastTouchTriggerMs[idx] = now; return true; } // ------------------------- Timer logic / Lógica temporizador ------------------------- const uint32_t START_TIME_MS = 10UL * 1000UL; // 10 seconds / 10 segundos // const uint32_t START_TIME_MS = 1UL * 60UL * 1000UL; // 1 minute / 1 minuto uint32_t remainingMs = START_TIME_MS; bool isRunning = false; bool isGameOver = false; uint32_t lastUpdateMs = 0; // ------------------------- Scoreboard / Marcador ------------------------- uint16_t goalsHome = 0; // Local uint16_t goalsAway = 0; // Visitante // Button mapping (indices in TOUCH_PINS) // Button 3 -> index 3 (pin 27) -> Home // Button 4 -> index 4 (pin 1) -> Away const uint8_t BTN_HOME_SCORE = 3; const uint8_t BTN_AWAY_SCORE = 4; // ------------------------- Game Over strobe / Estrobo ------------------------- uint32_t strobeLastMs = 0; uint8_t strobeStep = 0; const uint32_t STROBE_STEP_MS = 90; // ------------------------- Render ------------------------- void renderMainScreen() { display.clearDisplay(); // IMPORTANT: force white text every frame / Forzar texto blanco cada frame display.setTextColor(SSD1306_WHITE); display.setTextWrap(false); // Title display.setTextSize(1); display.setCursor(0, 0); display.print("Timer + Score"); // Timer (fixed position) uint32_t totalSeconds = remainingMs / 1000UL; uint32_t minutes = totalSeconds / 60UL; uint32_t seconds = totalSeconds % 60UL; char timeBuf[8]; snprintf(timeBuf, sizeof(timeBuf), "%lu:%02lu", (unsigned long)minutes, (unsigned long)seconds); display.setTextSize(2); display.setCursor(28, 16); // fixed display.print(timeBuf); // Score (fixed position) char scoreBuf[12]; snprintf(scoreBuf, sizeof(scoreBuf), "%u-%u", goalsHome, goalsAway); display.setTextSize(2); display.setCursor(36, 36); // fixed display.print(scoreBuf); // Footer display.setTextSize(1); display.setCursor(0, 56); if (isRunning) display.print("B0:Pause B3:+L B4:+V"); else display.print("B0:Start B1:Reset"); display.display(); } void renderGameOverScreen() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextWrap(false); display.setTextSize(2); display.setCursor(10, 8); display.print("GAME OVER"); char scoreBuf[12]; snprintf(scoreBuf, sizeof(scoreBuf), "%u-%u", goalsHome, goalsAway); display.setTextSize(2); display.setCursor(36, 30); display.print(scoreBuf); display.setTextSize(1); display.setCursor(12, 56); display.print("Press BTN1 to reset"); display.display(); } // ------------------------- Setup ------------------------- void setup() { Serial.begin(115200); delay(80); // LEDs pinModeFast(LED_R, OUTPUT); pinModeFast(LED_G, OUTPUT); pinModeFast(LED_B, OUTPUT); ledsAllOff(); // Touch pins for (uint8_t i = 0; i < 6; i++) { pinModeFast(TOUCH_PINS[i], INPUT_PULLUP); } // OLED Wire.begin(); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println("SSD1306 init failed (addr 0x3C?) / Fallo init SSD1306"); while (1) { delay(10); } } // Optional: set contrast / opcional display.ssd1306_command(SSD1306_SETCONTRAST); display.ssd1306_command(0xFF); remainingMs = START_TIME_MS; isRunning = false; isGameOver = false; goalsHome = 0; goalsAway = 0; lastUpdateMs = millis(); Serial.println("Ready / Listo"); renderMainScreen(); } // ------------------------- Loop ------------------------- void loop() { uint32_t now = millis(); bool btn0 = touchPressed(0); bool btn1 = touchPressed(1); bool btnHome = touchPressed(BTN_HOME_SCORE); bool btnAway = touchPressed(BTN_AWAY_SCORE); // GAME OVER state if (isGameOver) { if (btn1) { isGameOver = false; isRunning = false; remainingMs = START_TIME_MS; goalsHome = 0; goalsAway = 0; ledsAllOff(); renderMainScreen(); } else { if (now - strobeLastMs >= STROBE_STEP_MS) { strobeLastMs = now; ledsAllOff(); if (strobeStep == 0) ledOn(LED_R); if (strobeStep == 1) ledOn(LED_G); if (strobeStep == 2) ledOn(LED_B); strobeStep = (strobeStep + 1) % 3; } } return; } // BTN0 toggle if (btn0) { isRunning = !isRunning; lastUpdateMs = now; renderMainScreen(); } // BTN1 reset only if paused if (btn1 && !isRunning) { remainingMs = START_TIME_MS; goalsHome = 0; goalsAway = 0; renderMainScreen(); } // Scoring only while running if (isRunning) { if (btnHome) { goalsHome++; renderMainScreen(); } if (btnAway) { goalsAway++; renderMainScreen(); } } // Timer update if (isRunning) { uint32_t dt = now - lastUpdateMs; lastUpdateMs = now; if (dt > 1000UL) dt = 1000UL; if (remainingMs > dt) remainingMs -= dt; else remainingMs = 0; static uint32_t lastRefresh = 0; if (now - lastRefresh >= 150) { lastRefresh = now; renderMainScreen(); } if (remainingMs == 0) { isRunning = false; isGameOver = true; strobeLastMs = now; strobeStep = 0; ledsAllOff(); renderGameOverScreen(); } } else { lastUpdateMs = now; } }