Merge pull request #1 from skoelle/feature/improve-ui

Feature/improve UI
This commit is contained in:
Stefan Koelle
2026-08-02 08:21:54 +02:00
committed by GitHub
29 changed files with 302 additions and 817 deletions
-9
View File
@@ -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
+2 -1
View File
@@ -1,6 +1,7 @@
.pio/
.vscode/
.venv-platformio/
include/secrets.h
*.o
*.bin
.venv-platformio/
+9 -119
View File
@@ -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
+52 -35
View File
@@ -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`.
+18 -236
View File
@@ -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://<host>:<port>/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.01.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://<host>:<port>/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://<host>:<port>/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)
+16 -96
View File
@@ -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)
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <Arduino.h>
// 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
+9 -17
View File
@@ -1,35 +1,27 @@
#pragma once
#include <Arduino.h>
// 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;
}
+13 -15
View File
@@ -1,23 +1,21 @@
#pragma once
#include <M5Stack.h>
// "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;
+2 -24
View File
@@ -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
-12
View File
@@ -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."
+2 -26
View File
@@ -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
-15
View File
@@ -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."
+2 -11
View File
@@ -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;
}
-2
View File
@@ -16,6 +16,4 @@ struct CalendarData {
std::vector<CalendarEvent> events;
};
// Fetches and parses the calendar API (already limited to next 10 events
// server-side).
CalendarData fetchCalendar();
+2 -11
View File
@@ -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;
}
+2 -4
View File
@@ -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<Departure> 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();
-5
View File
@@ -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;
}
-4
View File
@@ -1,14 +1,10 @@
#pragma once
#include <Arduino.h>
// 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);
+3 -15
View File
@@ -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;
-6
View File
@@ -23,11 +23,5 @@ struct WeatherData {
std::vector<ForecastEntry> 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);
+54 -32
View File
@@ -2,16 +2,10 @@
#include <M5Stack.h>
#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);
+12 -42
View File
@@ -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);
}
+14 -20
View File
@@ -1,26 +1,14 @@
#include "calendar_detail_screen.h"
#include "../../include/text_utils.h"
#include <M5Stack.h>
#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);
+10 -34
View File
@@ -1,7 +1,8 @@
#include "home_screen.h"
#include "../../include/text_utils.h"
#include <M5Stack.h>
#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);
-5
View File
@@ -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();
+12 -16
View File
@@ -1,7 +1,7 @@
#include "mvg_screen.h"
#include "../../include/text_utils.h"
#include <M5Stack.h>
#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);
-3
View File
@@ -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,
+2 -2
View File
@@ -1,7 +1,7 @@
#include "weather_detail_screen.h"
#include "../../include/text_utils.h"
#include <M5Stack.h>
#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);