v1.5: World map background, LED heartbeat pulse, screenshot flash confirmation, splash screen resize

This commit is contained in:
Eiswolf-BG
2026-08-03 16:21:55 +02:00
parent ff2d3af420
commit 4e8bfcd30c
11 changed files with 210 additions and 37 deletions

View File

@@ -15,6 +15,10 @@ namespace {
uint32_t lastToggleMs = 0;
Mode lastMode = Mode::Off;
bool heartbeatActive = false;
uint32_t heartbeatStartMs = 0;
constexpr uint32_t HEARTBEAT_PULSE_MS = 200;
void setAllOff() {
digitalWrite(PIN_RED, HIGH);
digitalWrite(PIN_GREEN, HIGH);
@@ -30,10 +34,27 @@ void begin() {
initialized = true;
}
void pulseHeartbeat(uint32_t nowMs) {
heartbeatActive = true;
heartbeatStartMs = nowMs;
}
bool update(Mode mode, uint32_t nowMs) {
if (!initialized) begin();
if (mode == Mode::Off) {
if (heartbeatActive) {
if (nowMs - heartbeatStartMs < HEARTBEAT_PULSE_MS) {
digitalWrite(PIN_GREEN, LOW);
digitalWrite(PIN_RED, HIGH);
digitalWrite(PIN_BLUE, HIGH);
blinkState = false;
lastMode = mode;
return true;
}
heartbeatActive = false;
}
setAllOff();
blinkState = false;
lastMode = mode;
@@ -61,4 +82,14 @@ bool update(Mode mode, uint32_t nowMs) {
return blinkState;
}
void flashWhite(uint32_t durationMs) {
if (!initialized) begin();
digitalWrite(PIN_RED, LOW);
digitalWrite(PIN_GREEN, LOW);
digitalWrite(PIN_BLUE, LOW);
delay(durationMs);
setAllOff();
}
}

View File

@@ -7,17 +7,19 @@
namespace LedAlert {
enum class Mode {
Off, // keine LED
ProximityGreen, // Flugzeug innerhalb des Naeherungsradius -> gruen blinkt
EmergencyRed, // Notfall-Squawk (7500/7600/7700) -> rot blinkt schneller, hat Vorrang
Off,
ProximityGreen,
EmergencyRed,
};
// Einmalig in setup() aufrufen.
void begin();
// Haeufig aufrufen (z.B. alle 80-100ms). Schaltet die LED passend zum
// Modus an/aus (blinkend, volle Helligkeit) und gibt den aktuellen
// Blink-Zustand zurueck (true = LED gerade an), damit der Radar-
// Bildschirm z.B. den betroffenen Punkt synchron mitblinken lassen kann.
bool update(Mode mode, uint32_t nowMs);
void pulseHeartbeat(uint32_t nowMs);
// Blockierendes, kurzes weisses Aufblitzen (alle 3 Farbkanaele an) als
// sofortige Bestaetigung fuer eine einmalige Nutzeraktion (z.B. "Cam"-
// Button getroffen). Bewusst blockierend (delay), da nur aus dem
// Haupt-Loop bei einem Tap aufgerufen, nicht aus der NetTask-Schleife.
void flashWhite(uint32_t durationMs = 150);
}

View File

@@ -109,6 +109,8 @@ void updateStatusLine() {
}
void takeScreenshotWithFeedback() {
LedAlert::flashWhite();
String filename = Screenshot::save(tft);
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_NAVY);
@@ -126,11 +128,6 @@ void takeScreenshotWithFeedback() {
}
void haltWithSdRequiredScreen() {
// Ohne SD-Karte kann SettingsStore::load() nie laufen, also ist auch die
// Invertierungs-Einstellung dieses Geraets unbekannt. Dieses konkrete
// CYD-Board braucht invertDisplay(true), um Farben korrekt darzustellen
// (ohne das erscheint Schwarz als Weiss und Gruen als Rosa/Magenta) -
// hier fest setzen, damit Schwarz/Gruen GARANTIERT korrekt aussieht.
tft.invertDisplay(true);
tft.fillScreen(TFT_BLACK);

View File

@@ -17,9 +17,9 @@ namespace {
}
};
constexpr int16_t ROW_H = 26;
constexpr int16_t ROW_GAP = 3;
constexpr int16_t ROW_START_Y = 24;
constexpr int16_t ROW_H = 24;
constexpr int16_t ROW_GAP = 2;
constexpr int16_t ROW_START_Y = 20;
Rect rowRect(uint8_t index) {
return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)),
@@ -44,16 +44,17 @@ void run(TFT_eSPI& tft) {
Rect wifiBtn = rowRect(2);
Rect emergencyBtn = rowRect(3);
Rect proximityBtn = rowRect(4);
Rect logbookBtn = rowRect(5);
Rect statsBtn = rowRect(6);
Rect logFilesBtn = rowRect(7);
Rect backBtn = rowRect(8);
Rect heartbeatBtn = rowRect(5);
Rect logbookBtn = rowRect(6);
Rect statsBtn = rowRect(7);
Rect logFilesBtn = rowRect(8);
Rect backBtn = rowRect(9);
bool done = false;
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setCursor(10, 6);
tft.setCursor(10, 4);
tft.println("Settings");
drawButton(tft, calibBtn, "Calibrate touch");
@@ -67,6 +68,7 @@ void run(TFT_eSPI& tft) {
drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled()));
drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled()));
drawButton(tft, heartbeatBtn, "LED heartbeat: " + onOff(SettingsStore::ledHeartbeatEnabled()));
drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled()));
drawButton(tft, statsBtn, "Statistics");
drawButton(tft, logFilesBtn, "Logbook files");
@@ -91,6 +93,8 @@ void run(TFT_eSPI& tft) {
SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled());
} else if (proximityBtn.contains(tap.x, tap.y)) {
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
} else if (heartbeatBtn.contains(tap.x, tap.y)) {
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
} else if (logbookBtn.contains(tap.x, tap.y)) {
SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled());
} else if (statsBtn.contains(tap.x, tap.y)) {

View File

@@ -8,6 +8,7 @@
#include "settings_store.h"
#include "aircraft_details.h"
#include "flight_logbook.h"
#include "led_alert.h"
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -50,6 +51,14 @@ namespace {
AircraftTable::unlock();
FlightLogbook::update();
// Kurzer gruener LED-Blitz als "Herzschlag" - zeigt,
// dass gerade eine Abfrage gelaufen ist. Wird von
// LedAlert::update() automatisch ignoriert, solange
// ein Naeherungs-/Notfall-Alarm aktiv ist.
if (SettingsStore::ledHeartbeatEnabled()) {
LedAlert::pulseHeartbeat(millis());
}
} else {
Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode);
}

