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;
|
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};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user