v2.1: Fix persistent radar screen flickering (header overlap, label backgrounds, orphaned trails)

This commit is contained in:
Eiswolf-BG
2026-08-04 14:48:24 +02:00
parent 6e01e56c4c
commit 7fedf860e5
5 changed files with 104 additions and 36 deletions

View File

@@ -28,10 +28,10 @@ namespace {
uint8_t len = 0; uint8_t len = 0;
constexpr int16_t KEY_W = 74; constexpr int16_t KEY_W = 74;
constexpr int16_t KEY_H = 40; constexpr int16_t KEY_H = 34;
constexpr int16_t KEY_GAP = 4; 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_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","."}; const char* keys[12] = {"1","2","3","4","5","6","7","8","9","-","0","."};
Rect keyRects[12]; Rect keyRects[12];
@@ -43,8 +43,12 @@ namespace {
KEY_W, KEY_H}; 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)), 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 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}; Rect confirmBtn = {(int16_t)(Config::SCREEN_WIDTH - 120), (int16_t)(Config::SCREEN_HEIGHT - 50), 110, 40};

View File

@@ -35,11 +35,12 @@ namespace {
Layout computeLayout(int16_t top) { Layout computeLayout(int16_t top) {
Layout L; Layout L;
L.infoTop = Config::SCREEN_HEIGHT - INFO_BAR_H; 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 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.radius = min(maxRadiusByWidth, maxRadiusByHeight);
L.cx = Config::SCREEN_WIDTH / 2; 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}; L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 70), (int16_t)(L.infoTop + 4), 62, 22};
return L; return L;
} }
@@ -112,6 +113,7 @@ namespace {
int16_t ys[TRAIL_LEN] = {0}; int16_t ys[TRAIL_LEN] = {0};
uint8_t count = 0; uint8_t count = 0;
uint32_t lastUpdateMs = 0; uint32_t lastUpdateMs = 0;
uint16_t dimColor = TFT_DARKGREEN;
}; };
constexpr uint8_t MAX_TRAILS = Config::MAX_TRACKED_AIRCRAFT; constexpr uint8_t MAX_TRAILS = Config::MAX_TRACKED_AIRCRAFT;
TrailEntry trails[MAX_TRAILS]; TrailEntry trails[MAX_TRAILS];
@@ -452,8 +454,10 @@ void render(TFT_eSPI& tft, int16_t top) {
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk); bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
TrailEntry* trail = findOrCreateTrail(a.hex); 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); pushTrailPoint(trail, pt.x, pt.y);
if (trail) trail->dimColor = trailColor;
if (isSelected) { if (isSelected) {
tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE); 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.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); tft.setTextDatum(BC_DATUM);
const char* label = a.callsign[0] ? a.callsign : a.hex; const char* label = a.callsign[0] ? a.callsign : a.hex;
tft.drawString(label, pt.x, pt.y - 8); 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); Layout L = computeLayout(top);
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
tft.startWrite();
if (prevSweepAngleDeg >= 0.0f) { if (prevSweepAngleDeg >= 0.0f) {
drawSweepLine(tft, L, prevSweepAngleDeg, TFT_BLACK); drawSweepLine(tft, L, prevSweepAngleDeg, TFT_BLACK);
drawStaticBackground(tft, L, rangeKm); 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); drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN);
prevSweepAngleDeg = sweepAngleDeg; 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++) { for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) {
if (!hitPoints[i].valid) continue; if (!hitPoints[i].valid) continue;
const HitPoint& hp = hitPoints[i]; 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.drawCircle(hp.x, hp.y, 12, TFT_RED);
} }
tft.setTextColor(hp.color, TFT_BLACK); tft.setTextColor(hp.color);
tft.setTextDatum(BC_DATUM); tft.setTextDatum(BC_DATUM);
const char* label = hp.callsign[0] ? hp.callsign : hp.hex; const char* label = hp.callsign[0] ? hp.callsign : hp.hex;
tft.drawString(label, hp.x, hp.y - 8); tft.drawString(label, hp.x, hp.y - 8);
tft.setTextDatum(TL_DATUM); tft.setTextDatum(TL_DATUM);
} }
tft.endWrite();
} }
bool handleTap(int16_t x, int16_t y, int16_t top) { bool handleTap(int16_t x, int16_t y, int16_t top) {

View File

@@ -10,10 +10,7 @@ namespace {
constexpr int16_t STATUS_START_Y = 260; constexpr int16_t STATUS_START_Y = 260;
constexpr uint8_t MAX_STATUS_LINES = 3; constexpr uint8_t MAX_STATUS_LINES = 3;
// Dezente konzentrische Ringe + Fadenkreuz als "Zielfernrohr"-Hintergrund, // 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.
void drawRadarReticle(TFT_eSPI& tft, int16_t cx, int16_t cy) { void drawRadarReticle(TFT_eSPI& tft, int16_t cx, int16_t cy) {
uint16_t dim = 0x0320; uint16_t dim = 0x0320;
tft.drawCircle(cx, cy, 80, dim); tft.drawCircle(cx, cy, 80, dim);
@@ -23,10 +20,22 @@ namespace {
tft.drawFastVLine(cx, cy - 80, 160, dim); tft.drawFastVLine(cx, cy - 80, 160, dim);
} }
// Flugzeug-Silhouette: 40% kleiner (Skalierung 0.6) und 25 Grad im // Echte kleine Jet-Silhouette für den Hintergrund
// Uhrzeigersinn gedreht (Nase zeigt jetzt leicht nach rechts), um den void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) {
// Reticle-Mittelpunkt (cx, 174) - der Radar-Kreis selbst bleibt exakt tft.drawLine(x, y - 10, x, y + 12, color);
// gleich gross wie vorher. 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) { void drawAirplane(TFT_eSPI& tft, int16_t cx) {
uint16_t color = TFT_GREEN; uint16_t color = TFT_GREEN;
@@ -49,6 +58,18 @@ void begin(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK); tft.fillScreen(TFT_BLACK);
drawRadarReticle(tft, cx, 174); 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); drawAirplane(tft, cx);
tft.setTextDatum(MC_DATUM); tft.setTextDatum(MC_DATUM);

View File

@@ -3,18 +3,7 @@
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
namespace SplashScreen { 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); 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); 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(); void waitRemaining();
} }

