// ---------------------------------------------------------------- //
// Projekt: Tester Czasu Reakcji Żużlowca v0.3 //
// Opis: Dodano zapis najlepszego wyniku, kalibrację sprzęgła, //
// dźwięki i statystyki (średnia, najlepszy czas). //
// ---------------------------------------------------------------- //
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Preferences.h> // Do zapisu danych w pamięci flash
// --- Konfiguracja sprzętu ---
const int POT_GAZ_PIN = 34;
const int POT_SPRZEGLO_PIN = 35;
const int START_BUTTON_PIN = 23;
const int LED_RED_PIN = 18;
const int LED_GREEN_PIN = 19;
const int LED_BLUE_PIN = 5;
const int BUZZER_PIN = 25; // Nowy pin dla buzzera
const int LCD_COLUMNS = 16;
const int LCD_ROWS = 2;
const int LCD_ADDRESS = 0x27;
// --- Ustawienia gry ---
const long RED_LIGHT_MIN_TIME = 2000;
const long RED_LIGHT_MAX_TIME = 4000;
const long GREEN_LIGHT_MIN_TIME = 3000;
const long GREEN_LIGHT_MAX_TIME = 6000;
const int NUM_RESULTS_FOR_AVG = 5; // Liczba ostatnich wyników do średniej
// --- Zmienne globalne ---
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);
Preferences preferences; // Obiekt do zarządzania pamięcią
// Maszyna stanów
enum State {
WELCOME,
CALIBRATE,
APPROACH_GATE,
GET_READY,
RED_LIGHT,
GREEN_LIGHT,
MEASURING,
SHOW_RESULT,
FALSE_START
};
State currentState = WELCOME;
// Zmienne kalibracyjne i progowe
int sprzegloMinValue = 4095;
int sprzegloMaxValue = 0;
int SPRZEGLO_THRESHOLD = 3000; // Wartość domyślna, zostanie nadpisana przez kalibrację
int calibrationStep = 0;
// Zmienne czasowe i pomiarowe
unsigned long stateChangeTime = 0;
unsigned long randomDelay = 0;
unsigned long startTime = 0;
long reactionTime = 0;
int gasAtReaction = 0;
// Zmienne statystyk
unsigned long bestTime = 99999; // Domyślnie bardzo wysoki czas
long lastResults[NUM_RESULTS_FOR_AVG] = {0};
int resultIndex = 0;
int resultCount = 0;
int resultScreenPage = 0;
unsigned long lastScreenSwitchTime = 0;
byte blockChar[8] = { B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111 };
// --- Funkcja setup() ---
void setup() {
Serial.begin(115200);
pinMode(START_BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_RED_PIN, OUTPUT);
pinMode(LED_GREEN_PIN, OUTPUT);
pinMode(LED_BLUE_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
lcd.init();
lcd.backlight();
lcd.createChar(0, blockChar);
randomSeed(analogRead(0));
// Inicjalizacja pamięci i odczyt najlepszego wyniku
preferences.begin("reakcja", false); // "reakcja" to nazwa naszej "przestrzeni" w pamięci
bestTime = preferences.getULong("bestTime", 99999); // Odczytaj "bestTime", jeśli nie ma, użyj 99999
}
// --- Główna pętla programu ---
void loop() {
switch (currentState) {
case WELCOME:
displayWelcomeScreen();
if (isButtonPressed()) {
changeState(CALIBRATE);
}
break;
case CALIBRATE:
handleCalibration();
break;
case APPROACH_GATE:
if (millis() - stateChangeTime > 3000) changeState(GET_READY);
break;
case GET_READY:
// Czekaj na wciśnięcie sprzęgła (wartość musi być większa niż próg)
if (analogRead(POT_SPRZEGLO_PIN) > SPRZEGLO_THRESHOLD + 200) {
// Jeśli sprzęgło wciśnięte, odczekaj 2 sekundy i przejdź dalej
if (millis() - stateChangeTime > 2000) changeState(RED_LIGHT);
}
break;
case RED_LIGHT:
displayPotentiometers();
if (analogRead(POT_SPRZEGLO_PIN) < SPRZEGLO_THRESHOLD) changeState(FALSE_START);
if (millis() - stateChangeTime > randomDelay) changeState(GREEN_LIGHT);
break;
case GREEN_LIGHT:
displayPotentiometers();
if (analogRead(POT_SPRZEGLO_PIN) < SPRZEGLO_THRESHOLD) changeState(FALSE_START);
if (millis() - stateChangeTime > randomDelay) changeState(MEASURING);
break;
case MEASURING:
displayPotentiometers();
if (analogRead(POT_SPRZEGLO_PIN) < SPRZEGLO_THRESHOLD) {
reactionTime = millis() - startTime;
gasAtReaction = analogRead(POT_GAZ_PIN);
changeState(SHOW_RESULT);
}
break;
case SHOW_RESULT:
// Przełączanie ekranów wyników co 4 sekundy
if (millis() - lastScreenSwitchTime > 4000) {
resultScreenPage = (resultScreenPage + 1) % 3; // 3 ekrany: 0, 1, 2
displayResults(resultScreenPage);
lastScreenSwitchTime = millis();
}
if (isButtonPressed()) changeState(WELCOME);
break;
case FALSE_START:
// Mruganie diodą i dźwięk syreny
if (millis() % 500 < 250) { setLedColor(HIGH, LOW, LOW); tone(BUZZER_PIN, 800, 100); }
else { setLedColor(LOW, LOW, LOW); noTone(BUZZER_PIN); }
if (isButtonPressed()) changeState(WELCOME);
break;
}
}
// --- Funkcja zmiany stanu ---
void changeState(State newState) {
currentState = newState;
stateChangeTime = millis();
lcd.clear();
setLedColor(LOW, LOW, LOW);
noTone(BUZZER_PIN);
switch (newState) {
case WELCOME:
// Ekran jest rysowany w pętli
break;
case CALIBRATE:
calibrationStep = 0;
centerText("KALIBRACJA S-GA", 0);
centerText("Wcisnij do oporu", 1);
break;
case APPROACH_GATE:
centerText("PODJEDZ POD", 0);
centerText("TASME", 1);
break;
case GET_READY:
centerText("PRZYGOTUJ SIE", 0);
centerText("wcisnij sprzeglo", 1);
break;
case RED_LIGHT:
randomDelay = random(RED_LIGHT_MIN_TIME, RED_LIGHT_MAX_TIME);
setLedColor(HIGH, LOW, LOW);
break;
case GREEN_LIGHT:
randomDelay = random(GREEN_LIGHT_MIN_TIME, GREEN_LIGHT_MAX_TIME);
setLedColor(LOW, HIGH, LOW);
break;
case MEASURING:
startTime = millis();
tone(BUZZER_PIN, 1200, 150); // Dźwięk "start"
break;
case SHOW_RESULT:
updateStatistics();
resultScreenPage = 0;
lastScreenSwitchTime = millis();
displayResults(resultScreenPage);
if (reactionTime > 0 && reactionTime < bestTime) { // Dodano > 0, żeby nie zapisywać błędnych wyników
bestTime = reactionTime;
preferences.putULong("bestTime", bestTime); // Zapisz nowy rekord!
playSuccessSound();
}
break;
case FALSE_START:
centerText("TASMA", 0);
centerText("ZA WCZESNIE", 1);
break;
}
}
// --- Funkcje pomocnicze ---
void handleCalibration() {
if (calibrationStep == 0) { // Krok 1: Wciśnij sprzęgło
int val = analogRead(POT_SPRZEGLO_PIN);
if (val > 3800) { // Uznajemy, że jest wciśnięte (wysoka wartość analogowa)
sprzegloMaxValue = val;
calibrationStep = 1;
lcd.clear();
centerText("KALIBRACJA S-GA", 0);
centerText("Zwolnij calkowicie", 1);
beep(500, 100);
}
} else if (calibrationStep == 1) { // Krok 2: Zwolnij sprzęgło
int val = analogRead(POT_SPRZEGLO_PIN);
if (val < 200) { // Uznajemy, że jest zwolnione (niska wartość analogowa)
sprzegloMinValue = val;
// Ustaw próg na 25% zakresu ruchu od pozycji zwolnionej
SPRZEGLO_THRESHOLD = sprzegloMinValue + (sprzegloMaxValue - sprzegloMinValue) * 0.25;
Serial.print("Kalibracja ukonczona. Prog: "); Serial.println(SPRZEGLO_THRESHOLD);
beep(800, 100);
delay(100);
beep(800, 100);
changeState(APPROACH_GATE);
}
}
}
void updateStatistics() {
lastResults[resultIndex] = reactionTime;
resultIndex = (resultIndex + 1) % NUM_RESULTS_FOR_AVG;
if (resultCount < NUM_RESULTS_FOR_AVG) {
resultCount++;
}
}
long calculateAverage() {
if (resultCount == 0) return 0;
long sum = 0;
for (int i = 0; i < resultCount; i++) {
sum += lastResults[i];
}
return sum / resultCount;
}
void displayWelcomeScreen() {
centerText("---GooMoo#312---", 0);
centerText("..TEST REAKCJI..", 1);
}
void displayResults(int page) {
lcd.clear();
if (page == 0) {
String timeText = "Czas: " + String(reactionTime) + "ms";
centerText(timeText, 0);
int gasPercentage = map(gasAtReaction, 0, 4095, 0, 100);
String gasText = "Gaz: " + String(gasPercentage) + "%";
centerText(gasText, 1);
} else if (page == 1) {
centerText("NAJLEPSZY CZAS", 0);
if (bestTime >= 99999) {
centerText("--ms", 1);
} else {
centerText(String(bestTime) + "ms", 1);
}
} else if (page == 2) {
centerText("SREDNIA (" + String(resultCount) + ")", 0);
long avg = calculateAverage();
centerText(String(avg) + "ms", 1);
}
}
void displayPotentiometers() {
int gazValue = analogRead(POT_GAZ_PIN);
int sprzegloValue = analogRead(POT_SPRZEGLO_PIN);
int gazBarLength = map(gazValue, 0, 4095, 0, 12);
int sprzegloBarLength = map(sprzegloValue, 0, 4095, 0, 12);
lcd.setCursor(0, 0);
lcd.print("Gaz-");
drawProgressBar(0, gazBarLength, 12);
lcd.setCursor(0, 1);
lcd.print("Sgo-");
drawProgressBar(1, sprzegloBarLength, 12);
}
void drawProgressBar(int row, int length, int totalLength) {
lcd.setCursor(4, row); // Start rysowania paska za etykietą
for (int i = 0; i < totalLength; i++) {
if (i < length) lcd.write(byte(0));
else lcd.print(" ");
}
}
void centerText(String text, int row) {
int startPos = (LCD_COLUMNS - text.length()) / 2;
lcd.setCursor(startPos, row);
lcd.print(text);
}
void setLedColor(int r, int g, int b) {
digitalWrite(LED_RED_PIN, r);
digitalWrite(LED_GREEN_PIN, g);
digitalWrite(LED_BLUE_PIN, b);
}
bool isButtonPressed() {
if (digitalRead(START_BUTTON_PIN) == LOW) {
delay(50); // Prosty debouncing
if (digitalRead(START_BUTTON_PIN) == LOW) {
beep(400, 50);
while(digitalRead(START_BUTTON_PIN) == LOW); // Czekaj na puszczenie przycisku
return true;
}
}
return false;
}
void beep(int freq, int duration) {
tone(BUZZER_PIN, freq, duration);
delay(duration);
}
void playSuccessSound() {
tone(BUZZER_PIN, 600, 100); delay(120);
tone(BUZZER_PIN, 800, 100); delay(120);
tone(BUZZER_PIN, 1000, 150); delay(150);
noTone(BUZZER_PIN);
}