373 lines
11 KiB
C++
373 lines
11 KiB
C++
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
#include <TFT_eSPI.h>
|
|
#include <SD.h>
|
|
#include <WiFi.h>
|
|
#include <time.h>
|
|
|
|
#include "config.h"
|
|
#include "aircraft.h"
|
|
#include "radar_math.h"
|
|
#include "aircraft_table.h"
|
|
#include "airline_lookup.h"
|
|
#include "airline_filter.h"
|
|
#include "sd_storage.h"
|
|
#include "wifi_manager.h"
|
|
#include "location_manager.h"
|
|
#include "location_presets.h"
|
|
#include "adsb_client.h"
|
|
#include "touch_input.h"
|
|
#include "calibration_screen.h"
|
|
#include "wifi_setup_screen.h"
|
|
#include "menu_screen.h"
|
|
#include "settings_store.h"
|
|
#include "net_task.h"
|
|
#include "radar_screen.h"
|
|
#include "splash_screen.h"
|
|
#include "led_alert.h"
|
|
#include "flight_logbook.h"
|
|
#include "screenshot.h"
|
|
#include "i18n.h"
|
|
#include "first_run_language_screen.h"
|
|
#include "menu_stars.h"
|
|
#include "ui_font.h"
|
|
|
|
TFT_eSPI tft = TFT_eSPI();
|
|
|
|
constexpr int16_t HEADER_TITLE_H = 30;
|
|
constexpr int16_t STATUS_LINE_H = 12;
|
|
constexpr int16_t CONTENT_TOP = HEADER_TITLE_H + STATUS_LINE_H;
|
|
constexpr uint32_t POLL_INTERVAL_MS = 300;
|
|
constexpr uint32_t SWEEP_TICK_MS = 80;
|
|
constexpr uint32_t STATUS_LINE_UPDATE_MS = 1000;
|
|
uint32_t lastPollMs = 0;
|
|
uint32_t lastSweepMs = 0;
|
|
uint32_t lastStatusLineMs = 0;
|
|
uint32_t lastRenderedVersion = 0xFFFFFFFF;
|
|
bool forceRedraw = false;
|
|
bool wasEmergency = 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 {
|
|
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;
|
|
}
|
|
};
|
|
|
|
Rect menuBtn = {Config::SCREEN_WIDTH - 90, 3, 54, 22};
|
|
Rect camBtn = {(int16_t)(menuBtn.x - 46), 3, 42, 22};
|
|
|
|
void drawMenuButton() {
|
|
tft.fillRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_BLACK);
|
|
tft.drawRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_GREEN);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.drawString("Menu", menuBtn.x + menuBtn.w / 2, menuBtn.y + menuBtn.h / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
}
|
|
|
|
void drawCamButton() {
|
|
tft.fillRoundRect(camBtn.x, camBtn.y, camBtn.w, camBtn.h, 4, TFT_BLACK);
|
|
tft.drawRoundRect(camBtn.x, camBtn.y, camBtn.w, camBtn.h, 4, TFT_GREEN);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.drawString("Cam", camBtn.x + camBtn.w / 2, camBtn.y + camBtn.h / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
}
|
|
|
|
void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) {
|
|
uint8_t level;
|
|
if (rssi >= -55) level = 4;
|
|
else if (rssi >= -65) level = 3;
|
|
else if (rssi >= -75) level = 2;
|
|
else if (rssi >= -85) level = 1;
|
|
else level = 0;
|
|
|
|
constexpr uint8_t BAR_W = 3;
|
|
constexpr uint8_t BAR_GAP = 2;
|
|
constexpr uint8_t BAR_COUNT = 4;
|
|
int16_t baseline = rowTop + rowH - 1;
|
|
int16_t x = rightX - BAR_COUNT * (BAR_W + BAR_GAP);
|
|
|
|
for (uint8_t i = 0; i < BAR_COUNT; i++) {
|
|
int16_t barH = 3 + i * 2;
|
|
uint16_t color = (i < level) ? TFT_GREEN : TFT_DARKGREY;
|
|
tft.fillRect(x, baseline - barH, BAR_W, barH, color);
|
|
x += BAR_W + BAR_GAP;
|
|
}
|
|
}
|
|
|
|
constexpr int16_t WIFI_ICON_W = 22;
|
|
constexpr int16_t WIFI_ICON_H = 14;
|
|
|
|
void updateWifiIcon() {
|
|
int16_t iconX = Config::SCREEN_WIDTH - WIFI_ICON_W - 2;
|
|
int16_t iconY = 2;
|
|
tft.fillRect(iconX, iconY, WIFI_ICON_W, WIFI_ICON_H, TFT_BLACK);
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
drawWifiIcon(Config::SCREEN_WIDTH - 4, iconY, WIFI_ICON_H, WiFi.RSSI());
|
|
}
|
|
}
|
|
|
|
void drawHeader() {
|
|
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_BLACK);
|
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
|
tft.setTextSize(1);
|
|
tft.setCursor(6, 10);
|
|
tft.println("Eiswolfs FR");
|
|
drawCamButton();
|
|
drawMenuButton();
|
|
updateWifiIcon();
|
|
}
|
|
|
|
void updateStatusLine() {
|
|
if (wasEmergency) return;
|
|
|
|
tft.fillRect(0, HEADER_TITLE_H, Config::SCREEN_WIDTH, STATUS_LINE_H, TFT_BLACK);
|
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
|
|
|
time_t now = time(nullptr);
|
|
if (now > 8 * 3600 * 2) {
|
|
struct tm tmNow;
|
|
localtime_r(&now, &tmNow);
|
|
char timeBuf[6];
|
|
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
|
|
tft.setCursor(6, HEADER_TITLE_H + 2);
|
|
tft.print(timeBuf);
|
|
}
|
|
|
|
updateWifiIcon();
|
|
}
|
|
|
|
void takeScreenshotWithFeedback() {
|
|
LedAlert::flashWhite();
|
|
|
|
String filename = Screenshot::save(tft);
|
|
|
|
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_NAVY);
|
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
|
tft.setCursor(6, 10);
|
|
if (filename.length() > 0) {
|
|
tft.print(String(I18n::t(StringId::SCREENSHOT_SAVED_PREFIX)) + filename);
|
|
} else {
|
|
tft.setTextColor(TFT_RED, TFT_NAVY);
|
|
tft.print(I18n::t(StringId::SCREENSHOT_FAILED));
|
|
}
|
|
delay(1200);
|
|
drawHeader();
|
|
updateStatusLine();
|
|
}
|
|
|
|
void haltWithSdRequiredScreen() {
|
|
tft.invertDisplay(true);
|
|
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setTextDatum(MC_DATUM);
|
|
int16_t cx = Config::SCREEN_WIDTH / 2;
|
|
int16_t cy = Config::SCREEN_HEIGHT / 2;
|
|
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setTextSize(2);
|
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE1), cx, cy - 40);
|
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE2), cx, cy - 10);
|
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE3), cx, cy + 20);
|
|
|
|
tft.setTextSize(1);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.drawString(I18n::t(StringId::SD_REQUIRED_HINT), cx, cy + 60);
|
|
tft.setTextDatum(TL_DATUM);
|
|
|
|
while (true) {
|
|
delay(1000);
|
|
}
|
|
}
|
|
|
|
void updateEmergencyBanner(uint32_t nowMs) {
|
|
RadarScreen::EmergencyInfo emergency = RadarScreen::checkEmergency();
|
|
|
|
if (emergency.active) {
|
|
bannerBlinkOn = !bannerBlinkOn;
|
|
uint16_t bg = bannerBlinkOn ? TFT_RED : TFT_BLACK;
|
|
tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, bg);
|
|
tft.setTextColor(TFT_WHITE, bg);
|
|
tft.setTextSize(1);
|
|
tft.setCursor(4, 10);
|
|
tft.printf("EMERGENCY %s %s", emergency.squawk, emergency.callsign);
|
|
wasEmergency = true;
|
|
} else if (wasEmergency) {
|
|
wasEmergency = false;
|
|
drawHeader();
|
|
updateStatusLine();
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(300);
|
|
|
|
tft.init();
|
|
tft.setRotation(0);
|
|
tft.setFreeFont(&UiFont11pt);
|
|
tft.invertDisplay(true);
|
|
tft.fillScreen(TFT_BLACK);
|
|
|
|
ledcSetup(BACKLIGHT_PWM_CHANNEL, 5000, 8);
|
|
ledcAttachPin(TFT_BL, BACKLIGHT_PWM_CHANNEL);
|
|
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
|
|
|
|
TouchInput::begin();
|
|
LedAlert::begin();
|
|
|
|
bool sdOk = SdStorage::init();
|
|
if (!sdOk) {
|
|
haltWithSdRequiredScreen();
|
|
return;
|
|
}
|
|
SdStorage::seedDefaultDataFiles();
|
|
|
|
bool isFirstRun = !SD.exists(Config::SD_SETTINGS_FILE);
|
|
|
|
SettingsStore::load();
|
|
tft.invertDisplay(SettingsStore::displayInverted());
|
|
|
|
WifiMgr::init();
|
|
LocationPresets::init();
|
|
AirlineFilter::init();
|
|
|
|
SplashScreen::begin(tft);
|
|
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
|
|
|
if (!TouchInput::loadCalibration()) {
|
|
CalibrationScreen::run(tft);
|
|
}
|
|
|
|
if (WifiMgr::networkCount() == 0) {
|
|
WifiSetupScreen::run(tft);
|
|
} else {
|
|
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_CONNECTING_WIFI));
|
|
WifiMgr::beginConnect();
|
|
|
|
uint32_t waitStart = millis();
|
|
while (WifiMgr::getState() == WifiMgr::State::Connecting && millis() - waitStart < 16000) {
|
|
WifiMgr::update();
|
|
MenuStars::update(tft);
|
|
delay(100);
|
|
}
|
|
|
|
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
|
SplashScreen::setStatusLine(tft, 1, String(I18n::t(StringId::SPLASH_WIFI_OK_PREFIX)) + WifiMgr::getIP());
|
|
} else {
|
|
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_WIFI_FAILED), TFT_RED);
|
|
}
|
|
}
|
|
|
|
if (isFirstRun) {
|
|
FirstRunLanguageScreen::run(tft);
|
|
SplashScreen::begin(tft);
|
|
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
|
}
|
|
|
|
AdsbClient::primeTime();
|
|
|
|
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_GETTING_LOCATION));
|
|
LocationManager::init();
|
|
uint32_t locStart = millis();
|
|
while (LocationManager::currentSource() == LocationManager::Source::None &&
|
|
millis() - locStart < 8000) {
|
|
LocationManager::requestIpLookupIfNeeded();
|
|
MenuStars::update(tft);
|
|
delay(200);
|
|
}
|
|
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
|
|
|
|
if (LocationManager::hasUtcOffset()) {
|
|
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
|
|
}
|
|
|
|
AircraftTable::init();
|
|
AirlineLookup::init();
|
|
FlightLogbook::init();
|
|
|
|
NetTask::begin();
|
|
|
|
SplashScreen::waitRemaining(tft);
|
|
|
|
drawHeader();
|
|
updateStatusLine();
|
|
RadarScreen::render(tft, CONTENT_TOP);
|
|
|
|
lastInteractionMs = millis();
|
|
}
|
|
|
|
void loop() {
|
|
TouchInput::Point 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)) {
|
|
MenuScreen::run(tft);
|
|
drawHeader();
|
|
updateStatusLine();
|
|
forceRedraw = true;
|
|
} else if (camBtn.contains(tap.x, tap.y)) {
|
|
takeScreenshotWithFeedback();
|
|
} else if (tap.y >= CONTENT_TOP) {
|
|
if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) {
|
|
forceRedraw = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) {
|
|
lastPollMs = millis();
|
|
uint32_t currentVersion = AircraftTable::version();
|
|
if (forceRedraw || currentVersion != lastRenderedVersion) {
|
|
lastRenderedVersion = currentVersion;
|
|
forceRedraw = false;
|
|
RadarScreen::render(tft, CONTENT_TOP);
|
|
lastSweepMs = millis();
|
|
}
|
|
}
|
|
|
|
uint32_t nowMs = millis();
|
|
|
|
RadarScreen::updateProximityAlert(nowMs);
|
|
|
|
if (nowMs - lastSweepMs >= SWEEP_TICK_MS) {
|
|
uint32_t deltaMs = nowMs - lastSweepMs;
|
|
lastSweepMs = nowMs;
|
|
RadarScreen::tick(tft, CONTENT_TOP, deltaMs);
|
|
updateEmergencyBanner(nowMs);
|
|
}
|
|
|
|
if (nowMs - lastStatusLineMs >= STATUS_LINE_UPDATE_MS) {
|
|
lastStatusLineMs = nowMs;
|
|
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;
|
|
}
|
|
}
|
|
} |