mirror of
https://github.com/skoelle/wt32sc01-dashboard.git
synced 2026-09-17 17:30:25 +00:00
feat: add data manager system for background weather/calendar fetching
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
#include "data_manager.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
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; }
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#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();
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <secrets.h>
|
||||
#include <theme.h>
|
||||
#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.
|
||||
|
||||
@@ -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 <theme.h>
|
||||
#include <text_utils.h>
|
||||
#include <date_utils.h>
|
||||
@@ -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;
|
||||
|
||||
+45
-49
@@ -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 <theme.h>
|
||||
#include <text_utils.h>
|
||||
#include <date_utils.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <theme.h>
|
||||
#include <text_utils.h>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user