#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <SD.h>
// Piny dla wyświetlacza
#define TFT_CS 5
#define TFT_DC 4
#define TFT_RST 22
#define TFT_MOSI 23
#define TFT_CLK 18
#define TFT_MISO 19
// Pin dla karty SD
#define SD_CS 15
// Inicjalizacja wyświetlacza
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// Piny dla potencjometrów
#define POT_GAZ 34
#define POT_SPRZEGLO 35
// Piny dla diod
#define LED_CZERWONY 25
#define LED_ZIELONY 26
// Pin dla przycisku START
#define PRZYCISK 27
// Piny dla przycisków menu (zamiast enkodera)
#define PRZYCISK_LEWO 32
#define PRZYCISK_PRAWO 33
#define PRZYCISK_OK 21
// Zmienne dla obsługi przycisków menu
bool poprzedniStanLewo = HIGH;
bool poprzedniStanPrawo = HIGH;
bool poprzedniStanOK = HIGH;
unsigned long czasOstatniegoNacisniecia = 0;
const unsigned long OPOZNIENIE_PRZYCISKOW = 200; // 200ms debounce
// Zmienne globalne
int wartoscGazu = 0;
int wartoscSprzegla = 0;
bool poprzedniStanPrzycisku = HIGH;
// Zmienne dla zarządzania zawodnikami
#define MAX_ZAWODNIKOW 10
#define MAX_DLUGOSC_NICKU 6
String aktualnyZawodnik = "GOSC";
String listaZawodnikow[MAX_ZAWODNIKOW];
int liczbaZawodnikow = 0;
String nazwaPlikuZawodnikow = "/zawodnicy.txt";
// Tablica dostępnych znaków (dodana spacja)
const char DOSTEPNE_ZNAKI[] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const int LICZBA_ZNAKOW = sizeof(DOSTEPNE_ZNAKI) - 1; // -1 bo string kończy się '\0'
int indeksZnaku = 1; // Start od 'A' (indeks 1, bo 0 to spacja)
// Zmienne do obsługi menu
int pozycjaMenu = 0;
int pozycjaKursora = 0;
char wybranyZnak = 'A';
String nowyNick = "";
// Poprzednie wartości do porównania - zapobiega migotaniu
int poprzedniGaz = 0;
int poprzednieSprzeglo = 0;
// Tryb pracy (0 - ekran powitalny, 1 - test w toku, 2 - ekran rekordów)
int tryb = 0;
// Nowe tryby pracy
#define TRYB_MENU_ZAWODNIKA 3
#define TRYB_WYBOR_ZAWODNIKA 4
#define TRYB_DODAJ_ZAWODNIKA 5
// Stan testu
int stanTestu = 0;
// Zmienne dla testu reakcji
unsigned long czasStartu = 0;
unsigned long czasReakcji = 0;
unsigned long czasFazy = 0;
int procentGazuWyniku = 0;
// Zmienne dla obsługi karty SD
bool sdDostepna = false;
int numerTestu = 0;
String nazwaPlikuWynikow = "/wyniki.csv";
// Najlepszy wynik
unsigned long najlepszyWynik = 9999; // Domyślnie duża wartość
// Zmienne dla ekranu rekordów
int pozycjaPrzewijaniaRekordow = 0;
const int WIERSZY_NA_EKRANIE = 8;
int maksymalnaLiczbaRekordow = 0;
// Dla wykrywania długiego naciśnięcia przycisku
unsigned long czasNacisnieciaPrzycisku = 0;
bool przyciskNacisniety = false;
const unsigned long CZAS_DLUGIEGO_NACISNIECIA = 2000; // 2 sekundy
// Wbudowany prosty obraz logo - flaga szachownicy
const uint16_t LOGO_KOLORY[] PROGMEM = {
0xFFFF, 0x0000 // Biały, Czarny
};
void setup() {
Serial.begin(115200);
// Inicjalizacja wyświetlacza
tft.begin();
tft.setRotation(1); // Poziomo
// Konfiguracja pinów
pinMode(LED_CZERWONY, OUTPUT);
pinMode(LED_ZIELONY, OUTPUT);
pinMode(PRZYCISK, INPUT_PULLUP);
// Konfiguracja przycisków menu
pinMode(PRZYCISK_LEWO, INPUT_PULLUP);
pinMode(PRZYCISK_PRAWO, INPUT_PULLUP);
pinMode(PRZYCISK_OK, INPUT_PULLUP);
// Początkowy stan diod
digitalWrite(LED_CZERWONY, LOW);
digitalWrite(LED_ZIELONY, LOW);
// Inicjalizacja karty SD
sdDostepna = inicjujKarteSD();
// Wczytaj listę zawodników
if (sdDostepna) {
wczytajZawodnikow();
}
// Odczytaj najlepszy wynik z karty SD
if (sdDostepna) {
najlepszyWynik = odczytajNajlepszyWynik();
}
// Wyświetl logo wbudowane w kod
wyswietlLogoStartowe();
delay(3000); // Wyświetl logo przez 3 sekundy
// Inicjalizacja generatora liczb pseudolosowych
randomSeed(analogRead(39));
// Rysuj ekran powitalny
rysujEkranPowitalny();
}
// Funkcja rysująca proste logo - flagę szachownicy wyścigową
void wyswietlLogoStartowe() {
tft.fillScreen(ILI9341_BLACK);
// Narysuj szachownicę (flagę wyścigową) w środku ekranu
int rozmiarKwadratu = 30;
int startX = (tft.width() - rozmiarKwadratu * 8) / 2;
int startY = (tft.height() - rozmiarKwadratu * 4) / 2;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 8; col++) {
uint16_t kolor = LOGO_KOLORY[(row + col) % 2];
tft.fillRect(startX + col * rozmiarKwadratu,
startY + row * rozmiarKwadratu,
rozmiarKwadratu, rozmiarKwadratu, kolor);
}
}
// Dodaj tekst nad szachownicą
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(startX + 10, startY - 40);
tft.print("TESTER REFLEKSU");
// Dodaj tekst pod szachownicą
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(startX + 35, startY + rozmiarKwadratu * 4 + 20);
tft.print("GooMoo#312 - 2025");
}
// Funkcja do zarządzania zawodnikami
void wczytajZawodnikow() {
if (!sdDostepna) {
return;
}
if (!SD.exists(nazwaPlikuZawodnikow)) {
File plik = SD.open(nazwaPlikuZawodnikow, FILE_WRITE);
if (plik) {
plik.close();
}
return;
}
File plik = SD.open(nazwaPlikuZawodnikow, FILE_READ);
if (plik) {
liczbaZawodnikow = 0;
while (plik.available() && liczbaZawodnikow < MAX_ZAWODNIKOW) {
String linia = plik.readStringUntil('\n');
linia.trim();
if (linia.length() > 0) {
listaZawodnikow[liczbaZawodnikow] = linia;
liczbaZawodnikow++;
}
}
plik.close();
}
}
void zapiszNowegoZawodnika(String nick) {
if (!sdDostepna || liczbaZawodnikow >= MAX_ZAWODNIKOW) {
return;
}
// Sprawdź czy zawodnik już istnieje
for (int i = 0; i < liczbaZawodnikow; i++) {
if (listaZawodnikow[i] == nick) {
return; // Zawodnik już istnieje
}
}
// Dodaj do listy w pamięci
listaZawodnikow[liczbaZawodnikow] = nick;
liczbaZawodnikow++;
// Zapisz do pliku
File plik = SD.open(nazwaPlikuZawodnikow, FILE_WRITE);
if (plik) {
for (int i = 0; i < liczbaZawodnikow; i++) {
plik.println(listaZawodnikow[i]);
}
plik.close();
}
}
// Funkcja wyświetlająca menu zawodnika
void wyswietlMenuZawodnika(int wybranaPozycja) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(50, 30);
tft.print("MENU ZAWODNIKA");
// Opcje menu
int startY = 80;
int spacing = 40;
tft.setTextSize(2);
// Wybierz Zawodnika
tft.setTextColor(wybranaPozycja == 0 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY);
tft.print("Wybierz Zawodnika");
// Dodaj Zawodnika
tft.setTextColor(wybranaPozycja == 1 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY + spacing);
tft.print("Dodaj Zawodnika");
// Kontynuuj jako GOŚĆ
tft.setTextColor(wybranaPozycja == 2 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY + 2*spacing);
tft.print("Kontynuuj jako GOSC");
// Powrót
tft.setTextColor(wybranaPozycja == 3 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY + 3*spacing);
tft.print("Powrot");
// Instrukcja
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(20, 220);
tft.print("LEWO/PRAWO: wybor | OK: zatwierdz");
tft.setCursor(20, 230);
tft.print("START: powrot do ekranu glownego");
}
// Funkcja wyświetlająca listę zawodników
void wyswietlListeZawodnikow(int wybranyIndeks) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(40, 30);
tft.print("WYBIERZ ZAWODNIKA");
int startY = 80;
int spacing = 30;
// Zawsze dodajemy "GOSC" jako pierwszą opcję
tft.setTextSize(2);
tft.setTextColor(wybranyIndeks == 0 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY);
tft.print("GOSC");
// Wyświetl zapisanych zawodników
for (int i = 0; i < liczbaZawodnikow; i++) {
tft.setTextColor(wybranyIndeks == i + 1 ? ILI9341_CYAN : ILI9341_WHITE);
tft.setCursor(60, startY + (i+1)*spacing);
tft.print(listaZawodnikow[i]);
}
// Instrukcje
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(20, 220);
tft.print("LEWO/PRAWO: wybor | OK: zatwierdz | START: powrot");
}
// Funkcja wyświetlająca interfejs dodawania zawodnika
void wyswietlDodawanieZawodnika(String aktualnyNick, int pozycjaKursora, char wybranyZnak) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(40, 30);
tft.print("DODAJ ZAWODNIKA");
// Wyświetl aktualnie wprowadzany nick
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(80, 80);
// Wyświetl wprowadzone znaki i aktualne podkreślenie
for (int i = 0; i < MAX_DLUGOSC_NICKU; i++) {
if (i < aktualnyNick.length()) {
if (i == pozycjaKursora) {
// Aktualnie edytowany znak
tft.setTextColor(ILI9341_CYAN);
tft.print(aktualnyNick[i]);
tft.setTextColor(ILI9341_WHITE);
} else {
// Już wprowadzone znaki
tft.print(aktualnyNick[i]);
}
} else if (i == pozycjaKursora) {
// Aktualna pozycja, ale znak jeszcze nie wprowadzony
tft.setTextColor(ILI9341_CYAN);
tft.print(wybranyZnak);
tft.setTextColor(ILI9341_WHITE);
} else {
// Puste miejsca
tft.print("_");
}
}
// Jeśli wypełniono wszystkie znaki, pokaż opcje
if (pozycjaKursora >= MAX_DLUGOSC_NICKU) {
tft.setTextSize(2);
tft.setCursor(80, 140);
// Podświetl wybraną opcję
if (wybranyZnak == 'Z') { // 'Z' reprezentuje "ZAPISZ"
tft.setTextColor(ILI9341_CYAN);
tft.print("ZAPISZ");
tft.setCursor(80, 170);
tft.setTextColor(ILI9341_WHITE);
tft.print("POWROT");
} else { // 'P' reprezentuje "POWROT"
tft.setTextColor(ILI9341_WHITE);
tft.print("ZAPISZ");
tft.setCursor(80, 170);
tft.setTextColor(ILI9341_CYAN);
tft.print("POWROT");
}
} else {
// Wyświetl dostępne znaki - pokazuj 5 znaków (2 przed, aktualny, 2 po)
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(20, 140);
tft.print("LEWO/PRAWO: zmiana znaku | OK: zatwierdz");
// Wyświetl podgląd dostępnych znaków
tft.setCursor(80, 160);
tft.setTextColor(ILI9341_WHITE);
for (int i = -2; i <= 2; i++) {
int idx = (indeksZnaku + i + LICZBA_ZNAKOW) % LICZBA_ZNAKOW;
if (i == 0) {
tft.setTextColor(ILI9341_CYAN);
tft.print("[");
tft.print(DOSTEPNE_ZNAKI[idx]);
tft.print("]");
tft.setTextColor(ILI9341_WHITE);
} else {
tft.print(" ");
tft.print(DOSTEPNE_ZNAKI[idx]);
tft.print(" ");
}
}
}
// Instrukcje
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(20, 220);
tft.print("OK: zatwierdz | START: powrot do menu");
}
// Funkcja wyświetlająca ekran rekordów
void wyswietlEkranRekordow() {
tft.fillScreen(ILI9341_BLACK);
// Nagłówek
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(90, 10);
tft.print("REKORDY");
// Informacja o sterowaniu
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(10, 30);
tft.print("GAZ: przewijanie w dol | SPRZEGLO: w gore");
// Informacja o usuwaniu
tft.setTextColor(ILI9341_RED);
tft.setCursor(10, 230);
tft.print("Przytrzymaj START przez 2s aby usunac rekordy");
// Odczytaj i wyświetl rekordy
odczytajIWyswietlRekordy();
}
// Funkcja odczytująca i wyświetlająca rekordy z karty SD
void odczytajIWyswietlRekordy() {
if (!sdDostepna) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.setCursor(40, 100);
tft.print("Brak karty SD!");
return;
}
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_READ);
if (!plikWynikow) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.setCursor(40, 100);
tft.print("Nie mozna otworzyc pliku!");
return;
}
// Pomiń nagłówek i przejdź do odpowiedniej pozycji
int licznikWierszy = 0;
String linia = "";
// Przejdź do pierwszego wiersza danych (pomijając nagłówek)
if (plikWynikow.available()) {
while (plikWynikow.available()) {
char c = plikWynikow.read();
if (c == '\n') {
licznikWierszy++;
linia = "";
break;
}
linia += c;
}
}
// Liczenie całkowitej liczby rekordów
maksymalnaLiczbaRekordow = 0;
while (plikWynikow.available()) {
char c = plikWynikow.read();
if (c == '\n') {
maksymalnaLiczbaRekordow++;
}
}
// Zresetuj pozycję pliku
plikWynikow.seek(0);
// Pomiń nagłówek
linia = "";
while (plikWynikow.available()) {
char c = plikWynikow.read();
if (c == '\n') {
break;
}
}
// Przewiń do żądanej pozycji
licznikWierszy = 0;
while (licznikWierszy < pozycjaPrzewijaniaRekordow && plikWynikow.available()) {
char c = plikWynikow.read();
if (c == '\n') {
licznikWierszy++;
}
}
// Wyświetl rekordy
tft.setTextSize(1);
int y = 50;
int wyswietloneRekordy = 0;
while (plikWynikow.available() && wyswietloneRekordy < WIERSZY_NA_EKRANIE) {
linia = "";
bool koniecLinii = false;
while (plikWynikow.available() && !koniecLinii) {
char c = plikWynikow.read();
if (c == '\n') {
koniecLinii = true;
} else {
linia += c;
}
}
if (linia.length() > 0) {
// Parsuj linię
int indeksNr = 0;
int indeksData = linia.indexOf(',', indeksNr);
int indeksCzas = linia.indexOf(',', indeksData + 1);
int indeksGaz = linia.indexOf(',', indeksCzas + 1);
int indeksStatus = linia.indexOf(',', indeksGaz + 1);
int indeksZawodnik = linia.indexOf(',', indeksStatus + 1);
if (indeksNr >= 0 && indeksData >= 0 && indeksCzas >= 0 && indeksGaz >= 0 && indeksStatus >= 0) {
String nr = linia.substring(indeksNr, indeksData);
String czas = linia.substring(indeksCzas + 1, indeksGaz);
String gaz = linia.substring(indeksGaz + 1, indeksStatus);
String status = linia.substring(indeksStatus + 1);
String zawodnik = "GOSC";
// Sprawdź czy jest informacja o zawodniku
if (indeksZawodnik > 0 && indeksZawodnik < linia.length()) {
zawodnik = linia.substring(indeksStatus + 1, indeksZawodnik);
status = linia.substring(indeksStatus + 1, indeksZawodnik);
}
// Wyświetl tylko rekordy z prawidłowymi wynikami
if (status.startsWith("OK")) {
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(20, y);
tft.print("#");
tft.print(nr);
tft.print(": ");
tft.print(czas);
tft.print(" ms | ");
tft.print(zawodnik);
tft.print(" | Gaz: ");
tft.print(gaz);
tft.print("%");
y += 20;
wyswietloneRekordy++;
}
}
}
if (koniecLinii && linia.length() == 0) {
break;
}
}
plikWynikow.close();
// Jeśli nie ma rekordów do wyświetlenia
if (wyswietloneRekordy == 0) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(70, 100);
tft.print("Brak rekordow");
}
// Informacja o przewijaniu
if (pozycjaPrzewijaniaRekordow > 0) {
tft.fillTriangle(160, 40, 150, 50, 170, 50, ILI9341_WHITE); // Strzałka w górę
}
if (pozycjaPrzewijaniaRekordow + WIERSZY_NA_EKRANIE < maksymalnaLiczbaRekordow) {
tft.fillTriangle(160, 210, 150, 200, 170, 200, ILI9341_WHITE); // Strzałka w dół
}
}
// Funkcja usuwająca wszystkie rekordy
void usunWszystkieRekordy() {
if (!sdDostepna) {
return;
}
// Usuń plik wyników
SD.remove(nazwaPlikuWynikow);
// Utwórz nowy plik z nagłówkiem
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_WRITE);
if (plikWynikow) {
plikWynikow.println("Nr,Data,Czas_ms,Gaz_%,Status,Zawodnik");
plikWynikow.close();
}
// Zresetuj zmienne
numerTestu = 0;
pozycjaPrzewijaniaRekordow = 0;
maksymalnaLiczbaRekordow = 0;
najlepszyWynik = 9999;
// Pokaż komunikat
tft.fillRect(50, 100, 220, 40, ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.setCursor(60, 110);
tft.print("Rekordy usuniete!");
delay(1500);
}
// Funkcja do odczytywania najlepszego wyniku z karty SD
unsigned long odczytajNajlepszyWynik() {
unsigned long najlepszy = 9999; // Domyślna wysoka wartość
if (!sdDostepna) {
return najlepszy;
}
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_READ);
if (!plikWynikow) {
Serial.println("Nie można otworzyć pliku wyników do odczytu");
return najlepszy;
}
// Pomiń pierwszy wiersz (nagłówek)
String linia = "";
bool pominieteNaglowki = false;
while (plikWynikow.available()) {
char c = plikWynikow.read();
if (c == '\n') {
// Nowa linia - analizuj poprzednią linię jeśli nie jest nagłówkiem
if (pominieteNaglowki && linia.length() > 0) {
// Format: Nr,Data,Czas_ms,Gaz_%,Status,Zawodnik
// Potrzebujemy trzeciej kolumny (Czas_ms)
int indexPierwszegoKominka = linia.indexOf(',');
if (indexPierwszegoKominka >= 0) {
int indexDrugiegoKominka = linia.indexOf(',', indexPierwszegoKominka + 1);
if (indexDrugiegoKominka >= 0) {
int indexTrzeciegoKominka = linia.indexOf(',', indexDrugiegoKominka + 1);
if (indexTrzeciegoKominka >= 0) {
// Wyciągnij czas reakcji
String czasString = linia.substring(indexDrugiegoKominka + 1, indexTrzeciegoKominka);
unsigned long czas = czasString.toInt();
// Sprawdź status (ostatnia kolumna)
int ostatniKominek = linia.lastIndexOf(',');
String status = linia.substring(indexTrzeciegoKominka + 1, ostatniKominek > 0 ? ostatniKominek : linia.length());
// Uwzględnij tylko pomyślne wyniki (zaczynające się od "OK")
if (status.startsWith("OK") && czas > 0 && czas < najlepszy) {
najlepszy = czas;
Serial.print("Znaleziono lepszy wynik: ");
Serial.println(najlepszy);
}
}
}
}
}
linia = ""; // Zresetuj linię
pominieteNaglowki = true; // Po pierwszej linii nagłówki są pominięte
} else {
linia += c; // Dodaj znak do aktualnej linii
}
}
plikWynikow.close();
Serial.print("Najlepszy wynik z karty SD: ");
Serial.println(najlepszy);
return najlepszy;
}
bool inicjujKarteSD() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(20, 100);
tft.print("Inicjalizacja karty SD...");
// Inicjalizacja karty SD
if (!SD.begin(SD_CS)) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_RED);
tft.setCursor(40, 100);
tft.print("Blad karty SD!");
tft.setCursor(40, 130);
tft.print("Kontynuuje bez zapisu.");
delay(2000);
return false;
}
// Sprawdź czy plik wyników istnieje, jeśli nie - utwórz go z nagłówkami
if (!SD.exists(nazwaPlikuWynikow)) {
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_WRITE);
if (plikWynikow) {
plikWynikow.println("Nr,Data,Czas_ms,Gaz_%,Status,Zawodnik");
plikWynikow.close();
Serial.println("Utworzono nowy plik wyników");
} else {
Serial.println("Nie można utworzyć pliku wyników");
}
}
// Znajdź ostatni numer testu
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_READ);
if (plikWynikow) {
String linia = "";
while (plikWynikow.available()) {
char znak = plikWynikow.read();
if (znak == '\n') {
numerTestu++;
linia = "";
} else {
linia += znak;
}
}
plikWynikow.close();
// Odejmij 1 (nagłówek) jeśli plik nie jest pusty
if (numerTestu > 0) {
numerTestu--;
}
Serial.print("Ostatni numer testu: ");
Serial.println(numerTestu);
} else {
Serial.println("Nie można otworzyć pliku wyników");
}
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_GREEN);
tft.setCursor(40, 100);
tft.print("Karta SD gotowa!");
delay(1000);
return true;
}
void zapiszWynik(unsigned long czas, int poziomGazu, String status) {
if (!sdDostepna) {
Serial.println("Karta SD niedostępna - nie zapisuję wyniku");
return;
}
numerTestu++;
File plikWynikow = SD.open(nazwaPlikuWynikow, FILE_APPEND);
if (plikWynikow) {
// Format: Nr,Data,Czas_ms,Gaz_%,Status,Zawodnik
String dane = String(numerTestu) + "," +
String(millis()) + "," +
String(czas) + "," +
String(poziomGazu) + "," +
status + "," +
aktualnyZawodnik;
plikWynikow.println(dane);
plikWynikow.close();
Serial.print("Zapisano wynik: ");
Serial.println(dane);
// Sprawdź, czy to nowy najlepszy wynik (tylko dla pomyślnych testów)
if (status.startsWith("OK") && czas > 0 && czas < najlepszyWynik) {
najlepszyWynik = czas;
Serial.print("Nowy najlepszy wynik: ");
Serial.println(najlepszyWynik);
}
} else {
Serial.println("Błąd otwierania pliku do zapisu wyniku");
}
}
// Funkcja obsługi przycisków menu
void obslugaPrzyciskowMenu() {
// Sprawdź czy minął minimalny czas od ostatniego naciśnięcia
if (millis() - czasOstatniegoNacisniecia < OPOZNIENIE_PRZYCISKOW) {
return;
}
bool aktualnyStanLewo = digitalRead(PRZYCISK_LEWO);
bool aktualnyStanPrawo = digitalRead(PRZYCISK_PRAWO);
bool aktualnyStanOK = digitalRead(PRZYCISK_OK);
// Obsługa przycisku LEWO
if (aktualnyStanLewo == LOW && poprzedniStanLewo == HIGH) {
czasOstatniegoNacisniecia = millis();
if (tryb == TRYB_MENU_ZAWODNIKA) {
// Przesunięcie w górę menu
pozycjaMenu = (pozycjaMenu > 0) ? pozycjaMenu - 1 : 3;
wyswietlMenuZawodnika(pozycjaMenu);
}
else if (tryb == TRYB_WYBOR_ZAWODNIKA) {
// Przesunięcie w górę listy zawodników
int maxIndeks = liczbaZawodnikow;
pozycjaMenu = (pozycjaMenu > 0) ? pozycjaMenu - 1 : maxIndeks;
wyswietlListeZawodnikow(pozycjaMenu);
}
else if (tryb == TRYB_DODAJ_ZAWODNIKA) {
if (pozycjaKursora < MAX_DLUGOSC_NICKU) {
// Poprzedni znak w alfabecie
indeksZnaku = (indeksZnaku - 1 + LICZBA_ZNAKOW) % LICZBA_ZNAKOW;
wybranyZnak = DOSTEPNE_ZNAKI[indeksZnaku];
} else {
// Przełączanie między opcjami ZAPISZ/POWRÓT
wybranyZnak = (wybranyZnak == 'Z') ? 'P' : 'Z';
}
wyswietlDodawanieZawodnika(nowyNick, pozycjaKursora, wybranyZnak);
}
}
// Obsługa przycisku PRAWO
if (aktualnyStanPrawo == LOW && poprzedniStanPrawo == HIGH) {
czasOstatniegoNacisniecia = millis();
if (tryb == TRYB_MENU_ZAWODNIKA) {
// Przesunięcie w dół menu
pozycjaMenu = (pozycjaMenu < 3) ? pozycjaMenu + 1 : 0;
wyswietlMenuZawodnika(pozycjaMenu);
}
else if (tryb == TRYB_WYBOR_ZAWODNIKA) {
// Przesunięcie w dół listy zawodników
int maxIndeks = liczbaZawodnikow;
pozycjaMenu = (pozycjaMenu < maxIndeks) ? pozycjaMenu + 1 : 0;
wyswietlListeZawodnikow(pozycjaMenu);
}
else if (tryb == TRYB_DODAJ_ZAWODNIKA) {
if (pozycjaKursora < MAX_DLUGOSC_NICKU) {
// Następny znak w alfabecie
indeksZnaku = (indeksZnaku + 1) % LICZBA_ZNAKOW;
wybranyZnak = DOSTEPNE_ZNAKI[indeksZnaku];
} else {
// Przełączanie między opcjami ZAPISZ/POWRÓT
wybranyZnak = (wybranyZnak == 'Z') ? 'P' : 'Z';
}
wyswietlDodawanieZawodnika(nowyNick, pozycjaKursora, wybranyZnak);
}
}
// Obsługa przycisku OK
if (aktualnyStanOK == LOW && poprzedniStanOK == HIGH) {
czasOstatniegoNacisniecia = millis();
if (tryb == 0) {
// Z ekranu powitalnego przejdź do menu wyboru zawodnika
tryb = TRYB_MENU_ZAWODNIKA;
pozycjaMenu = 0;
wyswietlMenuZawodnika(pozycjaMenu);
}
else if (tryb == TRYB_MENU_ZAWODNIKA) {
// Obsługa opcji menu
switch (pozycjaMenu) {
case 0: // Wybierz Zawodnika
tryb = TRYB_WYBOR_ZAWODNIKA;
pozycjaMenu = 0;
wyswietlListeZawodnikow(pozycjaMenu);
break;
case 1: // Dodaj Zawodnika
tryb = TRYB_DODAJ_ZAWODNIKA;
pozycjaKursora = 0;
indeksZnaku = 1; // Start od 'A' (pomijamy spację na początku)
wybranyZnak = DOSTEPNE_ZNAKI[indeksZnaku];
nowyNick = "";
wyswietlDodawanieZawodnika(nowyNick, pozycjaKursora, wybranyZnak);
break;
case 2: // Kontynuuj jako GOŚĆ
aktualnyZawodnik = "GOSC";
tryb = 0;
rysujEkranPowitalny();
break;
case 3: // Powrót
tryb = 0;
rysujEkranPowitalny();
break;
}
}
else if (tryb == TRYB_WYBOR_ZAWODNIKA) {
// Wybierz zawodnika z listy
if (pozycjaMenu == 0) {
aktualnyZawodnik = "GOSC";
} else {
aktualnyZawodnik = listaZawodnikow[pozycjaMenu - 1];
}
tryb = 0;
rysujEkranPowitalny();
}
else if (tryb == TRYB_DODAJ_ZAWODNIKA) {
if (pozycjaKursora < MAX_DLUGOSC_NICKU) {
// Dodaj znak do nicku
if (nowyNick.length() <= pozycjaKursora) {
nowyNick += wybranyZnak;
} else {
// Zastąp istniejący znak
nowyNick.setCharAt(pozycjaKursora, wybranyZnak);
}
pozycjaKursora++;
if (pozycjaKursora >= MAX_DLUGOSC_NICKU) {
wybranyZnak = 'Z'; // Domyślnie wybierz "ZAPISZ"
} else {
indeksZnaku = 1; // Reset do 'A' dla następnego znaku
wybranyZnak = DOSTEPNE_ZNAKI[indeksZnaku];
}
wyswietlDodawanieZawodnika(nowyNick, pozycjaKursora, wybranyZnak);
} else {
// Obsługa opcji ZAPISZ/POWRÓT
if (wybranyZnak == 'Z') {
// Zapisz nowego zawodnika
zapiszNowegoZawodnika(nowyNick);
aktualnyZawodnik = nowyNick;
tryb = 0;
rysujEkranPowitalny();
} else {
// Powrót do menu
tryb = TRYB_MENU_ZAWODNIKA;
pozycjaMenu = 0;
wyswietlMenuZawodnika(pozycjaMenu);
}
}
}
}
// Zapamiętaj poprzednie stany
poprzedniStanLewo = aktualnyStanLewo;
poprzedniStanPrawo = aktualnyStanPrawo;
poprzedniStanOK = aktualnyStanOK;
}
void loop() {
// Obsługa przycisku START z wykrywaniem długiego naciśnięcia
bool aktualnyStanPrzycisku = digitalRead(PRZYCISK);
// Obsługa przycisków menu (zamiast enkodera)
obslugaPrzyciskowMenu();
// Wykrywanie naciśnięcia przycisku START
if (aktualnyStanPrzycisku == LOW && poprzedniStanPrzycisku == HIGH) {
przyciskNacisniety = true;
czasNacisnieciaPrzycisku = millis();
}
// Wykrywanie zwolnienia przycisku
else if (aktualnyStanPrzycisku == HIGH && przyciskNacisniety) {
przyciskNacisniety = false;
// Sprawdź czy to było długie naciśnięcie
if (millis() - czasNacisnieciaPrzycisku >= CZAS_DLUGIEGO_NACISNIECIA) {
// Obsługa długiego naciśnięcia
if (tryb == 0 || (tryb == 1 && (stanTestu == 4 || stanTestu == 5))) {
// Przejdź do ekranu rekordów
tryb = 2;
pozycjaPrzewijaniaRekordow = 0;
wyswietlEkranRekordow();
}
else if (tryb == 2) {
// Usuń wszystkie rekordy
usunWszystkieRekordy();
// Odśwież ekran rekordów
wyswietlEkranRekordow();
}
}
// Krótkie naciśnięcie
else {
// Powrót do ekranu głównego z dowolnego menu zawodnika
if (tryb == TRYB_MENU_ZAWODNIKA || tryb == TRYB_WYBOR_ZAWODNIKA || tryb == TRYB_DODAJ_ZAWODNIKA) {
tryb = 0;
rysujEkranPowitalny();
}
else if (tryb == 0) {
// Przejście do trybu testu
tryb = 1;
stanTestu = 0;
tft.fillScreen(ILI9341_BLACK);
// Wyświetl "PODJEDZ POD TASME" (ekran1)
tft.setTextSize(3);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(65, 80);
tft.print("PODJEDZ POD");
tft.setCursor(120, 120);
tft.print("TASME");
czasStartu = millis();
czasFazy = random(2000, 3000); // 2-3 sekundy
}
else if (tryb == 1 && (stanTestu == 4 || stanTestu == 5)) {
// Powrót do ekranu powitalnego
tryb = 0;
rysujEkranPowitalny();
}
else if (tryb == 2) {
// Powrót do ekranu powitalnego z ekranu rekordów
tryb = 0;
rysujEkranPowitalny();
}
}
}
poprzedniStanPrzycisku = aktualnyStanPrzycisku;
// Obsługa trybu testu
if (tryb == 1) {
// Odczyt z potencjometrów
wartoscGazu = analogRead(POT_GAZ);
wartoscSprzegla = analogRead(POT_SPRZEGLO);
// Konwersja na procenty (ESP32 ma 12-bitowe ADC)
int procentGazu = map(wartoscGazu, 0, 4095, 0, 100);
int procentSprzegla = map(wartoscSprzegla, 0, 4095, 0, 100);
// Obsługa stanów testu
unsigned long aktualnyMoment = millis();
if (stanTestu == 0) {
// "PODJEDZ POD TASME" - czekamy określony czas
if (aktualnyMoment - czasStartu >= czasFazy) {
stanTestu = 1;
tft.fillScreen(ILI9341_BLACK);
// Wyświetl "PRZYGOTUJ SIE - WCISNIJ SPRZEGLO" (ekran2)
tft.setTextSize(3);
tft.setTextColor(ILI9341_RED);
tft.setCursor(90, 60);
tft.print("PRZYGOTUJ");
tft.setCursor(145, 100);
tft.print("SIE");
tft.setTextSize(2);
tft.setCursor(75, 160);
tft.print("WCISNIJ SPRZEGLO");
// Zapal czerwoną diodę
digitalWrite(LED_CZERWONY, HIGH);
czasStartu = aktualnyMoment;
czasFazy = random(2000, 5000); // 2-5 sekund
}
}
else if (stanTestu == 1) {
// "PRZYGOTUJ SIE - WCISNIJ SPRZEGLO" - czekamy określony czas
if (aktualnyMoment - czasStartu >= czasFazy) {
// Sprawdź czy sprzęgło jest wciśnięte odpowiednio
if (procentSprzegla < 90) {
// Sprzęgło nie jest wystarczająco wciśnięte
stanTestu = 5;
digitalWrite(LED_CZERWONY, LOW);
digitalWrite(LED_ZIELONY, LOW);
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(3);
tft.setTextColor(ILI9341_RED);
tft.setCursor(90, 80);
tft.print("TASMA!!!");
tft.setTextSize(2);
tft.setCursor(30, 140);
tft.print("NIE WCISNALAS SPRZEGLA");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(35, 220);
tft.print("Nacisnij START aby powrocic");
// Zapisz wynik na karcie SD
zapiszWynik(0, procentGazu, "TASMA_SPRZEGLO");
} else {
// Przejście do kolejnego etapu - zielona dioda
stanTestu = 2;
tft.fillScreen(ILI9341_BLACK);
// Rysuj paski gazu i sprzęgła
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(20, 10);
tft.println("Test reakcji - CZEKAJ");
tft.setTextSize(1);
tft.setCursor(20, 50);
tft.println("Gaz:");
tft.setCursor(20, 120);
tft.print("Sprzeglo:");
tft.drawRect(20, 70, 280, 30, ILI9341_WHITE);
tft.drawRect(20, 140, 280, 30, ILI9341_WHITE);
// Zapal zieloną diodę, zgaś czerwoną
digitalWrite(LED_CZERWONY, LOW);
digitalWrite(LED_ZIELONY, HIGH);
czasStartu = aktualnyMoment;
czasFazy = random(2000, 6000); // 2-6 sekund
// Zainicjuj wartości pasków
poprzedniGaz = -1;
poprzednieSprzeglo = -1;
aktualizujWyswietlacz(procentGazu, procentSprzegla);
}
}
}
else if (stanTestu == 2) {
// Aktualizuj paski podczas świecenia zielonej diody
aktualizujWyswietlacz(procentGazu, procentSprzegla);
// Sprawdź czy sprzęgło zostało puszczone zbyt wcześnie
if (procentSprzegla < 80) {
// Taśma - puszczenie sprzęgła przed sygnałem
stanTestu = 5;
digitalWrite(LED_CZERWONY, LOW);
digitalWrite(LED_ZIELONY, LOW);
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(3);
tft.setTextColor(ILI9341_RED);
tft.setCursor(90, 80);
tft.print("TASMA!!!");
tft.setTextSize(2);
tft.setCursor(35, 140);
tft.print("ZA WCZESNIE - SKUP SIE");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(35, 220);
tft.print("Nacisnij START aby powrocic");
// Zapisz wynik na karcie SD
zapiszWynik(0, procentGazu, "TASMA_ZA_WCZESNIE");
}
// Sprawdź czy czas świecenia zielonej diody się skończył
if (aktualnyMoment - czasStartu >= czasFazy) {
stanTestu = 3;
// Zgaś zieloną diodę - sygnał do puszczenia sprzęgła
digitalWrite(LED_ZIELONY, LOW);
tft.fillRect(20, 10, 280, 30, ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.setCursor(20, 10);
tft.println("PUSC SPRZEGLO TERAZ!");
czasStartu = aktualnyMoment;
}
}
else if (stanTestu == 3) {
// Oczekiwanie na puszczenie sprzęgła
aktualizujWyswietlacz(procentGazu, procentSprzegla);
// Sprawdź czy sprzęgło zostało puszczone (poniżej 10%)
if (procentSprzegla < 10) {
// Reakcja - zapisz czas
czasReakcji = aktualnyMoment - czasStartu;
procentGazuWyniku = procentGazu;
stanTestu = 4;
// Wyświetl wynik (przesunięty w prawo i wyżej)
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(3);
tft.setTextColor(ILI9341_GREEN);
tft.setCursor(120, 20);
tft.print("WYNIK");
// Wyświetl zawodnika
tft.setTextSize(2);
tft.setTextColor(ILI9341_MAGENTA);
tft.setCursor(60, 55);
tft.print("Zawodnik: ");
tft.print(aktualnyZawodnik);
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(95, 85);
tft.print(czasReakcji);
tft.print(" ms");
// Wyświetl najlepszy wynik
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(40, 120);
tft.print("Najlepszy: ");
tft.print(najlepszyWynik);
tft.print(" ms");
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(60, 150);
tft.print("Poziom gazu: ");
tft.print(procentGazuWyniku);
tft.print("%");
// Informacja o zapisie na SD (jeśli dostępna)
if (sdDostepna) {
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(210, 20);
tft.print("SD: #");
tft.print(numerTestu + 1); // +1 bo zapis jeszcze nie nastąpił
}
// Dodaj komunikat jeśli gaz poniżej 75%
if (procentGazuWyniku < 75) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.setCursor(80, 175);
tft.print("Troche malo -");
tft.setTextSize(2);
tft.setTextColor(ILI9341_ORANGE);
tft.setCursor(50, 200);
tft.print("NAKREC TEGO GAZU!");
// Zapisz wynik na karcie SD
zapiszWynik(czasReakcji, procentGazuWyniku, "OK_MALO_GAZU");
} else {
// Zapisz wynik na karcie SD
zapiszWynik(czasReakcji, procentGazuWyniku, "OK");
}
// Przesuń instrukcję wyżej, aby była w pełni widoczna
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(35, 220);
tft.print("Nacisnij START aby powrocic");
}
// Jeśli test trwa za długo (ponad 2 sekundy), przerwij
if (aktualnyMoment - czasStartu > 2000) {
stanTestu = 5;
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(3);
tft.setTextColor(ILI9341_RED);
tft.setCursor(40, 100);
tft.print("BRAK REAKCJI!");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(70, 220);
tft.print("Nacisnij START aby powrocic");
// Zapisz wynik na karcie SD
zapiszWynik(2000, procentGazu, "BRAK_REAKCJI");
}
}
}
// Obsługa trybu ekranu rekordów
else if (tryb == 2) {
// Odczyt z potencjometrów dla przewijania
wartoscGazu = analogRead(POT_GAZ);
wartoscSprzegla = analogRead(POT_SPRZEGLO);
// Konwersja na procenty
int procentGazu = map(wartoscGazu, 0, 4095, 0, 100);
int procentSprzegla = map(wartoscSprzegla, 0, 4095, 0, 100);
// Przewijanie w dół (gaz > 90%)
static bool przewijanieDol = false;
if (procentGazu > 90) {
if (!przewijanieDol && pozycjaPrzewijaniaRekordow < maksymalnaLiczbaRekordow - WIERSZY_NA_EKRANIE) {
przewijanieDol = true;
pozycjaPrzewijaniaRekordow++;
wyswietlEkranRekordow();
delay(200); // Opóźnienie, aby zapobiec zbyt szybkiemu przewijaniu
}
} else {
przewijanieDol = false;
}
// Przewijanie w górę (sprzęgło < 10%)
static bool przewijanieGora = false;
if (procentSprzegla < 10) {
if (!przewijanieGora && pozycjaPrzewijaniaRekordow > 0) {
przewijanieGora = true;
pozycjaPrzewijaniaRekordow--;
wyswietlEkranRekordow();
delay(200); // Opóźnienie, aby zapobiec zbyt szybkiemu przewijaniu
}
} else {
przewijanieGora = false;
}
}
delay(10);
}
// Rysowanie ekranu powitalnego
void rysujEkranPowitalny() {
tft.fillScreen(ILI9341_BLACK);
// Tekst powitalny
tft.setTextSize(3);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(40, 40);
tft.print("TEST REAKCJI");
// Autor
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(90, 100);
tft.print("GooMoo#312");
// Wyświetl najlepszy wynik jeśli istnieje
if (najlepszyWynik < 9999) {
tft.setTextSize(2);
tft.setTextColor(ILI9341_GREEN);
tft.setCursor(60, 130);
tft.print("Rekord: ");
tft.print(najlepszyWynik);
tft.print(" ms");
}
// Aktualny zawodnik
tft.setTextSize(2);
tft.setTextColor(ILI9341_MAGENTA);
tft.setCursor(80, 160);
tft.print("Zawodnik: ");
tft.print(aktualnyZawodnik);
// Status karty SD
tft.setTextSize(1);
tft.setCursor(250, 10);
if (sdDostepna) {
tft.setTextColor(ILI9341_GREEN);
tft.print("SD OK");
} else {
tft.setTextColor(ILI9341_RED);
tft.print("Brak SD");
}
// Instrukcja
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(80, 190);
tft.print("Aby rozpoczac");
tft.setCursor(85, 220);
tft.print("nacisnij START");
// Instrukcja do menu zawodnika
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(20, 250);
tft.print("OK: menu zawodnika | START(przytrz.): rekordy");
}
// Aktualizacja wskaźników bez migotania
void aktualizujWyswietlacz(int procentGazu, int procentSprzegla) {
int szerokoscGazu = map(procentGazu, 0, 100, 0, 278);
int szerokoscSprzegla = map(procentSprzegla, 0, 100, 0, 278);
// Aktualizacja paska gazu tylko jeśli się zmienił
if (procentGazu != poprzedniGaz) {
if (szerokoscGazu > map(poprzedniGaz, 0, 100, 0, 278)) {
// Pasek się powiększa - dorysuj tylko nową część
int poprzedniaSzerokosc = map(poprzedniGaz, 0, 100, 0, 278);
tft.fillRect(21 + poprzedniaSzerokosc, 71,
szerokoscGazu - poprzedniaSzerokosc, 28, ILI9341_GREEN);
} else if (szerokoscGazu < map(poprzedniGaz, 0, 100, 0, 278)) {
// Pasek się zmniejsza - wyczyść nadmiarową część
tft.fillRect(21 + szerokoscGazu, 71,
278 - szerokoscGazu, 28, ILI9341_BLACK);
}
// Aktualizacja wartości procentowej
tft.fillRect(150, 80, 50, 10, ILI9341_BLACK);
tft.setCursor(150, 80);
tft.setTextColor(ILI9341_WHITE);
tft.print(procentGazu);
tft.print("%");
poprzedniGaz = procentGazu;
}
// Aktualizacja paska sprzęgła tylko jeśli się zmienił
if (procentSprzegla != poprzednieSprzeglo) {
if (szerokoscSprzegla > map(poprzednieSprzeglo, 0, 100, 0, 278)) {
// Pasek się powiększa - dorysuj tylko nową część
int poprzedniaSzerokosc = map(poprzednieSprzeglo, 0, 100, 0, 278);
tft.fillRect(21 + poprzedniaSzerokosc, 141,
szerokoscSprzegla - poprzedniaSzerokosc, 28, ILI9341_RED);
} else if (szerokoscSprzegla < map(poprzednieSprzeglo, 0, 100, 0, 278)) {
// Pasek się zmniejsza - wyczyść nadmiarową część
tft.fillRect(21 + szerokoscSprzegla, 141,
278 - szerokoscSprzegla, 28, ILI9341_BLACK);
}
// Aktualizacja wartości procentowej
tft.fillRect(150, 150, 50, 10, ILI9341_BLACK);
tft.setCursor(150, 150);
tft.setTextColor(ILI9341_WHITE);
tft.print(procentSprzegla);
tft.print("%");
poprzednieSprzeglo = procentSprzegla;
}
}