v2.4.0: Night dimming, aircraft watchlist, and nearest-airport lookup
This commit is contained in:
115
src/aircraft_watchlist.cpp
Normal file
115
src/aircraft_watchlist.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include "aircraft_watchlist.h"
|
||||
#include "config.h"
|
||||
#include "sd_mutex.h"
|
||||
#include "sd_storage.h"
|
||||
#include <SD.h>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
|
||||
namespace AircraftWatchlist {
|
||||
|
||||
namespace {
|
||||
constexpr const char* WATCHED_FILE = "/Flightradar_cyd/watched_aircraft.txt";
|
||||
|
||||
char watched[MAX_WATCHED][9] = {{0}};
|
||||
uint8_t watchedCount = 0;
|
||||
|
||||
// Ueberspringt fuehrende Leerzeichen, uebernimmt bis zu 8 Zeichen und
|
||||
// bricht bei einem Leerzeichen ab (ADS-B-Rufzeichen haben oft Padding),
|
||||
// alles in Grossbuchstaben.
|
||||
void normalize(const char* callsign, char* out) {
|
||||
int j = 0;
|
||||
int i = 0;
|
||||
while (callsign[i] == ' ') i++;
|
||||
for (; j < 8 && callsign[i] && callsign[i] != ' '; i++, j++) {
|
||||
out[j] = (char)toupper((unsigned char)callsign[i]);
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
void saveToSd() {
|
||||
if (!SdStorage::isMounted()) return;
|
||||
SdMutex::Guard guard;
|
||||
|
||||
File f = SD.open(WATCHED_FILE, FILE_WRITE);
|
||||
if (!f) return;
|
||||
for (uint8_t i = 0; i < watchedCount; i++) {
|
||||
f.println(watched[i]);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
void loadFromSd() {
|
||||
watchedCount = 0;
|
||||
if (!SdStorage::isMounted()) return;
|
||||
SdMutex::Guard guard;
|
||||
|
||||
if (!SD.exists(WATCHED_FILE)) return;
|
||||
File f = SD.open(WATCHED_FILE, FILE_READ);
|
||||
if (!f) return;
|
||||
|
||||
while (f.available() && watchedCount < MAX_WATCHED) {
|
||||
String line = f.readStringUntil('\n');
|
||||
line.trim();
|
||||
if (line.length() == 0) continue;
|
||||
strncpy(watched[watchedCount], line.c_str(), 8);
|
||||
watched[watchedCount][8] = 0;
|
||||
watchedCount++;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
loadFromSd();
|
||||
}
|
||||
|
||||
uint8_t count() { return watchedCount; }
|
||||
|
||||
String callsignAt(uint8_t index) {
|
||||
if (index >= watchedCount) return String();
|
||||
return String(watched[index]);
|
||||
}
|
||||
|
||||
bool addWatched(const char* callsign) {
|
||||
if (watchedCount >= MAX_WATCHED) return false;
|
||||
if (!callsign || !callsign[0]) return false;
|
||||
|
||||
char normalized[9] = {0};
|
||||
normalize(callsign, normalized);
|
||||
if (!normalized[0]) return false;
|
||||
|
||||
for (uint8_t j = 0; j < watchedCount; j++) {
|
||||
if (strcmp(watched[j], normalized) == 0) return true;
|
||||
}
|
||||
|
||||
strncpy(watched[watchedCount], normalized, 8);
|
||||
watched[watchedCount][8] = 0;
|
||||
watchedCount++;
|
||||
saveToSd();
|
||||
return true;
|
||||
}
|
||||
|
||||
void removeWatched(uint8_t index) {
|
||||
if (index >= watchedCount) return;
|
||||
for (uint8_t i = index; i < watchedCount - 1; i++) {
|
||||
strncpy(watched[i], watched[i + 1], 9);
|
||||
}
|
||||
watchedCount--;
|
||||
watched[watchedCount][0] = 0;
|
||||
saveToSd();
|
||||
}
|
||||
|
||||
bool isWatched(const char* callsign) {
|
||||
if (watchedCount == 0) return false;
|
||||
char normalized[9];
|
||||
normalize(callsign, normalized);
|
||||
if (!normalized[0]) return false;
|
||||
|
||||
for (uint8_t i = 0; i < watchedCount; i++) {
|
||||
if (strcmp(watched[i], normalized) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
16
src/aircraft_watchlist.h
Normal file
16
src/aircraft_watchlist.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace AircraftWatchlist {
|
||||
constexpr uint8_t MAX_WATCHED = 5;
|
||||
|
||||
void init();
|
||||
|
||||
uint8_t count();
|
||||
String callsignAt(uint8_t index);
|
||||
|
||||
bool addWatched(const char* callsign);
|
||||
void removeWatched(uint8_t index);
|
||||
|
||||
bool isWatched(const char* callsign);
|
||||
}
|
||||
320
src/aircraft_watchlist_screen.cpp
Normal file
320
src/aircraft_watchlist_screen.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "aircraft_watchlist_screen.h"
|
||||
#include "aircraft_watchlist.h"
|
||||
#include "touch_input.h"
|
||||
#include "menu_stars.h"
|
||||
#include "config.h"
|
||||
#include "i18n.h"
|
||||
|
||||
namespace AircraftWatchlistScreen {
|
||||
|
||||
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, bool danger = false) {
|
||||
uint16_t accent = danger ? TFT_RED : TFT_GREEN;
|
||||
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_BLACK);
|
||||
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, accent);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(accent, TFT_BLACK);
|
||||
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
}
|
||||
|
||||
// Vier Tastaturreihen (Ziffern oben, dann QWERTYUIOP/ASDFGHJKL/ZXCVBNM),
|
||||
// da Rufzeichen wie "DLH441" auch Zahlen enthalten. Buffer 8 Zeichen +
|
||||
// Nullterminierung (volles Rufzeichen statt nur 3-stelligem ICAO-Praefix).
|
||||
String runCallsignKeypad(TFT_eSPI& tft) {
|
||||
MenuStars::reset();
|
||||
constexpr const char* DIGITS = "1234567890";
|
||||
constexpr const char* ROW1 = "QWERTYUIOP";
|
||||
constexpr const char* ROW2 = "ASDFGHJKL";
|
||||
constexpr const char* ROW3 = "ZXCVBNM";
|
||||
|
||||
char buf[9] = {0};
|
||||
uint8_t len = 0;
|
||||
|
||||
constexpr int16_t KEY_H = 30;
|
||||
constexpr int16_t KEY_GAP = 3;
|
||||
constexpr int16_t ROW0_Y = 78;
|
||||
|
||||
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 digitRects[10], row1Rects[10], row2Rects[9], row3Rects[7];
|
||||
layoutRow(DIGITS, ROW0_Y, digitRects, 10);
|
||||
layoutRow(ROW1, ROW0_Y + (KEY_H + KEY_GAP), row1Rects, 10);
|
||||
layoutRow(ROW2, ROW0_Y + 2 * (KEY_H + KEY_GAP), row2Rects, 9);
|
||||
layoutRow(ROW3, ROW0_Y + 3 * (KEY_H + KEY_GAP), row3Rects, 7);
|
||||
|
||||
Rect backspaceBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 100, KEY_H};
|
||||
Rect cancelBtn = {(int16_t)(Config::SCREEN_WIDTH - 100), (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 96, KEY_H};
|
||||
Rect confirmBtn = {4, (int16_t)(ROW0_Y + 5 * (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_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_ADD_TITLE));
|
||||
|
||||
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK);
|
||||
tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_GREEN);
|
||||
tft.setTextSize(2);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(14, 66);
|
||||
tft.print(buf);
|
||||
tft.setTextSize(1);
|
||||
|
||||
for (uint8_t i = 0; i < 10; i++) drawButton(tft, digitRects[i], String(DIGITS[i]));
|
||||
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), true);
|
||||
drawButton(tft, confirmBtn, I18n::t(StringId::ADD));
|
||||
};
|
||||
|
||||
redraw();
|
||||
|
||||
while (!done) {
|
||||
TouchInput::Point tap;
|
||||
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
|
||||
|
||||
bool handled = false;
|
||||
for (uint8_t i = 0; i < 10 && !handled; i++) {
|
||||
if (digitRects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = DIGITS[i]; handled = true; }
|
||||
}
|
||||
for (uint8_t i = 0; i < 10 && !handled; i++) {
|
||||
if (row1Rects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = ROW1[i]; handled = true; }
|
||||
}
|
||||
for (uint8_t i = 0; i < 9 && !handled; i++) {
|
||||
if (row2Rects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = ROW2[i]; handled = true; }
|
||||
}
|
||||
for (uint8_t i = 0; i < 7 && !handled; i++) {
|
||||
if (row3Rects[i].contains(tap.x, tap.y) && len < 8) { 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();
|
||||
}
|
||||
|
||||
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
|
||||
int16_t lineHeight, const String& text, int16_t scrollY,
|
||||
int16_t viewTop, int16_t viewBottom, bool draw) {
|
||||
int16_t y = startY;
|
||||
int32_t start = 0;
|
||||
int32_t len = text.length();
|
||||
while (start < len) {
|
||||
while (start < len && text[start] == ' ') start++;
|
||||
if (start >= len) break;
|
||||
|
||||
String line = text.substring(start, len);
|
||||
while (tft.textWidth(line) > maxWidth) {
|
||||
int32_t lastSpace = line.lastIndexOf(' ');
|
||||
if (lastSpace <= 0) break;
|
||||
line = line.substring(0, lastSpace);
|
||||
}
|
||||
|
||||
if (draw) {
|
||||
int16_t screenY = y - scrollY;
|
||||
if (screenY >= viewTop && screenY <= viewBottom) {
|
||||
tft.setCursor(x, screenY);
|
||||
tft.print(line);
|
||||
}
|
||||
}
|
||||
y += lineHeight;
|
||||
start += line.length();
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
void runInfoScreen(TFT_eSPI& tft) {
|
||||
MenuStars::reset();
|
||||
|
||||
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
|
||||
constexpr int16_t LINE_H = 16;
|
||||
constexpr int16_t VIEW_TOP = 36;
|
||||
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
|
||||
|
||||
int16_t totalH = VIEW_TOP;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA1), 0, 0, 0, false);
|
||||
totalH += 8;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA2), 0, 0, 0, false);
|
||||
|
||||
int16_t maxScroll = totalH - VIEW_BOTTOM;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
bool scrollable = maxScroll > 0;
|
||||
int16_t scrollY = 0;
|
||||
|
||||
Rect backBtn = scrollable
|
||||
? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
|
||||
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
Rect upBtn = {146, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
constexpr int16_t SCROLL_STEP = 48;
|
||||
|
||||
auto redraw = [&]() {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_INFO_TITLE));
|
||||
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
int16_t y = VIEW_TOP;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||
if (scrollable) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
};
|
||||
|
||||
redraw();
|
||||
|
||||
while (true) {
|
||||
TouchInput::Point tap;
|
||||
if (TouchInput::wasTapped(tap)) {
|
||||
if (backBtn.contains(tap.x, tap.y)) return;
|
||||
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
|
||||
scrollY -= SCROLL_STEP;
|
||||
if (scrollY < 0) scrollY = 0;
|
||||
redraw();
|
||||
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
|
||||
scrollY += SCROLL_STEP;
|
||||
if (scrollY > maxScroll) scrollY = maxScroll;
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
MenuStars::update(tft);
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
MenuStars::reset();
|
||||
while (!done) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_TITLE));
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 26);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_DESC1));
|
||||
tft.setCursor(10, 38);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_DESC2));
|
||||
|
||||
Rect infoBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
|
||||
drawButton(tft, infoBtn, "?");
|
||||
|
||||
uint8_t count = AircraftWatchlist::count();
|
||||
int16_t y = 56;
|
||||
|
||||
Rect rowRects[AircraftWatchlist::MAX_WATCHED];
|
||||
Rect removeRects[AircraftWatchlist::MAX_WATCHED];
|
||||
|
||||
if (count == 0) {
|
||||
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
tft.setCursor(10, y + 14);
|
||||
tft.println(I18n::t(StringId::WATCHLIST_EMPTY));
|
||||
y += ROW_H;
|
||||
}
|
||||
|
||||
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_BLACK);
|
||||
tft.drawRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_GREEN);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.drawString(AircraftWatchlist::callsignAt(i), rowRect.x + rowRect.w / 2, rowRect.y + rowRect.h / 2);
|
||||
tft.setTextDatum(TL_DATUM);
|
||||
drawButton(tft, removeRect, "X", true);
|
||||
|
||||
y += ROW_H + ROW_GAP;
|
||||
}
|
||||
|
||||
Rect addBtn = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
bool canAdd = count < AircraftWatchlist::MAX_WATCHED;
|
||||
if (canAdd) {
|
||||
drawButton(tft, addBtn, I18n::t(StringId::WATCHLIST_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;
|
||||
MenuStars::update(tft);
|
||||
delay(20);
|
||||
}
|
||||
|
||||
bool handled = false;
|
||||
if (infoBtn.contains(tap.x, tap.y)) {
|
||||
runInfoScreen(tft);
|
||||
handled = true;
|
||||
}
|
||||
for (uint8_t i = 0; i < count && !handled; i++) {
|
||||
if (removeRects[i].contains(tap.x, tap.y)) {
|
||||
AircraftWatchlist::removeWatched(i);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) {
|
||||
String callsign = runCallsignKeypad(tft);
|
||||
if (callsign.length() > 0) {
|
||||
AircraftWatchlist::addWatched(callsign.c_str());
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
if (!handled && backBtn.contains(tap.x, tap.y)) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
7
src/aircraft_watchlist_screen.h
Normal file
7
src/aircraft_watchlist_screen.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace AircraftWatchlistScreen {
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
51
src/airport_lookup.cpp
Normal file
51
src/airport_lookup.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "airport_lookup.h"
|
||||
#include "sd_storage.h"
|
||||
#include "config.h"
|
||||
#include "radar_math.h"
|
||||
#include <SD.h>
|
||||
|
||||
namespace AirportLookup {
|
||||
|
||||
Nearest findNearest(double lat, double lon) {
|
||||
Nearest result;
|
||||
if (!SdStorage::isMounted()) return result;
|
||||
|
||||
File f = SD.open(Config::SD_AIRPORTS_CSV);
|
||||
if (!f) return result;
|
||||
|
||||
f.readStringUntil('\n');
|
||||
|
||||
float bestDistanceKm = 0;
|
||||
while (f.available()) {
|
||||
String line = f.readStringUntil('\n');
|
||||
line.trim();
|
||||
if (line.length() == 0) continue;
|
||||
|
||||
int c1 = line.indexOf(',');
|
||||
if (c1 < 0) continue;
|
||||
int c2 = line.indexOf(',', c1 + 1);
|
||||
if (c2 < 0) continue;
|
||||
int c3 = line.indexOf(',', c2 + 1);
|
||||
if (c3 < 0) continue;
|
||||
|
||||
String icao = line.substring(0, c1);
|
||||
String name = line.substring(c1 + 1, c2);
|
||||
double aptLat = line.substring(c2 + 1, c3).toDouble();
|
||||
double aptLon = line.substring(c3 + 1).toDouble();
|
||||
|
||||
float distanceKm = RadarMath::toPolar(lat, lon, aptLat, aptLon).distanceKm;
|
||||
|
||||
if (!result.found || distanceKm < bestDistanceKm) {
|
||||
bestDistanceKm = distanceKm;
|
||||
result.found = true;
|
||||
result.distanceKm = distanceKm;
|
||||
strncpy(result.icao, icao.c_str(), sizeof(result.icao) - 1);
|
||||
strncpy(result.name, name.c_str(), sizeof(result.name) - 1);
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
12
src/airport_lookup.h
Normal file
12
src/airport_lookup.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace AirportLookup {
|
||||
struct Nearest {
|
||||
bool found = false;
|
||||
char icao[5] = {0};
|
||||
char name[32] = {0};
|
||||
float distanceKm = 0;
|
||||
};
|
||||
Nearest findNearest(double lat, double lon);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ namespace Config {
|
||||
constexpr const char* SD_ROOT_DIR = "/Flightradar_cyd";
|
||||
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
|
||||
constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv";
|
||||
constexpr const char* SD_AIRPORTS_CSV = "/Flightradar_cyd/airports.csv";
|
||||
constexpr const char* SD_LOG_DIR = "/Flightradar_cyd/logs";
|
||||
constexpr const char* SD_SCREENSHOT_DIR = "/Flightradar_cyd/screenshots";
|
||||
constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt";
|
||||
|
||||
15
src/i18n.h
15
src/i18n.h
@@ -56,6 +56,7 @@ enum class StringId : uint8_t {
|
||||
MENU_MANAGE_WIFI,
|
||||
MENU_LOCATION_PRESETS,
|
||||
MENU_SCREEN_TIMEOUT_PREFIX,
|
||||
MENU_NIGHT_DIMMING,
|
||||
MENU_STATISTICS,
|
||||
MENU_LOGBOOK_FILES,
|
||||
MENU_MORE_SETTINGS,
|
||||
@@ -65,6 +66,8 @@ enum class StringId : uint8_t {
|
||||
MENU_FLIGHT_LOGBOOK,
|
||||
MENU_HIDE_GROUND,
|
||||
MENU_AIRLINE_FILTER,
|
||||
MENU_WATCHLIST,
|
||||
MENU_WATCHLIST_ALERT,
|
||||
MENU_BACKUP,
|
||||
MENU_RESTORE,
|
||||
MENU_BACKUP_SAVED,
|
||||
@@ -80,6 +83,16 @@ enum class StringId : uint8_t {
|
||||
AIRLINE_FILTER_ADD,
|
||||
AIRLINE_ADD_TITLE,
|
||||
|
||||
WATCHLIST_TITLE,
|
||||
WATCHLIST_DESC1,
|
||||
WATCHLIST_DESC2,
|
||||
WATCHLIST_ADD,
|
||||
WATCHLIST_ADD_TITLE,
|
||||
WATCHLIST_EMPTY,
|
||||
WATCHLIST_INFO_TITLE,
|
||||
WATCHLIST_INFO_PARA1,
|
||||
WATCHLIST_INFO_PARA2,
|
||||
|
||||
LOCATION_TITLE,
|
||||
LOCATION_AUTO,
|
||||
LOCATION_PRESET,
|
||||
@@ -87,10 +100,12 @@ enum class StringId : uint8_t {
|
||||
LOCATION_ADD,
|
||||
LOCATION_LAT_PROMPT,
|
||||
LOCATION_LON_PROMPT,
|
||||
LOCATION_NEAREST_AIRPORT_PREFIX,
|
||||
LOCATION_INFO_TITLE,
|
||||
LOCATION_INFO_PARA1,
|
||||
LOCATION_INFO_PARA2,
|
||||
LOCATION_INFO_PARA3,
|
||||
LOCATION_INFO_PARA4,
|
||||
|
||||
STATS_TITLE,
|
||||
STATS_TODAY,
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_DE[] = {
|
||||
"WLAN-Netzwerke verwalten",
|
||||
"Standort-Presets",
|
||||
"Bildschirm-Timeout: ",
|
||||
"Nachtdimmung (22-6 Uhr): ",
|
||||
"Statistik",
|
||||
"Logbuch-Dateien",
|
||||
"Weitere Einstellungen >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_DE[] = {
|
||||
"Flugbuch: ",
|
||||
"Bodenfahrzeuge ausblenden: ",
|
||||
"Airline-Filter",
|
||||
"Beobachtungsliste",
|
||||
"Beobachtungs-Alarm: ",
|
||||
"Einstellungen sichern",
|
||||
"Einstellungen wiederherstellen",
|
||||
"Backup gespeichert.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_DE[] = {
|
||||
"Airline hinzufügen",
|
||||
"Airline hinzufügen (ICAO-Code)",
|
||||
|
||||
"Beobachtete Flugzeuge",
|
||||
"Blaue LED blinkt, sobald eines",
|
||||
"dieser Flugzeuge sichtbar ist.",
|
||||
"Flugzeug hinzufügen",
|
||||
"Flugzeug hinzufügen (Rufzeichen)",
|
||||
"Noch keine Flugzeuge beobachtet.",
|
||||
"Wie die Beobachtungsliste funktioniert",
|
||||
"Trage das exakte Rufzeichen eines bestimmten Flugs ein (z.B. DLH441) - keinen Airline-Code wie beim Airline-Filter. Bis zu 5 Flugzeuge lassen sich gleichzeitig beobachten.",
|
||||
"Sobald ein beobachtetes Flugzeug irgendwo im Radar auftaucht, bekommt es einen cyanfarbenen Ring und die LED blinkt blau - praktisch, um einen bestimmten Flug zu erkennen, z.B. wenn jemand Bekanntes ankommt.",
|
||||
|
||||
"Standort-Presets",
|
||||
"Automatisch (GPS/IP)",
|
||||
"Preset",
|
||||
@@ -87,10 +100,12 @@ static const char* const I18N_DE[] = {
|
||||
"Preset hinzufügen",
|
||||
"Breitengrad (z.B. 48.6833)",
|
||||
"Längengrad (z.B. 9.8333)",
|
||||
"Nächster Flughafen: ",
|
||||
"Wie Presets funktionieren",
|
||||
"Nur EIN Standort ist zur selben Zeit aktiv - entweder Auto oder genau ein Preset. Tippe einen Eintrag an, um ihn zu aktivieren.",
|
||||
"Du kannst auch die Koordinaten eines beliebigen anderen Ortes eintragen - z.B. Mailand, New York oder Tokio - und siehst DORT den aktuellen Flugverkehr an, egal wo dein Gerät tatsächlich steht!",
|
||||
"Praktisch auch für mehr Genauigkeit als die automatische IP-Standortbestimmung, z.B. wenn das Gerät dauerhaft an einem festen Ort steht.",
|
||||
"Dieser Screen zeigt außerdem den nächstgelegenen Flughafen zum jeweils aktiven Standort an - praktisch, um genau zu sehen, wo ein fremder Preset eigentlich liegt.",
|
||||
|
||||
"Statistik",
|
||||
"Heute geloggte Flugzeuge",
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_EN[] = {
|
||||
"Manage WiFi networks",
|
||||
"Location presets",
|
||||
"Screen timeout: ",
|
||||
"Night dimming (10pm-6am): ",
|
||||
"Statistics",
|
||||
"Logbook files",
|
||||
"More settings >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_EN[] = {
|
||||
"Flight logbook: ",
|
||||
"Hide ground vehicles: ",
|
||||
"Airline filter",
|
||||
"Watchlist",
|
||||
"Watchlist alert: ",
|
||||
"Backup settings",
|
||||
"Restore settings",
|
||||
"Backup saved.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_EN[] = {
|
||||
"Add airline",
|
||||
"Add airline (ICAO code)",
|
||||
|
||||
"Watched aircraft",
|
||||
"Get notified (blue LED) when any",
|
||||
"of these aircraft appears.",
|
||||
"Add aircraft",
|
||||
"Add aircraft (callsign)",
|
||||
"No aircraft watched yet.",
|
||||
"How the watchlist works",
|
||||
"Enter the exact callsign of a specific flight (e.g. DLH441) - not an airline code like the Airline Filter. Up to 5 aircraft can be tracked at once.",
|
||||
"As soon as a watched aircraft appears anywhere on the radar, it gets a cyan ring and the LED blinks blue - great for spotting a specific flight, e.g. when someone you know is arriving.",
|
||||
|
||||
"Location presets",
|
||||
"Auto (GPS/IP)",
|
||||
"Preset",
|
||||
@@ -87,11 +100,13 @@ static const char* const I18N_EN[] = {
|
||||
"Add preset",
|
||||
"Latitude (e.g. 48.6833)",
|
||||
"Longitude (e.g. 9.8333)",
|
||||
"Nearest airport: ",
|
||||
|
||||
"How presets work",
|
||||
"Only ONE location is active at a time - either Auto or exactly one preset. Tap an entry to activate it.",
|
||||
"You can also enter the coordinates of any other place - e.g. Milan, New York or Tokyo - and see the live air traffic THERE, no matter where your device actually is!",
|
||||
"Also handy for more precision than automatic IP-based location, e.g. when the device stays in one fixed spot.",
|
||||
"This screen also shows the nearest airport to whichever location is currently active - handy to see exactly where a foreign preset points to.",
|
||||
|
||||
"Statistics",
|
||||
"Aircraft logged today",
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_ES[] = {
|
||||
"Gestionar redes WiFi",
|
||||
"Ubicaciones guardadas",
|
||||
"Apagado de pantalla: ",
|
||||
"Atenuación nocturna (22-6h): ",
|
||||
"Estadísticas",
|
||||
"Archivos del registro",
|
||||
"Más ajustes >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_ES[] = {
|
||||
"Registro de vuelos: ",
|
||||
"Ocultar vehículos terrestres: ",
|
||||
"Filtro de aerolíneas",
|
||||
"Lista de seguimiento",
|
||||
"Alerta de seguimiento: ",
|
||||
"Copia de seguridad",
|
||||
"Restaurar ajustes",
|
||||
"Copia guardada.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_ES[] = {
|
||||
"Añadir aerolínea",
|
||||
"Añadir aerolínea (código OACI)",
|
||||
|
||||
"Aviones vigilados",
|
||||
"El LED azul parpadea cuando",
|
||||
"aparece uno de estos aviones.",
|
||||
"Añadir avión",
|
||||
"Añadir avión (indicativo)",
|
||||
"Aún no hay aviones vigilados.",
|
||||
"Cómo funciona la lista de seguimiento",
|
||||
"Introduzca el indicativo exacto de un vuelo especifico (ej. DLH441) - no un codigo de aerolinea como en el Filtro de aerolineas. Se pueden seguir hasta 5 aviones a la vez.",
|
||||
"En cuanto aparece un avion vigilado en cualquier parte del radar, recibe un anillo cian y el LED parpadea en azul - genial para detectar un vuelo especifico, por ejemplo cuando llega alguien conocido.",
|
||||
|
||||
"Ubicaciones guardadas",
|
||||
"Auto (GPS/IP)",
|
||||
"Ubicación",
|
||||
@@ -87,10 +100,12 @@ static const char* const I18N_ES[] = {
|
||||
"Añadir ubicación",
|
||||
"Latitud (ej. 48.6833)",
|
||||
"Longitud (ej. 9.8333)",
|
||||
"Aeropuerto más cercano: ",
|
||||
"Como funcionan los presets",
|
||||
"Solo UNA ubicacion esta activa a la vez - Auto o exactamente un preset. Toque una entrada para activarla.",
|
||||
"Tambien puede introducir las coordenadas de cualquier otro lugar - ej. Milan, Nueva York o Tokio - y ver el trafico aereo en tiempo real ALLI, sin importar donde este su dispositivo!",
|
||||
"Tambien es util para mas precision que la geolocalizacion automatica por IP, por ejemplo si el dispositivo permanece siempre en un lugar fijo.",
|
||||
"Esta pantalla también muestra el aeropuerto más cercano a la ubicación activa - útil para ver exactamente dónde apunta un preset lejano.",
|
||||
|
||||
"Estadísticas",
|
||||
"Aviones registrados hoy",
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_FR[] = {
|
||||
"Gérer les réseaux WiFi",
|
||||
"Positions enregistrées",
|
||||
"Extinction écran : ",
|
||||
"Assombrissement nuit (22h-6h) : ",
|
||||
"Statistiques",
|
||||
"Fichiers du journal",
|
||||
"Plus de réglages >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_FR[] = {
|
||||
"Journal de vol : ",
|
||||
"Masquer véhicules au sol : ",
|
||||
"Filtre compagnies",
|
||||
"Liste de surveillance",
|
||||
"Alerte de surveillance : ",
|
||||
"Sauvegarder réglages",
|
||||
"Restaurer réglages",
|
||||
"Sauvegarde enregistrée.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_FR[] = {
|
||||
"Ajouter une compagnie",
|
||||
"Ajouter (code OACI)",
|
||||
|
||||
"Avions surveillés",
|
||||
"La LED bleue clignote des qu'un",
|
||||
"de ces avions apparait.",
|
||||
"Ajouter un avion",
|
||||
"Ajouter un avion (indicatif)",
|
||||
"Aucun avion surveille pour l'instant.",
|
||||
"Comment fonctionne la liste de surveillance",
|
||||
"Entrez l'indicatif exact d'un vol precis (ex. DLH441) - pas un code compagnie comme pour le filtre. Jusqu'a 5 avions peuvent etre suivis a la fois.",
|
||||
"Des qu'un avion surveille apparait quelque part sur le radar, il recoit un anneau cyan et la LED clignote en bleu - pratique pour reperer un vol precis, par exemple l'arrivee de quelqu'un que vous connaissez.",
|
||||
|
||||
"Positions enregistrées",
|
||||
"Auto (GPS/IP)",
|
||||
"Position",
|
||||
@@ -87,10 +100,12 @@ static const char* const I18N_FR[] = {
|
||||
"Ajouter une position",
|
||||
"Latitude (ex. 48.6833)",
|
||||
"Longitude (ex. 9.8333)",
|
||||
"Aéroport le plus proche : ",
|
||||
"Comment marchent les presets",
|
||||
"Un SEUL lieu est actif a la fois - soit Auto, soit un preset precis. Touchez une entree pour l'activer.",
|
||||
"Vous pouvez aussi entrer les coordonnees de n'importe quel autre lieu - ex. Milan, New York ou Tokyo - et voir le trafic aerien LA-BAS, peu importe ou se trouve votre appareil !",
|
||||
"Pratique aussi pour plus de precision que la geolocalisation IP automatique, par exemple si l'appareil reste toujours au meme endroit.",
|
||||
"Cet écran affiche aussi l'aéroport le plus proche de l'emplacement actif - pratique pour voir exactement où pointe un preset lointain.",
|
||||
|
||||
"Statistiques",
|
||||
"Avions enregistrés aujourd'hui",
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_IT[] = {
|
||||
"Gestisci reti WiFi",
|
||||
"Posizioni salvate",
|
||||
"Spegnimento schermo: ",
|
||||
"Attenuazione notte (22-6): ",
|
||||
"Statistiche",
|
||||
"File del registro",
|
||||
"Altre impostazioni >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_IT[] = {
|
||||
"Diario di volo: ",
|
||||
"Nascondi veicoli a terra: ",
|
||||
"Filtro compagnie",
|
||||
"Lista di controllo",
|
||||
"Allarme di controllo: ",
|
||||
"Backup impostazioni",
|
||||
"Ripristina impostazioni",
|
||||
"Backup salvato.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_IT[] = {
|
||||
"Aggiungi compagnia",
|
||||
"Aggiungi compagnia (codice ICAO)",
|
||||
|
||||
"Aerei monitorati",
|
||||
"Il LED blu lampeggia quando",
|
||||
"appare uno di questi aerei.",
|
||||
"Aggiungi aereo",
|
||||
"Aggiungi aereo (nominativo)",
|
||||
"Ancora nessun aereo monitorato.",
|
||||
"Come funziona la lista di controllo",
|
||||
"Inserisci il nominativo esatto di un volo specifico (es. DLH441) - non un codice compagnia come nel Filtro compagnie. Puoi monitorare fino a 5 aerei contemporaneamente.",
|
||||
"Non appena un aereo monitorato appare ovunque sul radar, riceve un anello ciano e il LED lampeggia in blu - utile per individuare un volo specifico, ad esempio l'arrivo di una persona conosciuta.",
|
||||
|
||||
"Posizioni salvate",
|
||||
"Auto (GPS/IP)",
|
||||
"Posizione",
|
||||
@@ -87,10 +100,12 @@ static const char* const I18N_IT[] = {
|
||||
"Aggiungi posizione",
|
||||
"Latitudine (es. 48.6833)",
|
||||
"Longitudine (es. 9.8333)",
|
||||
"Aeroporto più vicino: ",
|
||||
"Come funzionano i preset",
|
||||
"Solo UNA posizione e attiva alla volta - Auto oppure esattamente un preset. Tocca una voce per attivarla.",
|
||||
"Puoi anche inserire le coordinate di qualsiasi altro luogo - es. Milano, New York o Tokyo - e vedere il traffico aereo in tempo reale LI, indipendentemente da dove si trovi il dispositivo!",
|
||||
"Utile anche per maggiore precisione rispetto alla geolocalizzazione IP automatica, ad esempio se il dispositivo resta sempre in un punto fisso.",
|
||||
"Questa schermata mostra anche l'aeroporto più vicino alla posizione attiva - utile per vedere esattamente dove punta un preset lontano.",
|
||||
|
||||
"Statistiche",
|
||||
"Aerei registrati oggi",
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char* const I18N_TR[] = {
|
||||
"WiFi ağlarını yönet",
|
||||
"Konum ön ayarları",
|
||||
"Ekran zaman aşımı: ",
|
||||
"Gece kısma (22-06): ",
|
||||
"İstatistikler",
|
||||
"Kayıt defteri dosyaları",
|
||||
"Diğer ayarlar >",
|
||||
@@ -65,6 +66,8 @@ static const char* const I18N_TR[] = {
|
||||
"Uçuş kayıt defteri: ",
|
||||
"Kara araçlarını gizle: ",
|
||||
"Havayolu filtresi",
|
||||
"İzleme listesi",
|
||||
"İzleme uyarısı: ",
|
||||
"Ayarları yedekle",
|
||||
"Ayarları geri yükle",
|
||||
"Yedekleme kaydedildi.",
|
||||
@@ -80,6 +83,16 @@ static const char* const I18N_TR[] = {
|
||||
"Havayolu ekle",
|
||||
"Havayolu ekle (ICAO kodu)",
|
||||
|
||||
"İzlenen uçaklar",
|
||||
"Bu uçaklardan biri gorundugunde",
|
||||
"mavi LED yanip soner.",
|
||||
"Uçak ekle",
|
||||
"Uçak ekle (çağrı işareti)",
|
||||
"Henüz izlenen uçak yok.",
|
||||
"İzleme listesi nasıl çalışır",
|
||||
"Belirli bir ucusun tam cagri isaretini girin (orn. DLH441) - Havayolu Filtresi'ndeki gibi bir havayolu kodu degil. Ayni anda 5 adete kadar ucak izlenebilir.",
|
||||
"Izlenen bir ucak radarda herhangi bir yerde gorundugunde, cevresinde camgobegi bir halka belirir ve LED mavi yanip soner - taniidik birinin varisi gibi belirli bir ucusu fark etmek icin harika.",
|
||||
|
||||
"Konum ön ayarları",
|
||||
"Otomatik (GPS/IP)",
|
||||
"Ön ayar",
|
||||
@@ -87,10 +100,12 @@ static const char* const I18N_TR[] = {
|
||||
"Ön ayar ekle",
|
||||
"Enlem (örn. 48.6833)",
|
||||
"Boylam (örn. 9.8333)",
|
||||
"En yakın havalimanı: ",
|
||||
"On ayarlar nasil calisir",
|
||||
"Aynı anda SADECE bir konum aktiftir - Otomatik veya tam olarak bir on ayar. Aktif etmek icin bir girise dokunun.",
|
||||
"Ayrica baska herhangi bir yerin koordinatlarini da girebilirsiniz - orn. Milano, New York veya Tokyo - ve cihazinizin gercek konumundan bagimsiz olarak ORADAKI canli hava trafigini gorebilirsiniz!",
|
||||
"Cihaz surekli sabit bir yerde kaliyorsa, otomatik IP tabanli konumdan daha hassas olmasi icin de kullanislidir.",
|
||||
"Bu ekran ayrıca o an aktif olan konuma en yakın havalimanını da gösterir - uzak bir on ayarın tam olarak nereye işaret ettiğini görmek için kullanışlıdır.",
|
||||
|
||||
"İstatistikler",
|
||||
"Bugün kaydedilen uçaklar",
|
||||
|
||||
@@ -68,16 +68,18 @@ bool update(Mode mode, uint32_t nowMs) {
|
||||
}
|
||||
|
||||
uint32_t interval = (mode == Mode::EmergencyRed) ? RED_BLINK_INTERVAL_MS : GREEN_BLINK_INTERVAL_MS;
|
||||
uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED : PIN_GREEN;
|
||||
uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED
|
||||
: (mode == Mode::WatchlistBlue) ? PIN_BLUE
|
||||
: PIN_GREEN;
|
||||
|
||||
if (nowMs - lastToggleMs >= interval) {
|
||||
lastToggleMs = nowMs;
|
||||
blinkState = !blinkState;
|
||||
}
|
||||
|
||||
digitalWrite(pin, blinkState ? LOW : HIGH);
|
||||
digitalWrite(mode == Mode::EmergencyRed ? PIN_GREEN : PIN_RED, HIGH);
|
||||
digitalWrite(PIN_BLUE, HIGH);
|
||||
digitalWrite(PIN_RED, (pin == PIN_RED && blinkState) ? LOW : HIGH);
|
||||
digitalWrite(PIN_GREEN, (pin == PIN_GREEN && blinkState) ? LOW : HIGH);
|
||||
digitalWrite(PIN_BLUE, (pin == PIN_BLUE && blinkState) ? LOW : HIGH);
|
||||
|
||||
return blinkState;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace LedAlert {
|
||||
enum class Mode {
|
||||
Off,
|
||||
ProximityGreen,
|
||||
WatchlistBlue,
|
||||
EmergencyRed,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "location_presets_screen.h"
|
||||
#include "location_presets.h"
|
||||
#include "location_manager.h"
|
||||
#include "airport_lookup.h"
|
||||
#include "touch_input.h"
|
||||
#include "menu_stars.h"
|
||||
#include "config.h"
|
||||
@@ -170,8 +172,7 @@ namespace {
|
||||
|
||||
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
|
||||
constexpr int16_t LINE_H = 16;
|
||||
constexpr int16_t VIEW_TOP = 40;
|
||||
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
constexpr int16_t VIEW_TOP = 36;
|
||||
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
|
||||
|
||||
int16_t totalH = VIEW_TOP;
|
||||
@@ -180,14 +181,19 @@ namespace {
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), 0, 0, 0, false);
|
||||
totalH += 8;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), 0, 0, 0, false);
|
||||
totalH += 8;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), 0, 0, 0, false);
|
||||
|
||||
int16_t maxScroll = totalH - VIEW_BOTTOM;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
bool scrollable = maxScroll > 0;
|
||||
int16_t scrollY = 0;
|
||||
|
||||
Rect upBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 4, 30, 24};
|
||||
Rect downBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 30, 30, 24};
|
||||
Rect backBtn = scrollable
|
||||
? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
|
||||
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
Rect upBtn = {146, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
constexpr int16_t SCROLL_STEP = 48;
|
||||
|
||||
auto redraw = [&]() {
|
||||
@@ -196,20 +202,21 @@ namespace {
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::LOCATION_INFO_TITLE));
|
||||
|
||||
if (scrollable) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
int16_t y = VIEW_TOP;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||
if (scrollable) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
};
|
||||
|
||||
redraw();
|
||||
@@ -296,6 +303,25 @@ void run(TFT_eSPI& tft) {
|
||||
drawButton(tft, addBtn, I18n::t(StringId::LOCATION_ADD));
|
||||
}
|
||||
|
||||
{
|
||||
double activeLat = 0, activeLon = 0;
|
||||
if (active < 0) {
|
||||
LocationManager::getHomeLocation(activeLat, activeLon);
|
||||
} else {
|
||||
LocationPresets::getLatLon((uint8_t)active, activeLat, activeLon);
|
||||
}
|
||||
AirportLookup::Nearest nearest = AirportLookup::findNearest(activeLat, activeLon);
|
||||
if (nearest.found) {
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s %s (%.0f km)", nearest.icao, nearest.name, nearest.distanceKm);
|
||||
String line = String(I18n::t(StringId::LOCATION_NEAREST_AIRPORT_PREFIX)) + buf;
|
||||
line = truncateForWidth(tft, line, (int16_t)(Config::SCREEN_WIDTH - 20));
|
||||
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
|
||||
tft.setCursor(10, (int16_t)(Config::SCREEN_HEIGHT - 60));
|
||||
tft.print(line);
|
||||
}
|
||||
}
|
||||
|
||||
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||
|
||||
|
||||
36
src/main.cpp
36
src/main.cpp
@@ -11,6 +11,7 @@
|
||||
#include "aircraft_table.h"
|
||||
#include "airline_lookup.h"
|
||||
#include "airline_filter.h"
|
||||
#include "aircraft_watchlist.h"
|
||||
#include "sd_storage.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "location_manager.h"
|
||||
@@ -49,10 +50,12 @@ bool wasEmergency = false;
|
||||
bool bannerBlinkOn = false;
|
||||
|
||||
constexpr uint8_t BACKLIGHT_FULL = 255;
|
||||
constexpr uint8_t BACKLIGHT_NIGHT_DIM = 90;
|
||||
constexpr uint8_t BACKLIGHT_DIMMED = 12;
|
||||
constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0;
|
||||
uint32_t lastInteractionMs = 0;
|
||||
bool screenDimmed = false;
|
||||
bool nightDimActive = false;
|
||||
|
||||
struct Rect {
|
||||
int16_t x, y, w, h;
|
||||
@@ -146,6 +149,33 @@ void updateStatusLine() {
|
||||
updateWifiIcon();
|
||||
}
|
||||
|
||||
// Prueft, ob die aktuelle Lokalzeit im Nachtdimm-Fenster (22:00-06:00) liegt.
|
||||
// Ueber Mitternacht hinweg gerechnet. Solange die Uhrzeit noch nicht per NTP
|
||||
// synchronisiert ist, wird false zurueckgegeben statt zu raten (gleiche
|
||||
// Pruefung wie in updateStatusLine()).
|
||||
bool isNightDimHours() {
|
||||
time_t now = time(nullptr);
|
||||
if (now <= 8 * 3600 * 2) return false;
|
||||
|
||||
struct tm tmNow;
|
||||
localtime_r(&now, &tmNow);
|
||||
int hour = tmNow.tm_hour;
|
||||
return (hour >= 22 || hour < 6);
|
||||
}
|
||||
|
||||
// Sanfte Nachtdimmung des Backlights zwischen 22:00 und 06:00 Uhr, sofern in
|
||||
// den Einstellungen aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat
|
||||
// Vorrang und wird hier nicht ueberschrieben.
|
||||
void updateNightDimming() {
|
||||
if (screenDimmed) return;
|
||||
|
||||
bool shouldDim = SettingsStore::nightDimmingEnabled() && isNightDimHours();
|
||||
if (shouldDim != nightDimActive) {
|
||||
ledcWrite(BACKLIGHT_PWM_CHANNEL, shouldDim ? BACKLIGHT_NIGHT_DIM : BACKLIGHT_FULL);
|
||||
nightDimActive = shouldDim;
|
||||
}
|
||||
}
|
||||
|
||||
void takeScreenshotWithFeedback() {
|
||||
LedAlert::flashWhite();
|
||||
|
||||
@@ -240,6 +270,7 @@ void setup() {
|
||||
WifiMgr::init();
|
||||
LocationPresets::init();
|
||||
AirlineFilter::init();
|
||||
AircraftWatchlist::init();
|
||||
|
||||
SplashScreen::begin(tft);
|
||||
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
||||
@@ -314,7 +345,9 @@ void loop() {
|
||||
lastInteractionMs = millis();
|
||||
|
||||
if (screenDimmed) {
|
||||
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
|
||||
bool shouldNightDim = SettingsStore::nightDimmingEnabled() && isNightDimHours();
|
||||
ledcWrite(BACKLIGHT_PWM_CHANNEL, shouldNightDim ? BACKLIGHT_NIGHT_DIM : BACKLIGHT_FULL);
|
||||
nightDimActive = shouldNightDim;
|
||||
screenDimmed = false;
|
||||
tapped = false;
|
||||
}
|
||||
@@ -360,6 +393,7 @@ void loop() {
|
||||
if (nowMs - lastStatusLineMs >= STATUS_LINE_UPDATE_MS) {
|
||||
lastStatusLineMs = nowMs;
|
||||
updateStatusLine();
|
||||
updateNightDimming();
|
||||
}
|
||||
|
||||
uint8_t timeoutMin = SettingsStore::screenTimeoutMinutes();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "logbook_files_screen.h"
|
||||
#include "location_presets_screen.h"
|
||||
#include "airline_filter_screen.h"
|
||||
#include "aircraft_watchlist_screen.h"
|
||||
#include "language_screen.h"
|
||||
#include "units_screen.h"
|
||||
#include "settings_backup.h"
|
||||
@@ -24,8 +25,8 @@ namespace {
|
||||
}
|
||||
};
|
||||
|
||||
constexpr int16_t ROW_H = 24;
|
||||
constexpr int16_t ROW_GAP = 2;
|
||||
constexpr int16_t ROW_H = 22;
|
||||
constexpr int16_t ROW_GAP = 1;
|
||||
constexpr int16_t ROW_START_Y = 18;
|
||||
|
||||
Rect rowRect(uint8_t index) {
|
||||
@@ -152,12 +153,13 @@ void run(TFT_eSPI& tft) {
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM));
|
||||
|
||||
Rect calibBtn = rowRect(0);
|
||||
Rect invertBtn = rowRect(1);
|
||||
Rect timeoutBtn = rowRect(2);
|
||||
Rect backupBtn = rowRect(3);
|
||||
Rect restoreBtn = rowRect(4);
|
||||
Rect backBtn = rowRect(5);
|
||||
Rect calibBtn = rowRect(0);
|
||||
Rect invertBtn = rowRect(1);
|
||||
Rect timeoutBtn = rowRect(2);
|
||||
Rect nightDimBtn = rowRect(3);
|
||||
Rect backupBtn = rowRect(4);
|
||||
Rect restoreBtn = rowRect(5);
|
||||
Rect backBtn = rowRect(6);
|
||||
|
||||
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
|
||||
|
||||
@@ -167,6 +169,7 @@ void run(TFT_eSPI& tft) {
|
||||
drawButton(tft, invertBtn, invertLabel);
|
||||
|
||||
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
|
||||
drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled()));
|
||||
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
|
||||
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
||||
@@ -188,6 +191,8 @@ void run(TFT_eSPI& tft) {
|
||||
uint8_t current = SettingsStore::screenTimeoutMinutes();
|
||||
uint8_t next = (current >= 10) ? 0 : (current + 1);
|
||||
SettingsStore::setScreenTimeoutMinutes(next);
|
||||
} else if (nightDimBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
|
||||
} 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),
|
||||
@@ -215,8 +220,10 @@ void run(TFT_eSPI& tft) {
|
||||
Rect emergencyBtn = rowRect(5);
|
||||
Rect locationBtn = rowRect(6);
|
||||
Rect airlineBtn = rowRect(7);
|
||||
Rect groundBtn = rowRect(8);
|
||||
Rect backBtn = rowRect(9);
|
||||
Rect watchlistBtn = rowRect(8);
|
||||
Rect watchlistAlertBtn = rowRect(9);
|
||||
Rect groundBtn = rowRect(10);
|
||||
Rect backBtn = rowRect(11);
|
||||
|
||||
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
|
||||
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
|
||||
@@ -226,6 +233,8 @@ void run(TFT_eSPI& tft) {
|
||||
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
|
||||
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
|
||||
drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER));
|
||||
drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
|
||||
drawButton(tft, watchlistAlertBtn, I18n::t(StringId::MENU_WATCHLIST_ALERT) + onOff(SettingsStore::watchlistAlertEnabled()));
|
||||
drawButton(tft, groundBtn, I18n::t(StringId::MENU_HIDE_GROUND) + onOff(SettingsStore::hideGroundVehicles()));
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
||||
|
||||
@@ -252,6 +261,10 @@ void run(TFT_eSPI& tft) {
|
||||
LocationPresetsScreen::run(tft);
|
||||
} else if (airlineBtn.contains(tap.x, tap.y)) {
|
||||
AirlineFilterScreen::run(tft);
|
||||
} else if (watchlistBtn.contains(tap.x, tap.y)) {
|
||||
AircraftWatchlistScreen::run(tft);
|
||||
} else if (watchlistAlertBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setWatchlistAlertEnabled(!SettingsStore::watchlistAlertEnabled());
|
||||
} else if (groundBtn.contains(tap.x, tap.y)) {
|
||||
SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles());
|
||||
} else if (backBtn.contains(tap.x, tap.y)) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "location_manager.h"
|
||||
#include "world_map.h"
|
||||
#include "airline_filter.h"
|
||||
#include "aircraft_watchlist.h"
|
||||
#include "i18n.h"
|
||||
#include <math.h>
|
||||
|
||||
@@ -56,6 +57,7 @@ namespace {
|
||||
float headingDeg;
|
||||
float distanceKm;
|
||||
bool isEmergency;
|
||||
bool isWatched;
|
||||
};
|
||||
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
||||
HitPoint hitPoints[MAX_HIT_POINTS];
|
||||
@@ -487,6 +489,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
uint16_t color = colorForAltitude(a.altBaroFt);
|
||||
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
||||
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
||||
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
|
||||
|
||||
TrailEntry* trail = findOrCreateTrail(a.hex);
|
||||
uint16_t trailColor = dimColorForAltitude(a.altBaroFt);
|
||||
@@ -503,6 +506,8 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
|
||||
if (isEmergency) {
|
||||
tft.drawCircle(pt.x, pt.y, 12, TFT_RED);
|
||||
} else if (isWatched) {
|
||||
tft.drawCircle(pt.x, pt.y, 12, TFT_CYAN);
|
||||
}
|
||||
|
||||
tft.setTextColor(color);
|
||||
@@ -518,6 +523,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
||||
hitPoints[i].headingDeg = a.headingDeg;
|
||||
hitPoints[i].distanceKm = a.distanceKm;
|
||||
hitPoints[i].isEmergency = isEmergency;
|
||||
hitPoints[i].isWatched = isWatched;
|
||||
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
||||
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
|
||||
}
|
||||
@@ -602,6 +608,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
||||
|
||||
if (hp.isEmergency) {
|
||||
tft.drawCircle(hp.x, hp.y, 12, TFT_RED);
|
||||
} else if (hp.isWatched) {
|
||||
tft.drawCircle(hp.x, hp.y, 12, TFT_CYAN);
|
||||
}
|
||||
|
||||
tft.setTextColor(hp.color);
|
||||
@@ -671,22 +679,26 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
|
||||
void updateProximityAlert(uint32_t nowMs) {
|
||||
bool anyClose = false;
|
||||
bool anyEmergency = false;
|
||||
bool anyWatched = false;
|
||||
|
||||
bool proximityOn = SettingsStore::proximityAlertEnabled();
|
||||
bool emergencyOn = SettingsStore::emergencyAlertEnabled();
|
||||
bool watchlistOn = SettingsStore::watchlistAlertEnabled();
|
||||
|
||||
if (proximityOn || emergencyOn) {
|
||||
if (proximityOn || emergencyOn || watchlistOn) {
|
||||
AircraftTable::lock();
|
||||
Aircraft* table = AircraftTable::raw();
|
||||
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
|
||||
if (!table[i].valid) continue;
|
||||
if (proximityOn && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) anyClose = true;
|
||||
if (emergencyOn && isEmergencySquawk(table[i].squawk)) anyEmergency = true;
|
||||
if (watchlistOn && AircraftWatchlist::isWatched(table[i].callsign)) anyWatched = true;
|
||||
}
|
||||
AircraftTable::unlock();
|
||||
}
|
||||
|
||||
LedAlert::Mode mode = anyEmergency ? LedAlert::Mode::EmergencyRed
|
||||
: anyWatched ? LedAlert::Mode::WatchlistBlue
|
||||
: anyClose ? LedAlert::Mode::ProximityGreen
|
||||
: LedAlert::Mode::Off;
|
||||
|
||||
|
||||
@@ -50,6 +50,43 @@ namespace {
|
||||
"CRJ9,90\n"
|
||||
"DH8D,78\n";
|
||||
|
||||
const char* kDefaultAirportsCsv =
|
||||
"icao,name,lat,lon\n"
|
||||
"EDDF,Frankfurt,50.0379,8.5622\n"
|
||||
"EDDM,Muenchen,48.3538,11.7861\n"
|
||||
"EDDB,Berlin Brandenburg,52.3667,13.5033\n"
|
||||
"EDDL,Duesseldorf,51.2895,6.7668\n"
|
||||
"EDDH,Hamburg,53.6304,9.9882\n"
|
||||
"EDDS,Stuttgart,48.6900,9.2219\n"
|
||||
"LFPG,Paris CDG,49.0097,2.5479\n"
|
||||
"EGLL,London Heathrow,51.4700,-0.4543\n"
|
||||
"LEMD,Madrid Barajas,40.4936,-3.5668\n"
|
||||
"LIRF,Roma Fiumicino,41.8003,12.2389\n"
|
||||
"EHAM,Amsterdam Schiphol,52.3086,4.7639\n"
|
||||
"LSZH,Zuerich,47.4647,8.5492\n"
|
||||
"LOWW,Wien,48.1103,16.5697\n"
|
||||
"EKCH,Kopenhagen,55.6180,12.6560\n"
|
||||
"ESSA,Stockholm Arlanda,59.6519,17.9186\n"
|
||||
"LTFM,Istanbul,41.2753,28.7519\n"
|
||||
"OMDB,Dubai,25.2532,55.3657\n"
|
||||
"OTHH,Doha Hamad,25.2609,51.6138\n"
|
||||
"KJFK,New York JFK,40.6413,-73.7781\n"
|
||||
"KLAX,Los Angeles,33.9416,-118.4085\n"
|
||||
"KORD,Chicago O'Hare,41.9742,-87.9073\n"
|
||||
"CYYZ,Toronto Pearson,43.6777,-79.6248\n"
|
||||
"RJTT,Tokyo Haneda,35.5494,139.7798\n"
|
||||
"RJAA,Tokyo Narita,35.7647,140.3864\n"
|
||||
"ZBAA,Beijing Capital,40.0801,116.5846\n"
|
||||
"VHHH,Hong Kong,22.3080,113.9185\n"
|
||||
"WSSS,Singapore Changi,1.3644,103.9915\n"
|
||||
"YSSY,Sydney,-33.9399,151.1753\n"
|
||||
"FAOR,Johannesburg OR Tambo,-26.1392,28.2460\n"
|
||||
"SBGR,Sao Paulo Guarulhos,-23.4356,-46.4731\n"
|
||||
"OMAA,Abu Dhabi,24.4330,54.6511\n"
|
||||
"LGAV,Athen,37.9364,23.9445\n"
|
||||
"LPPT,Lissabon,38.7756,-9.1354\n"
|
||||
"EPWA,Warschau Chopin,52.1657,20.9671\n";
|
||||
|
||||
bool ensureDir(const char* path) {
|
||||
if (SD.exists(path)) return true;
|
||||
return SD.mkdir(path);
|
||||
@@ -86,6 +123,7 @@ void seedDefaultDataFiles() {
|
||||
SdMutex::Guard guard;
|
||||
writeIfAbsent(Config::SD_AIRLINES_CSV, kDefaultAirlinesCsv);
|
||||
writeIfAbsent(Config::SD_AIRCRAFT_TYPES_CSV, kDefaultAircraftTypesCsv);
|
||||
writeIfAbsent(Config::SD_AIRPORTS_CSV, kDefaultAirportsCsv);
|
||||
}
|
||||
|
||||
void logEvent(const char* csvLine) {
|
||||
|
||||
@@ -10,9 +10,11 @@ namespace {
|
||||
bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp)
|
||||
bool emergencyAlertOn = true;
|
||||
bool proximityAlertOn = true;
|
||||
bool watchlistAlertOn = true;
|
||||
bool flightLogbookOn = true;
|
||||
bool ledHeartbeatOn = true;
|
||||
uint8_t screenTimeoutMin = 0;
|
||||
bool nightDimmingOn = true;
|
||||
bool hideGroundVehiclesOn = true;
|
||||
uint8_t languageIdx = 0;
|
||||
uint8_t unitsModeVal = 0;
|
||||
@@ -29,6 +31,8 @@ namespace {
|
||||
emergencyAlertOn = (value.toInt() != 0);
|
||||
} else if (key == "proximity_alert") {
|
||||
proximityAlertOn = (value.toInt() != 0);
|
||||
} else if (key == "watchlist_alert") {
|
||||
watchlistAlertOn = (value.toInt() != 0);
|
||||
} else if (key == "flight_logbook") {
|
||||
flightLogbookOn = (value.toInt() != 0);
|
||||
} else if (key == "led_heartbeat") {
|
||||
@@ -36,6 +40,8 @@ namespace {
|
||||
} else if (key == "screen_timeout_min") {
|
||||
int v = value.toInt();
|
||||
if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v;
|
||||
} else if (key == "night_dimming") {
|
||||
nightDimmingOn = (value.toInt() != 0);
|
||||
} else if (key == "hide_ground_vehicles") {
|
||||
hideGroundVehiclesOn = (value.toInt() != 0);
|
||||
} else if (key == "language") {
|
||||
@@ -82,9 +88,11 @@ void save() {
|
||||
f.printf("invert=%d\n", inverted ? 1 : 0);
|
||||
f.printf("emergency_alert=%d\n", emergencyAlertOn ? 1 : 0);
|
||||
f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0);
|
||||
f.printf("watchlist_alert=%d\n", watchlistAlertOn ? 1 : 0);
|
||||
f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0);
|
||||
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
|
||||
f.printf("screen_timeout_min=%d\n", screenTimeoutMin);
|
||||
f.printf("night_dimming=%d\n", nightDimmingOn ? 1 : 0);
|
||||
f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0);
|
||||
f.printf("language=%d\n", languageIdx);
|
||||
f.printf("units_mode=%d\n", unitsModeVal);
|
||||
@@ -121,6 +129,13 @@ void setProximityAlertEnabled(bool on) {
|
||||
save();
|
||||
}
|
||||
|
||||
bool watchlistAlertEnabled() { return watchlistAlertOn; }
|
||||
|
||||
void setWatchlistAlertEnabled(bool on) {
|
||||
watchlistAlertOn = on;
|
||||
save();
|
||||
}
|
||||
|
||||
bool flightLogbookEnabled() { return flightLogbookOn; }
|
||||
|
||||
void setFlightLogbookEnabled(bool on) {
|
||||
@@ -144,6 +159,13 @@ void setScreenTimeoutMinutes(uint8_t minutes) {
|
||||
}
|
||||
}
|
||||
|
||||
bool nightDimmingEnabled() { return nightDimmingOn; }
|
||||
|
||||
void setNightDimmingEnabled(bool on) {
|
||||
nightDimmingOn = on;
|
||||
save();
|
||||
}
|
||||
|
||||
bool hideGroundVehicles() { return hideGroundVehiclesOn; }
|
||||
|
||||
void setHideGroundVehicles(bool on) {
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace SettingsStore {
|
||||
bool proximityAlertEnabled();
|
||||
void setProximityAlertEnabled(bool on);
|
||||
|
||||
bool watchlistAlertEnabled();
|
||||
void setWatchlistAlertEnabled(bool on);
|
||||
|
||||
bool flightLogbookEnabled();
|
||||
void setFlightLogbookEnabled(bool on);
|
||||
|
||||
@@ -26,6 +29,9 @@ namespace SettingsStore {
|
||||
uint8_t screenTimeoutMinutes();
|
||||
void setScreenTimeoutMinutes(uint8_t minutes);
|
||||
|
||||
bool nightDimmingEnabled();
|
||||
void setNightDimmingEnabled(bool on);
|
||||
|
||||
bool hideGroundVehicles();
|
||||
void setHideGroundVehicles(bool on);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "splash_screen.h"
|
||||
#include "menu_stars.h"
|
||||
#include <math.h>
|
||||
|
||||
namespace SplashScreen {
|
||||
|
||||
@@ -20,17 +21,74 @@ namespace {
|
||||
tft.drawFastVLine(cx, cy - 80, 160, dim);
|
||||
}
|
||||
|
||||
void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) {
|
||||
tft.drawLine(x, y - 10, x, y + 12, color);
|
||||
tft.drawLine(x - 12, y + 2, x + 12, y + 2, color);
|
||||
tft.drawLine(x - 5, y + 10, x + 5, y + 10, color);
|
||||
// Dreht einen Punkt (px, py) im lokalen Koordinatensystem der kleinen
|
||||
// Deko-Symbole um angleDeg und verschiebt ihn nach (x, y).
|
||||
void rotatePoint(int16_t x, int16_t y, float sinA, float cosA,
|
||||
float px, float py, int16_t& outX, int16_t& outY) {
|
||||
outX = x + (int16_t)lroundf(px * cosA - py * sinA);
|
||||
outY = y + (int16_t)lroundf(px * sinA + py * cosA);
|
||||
}
|
||||
|
||||
void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) {
|
||||
tft.drawLine(x - 8, y - 6, x + 8, y - 6, color);
|
||||
tft.drawLine(x, y - 6, x, y + 4, color);
|
||||
tft.drawCircle(x, y + 2, 4, color);
|
||||
tft.drawLine(x - 4, y + 7, x + 4, y + 7, color);
|
||||
// Kleines Flugzeug-Silhouette aus gefuellten Dreiecken (Rumpf, Tragflaechen,
|
||||
// Leitwerk), analog zum grossen Flugzeug in der Mitte, aber schlichter und
|
||||
// in beliebiger Richtung (angleDeg, 0 = Nase zeigt nach oben).
|
||||
void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
|
||||
float rad = angleDeg * (PI / 180.0f);
|
||||
float s = sinf(rad), c = cosf(rad);
|
||||
|
||||
int16_t nx, ny, flx, fly, frx, fry;
|
||||
rotatePoint(x, y, s, c, 0, -11, nx, ny);
|
||||
rotatePoint(x, y, s, c, -3, 11, flx, fly);
|
||||
rotatePoint(x, y, s, c, 3, 11, frx, fry);
|
||||
tft.fillTriangle(nx, ny, flx, fly, frx, fry, color);
|
||||
|
||||
int16_t wlx, wly, wrx, wry, wcx, wcy;
|
||||
rotatePoint(x, y, s, c, -13, 3, wlx, wly);
|
||||
rotatePoint(x, y, s, c, 13, 3, wrx, wry);
|
||||
rotatePoint(x, y, s, c, 0, -3, wcx, wcy);
|
||||
tft.fillTriangle(wlx, wly, wrx, wry, wcx, wcy, color);
|
||||
|
||||
int16_t tlx, tly, trx, try_, ttx, tty;
|
||||
rotatePoint(x, y, s, c, -5, 8, tlx, tly);
|
||||
rotatePoint(x, y, s, c, 5, 8, trx, try_);
|
||||
rotatePoint(x, y, s, c, 0, 13, ttx, tty);
|
||||
tft.fillTriangle(tlx, tly, trx, try_, ttx, tty, color);
|
||||
}
|
||||
|
||||
// Kleiner Helikopter: Rotorkreis mit Blaettern, gefuellter Rumpf, Heckausleger
|
||||
// mit Leitwerk. angleDeg bestimmt die Flugrichtung (0 = Nase nach oben).
|
||||
void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
|
||||
float rad = angleDeg * (PI / 180.0f);
|
||||
float s = sinf(rad), c = cosf(rad);
|
||||
|
||||
int16_t rcx, rcy;
|
||||
rotatePoint(x, y, s, c, 0, -7, rcx, rcy);
|
||||
tft.drawCircle(rcx, rcy, 6, color);
|
||||
|
||||
int16_t b1x, b1y, b2x, b2y;
|
||||
rotatePoint(x, y, s, c, -7, -7, b1x, b1y);
|
||||
rotatePoint(x, y, s, c, 7, -7, b2x, b2y);
|
||||
tft.drawLine(b1x, b1y, b2x, b2y, color);
|
||||
rotatePoint(x, y, s, c, 0, -13, b1x, b1y);
|
||||
rotatePoint(x, y, s, c, 0, -1, b2x, b2y);
|
||||
tft.drawLine(b1x, b1y, b2x, b2y, color);
|
||||
|
||||
int16_t cx1, cy1, cx2, cy2, cx3, cy3;
|
||||
rotatePoint(x, y, s, c, 0, -6, cx1, cy1);
|
||||
rotatePoint(x, y, s, c, -4, 4, cx2, cy2);
|
||||
rotatePoint(x, y, s, c, 4, 4, cx3, cy3);
|
||||
tft.fillTriangle(cx1, cy1, cx2, cy2, cx3, cy3, color);
|
||||
|
||||
int16_t tbx1, tby1, tbx2, tby2;
|
||||
rotatePoint(x, y, s, c, 0, 4, tbx1, tby1);
|
||||
rotatePoint(x, y, s, c, 0, 12, tbx2, tby2);
|
||||
tft.drawLine(tbx1, tby1, tbx2, tby2, color);
|
||||
|
||||
int16_t fx1, fy1, fx2, fy2, fx3, fy3;
|
||||
rotatePoint(x, y, s, c, -3, 10, fx1, fy1);
|
||||
rotatePoint(x, y, s, c, 3, 10, fx2, fy2);
|
||||
rotatePoint(x, y, s, c, 0, 15, fx3, fy3);
|
||||
tft.fillTriangle(fx1, fy1, fx2, fy2, fx3, fy3, color);
|
||||
}
|
||||
|
||||
void drawAirplane(TFT_eSPI& tft, int16_t cx) {
|
||||
@@ -55,10 +113,10 @@ void begin(TFT_eSPI& tft) {
|
||||
drawRadarReticle(tft, cx, 174);
|
||||
|
||||
uint16_t dimGreen = 0x0320;
|
||||
drawMiniJet(tft, cx - 45, 135, dimGreen);
|
||||
drawMiniJet(tft, cx + 50, 160, dimGreen);
|
||||
drawMiniHeli(tft, cx - 35, 215, dimGreen);
|
||||
drawMiniHeli(tft, cx + 45, 115, dimGreen);
|
||||
drawMiniJet(tft, cx - 45, 135, dimGreen, 25.0f);
|
||||
drawMiniJet(tft, cx + 50, 160, dimGreen, 160.0f);
|
||||
drawMiniHeli(tft, cx - 35, 215, dimGreen, 250.0f);
|
||||
drawMiniHeli(tft, cx + 45, 115, dimGreen, 100.0f);
|
||||
|
||||
drawAirplane(tft, cx);
|
||||
|
||||
|
||||
@@ -61,8 +61,7 @@ namespace {
|
||||
|
||||
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
|
||||
constexpr int16_t LINE_H = 16;
|
||||
constexpr int16_t VIEW_TOP = 40;
|
||||
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
constexpr int16_t VIEW_TOP = 36;
|
||||
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
|
||||
|
||||
int16_t totalH = VIEW_TOP;
|
||||
@@ -77,8 +76,11 @@ namespace {
|
||||
bool scrollable = maxScroll > 0;
|
||||
int16_t scrollY = 0;
|
||||
|
||||
Rect upBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 4, 30, 24};
|
||||
Rect downBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 30, 30, 24};
|
||||
Rect backBtn = scrollable
|
||||
? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
|
||||
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
Rect upBtn = {146, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
constexpr int16_t SCROLL_STEP = 48;
|
||||
|
||||
auto redraw = [&]() {
|
||||
@@ -87,11 +89,6 @@ namespace {
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::WIFI_INFO_TITLE));
|
||||
|
||||
if (scrollable) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
int16_t y = VIEW_TOP;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
@@ -101,6 +98,10 @@ namespace {
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||
if (scrollable) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
};
|
||||
|
||||
redraw();
|
||||
|
||||
Reference in New Issue
Block a user