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["baro_rate"]= true;
|
||||||
filterAc["gs"] = true;
|
filterAc["gs"] = true;
|
||||||
filterAc["track"] = true;
|
filterAc["track"] = true;
|
||||||
|
filterAc["squawk"] = true;
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
DeserializationError err = deserializeJson(
|
DeserializationError err = deserializeJson(
|
||||||
@@ -113,6 +114,9 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm,
|
|||||||
a.groundSpeedKt = ac["gs"] | 0.0f;
|
a.groundSpeedKt = ac["gs"] | 0.0f;
|
||||||
a.headingDeg = ac["track"] | 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.lastSeenMs = millis();
|
||||||
a.valid = (a.lat != 0.0f || a.lon != 0.0f);
|
a.valid = (a.lat != 0.0f || a.lon != 0.0f);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ struct Aircraft {
|
|||||||
char callsign[9] = {0};
|
char callsign[9] = {0};
|
||||||
char reg[9] = {0};
|
char reg[9] = {0};
|
||||||
char typeCode[5] = {0};
|
char typeCode[5] = {0};
|
||||||
|
char squawk[5] = {0}; // 4-stelliger Transponder-Code, z.B. "7700" (Notfall)
|
||||||
|
|
||||||
float lat = 0;
|
float lat = 0;
|
||||||
float lon = 0;
|
float lon = 0;
|
||||||
|
|||||||
19
src/config.h
19
src/config.h
@@ -3,10 +3,8 @@
|
|||||||
|
|
||||||
namespace Config {
|
namespace Config {
|
||||||
constexpr const char* IP_GEO_HOST = "ip-api.com";
|
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; };
|
struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; };
|
||||||
constexpr GpsPinPair GPS_PIN_CANDIDATES[] = {
|
constexpr GpsPinPair GPS_PIN_CANDIDATES[] = {
|
||||||
{22, 27, "G22/G27"}
|
{22, 27, "G22/G27"}
|
||||||
@@ -25,16 +23,11 @@ namespace Config {
|
|||||||
|
|
||||||
constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f;
|
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 float LED_ALERT_RADIUS_KM = 3.0f;
|
||||||
constexpr uint32_t ALERT_RETRIGGER_COOLDOWN_MS = 30000;
|
constexpr uint32_t ALERT_RETRIGGER_COOLDOWN_MS = 30000;
|
||||||
|
|
||||||
constexpr uint8_t MAX_TRACKED_AIRCRAFT = 40;
|
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_ROOT_DIR = "/Flightradar_cyd";
|
||||||
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
|
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
|
||||||
constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.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_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt";
|
||||||
constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.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_CS_PIN = 5;
|
||||||
constexpr uint8_t SD_SPI_MOSI_PIN = 23;
|
constexpr uint8_t SD_SPI_MOSI_PIN = 23;
|
||||||
constexpr uint8_t SD_SPI_MISO_PIN = 19;
|
constexpr uint8_t SD_SPI_MISO_PIN = 19;
|
||||||
constexpr uint8_t SD_SPI_CLK_PIN = 18;
|
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_CLK_PIN = 25;
|
||||||
constexpr uint8_t TOUCH_CS_PIN = 33;
|
constexpr uint8_t TOUCH_CS_PIN = 33;
|
||||||
constexpr uint8_t TOUCH_MOSI_PIN = 32;
|
constexpr uint8_t TOUCH_MOSI_PIN = 32;
|
||||||
@@ -59,7 +50,6 @@ namespace Config {
|
|||||||
constexpr int16_t SCREEN_WIDTH = 240;
|
constexpr int16_t SCREEN_WIDTH = 240;
|
||||||
constexpr int16_t SCREEN_HEIGHT = 320;
|
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_BLUE_KM = 25.0f;
|
||||||
constexpr float ZONE_YELLOW_KM = 10.0f;
|
constexpr float ZONE_YELLOW_KM = 10.0f;
|
||||||
constexpr float ZONE_AMBER_KM = 5.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_LOW_ALT_THRESHOLD_FT = 10000;
|
||||||
constexpr uint16_t COLOR_MID_ALT_THRESHOLD_FT = 30000;
|
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_RED = 4;
|
||||||
constexpr uint8_t PIN_GREEN = 16;
|
constexpr uint8_t PIN_GREEN = 16;
|
||||||
constexpr uint8_t PIN_BLUE = 17;
|
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 initialized = false;
|
||||||
bool blinkState = false;
|
bool blinkState = false;
|
||||||
uint32_t lastToggleMs = 0;
|
uint32_t lastToggleMs = 0;
|
||||||
|
Mode lastMode = Mode::Off;
|
||||||
|
|
||||||
|
void setAllOff() {
|
||||||
|
digitalWrite(PIN_RED, HIGH);
|
||||||
|
digitalWrite(PIN_GREEN, HIGH);
|
||||||
|
digitalWrite(PIN_BLUE, HIGH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
pinMode(PIN_RED, OUTPUT);
|
pinMode(PIN_RED, OUTPUT);
|
||||||
pinMode(PIN_GREEN, OUTPUT);
|
pinMode(PIN_GREEN, OUTPUT);
|
||||||
pinMode(PIN_BLUE, OUTPUT);
|
pinMode(PIN_BLUE, OUTPUT);
|
||||||
digitalWrite(PIN_RED, HIGH); // aus (LED ist active-low)
|
setAllOff();
|
||||||
digitalWrite(PIN_GREEN, HIGH); // aus
|
|
||||||
digitalWrite(PIN_BLUE, HIGH); // aus
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool update(bool active, uint32_t nowMs) {
|
bool update(Mode mode, uint32_t nowMs) {
|
||||||
if (!initialized) begin();
|
if (!initialized) begin();
|
||||||
|
|
||||||
if (!active) {
|
if (mode == Mode::Off) {
|
||||||
digitalWrite(PIN_GREEN, HIGH); // aus
|
setAllOff();
|
||||||
blinkState = false;
|
blinkState = false;
|
||||||
|
lastMode = mode;
|
||||||
return false;
|
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;
|
lastToggleMs = nowMs;
|
||||||
blinkState = !blinkState;
|
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;
|
return blinkState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,22 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher
|
// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher
|
||||||
// vorhanden, daher ersetzt die LED den akustischen Naeherungsalarm vom
|
// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer-
|
||||||
// Cardputer-Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17,
|
// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an).
|
||||||
// active-low (LOW = an).
|
|
||||||
namespace LedAlert {
|
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.
|
// Einmalig in setup() aufrufen.
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
// Haeufig aufrufen (z.B. alle 80-100ms). 'active' = mindestens ein
|
// Haeufig aufrufen (z.B. alle 80-100ms). Schaltet die LED passend zum
|
||||||
// Flugzeug ist innerhalb des Alarmradius. Schaltet die gruene LED
|
// Modus an/aus (blinkend, volle Helligkeit) und gibt den aktuellen
|
||||||
// entsprechend an/aus (blinkend, volle Helligkeit) und gibt den
|
// Blink-Zustand zurueck (true = LED gerade an), damit der Radar-
|
||||||
// aktuellen Blink-Zustand zurueck (true = LED gerade an), damit der
|
// Bildschirm z.B. den betroffenen Punkt synchron mitblinken lassen kann.
|
||||||
// Radar-Bildschirm den betroffenen Punkt synchron mitblinken lassen kann.
|
bool update(Mode mode, uint32_t nowMs);
|
||||||
bool update(bool active, uint32_t nowMs);
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@ namespace {
|
|||||||
TinyGPSPlus gps;
|
TinyGPSPlus gps;
|
||||||
HardwareSerial gpsSerial(1);
|
HardwareSerial gpsSerial(1);
|
||||||
|
|
||||||
|
|
||||||
bool gpsEnabled = false;
|
bool gpsEnabled = false;
|
||||||
uint8_t gpsPinIndex = 0;
|
uint8_t gpsPinIndex = 0;
|
||||||
bool gpsSerialStarted = false;
|
bool gpsSerialStarted = false;
|
||||||
@@ -32,9 +31,10 @@ namespace {
|
|||||||
|
|
||||||
Source source = Source::None;
|
Source source = Source::None;
|
||||||
|
|
||||||
// Schuetzt lastLat/lastLon/havePersisted/source: der Netzwerk-Task (Core 0)
|
bool haveUtcOffset = false;
|
||||||
// schreibt diese Werte, der Render-Loop (Core 1) liest sie ueber
|
int32_t utcOffsetSecs = 0;
|
||||||
// getHomeLocation()/currentSource().
|
bool metricUnits = true; // Standard: metrisch, bis die IP-Abfrage etwas anderes sagt
|
||||||
|
|
||||||
SemaphoreHandle_t mutex = nullptr;
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
|
|
||||||
void startGpsSerialIfNeeded() {
|
void startGpsSerialIfNeeded() {
|
||||||
@@ -44,9 +44,6 @@ namespace {
|
|||||||
gpsSerialStarted = true;
|
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) {
|
void persistLocationAndSource(double lat, double lon, Source newSource) {
|
||||||
prefs.putDouble("homeLat", lat);
|
prefs.putDouble("homeLat", lat);
|
||||||
prefs.putDouble("homeLon", lon);
|
prefs.putDouble("homeLon", lon);
|
||||||
@@ -127,6 +124,14 @@ void requestIpLookupIfNeeded() {
|
|||||||
double lon = doc["lon"] | 0.0;
|
double lon = doc["lon"] | 0.0;
|
||||||
if (lat == 0.0 && lon == 0.0) return;
|
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);
|
persistLocationAndSource(lat, lon, Source::IpGeolocation);
|
||||||
ipLookupDone = true;
|
ipLookupDone = true;
|
||||||
}
|
}
|
||||||
@@ -180,4 +185,8 @@ const char* currentGpsPinLabel() {
|
|||||||
|
|
||||||
bool hasGpsFix() { return gps.location.isValid(); }
|
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();
|
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 "radar_screen.h"
|
||||||
#include "splash_screen.h"
|
#include "splash_screen.h"
|
||||||
#include "led_alert.h"
|
#include "led_alert.h"
|
||||||
|
#include "flight_logbook.h"
|
||||||
|
|
||||||
TFT_eSPI tft = TFT_eSPI();
|
TFT_eSPI tft = TFT_eSPI();
|
||||||
|
|
||||||
constexpr int16_t CONTENT_TOP = 30;
|
constexpr int16_t CONTENT_TOP = 30; // schlanker Header, Radar bekommt den Rest des Screens
|
||||||
constexpr uint32_t POLL_INTERVAL_MS = 300;
|
constexpr uint32_t POLL_INTERVAL_MS = 300; // wie oft wir NACHSCHAUEN, ob sich Daten geaendert haben
|
||||||
constexpr uint32_t SWEEP_TICK_MS = 80;
|
constexpr uint32_t SWEEP_TICK_MS = 80; // wie oft der Sweep-Strahl ein Stueck weiterdreht
|
||||||
uint32_t lastPollMs = 0;
|
uint32_t lastPollMs = 0;
|
||||||
uint32_t lastSweepMs = 0;
|
uint32_t lastSweepMs = 0;
|
||||||
uint32_t lastRenderedVersion = 0xFFFFFFFF;
|
uint32_t lastRenderedVersion = 0xFFFFFFFF; // erzwingt einen ersten Render-Aufruf
|
||||||
bool forceRedraw = false;
|
bool forceRedraw = false;
|
||||||
|
bool wasEmergency = false;
|
||||||
|
bool bannerBlinkOn = false;
|
||||||
|
|
||||||
struct Rect {
|
struct Rect {
|
||||||
int16_t x, y, w, h;
|
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() {
|
void drawMenuButton() {
|
||||||
tft.fillRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_NAVY);
|
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.drawRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_DARKGREY);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
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);
|
tft.setTextDatum(TL_DATUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawHeader() {
|
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.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_BLACK);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
@@ -59,41 +65,99 @@ void drawHeader() {
|
|||||||
drawMenuButton();
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(300);
|
delay(300);
|
||||||
|
|
||||||
tft.init();
|
tft.init();
|
||||||
tft.setRotation(0);
|
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();
|
TouchInput::begin();
|
||||||
LedAlert::begin();
|
LedAlert::begin();
|
||||||
|
|
||||||
|
// --- SD-Karte: PFLICHT. Ohne Karte kein weiterer Screen. ---
|
||||||
bool sdOk = SdStorage::init();
|
bool sdOk = SdStorage::init();
|
||||||
if (sdOk) {
|
if (!sdOk) {
|
||||||
SdStorage::seedDefaultDataFiles();
|
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();
|
SettingsStore::load();
|
||||||
tft.invertDisplay(SettingsStore::displayInverted());
|
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::begin(tft);
|
||||||
SplashScreen::setStatusLine(tft, 0, sdOk ? "SD Card: OK" : "SD Card: ERROR",
|
SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE);
|
||||||
sdOk ? TFT_WHITE : TFT_RED);
|
|
||||||
|
|
||||||
|
// --- Touch-Kalibrierung (nur beim allerersten Start, oder wenn Datei fehlt) ---
|
||||||
if (!TouchInput::loadCalibration()) {
|
if (!TouchInput::loadCalibration()) {
|
||||||
CalibrationScreen::run(tft);
|
CalibrationScreen::run(tft);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool haveWifiFile = SD.exists(Config::SD_WIFI_CREDENTIALS_FILE);
|
// --- WLAN: Ersteinrichtung (falls noch kein Netzwerk gespeichert ist)
|
||||||
if (!haveWifiFile) {
|
// oder automatisches Verbinden mit dem ersten gerade sichtbaren
|
||||||
|
// gespeicherten Netzwerk (z.B. Zuhause ODER Auto-Hotspot). ---
|
||||||
|
if (WifiMgr::networkCount() == 0) {
|
||||||
WifiSetupScreen::run(tft);
|
WifiSetupScreen::run(tft);
|
||||||
} else {
|
} else {
|
||||||
if (!WifiMgr::hasStoredCredentials()) {
|
|
||||||
WifiMgr::loadCredentialsFromSd();
|
|
||||||
}
|
|
||||||
SplashScreen::setStatusLine(tft, 1, "Connecting WiFi...");
|
SplashScreen::setStatusLine(tft, 1, "Connecting WiFi...");
|
||||||
WifiMgr::beginConnect();
|
WifiMgr::beginConnect();
|
||||||
|
|
||||||
@@ -112,6 +176,7 @@ void setup() {
|
|||||||
|
|
||||||
AdsbClient::primeTime();
|
AdsbClient::primeTime();
|
||||||
|
|
||||||
|
// --- Standort per IP-Geolocation (einmalig blockierend beim Start) ---
|
||||||
SplashScreen::setStatusLine(tft, 2, "Getting location...");
|
SplashScreen::setStatusLine(tft, 2, "Getting location...");
|
||||||
LocationManager::init();
|
LocationManager::init();
|
||||||
uint32_t locStart = millis();
|
uint32_t locStart = millis();
|
||||||
@@ -122,11 +187,22 @@ void setup() {
|
|||||||
}
|
}
|
||||||
SplashScreen::setStatusLine(tft, 2, "Ready!");
|
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();
|
AircraftTable::init();
|
||||||
AirlineLookup::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();
|
NetTask::begin();
|
||||||
|
|
||||||
|
// Splash bleibt mindestens 5 Sekunden sichtbar, egal wie schnell der Rest war.
|
||||||
SplashScreen::waitRemaining();
|
SplashScreen::waitRemaining();
|
||||||
|
|
||||||
drawHeader();
|
drawHeader();
|
||||||
@@ -139,14 +215,19 @@ void loop() {
|
|||||||
if (menuBtn.contains(tap.x, tap.y)) {
|
if (menuBtn.contains(tap.x, tap.y)) {
|
||||||
MenuScreen::run(tft);
|
MenuScreen::run(tft);
|
||||||
drawHeader();
|
drawHeader();
|
||||||
forceRedraw = true;
|
forceRedraw = true; // sofort neu zeichnen
|
||||||
} else if (tap.y >= CONTENT_TOP) {
|
} else if (tap.y >= CONTENT_TOP) {
|
||||||
if (RadarScreen::handleTap(tap.x, 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) {
|
if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) {
|
||||||
lastPollMs = millis();
|
lastPollMs = millis();
|
||||||
uint32_t currentVersion = AircraftTable::version();
|
uint32_t currentVersion = AircraftTable::version();
|
||||||
@@ -154,19 +235,22 @@ void loop() {
|
|||||||
lastRenderedVersion = currentVersion;
|
lastRenderedVersion = currentVersion;
|
||||||
forceRedraw = false;
|
forceRedraw = false;
|
||||||
RadarScreen::render(tft, CONTENT_TOP);
|
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();
|
uint32_t nowMs = millis();
|
||||||
|
|
||||||
// Naeherungsalarm (LED) laeuft IMMER, auch wenn gerade das Detail-Fenster
|
// Naeherungs-/Notfall-Alarm (LED) laeuft IMMER, auch wenn gerade das
|
||||||
// offen ist - unabhaengig vom Sweep/Radar-Redraw.
|
// Detail-Fenster offen ist - unabhaengig vom Sweep/Radar-Redraw.
|
||||||
RadarScreen::updateProximityAlert(nowMs);
|
RadarScreen::updateProximityAlert(nowMs);
|
||||||
|
|
||||||
if (nowMs - lastSweepMs >= SWEEP_TICK_MS) {
|
if (nowMs - lastSweepMs >= SWEEP_TICK_MS) {
|
||||||
uint32_t deltaMs = nowMs - lastSweepMs;
|
uint32_t deltaMs = nowMs - lastSweepMs;
|
||||||
lastSweepMs = nowMs;
|
lastSweepMs = nowMs;
|
||||||
RadarScreen::tick(tft, CONTENT_TOP, deltaMs);
|
RadarScreen::tick(tft, CONTENT_TOP, deltaMs);
|
||||||
|
updateEmergencyBanner(nowMs); // gleiche Taktung wie der Sweep-Tick
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "menu_screen.h"
|
#include "menu_screen.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "calibration_screen.h"
|
#include "calibration_screen.h"
|
||||||
|
#include "wifi_manage_screen.h"
|
||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
#include "config.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) {
|
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.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);
|
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.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String onOff(bool on) { return on ? "ON" : "OFF"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void run(TFT_eSPI& tft) {
|
void run(TFT_eSPI& tft) {
|
||||||
Rect calibBtn = {10, 60, Config::SCREEN_WIDTH - 20, 40};
|
Rect calibBtn = rowRect(0);
|
||||||
Rect invertBtn = {10, 112, Config::SCREEN_WIDTH - 20, 40};
|
Rect invertBtn = rowRect(1);
|
||||||
Rect backBtn = {10, 260, Config::SCREEN_WIDTH - 20, 40};
|
Rect wifiBtn = rowRect(2);
|
||||||
|
Rect emergencyBtn = rowRect(3);
|
||||||
|
Rect proximityBtn = rowRect(4);
|
||||||
|
Rect logbookBtn = rowRect(5);
|
||||||
|
Rect backBtn = rowRect(6);
|
||||||
|
|
||||||
bool done = false;
|
bool done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 8);
|
||||||
tft.println("Einstellungen");
|
tft.println("Settings");
|
||||||
|
|
||||||
|
drawButton(tft, calibBtn, "Calibrate touch");
|
||||||
|
|
||||||
drawButton(tft, calibBtn, "Touch kalibrieren");
|
|
||||||
String invertLabel = SettingsStore::displayInverted()
|
String invertLabel = SettingsStore::displayInverted()
|
||||||
? "Display: invertiert (antippen)"
|
? "Display: inverted (tap)"
|
||||||
: "Display: normal (antippen)";
|
: "Display: normal (tap)";
|
||||||
drawButton(tft, invertBtn, invertLabel);
|
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;
|
TouchInput::Point tap;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (TouchInput::wasTapped(tap)) break;
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
@@ -56,6 +79,14 @@ void run(TFT_eSPI& tft) {
|
|||||||
bool newState = !SettingsStore::displayInverted();
|
bool newState = !SettingsStore::displayInverted();
|
||||||
SettingsStore::setDisplayInverted(newState);
|
SettingsStore::setDisplayInverted(newState);
|
||||||
tft.invertDisplay(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)) {
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "aircraft.h"
|
#include "aircraft.h"
|
||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
#include "aircraft_details.h"
|
#include "aircraft_details.h"
|
||||||
|
#include "flight_logbook.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
@@ -18,11 +19,6 @@ namespace {
|
|||||||
TaskHandle_t taskHandle = nullptr;
|
TaskHandle_t taskHandle = nullptr;
|
||||||
uint32_t lastFetchMs = 0;
|
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];
|
Aircraft tempTable[Config::MAX_TRACKED_AIRCRAFT];
|
||||||
|
|
||||||
void taskFunc(void*) {
|
void taskFunc(void*) {
|
||||||
@@ -30,9 +26,6 @@ namespace {
|
|||||||
WifiMgr::update();
|
WifiMgr::update();
|
||||||
LocationManager::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();
|
AircraftDetails::update();
|
||||||
|
|
||||||
if (millis() - lastFetchMs >= Config::FETCH_INTERVAL_MS) {
|
if (millis() - lastFetchMs >= Config::FETCH_INTERVAL_MS) {
|
||||||
@@ -46,10 +39,6 @@ namespace {
|
|||||||
|
|
||||||
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
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,
|
auto result = AdsbClient::fetch(lat, lon, rangeKm,
|
||||||
tempTable, Config::MAX_TRACKED_AIRCRAFT);
|
tempTable, Config::MAX_TRACKED_AIRCRAFT);
|
||||||
|
|
||||||
@@ -59,6 +48,8 @@ namespace {
|
|||||||
sizeof(Aircraft) * Config::MAX_TRACKED_AIRCRAFT);
|
sizeof(Aircraft) * Config::MAX_TRACKED_AIRCRAFT);
|
||||||
AircraftTable::postFetchUpdate(lat, lon);
|
AircraftTable::postFetchUpdate(lat, lon);
|
||||||
AircraftTable::unlock();
|
AircraftTable::unlock();
|
||||||
|
|
||||||
|
FlightLogbook::update();
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode);
|
Serial.printf("[NetTask] Abfrage fehlgeschlagen (HTTP %d)\n", result.httpCode);
|
||||||
}
|
}
|
||||||
@@ -74,11 +65,11 @@ void begin() {
|
|||||||
xTaskCreatePinnedToCore(
|
xTaskCreatePinnedToCore(
|
||||||
taskFunc,
|
taskFunc,
|
||||||
"NetTask",
|
"NetTask",
|
||||||
20480, // Stack: TLS-Handshake + JSON-Parsing braucht mehr als das Minimum
|
20480,
|
||||||
nullptr,
|
nullptr,
|
||||||
1, // Prioritaet
|
1,
|
||||||
&taskHandle,
|
&taskHandle,
|
||||||
0 // Core 0 (Core 1 bleibt frei fuer Rendering/Touch im main-Loop)
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "units.h"
|
#include "units.h"
|
||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
#include "led_alert.h"
|
#include "led_alert.h"
|
||||||
|
#include "location_manager.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
namespace RadarScreen {
|
namespace RadarScreen {
|
||||||
@@ -50,6 +51,7 @@ namespace {
|
|||||||
uint16_t color;
|
uint16_t color;
|
||||||
float headingDeg;
|
float headingDeg;
|
||||||
float distanceKm;
|
float distanceKm;
|
||||||
|
bool isEmergency;
|
||||||
};
|
};
|
||||||
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
||||||
HitPoint hitPoints[MAX_HIT_POINTS];
|
HitPoint hitPoints[MAX_HIT_POINTS];
|
||||||
@@ -58,6 +60,14 @@ namespace {
|
|||||||
|
|
||||||
bool ledBlinkOn = true;
|
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 sweepAngleDeg = 0.0f;
|
||||||
float prevSweepAngleDeg = -1.0f;
|
float prevSweepAngleDeg = -1.0f;
|
||||||
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
|
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
|
||||||
@@ -89,11 +99,29 @@ namespace {
|
|||||||
gfx.setTextDatum(TL_DATUM);
|
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) {
|
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] = {
|
struct { uint16_t color; const char* label; } items[3] = {
|
||||||
{TFT_GREEN, "<10k ft"},
|
{TFT_GREEN, lowLabel},
|
||||||
{TFT_YELLOW, "10-30k"},
|
{TFT_YELLOW, midLabel},
|
||||||
{TFT_RED, ">30k ft"},
|
{TFT_RED, highLabel},
|
||||||
};
|
};
|
||||||
int16_t segW = Config::SCREEN_WIDTH / 3;
|
int16_t segW = Config::SCREEN_WIDTH / 3;
|
||||||
gfx.setTextColor(TFT_WHITE, TFT_BLACK);
|
gfx.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
@@ -290,6 +318,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
|
|
||||||
uint16_t color = colorForAltitude(a.altBaroFt);
|
uint16_t color = colorForAltitude(a.altBaroFt);
|
||||||
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
||||||
|
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE);
|
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);
|
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;
|
double rad = a.headingDeg * PI / 180.0;
|
||||||
int16_t dx = (int16_t)(sin(rad) * 10);
|
int16_t dx = (int16_t)(sin(rad) * 10);
|
||||||
int16_t dy = (int16_t)(-cos(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].color = color;
|
||||||
hitPoints[i].headingDeg = a.headingDeg;
|
hitPoints[i].headingDeg = a.headingDeg;
|
||||||
hitPoints[i].distanceKm = a.distanceKm;
|
hitPoints[i].distanceKm = a.distanceKm;
|
||||||
|
hitPoints[i].isEmergency = isEmergency;
|
||||||
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
||||||
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 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);
|
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;
|
double rad = hp.headingDeg * PI / 180.0;
|
||||||
int16_t dx = (int16_t)(sin(rad) * 10);
|
int16_t dx = (int16_t)(sin(rad) * 10);
|
||||||
int16_t dy = (int16_t)(-cos(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) {
|
void updateProximityAlert(uint32_t nowMs) {
|
||||||
bool anyClose = false;
|
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();
|
AircraftTable::lock();
|
||||||
Aircraft* table = AircraftTable::raw();
|
Aircraft* table = AircraftTable::raw();
|
||||||
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
||||||
if (table[i].valid && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) {
|
if (table[i].valid && isEmergencySquawk(table[i].squawk)) {
|
||||||
anyClose = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AircraftTable::unlock();
|
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);
|
bool handleTap(int16_t x, int16_t y, int16_t top);
|
||||||
|
|
||||||
// Haeufig aufrufen (unabhaengig davon, ob ein Detail-Fenster offen ist):
|
// Haeufig aufrufen (unabhaengig davon, ob ein Detail-Fenster offen ist):
|
||||||
// prueft, ob ein Flugzeug innerhalb des Alarmradius ist, steuert die
|
// prueft, ob ein Flugzeug innerhalb des Alarmradius ist ODER ein
|
||||||
// gruene LED entsprechend und merkt sich den Blink-Zustand, damit tick()
|
// Notfall-Squawk sendet, steuert die LED entsprechend (Notfall hat
|
||||||
// den betroffenen Punkt synchron mitblinken lassen kann.
|
// 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);
|
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 {
|
namespace {
|
||||||
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
|
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
|
||||||
bool inverted = false;
|
bool inverted = false;
|
||||||
|
bool emergencyAlertOn = true;
|
||||||
|
bool proximityAlertOn = true;
|
||||||
|
bool flightLogbookOn = true;
|
||||||
|
|
||||||
void applyKeyValue(const String& key, const String& value) {
|
void applyKeyValue(const String& key, const String& value) {
|
||||||
if (key == "range_index") {
|
if (key == "range_index") {
|
||||||
@@ -16,26 +19,26 @@ namespace {
|
|||||||
}
|
}
|
||||||
} else if (key == "invert") {
|
} else if (key == "invert") {
|
||||||
inverted = (value.toInt() != 0);
|
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() {
|
void load() {
|
||||||
if (!SD.exists(Config::SD_SETTINGS_FILE)) return;
|
if (!SD.exists(Config::SD_SETTINGS_FILE)) return;
|
||||||
|
|
||||||
File f = SD.open(Config::SD_SETTINGS_FILE, FILE_READ);
|
File f = SD.open(Config::SD_SETTINGS_FILE, FILE_READ);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
|
|
||||||
while (f.available()) {
|
while (f.available()) {
|
||||||
String line = f.readStringUntil('\n');
|
String line = f.readStringUntil('\n');
|
||||||
line.trim();
|
line.trim();
|
||||||
if (line.length() == 0 || line.startsWith("#")) continue;
|
if (line.length() == 0 || line.startsWith("#")) continue;
|
||||||
|
|
||||||
int eq = line.indexOf('=');
|
int eq = line.indexOf('=');
|
||||||
if (eq < 0) continue;
|
if (eq < 0) continue;
|
||||||
|
|
||||||
String key = line.substring(0, eq);
|
String key = line.substring(0, eq);
|
||||||
String value = line.substring(eq + 1);
|
String value = line.substring(eq + 1);
|
||||||
key.trim();
|
key.trim();
|
||||||
@@ -50,23 +53,27 @@ void save() {
|
|||||||
if (!f) return;
|
if (!f) return;
|
||||||
f.printf("range_index=%d\n", rangeIdx);
|
f.printf("range_index=%d\n", rangeIdx);
|
||||||
f.printf("invert=%d\n", inverted ? 1 : 0);
|
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();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t rangeIndex() { return rangeIdx; }
|
uint8_t rangeIndex() { return rangeIdx; }
|
||||||
|
|
||||||
void setRangeIndex(uint8_t idx) {
|
void setRangeIndex(uint8_t idx) {
|
||||||
if (idx < Config::RANGE_STEP_COUNT) {
|
if (idx < Config::RANGE_STEP_COUNT) { rangeIdx = idx; save(); }
|
||||||
rangeIdx = idx;
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool displayInverted() { return inverted; }
|
bool displayInverted() { return inverted; }
|
||||||
|
void setDisplayInverted(bool inv) { inverted = inv; save(); }
|
||||||
|
|
||||||
void setDisplayInverted(bool inv) {
|
bool emergencyAlertEnabled() { return emergencyAlertOn; }
|
||||||
inverted = inv;
|
void setEmergencyAlertEnabled(bool on) { emergencyAlertOn = on; save(); }
|
||||||
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
|
#pragma once
|
||||||
#include <Arduino.h>
|
#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 {
|
namespace SettingsStore {
|
||||||
|
|
||||||
// Liest config.txt von der SD-Karte (falls vorhanden). Werte, die nicht
|
|
||||||
// in der Datei stehen, behalten ihren Standardwert.
|
|
||||||
void load();
|
void load();
|
||||||
|
|
||||||
// Schreibt die aktuellen Werte zurueck auf die SD-Karte.
|
|
||||||
void save();
|
void save();
|
||||||
|
|
||||||
uint8_t rangeIndex();
|
uint8_t rangeIndex();
|
||||||
@@ -19,4 +10,13 @@ namespace SettingsStore {
|
|||||||
|
|
||||||
bool displayInverted();
|
bool displayInverted();
|
||||||
void setDisplayInverted(bool inverted);
|
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 int16_t STATUS_START_Y = 260;
|
||||||
constexpr uint8_t MAX_STATUS_LINES = 3;
|
constexpr uint8_t MAX_STATUS_LINES = 3;
|
||||||
|
|
||||||
// Einfache Vektor-Silhouette eines Flugzeugs von oben (keine Bilddatei
|
// Dezente konzentrische Ringe + Fadenkreuz als "Zielfernrohr"-Hintergrund,
|
||||||
// eingebunden, daher per Dreiecken gezeichnet) - in Gruen, wie der
|
// in gedaempftem Gruen (gleicher Ton wie die Sweep-Nachzieh-Linie im
|
||||||
// Radar-Sweep-Strahl, passend zum restlichen Dark-Theme.
|
// 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) {
|
void drawAirplane(TFT_eSPI& tft, int16_t cx) {
|
||||||
uint16_t color = TFT_GREEN;
|
uint16_t color = TFT_GREEN;
|
||||||
|
|
||||||
// Rumpf
|
// Schlanker Rumpf (nur Umriss)
|
||||||
tft.fillTriangle(cx, 110, cx - 12, 240, cx + 12, 240, color);
|
tft.drawTriangle(cx, 105, cx - 6, 255, cx + 6, 255, color);
|
||||||
|
|
||||||
// Haupttragflaechen
|
// Deltafluegel, nach hinten gepfeilt (nur Umriss)
|
||||||
tft.fillTriangle(cx, 170, cx - 110, 230, cx + 110, 230, color);
|
tft.drawTriangle(cx, 160, cx - 100, 235, cx, 200, color);
|
||||||
|
tft.drawTriangle(cx, 160, cx + 100, 235, cx, 200, color);
|
||||||
|
|
||||||
// Leitwerk (Heckfluegel)
|
// Kleines Leitwerk (Hoehenruder) am Heck (nur Umriss)
|
||||||
tft.fillTriangle(cx, 232, cx - 35, 250, cx + 35, 250, color);
|
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;
|
int16_t cx = tft.width() / 2;
|
||||||
|
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
drawRadarReticle(tft, cx, 185);
|
||||||
drawAirplane(tft, cx);
|
drawAirplane(tft, cx);
|
||||||
|
|
||||||
tft.setTextDatum(MC_DATUM);
|
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 "wifi_manager.h"
|
||||||
#include "config.h"
|
|
||||||
#include "sd_storage.h"
|
#include "sd_storage.h"
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <Preferences.h>
|
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace WifiMgr {
|
namespace WifiMgr {
|
||||||
|
|
||||||
namespace {
|
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;
|
State state = State::Idle;
|
||||||
uint32_t connectStartMs = 0;
|
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};
|
char ipStr[16] = {0};
|
||||||
|
|
||||||
// Schuetzt 'state': der Netzwerk-Task (Core 0) schreibt es in
|
uint32_t lastReconnectAttemptMs = 0;
|
||||||
// beginConnect()/update(), der Render-Loop (Core 1) liest es ueber
|
constexpr uint32_t RECONNECT_RETRY_MS = 10000;
|
||||||
// getState(). ipStr wird nur einmal beim Verbinden geschrieben und danach
|
|
||||||
// nur gelesen, daher hier ohne eigenen Lock.
|
|
||||||
SemaphoreHandle_t mutex = nullptr;
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
|
|
||||||
void setState(State s) {
|
void setState(State s) {
|
||||||
@@ -27,50 +32,136 @@ namespace {
|
|||||||
state = s;
|
state = s;
|
||||||
xSemaphoreGive(mutex);
|
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() {
|
void init() {
|
||||||
if (mutex == nullptr) mutex = xSemaphoreCreateMutex();
|
if (mutex == nullptr) mutex = xSemaphoreCreateMutex();
|
||||||
prefs.begin("adsb_radar", /*readOnly=*/false);
|
loadFromSd();
|
||||||
setState(hasStoredCredentials() ? State::Idle : State::NoCredentials);
|
setState(networkCountVal > 0 ? State::Idle : State::NoCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasStoredCredentials() {
|
uint8_t networkCount() { return networkCountVal; }
|
||||||
String ssid = prefs.getString("ssid", "");
|
|
||||||
return ssid.length() > 0;
|
String networkSsid(uint8_t index) {
|
||||||
|
if (index >= networkCountVal) return String();
|
||||||
|
return String(networks[index].ssid);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveCredentials(const char* ssid, const char* password) {
|
bool addNetwork(const char* ssid, const char* password) {
|
||||||
prefs.putString("ssid", ssid);
|
if (networkCountVal >= Config::MAX_WIFI_NETWORKS) return false;
|
||||||
prefs.putString("pass", password);
|
|
||||||
|
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() {
|
void removeNetwork(uint8_t index) {
|
||||||
if (!hasStoredCredentials()) {
|
if (index >= networkCountVal) return;
|
||||||
setState(State::NoCredentials);
|
for (uint8_t i = index; i < networkCountVal - 1; i++) {
|
||||||
return;
|
networks[i] = networks[i + 1];
|
||||||
}
|
}
|
||||||
String ssid = prefs.getString("ssid", "");
|
networkCountVal--;
|
||||||
String pass = prefs.getString("pass", "");
|
networks[networkCountVal] = NetworkEntry{};
|
||||||
|
saveToSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectTo(const char* ssid, const char* password) {
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
WiFi.begin(ssid, password);
|
||||||
connectStartMs = millis();
|
connectStartMs = millis();
|
||||||
setState(State::Connecting);
|
setState(State::Connecting);
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void beginConnect() {
|
||||||
if (getState() != State::Connecting) return;
|
if (networkCountVal == 0) {
|
||||||
|
setState(State::NoCredentials);
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
strncpy(ipStr, WiFi.localIP().toString().c_str(), sizeof(ipStr) - 1);
|
|
||||||
setState(State::Connected);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (millis() - connectStartMs > CONNECT_TIMEOUT_MS) {
|
int visibleCount = WiFi.scanNetworks(/*async=*/false);
|
||||||
WiFi.disconnect(true);
|
|
||||||
|
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);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,37 +195,4 @@ int32_t getScanResultRSSI(int index) {
|
|||||||
|
|
||||||
const char* getIP() { return ipStr; }
|
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
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
namespace WifiMgr {
|
namespace WifiMgr {
|
||||||
enum class State {
|
enum class State {
|
||||||
@@ -8,20 +9,20 @@ namespace WifiMgr {
|
|||||||
|
|
||||||
void init();
|
void init();
|
||||||
void beginConnect();
|
void beginConnect();
|
||||||
|
void connectTo(const char* ssid, const char* password);
|
||||||
|
|
||||||
void update();
|
void update();
|
||||||
State getState();
|
State getState();
|
||||||
|
const char* getIP();
|
||||||
|
|
||||||
void saveCredentials(const char* ssid, const char* password);
|
uint8_t networkCount();
|
||||||
bool hasStoredCredentials();
|
String networkSsid(uint8_t index);
|
||||||
|
bool addNetwork(const char* ssid, const char* password);
|
||||||
bool loadCredentialsFromSd();
|
void removeNetwork(uint8_t index);
|
||||||
void saveCredentialsToSdIfMounted();
|
|
||||||
|
|
||||||
void beginScan();
|
void beginScan();
|
||||||
bool isScanComplete();
|
bool isScanComplete();
|
||||||
int getScanResultCount();
|
int getScanResultCount();
|
||||||
String getScanResultSSID(int index);
|
String getScanResultSSID(int index);
|
||||||
int32_t getScanResultRSSI(int index);
|
int32_t getScanResultRSSI(int index);
|
||||||
|
|
||||||
const char* getIP();
|
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,10 @@ namespace {
|
|||||||
|
|
||||||
struct KeyDef {
|
struct KeyDef {
|
||||||
KeyType type;
|
KeyType type;
|
||||||
char ch; // fuer KeyType::Char (Kleinbuchstabe/Grundzeichen)
|
char ch;
|
||||||
const char* label; // Anzeige, falls kein einzelnes Zeichen (z.B. "<-")
|
const char* label;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Tastatur-Layout ---------------------------------------------------
|
|
||||||
constexpr KeyDef ROW_LETTERS_A[] = {
|
constexpr KeyDef ROW_LETTERS_A[] = {
|
||||||
{KeyType::Char,'q',nullptr},{KeyType::Char,'w',nullptr},{KeyType::Char,'e',nullptr},
|
{KeyType::Char,'q',nullptr},{KeyType::Char,'w',nullptr},{KeyType::Char,'e',nullptr},
|
||||||
{KeyType::Char,'r',nullptr},{KeyType::Char,'t',nullptr},{KeyType::Char,'y',nullptr},
|
{KeyType::Char,'r',nullptr},{KeyType::Char,'t',nullptr},{KeyType::Char,'y',nullptr},
|
||||||
@@ -79,11 +78,11 @@ namespace {
|
|||||||
uint8_t passwordLen = 0;
|
uint8_t passwordLen = 0;
|
||||||
|
|
||||||
bool shiftOn = false;
|
bool shiftOn = false;
|
||||||
uint8_t page = 0; // 0 = Buchstaben, 1 = Symbole
|
uint8_t page = 0;
|
||||||
|
|
||||||
bool skipped = false;
|
bool skipped = false;
|
||||||
bool connectSucceeded = 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};
|
Rect cancelBtn = {Config::SCREEN_WIDTH - 34, 4, 30, 24};
|
||||||
|
|
||||||
@@ -97,7 +96,6 @@ namespace {
|
|||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechnet die Rects einer Tastenreihe aus (gleich verteilt ueber die Breite).
|
|
||||||
template <size_t N>
|
template <size_t N>
|
||||||
void layoutRow(const KeyDef (&row)[N], int16_t y, Rect outRects[N]) {
|
void layoutRow(const KeyDef (&row)[N], int16_t y, Rect outRects[N]) {
|
||||||
int16_t usableW = Config::SCREEN_WIDTH - 2 * SIDE_MARGIN;
|
int16_t usableW = Config::SCREEN_WIDTH - 2 * SIDE_MARGIN;
|
||||||
@@ -184,10 +182,10 @@ bool run(TFT_eSPI& tft) {
|
|||||||
WifiMgr::beginScan();
|
WifiMgr::beginScan();
|
||||||
|
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.println("WLAN-Suche laeuft...");
|
tft.println("Scanning WiFi...");
|
||||||
drawCancelButton(tft);
|
drawCancelButton(tft);
|
||||||
|
|
||||||
while (!skipped) {
|
while (!skipped) {
|
||||||
@@ -199,7 +197,6 @@ bool run(TFT_eSPI& tft) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Uebergaenge, die nicht vom Touch kommen -----------------------
|
|
||||||
if (stage == Stage::Scanning && WifiMgr::isScanComplete()) {
|
if (stage == Stage::Scanning && WifiMgr::isScanComplete()) {
|
||||||
ssidCount = (uint8_t)min((int)WifiMgr::getScanResultCount(), (int)MAX_LIST);
|
ssidCount = (uint8_t)min((int)WifiMgr::getScanResultCount(), (int)MAX_LIST);
|
||||||
for (uint8_t i = 0; i < ssidCount; i++) ssidList[i] = WifiMgr::getScanResultSSID(i);
|
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.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println(ssidCount == 0 ? "Keine Netzwerke gefunden" : "WLAN waehlen:");
|
tft.println(ssidCount == 0 ? "No networks found" : "Select WiFi:");
|
||||||
drawCancelButton(tft);
|
drawCancelButton(tft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,12 +214,12 @@ bool run(TFT_eSPI& tft) {
|
|||||||
WifiMgr::update();
|
WifiMgr::update();
|
||||||
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
||||||
connectSucceeded = true;
|
connectSucceeded = true;
|
||||||
WifiMgr::saveCredentialsToSdIfMounted();
|
WifiMgr::addNetwork(ssidList[selectedIndex].c_str(), passwordBuf);
|
||||||
stage = Stage::Done;
|
stage = Stage::Done;
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println("Verbunden!");
|
tft.println("Connected!");
|
||||||
delay(900);
|
delay(900);
|
||||||
return true;
|
return true;
|
||||||
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
||||||
@@ -230,10 +227,10 @@ bool run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_RED, TFT_BLACK);
|
tft.setTextColor(TFT_RED, TFT_BLACK);
|
||||||
tft.println("Verbindung fehlgeschlagen.");
|
tft.println("Connection failed.");
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 30);
|
tft.setCursor(10, 30);
|
||||||
tft.println("Zurueck zur Netzwerkliste...");
|
tft.println("Back to network list...");
|
||||||
delay(1400);
|
delay(1400);
|
||||||
stage = Stage::PickSsid;
|
stage = Stage::PickSsid;
|
||||||
WifiMgr::beginScan();
|
WifiMgr::beginScan();
|
||||||
@@ -241,10 +238,8 @@ bool run(TFT_eSPI& tft) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Touch-Eingaben je nach Stage -----------------------------------
|
|
||||||
if (tapped && stage == Stage::PickSsid) {
|
if (tapped && stage == Stage::PickSsid) {
|
||||||
if (ssidCount > 0) {
|
if (ssidCount > 0) {
|
||||||
// Liste
|
|
||||||
for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) {
|
for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) {
|
||||||
uint8_t idx = scrollOffset + row;
|
uint8_t idx = scrollOffset + row;
|
||||||
if (idx >= ssidCount) break;
|
if (idx >= ssidCount) break;
|
||||||
@@ -256,7 +251,6 @@ bool run(TFT_eSPI& tft) {
|
|||||||
needsRedraw = true;
|
needsRedraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scroll-Pfeile
|
|
||||||
if (ssidCount > VISIBLE_ITEMS) {
|
if (ssidCount > VISIBLE_ITEMS) {
|
||||||
Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26};
|
Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26};
|
||||||
Rect downBtn = {Config::SCREEN_WIDTH - 34, 64 + (VISIBLE_ITEMS - 1) * 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;
|
passwordBuf[passwordLen] = 0;
|
||||||
}
|
}
|
||||||
} else if (connectBtn.contains(tap.x, tap.y)) {
|
} else if (connectBtn.contains(tap.x, tap.y)) {
|
||||||
WifiMgr::saveCredentials(ssidList[selectedIndex].c_str(), passwordBuf);
|
if (WifiMgr::networkCount() >= Config::MAX_WIFI_NETWORKS) {
|
||||||
WifiMgr::beginConnect();
|
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;
|
stage = Stage::Connecting;
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println("Verbinde...");
|
tft.println("Connecting...");
|
||||||
} else if (backBtn.contains(tap.x, tap.y)) {
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
stage = Stage::PickSsid;
|
stage = Stage::PickSsid;
|
||||||
needsRedraw = true;
|
needsRedraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
needsRedraw = true; // Tastatureingabe (Zeichen/Shift/Seite/Backspace/Leerzeichen) aendert den Bildschirm
|
needsRedraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Zeichnen (nur bei Aenderung, verhindert Flackern) ----------------
|
|
||||||
if (!needsRedraw) {
|
if (!needsRedraw) {
|
||||||
delay(20);
|
delay(20);
|
||||||
continue;
|
continue;
|
||||||
@@ -333,16 +337,16 @@ bool run(TFT_eSPI& tft) {
|
|||||||
drawCancelButton(tft);
|
drawCancelButton(tft);
|
||||||
} else if (stage == Stage::EnterPassword) {
|
} else if (stage == Stage::EnterPassword) {
|
||||||
tft.fillRect(0, 0, Config::SCREEN_WIDTH, KB_TOP - 4, TFT_BLACK);
|
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.setCursor(10, 6);
|
||||||
tft.printf("WLAN: %s", ssidList[selectedIndex].c_str());
|
tft.printf("WiFi: %s", ssidList[selectedIndex].c_str());
|
||||||
tft.setCursor(10, 22);
|
tft.setCursor(10, 22);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println("Passwort:");
|
tft.println("Password:");
|
||||||
tft.fillRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_NAVY);
|
tft.fillRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_NAVY);
|
||||||
tft.drawRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_DARKGREY);
|
tft.drawRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_DARKGREY);
|
||||||
tft.setCursor(12, 44);
|
tft.setCursor(12, 44);
|
||||||
tft.setTextColor(TFT_YELLOW, TFT_NAVY);
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
tft.print(passwordBuf);
|
tft.print(passwordBuf);
|
||||||
|
|
||||||
int16_t rowY0 = KB_TOP;
|
int16_t rowY0 = KB_TOP;
|
||||||
@@ -363,15 +367,15 @@ bool run(TFT_eSPI& tft) {
|
|||||||
Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H};
|
Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H};
|
||||||
Rect connectBtn = {SIDE_MARGIN + 154, rowYFn, Config::SCREEN_WIDTH - 2*SIDE_MARGIN - 154, 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};
|
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, spaceBtn, "Space");
|
||||||
drawButton(tft, connectBtn, "Verbinden");
|
drawButton(tft, connectBtn, "Connect");
|
||||||
drawButton(tft, backBtn, "Zurueck zur Liste");
|
drawButton(tft, backBtn, "Back to list");
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(20);
|
delay(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false; // uebersprungen
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user