From 2cff48a2ac83d0fb6d21b67aeae6d39b6dccf31e Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sun, 2 Aug 2026 21:20:25 +0200 Subject: [PATCH] feat: add data manager system for background weather/calendar fetching --- src/data/data_manager.cpp | 93 ++++++++++++++++++++++++++++++ src/data/data_manager.h | 35 ++++++++++++ src/main.cpp | 5 ++ src/ui/calendar_detail_screen.cpp | 28 +++++---- src/ui/home_screen.cpp | 94 +++++++++++++++---------------- src/ui/weather_detail_screen.cpp | 28 +++++---- 6 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 src/data/data_manager.cpp create mode 100644 src/data/data_manager.h diff --git a/src/data/data_manager.cpp b/src/data/data_manager.cpp new file mode 100644 index 0000000..e25a01e --- /dev/null +++ b/src/data/data_manager.cpp @@ -0,0 +1,93 @@ +#include "data_manager.h" +#include +#include + +namespace { + +constexpr unsigned long REFRESH_INTERVAL_MS = 10UL * 60UL * 1000UL; +constexpr uint32_t TASK_STACK_WORDS = 16384; // 64 KB — JSON parsing needs headroom + +// --- Shared state between background task and main loop --- +// The task is the only writer of s_weather/s_calendar; the main loop is the +// only reader. Access to the data structs is serialized via s_mux (a +// portMUX_TYPE critical section which, on ESP32, suspends task switches on +// the current core). Simple flag/counter reads (bool/uint32_t) are atomic +// on 32-bit Xtensa and only need `volatile`. +static WeatherData s_weather; +static CalendarData s_calendar; +static volatile bool s_weatherOk = false; +static volatile bool s_calendarOk = false; +static volatile bool s_loading = false; +static volatile uint32_t s_dataVersion = 0; +static volatile unsigned long s_lastFetchMs = 0; +static volatile bool s_refreshRequested = false; +static portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED; + +static void dataFetchTask(void *param) { + (void)param; + while (true) { + bool needRefresh = s_refreshRequested || + s_lastFetchMs == 0 || + (millis() - s_lastFetchMs) >= REFRESH_INTERVAL_MS; + + if (needRefresh) { + s_refreshRequested = false; + s_loading = true; + + WeatherData w = fetchWeather(); + CalendarData c = fetchCalendar(); + + portENTER_CRITICAL(&s_mux); + if (w.valid) { + s_weather = std::move(w); + s_weatherOk = true; + } else { + s_weatherOk = false; + } + if (c.valid) { + s_calendar = std::move(c); + s_calendarOk = true; + } else { + s_calendarOk = false; + } + s_dataVersion++; + s_lastFetchMs = millis(); + s_loading = false; + portEXIT_CRITICAL(&s_mux); + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +} // namespace + +void dataManager_begin() { + s_lastFetchMs = 0; // ensure first iteration fetches immediately + xTaskCreatePinnedToCore(dataFetchTask, "dataFetch", TASK_STACK_WORDS, + nullptr, 1, nullptr, 1); +} + +void dataManager_tick() {} + +void dataManager_triggerRefresh() { + s_refreshRequested = true; +} + +void dataManager_getWeather(WeatherData &out) { + portENTER_CRITICAL(&s_mux); + out = s_weather; + portEXIT_CRITICAL(&s_mux); +} + +void dataManager_getCalendar(CalendarData &out) { + portENTER_CRITICAL(&s_mux); + out = s_calendar; + portEXIT_CRITICAL(&s_mux); +} + +bool dataManager_isWeatherOk() { return s_weatherOk; } +bool dataManager_isCalendarOk() { return s_calendarOk; } +bool dataManager_isLoading() { return s_loading; } +uint32_t dataManager_dataVersion() { return s_dataVersion; } +unsigned long dataManager_lastFetchMs() { return s_lastFetchMs; } diff --git a/src/data/data_manager.h b/src/data/data_manager.h new file mode 100644 index 0000000..728fd80 --- /dev/null +++ b/src/data/data_manager.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include "api/weather_api.h" +#include "api/calendar_api.h" + +// Centralized async data manager for weather + calendar. +// +// A background FreeRTOS task fetches weather and calendar data every 10 +// minutes (or on manual trigger). Results are cached in memory and exposed +// to the UI screens via thread-safe getters. +// +// Screens poll dataManager_dataVersion() in their tick_fn to detect changes +// and re-render from cache. Navigation between screens never blocks on a +// fetch — views always render instantly from whatever is cached. + +// Create the background task and trigger the initial fetch. Call once in setup(). +void dataManager_begin(); + +// No-op for now (background task runs independently). Reserved for future use. +void dataManager_tick(); + +// Request a manual refresh (non-blocking). The background task picks this up +// within ~1 second. +void dataManager_triggerRefresh(); + +// Thread-safe copy of the cached weather/calendar data into `out`. +void dataManager_getWeather(WeatherData &out); +void dataManager_getCalendar(CalendarData &out); + +// Status flags (safe to read from the main loop without a mutex). +bool dataManager_isWeatherOk(); +bool dataManager_isCalendarOk(); +bool dataManager_isLoading(); +uint32_t dataManager_dataVersion(); +unsigned long dataManager_lastFetchMs(); diff --git a/src/main.cpp b/src/main.cpp index 9edd75f..56b82a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include #include "display/display_setup.h" +#include "data/data_manager.h" #include "ui/screen_base.h" #include "ui/home_screen.h" #include "ui/weather_detail_screen.h" @@ -53,6 +54,9 @@ void setup() { display_init(); connectWifi(); + // Start background data fetcher (weather + calendar every 10 min). + dataManager_begin(); + // Build all four screens and wire navigation callbacks. screens[(int)ScreenId::HOME] = homeScreen_make(); screens[(int)ScreenId::WEATHER_DETAIL] = weatherDetailScreen_make(); @@ -73,6 +77,7 @@ void loop() { if (now - lastTickMs >= 5) { lastTickMs = now; display_loop(); + dataManager_tick(); screens[(int)current].tick(); // Inactivity: return to home after 5 minutes without input. diff --git a/src/ui/calendar_detail_screen.cpp b/src/ui/calendar_detail_screen.cpp index 7951fee..201b742 100644 --- a/src/ui/calendar_detail_screen.cpp +++ b/src/ui/calendar_detail_screen.cpp @@ -1,6 +1,6 @@ #include "calendar_detail_screen.h" #include "ui/widgets/back_button.h" -#include "api/calendar_api.h" +#include "data/data_manager.h" #include #include #include @@ -11,24 +11,28 @@ namespace { ScreenId (*g_navigate)(ScreenId) = nullptr; void on_back(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::HOME); } -CalendarData lastCalendar; -bool ok = false; lv_obj_t *list = nullptr; void buildList() { if (!list) return; lv_obj_clean(list); + CalendarData c; + dataManager_getCalendar(c); + bool ok = dataManager_isCalendarOk(); + bool loading = dataManager_isLoading(); + if (!ok) { lv_obj_t *lbl = lv_label_create(list); - lv_label_set_text(lbl, LV_SYMBOL_WARNING " Keine Verbindung"); - lv_obj_set_style_text_color(lbl, Theme::accentError(), 0); + lv_label_set_text(lbl, loading ? LV_SYMBOL_REFRESH " Lade..." + : LV_SYMBOL_WARNING " Keine Verbindung"); + lv_obj_set_style_text_color(lbl, loading ? Theme::textDim() : Theme::accentError(), 0); return; } // Columns (content width = 280 - 2*6 pad = 268), side by side like a table: // date 72 px left (dim grey) | time 56 px centered (grey) | summary (white, wraps) - for (const auto &ev : lastCalendar.events) { + for (const auto &ev : c.events) { lv_obj_t *row = lv_obj_create(list); lv_obj_set_size(row, 280, LV_SIZE_CONTENT); lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); @@ -78,13 +82,17 @@ void calendarDetailScreen_create(Screen &s) { } void calendarDetailScreen_refresh(Screen &) { - CalendarData c = fetchCalendar(); - ok = c.valid; - if (c.valid) lastCalendar = c; buildList(); } -void calendarDetailScreen_tick(Screen &) {} +void calendarDetailScreen_tick(Screen &) { + static uint32_t lastVersion = 0; + uint32_t v = dataManager_dataVersion(); + if (v != lastVersion) { + lastVersion = v; + buildList(); + } +} Screen calendarDetailScreen_make() { Screen s; diff --git a/src/ui/home_screen.cpp b/src/ui/home_screen.cpp index c20d80d..aea72a9 100644 --- a/src/ui/home_screen.cpp +++ b/src/ui/home_screen.cpp @@ -1,8 +1,7 @@ #include "home_screen.h" #include "ui/widgets/tile_button.h" #include "icons/icons.h" -#include "api/weather_api.h" -#include "api/calendar_api.h" +#include "data/data_manager.h" #include #include #include @@ -13,21 +12,9 @@ namespace { // --- navigation callback target (set by main.cpp) --- ScreenId (*g_navigate)(ScreenId) = nullptr; -// Forward declarations (definitions below). -void updateWidgets(); -void doFetch(); - void on_calendar_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::CALENDAR_DETAIL); } void on_mvg_tile(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::MVG); } -// --- cached data + refresh timer (ported from old home_screen.cpp) --- -WeatherData lastWeather; -CalendarData lastCalendar; -bool weatherOk = false; -bool calendarOk = false; -unsigned long lastFetchMs = 0; -const unsigned long REFRESH_INTERVAL_MS = 10UL * 60UL * 1000UL; - // --- tile handles (value labels, for updating without rebuild) --- TileButton weatherTile{}; TileButton calendarTile{}; @@ -43,45 +30,28 @@ void rebuildWeatherIcon(const String &symbol, const String &description) { lv_obj_align(weatherTile.icon, LV_ALIGN_CENTER, 0, -14); } -void updateRainWarning() { +void updateRainWarning(const WeatherData &w, bool ok) { if (rainWarningIcon) { lv_obj_del(rainWarningIcon); rainWarningIcon = nullptr; } - if (weatherOk && willRainSoon(lastWeather, 8)) { + if (ok && willRainSoon(w, 8)) { rainWarningIcon = Icons::createRainWarning(weatherTile.btn, 28); lv_obj_align(rainWarningIcon, LV_ALIGN_TOP_RIGHT, -4, 4); } } -void doFetch() { - WeatherData w = fetchWeather(); - weatherOk = w.valid; - if (w.valid) lastWeather = w; - CalendarData c = fetchCalendar(); - calendarOk = c.valid; - if (c.valid) lastCalendar = c; - lastFetchMs = millis(); -} - // Manual retry: tapping the weather tile when it is in error state triggers -// a refresh instead of navigating away. Acts as the SPEC.md "retry per tap". -void on_weather_tile(lv_event_t *e) { - if (!weatherOk) { - doFetch(); - updateWidgets(); +// a background refresh instead of navigating away. +void on_weather_tile(lv_event_t *) { + if (!dataManager_isWeatherOk()) { + dataManager_triggerRefresh(); return; } if (g_navigate) g_navigate(ScreenId::WEATHER_DETAIL); } -// --- formatting helpers (ported from old home_screen.cpp rendering) --- - -String weatherIconGlyph(const String &symbol) { - // Kept for fallback; real icons are drawn as canvas via rebuildWeatherIcon. - (void)symbol; - return LV_SYMBOL_IMAGE; -} +// --- formatting helpers --- String formatWeatherValue(const WeatherData &w) { if (!w.valid) return "Wetter n.a."; @@ -96,9 +66,9 @@ String formatCalendarPreview(const CalendarData &c) { int shown = 0; for (const auto &ev : c.events) { if (shown >= 2) break; - if (shown > 0) s += "\n\n"; - s += DateUtils::formatShortDE(ev.startAt, ev.allDay); - s += "\n" + sanitizeGermanText(ev.summary.substring(0, 22)); + if (shown > 0) s += "\n"; + s += sanitizeGermanText(ev.summary.substring(0, 22)); + s += "\n" + DateUtils::formatShortDE(ev.startAt, ev.allDay); ++shown; } if (s.length() == 0) s = "Keine Termine"; @@ -107,12 +77,38 @@ String formatCalendarPreview(const CalendarData &c) { void updateWidgets() { if (!widgetsBuilt) return; - rebuildWeatherIcon(lastWeather.current.symbol, lastWeather.current.description); - updateRainWarning(); - tile_button_set_value(weatherTile, formatWeatherValue(lastWeather).c_str()); + + WeatherData w; + CalendarData c; + dataManager_getWeather(w); + dataManager_getCalendar(c); + bool weatherOk = dataManager_isWeatherOk(); + bool calendarOk = dataManager_isCalendarOk(); + bool loading = dataManager_isLoading(); + + // Weather tile + if (weatherOk) { + rebuildWeatherIcon(w.current.symbol, w.current.description); + updateRainWarning(w, true); + tile_button_set_value(weatherTile, formatWeatherValue(w).c_str()); + } else if (loading) { + tile_button_set_value(weatherTile, "Lade..."); + } else { + tile_button_set_value(weatherTile, "Wetter n.a."); + } lv_obj_align(weatherTile.value, LV_ALIGN_CENTER, 0, 50); - tile_button_set_value(calendarTile, formatCalendarPreview(lastCalendar).c_str()); + + // Calendar tile + if (calendarOk) { + tile_button_set_value(calendarTile, formatCalendarPreview(c).c_str()); + } else if (loading) { + tile_button_set_value(calendarTile, "Lade..."); + } else { + tile_button_set_value(calendarTile, "Kalender n.a."); + } tile_button_set_icon(calendarTile, LV_SYMBOL_FILE); + + // MVG tile tile_button_set_value(mvgTile, "Abfahrten"); } @@ -148,13 +144,14 @@ void homeScreen_create(Screen &s) { } void homeScreen_refresh(Screen &) { - doFetch(); updateWidgets(); } void homeScreen_tick(Screen &) { - if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) { - doFetch(); + static uint32_t lastVersion = 0; + uint32_t v = dataManager_dataVersion(); + if (v != lastVersion) { + lastVersion = v; updateWidgets(); } } @@ -166,4 +163,3 @@ Screen homeScreen_make() { s.tick_fn = homeScreen_tick; return s; } - diff --git a/src/ui/weather_detail_screen.cpp b/src/ui/weather_detail_screen.cpp index 1bb05e4..a2aaaa8 100644 --- a/src/ui/weather_detail_screen.cpp +++ b/src/ui/weather_detail_screen.cpp @@ -1,6 +1,6 @@ #include "weather_detail_screen.h" #include "ui/widgets/back_button.h" -#include "api/weather_api.h" +#include "data/data_manager.h" #include "icons/icons.h" #include #include @@ -11,18 +11,22 @@ namespace { ScreenId (*g_navigate)(ScreenId) = nullptr; void on_back(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::HOME); } -WeatherData lastWeather; -bool ok = false; lv_obj_t *list = nullptr; void buildList() { if (!list) return; lv_obj_clean(list); + WeatherData w; + dataManager_getWeather(w); + bool ok = dataManager_isWeatherOk(); + bool loading = dataManager_isLoading(); + if (!ok) { lv_obj_t *lbl = lv_label_create(list); - lv_label_set_text(lbl, LV_SYMBOL_WARNING " Keine Verbindung"); - lv_obj_set_style_text_color(lbl, Theme::accentError(), 0); + lv_label_set_text(lbl, loading ? LV_SYMBOL_REFRESH " Lade..." + : LV_SYMBOL_WARNING " Keine Verbindung"); + lv_obj_set_style_text_color(lbl, loading ? Theme::textDim() : Theme::accentError(), 0); return; } @@ -39,7 +43,7 @@ void buildList() { lv_obj_set_style_line_color(header_line, Theme::textDim(), 0); lv_obj_set_style_line_width(header_line, 1, 0); - for (const auto &fe : lastWeather.forecast) { + for (const auto &fe : w.forecast) { lv_obj_t *row = lv_obj_create(list); lv_obj_set_size(row, 280, 30); lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); @@ -105,13 +109,17 @@ void weatherDetailScreen_create(Screen &s) { } void weatherDetailScreen_refresh(Screen &) { - WeatherData w = fetchWeather(); - ok = w.valid; - if (w.valid) lastWeather = w; buildList(); } -void weatherDetailScreen_tick(Screen &) {} +void weatherDetailScreen_tick(Screen &) { + static uint32_t lastVersion = 0; + uint32_t v = dataManager_dataVersion(); + if (v != lastVersion) { + lastVersion = v; + buildList(); + } +} Screen weatherDetailScreen_make() { Screen s;