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:
23
CLAUDE.md
23
CLAUDE.md
@@ -193,6 +193,29 @@ in dieser Reihenfolge:
|
||||
sonst schlägt `pio run --target upload` fehl, weil es die Datei unter
|
||||
dem Namen `firmware.bin` erwartet.
|
||||
|
||||
SICHERHEITSPROBLEM bei dieser Umbenennung: Falls im
|
||||
`.pio/build/esp32dev/`-Ordner bereits eine `CYD-flightradar.bin` von
|
||||
einem vorherigen Release liegt (weil Alex vergessen hat, sie nach dem
|
||||
Release manuell zu löschen), darf das Umbenennen NICHT einfach
|
||||
übersprungen werden (z.B. weil ein `mv`/Kopiervorgang auf eine bereits
|
||||
existierende Zieldatei fehlschlägt oder stillschweigend nichts tut) -
|
||||
sonst verwendet Alex beim nächsten GitHub-Release versehentlich die
|
||||
ALTE, veraltete `CYD-flightradar.bin`, obwohl der frisch gebaute Code
|
||||
neuer ist. Das ist ein ernsthaftes Risiko (veraltete Firmware wird
|
||||
veröffentlicht, ohne dass es auffällt). Deshalb bei JEDEM Release als
|
||||
expliziter, nicht überspringbarer Schritt:
|
||||
a) Prüfen, ob im `.pio/build/esp32dev/`-Ordner bereits eine Datei
|
||||
namens `CYD-flightradar.bin` existiert.
|
||||
b) Falls ja: diese alte Datei IMMER ZUERST LÖSCHEN, bevor die aktuelle
|
||||
`firmware.bin` umbenannt wird (nicht überspringen, nicht
|
||||
stillschweigend stehen lassen).
|
||||
c) Danach die aktuelle `firmware.bin` zu `CYD-flightradar.bin`
|
||||
umbenennen (wie oben beschrieben).
|
||||
d) Am Ende der Release-Zusammenfassung IMMER explizit erwähnen, ob eine
|
||||
alte `CYD-flightradar.bin` gefunden und gelöscht wurde, damit Alex
|
||||
das mitbekommt (z.B. "Hinweis: eine alte CYD-flightradar.bin lag
|
||||
noch im Ordner und wurde vor dem Umbenennen gelöscht").
|
||||
|
||||
## Nach jedem erfolgreichen Build automatisch flashen
|
||||
|
||||
Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt
|
||||
|
||||
@@ -33,10 +33,11 @@ You can flash the firmware directly from your browser to your CYD display withou
|
||||
- **Bearing indicator** – with an aircraft selected, a dotted line plus a heading-in-degrees label points from the radar center to the compass edge, showing exactly which direction to look to spot it in the sky (not to be confused with the aircraft's own heading arrow)
|
||||
- **Empty-sky timer** – when no aircraft are currently in range, the info bar counts up how long the sky's been empty instead of showing the usual "tap for details" hint
|
||||
- **Adjustable range** (10/25/50/100 km) via on-screen button
|
||||
- **Aircraft list view** – a sortable list (by distance, altitude, or callsign) of every aircraft currently in range, tap any row to jump straight to its detail panel (see [Aircraft List](#-aircraft-list) below)
|
||||
- **Touch-driven WiFi setup** and on-screen keyboard for first-time configuration, with up to 3 saved networks (see [WiFi Manager](#-wifi-manager-up-to-3-saved-networks) below)
|
||||
- **Location presets** – save up to 3 fixed locations, or point the radar at any place in the world (see [Location Presets](#-location-presets) below)
|
||||
- **Aircraft watchlist** – track up to 5 specific flights by callsign, with a cyan radar ring and blue LED alert when one appears (see [Watchlist](#-watchlist) below)
|
||||
- **Night dimming** – automatically dims the backlight to a softer (but still readable) level between 22:00 and 06:00 local time (see [Night Dimming](#-night-dimming) below)
|
||||
- **Night dimming** – automatically dims the backlight, plus the radar's aircraft markers and sweep line, to a softer (but still readable) look between 22:00 and 06:00 local time (see [Night Dimming](#-night-dimming) below)
|
||||
- **History chart** – a simple 7-day bar chart of logged aircraft counts, right next to the plain logbook file list (see [History Chart](#-history-chart) below)
|
||||
- **Web export** – a small built-in web page (reachable via the device's IP while on the same WiFi) to download the complete flight logbook as one merged CSV file (see [Web Export](#-web-export) below)
|
||||
- **IP-based geolocation** (no GPS module needed) to center the radar on your location
|
||||
@@ -97,10 +98,15 @@ Between 22:00 and 06:00 local time, the backlight automatically dims to a softer
|
||||
|
||||
Toggle it under **Menu → System → "Night dimming"**. When you tap the screen while it's night-dimmed, it wakes back up to the soft night level (not full brightness) if it's still within the night window - no sudden bright flash in a dark room.
|
||||
|
||||
Since this update, the same night window also softens the **radar screen itself** — aircraft markers and the rotating sweep line switch to darker green/yellow/red tones, so the whole display is easier on the eyes at night, not just the backlight. No separate toggle needed - it uses the same **Night dimming** setting.
|
||||
|
||||
## ✈️ Flight Options — every function in detail
|
||||
|
||||
All the functions below are found under **Menu → Flight Options**.
|
||||
|
||||
### 📋 Aircraft List
|
||||
Menu → Flight Options → **"Aircraft list"** (top entry) shows every aircraft currently visible on the radar as a scrollable, sortable list instead of dots — handy for a quick text overview instead of parsing the radar view. Shows callsign, altitude (color-coded the same way as the radar: green/yellow/red), and distance for each entry. A button at the top cycles the sort order through **distance → altitude → callsign**. Emergency squawks get a red border, watched aircraft (see [Watchlist](#-watchlist)) a cyan one, matching the radar's own markers. Tap any row to jump straight back to the radar with that aircraft's detail panel already open.
|
||||
|
||||
### 📊 Statistics
|
||||
Shows at a glance:
|
||||
- **Aircraft logged today** – number of distinct aircraft first seen today
|
||||
|
||||
@@ -27,11 +27,5 @@ build_flags =
|
||||
-D SPI_FREQUENCY=55000000
|
||||
-D SPI_READ_FREQUENCY=20000000
|
||||
-D SPI_TOUCH_FREQUENCY=2500000
|
||||
-D LOAD_GLCD=1
|
||||
-D LOAD_FONT2=1
|
||||
-D LOAD_FONT4=1
|
||||
-D LOAD_FONT6=1
|
||||
-D LOAD_FONT7=1
|
||||
-D LOAD_FONT8=1
|
||||
-D LOAD_GFXFF=1
|
||||
-D SMOOTH_FONT=1
|
||||
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