v1.2: Settings toggles, flight logbook, local time, WiFi auto-reconnect, metric units
This commit is contained in:
@@ -68,6 +68,7 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm,
|
||||
filterAc["baro_rate"]= true;
|
||||
filterAc["gs"] = true;
|
||||
filterAc["track"] = true;
|
||||
filterAc["squawk"] = true;
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(
|
||||
@@ -113,6 +114,9 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm,
|
||||
a.groundSpeedKt = ac["gs"] | 0.0f;
|
||||
a.headingDeg = ac["track"] | 0.0f;
|
||||
|
||||
const char* squawk = ac["squawk"] | "";
|
||||
strncpy(a.squawk, squawk, sizeof(a.squawk) - 1);
|
||||
|
||||
a.lastSeenMs = millis();
|
||||
a.valid = (a.lat != 0.0f || a.lon != 0.0f);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ struct Aircraft {
|
||||
char callsign[9] = {0};
|
||||
char reg[9] = {0};
|
||||
char typeCode[5] = {0};
|
||||
char squawk[5] = {0}; // 4-stelliger Transponder-Code, z.B. "7700" (Notfall)
|
||||
|
||||
float lat = 0;
|
||||
float lon = 0;
|
||||
|
||||
19
src/config.h
19
src/config.h
@@ -3,10 +3,8 @@
|
||||
|
||||
namespace Config {
|
||||
constexpr const char* IP_GEO_HOST = "ip-api.com";
|
||||
constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon";
|
||||
constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode";
|
||||
|
||||
// Kein GPS-Modul am CYD angeschlossen - Platzhalter, falls spaeter eins
|
||||
// per Erweiterungspins nachgeruestet wird.
|
||||
struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; };
|
||||
constexpr GpsPinPair GPS_PIN_CANDIDATES[] = {
|
||||
{22, 27, "G22/G27"}
|
||||
@@ -25,16 +23,11 @@ namespace Config {
|
||||
|
||||
constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f;
|
||||
|
||||
// Radius, innerhalb dessen die rueckseitige RGB-LED (gruen) blinkt, um auf
|
||||
// ein nahes Flugzeug hinzuweisen (Ersatz fuer den fehlenden Lautsprecher
|
||||
// beim CYD).
|
||||
constexpr float LED_ALERT_RADIUS_KM = 3.0f;
|
||||
constexpr uint32_t ALERT_RETRIGGER_COOLDOWN_MS = 30000;
|
||||
|
||||
constexpr uint8_t MAX_TRACKED_AIRCRAFT = 40;
|
||||
|
||||
// Eigener Ordner, getrennt vom Cardputer-Projekt (das nutzt /adsb_radar) -
|
||||
// so kann dieselbe SD-Karte in beiden Geraeten verwendet werden.
|
||||
constexpr const char* SD_ROOT_DIR = "/Flightradar_cyd";
|
||||
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
|
||||
constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv";
|
||||
@@ -43,13 +36,11 @@ namespace Config {
|
||||
constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt";
|
||||
constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt";
|
||||
|
||||
// microSD-Slot beim CYD (ESP32-2432S028): eigener SPI-Bus, Standard-VSPI-Pins.
|
||||
constexpr uint8_t SD_SPI_CS_PIN = 5;
|
||||
constexpr uint8_t SD_SPI_MOSI_PIN = 23;
|
||||
constexpr uint8_t SD_SPI_MISO_PIN = 19;
|
||||
constexpr uint8_t SD_SPI_CLK_PIN = 18;
|
||||
|
||||
// Touch-Controller (XPT2046), eigener SPI-Bus, getrennt von Display und SD.
|
||||
constexpr uint8_t TOUCH_CLK_PIN = 25;
|
||||
constexpr uint8_t TOUCH_CS_PIN = 33;
|
||||
constexpr uint8_t TOUCH_MOSI_PIN = 32;
|
||||
@@ -59,7 +50,6 @@ namespace Config {
|
||||
constexpr int16_t SCREEN_WIDTH = 240;
|
||||
constexpr int16_t SCREEN_HEIGHT = 320;
|
||||
|
||||
// Fuer spaetere Naeherungs-Alarme (Phase 4: Bildschirmrand blinkt statt LED)
|
||||
constexpr float ZONE_BLUE_KM = 25.0f;
|
||||
constexpr float ZONE_YELLOW_KM = 10.0f;
|
||||
constexpr float ZONE_AMBER_KM = 5.0f;
|
||||
@@ -67,4 +57,11 @@ namespace Config {
|
||||
|
||||
constexpr uint16_t COLOR_LOW_ALT_THRESHOLD_FT = 10000;
|
||||
constexpr uint16_t COLOR_MID_ALT_THRESHOLD_FT = 30000;
|
||||
|
||||
// Notfall-Squawk-Codes: 7500 (Entfuehrung), 7600 (Funkausfall), 7700 (allg. Notfall)
|
||||
constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"};
|
||||
constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3;
|
||||
|
||||
// Maximal gespeicherte WLAN-Netzwerke (z.B. Zuhause, Auto-Hotspot, Arbeit)
|
||||
constexpr uint8_t MAX_WIFI_NETWORKS = 3;
|
||||
}
|
||||
143
src/flight_logbook.cpp
Normal file
143
src/flight_logbook.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include "flight_logbook.h"
|
||||
#include "config.h"
|
||||
#include "aircraft.h"
|
||||
#include "aircraft_table.h"
|
||||
#include "settings_store.h"
|
||||
#include <SD.h>
|
||||
#include <time.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace FlightLogbook {
|
||||
|
||||
namespace {
|
||||
constexpr uint16_t MAX_SEEN = 400;
|
||||
char seenHex[MAX_SEEN][7];
|
||||
uint16_t seenCount = 0;
|
||||
char currentDateStr[11] = {0}; // "YYYY-MM-DD"
|
||||
|
||||
bool computeDateStr(char* out, size_t outSize) {
|
||||
time_t now = time(nullptr);
|
||||
if (now < 8 * 3600 * 2) return false;
|
||||
struct tm tmNow;
|
||||
localtime_r(&now, &tmNow);
|
||||
snprintf(out, outSize, "%04d-%02d-%02d", tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday);
|
||||
return true;
|
||||
}
|
||||
|
||||
void logFilename(char* out, size_t outSize) {
|
||||
snprintf(out, outSize, "%s/%s.csv", Config::SD_LOG_DIR, currentDateStr);
|
||||
}
|
||||
|
||||
bool alreadySeen(const char* hex) {
|
||||
for (uint16_t i = 0; i < seenCount; i++) {
|
||||
if (strcmp(seenHex[i], hex) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void markSeen(const char* hex) {
|
||||
if (seenCount >= MAX_SEEN) return;
|
||||
strncpy(seenHex[seenCount], hex, sizeof(seenHex[seenCount]) - 1);
|
||||
seenHex[seenCount][sizeof(seenHex[seenCount]) - 1] = 0;
|
||||
seenCount++;
|
||||
}
|
||||
|
||||
void loadSeenFromTodayFile() {
|
||||
seenCount = 0;
|
||||
char filename[64];
|
||||
logFilename(filename, sizeof(filename));
|
||||
if (!SD.exists(filename)) return;
|
||||
|
||||
File f = SD.open(filename, FILE_READ);
|
||||
if (!f) return;
|
||||
|
||||
bool firstLine = true;
|
||||
while (f.available() && seenCount < MAX_SEEN) {
|
||||
String line = f.readStringUntil('\n');
|
||||
line.trim();
|
||||
if (line.length() == 0) continue;
|
||||
if (firstLine) { firstLine = false; continue; }
|
||||
|
||||
int firstComma = line.indexOf(',');
|
||||
if (firstComma < 0) continue;
|
||||
int secondComma = line.indexOf(',', firstComma + 1);
|
||||
String hex = (secondComma < 0) ? line.substring(firstComma + 1)
|
||||
: line.substring(firstComma + 1, secondComma);
|
||||
hex.trim();
|
||||
if (hex.length() > 0) markSeen(hex.c_str());
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
void ensureCurrentDate() {
|
||||
char today[11];
|
||||
if (!computeDateStr(today, sizeof(today))) return;
|
||||
|
||||
if (strcmp(today, currentDateStr) != 0) {
|
||||
strncpy(currentDateStr, today, sizeof(currentDateStr) - 1);
|
||||
loadSeenFromTodayFile();
|
||||
}
|
||||
}
|
||||
|
||||
void writeLogLine(const Aircraft& a) {
|
||||
char filename[64];
|
||||
logFilename(filename, sizeof(filename));
|
||||
|
||||
bool needsHeader = !SD.exists(filename);
|
||||
|
||||
File f = SD.open(filename, FILE_APPEND);
|
||||
if (!f) return;
|
||||
|
||||
if (needsHeader) {
|
||||
f.println("timestamp,hex,callsign,reg,type,distance_km,altitude_ft");
|
||||
}
|
||||
|
||||
time_t now = time(nullptr);
|
||||
struct tm tmNow;
|
||||
localtime_r(&now, &tmNow);
|
||||
char timestamp[20];
|
||||
snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday,
|
||||
tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec);
|
||||
|
||||
f.printf("%s,%s,%s,%s,%s,%.1f,%d\n",
|
||||
timestamp,
|
||||
a.hex,
|
||||
a.callsign[0] ? a.callsign : "",
|
||||
a.reg[0] ? a.reg : "",
|
||||
a.typeCode[0] ? a.typeCode : "",
|
||||
a.distanceKm,
|
||||
(int)a.altBaroFt);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
ensureCurrentDate();
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!SettingsStore::flightLogbookEnabled()) return;
|
||||
|
||||
ensureCurrentDate();
|
||||
if (currentDateStr[0] == 0) return;
|
||||
|
||||
static Aircraft snapshot[Config::MAX_TRACKED_AIRCRAFT];
|
||||
uint8_t count = 0;
|
||||
|
||||
AircraftTable::lock();
|
||||
Aircraft* table = AircraftTable::raw();
|
||||
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
||||
if (table[i].valid) snapshot[count++] = table[i];
|
||||
}
|
||||
AircraftTable::unlock();
|
||||
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
if (!snapshot[i].hex[0]) continue;
|
||||
if (alreadySeen(snapshot[i].hex)) continue;
|
||||
markSeen(snapshot[i].hex);
|
||||
writeLogLine(snapshot[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
22
src/flight_logbook.h
Normal file
22
src/flight_logbook.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// SD-Karten-Flugbuch: protokolliert jedes NEU gesichtete Flugzeug (Zeit,
|
||||
// Hex-Code, Rufzeichen, Kennzeichen, Typ, Distanz, Hoehe) in eine taegliche
|
||||
// CSV-Datei auf der SD-Karte. Kann in den Einstellungen ("Flight Logbook")
|
||||
// an-/ausgeschaltet werden. Ueberlebt einen Neustart am selben Tag, ohne
|
||||
// bereits geloggte Flugzeuge erneut einzutragen (rekonstruiert die Liste
|
||||
// beim Start aus der heutigen CSV-Datei).
|
||||
namespace FlightLogbook {
|
||||
|
||||
// Einmalig beim Boot aufrufen, NACHDEM die lokale Uhrzeit (UTC-Offset)
|
||||
// bekannt ist - liest die heutige CSV-Datei (falls vorhanden), um bereits
|
||||
// geloggte Flugzeuge nicht doppelt einzutragen.
|
||||
void init();
|
||||
|
||||
// Periodisch aufrufen (z.B. nach jeder erfolgreichen ADS-B-Abfrage):
|
||||
// prueft auf neue, bisher ungesehene Flugzeuge und schreibt sie ins
|
||||
// heutige Logbuch. Kuemmert sich auch um den Tageswechsel (neue Datei,
|
||||
// neue "gesehen"-Liste).
|
||||
void update();
|
||||
}
|
||||
@@ -6,37 +6,58 @@ namespace {
|
||||
constexpr uint8_t PIN_RED = 4;
|
||||
constexpr uint8_t PIN_GREEN = 16;
|
||||
constexpr uint8_t PIN_BLUE = 17;
|
||||
constexpr uint32_t BLINK_INTERVAL_MS = 400;
|
||||
|
||||
constexpr uint32_t GREEN_BLINK_INTERVAL_MS = 400;
|
||||
constexpr uint32_t RED_BLINK_INTERVAL_MS = 150;
|
||||
|
||||
bool initialized = false;
|
||||
bool blinkState = false;
|
||||
uint32_t lastToggleMs = 0;
|
||||
Mode lastMode = Mode::Off;
|
||||
|
||||
void setAllOff() {
|
||||
digitalWrite(PIN_RED, HIGH);
|
||||
digitalWrite(PIN_GREEN, HIGH);
|
||||
digitalWrite(PIN_BLUE, HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
void begin() {
|
||||
pinMode(PIN_RED, OUTPUT);
|
||||
pinMode(PIN_GREEN, OUTPUT);
|
||||
pinMode(PIN_BLUE, OUTPUT);
|
||||
digitalWrite(PIN_RED, HIGH); // aus (LED ist active-low)
|
||||
digitalWrite(PIN_GREEN, HIGH); // aus
|
||||
digitalWrite(PIN_BLUE, HIGH); // aus
|
||||
setAllOff();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
bool update(bool active, uint32_t nowMs) {
|
||||
bool update(Mode mode, uint32_t nowMs) {
|
||||
if (!initialized) begin();
|
||||
|
||||
if (!active) {
|
||||
digitalWrite(PIN_GREEN, HIGH); // aus
|
||||
if (mode == Mode::Off) {
|
||||
setAllOff();
|
||||
blinkState = false;
|
||||
lastMode = mode;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nowMs - lastToggleMs >= BLINK_INTERVAL_MS) {
|
||||
if (mode != lastMode) {
|
||||
lastToggleMs = nowMs;
|
||||
blinkState = true;
|
||||
lastMode = mode;
|
||||
}
|
||||
|
||||
uint32_t interval = (mode == Mode::EmergencyRed) ? RED_BLINK_INTERVAL_MS : GREEN_BLINK_INTERVAL_MS;
|
||||
uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED : PIN_GREEN;
|
||||
|
||||
if (nowMs - lastToggleMs >= interval) {
|
||||
lastToggleMs = nowMs;
|
||||
blinkState = !blinkState;
|
||||
digitalWrite(PIN_GREEN, blinkState ? LOW : HIGH); // LOW = an, volle Helligkeit (kein PWM noetig)
|
||||
}
|
||||
|
||||
digitalWrite(pin, blinkState ? LOW : HIGH);
|
||||
digitalWrite(mode == Mode::EmergencyRed ? PIN_GREEN : PIN_RED, HIGH);
|
||||
digitalWrite(PIN_BLUE, HIGH);
|
||||
|
||||
return blinkState;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher
|
||||
// vorhanden, daher ersetzt die LED den akustischen Naeherungsalarm vom
|
||||
// Cardputer-Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17,
|
||||
// active-low (LOW = an).
|
||||
// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer-
|
||||
// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an).
|
||||
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
|
||||
};
|
||||
|
||||
// Einmalig in setup() aufrufen.
|
||||
void begin();
|
||||
|
||||
// Haeufig aufrufen (z.B. alle 80-100ms). 'active' = mindestens ein
|
||||
// Flugzeug ist innerhalb des Alarmradius. Schaltet die gruene LED
|
||||
// entsprechend an/aus (blinkend, volle Helligkeit) und gibt den
|
||||
// aktuellen Blink-Zustand zurueck (true = LED gerade an), damit der
|
||||
// Radar-Bildschirm den betroffenen Punkt synchron mitblinken lassen kann.
|
||||
bool update(bool active, uint32_t nowMs);
|
||||
// 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);
|
||||
}
|
||||
@@ -18,7 +18,6 @@ namespace {
|
||||
TinyGPSPlus gps;
|
||||
HardwareSerial gpsSerial(1);
|
||||
|
||||
|
||||
bool gpsEnabled = false;
|
||||
uint8_t gpsPinIndex = 0;
|
||||
bool gpsSerialStarted = false;
|
||||
@@ -32,9 +31,10 @@ namespace {
|
||||
|
||||
Source source = Source::None;
|
||||
|
||||
// Schuetzt lastLat/lastLon/havePersisted/source: der Netzwerk-Task (Core 0)
|
||||
// schreibt diese Werte, der Render-Loop (Core 1) liest sie ueber
|
||||
// getHomeLocation()/currentSource().
|
||||
bool haveUtcOffset = false;
|
||||
int32_t utcOffsetSecs = 0;
|
||||
bool metricUnits = true; // Standard: metrisch, bis die IP-Abfrage etwas anderes sagt
|
||||
|
||||
SemaphoreHandle_t mutex = nullptr;
|
||||
|
||||
void startGpsSerialIfNeeded() {
|
||||
@@ -44,9 +44,6 @@ namespace {
|
||||
gpsSerialStarted = true;
|
||||
}
|
||||
|
||||
// Schreibt lastLat/lastLon/havePersisted UND setzt 'source' gleich mit,
|
||||
// damit beide unter demselben Lock aktualisiert werden (kein Zwischenzustand
|
||||
// sichtbar fuer den lesenden Core).
|
||||
void persistLocationAndSource(double lat, double lon, Source newSource) {
|
||||
prefs.putDouble("homeLat", lat);
|
||||
prefs.putDouble("homeLon", lon);
|
||||
@@ -127,6 +124,14 @@ void requestIpLookupIfNeeded() {
|
||||
double lon = doc["lon"] | 0.0;
|
||||
if (lat == 0.0 && lon == 0.0) return;
|
||||
|
||||
utcOffsetSecs = doc["offset"] | 0;
|
||||
haveUtcOffset = true;
|
||||
|
||||
const char* countryCode = doc["countryCode"] | "";
|
||||
// Nur die USA nutzen (fuer diesen Zweck) Fuss statt Meter. Bei Bedarf
|
||||
// hier weitere Laender ergaenzen (z.B. "LR", "MM").
|
||||
metricUnits = (strcmp(countryCode, "US") != 0);
|
||||
|
||||
persistLocationAndSource(lat, lon, Source::IpGeolocation);
|
||||
ipLookupDone = true;
|
||||
}
|
||||
@@ -180,4 +185,8 @@ const char* currentGpsPinLabel() {
|
||||
|
||||
bool hasGpsFix() { return gps.location.isValid(); }
|
||||
|
||||
bool hasUtcOffset() { return haveUtcOffset; }
|
||||
int32_t utcOffsetSeconds() { return utcOffsetSecs; }
|
||||
bool useMetricUnits() { return metricUnits; }
|
||||
|
||||
}
|
||||
@@ -20,4 +20,14 @@ namespace LocationManager {
|
||||
|
||||
bool hasGpsFix();
|
||||
|
||||
// UTC-Offset in Sekunden (inkl. evtl. Sommerzeit), ermittelt bei der
|
||||
// IP-Geolocation-Abfrage. 0/false, falls noch nicht bekannt.
|
||||
bool hasUtcOffset();
|
||||
int32_t utcOffsetSeconds();
|
||||
|
||||
// Ob die Region (per IP-Geolocation-Laendercode) metrische Einheiten
|
||||
// nutzt (Meter/km) statt Fuss/Meilen. Default true (metrisch), bis die
|
||||
// IP-Abfrage etwas anderes ermittelt hat - nur die USA nutzen aktuell
|
||||
// eine Ausnahme.
|
||||
bool useMetricUnits();
|
||||
}
|
||||
126
src/main.cpp
126
src/main.cpp
@@ -21,16 +21,19 @@
|
||||
#include "radar_screen.h"
|
||||
#include "splash_screen.h"
|
||||
#include "led_alert.h"
|
||||
#include "flight_logbook.h"
|
||||
|
||||
TFT_eSPI tft = TFT_eSPI();
|
||||
|
||||
constexpr int16_t CONTENT_TOP = 30;
|
||||
constexpr uint32_t POLL_INTERVAL_MS = 300;
|
||||
constexpr uint32_t SWEEP_TICK_MS = 80;
|
||||
constexpr int16_t CONTENT_TOP = 30; // schlanker Header, Radar bekommt den Rest des Screens
|
||||
constexpr uint32_t POLL_INTERVAL_MS = 300; // wie oft wir NACHSCHAUEN, ob sich Daten geaendert haben
|
||||
constexpr uint32_t SWEEP_TICK_MS = 80; // wie oft der Sweep-Strahl ein Stueck weiterdreht
|
||||
uint32_t lastPollMs = 0;
|
||||
uint32_t lastSweepMs = 0;
|
||||
uint32_t lastRenderedVersion = 0xFFFFFFFF;
|
||||
uint32_t lastRenderedVersion = 0xFFFFFFFF; // erzwingt einen ersten Render-Aufruf
|
||||
bool forceRedraw = false;
|
||||
bool wasEmergency = false;
|
||||
bool bannerBlinkOn = false;
|
||||
|
||||
struct Rect {
|
||||
int16_t x, y, w, h;
|
||||
@@ -39,18 +42,21 @@ struct Rect {
|
||||
}
|
||||
};
|
||||
|
||||
Rect menuBtn = {Config::SCREEN_WIDTH - 38, 3, 32, 22};
|
||||
Rect menuBtn = {Config::SCREEN_WIDTH - 60, 3, 54, 22};
|
||||
|
||||
void drawMenuButton() {
|
||||
tft.fillRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_NAVY);
|
||||
tft.drawRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_DARKGREY);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||
tft.drawString("...", menuBtn.x + menuBtn.w / 2, menuBtn.y + menuBtn.h / 2);
|
||||
tft.drawString("Menu", menuBtn.x + menuBtn.w / 2, menuBtn.y + menuBtn.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
|
||||
void drawHeader() {
|
||||
// Bis CONTENT_TOP loeschen, damit kein Bildrest vom Menue zwischen Header
|
||||
// und Radar-Bildschirm haengen bleibt. Schlanker Header, damit der Radar-
|
||||
// Kreis darunter maximal viel Platz bekommt.
|
||||
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_BLACK);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setTextSize(1);
|
||||
@@ -59,41 +65,99 @@ void drawHeader() {
|
||||
drawMenuButton();
|
||||
}
|
||||
|
||||
// Zeigt eine grosse, unmissverstaendliche Meldung und haelt das Geraet an -
|
||||
// KEIN weiterer Bildschirm erscheint, solange keine SD-Karte steckt. Die App
|
||||
// braucht die Karte fuer Einstellungen, WLAN-Zugangsdaten und Nachschlage-
|
||||
// tabellen, daher macht ein Weiterlaufen ohne sie keinen Sinn.
|
||||
void haltWithSdRequiredScreen() {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
int16_t cx = Config::SCREEN_WIDTH / 2;
|
||||
int16_t cy = Config::SCREEN_HEIGHT / 2;
|
||||
|
||||
tft.setTextColor(TFT_RED, TFT_BLACK);
|
||||
tft.setTextSize(2);
|
||||
tft.drawString("For this app a", cx, cy - 40);
|
||||
tft.drawString("SD card is", cx, cy - 10);
|
||||
tft.drawString("required", cx, cy + 20);
|
||||
|
||||
tft.setTextSize(1);
|
||||
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
tft.drawString("Insert a card and restart the device", cx, cy + 60);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
|
||||
while (true) {
|
||||
delay(1000); // haengen bleiben - absichtlich kein weiterer Screen
|
||||
}
|
||||
}
|
||||
|
||||
// Zeigt/versteckt ein blinkendes Notfall-Banner im Header-Bereich, wenn ein
|
||||
// Flugzeug einen Notfall-Squawk (7500/7600/7700) sendet. Ersetzt kurzzeitig
|
||||
// den normalen Titel; sobald der Notfall vorbei ist, wird drawHeader() genau
|
||||
// einmal wieder aufgerufen, um den Titel sauber wiederherzustellen.
|
||||
void updateEmergencyBanner(uint32_t nowMs) {
|
||||
RadarScreen::EmergencyInfo emergency = RadarScreen::checkEmergency();
|
||||
|
||||
if (emergency.active) {
|
||||
bannerBlinkOn = !bannerBlinkOn;
|
||||
uint16_t bg = bannerBlinkOn ? TFT_RED : TFT_BLACK;
|
||||
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, bg);
|
||||
tft.setTextColor(TFT_WHITE, bg);
|
||||
tft.setTextSize(1);
|
||||
tft.setCursor(4, 10);
|
||||
tft.printf("EMERGENCY %s %s", emergency.squawk, emergency.callsign);
|
||||
wasEmergency = true;
|
||||
} else if (wasEmergency) {
|
||||
wasEmergency = false;
|
||||
drawHeader();
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
|
||||
tft.init();
|
||||
tft.setRotation(0);
|
||||
// Sofort loeschen, bevor irgendetwas anderes passiert (SD-Init etc.
|
||||
// braucht einen Moment) - sonst zeigt das Display kurz zufaelligen
|
||||
// Bildspeicher-Muell an, bevor der erste echte Screen gezeichnet wird.
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
|
||||
TouchInput::begin();
|
||||
LedAlert::begin();
|
||||
|
||||
// --- SD-Karte: PFLICHT. Ohne Karte kein weiterer Screen. ---
|
||||
bool sdOk = SdStorage::init();
|
||||
if (sdOk) {
|
||||
SdStorage::seedDefaultDataFiles();
|
||||
if (!sdOk) {
|
||||
haltWithSdRequiredScreen();
|
||||
return; // unerreichbar (haltWithSdRequiredScreen() haengt fuer immer), nur zur Klarheit
|
||||
}
|
||||
SdStorage::seedDefaultDataFiles();
|
||||
|
||||
// Einstellungen (u.a. Display-Invertierung) VOR dem Splash laden und
|
||||
// anwenden, damit der Splash selbst schon in der richtigen Ausrichtung
|
||||
// gezeichnet wird (kein Farbwechsel mitten in der Anzeige).
|
||||
SettingsStore::load();
|
||||
tft.invertDisplay(SettingsStore::displayInverted());
|
||||
|
||||
WifiMgr::init();
|
||||
WifiMgr::init(); // laedt die gespeicherten Netzwerke (bis zu 3) von der SD-Karte
|
||||
|
||||
// --- Splash-Screen: schwarzer Hintergrund, gruenes Flugzeug, mind. 5s ---
|
||||
SplashScreen::begin(tft);
|
||||
SplashScreen::setStatusLine(tft, 0, sdOk ? "SD Card: OK" : "SD Card: ERROR",
|
||||
sdOk ? TFT_WHITE : TFT_RED);
|
||||
SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE);
|
||||
|
||||
// --- Touch-Kalibrierung (nur beim allerersten Start, oder wenn Datei fehlt) ---
|
||||
if (!TouchInput::loadCalibration()) {
|
||||
CalibrationScreen::run(tft);
|
||||
}
|
||||
|
||||
bool haveWifiFile = SD.exists(Config::SD_WIFI_CREDENTIALS_FILE);
|
||||
if (!haveWifiFile) {
|
||||
// --- WLAN: Ersteinrichtung (falls noch kein Netzwerk gespeichert ist)
|
||||
// oder automatisches Verbinden mit dem ersten gerade sichtbaren
|
||||
// gespeicherten Netzwerk (z.B. Zuhause ODER Auto-Hotspot). ---
|
||||
if (WifiMgr::networkCount() == 0) {
|
||||
WifiSetupScreen::run(tft);
|
||||
} else {
|
||||
if (!WifiMgr::hasStoredCredentials()) {
|
||||
WifiMgr::loadCredentialsFromSd();
|
||||
}
|
||||
SplashScreen::setStatusLine(tft, 1, "Connecting WiFi...");
|
||||
WifiMgr::beginConnect();
|
||||
|
||||
@@ -112,6 +176,7 @@ void setup() {
|
||||
|
||||
AdsbClient::primeTime();
|
||||
|
||||
// --- Standort per IP-Geolocation (einmalig blockierend beim Start) ---
|
||||
SplashScreen::setStatusLine(tft, 2, "Getting location...");
|
||||
LocationManager::init();
|
||||
uint32_t locStart = millis();
|
||||
@@ -122,11 +187,22 @@ void setup() {
|
||||
}
|
||||
SplashScreen::setStatusLine(tft, 2, "Ready!");
|
||||
|
||||
// Sobald die IP-Geolocation einen UTC-Offset geliefert hat, die
|
||||
// Zeitzone entsprechend setzen - Zeitstempel (Flugbuch, Log-Dateinamen)
|
||||
// zeigen dann die ECHTE Ortszeit statt UTC, automatisch weltweit richtig.
|
||||
if (LocationManager::hasUtcOffset()) {
|
||||
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
|
||||
}
|
||||
|
||||
AircraftTable::init();
|
||||
AirlineLookup::init();
|
||||
FlightLogbook::init(); // rekonstruiert die "heute schon geloggt"-Liste aus der SD-Karte
|
||||
|
||||
// --- Ab hier uebernimmt der Netzwerk-Task (Core 0) laufend WLAN-Status,
|
||||
// Standort-Updates und ADS-B-Abfragen im Hintergrund. ---
|
||||
NetTask::begin();
|
||||
|
||||
// Splash bleibt mindestens 5 Sekunden sichtbar, egal wie schnell der Rest war.
|
||||
SplashScreen::waitRemaining();
|
||||
|
||||
drawHeader();
|
||||
@@ -139,14 +215,19 @@ void loop() {
|
||||
if (menuBtn.contains(tap.x, tap.y)) {
|
||||
MenuScreen::run(tft);
|
||||
drawHeader();
|
||||
forceRedraw = true;
|
||||
forceRedraw = true; // sofort neu zeichnen
|
||||
} else if (tap.y >= CONTENT_TOP) {
|
||||
if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) {
|
||||
forceRedraw = true;
|
||||
forceRedraw = true; // sofort neu zeichnen nach Interaktion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nur alle POLL_INTERVAL_MS kurz nachschauen (billige Abfrage eines
|
||||
// Zaehlers), statt staendig teuer neu zu zeichnen. Ein echtes Neuzeichnen
|
||||
// (render()) passiert nur, wenn sich die Flugzeugdaten TATSAECHLICH
|
||||
// geaendert haben (neue Abfrage im Hintergrund fertig) oder der Nutzer
|
||||
// etwas angetippt hat - das vermeidet unnoetiges Flackern.
|
||||
if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) {
|
||||
lastPollMs = millis();
|
||||
uint32_t currentVersion = AircraftTable::version();
|
||||
@@ -154,19 +235,22 @@ void loop() {
|
||||
lastRenderedVersion = currentVersion;
|
||||
forceRedraw = false;
|
||||
RadarScreen::render(tft, CONTENT_TOP);
|
||||
lastSweepMs = millis();
|
||||
lastSweepMs = millis(); // Sweep-Delta nicht ueber den Vollbild-Redraw hinweg aufaddieren
|
||||
}
|
||||
}
|
||||
|
||||
// Sweep-Strahl dreht sich unabhaengig von den Flugzeugdaten weiter -
|
||||
// billige Linien-Zeichnung ohne Vollbild-Clear, daher kein Flackern.
|
||||
uint32_t nowMs = millis();
|
||||
|
||||
// Naeherungsalarm (LED) laeuft IMMER, auch wenn gerade das Detail-Fenster
|
||||
// offen ist - unabhaengig vom Sweep/Radar-Redraw.
|
||||
// Naeherungs-/Notfall-Alarm (LED) laeuft IMMER, auch wenn gerade das
|
||||
// Detail-Fenster offen ist - unabhaengig vom Sweep/Radar-Redraw.
|
||||
RadarScreen::updateProximityAlert(nowMs);
|
||||
|
||||
if (nowMs - lastSweepMs >= SWEEP_TICK_MS) {
|
||||
uint32_t deltaMs = nowMs - lastSweepMs;
|
||||
lastSweepMs = nowMs;
|
||||
RadarScreen::tick(tft, CONTENT_TOP, deltaMs);
|
||||
updateEmergencyBanner(nowMs); // gleiche Taktung wie der Sweep-Tick
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "menu_screen.h"
|
||||
#include "touch_input.h"
|
||||
#include "calibration_screen.h"
|
||||
#include "wifi_manage_screen.h"
|
||||
#include "settings_store.h"
|
||||
#include "config.h"
|
||||
|
||||
@@ -14,6 +15,15 @@ namespace {
|
||||
}
|
||||
};
|
||||
|
||||
constexpr int16_t ROW_H = 30;
|
||||
constexpr int16_t ROW_GAP = 4;
|
||||
constexpr int16_t ROW_START_Y = 26;
|
||||
|
||||
Rect rowRect(uint8_t index) {
|
||||
return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)),
|
||||
(int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
|
||||
}
|
||||
|
||||
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) {
|
||||
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_NAVY);
|
||||
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY);
|
||||
@@ -22,28 +32,41 @@ namespace {
|
||||
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
|
||||
String onOff(bool on) { return on ? "ON" : "OFF"; }
|
||||
}
|
||||
|
||||
void run(TFT_eSPI& tft) {
|
||||
Rect calibBtn = {10, 60, Config::SCREEN_WIDTH - 20, 40};
|
||||
Rect invertBtn = {10, 112, Config::SCREEN_WIDTH - 20, 40};
|
||||
Rect backBtn = {10, 260, Config::SCREEN_WIDTH - 20, 40};
|
||||
Rect calibBtn = rowRect(0);
|
||||
Rect invertBtn = rowRect(1);
|
||||
Rect wifiBtn = rowRect(2);
|
||||
Rect emergencyBtn = rowRect(3);
|
||||
Rect proximityBtn = rowRect(4);
|
||||
Rect logbookBtn = rowRect(5);
|
||||
Rect backBtn = rowRect(6);
|
||||
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.println("Einstellungen");
|
||||
tft.setCursor(10, 8);
|
||||
tft.println("Settings");
|
||||
|
||||
drawButton(tft, calibBtn, "Calibrate touch");
|
||||
|
||||
drawButton(tft, calibBtn, "Touch kalibrieren");
|
||||
String invertLabel = SettingsStore::displayInverted()
|
||||
? "Display: invertiert (antippen)"
|
||||
: "Display: normal (antippen)";
|
||||
? "Display: inverted (tap)"
|
||||
: "Display: normal (tap)";
|
||||
drawButton(tft, invertBtn, invertLabel);
|
||||
drawButton(tft, backBtn, "Zurueck");
|
||||
|
||||
// Auf genau einen Tap warten
|
||||
drawButton(tft, wifiBtn, "Manage WiFi networks");
|
||||
|
||||
drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled()));
|
||||
drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled()));
|
||||
drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled()));
|
||||
|
||||
drawButton(tft, backBtn, "Back");
|
||||
|
||||
TouchInput::Point tap;
|
||||
while (true) {
|
||||
if (TouchInput::wasTapped(tap)) break;
|
||||
@@ -56,6 +79,14 @@ void run(TFT_eSPI& tft) {
|
||||
bool newState = !SettingsStore::displayInverted();
|
||||
SettingsStore::setDisplayInverted(newState);
|
||||
tft.invertDisplay(newState);
|
||||
} else if (wifiBtn.contains(tap.x, tap.y)) {
|
||||
WifiManageScreen::run(tft);
|
||||
} else if (emergencyBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled());
|
||||
} else if (proximityBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
|
||||
} else if (logbookBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled());
|
||||
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||
done = true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "aircraft.h"
|
||||
#include "settings_store.h"
|
||||
#include "aircraft_details.h"
|
||||
#include "flight_logbook.h"
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
@@ -18,11 +19,6 @@ namespace {
|
||||
TaskHandle_t taskHandle = nullptr;
|
||||
uint32_t lastFetchMs = 0;
|
||||
|
||||
// Eigener Zwischenspeicher fuer die Netzwerk-Antwort. Die eigentliche
|
||||
// AircraftTable wird NUR fuer den kurzen Kopiervorgang gesperrt, NICHT
|
||||
// waehrend der (langsamen) Netzwerkabfrage selbst - sonst friert der
|
||||
// Radar-Bildschirm fuer die Dauer der HTTPS-Anfrage ein, weil er auf
|
||||
// denselben Lock wartet.
|
||||
Aircraft tempTable[Config::MAX_TRACKED_AIRCRAFT];
|
||||
|
||||
void taskFunc(void*) {
|
||||
@@ -30,9 +26,6 @@ namespace {
|
||||
WifiMgr::update();
|
||||
LocationManager::update();
|
||||
|
||||
// Erledigt eine evtl. anstehende Detail-Abfrage (Modell/Route)
|
||||
// fuer das aktuell vom Nutzer ausgewaehlte Flugzeug, falls es eine
|
||||
// gibt. Guenstig, wenn nichts ansteht (nur ein Mutex-Check).
|
||||
AircraftDetails::update();
|
||||
|
||||
if (millis() - lastFetchMs >= Config::FETCH_INTERVAL_MS) {
|
||||
@@ -46,10 +39,6 @@ namespace {
|
||||
|
||||
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
||||
|
||||
// Netzwerkabfrage OHNE Lock - schreibt nur in den lokalen
|
||||
// Zwischenspeicher, den sonst niemand anfasst. Der
|
||||
// Radar-Bildschirm kann waehrenddessen ganz normal mit den
|
||||
// ALTEN Daten weiterzeichnen.
|
||||
auto result = AdsbClient::fetch(lat, lon, rangeKm,
|
||||
tempTable, Config::MAX_TRACKED_AIRCRAFT);
|
||||
|
||||
@@ -59,6 +48,8 @@ namespace {
|
||||
sizeof(Aircraft) * Config::MAX_TRACKED_AIRCRAFT);
|
||||
AircraftTable::postFetchUpdate(lat, lon);
|
||||
AircraftTable::unlock();
|
||||
|
||||
FlightLogbook::update();
|
||||
} else {
|
||||
Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode);
|
||||
}
|
||||
@@ -74,11 +65,11 @@ void begin() {
|
||||
xTaskCreatePinnedToCore(
|
||||
taskFunc,
|
||||
"NetTask",
|
||||
20480, // Stack: TLS-Handshake + JSON-Parsing braucht mehr als das Minimum
|
||||
20480,
|
||||
nullptr,
|
||||
1, // Prioritaet
|
||||
1,
|
||||
&taskHandle,
|
||||
0 // Core 0 (Core 1 bleibt frei fuer Rendering/Touch im main-Loop)
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "units.h"
|
||||
#include "settings_store.h"
|
||||
#include "led_alert.h"
|
||||
#include "location_manager.h"
|
||||
#include <math.h>
|
||||
|
||||
namespace RadarScreen {
|
||||
@@ -50,6 +51,7 @@ namespace {
|
||||
uint16_t color;
|
||||
float headingDeg;
|
||||
float distanceKm;
|
||||
bool isEmergency;
|
||||
};
|
||||
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
||||
HitPoint hitPoints[MAX_HIT_POINTS];
|
||||
@@ -58,6 +60,14 @@ namespace {
|
||||
|
||||
bool ledBlinkOn = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
float sweepAngleDeg = 0.0f;
|
||||
float prevSweepAngleDeg = -1.0f;
|
||||
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
|
||||
@@ -89,11 +99,29 @@ namespace {
|
||||
gfx.setTextDatum(TL_DATUM);
|
||||
}
|
||||
|
||||
// Kleine Farb-Legende (Hoehenband -> Farbe), damit auf den ersten Blick
|
||||
// klar ist, was Gruen/Gelb/Rot bei den Flugzeug-Punkten bedeuten. Zeigt
|
||||
// Meter statt Fuss, wenn die Region (per Geolocation) metrisch ist.
|
||||
void drawLegend(TFT_eSPI& gfx, int16_t y) {
|
||||
bool metric = LocationManager::useMetricUnits();
|
||||
|
||||
char lowLabel[10], midLabel[10], highLabel[10];
|
||||
if (metric) {
|
||||
int lowM = (int)(Units::feetToMeters(Config::COLOR_LOW_ALT_THRESHOLD_FT) / 100) * 100;
|
||||
int midM = (int)(Units::feetToMeters(Config::COLOR_MID_ALT_THRESHOLD_FT) / 100) * 100;
|
||||
snprintf(lowLabel, sizeof(lowLabel), "<%dm", lowM);
|
||||
snprintf(midLabel, sizeof(midLabel), "%d-%dm", lowM, midM);
|
||||
snprintf(highLabel, sizeof(highLabel), ">%dm", midM);
|
||||
} else {
|
||||
snprintf(lowLabel, sizeof(lowLabel), "<10k ft");
|
||||
snprintf(midLabel, sizeof(midLabel), "10-30k");
|
||||
snprintf(highLabel, sizeof(highLabel), ">30k ft");
|
||||
}
|
||||
|
||||
struct { uint16_t color; const char* label; } items[3] = {
|
||||
{TFT_GREEN, "<10k ft"},
|
||||
{TFT_YELLOW, "10-30k"},
|
||||
{TFT_RED, ">30k ft"},
|
||||
{TFT_GREEN, lowLabel},
|
||||
{TFT_YELLOW, midLabel},
|
||||
{TFT_RED, highLabel},
|
||||
};
|
||||
int16_t segW = Config::SCREEN_WIDTH / 3;
|
||||
gfx.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
@@ -290,6 +318,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
|
||||
uint16_t color = colorForAltitude(a.altBaroFt);
|
||||
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
||||
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
||||
|
||||
if (isSelected) {
|
||||
tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE);
|
||||
@@ -298,6 +327,10 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
}
|
||||
tft.fillCircle(pt.x, pt.y, 5, color);
|
||||
|
||||
if (isEmergency) {
|
||||
tft.drawCircle(pt.x, pt.y, 12, TFT_RED);
|
||||
}
|
||||
|
||||
double rad = a.headingDeg * PI / 180.0;
|
||||
int16_t dx = (int16_t)(sin(rad) * 10);
|
||||
int16_t dy = (int16_t)(-cos(rad) * 10);
|
||||
@@ -315,6 +348,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
hitPoints[i].color = color;
|
||||
hitPoints[i].headingDeg = a.headingDeg;
|
||||
hitPoints[i].distanceKm = a.distanceKm;
|
||||
hitPoints[i].isEmergency = isEmergency;
|
||||
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
||||
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
|
||||
}
|
||||
@@ -375,6 +409,10 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
||||
|
||||
tft.fillCircle(hp.x, hp.y, 5, hp.color);
|
||||
|
||||
if (hp.isEmergency) {
|
||||
tft.drawCircle(hp.x, hp.y, 12, TFT_RED);
|
||||
}
|
||||
|
||||
double rad = hp.headingDeg * PI / 180.0;
|
||||
int16_t dx = (int16_t)(sin(rad) * 10);
|
||||
int16_t dy = (int16_t)(-cos(rad) * 10);
|
||||
@@ -429,18 +467,47 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
|
||||
|
||||
void updateProximityAlert(uint32_t nowMs) {
|
||||
bool anyClose = false;
|
||||
bool anyEmergency = false;
|
||||
|
||||
bool proximityOn = SettingsStore::proximityAlertEnabled();
|
||||
bool emergencyOn = SettingsStore::emergencyAlertEnabled();
|
||||
|
||||
if (proximityOn || emergencyOn) {
|
||||
AircraftTable::lock();
|
||||
Aircraft* table = AircraftTable::raw();
|
||||
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
||||
if (!table[i].valid) continue;
|
||||
if (proximityOn && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) anyClose = true;
|
||||
if (emergencyOn && isEmergencySquawk(table[i].squawk)) anyEmergency = true;
|
||||
}
|
||||
AircraftTable::unlock();
|
||||
}
|
||||
|
||||
LedAlert::Mode mode = anyEmergency ? LedAlert::Mode::EmergencyRed
|
||||
: anyClose ? LedAlert::Mode::ProximityGreen
|
||||
: LedAlert::Mode::Off;
|
||||
|
||||
ledBlinkOn = LedAlert::update(mode, nowMs);
|
||||
}
|
||||
|
||||
EmergencyInfo checkEmergency() {
|
||||
EmergencyInfo info;
|
||||
if (!SettingsStore::emergencyAlertEnabled()) return info;
|
||||
|
||||
AircraftTable::lock();
|
||||
Aircraft* table = AircraftTable::raw();
|
||||
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
||||
if (table[i].valid && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) {
|
||||
anyClose = true;
|
||||
if (table[i].valid && isEmergencySquawk(table[i].squawk)) {
|
||||
info.active = true;
|
||||
strncpy(info.callsign, table[i].callsign[0] ? table[i].callsign : table[i].hex,
|
||||
sizeof(info.callsign) - 1);
|
||||
strncpy(info.squawk, table[i].squawk, sizeof(info.squawk) - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
AircraftTable::unlock();
|
||||
|
||||
ledBlinkOn = LedAlert::update(anyClose, nowMs);
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,8 +23,19 @@ namespace RadarScreen {
|
||||
bool handleTap(int16_t x, int16_t y, int16_t top);
|
||||
|
||||
// Haeufig aufrufen (unabhaengig davon, ob ein Detail-Fenster offen ist):
|
||||
// prueft, ob ein Flugzeug innerhalb des Alarmradius ist, steuert die
|
||||
// gruene LED entsprechend und merkt sich den Blink-Zustand, damit tick()
|
||||
// den betroffenen Punkt synchron mitblinken lassen kann.
|
||||
// prueft, ob ein Flugzeug innerhalb des Alarmradius ist ODER ein
|
||||
// Notfall-Squawk sendet, steuert die LED entsprechend (Notfall hat
|
||||
// Vorrang, blinkt rot statt gruen) und merkt sich den Blink-Zustand,
|
||||
// damit tick() den betroffenen Punkt synchron mitblinken lassen kann.
|
||||
void updateProximityAlert(uint32_t nowMs);
|
||||
|
||||
struct EmergencyInfo {
|
||||
bool active = false;
|
||||
char callsign[9] = {0};
|
||||
char squawk[5] = {0};
|
||||
};
|
||||
|
||||
// Reine Abfrage (kein Seiteneffekt): gibt das erste Flugzeug mit einem
|
||||
// Notfall-Squawk zurueck, falls vorhanden. Fuer das Banner im Header.
|
||||
EmergencyInfo checkEmergency();
|
||||
}
|
||||
@@ -7,6 +7,9 @@ namespace SettingsStore {
|
||||
namespace {
|
||||
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
|
||||
bool inverted = false;
|
||||
bool emergencyAlertOn = true;
|
||||
bool proximityAlertOn = true;
|
||||
bool flightLogbookOn = true;
|
||||
|
||||
void applyKeyValue(const String& key, const String& value) {
|
||||
if (key == "range_index") {
|
||||
@@ -16,26 +19,26 @@ namespace {
|
||||
}
|
||||
} else if (key == "invert") {
|
||||
inverted = (value.toInt() != 0);
|
||||
} else if (key == "emergency_alert") {
|
||||
emergencyAlertOn = (value.toInt() != 0);
|
||||
} else if (key == "proximity_alert") {
|
||||
proximityAlertOn = (value.toInt() != 0);
|
||||
} else if (key == "flight_logbook") {
|
||||
flightLogbookOn = (value.toInt() != 0);
|
||||
}
|
||||
// Weitere Einstellungen hier ergaenzen (unbekannte Keys werden
|
||||
// einfach ignoriert, damit alte config.txt-Dateien kompatibel bleiben).
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -50,23 +53,27 @@ void save() {
|
||||
if (!f) return;
|
||||
f.printf("range_index=%d\n", rangeIdx);
|
||||
f.printf("invert=%d\n", inverted ? 1 : 0);
|
||||
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.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(); }
|
||||
|
||||
bool proximityAlertEnabled() { return proximityAlertOn; }
|
||||
void setProximityAlertEnabled(bool on) { proximityAlertOn = on; save(); }
|
||||
|
||||
bool flightLogbookEnabled() { return flightLogbookOn; }
|
||||
void setFlightLogbookEnabled(bool on) { flightLogbookOn = on; save(); }
|
||||
|
||||
}
|
||||
@@ -1,17 +1,8 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Einfacher key=value Einstellungs-Speicher in /Flightradar_cyd/config.txt.
|
||||
// Neue Einstellungen koennen spaeter einfach als weitere getX()/setX()-Paare
|
||||
// dazu kommen, ohne das Dateiformat zu aendern (unbekannte Zeilen werden
|
||||
// beim Laden ignoriert).
|
||||
namespace SettingsStore {
|
||||
|
||||
// Liest config.txt von der SD-Karte (falls vorhanden). Werte, die nicht
|
||||
// in der Datei stehen, behalten ihren Standardwert.
|
||||
void load();
|
||||
|
||||
// Schreibt die aktuellen Werte zurueck auf die SD-Karte.
|
||||
void save();
|
||||
|
||||
uint8_t rangeIndex();
|
||||
@@ -19,4 +10,13 @@ namespace SettingsStore {
|
||||
|
||||
bool displayInverted();
|
||||
void setDisplayInverted(bool inverted);
|
||||
|
||||
bool emergencyAlertEnabled();
|
||||
void setEmergencyAlertEnabled(bool on);
|
||||
|
||||
bool proximityAlertEnabled();
|
||||
void setProximityAlertEnabled(bool on);
|
||||
|
||||
bool flightLogbookEnabled();
|
||||
void setFlightLogbookEnabled(bool on);
|
||||
}
|
||||
@@ -10,20 +10,33 @@ namespace {
|
||||
constexpr int16_t STATUS_START_Y = 260;
|
||||
constexpr uint8_t MAX_STATUS_LINES = 3;
|
||||
|
||||
// Einfache Vektor-Silhouette eines Flugzeugs von oben (keine Bilddatei
|
||||
// eingebunden, daher per Dreiecken gezeichnet) - in Gruen, wie der
|
||||
// Radar-Sweep-Strahl, passend zum restlichen Dark-Theme.
|
||||
// Dezente konzentrische Ringe + Fadenkreuz als "Zielfernrohr"-Hintergrund,
|
||||
// in gedaempftem Gruen (gleicher Ton wie die Sweep-Nachzieh-Linie im
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
void drawAirplane(TFT_eSPI& tft, int16_t cx) {
|
||||
uint16_t color = TFT_GREEN;
|
||||
|
||||
// Rumpf
|
||||
tft.fillTriangle(cx, 110, cx - 12, 240, cx + 12, 240, color);
|
||||
// Schlanker Rumpf (nur Umriss)
|
||||
tft.drawTriangle(cx, 105, cx - 6, 255, cx + 6, 255, color);
|
||||
|
||||
// Haupttragflaechen
|
||||
tft.fillTriangle(cx, 170, cx - 110, 230, cx + 110, 230, 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);
|
||||
|
||||
// Leitwerk (Heckfluegel)
|
||||
tft.fillTriangle(cx, 232, cx - 35, 250, cx + 35, 250, color);
|
||||
// Kleines Leitwerk (Hoehenruder) am Heck (nur Umriss)
|
||||
tft.drawTriangle(cx, 250, cx - 22, 268, cx + 22, 268, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +46,7 @@ void begin(TFT_eSPI& tft) {
|
||||
int16_t cx = tft.width() / 2;
|
||||
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
drawRadarReticle(tft, cx, 185);
|
||||
drawAirplane(tft, cx);
|
||||
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
|
||||
98
src/wifi_manage_screen.cpp
Normal file
98
src/wifi_manage_screen.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "wifi_manage_screen.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "wifi_setup_screen.h"
|
||||
#include "touch_input.h"
|
||||
#include "config.h"
|
||||
|
||||
namespace WifiManageScreen {
|
||||
|
||||
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, uint16_t bg = TFT_NAVY) {
|
||||
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, bg);
|
||||
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_WHITE, bg);
|
||||
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
}
|
||||
|
||||
void run(TFT_eSPI& tft) {
|
||||
constexpr int16_t ROW_H = 46;
|
||||
constexpr int16_t ROW_GAP = 8;
|
||||
constexpr int16_t REMOVE_BTN_W = 60;
|
||||
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.println("WiFi networks");
|
||||
|
||||
uint8_t count = WifiMgr::networkCount();
|
||||
int16_t y = 40;
|
||||
|
||||
Rect rowRects[Config::MAX_WIFI_NETWORKS];
|
||||
Rect removeRects[Config::MAX_WIFI_NETWORKS];
|
||||
|
||||
for (uint8_t i = 0; i < Config::MAX_WIFI_NETWORKS; i++) {
|
||||
Rect rowRect = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20 - REMOVE_BTN_W - 6), ROW_H};
|
||||
Rect removeRect = {(int16_t)(Config::SCREEN_WIDTH - 10 - REMOVE_BTN_W), y, REMOVE_BTN_W, ROW_H};
|
||||
rowRects[i] = rowRect;
|
||||
removeRects[i] = removeRect;
|
||||
|
||||
if (i < count) {
|
||||
drawButton(tft, rowRect, WifiMgr::networkSsid(i));
|
||||
drawButton(tft, removeRect, "X", TFT_MAROON);
|
||||
} else {
|
||||
tft.fillRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_BLACK);
|
||||
tft.drawRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_DARKGREY);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
tft.drawString("(empty)", rowRect.x + rowRect.w / 2, rowRect.y + rowRect.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
y += ROW_H + ROW_GAP;
|
||||
}
|
||||
|
||||
Rect addBtn = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
bool canAdd = count < Config::MAX_WIFI_NETWORKS;
|
||||
if (canAdd) {
|
||||
drawButton(tft, addBtn, "Add network");
|
||||
}
|
||||
y += 48;
|
||||
|
||||
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
drawButton(tft, backBtn, "Back");
|
||||
|
||||
TouchInput::Point tap;
|
||||
while (true) {
|
||||
if (TouchInput::wasTapped(tap)) break;
|
||||
delay(20);
|
||||
}
|
||||
|
||||
bool handled = false;
|
||||
for (uint8_t i = 0; i < count && !handled; i++) {
|
||||
if (removeRects[i].contains(tap.x, tap.y)) {
|
||||
WifiMgr::removeNetwork(i);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) {
|
||||
WifiSetupScreen::run(tft);
|
||||
handled = true;
|
||||
}
|
||||
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/wifi_manage_screen.h
Normal file
11
src/wifi_manage_screen.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace WifiManageScreen {
|
||||
// Blockierend: zeigt die bis zu 3 gespeicherten WLAN-Netzwerke, erlaubt
|
||||
// Loeschen einzelner Eintraege und (falls noch Platz ist) das Hinzufuegen
|
||||
// eines neuen ueber den Scan+Passwort-Bildschirm. Kehrt zurueck, sobald
|
||||
// "Back" angetippt wird.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -1,25 +1,30 @@
|
||||
#include "wifi_manager.h"
|
||||
#include "config.h"
|
||||
#include "sd_storage.h"
|
||||
#include <WiFi.h>
|
||||
#include <Preferences.h>
|
||||
#include <SD.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace WifiMgr {
|
||||
|
||||
namespace {
|
||||
Preferences prefs;
|
||||
struct NetworkEntry {
|
||||
char ssid[33] = {0};
|
||||
char pass[64] = {0};
|
||||
};
|
||||
|
||||
NetworkEntry networks[Config::MAX_WIFI_NETWORKS];
|
||||
uint8_t networkCountVal = 0;
|
||||
|
||||
State state = State::Idle;
|
||||
uint32_t connectStartMs = 0;
|
||||
constexpr uint32_t CONNECT_TIMEOUT_MS = 15000; // bounded, same fix as the Kinect sketch
|
||||
constexpr uint32_t CONNECT_TIMEOUT_MS = 15000;
|
||||
char ipStr[16] = {0};
|
||||
|
||||
// Schuetzt 'state': der Netzwerk-Task (Core 0) schreibt es in
|
||||
// beginConnect()/update(), der Render-Loop (Core 1) liest es ueber
|
||||
// getState(). ipStr wird nur einmal beim Verbinden geschrieben und danach
|
||||
// nur gelesen, daher hier ohne eigenen Lock.
|
||||
uint32_t lastReconnectAttemptMs = 0;
|
||||
constexpr uint32_t RECONNECT_RETRY_MS = 10000;
|
||||
|
||||
SemaphoreHandle_t mutex = nullptr;
|
||||
|
||||
void setState(State s) {
|
||||
@@ -27,51 +32,137 @@ namespace {
|
||||
state = s;
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
|
||||
void loadFromSd() {
|
||||
networkCountVal = 0;
|
||||
if (!SdStorage::isMounted()) return;
|
||||
if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return;
|
||||
|
||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ);
|
||||
if (!f) return;
|
||||
|
||||
while (networkCountVal < Config::MAX_WIFI_NETWORKS && f.available()) {
|
||||
String ssid = f.readStringUntil('\n');
|
||||
String pass = f.readStringUntil('\n');
|
||||
ssid.trim();
|
||||
pass.trim();
|
||||
if (ssid.length() == 0) break;
|
||||
|
||||
strncpy(networks[networkCountVal].ssid, ssid.c_str(), sizeof(networks[networkCountVal].ssid) - 1);
|
||||
strncpy(networks[networkCountVal].pass, pass.c_str(), sizeof(networks[networkCountVal].pass) - 1);
|
||||
networkCountVal++;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
void saveToSd() {
|
||||
if (!SdStorage::isMounted()) return;
|
||||
|
||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE);
|
||||
if (!f) return;
|
||||
for (uint8_t i = 0; i < networkCountVal; i++) {
|
||||
f.println(networks[i].ssid);
|
||||
f.println(networks[i].pass);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
if (mutex == nullptr) mutex = xSemaphoreCreateMutex();
|
||||
prefs.begin("adsb_radar", /*readOnly=*/false);
|
||||
setState(hasStoredCredentials() ? State::Idle : State::NoCredentials);
|
||||
loadFromSd();
|
||||
setState(networkCountVal > 0 ? State::Idle : State::NoCredentials);
|
||||
}
|
||||
|
||||
bool hasStoredCredentials() {
|
||||
String ssid = prefs.getString("ssid", "");
|
||||
return ssid.length() > 0;
|
||||
uint8_t networkCount() { return networkCountVal; }
|
||||
|
||||
String networkSsid(uint8_t index) {
|
||||
if (index >= networkCountVal) return String();
|
||||
return String(networks[index].ssid);
|
||||
}
|
||||
|
||||
void saveCredentials(const char* ssid, const char* password) {
|
||||
prefs.putString("ssid", ssid);
|
||||
prefs.putString("pass", password);
|
||||
bool addNetwork(const char* ssid, const char* password) {
|
||||
if (networkCountVal >= Config::MAX_WIFI_NETWORKS) return false;
|
||||
|
||||
strncpy(networks[networkCountVal].ssid, ssid, sizeof(networks[networkCountVal].ssid) - 1);
|
||||
strncpy(networks[networkCountVal].pass, password, sizeof(networks[networkCountVal].pass) - 1);
|
||||
networkCountVal++;
|
||||
saveToSd();
|
||||
return true;
|
||||
}
|
||||
|
||||
void beginConnect() {
|
||||
if (!hasStoredCredentials()) {
|
||||
setState(State::NoCredentials);
|
||||
return;
|
||||
void removeNetwork(uint8_t index) {
|
||||
if (index >= networkCountVal) return;
|
||||
for (uint8_t i = index; i < networkCountVal - 1; i++) {
|
||||
networks[i] = networks[i + 1];
|
||||
}
|
||||
networkCountVal--;
|
||||
networks[networkCountVal] = NetworkEntry{};
|
||||
saveToSd();
|
||||
}
|
||||
String ssid = prefs.getString("ssid", "");
|
||||
String pass = prefs.getString("pass", "");
|
||||
|
||||
void connectTo(const char* ssid, const char* password) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
||||
WiFi.begin(ssid, password);
|
||||
connectStartMs = millis();
|
||||
setState(State::Connecting);
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (getState() != State::Connecting) return;
|
||||
void beginConnect() {
|
||||
if (networkCountVal == 0) {
|
||||
setState(State::NoCredentials);
|
||||
return;
|
||||
}
|
||||
|
||||
int visibleCount = WiFi.scanNetworks(/*async=*/false);
|
||||
|
||||
int8_t chosen = -1;
|
||||
for (uint8_t i = 0; i < networkCountVal && chosen < 0; i++) {
|
||||
for (int j = 0; j < visibleCount; j++) {
|
||||
if (WiFi.SSID(j) == networks[i].ssid) {
|
||||
chosen = (int8_t)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chosen < 0) {
|
||||
setState(State::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
connectTo(networks[chosen].ssid, networks[chosen].pass);
|
||||
}
|
||||
|
||||
void update() {
|
||||
State s = getState();
|
||||
|
||||
if (s == State::Connecting) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ipStr, WiFi.localIP().toString().c_str(), sizeof(ipStr) - 1);
|
||||
setState(State::Connected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - connectStartMs > CONNECT_TIMEOUT_MS) {
|
||||
WiFi.disconnect(true);
|
||||
setState(State::Failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s == State::Connected && WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[WifiMgr] Verbindung verloren, versuche automatisch neu zu verbinden...");
|
||||
setState(State::Idle);
|
||||
lastReconnectAttemptMs = millis() - RECONNECT_RETRY_MS;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((s == State::Idle || s == State::Failed) && networkCountVal > 0) {
|
||||
if (millis() - lastReconnectAttemptMs >= RECONNECT_RETRY_MS) {
|
||||
lastReconnectAttemptMs = millis();
|
||||
beginConnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State getState() {
|
||||
@@ -104,37 +195,4 @@ int32_t getScanResultRSSI(int index) {
|
||||
|
||||
const char* getIP() { return ipStr; }
|
||||
|
||||
bool loadCredentialsFromSd() {
|
||||
if (!SdStorage::isMounted()) return false;
|
||||
if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return false;
|
||||
|
||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ);
|
||||
if (!f) return false;
|
||||
|
||||
String ssid = f.readStringUntil('\n');
|
||||
String pass = f.readStringUntil('\n');
|
||||
f.close();
|
||||
|
||||
ssid.trim();
|
||||
pass.trim();
|
||||
if (ssid.length() == 0) return false;
|
||||
|
||||
saveCredentials(ssid.c_str(), pass.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void saveCredentialsToSdIfMounted() {
|
||||
if (!SdStorage::isMounted()) return;
|
||||
|
||||
String ssid = prefs.getString("ssid", "");
|
||||
String pass = prefs.getString("pass", "");
|
||||
if (ssid.length() == 0) return;
|
||||
|
||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE);
|
||||
if (!f) return;
|
||||
f.println(ssid);
|
||||
f.println(pass);
|
||||
f.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
|
||||
namespace WifiMgr {
|
||||
enum class State {
|
||||
@@ -8,20 +9,20 @@ namespace WifiMgr {
|
||||
|
||||
void init();
|
||||
void beginConnect();
|
||||
void connectTo(const char* ssid, const char* password);
|
||||
|
||||
void update();
|
||||
State getState();
|
||||
const char* getIP();
|
||||
|
||||
void saveCredentials(const char* ssid, const char* password);
|
||||
bool hasStoredCredentials();
|
||||
|
||||
bool loadCredentialsFromSd();
|
||||
void saveCredentialsToSdIfMounted();
|
||||
uint8_t networkCount();
|
||||
String networkSsid(uint8_t index);
|
||||
bool addNetwork(const char* ssid, const char* password);
|
||||
void removeNetwork(uint8_t index);
|
||||
|
||||
void beginScan();
|
||||
bool isScanComplete();
|
||||
int getScanResultCount();
|
||||
String getScanResultSSID(int index);
|
||||
int32_t getScanResultRSSI(int index);
|
||||
|
||||
const char* getIP();
|
||||
}
|
||||
@@ -17,11 +17,10 @@ namespace {
|
||||
|
||||
struct KeyDef {
|
||||
KeyType type;
|
||||
char ch; // fuer KeyType::Char (Kleinbuchstabe/Grundzeichen)
|
||||
const char* label; // Anzeige, falls kein einzelnes Zeichen (z.B. "<-")
|
||||
char ch;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
// --- Tastatur-Layout ---------------------------------------------------
|
||||
constexpr KeyDef ROW_LETTERS_A[] = {
|
||||
{KeyType::Char,'q',nullptr},{KeyType::Char,'w',nullptr},{KeyType::Char,'e',nullptr},
|
||||
{KeyType::Char,'r',nullptr},{KeyType::Char,'t',nullptr},{KeyType::Char,'y',nullptr},
|
||||
@@ -79,11 +78,11 @@ namespace {
|
||||
uint8_t passwordLen = 0;
|
||||
|
||||
bool shiftOn = false;
|
||||
uint8_t page = 0; // 0 = Buchstaben, 1 = Symbole
|
||||
uint8_t page = 0;
|
||||
|
||||
bool skipped = false;
|
||||
bool connectSucceeded = false;
|
||||
bool needsRedraw = true; // nur neu zeichnen, wenn sich wirklich was geaendert hat (verhindert Flackern)
|
||||
bool needsRedraw = true;
|
||||
|
||||
Rect cancelBtn = {Config::SCREEN_WIDTH - 34, 4, 30, 24};
|
||||
|
||||
@@ -97,7 +96,6 @@ namespace {
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
|
||||
// Rechnet die Rects einer Tastenreihe aus (gleich verteilt ueber die Breite).
|
||||
template <size_t N>
|
||||
void layoutRow(const KeyDef (&row)[N], int16_t y, Rect outRects[N]) {
|
||||
int16_t usableW = Config::SCREEN_WIDTH - 2 * SIDE_MARGIN;
|
||||
@@ -184,10 +182,10 @@ bool run(TFT_eSPI& tft) {
|
||||
WifiMgr::beginScan();
|
||||
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setTextSize(1);
|
||||
tft.setCursor(10, 10);
|
||||
tft.println("WLAN-Suche laeuft...");
|
||||
tft.println("Scanning WiFi...");
|
||||
drawCancelButton(tft);
|
||||
|
||||
while (!skipped) {
|
||||
@@ -199,7 +197,6 @@ bool run(TFT_eSPI& tft) {
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Uebergaenge, die nicht vom Touch kommen -----------------------
|
||||
if (stage == Stage::Scanning && WifiMgr::isScanComplete()) {
|
||||
ssidCount = (uint8_t)min((int)WifiMgr::getScanResultCount(), (int)MAX_LIST);
|
||||
for (uint8_t i = 0; i < ssidCount; i++) ssidList[i] = WifiMgr::getScanResultSSID(i);
|
||||
@@ -208,8 +205,8 @@ bool run(TFT_eSPI& tft) {
|
||||
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.println(ssidCount == 0 ? "Keine Netzwerke gefunden" : "WLAN waehlen:");
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.println(ssidCount == 0 ? "No networks found" : "Select WiFi:");
|
||||
drawCancelButton(tft);
|
||||
}
|
||||
|
||||
@@ -217,12 +214,12 @@ bool run(TFT_eSPI& tft) {
|
||||
WifiMgr::update();
|
||||
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
||||
connectSucceeded = true;
|
||||
WifiMgr::saveCredentialsToSdIfMounted();
|
||||
WifiMgr::addNetwork(ssidList[selectedIndex].c_str(), passwordBuf);
|
||||
stage = Stage::Done;
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.println("Verbunden!");
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.println("Connected!");
|
||||
delay(900);
|
||||
return true;
|
||||
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
||||
@@ -230,10 +227,10 @@ bool run(TFT_eSPI& tft) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.setTextColor(TFT_RED, TFT_BLACK);
|
||||
tft.println("Verbindung fehlgeschlagen.");
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.println("Connection failed.");
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setCursor(10, 30);
|
||||
tft.println("Zurueck zur Netzwerkliste...");
|
||||
tft.println("Back to network list...");
|
||||
delay(1400);
|
||||
stage = Stage::PickSsid;
|
||||
WifiMgr::beginScan();
|
||||
@@ -241,10 +238,8 @@ bool run(TFT_eSPI& tft) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Touch-Eingaben je nach Stage -----------------------------------
|
||||
if (tapped && stage == Stage::PickSsid) {
|
||||
if (ssidCount > 0) {
|
||||
// Liste
|
||||
for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) {
|
||||
uint8_t idx = scrollOffset + row;
|
||||
if (idx >= ssidCount) break;
|
||||
@@ -256,7 +251,6 @@ bool run(TFT_eSPI& tft) {
|
||||
needsRedraw = true;
|
||||
}
|
||||
}
|
||||
// Scroll-Pfeile
|
||||
if (ssidCount > VISIBLE_ITEMS) {
|
||||
Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26};
|
||||
Rect downBtn = {Config::SCREEN_WIDTH - 34, 64 + (VISIBLE_ITEMS - 1) * 34, 30, 26};
|
||||
@@ -294,22 +288,32 @@ bool run(TFT_eSPI& tft) {
|
||||
passwordBuf[passwordLen] = 0;
|
||||
}
|
||||
} else if (connectBtn.contains(tap.x, tap.y)) {
|
||||
WifiMgr::saveCredentials(ssidList[selectedIndex].c_str(), passwordBuf);
|
||||
WifiMgr::beginConnect();
|
||||
if (WifiMgr::networkCount() >= Config::MAX_WIFI_NETWORKS) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.setTextColor(TFT_RED, TFT_BLACK);
|
||||
tft.println("Already 3 networks saved.");
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setCursor(10, 30);
|
||||
tft.println("Remove one first.");
|
||||
delay(1600);
|
||||
skipped = true;
|
||||
break;
|
||||
}
|
||||
WifiMgr::connectTo(ssidList[selectedIndex].c_str(), passwordBuf);
|
||||
stage = Stage::Connecting;
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setCursor(10, 10);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.println("Verbinde...");
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.println("Connecting...");
|
||||
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||
stage = Stage::PickSsid;
|
||||
needsRedraw = true;
|
||||
}
|
||||
}
|
||||
needsRedraw = true; // Tastatureingabe (Zeichen/Shift/Seite/Backspace/Leerzeichen) aendert den Bildschirm
|
||||
needsRedraw = true;
|
||||
}
|
||||
|
||||
// --- Zeichnen (nur bei Aenderung, verhindert Flackern) ----------------
|
||||
if (!needsRedraw) {
|
||||
delay(20);
|
||||
continue;
|
||||
@@ -333,16 +337,16 @@ bool run(TFT_eSPI& tft) {
|
||||
drawCancelButton(tft);
|
||||
} else if (stage == Stage::EnterPassword) {
|
||||
tft.fillRect(0, 0, Config::SCREEN_WIDTH, KB_TOP - 4, TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.setCursor(10, 6);
|
||||
tft.printf("WLAN: %s", ssidList[selectedIndex].c_str());
|
||||
tft.printf("WiFi: %s", ssidList[selectedIndex].c_str());
|
||||
tft.setCursor(10, 22);
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.println("Passwort:");
|
||||
tft.println("Password:");
|
||||
tft.fillRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_NAVY);
|
||||
tft.drawRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_DARKGREY);
|
||||
tft.setCursor(12, 44);
|
||||
tft.setTextColor(TFT_YELLOW, TFT_NAVY);
|
||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||
tft.print(passwordBuf);
|
||||
|
||||
int16_t rowY0 = KB_TOP;
|
||||
@@ -363,15 +367,15 @@ bool run(TFT_eSPI& tft) {
|
||||
Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H};
|
||||
Rect connectBtn = {SIDE_MARGIN + 154, rowYFn, Config::SCREEN_WIDTH - 2*SIDE_MARGIN - 154, ROW_H};
|
||||
Rect backBtn = {SIDE_MARGIN, (int16_t)(rowYFn + ROW_H + ROW_GAP), (int16_t)(Config::SCREEN_WIDTH - 2*SIDE_MARGIN), 28};
|
||||
drawButton(tft, spaceBtn, "Leerzeichen");
|
||||
drawButton(tft, connectBtn, "Verbinden");
|
||||
drawButton(tft, backBtn, "Zurueck zur Liste");
|
||||
drawButton(tft, spaceBtn, "Space");
|
||||
drawButton(tft, connectBtn, "Connect");
|
||||
drawButton(tft, backBtn, "Back to list");
|
||||
}
|
||||
|
||||
delay(20);
|
||||
}
|
||||
|
||||
return false; // uebersprungen
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user