Add proper weather forecast screen

This commit is contained in:
jpirnay
2026-04-02 17:17:50 +02:00
parent 9fb03ae237
commit 049a63a8c1
17 changed files with 833 additions and 95 deletions
+4
View File
@@ -177,3 +177,7 @@ CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I
was making CrossPoint.
## Third-Party Attribution
- Weather icon artwork used by the Weather activity is adapted from [weather-icons by Erik Flowers](https://github.com/erikflowers/weather-icons).
+55 -17
View File
@@ -290,22 +290,60 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Take screenshot"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_WEATHER_LOCATION: "Location: "
STR_WEATHER: "Weather"
STR_WEATHER_LOCATION: "Location"
STR_WEATHER_NO_LOCATION: "No location set for weather"
STR_WEATHER_FETCH_FAILED: "Failed to fetch weather"
STR_WEATHER_SETTINGS: "Settings"
STR_WEATHER_REFRESH: "Refresh Weather"
STR_WEATHER_FEELS_LIKE: "Feels like: "
STR_WEATHER_HUMIDITY: "Humidity: "
STR_WEATHER_WIND: "Wind: "
STR_WEATHER_PRESSURE: "Pressure: "
STR_WEATHER_LAST_UPDATED: "Last updated: "
STR_WEATHER_PRECIP: "Precipitation: "
STR_WEATHER_PRECIP_UNIT: "Units:"
STR_WEATHER_WIND_UNIT: "Units:"
STR_WEATHER_TEMP_UNIT: "Units:"
STR_WEATHER_48H_FORECAST: "48-hour forecast"
STR_WEATHER_SEARCH_CITY: "Search city"
STR_WEATHER_LONGITUDE: "Longitude: "
STR_WEATHER_LATITUDE: "Latitude: "
STR_WEATHER_SEARCH_RESULTS: "Search results"
STR_WEATHER_SETTINGS: "Weather Settings"
STR_WEATHER_SETTINGS_SHORT: "Settings"
STR_WEATHER_REFRESH: "Refresh"
STR_WEATHER_FEELS_LIKE: "Feels like"
STR_WEATHER_HUMIDITY: "Humidity"
STR_WEATHER_WIND: "Wind"
STR_WEATHER_PRESSURE: "Pressure"
STR_WEATHER_LAST_UPDATED: "Last updated"
STR_WEATHER_PRECIP: "Precip."
STR_WEATHER_PRECIP_UNIT: "Precipitation Unit"
STR_WEATHER_WIND_UNIT: "Wind Speed Unit"
STR_WEATHER_TEMP_UNIT: "Temperature Unit"
STR_WEATHER_48H_FORECAST: "48-Hour Forecast"
STR_WEATHER_SEARCH_CITY: "Search City"
STR_WEATHER_LONGITUDE: "Longitude"
STR_WEATHER_LATITUDE: "Latitude"
STR_WEATHER_SEARCH_RESULTS: "Search Results"
STR_WEATHER_DAY_MON: "Mon"
STR_WEATHER_DAY_TUE: "Tue"
STR_WEATHER_DAY_WED: "Wed"
STR_WEATHER_DAY_THU: "Thu"
STR_WEATHER_DAY_FRI: "Fri"
STR_WEATHER_DAY_SAT: "Sat"
STR_WEATHER_DAY_SUN: "Sun"
STR_WEATHER_DESC_CLEAR_SKY: "Clear sky"
STR_WEATHER_DESC_MAINLY_CLEAR: "Mainly clear"
STR_WEATHER_DESC_PARTLY_CLOUDY: "Partly cloudy"
STR_WEATHER_DESC_OVERCAST: "Overcast"
STR_WEATHER_DESC_FOG: "Fog"
STR_WEATHER_DESC_RIME_FOG: "Rime fog"
STR_WEATHER_DESC_LIGHT_DRIZZLE: "Light drizzle"
STR_WEATHER_DESC_DRIZZLE: "Drizzle"
STR_WEATHER_DESC_DENSE_DRIZZLE: "Dense drizzle"
STR_WEATHER_DESC_FREEZING_DRIZZLE: "Freezing drizzle"
STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE: "Dense freezing drizzle"
STR_WEATHER_DESC_SLIGHT_RAIN: "Slight rain"
STR_WEATHER_DESC_MODERATE_RAIN: "Moderate rain"
STR_WEATHER_DESC_HEAVY_RAIN: "Heavy rain"
STR_WEATHER_DESC_FREEZING_RAIN: "Freezing rain"
STR_WEATHER_DESC_HEAVY_FREEZING_RAIN: "Heavy freezing rain"
STR_WEATHER_DESC_SLIGHT_SNOW: "Slight snow"
STR_WEATHER_DESC_MODERATE_SNOW: "Moderate snow"
STR_WEATHER_DESC_HEAVY_SNOW: "Heavy snow"
STR_WEATHER_DESC_SNOW_GRAINS: "Snow grains"
STR_WEATHER_DESC_SLIGHT_SHOWERS: "Slight showers"
STR_WEATHER_DESC_MODERATE_SHOWERS: "Moderate showers"
STR_WEATHER_DESC_VIOLENT_SHOWERS: "Violent showers"
STR_WEATHER_DESC_SNOW_SHOWERS: "Snow showers"
STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS: "Heavy snow showers"
STR_WEATHER_DESC_THUNDERSTORM: "Thunderstorm"
STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Thunderstorm, hail"
STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Thunderstorm, heavy hail"
STR_WEATHER_DESC_UNKNOWN: "Unknown"
+60
View File
@@ -320,3 +320,63 @@ STR_UPTIME: "Laufzeit"
STR_CHARGING: "Lädt"
STR_GATHERING_DATA: "Daten werden gesammelt..."
STR_READING: "Lese..."
STR_WEATHER: "Wetter"
STR_WEATHER_LOCATION: "Ort"
STR_WEATHER_NO_LOCATION: "Kein Ort für Wetter festgelegt"
STR_WEATHER_FETCH_FAILED: "Wetterdaten konnten nicht geladen werden"
STR_WEATHER_SETTINGS: "Wettereinstellungen"
STR_WEATHER_SETTINGS_SHORT: "Einstell."
STR_WEATHER_REFRESH: "Aktual."
STR_WEATHER_FEELS_LIKE: "Gefühlt"
STR_WEATHER_HUMIDITY: "Luftfeuchte"
STR_WEATHER_WIND: "Wind"
STR_WEATHER_PRESSURE: "Luftdruck"
STR_WEATHER_LAST_UPDATED: "Zuletzt aktualisiert"
STR_WEATHER_PRECIP: "Niederschl."
STR_WEATHER_PRECIP_UNIT: "Niederschlagseinheit"
STR_WEATHER_WIND_UNIT: "Windgeschwindigkeitseinheit"
STR_WEATHER_TEMP_UNIT: "Temperatureinheit"
STR_WEATHER_48H_FORECAST: "48-Stunden-Vorhersage"
STR_WEATHER_SEARCH_CITY: "Stadt suchen"
STR_WEATHER_LONGITUDE: "Längengrad"
STR_WEATHER_LATITUDE: "Breitengrad"
STR_WEATHER_SEARCH_RESULTS: "Suchergebnisse"
STR_WEATHER_DAY_MON: "Mo"
STR_WEATHER_DAY_TUE: "Di"
STR_WEATHER_DAY_WED: "Mi"
STR_WEATHER_DAY_THU: "Do"
STR_WEATHER_DAY_FRI: "Fr"
STR_WEATHER_DAY_SAT: "Sa"
STR_WEATHER_DAY_SUN: "So"
STR_WEATHER_DESC_CLEAR_SKY: "Klarer Himmel"
STR_WEATHER_DESC_MAINLY_CLEAR: "Überwiegend klar"
STR_WEATHER_DESC_PARTLY_CLOUDY: "Teilweise bewölkt"
STR_WEATHER_DESC_OVERCAST: "Bedeckt"
STR_WEATHER_DESC_FOG: "Nebel"
STR_WEATHER_DESC_RIME_FOG: "Raureifnebel"
STR_WEATHER_DESC_LIGHT_DRIZZLE: "Leichter Nieselregen"
STR_WEATHER_DESC_DRIZZLE: "Nieselregen"
STR_WEATHER_DESC_DENSE_DRIZZLE: "Dichter Nieselregen"
STR_WEATHER_DESC_FREEZING_DRIZZLE: "Gefrierender Nieselregen"
STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE: "Dichter gefrierender Nieselregen"
STR_WEATHER_DESC_SLIGHT_RAIN: "Leichter Regen"
STR_WEATHER_DESC_MODERATE_RAIN: "Mäßiger Regen"
STR_WEATHER_DESC_HEAVY_RAIN: "Starker Regen"
STR_WEATHER_DESC_FREEZING_RAIN: "Gefrierender Regen"
STR_WEATHER_DESC_HEAVY_FREEZING_RAIN: "Starker gefrierender Regen"
STR_WEATHER_DESC_SLIGHT_SNOW: "Leichter Schneefall"
STR_WEATHER_DESC_MODERATE_SNOW: "Mäßiger Schneefall"
STR_WEATHER_DESC_HEAVY_SNOW: "Starker Schneefall"
STR_WEATHER_DESC_SNOW_GRAINS: "Schneegriesel"
STR_WEATHER_DESC_SLIGHT_SHOWERS: "Leichte Schauer"
STR_WEATHER_DESC_MODERATE_SHOWERS: "Mäßige Schauer"
STR_WEATHER_DESC_VIOLENT_SHOWERS: "Heftige Schauer"
STR_WEATHER_DESC_SNOW_SHOWERS: "Schneeschauer"
STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS: "Starke Schneeschauer"
STR_WEATHER_DESC_THUNDERSTORM: "Gewitter"
STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Gewitter mit Hagel"
STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Gewitter mit starkem Hagel"
STR_WEATHER_DESC_UNKNOWN: "Unbekannt"
+18 -2
View File
@@ -12,7 +12,9 @@ namespace {
constexpr char WEATHER_CACHE_FILE[] = "/.crosspoint/weather_cache.json";
std::string buildForecastUrl(const WeatherSettingsStore& settings) {
std::string url = "https://api.open-meteo.com/v1/forecast?";
// Open-Meteo supports plain HTTP. Using HTTP here avoids TLS handshake
// instability observed on some ESP32-C3 builds.
std::string url = "http://api.open-meteo.com/v1/forecast?";
url += "latitude=" + std::to_string(settings.getLatitude());
url += "&longitude=" + std::to_string(settings.getLongitude());
url +=
@@ -50,7 +52,15 @@ WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool
return cached;
}
LOG_DBG("WEA", "Cache expired (age: %ld s)", (long)(now - cached.fetchedAt));
// Cache-first behavior: return stale cache and let the caller decide
// whether/when to perform a network refresh.
return cached;
}
LOG_DBG("WEA", "No cache available; caller should establish network and force refresh");
WeatherData data;
data.errorMessage = "No cache";
return data;
}
return fetchFromApi(settings);
@@ -59,24 +69,30 @@ WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool
WeatherData WeatherClient::fetchFromApi(const WeatherSettingsStore& settings) {
WeatherData data;
std::string url = buildForecastUrl(settings);
LOG_DBG("WEA", "fetchFromApi[1] start");
LOG_DBG("WEA", "fetchFromApi[2] url length=%zu", url.size());
LOG_DBG("WEA", "Fetching weather from API");
std::string response;
LOG_DBG("WEA", "fetchFromApi[3] HttpDownloader::fetchUrl before");
if (!HttpDownloader::fetchUrl(url, response)) {
data.errorMessage = "Network error";
LOG_ERR("WEA", "Failed to fetch weather data");
return data;
}
LOG_DBG("WEA", "fetchFromApi[4] HttpDownloader::fetchUrl after; bytes=%zu", response.size());
LOG_DBG("WEA", "fetchFromApi[5] parseWeatherJson before");
if (!parseWeatherJson(response, data)) {
LOG_ERR("WEA", "Failed to parse weather JSON");
return data;
}
LOG_DBG("WEA", "fetchFromApi[6] parseWeatherJson after");
data.valid = true;
time(&data.fetchedAt);
saveCache(data);
LOG_DBG("WEA", "Weather data fetched and cached");
LOG_DBG("WEA", "fetchFromApi[7] Weather data fetched and cached");
return data;
}
+23 -10
View File
@@ -1,5 +1,11 @@
#include "WeatherIcons.h"
#include "WeatherIcons48.h"
// Weather icon glyph source attribution:
// https://github.com/erikflowers/weather-icons
// 48x48 monochrome icon bitmaps in this file are adapted from that icon set.
// ============================================================================
// 48x48 Large Weather Icons (1-bit, MSB-first, row-major)
// Each row = 48 pixels = 6 bytes. Total = 48 * 6 = 288 bytes per icon.
@@ -178,6 +184,8 @@ static const uint8_t ICON_FOG_48[] = {
// Each row = 24 pixels = 3 bytes. Total = 24 * 3 = 72 bytes per icon.
// ============================================================================
#if WEATHER_ENABLE_SMALL_ICONS
// clang-format off
// Small Clear Day - Sun
@@ -262,6 +270,8 @@ static const uint8_t ICON_FOG_24[] = {
// clang-format on
#endif // WEATHER_ENABLE_SMALL_ICONS
// ============================================================================
// Lookup functions
// ============================================================================
@@ -269,30 +279,32 @@ static const uint8_t ICON_FOG_24[] = {
const uint8_t* getWeatherIconLarge(WeatherIconType type) {
switch (type) {
case WeatherIconType::CLEAR_DAY:
return ICON_CLEAR_DAY_48;
return WI48_CLEAR_DAY;
case WeatherIconType::CLEAR_NIGHT:
return ICON_CLEAR_NIGHT_48;
return WI48_CLEAR_NIGHT;
case WeatherIconType::PARTLY_CLOUDY_DAY:
return ICON_PARTLY_CLOUDY_DAY_48;
return WI48_PARTLY_CLOUDY_DAY;
case WeatherIconType::PARTLY_CLOUDY_NIGHT:
return ICON_PARTLY_CLOUDY_NIGHT_48;
return WI48_PARTLY_CLOUDY_NIGHT;
case WeatherIconType::OVERCAST:
return ICON_OVERCAST_48;
return WI48_OVERCAST;
case WeatherIconType::FOG:
return ICON_FOG_48;
return WI48_FOG;
case WeatherIconType::DRIZZLE:
return WI48_DRIZZLE;
case WeatherIconType::RAIN:
case WeatherIconType::SHOWERS:
return ICON_RAIN_48;
return WI48_RAIN;
case WeatherIconType::SNOW:
return ICON_SNOW_48;
return WI48_SNOW;
case WeatherIconType::THUNDERSTORM:
return ICON_THUNDERSTORM_48;
return WI48_THUNDERSTORM;
default:
return ICON_OVERCAST_48;
return WI48_OVERCAST;
}
}
#if WEATHER_ENABLE_SMALL_ICONS
const uint8_t* getWeatherIconSmall(WeatherIconType type) {
switch (type) {
case WeatherIconType::CLEAR_DAY:
@@ -318,6 +330,7 @@ const uint8_t* getWeatherIconSmall(WeatherIconType type) {
return ICON_OVERCAST_24;
}
}
#endif // WEATHER_ENABLE_SMALL_ICONS
const char* getWeatherDescription(int wmoCode) {
switch (wmoCode) {
+8
View File
@@ -1,6 +1,12 @@
#pragma once
#include <cstdint>
// Set to 1 (for example via build flag -DWEATHER_ENABLE_SMALL_ICONS=1)
// to include 24x24 weather icon assets and API.
#ifndef WEATHER_ENABLE_SMALL_ICONS
#define WEATHER_ENABLE_SMALL_ICONS 0
#endif
// Weather icon size constants
constexpr int WEATHER_ICON_LARGE = 48; // Large icon for current weather
constexpr int WEATHER_ICON_SMALL = 24; // Small icon for daily forecast
@@ -73,7 +79,9 @@ const char* getWeatherDescription(int wmoCode);
const uint8_t* getWeatherIconLarge(WeatherIconType type);
// Get the appropriate small icon bitmap (24x24, 1-bit, MSB first)
#if WEATHER_ENABLE_SMALL_ICONS
const uint8_t* getWeatherIconSmall(WeatherIconType type);
#endif
// Wind direction arrow text (N, NE, E, SE, S, SW, W, NW)
const char* getWindDirectionText(int degrees);
+215
View File
@@ -0,0 +1,215 @@
#pragma once
#include <cstdint>
// Generated from erikflowers/weather-icons (font + css mappings)
// 48x48, 1-bit, MSB-first, row-major, rotated for device orientation
static const uint8_t WI48_CLEAR_DAY[] = {
0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F,
0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xF8, 0x7F, 0xFC, 0x7F,
0xFC, 0x3F, 0xFC, 0xFF, 0xF8, 0x3F, 0xFC, 0x1F, 0xFF, 0xFF, 0xF0, 0x3F, 0xFE, 0x0F, 0xFF, 0xFF,
0xC0, 0x7F, 0xFF, 0x0F, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0x8F,
0xC0, 0x0F, 0xC3, 0xFF, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x01, 0xFF, 0xFF,
0xFF, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xC0, 0x7F, 0xFF, 0xFF, 0xF8, 0x3F, 0xF0,
0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xF8, 0x3F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xE0,
0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFE, 0x1F, 0xFF, 0x00, 0xE1, 0xFF, 0xFE, 0x1F, 0x00,
0x00, 0xE1, 0xFF, 0xFE, 0x1F, 0x00, 0x00, 0xE1, 0xFF, 0xFE, 0x1F, 0x00, 0x81, 0xE1, 0xFF, 0xFE,
0x1F, 0x81, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xF0,
0x7F, 0xF8, 0x3F, 0xFF, 0xFF, 0xF0, 0x3F, 0xF0, 0x3F, 0xFF, 0xFF, 0xF8, 0x1F, 0xE0, 0x7F, 0xFF,
0xFF, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03,
0xFF, 0xFF, 0xFF, 0xCF, 0xC0, 0x0F, 0xE7, 0xFF, 0xFF, 0x8F, 0xF0, 0x3F, 0xC3, 0xFF, 0xFF, 0x0F,
0xFF, 0xFF, 0xC0, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xC0, 0x7F, 0xFC, 0x1F, 0xFF, 0xFF, 0xE0, 0x3F,
0xFC, 0x1F, 0xFC, 0xFF, 0xF0, 0x3F, 0xFC, 0x3F, 0xF8, 0x7F, 0xFC, 0x3F, 0xFE, 0x7F, 0xF8, 0x7F,
0xFE, 0x7F, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF,
};
static const uint8_t WI48_CLEAR_NIGHT[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF,
0xF0, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0, 0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0xFE, 0x07, 0xFF,
0xFF, 0xFF, 0xE1, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0x83, 0xFF,
0xC1, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xC1, 0xFF, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0xFF, 0xFF, 0x00,
0x0F, 0xFF, 0xE0, 0xFF, 0xFE, 0x00, 0x1F, 0xFF, 0xF0, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xF0, 0xFF,
0xFE, 0x01, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF,
0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0x07,
0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC1, 0xFF,
0xFF, 0x81, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xE0, 0x7F, 0xFE,
0x07, 0xFF, 0xFF, 0xE0, 0x1F, 0xF8, 0x0F, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1F, 0xFF, 0xFF, 0xFC,
0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_PARTLY_CLOUDY_DAY[] = {
0xFE, 0x3F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFE, 0x0F, 0xFF, 0xFF,
0xF8, 0x3F, 0xFE, 0x07, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0x03, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0x83,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x01, 0xE3, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x07, 0xE0,
0x00, 0x0F, 0xFF, 0xFC, 0x1F, 0xF0, 0x00, 0x07, 0xFF, 0xF8, 0x3F, 0xE0, 0x7F, 0x03, 0xFF, 0xF8,
0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0xF0, 0x7F, 0xC1, 0xFF, 0xC1, 0xFF, 0xF0, 0x7F, 0x83, 0xFF, 0xE0,
0x00, 0x70, 0xFF, 0x83, 0xFF, 0xE0, 0x00, 0x70, 0xFF, 0x87, 0xFF, 0xF0, 0x00, 0x70, 0xFF, 0x87,
0xFF, 0xF0, 0x80, 0xF0, 0xFF, 0x87, 0xFF, 0xF0, 0xFF, 0xF0, 0x7C, 0x07, 0xFF, 0xF0, 0xFF, 0xF0,
0x78, 0x07, 0xFF, 0xF0, 0xFF, 0xF8, 0x30, 0x07, 0xFF, 0xF0, 0xFF, 0xF8, 0x00, 0x1F, 0xFF, 0xF0,
0xFF, 0xFC, 0x00, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x07, 0xFF,
0xFF, 0xF0, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE,
0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0x9E, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0x0E, 0x1F, 0xFF, 0xFF, 0xF0,
0xFE, 0x0E, 0x1F, 0xFF, 0xFF, 0xF0, 0xFE, 0x1E, 0x1F, 0xFF, 0xFF, 0xF0, 0xFE, 0x1E, 0x1F, 0xFF,
0xFF, 0xF0, 0xFF, 0x3E, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF,
0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xF0,
0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xE0, 0x0F,
0xFF, 0xF0, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xF0, 0xFF, 0xFF,
0xFE, 0x07, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xE0,
};
static const uint8_t WI48_PARTLY_CLOUDY_NIGHT[] = {
0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFC, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x60,
0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFE, 0x08, 0x00, 0x01, 0xFF, 0xFF, 0xFC,
0x0C, 0x00, 0x00, 0xFF, 0xFC, 0x60, 0x1C, 0x0F, 0xE0, 0x7F, 0xF8, 0x00, 0x38, 0x1F, 0xF0, 0x3F,
0xF8, 0x00, 0x78, 0x3F, 0xF8, 0x3F, 0xF8, 0x01, 0xF0, 0x7F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x7F,
0xFC, 0x1F, 0xF8, 0x7F, 0xF0, 0xFF, 0xFE, 0x1F, 0xF8, 0x7F, 0xF0, 0xFF, 0xFE, 0x1F, 0xF8, 0x3F,
0xF0, 0xFF, 0xFE, 0x1F, 0xF8, 0x3F, 0x80, 0xFF, 0xFE, 0x1F, 0xFC, 0x1F, 0x00, 0xFF, 0xFE, 0x1F,
0xFC, 0x1E, 0x00, 0xFF, 0xFE, 0x1F, 0xFE, 0x04, 0x03, 0xFF, 0xFE, 0x1F, 0xFE, 0x00, 0x1F, 0xFF,
0xFE, 0x1F, 0xFF, 0x00, 0x3F, 0xFF, 0xFE, 0x1F, 0xFF, 0x80, 0x7F, 0xFF, 0xFE, 0x1F, 0xFF, 0xE0,
0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xE1, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC1, 0xFF, 0xFF, 0xFE, 0x1F,
0xFF, 0xC1, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC3, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC3, 0xFF, 0xFF,
0xFE, 0x1F, 0xFF, 0xC3, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC3, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC1,
0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xC1, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xE1, 0xFF, 0xFF, 0xFE, 0x1F,
0xFF, 0xE0, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xE0, 0x7F, 0xFF, 0xFE, 0x1F, 0xFF, 0xF0, 0x3F, 0xFF,
0xFE, 0x1F, 0xFF, 0xF8, 0x1F, 0xFF, 0xFE, 0x1F, 0xFF, 0xFC, 0x01, 0xFF, 0xFE, 0x1F, 0xFF, 0xFE,
0x00, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0x00, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xC0, 0xFF, 0xFE, 0x1F,
0xFF, 0xFF, 0xF0, 0x7F, 0xFC, 0x1F, 0xFF, 0xFF, 0xF8, 0x7F, 0xFC, 0x1F, 0xFF, 0xFF, 0xF8, 0x3F,
0xF8, 0x3F, 0xFF, 0xFF, 0xF8, 0x1F, 0xF0, 0x3F, 0xFF, 0xFF, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0xFF,
0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x03, 0xFF,
};
static const uint8_t WI48_OVERCAST[] = {
0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x03,
0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xC1, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFE,
0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFE, 0x3F, 0xF8, 0xFF, 0xFF, 0xFF, 0xFE, 0x3F, 0xF8, 0xFF, 0xFF,
0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xF0, 0x7F, 0xFF, 0xFF, 0xC0, 0x3F, 0x80,
0x0F, 0xFF, 0xFF, 0x80, 0x7F, 0x00, 0x07, 0xFF, 0xFF, 0x87, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0x0F,
0xFC, 0x1F, 0x81, 0xFF, 0xFF, 0x1F, 0xFC, 0x3F, 0xE1, 0xFF, 0xFE, 0x1F, 0xF8, 0x7F, 0xF0, 0xFF,
0xFE, 0x3F, 0xF8, 0x7F, 0xF0, 0xFF, 0xFE, 0x3F, 0xF8, 0xFF, 0xF8, 0xFF, 0xFE, 0x3F, 0xF8, 0xFF,
0xF8, 0xFF, 0xFE, 0x3F, 0xF8, 0xFF, 0xF8, 0xFF, 0xFE, 0x1F, 0xC0, 0xFF, 0xF8, 0xFF, 0xFE, 0x1F,
0x80, 0xFF, 0xF8, 0xFF, 0xFF, 0x0E, 0x00, 0xFF, 0xF8, 0xFF, 0xFF, 0x06, 0x0F, 0xFF, 0xF8, 0xFF,
0xFF, 0x8C, 0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0x88, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF,
0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF1,
0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xF8, 0xFF,
0xFF, 0xF1, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF,
0xF8, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0x0F, 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0xF8, 0xFF,
0xFF, 0xFF, 0x80, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F,
0xF0, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xE1, 0xFF, 0xFF, 0xFF,
0xFC, 0x1F, 0xC1, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0xFF,
};
static const uint8_t WI48_FOG[] = {
0xFF, 0xFF, 0xFD, 0xE7, 0xFF, 0x0F, 0xFF, 0xFF, 0xE1, 0xC3, 0xFF, 0x0F, 0xFF, 0xFF, 0xC1, 0xC3,
0xFF, 0x0F, 0xFF, 0xFF, 0x81, 0xC3, 0xFF, 0x0F, 0xFF, 0xFF, 0x01, 0xC3, 0xFF, 0x0F, 0xFF, 0xFE,
0x07, 0xC3, 0xFF, 0x0F, 0xFF, 0xFE, 0x0F, 0xC3, 0xCF, 0x0F, 0xFF, 0xFC, 0x1F, 0xC3, 0x87, 0x0F,
0xFF, 0xFC, 0x1F, 0xC3, 0x87, 0x0F, 0xFF, 0xFC, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xFC, 0x3F, 0xC3,
0x87, 0x0F, 0xFF, 0xFC, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xF0, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xC0,
0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0x80, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0x00, 0x3F, 0xC3, 0x87, 0x0F,
0xFE, 0x07, 0xFF, 0xC3, 0x87, 0x0F, 0xFC, 0x0F, 0xFF, 0xC3, 0x87, 0x0F, 0xFC, 0x1F, 0xFF, 0xC3,
0x87, 0x0F, 0xF8, 0x3F, 0xFF, 0xC3, 0x87, 0x0F, 0xF8, 0x7F, 0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0x7F,
0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0x7F, 0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0xFF, 0xFF, 0xC3, 0x87, 0x0F,
0xF0, 0xFF, 0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0xFF, 0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0xFF, 0xFF, 0xC3,
0x87, 0x0F, 0xF0, 0xFF, 0xFF, 0xC3, 0x87, 0x0F, 0xF0, 0x7F, 0xFF, 0xC3, 0x87, 0x0F, 0xF8, 0x7F,
0xFF, 0xC3, 0x87, 0x0F, 0xF8, 0x3F, 0xFF, 0xC3, 0x87, 0x0F, 0xF8, 0x3F, 0xFF, 0xC3, 0x87, 0x0F,
0xFC, 0x1F, 0xFF, 0xC3, 0x87, 0x0F, 0xFE, 0x07, 0xFF, 0xC3, 0x87, 0x0F, 0xFE, 0x00, 0xFF, 0xC3,
0x87, 0x0F, 0xFF, 0x00, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xC0, 0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xE0,
0x3F, 0xC3, 0x87, 0x0F, 0xFF, 0xFC, 0x1F, 0xC3, 0x87, 0x0F, 0xFF, 0xFC, 0x1F, 0xC3, 0x87, 0x0F,
0xFF, 0xFE, 0x0F, 0xC3, 0x87, 0x0F, 0xFF, 0xFE, 0x07, 0xC3, 0x87, 0x0F, 0xFF, 0xFF, 0x03, 0xC3,
0x87, 0x1F, 0xFF, 0xFF, 0x81, 0xC3, 0x87, 0xFF, 0xFF, 0xFF, 0xC1, 0xC3, 0x87, 0xFF, 0xFF, 0xFF,
0xE1, 0xC3, 0x87, 0xFF, 0xFF, 0xFF, 0xF9, 0xC7, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xFF,
};
static const uint8_t WI48_DRIZZLE[] = {
0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0x03, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x07, 0xFC, 0x0F, 0xFF, 0xFF, 0xFE, 0x0F, 0xFE, 0x0F, 0xFF,
0xFF, 0xFC, 0x1F, 0xFF, 0x07, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0x07, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF,
0x87, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0x87, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0xC0, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF,
0x00, 0x7F, 0xF8, 0x3F, 0xFF, 0xFE, 0x00, 0x3F, 0xF8, 0x3F, 0xE1, 0xFC, 0x00, 0x3F, 0xF0, 0x7F,
0xC0, 0xF8, 0x00, 0x3F, 0xF0, 0x7F, 0xC0, 0xF0, 0x00, 0x3F, 0xF0, 0xFF, 0xE1, 0xF8, 0x00, 0x3F,
0xF0, 0xFF, 0xFF, 0xFC, 0x00, 0x3F, 0xF0, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF,
0x80, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xF0, 0x7F, 0xFF, 0x83, 0xFF, 0xFF, 0xF0, 0x7F,
0xFF, 0x03, 0xFF, 0xFF, 0xF8, 0x7F, 0xFC, 0x01, 0xFF, 0xFF, 0xF8, 0x3F, 0xFC, 0x01, 0xFF, 0xFF,
0xF8, 0x1F, 0xFE, 0x03, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0x83, 0xFF, 0xFF, 0xFE, 0x07, 0xFF, 0xEF,
0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0x87, 0xFF, 0xFF, 0xC0,
0x3F, 0xFF, 0x87, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0x87, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0x07, 0xFF,
0xFF, 0xFC, 0x1F, 0xFF, 0x0F, 0xFF, 0xFF, 0xFE, 0x0F, 0xFE, 0x0F, 0xFF, 0xFF, 0xFE, 0x07, 0xFC,
0x1F, 0xFF, 0xFF, 0xFF, 0x01, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0xFF, 0xFF,
};
static const uint8_t WI48_RAIN[] = {
0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xF8,
0x1F, 0xC0, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0, 0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFF,
0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01,
0xFE, 0x07, 0xFF, 0xFF, 0xFE, 0x01, 0xFE, 0x00, 0xFF, 0xFF, 0xF8, 0x01, 0xFE, 0x00, 0x0F, 0xFF,
0xF0, 0x0F, 0xFF, 0x00, 0x01, 0xFF, 0xF0, 0x3F, 0xFF, 0xE0, 0x00, 0xFF, 0xE0, 0xFF, 0xFF, 0xFE,
0x00, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xC1, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xC3, 0xFF,
0xFE, 0x03, 0xFF, 0xFF, 0x83, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0x87, 0xFF, 0xFE, 0x00, 0x07, 0xFF,
0x87, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0x87, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0x87, 0xFF, 0xFF, 0xFF,
0x00, 0x01, 0x87, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x83, 0xFF, 0xFE, 0x1F, 0xFE, 0x01, 0x83, 0xFF,
0xFE, 0x01, 0xFF, 0xE3, 0xC3, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xC1, 0xFF, 0xFE, 0x00, 0x03, 0xFF,
0xC0, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xE0, 0x7F, 0xFF, 0xF8, 0x00, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF,
0x80, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xF1, 0xFF, 0xFC, 0x01, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x01,
0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x81, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF,
0xFF, 0xE0, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0,
0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x80, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xFF,
};
static const uint8_t WI48_SNOW[] = {
0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xF8,
0x1F, 0xC0, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0, 0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFF,
0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0xFF, 0xFC, 0x3C, 0x3F,
0xF0, 0x0F, 0xFF, 0xFC, 0x3C, 0x3F, 0xF0, 0x3F, 0xFF, 0xFC, 0x3C, 0x3F, 0xE0, 0xFF, 0xFF, 0xFE,
0x7E, 0x7F, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xE7, 0xE7, 0xF3, 0x87, 0xFF, 0xFF, 0xC3, 0xC3, 0xE1,
0x87, 0xFF, 0xFF, 0xC3, 0xC3, 0xE1, 0x87, 0xFF, 0xFF, 0xC3, 0xC3, 0xE1, 0x87, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFC, 0x3C, 0x3F, 0xC1, 0xFF, 0xFF, 0xFC, 0x3C, 0x3F,
0xC0, 0xFF, 0xFF, 0xFC, 0x3C, 0x3F, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF,
0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x01,
0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x81, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFF,
0xFF, 0xE0, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0,
0x7F, 0xFF, 0xFF, 0xF8, 0x0F, 0x80, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xFF,
};
static const uint8_t WI48_THUNDERSTORM[] = {
0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x0F,
0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF0,
0x3F, 0x81, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xE0, 0xFF, 0xFF,
0xFF, 0xC1, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xC1, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xC3, 0xFF, 0xF8,
0x7F, 0xFF, 0xFF, 0xC3, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xC3, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0x03,
0xFC, 0x0F, 0xFF, 0xFF, 0xFC, 0x03, 0xFC, 0x00, 0xFF, 0xFF, 0xF0, 0x03, 0xFC, 0x00, 0x0F, 0xFF,
0xE0, 0x1F, 0xFE, 0x00, 0x03, 0xFF, 0xE0, 0x7F, 0xFF, 0xE0, 0x01, 0xFF, 0xC1, 0xFF, 0xFF, 0xFE,
0x01, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0x83, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0x07, 0xFF,
0xFC, 0x07, 0xFF, 0xFF, 0x07, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0x0F, 0xFF, 0xFC, 0x00, 0x07, 0xFF,
0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x07, 0x0F, 0xFF, 0xFF, 0xFF,
0x00, 0x03, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x07, 0xFF, 0xFD, 0xFF, 0xFF, 0x03, 0x07, 0xFF,
0xFC, 0x7F, 0x9F, 0xFF, 0x87, 0xFF, 0xFC, 0x1F, 0x8F, 0xFF, 0x83, 0xFF, 0xFC, 0x07, 0x83, 0xFF,
0x81, 0xFF, 0xFC, 0x01, 0x81, 0xFF, 0xC0, 0xFF, 0xFC, 0x00, 0x80, 0x7F, 0xE0, 0x7F, 0xFC, 0x00,
0x00, 0x3F, 0xF0, 0x07, 0xFC, 0x00, 0x00, 0x0F, 0xF8, 0x03, 0xFE, 0x00, 0x04, 0x07, 0xFC, 0x03,
0xFF, 0xC0, 0x07, 0xC1, 0xFF, 0x03, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xC1, 0xFF, 0xF0, 0x07, 0xFF,
0xFF, 0xE1, 0xFF, 0xF0, 0xC7, 0xFF, 0xFF, 0xE0, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xC1,
0xFF, 0xFF, 0xFF, 0xF0, 0x1F, 0x01, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFC,
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF,
};
+3
View File
@@ -12,6 +12,7 @@
#include "reader/ReaderActivity.h"
#include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h"
#include "weather/WeatherActivity.h"
void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
@@ -196,6 +197,8 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::
replaceActivity(std::make_unique<FullScreenMessageActivity>(renderer, mappedInput, std::move(message), style));
}
void ActivityManager::goToWeather() { replaceActivity(std::make_unique<WeatherActivity>(renderer, mappedInput)); }
void ActivityManager::goHome() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); }
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
+1
View File
@@ -86,6 +86,7 @@ class ActivityManager {
void goToSleep();
void goToBoot();
void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR);
void goToWeather();
void goHome();
// This will move current activity to stack instead of deleting it
+9 -4
View File
@@ -20,7 +20,7 @@
#include "fontIds.h"
int HomeActivity::getMenuItemCount() const {
int count = 4; // File Browser, Recents, File transfer, Settings
int count = 5; // File Browser, Recents, File transfer, Weather, Settings
if (!recentBooks.empty()) {
count += recentBooks.size();
}
@@ -192,6 +192,7 @@ void HomeActivity::loop() {
const int recentsIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int fileTransferIdx = idx++;
const int weatherIdx = idx++;
const int settingsIdx = idx;
if (selectorIndex < recentBooks.size()) {
@@ -202,6 +203,8 @@ void HomeActivity::loop() {
onRecentsOpen();
} else if (menuSelectedIndex == opdsLibraryIdx) {
onOpdsBrowserOpen();
} else if (menuSelectedIndex == weatherIdx) {
onWeatherOpen();
} else if (menuSelectedIndex == fileTransferIdx) {
onFileTransferOpen();
} else if (menuSelectedIndex == settingsIdx) {
@@ -226,11 +229,11 @@ void HomeActivity::render(RenderLock&&) {
// Build menu items dynamically
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Library, Transfer, Settings};
if (hasOpdsUrl) {
// Insert OPDS Browser after File Browser
// Insert OPDS Browser after Recents (before File Transfer)
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
menuIcons.insert(menuIcons.begin() + 2, Library);
}
@@ -269,3 +272,5 @@ void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); }
void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); }
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }
void HomeActivity::onWeatherOpen() { activityManager.goToWeather(); }
+1
View File
@@ -26,6 +26,7 @@ class HomeActivity final : public Activity {
void onSettingsOpen();
void onFileTransferOpen();
void onOpdsBrowserOpen();
void onWeatherOpen();
int getMenuItemCount() const;
bool storeCoverBuffer(); // Store frame buffer for cover image
@@ -14,6 +14,7 @@
#include "SettingsList.h"
#include "StatusBarSettingsActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -52,6 +53,7 @@ void SettingsActivity::onEnter() {
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather));
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Reset selection to first category
@@ -192,6 +194,9 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Weather:
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::None:
// Do nothing
break;
@@ -21,6 +21,7 @@ enum class SettingAction {
ClearCache,
CheckForUpdates,
Language,
Weather,
};
struct SettingInfo {
+396 -62
View File
@@ -9,9 +9,11 @@
#include <WiFi.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <ctime>
#include <vector>
#include "MappedInputManager.h"
#include "WeatherSettingsActivity.h"
@@ -19,6 +21,153 @@
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
inline bool getBitmapBit(const uint8_t* bitmap, const int size, const int x, const int y) {
const int rowBytes = size / 8;
const int idx = y * rowBytes + (x / 8);
const uint8_t mask = static_cast<uint8_t>(0x80 >> (x % 8));
return (bitmap[idx] & mask) != 0;
}
inline void setBitmapBit(uint8_t* bitmap, const int size, const int x, const int y, const bool value) {
const int rowBytes = size / 8;
const int idx = y * rowBytes + (x / 8);
const uint8_t mask = static_cast<uint8_t>(0x80 >> (x % 8));
if (value) {
bitmap[idx] |= mask;
} else {
bitmap[idx] &= static_cast<uint8_t>(~mask);
}
}
void drawWeatherIconWithOrientation(const GfxRenderer& renderer, const uint8_t* icon, const int x, const int y,
const int size) {
// Weather activity uses LandscapeClockwise; rotate icon pixels 90deg so
// directional glyphs align with the landscape-oriented UI.
if (renderer.getOrientation() != GfxRenderer::Orientation::LandscapeClockwise) {
renderer.drawImage(icon, x, y, size, size);
return;
}
constexpr int kLargeIconBytes = (WEATHER_ICON_LARGE * WEATHER_ICON_LARGE) / 8;
std::array<uint8_t, kLargeIconBytes> rotatedRaw{};
rotatedRaw.fill(0xFF);
for (int srcY = 0; srcY < size; srcY++) {
for (int srcX = 0; srcX < size; srcX++) {
// 90deg counter-clockwise: (x, y) -> (y, size - 1 - x)
const int dstX = srcY;
const int dstY = size - 1 - srcX;
setBitmapBit(rotatedRaw.data(), size, dstX, dstY, getBitmapBit(icon, size, srcX, srcY));
}
}
// Re-center rotated icon by its painted-pixel bounds to avoid asymmetric
// source padding appearing as left-side clipping after rotation.
int minX = size, minY = size, maxX = -1, maxY = -1;
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
if (!getBitmapBit(rotatedRaw.data(), size, px, py)) {
minX = std::min(minX, px);
minY = std::min(minY, py);
maxX = std::max(maxX, px);
maxY = std::max(maxY, py);
}
}
}
if (maxX < minX || maxY < minY) {
renderer.drawImage(rotatedRaw.data(), x, y, size, size);
return;
}
const int bboxW = maxX - minX + 1;
const int bboxH = maxY - minY + 1;
const int targetMinX = (size - bboxW) / 2;
const int targetMinY = (size - bboxH) / 2;
const int shiftX = targetMinX - minX;
const int shiftY = targetMinY - minY;
std::array<uint8_t, kLargeIconBytes> rotatedCentered{};
rotatedCentered.fill(0xFF);
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
const int dstX = px + shiftX;
const int dstY = py + shiftY;
if (dstX < 0 || dstX >= size || dstY < 0 || dstY >= size) {
continue;
}
setBitmapBit(rotatedCentered.data(), size, dstX, dstY, getBitmapBit(rotatedRaw.data(), size, px, py));
}
}
renderer.drawImage(rotatedCentered.data(), x, y, size, size);
}
StrId getWeatherDescriptionStrId(const int wmoCode) {
switch (wmoCode) {
case 0:
return StrId::STR_WEATHER_DESC_CLEAR_SKY;
case 1:
return StrId::STR_WEATHER_DESC_MAINLY_CLEAR;
case 2:
return StrId::STR_WEATHER_DESC_PARTLY_CLOUDY;
case 3:
return StrId::STR_WEATHER_DESC_OVERCAST;
case 45:
return StrId::STR_WEATHER_DESC_FOG;
case 48:
return StrId::STR_WEATHER_DESC_RIME_FOG;
case 51:
return StrId::STR_WEATHER_DESC_LIGHT_DRIZZLE;
case 53:
return StrId::STR_WEATHER_DESC_DRIZZLE;
case 55:
return StrId::STR_WEATHER_DESC_DENSE_DRIZZLE;
case 56:
return StrId::STR_WEATHER_DESC_FREEZING_DRIZZLE;
case 57:
return StrId::STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE;
case 61:
return StrId::STR_WEATHER_DESC_SLIGHT_RAIN;
case 63:
return StrId::STR_WEATHER_DESC_MODERATE_RAIN;
case 65:
return StrId::STR_WEATHER_DESC_HEAVY_RAIN;
case 66:
return StrId::STR_WEATHER_DESC_FREEZING_RAIN;
case 67:
return StrId::STR_WEATHER_DESC_HEAVY_FREEZING_RAIN;
case 71:
return StrId::STR_WEATHER_DESC_SLIGHT_SNOW;
case 73:
return StrId::STR_WEATHER_DESC_MODERATE_SNOW;
case 75:
return StrId::STR_WEATHER_DESC_HEAVY_SNOW;
case 77:
return StrId::STR_WEATHER_DESC_SNOW_GRAINS;
case 80:
return StrId::STR_WEATHER_DESC_SLIGHT_SHOWERS;
case 81:
return StrId::STR_WEATHER_DESC_MODERATE_SHOWERS;
case 82:
return StrId::STR_WEATHER_DESC_VIOLENT_SHOWERS;
case 85:
return StrId::STR_WEATHER_DESC_SNOW_SHOWERS;
case 86:
return StrId::STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS;
case 95:
return StrId::STR_WEATHER_DESC_THUNDERSTORM;
case 96:
return StrId::STR_WEATHER_DESC_THUNDERSTORM_HAIL;
case 99:
return StrId::STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL;
default:
return StrId::STR_WEATHER_DESC_UNKNOWN;
}
}
} // namespace
void WeatherActivity::onEnter() {
Activity::onEnter();
@@ -28,6 +177,7 @@ void WeatherActivity::onEnter() {
state = State::LOADING_CACHE;
errorMessage.clear();
forceRefresh = false;
showRefreshPopup = false;
requestUpdate();
loadAndDisplay();
@@ -53,13 +203,30 @@ void WeatherActivity::loadAndDisplay() {
// Try cache first
WeatherData cached = WeatherClient::getWeather(WEATHER_SETTINGS, forceRefresh);
if (cached.valid) {
const bool shouldRefresh = forceRefresh;
if (!shouldRefresh) {
const time_t now = time(nullptr);
const long age = (cached.fetchedAt > 0) ? static_cast<long>(now - cached.fetchedAt) : -1;
if (age >= 0 && age > 30L * 60L) {
LOG_DBG("WEA", "Displayed stale cache (age=%ld s); scheduling refresh", age);
} else {
LOG_DBG("WEA", "Displayed cache (age=%ld s)", age);
}
}
weatherData = std::move(cached);
state = State::WEATHER_DISPLAY;
requestUpdate();
// After first paint, refresh stale/non-forced data via WiFi-gated path.
if (!forceRefresh) {
checkAndConnectWifi();
}
return;
}
// Need fresh data - check WiFi
LOG_DBG("WEA", "No cache to display; proceeding to WiFi/network fetch path");
checkAndConnectWifi();
}
@@ -85,11 +252,13 @@ void WeatherActivity::launchWifiSelection() {
void WeatherActivity::onWifiSelectionComplete(bool connected) {
// Re-apply landscape after returning from WiFi selection (which uses portrait)
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
LOG_DBG("WEA", "onWifiSelectionComplete connected=%d wifiStatus=%d", connected ? 1 : 0, (int)WiFi.status());
if (connected) {
state = State::FETCHING;
state = State::CHECK_WIFI;
wifiWaitStartedAtMs = millis();
LOG_DBG("WEA", "state -> CHECK_WIFI (waitStart=%lu)", (unsigned long)wifiWaitStartedAtMs);
requestUpdate(true);
fetchWeather();
} else {
WiFi.disconnect();
WiFi.mode(WIFI_OFF);
@@ -100,13 +269,26 @@ void WeatherActivity::onWifiSelectionComplete(bool connected) {
}
void WeatherActivity::fetchWeather() {
LOG_DBG("WEA", "fetchWeather[1] enter status=%d ip=%s", (int)WiFi.status(), WiFi.localIP().toString().c_str());
if (WiFi.status() != WL_CONNECTED || WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
LOG_ERR("WEA", "fetchWeather[2] WiFi not ready status=%d ip=%s", (int)WiFi.status(),
WiFi.localIP().toString().c_str());
state = State::ERROR;
errorMessage = tr(STR_WIFI_CONN_FAILED);
requestUpdate();
return;
}
LOG_DBG("WEA", "fetchWeather[3] WeatherClient::getWeather before");
weatherData = WeatherClient::getWeather(WEATHER_SETTINGS, true);
LOG_DBG("WEA", "fetchWeather[4] WeatherClient::getWeather after valid=%d", weatherData.valid ? 1 : 0);
if (weatherData.valid) {
state = State::WEATHER_DISPLAY;
} else {
state = State::ERROR;
errorMessage = weatherData.errorMessage.empty() ? tr(STR_WEATHER_FETCH_FAILED) : weatherData.errorMessage.c_str();
}
showRefreshPopup = false;
requestUpdate();
}
@@ -117,7 +299,16 @@ void WeatherActivity::loop() {
if (state == State::ERROR) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Retry
// Open weather settings (useful when no location is configured)
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
forceRefresh = true;
loadAndDisplay();
});
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Retry fetch
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
@@ -132,6 +323,31 @@ void WeatherActivity::loop() {
}
if (state == State::LOADING_CACHE || state == State::CHECK_WIFI || state == State::FETCHING) {
if (state == State::CHECK_WIFI) {
LOG_DBG("WEA", "loop CHECK_WIFI status=%d ip=%s elapsed=%lu", (int)WiFi.status(),
WiFi.localIP().toString().c_str(), (unsigned long)(millis() - wifiWaitStartedAtMs));
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
// Give lwIP/sockets a short window to settle after activity handoff.
if (wifiWaitStartedAtMs > 0 && millis() - wifiWaitStartedAtMs < 1200) {
LOG_DBG("WEA", "loop CHECK_WIFI waiting for settle window");
return;
}
LOG_DBG("WEA", "state CHECK_WIFI -> FETCHING");
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
return;
}
if (wifiWaitStartedAtMs > 0 && millis() - wifiWaitStartedAtMs > 15000) {
LOG_ERR("WEA", "CHECK_WIFI timeout after %lu ms", (unsigned long)(millis() - wifiWaitStartedAtMs));
state = State::ERROR;
errorMessage = tr(STR_WIFI_CONN_FAILED);
requestUpdate();
return;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
}
@@ -153,9 +369,15 @@ void WeatherActivity::loop() {
});
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Manual refresh
forceRefresh = true;
loadAndDisplay();
// Manual refresh with popup feedback.
showRefreshPopup = true;
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
}
}
@@ -165,7 +387,7 @@ void WeatherActivity::render(RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth(); // 800 in landscape
const auto pageHeight = renderer.getScreenHeight(); // 480 in landscape
if (state == State::LOADING_CACHE || state == State::FETCHING) {
if (state == State::LOADING_CACHE || (state == State::FETCHING && (!showRefreshPopup || !weatherData.valid))) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2, tr(STR_LOADING));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -176,7 +398,7 @@ void WeatherActivity::render(RenderLock&&) {
if (state == State::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS), tr(STR_WEATHER_REFRESH), "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
@@ -197,30 +419,34 @@ void WeatherActivity::render(RenderLock&&) {
// | [Back] [Settings] [Refresh] Button hints |
// +--------------------------------------------------------+
const auto& metrics = UITheme::getInstance().getMetrics();
// In LandscapeClockwise, physical bottom buttons are at the logical LEFT edge
const int contentX = metrics.buttonHintsHeight;
const int contentWidth = pageWidth - contentX;
constexpr int leftPanelWidth = 220;
constexpr int buttonHintsHeight = 30;
constexpr int graphHeight = 190;
const int topSectionHeight = pageHeight - graphHeight - buttonHintsHeight;
constexpr int graphHeight = 240;
const int topSectionHeight = pageHeight - graphHeight;
// Draw current conditions (left panel)
renderCurrentConditions(0, 0, leftPanelWidth, topSectionHeight);
// Draw current conditions (left panel, offset by button hints)
renderCurrentConditions(contentX, 0, leftPanelWidth, topSectionHeight);
// Draw daily forecast (right of current conditions)
renderDailyForecast(leftPanelWidth, 0, pageWidth - leftPanelWidth, topSectionHeight);
renderDailyForecast(contentX + leftPanelWidth, 0, contentWidth - leftPanelWidth, topSectionHeight);
// Separator line
renderer.drawLine(0, topSectionHeight, pageWidth, topSectionHeight);
renderer.drawLine(contentX, topSectionHeight, pageWidth, topSectionHeight);
// Draw hourly graph
renderHourlyGraph(0, topSectionHeight + 1, pageWidth, graphHeight - 1);
// Separator line above button hints
renderer.drawLine(0, pageHeight - buttonHintsHeight, pageWidth, pageHeight - buttonHintsHeight);
renderHourlyGraph(contentX, topSectionHeight + 1, contentWidth, graphHeight - 1);
// Button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS), tr(STR_WEATHER_REFRESH), "");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS_SHORT), tr(STR_WEATHER_REFRESH), "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (state == State::FETCHING && showRefreshPopup) {
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
}
renderer.displayBuffer();
}
@@ -251,27 +477,28 @@ void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
auto iconType = getWeatherIconType(cur.weatherCode, cur.isDay);
const uint8_t* icon = getWeatherIconLarge(iconType);
int iconX = x + (w - WEATHER_ICON_LARGE) / 2;
renderer.drawImage(icon, iconX, textY, WEATHER_ICON_LARGE, WEATHER_ICON_LARGE);
drawWeatherIconWithOrientation(renderer, icon, iconX, textY, WEATHER_ICON_LARGE);
textY += WEATHER_ICON_LARGE + 5;
// Temperature (large)
char tempBuf[16];
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
snprintf(tempBuf, sizeof(tempBuf), "%.1f %s", cur.temperature, unitSuffix);
renderer.drawCenteredText(UI_12_FONT_ID, textY, tempBuf, true, EpdFontFamily::BOLD);
int tempWidth = renderer.getTextWidth(UI_12_FONT_ID, tempBuf, EpdFontFamily::BOLD);
renderer.drawText(UI_12_FONT_ID, x + (w - tempWidth) / 2, textY, tempBuf, true, EpdFontFamily::BOLD);
textY += 22;
// Feels like
snprintf(tempBuf, sizeof(tempBuf), "%.1f %s", cur.apparentTemperature, unitSuffix);
char feelsLikeBuf[48];
snprintf(feelsLikeBuf, sizeof(feelsLikeBuf), "%s %s", tr(STR_WEATHER_FEELS_LIKE), tempBuf);
snprintf(feelsLikeBuf, sizeof(feelsLikeBuf), "%s: %s", tr(STR_WEATHER_FEELS_LIKE), tempBuf);
auto feelsText = renderer.truncatedText(SMALL_FONT_ID, feelsLikeBuf, w - 10);
int feelsWidth = renderer.getTextWidth(SMALL_FONT_ID, feelsText.c_str());
renderer.drawText(SMALL_FONT_ID, x + (w - feelsWidth) / 2, textY, feelsText.c_str());
textY += 16;
// Weather description
const char* desc = getWeatherDescription(cur.weatherCode);
const char* desc = I18N.get(getWeatherDescriptionStrId(cur.weatherCode));
int descWidth = renderer.getTextWidth(SMALL_FONT_ID, desc);
renderer.drawText(SMALL_FONT_ID, x + (w - descWidth) / 2, textY, desc);
textY += 20;
@@ -279,27 +506,36 @@ void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
// Details grid
const char* windDir = getWindDirectionText(cur.windDirection);
char detailBuf[64];
const auto drawCenteredDetail = [&](const char* text) {
auto truncated = renderer.truncatedText(SMALL_FONT_ID, text, w - 10);
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, truncated.c_str());
renderer.drawText(SMALL_FONT_ID, x + (w - textWidth) / 2, textY, truncated.c_str());
};
// Wind
snprintf(detailBuf, sizeof(detailBuf), "%s: %.0f %s %s", tr(STR_WEATHER_WIND), cur.windSpeed,
WEATHER_SETTINGS.getWindUnitParam(), windDir);
renderer.drawText(SMALL_FONT_ID, x + 5, textY, detailBuf);
drawCenteredDetail(detailBuf);
textY += 14;
// Humidity
snprintf(detailBuf, sizeof(detailBuf), "%s: %d%%", tr(STR_WEATHER_HUMIDITY), cur.humidity);
renderer.drawText(SMALL_FONT_ID, x + 5, textY, detailBuf);
drawCenteredDetail(detailBuf);
textY += 14;
// Precipitation
snprintf(detailBuf, sizeof(detailBuf), "%s: %.1f %s", tr(STR_WEATHER_PRECIP), cur.precipitation,
WEATHER_SETTINGS.getPrecipUnitParam());
renderer.drawText(SMALL_FONT_ID, x + 5, textY, detailBuf);
if (std::fabs(cur.precipitation) < 0.0001f) {
snprintf(detailBuf, sizeof(detailBuf), "%s: --", tr(STR_WEATHER_PRECIP));
} else {
snprintf(detailBuf, sizeof(detailBuf), "%s: %.1f %s", tr(STR_WEATHER_PRECIP), cur.precipitation,
WEATHER_SETTINGS.getPrecipUnitParam());
}
drawCenteredDetail(detailBuf);
textY += 14;
// UV Index
snprintf(detailBuf, sizeof(detailBuf), "UV: %.1f", cur.uvIndex);
renderer.drawText(SMALL_FONT_ID, x + 5, textY, detailBuf);
drawCenteredDetail(detailBuf);
// Vertical separator
renderer.drawLine(x + w, y, x + w, y + h);
@@ -308,63 +544,113 @@ void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) {
if (weatherData.daily.empty()) return;
// Localized weekday names (indexed by tm_wday: 0=Sun..6=Sat)
const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE,
StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
StrId::STR_WEATHER_DAY_SAT};
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
int numDays = static_cast<int>(weatherData.daily.size());
int cardWidth = w / numDays;
if (numDays <= 0) return;
// Derive weekday progression from the first forecast day to avoid timezone
// conversion edge cases causing repeated day labels.
struct tm firstDayInfo;
time_t firstLocalDate = weatherData.daily[0].date + weatherData.utcOffsetSeconds;
gmtime_r(&firstLocalDate, &firstDayInfo);
const int firstWeekday = firstDayInfo.tm_wday;
// Keep all day panels evenly split (distribute remainder 1px left-to-right)
const int baseDayWidth = w / numDays;
const int extraPx = w % numDays;
int cardX = x;
for (int i = 0; i < numDays; i++) {
const auto& day = weatherData.daily[i];
int cardX = x + i * cardWidth;
int cardWidth = baseDayWidth + (i < extraPx ? 1 : 0);
int textY = y + 10;
// Day name
// Day name (localized)
struct tm timeinfo;
time_t localDate = day.date + weatherData.utcOffsetSeconds;
gmtime_r(&localDate, &timeinfo);
char dayName[16];
// Manual short day names to avoid locale issues on ESP32
const char* dayNames[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
snprintf(dayName, sizeof(dayName), "%s", dayNames[timeinfo.tm_wday]);
const int weekdayIndex = (firstWeekday + i) % 7;
const char* dayName = I18N.get(dayNameIds[weekdayIndex]);
int dayNameWidth = renderer.getTextWidth(SMALL_FONT_ID, dayName, EpdFontFamily::BOLD);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dayNameWidth) / 2, textY, dayName, true, EpdFontFamily::BOLD);
int dayNameWidth = renderer.getTextWidth(UI_10_FONT_ID, dayName, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, cardX + (cardWidth - dayNameWidth) / 2, textY, dayName, true, EpdFontFamily::BOLD);
textY += 20;
// Date (e.g. "Apr 3") - for all days
char dateBuf[16];
const char* monthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthNames[timeinfo.tm_mon], timeinfo.tm_mday);
int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf);
textY += 18;
// Weather icon (24x24)
// Use isDay=true for daily forecasts
// Weather icon (48x48 for all days)
auto iconType = getWeatherIconType(day.weatherCode, true);
const uint8_t* icon = getWeatherIconSmall(iconType);
int iconX = cardX + (cardWidth - WEATHER_ICON_SMALL) / 2;
renderer.drawImage(icon, iconX, textY, WEATHER_ICON_SMALL, WEATHER_ICON_SMALL);
textY += WEATHER_ICON_SMALL + 5;
const uint8_t* icon = getWeatherIconLarge(iconType);
int iconX = cardX + (cardWidth - WEATHER_ICON_LARGE) / 2;
drawWeatherIconWithOrientation(renderer, icon, iconX, textY, WEATHER_ICON_LARGE);
textY += WEATHER_ICON_LARGE + 8;
// High / Low temp
char tempBuf[24];
snprintf(tempBuf, sizeof(tempBuf), "%.0f/%.0f", day.tempMax, day.tempMin);
int tempWidth = renderer.getTextWidth(SMALL_FONT_ID, tempBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - tempWidth) / 2, textY, tempBuf);
textY += 15;
// Weather description (short)
const char* desc = I18N.get(getWeatherDescriptionStrId(day.weatherCode));
auto truncDesc = renderer.truncatedText(SMALL_FONT_ID, desc, cardWidth - 8);
int descWidth = renderer.getTextWidth(SMALL_FONT_ID, truncDesc.c_str());
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - descWidth) / 2, textY, truncDesc.c_str());
textY += 16;
// High / Low temp with unit
char tempBuf[32];
snprintf(tempBuf, sizeof(tempBuf), "%.0f / %.0f %s", day.tempMax, day.tempMin, unitSuffix);
int tempWidth = renderer.getTextWidth(SMALL_FONT_ID, tempBuf, EpdFontFamily::BOLD);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - tempWidth) / 2, textY, tempBuf, true, EpdFontFamily::BOLD);
textY += 16;
// Precipitation
if (day.precipSum > 0) {
char precipBuf[16];
snprintf(precipBuf, sizeof(precipBuf), "%.1f%s", day.precipSum, WEATHER_SETTINGS.getPrecipUnitParam());
int precipWidth = renderer.getTextWidth(SMALL_FONT_ID, precipBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - precipWidth) / 2, textY, precipBuf);
char precipBuf[24];
if (std::fabs(day.precipSum) < 0.0001f) {
snprintf(precipBuf, sizeof(precipBuf), "%s: --", tr(STR_WEATHER_PRECIP));
} else {
snprintf(precipBuf, sizeof(precipBuf), "%s: %.1f %s", tr(STR_WEATHER_PRECIP), day.precipSum,
WEATHER_SETTINGS.getPrecipUnitParam());
}
auto truncPrecip = renderer.truncatedText(SMALL_FONT_ID, precipBuf, cardWidth - 8);
int precipWidth = renderer.getTextWidth(SMALL_FONT_ID, truncPrecip.c_str());
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - precipWidth) / 2, textY, truncPrecip.c_str());
textY += 16;
// UV Index
char uvBuf[16];
snprintf(uvBuf, sizeof(uvBuf), "UV: %.0f", day.uvIndexMax);
int uvWidth = renderer.getTextWidth(SMALL_FONT_ID, uvBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - uvWidth) / 2, textY, uvBuf);
// Card separator
if (i < numDays - 1) {
renderer.drawLine(cardX + cardWidth, y, cardX + cardWidth, y + h);
}
cardX += cardWidth;
}
}
void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
if (weatherData.hourly.empty()) return;
constexpr int leftMargin = 40; // Space for Y-axis labels
// Localized weekday names (indexed by tm_wday: 0=Sun..6=Sat)
const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE,
StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
StrId::STR_WEATHER_DAY_SAT};
constexpr int leftMargin = 56; // Space for Y-axis labels with unit suffix
constexpr int rightMargin = 10;
constexpr int topMargin = 15;
constexpr int topMargin = 28; // Keep heading clear of top temperature label
constexpr int bottomMargin = 25; // Space for X-axis labels
constexpr int precipBarMaxHeight = 30;
@@ -377,6 +663,23 @@ void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
size_t numPoints = weatherData.hourly.size();
if (numPoints < 2) return;
// Find day boundaries in hourly data (start index of each day).
std::vector<size_t> dayStartIndices;
dayStartIndices.reserve(8);
dayStartIndices.push_back(0);
struct tm prevDayInfo;
time_t prevLocalTime = weatherData.hourly[0].time + weatherData.utcOffsetSeconds;
gmtime_r(&prevLocalTime, &prevDayInfo);
for (size_t i = 1; i < numPoints; i++) {
struct tm curDayInfo;
time_t localTime = weatherData.hourly[i].time + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &curDayInfo);
if (curDayInfo.tm_year != prevDayInfo.tm_year || curDayInfo.tm_yday != prevDayInfo.tm_yday) {
dayStartIndices.push_back(i);
prevDayInfo = curDayInfo;
}
}
// Find temperature range
float tempMin = weatherData.hourly[0].temperature;
float tempMax = tempMin;
@@ -399,16 +702,40 @@ void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
tempMax += 1.0f;
tempRange = tempMax - tempMin;
// Draw graph title
renderer.drawText(SMALL_FONT_ID, x + 5, y + 3, tr(STR_WEATHER_48H_FORECAST), true, EpdFontFamily::BOLD);
// Draw graph title centered over the graph area.
const char* graphTitle = tr(STR_WEATHER_48H_FORECAST);
const int graphTitleWidth = renderer.getTextWidth(SMALL_FONT_ID, graphTitle, EpdFontFamily::BOLD);
const int graphTitleX = graphX + (graphW - graphTitleWidth) / 2;
renderer.drawText(SMALL_FONT_ID, graphTitleX, y + 3, graphTitle, true, EpdFontFamily::BOLD);
// Draw weekday labels above the chart area at each day start.
for (size_t startIdx : dayStartIndices) {
struct tm dayInfo;
time_t localTime = weatherData.hourly[startIdx].time + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &dayInfo);
const char* dayName = I18N.get(dayNameIds[dayInfo.tm_wday]);
const int dayLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, dayName, EpdFontFamily::BOLD);
const int px = graphX + static_cast<int>(startIdx * graphW / (numPoints - 1));
int labelX = px + 2;
const int maxLabelX = graphX + graphW - dayLabelWidth - 2;
if (labelX > maxLabelX) {
labelX = maxLabelX;
}
if (labelX < graphX + 2) {
labelX = graphX + 2;
}
renderer.drawText(SMALL_FONT_ID, labelX, y + 14, dayName, true, EpdFontFamily::BOLD);
}
// Draw Y-axis labels (temperature)
float tempStep = tempRange / 4.0f;
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
for (int i = 0; i <= 4; i++) {
float temp = tempMax - i * tempStep;
int labelY = graphY + i * graphH / 4;
char label[8];
snprintf(label, sizeof(label), "%.0f", temp);
char label[16];
snprintf(label, sizeof(label), "%.0f %s", temp, unitSuffix);
renderer.drawText(SMALL_FONT_ID, x + 2, labelY - 4, label);
// Grid line (dashed effect using short segments)
for (int gx = graphX; gx < graphX + graphW; gx += 8) {
@@ -444,6 +771,13 @@ void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
}
}
// Draw day separator lines for day boundaries after the first day.
for (size_t boundaryIdx = 1; boundaryIdx < dayStartIndices.size(); boundaryIdx++) {
const size_t i = dayStartIndices[boundaryIdx];
const int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
renderer.drawLine(px, graphY, px, precipY + precipBarMaxHeight);
}
// Draw X-axis labels (every 6 hours)
for (size_t i = 0; i < numPoints; i += 6) {
int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
+2
View File
@@ -35,6 +35,8 @@ class WeatherActivity final : public Activity {
WeatherData weatherData;
std::string errorMessage;
bool forceRefresh = false;
bool showRefreshPopup = false;
uint32_t wifiWaitStartedAtMs = 0;
void loadAndDisplay();
void checkAndConnectWifi();
+2
View File
@@ -20,6 +20,7 @@
#include "KOReaderCredentialStore.h"
#include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "WeatherSettingsStore.h"
#include "activities/Activity.h"
#include "activities/ActivityManager.h"
#include "components/UITheme.h"
@@ -260,6 +261,7 @@ void setup() {
SETTINGS.loadFromFile();
I18N.loadSettings();
KOREADER_STORE.loadFromFile();
WEATHER_SETTINGS.loadFromFile();
UITheme::getInstance().reload();
ButtonNavigator::setMappedInputManager(mappedInputManager);
+30
View File
@@ -55,37 +55,52 @@ class FileWriteStream final : public Stream {
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "fetchUrl[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "fetchUrl[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
LOG_DBG("HTTP", "fetchUrl[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "fetchUrl[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "fetchUrl[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "fetchUrl[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "fetchUrl[8] auth header added");
}
LOG_DBG("HTTP", "fetchUrl[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "fetchUrl[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
LOG_DBG("HTTP", "fetchUrl[11] end() after GET failure");
http.end();
return false;
}
LOG_DBG("HTTP", "fetchUrl[12] writeToStream() before");
http.writeToStream(&outContent);
LOG_DBG("HTTP", "fetchUrl[13] writeToStream() after");
LOG_DBG("HTTP", "fetchUrl[14] end() before success return");
http.end();
LOG_DBG("HTTP", "Fetch success");
@@ -105,30 +120,41 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
ProgressCallback progress) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "downloadToFile[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "downloadToFile[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
LOG_DBG("HTTP", "downloadToFile[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "downloadToFile[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "downloadToFile[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "downloadToFile[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "downloadToFile[8] auth header added");
}
LOG_DBG("HTTP", "downloadToFile[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "downloadToFile[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Download failed: %d", httpCode);
http.end();
@@ -158,10 +184,14 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
FileWriteStream fileStream(file, contentLength, progress);
LOG_DBG("HTTP", "downloadToFile[11] writeToStream() before");
const int writeResult = http.writeToStream(&fileStream);
LOG_DBG("HTTP", "downloadToFile[12] writeToStream() after result=%d", writeResult);
file.close();
LOG_DBG("HTTP", "downloadToFile[13] file closed");
http.end();
LOG_DBG("HTTP", "downloadToFile[14] http end done");
if (writeResult < 0) {
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);