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:
213
src/aircraft_list_screen.cpp
Normal file
213
src/aircraft_list_screen.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
13
src/aircraft_list_screen.h
Normal file
13
src/aircraft_list_screen.h
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user