diff --git a/src/adsb_client.cpp b/src/adsb_client.cpp index fb7bf6a..21e77a9 100644 --- a/src/adsb_client.cpp +++ b/src/adsb_client.cpp @@ -68,6 +68,7 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm, filterAc["baro_rate"]= true; filterAc["gs"] = true; filterAc["track"] = true; + filterAc["squawk"] = true; JsonDocument doc; DeserializationError err = deserializeJson( @@ -113,6 +114,9 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm, a.groundSpeedKt = ac["gs"] | 0.0f; a.headingDeg = ac["track"] | 0.0f; + const char* squawk = ac["squawk"] | ""; + strncpy(a.squawk, squawk, sizeof(a.squawk) - 1); + a.lastSeenMs = millis(); a.valid = (a.lat != 0.0f || a.lon != 0.0f); diff --git a/src/aircraft.h b/src/aircraft.h index 0ad3ebe..62fee3a 100644 --- a/src/aircraft.h +++ b/src/aircraft.h @@ -5,6 +5,7 @@ struct Aircraft { char callsign[9] = {0}; char reg[9] = {0}; char typeCode[5] = {0}; + char squawk[5] = {0}; // 4-stelliger Transponder-Code, z.B. "7700" (Notfall) float lat = 0; float lon = 0; diff --git a/src/config.h b/src/config.h index 882b3fe..5e060a9 100644 --- a/src/config.h +++ b/src/config.h @@ -3,10 +3,8 @@ namespace Config { constexpr const char* IP_GEO_HOST = "ip-api.com"; - constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon"; + constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode"; - // Kein GPS-Modul am CYD angeschlossen - Platzhalter, falls spaeter eins - // per Erweiterungspins nachgeruestet wird. struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; }; constexpr GpsPinPair GPS_PIN_CANDIDATES[] = { {22, 27, "G22/G27"} @@ -25,16 +23,11 @@ namespace Config { constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f; - // Radius, innerhalb dessen die rueckseitige RGB-LED (gruen) blinkt, um auf - // ein nahes Flugzeug hinzuweisen (Ersatz fuer den fehlenden Lautsprecher - // beim CYD). constexpr float LED_ALERT_RADIUS_KM = 3.0f; constexpr uint32_t ALERT_RETRIGGER_COOLDOWN_MS = 30000; constexpr uint8_t MAX_TRACKED_AIRCRAFT = 40; - // Eigener Ordner, getrennt vom Cardputer-Projekt (das nutzt /adsb_radar) - - // so kann dieselbe SD-Karte in beiden Geraeten verwendet werden. constexpr const char* SD_ROOT_DIR = "/Flightradar_cyd"; constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv"; constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv"; @@ -43,13 +36,11 @@ namespace Config { constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt"; constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt"; - // microSD-Slot beim CYD (ESP32-2432S028): eigener SPI-Bus, Standard-VSPI-Pins. constexpr uint8_t SD_SPI_CS_PIN = 5; constexpr uint8_t SD_SPI_MOSI_PIN = 23; constexpr uint8_t SD_SPI_MISO_PIN = 19; constexpr uint8_t SD_SPI_CLK_PIN = 18; - // Touch-Controller (XPT2046), eigener SPI-Bus, getrennt von Display und SD. constexpr uint8_t TOUCH_CLK_PIN = 25; constexpr uint8_t TOUCH_CS_PIN = 33; constexpr uint8_t TOUCH_MOSI_PIN = 32; @@ -59,7 +50,6 @@ namespace Config { constexpr int16_t SCREEN_WIDTH = 240; constexpr int16_t SCREEN_HEIGHT = 320; - // Fuer spaetere Naeherungs-Alarme (Phase 4: Bildschirmrand blinkt statt LED) constexpr float ZONE_BLUE_KM = 25.0f; constexpr float ZONE_YELLOW_KM = 10.0f; constexpr float ZONE_AMBER_KM = 5.0f; @@ -67,4 +57,11 @@ namespace Config { constexpr uint16_t COLOR_LOW_ALT_THRESHOLD_FT = 10000; constexpr uint16_t COLOR_MID_ALT_THRESHOLD_FT = 30000; + + // Notfall-Squawk-Codes: 7500 (Entfuehrung), 7600 (Funkausfall), 7700 (allg. Notfall) + constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"}; + constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3; + + // Maximal gespeicherte WLAN-Netzwerke (z.B. Zuhause, Auto-Hotspot, Arbeit) + constexpr uint8_t MAX_WIFI_NETWORKS = 3; } \ No newline at end of file diff --git a/src/flight_logbook.cpp b/src/flight_logbook.cpp new file mode 100644 index 0000000..f56e6c3 --- /dev/null +++ b/src/flight_logbook.cpp @@ -0,0 +1,143 @@ +#include "flight_logbook.h" +#include "config.h" +#include "aircraft.h" +#include "aircraft_table.h" +#include "settings_store.h" +#include +#include +#include + +namespace FlightLogbook { + +namespace { + constexpr uint16_t MAX_SEEN = 400; + char seenHex[MAX_SEEN][7]; + uint16_t seenCount = 0; + char currentDateStr[11] = {0}; // "YYYY-MM-DD" + + bool computeDateStr(char* out, size_t outSize) { + time_t now = time(nullptr); + if (now < 8 * 3600 * 2) return false; + struct tm tmNow; + localtime_r(&now, &tmNow); + snprintf(out, outSize, "%04d-%02d-%02d", tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday); + return true; + } + + void logFilename(char* out, size_t outSize) { + snprintf(out, outSize, "%s/%s.csv", Config::SD_LOG_DIR, currentDateStr); + } + + bool alreadySeen(const char* hex) { + for (uint16_t i = 0; i < seenCount; i++) { + if (strcmp(seenHex[i], hex) == 0) return true; + } + return false; + } + + void markSeen(const char* hex) { + if (seenCount >= MAX_SEEN) return; + strncpy(seenHex[seenCount], hex, sizeof(seenHex[seenCount]) - 1); + seenHex[seenCount][sizeof(seenHex[seenCount]) - 1] = 0; + seenCount++; + } + + void loadSeenFromTodayFile() { + seenCount = 0; + char filename[64]; + logFilename(filename, sizeof(filename)); + if (!SD.exists(filename)) return; + + File f = SD.open(filename, FILE_READ); + if (!f) return; + + bool firstLine = true; + while (f.available() && seenCount < MAX_SEEN) { + String line = f.readStringUntil('\n'); + line.trim(); + if (line.length() == 0) continue; + if (firstLine) { firstLine = false; continue; } + + int firstComma = line.indexOf(','); + if (firstComma < 0) continue; + int secondComma = line.indexOf(',', firstComma + 1); + String hex = (secondComma < 0) ? line.substring(firstComma + 1) + : line.substring(firstComma + 1, secondComma); + hex.trim(); + if (hex.length() > 0) markSeen(hex.c_str()); + } + f.close(); + } + + void ensureCurrentDate() { + char today[11]; + if (!computeDateStr(today, sizeof(today))) return; + + if (strcmp(today, currentDateStr) != 0) { + strncpy(currentDateStr, today, sizeof(currentDateStr) - 1); + loadSeenFromTodayFile(); + } + } + + void writeLogLine(const Aircraft& a) { + char filename[64]; + logFilename(filename, sizeof(filename)); + + bool needsHeader = !SD.exists(filename); + + File f = SD.open(filename, FILE_APPEND); + if (!f) return; + + if (needsHeader) { + f.println("timestamp,hex,callsign,reg,type,distance_km,altitude_ft"); + } + + time_t now = time(nullptr); + struct tm tmNow; + localtime_r(&now, &tmNow); + char timestamp[20]; + snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d", + tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday, + tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec); + + f.printf("%s,%s,%s,%s,%s,%.1f,%d\n", + timestamp, + a.hex, + a.callsign[0] ? a.callsign : "", + a.reg[0] ? a.reg : "", + a.typeCode[0] ? a.typeCode : "", + a.distanceKm, + (int)a.altBaroFt); + f.close(); + } +} + +void init() { + ensureCurrentDate(); +} + +void update() { + if (!SettingsStore::flightLogbookEnabled()) return; + + ensureCurrentDate(); + if (currentDateStr[0] == 0) return; + + static Aircraft snapshot[Config::MAX_TRACKED_AIRCRAFT]; + uint8_t count = 0; + + AircraftTable::lock(); + Aircraft* table = AircraftTable::raw(); + for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { + if (table[i].valid) snapshot[count++] = table[i]; + } + AircraftTable::unlock(); + + for (uint8_t i = 0; i < count; i++) { + if (!snapshot[i].hex[0]) continue; + if (alreadySeen(snapshot[i].hex)) continue; + markSeen(snapshot[i].hex); + writeLogLine(snapshot[i]); + } +} + +} \ No newline at end of file diff --git a/src/flight_logbook.h b/src/flight_logbook.h new file mode 100644 index 0000000..439c321 --- /dev/null +++ b/src/flight_logbook.h @@ -0,0 +1,22 @@ +#pragma once +#include + +// SD-Karten-Flugbuch: protokolliert jedes NEU gesichtete Flugzeug (Zeit, +// Hex-Code, Rufzeichen, Kennzeichen, Typ, Distanz, Hoehe) in eine taegliche +// CSV-Datei auf der SD-Karte. Kann in den Einstellungen ("Flight Logbook") +// an-/ausgeschaltet werden. Ueberlebt einen Neustart am selben Tag, ohne +// bereits geloggte Flugzeuge erneut einzutragen (rekonstruiert die Liste +// beim Start aus der heutigen CSV-Datei). +namespace FlightLogbook { + + // Einmalig beim Boot aufrufen, NACHDEM die lokale Uhrzeit (UTC-Offset) + // bekannt ist - liest die heutige CSV-Datei (falls vorhanden), um bereits + // geloggte Flugzeuge nicht doppelt einzutragen. + void init(); + + // Periodisch aufrufen (z.B. nach jeder erfolgreichen ADS-B-Abfrage): + // prueft auf neue, bisher ungesehene Flugzeuge und schreibt sie ins + // heutige Logbuch. Kuemmert sich auch um den Tageswechsel (neue Datei, + // neue "gesehen"-Liste). + void update(); +} \ No newline at end of file diff --git a/src/led_alert.cpp b/src/led_alert.cpp index 3125b20..caa5fba 100644 --- a/src/led_alert.cpp +++ b/src/led_alert.cpp @@ -6,37 +6,58 @@ namespace { constexpr uint8_t PIN_RED = 4; constexpr uint8_t PIN_GREEN = 16; constexpr uint8_t PIN_BLUE = 17; - constexpr uint32_t BLINK_INTERVAL_MS = 400; + + constexpr uint32_t GREEN_BLINK_INTERVAL_MS = 400; + constexpr uint32_t RED_BLINK_INTERVAL_MS = 150; bool initialized = false; bool blinkState = false; uint32_t lastToggleMs = 0; + Mode lastMode = Mode::Off; + + void setAllOff() { + digitalWrite(PIN_RED, HIGH); + digitalWrite(PIN_GREEN, HIGH); + digitalWrite(PIN_BLUE, HIGH); + } } void begin() { pinMode(PIN_RED, OUTPUT); pinMode(PIN_GREEN, OUTPUT); pinMode(PIN_BLUE, OUTPUT); - digitalWrite(PIN_RED, HIGH); // aus (LED ist active-low) - digitalWrite(PIN_GREEN, HIGH); // aus - digitalWrite(PIN_BLUE, HIGH); // aus + setAllOff(); initialized = true; } -bool update(bool active, uint32_t nowMs) { +bool update(Mode mode, uint32_t nowMs) { if (!initialized) begin(); - if (!active) { - digitalWrite(PIN_GREEN, HIGH); // aus + if (mode == Mode::Off) { + setAllOff(); blinkState = false; + lastMode = mode; return false; } - if (nowMs - lastToggleMs >= BLINK_INTERVAL_MS) { + if (mode != lastMode) { + lastToggleMs = nowMs; + blinkState = true; + lastMode = mode; + } + + uint32_t interval = (mode == Mode::EmergencyRed) ? RED_BLINK_INTERVAL_MS : GREEN_BLINK_INTERVAL_MS; + uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED : PIN_GREEN; + + if (nowMs - lastToggleMs >= interval) { lastToggleMs = nowMs; blinkState = !blinkState; - digitalWrite(PIN_GREEN, blinkState ? LOW : HIGH); // LOW = an, volle Helligkeit (kein PWM noetig) } + + digitalWrite(pin, blinkState ? LOW : HIGH); + digitalWrite(mode == Mode::EmergencyRed ? PIN_GREEN : PIN_RED, HIGH); + digitalWrite(PIN_BLUE, HIGH); + return blinkState; } diff --git a/src/led_alert.h b/src/led_alert.h index 07eabdd..a31e7b3 100644 --- a/src/led_alert.h +++ b/src/led_alert.h @@ -2,18 +2,22 @@ #include // Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher -// vorhanden, daher ersetzt die LED den akustischen Naeherungsalarm vom -// Cardputer-Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, -// active-low (LOW = an). +// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer- +// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an). namespace LedAlert { + enum class Mode { + Off, // keine LED + ProximityGreen, // Flugzeug innerhalb des Naeherungsradius -> gruen blinkt + EmergencyRed, // Notfall-Squawk (7500/7600/7700) -> rot blinkt schneller, hat Vorrang + }; + // Einmalig in setup() aufrufen. void begin(); - // Haeufig aufrufen (z.B. alle 80-100ms). 'active' = mindestens ein - // Flugzeug ist innerhalb des Alarmradius. Schaltet die gruene LED - // entsprechend an/aus (blinkend, volle Helligkeit) und gibt den - // aktuellen Blink-Zustand zurueck (true = LED gerade an), damit der - // Radar-Bildschirm den betroffenen Punkt synchron mitblinken lassen kann. - bool update(bool active, uint32_t nowMs); + // Haeufig aufrufen (z.B. alle 80-100ms). Schaltet die LED passend zum + // Modus an/aus (blinkend, volle Helligkeit) und gibt den aktuellen + // Blink-Zustand zurueck (true = LED gerade an), damit der Radar- + // Bildschirm z.B. den betroffenen Punkt synchron mitblinken lassen kann. + bool update(Mode mode, uint32_t nowMs); } \ No newline at end of file diff --git a/src/location_manager.cpp b/src/location_manager.cpp index a9f5e86..3dee717 100644 --- a/src/location_manager.cpp +++ b/src/location_manager.cpp @@ -18,7 +18,6 @@ namespace { TinyGPSPlus gps; HardwareSerial gpsSerial(1); - bool gpsEnabled = false; uint8_t gpsPinIndex = 0; bool gpsSerialStarted = false; @@ -32,9 +31,10 @@ namespace { Source source = Source::None; - // Schuetzt lastLat/lastLon/havePersisted/source: der Netzwerk-Task (Core 0) - // schreibt diese Werte, der Render-Loop (Core 1) liest sie ueber - // getHomeLocation()/currentSource(). + bool haveUtcOffset = false; + int32_t utcOffsetSecs = 0; + bool metricUnits = true; // Standard: metrisch, bis die IP-Abfrage etwas anderes sagt + SemaphoreHandle_t mutex = nullptr; void startGpsSerialIfNeeded() { @@ -44,9 +44,6 @@ namespace { gpsSerialStarted = true; } - // Schreibt lastLat/lastLon/havePersisted UND setzt 'source' gleich mit, - // damit beide unter demselben Lock aktualisiert werden (kein Zwischenzustand - // sichtbar fuer den lesenden Core). void persistLocationAndSource(double lat, double lon, Source newSource) { prefs.putDouble("homeLat", lat); prefs.putDouble("homeLon", lon); @@ -127,6 +124,14 @@ void requestIpLookupIfNeeded() { double lon = doc["lon"] | 0.0; if (lat == 0.0 && lon == 0.0) return; + utcOffsetSecs = doc["offset"] | 0; + haveUtcOffset = true; + + const char* countryCode = doc["countryCode"] | ""; + // Nur die USA nutzen (fuer diesen Zweck) Fuss statt Meter. Bei Bedarf + // hier weitere Laender ergaenzen (z.B. "LR", "MM"). + metricUnits = (strcmp(countryCode, "US") != 0); + persistLocationAndSource(lat, lon, Source::IpGeolocation); ipLookupDone = true; } @@ -180,4 +185,8 @@ const char* currentGpsPinLabel() { bool hasGpsFix() { return gps.location.isValid(); } +bool hasUtcOffset() { return haveUtcOffset; } +int32_t utcOffsetSeconds() { return utcOffsetSecs; } +bool useMetricUnits() { return metricUnits; } + } \ No newline at end of file diff --git a/src/location_manager.h b/src/location_manager.h index ba86383..0a4323c 100644 --- a/src/location_manager.h +++ b/src/location_manager.h @@ -20,4 +20,14 @@ namespace LocationManager { bool hasGpsFix(); + // UTC-Offset in Sekunden (inkl. evtl. Sommerzeit), ermittelt bei der + // IP-Geolocation-Abfrage. 0/false, falls noch nicht bekannt. + bool hasUtcOffset(); + int32_t utcOffsetSeconds(); + + // Ob die Region (per IP-Geolocation-Laendercode) metrische Einheiten + // nutzt (Meter/km) statt Fuss/Meilen. Default true (metrisch), bis die + // IP-Abfrage etwas anderes ermittelt hat - nur die USA nutzen aktuell + // eine Ausnahme. + bool useMetricUnits(); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0997a7f..b4e6684 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,16 +21,19 @@ #include "radar_screen.h" #include "splash_screen.h" #include "led_alert.h" +#include "flight_logbook.h" TFT_eSPI tft = TFT_eSPI(); -constexpr int16_t CONTENT_TOP = 30; -constexpr uint32_t POLL_INTERVAL_MS = 300; -constexpr uint32_t SWEEP_TICK_MS = 80; +constexpr int16_t CONTENT_TOP = 30; // schlanker Header, Radar bekommt den Rest des Screens +constexpr uint32_t POLL_INTERVAL_MS = 300; // wie oft wir NACHSCHAUEN, ob sich Daten geaendert haben +constexpr uint32_t SWEEP_TICK_MS = 80; // wie oft der Sweep-Strahl ein Stueck weiterdreht uint32_t lastPollMs = 0; uint32_t lastSweepMs = 0; -uint32_t lastRenderedVersion = 0xFFFFFFFF; +uint32_t lastRenderedVersion = 0xFFFFFFFF; // erzwingt einen ersten Render-Aufruf bool forceRedraw = false; +bool wasEmergency = false; +bool bannerBlinkOn = false; struct Rect { int16_t x, y, w, h; @@ -39,18 +42,21 @@ struct Rect { } }; -Rect menuBtn = {Config::SCREEN_WIDTH - 38, 3, 32, 22}; +Rect menuBtn = {Config::SCREEN_WIDTH - 60, 3, 54, 22}; void drawMenuButton() { tft.fillRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_NAVY); tft.drawRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_DARKGREY); tft.setTextDatum(MC_DATUM); tft.setTextColor(TFT_WHITE, TFT_NAVY); - tft.drawString("...", menuBtn.x + menuBtn.w / 2, menuBtn.y + menuBtn.h / 2); + tft.drawString("Menu", menuBtn.x + menuBtn.w / 2, menuBtn.y + menuBtn.h / 2); tft.setTextDatum(TL_DATUM); } void drawHeader() { + // Bis CONTENT_TOP loeschen, damit kein Bildrest vom Menue zwischen Header + // und Radar-Bildschirm haengen bleibt. Schlanker Header, damit der Radar- + // Kreis darunter maximal viel Platz bekommt. tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(1); @@ -59,41 +65,99 @@ void drawHeader() { drawMenuButton(); } +// Zeigt eine grosse, unmissverstaendliche Meldung und haelt das Geraet an - +// KEIN weiterer Bildschirm erscheint, solange keine SD-Karte steckt. Die App +// braucht die Karte fuer Einstellungen, WLAN-Zugangsdaten und Nachschlage- +// tabellen, daher macht ein Weiterlaufen ohne sie keinen Sinn. +void haltWithSdRequiredScreen() { + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + int16_t cx = Config::SCREEN_WIDTH / 2; + int16_t cy = Config::SCREEN_HEIGHT / 2; + + tft.setTextColor(TFT_RED, TFT_BLACK); + tft.setTextSize(2); + tft.drawString("For this app a", cx, cy - 40); + tft.drawString("SD card is", cx, cy - 10); + tft.drawString("required", cx, cy + 20); + + tft.setTextSize(1); + tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + tft.drawString("Insert a card and restart the device", cx, cy + 60); + tft.setTextDatum(TL_DATUM); + + while (true) { + delay(1000); // haengen bleiben - absichtlich kein weiterer Screen + } +} + +// Zeigt/versteckt ein blinkendes Notfall-Banner im Header-Bereich, wenn ein +// Flugzeug einen Notfall-Squawk (7500/7600/7700) sendet. Ersetzt kurzzeitig +// den normalen Titel; sobald der Notfall vorbei ist, wird drawHeader() genau +// einmal wieder aufgerufen, um den Titel sauber wiederherzustellen. +void updateEmergencyBanner(uint32_t nowMs) { + RadarScreen::EmergencyInfo emergency = RadarScreen::checkEmergency(); + + if (emergency.active) { + bannerBlinkOn = !bannerBlinkOn; + uint16_t bg = bannerBlinkOn ? TFT_RED : TFT_BLACK; + tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, bg); + tft.setTextColor(TFT_WHITE, bg); + tft.setTextSize(1); + tft.setCursor(4, 10); + tft.printf("EMERGENCY %s %s", emergency.squawk, emergency.callsign); + wasEmergency = true; + } else if (wasEmergency) { + wasEmergency = false; + drawHeader(); + } +} + void setup() { Serial.begin(115200); delay(300); tft.init(); tft.setRotation(0); + // Sofort loeschen, bevor irgendetwas anderes passiert (SD-Init etc. + // braucht einen Moment) - sonst zeigt das Display kurz zufaelligen + // Bildspeicher-Muell an, bevor der erste echte Screen gezeichnet wird. + tft.fillScreen(TFT_BLACK); TouchInput::begin(); LedAlert::begin(); + // --- SD-Karte: PFLICHT. Ohne Karte kein weiterer Screen. --- bool sdOk = SdStorage::init(); - if (sdOk) { - SdStorage::seedDefaultDataFiles(); + if (!sdOk) { + haltWithSdRequiredScreen(); + return; // unerreichbar (haltWithSdRequiredScreen() haengt fuer immer), nur zur Klarheit } + SdStorage::seedDefaultDataFiles(); + // Einstellungen (u.a. Display-Invertierung) VOR dem Splash laden und + // anwenden, damit der Splash selbst schon in der richtigen Ausrichtung + // gezeichnet wird (kein Farbwechsel mitten in der Anzeige). SettingsStore::load(); tft.invertDisplay(SettingsStore::displayInverted()); - WifiMgr::init(); + WifiMgr::init(); // laedt die gespeicherten Netzwerke (bis zu 3) von der SD-Karte + // --- Splash-Screen: schwarzer Hintergrund, gruenes Flugzeug, mind. 5s --- SplashScreen::begin(tft); - SplashScreen::setStatusLine(tft, 0, sdOk ? "SD Card: OK" : "SD Card: ERROR", - sdOk ? TFT_WHITE : TFT_RED); + SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE); + // --- Touch-Kalibrierung (nur beim allerersten Start, oder wenn Datei fehlt) --- if (!TouchInput::loadCalibration()) { CalibrationScreen::run(tft); } - bool haveWifiFile = SD.exists(Config::SD_WIFI_CREDENTIALS_FILE); - if (!haveWifiFile) { + // --- WLAN: Ersteinrichtung (falls noch kein Netzwerk gespeichert ist) + // oder automatisches Verbinden mit dem ersten gerade sichtbaren + // gespeicherten Netzwerk (z.B. Zuhause ODER Auto-Hotspot). --- + if (WifiMgr::networkCount() == 0) { WifiSetupScreen::run(tft); } else { - if (!WifiMgr::hasStoredCredentials()) { - WifiMgr::loadCredentialsFromSd(); - } SplashScreen::setStatusLine(tft, 1, "Connecting WiFi..."); WifiMgr::beginConnect(); @@ -112,6 +176,7 @@ void setup() { AdsbClient::primeTime(); + // --- Standort per IP-Geolocation (einmalig blockierend beim Start) --- SplashScreen::setStatusLine(tft, 2, "Getting location..."); LocationManager::init(); uint32_t locStart = millis(); @@ -122,11 +187,22 @@ void setup() { } SplashScreen::setStatusLine(tft, 2, "Ready!"); + // Sobald die IP-Geolocation einen UTC-Offset geliefert hat, die + // Zeitzone entsprechend setzen - Zeitstempel (Flugbuch, Log-Dateinamen) + // zeigen dann die ECHTE Ortszeit statt UTC, automatisch weltweit richtig. + if (LocationManager::hasUtcOffset()) { + configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov"); + } + AircraftTable::init(); AirlineLookup::init(); + FlightLogbook::init(); // rekonstruiert die "heute schon geloggt"-Liste aus der SD-Karte + // --- Ab hier uebernimmt der Netzwerk-Task (Core 0) laufend WLAN-Status, + // Standort-Updates und ADS-B-Abfragen im Hintergrund. --- NetTask::begin(); + // Splash bleibt mindestens 5 Sekunden sichtbar, egal wie schnell der Rest war. SplashScreen::waitRemaining(); drawHeader(); @@ -139,14 +215,19 @@ void loop() { if (menuBtn.contains(tap.x, tap.y)) { MenuScreen::run(tft); drawHeader(); - forceRedraw = true; + forceRedraw = true; // sofort neu zeichnen } else if (tap.y >= CONTENT_TOP) { if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) { - forceRedraw = true; + forceRedraw = true; // sofort neu zeichnen nach Interaktion } } } + // Nur alle POLL_INTERVAL_MS kurz nachschauen (billige Abfrage eines + // Zaehlers), statt staendig teuer neu zu zeichnen. Ein echtes Neuzeichnen + // (render()) passiert nur, wenn sich die Flugzeugdaten TATSAECHLICH + // geaendert haben (neue Abfrage im Hintergrund fertig) oder der Nutzer + // etwas angetippt hat - das vermeidet unnoetiges Flackern. if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) { lastPollMs = millis(); uint32_t currentVersion = AircraftTable::version(); @@ -154,19 +235,22 @@ void loop() { lastRenderedVersion = currentVersion; forceRedraw = false; RadarScreen::render(tft, CONTENT_TOP); - lastSweepMs = millis(); + lastSweepMs = millis(); // Sweep-Delta nicht ueber den Vollbild-Redraw hinweg aufaddieren } } + // Sweep-Strahl dreht sich unabhaengig von den Flugzeugdaten weiter - + // billige Linien-Zeichnung ohne Vollbild-Clear, daher kein Flackern. uint32_t nowMs = millis(); - // Naeherungsalarm (LED) laeuft IMMER, auch wenn gerade das Detail-Fenster - // offen ist - unabhaengig vom Sweep/Radar-Redraw. + // Naeherungs-/Notfall-Alarm (LED) laeuft IMMER, auch wenn gerade das + // Detail-Fenster offen ist - unabhaengig vom Sweep/Radar-Redraw. RadarScreen::updateProximityAlert(nowMs); if (nowMs - lastSweepMs >= SWEEP_TICK_MS) { uint32_t deltaMs = nowMs - lastSweepMs; lastSweepMs = nowMs; RadarScreen::tick(tft, CONTENT_TOP, deltaMs); + updateEmergencyBanner(nowMs); // gleiche Taktung wie der Sweep-Tick } } \ No newline at end of file diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 0fc461b..dc1fef6 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -1,6 +1,7 @@ #include "menu_screen.h" #include "touch_input.h" #include "calibration_screen.h" +#include "wifi_manage_screen.h" #include "settings_store.h" #include "config.h" @@ -14,6 +15,15 @@ namespace { } }; + constexpr int16_t ROW_H = 30; + constexpr int16_t ROW_GAP = 4; + constexpr int16_t ROW_START_Y = 26; + + Rect rowRect(uint8_t index) { + return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)), + (int16_t)(Config::SCREEN_WIDTH - 20), ROW_H}; + } + void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) { tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_NAVY); tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY); @@ -22,28 +32,41 @@ namespace { tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2); tft.setTextDatum(TL_DATUM); } + + String onOff(bool on) { return on ? "ON" : "OFF"; } } void run(TFT_eSPI& tft) { - Rect calibBtn = {10, 60, Config::SCREEN_WIDTH - 20, 40}; - Rect invertBtn = {10, 112, Config::SCREEN_WIDTH - 20, 40}; - Rect backBtn = {10, 260, Config::SCREEN_WIDTH - 20, 40}; + Rect calibBtn = rowRect(0); + Rect invertBtn = rowRect(1); + Rect wifiBtn = rowRect(2); + Rect emergencyBtn = rowRect(3); + Rect proximityBtn = rowRect(4); + Rect logbookBtn = rowRect(5); + Rect backBtn = rowRect(6); bool done = false; while (!done) { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.setCursor(10, 10); - tft.println("Einstellungen"); + tft.setCursor(10, 8); + tft.println("Settings"); + + drawButton(tft, calibBtn, "Calibrate touch"); - drawButton(tft, calibBtn, "Touch kalibrieren"); String invertLabel = SettingsStore::displayInverted() - ? "Display: invertiert (antippen)" - : "Display: normal (antippen)"; + ? "Display: inverted (tap)" + : "Display: normal (tap)"; drawButton(tft, invertBtn, invertLabel); - drawButton(tft, backBtn, "Zurueck"); - // Auf genau einen Tap warten + drawButton(tft, wifiBtn, "Manage WiFi networks"); + + drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled())); + drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled())); + drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled())); + + drawButton(tft, backBtn, "Back"); + TouchInput::Point tap; while (true) { if (TouchInput::wasTapped(tap)) break; @@ -56,6 +79,14 @@ void run(TFT_eSPI& tft) { bool newState = !SettingsStore::displayInverted(); SettingsStore::setDisplayInverted(newState); tft.invertDisplay(newState); + } else if (wifiBtn.contains(tap.x, tap.y)) { + WifiManageScreen::run(tft); + } else if (emergencyBtn.contains(tap.x, tap.y)) { + SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled()); + } else if (proximityBtn.contains(tap.x, tap.y)) { + SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); + } else if (logbookBtn.contains(tap.x, tap.y)) { + SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled()); } else if (backBtn.contains(tap.x, tap.y)) { done = true; } diff --git a/src/net_task.cpp b/src/net_task.cpp index efc8a49..22cb49b 100644 --- a/src/net_task.cpp +++ b/src/net_task.cpp @@ -7,6 +7,7 @@ #include "aircraft.h" #include "settings_store.h" #include "aircraft_details.h" +#include "flight_logbook.h" #include #include #include @@ -18,11 +19,6 @@ namespace { TaskHandle_t taskHandle = nullptr; uint32_t lastFetchMs = 0; - // Eigener Zwischenspeicher fuer die Netzwerk-Antwort. Die eigentliche - // AircraftTable wird NUR fuer den kurzen Kopiervorgang gesperrt, NICHT - // waehrend der (langsamen) Netzwerkabfrage selbst - sonst friert der - // Radar-Bildschirm fuer die Dauer der HTTPS-Anfrage ein, weil er auf - // denselben Lock wartet. Aircraft tempTable[Config::MAX_TRACKED_AIRCRAFT]; void taskFunc(void*) { @@ -30,9 +26,6 @@ namespace { WifiMgr::update(); LocationManager::update(); - // Erledigt eine evtl. anstehende Detail-Abfrage (Modell/Route) - // fuer das aktuell vom Nutzer ausgewaehlte Flugzeug, falls es eine - // gibt. Guenstig, wenn nichts ansteht (nur ein Mutex-Check). AircraftDetails::update(); if (millis() - lastFetchMs >= Config::FETCH_INTERVAL_MS) { @@ -46,10 +39,6 @@ namespace { float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; - // Netzwerkabfrage OHNE Lock - schreibt nur in den lokalen - // Zwischenspeicher, den sonst niemand anfasst. Der - // Radar-Bildschirm kann waehrenddessen ganz normal mit den - // ALTEN Daten weiterzeichnen. auto result = AdsbClient::fetch(lat, lon, rangeKm, tempTable, Config::MAX_TRACKED_AIRCRAFT); @@ -59,6 +48,8 @@ namespace { sizeof(Aircraft) * Config::MAX_TRACKED_AIRCRAFT); AircraftTable::postFetchUpdate(lat, lon); AircraftTable::unlock(); + + FlightLogbook::update(); } else { Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode); } @@ -74,11 +65,11 @@ void begin() { xTaskCreatePinnedToCore( taskFunc, "NetTask", - 20480, // Stack: TLS-Handshake + JSON-Parsing braucht mehr als das Minimum + 20480, nullptr, - 1, // Prioritaet + 1, &taskHandle, - 0 // Core 0 (Core 1 bleibt frei fuer Rendering/Touch im main-Loop) + 0 ); } diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index 706b80a..8646c31 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -8,6 +8,7 @@ #include "units.h" #include "settings_store.h" #include "led_alert.h" +#include "location_manager.h" #include namespace RadarScreen { @@ -50,6 +51,7 @@ namespace { uint16_t color; float headingDeg; float distanceKm; + bool isEmergency; }; constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT; HitPoint hitPoints[MAX_HIT_POINTS]; @@ -58,6 +60,14 @@ namespace { bool ledBlinkOn = true; + bool isEmergencySquawk(const char* squawk) { + if (!squawk[0]) return false; + for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) { + if (strcmp(squawk, Config::EMERGENCY_SQUAWKS[i]) == 0) return true; + } + return false; + } + float sweepAngleDeg = 0.0f; float prevSweepAngleDeg = -1.0f; constexpr float SWEEP_DEGREES_PER_SEC = 45.0f; @@ -89,11 +99,29 @@ namespace { gfx.setTextDatum(TL_DATUM); } + // Kleine Farb-Legende (Hoehenband -> Farbe), damit auf den ersten Blick + // klar ist, was Gruen/Gelb/Rot bei den Flugzeug-Punkten bedeuten. Zeigt + // Meter statt Fuss, wenn die Region (per Geolocation) metrisch ist. void drawLegend(TFT_eSPI& gfx, int16_t y) { + bool metric = LocationManager::useMetricUnits(); + + char lowLabel[10], midLabel[10], highLabel[10]; + if (metric) { + int lowM = (int)(Units::feetToMeters(Config::COLOR_LOW_ALT_THRESHOLD_FT) / 100) * 100; + int midM = (int)(Units::feetToMeters(Config::COLOR_MID_ALT_THRESHOLD_FT) / 100) * 100; + snprintf(lowLabel, sizeof(lowLabel), "<%dm", lowM); + snprintf(midLabel, sizeof(midLabel), "%d-%dm", lowM, midM); + snprintf(highLabel, sizeof(highLabel), ">%dm", midM); + } else { + snprintf(lowLabel, sizeof(lowLabel), "<10k ft"); + snprintf(midLabel, sizeof(midLabel), "10-30k"); + snprintf(highLabel, sizeof(highLabel), ">30k ft"); + } + struct { uint16_t color; const char* label; } items[3] = { - {TFT_GREEN, "<10k ft"}, - {TFT_YELLOW, "10-30k"}, - {TFT_RED, ">30k ft"}, + {TFT_GREEN, lowLabel}, + {TFT_YELLOW, midLabel}, + {TFT_RED, highLabel}, }; int16_t segW = Config::SCREEN_WIDTH / 3; gfx.setTextColor(TFT_WHITE, TFT_BLACK); @@ -290,6 +318,7 @@ void render(TFT_eSPI& tft, int16_t top) { uint16_t color = colorForAltitude(a.altBaroFt); bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0; + bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk); if (isSelected) { tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE); @@ -298,6 +327,10 @@ void render(TFT_eSPI& tft, int16_t top) { } tft.fillCircle(pt.x, pt.y, 5, color); + if (isEmergency) { + tft.drawCircle(pt.x, pt.y, 12, TFT_RED); + } + double rad = a.headingDeg * PI / 180.0; int16_t dx = (int16_t)(sin(rad) * 10); int16_t dy = (int16_t)(-cos(rad) * 10); @@ -315,6 +348,7 @@ void render(TFT_eSPI& tft, int16_t top) { hitPoints[i].color = color; hitPoints[i].headingDeg = a.headingDeg; hitPoints[i].distanceKm = a.distanceKm; + hitPoints[i].isEmergency = isEmergency; strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1); strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1); } @@ -375,6 +409,10 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { tft.fillCircle(hp.x, hp.y, 5, hp.color); + if (hp.isEmergency) { + tft.drawCircle(hp.x, hp.y, 12, TFT_RED); + } + double rad = hp.headingDeg * PI / 180.0; int16_t dx = (int16_t)(sin(rad) * 10); int16_t dy = (int16_t)(-cos(rad) * 10); @@ -429,18 +467,47 @@ bool handleTap(int16_t x, int16_t y, int16_t top) { void updateProximityAlert(uint32_t nowMs) { bool anyClose = false; + bool anyEmergency = false; + + bool proximityOn = SettingsStore::proximityAlertEnabled(); + bool emergencyOn = SettingsStore::emergencyAlertEnabled(); + + if (proximityOn || emergencyOn) { + AircraftTable::lock(); + Aircraft* table = AircraftTable::raw(); + for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { + if (!table[i].valid) continue; + if (proximityOn && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) anyClose = true; + if (emergencyOn && isEmergencySquawk(table[i].squawk)) anyEmergency = true; + } + AircraftTable::unlock(); + } + + LedAlert::Mode mode = anyEmergency ? LedAlert::Mode::EmergencyRed + : anyClose ? LedAlert::Mode::ProximityGreen + : LedAlert::Mode::Off; + + ledBlinkOn = LedAlert::update(mode, nowMs); +} + +EmergencyInfo checkEmergency() { + EmergencyInfo info; + if (!SettingsStore::emergencyAlertEnabled()) return info; AircraftTable::lock(); Aircraft* table = AircraftTable::raw(); for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { - if (table[i].valid && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) { - anyClose = true; + if (table[i].valid && isEmergencySquawk(table[i].squawk)) { + info.active = true; + strncpy(info.callsign, table[i].callsign[0] ? table[i].callsign : table[i].hex, + sizeof(info.callsign) - 1); + strncpy(info.squawk, table[i].squawk, sizeof(info.squawk) - 1); break; } } AircraftTable::unlock(); - ledBlinkOn = LedAlert::update(anyClose, nowMs); + return info; } } \ No newline at end of file diff --git a/src/radar_screen.h b/src/radar_screen.h index d57c2c5..771595b 100644 --- a/src/radar_screen.h +++ b/src/radar_screen.h @@ -23,8 +23,19 @@ namespace RadarScreen { bool handleTap(int16_t x, int16_t y, int16_t top); // Haeufig aufrufen (unabhaengig davon, ob ein Detail-Fenster offen ist): - // prueft, ob ein Flugzeug innerhalb des Alarmradius ist, steuert die - // gruene LED entsprechend und merkt sich den Blink-Zustand, damit tick() - // den betroffenen Punkt synchron mitblinken lassen kann. + // prueft, ob ein Flugzeug innerhalb des Alarmradius ist ODER ein + // Notfall-Squawk sendet, steuert die LED entsprechend (Notfall hat + // Vorrang, blinkt rot statt gruen) und merkt sich den Blink-Zustand, + // damit tick() den betroffenen Punkt synchron mitblinken lassen kann. void updateProximityAlert(uint32_t nowMs); + + struct EmergencyInfo { + bool active = false; + char callsign[9] = {0}; + char squawk[5] = {0}; + }; + + // Reine Abfrage (kein Seiteneffekt): gibt das erste Flugzeug mit einem + // Notfall-Squawk zurueck, falls vorhanden. Fuer das Banner im Header. + EmergencyInfo checkEmergency(); } \ No newline at end of file diff --git a/src/settings_store.cpp b/src/settings_store.cpp index b018a18..d8b1f98 100644 --- a/src/settings_store.cpp +++ b/src/settings_store.cpp @@ -7,6 +7,9 @@ namespace SettingsStore { namespace { uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX; bool inverted = false; + bool emergencyAlertOn = true; + bool proximityAlertOn = true; + bool flightLogbookOn = true; void applyKeyValue(const String& key, const String& value) { if (key == "range_index") { @@ -16,26 +19,26 @@ namespace { } } else if (key == "invert") { inverted = (value.toInt() != 0); + } else if (key == "emergency_alert") { + emergencyAlertOn = (value.toInt() != 0); + } else if (key == "proximity_alert") { + proximityAlertOn = (value.toInt() != 0); + } else if (key == "flight_logbook") { + flightLogbookOn = (value.toInt() != 0); } - // Weitere Einstellungen hier ergaenzen (unbekannte Keys werden - // einfach ignoriert, damit alte config.txt-Dateien kompatibel bleiben). } } void load() { if (!SD.exists(Config::SD_SETTINGS_FILE)) return; - File f = SD.open(Config::SD_SETTINGS_FILE, FILE_READ); if (!f) return; - while (f.available()) { String line = f.readStringUntil('\n'); line.trim(); if (line.length() == 0 || line.startsWith("#")) continue; - int eq = line.indexOf('='); if (eq < 0) continue; - String key = line.substring(0, eq); String value = line.substring(eq + 1); key.trim(); @@ -50,23 +53,27 @@ void save() { if (!f) return; f.printf("range_index=%d\n", rangeIdx); f.printf("invert=%d\n", inverted ? 1 : 0); + f.printf("emergency_alert=%d\n", emergencyAlertOn ? 1 : 0); + f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0); + f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0); f.close(); } uint8_t rangeIndex() { return rangeIdx; } - void setRangeIndex(uint8_t idx) { - if (idx < Config::RANGE_STEP_COUNT) { - rangeIdx = idx; - save(); - } + if (idx < Config::RANGE_STEP_COUNT) { rangeIdx = idx; save(); } } bool displayInverted() { return inverted; } +void setDisplayInverted(bool inv) { inverted = inv; save(); } -void setDisplayInverted(bool inv) { - inverted = inv; - save(); -} +bool emergencyAlertEnabled() { return emergencyAlertOn; } +void setEmergencyAlertEnabled(bool on) { emergencyAlertOn = on; save(); } + +bool proximityAlertEnabled() { return proximityAlertOn; } +void setProximityAlertEnabled(bool on) { proximityAlertOn = on; save(); } + +bool flightLogbookEnabled() { return flightLogbookOn; } +void setFlightLogbookEnabled(bool on) { flightLogbookOn = on; save(); } } \ No newline at end of file diff --git a/src/settings_store.h b/src/settings_store.h index 2388a47..8f3875d 100644 --- a/src/settings_store.h +++ b/src/settings_store.h @@ -1,17 +1,8 @@ #pragma once #include -// Einfacher key=value Einstellungs-Speicher in /Flightradar_cyd/config.txt. -// Neue Einstellungen koennen spaeter einfach als weitere getX()/setX()-Paare -// dazu kommen, ohne das Dateiformat zu aendern (unbekannte Zeilen werden -// beim Laden ignoriert). namespace SettingsStore { - - // Liest config.txt von der SD-Karte (falls vorhanden). Werte, die nicht - // in der Datei stehen, behalten ihren Standardwert. void load(); - - // Schreibt die aktuellen Werte zurueck auf die SD-Karte. void save(); uint8_t rangeIndex(); @@ -19,4 +10,13 @@ namespace SettingsStore { bool displayInverted(); void setDisplayInverted(bool inverted); + + bool emergencyAlertEnabled(); + void setEmergencyAlertEnabled(bool on); + + bool proximityAlertEnabled(); + void setProximityAlertEnabled(bool on); + + bool flightLogbookEnabled(); + void setFlightLogbookEnabled(bool on); } \ No newline at end of file diff --git a/src/splash_screen.cpp b/src/splash_screen.cpp index 0e1cc32..bb70b1e 100644 --- a/src/splash_screen.cpp +++ b/src/splash_screen.cpp @@ -10,20 +10,33 @@ namespace { constexpr int16_t STATUS_START_Y = 260; constexpr uint8_t MAX_STATUS_LINES = 3; - // Einfache Vektor-Silhouette eines Flugzeugs von oben (keine Bilddatei - // eingebunden, daher per Dreiecken gezeichnet) - in Gruen, wie der - // Radar-Sweep-Strahl, passend zum restlichen Dark-Theme. + // Dezente konzentrische Ringe + Fadenkreuz als "Zielfernrohr"-Hintergrund, + // in gedaempftem Gruen (gleicher Ton wie die Sweep-Nachzieh-Linie im + // Radar-Bildschirm), damit das Flugzeug wie ins Visier genommen wirkt. + void drawRadarReticle(TFT_eSPI& tft, int16_t cx, int16_t cy) { + uint16_t dim = 0x0320; + tft.drawCircle(cx, cy, 112, dim); + tft.drawCircle(cx, cy, 76, dim); + tft.drawCircle(cx, cy, 40, dim); + tft.drawFastHLine(cx - 112, cy, 224, dim); + tft.drawFastVLine(cx, cy - 112, 224, dim); + } + + // Einfache Vektor-Silhouette eines schlanken Duesenjets von oben (keine + // Bilddatei eingebunden, daher per Dreiecken gezeichnet) - nur als Umriss + // (nicht ausgefuellt), in Gruen, passend zum restlichen Dark-Theme. void drawAirplane(TFT_eSPI& tft, int16_t cx) { uint16_t color = TFT_GREEN; - // Rumpf - tft.fillTriangle(cx, 110, cx - 12, 240, cx + 12, 240, color); + // Schlanker Rumpf (nur Umriss) + tft.drawTriangle(cx, 105, cx - 6, 255, cx + 6, 255, color); - // Haupttragflaechen - tft.fillTriangle(cx, 170, cx - 110, 230, cx + 110, 230, color); + // Deltafluegel, nach hinten gepfeilt (nur Umriss) + tft.drawTriangle(cx, 160, cx - 100, 235, cx, 200, color); + tft.drawTriangle(cx, 160, cx + 100, 235, cx, 200, color); - // Leitwerk (Heckfluegel) - tft.fillTriangle(cx, 232, cx - 35, 250, cx + 35, 250, color); + // Kleines Leitwerk (Hoehenruder) am Heck (nur Umriss) + tft.drawTriangle(cx, 250, cx - 22, 268, cx + 22, 268, color); } } @@ -33,6 +46,7 @@ void begin(TFT_eSPI& tft) { int16_t cx = tft.width() / 2; tft.fillScreen(TFT_BLACK); + drawRadarReticle(tft, cx, 185); drawAirplane(tft, cx); tft.setTextDatum(MC_DATUM); diff --git a/src/wifi_manage_screen.cpp b/src/wifi_manage_screen.cpp new file mode 100644 index 0000000..0b9ad61 --- /dev/null +++ b/src/wifi_manage_screen.cpp @@ -0,0 +1,98 @@ +#include "wifi_manage_screen.h" +#include "wifi_manager.h" +#include "wifi_setup_screen.h" +#include "touch_input.h" +#include "config.h" + +namespace WifiManageScreen { + +namespace { + struct Rect { + int16_t x, y, w, h; + bool contains(int16_t px, int16_t py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } + }; + + void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, uint16_t bg = TFT_NAVY) { + tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, bg); + tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_WHITE, bg); + tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2); + tft.setTextDatum(TL_DATUM); + } +} + +void run(TFT_eSPI& tft) { + constexpr int16_t ROW_H = 46; + constexpr int16_t ROW_GAP = 8; + constexpr int16_t REMOVE_BTN_W = 60; + + bool done = false; + while (!done) { + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setCursor(10, 10); + tft.println("WiFi networks"); + + uint8_t count = WifiMgr::networkCount(); + int16_t y = 40; + + Rect rowRects[Config::MAX_WIFI_NETWORKS]; + Rect removeRects[Config::MAX_WIFI_NETWORKS]; + + for (uint8_t i = 0; i < Config::MAX_WIFI_NETWORKS; i++) { + Rect rowRect = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20 - REMOVE_BTN_W - 6), ROW_H}; + Rect removeRect = {(int16_t)(Config::SCREEN_WIDTH - 10 - REMOVE_BTN_W), y, REMOVE_BTN_W, ROW_H}; + rowRects[i] = rowRect; + removeRects[i] = removeRect; + + if (i < count) { + drawButton(tft, rowRect, WifiMgr::networkSsid(i)); + drawButton(tft, removeRect, "X", TFT_MAROON); + } else { + tft.fillRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_BLACK); + tft.drawRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_DARKGREY); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + tft.drawString("(empty)", rowRect.x + rowRect.w / 2, rowRect.y + rowRect.h / 2); + tft.setTextDatum(TL_DATUM); + } + y += ROW_H + ROW_GAP; + } + + Rect addBtn = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), 40}; + bool canAdd = count < Config::MAX_WIFI_NETWORKS; + if (canAdd) { + drawButton(tft, addBtn, "Add network"); + } + y += 48; + + Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40}; + drawButton(tft, backBtn, "Back"); + + TouchInput::Point tap; + while (true) { + if (TouchInput::wasTapped(tap)) break; + delay(20); + } + + bool handled = false; + for (uint8_t i = 0; i < count && !handled; i++) { + if (removeRects[i].contains(tap.x, tap.y)) { + WifiMgr::removeNetwork(i); + handled = true; + } + } + if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) { + WifiSetupScreen::run(tft); + handled = true; + } + if (!handled && backBtn.contains(tap.x, tap.y)) { + done = true; + } + } +} + +} \ No newline at end of file diff --git a/src/wifi_manage_screen.h b/src/wifi_manage_screen.h new file mode 100644 index 0000000..ed2b08a --- /dev/null +++ b/src/wifi_manage_screen.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +namespace WifiManageScreen { + // Blockierend: zeigt die bis zu 3 gespeicherten WLAN-Netzwerke, erlaubt + // Loeschen einzelner Eintraege und (falls noch Platz ist) das Hinzufuegen + // eines neuen ueber den Scan+Passwort-Bildschirm. Kehrt zurueck, sobald + // "Back" angetippt wird. + void run(TFT_eSPI& tft); +} \ No newline at end of file diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp index 0a5dd08..7f3526d 100644 --- a/src/wifi_manager.cpp +++ b/src/wifi_manager.cpp @@ -1,25 +1,30 @@ #include "wifi_manager.h" -#include "config.h" #include "sd_storage.h" #include -#include #include #include #include +#include namespace WifiMgr { namespace { - Preferences prefs; + struct NetworkEntry { + char ssid[33] = {0}; + char pass[64] = {0}; + }; + + NetworkEntry networks[Config::MAX_WIFI_NETWORKS]; + uint8_t networkCountVal = 0; + State state = State::Idle; uint32_t connectStartMs = 0; - constexpr uint32_t CONNECT_TIMEOUT_MS = 15000; // bounded, same fix as the Kinect sketch + constexpr uint32_t CONNECT_TIMEOUT_MS = 15000; char ipStr[16] = {0}; - // Schuetzt 'state': der Netzwerk-Task (Core 0) schreibt es in - // beginConnect()/update(), der Render-Loop (Core 1) liest es ueber - // getState(). ipStr wird nur einmal beim Verbinden geschrieben und danach - // nur gelesen, daher hier ohne eigenen Lock. + uint32_t lastReconnectAttemptMs = 0; + constexpr uint32_t RECONNECT_RETRY_MS = 10000; + SemaphoreHandle_t mutex = nullptr; void setState(State s) { @@ -27,50 +32,136 @@ namespace { state = s; xSemaphoreGive(mutex); } + + void loadFromSd() { + networkCountVal = 0; + if (!SdStorage::isMounted()) return; + if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return; + + File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ); + if (!f) return; + + while (networkCountVal < Config::MAX_WIFI_NETWORKS && f.available()) { + String ssid = f.readStringUntil('\n'); + String pass = f.readStringUntil('\n'); + ssid.trim(); + pass.trim(); + if (ssid.length() == 0) break; + + strncpy(networks[networkCountVal].ssid, ssid.c_str(), sizeof(networks[networkCountVal].ssid) - 1); + strncpy(networks[networkCountVal].pass, pass.c_str(), sizeof(networks[networkCountVal].pass) - 1); + networkCountVal++; + } + f.close(); + } + + void saveToSd() { + if (!SdStorage::isMounted()) return; + + File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE); + if (!f) return; + for (uint8_t i = 0; i < networkCountVal; i++) { + f.println(networks[i].ssid); + f.println(networks[i].pass); + } + f.close(); + } } void init() { if (mutex == nullptr) mutex = xSemaphoreCreateMutex(); - prefs.begin("adsb_radar", /*readOnly=*/false); - setState(hasStoredCredentials() ? State::Idle : State::NoCredentials); + loadFromSd(); + setState(networkCountVal > 0 ? State::Idle : State::NoCredentials); } -bool hasStoredCredentials() { - String ssid = prefs.getString("ssid", ""); - return ssid.length() > 0; +uint8_t networkCount() { return networkCountVal; } + +String networkSsid(uint8_t index) { + if (index >= networkCountVal) return String(); + return String(networks[index].ssid); } -void saveCredentials(const char* ssid, const char* password) { - prefs.putString("ssid", ssid); - prefs.putString("pass", password); +bool addNetwork(const char* ssid, const char* password) { + if (networkCountVal >= Config::MAX_WIFI_NETWORKS) return false; + + strncpy(networks[networkCountVal].ssid, ssid, sizeof(networks[networkCountVal].ssid) - 1); + strncpy(networks[networkCountVal].pass, password, sizeof(networks[networkCountVal].pass) - 1); + networkCountVal++; + saveToSd(); + return true; } -void beginConnect() { - if (!hasStoredCredentials()) { - setState(State::NoCredentials); - return; +void removeNetwork(uint8_t index) { + if (index >= networkCountVal) return; + for (uint8_t i = index; i < networkCountVal - 1; i++) { + networks[i] = networks[i + 1]; } - String ssid = prefs.getString("ssid", ""); - String pass = prefs.getString("pass", ""); + networkCountVal--; + networks[networkCountVal] = NetworkEntry{}; + saveToSd(); +} +void connectTo(const char* ssid, const char* password) { WiFi.mode(WIFI_STA); - WiFi.begin(ssid.c_str(), pass.c_str()); + WiFi.begin(ssid, password); connectStartMs = millis(); setState(State::Connecting); } -void update() { - if (getState() != State::Connecting) return; - - if (WiFi.status() == WL_CONNECTED) { - strncpy(ipStr, WiFi.localIP().toString().c_str(), sizeof(ipStr) - 1); - setState(State::Connected); +void beginConnect() { + if (networkCountVal == 0) { + setState(State::NoCredentials); return; } - if (millis() - connectStartMs > CONNECT_TIMEOUT_MS) { - WiFi.disconnect(true); + int visibleCount = WiFi.scanNetworks(/*async=*/false); + + int8_t chosen = -1; + for (uint8_t i = 0; i < networkCountVal && chosen < 0; i++) { + for (int j = 0; j < visibleCount; j++) { + if (WiFi.SSID(j) == networks[i].ssid) { + chosen = (int8_t)i; + break; + } + } + } + + if (chosen < 0) { setState(State::Failed); + return; + } + + connectTo(networks[chosen].ssid, networks[chosen].pass); +} + +void update() { + State s = getState(); + + if (s == State::Connecting) { + if (WiFi.status() == WL_CONNECTED) { + strncpy(ipStr, WiFi.localIP().toString().c_str(), sizeof(ipStr) - 1); + setState(State::Connected); + return; + } + if (millis() - connectStartMs > CONNECT_TIMEOUT_MS) { + WiFi.disconnect(true); + setState(State::Failed); + } + return; + } + + if (s == State::Connected && WiFi.status() != WL_CONNECTED) { + Serial.println("[WifiMgr] Verbindung verloren, versuche automatisch neu zu verbinden..."); + setState(State::Idle); + lastReconnectAttemptMs = millis() - RECONNECT_RETRY_MS; + return; + } + + if ((s == State::Idle || s == State::Failed) && networkCountVal > 0) { + if (millis() - lastReconnectAttemptMs >= RECONNECT_RETRY_MS) { + lastReconnectAttemptMs = millis(); + beginConnect(); + } } } @@ -104,37 +195,4 @@ int32_t getScanResultRSSI(int index) { const char* getIP() { return ipStr; } -bool loadCredentialsFromSd() { - if (!SdStorage::isMounted()) return false; - if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return false; - - File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ); - if (!f) return false; - - String ssid = f.readStringUntil('\n'); - String pass = f.readStringUntil('\n'); - f.close(); - - ssid.trim(); - pass.trim(); - if (ssid.length() == 0) return false; - - saveCredentials(ssid.c_str(), pass.c_str()); - return true; -} - -void saveCredentialsToSdIfMounted() { - if (!SdStorage::isMounted()) return; - - String ssid = prefs.getString("ssid", ""); - String pass = prefs.getString("pass", ""); - if (ssid.length() == 0) return; - - File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE); - if (!f) return; - f.println(ssid); - f.println(pass); - f.close(); -} - } \ No newline at end of file diff --git a/src/wifi_manager.h b/src/wifi_manager.h index a14da8d..505d674 100644 --- a/src/wifi_manager.h +++ b/src/wifi_manager.h @@ -1,5 +1,6 @@ #pragma once #include +#include "config.h" namespace WifiMgr { enum class State { @@ -8,20 +9,20 @@ namespace WifiMgr { void init(); void beginConnect(); + void connectTo(const char* ssid, const char* password); + void update(); State getState(); + const char* getIP(); - void saveCredentials(const char* ssid, const char* password); - bool hasStoredCredentials(); - - bool loadCredentialsFromSd(); - void saveCredentialsToSdIfMounted(); + uint8_t networkCount(); + String networkSsid(uint8_t index); + bool addNetwork(const char* ssid, const char* password); + void removeNetwork(uint8_t index); void beginScan(); bool isScanComplete(); int getScanResultCount(); String getScanResultSSID(int index); int32_t getScanResultRSSI(int index); - - const char* getIP(); } \ No newline at end of file diff --git a/src/wifi_setup_screen.cpp b/src/wifi_setup_screen.cpp index 3ddec7f..0cb5a68 100644 --- a/src/wifi_setup_screen.cpp +++ b/src/wifi_setup_screen.cpp @@ -17,11 +17,10 @@ namespace { struct KeyDef { KeyType type; - char ch; // fuer KeyType::Char (Kleinbuchstabe/Grundzeichen) - const char* label; // Anzeige, falls kein einzelnes Zeichen (z.B. "<-") + char ch; + const char* label; }; - // --- Tastatur-Layout --------------------------------------------------- constexpr KeyDef ROW_LETTERS_A[] = { {KeyType::Char,'q',nullptr},{KeyType::Char,'w',nullptr},{KeyType::Char,'e',nullptr}, {KeyType::Char,'r',nullptr},{KeyType::Char,'t',nullptr},{KeyType::Char,'y',nullptr}, @@ -79,11 +78,11 @@ namespace { uint8_t passwordLen = 0; bool shiftOn = false; - uint8_t page = 0; // 0 = Buchstaben, 1 = Symbole + uint8_t page = 0; bool skipped = false; bool connectSucceeded = false; - bool needsRedraw = true; // nur neu zeichnen, wenn sich wirklich was geaendert hat (verhindert Flackern) + bool needsRedraw = true; Rect cancelBtn = {Config::SCREEN_WIDTH - 34, 4, 30, 24}; @@ -97,7 +96,6 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Rechnet die Rects einer Tastenreihe aus (gleich verteilt ueber die Breite). template void layoutRow(const KeyDef (&row)[N], int16_t y, Rect outRects[N]) { int16_t usableW = Config::SCREEN_WIDTH - 2 * SIDE_MARGIN; @@ -184,10 +182,10 @@ bool run(TFT_eSPI& tft) { WifiMgr::beginScan(); tft.fillScreen(TFT_BLACK); - tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(1); tft.setCursor(10, 10); - tft.println("WLAN-Suche laeuft..."); + tft.println("Scanning WiFi..."); drawCancelButton(tft); while (!skipped) { @@ -199,7 +197,6 @@ bool run(TFT_eSPI& tft) { break; } - // --- Uebergaenge, die nicht vom Touch kommen ----------------------- if (stage == Stage::Scanning && WifiMgr::isScanComplete()) { ssidCount = (uint8_t)min((int)WifiMgr::getScanResultCount(), (int)MAX_LIST); for (uint8_t i = 0; i < ssidCount; i++) ssidList[i] = WifiMgr::getScanResultSSID(i); @@ -208,8 +205,8 @@ bool run(TFT_eSPI& tft) { tft.fillScreen(TFT_BLACK); tft.setCursor(10, 10); - tft.setTextColor(TFT_GREEN, TFT_BLACK); - tft.println(ssidCount == 0 ? "Keine Netzwerke gefunden" : "WLAN waehlen:"); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.println(ssidCount == 0 ? "No networks found" : "Select WiFi:"); drawCancelButton(tft); } @@ -217,12 +214,12 @@ bool run(TFT_eSPI& tft) { WifiMgr::update(); if (WifiMgr::getState() == WifiMgr::State::Connected) { connectSucceeded = true; - WifiMgr::saveCredentialsToSdIfMounted(); + WifiMgr::addNetwork(ssidList[selectedIndex].c_str(), passwordBuf); stage = Stage::Done; tft.fillScreen(TFT_BLACK); tft.setCursor(10, 10); - tft.setTextColor(TFT_GREEN, TFT_BLACK); - tft.println("Verbunden!"); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.println("Connected!"); delay(900); return true; } else if (WifiMgr::getState() == WifiMgr::State::Failed) { @@ -230,10 +227,10 @@ bool run(TFT_eSPI& tft) { tft.fillScreen(TFT_BLACK); tft.setCursor(10, 10); tft.setTextColor(TFT_RED, TFT_BLACK); - tft.println("Verbindung fehlgeschlagen."); - tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.println("Connection failed."); + tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setCursor(10, 30); - tft.println("Zurueck zur Netzwerkliste..."); + tft.println("Back to network list..."); delay(1400); stage = Stage::PickSsid; WifiMgr::beginScan(); @@ -241,10 +238,8 @@ bool run(TFT_eSPI& tft) { } } - // --- Touch-Eingaben je nach Stage ----------------------------------- if (tapped && stage == Stage::PickSsid) { if (ssidCount > 0) { - // Liste for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) { uint8_t idx = scrollOffset + row; if (idx >= ssidCount) break; @@ -256,7 +251,6 @@ bool run(TFT_eSPI& tft) { needsRedraw = true; } } - // Scroll-Pfeile if (ssidCount > VISIBLE_ITEMS) { Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26}; Rect downBtn = {Config::SCREEN_WIDTH - 34, 64 + (VISIBLE_ITEMS - 1) * 34, 30, 26}; @@ -294,22 +288,32 @@ bool run(TFT_eSPI& tft) { passwordBuf[passwordLen] = 0; } } else if (connectBtn.contains(tap.x, tap.y)) { - WifiMgr::saveCredentials(ssidList[selectedIndex].c_str(), passwordBuf); - WifiMgr::beginConnect(); + if (WifiMgr::networkCount() >= Config::MAX_WIFI_NETWORKS) { + tft.fillScreen(TFT_BLACK); + tft.setCursor(10, 10); + tft.setTextColor(TFT_RED, TFT_BLACK); + tft.println("Already 3 networks saved."); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setCursor(10, 30); + tft.println("Remove one first."); + delay(1600); + skipped = true; + break; + } + WifiMgr::connectTo(ssidList[selectedIndex].c_str(), passwordBuf); stage = Stage::Connecting; tft.fillScreen(TFT_BLACK); tft.setCursor(10, 10); - tft.setTextColor(TFT_GREEN, TFT_BLACK); - tft.println("Verbinde..."); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.println("Connecting..."); } else if (backBtn.contains(tap.x, tap.y)) { stage = Stage::PickSsid; needsRedraw = true; } } - needsRedraw = true; // Tastatureingabe (Zeichen/Shift/Seite/Backspace/Leerzeichen) aendert den Bildschirm + needsRedraw = true; } - // --- Zeichnen (nur bei Aenderung, verhindert Flackern) ---------------- if (!needsRedraw) { delay(20); continue; @@ -333,16 +337,16 @@ bool run(TFT_eSPI& tft) { drawCancelButton(tft); } else if (stage == Stage::EnterPassword) { tft.fillRect(0, 0, Config::SCREEN_WIDTH, KB_TOP - 4, TFT_BLACK); - tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setCursor(10, 6); - tft.printf("WLAN: %s", ssidList[selectedIndex].c_str()); + tft.printf("WiFi: %s", ssidList[selectedIndex].c_str()); tft.setCursor(10, 22); tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.println("Passwort:"); + tft.println("Password:"); tft.fillRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_NAVY); tft.drawRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_DARKGREY); tft.setCursor(12, 44); - tft.setTextColor(TFT_YELLOW, TFT_NAVY); + tft.setTextColor(TFT_WHITE, TFT_NAVY); tft.print(passwordBuf); int16_t rowY0 = KB_TOP; @@ -363,15 +367,15 @@ bool run(TFT_eSPI& tft) { Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H}; Rect connectBtn = {SIDE_MARGIN + 154, rowYFn, Config::SCREEN_WIDTH - 2*SIDE_MARGIN - 154, ROW_H}; Rect backBtn = {SIDE_MARGIN, (int16_t)(rowYFn + ROW_H + ROW_GAP), (int16_t)(Config::SCREEN_WIDTH - 2*SIDE_MARGIN), 28}; - drawButton(tft, spaceBtn, "Leerzeichen"); - drawButton(tft, connectBtn, "Verbinden"); - drawButton(tft, backBtn, "Zurueck zur Liste"); + drawButton(tft, spaceBtn, "Space"); + drawButton(tft, connectBtn, "Connect"); + drawButton(tft, backBtn, "Back to list"); } delay(20); } - return false; // uebersprungen + return false; } } \ No newline at end of file