#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <Adafruit_NeoPixel.h>
#include <Servo.h>
constexpr uint8_t PIN_STRIP = 5;
constexpr uint8_t PIN_WIN_LED = 7;
constexpr uint8_t PIN_BUZZER = 12;
constexpr uint8_t PIN_SERVO = 6;
constexpr uint8_t PIN_START_BTN = 10;
constexpr uint8_t PIN_STOP_BTN = 11;
constexpr uint8_t PIN_GO1 = 8;
constexpr uint8_t PIN_GO2 = 9;
constexpr uint8_t PIN_LED_GO1 = A1;
constexpr uint8_t PIN_LED_GO2 = A2;
constexpr uint8_t PIN_COM = A0;
constexpr uint8_t SERVO_CLOSED = 0;
constexpr uint8_t SERVO_OPEN = 180;
constexpr uint8_t NUM_LEDS_STRIP = 32;
constexpr uint16_t DEBOUNCE_MS = 300;
constexpr uint16_t SERVO_MOVE_MS = 2000;
constexpr uint32_t GAME_DURATION_SEC = 3600UL;
const uint8_t fontTimer[] PROGMEM = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,0,6,126,255,195,195,255,126,6,0,0,255,255,0,0,
6,243,251,219,219,223,206,6,219,219,219,219,255,126,6,15,31,24,24,255,255,6,
223,223,219,219,251,115,6,126,255,219,219,251,122,6,3,3,3,3,255,254,
6,118,255,219,219,255,118,6,78,223,219,219,255,126,2,102,102
};
enum class GameState : uint8_t {
IDLE,
COUNTDOWN,
RUNNING,
TIMEOUT,
WON
};
struct Game {
GameState state = GameState::IDLE;
uint32_t startMillis = 0;
uint32_t frozenSeconds = 0;
uint32_t lastBeepSecond = GAME_DURATION_SEC + 1;
uint32_t lastDisplayedSec = ~0UL;
uint32_t lastMinuteBeep = 0;
uint32_t lastBlinkMs = 0;
uint8_t blinkCounter = 0;
bool bothGO_active = false;
bool stopEnabled = false;
bool winLED_rainbow = false;
char timeBuf[6] = "00:00";
} game;
MD_Parola display(MD_MAX72XX::PAROLA_HW, 2, 4, 3, 4); // DIN, CLK, CS, nDevices
Adafruit_NeoPixel strip(NUM_LEDS_STRIP, PIN_STRIP, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel winLed(1, PIN_WIN_LED, NEO_GRB + NEO_KHZ800);
Servo servo;
inline bool elapsed(uint32_t from, uint32_t duration, uint32_t now) {
return (now - from) >= duration;
}
uint32_t secondsRemaining(uint32_t now) {
if (game.startMillis == 0) return GAME_DURATION_SEC;
uint32_t passed = (now - game.startMillis) / 1000UL;
return (passed >= GAME_DURATION_SEC) ? 0 : GAME_DURATION_SEC - passed;
}
void formatTime(uint32_t sec, char* buf) {
uint32_t m = sec / 60;
uint32_t s = sec % 60;
buf[0] = '0' + m / 10;
buf[1] = '0' + m % 10;
buf[2] = ':';
buf[3] = '0' + s / 10;
buf[4] = '0' + s % 10;
buf[5] = '\0';
}
uint32_t cometSpeed(uint32_t remainingSec) {
if (remainingSec >= 3600) return 100;
if (remainingSec <= 60) return 30;
uint32_t progress = remainingSec - 60;
return 30 + (70UL * progress) / (3600 - 60);
}
uint32_t cometColor(uint32_t remainingSec) {
uint32_t min = remainingSec / 60;
if (min >= 51) return strip.Color( 0, 255, 0);
if (min >= 41) return strip.Color( 80, 255, 0);
if (min >= 31) return strip.Color(255, 255, 0);
if (min >= 21) return strip.Color(255, 160, 0);
if (min >= 11) return strip.Color(255, 80, 0);
return strip.Color(255, 0, 0);
}
// ANIMAZIONE COMETA
void drawComets(uint32_t color, uint32_t speed_ms, uint32_t now) {
static int8_t headLeft = 0, dirLeft = 1;
static int8_t headRight = 31, dirRight = -1;
static uint32_t lastUpdate = 0;
if (!elapsed(lastUpdate, speed_ms, now)) return;
lastUpdate = now;
strip.clear();
// Cometa sinistra (0-15)
for (uint8_t i = 0; i < 5; i++) {
int8_t px = headLeft - dirLeft * i;
if (px >= 0 && px <= 15) {
uint8_t bri = 255 - i * 48;
uint32_t c = strip.Color(
((color >> 16) & 255) * bri / 255,
((color >> 8) & 255) * bri / 255,
( color & 255) * bri / 255
);
strip.setPixelColor(px, c);
}
}
// Cometa destra (16-31)
for (uint8_t i = 0; i < 5; i++) {
int8_t px = headRight - dirRight * i;
if (px >= 16 && px <= 31) {
uint8_t bri = 255 - i * 48;
uint32_t c = strip.Color(
((color >> 16) & 255) * bri / 255,
((color >> 8) & 255) * bri / 255,
( color & 255) * bri / 255
);
strip.setPixelColor(px, c);
}
}
strip.show();
headLeft += dirLeft;
headRight += dirRight;
if (headLeft <= 0) { dirLeft = 1; headLeft = 0; }
if (headLeft >= 15) { dirLeft = -1; headLeft = 15; }
if (headRight <= 16) { dirRight = 1; headRight = 16; }
if (headRight >= 31) { dirRight = -1; headRight = 31; }
}
// SETUP
void setup() {
pinMode(PIN_START_BTN, INPUT_PULLUP);
pinMode(PIN_STOP_BTN, INPUT_PULLUP);
pinMode(PIN_GO1, INPUT_PULLUP);
pinMode(PIN_GO2, INPUT_PULLUP);
pinMode(PIN_COM, OUTPUT);
pinMode(PIN_LED_GO1, OUTPUT);
pinMode(PIN_LED_GO2, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_COM, LOW);
digitalWrite(PIN_LED_GO1, LOW);
digitalWrite(PIN_LED_GO2, LOW);
strip.begin();
strip.setBrightness(200);
strip.fill(strip.Color(100, 0, 0));
strip.show();
display.begin();
display.setFont(fontTimer);
display.setTextAlignment(PA_CENTER);
display.displayReset();
display.print(".");
servo.attach(PIN_SERVO);
servo.write(SERVO_CLOSED);
delay(SERVO_MOVE_MS);
strip.clear();
strip.show();
winLed.begin();
winLed.setBrightness(200);
winLed.clear();
winLed.show();
display.print("00:00");
}
// LOOP
void loop() {
uint32_t now = millis();
handleButtons(now);
handleGO(now);
updateGame(now);
}
// GESTIONE PULSANTI
void handleButtons(uint32_t now) {
static uint32_t lastStart = 0, lastStop = 0;
// START
if (!digitalRead(PIN_START_BTN) && elapsed(lastStart, DEBOUNCE_MS, now)) {
lastStart = now;
if (game.state == GameState::IDLE) {
game.state = GameState::COUNTDOWN;
game.blinkCounter = 0;
game.lastBlinkMs = now;
game.bothGO_active = false;
game.stopEnabled = false;
game.lastDisplayedSec = ~0UL;
strip.clear(); strip.show();
}
}
// STOP (solo se abilitato)
if (game.stopEnabled && !digitalRead(PIN_STOP_BTN) && elapsed(lastStop, DEBOUNCE_MS, now)) {
lastStop = now;
if (game.state == GameState::RUNNING) {
game.frozenSeconds = secondsRemaining(now);
formatTime(game.frozenSeconds, game.timeBuf);
game.state = GameState::WON;
game.winLED_rainbow = true;
digitalWrite(PIN_COM, LOW);
digitalWrite(PIN_LED_GO1, LOW);
digitalWrite(PIN_LED_GO2, LOW);
tone(PIN_BUZZER, 1250, 700);
servo.detach();
strip.clear();
strip.show();
}
}
}
// CONTROLLO ENTRAMBE LE CHIAVI (GO)
void handleGO(uint32_t now) {
static uint32_t lastCheck = 0;
static uint32_t stableSince = 0;
static bool ledsOn = false;
static uint32_t lastReminder = 0; // nuovo
static bool reminderActive = false; // nuovo
// ────────────────────────────────────────────────
// Fase 1: lampeggio "vittoria" quando stopEnabled
// ────────────────────────────────────────────────
if (game.state == GameState::RUNNING && game.stopEnabled) {
if (!elapsed(lastCheck, DEBOUNCE_MS, now)) return;
lastCheck = now;
digitalWrite(PIN_LED_GO1, ledsOn ? HIGH : LOW);
digitalWrite(PIN_LED_GO2, ledsOn ? HIGH : LOW);
ledsOn = !ledsOn;
return;
}
// ────────────────────────────────────────────────
// Fase 2: stato normale (prima di aprire il servo)
// ────────────────────────────────────────────────
if (game.state != GameState::RUNNING) return;
if (!elapsed(lastCheck, 50, now)) return; // controlliamo spesso lo stato reale
lastCheck = now;
bool go1_pressed = !digitalRead(PIN_GO1);
bool go2_pressed = !digitalRead(PIN_GO2);
digitalWrite(PIN_LED_GO1, go1_pressed ? HIGH : LOW);
digitalWrite(PIN_LED_GO2, go2_pressed ? HIGH : LOW);
bool needReminder = !go1_pressed || !go2_pressed;
if (needReminder) {
if (elapsed(lastReminder, 5000, now)) {
lastReminder = now;
reminderActive = true;
if (!go1_pressed) digitalWrite(PIN_LED_GO1, HIGH);
if (!go2_pressed) digitalWrite(PIN_LED_GO2, HIGH);
}
if (reminderActive && elapsed(lastReminder, 120, now)) {
reminderActive = false;
digitalWrite(PIN_LED_GO1, go1_pressed ? HIGH : LOW);
digitalWrite(PIN_LED_GO2, go2_pressed ? HIGH : LOW);
}
} else {
lastReminder = 0;
reminderActive = false;
}
bool bothPressed = go1_pressed && go2_pressed;
if (bothPressed) {
if (stableSince == 0) stableSince = now;
if (elapsed(stableSince, DEBOUNCE_MS, now)) {
if (!game.bothGO_active && !game.stopEnabled) {
noTone(PIN_BUZZER);
servo.attach(PIN_SERVO);
servo.write(SERVO_OPEN);
game.stopEnabled = true;
game.winLED_rainbow = true;
tone(PIN_BUZZER, 1650, 140);
}
game.bothGO_active = true;
}
} else {
stableSince = 0;
}
}
// AGGIORNAMENTO STATO
void updateGame(uint32_t now) {
switch (game.state) {
case GameState::IDLE:
drawComets(strip.Color(30,30,220), 110, now);
break;
case GameState::COUNTDOWN:
drawComets(strip.Color(220,20,0), 55, now);
if (elapsed(game.lastBlinkMs, 500, now)) {
game.lastBlinkMs = now;
if (game.blinkCounter & 1) {
display.print(" : ");
strip.clear();
} else {
display.print("60:00");
tone(PIN_BUZZER, 980, 35);
}
strip.show();
if (++game.blinkCounter >= 7) {
tone(PIN_BUZZER, 1550, 120);
game.state = GameState::RUNNING;
game.startMillis = now;
game.lastBeepSecond = GAME_DURATION_SEC + 1;
game.lastDisplayedSec = ~0UL;
digitalWrite(PIN_COM, HIGH);
}
}
break;
case GameState::RUNNING: {
uint32_t secLeft = secondsRemaining(now);
drawComets(cometColor(secLeft), cometSpeed(secLeft), now);
if (secLeft == 0) {
game.state = GameState::TIMEOUT;
digitalWrite(PIN_COM, LOW);
tone(PIN_BUZZER, 380, 1400);
display.print("00:00");
break;
}
if (secLeft != game.lastDisplayedSec) {
formatTime(secLeft, game.timeBuf);
display.print(game.timeBuf);
game.lastDisplayedSec = secLeft;
if (secLeft <= 60 && secLeft != game.lastBeepSecond) {
game.lastBeepSecond = secLeft;
tone(PIN_BUZZER, 520, 35);
}
uint32_t minLeft = secLeft / 60;
if (minLeft != game.lastMinuteBeep && secLeft <= 600) {
game.lastMinuteBeep = minLeft;
tone(PIN_BUZZER, 420, 12);
}
}
break;
}
case GameState::WON: {
uint8_t phase = (now / 25) % 256;
drawComets(strip.ColorHSV(phase * 256), 110, now);
display.print((now % 1000 < 500) ? game.timeBuf : " ");
break;
}
case GameState::TIMEOUT:
for (int b = 255; b >= 0; b -= 3) {
strip.fill(strip.Color(b, 0, 0));
strip.show();
delay(4);
}
delay(220);
for (int b = 0; b <= 255; b += 3) {
strip.fill(strip.Color(b, 0, 0));
strip.show();
delay(4);
}
delay(220);
break;
}
if (game.winLED_rainbow) {
uint8_t hue = (now / 25) % 256;
winLed.setPixelColor(0, winLed.ColorHSV(hue * 256));
winLed.show();
}
}
A0 Nano slaves
START
WIN
+5V 5A
GO1
GO2