v2.1: Fix persistent radar screen flickering (header overlap, label backgrounds, orphaned trails)
This commit is contained in:
@@ -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};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,18 +3,7 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user