v2.7.2: Aircraft list screen, radar night dimming, and flash savings

New:
- Aircraft list (Menu -> Flight Options, top entry): sortable list
  (distance/altitude/callsign) of every aircraft currently in range,
  with the same altitude color-coding and emergency/watchlist border
  highlighting as the radar. Tapping a row jumps back to the radar
  with that aircraft's detail panel already open.
- Night dimming now also softens the radar screen itself (aircraft
  markers and the sweep line switch to darker tones) during the
  existing 22:00-06:00 window, reusing the same setting - no new
  toggle.

Improved:
- Removed the unused built-in TFT_eSPI number fonts (GLCD/Font2/
  Font4/Font6/Font7/Font8) from platformio.ini - the project only
  ever uses the custom UI font via setFreeFont(). Saves ~15KB flash.

Also documents a CYD-flightradar.bin staleness safety check in the
CLAUDE.md release workflow (delete any leftover copy from a previous
release before renaming the fresh build, so an old binary can never
be silently re-used for a GitHub release).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-06 23:03:58 +02:00
parent 537c1600d5
commit 5dd4e53258
15 changed files with 404 additions and 28 deletions

View File

@@ -0,0 +1,213 @@
#include "aircraft_list_screen.h"
#include "aircraft_table.h"
#include "aircraft.h"
#include "airline_filter.h"
#include "aircraft_watchlist.h"
#include "radar_screen.h"
#include "settings_store.h"
#include "touch_input.h"
#include "menu_stars.h"
#include "config.h"
#include "i18n.h"
#include <algorithm>
#include <cstring>
namespace AircraftListScreen {
namespace {
struct Rect {
int16_t x, y, w, h;
bool contains(int16_t px, int16_t py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
};
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) {
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_BLACK);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_GREEN);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
tft.setTextDatum(TL_DATUM);
}
// Gleiche Hoehen-Farblogik wie RadarScreen (dort intern, hier bewusst
// dupliziert - siehe CLAUDE.md-Konvention "jeder Screen unabhaengig").
// Auf die Nachtdimmung wird hier verzichtet: diese Liste ist ein Menue-
// Screen wie jeder andere, kein Dauerbild wie das Radar.
uint16_t colorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_GREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_YELLOW;
return TFT_RED;
}
bool isEmergencySquawk(const char* squawk) {
if (!squawk[0]) return false;
for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) {
if (strcmp(squawk, Config::EMERGENCY_SQUAWKS[i]) == 0) return true;
}
return false;
}
enum class SortMode : uint8_t { Distance, Altitude, Callsign };
// Bleibt bis zum naechsten Neustart erhalten (kein SD-Speichern noetig -
// das waere fuer eine reine Anzeige-Praeferenz unnoetiger Aufwand).
SortMode sortMode = SortMode::Distance;
const char* sortModeLabel() {
switch (sortMode) {
case SortMode::Altitude: return I18n::t(StringId::AIRCRAFT_LIST_SORT_ALTITUDE);
case SortMode::Callsign: return I18n::t(StringId::AIRCRAFT_LIST_SORT_CALLSIGN);
default: return I18n::t(StringId::AIRCRAFT_LIST_SORT_DISTANCE);
}
}
}
bool run(TFT_eSPI& tft) {
constexpr int16_t ROW_H = 28;
constexpr int16_t ROW_GAP = 4;
constexpr int16_t LIST_TOP = 62;
constexpr int16_t LIST_BOTTOM = Config::SCREEN_HEIGHT - 56;
constexpr uint8_t ROWS_VISIBLE = (LIST_BOTTOM - LIST_TOP) / (ROW_H + ROW_GAP);
uint8_t scrollTop = 0; // Index des ersten sichtbaren Eintrags in der sortierten Liste
bool selected = false;
bool done = false;
MenuStars::reset();
while (!done && !selected) {
static Aircraft snapshot[Config::MAX_TRACKED_AIRCRAFT];
uint8_t count = 0;
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
// Gleiche Filter wie das Radar (Reichweite, Bodenfahrzeuge,
// Airline-Filter), damit die Liste genau die Flugzeuge zeigt, die
// gerade auch als Punkte auf dem Radar zu sehen sind.
AircraftTable::lock();
Aircraft* table = AircraftTable::raw();
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
if (!table[i].valid) continue;
if (table[i].distanceKm > rangeKm * 1.05f) continue;
if (SettingsStore::hideGroundVehicles() && table[i].category[0] == 'C') continue;
if (AirlineFilter::isHidden(table[i].callsign)) continue;
snapshot[count++] = table[i];
}
AircraftTable::unlock();
std::sort(snapshot, snapshot + count, [](const Aircraft& a, const Aircraft& b) {
switch (sortMode) {
case SortMode::Altitude:
return a.altBaroFt < b.altBaroFt;
case SortMode::Callsign: {
const char* ca = a.callsign[0] ? a.callsign : a.hex;
const char* cb = b.callsign[0] ? b.callsign : b.hex;
return strcmp(ca, cb) < 0;
}
default:
return a.distanceKm < b.distanceKm;
}
});
if (scrollTop > 0 && scrollTop + ROWS_VISIBLE > count) {
scrollTop = (count > ROWS_VISIBLE) ? (uint8_t)(count - ROWS_VISIBLE) : 0;
}
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::AIRCRAFT_LIST_TITLE));
Rect sortBtn = {10, 26, (int16_t)(Config::SCREEN_WIDTH - 20), 24};
String sortLabel = String(I18n::t(StringId::AIRCRAFT_LIST_SORT_PREFIX)) + sortModeLabel();
drawButton(tft, sortBtn, sortLabel);
Rect rowRects[ROWS_VISIBLE];
uint8_t visibleRowCount = 0;
if (count == 0) {
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.setCursor(10, LIST_TOP + 14);
tft.println(I18n::t(StringId::AIRCRAFT_LIST_EMPTY));
} else {
int16_t y = LIST_TOP;
for (uint8_t i = scrollTop; i < count && visibleRowCount < ROWS_VISIBLE; i++) {
Aircraft& a = snapshot[i];
Rect r = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
rowRects[visibleRowCount] = r;
bool emergency = isEmergencySquawk(a.squawk);
bool watched = AircraftWatchlist::isWatched(a.callsign);
uint16_t borderColor = emergency ? TFT_RED : (watched ? TFT_CYAN : TFT_GREEN);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, borderColor);
// WICHTIG (siehe CLAUDE.md-Falle "Font ist Baseline-verankert"):
// Alle drei Textfelder ueber drawString() mit einem MITTIGEN
// Datum auf die Zeilenmitte zentrieren, statt Baseline- und
// Top-verankerte Aufrufe zu mischen - genau das hatte vorher
// dazu gefuehrt, dass Hoehe/Distanz unten aus der Box liefen,
// waehrend das Rufzeichen (Baseline-verankert) korrekt sass.
int16_t midY = r.y + r.h / 2;
const char* label = a.callsign[0] ? a.callsign : a.hex;
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(ML_DATUM);
tft.drawString(label, r.x + 6, midY);
char buf[24];
snprintf(buf, sizeof(buf), "%.0fft", (float)a.altBaroFt);
tft.setTextColor(colorForAltitude(a.altBaroFt), TFT_BLACK);
tft.setTextDatum(MR_DATUM);
tft.drawString(buf, r.x + r.w - 62, midY);
snprintf(buf, sizeof(buf), "%.0fkm", a.distanceKm);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(MR_DATUM);
tft.drawString(buf, r.x + r.w - 6, midY);
tft.setTextDatum(TL_DATUM);
y += ROW_H + ROW_GAP;
visibleRowCount++;
}
}
bool canUp = scrollTop > 0;
bool canDown = (uint16_t)(scrollTop + ROWS_VISIBLE) < count;
Rect upBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), 60, 40};
Rect downBtn = {76, (int16_t)(Config::SCREEN_HEIGHT - 50), 60, 40};
Rect backBtn = {146, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 156), 40};
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
drawButton(tft, backBtn, I18n::t(StringId::BACK));
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
if (sortBtn.contains(tap.x, tap.y)) {
sortMode = static_cast<SortMode>((static_cast<uint8_t>(sortMode) + 1) % 3);
scrollTop = 0;
} else if (canUp && upBtn.contains(tap.x, tap.y)) {
scrollTop--;
} else if (canDown && downBtn.contains(tap.x, tap.y)) {
scrollTop++;
} else if (backBtn.contains(tap.x, tap.y)) {
done = true;
} else {
for (uint8_t i = 0; i < visibleRowCount; i++) {
if (rowRects[i].contains(tap.x, tap.y)) {
RadarScreen::selectAircraft(snapshot[scrollTop + i].hex);
selected = true;
break;
}
}
}
}
return selected;
}
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
// Sortierbare Liste aller aktuell erkannten Flugzeuge (gleiche Filter wie
// das Radar: Reichweite, Bodenfahrzeuge, Airline-Filter). Antippen eines
// Eintrags springt zurueck zum Radar mit direkt geoeffnetem Detail-Panel.
namespace AircraftListScreen {
// Rueckgabewert true = der Nutzer hat ein Flugzeug angetippt (Aufrufer
// soll bis zum Radar zurueckspringen, nicht nur die eigene Seite
// schliessen). false = nur "Zurueck" gedrueckt, normal weiter im Menue.
bool run(TFT_eSPI& tft);
}

