v1.5: World map background, LED heartbeat pulse, screenshot flash confirmation, splash screen resize
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
40
src/world_map.cpp
Normal 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
14
src/world_map.h
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user