From 82d466cada853eb5d114a98ad2139162123d9c8d Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sun, 2 Aug 2026 10:29:01 +0200 Subject: [PATCH] Phase 0: minimal main.cpp (WiFi + display + LVGL test button) + carry over hardware-independent helpers - src/main.cpp rewritten for ESP32-S3: Serial + display_init + WiFi connect (logic ported from old main.cpp, M5.Lcd prints -> Serial), LVGL test button screen to verify touch in Phase 1 - include/text_utils.h, date_utils.h: kept 1:1 (no M5Stack dependency) - include/secrets.h.example, .gitignore, scripts/{build,deploy}.sh, .github/workflows/build.yml: kept 1:1 (hardware-independent) --- .github/workflows/build.yml | 24 +++++++++++++ .gitignore | 7 ++++ include/date_utils.h | 66 +++++++++++++++++++++++++++++++++++ include/secrets.h.example | 11 ++++++ include/text_utils.h | 27 +++++++++++++++ scripts/build.sh | 13 +++++++ scripts/deploy.sh | 20 +++++++++++ src/main.cpp | 69 +++++++++++++++++++++++++++++++++++++ 8 files changed, 237 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 include/date_utils.h create mode 100644 include/secrets.h.example create mode 100644 include/text_utils.h create mode 100755 scripts/build.sh create mode 100644 scripts/deploy.sh create mode 100644 src/main.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7d0d8f6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: PlatformIO CI + +on: [push, pull_request] + +jobs: + build: + 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 + - 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 new file mode 100644 index 0000000..fcace53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.pio/ +.vscode/ +include/secrets.h +*.o +*.bin + +.venv-platformio/ 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/secrets.h.example b/include/secrets.h.example new file mode 100644 index 0000000..775d156 --- /dev/null +++ b/include/secrets.h.example @@ -0,0 +1,11 @@ +#pragma once + +// Copy this file to secrets.h and fill in your real values. +// secrets.h is git-ignored and must never be committed. + +#define WIFI_SSID "YOUR_WIFI_SSID" +#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" + +#define WEATHER_API_URL "http://:/api/weather" +#define CALENDAR_API_URL "http://:/api/events" +#define DEPARTURES_API_URL "http://:/api/departures" diff --git a/include/text_utils.h b/include/text_utils.h new file mode 100644 index 0000000..002f47f --- /dev/null +++ b/include/text_utils.h @@ -0,0 +1,27 @@ +#pragma once +#include + +// 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]; + 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; + default: break; + } + } + out += (char)c1; + } + return out; +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..7132ab1 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +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.sh b/scripts/deploy.sh new file mode 100644 index 0000000..fb9df48 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +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" + "$PIO" run --target upload --upload-port "$PORT" +else + echo "Auto-detecting upload port..." + "$PIO" run --target upload +fi +echo "Build + flash complete." diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ea0c12e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include "display/display_setup.h" + +namespace { + +unsigned long lastTickMs = 0; + +void connectWifi() { + Serial.println("Verbinde mit WLAN..."); + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { + delay(300); + Serial.print("."); + } + Serial.println(); + if (WiFi.status() == WL_CONNECTED) { + Serial.print("WLAN verbunden, IP: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println("WLAN fehlgeschlagen"); + } +} + +void btn_event_cb(lv_event_t *e) { + lv_obj_t *label = (lv_obj_t *)lv_event_get_user_data(e); + static int n = 0; + lv_label_set_text_fmt(label, "Taps: %d", ++n); +} + +void buildTestScreen() { + lv_obj_t *scr = lv_screen_active(); + lv_obj_set_style_bg_color(scr, Theme::bg(), 0); + + lv_obj_t *btn = lv_button_create(scr); + lv_obj_set_size(btn, 200, 80); + lv_obj_center(btn); + lv_obj_set_style_bg_color(btn, Theme::accentWeather(), 0); + + lv_obj_t *label = lv_label_create(btn); + lv_label_set_text(label, "Taps: 0"); + lv_obj_center(label); + lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, label); +} + +} // namespace + +void setup() { + Serial.begin(115200); + delay(200); + Serial.println("WT32-SC01 Plus booting..."); + display_init(); + connectWifi(); + buildTestScreen(); + Serial.println("Setup done."); +} + +void loop() { + unsigned long now = millis(); + if (now - lastTickMs >= 5) { + lastTickMs = now; + display_loop(); + } +}