#include #include #include #include // ==== Pins ==== #define ESC_PIN 2 #define BUTTON_PIN 3 #define PULSE_PIN 6 #define SERVO_PIN 4 // OLED SPI #define OLED_CS 7 #define OLED_DC 21 #define OLED_RST 5 // SD #define SD_CS 20 #define SD_SCK 8 #define SD_MISO 9 #define SD_MOSI 10 // ==== Histogram ==== #define HISTOGRAM_FILE "/histogram.txt" #define MAX_SCORE 100 int histogram[MAX_SCORE + 1] = {0}; int lastGameScore = 0; // ==== Fan Control ==== int pulseWidth = 1000; const int pulseStartup = 1000; const int pulseIdleMin = 1100; const int pulseMax = 1500; const int pulseStep = 4; bool fanInitialized = false; // ==== Game Logic ==== volatile int score = 0; volatile unsigned long lastPulseTime = 0; bool gameActive = false; bool waitingForRestart = true; bool gameHasBeenPlayed = false; unsigned long gameStartTime = 0; unsigned long gameOverTime = 0; const unsigned long GAME_DURATION = 60000; const unsigned long GAME_OVER_DISPLAY = 5000; // ==== OLED ==== U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI u8g2(U8G2_R0, OLED_CS, OLED_DC, OLED_RST); // ==== Servo Logic ==== Servo servo; int servoAngle = 90; int servoTargetAngle = 90; int servoStepDirection = -1; bool servoMoving = false; unsigned long lastServoMoveTime = 0; const int servoDelay = 30; // ==== Interrupt ==== void IRAM_ATTR onPulse() { unsigned long now = millis(); if (now - lastPulseTime > 100) { score++; lastPulseTime = now; if (servoTargetAngle == 90) { servoTargetAngle = 80; servoStepDirection = -1; } else if (servoTargetAngle == 80 && servoStepDirection == -1) { servoTargetAngle = 70; } else if (servoTargetAngle == 70) { servoTargetAngle = 80; servoStepDirection = +1; } else if (servoTargetAngle == 80 && servoStepDirection == +1) { servoTargetAngle = 90; } servoMoving = true; } } // ==== Histogram ==== void loadHistogram() { File file = SD.open(HISTOGRAM_FILE, FILE_READ); if (file) { for (int i = 0; i <= MAX_SCORE && file.available(); i++) { histogram[i] = file.parseInt(); } file.close(); } } void saveHistogram() { File file = SD.open(HISTOGRAM_FILE, FILE_WRITE); if (file) { for (int i = 0; i <= MAX_SCORE; i++) { file.println(histogram[i]); } file.close(); } } // ==== Setup ==== void setup() { Serial.begin(115200); pinMode(ESC_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(PULSE_PIN, INPUT_PULLDOWN); attachInterrupt(digitalPinToInterrupt(PULSE_PIN), onPulse, RISING); pinMode(OLED_RST, OUTPUT); digitalWrite(OLED_RST, LOW); delay(10); digitalWrite(OLED_RST, HIGH); delay(10); u8g2.begin(); pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); digitalWrite(OLED_CS, HIGH); delay(100); SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); if (SD.begin(SD_CS)) { File f = SD.open("/test.txt", FILE_WRITE); if (f) { f.println("Game started."); f.close(); } loadHistogram(); } servo.attach(SERVO_PIN); servo.write(servoAngle); for (int i = 0; i < 250; i++) { digitalWrite(ESC_PIN, HIGH); delayMicroseconds(pulseStartup); digitalWrite(ESC_PIN, LOW); delayMicroseconds(20000 - pulseStartup); } pulseWidth = pulseIdleMin; fanInitialized = true; } // ==== Main Loop ==== void loop() { bool buttonPressed = (digitalRead(BUTTON_PIN) == LOW); if (fanInitialized) { if (buttonPressed) { pulseWidth = min(pulseWidth + pulseStep, pulseMax); } else { pulseWidth = max(pulseWidth - pulseStep, pulseIdleMin); } digitalWrite(ESC_PIN, HIGH); delayMicroseconds(pulseWidth); digitalWrite(ESC_PIN, LOW); delayMicroseconds(20000 - pulseWidth); } if (gameActive) { unsigned long elapsed = millis() - gameStartTime; unsigned long remaining = GAME_DURATION > elapsed ? GAME_DURATION - elapsed : 0; if (remaining == 0) { gameActive = false; gameHasBeenPlayed = true; gameOverTime = millis(); servoTargetAngle = 90; servoMoving = true; noInterrupts(); lastGameScore = score; interrupts(); if (lastGameScore >= 0 && lastGameScore <= MAX_SCORE) { histogram[lastGameScore]++; saveHistogram(); } } else { int currentScore; noInterrupts(); currentScore = score; interrupts(); displayGame(remaining / 1000, currentScore); } } else { unsigned long sinceGameOver = millis() - gameOverTime; if (!gameHasBeenPlayed) { displayIdleFirst(); if (buttonPressed) { noInterrupts(); score = 0; lastPulseTime = 0; interrupts(); gameActive = true; gameStartTime = millis(); waitingForRestart = false; } } else if (sinceGameOver < GAME_OVER_DISPLAY) { displayGameSummary(); // Show GAME OVER for 5 seconds } else { waitingForRestart = true; displayIdle(); if (buttonPressed) { noInterrupts(); score = 0; lastPulseTime = 0; interrupts(); gameActive = true; gameStartTime = millis(); waitingForRestart = false; } } } handleServoSmoothMove(); delay(10); } // ==== Display Functions ==== void displayGame(int secondsLeft, int score) { int totalGames = 0; for (int i = 0; i <= MAX_SCORE; i++) { totalGames += histogram[i]; } int rank = totalGames + 1; for (int i = 0; i < score; i++) { rank -= histogram[i]; } totalGames++; u8g2.clearBuffer(); u8g2.setFontDirection(1); u8g2.setFont(u8g2_font_logisoso32_tr); u8g2.setCursor(96, 0); u8g2.print(secondsLeft); u8g2.print("s"); u8g2.setCursor(54, 0); u8g2.print(score); u8g2.print("p"); u8g2.setCursor(26, 0); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print(rank); u8g2.print("/"); u8g2.setCursor(0, 10); u8g2.print(totalGames); u8g2.sendBuffer(); } void displayGameSummary() { int totalGames = 0; for (int i = 0; i <= MAX_SCORE; i++) { totalGames += histogram[i]; } int rank = totalGames; for (int i = 0; i < lastGameScore; i++) { rank -= histogram[i]; } u8g2.clearBuffer(); u8g2.setFontDirection(1); u8g2.setCursor(112, 10); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print("GAME"); u8g2.setCursor(93, 10); u8g2.print("OVER"); u8g2.setFont(u8g2_font_logisoso32_tr); u8g2.setCursor(54, 0); u8g2.print(lastGameScore); u8g2.print("p"); u8g2.setCursor(26, 0); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print(rank); u8g2.print("/"); u8g2.setCursor(0, 10); u8g2.print(totalGames); u8g2.sendBuffer(); } void displayIdle() { int totalGames = 0; for (int i = 0; i <= MAX_SCORE; i++) { totalGames += histogram[i]; } int rank = totalGames; for (int i = 0; i < lastGameScore; i++) { rank -= histogram[i]; } u8g2.clearBuffer(); u8g2.setFontDirection(1); u8g2.setCursor(112, 00); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print("Press"); u8g2.setCursor(93, 1); u8g2.print("to play"); u8g2.setFont(u8g2_font_logisoso32_tr); u8g2.setCursor(54, 0); u8g2.print(lastGameScore); u8g2.print("p"); u8g2.setCursor(26, 0); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print(rank); u8g2.print("/"); u8g2.setCursor(0, 10); u8g2.print(totalGames); u8g2.sendBuffer(); } void displayIdleFirst() { u8g2.clearBuffer(); u8g2.setFontDirection(1); u8g2.setCursor(112, 00); u8g2.setFont(u8g2_font_logisoso16_tr); u8g2.print("Press"); u8g2.setCursor(93, 1); u8g2.print("to play"); u8g2.sendBuffer(); } void handleServoSmoothMove() { if (!servoMoving) return; if (millis() - lastServoMoveTime >= servoDelay) { lastServoMoveTime = millis(); if (servoAngle < servoTargetAngle) { servoAngle++; servo.write(servoAngle); } else if (servoAngle > servoTargetAngle) { servoAngle--; servo.write(servoAngle); } else { servoMoving = false; } } }