diff --git a/src/led_alert.cpp b/src/led_alert.cpp index caa5fba..070de41 100644 --- a/src/led_alert.cpp +++ b/src/led_alert.cpp @@ -15,6 +15,10 @@ namespace { uint32_t lastToggleMs = 0; Mode lastMode = Mode::Off; + bool heartbeatActive = false; + uint32_t heartbeatStartMs = 0; + constexpr uint32_t HEARTBEAT_PULSE_MS = 200; + void setAllOff() { digitalWrite(PIN_RED, HIGH); digitalWrite(PIN_GREEN, HIGH); @@ -30,10 +34,27 @@ void begin() { initialized = true; } +void pulseHeartbeat(uint32_t nowMs) { + heartbeatActive = true; + heartbeatStartMs = nowMs; +} + bool update(Mode mode, uint32_t nowMs) { if (!initialized) begin(); if (mode == Mode::Off) { + if (heartbeatActive) { + if (nowMs - heartbeatStartMs < HEARTBEAT_PULSE_MS) { + digitalWrite(PIN_GREEN, LOW); + digitalWrite(PIN_RED, HIGH); + digitalWrite(PIN_BLUE, HIGH); + blinkState = false; + lastMode = mode; + return true; + } + heartbeatActive = false; + } + setAllOff(); blinkState = false; lastMode = mode; @@ -61,4 +82,14 @@ bool update(Mode mode, uint32_t nowMs) { return blinkState; } +void flashWhite(uint32_t durationMs) { + if (!initialized) begin(); + + digitalWrite(PIN_RED, LOW); + digitalWrite(PIN_GREEN, LOW); + digitalWrite(PIN_BLUE, LOW); + delay(durationMs); + setAllOff(); +} + } \ No newline at end of file diff --git a/src/led_alert.h b/src/led_alert.h index a31e7b3..91c976c 100644 --- a/src/led_alert.h +++ b/src/led_alert.h @@ -7,17 +7,19 @@ 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 + Off, + ProximityGreen, + EmergencyRed, }; - // Einmalig in setup() aufrufen. void begin(); - - // 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); + + void pulseHeartbeat(uint32_t nowMs); + + // Blockierendes, kurzes weisses Aufblitzen (alle 3 Farbkanaele an) als + // sofortige Bestaetigung fuer eine einmalige Nutzeraktion (z.B. "Cam"- + // Button getroffen). Bewusst blockierend (delay), da nur aus dem + // Haupt-Loop bei einem Tap aufgerufen, nicht aus der NetTask-Schleife. + void flashWhite(uint32_t durationMs = 150); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 662b884..5b1246b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -109,6 +109,8 @@ void updateStatusLine() { } void takeScreenshotWithFeedback() { + LedAlert::flashWhite(); + String filename = Screenshot::save(tft); tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_NAVY); @@ -126,11 +128,6 @@ void takeScreenshotWithFeedback() { } void haltWithSdRequiredScreen() { - // Ohne SD-Karte kann SettingsStore::load() nie laufen, also ist auch die - // Invertierungs-Einstellung dieses Geraets unbekannt. Dieses konkrete - // CYD-Board braucht invertDisplay(true), um Farben korrekt darzustellen - // (ohne das erscheint Schwarz als Weiss und Gruen als Rosa/Magenta) - - // hier fest setzen, damit Schwarz/Gruen GARANTIERT korrekt aussieht. tft.invertDisplay(true); tft.fillScreen(TFT_BLACK); diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 8f9b480..6378ce7 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -17,9 +17,9 @@ namespace { } }; - constexpr int16_t ROW_H = 26; - constexpr int16_t ROW_GAP = 3; - constexpr int16_t ROW_START_Y = 24; + constexpr int16_t ROW_H = 24; + constexpr int16_t ROW_GAP = 2; + constexpr int16_t ROW_START_Y = 20; Rect rowRect(uint8_t index) { return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)), @@ -44,16 +44,17 @@ void run(TFT_eSPI& tft) { Rect wifiBtn = rowRect(2); Rect emergencyBtn = rowRect(3); Rect proximityBtn = rowRect(4); - Rect logbookBtn = rowRect(5); - Rect statsBtn = rowRect(6); - Rect logFilesBtn = rowRect(7); - Rect backBtn = rowRect(8); + Rect heartbeatBtn = rowRect(5); + Rect logbookBtn = rowRect(6); + Rect statsBtn = rowRect(7); + Rect logFilesBtn = rowRect(8); + Rect backBtn = rowRect(9); bool done = false; while (!done) { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.setCursor(10, 6); + tft.setCursor(10, 4); tft.println("Settings"); drawButton(tft, calibBtn, "Calibrate touch"); @@ -67,6 +68,7 @@ void run(TFT_eSPI& tft) { drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled())); drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled())); + drawButton(tft, heartbeatBtn, "LED heartbeat: " + onOff(SettingsStore::ledHeartbeatEnabled())); drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled())); drawButton(tft, statsBtn, "Statistics"); drawButton(tft, logFilesBtn, "Logbook files"); @@ -91,6 +93,8 @@ void run(TFT_eSPI& tft) { SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled()); } else if (proximityBtn.contains(tap.x, tap.y)) { SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); + } else if (heartbeatBtn.contains(tap.x, tap.y)) { + SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled()); } else if (logbookBtn.contains(tap.x, tap.y)) { SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled()); } else if (statsBtn.contains(tap.x, tap.y)) { diff --git a/src/net_task.cpp b/src/net_task.cpp index 22cb49b..29ac1c9 100644 --- a/src/net_task.cpp +++ b/src/net_task.cpp @@ -8,6 +8,7 @@ #include "settings_store.h" #include "aircraft_details.h" #include "flight_logbook.h" +#include "led_alert.h" #include #include #include @@ -50,6 +51,14 @@ namespace { AircraftTable::unlock(); FlightLogbook::update(); + + // Kurzer gruener LED-Blitz als "Herzschlag" - zeigt, + // dass gerade eine Abfrage gelaufen ist. Wird von + // LedAlert::update() automatisch ignoriert, solange + // ein Naeherungs-/Notfall-Alarm aktiv ist. + if (SettingsStore::ledHeartbeatEnabled()) { + LedAlert::pulseHeartbeat(millis()); + } } else { Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode); } diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index f17ae8a..471c16a 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -9,6 +9,7 @@ #include "settings_store.h" #include "led_alert.h" #include "location_manager.h" +#include "world_map.h" #include namespace RadarScreen { @@ -211,6 +212,37 @@ namespace { } } + // Dezente Weltkarten-Silhouette als Hintergrund-Layer, wie bei einem + // klassischen ATC-Radarschirm - rein dekorativ (nicht massstabsgetreu + // zu den Entfernungsringen). Wird auf das quadratische Kreis-Layout + // gestreckt (die Karte ist 2:1, der Kreis-Bereich 1:1 - das verzerrt + // die Kontinente etwas in der Hoehe, faellt aber bei dieser Groesse + // kaum auf und haelt die Zeichenlogik einfach). Wird NUR bei render() + // gezeichnet, NICHT bei jedem Sweep-Tick (zu teuer fuer 12x/Sekunde). + void drawWorldMap(TFT_eSPI& gfx, const Layout& L) { + constexpr uint16_t dim = 0x0320; // sehr gedaempftes Gruen + float scaleX = (2.0f * L.radius) / WorldMap::GRID_W; + float scaleY = (2.0f * L.radius) / WorldMap::GRID_H; + int32_t radiusSq = (int32_t)(L.radius - 2) * (L.radius - 2); + + for (uint8_t row = 0; row < WorldMap::GRID_H; row++) { + uint64_t bits = WorldMap::ROWS[row]; + if (bits == 0) continue; + for (uint8_t col = 0; col < WorldMap::GRID_W; col++) { + if (!(bits & (1ULL << (WorldMap::GRID_W - 1 - col)))) continue; + + int16_t px = L.cx - L.radius + (int16_t)((col + 0.5f) * scaleX); + int16_t py = L.cy - L.radius + (int16_t)((row + 0.5f) * scaleY); + + int16_t dx = px - L.cx; + int16_t dy = py - L.cy; + if ((int32_t)dx * dx + (int32_t)dy * dy > radiusSq) continue; + + gfx.drawPixel(px, py, dim); + } + } + } + void drawSweepLine(TFT_eSPI& gfx, const Layout& L, float angleDeg, uint16_t color) { double rad = angleDeg * DEG_TO_RAD; int16_t x2 = L.cx + (int16_t)(L.radius * sin(rad)); @@ -368,6 +400,7 @@ void render(TFT_eSPI& tft, int16_t top) { tft.fillRect(0, top, Config::SCREEN_WIDTH, Config::SCREEN_HEIGHT - top, TFT_BLACK); + drawWorldMap(tft, L); drawStaticBackground(tft, L, rangeKm); drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN); diff --git a/src/settings_store.cpp b/src/settings_store.cpp index d8b1f98..b778dbf 100644 --- a/src/settings_store.cpp +++ b/src/settings_store.cpp @@ -10,6 +10,7 @@ namespace { bool emergencyAlertOn = true; bool proximityAlertOn = true; bool flightLogbookOn = true; + bool ledHeartbeatOn = true; void applyKeyValue(const String& key, const String& value) { if (key == "range_index") { @@ -25,20 +26,26 @@ namespace { proximityAlertOn = (value.toInt() != 0); } else if (key == "flight_logbook") { flightLogbookOn = (value.toInt() != 0); + } else if (key == "led_heartbeat") { + ledHeartbeatOn = (value.toInt() != 0); } } } 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(); @@ -56,24 +63,52 @@ void save() { 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.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 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(); } + +void setEmergencyAlertEnabled(bool on) { + emergencyAlertOn = on; + save(); +} bool proximityAlertEnabled() { return proximityAlertOn; } -void setProximityAlertEnabled(bool on) { proximityAlertOn = on; save(); } + +void setProximityAlertEnabled(bool on) { + proximityAlertOn = on; + save(); +} bool flightLogbookEnabled() { return flightLogbookOn; } -void setFlightLogbookEnabled(bool on) { flightLogbookOn = on; save(); } + +void setFlightLogbookEnabled(bool on) { + flightLogbookOn = on; + save(); +} + +bool ledHeartbeatEnabled() { return ledHeartbeatOn; } + +void setLedHeartbeatEnabled(bool on) { + ledHeartbeatOn = on; + save(); +} } \ No newline at end of file diff --git a/src/settings_store.h b/src/settings_store.h index 8f3875d..cd21b35 100644 --- a/src/settings_store.h +++ b/src/settings_store.h @@ -19,4 +19,10 @@ namespace SettingsStore { bool flightLogbookEnabled(); void setFlightLogbookEnabled(bool on); + + // Kurzes gruenes Aufblitzen der Rueckseiten-LED synchron zu jeder + // ADS-B-Abfrage (alle 8s) - zeigt "Geraet ist aktiv", auch wenn gerade + // kein Naeherungs-/Notfall-Alarm laeuft. + bool ledHeartbeatEnabled(); + void setLedHeartbeatEnabled(bool on); } \ No newline at end of file diff --git a/src/splash_screen.cpp b/src/splash_screen.cpp index bb70b1e..2eb76f2 100644 --- a/src/splash_screen.cpp +++ b/src/splash_screen.cpp @@ -15,28 +15,30 @@ namespace { // 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); + tft.drawCircle(cx, cy, 80, dim); + tft.drawCircle(cx, cy, 54, dim); + tft.drawCircle(cx, cy, 29, dim); + tft.drawFastHLine(cx - 80, cy, 160, dim); + tft.drawFastVLine(cx, cy - 80, 160, 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. + // Etwas kleiner als frueher, damit er sauber zwischen Titel und + // Statuszeilen passt, ohne diese zu ueberlappen. void drawAirplane(TFT_eSPI& tft, int16_t cx) { uint16_t color = TFT_GREEN; // Schlanker Rumpf (nur Umriss) - tft.drawTriangle(cx, 105, cx - 6, 255, cx + 6, 255, color); + tft.drawTriangle(cx, 116, cx - 4, 223, cx + 4, 223, 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); + tft.drawTriangle(cx, 155, cx - 71, 209, cx, 184, color); + tft.drawTriangle(cx, 155, cx + 71, 209, cx, 184, color); // Kleines Leitwerk (Hoehenruder) am Heck (nur Umriss) - tft.drawTriangle(cx, 250, cx - 22, 268, cx + 22, 268, color); + tft.drawTriangle(cx, 219, cx - 16, 232, cx + 16, 232, color); } } @@ -46,7 +48,7 @@ void begin(TFT_eSPI& tft) { int16_t cx = tft.width() / 2; tft.fillScreen(TFT_BLACK); - drawRadarReticle(tft, cx, 185); + drawRadarReticle(tft, cx, 174); drawAirplane(tft, cx); tft.setTextDatum(MC_DATUM); diff --git a/src/world_map.cpp b/src/world_map.cpp new file mode 100644 index 0000000..592a8a6 --- /dev/null +++ b/src/world_map.cpp @@ -0,0 +1,40 @@ +#include "world_map.h" + +namespace WorldMap { + +const uint64_t ROWS[GRID_H] = { + 0x0000000000000000ULL, + 0x0000F7FC00000000ULL, + 0x001B07F000107080ULL, + 0x1C0DF3E0080FFFF0ULL, + 0x3FFF31883DFFFFFFULL, + 0x19FE38006FFFFF88ULL, + 0x00FFBC037FFFFF80ULL, + 0x003FF601FFFFFF80ULL, + 0x003FE003B9FFFE00ULL, + 0x003FE000C3FFFE80ULL, + 0x001FC003EFFFFC00ULL, + 0x00060007FD9FF800ULL, + 0x00060007FFC67000ULL, + 0x00008007FE846400ULL, + 0x00007803FF804000ULL, + 0x00003E003F004800ULL, + 0x00003F803E002980ULL, + 0x00003FC03E000E00ULL, + 0x00003F803E000340ULL, + 0x00000F803C8007C0ULL, + 0x00001E001C000FE0ULL, + 0x00001E00180007E0ULL, + 0x00001C0000000060ULL, + 0x0000100000000002ULL, + 0x0000100000000000ULL, + 0x0000100000000000ULL, + 0x0000000000000000ULL, + 0x0000000000000000ULL, + 0x0000180077F7FFF8ULL, + 0x03FFF00FFFFFFFF8ULL, + 0x0FFFF07FFFFFFFF8ULL, + 0xFFFFFFFFFFFFFFFFULL, +}; + +} \ No newline at end of file diff --git a/src/world_map.h b/src/world_map.h new file mode 100644 index 0000000..edfc672 --- /dev/null +++ b/src/world_map.h @@ -0,0 +1,14 @@ +#pragma once +#include + +// Sehr stark vereinfachte Weltkarte (Kontinent-Umrisse) als 64x32-Raster- +// Bitmap, generiert aus Natural-Earth-Daten (1:110m-Aufloesung, die groebste +// oeffentlich verfuegbare Stufe - genau richtig fuer ein kleines Display). +// Jede Zeile ist ein 64-Bit-Wert; Bit (63-i) gesetzt = Landflaeche an +// Spalte i. Equirektangulare Projektion: Zeile 0 = Nordpol, Zeile 31 = +// Suedpol, Spalte 0 = -180 Grad Laenge, Spalte 63 = +180 Grad Laenge. +namespace WorldMap { + constexpr uint8_t GRID_W = 64; + constexpr uint8_t GRID_H = 32; + extern const uint64_t ROWS[GRID_H]; +} \ No newline at end of file