View File

@@ -178,6 +178,14 @@ enum class StringId : uint8_t {
WIFI_INFO_PARA2,
WIFI_INFO_PARA3,
MENU_AIRCRAFT_LIST,
AIRCRAFT_LIST_TITLE,
AIRCRAFT_LIST_EMPTY,
AIRCRAFT_LIST_SORT_PREFIX,
AIRCRAFT_LIST_SORT_DISTANCE,
AIRCRAFT_LIST_SORT_ALTITUDE,
AIRCRAFT_LIST_SORT_CALLSIGN,
COUNT
};

View File

@@ -178,6 +178,14 @@ static const char* const I18N_DE[] = {
"Du kannst bis zu 3 WLAN-Netzwerke speichern. Beim Start verbindet sich das Gerät automatisch mit dem, das gerade erreichbar ist.",
"Praktisch für unterwegs (Büro, Freunde, Ferienhaus), für Gast- und Haupt-WLAN am selben Router, oder wenn du mehrere Access Points im Haus hast.",
"Über das rote 'X' kannst du ein gespeichertes Netzwerk jederzeit wieder entfernen, um Platz für ein neues zu schaffen.",
"Flugzeugliste",
"Flugzeuge in Reichweite",
"Keine Flugzeuge in Reichweite",
"Sortiert: ",
"Distanz",
"Höhe",
"Rufzeichen",
};
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,

