// --- ESP32 + ILI9341 + 3 Hardware-Taster (WOKWI) ---
// Screen 1: Welcome + Fake-Button "Start Engine"
// Screen 2: "Menu Ready for Take Off" (per grünem Taster)
// ======================
// Vorab: Typen & Prototypen
// ======================
// ---- Screen-IDs (oben platzieren, damit der Arduino-Preprocessor keine Probleme macht) ----
enum ScreenId {
SCREEN_WELCOME = 0,
SCREEN_READY = 1,
SCREEN_MENU = 2,
SCREEN_STATUS = 3
};
// ---- Funktionsprototypen, die benutzerdefinierte Typen nutzen ----
void switchTo(ScreenId next);
void renderCurrentScreen();
void renderWelcomeScreen();
void renderReadyScreen();
void pollButtonsAndAct();
// ======================
// Includes & globale Config
// ======================
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ---- ILI9341 Pins ----
#define TFT_DC 4
#define TFT_CS 5
Adafruit_ILI9341 tft(TFT_CS, TFT_DC); // VSPI: SCK=18, MOSI=23, MISO=19
// ---- Button-Pins (ESP32 DevKit v1) ----
const int BTN_RED_PIN = 32; // links
const int BTN_GREEN_PIN = 33; // mitte
const int BTN_BLUE_PIN = 27; // rechts
// ---- Farb-Helfer ----
static inline uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
const uint16_t COLOR_ORANGE = RGB565(255,165, 0);
const uint16_t COLOR_DARKGREY = RGB565( 60, 60, 60);
const uint16_t COLOR_LIGHTGREY = RGB565(200,200,200);
const uint16_t COLOR_RED = RGB565(255,0,0);
// ---- Globaler Screen-State ----
ScreenId currentScreen = SCREEN_WELCOME;
ScreenId lastScreen = (ScreenId)(-1); // erzwingt Initial-Render
uint32_t screenStartMs = 0; // optional für Zeitsteuerung
// ---- TFT-Abmessungen ----
int16_t tftW = 0, tftH = 0;
// ---- Button-Zustände + optionale Entprellung ----
bool lastRed = HIGH; // wegen Pull-up: ungedrückt = HIGH
bool lastGreen = HIGH;
bool lastBlue = HIGH;
const uint16_t DEBOUNCE_MS = 20;
uint32_t lastChangeRed = 0;
uint32_t lastChangeGreen = 0;
uint32_t lastChangeBlue = 0;
// Werte für SCREEN_STATUS(Simulation)
int fuelLevel = 85; // %
int oilTemp = 72; // °C
int oilPress = 45; // psi
int hydrPress = 88; // %
// ======================
// UI-Helfer
// ======================
int16_t textWidthPx(const char* s, uint8_t textSize) {
// Default-GFX-Font: ~6 px pro Zeichen (5 + 1 spacing)
int len = 0;
for (const char* p = s; *p; ++p) len++;
return len * 6 * textSize;
}
void drawCenteredText(const char* s, int16_t centerX, int16_t y, uint16_t color, uint8_t textSize) {
int16_t w = textWidthPx(s, textSize);
int16_t x = centerX - w / 2;
tft.setCursor(x, y);
tft.setTextColor(color);
tft.setTextSize(textSize);
tft.print(s);
}
void drawFakeButton(const char* label, int16_t centerX, int16_t centerY,
int16_t btnW, int16_t btnH,
uint16_t outlineColor, uint16_t fillColor, uint16_t textColor,
uint8_t textSize = 2, uint8_t radius = 10) {
int16_t x = centerX - btnW / 2;
int16_t y = centerY - btnH / 2;
tft.fillRoundRect(x, y, btnW, btnH, radius, fillColor);
tft.drawRoundRect(x, y, btnW, btnH, radius, outlineColor);
int16_t labelW = textWidthPx(label, textSize);
int16_t labelX = centerX - labelW / 2;
int16_t labelY = centerY - (8 * textSize) / 2; // grobe Zeilenhöhe ~8px*size
tft.setCursor(labelX, labelY);
tft.setTextColor(textColor);
tft.setTextSize(textSize);
tft.print(label);
}
// --- Helper für die untere Button-Legende ---
void drawLegendButton(int16_t centerX, int16_t yBar, int16_t barH,
uint16_t color, const char* label) {
const int16_t bw = 60;
const int16_t bh = 22;
const int16_t r = 6;
int16_t x = centerX - bw / 2;
int16_t y = yBar + (barH - bh) / 2;
tft.fillRoundRect(x, y, bw, bh, r, color);
tft.drawRoundRect(x, y, bw, bh, r, COLOR_LIGHTGREY);
drawCenteredText(label ? label : "", centerX, y + 6, ILI9341_WHITE, 1);
}
// Zeichnet unten (am Rand) die farbigen Button-Hinweise: Rot (links), Grün (Mitte), Blau (rechts)
void drawBottomButtonLegend(const char* leftLabel, const char* midLabel, const char* rightLabel) {
const int16_t barH = 34;
const int16_t yBar = tftH - barH;
tft.fillRect(0, yBar, tftW, barH, RGB565(20,20,20)); // dunkle Leiste
// Positionsanker: links/mid/rechts
int16_t cxL = (tftW * 1) / 6;
int16_t cxM = (tftW * 1) / 2;
int16_t cxR = (tftW * 5) / 6;
drawLegendButton(cxL, yBar, barH, ILI9341_RED, leftLabel);
drawLegendButton(cxM, yBar, barH, ILI9341_GREEN, midLabel);
drawLegendButton(cxR, yBar, barH, ILI9341_BLUE, rightLabel);
}
// Zeichnet unten (am Rand) einen farbigen Button-Hinweise: Grün (Mitte)
void drawSingleBottomButtonLegend(const char* midLabel) {
const int16_t barH = 34;
const int16_t yBar = tftH - barH;
tft.fillRect(0, yBar, tftW, barH, RGB565(20,20,20)); // dunkle Leiste
// Positionsanker: links/mid/rechts
int16_t cxM = (tftW * 1) / 2;
drawLegendButton(cxM, yBar, barH, ILI9341_GREEN, midLabel);
}
// ======================
// Screens
// ======================
void renderWelcomeScreen() {
tft.fillScreen(ILI9341_BLACK);
int16_t cx = tftW / 2;
drawCenteredText("Welcome Pilot", cx, 30, ILI9341_WHITE, 3);
drawCenteredText("on Jet FA 18", cx, 70, ILI9341_YELLOW, 3);
// Fake-Button (nur Anzeige; Aktion via GRUEN-Button)
drawFakeButton("Start Engine", cx, 150, 240, 60, COLOR_ORANGE, COLOR_DARKGREY, ILI9341_WHITE, 2, 10);
// Hinweis
drawCenteredText("Press GREEN to start", cx, 210, COLOR_LIGHTGREY, 1);
// Legende unten
drawBottomButtonLegend("Back", "Start", "Info");
}
void renderReadyScreen() {
tft.fillScreen(ILI9341_BLACK);
int16_t cx = tftW / 2;
drawCenteredText("Menu", cx, 20, ILI9341_CYAN, 3);
drawCenteredText("Ready", cx, 50, ILI9341_GREEN, 3);
drawCenteredText("for Take Off", cx, 80, ILI9341_GREEN, 3);
drawCenteredText("System checks: OK", cx, 140, ILI9341_WHITE, 2);
drawCenteredText("Fuel: 100%", cx, 170, ILI9341_WHITE, 2);
drawCenteredText("Press RED to go back", cx, 210, COLOR_LIGHTGREY, 1);
// Legende: Rot=Back, Gruen=Continue (Platzhalter), Blau=Options
drawBottomButtonLegend("Back", "", "Info");
}
void renderMenuScreen() {
tft.fillScreen(ILI9341_BLACK);
int16_t cx = tftW / 2;
drawCenteredText("Info", cx, 20, ILI9341_CYAN, 3);
// Fake-Button (nur Anzeige; Aktion via GRUEN-Button)
drawFakeButton("Fuel", cx-120, 120, 80, 60, COLOR_ORANGE, COLOR_DARKGREY, ILI9341_WHITE, 2, 10);
drawFakeButton("Engine", cx, 120, 80, 60, COLOR_ORANGE, COLOR_DARKGREY, ILI9341_WHITE, 2, 10);
drawFakeButton("Status", cx+120, 120, 80, 60, COLOR_ORANGE, COLOR_DARKGREY, ILI9341_WHITE, 2, 10);
drawFakeButton("Stop", cx, 120+80, 80, 60, COLOR_RED, COLOR_DARKGREY, COLOR_RED, 2, 10);
}
void renderCurrentScreen() {
switch (currentScreen) {
case SCREEN_WELCOME: renderWelcomeScreen(); break;
case SCREEN_READY: renderReadyScreen(); break;
case SCREEN_STATUS: renderMenuScreen(); break;
default:
tft.fillScreen(ILI9341_BLACK);
drawCenteredText("Unknown Screen", tftW/2, tftH/2 - 8, ILI9341_RED, 2);
break;
}
}
void drawBar(int16_t x, int16_t y, int16_t w, int16_t h, int value, int maxVal,
uint16_t color, const char* label) {
// Rahmen
tft.drawRect(x, y, w, h, COLOR_LIGHTGREY);
// Füllung proportional
int fillW = map(value, 0, maxVal, 0, w);
tft.fillRect(x+1, y+1, fillW-2, h-2, color);
// Label
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
tft.setCursor(x, y - 12);
tft.print(label);
// Wert
tft.setCursor(x + w + 6, y + (h/2) - 4);
tft.print(value);
}
void renderStatusScreen() {
tft.fillScreen(ILI9341_BLACK);
int16_t cx = tftW / 2;
drawCenteredText("Engine Status", cx, 20, ILI9341_CYAN, 3);
int16_t startX = 20;
int16_t startY = 70;
int16_t barW = 180;
int16_t barH = 18;
int16_t gapY = 40;
drawBar(startX, startY, barW, barH, fuelLevel, 100, ILI9341_GREEN, "Fuel");
drawBar(startX, startY + gapY, barW, barH, oilTemp, 120, ILI9341_YELLOW, "Oil Temp");
drawBar(startX, startY + 2*gapY, barW, barH, oilPress, 100, ILI9341_BLUE, "Oil Press");
drawBar(startX, startY + 3*gapY, barW, barH, hydrPress, 100, COLOR_ORANGE, "Hydraulic");
drawBottomButtonLegend("Back", "", "");
}
// ======================
// Screenwechsel & Buttons
// ======================
void switchTo(ScreenId next) {
currentScreen = next;
screenStartMs = millis();
// Render erfolgt in loop() (nur bei Änderung)
}
bool buttonPressedEdge(int pin, bool &lastState, uint32_t &lastChangeTs) {
bool raw = digitalRead(pin);
uint32_t now = millis();
if (raw != lastState && (now - lastChangeTs) < DEBOUNCE_MS) {
return false; // innerhalb Entprellzeit ignorieren
}
if (raw != lastState) {
lastChangeTs = now;
bool wasHigh = lastState;
lastState = raw;
// Flanke: HIGH -> LOW (Press)
if (wasHigh == HIGH && raw == LOW) {
return true;
}
}
return false;
}
void pollButtonsAndAct() {
// ROT
if (buttonPressedEdge(BTN_RED_PIN, lastRed, lastChangeRed)) {
if (currentScreen == SCREEN_READY || currentScreen == SCREEN_MENU) {
switchTo(SCREEN_WELCOME); // Zurück
} else {
// optional: ignorieren oder später belegen
}
}
// GRUEN
if (buttonPressedEdge(BTN_GREEN_PIN, lastGreen, lastChangeGreen)) {
if (currentScreen == SCREEN_WELCOME) {
switchTo(SCREEN_READY); // Start -> Ready for Take Off
} else if (currentScreen == SCREEN_READY) {
// Platzhalter: Continue / nächster Screen
// switchTo(SCREEN_NEXT);
}
}
// BLAU
if (buttonPressedEdge(BTN_BLUE_PIN, lastBlue, lastChangeBlue)) {
Serial.println("Menu Button");
if (currentScreen == SCREEN_READY) {
switchTo(SCREEN_STATUS); // Menu -> Fuell, Engine, Status
// switchTo(SCREEN_OPTIONS);
} else {
// optional: Menü aus Welcome
//switchTo(SCREEN_READY);
}
}
}
// ======================
// Arduino-Setup/Loop
// ======================
void setup() {
// Display
tft.begin();
tft.setRotation(1); // Querformat
tftW = tft.width();
tftH = tft.height();
// Buttons
pinMode(BTN_RED_PIN, INPUT_PULLUP);
pinMode(BTN_GREEN_PIN, INPUT_PULLUP);
pinMode(BTN_BLUE_PIN, INPUT_PULLUP);
// Startscreen
switchTo(SCREEN_WELCOME);
Serial.begin(115200);
}
void loop() {
// Nur neu zeichnen, wenn sich der Screen geändert hat
if (currentScreen != lastScreen) {
renderCurrentScreen();
lastScreen = currentScreen;
}
// Buttons abfragen
pollButtonsAndAct();
// kleiner Loop-Sleep
delay(5);
}