v2.0: Full localization (6 languages), first-run language screen, aircraft detail overhaul, aircraft-model API fallback, and multiple bugfixes
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
#include <WiFi.h>
|
|
||||||
#include "adsb_client.h"
|
#include "adsb_client.h"
|
||||||
|
#include <WiFi.h>
|
||||||
#include <WiFiClientSecure.h>
|
#include <WiFiClientSecure.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
|
||||||
namespace AdsbClient {
|
namespace AdsbClient {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -71,6 +70,7 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm,
|
|||||||
filterAc["gs"] = true;
|
filterAc["gs"] = true;
|
||||||
filterAc["track"] = true;
|
filterAc["track"] = true;
|
||||||
filterAc["squawk"] = true;
|
filterAc["squawk"] = true;
|
||||||
|
filterAc["category"] = true;
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
DeserializationError err = deserializeJson(
|
DeserializationError err = deserializeJson(
|
||||||
@@ -119,6 +119,9 @@ FetchResult fetch(double homeLat, double homeLon, float radiusKm,
|
|||||||
const char* squawk = ac["squawk"] | "";
|
const char* squawk = ac["squawk"] | "";
|
||||||
strncpy(a.squawk, squawk, sizeof(a.squawk) - 1);
|
strncpy(a.squawk, squawk, sizeof(a.squawk) - 1);
|
||||||
|
|
||||||
|
const char* category = ac["category"] | "";
|
||||||
|
strncpy(a.category, category, sizeof(a.category) - 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,24 +5,25 @@ 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)
|
char squawk[5] = {0};
|
||||||
|
char category[3] = {0};
|
||||||
|
|
||||||
float lat = 0;
|
float lat = 0;
|
||||||
float lon = 0;
|
float lon = 0;
|
||||||
int32_t altBaroFt = 0; // barometric altitude, ft
|
int32_t altBaroFt = 0;
|
||||||
int16_t vertRateFtMin = 0; // vertical speed, ft/min (+climb / -descend)
|
int16_t vertRateFtMin = 0;
|
||||||
float groundSpeedKt = 0;
|
float groundSpeedKt = 0;
|
||||||
float headingDeg = 0; // track/heading
|
float headingDeg = 0;
|
||||||
|
|
||||||
float distanceKm = 0; // computed relative to home, via Haversine
|
float distanceKm = 0;
|
||||||
float bearingDeg = 0; // computed relative to home
|
float bearingDeg = 0;
|
||||||
|
|
||||||
uint32_t lastSeenMs = 0; // millis() at last update, for stale-entry eviction
|
uint32_t lastSeenMs = 0;
|
||||||
bool alerted = false;// whether proximity beep already fired this pass
|
bool alerted = false;
|
||||||
uint32_t alertedAtMs = 0;
|
uint32_t alertedAtMs = 0;
|
||||||
|
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
||||||
char airlineName[24] = {0}; // resolved from callsign ICAO prefix
|
char airlineName[24] = {0};
|
||||||
uint16_t estSeats = 0; // rough capacity estimate from typeCode, 0 = unknown
|
uint16_t estSeats = 0;
|
||||||
};
|
};
|
||||||
@@ -75,8 +75,11 @@ void update() {
|
|||||||
|
|
||||||
WiFiClientSecure client;
|
WiFiClientSecure client;
|
||||||
client.setInsecure();
|
client.setInsecure();
|
||||||
client.setTimeout(5000);
|
|
||||||
|
|
||||||
|
// hexdb.io zuerst versuchen (bisherige Quelle) - aber mit kuerzerem
|
||||||
|
// Timeout (3s statt 5s), damit ein kompletter Ausfall des Dienstes die
|
||||||
|
// ADS-B-Abfrage nicht unnoetig lange blockiert, bevor der Fallback greift.
|
||||||
|
client.setTimeout(3000);
|
||||||
String body;
|
String body;
|
||||||
if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) {
|
if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) {
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
@@ -92,6 +95,27 @@ void update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: hexdb.io war nicht erreichbar/lieferte kein Modell -
|
||||||
|
// adsbdb.com als zweite, unabhaengige Quelle versuchen (andere API-Form,
|
||||||
|
// aber inhaltlich aequivalent: Hersteller + Typ ueber den Hex-Code).
|
||||||
|
if (!result.model[0]) {
|
||||||
|
client.setTimeout(4000);
|
||||||
|
String body2;
|
||||||
|
if (httpGetString(client, String("https://api.adsbdb.com/v0/aircraft/") + hex, body2)) {
|
||||||
|
JsonDocument doc2;
|
||||||
|
DeserializationError err2 = deserializeJson(doc2, body2);
|
||||||
|
if (!err2) {
|
||||||
|
const char* manufacturer = doc2["response"]["aircraft"]["manufacturer"] | "";
|
||||||
|
const char* type = doc2["response"]["aircraft"]["type"] | "";
|
||||||
|
if (manufacturer[0] && type[0]) {
|
||||||
|
snprintf(result.model, sizeof(result.model), "%s %s", manufacturer, type);
|
||||||
|
} else if (type[0]) {
|
||||||
|
strncpy(result.model, type, sizeof(result.model) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||||
strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
|
strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
|
||||||
cached = result;
|
cached = result;
|
||||||
|
|||||||
114
src/airline_filter.cpp
Normal file
114
src/airline_filter.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include "airline_filter.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
|
#include "sd_storage.h"
|
||||||
|
#include <SD.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace AirlineFilter {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char* HIDDEN_FILE = "/Flightradar_cyd/hidden_airlines.txt";
|
||||||
|
|
||||||
|
char hidden[MAX_HIDDEN][4] = {{0}};
|
||||||
|
uint8_t hiddenCount = 0;
|
||||||
|
|
||||||
|
void extractPrefix(const char* callsign, char* out) {
|
||||||
|
int i = 0;
|
||||||
|
for (; i < 3 && callsign[i] && isalpha((unsigned char)callsign[i]); i++) {
|
||||||
|
out[i] = (char)toupper((unsigned char)callsign[i]);
|
||||||
|
}
|
||||||
|
out[i] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveToSd() {
|
||||||
|
if (!SdStorage::isMounted()) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
File f = SD.open(HIDDEN_FILE, FILE_WRITE);
|
||||||
|
if (!f) return;
|
||||||
|
for (uint8_t i = 0; i < hiddenCount; i++) {
|
||||||
|
f.println(hidden[i]);
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadFromSd() {
|
||||||
|
hiddenCount = 0;
|
||||||
|
if (!SdStorage::isMounted()) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
if (!SD.exists(HIDDEN_FILE)) return;
|
||||||
|
File f = SD.open(HIDDEN_FILE, FILE_READ);
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
while (f.available() && hiddenCount < MAX_HIDDEN) {
|
||||||
|
String line = f.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.length() == 0) continue;
|
||||||
|
strncpy(hidden[hiddenCount], line.c_str(), 3);
|
||||||
|
hidden[hiddenCount][3] = 0;
|
||||||
|
hiddenCount++;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
loadFromSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t count() { return hiddenCount; }
|
||||||
|
|
||||||
|
String icaoAt(uint8_t index) {
|
||||||
|
if (index >= hiddenCount) return String();
|
||||||
|
return String(hidden[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addHidden(const char* icaoPrefix) {
|
||||||
|
if (hiddenCount >= MAX_HIDDEN) return false;
|
||||||
|
if (!icaoPrefix || !icaoPrefix[0]) return false;
|
||||||
|
|
||||||
|
char normalized[4] = {0};
|
||||||
|
uint8_t i = 0;
|
||||||
|
for (; i < 3 && icaoPrefix[i]; i++) {
|
||||||
|
normalized[i] = (char)toupper((unsigned char)icaoPrefix[i]);
|
||||||
|
}
|
||||||
|
normalized[i] = 0;
|
||||||
|
if (i == 0) return false;
|
||||||
|
|
||||||
|
for (uint8_t j = 0; j < hiddenCount; j++) {
|
||||||
|
if (strcmp(hidden[j], normalized) == 0) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(hidden[hiddenCount], normalized, 3);
|
||||||
|
hidden[hiddenCount][3] = 0;
|
||||||
|
hiddenCount++;
|
||||||
|
saveToSd();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeHidden(uint8_t index) {
|
||||||
|
if (index >= hiddenCount) return;
|
||||||
|
for (uint8_t i = index; i < hiddenCount - 1; i++) {
|
||||||
|
strncpy(hidden[i], hidden[i + 1], 4);
|
||||||
|
}
|
||||||
|
hiddenCount--;
|
||||||
|
hidden[hiddenCount][0] = 0;
|
||||||
|
saveToSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isHidden(const char* callsign) {
|
||||||
|
if (hiddenCount == 0) return false;
|
||||||
|
char prefix[4];
|
||||||
|
extractPrefix(callsign, prefix);
|
||||||
|
if (!prefix[0]) return false;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < hiddenCount; i++) {
|
||||||
|
if (strcmp(hidden[i], prefix) == 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/airline_filter.h
Normal file
16
src/airline_filter.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace AirlineFilter {
|
||||||
|
constexpr uint8_t MAX_HIDDEN = 10;
|
||||||
|
|
||||||
|
void init();
|
||||||
|
|
||||||
|
uint8_t count();
|
||||||
|
String icaoAt(uint8_t index);
|
||||||
|
|
||||||
|
bool addHidden(const char* icaoPrefix);
|
||||||
|
void removeHidden(uint8_t index);
|
||||||
|
|
||||||
|
bool isHidden(const char* callsign);
|
||||||
|
}
|
||||||
197
src/airline_filter_screen.cpp
Normal file
197
src/airline_filter_screen.cpp
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#include "airline_filter_screen.h"
|
||||||
|
#include "airline_filter.h"
|
||||||
|
#include "touch_input.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
namespace AirlineFilterScreen {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
String runLetterKeypad(TFT_eSPI& tft) {
|
||||||
|
constexpr const char* ROW1 = "QWERTYUIOP";
|
||||||
|
constexpr const char* ROW2 = "ASDFGHJKL";
|
||||||
|
constexpr const char* ROW3 = "ZXCVBNM";
|
||||||
|
|
||||||
|
char buf[4] = {0};
|
||||||
|
uint8_t len = 0;
|
||||||
|
|
||||||
|
constexpr int16_t KEY_H = 32;
|
||||||
|
constexpr int16_t KEY_GAP = 3;
|
||||||
|
constexpr int16_t ROW0_Y = 90;
|
||||||
|
|
||||||
|
auto layoutRow = [&](const char* row, int16_t y, Rect* outRects, uint8_t n) {
|
||||||
|
int16_t usableW = Config::SCREEN_WIDTH - 8;
|
||||||
|
int16_t keyW = (usableW - (n - 1) * KEY_GAP) / n;
|
||||||
|
int16_t x = 4;
|
||||||
|
for (uint8_t i = 0; i < n; i++) {
|
||||||
|
outRects[i] = {x, y, keyW, KEY_H};
|
||||||
|
x += keyW + KEY_GAP;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Rect row1Rects[10], row2Rects[9], row3Rects[7];
|
||||||
|
layoutRow(ROW1, ROW0_Y, row1Rects, 10);
|
||||||
|
layoutRow(ROW2, ROW0_Y + KEY_H + KEY_GAP, row2Rects, 9);
|
||||||
|
layoutRow(ROW3, ROW0_Y + 2 * (KEY_H + KEY_GAP), row3Rects, 7);
|
||||||
|
|
||||||
|
Rect backspaceBtn = {4, (int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), 100, KEY_H};
|
||||||
|
Rect cancelBtn = {(int16_t)(Config::SCREEN_WIDTH - 100), (int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), 96, KEY_H};
|
||||||
|
Rect confirmBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8), KEY_H};
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
bool confirmed = false;
|
||||||
|
|
||||||
|
auto redraw = [&]() {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 10);
|
||||||
|
tft.println(I18n::t(StringId::AIRLINE_ADD_TITLE));
|
||||||
|
|
||||||
|
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 30, TFT_NAVY);
|
||||||
|
tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 30, TFT_DARKGREY);
|
||||||
|
tft.setTextSize(2);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
|
tft.setCursor(14, 47);
|
||||||
|
tft.print(buf);
|
||||||
|
tft.setTextSize(1);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 10; i++) drawButton(tft, row1Rects[i], String(ROW1[i]));
|
||||||
|
for (uint8_t i = 0; i < 9; i++) drawButton(tft, row2Rects[i], String(ROW2[i]));
|
||||||
|
for (uint8_t i = 0; i < 7; i++) drawButton(tft, row3Rects[i], String(ROW3[i]));
|
||||||
|
|
||||||
|
drawButton(tft, backspaceBtn, "<-");
|
||||||
|
drawButton(tft, cancelBtn, I18n::t(StringId::CANCEL), TFT_MAROON);
|
||||||
|
drawButton(tft, confirmBtn, I18n::t(StringId::ADD));
|
||||||
|
};
|
||||||
|
|
||||||
|
redraw();
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
TouchInput::Point tap;
|
||||||
|
if (!TouchInput::wasTapped(tap)) { delay(20); continue; }
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
for (uint8_t i = 0; i < 10 && !handled; i++) {
|
||||||
|
if (row1Rects[i].contains(tap.x, tap.y) && len < 3) { buf[len++] = ROW1[i]; handled = true; }
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < 9 && !handled; i++) {
|
||||||
|
if (row2Rects[i].contains(tap.x, tap.y) && len < 3) { buf[len++] = ROW2[i]; handled = true; }
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < 7 && !handled; i++) {
|
||||||
|
if (row3Rects[i].contains(tap.x, tap.y) && len < 3) { buf[len++] = ROW3[i]; handled = true; }
|
||||||
|
}
|
||||||
|
if (!handled && backspaceBtn.contains(tap.x, tap.y)) {
|
||||||
|
if (len > 0) { len--; buf[len] = 0; }
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && cancelBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
confirmed = false;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && confirmBtn.contains(tap.x, tap.y) && len > 0) {
|
||||||
|
done = true;
|
||||||
|
confirmed = true;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed ? String(buf) : String();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(TFT_eSPI& tft) {
|
||||||
|
constexpr int16_t ROW_H = 32;
|
||||||
|
constexpr int16_t ROW_GAP = 6;
|
||||||
|
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, 8);
|
||||||
|
tft.println(I18n::t(StringId::AIRLINE_FILTER_TITLE));
|
||||||
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 26);
|
||||||
|
tft.println(I18n::t(StringId::AIRLINE_FILTER_DESC1));
|
||||||
|
tft.setCursor(10, 38);
|
||||||
|
tft.println(I18n::t(StringId::AIRLINE_FILTER_DESC2));
|
||||||
|
|
||||||
|
uint8_t count = AirlineFilter::count();
|
||||||
|
int16_t y = 56;
|
||||||
|
|
||||||
|
Rect rowRects[AirlineFilter::MAX_HIDDEN];
|
||||||
|
Rect removeRects[AirlineFilter::MAX_HIDDEN];
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < count; 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;
|
||||||
|
|
||||||
|
tft.fillRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_NAVY);
|
||||||
|
tft.drawRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_DARKGREY);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
|
tft.drawString(AirlineFilter::icaoAt(i), rowRect.x + rowRect.w / 2, rowRect.y + rowRect.h / 2);
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
drawButton(tft, removeRect, "X", TFT_MAROON);
|
||||||
|
|
||||||
|
y += ROW_H + ROW_GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect addBtn = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||||
|
bool canAdd = count < AirlineFilter::MAX_HIDDEN;
|
||||||
|
if (canAdd) {
|
||||||
|
drawButton(tft, addBtn, I18n::t(StringId::AIRLINE_FILTER_ADD));
|
||||||
|
y += 40 + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::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)) {
|
||||||
|
AirlineFilter::removeHidden(i);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) {
|
||||||
|
String code = runLetterKeypad(tft);
|
||||||
|
if (code.length() > 0) {
|
||||||
|
AirlineFilter::addHidden(code.c_str());
|
||||||
|
}
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/airline_filter_screen.h
Normal file
7
src/airline_filter_screen.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
|
namespace AirlineFilterScreen {
|
||||||
|
void run(TFT_eSPI& tft);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "calibration_screen.h"
|
#include "calibration_screen.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
namespace CalibrationScreen {
|
namespace CalibrationScreen {
|
||||||
|
|
||||||
@@ -20,8 +21,6 @@ namespace {
|
|||||||
tft.fillCircle(x, y, 3, TFT_WHITE);
|
tft.fillCircle(x, y, 3, TFT_WHITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wartet auf eine Beruehrung, mittelt ein paar Rohwerte waehrend sie
|
|
||||||
// gehalten wird, und wartet danach auf das Loslassen, bevor es zurueckkehrt.
|
|
||||||
RawAvg waitForTap() {
|
RawAvg waitForTap() {
|
||||||
RawAvg avg;
|
RawAvg avg;
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ namespace {
|
|||||||
while (TouchInput::rawPoint().touched) {
|
while (TouchInput::rawPoint().touched) {
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
delay(150); // kleine Pause, damit der naechste Tap nicht sofort durchrutscht
|
delay(150);
|
||||||
|
|
||||||
return avg;
|
return avg;
|
||||||
}
|
}
|
||||||
@@ -54,11 +53,11 @@ void run(TFT_eSPI& tft) {
|
|||||||
const int16_t W = Config::SCREEN_WIDTH;
|
const int16_t W = Config::SCREEN_WIDTH;
|
||||||
const int16_t H = Config::SCREEN_HEIGHT;
|
const int16_t H = Config::SCREEN_HEIGHT;
|
||||||
|
|
||||||
struct { int16_t x, y; const char* label; } targets[4] = {
|
struct { int16_t x, y; StringId label; } targets[4] = {
|
||||||
{ M, M, "Oben links" },
|
{ M, M, StringId::CALIB_TOP_LEFT },
|
||||||
{ W - M, M, "Oben rechts" },
|
{ W - M, M, StringId::CALIB_TOP_RIGHT },
|
||||||
{ W - M, H - M, "Unten rechts" },
|
{ W - M, H - M, StringId::CALIB_BOTTOM_RIGHT },
|
||||||
{ M, H - M, "Unten links" },
|
{ M, H - M, StringId::CALIB_BOTTOM_LEFT },
|
||||||
};
|
};
|
||||||
|
|
||||||
RawAvg samples[4];
|
RawAvg samples[4];
|
||||||
@@ -67,24 +66,20 @@ void run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
// Text mittig zeichnen - weit weg von allen 4 Eck-Positionen, damit er
|
|
||||||
// nie vom Ziel-Kreis ueberdeckt wird.
|
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.drawString("Touch-Kalibrierung", W / 2, H / 2 - 12);
|
tft.drawString(I18n::t(StringId::CALIB_TITLE), W / 2, H / 2 - 12);
|
||||||
char msg[40];
|
tft.drawString(I18n::t(StringId::CALIB_PROMPT), W / 2, H / 2 + 4);
|
||||||
snprintf(msg, sizeof(msg), "Bitte Kreis beruehren:");
|
tft.drawString(I18n::t(targets[i].label), W / 2, H / 2 + 20);
|
||||||
tft.drawString(msg, W / 2, H / 2 + 4);
|
|
||||||
tft.drawString(targets[i].label, W / 2, H / 2 + 20);
|
|
||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
|
||||||
drawTarget(tft, targets[i].x, targets[i].y);
|
drawTarget(tft, targets[i].x, targets[i].y);
|
||||||
samples[i] = waitForTap();
|
samples[i] = waitForTap();
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t xmin = (samples[0].avgX() + samples[3].avgX()) / 2; // links oben+unten
|
int16_t xmin = (samples[0].avgX() + samples[3].avgX()) / 2;
|
||||||
int16_t xmax = (samples[1].avgX() + samples[2].avgX()) / 2; // rechts oben+unten
|
int16_t xmax = (samples[1].avgX() + samples[2].avgX()) / 2;
|
||||||
int16_t ymin = (samples[0].avgY() + samples[1].avgY()) / 2; // oben links+rechts
|
int16_t ymin = (samples[0].avgY() + samples[1].avgY()) / 2;
|
||||||
int16_t ymax = (samples[3].avgY() + samples[2].avgY()) / 2; // unten links+rechts
|
int16_t ymax = (samples[3].avgY() + samples[2].avgY()) / 2;
|
||||||
|
|
||||||
swapIfNeeded(xmin, xmax);
|
swapIfNeeded(xmin, xmax);
|
||||||
swapIfNeeded(ymin, ymax);
|
swapIfNeeded(ymin, ymax);
|
||||||
@@ -95,7 +90,7 @@ void run(TFT_eSPI& tft) {
|
|||||||
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, 10);
|
||||||
tft.println("Kalibrierung gespeichert!");
|
tft.println(I18n::t(StringId::CALIB_SAVED));
|
||||||
delay(800);
|
delay(800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
src/first_run_language_screen.cpp
Normal file
63
src/first_run_language_screen.cpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#include "first_run_language_screen.h"
|
||||||
|
#include "settings_store.h"
|
||||||
|
#include "touch_input.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace FirstRunLanguageScreen {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int16_t ROW_H = 36;
|
||||||
|
constexpr int16_t ROW_GAP = 6;
|
||||||
|
constexpr int16_t START_Y = 60;
|
||||||
|
|
||||||
|
Rect rowRect(uint8_t index) {
|
||||||
|
return {20, (int16_t)(START_Y + index * (ROW_H + ROW_GAP)),
|
||||||
|
(int16_t)(Config::SCREEN_WIDTH - 40), ROW_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) {
|
||||||
|
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_GREEN);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
|
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(TFT_eSPI& tft) {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.setTextSize(2);
|
||||||
|
tft.drawString("Language / Sprache", Config::SCREEN_WIDTH / 2, 24);
|
||||||
|
tft.setTextSize(1);
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
|
||||||
|
Rect langRects[I18n::LANG_COUNT];
|
||||||
|
for (uint8_t i = 0; i < I18n::LANG_COUNT; i++) {
|
||||||
|
langRects[i] = rowRect(i);
|
||||||
|
drawButton(tft, langRects[i], I18n::languageName(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
TouchInput::Point tap;
|
||||||
|
if (!TouchInput::wasTapped(tap)) { delay(20); continue; }
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < I18n::LANG_COUNT; i++) {
|
||||||
|
if (langRects[i].contains(tap.x, tap.y)) {
|
||||||
|
SettingsStore::setLanguage(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/first_run_language_screen.h
Normal file
7
src/first_run_language_screen.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
|
namespace FirstRunLanguageScreen {
|
||||||
|
void run(TFT_eSPI& tft);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "aircraft.h"
|
#include "aircraft.h"
|
||||||
#include "aircraft_table.h"
|
#include "aircraft_table.h"
|
||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -42,6 +43,22 @@ namespace {
|
|||||||
seenCount++;
|
seenCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t countLinesFast(File& f) {
|
||||||
|
constexpr size_t BUF_SIZE = 1024;
|
||||||
|
static uint8_t buf[BUF_SIZE];
|
||||||
|
uint32_t lines = 0;
|
||||||
|
uint32_t blocksRead = 0;
|
||||||
|
while (f.available()) {
|
||||||
|
size_t n = f.read(buf, BUF_SIZE);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
if (buf[i] == '\n') lines++;
|
||||||
|
}
|
||||||
|
blocksRead++;
|
||||||
|
if (blocksRead % 8 == 0) yield();
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
void loadSeenFromTodayFile() {
|
void loadSeenFromTodayFile() {
|
||||||
seenCount = 0;
|
seenCount = 0;
|
||||||
char filename[64];
|
char filename[64];
|
||||||
@@ -51,21 +68,42 @@ namespace {
|
|||||||
File f = SD.open(filename, FILE_READ);
|
File f = SD.open(filename, FILE_READ);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
|
|
||||||
|
constexpr size_t BUF_SIZE = 1024;
|
||||||
|
static uint8_t buf[BUF_SIZE];
|
||||||
|
char lineBuf[48];
|
||||||
|
size_t lineLen = 0;
|
||||||
bool firstLine = true;
|
bool firstLine = true;
|
||||||
while (f.available() && seenCount < MAX_SEEN) {
|
uint32_t blocksRead = 0;
|
||||||
String line = f.readStringUntil('\n');
|
|
||||||
line.trim();
|
|
||||||
if (line.length() == 0) continue;
|
|
||||||
if (firstLine) { firstLine = false; continue; }
|
|
||||||
|
|
||||||
int firstComma = line.indexOf(',');
|
auto processLine = [&]() {
|
||||||
if (firstComma < 0) continue;
|
if (lineLen == 0) return;
|
||||||
int secondComma = line.indexOf(',', firstComma + 1);
|
if (firstLine) { firstLine = false; return; }
|
||||||
String hex = (secondComma < 0) ? line.substring(firstComma + 1)
|
lineBuf[lineLen] = 0;
|
||||||
: line.substring(firstComma + 1, secondComma);
|
char* firstComma = strchr(lineBuf, ',');
|
||||||
hex.trim();
|
if (!firstComma) return;
|
||||||
if (hex.length() > 0) markSeen(hex.c_str());
|
char* secondComma = strchr(firstComma + 1, ',');
|
||||||
yield();
|
size_t hexLen = secondComma ? (size_t)(secondComma - (firstComma + 1))
|
||||||
|
: strlen(firstComma + 1);
|
||||||
|
if (hexLen > 0 && hexLen < sizeof(seenHex[0])) {
|
||||||
|
char hexBuf[7] = {0};
|
||||||
|
memcpy(hexBuf, firstComma + 1, hexLen);
|
||||||
|
markSeen(hexBuf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while (f.available() && seenCount < MAX_SEEN) {
|
||||||
|
size_t n = f.read(buf, BUF_SIZE);
|
||||||
|
for (size_t i = 0; i < n && seenCount < MAX_SEEN; i++) {
|
||||||
|
char c = (char)buf[i];
|
||||||
|
if (c == '\n' || c == '\r') {
|
||||||
|
if (lineLen > 0) processLine();
|
||||||
|
lineLen = 0;
|
||||||
|
} else if (lineLen < sizeof(lineBuf) - 1) {
|
||||||
|
lineBuf[lineLen++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blocksRead++;
|
||||||
|
if (blocksRead % 8 == 0) yield();
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
@@ -101,12 +139,15 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
|
SdMutex::Guard guard;
|
||||||
ensureCurrentDate();
|
ensureCurrentDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
if (!SettingsStore::flightLogbookEnabled()) return;
|
if (!SettingsStore::flightLogbookEnabled()) return;
|
||||||
|
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
ensureCurrentDate();
|
ensureCurrentDate();
|
||||||
if (currentDateStr[0] == 0) return;
|
if (currentDateStr[0] == 0) return;
|
||||||
|
|
||||||
@@ -154,6 +195,8 @@ void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) {
|
|||||||
totalAircraft = 0;
|
totalAircraft = 0;
|
||||||
totalDays = 0;
|
totalDays = 0;
|
||||||
|
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
File dir = SD.open(Config::SD_LOG_DIR);
|
File dir = SD.open(Config::SD_LOG_DIR);
|
||||||
if (!dir || !dir.isDirectory()) return;
|
if (!dir || !dir.isDirectory()) return;
|
||||||
|
|
||||||
@@ -163,12 +206,7 @@ void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) {
|
|||||||
String name = String(entry.name());
|
String name = String(entry.name());
|
||||||
if (name.endsWith(".csv")) {
|
if (name.endsWith(".csv")) {
|
||||||
totalDays++;
|
totalDays++;
|
||||||
uint32_t lines = 0;
|
uint32_t lines = countLinesFast(entry);
|
||||||
while (entry.available()) {
|
|
||||||
entry.readStringUntil('\n');
|
|
||||||
lines++;
|
|
||||||
if (lines % 50 == 0) yield();
|
|
||||||
}
|
|
||||||
if (lines > 0) totalAircraft += (lines - 1);
|
if (lines > 0) totalAircraft += (lines - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +219,8 @@ void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) {
|
|||||||
uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
|
uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
|
||||||
uint8_t filled = 0;
|
uint8_t filled = 0;
|
||||||
|
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
File dir = SD.open(Config::SD_LOG_DIR);
|
File dir = SD.open(Config::SD_LOG_DIR);
|
||||||
if (!dir || !dir.isDirectory()) return 0;
|
if (!dir || !dir.isDirectory()) return 0;
|
||||||
|
|
||||||
@@ -193,12 +233,7 @@ uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
|
|||||||
int slashIdx = dateOnly.lastIndexOf('/');
|
int slashIdx = dateOnly.lastIndexOf('/');
|
||||||
if (slashIdx >= 0) dateOnly = dateOnly.substring(slashIdx + 1);
|
if (slashIdx >= 0) dateOnly = dateOnly.substring(slashIdx + 1);
|
||||||
|
|
||||||
uint32_t lines = 0;
|
uint32_t lines = countLinesFast(entry);
|
||||||
while (entry.available()) {
|
|
||||||
entry.readStringUntil('\n');
|
|
||||||
lines++;
|
|
||||||
if (lines % 50 == 0) yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
strncpy(out[filled].date, dateOnly.c_str(), sizeof(out[filled].date) - 1);
|
strncpy(out[filled].date, dateOnly.c_str(), sizeof(out[filled].date) - 1);
|
||||||
out[filled].count = (lines > 0) ? (lines - 1) : 0;
|
out[filled].count = (lines > 0) ? (lines - 1) : 0;
|
||||||
@@ -213,4 +248,31 @@ uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
|
|||||||
return filled;
|
return filled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetAllData() {
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
File dir = SD.open(Config::SD_LOG_DIR);
|
||||||
|
if (dir && dir.isDirectory()) {
|
||||||
|
File entry = dir.openNextFile();
|
||||||
|
while (entry) {
|
||||||
|
bool isDir = entry.isDirectory();
|
||||||
|
String name = String(entry.name());
|
||||||
|
entry.close();
|
||||||
|
|
||||||
|
if (!isDir) {
|
||||||
|
String fullPath = name.startsWith("/")
|
||||||
|
? name
|
||||||
|
: String(Config::SD_LOG_DIR) + "/" + name;
|
||||||
|
SD.remove(fullPath);
|
||||||
|
}
|
||||||
|
entry = dir.openNextFile();
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
seenCount = 0;
|
||||||
|
currentDateStr[0] = 0;
|
||||||
|
ensureCurrentDate();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#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 {
|
namespace FlightLogbook {
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
@@ -17,13 +11,14 @@ namespace FlightLogbook {
|
|||||||
void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays);
|
void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays);
|
||||||
|
|
||||||
struct DayEntry {
|
struct DayEntry {
|
||||||
char date[11] = {0}; // "YYYY-MM-DD"
|
char date[11] = {0};
|
||||||
uint32_t count = 0;
|
uint32_t count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fuellt 'out' mit bis zu 'maxEntries' Tagen (Dateiname + Anzahl
|
|
||||||
// Eintraege), sortiert wie sie auf der SD-Karte liegen (i.d.R.
|
|
||||||
// chronologisch). Gibt die tatsaechliche Anzahl gefuellter Eintraege
|
|
||||||
// zurueck.
|
|
||||||
uint8_t listDays(DayEntry* out, uint8_t maxEntries);
|
uint8_t listDays(DayEntry* out, uint8_t maxEntries);
|
||||||
|
|
||||||
|
// Loescht ALLE Logbuch-CSV-Dateien auf der SD-Karte unwiderruflich und
|
||||||
|
// setzt die "heute schon gesehen"-Liste zurueck. Fuer den Reset-Button
|
||||||
|
// im Statistik-Bildschirm gedacht.
|
||||||
|
void resetAllData();
|
||||||
}
|
}
|
||||||
37
src/i18n.cpp
Normal file
37
src/i18n.cpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#include "i18n.h"
|
||||||
|
#include "i18n_en.h"
|
||||||
|
#include "i18n_de.h"
|
||||||
|
#include "i18n_fr.h"
|
||||||
|
#include "i18n_tr.h"
|
||||||
|
#include "i18n_es.h"
|
||||||
|
#include "i18n_it.h"
|
||||||
|
#include "settings_store.h"
|
||||||
|
|
||||||
|
namespace I18n {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char* const* TABLES[LANG_COUNT] = {
|
||||||
|
I18N_EN, I18N_DE, I18N_FR, I18N_TR, I18N_ES, I18N_IT
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* const LANGUAGE_NAMES[LANG_COUNT] = {
|
||||||
|
"English", "Deutsch", "Francais", "Turkce", "Espanol", "Italiano"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* t(StringId id) {
|
||||||
|
uint8_t lang = SettingsStore::language();
|
||||||
|
if (lang >= LANG_COUNT) lang = 0;
|
||||||
|
|
||||||
|
uint8_t idx = (uint8_t)id;
|
||||||
|
if (idx >= (uint8_t)StringId::COUNT) return "?";
|
||||||
|
|
||||||
|
return TABLES[lang][idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* languageName(uint8_t index) {
|
||||||
|
if (index >= LANG_COUNT) return "?";
|
||||||
|
return LANGUAGE_NAMES[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
155
src/i18n.h
Normal file
155
src/i18n.h
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Zentrales Uebersetzungssystem: jeder feste UI-Text der App hat eine
|
||||||
|
// StringId. Die eigentlichen Uebersetzungen liegen in je einer Datei pro
|
||||||
|
// Sprache (i18n_en.h, i18n_de.h, i18n_fr.h, i18n_tr.h, i18n_es.h, i18n_it.h) -
|
||||||
|
// jede Datei enthaelt ein Array in GENAU derselben Reihenfolge wie dieses
|
||||||
|
// Enum. Neue Menuepunkte/Texte: hier eine neue StringId VOR COUNT einfuegen,
|
||||||
|
// dann in ALLEN 6 Sprachdateien an derselben Position ergaenzen.
|
||||||
|
enum class StringId : uint8_t {
|
||||||
|
OK = 0,
|
||||||
|
CANCEL,
|
||||||
|
BACK,
|
||||||
|
BACK_ARROW,
|
||||||
|
LOADING,
|
||||||
|
ADD,
|
||||||
|
|
||||||
|
ON,
|
||||||
|
OFF,
|
||||||
|
NEVER,
|
||||||
|
|
||||||
|
SPLASH_SD_OK,
|
||||||
|
SPLASH_CONNECTING_WIFI,
|
||||||
|
SPLASH_WIFI_OK_PREFIX,
|
||||||
|
SPLASH_WIFI_FAILED,
|
||||||
|
SPLASH_GETTING_LOCATION,
|
||||||
|
SPLASH_READY,
|
||||||
|
|
||||||
|
SD_REQUIRED_LINE1,
|
||||||
|
SD_REQUIRED_LINE2,
|
||||||
|
SD_REQUIRED_LINE3,
|
||||||
|
SD_REQUIRED_HINT,
|
||||||
|
|
||||||
|
CALIB_TITLE,
|
||||||
|
CALIB_PROMPT,
|
||||||
|
CALIB_TOP_LEFT,
|
||||||
|
CALIB_TOP_RIGHT,
|
||||||
|
CALIB_BOTTOM_RIGHT,
|
||||||
|
CALIB_BOTTOM_LEFT,
|
||||||
|
CALIB_SAVED,
|
||||||
|
|
||||||
|
WIFI_SCANNING,
|
||||||
|
WIFI_NO_NETWORKS,
|
||||||
|
WIFI_SELECT,
|
||||||
|
WIFI_CONNECTED_BANG,
|
||||||
|
WIFI_CONNECTION_FAILED,
|
||||||
|
WIFI_BACK_TO_LIST_MSG,
|
||||||
|
WIFI_PASSWORD_LABEL,
|
||||||
|
WIFI_SPACE,
|
||||||
|
WIFI_CONNECT,
|
||||||
|
WIFI_BACK_TO_LIST_BTN,
|
||||||
|
WIFI_ALREADY_3,
|
||||||
|
WIFI_REMOVE_ONE_FIRST,
|
||||||
|
WIFI_CONNECTING,
|
||||||
|
WIFI_LABEL_PREFIX,
|
||||||
|
|
||||||
|
MENU_SETTINGS,
|
||||||
|
MENU_SETTINGS_PAGE2,
|
||||||
|
MENU_CALIBRATE,
|
||||||
|
MENU_DISPLAY_INVERTED,
|
||||||
|
MENU_DISPLAY_NORMAL,
|
||||||
|
MENU_MANAGE_WIFI,
|
||||||
|
MENU_LOCATION_PRESETS,
|
||||||
|
MENU_SCREEN_TIMEOUT_PREFIX,
|
||||||
|
MENU_STATISTICS,
|
||||||
|
MENU_LOGBOOK_FILES,
|
||||||
|
MENU_MORE_SETTINGS,
|
||||||
|
MENU_EMERGENCY_ALERT,
|
||||||
|
MENU_PROXIMITY_LED,
|
||||||
|
MENU_LED_HEARTBEAT,
|
||||||
|
MENU_FLIGHT_LOGBOOK,
|
||||||
|
MENU_HIDE_GROUND,
|
||||||
|
MENU_AIRLINE_FILTER,
|
||||||
|
MENU_BACKUP,
|
||||||
|
MENU_RESTORE,
|
||||||
|
MENU_BACKUP_SAVED,
|
||||||
|
MENU_BACKUP_FAILED,
|
||||||
|
MENU_RESTORED,
|
||||||
|
MENU_RESTORE_FAILED,
|
||||||
|
MENU_LANGUAGE,
|
||||||
|
MENU_UNITS,
|
||||||
|
|
||||||
|
AIRLINE_FILTER_TITLE,
|
||||||
|
AIRLINE_FILTER_DESC1,
|
||||||
|
AIRLINE_FILTER_DESC2,
|
||||||
|
AIRLINE_FILTER_ADD,
|
||||||
|
AIRLINE_ADD_TITLE,
|
||||||
|
|
||||||
|
LOCATION_TITLE,
|
||||||
|
LOCATION_AUTO,
|
||||||
|
LOCATION_PRESET,
|
||||||
|
LOCATION_PRESET_EMPTY,
|
||||||
|
LOCATION_ADD,
|
||||||
|
LOCATION_LAT_PROMPT,
|
||||||
|
LOCATION_LON_PROMPT,
|
||||||
|
|
||||||
|
STATS_TITLE,
|
||||||
|
STATS_TODAY,
|
||||||
|
STATS_ALLTIME,
|
||||||
|
STATS_DAYS,
|
||||||
|
STATS_AVG,
|
||||||
|
STATS_RESET_BTN,
|
||||||
|
STATS_RESET_CONFIRM,
|
||||||
|
STATS_RESET_DONE,
|
||||||
|
STATS_UPTIME_PREFIX,
|
||||||
|
|
||||||
|
LOGFILES_TITLE,
|
||||||
|
LOGFILES_EMPTY,
|
||||||
|
LOGFILES_SHOWING_PREFIX,
|
||||||
|
LOGFILES_OF,
|
||||||
|
LOGFILES_DAYS_SUFFIX,
|
||||||
|
LOGFILES_AIRCRAFT_SUFFIX,
|
||||||
|
|
||||||
|
RADAR_TAP_FOR_DETAILS,
|
||||||
|
|
||||||
|
DETAIL_MODEL,
|
||||||
|
DETAIL_TYPE,
|
||||||
|
DETAIL_LOADING_DOTS,
|
||||||
|
DETAIL_UNKNOWN,
|
||||||
|
DETAIL_ALT,
|
||||||
|
DETAIL_SPEED,
|
||||||
|
DETAIL_DIST,
|
||||||
|
DETAIL_HDG,
|
||||||
|
DETAIL_SEATS_EST,
|
||||||
|
DETAIL_SEATS_UNKNOWN,
|
||||||
|
DETAIL_TAP_CLOSE,
|
||||||
|
DETAIL_CLIMB,
|
||||||
|
DETAIL_DESCENT,
|
||||||
|
DETAIL_LEVEL,
|
||||||
|
DETAIL_SQUAWK,
|
||||||
|
|
||||||
|
SCREENSHOT_SAVED_PREFIX,
|
||||||
|
SCREENSHOT_FAILED,
|
||||||
|
|
||||||
|
UNITS_TITLE,
|
||||||
|
UNITS_AUTO,
|
||||||
|
UNITS_METRIC,
|
||||||
|
UNITS_IMPERIAL,
|
||||||
|
|
||||||
|
LANGUAGE_TITLE,
|
||||||
|
|
||||||
|
COUNT // IMMER als letztes Element - markiert die Gesamtanzahl
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace I18n {
|
||||||
|
constexpr uint8_t LANG_COUNT = 6;
|
||||||
|
|
||||||
|
// Liefert den uebersetzten Text fuer 'id' in der aktuell in den
|
||||||
|
// Einstellungen gewaehlten Sprache (SettingsStore::language()).
|
||||||
|
const char* t(StringId id);
|
||||||
|
|
||||||
|
// Eigennamen der Sprachen (fuer das Sprachauswahl-Menue), IMMER im
|
||||||
|
// jeweiligen Idiom selbst geschrieben (z.B. "Deutsch", nicht "German").
|
||||||
|
const char* languageName(uint8_t index);
|
||||||
|
}
|
||||||
138
src/i18n_de.h
Normal file
138
src/i18n_de.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_DE[] = {
|
||||||
|
"OK",
|
||||||
|
"Abbrechen",
|
||||||
|
"Zurueck",
|
||||||
|
"< Zurueck",
|
||||||
|
"Laedt...",
|
||||||
|
"Hinzufuegen",
|
||||||
|
|
||||||
|
"AN",
|
||||||
|
"AUS",
|
||||||
|
"Nie",
|
||||||
|
|
||||||
|
"SD-Karte: OK",
|
||||||
|
"Verbinde mit WLAN...",
|
||||||
|
"WLAN OK: ",
|
||||||
|
"WLAN FEHLGESCHLAGEN",
|
||||||
|
"Ermittle Standort...",
|
||||||
|
"Bereit!",
|
||||||
|
|
||||||
|
"Fuer diese App wird",
|
||||||
|
"eine SD-Karte",
|
||||||
|
"benoetigt",
|
||||||
|
"SD-Karte einlegen und Geraet neu starten",
|
||||||
|
|
||||||
|
"Touch-Kalibrierung",
|
||||||
|
"Bitte den Kreis beruehren:",
|
||||||
|
"Oben links",
|
||||||
|
"Oben rechts",
|
||||||
|
"Unten rechts",
|
||||||
|
"Unten links",
|
||||||
|
"Kalibrierung gespeichert!",
|
||||||
|
|
||||||
|
"Suche WLAN...",
|
||||||
|
"Keine Netzwerke gefunden",
|
||||||
|
"WLAN waehlen:",
|
||||||
|
"Verbunden!",
|
||||||
|
"Verbindung fehlgeschlagen.",
|
||||||
|
"Zurueck zur Netzwerkliste...",
|
||||||
|
"Passwort:",
|
||||||
|
"Leerzeichen",
|
||||||
|
"Verbinden",
|
||||||
|
"Zurueck zur Liste",
|
||||||
|
"Bereits 3 Netzwerke gespeichert.",
|
||||||
|
"Erst eins entfernen.",
|
||||||
|
"Verbinde...",
|
||||||
|
"WLAN: ",
|
||||||
|
|
||||||
|
"Einstellungen",
|
||||||
|
"Einstellungen (2/2)",
|
||||||
|
"Touch kalibrieren",
|
||||||
|
"Anzeige: invertiert (tippen)",
|
||||||
|
"Anzeige: normal (tippen)",
|
||||||
|
"WLAN-Netzwerke verwalten",
|
||||||
|
"Standort-Presets",
|
||||||
|
"Bildschirm-Timeout: ",
|
||||||
|
"Statistik",
|
||||||
|
"Logbuch-Dateien",
|
||||||
|
"Weitere Einstellungen >",
|
||||||
|
"Notfall-Alarm: ",
|
||||||
|
"Naeherungs-LED: ",
|
||||||
|
"LED-Herzschlag: ",
|
||||||
|
"Flugbuch: ",
|
||||||
|
"Bodenfahrzeuge ausblenden: ",
|
||||||
|
"Airline-Filter",
|
||||||
|
"Einstellungen sichern",
|
||||||
|
"Einstellungen wiederherstellen",
|
||||||
|
"Backup gespeichert.",
|
||||||
|
"Backup fehlgeschlagen.",
|
||||||
|
"Wiederhergestellt. Geraet neu starten!",
|
||||||
|
"Wiederherstellung fehlgeschlagen.",
|
||||||
|
"Sprache",
|
||||||
|
"Einheiten",
|
||||||
|
|
||||||
|
"Ausgeblendete Airlines",
|
||||||
|
"Flugzeuge dieser Airlines werden",
|
||||||
|
"im Radar ausgeblendet.",
|
||||||
|
"Airline hinzufuegen",
|
||||||
|
"Airline hinzufuegen (ICAO-Code)",
|
||||||
|
|
||||||
|
"Standort-Presets",
|
||||||
|
"Automatisch (GPS/IP)",
|
||||||
|
"Preset",
|
||||||
|
"(leer)",
|
||||||
|
"Preset hinzufuegen",
|
||||||
|
"Breitengrad (z.B. 48.6833)",
|
||||||
|
"Laengengrad (z.B. 9.8333)",
|
||||||
|
|
||||||
|
"Statistik",
|
||||||
|
"Heute geloggte Flugzeuge",
|
||||||
|
"Insgesamt geloggte Flugzeuge",
|
||||||
|
"Tage mit Sichtungen",
|
||||||
|
"Durchschnitt pro Tag",
|
||||||
|
"Flugbuch zuruecksetzen",
|
||||||
|
"Nochmal tippen zum Bestaetigen!",
|
||||||
|
"Flugbuch-Daten zurueckgesetzt.",
|
||||||
|
"Laufzeit: ",
|
||||||
|
|
||||||
|
"Logbuch-Dateien",
|
||||||
|
"Noch keine Flugbuch-Eintraege.",
|
||||||
|
"Zeige die letzten ",
|
||||||
|
" von ",
|
||||||
|
" Tagen",
|
||||||
|
" Flugzeuge",
|
||||||
|
|
||||||
|
"Fuer Details antippen",
|
||||||
|
|
||||||
|
"Modell: ",
|
||||||
|
"Typ: ",
|
||||||
|
"laedt...",
|
||||||
|
"unbekannt",
|
||||||
|
"Hoehe: ",
|
||||||
|
"Geschw.: ",
|
||||||
|
"Distanz: ",
|
||||||
|
"Kurs: ",
|
||||||
|
"Sitzplaetze (geschaetzt): ",
|
||||||
|
"Sitzplaetze: unbekannt",
|
||||||
|
"Woanders antippen zum Schliessen",
|
||||||
|
"Steigen: ",
|
||||||
|
"Sinken: ",
|
||||||
|
"Horizontalflug",
|
||||||
|
"Squawk: ",
|
||||||
|
|
||||||
|
"Gespeichert: ",
|
||||||
|
"Screenshot fehlgeschlagen",
|
||||||
|
|
||||||
|
"Einheiten",
|
||||||
|
"Automatisch (nach Standort)",
|
||||||
|
"Metrisch (km, m, km/h)",
|
||||||
|
"Imperial (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Sprache",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_DE table size must match StringId::COUNT");
|
||||||
138
src/i18n_en.h
Normal file
138
src/i18n_en.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_EN[] = {
|
||||||
|
"OK",
|
||||||
|
"Cancel",
|
||||||
|
"Back",
|
||||||
|
"< Back",
|
||||||
|
"Loading...",
|
||||||
|
"Add",
|
||||||
|
|
||||||
|
"ON",
|
||||||
|
"OFF",
|
||||||
|
"Never",
|
||||||
|
|
||||||
|
"SD Card: OK",
|
||||||
|
"Connecting WiFi...",
|
||||||
|
"WiFi OK: ",
|
||||||
|
"WiFi FAILED",
|
||||||
|
"Getting location...",
|
||||||
|
"Ready!",
|
||||||
|
|
||||||
|
"For this app a",
|
||||||
|
"SD card is",
|
||||||
|
"required",
|
||||||
|
"Insert a card and restart the device",
|
||||||
|
|
||||||
|
"Touch Calibration",
|
||||||
|
"Please touch the circle:",
|
||||||
|
"Top left",
|
||||||
|
"Top right",
|
||||||
|
"Bottom right",
|
||||||
|
"Bottom left",
|
||||||
|
"Calibration saved!",
|
||||||
|
|
||||||
|
"Scanning WiFi...",
|
||||||
|
"No networks found",
|
||||||
|
"Select WiFi:",
|
||||||
|
"Connected!",
|
||||||
|
"Connection failed.",
|
||||||
|
"Back to network list...",
|
||||||
|
"Password:",
|
||||||
|
"Space",
|
||||||
|
"Connect",
|
||||||
|
"Back to list",
|
||||||
|
"Already 3 networks saved.",
|
||||||
|
"Remove one first.",
|
||||||
|
"Connecting...",
|
||||||
|
"WiFi: ",
|
||||||
|
|
||||||
|
"Settings",
|
||||||
|
"Settings (2/2)",
|
||||||
|
"Calibrate touch",
|
||||||
|
"Display: inverted (tap)",
|
||||||
|
"Display: normal (tap)",
|
||||||
|
"Manage WiFi networks",
|
||||||
|
"Location presets",
|
||||||
|
"Screen timeout: ",
|
||||||
|
"Statistics",
|
||||||
|
"Logbook files",
|
||||||
|
"More settings >",
|
||||||
|
"Emergency alert: ",
|
||||||
|
"Proximity LED: ",
|
||||||
|
"LED heartbeat: ",
|
||||||
|
"Flight logbook: ",
|
||||||
|
"Hide ground vehicles: ",
|
||||||
|
"Airline filter",
|
||||||
|
"Backup settings",
|
||||||
|
"Restore settings",
|
||||||
|
"Backup saved.",
|
||||||
|
"Backup failed.",
|
||||||
|
"Restored. Restart device!",
|
||||||
|
"Restore failed.",
|
||||||
|
"Language",
|
||||||
|
"Units",
|
||||||
|
|
||||||
|
"Hidden airlines",
|
||||||
|
"Aircraft from these airlines are",
|
||||||
|
"hidden from the radar.",
|
||||||
|
"Add airline",
|
||||||
|
"Add airline (ICAO code)",
|
||||||
|
|
||||||
|
"Location presets",
|
||||||
|
"Auto (GPS/IP)",
|
||||||
|
"Preset",
|
||||||
|
"(empty)",
|
||||||
|
"Add preset",
|
||||||
|
"Latitude (e.g. 48.6833)",
|
||||||
|
"Longitude (e.g. 9.8333)",
|
||||||
|
|
||||||
|
"Statistics",
|
||||||
|
"Aircraft logged today",
|
||||||
|
"Aircraft logged all-time",
|
||||||
|
"Days with sightings",
|
||||||
|
"Average per day",
|
||||||
|
"Reset logbook data",
|
||||||
|
"Tap again to confirm!",
|
||||||
|
"Logbook data reset.",
|
||||||
|
"Uptime: ",
|
||||||
|
|
||||||
|
"Logbook files",
|
||||||
|
"No logbook entries yet.",
|
||||||
|
"Showing last ",
|
||||||
|
" of ",
|
||||||
|
" days",
|
||||||
|
" aircraft",
|
||||||
|
|
||||||
|
"Tap for details",
|
||||||
|
|
||||||
|
"Model: ",
|
||||||
|
"Type: ",
|
||||||
|
"loading...",
|
||||||
|
"unknown",
|
||||||
|
"Alt: ",
|
||||||
|
"Speed: ",
|
||||||
|
"Dist: ",
|
||||||
|
"Hdg: ",
|
||||||
|
"Seats (estimated): ",
|
||||||
|
"Seats: unknown",
|
||||||
|
"Tap elsewhere to close",
|
||||||
|
"Climb: ",
|
||||||
|
"Descent: ",
|
||||||
|
"Level flight",
|
||||||
|
"Squawk: ",
|
||||||
|
|
||||||
|
"Saved: ",
|
||||||
|
"Screenshot failed",
|
||||||
|
|
||||||
|
"Units",
|
||||||
|
"Auto (by location)",
|
||||||
|
"Metric (km, m, km/h)",
|
||||||
|
"Imperial (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Language",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_EN table size must match StringId::COUNT");
|
||||||
138
src/i18n_es.h
Normal file
138
src/i18n_es.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_ES[] = {
|
||||||
|
"OK",
|
||||||
|
"Cancelar",
|
||||||
|
"Atras",
|
||||||
|
"< Atras",
|
||||||
|
"Cargando...",
|
||||||
|
"Anadir",
|
||||||
|
|
||||||
|
"ON",
|
||||||
|
"OFF",
|
||||||
|
"Nunca",
|
||||||
|
|
||||||
|
"Tarjeta SD: OK",
|
||||||
|
"Conectando WiFi...",
|
||||||
|
"WiFi OK: ",
|
||||||
|
"WiFi FALLO",
|
||||||
|
"Obteniendo ubicacion...",
|
||||||
|
"Listo!",
|
||||||
|
|
||||||
|
"Esta app necesita",
|
||||||
|
"una tarjeta SD",
|
||||||
|
"requerida",
|
||||||
|
"Inserte una tarjeta y reinicie el dispositivo",
|
||||||
|
|
||||||
|
"Calibracion tactil",
|
||||||
|
"Toque el circulo:",
|
||||||
|
"Arriba izquierda",
|
||||||
|
"Arriba derecha",
|
||||||
|
"Abajo derecha",
|
||||||
|
"Abajo izquierda",
|
||||||
|
"Calibracion guardada!",
|
||||||
|
|
||||||
|
"Buscando WiFi...",
|
||||||
|
"No se encontraron redes",
|
||||||
|
"Elegir WiFi:",
|
||||||
|
"Conectado!",
|
||||||
|
"Conexion fallida.",
|
||||||
|
"Volviendo a la lista...",
|
||||||
|
"Contrasena:",
|
||||||
|
"Espacio",
|
||||||
|
"Conectar",
|
||||||
|
"Volver a la lista",
|
||||||
|
"Ya hay 3 redes guardadas.",
|
||||||
|
"Elimine una primero.",
|
||||||
|
"Conectando...",
|
||||||
|
"WiFi: ",
|
||||||
|
|
||||||
|
"Ajustes",
|
||||||
|
"Ajustes (2/2)",
|
||||||
|
"Calibrar tactil",
|
||||||
|
"Pantalla: invertida (toque)",
|
||||||
|
"Pantalla: normal (toque)",
|
||||||
|
"Gestionar redes WiFi",
|
||||||
|
"Ubicaciones guardadas",
|
||||||
|
"Apagado de pantalla: ",
|
||||||
|
"Estadisticas",
|
||||||
|
"Archivos del registro",
|
||||||
|
"Mas ajustes >",
|
||||||
|
"Alerta de emergencia: ",
|
||||||
|
"LED de proximidad: ",
|
||||||
|
"Latido LED: ",
|
||||||
|
"Registro de vuelos: ",
|
||||||
|
"Ocultar vehiculos terrestres: ",
|
||||||
|
"Filtro de aerolineas",
|
||||||
|
"Copia de seguridad",
|
||||||
|
"Restaurar ajustes",
|
||||||
|
"Copia guardada.",
|
||||||
|
"Fallo la copia de seguridad.",
|
||||||
|
"Restaurado. Reinicie el dispositivo!",
|
||||||
|
"Fallo la restauracion.",
|
||||||
|
"Idioma",
|
||||||
|
"Unidades",
|
||||||
|
|
||||||
|
"Aerolineas ocultas",
|
||||||
|
"Los aviones de estas aerolineas",
|
||||||
|
"se ocultan del radar.",
|
||||||
|
"Anadir aerolinea",
|
||||||
|
"Anadir aerolinea (codigo OACI)",
|
||||||
|
|
||||||
|
"Ubicaciones guardadas",
|
||||||
|
"Auto (GPS/IP)",
|
||||||
|
"Ubicacion",
|
||||||
|
"(vacio)",
|
||||||
|
"Anadir ubicacion",
|
||||||
|
"Latitud (ej. 48.6833)",
|
||||||
|
"Longitud (ej. 9.8333)",
|
||||||
|
|
||||||
|
"Estadisticas",
|
||||||
|
"Aviones registrados hoy",
|
||||||
|
"Aviones registrados en total",
|
||||||
|
"Dias con avistamientos",
|
||||||
|
"Promedio por dia",
|
||||||
|
"Reiniciar registro",
|
||||||
|
"Toque de nuevo para confirmar!",
|
||||||
|
"Registro reiniciado.",
|
||||||
|
"Tiempo activo: ",
|
||||||
|
|
||||||
|
"Archivos del registro",
|
||||||
|
"Aun no hay registros.",
|
||||||
|
"Mostrando los ultimos ",
|
||||||
|
" de ",
|
||||||
|
" dias",
|
||||||
|
" aviones",
|
||||||
|
|
||||||
|
"Toque para ver detalles",
|
||||||
|
|
||||||
|
"Modelo: ",
|
||||||
|
"Tipo: ",
|
||||||
|
"cargando...",
|
||||||
|
"desconocido",
|
||||||
|
"Alt.: ",
|
||||||
|
"Vel.: ",
|
||||||
|
"Dist.: ",
|
||||||
|
"Rumbo: ",
|
||||||
|
"Asientos (estimado): ",
|
||||||
|
"Asientos: desconocido",
|
||||||
|
"Toque otro lugar para cerrar",
|
||||||
|
"Subida: ",
|
||||||
|
"Descenso: ",
|
||||||
|
"Vuelo nivelado",
|
||||||
|
"Squawk: ",
|
||||||
|
|
||||||
|
"Guardado: ",
|
||||||
|
"Fallo la captura de pantalla",
|
||||||
|
|
||||||
|
"Unidades",
|
||||||
|
"Auto (segun ubicacion)",
|
||||||
|
"Metrico (km, m, km/h)",
|
||||||
|
"Imperial (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Idioma",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_ES table size must match StringId::COUNT");
|
||||||
138
src/i18n_fr.h
Normal file
138
src/i18n_fr.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_FR[] = {
|
||||||
|
"OK",
|
||||||
|
"Annuler",
|
||||||
|
"Retour",
|
||||||
|
"< Retour",
|
||||||
|
"Chargement...",
|
||||||
|
"Ajouter",
|
||||||
|
|
||||||
|
"ON",
|
||||||
|
"OFF",
|
||||||
|
"Jamais",
|
||||||
|
|
||||||
|
"Carte SD: OK",
|
||||||
|
"Connexion WiFi...",
|
||||||
|
"WiFi OK: ",
|
||||||
|
"ECHEC WiFi",
|
||||||
|
"Localisation...",
|
||||||
|
"Pret !",
|
||||||
|
|
||||||
|
"Pour cette appli une",
|
||||||
|
"carte SD est",
|
||||||
|
"requise",
|
||||||
|
"Inserez une carte et redemarrez l'appareil",
|
||||||
|
|
||||||
|
"Etalonnage tactile",
|
||||||
|
"Touchez le cercle :",
|
||||||
|
"Haut gauche",
|
||||||
|
"Haut droite",
|
||||||
|
"Bas droite",
|
||||||
|
"Bas gauche",
|
||||||
|
"Etalonnage enregistre !",
|
||||||
|
|
||||||
|
"Recherche WiFi...",
|
||||||
|
"Aucun reseau trouve",
|
||||||
|
"Choisir WiFi :",
|
||||||
|
"Connecte !",
|
||||||
|
"Echec de connexion.",
|
||||||
|
"Retour a la liste...",
|
||||||
|
"Mot de passe :",
|
||||||
|
"Espace",
|
||||||
|
"Connecter",
|
||||||
|
"Retour a la liste",
|
||||||
|
"Deja 3 reseaux enregistres.",
|
||||||
|
"Supprimez-en un d'abord.",
|
||||||
|
"Connexion...",
|
||||||
|
"WiFi : ",
|
||||||
|
|
||||||
|
"Reglages",
|
||||||
|
"Reglages (2/2)",
|
||||||
|
"Etalonner le tactile",
|
||||||
|
"Ecran : inverse (touchez)",
|
||||||
|
"Ecran : normal (touchez)",
|
||||||
|
"Gerer les reseaux WiFi",
|
||||||
|
"Positions enregistrees",
|
||||||
|
"Extinction ecran : ",
|
||||||
|
"Statistiques",
|
||||||
|
"Fichiers du journal",
|
||||||
|
"Plus de reglages >",
|
||||||
|
"Alerte urgence : ",
|
||||||
|
"LED de proximite : ",
|
||||||
|
"Battement LED : ",
|
||||||
|
"Journal de vol : ",
|
||||||
|
"Masquer vehicules au sol : ",
|
||||||
|
"Filtre compagnies",
|
||||||
|
"Sauvegarder reglages",
|
||||||
|
"Restaurer reglages",
|
||||||
|
"Sauvegarde enregistree.",
|
||||||
|
"Echec de la sauvegarde.",
|
||||||
|
"Restaure. Redemarrez l'appareil !",
|
||||||
|
"Echec de la restauration.",
|
||||||
|
"Langue",
|
||||||
|
"Unites",
|
||||||
|
|
||||||
|
"Compagnies masquees",
|
||||||
|
"Les avions de ces compagnies sont",
|
||||||
|
"masques du radar.",
|
||||||
|
"Ajouter une compagnie",
|
||||||
|
"Ajouter (code OACI)",
|
||||||
|
|
||||||
|
"Positions enregistrees",
|
||||||
|
"Auto (GPS/IP)",
|
||||||
|
"Position",
|
||||||
|
"(vide)",
|
||||||
|
"Ajouter une position",
|
||||||
|
"Latitude (ex. 48.6833)",
|
||||||
|
"Longitude (ex. 9.8333)",
|
||||||
|
|
||||||
|
"Statistiques",
|
||||||
|
"Avions enregistres aujourd'hui",
|
||||||
|
"Avions enregistres au total",
|
||||||
|
"Jours avec observations",
|
||||||
|
"Moyenne par jour",
|
||||||
|
"Reinitialiser le journal",
|
||||||
|
"Retouchez pour confirmer !",
|
||||||
|
"Journal reinitialise.",
|
||||||
|
"Duree de fonctionnement : ",
|
||||||
|
|
||||||
|
"Fichiers du journal",
|
||||||
|
"Aucune entree pour le moment.",
|
||||||
|
"Affichage des ",
|
||||||
|
" derniers sur ",
|
||||||
|
" jours",
|
||||||
|
" avions",
|
||||||
|
|
||||||
|
"Touchez pour les details",
|
||||||
|
|
||||||
|
"Modele : ",
|
||||||
|
"Type : ",
|
||||||
|
"chargement...",
|
||||||
|
"inconnu",
|
||||||
|
"Alt. : ",
|
||||||
|
"Vitesse : ",
|
||||||
|
"Dist. : ",
|
||||||
|
"Cap : ",
|
||||||
|
"Sieges (estime) : ",
|
||||||
|
"Sieges : inconnu",
|
||||||
|
"Touchez ailleurs pour fermer",
|
||||||
|
"Montee : ",
|
||||||
|
"Descente : ",
|
||||||
|
"Vol en palier",
|
||||||
|
"Squawk : ",
|
||||||
|
|
||||||
|
"Enregistre : ",
|
||||||
|
"Echec de la capture d'ecran",
|
||||||
|
|
||||||
|
"Unites",
|
||||||
|
"Auto (selon la position)",
|
||||||
|
"Metrique (km, m, km/h)",
|
||||||
|
"Imperial (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Langue",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_FR table size must match StringId::COUNT");
|
||||||
138
src/i18n_it.h
Normal file
138
src/i18n_it.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_IT[] = {
|
||||||
|
"OK",
|
||||||
|
"Annulla",
|
||||||
|
"Indietro",
|
||||||
|
"< Indietro",
|
||||||
|
"Caricamento...",
|
||||||
|
"Aggiungi",
|
||||||
|
|
||||||
|
"ON",
|
||||||
|
"OFF",
|
||||||
|
"Mai",
|
||||||
|
|
||||||
|
"Scheda SD: OK",
|
||||||
|
"Connessione WiFi...",
|
||||||
|
"WiFi OK: ",
|
||||||
|
"WiFi FALLITO",
|
||||||
|
"Rilevamento posizione...",
|
||||||
|
"Pronto!",
|
||||||
|
|
||||||
|
"Questa app richiede",
|
||||||
|
"una scheda SD",
|
||||||
|
"richiesta",
|
||||||
|
"Inserire una scheda e riavviare il dispositivo",
|
||||||
|
|
||||||
|
"Calibrazione touch",
|
||||||
|
"Toccare il cerchio:",
|
||||||
|
"In alto a sinistra",
|
||||||
|
"In alto a destra",
|
||||||
|
"In basso a destra",
|
||||||
|
"In basso a sinistra",
|
||||||
|
"Calibrazione salvata!",
|
||||||
|
|
||||||
|
"Ricerca WiFi...",
|
||||||
|
"Nessuna rete trovata",
|
||||||
|
"Seleziona WiFi:",
|
||||||
|
"Connesso!",
|
||||||
|
"Connessione fallita.",
|
||||||
|
"Ritorno alla lista...",
|
||||||
|
"Password:",
|
||||||
|
"Spazio",
|
||||||
|
"Connetti",
|
||||||
|
"Torna alla lista",
|
||||||
|
"Gia 3 reti salvate.",
|
||||||
|
"Rimuovine una prima.",
|
||||||
|
"Connessione...",
|
||||||
|
"WiFi: ",
|
||||||
|
|
||||||
|
"Impostazioni",
|
||||||
|
"Impostazioni (2/2)",
|
||||||
|
"Calibra touch",
|
||||||
|
"Schermo: invertito (tocca)",
|
||||||
|
"Schermo: normale (tocca)",
|
||||||
|
"Gestisci reti WiFi",
|
||||||
|
"Posizioni salvate",
|
||||||
|
"Spegnimento schermo: ",
|
||||||
|
"Statistiche",
|
||||||
|
"File del registro",
|
||||||
|
"Altre impostazioni >",
|
||||||
|
"Allarme emergenza: ",
|
||||||
|
"LED di prossimita: ",
|
||||||
|
"Battito LED: ",
|
||||||
|
"Diario di volo: ",
|
||||||
|
"Nascondi veicoli a terra: ",
|
||||||
|
"Filtro compagnie",
|
||||||
|
"Backup impostazioni",
|
||||||
|
"Ripristina impostazioni",
|
||||||
|
"Backup salvato.",
|
||||||
|
"Backup fallito.",
|
||||||
|
"Ripristinato. Riavviare il dispositivo!",
|
||||||
|
"Ripristino fallito.",
|
||||||
|
"Lingua",
|
||||||
|
"Unita",
|
||||||
|
|
||||||
|
"Compagnie nascoste",
|
||||||
|
"Gli aerei di queste compagnie",
|
||||||
|
"sono nascosti dal radar.",
|
||||||
|
"Aggiungi compagnia",
|
||||||
|
"Aggiungi compagnia (codice ICAO)",
|
||||||
|
|
||||||
|
"Posizioni salvate",
|
||||||
|
"Auto (GPS/IP)",
|
||||||
|
"Posizione",
|
||||||
|
"(vuoto)",
|
||||||
|
"Aggiungi posizione",
|
||||||
|
"Latitudine (es. 48.6833)",
|
||||||
|
"Longitudine (es. 9.8333)",
|
||||||
|
|
||||||
|
"Statistiche",
|
||||||
|
"Aerei registrati oggi",
|
||||||
|
"Aerei registrati totali",
|
||||||
|
"Giorni con avvistamenti",
|
||||||
|
"Media al giorno",
|
||||||
|
"Reimposta registro",
|
||||||
|
"Tocca di nuovo per confermare!",
|
||||||
|
"Registro reimpostato.",
|
||||||
|
"Tempo di attivita: ",
|
||||||
|
|
||||||
|
"File del registro",
|
||||||
|
"Ancora nessun dato.",
|
||||||
|
"Mostra gli ultimi ",
|
||||||
|
" di ",
|
||||||
|
" giorni",
|
||||||
|
" aerei",
|
||||||
|
|
||||||
|
"Tocca per i dettagli",
|
||||||
|
|
||||||
|
"Modello: ",
|
||||||
|
"Tipo: ",
|
||||||
|
"caricamento...",
|
||||||
|
"sconosciuto",
|
||||||
|
"Alt.: ",
|
||||||
|
"Vel.: ",
|
||||||
|
"Dist.: ",
|
||||||
|
"Rotta: ",
|
||||||
|
"Posti (stimati): ",
|
||||||
|
"Posti: sconosciuto",
|
||||||
|
"Tocca altrove per chiudere",
|
||||||
|
"Salita: ",
|
||||||
|
"Discesa: ",
|
||||||
|
"Volo livellato",
|
||||||
|
"Squawk: ",
|
||||||
|
|
||||||
|
"Salvato: ",
|
||||||
|
"Screenshot fallito",
|
||||||
|
|
||||||
|
"Unita",
|
||||||
|
"Auto (in base alla posizione)",
|
||||||
|
"Metrico (km, m, km/h)",
|
||||||
|
"Imperiale (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Lingua",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_IT table size must match StringId::COUNT");
|
||||||
138
src/i18n_tr.h
Normal file
138
src/i18n_tr.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
static const char* const I18N_TR[] = {
|
||||||
|
"Tamam",
|
||||||
|
"Iptal",
|
||||||
|
"Geri",
|
||||||
|
"< Geri",
|
||||||
|
"Yukleniyor...",
|
||||||
|
"Ekle",
|
||||||
|
|
||||||
|
"ACIK",
|
||||||
|
"KAPALI",
|
||||||
|
"Asla",
|
||||||
|
|
||||||
|
"SD Kart: OK",
|
||||||
|
"WiFi'ye baglaniyor...",
|
||||||
|
"WiFi OK: ",
|
||||||
|
"WiFi BASARISIZ",
|
||||||
|
"Konum aliniyor...",
|
||||||
|
"Hazir!",
|
||||||
|
|
||||||
|
"Bu uygulama icin bir",
|
||||||
|
"SD kart",
|
||||||
|
"gereklidir",
|
||||||
|
"Bir kart takin ve cihazi yeniden baslatin",
|
||||||
|
|
||||||
|
"Dokunmatik Kalibrasyon",
|
||||||
|
"Lutfen daireye dokunun:",
|
||||||
|
"Sol ust",
|
||||||
|
"Sag ust",
|
||||||
|
"Sag alt",
|
||||||
|
"Sol alt",
|
||||||
|
"Kalibrasyon kaydedildi!",
|
||||||
|
|
||||||
|
"WiFi taraniyor...",
|
||||||
|
"Ag bulunamadi",
|
||||||
|
"WiFi secin:",
|
||||||
|
"Baglandi!",
|
||||||
|
"Baglanti basarisiz.",
|
||||||
|
"Ag listesine donuluyor...",
|
||||||
|
"Sifre:",
|
||||||
|
"Bosluk",
|
||||||
|
"Baglan",
|
||||||
|
"Listeye don",
|
||||||
|
"Zaten 3 ag kayitli.",
|
||||||
|
"Once birini kaldirin.",
|
||||||
|
"Baglaniyor...",
|
||||||
|
"WiFi: ",
|
||||||
|
|
||||||
|
"Ayarlar",
|
||||||
|
"Ayarlar (2/2)",
|
||||||
|
"Dokunmatigi kalibre et",
|
||||||
|
"Ekran: ters (dokun)",
|
||||||
|
"Ekran: normal (dokun)",
|
||||||
|
"WiFi aglarini yonet",
|
||||||
|
"Konum on ayarlari",
|
||||||
|
"Ekran zaman asimi: ",
|
||||||
|
"Istatistikler",
|
||||||
|
"Kayit defteri dosyalari",
|
||||||
|
"Diger ayarlar >",
|
||||||
|
"Acil durum alarmi: ",
|
||||||
|
"Yakinlik LED'i: ",
|
||||||
|
"LED kalp atisi: ",
|
||||||
|
"Ucus kayit defteri: ",
|
||||||
|
"Kara araclarini gizle: ",
|
||||||
|
"Havayolu filtresi",
|
||||||
|
"Ayarlari yedekle",
|
||||||
|
"Ayarlari geri yukle",
|
||||||
|
"Yedekleme kaydedildi.",
|
||||||
|
"Yedekleme basarisiz.",
|
||||||
|
"Geri yuklendi. Cihazi yeniden baslatin!",
|
||||||
|
"Geri yukleme basarisiz.",
|
||||||
|
"Dil",
|
||||||
|
"Birimler",
|
||||||
|
|
||||||
|
"Gizli havayollari",
|
||||||
|
"Bu havayollarina ait ucaklar",
|
||||||
|
"radarda gizlenir.",
|
||||||
|
"Havayolu ekle",
|
||||||
|
"Havayolu ekle (ICAO kodu)",
|
||||||
|
|
||||||
|
"Konum on ayarlari",
|
||||||
|
"Otomatik (GPS/IP)",
|
||||||
|
"On ayar",
|
||||||
|
"(bos)",
|
||||||
|
"On ayar ekle",
|
||||||
|
"Enlem (orn. 48.6833)",
|
||||||
|
"Boylam (orn. 9.8333)",
|
||||||
|
|
||||||
|
"Istatistikler",
|
||||||
|
"Bugun kaydedilen ucaklar",
|
||||||
|
"Toplam kaydedilen ucaklar",
|
||||||
|
"Gozlem yapilan gunler",
|
||||||
|
"Gunluk ortalama",
|
||||||
|
"Kayit defterini sifirla",
|
||||||
|
"Onaylamak icin tekrar dokunun!",
|
||||||
|
"Kayit defteri sifirlandi.",
|
||||||
|
"Calisma suresi: ",
|
||||||
|
|
||||||
|
"Kayit defteri dosyalari",
|
||||||
|
"Henuz kayit yok.",
|
||||||
|
"Son ",
|
||||||
|
" / ",
|
||||||
|
" gun gosteriliyor",
|
||||||
|
" ucak",
|
||||||
|
|
||||||
|
"Detaylar icin dokunun",
|
||||||
|
|
||||||
|
"Model: ",
|
||||||
|
"Tip: ",
|
||||||
|
"yukleniyor...",
|
||||||
|
"bilinmiyor",
|
||||||
|
"Yuk.: ",
|
||||||
|
"Hiz: ",
|
||||||
|
"Mesafe: ",
|
||||||
|
"Yon: ",
|
||||||
|
"Koltuk (tahmini): ",
|
||||||
|
"Koltuk: bilinmiyor",
|
||||||
|
"Kapatmak icin baska yere dokunun",
|
||||||
|
"Tirmanis: ",
|
||||||
|
"Alcalma: ",
|
||||||
|
"Duz ucus",
|
||||||
|
"Squawk: ",
|
||||||
|
|
||||||
|
"Kaydedildi: ",
|
||||||
|
"Ekran goruntusu basarisiz",
|
||||||
|
|
||||||
|
"Birimler",
|
||||||
|
"Otomatik (konuma gore)",
|
||||||
|
"Metrik (km, m, km/sa)",
|
||||||
|
"Imperyal (ft, kt, nm)",
|
||||||
|
|
||||||
|
"Dil",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
||||||
|
"I18N_TR table size must match StringId::COUNT");
|
||||||
75
src/language_screen.cpp
Normal file
75
src/language_screen.cpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#include "language_screen.h"
|
||||||
|
#include "settings_store.h"
|
||||||
|
#include "touch_input.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace LanguageScreen {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int16_t ROW_H = 36;
|
||||||
|
constexpr int16_t ROW_GAP = 6;
|
||||||
|
constexpr int16_t START_Y = 30;
|
||||||
|
|
||||||
|
Rect rowRect(uint8_t index) {
|
||||||
|
return {10, (int16_t)(START_Y + index * (ROW_H + ROW_GAP)),
|
||||||
|
(int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(TFT_eSPI& tft) {
|
||||||
|
bool done = false;
|
||||||
|
while (!done) {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 6);
|
||||||
|
tft.println(I18n::t(StringId::LANGUAGE_TITLE));
|
||||||
|
|
||||||
|
uint8_t current = SettingsStore::language();
|
||||||
|
Rect langRects[I18n::LANG_COUNT];
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < I18n::LANG_COUNT; i++) {
|
||||||
|
langRects[i] = rowRect(i);
|
||||||
|
drawButton(tft, langRects[i], I18n::languageName(i),
|
||||||
|
(i == current) ? TFT_DARKGREEN : TFT_NAVY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect backBtn = rowRect(I18n::LANG_COUNT);
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
for (uint8_t i = 0; i < I18n::LANG_COUNT && !handled; i++) {
|
||||||
|
if (langRects[i].contains(tap.x, tap.y)) {
|
||||||
|
SettingsStore::setLanguage(i);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/language_screen.h
Normal file
7
src/language_screen.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
|
namespace LanguageScreen {
|
||||||
|
void run(TFT_eSPI& tft);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "location_manager.h"
|
#include "location_manager.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "location_presets.h"
|
||||||
|
#include "settings_store.h"
|
||||||
#include <TinyGPSPlus.h>
|
#include <TinyGPSPlus.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
@@ -33,7 +35,7 @@ namespace {
|
|||||||
|
|
||||||
bool haveUtcOffset = false;
|
bool haveUtcOffset = false;
|
||||||
int32_t utcOffsetSecs = 0;
|
int32_t utcOffsetSecs = 0;
|
||||||
bool metricUnits = true; // Standard: metrisch, bis die IP-Abfrage etwas anderes sagt
|
bool metricUnits = true;
|
||||||
|
|
||||||
SemaphoreHandle_t mutex = nullptr;
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
|
|
||||||
@@ -128,8 +130,6 @@ void requestIpLookupIfNeeded() {
|
|||||||
haveUtcOffset = true;
|
haveUtcOffset = true;
|
||||||
|
|
||||||
const char* countryCode = doc["countryCode"] | "";
|
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);
|
metricUnits = (strcmp(countryCode, "US") != 0);
|
||||||
|
|
||||||
persistLocationAndSource(lat, lon, Source::IpGeolocation);
|
persistLocationAndSource(lat, lon, Source::IpGeolocation);
|
||||||
@@ -137,6 +137,12 @@ void requestIpLookupIfNeeded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getHomeLocation(double& lat, double& lon) {
|
void getHomeLocation(double& lat, double& lon) {
|
||||||
|
int8_t presetIdx = LocationPresets::activeIndex();
|
||||||
|
if (presetIdx >= 0) {
|
||||||
|
LocationPresets::getLatLon((uint8_t)presetIdx, lat, lon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (gps.location.isValid()) {
|
if (gps.location.isValid()) {
|
||||||
lat = gps.location.lat();
|
lat = gps.location.lat();
|
||||||
lon = gps.location.lng();
|
lon = gps.location.lng();
|
||||||
@@ -151,6 +157,8 @@ void getHomeLocation(double& lat, double& lon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Source currentSource() {
|
Source currentSource() {
|
||||||
|
if (LocationPresets::activeIndex() >= 0) return Source::Manual;
|
||||||
|
|
||||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||||
Source s = source;
|
Source s = source;
|
||||||
xSemaphoreGive(mutex);
|
xSemaphoreGive(mutex);
|
||||||
@@ -187,6 +195,11 @@ bool hasGpsFix() { return gps.location.isValid(); }
|
|||||||
|
|
||||||
bool hasUtcOffset() { return haveUtcOffset; }
|
bool hasUtcOffset() { return haveUtcOffset; }
|
||||||
int32_t utcOffsetSeconds() { return utcOffsetSecs; }
|
int32_t utcOffsetSeconds() { return utcOffsetSecs; }
|
||||||
bool useMetricUnits() { return metricUnits; }
|
bool useMetricUnits() {
|
||||||
|
uint8_t mode = SettingsStore::unitsMode();
|
||||||
|
if (mode == 1) return true;
|
||||||
|
if (mode == 2) return false;
|
||||||
|
return metricUnits;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
117
src/location_presets.cpp
Normal file
117
src/location_presets.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#include "location_presets.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
|
#include "sd_storage.h"
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
namespace LocationPresets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char* PRESETS_FILE = "/Flightradar_cyd/locations.txt";
|
||||||
|
|
||||||
|
struct Preset {
|
||||||
|
double lat = 0;
|
||||||
|
double lon = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Preset presets[MAX_PRESETS];
|
||||||
|
uint8_t presetCount = 0;
|
||||||
|
int8_t activeIdx = -1;
|
||||||
|
|
||||||
|
void saveToSd() {
|
||||||
|
if (!SdStorage::isMounted()) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
File f = SD.open(PRESETS_FILE, FILE_WRITE);
|
||||||
|
if (!f) return;
|
||||||
|
f.printf("active=%d\n", (int)activeIdx);
|
||||||
|
for (uint8_t i = 0; i < presetCount; i++) {
|
||||||
|
f.printf("%.6f,%.6f\n", presets[i].lat, presets[i].lon);
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadFromSd() {
|
||||||
|
presetCount = 0;
|
||||||
|
activeIdx = -1;
|
||||||
|
if (!SdStorage::isMounted()) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
if (!SD.exists(PRESETS_FILE)) return;
|
||||||
|
File f = SD.open(PRESETS_FILE, FILE_READ);
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
bool firstLine = true;
|
||||||
|
while (f.available()) {
|
||||||
|
String line = f.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.length() == 0) continue;
|
||||||
|
|
||||||
|
if (firstLine) {
|
||||||
|
firstLine = false;
|
||||||
|
if (line.startsWith("active=")) {
|
||||||
|
activeIdx = (int8_t)line.substring(7).toInt();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int comma = line.indexOf(',');
|
||||||
|
if (comma < 0) continue;
|
||||||
|
if (presetCount >= MAX_PRESETS) break;
|
||||||
|
|
||||||
|
presets[presetCount].lat = line.substring(0, comma).toDouble();
|
||||||
|
presets[presetCount].lon = line.substring(comma + 1).toDouble();
|
||||||
|
presetCount++;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
if (activeIdx >= (int8_t)presetCount) activeIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
loadFromSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t count() { return presetCount; }
|
||||||
|
|
||||||
|
void getLatLon(uint8_t index, double& lat, double& lon) {
|
||||||
|
if (index >= presetCount) return;
|
||||||
|
lat = presets[index].lat;
|
||||||
|
lon = presets[index].lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addPreset(double lat, double lon) {
|
||||||
|
if (presetCount >= MAX_PRESETS) return false;
|
||||||
|
presets[presetCount].lat = lat;
|
||||||
|
presets[presetCount].lon = lon;
|
||||||
|
presetCount++;
|
||||||
|
saveToSd();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePreset(uint8_t index) {
|
||||||
|
if (index >= presetCount) return;
|
||||||
|
for (uint8_t i = index; i < presetCount - 1; i++) {
|
||||||
|
presets[i] = presets[i + 1];
|
||||||
|
}
|
||||||
|
presetCount--;
|
||||||
|
presets[presetCount] = Preset{};
|
||||||
|
|
||||||
|
if (activeIdx == (int8_t)index) {
|
||||||
|
activeIdx = -1;
|
||||||
|
} else if (activeIdx > (int8_t)index) {
|
||||||
|
activeIdx--;
|
||||||
|
}
|
||||||
|
saveToSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t activeIndex() { return activeIdx; }
|
||||||
|
|
||||||
|
void setActiveIndex(int8_t index) {
|
||||||
|
if (index >= (int8_t)presetCount) return;
|
||||||
|
activeIdx = index;
|
||||||
|
saveToSd();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
src/location_presets.h
Normal file
17
src/location_presets.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace LocationPresets {
|
||||||
|
constexpr uint8_t MAX_PRESETS = 3;
|
||||||
|
|
||||||
|
void init();
|
||||||
|
|
||||||
|
uint8_t count();
|
||||||
|
void getLatLon(uint8_t index, double& lat, double& lon);
|
||||||
|
|
||||||
|
bool addPreset(double lat, double lon);
|
||||||
|
void removePreset(uint8_t index);
|
||||||
|
|
||||||
|
int8_t activeIndex();
|
||||||
|
void setActiveIndex(int8_t index);
|
||||||
|
}
|
||||||
215
src/location_presets_screen.cpp
Normal file
215
src/location_presets_screen.cpp
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
#include "location_presets_screen.h"
|
||||||
|
#include "location_presets.h"
|
||||||
|
#include "touch_input.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
namespace LocationPresetsScreen {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
String runNumericKeypad(TFT_eSPI& tft, const String& title) {
|
||||||
|
char buf[16] = {0};
|
||||||
|
uint8_t len = 0;
|
||||||
|
|
||||||
|
constexpr int16_t KEY_W = 74;
|
||||||
|
constexpr int16_t KEY_H = 40;
|
||||||
|
constexpr int16_t KEY_GAP = 4;
|
||||||
|
constexpr int16_t GRID_LEFT = (Config::SCREEN_WIDTH - 3 * KEY_W - 2 * KEY_GAP) / 2;
|
||||||
|
constexpr int16_t GRID_TOP = 90;
|
||||||
|
|
||||||
|
const char* keys[12] = {"1","2","3","4","5","6","7","8","9","-","0","."};
|
||||||
|
Rect keyRects[12];
|
||||||
|
for (uint8_t i = 0; i < 12; i++) {
|
||||||
|
int16_t col = i % 3;
|
||||||
|
int16_t row = i / 3;
|
||||||
|
keyRects[i] = {(int16_t)(GRID_LEFT + col * (KEY_W + KEY_GAP)),
|
||||||
|
(int16_t)(GRID_TOP + row * (KEY_H + KEY_GAP)),
|
||||||
|
KEY_W, KEY_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect backspaceBtn = {(int16_t)GRID_LEFT, (int16_t)(GRID_TOP + 4 * (KEY_H + KEY_GAP)),
|
||||||
|
(int16_t)(3 * KEY_W + 2 * KEY_GAP), 36};
|
||||||
|
Rect cancelBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), 110, 40};
|
||||||
|
Rect confirmBtn = {(int16_t)(Config::SCREEN_WIDTH - 120), (int16_t)(Config::SCREEN_HEIGHT - 50), 110, 40};
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
bool confirmed = false;
|
||||||
|
|
||||||
|
auto redraw = [&]() {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 10);
|
||||||
|
tft.println(title);
|
||||||
|
|
||||||
|
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_NAVY);
|
||||||
|
tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_DARKGREY);
|
||||||
|
tft.setTextSize(2);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
|
tft.setCursor(14, 49);
|
||||||
|
tft.print(buf);
|
||||||
|
tft.setTextSize(1);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 12; i++) drawButton(tft, keyRects[i], keys[i]);
|
||||||
|
drawButton(tft, backspaceBtn, "<- Backspace");
|
||||||
|
drawButton(tft, cancelBtn, I18n::t(StringId::CANCEL), TFT_MAROON);
|
||||||
|
drawButton(tft, confirmBtn, I18n::t(StringId::OK));
|
||||||
|
};
|
||||||
|
|
||||||
|
redraw();
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
TouchInput::Point tap;
|
||||||
|
if (!TouchInput::wasTapped(tap)) { delay(20); continue; }
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
for (uint8_t i = 0; i < 12 && !handled; i++) {
|
||||||
|
if (keyRects[i].contains(tap.x, tap.y) && len < sizeof(buf) - 1) {
|
||||||
|
buf[len++] = keys[i][0];
|
||||||
|
buf[len] = 0;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && backspaceBtn.contains(tap.x, tap.y)) {
|
||||||
|
if (len > 0) { len--; buf[len] = 0; }
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && cancelBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
confirmed = false;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && confirmBtn.contains(tap.x, tap.y) && len > 0) {
|
||||||
|
done = true;
|
||||||
|
confirmed = true;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed ? String(buf) : String();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addPresetFlow(TFT_eSPI& tft) {
|
||||||
|
String latStr = runNumericKeypad(tft, I18n::t(StringId::LOCATION_LAT_PROMPT));
|
||||||
|
if (latStr.length() == 0) return false;
|
||||||
|
|
||||||
|
String lonStr = runNumericKeypad(tft, I18n::t(StringId::LOCATION_LON_PROMPT));
|
||||||
|
if (lonStr.length() == 0) return false;
|
||||||
|
|
||||||
|
double lat = latStr.toDouble();
|
||||||
|
double lon = lonStr.toDouble();
|
||||||
|
if (lat == 0.0 && lon == 0.0) return false;
|
||||||
|
|
||||||
|
return LocationPresets::addPreset(lat, lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(TFT_eSPI& tft) {
|
||||||
|
constexpr int16_t ROW_H = 32;
|
||||||
|
constexpr int16_t ROW_GAP = 6;
|
||||||
|
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, 8);
|
||||||
|
tft.println(I18n::t(StringId::LOCATION_TITLE));
|
||||||
|
|
||||||
|
int8_t active = LocationPresets::activeIndex();
|
||||||
|
uint8_t count = LocationPresets::count();
|
||||||
|
int16_t y = 36;
|
||||||
|
|
||||||
|
Rect autoRect = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
|
||||||
|
String autoLabel = I18n::t(StringId::LOCATION_AUTO);
|
||||||
|
drawButton(tft, autoRect, active < 0 ? ("> " + autoLabel) : autoLabel,
|
||||||
|
active < 0 ? TFT_DARKGREEN : TFT_NAVY);
|
||||||
|
y += ROW_H + ROW_GAP;
|
||||||
|
|
||||||
|
Rect rowRects[LocationPresets::MAX_PRESETS];
|
||||||
|
Rect removeRects[LocationPresets::MAX_PRESETS];
|
||||||
|
String presetWord = I18n::t(StringId::LOCATION_PRESET);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < LocationPresets::MAX_PRESETS; 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) {
|
||||||
|
double lat = 0, lon = 0;
|
||||||
|
LocationPresets::getLatLon(i, lat, lon);
|
||||||
|
char coords[24];
|
||||||
|
snprintf(coords, sizeof(coords), "%d: %.3f, %.3f", i + 1, lat, lon);
|
||||||
|
String label = (active == (int8_t)i ? "> " : "") + presetWord + " " + coords;
|
||||||
|
drawButton(tft, rowRect, label, (active == (int8_t)i) ? TFT_DARKGREEN : TFT_NAVY);
|
||||||
|
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);
|
||||||
|
String label = presetWord + " " + String(i + 1) + " " + I18n::t(StringId::LOCATION_PRESET_EMPTY);
|
||||||
|
tft.drawString(label, 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), 36};
|
||||||
|
bool canAdd = count < LocationPresets::MAX_PRESETS;
|
||||||
|
if (canAdd) {
|
||||||
|
drawButton(tft, addBtn, I18n::t(StringId::LOCATION_ADD));
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
if (autoRect.contains(tap.x, tap.y)) {
|
||||||
|
LocationPresets::setActiveIndex(-1);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < count && !handled; i++) {
|
||||||
|
if (removeRects[i].contains(tap.x, tap.y)) {
|
||||||
|
LocationPresets::removePreset(i);
|
||||||
|
handled = true;
|
||||||
|
} else if (rowRects[i].contains(tap.x, tap.y)) {
|
||||||
|
LocationPresets::setActiveIndex((int8_t)i);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) {
|
||||||
|
addPresetFlow(tft);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/location_presets_screen.h
Normal file
7
src/location_presets_screen.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
|
namespace LocationPresetsScreen {
|
||||||
|
void run(TFT_eSPI& tft);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "flight_logbook.h"
|
#include "flight_logbook.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
namespace LogbookFilesScreen {
|
namespace LogbookFilesScreen {
|
||||||
|
|
||||||
@@ -27,6 +28,14 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void run(TFT_eSPI& tft) {
|
void run(TFT_eSPI& tft) {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 8);
|
||||||
|
tft.println(I18n::t(StringId::LOGFILES_TITLE));
|
||||||
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 30);
|
||||||
|
tft.print(I18n::t(StringId::LOADING));
|
||||||
|
|
||||||
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
|
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
|
||||||
uint8_t count = FlightLogbook::listDays(days, MAX_DAYS_QUERIED);
|
uint8_t count = FlightLogbook::listDays(days, MAX_DAYS_QUERIED);
|
||||||
|
|
||||||
@@ -35,12 +44,12 @@ void run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 8);
|
tft.setCursor(10, 8);
|
||||||
tft.println("Logbook files");
|
tft.println(I18n::t(StringId::LOGFILES_TITLE));
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
tft.setCursor(10, 40);
|
tft.setCursor(10, 40);
|
||||||
tft.println("No logbook entries yet.");
|
tft.println(I18n::t(StringId::LOGFILES_EMPTY));
|
||||||
} else {
|
} else {
|
||||||
uint8_t startIdx = (count > VISIBLE_ROWS) ? (count - VISIBLE_ROWS) : 0;
|
uint8_t startIdx = (count > VISIBLE_ROWS) ? (count - VISIBLE_ROWS) : 0;
|
||||||
int16_t y = 30;
|
int16_t y = 30;
|
||||||
@@ -48,7 +57,8 @@ void run(TFT_eSPI& tft) {
|
|||||||
if (count > VISIBLE_ROWS) {
|
if (count > VISIBLE_ROWS) {
|
||||||
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
tft.setCursor(10, y);
|
tft.setCursor(10, y);
|
||||||
tft.printf("Showing last %d of %d days", VISIBLE_ROWS, count);
|
tft.print(String(I18n::t(StringId::LOGFILES_SHOWING_PREFIX)) + VISIBLE_ROWS +
|
||||||
|
I18n::t(StringId::LOGFILES_OF) + count + I18n::t(StringId::LOGFILES_DAYS_SUFFIX));
|
||||||
y += 16;
|
y += 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +68,12 @@ void run(TFT_eSPI& tft) {
|
|||||||
tft.print(days[i].date);
|
tft.print(days[i].date);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(110, y);
|
tft.setCursor(110, y);
|
||||||
tft.printf("%lu aircraft", (unsigned long)days[i].count);
|
tft.print(String(days[i].count) + I18n::t(StringId::LOGFILES_AIRCRAFT_SUFFIX));
|
||||||
y += 20;
|
y += 20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawButton(tft, backBtn, "Back");
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
|
|
||||||
bool done = false;
|
bool done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
|||||||
84
src/main.cpp
84
src/main.cpp
@@ -10,9 +10,11 @@
|
|||||||
#include "radar_math.h"
|
#include "radar_math.h"
|
||||||
#include "aircraft_table.h"
|
#include "aircraft_table.h"
|
||||||
#include "airline_lookup.h"
|
#include "airline_lookup.h"
|
||||||
|
#include "airline_filter.h"
|
||||||
#include "sd_storage.h"
|
#include "sd_storage.h"
|
||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "location_manager.h"
|
#include "location_manager.h"
|
||||||
|
#include "location_presets.h"
|
||||||
#include "adsb_client.h"
|
#include "adsb_client.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "calibration_screen.h"
|
#include "calibration_screen.h"
|
||||||
@@ -25,12 +27,11 @@
|
|||||||
#include "led_alert.h"
|
#include "led_alert.h"
|
||||||
#include "flight_logbook.h"
|
#include "flight_logbook.h"
|
||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include "first_run_language_screen.h"
|
||||||
|
|
||||||
TFT_eSPI tft = TFT_eSPI();
|
TFT_eSPI tft = TFT_eSPI();
|
||||||
|
|
||||||
// Schlanker Header (Titel + Buttons) PLUS eine zweite, duenne Statuszeile
|
|
||||||
// (Uhrzeit + WLAN-Signalstaerke) darunter - der Radar-Kreis bekommt den Rest
|
|
||||||
// des Screens.
|
|
||||||
constexpr int16_t HEADER_TITLE_H = 30;
|
constexpr int16_t HEADER_TITLE_H = 30;
|
||||||
constexpr int16_t STATUS_LINE_H = 12;
|
constexpr int16_t STATUS_LINE_H = 12;
|
||||||
constexpr int16_t CONTENT_TOP = HEADER_TITLE_H + STATUS_LINE_H;
|
constexpr int16_t CONTENT_TOP = HEADER_TITLE_H + STATUS_LINE_H;
|
||||||
@@ -45,6 +46,12 @@ bool forceRedraw = false;
|
|||||||
bool wasEmergency = false;
|
bool wasEmergency = false;
|
||||||
bool bannerBlinkOn = false;
|
bool bannerBlinkOn = false;
|
||||||
|
|
||||||
|
constexpr uint8_t BACKLIGHT_FULL = 255;
|
||||||
|
constexpr uint8_t BACKLIGHT_DIMMED = 12;
|
||||||
|
constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0;
|
||||||
|
uint32_t lastInteractionMs = 0;
|
||||||
|
bool screenDimmed = false;
|
||||||
|
|
||||||
struct Rect {
|
struct Rect {
|
||||||
int16_t x, y, w, h;
|
int16_t x, y, w, h;
|
||||||
bool contains(int16_t px, int16_t py) const {
|
bool contains(int16_t px, int16_t py) const {
|
||||||
@@ -117,10 +124,10 @@ void takeScreenshotWithFeedback() {
|
|||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
tft.setCursor(6, 10);
|
tft.setCursor(6, 10);
|
||||||
if (filename.length() > 0) {
|
if (filename.length() > 0) {
|
||||||
tft.print("Saved: " + filename);
|
tft.print(String(I18n::t(StringId::SCREENSHOT_SAVED_PREFIX)) + filename);
|
||||||
} else {
|
} else {
|
||||||
tft.setTextColor(TFT_RED, TFT_NAVY);
|
tft.setTextColor(TFT_RED, TFT_NAVY);
|
||||||
tft.print("Screenshot failed");
|
tft.print(I18n::t(StringId::SCREENSHOT_FAILED));
|
||||||
}
|
}
|
||||||
delay(1200);
|
delay(1200);
|
||||||
drawHeader();
|
drawHeader();
|
||||||
@@ -137,13 +144,13 @@ void haltWithSdRequiredScreen() {
|
|||||||
|
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(2);
|
||||||
tft.drawString("For this app a", cx, cy - 40);
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE1), cx, cy - 40);
|
||||||
tft.drawString("SD card is", cx, cy - 10);
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE2), cx, cy - 10);
|
||||||
tft.drawString("required", cx, cy + 20);
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE3), cx, cy + 20);
|
||||||
|
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
tft.drawString("Insert a card and restart the device", cx, cy + 60);
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_HINT), cx, cy + 60);
|
||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -176,8 +183,13 @@ void setup() {
|
|||||||
|
|
||||||
tft.init();
|
tft.init();
|
||||||
tft.setRotation(0);
|
tft.setRotation(0);
|
||||||
|
tft.invertDisplay(true);
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
|
ledcSetup(BACKLIGHT_PWM_CHANNEL, 5000, 8);
|
||||||
|
ledcAttachPin(TFT_BL, BACKLIGHT_PWM_CHANNEL);
|
||||||
|
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
|
||||||
|
|
||||||
TouchInput::begin();
|
TouchInput::begin();
|
||||||
LedAlert::begin();
|
LedAlert::begin();
|
||||||
|
|
||||||
@@ -188,13 +200,21 @@ void setup() {
|
|||||||
}
|
}
|
||||||
SdStorage::seedDefaultDataFiles();
|
SdStorage::seedDefaultDataFiles();
|
||||||
|
|
||||||
|
// "Allererster Start" erkennen: config.txt existiert noch nicht (wird
|
||||||
|
// erst beim ersten SettingsStore::save()-Aufruf angelegt). Muss VOR
|
||||||
|
// SettingsStore::load() geprueft werden, da load() die Datei nicht
|
||||||
|
// erzeugt, nur liest.
|
||||||
|
bool isFirstRun = !SD.exists(Config::SD_SETTINGS_FILE);
|
||||||
|
|
||||||
SettingsStore::load();
|
SettingsStore::load();
|
||||||
tft.invertDisplay(SettingsStore::displayInverted());
|
tft.invertDisplay(SettingsStore::displayInverted());
|
||||||
|
|
||||||
WifiMgr::init();
|
WifiMgr::init();
|
||||||
|
LocationPresets::init();
|
||||||
|
AirlineFilter::init();
|
||||||
|
|
||||||
SplashScreen::begin(tft);
|
SplashScreen::begin(tft);
|
||||||
SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE);
|
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
||||||
|
|
||||||
if (!TouchInput::loadCalibration()) {
|
if (!TouchInput::loadCalibration()) {
|
||||||
CalibrationScreen::run(tft);
|
CalibrationScreen::run(tft);
|
||||||
@@ -203,7 +223,7 @@ void setup() {
|
|||||||
if (WifiMgr::networkCount() == 0) {
|
if (WifiMgr::networkCount() == 0) {
|
||||||
WifiSetupScreen::run(tft);
|
WifiSetupScreen::run(tft);
|
||||||
} else {
|
} else {
|
||||||
SplashScreen::setStatusLine(tft, 1, "Connecting WiFi...");
|
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_CONNECTING_WIFI));
|
||||||
WifiMgr::beginConnect();
|
WifiMgr::beginConnect();
|
||||||
|
|
||||||
uint32_t waitStart = millis();
|
uint32_t waitStart = millis();
|
||||||
@@ -213,15 +233,24 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
||||||
SplashScreen::setStatusLine(tft, 1, String("WiFi OK: ") + WifiMgr::getIP());
|
SplashScreen::setStatusLine(tft, 1, String(I18n::t(StringId::SPLASH_WIFI_OK_PREFIX)) + WifiMgr::getIP());
|
||||||
} else {
|
} else {
|
||||||
SplashScreen::setStatusLine(tft, 1, "WiFi FAILED", TFT_RED);
|
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_WIFI_FAILED), TFT_RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Sprachauswahl: NUR beim allerersten Start, direkt nach Touch-
|
||||||
|
// Kalibrierung und WLAN-Ersteinrichtung, bevor irgendwelche weiteren
|
||||||
|
// Texte in (falscher) Standardsprache gezeigt werden. ---
|
||||||
|
if (isFirstRun) {
|
||||||
|
FirstRunLanguageScreen::run(tft);
|
||||||
|
SplashScreen::begin(tft);
|
||||||
|
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
AdsbClient::primeTime();
|
AdsbClient::primeTime();
|
||||||
|
|
||||||
SplashScreen::setStatusLine(tft, 2, "Getting location...");
|
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_GETTING_LOCATION));
|
||||||
LocationManager::init();
|
LocationManager::init();
|
||||||
uint32_t locStart = millis();
|
uint32_t locStart = millis();
|
||||||
while (LocationManager::currentSource() == LocationManager::Source::None &&
|
while (LocationManager::currentSource() == LocationManager::Source::None &&
|
||||||
@@ -229,7 +258,7 @@ void setup() {
|
|||||||
LocationManager::requestIpLookupIfNeeded();
|
LocationManager::requestIpLookupIfNeeded();
|
||||||
delay(200);
|
delay(200);
|
||||||
}
|
}
|
||||||
SplashScreen::setStatusLine(tft, 2, "Ready!");
|
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
|
||||||
|
|
||||||
if (LocationManager::hasUtcOffset()) {
|
if (LocationManager::hasUtcOffset()) {
|
||||||
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
|
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
|
||||||
@@ -246,11 +275,25 @@ void setup() {
|
|||||||
drawHeader();
|
drawHeader();
|
||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
RadarScreen::render(tft, CONTENT_TOP);
|
RadarScreen::render(tft, CONTENT_TOP);
|
||||||
|
|
||||||
|
lastInteractionMs = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
TouchInput::Point tap;
|
TouchInput::Point tap;
|
||||||
if (TouchInput::wasTapped(tap)) {
|
bool tapped = TouchInput::wasTapped(tap);
|
||||||
|
|
||||||
|
if (tapped) {
|
||||||
|
lastInteractionMs = millis();
|
||||||
|
|
||||||
|
if (screenDimmed) {
|
||||||
|
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
|
||||||
|
screenDimmed = false;
|
||||||
|
tapped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tapped) {
|
||||||
if (menuBtn.contains(tap.x, tap.y)) {
|
if (menuBtn.contains(tap.x, tap.y)) {
|
||||||
MenuScreen::run(tft);
|
MenuScreen::run(tft);
|
||||||
drawHeader();
|
drawHeader();
|
||||||
@@ -291,4 +334,13 @@ void loop() {
|
|||||||
lastStatusLineMs = nowMs;
|
lastStatusLineMs = nowMs;
|
||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t timeoutMin = SettingsStore::screenTimeoutMinutes();
|
||||||
|
if (!screenDimmed && timeoutMin > 0) {
|
||||||
|
uint32_t timeoutMs = (uint32_t)timeoutMin * 60000UL;
|
||||||
|
if (nowMs - lastInteractionMs >= timeoutMs) {
|
||||||
|
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_DIMMED);
|
||||||
|
screenDimmed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,13 @@
|
|||||||
#include "wifi_manage_screen.h"
|
#include "wifi_manage_screen.h"
|
||||||
#include "stats_screen.h"
|
#include "stats_screen.h"
|
||||||
#include "logbook_files_screen.h"
|
#include "logbook_files_screen.h"
|
||||||
|
#include "location_presets_screen.h"
|
||||||
|
#include "airline_filter_screen.h"
|
||||||
|
#include "language_screen.h"
|
||||||
|
#include "units_screen.h"
|
||||||
|
#include "settings_backup.h"
|
||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
|
#include "i18n.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
namespace MenuScreen {
|
namespace MenuScreen {
|
||||||
@@ -17,8 +23,8 @@ namespace {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int16_t ROW_H = 24;
|
constexpr int16_t ROW_H = 26;
|
||||||
constexpr int16_t ROW_GAP = 2;
|
constexpr int16_t ROW_GAP = 3;
|
||||||
constexpr int16_t ROW_START_Y = 20;
|
constexpr int16_t ROW_START_Y = 20;
|
||||||
|
|
||||||
Rect rowRect(uint8_t index) {
|
Rect rowRect(uint8_t index) {
|
||||||
@@ -26,54 +32,68 @@ namespace {
|
|||||||
(int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
|
(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, uint16_t bg = TFT_NAVY) {
|
||||||
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, 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.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
tft.setTextColor(TFT_WHITE, bg);
|
||||||
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"; }
|
String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); }
|
||||||
|
|
||||||
|
String screenTimeoutLabel(uint8_t minutes) {
|
||||||
|
String prefix = I18n::t(StringId::MENU_SCREEN_TIMEOUT_PREFIX);
|
||||||
|
if (minutes == 0) return prefix + I18n::t(StringId::NEVER);
|
||||||
|
return prefix + String(minutes) + " min";
|
||||||
|
}
|
||||||
|
|
||||||
|
void showBriefMessage(TFT_eSPI& tft, const String& msg, uint16_t color) {
|
||||||
|
tft.fillRect(0, Config::SCREEN_HEIGHT - 18, Config::SCREEN_WIDTH, 18, TFT_BLACK);
|
||||||
|
tft.setTextColor(color, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.drawString(msg, Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT - 9);
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
delay(1200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void run(TFT_eSPI& tft) {
|
void run(TFT_eSPI& tft) {
|
||||||
Rect calibBtn = rowRect(0);
|
uint8_t page = 0;
|
||||||
Rect invertBtn = rowRect(1);
|
|
||||||
Rect wifiBtn = rowRect(2);
|
|
||||||
Rect emergencyBtn = rowRect(3);
|
|
||||||
Rect proximityBtn = rowRect(4);
|
|
||||||
Rect heartbeatBtn = rowRect(5);
|
|
||||||
Rect logbookBtn = rowRect(6);
|
|
||||||
Rect statsBtn = rowRect(7);
|
|
||||||
Rect logFilesBtn = rowRect(8);
|
|
||||||
Rect backBtn = rowRect(9);
|
|
||||||
|
|
||||||
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, 4);
|
tft.setCursor(10, 2);
|
||||||
tft.println("Settings");
|
tft.println(page == 0 ? I18n::t(StringId::MENU_SETTINGS) : I18n::t(StringId::MENU_SETTINGS_PAGE2));
|
||||||
|
|
||||||
drawButton(tft, calibBtn, "Calibrate touch");
|
if (page == 0) {
|
||||||
|
Rect calibBtn = rowRect(0);
|
||||||
|
Rect invertBtn = rowRect(1);
|
||||||
|
Rect wifiBtn = rowRect(2);
|
||||||
|
Rect locationBtn = rowRect(3);
|
||||||
|
Rect timeoutBtn = rowRect(4);
|
||||||
|
Rect statsBtn = rowRect(5);
|
||||||
|
Rect logFilesBtn = rowRect(6);
|
||||||
|
Rect moreBtn = rowRect(7);
|
||||||
|
Rect backBtn = rowRect(8);
|
||||||
|
|
||||||
|
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
|
||||||
|
|
||||||
String invertLabel = SettingsStore::displayInverted()
|
String invertLabel = SettingsStore::displayInverted()
|
||||||
? "Display: inverted (tap)"
|
? I18n::t(StringId::MENU_DISPLAY_INVERTED)
|
||||||
: "Display: normal (tap)";
|
: I18n::t(StringId::MENU_DISPLAY_NORMAL);
|
||||||
drawButton(tft, invertBtn, invertLabel);
|
drawButton(tft, invertBtn, invertLabel);
|
||||||
|
|
||||||
drawButton(tft, wifiBtn, "Manage WiFi networks");
|
drawButton(tft, wifiBtn, I18n::t(StringId::MENU_MANAGE_WIFI));
|
||||||
|
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
|
||||||
drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled()));
|
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
|
||||||
drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled()));
|
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
|
||||||
drawButton(tft, heartbeatBtn, "LED heartbeat: " + onOff(SettingsStore::ledHeartbeatEnabled()));
|
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
|
||||||
drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled()));
|
drawButton(tft, moreBtn, I18n::t(StringId::MENU_MORE_SETTINGS));
|
||||||
drawButton(tft, statsBtn, "Statistics");
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
drawButton(tft, logFilesBtn, "Logbook files");
|
|
||||||
|
|
||||||
drawButton(tft, backBtn, "Back");
|
|
||||||
|
|
||||||
TouchInput::Point tap;
|
TouchInput::Point tap;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -89,7 +109,47 @@ void run(TFT_eSPI& tft) {
|
|||||||
tft.invertDisplay(newState);
|
tft.invertDisplay(newState);
|
||||||
} else if (wifiBtn.contains(tap.x, tap.y)) {
|
} else if (wifiBtn.contains(tap.x, tap.y)) {
|
||||||
WifiManageScreen::run(tft);
|
WifiManageScreen::run(tft);
|
||||||
} else if (emergencyBtn.contains(tap.x, tap.y)) {
|
} else if (locationBtn.contains(tap.x, tap.y)) {
|
||||||
|
LocationPresetsScreen::run(tft);
|
||||||
|
} else if (timeoutBtn.contains(tap.x, tap.y)) {
|
||||||
|
uint8_t current = SettingsStore::screenTimeoutMinutes();
|
||||||
|
uint8_t next = (current >= 10) ? 0 : (current + 1);
|
||||||
|
SettingsStore::setScreenTimeoutMinutes(next);
|
||||||
|
} else if (statsBtn.contains(tap.x, tap.y)) {
|
||||||
|
StatsScreen::run(tft);
|
||||||
|
} else if (logFilesBtn.contains(tap.x, tap.y)) {
|
||||||
|
LogbookFilesScreen::run(tft);
|
||||||
|
} else if (moreBtn.contains(tap.x, tap.y)) {
|
||||||
|
page = 1;
|
||||||
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
} else if (page == 1) {
|
||||||
|
Rect emergencyBtn = rowRect(0);
|
||||||
|
Rect proximityBtn = rowRect(1);
|
||||||
|
Rect heartbeatBtn = rowRect(2);
|
||||||
|
Rect logbookBtn = rowRect(3);
|
||||||
|
Rect groundBtn = rowRect(4);
|
||||||
|
Rect airlineBtn = rowRect(5);
|
||||||
|
Rect moreBtn = rowRect(6);
|
||||||
|
Rect backBtn = rowRect(7);
|
||||||
|
|
||||||
|
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
|
||||||
|
drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled()));
|
||||||
|
drawButton(tft, heartbeatBtn, I18n::t(StringId::MENU_LED_HEARTBEAT) + onOff(SettingsStore::ledHeartbeatEnabled()));
|
||||||
|
drawButton(tft, logbookBtn, I18n::t(StringId::MENU_FLIGHT_LOGBOOK) + onOff(SettingsStore::flightLogbookEnabled()));
|
||||||
|
drawButton(tft, groundBtn, I18n::t(StringId::MENU_HIDE_GROUND) + onOff(SettingsStore::hideGroundVehicles()));
|
||||||
|
drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER));
|
||||||
|
drawButton(tft, moreBtn, I18n::t(StringId::MENU_MORE_SETTINGS));
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emergencyBtn.contains(tap.x, tap.y)) {
|
||||||
SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled());
|
SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled());
|
||||||
} else if (proximityBtn.contains(tap.x, tap.y)) {
|
} else if (proximityBtn.contains(tap.x, tap.y)) {
|
||||||
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
|
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
|
||||||
@@ -97,12 +157,52 @@ void run(TFT_eSPI& tft) {
|
|||||||
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
|
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
|
||||||
} else if (logbookBtn.contains(tap.x, tap.y)) {
|
} else if (logbookBtn.contains(tap.x, tap.y)) {
|
||||||
SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled());
|
SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled());
|
||||||
} else if (statsBtn.contains(tap.x, tap.y)) {
|
} else if (groundBtn.contains(tap.x, tap.y)) {
|
||||||
StatsScreen::run(tft);
|
SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles());
|
||||||
} else if (logFilesBtn.contains(tap.x, tap.y)) {
|
} else if (airlineBtn.contains(tap.x, tap.y)) {
|
||||||
LogbookFilesScreen::run(tft);
|
AirlineFilterScreen::run(tft);
|
||||||
|
} else if (moreBtn.contains(tap.x, tap.y)) {
|
||||||
|
page = 2;
|
||||||
} else if (backBtn.contains(tap.x, tap.y)) {
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
done = true;
|
page = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Rect languageBtn = rowRect(0);
|
||||||
|
Rect unitsBtn = rowRect(1);
|
||||||
|
Rect backupBtn = rowRect(2);
|
||||||
|
Rect restoreBtn = rowRect(3);
|
||||||
|
Rect backBtn = rowRect(4);
|
||||||
|
|
||||||
|
drawButton(tft, languageBtn, String(I18n::t(StringId::MENU_LANGUAGE)) + ": " + I18n::languageName(SettingsStore::language()));
|
||||||
|
drawButton(tft, unitsBtn, I18n::t(StringId::MENU_UNITS));
|
||||||
|
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
|
||||||
|
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE),
|
||||||
|
SettingsBackup::hasBackup() ? TFT_NAVY : TFT_DARKGREY);
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageBtn.contains(tap.x, tap.y)) {
|
||||||
|
LanguageScreen::run(tft);
|
||||||
|
} else if (unitsBtn.contains(tap.x, tap.y)) {
|
||||||
|
UnitsScreen::run(tft);
|
||||||
|
} else if (backupBtn.contains(tap.x, tap.y)) {
|
||||||
|
bool ok = SettingsBackup::backup();
|
||||||
|
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED),
|
||||||
|
ok ? TFT_GREEN : TFT_RED);
|
||||||
|
} else if (restoreBtn.contains(tap.x, tap.y)) {
|
||||||
|
if (SettingsBackup::hasBackup()) {
|
||||||
|
bool ok = SettingsBackup::restore();
|
||||||
|
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_RESTORED : StringId::MENU_RESTORE_FAILED),
|
||||||
|
ok ? TFT_GREEN : TFT_RED);
|
||||||
|
}
|
||||||
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#include <TFT_eSPI.h>
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
namespace MenuScreen {
|
namespace MenuScreen {
|
||||||
// Blockierend: einfaches Menue (aktuell nur "Touch kalibrieren").
|
// Blockierend: einfaches Menue (Kalibrierung, Anzeige-Invertierung,
|
||||||
|
// WLAN-Verwaltung, Alarm-Toggles, Statistik, Logbuch-Dateien).
|
||||||
// Kehrt zurueck, sobald "Zurueck" angetippt wird.
|
// Kehrt zurueck, sobald "Zurueck" angetippt wird.
|
||||||
void run(TFT_eSPI& tft);
|
void run(TFT_eSPI& tft);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "led_alert.h"
|
#include "led_alert.h"
|
||||||
#include "location_manager.h"
|
#include "location_manager.h"
|
||||||
#include "world_map.h"
|
#include "world_map.h"
|
||||||
|
#include "airline_filter.h"
|
||||||
|
#include "i18n.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
namespace RadarScreen {
|
namespace RadarScreen {
|
||||||
@@ -42,7 +44,7 @@ namespace {
|
|||||||
return L;
|
return L;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int16_t DETAIL_PANEL_H = 150;
|
constexpr int16_t DETAIL_PANEL_H = 190;
|
||||||
|
|
||||||
struct HitPoint {
|
struct HitPoint {
|
||||||
int16_t x, y;
|
int16_t x, y;
|
||||||
@@ -212,15 +214,8 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dezente Weltkarten-Silhouette als Hintergrund-Layer, wie bei einem
|
|
||||||
// klassischen ATC-Radarschirm - rein dekorativ (nicht massstabsgetreu
|
|
||||||
// zu den Entfernungsringen). Wird auf das quadratische Kreis-Layout
|
|
||||||
// gestreckt (die Karte ist 2:1, der Kreis-Bereich 1:1 - das verzerrt
|
|
||||||
// die Kontinente etwas in der Hoehe, faellt aber bei dieser Groesse
|
|
||||||
// kaum auf und haelt die Zeichenlogik einfach). Wird NUR bei render()
|
|
||||||
// gezeichnet, NICHT bei jedem Sweep-Tick (zu teuer fuer 12x/Sekunde).
|
|
||||||
void drawWorldMap(TFT_eSPI& gfx, const Layout& L) {
|
void drawWorldMap(TFT_eSPI& gfx, const Layout& L) {
|
||||||
constexpr uint16_t dim = 0x0320; // sehr gedaempftes Gruen
|
constexpr uint16_t dim = 0x0320;
|
||||||
float scaleX = (2.0f * L.radius) / WorldMap::GRID_W;
|
float scaleX = (2.0f * L.radius) / WorldMap::GRID_W;
|
||||||
float scaleY = (2.0f * L.radius) / WorldMap::GRID_H;
|
float scaleY = (2.0f * L.radius) / WorldMap::GRID_H;
|
||||||
int32_t radiusSq = (int32_t)(L.radius - 2) * (L.radius - 2);
|
int32_t radiusSq = (int32_t)(L.radius - 2) * (L.radius - 2);
|
||||||
@@ -274,8 +269,8 @@ namespace {
|
|||||||
struct PanelState {
|
struct PanelState {
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
char hex[7] = {0};
|
char hex[7] = {0};
|
||||||
String callsignText, airlineText, modelText,
|
String callsignText, airlineText, modelText, typeText,
|
||||||
altText, speedText, distHeadingText, seatsText;
|
altText, speedText, climbText, distHeadingText, squawkText, seatsText;
|
||||||
};
|
};
|
||||||
PanelState lastPanel;
|
PanelState lastPanel;
|
||||||
|
|
||||||
@@ -322,43 +317,59 @@ namespace {
|
|||||||
String(a.airlineName), forceFull);
|
String(a.airlineName), forceFull);
|
||||||
y += 15;
|
y += 15;
|
||||||
|
|
||||||
String modelLine;
|
String modelLine = details.loading
|
||||||
if (details.loading) {
|
? String(I18n::t(StringId::DETAIL_MODEL)) + I18n::t(StringId::DETAIL_LOADING_DOTS)
|
||||||
modelLine = "Model: loading...";
|
: String(I18n::t(StringId::DETAIL_MODEL)) + (details.model[0] ? details.model : I18n::t(StringId::DETAIL_UNKNOWN));
|
||||||
} else if (details.model[0]) {
|
|
||||||
modelLine = String("Model: ") + details.model;
|
|
||||||
} else {
|
|
||||||
modelLine = String("Type: ") + (a.typeCode[0] ? a.typeCode : "unknown");
|
|
||||||
}
|
|
||||||
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.modelText, modelLine, forceFull);
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.modelText, modelLine, forceFull);
|
||||||
y += 15;
|
y += 15;
|
||||||
|
|
||||||
char buf[40];
|
String typeLine = String(I18n::t(StringId::DETAIL_TYPE)) + (a.typeCode[0] ? a.typeCode : I18n::t(StringId::DETAIL_UNKNOWN));
|
||||||
snprintf(buf, sizeof(buf), "Alt: %.0fm / %.0fft",
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.typeText, typeLine, forceFull);
|
||||||
|
y += 15;
|
||||||
|
|
||||||
|
char buf[48];
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.0fm / %.0fft", I18n::t(StringId::DETAIL_ALT),
|
||||||
Units::feetToMeters((float)a.altBaroFt), (float)a.altBaroFt);
|
Units::feetToMeters((float)a.altBaroFt), (float)a.altBaroFt);
|
||||||
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.altText, String(buf), forceFull);
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.altText, String(buf), forceFull);
|
||||||
y += 15;
|
y += 15;
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "Speed: %.0fkm/h / %.0fkt",
|
snprintf(buf, sizeof(buf), "%s%.0fkm/h / %.0fkt", I18n::t(StringId::DETAIL_SPEED),
|
||||||
Units::ktToKmh(a.groundSpeedKt), a.groundSpeedKt);
|
Units::ktToKmh(a.groundSpeedKt), a.groundSpeedKt);
|
||||||
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.speedText, String(buf), forceFull);
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.speedText, String(buf), forceFull);
|
||||||
y += 15;
|
y += 15;
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "Dist: %.0fkm / %.0fnm Hdg: %.0f",
|
String climbLine;
|
||||||
a.distanceKm, Units::kmToNm(a.distanceKm), a.headingDeg);
|
if (a.vertRateFtMin > 100) {
|
||||||
|
snprintf(buf, sizeof(buf), "%s+%dft/min", I18n::t(StringId::DETAIL_CLIMB), a.vertRateFtMin);
|
||||||
|
climbLine = buf;
|
||||||
|
} else if (a.vertRateFtMin < -100) {
|
||||||
|
snprintf(buf, sizeof(buf), "%s%dft/min", I18n::t(StringId::DETAIL_DESCENT), a.vertRateFtMin);
|
||||||
|
climbLine = buf;
|
||||||
|
} else {
|
||||||
|
climbLine = I18n::t(StringId::DETAIL_LEVEL);
|
||||||
|
}
|
||||||
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.climbText, climbLine, forceFull);
|
||||||
|
y += 15;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.0fkm / %.0fnm %s%.0f", I18n::t(StringId::DETAIL_DIST),
|
||||||
|
a.distanceKm, Units::kmToNm(a.distanceKm), I18n::t(StringId::DETAIL_HDG), a.headingDeg);
|
||||||
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.distHeadingText, String(buf), forceFull);
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.distHeadingText, String(buf), forceFull);
|
||||||
y += 15;
|
y += 15;
|
||||||
|
|
||||||
|
String squawkLine = String(I18n::t(StringId::DETAIL_SQUAWK)) + (a.squawk[0] ? a.squawk : I18n::t(StringId::DETAIL_UNKNOWN));
|
||||||
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.squawkText, squawkLine, forceFull);
|
||||||
|
y += 15;
|
||||||
|
|
||||||
String seatsLine = a.estSeats > 0
|
String seatsLine = a.estSeats > 0
|
||||||
? String("Seats (estimated): ") + a.estSeats
|
? String(I18n::t(StringId::DETAIL_SEATS_EST)) + a.estSeats
|
||||||
: String("Seats: unknown");
|
: String(I18n::t(StringId::DETAIL_SEATS_UNKNOWN));
|
||||||
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.seatsText, seatsLine, forceFull);
|
updateLine(gfx, y, 15, textMaxWidth, TFT_WHITE, lastPanel.seatsText, seatsLine, forceFull);
|
||||||
y += 18;
|
y += 18;
|
||||||
|
|
||||||
if (forceFull) {
|
if (forceFull) {
|
||||||
gfx.setTextColor(TFT_DARKGREY, TFT_NAVY);
|
gfx.setTextColor(TFT_DARKGREY, TFT_NAVY);
|
||||||
gfx.setCursor(8, y);
|
gfx.setCursor(8, y);
|
||||||
gfx.print("Tap elsewhere to close");
|
gfx.print(I18n::t(StringId::DETAIL_TAP_CLOSE));
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPanel.valid = true;
|
lastPanel.valid = true;
|
||||||
@@ -429,6 +440,10 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
Aircraft& a = snapshot[i];
|
Aircraft& a = snapshot[i];
|
||||||
if (a.distanceKm > rangeKm * 1.05f) continue;
|
if (a.distanceKm > rangeKm * 1.05f) continue;
|
||||||
|
|
||||||
|
if (SettingsStore::hideGroundVehicles() && a.category[0] == 'C') continue;
|
||||||
|
|
||||||
|
if (AirlineFilter::isHidden(a.callsign)) continue;
|
||||||
|
|
||||||
RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg};
|
RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg};
|
||||||
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
|
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
|
||||||
|
|
||||||
@@ -482,7 +497,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
tft.drawFastHLine(0, infoTop, Config::SCREEN_WIDTH, TFT_DARKGREY);
|
tft.drawFastHLine(0, infoTop, Config::SCREEN_WIDTH, TFT_DARKGREY);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(8, infoTop + 6);
|
tft.setCursor(8, infoTop + 6);
|
||||||
tft.print("Tap for details");
|
tft.print(I18n::t(StringId::RADAR_TAP_FOR_DETAILS));
|
||||||
|
|
||||||
char rangeLabel[8];
|
char rangeLabel[8];
|
||||||
snprintf(rangeLabel, sizeof(rangeLabel), "%.0fkm", rangeKm);
|
snprintf(rangeLabel, sizeof(rangeLabel), "%.0fkm", rangeKm);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String save(TFT_eSPI& tft) {
|
String save(TFT_eSPI& tft) {
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
if (!SD.exists(Config::SD_SCREENSHOT_DIR)) {
|
if (!SD.exists(Config::SD_SCREENSHOT_DIR)) {
|
||||||
SD.mkdir(Config::SD_SCREENSHOT_DIR);
|
SD.mkdir(Config::SD_SCREENSHOT_DIR);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/sd_mutex.cpp
Normal file
28
src/sd_mutex.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include "sd_mutex.h"
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
namespace SdMutex {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
if (mutex == nullptr) {
|
||||||
|
mutex = xSemaphoreCreateRecursiveMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void lock() {
|
||||||
|
if (mutex == nullptr) init();
|
||||||
|
xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlock() {
|
||||||
|
if (mutex != nullptr) {
|
||||||
|
xSemaphoreGiveRecursive(mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/sd_mutex.h
Normal file
27
src/sd_mutex.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Gemeinsames Lock fuer ALLE SD-Kartenzugriffe im gesamten Projekt. Die
|
||||||
|
// SD-Bibliothek ist nicht thread-sicher: ohne dieses Lock koennen
|
||||||
|
// gleichzeitige Zugriffe von NetTask (Core 0, z.B. Flugbuch-Eintrag
|
||||||
|
// schreiben alle 8s) und dem Haupt-Loop (Core 1, z.B. Einstellungen im
|
||||||
|
// Menue speichern) das Geraet einfrieren, wenn sie zeitlich zusammentreffen.
|
||||||
|
// Rekursiv, damit verschachtelte Aufrufe (z.B. FlightLogbook::update() ruft
|
||||||
|
// intern ensureCurrentDate() auf, das seinerseits auch sperrt) nicht
|
||||||
|
// blockieren.
|
||||||
|
namespace SdMutex {
|
||||||
|
void init();
|
||||||
|
void lock();
|
||||||
|
void unlock();
|
||||||
|
|
||||||
|
// RAII-Helfer: sperrt im Konstruktor, gibt im Destruktor frei - so kann
|
||||||
|
// man das Lock nicht versehentlich vergessen freizugeben (z.B. bei
|
||||||
|
// einem fruehen "return").
|
||||||
|
class Guard {
|
||||||
|
public:
|
||||||
|
Guard() { lock(); }
|
||||||
|
~Guard() { unlock(); }
|
||||||
|
Guard(const Guard&) = delete;
|
||||||
|
Guard& operator=(const Guard&) = delete;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "sd_storage.h"
|
#include "sd_storage.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@@ -64,6 +65,9 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool init() {
|
bool init() {
|
||||||
|
SdMutex::init();
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
sdSpi.begin(Config::SD_SPI_CLK_PIN, Config::SD_SPI_MISO_PIN,
|
sdSpi.begin(Config::SD_SPI_CLK_PIN, Config::SD_SPI_MISO_PIN,
|
||||||
Config::SD_SPI_MOSI_PIN, Config::SD_SPI_CS_PIN);
|
Config::SD_SPI_MOSI_PIN, Config::SD_SPI_CS_PIN);
|
||||||
mounted = SD.begin(Config::SD_SPI_CS_PIN, sdSpi, 4000000);
|
mounted = SD.begin(Config::SD_SPI_CS_PIN, sdSpi, 4000000);
|
||||||
@@ -79,12 +83,14 @@ bool isMounted() { return mounted; }
|
|||||||
|
|
||||||
void seedDefaultDataFiles() {
|
void seedDefaultDataFiles() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
writeIfAbsent(Config::SD_AIRLINES_CSV, kDefaultAirlinesCsv);
|
writeIfAbsent(Config::SD_AIRLINES_CSV, kDefaultAirlinesCsv);
|
||||||
writeIfAbsent(Config::SD_AIRCRAFT_TYPES_CSV, kDefaultAircraftTypesCsv);
|
writeIfAbsent(Config::SD_AIRCRAFT_TYPES_CSV, kDefaultAircraftTypesCsv);
|
||||||
}
|
}
|
||||||
|
|
||||||
void logEvent(const char* csvLine) {
|
void logEvent(const char* csvLine) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
time_t now = time(nullptr);
|
time_t now = time(nullptr);
|
||||||
struct tm tmNow;
|
struct tm tmNow;
|
||||||
|
|||||||
60
src/settings_backup.cpp
Normal file
60
src/settings_backup.cpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#include "settings_backup.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
|
#include "sd_storage.h"
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
namespace SettingsBackup {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char* WIFI_BACKUP_FILE = "/Flightradar_cyd/wifi_backup.txt";
|
||||||
|
constexpr const char* SETTINGS_BACKUP_FILE = "/Flightradar_cyd/config_backup.txt";
|
||||||
|
|
||||||
|
bool copyFile(const char* srcPath, const char* dstPath) {
|
||||||
|
if (!SD.exists(srcPath)) return false;
|
||||||
|
|
||||||
|
File src = SD.open(srcPath, FILE_READ);
|
||||||
|
if (!src) return false;
|
||||||
|
|
||||||
|
File dst = SD.open(dstPath, FILE_WRITE);
|
||||||
|
if (!dst) { src.close(); return false; }
|
||||||
|
|
||||||
|
constexpr size_t BUF_SIZE = 512;
|
||||||
|
static uint8_t buf[BUF_SIZE];
|
||||||
|
while (src.available()) {
|
||||||
|
size_t n = src.read(buf, BUF_SIZE);
|
||||||
|
dst.write(buf, n);
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
src.close();
|
||||||
|
dst.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool backup() {
|
||||||
|
if (!SdStorage::isMounted()) return false;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
bool okSettings = copyFile(Config::SD_SETTINGS_FILE, SETTINGS_BACKUP_FILE);
|
||||||
|
bool okWifi = copyFile(Config::SD_WIFI_CREDENTIALS_FILE, WIFI_BACKUP_FILE);
|
||||||
|
return okSettings || okWifi;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool restore() {
|
||||||
|
if (!SdStorage::isMounted()) return false;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
|
bool okSettings = copyFile(SETTINGS_BACKUP_FILE, Config::SD_SETTINGS_FILE);
|
||||||
|
bool okWifi = copyFile(WIFI_BACKUP_FILE, Config::SD_WIFI_CREDENTIALS_FILE);
|
||||||
|
return okSettings || okWifi;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasBackup() {
|
||||||
|
if (!SdStorage::isMounted()) return false;
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
return SD.exists(SETTINGS_BACKUP_FILE) || SD.exists(WIFI_BACKUP_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/settings_backup.h
Normal file
8
src/settings_backup.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace SettingsBackup {
|
||||||
|
bool backup();
|
||||||
|
bool restore();
|
||||||
|
bool hasBackup();
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
#include "settings_store.h"
|
#include "settings_store.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
namespace SettingsStore {
|
namespace SettingsStore {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
|
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
|
||||||
bool inverted = false;
|
bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp)
|
||||||
bool emergencyAlertOn = true;
|
bool emergencyAlertOn = true;
|
||||||
bool proximityAlertOn = true;
|
bool proximityAlertOn = true;
|
||||||
bool flightLogbookOn = true;
|
bool flightLogbookOn = true;
|
||||||
bool ledHeartbeatOn = true;
|
bool ledHeartbeatOn = true;
|
||||||
|
uint8_t screenTimeoutMin = 0;
|
||||||
|
bool hideGroundVehiclesOn = true;
|
||||||
|
uint8_t languageIdx = 0;
|
||||||
|
uint8_t unitsModeVal = 0;
|
||||||
|
|
||||||
void applyKeyValue(const String& key, const String& value) {
|
void applyKeyValue(const String& key, const String& value) {
|
||||||
if (key == "range_index") {
|
if (key == "range_index") {
|
||||||
@@ -28,11 +33,24 @@ namespace {
|
|||||||
flightLogbookOn = (value.toInt() != 0);
|
flightLogbookOn = (value.toInt() != 0);
|
||||||
} else if (key == "led_heartbeat") {
|
} else if (key == "led_heartbeat") {
|
||||||
ledHeartbeatOn = (value.toInt() != 0);
|
ledHeartbeatOn = (value.toInt() != 0);
|
||||||
|
} else if (key == "screen_timeout_min") {
|
||||||
|
int v = value.toInt();
|
||||||
|
if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v;
|
||||||
|
} else if (key == "hide_ground_vehicles") {
|
||||||
|
hideGroundVehiclesOn = (value.toInt() != 0);
|
||||||
|
} else if (key == "language") {
|
||||||
|
int v = value.toInt();
|
||||||
|
if (v >= 0 && v <= 5) languageIdx = (uint8_t)v;
|
||||||
|
} else if (key == "units_mode") {
|
||||||
|
int v = value.toInt();
|
||||||
|
if (v >= 0 && v <= 2) unitsModeVal = (uint8_t)v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
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);
|
||||||
@@ -56,6 +74,8 @@ void load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void save() {
|
void save() {
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
File f = SD.open(Config::SD_SETTINGS_FILE, FILE_WRITE);
|
File f = SD.open(Config::SD_SETTINGS_FILE, FILE_WRITE);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
f.printf("range_index=%d\n", rangeIdx);
|
f.printf("range_index=%d\n", rangeIdx);
|
||||||
@@ -64,6 +84,10 @@ void save() {
|
|||||||
f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0);
|
f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0);
|
||||||
f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0);
|
f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0);
|
||||||
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
|
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
|
||||||
|
f.printf("screen_timeout_min=%d\n", screenTimeoutMin);
|
||||||
|
f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0);
|
||||||
|
f.printf("language=%d\n", languageIdx);
|
||||||
|
f.printf("units_mode=%d\n", unitsModeVal);
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,4 +135,38 @@ void setLedHeartbeatEnabled(bool on) {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t screenTimeoutMinutes() { return screenTimeoutMin; }
|
||||||
|
|
||||||
|
void setScreenTimeoutMinutes(uint8_t minutes) {
|
||||||
|
if (minutes <= 10) {
|
||||||
|
screenTimeoutMin = minutes;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hideGroundVehicles() { return hideGroundVehiclesOn; }
|
||||||
|
|
||||||
|
void setHideGroundVehicles(bool on) {
|
||||||
|
hideGroundVehiclesOn = on;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t language() { return languageIdx; }
|
||||||
|
|
||||||
|
void setLanguage(uint8_t lang) {
|
||||||
|
if (lang <= 5) {
|
||||||
|
languageIdx = lang;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t unitsMode() { return unitsModeVal; }
|
||||||
|
|
||||||
|
void setUnitsMode(uint8_t mode) {
|
||||||
|
if (mode <= 2) {
|
||||||
|
unitsModeVal = mode;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,21 @@ namespace SettingsStore {
|
|||||||
bool flightLogbookEnabled();
|
bool flightLogbookEnabled();
|
||||||
void setFlightLogbookEnabled(bool on);
|
void setFlightLogbookEnabled(bool on);
|
||||||
|
|
||||||
// Kurzes gruenes Aufblitzen der Rueckseiten-LED synchron zu jeder
|
|
||||||
// ADS-B-Abfrage (alle 8s) - zeigt "Geraet ist aktiv", auch wenn gerade
|
|
||||||
// kein Naeherungs-/Notfall-Alarm laeuft.
|
|
||||||
bool ledHeartbeatEnabled();
|
bool ledHeartbeatEnabled();
|
||||||
void setLedHeartbeatEnabled(bool on);
|
void setLedHeartbeatEnabled(bool on);
|
||||||
|
|
||||||
|
uint8_t screenTimeoutMinutes();
|
||||||
|
void setScreenTimeoutMinutes(uint8_t minutes);
|
||||||
|
|
||||||
|
bool hideGroundVehicles();
|
||||||
|
void setHideGroundVehicles(bool on);
|
||||||
|
|
||||||
|
// Sprache der Benutzeroberflaeche: 0=EN,1=DE,2=FR,3=TR,4=ES,5=IT.
|
||||||
|
uint8_t language();
|
||||||
|
void setLanguage(uint8_t lang);
|
||||||
|
|
||||||
|
// Einheiten-Modus: 0=Auto (per IP-Standort geschaetzt), 1=Metrisch
|
||||||
|
// erzwingen, 2=Imperial (Fuss/Knoten/Meilen) erzwingen.
|
||||||
|
uint8_t unitsMode();
|
||||||
|
void setUnitsMode(uint8_t mode);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "flight_logbook.h"
|
#include "flight_logbook.h"
|
||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
namespace StatsScreen {
|
namespace StatsScreen {
|
||||||
|
|
||||||
@@ -13,11 +14,11 @@ namespace {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) {
|
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, 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.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
tft.setTextColor(TFT_WHITE, bg);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -32,40 +33,98 @@ namespace {
|
|||||||
tft.print(value);
|
tft.print(value);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t CONFIRM_WINDOW_MS = 4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
void run(TFT_eSPI& tft) {
|
void run(TFT_eSPI& tft) {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 10);
|
||||||
|
tft.println(I18n::t(StringId::STATS_TITLE));
|
||||||
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 44);
|
||||||
|
tft.print(I18n::t(StringId::LOADING));
|
||||||
|
|
||||||
uint16_t today = FlightLogbook::todayCount();
|
uint16_t today = FlightLogbook::todayCount();
|
||||||
uint32_t allTimeAircraft = 0;
|
uint32_t allTimeAircraft = 0;
|
||||||
uint16_t allTimeDays = 0;
|
uint16_t allTimeDays = 0;
|
||||||
FlightLogbook::computeAllTimeStats(allTimeAircraft, allTimeDays);
|
FlightLogbook::computeAllTimeStats(allTimeAircraft, allTimeDays);
|
||||||
|
|
||||||
|
Rect resetBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 100), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||||
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||||
|
|
||||||
|
bool confirmPending = false;
|
||||||
|
uint32_t confirmArmedAtMs = 0;
|
||||||
|
bool justReset = false;
|
||||||
|
|
||||||
|
auto redraw = [&]() {
|
||||||
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, 10);
|
||||||
tft.println("Statistics");
|
tft.println(I18n::t(StringId::STATS_TITLE));
|
||||||
|
|
||||||
drawStatRow(tft, 44, "Aircraft logged today", String(today));
|
drawStatRow(tft, 44, I18n::t(StringId::STATS_TODAY), String(today));
|
||||||
drawStatRow(tft, 90, "Aircraft logged all-time", String(allTimeAircraft));
|
drawStatRow(tft, 90, I18n::t(StringId::STATS_ALLTIME), String(allTimeAircraft));
|
||||||
drawStatRow(tft, 136, "Days with sightings", String(allTimeDays));
|
drawStatRow(tft, 136, I18n::t(StringId::STATS_DAYS), String(allTimeDays));
|
||||||
|
|
||||||
if (allTimeDays > 0) {
|
if (justReset) {
|
||||||
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 182);
|
||||||
|
tft.print(I18n::t(StringId::STATS_RESET_DONE));
|
||||||
|
} else if (allTimeDays > 0) {
|
||||||
float avgPerDay = (float)allTimeAircraft / (float)allTimeDays;
|
float avgPerDay = (float)allTimeAircraft / (float)allTimeDays;
|
||||||
char buf[16];
|
char buf[16];
|
||||||
snprintf(buf, sizeof(buf), "%.1f", avgPerDay);
|
snprintf(buf, sizeof(buf), "%.1f", avgPerDay);
|
||||||
drawStatRow(tft, 182, "Average per day", String(buf));
|
drawStatRow(tft, 182, I18n::t(StringId::STATS_AVG), String(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
drawButton(tft, backBtn, "Back");
|
uint32_t upSec = millis() / 1000;
|
||||||
|
uint32_t upH = upSec / 3600;
|
||||||
|
uint32_t upM = (upSec % 3600) / 60;
|
||||||
|
char upBuf[8];
|
||||||
|
snprintf(upBuf, sizeof(upBuf), "%luh %lum", (unsigned long)upH, (unsigned long)upM);
|
||||||
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 210);
|
||||||
|
tft.print(String(I18n::t(StringId::STATS_UPTIME_PREFIX)) + upBuf);
|
||||||
|
|
||||||
|
if (confirmPending) {
|
||||||
|
drawButton(tft, resetBtn, I18n::t(StringId::STATS_RESET_CONFIRM), TFT_MAROON);
|
||||||
|
} else {
|
||||||
|
drawButton(tft, resetBtn, I18n::t(StringId::STATS_RESET_BTN));
|
||||||
|
}
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
|
};
|
||||||
|
|
||||||
|
redraw();
|
||||||
|
|
||||||
bool done = false;
|
bool done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
if (confirmPending && millis() - confirmArmedAtMs > CONFIRM_WINDOW_MS) {
|
||||||
|
confirmPending = false;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
TouchInput::Point tap;
|
TouchInput::Point tap;
|
||||||
if (TouchInput::wasTapped(tap) && backBtn.contains(tap.x, tap.y)) {
|
if (TouchInput::wasTapped(tap)) {
|
||||||
|
if (resetBtn.contains(tap.x, tap.y)) {
|
||||||
|
if (confirmPending) {
|
||||||
|
FlightLogbook::resetAllData();
|
||||||
|
today = 0;
|
||||||
|
allTimeAircraft = 0;
|
||||||
|
allTimeDays = 0;
|
||||||
|
confirmPending = false;
|
||||||
|
justReset = true;
|
||||||
|
redraw();
|
||||||
|
} else {
|
||||||
|
confirmPending = true;
|
||||||
|
confirmArmedAtMs = millis();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
delay(20);
|
delay(20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/units_screen.cpp
Normal file
78
src/units_screen.cpp
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include "units_screen.h"
|
||||||
|
#include "settings_store.h"
|
||||||
|
#include "touch_input.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace UnitsScreen {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int16_t ROW_H = 40;
|
||||||
|
constexpr int16_t ROW_GAP = 8;
|
||||||
|
constexpr int16_t START_Y = 40;
|
||||||
|
|
||||||
|
Rect rowRect(uint8_t index) {
|
||||||
|
return {10, (int16_t)(START_Y + index * (ROW_H + ROW_GAP)),
|
||||||
|
(int16_t)(Config::SCREEN_WIDTH - 20), ROW_H};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(TFT_eSPI& tft) {
|
||||||
|
constexpr uint8_t MODE_COUNT = 3;
|
||||||
|
StringId labels[MODE_COUNT] = {StringId::UNITS_AUTO, StringId::UNITS_METRIC, StringId::UNITS_IMPERIAL};
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
while (!done) {
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setCursor(10, 10);
|
||||||
|
tft.println(I18n::t(StringId::UNITS_TITLE));
|
||||||
|
|
||||||
|
uint8_t current = SettingsStore::unitsMode();
|
||||||
|
Rect modeRects[MODE_COUNT];
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < MODE_COUNT; i++) {
|
||||||
|
modeRects[i] = rowRect(i);
|
||||||
|
drawButton(tft, modeRects[i], I18n::t(labels[i]),
|
||||||
|
(i == current) ? TFT_DARKGREEN : TFT_NAVY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect backBtn = rowRect(MODE_COUNT);
|
||||||
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
for (uint8_t i = 0; i < MODE_COUNT && !handled; i++) {
|
||||||
|
if (modeRects[i].contains(tap.x, tap.y)) {
|
||||||
|
SettingsStore::setUnitsMode(i);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/units_screen.h
Normal file
7
src/units_screen.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
|
namespace UnitsScreen {
|
||||||
|
void run(TFT_eSPI& tft);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "sd_storage.h"
|
#include "sd_storage.h"
|
||||||
|
#include "sd_mutex.h"
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
@@ -25,12 +26,6 @@ namespace {
|
|||||||
uint32_t lastReconnectAttemptMs = 0;
|
uint32_t lastReconnectAttemptMs = 0;
|
||||||
constexpr uint32_t RECONNECT_RETRY_MS = 10000;
|
constexpr uint32_t RECONNECT_RETRY_MS = 10000;
|
||||||
|
|
||||||
// Der Reconnect-Scan MUSS asynchron laufen: update() wird dauerhaft in
|
|
||||||
// der NetTask-Schleife (Core 0) aufgerufen, und ein blockierender Scan
|
|
||||||
// (WiFi.scanNetworks(false)) haelt dort mehrere Sekunden am Stueck fest -
|
|
||||||
// das hat frueher den Watchdog ausgeloest (Bootloop). Stattdessen wird
|
|
||||||
// der Scan hier gestartet und ueber mehrere update()-Aufrufe hinweg
|
|
||||||
// nicht-blockierend abgefragt.
|
|
||||||
bool reconnectScanPending = false;
|
bool reconnectScanPending = false;
|
||||||
|
|
||||||
SemaphoreHandle_t mutex = nullptr;
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
@@ -44,6 +39,9 @@ namespace {
|
|||||||
void loadFromSd() {
|
void loadFromSd() {
|
||||||
networkCountVal = 0;
|
networkCountVal = 0;
|
||||||
if (!SdStorage::isMounted()) return;
|
if (!SdStorage::isMounted()) return;
|
||||||
|
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return;
|
if (!SD.exists(Config::SD_WIFI_CREDENTIALS_FILE)) return;
|
||||||
|
|
||||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ);
|
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_READ);
|
||||||
@@ -66,6 +64,8 @@ namespace {
|
|||||||
void saveToSd() {
|
void saveToSd() {
|
||||||
if (!SdStorage::isMounted()) return;
|
if (!SdStorage::isMounted()) return;
|
||||||
|
|
||||||
|
SdMutex::Guard guard;
|
||||||
|
|
||||||
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE);
|
File f = SD.open(Config::SD_WIFI_CREDENTIALS_FILE, FILE_WRITE);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
for (uint8_t i = 0; i < networkCountVal; i++) {
|
for (uint8_t i = 0; i < networkCountVal; i++) {
|
||||||
@@ -75,11 +75,6 @@ namespace {
|
|||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nicht-blockierender Ersatz fuer beginConnect() zur Verwendung INNERHALB
|
|
||||||
// von update() (NetTask). Startet beim ersten Aufruf einen asynchronen
|
|
||||||
// Scan; bei weiteren Aufrufen wird nur kurz nachgeschaut, ob er fertig
|
|
||||||
// ist (WiFi.scanComplete() ist eine billige, sofort zurueckkehrende
|
|
||||||
// Abfrage) - blockiert also nie.
|
|
||||||
void tryReconnectAsync() {
|
void tryReconnectAsync() {
|
||||||
if (!reconnectScanPending) {
|
if (!reconnectScanPending) {
|
||||||
WiFi.scanNetworks(/*async=*/true);
|
WiFi.scanNetworks(/*async=*/true);
|
||||||
@@ -160,12 +155,6 @@ void beginConnect() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blockierender Scan (ca. 2-3s) - passiert nur einmal beim Booten, VOR
|
|
||||||
// dem Start von NetTask, daher unkritisch (kein Watchdog-Risiko). Wir
|
|
||||||
// verbinden uns mit dem ERSTEN gespeicherten Netzwerk, das gerade
|
|
||||||
// sichtbar ist (Prioritaet = Speicherreihenfolge), statt blind das erste
|
|
||||||
// gespeicherte zu versuchen - so klappt es automatisch mit Zuhause-WLAN
|
|
||||||
// ODER dem Auto-Hotspot, je nachdem was gerade in Reichweite ist.
|
|
||||||
int visibleCount = WiFi.scanNetworks(/*async=*/false);
|
int visibleCount = WiFi.scanNetworks(/*async=*/false);
|
||||||
|
|
||||||
int8_t chosen = -1;
|
int8_t chosen = -1;
|
||||||
@@ -209,10 +198,6 @@ void update() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Idle/Failed (auch nach einem fehlgeschlagenen Erstverbindungsversuch
|
|
||||||
// beim Booten) in regelmaessigen Abstaenden automatisch erneut versuchen,
|
|
||||||
// alle gespeicherten Netzwerke durchzuscannen - NICHT-blockierend (siehe
|
|
||||||
// tryReconnectAsync()), damit NetTask den Watchdog nicht verpasst.
|
|
||||||
if ((s == State::Idle || s == State::Failed) && networkCountVal > 0) {
|
if ((s == State::Idle || s == State::Failed) && networkCountVal > 0) {
|
||||||
if (reconnectScanPending) {
|
if (reconnectScanPending) {
|
||||||
tryReconnectAsync();
|
tryReconnectAsync();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "touch_input.h"
|
#include "touch_input.h"
|
||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
namespace WifiSetupScreen {
|
namespace WifiSetupScreen {
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ bool run(TFT_eSPI& tft) {
|
|||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.println("Scanning WiFi...");
|
tft.println(I18n::t(StringId::WIFI_SCANNING));
|
||||||
drawCancelButton(tft);
|
drawCancelButton(tft);
|
||||||
|
|
||||||
while (!skipped) {
|
while (!skipped) {
|
||||||
@@ -206,7 +207,7 @@ bool run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println(ssidCount == 0 ? "No networks found" : "Select WiFi:");
|
tft.println(ssidCount == 0 ? I18n::t(StringId::WIFI_NO_NETWORKS) : I18n::t(StringId::WIFI_SELECT));
|
||||||
drawCancelButton(tft);
|
drawCancelButton(tft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ bool run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println("Connected!");
|
tft.println(I18n::t(StringId::WIFI_CONNECTED_BANG));
|
||||||
delay(900);
|
delay(900);
|
||||||
return true;
|
return true;
|
||||||
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
||||||
@@ -227,10 +228,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("Connection failed.");
|
tft.println(I18n::t(StringId::WIFI_CONNECTION_FAILED));
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 30);
|
tft.setCursor(10, 30);
|
||||||
tft.println("Back to network list...");
|
tft.println(I18n::t(StringId::WIFI_BACK_TO_LIST_MSG));
|
||||||
delay(1400);
|
delay(1400);
|
||||||
stage = Stage::PickSsid;
|
stage = Stage::PickSsid;
|
||||||
WifiMgr::beginScan();
|
WifiMgr::beginScan();
|
||||||
@@ -292,10 +293,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("Already 3 networks saved.");
|
tft.println(I18n::t(StringId::WIFI_ALREADY_3));
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 30);
|
tft.setCursor(10, 30);
|
||||||
tft.println("Remove one first.");
|
tft.println(I18n::t(StringId::WIFI_REMOVE_ONE_FIRST));
|
||||||
delay(1600);
|
delay(1600);
|
||||||
skipped = true;
|
skipped = true;
|
||||||
break;
|
break;
|
||||||
@@ -305,7 +306,7 @@ bool run(TFT_eSPI& tft) {
|
|||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setCursor(10, 10);
|
tft.setCursor(10, 10);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.println("Connecting...");
|
tft.println(I18n::t(StringId::WIFI_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;
|
||||||
@@ -339,10 +340,10 @@ bool run(TFT_eSPI& tft) {
|
|||||||
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_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setCursor(10, 6);
|
tft.setCursor(10, 6);
|
||||||
tft.printf("WiFi: %s", ssidList[selectedIndex].c_str());
|
tft.printf("%s%s", I18n::t(StringId::WIFI_LABEL_PREFIX), 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("Password:");
|
tft.println(I18n::t(StringId::WIFI_PASSWORD_LABEL));
|
||||||
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);
|
||||||
@@ -367,9 +368,9 @@ 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, "Space");
|
drawButton(tft, spaceBtn, I18n::t(StringId::WIFI_SPACE));
|
||||||
drawButton(tft, connectBtn, "Connect");
|
drawButton(tft, connectBtn, I18n::t(StringId::WIFI_CONNECT));
|
||||||
drawButton(tft, backBtn, "Back to list");
|
drawButton(tft, backBtn, I18n::t(StringId::WIFI_BACK_TO_LIST_BTN));
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(20);
|
delay(20);
|
||||||
|
|||||||
Reference in New Issue
Block a user