View File

@@ -78,7 +78,14 @@ namespace {
char passwordBuf[64] = {0}; char passwordBuf[64] = {0};
uint8_t passwordLen = 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; uint8_t page = 0;
bool skipped = false; bool skipped = false;
@@ -110,7 +117,7 @@ namespace {
String keyLabel(const KeyDef& k) { String keyLabel(const KeyDef& k) {
if (k.label) return String(k.label); 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); return String(c);
} }
@@ -124,13 +131,31 @@ namespace {
switch (k.type) { switch (k.type) {
case KeyType::Char: case KeyType::Char:
if (passwordLen < sizeof(passwordBuf) - 1) { 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; 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; break;
case KeyType::Shift: case KeyType::Shift: {
shiftOn = !shiftOn; 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; break;
}
case KeyType::Backspace: case KeyType::Backspace:
if (passwordLen > 0) { if (passwordLen > 0) {
passwordLen--; passwordLen--;
@@ -153,7 +178,7 @@ namespace {
Rect rects[N]; Rect rects[N];
layoutRow(row, y, rects); layoutRow(row, y, rects);
for (size_t i = 0; i < N; i++) { 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); drawButton(tft, rects[i], keyLabel(row[i]), hl);
} }
} }
@@ -161,7 +186,7 @@ namespace {
void resetKeyboardState() { void resetKeyboardState() {
passwordLen = 0; passwordLen = 0;
passwordBuf[0] = 0; passwordBuf[0] = 0;
shiftOn = false; shiftState = ShiftState::Off;
page = 0; page = 0;
} }