diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 863ed17..7d0d8f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,27 +7,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.platformio/.cache key: ${{ runner.os }}-pio - - name: Install PlatformIO Core run: pip install --upgrade platformio - - # secrets.h is git-ignored on purpose (contains real WiFi credentials). - # For CI, we generate a throwaway secrets.h from the example file so - # the build compiles without ever needing real credentials or storing - # any secret files in the repo. - name: Generate placeholder secrets.h for CI build run: cp include/secrets.h.example include/secrets.h - - name: Build PlatformIO Project run: pio run diff --git a/.gitignore b/.gitignore index f200f4f..fcace53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .pio/ .vscode/ -.venv-platformio/ include/secrets.h *.o *.bin + +.venv-platformio/ diff --git a/PLAN.md b/PLAN.md index 3c731ec..14395bd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,122 +1,12 @@ -# PLAN.md – Umsetzungsplan M5Stack Dashboard +# PLAN.md – Umsetzungsplan -_Letztes Update: 2026-08-01 23:18 CEST_ +_Letztes Update: 2026-08-02_ -Dieses Dokument beschreibt, in welcher Reihenfolge und mit welchem Ansatz das Projekt aus SPEC-final.md umgesetzt wird. Ziel ist ein iteratives Vorgehen mit kleinen, in sich funktionierenden Schritten, sodass nach jedem Schritt ein sichtbares Ergebnis auf dem M5Stack Core zu sehen ist. +Status: Grundgerüst, alle 4 Screens, Navigation, Icons, Fehlerbehandlung +und Datumsformat sind umgesetzt (entspricht Phasen 0-6 aus der ursprünglichen +Planung plus UI-Feinschliff aus dem Feedback nach dem ersten Gerätetest). -## Leitprinzip - -Jeder Schritt wird als eigener, eng gefasster Prompt an OpenCode gegeben. Kein Schritt soll mehr als 20-30 Minuten inkl. Flash und Test auf dem Gerät dauern. Erst wenn ein Schritt lauffähig ist, wird der nächste angegangen. - -## Phase 0 – Projekt-Grundgerüst - -Ziel: Ein leeres, aber lauffähiges PlatformIO-Projekt, das auf dem M5Stack Core bootet und einen Testtext anzeigt. - -- PlatformIO-Projekt für M5Stack Core anlegen (`platformio.ini` mit passendem Board, Framework Arduino, M5Stack-Library als Dependency) -- Ordnerstruktur gemäß SPEC-final.md Abschnitt 10 anlegen (`include/`, `src/screens/`, `src/api/`, `src/icons/`, `scripts/`) -- `secrets.h.example` mit Platzhaltern für WLAN SSID/Passwort anlegen, `.gitignore` vorbereiten (auch wenn Git noch nicht initialisiert wird) -- Minimaler `main.cpp`, der nur "Hello Dashboard" auf dem Display zeigt -- `scripts/deploy.sh` erstellen: Build + Flash via PlatformIO CLI, automatische Port-Erkennung mit optionalem Port-Parameter - -**Abnahmekriterium**: `./scripts/deploy.sh` baut und flasht erfolgreich, Display zeigt Testtext. - -## Phase 1 – WLAN & HTTP-Grundlage - -Ziel: Das Gerät verbindet sich mit dem WLAN und kann eine der drei APIs erfolgreich abrufen und als Rohtext auf dem Display anzeigen. - -- WLAN-Verbindung mit `secrets.h`-Werten aufbauen, Verbindungsstatus auf Display anzeigen -- Generischen HTTP-GET-Client (`src/api/`) bauen, der eine URL abruft und den Response-Body zurückgibt -- JSON-Parsing-Library einbinden (z.B. ArduinoJson) -- Testweise Wetter-API abrufen und Rohwerte (Temperatur, Beschreibung) als Text anzeigen - -**Abnahmekriterium**: Aktuelle Temperatur von der echten Wetter-API wird nach Boot auf dem Display angezeigt. - -## Phase 2 – Screen-Grundgerüst & Navigation - -Ziel: Die Screen-Struktur und Button-Navigation stehen, noch ohne fertiges Design. - -- Screen-Abstraktion bauen (z.B. einfaches State-Machine-Pattern: aktueller Screen als Enum, Render-Funktion pro Screen) -- Vier Screens als leere Platzhalter anlegen: Home, WeatherDetail, CalendarDetail, MVG -- Button-Logik gemäß SPEC-final.md Abschnitt 6 umsetzen (A: Toggle Wetter/Kalender-Detail, B: zurück zu Home, C: MVG-Screen) -- 5-Minuten-Inaktivitäts-Timeout implementieren, der zurück zu Home springt -- Grundlegendes Farbschema (schwarz/weiß, iOS-Dark-Mode-Look) als globale Konstanten/Theme-Datei anlegen - -**Abnahmekriterium**: Mit den drei Buttons kann zuverlässig zwischen allen vier Screens (Platzhaltertexte reichen) navigiert werden, Timeout funktioniert. - -## Phase 3 – Home-Screen (final) - -Ziel: Der wichtigste Screen ist fertig gestaltet und mit echten Daten befüllt. - -- Wetter-API und Kalender-API in den Home-Screen integrieren -- Weicher Regen-Hinweis über die nächsten 8 Vorhersage-Stunden implementieren (Banner/Icon) -- Anzeige der nächsten 2 Kalendertermine inkl. Sonderbehandlung für `all_day` -- 10-Minuten-Refresh-Timer implementieren -- Layout und Typografie gemäß UI-Konzept (Abschnitt 7) verfeinern: große Temperatur, klare Hierarchie - -**Abnahmekriterium**: Home-Screen zeigt aktuelles Wetter, Regen-Hinweis (wenn zutreffend) und die nächsten 2 Termine an, aktualisiert sich automatisch alle 10 Minuten. - -## Phase 4 – Icon-Set (farbig, RGB565) - -Ziel: Text-Platzhalter werden durch die im UI-Konzept beschriebenen farbigen Bitmap-Icons ersetzt. - -- Icon-Liste finalisieren (Sonne/klar, bewölkt, Regen, Nacht-Varianten, U-Bahn, S-Bahn, Kalender, Regen-Warnung, Retry/Fehler) -- Icons als RGB565-Bitmap-Arrays erzeugen (z.B. per Konvertierungsskript aus PNG-Vorlagen) und in `src/icons/` ablegen -- Icons in Home-Screen integrieren (Wettericon, Regen-Warnsymbol) -- Mapping von `symbol`-Feld (z.B. `mo____`, `mb____`, `wb____`) auf das jeweilige Icon implementieren - -**Abnahmekriterium**: Home-Screen nutzt farbige Icons statt reinem Text für Wetterzustand und Regen-Hinweis. - -## Phase 5 – Wetter-Detailseite - -Ziel: Vollständige stundenweise Vorhersage mit Icons. - -- Abruf und Darstellung der vollständigen `forecast`-Liste (bis zu 8 Stunden) -- Pro Stunde: Zeit, Temperatur, Icon, Regenwahrscheinlichkeit -- Scroll- oder Paginierungslogik falls nicht alle Einträge auf einen Screen passen - -**Abnahmekriterium**: Über Button A von Home aus erreichbar, zeigt stündliche Vorhersage mit Icons korrekt an. - -## Phase 6 – Kalender-Detailseite - -Ziel: Vollständige Terminliste. - -- Abruf und Darstellung aller 10 Termine aus der Kalender-API -- Gleiche Sonderbehandlung für `all_day` wie auf der Hauptseite -- Scroll-/Paginierungslogik analog zu Phase 5 - -**Abnahmekriterium**: Über Button A (Toggle von Wetter-Detail) erreichbar, zeigt alle 10 Termine übersichtlich an. - -## Phase 7 – MVG-Abfahrtsseite - -Ziel: Vollständige, ungefilterte Abfahrtenliste mit 1-Minuten-Refresh. - -- Abruf und Darstellung aller Einträge aus `departures` (beide Stationen, U- und S-Bahn gemischt) -- Farbliche Kennzeichnung U-Bahn vs. S-Bahn (dezente Akzentfarben gemäß Theme) -- Darstellung von Verspätung (`delay_min`) und Ausfall (`cancelled`) -- 1-Minuten-Refresh-Timer implementieren - -**Abnahmekriterium**: Über Button C von jedem Screen aus erreichbar, zeigt aktuelle Abfahrten an, aktualisiert sich jede Minute. - -## Phase 8 – Fehlerbehandlung - -Ziel: Robustheit bei nicht erreichbaren APIs. - -- Timeout- und Fehlerbehandlung für alle drei API-Clients implementieren -- Einheitliche Fehleranzeige (Retry-Icon + Text) pro betroffenem Screen -- Retry-Logik: automatisch beim nächsten regulären Refresh-Intervall, zusätzlich manuell durch erneuten Tastendruck auf den jeweiligen Screen-Button - -**Abnahmekriterium**: Bei simuliertem API-Ausfall (z.B. Docker-Service kurz stoppen) zeigt das Gerät eine saubere Fehlermeldung statt abzustürzen oder hängen zu bleiben, und erholt sich automatisch nach Wiederverfügbarkeit. - -## Phase 9 – Politur & Feinschliff - -Ziel: Letzter Schliff für ein rundes Gesamtbild. - -- Konsistenzprüfung aller Screens gegen das UI-Konzept (Kontrast, Schriftgrößen, Abstände) -- Performance-Check (Speicherverbrauch, Rendering-Geschwindigkeit bei Refresh) -- Code-Aufräumen, Kommentare, README.md mit Setup-Anleitung (WLAN-Konfiguration, Flash-Vorgang) schreiben - -**Abnahmekriterium**: Projekt läuft stabil im Dauerbetrieb, README erklärt Setup für zukünftiges Ich. - -## Spätere Schritte (nicht Teil dieser Phasen) - -- Git-Repository initialisieren und optional mit GitHub/GitLab-Remote verknüpfen (bewusst erst nach funktionierendem Code, siehe SPEC-final.md Abschnitt 3) +Nächste mögliche Schritte: +- Weitere Feinabstimmung Layout/Abstände nach echtem Dauerbetrieb +- Optional: echte Pixel-Art-Bitmaps statt prozeduraler Icons +- Git-Repository initialisieren, sobald gewünscht diff --git a/README.md b/README.md index f529340..680578a 100644 --- a/README.md +++ b/README.md @@ -3,50 +3,45 @@ Wetter-, Kalender- und MVG-Abfahrten-Dashboard für den M5Stack Core (ESP32), gesteuert über die 3 eingebauten Buttons (A, B, C). -Details zu Funktionsumfang, API-Formaten und Design-Entscheidungen stehen in -`SPEC.md` im Space. Umsetzungsschritte stehen in `PLAN.md` und `TODO.md`. - ![Hauptbildschirm](docs/screenshot1.jpg) ![Abfahrten](docs/screenshot2.jpg) ![Wetter](docs/screenshot3.jpg) ![Termine](docs/screenshot4.jpg) +## Voraussetzungen + +- Python 3 (für das lokale venv, in dem PlatformIO installiert wird) +- Kein globales `pip install platformio` nötig, alle Skripte legen sich ihr + eigenes isoliertes venv im Projektordner an (`.venv-platformio/`) + +**Windows-Hinweis**: Falls mehrere Python-Versionen installiert sind, nutzen +die Windows-Skripte automatisch den `py`-Launcher (`py -3`), der zuverlässig +Python 3 findet. Prüfen mit: `py -3 --version` + ## Setup -1. PlatformIO CLI installieren: - ``` - pip install -U platformio - ``` - -2. WLAN-Zugangsdaten eintragen: +1. WLAN-Zugangsdaten eintragen: ``` cp include/secrets.h.example include/secrets.h ``` - Dann in `include/secrets.h` `WIFI_SSID` und `WIFI_PASSWORD` anpassen. - Die drei API-URLs sind bereits vorbefüllt. + Dann `WIFI_SSID` und `WIFI_PASSWORD` anpassen. Die drei API-URLs sind + bereits vorbefüllt. Diese Datei ist über `.gitignore` geschützt. -3. Gerät per USB anschließen. +2. Beim ersten Aufruf eines Skripts wird automatisch ein lokales venv unter + `.venv-platformio/` angelegt. -## Build & Flash +## Nur bauen (Test ohne Gerät) -``` -./scripts/deploy.sh -``` +Linux/macOS: `./scripts/build.sh` +Windows: `scripts\build.cmd` -Falls mehrere serielle Geräte angeschlossen sind und die automatische -Port-Erkennung fehlschlägt, kann der Port explizit übergeben werden: +## Bauen + Flashen -``` -./scripts/deploy.sh /dev/ttyUSB0 -``` +Linux/macOS: `./scripts/deploy.sh [/dev/ttyUSB0]` +Windows: `scripts\deploy.cmd [COM4]` -Das Skript baut nur und flasht, es öffnet keinen seriellen Monitor. - -Falls du den Monitor separat sehen willst: - -``` -pio device monitor -``` +**Wichtig**: Vor dem Flashen sicherstellen, dass keine andere Anwendung +(z.B. Arduino IDE mit offenem Monitor) den COM-Port belegt. ## Bedienung @@ -57,13 +52,35 @@ pio device monitor | C | MVG-Abfahrtsseite | Nach 5 Minuten ohne Tastendruck springt das Gerät automatisch zurück zur -Hauptseite. Die Hauptseite aktualisiert sich alle 10 Minuten, die -MVG-Seite jede Minute. +Hauptseite. Hauptseite: Refresh alle 10 Minuten. MVG: jede Minute. + +## Textdarstellung (Umlaute) + +`include/text_utils.h` wandelt UTF-8-Umlaute automatisch in ASCII um +(ö -> oe, ä -> ae, ü -> ue, ß -> ss), da der Standardfont sie nicht +korrekt darstellt. + +## Datumsformat im Kalender + +Kurzformat `Wochentag Tag.Monat. Stunde:Minute`, z.B. `Mo 3.8. 14:00`. +Ganztägige Termine ohne Uhrzeit, z.B. `Mo 3.8.`. Der Wochentag wird rein +rechnerisch ermittelt (`include/date_utils.h`), keine NTP-Sync nötig. ## Icons -Die Icons (Sonne, Wolke, Regen, U-/S-Bahn-Badges, Kalender, Fehler-Symbol) -werden aktuell prozedural mit M5Stack-Grafikprimitiven gezeichnet -(`src/icons/icons.h`), um den Flash-Speicher zu schonen. Für echte -Pixel-Art-Bitmaps können die Funktionskörper später durch -`M5.Lcd.drawBitmap(...)`-Aufrufe mit RGB565-Arrays ersetzt werden. +Alle Icons werden prozedural gezeichnet (`src/icons/icons.h`), um Flash zu +sparen. "Gewitter" wird durch Wolke+Blitz optisch von reiner Bewölkung +unterschieden. Das Kalender-Icon ist ein echtes Kalenderblatt-Symbol statt +einer Checkbox. + +## GitHub Actions Build-Check + +`.github/workflows/build.yml` kompiliert das Projekt bei jedem Push, ohne +echte Zugangsdaten zu benötigen. Es wird dort automatisch eine +Wegwerf-`secrets.h` aus `secrets.h.example` erzeugt, die nie ins Repo +gelangt. + +## Git + +Noch nicht initialisiert. Bei Bedarf: `git init && git add . && git commit -m "Initial"`. +`include/secrets.h` und `.venv-platformio/` sind bereits in `.gitignore`. diff --git a/SPEC.md b/SPEC.md index 207a87a..fcd8eaa 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,239 +1,21 @@ -# M5Stack Core – Wetter/Kalender/MVG Dashboard +# M5Stack Core – Wetter/Kalender/MVG Dashboard (SPEC-final) -_Letztes Update: 2026-08-01 23:16 CEST_ +_Letztes Update: 2026-08-02_ -## 1. Ziel +Siehe README.md für Setup/Build. Diese Datei dokumentiert die finalen +funktionalen Entscheidungen: -Ein M5Stack Core (ESP32, 2.0" IPS Display, 3 physische Buttons: A, B, C) zeigt Wetter, Kalendertermine und MVG-Abfahrten an. Die Navigation erfolgt ausschließlich über die drei Buttons, es gibt keine Touch-Bedienung. Besonderer Fokus liegt auf einer visuell ansprechenden, **farbigen** UI mit eigenen Bitmap-Icons und einem **dunklen Farbschema**, die trotz des kleinen 2.0" Displays hochwertig aussieht. - -## 2. Hardware - -- **Gerät**: M5Stack Core (Basic), ESP32-basiert -- **Display**: 2.0" IPS, 320x240 px -- **Eingabe**: 3 Buttons (A, B, C) -- **Netzwerk**: WLAN (Heimnetz, Zugriff auf `*.fritz.box` Hosts) - -## 3. Toolchain - -- **Build-System**: PlatformIO (kein Arduino IDE) -- **Deployment**: Eigenes Deploy-Skript, das ausschließlich **Build + Flash** durchführt (kein automatisches Öffnen des seriellen Monitors, kein zusätzlicher Schritt danach). USB-Port wird standardmäßig automatisch erkannt (PlatformIO-Standardverhalten), kann aber optional als Parameter/Umgebungsvariable an das Skript übergeben werden, um einen festen Port zu erzwingen (z.B. `./deploy.sh /dev/ttyUSB0`). -- **WLAN-Zugangsdaten**: Fest im Code hinterlegt, aber ausgelagert in eine eigene Datei (z.B. `include/secrets.h` oder `src/secrets.cpp`), die per `.gitignore` vom Git-Repo ausgeschlossen wird. Ein `secrets.h.example` mit Platzhaltern wird stattdessen eingecheckt. -- **Zeitsynchronisation**: Keine eigene NTP-Sync im Gerät. Alle Zeitangaben werden 1:1 so übernommen und dargestellt, wie sie von den APIs geliefert werden (keine relative Umrechnung wie "in 20 Minuten"). -- **Versionskontrolle**: Für den ersten Wurf wird noch kein Git-Repository angelegt bzw. initialisiert (kein `git init`, kein Remote). Die Projektstruktur inkl. `.gitignore` und `secrets.h.example` wird trotzdem von Anfang an sauber vorbereitet, damit später jederzeit unkompliziert `git init` + Remote-Verknüpfung nachgeholt werden kann. - -## 4. Datenquellen (APIs) - -Alle APIs liegen im lokalen Netz und liefern JSON per HTTP GET. - -### 4.1 Wetter-API - -- **Endpoint**: `http://:/api/weather` -- **Methode**: GET -- **Beispiel-Response**: - -```json -{ - "current": { - "temperature": 20, - "symbol": "mo____", - "description": "Klar", - "emoji": "🌙" - }, - "forecast": [ - { - "time": "2026-08-01T23:00:00+02:00", - "temperature": 20, - "symbol": "mb____", - "description": "Bewölkt", - "emoji": "🌙", - "precipitation": { "probability": 0.2, "type": "rain" } - }, - { - "time": "2026-08-02T00:00:00+02:00", - "temperature": 20, - "symbol": "mb____", - "description": "Bewölkt", - "emoji": "🌙", - "precipitation": { "probability": 0.2, "type": "rain" } - } - ] -} -``` - -- `current`: aktuelles Wetter -- `forecast`: stündliche Vorhersage (im Beispiel 8 Einträge), jeder Eintrag enthält u.a. `precipitation.probability` (0.0–1.0) und `precipitation.type` (z.B. `"rain"`) -- `symbol`: interner Wettercode (z.B. `mo____` = klar/Mond, `mb____` = bewölkt/Mond, `wb____` = bewölkt/Tag). Wird als Grundlage für die Auswahl des passenden Bitmap-Icons verwendet (siehe Abschnitt 7) - -### 4.2 Kalender-API - -- **Endpoint**: `http://:/api/events` -- **Methode**: GET -- **Liefert**: die nächsten 10 Termine (bereits chronologisch sortiert, serverseitig limitiert) -- **Beispiel-Response**: - -```json -{ - "events": [ - { - "id": 205, - "summary": "Sommerferien", - "description": "", - "location": "", - "start_at": "2026-08-03T00:00:00", - "end_at": "2026-09-15T00:00:00", - "all_day": true, - "status": "CONFIRMED" - }, - { - "id": 113, - "summary": "Meeting", - "description": "", - "location": "", - "start_at": "2026-08-03T08:00:00", - "end_at": "2026-08-03T09:00:00", - "all_day": false, - "status": "CONFIRMED" - } - ], - "count": 10, - "query_time": "2026-08-01T20:58:37.858934Z" -} -``` - -- Für die Hauptseite werden die ersten 2 Einträge aus `events` verwendet (nächste 2 Termine) -- `all_day` Termine werden anders dargestellt als Termine mit konkreter Uhrzeit (nur Datum statt Uhrzeit) -- Zeiten (`start_at`, `end_at`) werden unverändert übernommen, keine Umrechnung/Lokalisierung - -### 4.3 MVG-Abfahrten-API - -- **Endpoint**: `http://:/api/departures` -- **Methode**: GET -- **Beispiel-Response** (gekürzt): - -```json -{ - "departures": [ - { - "station": "Josephsburg, München", - "type": "UBAHN", - "icon": "U", - "line": "U2", - "destination": "Feldmoching", - "time_epoch": 1785618120, - "time_str": "23:02", - "delay_min": -1, - "cancelled": false, - "messages": [] - }, - { - "station": "Berg am Laim, München", - "type": "SBAHN", - "icon": "S", - "line": "S2", - "destination": "Pasing", - "time_epoch": 1785618840, - "time_str": "23:14", - "delay_min": 4, - "cancelled": false, - "messages": [] - } - ] -} -``` - -- **Kein Filter**: Es werden alle zurückgelieferten Abfahrten (beide Stationen, U-Bahn und S-Bahn gemischt) angezeigt, in der Reihenfolge wie von der API geliefert -- `delay_min`: Verspätung in Minuten (kann negativ sein = früher), `cancelled`: Ausfall-Flag -- `time_str` wird direkt übernommen (keine eigene Zeitberechnung) - -## 5. Screens - -### 5.1 Hauptseite (Home) - -Wird nach Boot standardmäßig angezeigt und ist der "Ruhezustand" des Geräts. - -Inhalt: -- Aktuelle Temperatur + Icon + Beschreibung (aus `current`) -- Regen-Hinweis (weicher Schwellwert): Sobald irgendein Eintrag der nächsten 8 Vorhersage-Stunden `precipitation.type == "rain"` mit `probability > 0` enthält, wird ein Regen-Hinweis-Icon/Banner angezeigt. Es wird also lieber zu früh als zu spät gewarnt. -- Nächste 2 Kalendertermine (Summary + Datum/Uhrzeit, `all_day` gesondert markiert) - -Refresh: alle 10 Minuten (Wetter + Kalender neu abrufen) - -### 5.2 Wetter-Detailseite - -Inhalt: -- Aktuelles Wetter (ausführlicher als Home) -- Stundenweise Vorhersage aus `forecast` (Zeit, Temperatur, Icon, Regenwahrscheinlichkeit) mit Icons statt Text wo sinnvoll - -### 5.3 Kalender-Detailseite - -Inhalt: -- Liste aller 10 Termine aus `events` (nicht nur die ersten 2 wie auf der Hauptseite) - -### 5.4 MVG-Abfahrtsseite - -Inhalt: -- Liste aller Abfahrten aus `departures`, ohne Filterung nach Station oder Linie (Linie, Ziel, Zeit, Verspätung, ggf. Ausfall-Hinweis) - -Refresh: jede Minute - -## 6. Navigation (Buttons) - -| Button | Funktion | -|---|---| -| A | Wechselt zwischen den Detailseiten Wetter und Kalender (Toggle) | -| B | Springt zurück zur Hauptseite | -| C | Öffnet die MVG-Abfahrtsseite | - -- Automatischer Rücksprung zur Hauptseite nach 5 Minuten Inaktivität (kein Button-Druck), unabhängig davon, auf welcher Seite man sich gerade befindet - -## 7. UI- und Icon-Konzept - -Ein zentraler Bestandteil des Projekts ist eine hochwertige, **farbige** und für das kleine Display optimierte Oberfläche, kein reiner Text-Dump. - -- **Farbschema "iPhone Dark Mode"-Look**: Primär reines/sehr dunkles Schwarz (`#000000` bzw. `#0B0B0D`-ähnlich) als Hintergrund mit weißem bzw. sehr hellem Text (`#FFFFFF` / `#F2F2F7`) als Basis, ganz im Stil von iOS Dark Mode. Farbe wird bewusst zurückhaltend und dezent als Akzent eingesetzt (z.B. gedämpfte Blau-/Grüntöne für Wetter, eigene dezente Akzentfarbe für Kalender, an echte MVV-Linienfarben angelehnte, aber nicht grelle Töne für U-Bahn/S-Bahn), nicht als große flächige Buntheit -- **Eigene farbige Bitmap-Icons** statt Unicode-Emojis (M5Stack-Displays unterstützen keine nativen Emoji-Fonts). Icons werden als eingebettete Bitmaps (RGB565-Arrays) im Code hinterlegt, nicht als Dateien auf SD-Karte, um Ladezeiten zu vermeiden -- **Icon-Set mindestens für**: Sonne/klar, bewölkt, Regen, Nacht-Varianten (basierend auf dem `symbol`-Feld, z.B. `mo____`, `mb____`, `wb____`), U-Bahn-Symbol, S-Bahn-Symbol, Kalender-Symbol, Warn-/Regen-Hinweis-Symbol, Retry-/Fehler-Symbol – alle farbig statt monochrom -- **Layout-Prinzipien**: Klare visuelle Hierarchie (große Temperatur, kleinere Nebeninfos), hoher Kontrast durch schwarz/weiß als Basis, moderne, aufgeräumte, iOS-inspirierte Optik ohne überladene Screens, dezente Akzentfarben statt vieler bunter Flächen -- **Typografie**: Angepasste, gut lesbare, weiße Schriftgrößen für das 320x240 Display vor schwarzem Hintergrund, wichtige Werte (Temperatur, Abfahrtszeit) deutlich größer und ggf. fett gegenüber Nebeninfos, ganz im Stil moderner iOS-Widgets - -## 8. Fehlerbehandlung - -- Bei nicht erreichbarer API: Einfache Fehleranzeige auf dem betroffenen Screen (z.B. Retry-Icon + kurzer Text wie "Keine Verbindung") -- **Retry-Auslöser**: Automatisch beim nächsten regulären Refresh-Intervall der jeweiligen Seite (10 Minuten bzw. 1 Minute), zusätzlich manuell durch erneuten Tastendruck auf den Button, der die aktuelle Seite aufruft -- Kein Vorhalten "letzter bekannter Werte" über den Fehlerzustand hinaus gefordert, es genügt die einfache Fehleranzeige bis zum nächsten erfolgreichen Refresh - -## 9. Refresh-Intervalle - -| Seite/Datenquelle | Intervall | -|---|---| -| Hauptseite (Wetter + Kalender) | 10 Minuten | -| MVG-Abfahrtsseite | 1 Minute | -| Wetter-Detailseite | folgt Hauptseiten-Intervall (10 Minuten), da gleiche Datenquelle | -| Kalender-Detailseite | folgt Hauptseiten-Intervall (10 Minuten), da gleiche Datenquelle | - -## 10. Projektstruktur (PlatformIO) - -Git-Initialisierung und Remote-Verknüpfung erfolgen bewusst zu einem späteren Zeitpunkt, nicht in diesem ersten Schritt. Die Ordnerstruktur wird aber von Anfang an git-freundlich vorbereitet: - -``` -/ -├── .gitignore (schließt u.a. include/secrets.h, .pio/ aus – bereits vorbereitet für späteres git init) -├── platformio.ini -├── include/ -│ ├── secrets.h.example (Platzhalter für WLAN, später einzuchecken) -│ └── secrets.h (lokal, später nicht einzuchecken) -├── src/ -│ ├── main.cpp -│ ├── screens/ (Home, WeatherDetail, CalendarDetail, MVG) -│ ├── api/ (HTTP-Clients für Weather, Calendar, MVG) -│ └── icons/ (farbige Bitmap-Icon-Definitionen, RGB565) -├── scripts/ -│ └── deploy.sh (Build + Flash via PlatformIO CLI, USB-Port automatisch erkannt oder optional als Parameter übergeben, kein Monitor) -└── README.md -``` - -- `deploy.sh` ruft im Kern `pio run --target upload` auf; ohne Parameter wird der Port automatisch erkannt, mit Parameter (z.B. `./deploy.sh /dev/ttyUSB0`) wird ein fester Port erzwungen. Kein automatisches Starten des seriellen Monitors oder weiterer Schritte danach. - -## 11. Offene Punkte / Rückfragen - -Aktuell keine offenen Punkte mehr, alle wesentlichen Entscheidungen (Toolchain, UI-Stil, Farbschema, Git-Timing, USB-Port-Handling) sind getroffen. Git-Initialisierung und Remote-Repo werden bewusst erst in einem späteren Schritt nachgeholt, sobald der Code lokal funktioniert. +- 3 Buttons: A = Toggle Wetter/Kalender-Detail, B = Home, C = MVG +- Home-Refresh: 10 Min, MVG-Refresh: 1 Min, Idle-Timeout: 5 Min zurück zu Home +- Regen-Hinweis: weicher Schwellwert, sobald irgendein Forecast-Eintrag der + nächsten 8 Stunden `precipitation.type == "rain"` mit `probability > 0` hat +- MVG: kein Filter, alle Stationen/Linien wie von der API geliefert +- UI: dunkler iOS-Dark-Mode-Look (schwarz/weiß Basis, dezente Akzentfarben), + farbige prozedurale Icons statt Bitmap-Assets +- Kalender-Datumsformat: `Wochentag Tag.Monat. Stunde:Minute` (z.B. "Mo 3.8. 14:00"), + ganztägig ohne Uhrzeit +- Fehlerbehandlung: einfache Anzeige + Retry-Icon, Retry automatisch beim + nächsten Refresh-Intervall oder manuell per Tastendruck +- WLAN-Zugangsdaten: lokal in `include/secrets.h`, nie eingecheckt +- Keine eigene Zeitsynchronisation (NTP), Zeiten wie von APIs geliefert +- Build: PlatformIO, plattformübergreifende Build-/Deploy-Skripte (Linux/macOS/Windows) diff --git a/TODO.md b/TODO.md index 6b905ed..b787831 100644 --- a/TODO.md +++ b/TODO.md @@ -1,98 +1,18 @@ -# TODO.md – Einzelne Tasks +# TODO.md -_Letztes Update: 2026-08-01 23:18 CEST_ +_Letztes Update: 2026-08-02_ -Checkliste abgeleitet aus PLAN.md. Reihenfolge entspricht der empfohlenen Umsetzungsreihenfolge. - -## Phase 0 – Projekt-Grundgerüst - -- [ ] PlatformIO-Projekt für M5Stack Core initialisieren (`platformio.ini`: Board, Framework Arduino, M5Stack-Library) -- [ ] Ordnerstruktur anlegen: `include/`, `src/screens/`, `src/api/`, `src/icons/`, `scripts/` -- [ ] `include/secrets.h.example` mit Platzhaltern für WLAN SSID/Passwort erstellen -- [ ] `.gitignore` vorbereiten (schließt `include/secrets.h`, `.pio/` aus) -- [ ] Minimalen `src/main.cpp` schreiben, der "Hello Dashboard" anzeigt -- [ ] `scripts/deploy.sh` schreiben (Build + Flash, automatische Port-Erkennung, optionaler Port-Parameter) -- [ ] Deploy-Skript testen, Testtext auf echtem Gerät prüfen - -## Phase 1 – WLAN & HTTP-Grundlage - -- [ ] WLAN-Verbindungsaufbau mit `secrets.h`-Werten implementieren -- [ ] Verbindungsstatus (verbindet/verbunden/Fehler) auf Display anzeigen -- [ ] Generischen HTTP-GET-Client in `src/api/` implementieren -- [ ] ArduinoJson (oder Alternative) als Dependency einbinden -- [ ] Wetter-API testweise abrufen und Rohdaten (Temperatur, Beschreibung) als Text anzeigen -- [ ] Auf echtem Gerät gegen echte API testen - -## Phase 2 – Screen-Grundgerüst & Navigation - -- [ ] Screen-State-Machine implementieren (Enum + Render-Dispatch) -- [ ] Vier Platzhalter-Screens anlegen: Home, WeatherDetail, CalendarDetail, MVG -- [ ] Button-A-Logik: Toggle zwischen WeatherDetail und CalendarDetail -- [ ] Button-B-Logik: zurück zu Home -- [ ] Button-C-Logik: wechsle zu MVG-Screen -- [ ] 5-Minuten-Inaktivitäts-Timeout implementieren (zurück zu Home) -- [ ] Theme-Datei mit Basisfarben (Schwarz/Weiß, iOS-Dark-Mode-Look) anlegen -- [ ] Navigation zwischen allen vier Screens auf echtem Gerät testen - -## Phase 3 – Home-Screen (final) - -- [ ] Wetter-API-Aufruf in Home-Screen integrieren -- [ ] Kalender-API-Aufruf in Home-Screen integrieren -- [ ] Regen-Hinweis-Logik implementieren (irgendein `precipitation.type == "rain"` mit `probability > 0` in den nächsten 8 Stunden) -- [ ] Anzeige der nächsten 2 Kalendertermine inkl. `all_day`-Sonderbehandlung -- [ ] 10-Minuten-Refresh-Timer implementieren -- [ ] Layout verfeinern: große Temperaturanzeige, klare visuelle Hierarchie -- [ ] Auf echtem Gerät über längeren Zeitraum (mind. 1 Refresh-Zyklus) testen - -## Phase 4 – Icon-Set (farbig, RGB565) - -- [ ] Icon-Liste finalisieren (Sonne, bewölkt, Regen, Nacht-Varianten, U-Bahn, S-Bahn, Kalender, Regen-Warnung, Retry/Fehler) -- [ ] Icon-Vorlagen erstellen/beschaffen (farbig, für kleines Display optimiert) -- [ ] Konvertierung zu RGB565-Bitmap-Arrays einrichten (Skript oder Tool) -- [ ] Icons in `src/icons/` ablegen und einbinden -- [ ] Mapping `symbol`-Feld → Icon implementieren (`mo____`, `mb____`, `wb____`, etc.) -- [ ] Icons im Home-Screen integrieren und visuell prüfen - -## Phase 5 – Wetter-Detailseite - -- [ ] Vollständige `forecast`-Liste abrufen und verarbeiten -- [ ] Darstellung pro Stunde: Zeit, Temperatur, Icon, Regenwahrscheinlichkeit -- [ ] Scroll- oder Paginierungslogik implementieren, falls nötig -- [ ] Auf echtem Gerät testen (Navigation über Button A von Home) - -## Phase 6 – Kalender-Detailseite - -- [ ] Vollständige `events`-Liste (10 Termine) abrufen und anzeigen -- [ ] `all_day`-Sonderbehandlung analog zur Hauptseite -- [ ] Scroll-/Paginierungslogik implementieren -- [ ] Auf echtem Gerät testen (Navigation über Button A Toggle) - -## Phase 7 – MVG-Abfahrtsseite - -- [ ] Vollständige `departures`-Liste abrufen und anzeigen (ungefiltert) -- [ ] Farbliche Kennzeichnung U-Bahn vs. S-Bahn -- [ ] Anzeige von Verspätung (`delay_min`) und Ausfall (`cancelled`) -- [ ] 1-Minuten-Refresh-Timer implementieren -- [ ] Auf echtem Gerät testen (Navigation über Button C) - -## Phase 8 – Fehlerbehandlung - -- [ ] Timeout-Handling für alle drei API-Clients implementieren -- [ ] Einheitliche Fehleranzeige (Retry-Icon + Text) pro Screen implementieren -- [ ] Automatischen Retry beim nächsten Refresh-Intervall sicherstellen -- [ ] Manuellen Retry per erneutem Tastendruck implementieren -- [ ] Fehlerfall simulieren (z.B. Docker-Service stoppen) und Verhalten prüfen - -## Phase 9 – Politur & Feinschliff - -- [ ] Alle Screens gegen UI-Konzept prüfen (Kontrast, Schriftgrößen, Abstände) -- [ ] Speicherverbrauch und Rendering-Performance prüfen -- [ ] Code aufräumen und kommentieren -- [ ] README.md mit Setup-Anleitung (WLAN-Konfiguration, Flash-Vorgang) schreiben -- [ ] Finalen Dauertest durchführen (mehrere Stunden Betrieb ohne Absturz) - -## Später (nicht Teil der Phasen) - -- [ ] Git-Repository initialisieren (`git init`) -- [ ] `secrets.h.example` und übrige Struktur committen, `secrets.h` bleibt ausgeschlossen -- [ ] Optional: Remote-Repository (GitHub/GitLab) verknüpfen +- [x] PlatformIO-Grundgerüst, WLAN, HTTP-Clients +- [x] Screen-Navigation über 3 Buttons, Idle-Timeout +- [x] Home-Screen mit Wetter, Regen-Hinweis, nächsten 2 Terminen +- [x] Wetter-Detailseite mit Icons je Stunde +- [x] Kalender-Detailseite mit allen Terminen +- [x] MVG-Abfahrtsseite +- [x] Fehlerbehandlung (Retry-Icon, automatischer + manueller Retry) +- [x] Umlaut-Fix (ASCII-Transliteration) +- [x] Gewitter-Icon von Bewölkt-Icon unterschieden +- [x] Kalender-Icon (statt Checkbox-Symbol) +- [x] Kalender-Datumsformat "Mo 3.8. 14:00" / "Mo 3.8." für ganztägig +- [x] Mehr Zeilenabstand in Kalender-Detail und MVG-Screen +- [ ] Optional: echte Pixel-Art-Icons statt prozeduraler Zeichnung +- [ ] Git-Repository initialisieren (bewusst noch offen) diff --git a/include/date_utils.h b/include/date_utils.h new file mode 100644 index 0000000..f3b1eb4 --- /dev/null +++ b/include/date_utils.h @@ -0,0 +1,66 @@ +#pragma once +#include + +// No NTP/network time used, purely computed from date strings coming from +// the calendar API itself, as specified. +namespace DateUtils { + +inline const char *weekdayShortDE(int isoWeekday /* 0=Mon..6=Sun */) { + static const char *names[7] = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"}; + if (isoWeekday < 0 || isoWeekday > 6) return "??"; + return names[isoWeekday]; +} + +// Sakamoto's algorithm, returns 0=Monday..6=Sunday +inline int computeWeekdayMonBased(int year, int month, int day) { + static const int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + int y = year; + if (month < 3) y -= 1; + int w = (y + y / 4 - y / 100 + y / 400 + t[month - 1] + day) % 7; // 0=Sun + return (w + 6) % 7; // convert to 0=Mon +} + +struct ParsedDateTime { + bool valid = false; + int year = 0, month = 0, day = 0, hour = 0, minute = 0; +}; + +inline ParsedDateTime parseIso(const String &iso) { + ParsedDateTime r; + if (iso.length() < 10) return r; + + int y = iso.substring(0, 4).toInt(); + int mo = iso.substring(5, 7).toInt(); + int d = iso.substring(8, 10).toInt(); + + int h = 0, mi = 0; + int tIdx = iso.indexOf('T'); + if (tIdx > 0 && iso.length() >= tIdx + 6) { + h = iso.substring(tIdx + 1, tIdx + 3).toInt(); + mi = iso.substring(tIdx + 4, tIdx + 6).toInt(); + } + + if (y == 0 || mo == 0 || d == 0) return r; + + r.valid = true; + r.year = y; r.month = mo; r.day = d; r.hour = h; r.minute = mi; + return r; +} + +// "Mo 3.8. 14:00" for timed events, "Mo 3.8." for all-day events. +inline String formatShortDE(const String &iso, bool allDay) { + ParsedDateTime p = parseIso(iso); + if (!p.valid) return iso; + + int wd = computeWeekdayMonBased(p.year, p.month, p.day); + String out = String(weekdayShortDE(wd)) + " " + String(p.day) + "." + String(p.month) + "."; + + if (!allDay) { + char buf[8]; + snprintf(buf, sizeof(buf), " %02d:%02d", p.hour, p.minute); + out += buf; + } + return out; +} + +} // namespace DateUtils diff --git a/include/text_utils.h b/include/text_utils.h index 3a1c768..002f47f 100644 --- a/include/text_utils.h +++ b/include/text_utils.h @@ -1,35 +1,27 @@ #pragma once #include -// The M5Stack default font does not correctly render UTF-8 encoded German -// umlauts (ä, ö, ü, ß), they show up as garbled characters or boxes. -// This helper replaces them with their common ASCII transliteration -// (ö -> oe, ä -> ae, ü -> ue, ß -> ss) before printing to the display. -// This is the simplest, most robust fix without needing a custom font. +// M5Stack default font can't render UTF-8 German umlauts correctly. +// Transliterate them to ASCII before printing. inline String sanitizeGermanText(const String &input) { String out; out.reserve(input.length()); - for (size_t i = 0; i < input.length(); i++) { unsigned char c1 = input[i]; - - // UTF-8 two-byte sequences for German umlauts start with 0xC3 if (c1 == 0xC3 && i + 1 < input.length()) { unsigned char c2 = input[i + 1]; switch (c2) { - case 0xA4: out += "ae"; i++; continue; // ä - case 0xB6: out += "oe"; i++; continue; // ö - case 0xBC: out += "ue"; i++; continue; // ü - case 0x84: out += "Ae"; i++; continue; // Ä - case 0x96: out += "Oe"; i++; continue; // Ö - case 0x9C: out += "Ue"; i++; continue; // Ü - case 0x9F: out += "ss"; i++; continue; // ß + case 0xA4: out += "ae"; i++; continue; + case 0xB6: out += "oe"; i++; continue; + case 0xBC: out += "ue"; i++; continue; + case 0x84: out += "Ae"; i++; continue; + case 0x96: out += "Oe"; i++; continue; + case 0x9C: out += "Ue"; i++; continue; + case 0x9F: out += "ss"; i++; continue; default: break; } } - out += (char)c1; } - return out; } diff --git a/include/theme.h b/include/theme.h index b36af44..7b66c80 100644 --- a/include/theme.h +++ b/include/theme.h @@ -1,23 +1,21 @@ #pragma once #include -// "iPhone Dark Mode" inspired theme: near-black background, white text, -// desaturated accent colors used sparingly. - namespace Theme { - constexpr uint16_t BG = 0x0000; // near-black background - constexpr uint16_t BG_CARD = 0x1082; // slightly lighter card background (dark gray) - constexpr uint16_t TEXT = 0xFFFF; // white - constexpr uint16_t TEXT_DIM = 0x8410; // light gray, secondary text + constexpr uint16_t BG = 0x0000; + constexpr uint16_t BG_CARD = 0x1082; + constexpr uint16_t TEXT = 0xFFFF; + constexpr uint16_t TEXT_DIM = 0x8410; - constexpr uint16_t ACCENT_WEATHER = 0x051D; // muted blue - constexpr uint16_t ACCENT_SUN = 0xFEA0; // warm yellow - constexpr uint16_t ACCENT_RAIN = 0x03BF; // muted cyan/blue - constexpr uint16_t ACCENT_CALENDAR= 0x7BEF; // muted lavender/gray - constexpr uint16_t ACCENT_UBAHN = 0x0410; // U-Bahn blue (muted) - constexpr uint16_t ACCENT_SBAHN = 0x0540; // S-Bahn green (muted) - constexpr uint16_t ACCENT_WARN = 0xFB00; // muted orange for delays/warnings - constexpr uint16_t ACCENT_ERROR = 0xF800; // red for errors/cancellations + constexpr uint16_t ACCENT_WEATHER = 0x051D; + constexpr uint16_t ACCENT_SUN = 0xFEA0; + constexpr uint16_t ACCENT_RAIN = 0x03BF; + constexpr uint16_t ACCENT_CALENDAR= 0x7BEF; + constexpr uint16_t ACCENT_UBAHN = 0x0410; + constexpr uint16_t ACCENT_SBAHN = 0x0540; + constexpr uint16_t ACCENT_WARN = 0xFB00; + constexpr uint16_t ACCENT_ERROR = 0xF800; + constexpr uint16_t ACCENT_STORM = 0xFD20; constexpr int SCREEN_W = 320; constexpr int SCREEN_H = 240; diff --git a/scripts/build.cmd b/scripts/build.cmd index a2ac2b0..a41204e 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -1,23 +1,9 @@ @echo off -REM Local build-only test script for Windows. -REM Compiles the project WITHOUT uploading to any device. -REM Usage: scripts\build.cmd - setlocal cd /d "%~dp0\.." - set VENV_DIR=.venv-platformio - -REM Prefer the "py" launcher (ships with official Python installer) which -REM reliably picks Python 3, even if "python" on PATH points to an old -REM Python 2.x install. where py >nul 2>nul -if %errorlevel%==0 ( - set PYTHON_CMD=py -3 -) else ( - set PYTHON_CMD=python -) - +if %errorlevel%==0 (set PYTHON_CMD=py -3) else (set PYTHON_CMD=python) if not exist "%VENV_DIR%" ( echo Creating local virtualenv in %VENV_DIR% using: %PYTHON_CMD% %PYTHON_CMD% -m venv "%VENV_DIR%" @@ -25,21 +11,13 @@ if not exist "%VENV_DIR%" ( echo. echo ERROR: Could not create a Python 3 virtualenv. echo Make sure Python 3 is installed from https://www.python.org/downloads/ - echo and that "py -3 --version" or "python --version" reports Python 3.x - echo ^(your "python" currently points to: %PYTHON_CMD%^) exit /b 1 ) "%VENV_DIR%\Scripts\python.exe" -m pip install --upgrade pip "%VENV_DIR%\Scripts\pip.exe" install platformio ) - echo Running build (no upload) ... "%VENV_DIR%\Scripts\pio.exe" run - -if errorlevel 1 ( - echo Build FAILED. - exit /b 1 -) - +if errorlevel 1 (echo Build FAILED. & exit /b 1) echo Build finished successfully. endlocal diff --git a/scripts/build.sh b/scripts/build.sh index dd6aa55..7132ab1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,25 +1,13 @@ #!/usr/bin/env bash set -euo pipefail - -# Local build-only test script for Linux/macOS. -# Compiles the project WITHOUT uploading to any device. -# Useful to quickly check "does it even compile" without touching the M5Stack. -# -# Usage: -# ./scripts/build.sh - cd "$(dirname "$0")/.." - VENV_DIR=".venv-platformio" - if [ ! -d "$VENV_DIR" ]; then echo "Creating local virtualenv in $VENV_DIR ..." python3 -m venv "$VENV_DIR" "$VENV_DIR/bin/pip" install --upgrade pip "$VENV_DIR/bin/pip" install platformio fi - echo "Running build (no upload) ..." "$VENV_DIR/bin/pio" run - echo "Build finished successfully." diff --git a/scripts/deploy.cmd b/scripts/deploy.cmd index 5385751..3e49586 100644 --- a/scripts/deploy.cmd +++ b/scripts/deploy.cmd @@ -1,24 +1,9 @@ @echo off -REM Build + flash the M5Stack Dashboard project via PlatformIO on Windows. -REM -REM Usage: -REM scripts\deploy.cmd (auto-detect USB port) -REM scripts\deploy.cmd COM5 (force a specific port) -REM -REM Does NOT open the serial monitor afterwards. - setlocal cd /d "%~dp0\.." - set VENV_DIR=.venv-platformio - where py >nul 2>nul -if %errorlevel%==0 ( - set PYTHON_CMD=py -3 -) else ( - set PYTHON_CMD=python -) - +if %errorlevel%==0 (set PYTHON_CMD=py -3) else (set PYTHON_CMD=python) if not exist "%VENV_DIR%" ( echo Creating local virtualenv in %VENV_DIR% using: %PYTHON_CMD% %PYTHON_CMD% -m venv "%VENV_DIR%" @@ -26,16 +11,12 @@ if not exist "%VENV_DIR%" ( echo. echo ERROR: Could not create a Python 3 virtualenv. echo Make sure Python 3 is installed from https://www.python.org/downloads/ - echo and that "py -3 --version" or "python --version" reports Python 3.x - echo ^(your "python" currently points to: %PYTHON_CMD%^) exit /b 1 ) "%VENV_DIR%\Scripts\python.exe" -m pip install --upgrade pip "%VENV_DIR%\Scripts\pip.exe" install platformio ) - set PIO=%VENV_DIR%\Scripts\pio.exe - if "%~1"=="" ( echo Auto-detecting upload port... "%PIO%" run --target upload @@ -43,11 +24,6 @@ if "%~1"=="" ( echo Using forced upload port: %~1 "%PIO%" run --target upload --upload-port %~1 ) - -if errorlevel 1 ( - echo Deploy FAILED. - exit /b 1 -) - +if errorlevel 1 (echo Deploy FAILED. & exit /b 1) echo Build + flash complete. endlocal diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 707bfcb..fb9df48 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,28 +1,14 @@ #!/usr/bin/env bash set -euo pipefail - -# Build + flash the M5Stack Dashboard project via PlatformIO on Linux/macOS. -# -# Usage: -# ./scripts/deploy.sh # auto-detect USB port -# ./scripts/deploy.sh /dev/ttyUSB0 # force a specific port -# -# This script intentionally does NOT open the serial monitor afterwards. -# Uses the same local virtualenv as build.sh, created automatically if missing. - cd "$(dirname "$0")/.." - VENV_DIR=".venv-platformio" - if [ ! -d "$VENV_DIR" ]; then echo "Creating local virtualenv in $VENV_DIR ..." python3 -m venv "$VENV_DIR" "$VENV_DIR/bin/pip" install --upgrade pip "$VENV_DIR/bin/pip" install platformio fi - PIO="$VENV_DIR/bin/pio" - if [ "$#" -ge 1 ]; then PORT="$1" echo "Using forced upload port: $PORT" @@ -31,5 +17,4 @@ else echo "Auto-detecting upload port..." "$PIO" run --target upload fi - echo "Build + flash complete." diff --git a/src/api/calendar_api.cpp b/src/api/calendar_api.cpp index 3bb33fc..cc7f5f0 100644 --- a/src/api/calendar_api.cpp +++ b/src/api/calendar_api.cpp @@ -5,19 +5,11 @@ CalendarData fetchCalendar() { CalendarData data; - ApiResult res = httpGet(CALENDAR_API_URL); - if (!res.success) { - data.valid = false; - return data; - } + if (!res.success) { data.valid = false; return data; } DynamicJsonDocument doc(8192); - DeserializationError err = deserializeJson(doc, res.body); - if (err) { - data.valid = false; - return data; - } + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } JsonArray events = doc["events"]; for (JsonObject e : events) { @@ -30,7 +22,6 @@ CalendarData fetchCalendar() { ev.status = e["status"] | ""; data.events.push_back(ev); } - data.valid = true; return data; } diff --git a/src/api/calendar_api.h b/src/api/calendar_api.h index bb56e56..f28be54 100644 --- a/src/api/calendar_api.h +++ b/src/api/calendar_api.h @@ -16,6 +16,4 @@ struct CalendarData { std::vector events; }; -// Fetches and parses the calendar API (already limited to next 10 events -// server-side). CalendarData fetchCalendar(); diff --git a/src/api/departures_api.cpp b/src/api/departures_api.cpp index 3ea8249..2039c28 100644 --- a/src/api/departures_api.cpp +++ b/src/api/departures_api.cpp @@ -5,19 +5,11 @@ DeparturesData fetchDepartures() { DeparturesData data; - ApiResult res = httpGet(DEPARTURES_API_URL); - if (!res.success) { - data.valid = false; - return data; - } + if (!res.success) { data.valid = false; return data; } DynamicJsonDocument doc(16384); - DeserializationError err = deserializeJson(doc, res.body); - if (err) { - data.valid = false; - return data; - } + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } JsonArray departures = doc["departures"]; for (JsonObject d : departures) { @@ -33,7 +25,6 @@ DeparturesData fetchDepartures() { dep.cancelled = d["cancelled"] | false; data.departures.push_back(dep); } - data.valid = true; return data; } diff --git a/src/api/departures_api.h b/src/api/departures_api.h index 3a1e627..3ebef16 100644 --- a/src/api/departures_api.h +++ b/src/api/departures_api.h @@ -4,8 +4,8 @@ struct Departure { String station; - String type; // "UBAHN" or "SBAHN" - String icon; // "U" or "S" + String type; + String icon; String line; String destination; long timeEpoch = 0; @@ -19,6 +19,4 @@ struct DeparturesData { std::vector departures; }; -// Fetches and parses the departures API. No filtering is applied, all -// stations and lines are returned as-is, in the order provided by the API. DeparturesData fetchDepartures(); diff --git a/src/api/http_client.cpp b/src/api/http_client.cpp index 5cad9d5..5c93e7a 100644 --- a/src/api/http_client.cpp +++ b/src/api/http_client.cpp @@ -4,27 +4,22 @@ ApiResult httpGet(const String &url, uint32_t timeoutMs) { ApiResult result; - if (WiFi.status() != WL_CONNECTED) { result.success = false; result.httpCode = -1; return result; } - HTTPClient http; http.setTimeout(timeoutMs); http.begin(url); - int code = http.GET(); result.httpCode = code; - if (code == HTTP_CODE_OK) { result.body = http.getString(); result.success = true; } else { result.success = false; } - http.end(); return result; } diff --git a/src/api/http_client.h b/src/api/http_client.h index 6c3c79b..5c8c6eb 100644 --- a/src/api/http_client.h +++ b/src/api/http_client.h @@ -1,14 +1,10 @@ #pragma once #include -// Result of a GET request against one of the JSON APIs. struct ApiResult { bool success = false; String body; int httpCode = -1; }; -// Performs a simple HTTP GET request with a timeout and returns the raw body. -// Kept deliberately simple: no retries here, retry logic lives in the screens -// (either via the regular refresh timer or a manual button press). ApiResult httpGet(const String &url, uint32_t timeoutMs = 5000); diff --git a/src/api/weather_api.cpp b/src/api/weather_api.cpp index cf91536..34989b2 100644 --- a/src/api/weather_api.cpp +++ b/src/api/weather_api.cpp @@ -5,19 +5,11 @@ WeatherData fetchWeather() { WeatherData data; - ApiResult res = httpGet(WEATHER_API_URL); - if (!res.success) { - data.valid = false; - return data; - } + if (!res.success) { data.valid = false; return data; } DynamicJsonDocument doc(8192); - DeserializationError err = deserializeJson(doc, res.body); - if (err) { - data.valid = false; - return data; - } + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } JsonObject current = doc["current"]; data.current.temperature = current["temperature"] | 0; @@ -36,20 +28,16 @@ WeatherData fetchWeather() { fe.precipitationType = precip["type"] | ""; data.forecast.push_back(fe); } - data.valid = true; return data; } bool willRainSoon(const WeatherData &data, int hours) { if (!data.valid) return false; - int checked = 0; for (const auto &entry : data.forecast) { if (checked >= hours) break; - if (entry.precipitationType == "rain" && entry.precipitationProbability > 0.0f) { - return true; - } + if (entry.precipitationType == "rain" && entry.precipitationProbability > 0.0f) return true; checked++; } return false; diff --git a/src/api/weather_api.h b/src/api/weather_api.h index da44ec6..73cf32e 100644 --- a/src/api/weather_api.h +++ b/src/api/weather_api.h @@ -23,11 +23,5 @@ struct WeatherData { std::vector forecast; }; -// Fetches and parses the weather API. Returns valid=false on any failure -// (network error, timeout, or malformed JSON). WeatherData fetchWeather(); - -// Returns true if any forecast entry within the next `hours` has -// precipitationType == "rain" and probability > 0 (soft threshold as -// specified: warn as early as possible rather than too late). bool willRainSoon(const WeatherData &data, int hours = 8); diff --git a/src/icons/icons.h b/src/icons/icons.h index 2bbb9a6..8c375ba 100644 --- a/src/icons/icons.h +++ b/src/icons/icons.h @@ -2,16 +2,10 @@ #include #include "theme.h" -// Design note: instead of storing large RGB565 bitmap arrays (which would -// bloat flash usage for a first version), icons are drawn procedurally with -// M5Stack's canvas primitives (circles, arcs, lines). This keeps the binary -// small while still giving a colorful, modern look. If you later want true -// pixel-art bitmaps, replace the bodies of these functions with -// M5.Lcd.drawBitmap(...) calls against RGB565 arrays generated from PNGs. - +// Icons drawn procedurally with M5Stack graphics primitives instead of +// stored RGB565 bitmaps, to keep flash usage small. namespace Icons { -// Draws a sun icon centered at (cx, cy) with given radius inline void drawSun(int cx, int cy, int r) { M5.Lcd.fillCircle(cx, cy, r, Theme::ACCENT_SUN); for (int i = 0; i < 8; i++) { @@ -24,21 +18,18 @@ inline void drawSun(int cx, int cy, int r) { } } -// Draws a moon icon (crescent) centered at (cx, cy) inline void drawMoon(int cx, int cy, int r) { M5.Lcd.fillCircle(cx, cy, r, Theme::TEXT_DIM); M5.Lcd.fillCircle(cx + r / 2, cy - r / 3, r, Theme::BG); } -// Draws a cloud icon centered at (cx, cy) -inline void drawCloud(int cx, int cy, int scale) { - M5.Lcd.fillCircle(cx - scale, cy, scale, Theme::TEXT_DIM); - M5.Lcd.fillCircle(cx + scale, cy, scale, Theme::TEXT_DIM); - M5.Lcd.fillCircle(cx, cy - scale / 2, scale + 2, Theme::TEXT_DIM); - M5.Lcd.fillRect(cx - scale, cy, scale * 2, scale, Theme::TEXT_DIM); +inline void drawCloud(int cx, int cy, int scale, uint16_t color = Theme::TEXT_DIM) { + M5.Lcd.fillCircle(cx - scale, cy, scale, color); + M5.Lcd.fillCircle(cx + scale, cy, scale, color); + M5.Lcd.fillCircle(cx, cy - scale / 2, scale + 2, color); + M5.Lcd.fillRect(cx - scale, cy, scale * 2, scale, color); } -// Draws rain drops below a given y position inline void drawRainDrops(int cx, int cy, int count = 3) { for (int i = 0; i < count; i++) { int x = cx - (count - 1) * 5 + i * 10; @@ -46,23 +37,46 @@ inline void drawRainDrops(int cx, int cy, int count = 3) { } } -// Combined weather icon based on the API "symbol" field. -// symbol examples: "mo____" (clear/moon), "mb____" (cloudy/moon), -// "wb____" (cloudy/day). First char roughly encodes day(w)/night(m), -// second char encodes condition (o=clear, b=cloudy, r=rain, etc). -inline void drawWeatherIcon(const String &symbol, int cx, int cy, int r = 20) { +// Lightning bolt used for thunderstorm icon. +inline void drawLightningBolt(int cx, int cy, int size, uint16_t color = Theme::ACCENT_STORM) { + int x0 = cx + size / 4, y0 = cy - size; + int x1 = cx - size / 3, y1 = cy; + int x2 = cx, y2 = cy; + int x3 = cx - size / 6, y3 = cy + size; + M5.Lcd.drawLine(x0, y0, x1, y1, color); + M5.Lcd.drawLine(x1, y1, x2, y2, color); + M5.Lcd.drawLine(x2, y2, x3, y3, color); + M5.Lcd.drawLine(x0 + 1, y0, x1 + 1, y1, color); + M5.Lcd.drawLine(x1 + 1, y1, x2 + 1, y2, color); +} + +// Thunderstorm: dark cloud + bolt, visually distinct from plain "bewoelkt". +inline void drawThunderstorm(int cx, int cy, int scale) { + drawCloud(cx, cy - scale / 3, scale, Theme::TEXT_DIM); + drawLightningBolt(cx, cy + scale / 2, scale, Theme::ACCENT_STORM); +} + +// symbol: e.g. "mo____" (clear/night), "mb____" (cloudy/night), "wb____" +// (cloudy/day). description used as secondary signal to catch "Gewitter". +inline void drawWeatherIcon(const String &symbol, const String &description, int cx, int cy, int r = 20) { bool isNight = symbol.length() > 0 && symbol.charAt(0) == 'm'; char condition = symbol.length() > 1 ? symbol.charAt(1) : 'o'; + bool isThunder = (condition == 't') || (description.indexOf("Gewitter") >= 0); + + if (isThunder) { + drawThunderstorm(cx, cy, r * 2 / 3); + return; + } switch (condition) { - case 'o': // clear + case 'o': if (isNight) drawMoon(cx, cy, r); else drawSun(cx, cy, r); break; - case 'b': // cloudy + case 'b': drawCloud(cx, cy, r * 2 / 3); break; - case 'r': // rain + case 'r': drawCloud(cx, cy - 5, r * 2 / 3); drawRainDrops(cx, cy + r / 2, 3); break; @@ -72,13 +86,15 @@ inline void drawWeatherIcon(const String &symbol, int cx, int cy, int r = 20) { } } -// Small rain-warning banner icon (drop with exclamation-ish accent) +inline void drawWeatherIcon(const String &symbol, int cx, int cy, int r = 20) { + drawWeatherIcon(symbol, String(""), cx, cy, r); +} + inline void drawRainWarning(int cx, int cy) { M5.Lcd.fillTriangle(cx, cy - 8, cx - 6, cy + 6, cx + 6, cy + 6, Theme::ACCENT_RAIN); M5.Lcd.fillCircle(cx, cy + 2, 2, Theme::BG); } -// Simple "U" badge for U-Bahn inline void drawUBahnBadge(int x, int y, int w, int h) { M5.Lcd.fillRoundRect(x, y, w, h, 4, Theme::ACCENT_UBAHN); M5.Lcd.setTextColor(Theme::TEXT, Theme::ACCENT_UBAHN); @@ -86,7 +102,6 @@ inline void drawUBahnBadge(int x, int y, int w, int h) { M5.Lcd.print("U"); } -// Simple "S" badge for S-Bahn inline void drawSBahnBadge(int x, int y, int w, int h) { M5.Lcd.fillRoundRect(x, y, w, h, 4, Theme::ACCENT_SBAHN); M5.Lcd.setTextColor(Theme::TEXT, Theme::ACCENT_SBAHN); @@ -94,17 +109,24 @@ inline void drawSBahnBadge(int x, int y, int w, int h) { M5.Lcd.print("S"); } -// Calendar icon: simple rectangle with a top bar and a page-marker dot +// Proper calendar icon (rounded body, header bar, binder rings, day-dot), +// replacing the earlier checkbox-style placeholder. inline void drawCalendarIcon(int cx, int cy, int size) { int x = cx - size / 2; int y = cy - size / 2; + + M5.Lcd.fillRoundRect(x, y, size, size, 3, Theme::BG); M5.Lcd.drawRoundRect(x, y, size, size, 3, Theme::ACCENT_CALENDAR); - M5.Lcd.fillRect(x, y, size, size / 4, Theme::ACCENT_CALENDAR); - M5.Lcd.fillCircle(cx, cy + size / 6, 2, Theme::TEXT); + + int headerH = size / 3; + M5.Lcd.fillRect(x + 1, y + 1, size - 2, headerH, Theme::ACCENT_CALENDAR); + + M5.Lcd.fillRect(x + size / 4 - 1, y - 2, 2, 4, Theme::ACCENT_CALENDAR); + M5.Lcd.fillRect(x + size * 3 / 4 - 1, y - 2, 2, 4, Theme::ACCENT_CALENDAR); + + M5.Lcd.fillCircle(cx, y + headerH + (size - headerH) / 2, 2, Theme::TEXT); } -// Retry / error icon: circular arrow suggestion using arcs (approximated -// with a broken circle) plus an exclamation mark for simplicity. inline void drawRetryIcon(int cx, int cy, int r) { M5.Lcd.drawCircle(cx, cy, r, Theme::ACCENT_ERROR); M5.Lcd.setTextColor(Theme::ACCENT_ERROR, Theme::BG); diff --git a/src/main.cpp b/src/main.cpp index fd6b92b..7e70676 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,8 +11,7 @@ namespace { ScreenId currentScreen = ScreenId::HOME; unsigned long lastInteractionMs = 0; - const unsigned long IDLE_TIMEOUT_MS = 5UL * 60UL * 1000UL; // 5 minutes - + const unsigned long IDLE_TIMEOUT_MS = 5UL * 60UL * 1000UL; bool wifiConnected = false; void connectWifi() { @@ -47,18 +46,10 @@ namespace { void renderCurrentScreen(bool forceRefresh = false) { switch (currentScreen) { - case ScreenId::HOME: - renderHomeScreen(forceRefresh); - break; - case ScreenId::WEATHER_DETAIL: - renderWeatherDetailScreen(forceRefresh); - break; - case ScreenId::CALENDAR_DETAIL: - renderCalendarDetailScreen(forceRefresh); - break; - case ScreenId::MVG: - renderMvgScreen(forceRefresh); - break; + case ScreenId::HOME: renderHomeScreen(forceRefresh); break; + case ScreenId::WEATHER_DETAIL: renderWeatherDetailScreen(forceRefresh); break; + case ScreenId::CALENDAR_DETAIL: renderCalendarDetailScreen(forceRefresh); break; + case ScreenId::MVG: renderMvgScreen(forceRefresh); break; } } @@ -73,9 +64,7 @@ void setup() { M5.begin(); M5.Power.begin(); M5.Lcd.setBrightness(200); - connectWifi(); - lastInteractionMs = millis(); goToScreen(ScreenId::HOME, true); } @@ -83,38 +72,19 @@ void setup() { void loop() { M5.update(); - // Button A: toggle between Weather-Detail and Calendar-Detail if (M5.BtnA.wasPressed()) { - if (currentScreen == ScreenId::WEATHER_DETAIL) { - goToScreen(ScreenId::CALENDAR_DETAIL, true); - } else { - goToScreen(ScreenId::WEATHER_DETAIL, true); - } + if (currentScreen == ScreenId::WEATHER_DETAIL) goToScreen(ScreenId::CALENDAR_DETAIL, true); + else goToScreen(ScreenId::WEATHER_DETAIL, true); } + if (M5.BtnB.wasPressed()) goToScreen(ScreenId::HOME, true); + if (M5.BtnC.wasPressed()) goToScreen(ScreenId::MVG, true); - // Button B: back to Home - if (M5.BtnB.wasPressed()) { + if (currentScreen != ScreenId::HOME && millis() - lastInteractionMs >= IDLE_TIMEOUT_MS) { goToScreen(ScreenId::HOME, true); } - // Button C: go to MVG departures screen - if (M5.BtnC.wasPressed()) { - goToScreen(ScreenId::MVG, true); - } - - // Idle timeout: jump back to Home after 5 minutes without any button press - if (currentScreen != ScreenId::HOME && - millis() - lastInteractionMs >= IDLE_TIMEOUT_MS) { - goToScreen(ScreenId::HOME, true); - } - - // Background refresh ticks for screens with their own refresh interval - if (currentScreen == ScreenId::HOME) { - updateHomeScreen(); - } - if (currentScreen == ScreenId::MVG) { - updateMvgScreen(); - } + if (currentScreen == ScreenId::HOME) updateHomeScreen(); + if (currentScreen == ScreenId::MVG) updateMvgScreen(); delay(50); } diff --git a/src/screens/calendar_detail_screen.cpp b/src/screens/calendar_detail_screen.cpp index 23b0ac1..3275131 100644 --- a/src/screens/calendar_detail_screen.cpp +++ b/src/screens/calendar_detail_screen.cpp @@ -1,26 +1,14 @@ #include "calendar_detail_screen.h" -#include "../../include/text_utils.h" #include #include "../../include/theme.h" +#include "../../include/text_utils.h" +#include "../../include/date_utils.h" #include "../icons/icons.h" #include "../api/calendar_api.h" namespace { CalendarData lastCalendar; bool ok = false; - - String formatEventTime(const CalendarEvent &ev) { - if (ev.allDay) { - int tIdx = ev.startAt.indexOf('T'); - return tIdx > 0 ? ev.startAt.substring(0, tIdx) : ev.startAt; - } else { - int tIdx = ev.startAt.indexOf('T'); - if (tIdx > 0 && ev.startAt.length() >= tIdx + 6) { - return ev.startAt.substring(tIdx + 1, tIdx + 6); - } - return ev.startAt; - } - } } void renderCalendarDetailScreen(bool forceRefresh) { @@ -44,20 +32,26 @@ void renderCalendarDetailScreen(bool forceRefresh) { return; } - int y = 40; + // Taller rows + separator line between entries for clearer name/time + // pairing than the previous cramped layout. + int y = 38; + const int rowH = 24; + for (const auto &ev : lastCalendar.events) { if (y > 215) break; M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); M5.Lcd.setTextSize(1); M5.Lcd.setCursor(10, y); - M5.Lcd.print(sanitizeGermanText(ev.summary.substring(0, 28))); + M5.Lcd.print(sanitizeGermanText(ev.summary.substring(0, 24))); - M5.Lcd.setTextColor(Theme::TEXT_DIM, Theme::BG); - M5.Lcd.setCursor(230, y); - M5.Lcd.print(formatEventTime(ev)); + M5.Lcd.setTextColor(Theme::ACCENT_CALENDAR, Theme::BG); + M5.Lcd.setCursor(10, y + 11); + M5.Lcd.print(DateUtils::formatShortDE(ev.startAt, ev.allDay)); - y += 18; + M5.Lcd.drawFastHLine(10, y + rowH - 4, Theme::SCREEN_W - 20, Theme::BG_CARD); + + y += rowH; } M5.Lcd.setTextColor(Theme::TEXT_DIM, Theme::BG); diff --git a/src/screens/home_screen.cpp b/src/screens/home_screen.cpp index f5dc559..960b25c 100644 --- a/src/screens/home_screen.cpp +++ b/src/screens/home_screen.cpp @@ -1,7 +1,8 @@ #include "home_screen.h" -#include "../../include/text_utils.h" #include #include "../../include/theme.h" +#include "../../include/text_utils.h" +#include "../../include/date_utils.h" #include "../icons/icons.h" #include "../api/weather_api.h" #include "../api/calendar_api.h" @@ -12,32 +13,15 @@ namespace { bool weatherOk = false; bool calendarOk = false; unsigned long lastFetchMs = 0; - const unsigned long REFRESH_INTERVAL_MS = 10UL * 60UL * 1000UL; // 10 minutes - - String formatEventTime(const CalendarEvent &ev) { - // Times are shown exactly as provided by the API, no timezone math. - if (ev.allDay) { - // start_at looks like "2026-08-03T00:00:00" -> show date part only - int tIdx = ev.startAt.indexOf('T'); - return tIdx > 0 ? ev.startAt.substring(0, tIdx) : ev.startAt; - } else { - int tIdx = ev.startAt.indexOf('T'); - if (tIdx > 0 && ev.startAt.length() >= tIdx + 6) { - return ev.startAt.substring(tIdx + 1, tIdx + 6); // HH:MM - } - return ev.startAt; - } - } + const unsigned long REFRESH_INTERVAL_MS = 10UL * 60UL * 1000UL; 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(); } @@ -50,21 +34,16 @@ namespace { } void updateHomeScreen() { - if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) { - doFetch(); - } + if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) doFetch(); } void renderHomeScreen(bool forceRefresh) { - if (forceRefresh || lastFetchMs == 0) { - doFetch(); - } + if (forceRefresh || lastFetchMs == 0) doFetch(); M5.Lcd.fillScreen(Theme::BG); - // --- Weather block (top) --- if (weatherOk) { - Icons::drawWeatherIcon(lastWeather.current.symbol, 45, 55, 24); + Icons::drawWeatherIcon(lastWeather.current.symbol, lastWeather.current.description, 45, 55, 24); M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); M5.Lcd.setTextSize(4); @@ -91,12 +70,10 @@ void renderHomeScreen(bool forceRefresh) { M5.Lcd.drawFastHLine(10, 95, Theme::SCREEN_W - 20, Theme::BG_CARD); - // --- Calendar block (next 2 events) --- + Icons::drawCalendarIcon(20, 112, 16); M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); M5.Lcd.setTextSize(2); - M5.Lcd.setCursor(10, 105); - Icons::drawCalendarIcon(20, 115, 14); - M5.Lcd.setCursor(35, 108); + M5.Lcd.setCursor(38, 104); M5.Lcd.print("Termine"); if (calendarOk) { @@ -112,16 +89,15 @@ void renderHomeScreen(bool forceRefresh) { M5.Lcd.setTextColor(Theme::TEXT_DIM, Theme::BG); M5.Lcd.setTextSize(1); M5.Lcd.setCursor(10, y + 20); - M5.Lcd.print(formatEventTime(ev)); + M5.Lcd.print(DateUtils::formatShortDE(ev.startAt, ev.allDay)); - y += 40; + y += 42; shown++; } } else { drawErrorState(20, 150, "Kalender n.a."); } - // --- Footer hint --- M5.Lcd.setTextColor(Theme::TEXT_DIM, Theme::BG); M5.Lcd.setTextSize(1); M5.Lcd.setCursor(10, 225); diff --git a/src/screens/home_screen.h b/src/screens/home_screen.h index 0a1c565..6e97f8a 100644 --- a/src/screens/home_screen.h +++ b/src/screens/home_screen.h @@ -1,9 +1,4 @@ #pragma once -// Renders the home screen: current weather, soft rain warning, and the -// next 2 calendar events. Fetches weather + calendar data if the 10 minute -// refresh interval has elapsed, or if forced (e.g. manual retry). void renderHomeScreen(bool forceRefresh = false); - -// Call regularly from loop() to check if a refresh is due. void updateHomeScreen(); diff --git a/src/screens/mvg_screen.cpp b/src/screens/mvg_screen.cpp index 1e90008..178035f 100644 --- a/src/screens/mvg_screen.cpp +++ b/src/screens/mvg_screen.cpp @@ -1,7 +1,7 @@ #include "mvg_screen.h" -#include "../../include/text_utils.h" #include #include "../../include/theme.h" +#include "../../include/text_utils.h" #include "../icons/icons.h" #include "../api/departures_api.h" @@ -9,7 +9,7 @@ namespace { DeparturesData lastData; bool ok = false; unsigned long lastFetchMs = 0; - const unsigned long REFRESH_INTERVAL_MS = 60UL * 1000UL; // 1 minute + const unsigned long REFRESH_INTERVAL_MS = 60UL * 1000UL; void doFetch() { DeparturesData d = fetchDepartures(); @@ -20,15 +20,11 @@ namespace { } void updateMvgScreen() { - if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) { - doFetch(); - } + if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) doFetch(); } void renderMvgScreen(bool forceRefresh) { - if (forceRefresh || lastFetchMs == 0) { - doFetch(); - } + if (forceRefresh || lastFetchMs == 0) doFetch(); M5.Lcd.fillScreen(Theme::BG); M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); @@ -44,15 +40,15 @@ void renderMvgScreen(bool forceRefresh) { return; } + // Slightly more vertical breathing room per row than the earlier version. int y = 38; - for (const auto &dep : lastData.departures) { - if (y > 215) break; + const int rowH = 20; - if (dep.type == "UBAHN") { - Icons::drawUBahnBadge(8, y, 16, 14); - } else { - Icons::drawSBahnBadge(8, y, 16, 14); - } + for (const auto &dep : lastData.departures) { + if (y > 212) break; + + if (dep.type == "UBAHN") Icons::drawUBahnBadge(8, y, 16, 14); + else Icons::drawSBahnBadge(8, y, 16, 14); M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); M5.Lcd.setTextSize(1); @@ -75,7 +71,7 @@ void renderMvgScreen(bool forceRefresh) { } } - y += 18; + y += rowH; } M5.Lcd.setTextColor(Theme::TEXT_DIM, Theme::BG); diff --git a/src/screens/screen_base.h b/src/screens/screen_base.h index 6cde191..2ed7c9e 100644 --- a/src/screens/screen_base.h +++ b/src/screens/screen_base.h @@ -1,8 +1,5 @@ #pragma once -// Simple screen enum used as a state machine. Each screen has a render -// function that is called on refresh, and each screen owns its own last- -// fetch timestamp so refresh intervals can differ per screen. enum class ScreenId { HOME, WEATHER_DETAIL, diff --git a/src/screens/weather_detail_screen.cpp b/src/screens/weather_detail_screen.cpp index 922fd15..eae4e75 100644 --- a/src/screens/weather_detail_screen.cpp +++ b/src/screens/weather_detail_screen.cpp @@ -1,7 +1,7 @@ #include "weather_detail_screen.h" -#include "../../include/text_utils.h" #include #include "../../include/theme.h" +#include "../../include/text_utils.h" #include "../icons/icons.h" #include "../api/weather_api.h" @@ -36,7 +36,7 @@ void renderWeatherDetailScreen(bool forceRefresh) { for (const auto &fe : lastWeather.forecast) { if (shown >= 6 || y > 220) break; - Icons::drawWeatherIcon(fe.symbol, 20, y + 8, 10); + Icons::drawWeatherIcon(fe.symbol, fe.description, 20, y + 8, 10); M5.Lcd.setTextColor(Theme::TEXT, Theme::BG); M5.Lcd.setTextSize(1);