View File

@@ -179,6 +179,14 @@ static const char* const I18N_EN[] = {
"You can save up to 3 WiFi networks. On boot, the device automatically connects to whichever one it can currently reach.",
"Handy for portable use (office, friends, holiday home), for guest + main WiFi on the same router, or if you have multiple access points around the house.",
"Remove a saved network anytime via the red 'X' to free up a slot for a new one.",
"Aircraft list",
"Aircraft in range",
"No aircraft in range",
"Sorted by: ",
"Distance",
"Altitude",
"Callsign",
};
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,

View File

@@ -178,6 +178,14 @@ static const char* const I18N_ES[] = {
"Puede guardar hasta 3 redes WiFi. Al iniciar, el dispositivo se conecta automáticamente a la que pueda alcanzar en ese momento.",
"Útil para uso móvil (oficina, amigos, casa de vacaciones), para WiFi de invitados + principal en el mismo router, o si tiene varios puntos de acceso en casa.",
"Elimine una red guardada en cualquier momento con la 'X' roja para liberar un espacio para una nueva.",
"Lista de aviones",
"Aviones en alcance",
"Ningún avión en alcance",
"Ordenado por: ",
"Distancia",
"Altitud",
"Indicativo",
};
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,

View File

@@ -178,6 +178,14 @@ static const char* const I18N_FR[] = {
"Vous pouvez enregistrer jusqu'à 3 réseaux WiFi. Au démarrage, l'appareil se connecte automatiquement à celui qu'il peut atteindre.",
"Pratique en déplacement (bureau, amis, maison de vacances), pour un WiFi invité + principal sur le même routeur, ou si vous avez plusieurs points d'accès.",
"Supprimez un réseau enregistré à tout moment via le 'X' rouge pour libérer une place pour un nouveau.",
"Liste des avions",
"Avions à portée",
"Aucun avion à portée",
"Trié par : ",
"Distance",
"Altitude",
"Indicatif",
};
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,

View File

@@ -178,6 +178,14 @@ static const char* const I18N_IT[] = {
"Puoi salvare fino a 3 reti WiFi. All'avvio, il dispositivo si connette automaticamente a quella che riesce a raggiungere in quel momento.",
"Utile per l'uso portatile (ufficio, amici, casa vacanze), per WiFi ospiti + principale sullo stesso router, o se hai più punti di accesso in casa.",
"Rimuovi una rete salvata in qualsiasi momento tramite la 'X' rossa per liberare uno slot per una nuova.",
"Elenco aerei",
"Aerei nel raggio",
"Nessun aereo nel raggio",
"Ordinato per: ",
"Distanza",
"Altitudine",
"Nominativo",
};
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,

