From 56293cf3c3d319cadfd49f5de2dc1a93a9063c8b Mon Sep 17 00:00:00 2001 From: Eiswolf-BG <37225519+Eiswolf-BG@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:07:52 +0200 Subject: [PATCH] v2.8.0: Weather icon in header Adds a small weather icon in the header, right where the old camera button used to be. Shows current conditions (sun, cloud, sun-behind-cloud, rain, snow, thunderstorm) for whichever location is currently active, including active location presets, so switching to e.g. Milan shows Milan's weather. Powered by the free Open-Meteo API (no API key needed), refreshed automatically every 10 minutes in the background, or immediately after switching to a different location preset. Icons are drawn entirely with TFT_eSPI basic shapes (circles, lines), no image files or extra fonts needed. The initial implementation failed to parse Open-Meteo's response (ArduinoJson "InvalidInput") because the API replies with Transfer-Encoding: chunked and streaming directly from http.getStream() didn't reassemble the chunks correctly. Fixed by collecting the full body via http.getString() first, then parsing that. Co-Authored-By: Claude Sonnet 5 --- README.md | 8 ++++ src/config.h | 8 +++- src/main.cpp | 86 +++++++++++++++++++++++++++++++++++++++ src/net_task.cpp | 2 + src/weather.cpp | 103 +++++++++++++++++++++++++++++++++++++++++++++++ src/weather.h | 33 +++++++++++++++ 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/weather.cpp create mode 100644 src/weather.h diff --git a/README.md b/README.md index 8ea6982..ebd18a9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ You can flash the firmware directly from your browser to your CYD display withou - **Night dimming** – automatically dims the backlight, plus the radar's aircraft markers and sweep line, to a softer (but still readable) look between 22:00 and 06:00 local time (see [Night Dimming](#-night-dimming) below) - **History chart** – a simple 7-day bar chart of logged aircraft counts, right next to the plain logbook file list (see [History Chart](#-history-chart) below) - **Web export** – a small built-in web page (reachable via the device's IP while on the same WiFi, with its own "Logbook / WebUI" entry in Menu → System) to view, download and individually delete the flight logbook, in addition to a merged CSV of all days (see [Web Export](#-web-export) below) +- **Weather icon** – a small icon in the header shows the current conditions (sun, cloud, sun-behind-cloud, rain, snow, thunderstorm) for whichever location is currently active, including active location presets (see [Weather Icon](#-weather-icon) below) - **IP-based geolocation** (no GPS module needed) to center the radar on your location - **Proximity LED alert** – the onboard RGB LED blinks green when an aircraft is within 3 km (no speaker on this board, so this replaces an audible alert) - **Dual-core design** – all networking (WiFi, ADS-B polling, aircraft detail lookups) runs on Core 0, while the display and touch input run on Core 1, so the UI never freezes during a network request @@ -138,6 +139,13 @@ In addition to viewing the logbook on the device itself, a small built-in web pa Don't know the device's current IP? **Menu > System > "Logbook / WebUI"** shows it live on the device, along with a short explanation of the page (the "?" info button on the **Logbook files** screen also still shows it). +### ☀️ Weather Icon +A small icon in the header (where the camera button used to be) shows the current weather conditions – sun, cloud, sun-behind-cloud, rain, snow, or thunderstorm – drawn entirely with simple shapes, no image files or extra fonts needed. + +It always follows whichever location is currently active, including active [location presets](#-location-presets) – switch the active preset to e.g. Milan or Tokyo and the icon updates to show that location's weather. + +Powered by the free [Open-Meteo](https://open-meteo.com) API (no API key needed). Refreshed automatically in the background every 10 minutes, or immediately after switching to a different location. + ### 📖 Flight logbook (ON/OFF) When enabled, the device logs **every newly sighted aircraft** (timestamp, hex code, callsign, registration, type, distance, altitude) into a CSV file on the SD card. An aircraft is only logged once per activation, even if it crosses the radar multiple times. diff --git a/src/config.h b/src/config.h index bf2ea84..f8f0f5a 100644 --- a/src/config.h +++ b/src/config.h @@ -6,7 +6,7 @@ namespace Config { // CLAUDE.md-Workflow "Standard-Workflow: Push & Release") - erscheint // im Info-Screen (Menue > System > Info) und muss zum jeweiligen // Git-Tag passen. - constexpr const char* APP_VERSION = "2.7.5"; + constexpr const char* APP_VERSION = "2.8.0"; // Display-Helligkeit (Menue > System > Helligkeit), in Prozent. // MIN bewusst nicht 0 - ein komplett dunkles Display koennte sonst wie @@ -40,6 +40,12 @@ namespace Config { constexpr uint32_t FETCH_INTERVAL_MS = 8000; constexpr uint32_t HTTP_TIMEOUT_MS = 6000; + // Wetter-Icon im Header (siehe weather.cpp) - deutlich seltener + // abgefragt als die ADS-B-Daten, das Wetter aendert sich nicht + // minuetlich und die kostenlose Open-Meteo-API soll nicht unnoetig oft + // belastet werden. + constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 Minuten + constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f; constexpr float LED_ALERT_RADIUS_KM = 3.0f; diff --git a/src/main.cpp b/src/main.cpp index 1ef7284..124e756 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,10 +27,12 @@ #include "splash_screen.h" #include "led_alert.h" #include "flight_logbook.h" +#include "weather.h" #include "i18n.h" #include "first_run_language_screen.h" #include "menu_stars.h" #include "ui_font.h" +#include TFT_eSPI tft = TFT_eSPI(); @@ -44,6 +46,7 @@ uint32_t lastPollMs = 0; uint32_t lastSweepMs = 0; uint32_t lastStatusLineMs = 0; uint32_t lastRenderedVersion = 0xFFFFFFFF; +Weather::Condition lastRenderedWeather = Weather::Condition::Unknown; bool forceRedraw = false; bool wasEmergency = false; bool bannerBlinkOn = false; @@ -80,6 +83,11 @@ struct Rect { Rect menuBtn = {Config::SCREEN_WIDTH - 90, 3, 54, 22}; +// Platz, an dem frueher der Cam-Button war (siehe entfernte Screenshot- +// Funktion) - zeigt jetzt stattdessen ein kleines Wetter-Icon fuer den +// aktuell aktiven Standort (siehe weather.cpp/Weather::update()). +Rect weatherIconRect = {(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); @@ -89,6 +97,73 @@ void drawMenuButton() { tft.setTextDatum(TL_DATUM); } +// Kleine, mit einfachen TFT_eSPI-Grundformen gezeichnete Wolke (kein +// Bild/Font noetig) - Basis fuer die Regen/Schnee/Gewitter-Varianten. +void drawCloudShape(int16_t cx, int16_t cy, uint16_t color) { + tft.fillCircle(cx - 5, cy + 1, 3, color); + tft.fillCircle(cx - 1, cy - 2, 4, color); + tft.fillCircle(cx + 4, cy, 4, color); + tft.fillRect(cx - 8, cy, 13, 3, color); +} + +// Sonne mit ein paar Strahlen ringsum, ebenfalls nur aus Grundformen. +void drawSunShape(int16_t cx, int16_t cy, int16_t r, uint16_t color) { + tft.fillCircle(cx, cy, r, color); + for (uint8_t i = 0; i < 8; i++) { + float angle = i * (PI / 4.0f); + int16_t x1 = cx + (int16_t)((r + 2) * cosf(angle)); + int16_t y1 = cy + (int16_t)((r + 2) * sinf(angle)); + int16_t x2 = cx + (int16_t)((r + 4) * cosf(angle)); + int16_t y2 = cy + (int16_t)((r + 4) * sinf(angle)); + tft.drawLine(x1, y1, x2, y2, color); + } +} + +// Zeichnet das Wetter-Icon passend zur zuletzt abgefragten Wetterlage +// (Weather::current()) - bei Weather::Condition::Unknown (noch keine +// erfolgreiche Abfrage, z.B. kurz nach dem Booten) bleibt die Flaeche +// einfach leer, statt einen Platzhalter anzuzeigen. +void drawWeatherIcon() { + int16_t cx = weatherIconRect.x + weatherIconRect.w / 2; + int16_t cy = weatherIconRect.y + weatherIconRect.h / 2; + + tft.fillRect(weatherIconRect.x, weatherIconRect.y, weatherIconRect.w, weatherIconRect.h, TFT_BLACK); + + switch (Weather::current()) { + case Weather::Condition::Clear: + drawSunShape(cx, cy, 6, TFT_YELLOW); + break; + case Weather::Condition::PartlyCloudy: + drawSunShape((int16_t)(cx - 4), (int16_t)(cy - 3), 4, TFT_YELLOW); + drawCloudShape((int16_t)(cx + 3), (int16_t)(cy + 2), TFT_LIGHTGREY); + break; + case Weather::Condition::Cloudy: + drawCloudShape(cx, cy, TFT_LIGHTGREY); + break; + case Weather::Condition::Rain: + drawCloudShape(cx, (int16_t)(cy - 3), TFT_LIGHTGREY); + tft.drawLine(cx - 4, cy + 4, cx - 6, cy + 8, TFT_SKYBLUE); + tft.drawLine(cx, cy + 4, cx - 2, cy + 8, TFT_SKYBLUE); + tft.drawLine(cx + 4, cy + 4, cx + 2, cy + 8, TFT_SKYBLUE); + break; + case Weather::Condition::Snow: + drawCloudShape(cx, (int16_t)(cy - 3), TFT_LIGHTGREY); + tft.drawPixel(cx - 4, cy + 6, TFT_WHITE); + tft.drawPixel(cx, cy + 7, TFT_WHITE); + tft.drawPixel(cx + 4, cy + 6, TFT_WHITE); + break; + case Weather::Condition::Thunderstorm: + drawCloudShape(cx, (int16_t)(cy - 3), TFT_LIGHTGREY); + tft.drawLine(cx, cy + 3, cx - 3, cy + 7, TFT_YELLOW); + tft.drawLine(cx - 3, cy + 7, cx + 1, cy + 7, TFT_YELLOW); + tft.drawLine(cx + 1, cy + 7, cx - 2, cy + 11, TFT_YELLOW); + break; + case Weather::Condition::Unknown: + default: + break; + } +} + void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) { uint8_t level; if (rssi >= -55) level = 4; @@ -131,6 +206,8 @@ void drawHeader() { tft.println("Eiswolfs FR"); drawMenuButton(); updateWifiIcon(); + drawWeatherIcon(); + lastRenderedWeather = Weather::current(); } void updateStatusLine() { @@ -165,6 +242,15 @@ void updateStatusLine() { } updateWifiIcon(); + + // Icon nur bei tatsaechlicher Aenderung neu zeichnen (Weather::update() + // laeuft im Hintergrund auf Core 0 und aktualisiert typischerweise nur + // alle paar Minuten) - vermeidet unnoetiges Neuzeichnen jede Sekunde. + Weather::Condition weatherNow = Weather::current(); + if (weatherNow != lastRenderedWeather) { + lastRenderedWeather = weatherNow; + drawWeatherIcon(); + } } // Prueft, ob die aktuelle Lokalzeit im Nachtdimm-Fenster (22:00-06:00) liegt. diff --git a/src/net_task.cpp b/src/net_task.cpp index b5a97f7..88cd7f0 100644 --- a/src/net_task.cpp +++ b/src/net_task.cpp @@ -10,6 +10,7 @@ #include "flight_logbook.h" #include "led_alert.h" #include "web_export_server.h" +#include "weather.h" #include #include #include @@ -39,6 +40,7 @@ namespace { } AircraftDetails::update(); + Weather::update(); if (millis() - lastFetchMs >= Config::FETCH_INTERVAL_MS) { lastFetchMs = millis(); diff --git a/src/weather.cpp b/src/weather.cpp new file mode 100644 index 0000000..bf25265 --- /dev/null +++ b/src/weather.cpp @@ -0,0 +1,103 @@ +#include "weather.h" +#include "config.h" +#include "location_manager.h" +#include +#include +#include +#include +#include + +namespace Weather { + +namespace { + WiFiClientSecure client; + bool clientConfigured = false; + + Condition currentCondition = Condition::Unknown; + uint32_t lastFetchMs = 0; + double lastLat = 0; + double lastLon = 0; + bool hasLastLocation = false; + + // Ordnet den WMO-Wettercode von Open-Meteo (Feld "weathercode", siehe + // https://open-meteo.com/en/docs) einer der wenigen Icon-Kategorien zu, + // die main.cpp zeichnen kann. + Condition conditionFromWmoCode(int code) { + if (code == 0) return Condition::Clear; + if (code == 1 || code == 2) return Condition::PartlyCloudy; + if (code == 3 || code == 45 || code == 48) return Condition::Cloudy; + if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) return Condition::Rain; + if (code >= 71 && code <= 77) return Condition::Snow; + if (code >= 95) return Condition::Thunderstorm; + return Condition::Unknown; + } + + void fetchNow(double lat, double lon) { + if (WiFi.status() != WL_CONNECTED) return; + + if (!clientConfigured) { + client.setInsecure(); + client.setTimeout(Config::HTTP_TIMEOUT_MS); + clientConfigured = true; + } + + HTTPClient http; + char url[160]; + snprintf(url, sizeof(url), + "https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f¤t_weather=true", + lat, lon); + + http.setTimeout(Config::HTTP_TIMEOUT_MS); + if (!http.begin(client, url)) return; + + int code = http.GET(); + if (code != HTTP_CODE_OK) { + http.end(); + return; + } + + // Body erst komplett als String einsammeln (getString() kuemmert + // sich zuverlaessig um Chunked-Transfer-Encoding), statt direkt aus + // http.getStream() zu parsen - Letzteres scheiterte bei Open-Meteo + // zuverlaessig mit einem ArduinoJson-"InvalidInput"-Fehler. + String body = http.getString(); + http.end(); + + JsonDocument doc; + DeserializationError err = deserializeJson(doc, body); + if (err) return; + + int wmoCode = doc["current_weather"]["weathercode"] | -1; + if (wmoCode < 0) return; + + currentCondition = conditionFromWmoCode(wmoCode); + } +} + +void update() { + double lat = 0, lon = 0; + LocationManager::getHomeLocation(lat, lon); + if (lat == 0 && lon == 0) return; + + // Deutliche Standort-Aenderung (z.B. anderes Standort-Preset aktiviert) + // - sofort neu abfragen statt bis zum naechsten regulaeren Intervall zu + // warten, damit das Icon nicht minutenlang das Wetter des alten + // Standorts zeigt. + bool locationChanged = !hasLastLocation || + fabs(lat - lastLat) > 0.01 || fabs(lon - lastLon) > 0.01; + + uint32_t now = millis(); + if (!locationChanged && (now - lastFetchMs < Config::WEATHER_FETCH_INTERVAL_MS)) { + return; + } + + lastFetchMs = now; + lastLat = lat; + lastLon = lon; + hasLastLocation = true; + fetchNow(lat, lon); +} + +Condition current() { return currentCondition; } + +} diff --git a/src/weather.h b/src/weather.h new file mode 100644 index 0000000..453056e --- /dev/null +++ b/src/weather.h @@ -0,0 +1,33 @@ +#pragma once +#include + +// Sehr einfache Wetteranzeige (Icon im Header, dort wo frueher der +// Cam-Button war) - fragt periodisch die aktuelle Wetterlage fuer den +// gerade aktiven Standort ab (also inkl. aktivem Standort-Preset, siehe +// LocationManager::getHomeLocation() - wenn dort z.B. Mailand ausgewaehlt +// ist, zeigt das Icon das Wetter in Mailand). Nutzt die kostenlose +// Open-Meteo-API (kein API-Key noetig). +namespace Weather { + enum class Condition { + Unknown, // noch keine erfolgreiche Abfrage - Icon zeigt nichts an + Clear, + PartlyCloudy, + Cloudy, + Rain, + Snow, + Thunderstorm + }; + + // Muss regelmaessig aus dem NetTask (Core 0) aufgerufen werden - kuemmert + // sich intern um das Abfrage-Intervall (Config::WEATHER_FETCH_INTERVAL_MS) + // und erkennt eine Standort-Aenderung (z.B. anderes aktives Preset), um + // dann sofort neu abzufragen statt bis zum naechsten reguleren Intervall + // zu warten. + void update(); + + // Aktuell bekannte Wetterlage, threadsicher genug fuer diesen Zweck (ein + // einzelnes uint8-artiges Enum, das nur vom NetTask geschrieben und vom + // UI-Thread gelesen wird - kein Lock noetig, ein kurzfristig veraltetes + // Lesen ist unkritisch). + Condition current(); +}