diff --git a/src/location_presets_screen.cpp b/src/location_presets_screen.cpp index 7fe2564..a0b89d5 100644 --- a/src/location_presets_screen.cpp +++ b/src/location_presets_screen.cpp @@ -28,10 +28,10 @@ namespace { uint8_t len = 0; constexpr int16_t KEY_W = 74; - constexpr int16_t KEY_H = 40; - constexpr int16_t KEY_GAP = 4; + constexpr int16_t KEY_H = 34; + constexpr int16_t KEY_GAP = 3; constexpr int16_t GRID_LEFT = (Config::SCREEN_WIDTH - 3 * KEY_W - 2 * KEY_GAP) / 2; - constexpr int16_t GRID_TOP = 90; + constexpr int16_t GRID_TOP = 84; const char* keys[12] = {"1","2","3","4","5","6","7","8","9","-","0","."}; Rect keyRects[12]; @@ -43,8 +43,12 @@ namespace { KEY_W, KEY_H}; } + // WICHTIG: Backspace muss VOR den Cancel/OK-Buttons enden (mit + // Abstand), sonst ueberlappen sich die Tap-Flaechen - Backspace wird + // in der Pruef-Reihenfolge zuerst getestet, wuerde also faelschlich + // jeden Tap auf "Cancel" abfangen (Bug: Cancel-Button reagierte nicht). Rect backspaceBtn = {(int16_t)GRID_LEFT, (int16_t)(GRID_TOP + 4 * (KEY_H + KEY_GAP)), - (int16_t)(3 * KEY_W + 2 * KEY_GAP), 36}; + (int16_t)(3 * KEY_W + 2 * KEY_GAP), 30}; Rect cancelBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), 110, 40}; Rect confirmBtn = {(int16_t)(Config::SCREEN_WIDTH - 120), (int16_t)(Config::SCREEN_HEIGHT - 50), 110, 40}; diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index acc2dc7..6f80cac 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -35,11 +35,12 @@ namespace { Layout computeLayout(int16_t top) { Layout L; L.infoTop = Config::SCREEN_HEIGHT - INFO_BAR_H; + constexpr int16_t TOP_LABEL_MARGIN = 10; int16_t maxRadiusByWidth = Config::SCREEN_WIDTH / 2 - 6; - int16_t maxRadiusByHeight = (L.infoTop - top) / 2 - 6; + int16_t maxRadiusByHeight = (L.infoTop - top - TOP_LABEL_MARGIN) / 2 - 6; L.radius = min(maxRadiusByWidth, maxRadiusByHeight); L.cx = Config::SCREEN_WIDTH / 2; - L.cy = top + L.radius + 6; + L.cy = top + L.radius + 6 + TOP_LABEL_MARGIN; L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 70), (int16_t)(L.infoTop + 4), 62, 22}; return L; } @@ -112,6 +113,7 @@ namespace { int16_t ys[TRAIL_LEN] = {0}; uint8_t count = 0; uint32_t lastUpdateMs = 0; + uint16_t dimColor = TFT_DARKGREEN; }; constexpr uint8_t MAX_TRAILS = Config::MAX_TRACKED_AIRCRAFT; TrailEntry trails[MAX_TRAILS]; @@ -452,8 +454,10 @@ void render(TFT_eSPI& tft, int16_t top) { bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk); TrailEntry* trail = findOrCreateTrail(a.hex); - drawTrail(tft, trail, dimColorForAltitude(a.altBaroFt)); + uint16_t trailColor = dimColorForAltitude(a.altBaroFt); + drawTrail(tft, trail, trailColor); pushTrailPoint(trail, pt.x, pt.y); + if (trail) trail->dimColor = trailColor; if (isSelected) { tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE); @@ -466,7 +470,13 @@ void render(TFT_eSPI& tft, int16_t top) { tft.drawCircle(pt.x, pt.y, 12, TFT_RED); } - tft.setTextColor(color, TFT_BLACK); + // Rufzeichen als kleines farbiges Label ueber dem Punkt + // Transparent zeichnen (KEIN schwarzer Hintergrund-Kasten) - sonst + // "beisst" der Label-Hintergrund ein Stueck der Ring-/Fadenkreuz- + // Linie weg, wenn sich beides ueberschneidet. Da das Label bei jedem + // Sweep-Tick am selben Fleck neu gezeichnet wird, sah das wie + // Dauerflackern genau an den Beschriftungen aus. + tft.setTextColor(color); tft.setTextDatum(BC_DATUM); const char* label = a.callsign[0] ? a.callsign : a.hex; tft.drawString(label, pt.x, pt.y - 8); @@ -515,6 +525,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { Layout L = computeLayout(top); float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; + tft.startWrite(); + if (prevSweepAngleDeg >= 0.0f) { drawSweepLine(tft, L, prevSweepAngleDeg, TFT_BLACK); drawStaticBackground(tft, L, rangeKm); @@ -527,6 +539,21 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN); prevSweepAngleDeg = sweepAngleDeg; + for (uint8_t i = 0; i < MAX_TRAILS; i++) { + if (!trails[i].active) continue; + + bool stillVisible = false; + for (uint8_t j = 0; j < MAX_HIT_POINTS; j++) { + if (hitPoints[j].valid && strcmp(hitPoints[j].hex, trails[i].hex) == 0) { + stillVisible = true; + break; + } + } + if (stillVisible) { + drawTrail(tft, &trails[i], trails[i].dimColor); + } + } + for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) { if (!hitPoints[i].valid) continue; const HitPoint& hp = hitPoints[i]; @@ -543,12 +570,14 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { tft.drawCircle(hp.x, hp.y, 12, TFT_RED); } - tft.setTextColor(hp.color, TFT_BLACK); + tft.setTextColor(hp.color); tft.setTextDatum(BC_DATUM); const char* label = hp.callsign[0] ? hp.callsign : hp.hex; tft.drawString(label, hp.x, hp.y - 8); tft.setTextDatum(TL_DATUM); } + + tft.endWrite(); } bool handleTap(int16_t x, int16_t y, int16_t top) { diff --git a/src/splash_screen.cpp b/src/splash_screen.cpp index 76bfa8a..1b7fa66 100644 --- a/src/splash_screen.cpp +++ b/src/splash_screen.cpp @@ -10,10 +10,7 @@ namespace { constexpr int16_t STATUS_START_Y = 260; constexpr uint8_t MAX_STATUS_LINES = 3; - // 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. - // UNVERAENDERT gegenueber vorher - nur das Flugzeug wurde verkleinert/gedreht. + // Dezente konzentrische Ringe + Fadenkreuz als "Zielfernrohr"-Hintergrund void drawRadarReticle(TFT_eSPI& tft, int16_t cx, int16_t cy) { uint16_t dim = 0x0320; tft.drawCircle(cx, cy, 80, dim); @@ -23,10 +20,22 @@ namespace { tft.drawFastVLine(cx, cy - 80, 160, dim); } - // Flugzeug-Silhouette: 40% kleiner (Skalierung 0.6) und 25 Grad im - // Uhrzeigersinn gedreht (Nase zeigt jetzt leicht nach rechts), um den - // Reticle-Mittelpunkt (cx, 174) - der Radar-Kreis selbst bleibt exakt - // gleich gross wie vorher. + // Echte kleine Jet-Silhouette für den Hintergrund + void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { + tft.drawLine(x, y - 10, x, y + 12, color); + tft.drawLine(x - 12, y + 2, x + 12, y + 2, color); + tft.drawLine(x - 5, y + 10, x + 5, y + 10, color); + } + + // Echte kleine Helikopter-Silhouette für den Hintergrund + void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { + tft.drawLine(x - 8, y - 6, x + 8, y - 6, color); + tft.drawLine(x, y - 6, x, y + 4, color); + tft.drawCircle(x, y + 2, 4, color); + tft.drawLine(x - 4, y + 7, x + 4, y + 7, color); + } + + // Haupt-Flugzeug in der Mitte (Unverändert) void drawAirplane(TFT_eSPI& tft, int16_t cx) { uint16_t color = TFT_GREEN; @@ -49,6 +58,18 @@ void begin(TFT_eSPI& tft) { tft.fillScreen(TFT_BLACK); drawRadarReticle(tft, cx, 174); + + // Hintergrund-Flugzeuge & Helikopter dezent im Hintergrund - gleicher + // gedaempfter Gruenton wie die Radar-Ringe (0x0320), NICHT das volle + // TFT_GREEN (das war vorher gleich hell wie das zentrale Flugzeug und + // wirkte dadurch nicht wie ein dezenter Hintergrund). + uint16_t dimGreen = 0x0320; + drawMiniJet(tft, cx - 45, 135, dimGreen); + drawMiniJet(tft, cx + 50, 160, dimGreen); + drawMiniHeli(tft, cx - 35, 215, dimGreen); + drawMiniHeli(tft, cx + 45, 115, dimGreen); + + // Zentrales Flugzeug (unverändert im Vordergrund) drawAirplane(tft, cx); tft.setTextDatum(MC_DATUM); diff --git a/src/splash_screen.h b/src/splash_screen.h index 9c3f2fd..c575269 100644 --- a/src/splash_screen.h +++ b/src/splash_screen.h @@ -3,18 +3,7 @@ #include namespace SplashScreen { - // Zeichnet den Splash-Hintergrund (Titel + gruene Flugzeug-Silhouette auf - // schwarzem Grund) und merkt sich den Startzeitpunkt. NICHT blockierend - - // der Aufrufer kann danach normal weiter booten und Status-Zeilen - // draufschreiben (siehe setStatusLine()). void begin(TFT_eSPI& tft); - - // Schreibt/ueberschreibt eine der (begrenzt vielen) Status-Zeilen unten - // im Splash-Screen, z.B. "SD-Karte: OK" oder "WLAN verbunden". - // slot: 0 = erste Zeile, 1 = zweite Zeile, usw. void setStatusLine(TFT_eSPI& tft, uint8_t slot, const String& text, uint16_t color = TFT_WHITE); - - // Blockiert, bis seit begin() mindestens MIN_DISPLAY_MS vergangen sind. - // Direkt vor dem Verlassen des Splash-Screens aufrufen. void waitRemaining(); -} \ No newline at end of file +} diff --git a/src/wifi_setup_screen.cpp b/src/wifi_setup_screen.cpp index 52f0e9d..3a67237 100644 --- a/src/wifi_setup_screen.cpp +++ b/src/wifi_setup_screen.cpp @@ -78,7 +78,14 @@ namespace { char passwordBuf[64] = {0}; uint8_t passwordLen = 0; - bool shiftOn = false; + // Shift-Verhalten wie am Handy: einmal antippen = NUR der naechste + // Buchstabe wird gross geschrieben, danach automatisch wieder klein. + // Zweimal schnell hintereinander antippen (Doppeltipp) = Feststelltaste + // (bleibt an, bis erneut einmal angetippt wird). + enum class ShiftState { Off, OneShot, Locked }; + ShiftState shiftState = ShiftState::Off; + uint32_t lastShiftTapMs = 0; + constexpr uint32_t SHIFT_DOUBLE_TAP_MS = 400; uint8_t page = 0; bool skipped = false; @@ -110,7 +117,7 @@ namespace { String keyLabel(const KeyDef& k) { if (k.label) return String(k.label); - char c = shiftOn ? (char)toupper(k.ch) : k.ch; + char c = (shiftState != ShiftState::Off) ? (char)toupper(k.ch) : k.ch; return String(c); } @@ -124,13 +131,31 @@ namespace { switch (k.type) { case KeyType::Char: if (passwordLen < sizeof(passwordBuf) - 1) { - passwordBuf[passwordLen++] = shiftOn ? (char)toupper(k.ch) : k.ch; + bool upper = (shiftState != ShiftState::Off); + passwordBuf[passwordLen++] = upper ? (char)toupper(k.ch) : k.ch; passwordBuf[passwordLen] = 0; + // Nach genau einem Buchstaben automatisch wieder auf + // klein zurueckfallen - AUSSER die Feststelltaste + // (Doppeltipp) ist aktiv, die bleibt an. + if (shiftState == ShiftState::OneShot) shiftState = ShiftState::Off; } break; - case KeyType::Shift: - shiftOn = !shiftOn; + case KeyType::Shift: { + uint32_t nowMs = millis(); + bool isDoubleTap = (nowMs - lastShiftTapMs) <= SHIFT_DOUBLE_TAP_MS; + lastShiftTapMs = nowMs; + if (isDoubleTap) { + // Doppeltipp: Feststelltaste an-/ausschalten. + shiftState = (shiftState == ShiftState::Locked) ? ShiftState::Off : ShiftState::Locked; + } else if (shiftState == ShiftState::Off) { + // Einfacher Tipp: nur der naechste Buchstabe wird gross. + shiftState = ShiftState::OneShot; + } else { + // Einfacher Tipp waehrend OneShot/Locked schaltet ab. + shiftState = ShiftState::Off; + } break; + } case KeyType::Backspace: if (passwordLen > 0) { passwordLen--; @@ -153,7 +178,7 @@ namespace { Rect rects[N]; layoutRow(row, y, rects); for (size_t i = 0; i < N; i++) { - bool hl = (row[i].type == KeyType::Shift && shiftOn); + bool hl = (row[i].type == KeyType::Shift && shiftState != ShiftState::Off); drawButton(tft, rects[i], keyLabel(row[i]), hl); } } @@ -161,7 +186,7 @@ namespace { void resetKeyboardState() { passwordLen = 0; passwordBuf[0] = 0; - shiftOn = false; + shiftState = ShiftState::Off; page = 0; }