View File

@@ -9,6 +9,7 @@
#include "settings_store.h"
#include "led_alert.h"
#include "location_manager.h"
#include "world_map.h"
#include <math.h>
namespace RadarScreen {
@@ -211,6 +212,37 @@ namespace {
}
}
// Dezente Weltkarten-Silhouette als Hintergrund-Layer, wie bei einem
// klassischen ATC-Radarschirm - rein dekorativ (nicht massstabsgetreu
// zu den Entfernungsringen). Wird auf das quadratische Kreis-Layout
// gestreckt (die Karte ist 2:1, der Kreis-Bereich 1:1 - das verzerrt
// die Kontinente etwas in der Hoehe, faellt aber bei dieser Groesse
// kaum auf und haelt die Zeichenlogik einfach). Wird NUR bei render()
// gezeichnet, NICHT bei jedem Sweep-Tick (zu teuer fuer 12x/Sekunde).
void drawWorldMap(TFT_eSPI& gfx, const Layout& L) {
constexpr uint16_t dim = 0x0320; // sehr gedaempftes Gruen
float scaleX = (2.0f * L.radius) / WorldMap::GRID_W;
float scaleY = (2.0f * L.radius) / WorldMap::GRID_H;
int32_t radiusSq = (int32_t)(L.radius - 2) * (L.radius - 2);
for (uint8_t row = 0; row < WorldMap::GRID_H; row++) {
uint64_t bits = WorldMap::ROWS[row];
if (bits == 0) continue;
for (uint8_t col = 0; col < WorldMap::GRID_W; col++) {
if (!(bits & (1ULL << (WorldMap::GRID_W - 1 - col)))) continue;
int16_t px = L.cx - L.radius + (int16_t)((col + 0.5f) * scaleX);
int16_t py = L.cy - L.radius + (int16_t)((row + 0.5f) * scaleY);
int16_t dx = px - L.cx;
int16_t dy = py - L.cy;
if ((int32_t)dx * dx + (int32_t)dy * dy > radiusSq) continue;
gfx.drawPixel(px, py, dim);
}
}
}
void drawSweepLine(TFT_eSPI& gfx, const Layout& L, float angleDeg, uint16_t color) {
double rad = angleDeg * DEG_TO_RAD;
int16_t x2 = L.cx + (int16_t)(L.radius * sin(rad));
@@ -368,6 +400,7 @@ void render(TFT_eSPI& tft, int16_t top) {
tft.fillRect(0, top, Config::SCREEN_WIDTH, Config::SCREEN_HEIGHT - top, TFT_BLACK);
drawWorldMap(tft, L);
drawStaticBackground(tft, L, rangeKm);
drawSweepLine(tft, L, sweepAngleDeg, TFT_GREEN);

View File

@@ -10,6 +10,7 @@ namespace {
bool emergencyAlertOn = true;
bool proximityAlertOn = true;
bool flightLogbookOn = true;
bool ledHeartbeatOn = true;
void applyKeyValue(const String& key, const String& value) {
if (key == "range_index") {
@@ -25,20 +26,26 @@ namespace {
proximityAlertOn = (value.toInt() != 0);
} else if (key == "flight_logbook") {
flightLogbookOn = (value.toInt() != 0);
} else if (key == "led_heartbeat") {
ledHeartbeatOn = (value.toInt() != 0);
}
}
}
void load() {
if (!SD.exists(Config::SD_SETTINGS_FILE)) return;
File f = SD.open(Config::SD_SETTINGS_FILE, FILE_READ);
if (!f) return;
while (f.available()) {
String line = f.readStringUntil('\n');
line.trim();
if (line.length() == 0 || line.startsWith("#")) continue;
int eq = line.indexOf('=');
if (eq < 0) continue;
String key = line.substring(0, eq);
String value = line.substring(eq + 1);
key.trim();
@@ -56,24 +63,52 @@ void save() {
f.printf("emergency_alert=%d\n", emergencyAlertOn ? 1 : 0);
f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0);
f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0);
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
f.close();
}
uint8_t rangeIndex() { return rangeIdx; }
void setRangeIndex(uint8_t idx) {
if (idx < Config::RANGE_STEP_COUNT) { rangeIdx = idx; save(); }
if (idx < Config::RANGE_STEP_COUNT) {
rangeIdx = idx;
save();
}
}
bool displayInverted() { return inverted; }
void setDisplayInverted(bool inv) { inverted = inv; save(); }
void setDisplayInverted(bool inv) {
inverted = inv;
save();
}
bool emergencyAlertEnabled() { return emergencyAlertOn; }
void setEmergencyAlertEnabled(bool on) { emergencyAlertOn = on; save(); }
void setEmergencyAlertEnabled(bool on) {
emergencyAlertOn = on;
save();
}
bool proximityAlertEnabled() { return proximityAlertOn; }
void setProximityAlertEnabled(bool on) { proximityAlertOn = on; save(); }
void setProximityAlertEnabled(bool on) {
proximityAlertOn = on;
save();
}
bool flightLogbookEnabled() { return flightLogbookOn; }
void setFlightLogbookEnabled(bool on) { flightLogbookOn = on; save(); }
void setFlightLogbookEnabled(bool on) {
flightLogbookOn = on;
save();
}
bool ledHeartbeatEnabled() { return ledHeartbeatOn; }
void setLedHeartbeatEnabled(bool on) {
ledHeartbeatOn = on;
save();
}
}

View File

@@ -19,4 +19,10 @@ namespace SettingsStore {
bool flightLogbookEnabled();
void setFlightLogbookEnabled(bool on);
// Kurzes gruenes Aufblitzen der Rueckseiten-LED synchron zu jeder
// ADS-B-Abfrage (alle 8s) - zeigt "Geraet ist aktiv", auch wenn gerade
// kein Naeherungs-/Notfall-Alarm laeuft.
bool ledHeartbeatEnabled();
void setLedHeartbeatEnabled(bool on);
}

View File

@@ -15,28 +15,30 @@ namespace {
// Radar-Bildschirm), damit das Flugzeug wie ins Visier genommen wirkt.
void drawRadarReticle(TFT_eSPI& tft, int16_t cx, int16_t cy) {
uint16_t dim = 0x0320;
tft.drawCircle(cx, cy, 112, dim);
tft.drawCircle(cx, cy, 76, dim);
tft.drawCircle(cx, cy, 40, dim);
tft.drawFastHLine(cx - 112, cy, 224, dim);
tft.drawFastVLine(cx, cy - 112, 224, dim);
tft.drawCircle(cx, cy, 80, dim);
tft.drawCircle(cx, cy, 54, dim);
tft.drawCircle(cx, cy, 29, dim);
tft.drawFastHLine(cx - 80, cy, 160, dim);
tft.drawFastVLine(cx, cy - 80, 160, dim);
}
// Einfache Vektor-Silhouette eines schlanken Duesenjets von oben (keine
// Bilddatei eingebunden, daher per Dreiecken gezeichnet) - nur als Umriss
// (nicht ausgefuellt), in Gruen, passend zum restlichen Dark-Theme.
// Etwas kleiner als frueher, damit er sauber zwischen Titel und
// Statuszeilen passt, ohne diese zu ueberlappen.
void drawAirplane(TFT_eSPI& tft, int16_t cx) {
uint16_t color = TFT_GREEN;
// Schlanker Rumpf (nur Umriss)
tft.drawTriangle(cx, 105, cx - 6, 255, cx + 6, 255, color);
tft.drawTriangle(cx, 116, cx - 4, 223, cx + 4, 223, color);
// Deltafluegel, nach hinten gepfeilt (nur Umriss)
tft.drawTriangle(cx, 160, cx - 100, 235, cx, 200, color);
tft.drawTriangle(cx, 160, cx + 100, 235, cx, 200, color);
tft.drawTriangle(cx, 155, cx - 71, 209, cx, 184, color);
tft.drawTriangle(cx, 155, cx + 71, 209, cx, 184, color);
// Kleines Leitwerk (Hoehenruder) am Heck (nur Umriss)
tft.drawTriangle(cx, 250, cx - 22, 268, cx + 22, 268, color);
tft.drawTriangle(cx, 219, cx - 16, 232, cx + 16, 232, color);
}
}
@@ -46,7 +48,7 @@ void begin(TFT_eSPI& tft) {
int16_t cx = tft.width() / 2;
tft.fillScreen(TFT_BLACK);
drawRadarReticle(tft, cx, 185);
drawRadarReticle(tft, cx, 174);
drawAirplane(tft, cx);
tft.setTextDatum(MC_DATUM);

40
src/world_map.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "world_map.h"
namespace WorldMap {
const uint64_t ROWS[GRID_H] = {
0x0000000000000000ULL,
0x0000F7FC00000000ULL,
0x001B07F000107080ULL,
0x1C0DF3E0080FFFF0ULL,
0x3FFF31883DFFFFFFULL,
0x19FE38006FFFFF88ULL,
0x00FFBC037FFFFF80ULL,
0x003FF601FFFFFF80ULL,
0x003FE003B9FFFE00ULL,
0x003FE000C3FFFE80ULL,
0x001FC003EFFFFC00ULL,
0x00060007FD9FF800ULL,
0x00060007FFC67000ULL,
0x00008007FE846400ULL,
0x00007803FF804000ULL,
0x00003E003F004800ULL,
0x00003F803E002980ULL,
0x00003FC03E000E00ULL,
0x00003F803E000340ULL,
0x00000F803C8007C0ULL,
0x00001E001C000FE0ULL,
0x00001E00180007E0ULL,
0x00001C0000000060ULL,
0x0000100000000002ULL,
0x0000100000000000ULL,
0x0000100000000000ULL,
0x0000000000000000ULL,
0x0000000000000000ULL,
0x0000180077F7FFF8ULL,
0x03FFF00FFFFFFFF8ULL,
0x0FFFF07FFFFFFFF8ULL,
0xFFFFFFFFFFFFFFFFULL,
};
}

14
src/world_map.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
// Sehr stark vereinfachte Weltkarte (Kontinent-Umrisse) als 64x32-Raster-
// Bitmap, generiert aus Natural-Earth-Daten (1:110m-Aufloesung, die groebste
// oeffentlich verfuegbare Stufe - genau richtig fuer ein kleines Display).
// Jede Zeile ist ein 64-Bit-Wert; Bit (63-i) gesetzt = Landflaeche an
// Spalte i. Equirektangulare Projektion: Zeile 0 = Nordpol, Zeile 31 =
// Suedpol, Spalte 0 = -180 Grad Laenge, Spalte 63 = +180 Grad Laenge.
namespace WorldMap {
constexpr uint8_t GRID_W = 64;
constexpr uint8_t GRID_H = 32;
extern const uint64_t ROWS[GRID_H];
}