mirror of
https://github.com/skoelle/wt32sc01-dashboard.git
synced 2026-09-17 17:30:25 +00:00
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)
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.pio/
|
||||||
|
.vscode/
|
||||||
|
include/secrets.h
|
||||||
|
*.o
|
||||||
|
*.bin
|
||||||
|
|
||||||
|
.venv-platformio/
|
||||||
@@ -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
|
||||||
@@ -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://<host>:<port>/api/weather"
|
||||||
|
#define CALENDAR_API_URL "http://<host>:<port>/api/events"
|
||||||
|
#define DEPARTURES_API_URL "http://<host>:<port>/api/departures"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
Executable
+13
@@ -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."
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <secrets.h>
|
||||||
|
#include <theme.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user