View File

@@ -178,6 +178,14 @@ static const char* const I18N_TR[] = {
"3 adete kadar WiFi ağı kaydedebilirsiniz. Başlangıçta cihaz, o an erişebildiği ağa otomatik olarak bağlanır.",
"Taşınabilir kullanım için kullanışlıdır (ofis, arkadaşlar, tatil evi), aynı yönlendiricide misafir + ana WiFi için, veya evde birden fazla erişim noktanız varsa.",
"Kayıtlı bir ağı istediğiniz zaman kırmızı 'X' ile kaldırarak yeni bir ağ için yer açabilirsiniz.",
"Uçak listesi",
"Menzildeki uçaklar",
"Menzilde uçak yok",
"Sıralama: ",
"Mesafe",
"İrtifa",
"Çağrı işareti",
};
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,

View File

@@ -8,6 +8,7 @@
#include "location_presets_screen.h"
#include "airline_filter_screen.h"
#include "aircraft_watchlist_screen.h"
#include "aircraft_list_screen.h"
#include "language_screen.h"
#include "units_screen.h"
#include "settings_backup.h"
@@ -44,6 +45,16 @@ namespace {
(int16_t)(Config::SCREEN_WIDTH - 20), CAT_ROW_H};
}
// Eigene, etwas kompaktere Zeilenhoehe NUR fuer die Flugoptionen-Seite:
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
// bei der normalen Hoehe unveraendert, um dort nichts zu verschieben.
constexpr int16_t FLIGHT_ROW_H = 20;
Rect flightRowRect(uint8_t index) {
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
(int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H};
}
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label,
bool active = false, bool danger = false) {
uint16_t accent = danger ? TFT_RED : TFT_GREEN;
@@ -213,20 +224,22 @@ void run(TFT_eSPI& tft) {
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT));
Rect statsBtn = rowRect(0);
Rect statsHistoryBtn = rowRect(1);
Rect logFilesBtn = rowRect(2);
Rect logbookBtn = rowRect(3);
Rect heartbeatBtn = rowRect(4);
Rect proximityBtn = rowRect(5);
Rect emergencyBtn = rowRect(6);
Rect locationBtn = rowRect(7);
Rect airlineBtn = rowRect(8);
Rect watchlistBtn = rowRect(9);
Rect watchlistAlertBtn = rowRect(10);
Rect groundBtn = rowRect(11);
Rect backBtn = rowRect(12);
Rect aircraftListBtn = flightRowRect(0);
Rect statsBtn = flightRowRect(1);
Rect statsHistoryBtn = flightRowRect(2);
Rect logFilesBtn = flightRowRect(3);
Rect logbookBtn = flightRowRect(4);
Rect heartbeatBtn = flightRowRect(5);
Rect proximityBtn = flightRowRect(6);
Rect emergencyBtn = flightRowRect(7);
Rect locationBtn = flightRowRect(8);
Rect airlineBtn = flightRowRect(9);
Rect watchlistBtn = flightRowRect(10);
Rect watchlistAlertBtn = flightRowRect(11);
Rect groundBtn = flightRowRect(12);
Rect backBtn = flightRowRect(13);
drawButton(tft, aircraftListBtn, I18n::t(StringId::MENU_AIRCRAFT_LIST));
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
drawButton(tft, statsHistoryBtn, I18n::t(StringId::MENU_STATS_HISTORY));
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
@@ -248,7 +261,14 @@ void run(TFT_eSPI& tft) {
delay(20);
}
if (statsBtn.contains(tap.x, tap.y)) {
if (aircraftListBtn.contains(tap.x, tap.y)) {
if (AircraftListScreen::run(tft)) {
// Ein Flugzeug wurde in der Liste ausgewaehlt - direkt bis
// zum Radar zurueckspringen (mit offenem Detail-Panel),
// statt in der Flugoptionen-Seite stehen zu bleiben.
done = true;
}
} else if (statsBtn.contains(tap.x, tap.y)) {
StatsScreen::run(tft);
} else if (statsHistoryBtn.contains(tap.x, tap.y)) {
StatsHistoryScreen::run(tft);

View File

@@ -14,6 +14,7 @@
#include "aircraft_watchlist.h"
#include "i18n.h"
#include <math.h>
#include <time.h>
namespace RadarScreen {
@@ -84,10 +85,44 @@ namespace {
float prevSweepAngleDeg = -1.0f;
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
uint16_t colorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_GREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_YELLOW;
return TFT_RED;
// Gleiches Nachtdimm-Fenster wie main.cpp::isNightDimHours() (22:00-06:00,
// ueber Mitternacht hinweg gerechnet). Hier bewusst dupliziert statt
// geteilt, siehe CLAUDE.md-Konvention ("jeder Screen unabhaengig
// lauffaehig, kein gemeinsames Modul fuer solche Kleinigkeiten").
bool isNightHours() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return false; // Uhrzeit noch nicht per NTP synchronisiert
struct tm tmNow;
localtime_r(&now, &tmNow);
int hour = tmNow.tm_hour;
return (hour >= 22 || hour < 6);
}
// Nur aktiv, wenn der Nutzer die Nachtdimmung (Menue > System) eingeschaltet
// hat UND wir gerade im Nachtfenster sind - dieselbe Einstellung, die sonst
// nur die Hintergrundbeleuchtung dimmt, dimmt jetzt zusaetzlich auch die
// Radar-Farben (Flugzeug-Marker + Sweep-Linie), damit das Display nachts
// insgesamt weniger blendet.
bool nightDimActiveNow() {
return SettingsStore::nightDimmingEnabled() && isNightHours();
}
// Farbe je nach Flughoehe - gedaempft (dunkleres Gruen/Oliv/Rot) waehrend
// der Nachtdimmung. Bewusst eigene, etwas hellere Toene als
// dimColorForAltitude() weiter unten (das ist die IMMER dunklere
// Trail-Farbe hinter jedem Flugzeug) - sonst waeren Kopf-Marker und
// Trail nachts nicht mehr auseinanderzuhalten.
uint16_t colorForAltitude(TFT_eSPI& gfx, int32_t altFt) {
bool dim = nightDimActiveNow();
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT)
return dim ? gfx.color565(0, 160, 0) : TFT_GREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT)
return dim ? gfx.color565(160, 160, 0) : TFT_YELLOW;
return dim ? gfx.color565(160, 30, 0) : TFT_RED;
}
uint16_t sweepLineColor(TFT_eSPI& gfx) {
return nightDimActiveNow() ? gfx.color565(0, 110, 0) : TFT_GREEN;
}
// Zeigt die PEILUNG (nicht zu verwechseln mit der Flugrichtung/heading)
@@ -518,7 +553,7 @@ void render(TFT_eSPI& tft, int16_t top) {
drawWorldMap(tft, L);
drawStaticBackground(tft, L, rangeKm);
drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN);
drawSweepLine(tft, L, sweepAngleDeg, sweepLineColor(tft));
prevSweepAngleDeg = sweepAngleDeg;
tft.fillCircle(L.cx, L.cy, 3, TFT_WHITE);
@@ -554,7 +589,7 @@ void render(TFT_eSPI& tft, int16_t top) {
RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg};
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
uint16_t color = colorForAltitude(a.altBaroFt);
uint16_t color = colorForAltitude(tft, a.altBaroFt);
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
@@ -671,7 +706,7 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
sweepAngleDeg += SWEEP_DEGREES_PER_SEC * (deltaMs / 1000.0f);
if (sweepAngleDeg >= 360.0f) sweepAngleDeg -= 360.0f;
drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN);
drawSweepLine(tft, L, sweepAngleDeg, sweepLineColor(tft));
prevSweepAngleDeg = sweepAngleDeg;
for (uint8_t i = 0; i < MAX_TRAILS; i++) {
@@ -771,6 +806,16 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
return true;
}
void selectAircraft(const char* hex) {
strncpy(selectedHex, hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hex);
// Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten
// render() - wichtig, falls der Bildschirm zwischenzeitlich von einem
// anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst
// wuerden unveraenderte Zeilen faelschlich als "schon da" uebersprungen.
lastPanel.valid = false;
}
void updateProximityAlert(uint32_t nowMs) {
bool anyClose = false;
bool anyEmergency = false;

View File

@@ -8,6 +8,12 @@ namespace RadarScreen {
bool handleTap(int16_t x, int16_t y, int16_t top);
void updateProximityAlert(uint32_t nowMs);
// Waehlt ein Flugzeug programmgesteuert aus (z.B. von der Flugzeugliste
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
// man es direkt im Radar angetippt.
void selectAircraft(const char* hex);
struct EmergencyInfo {
bool active = false;
char callsign[9] = {0};