From bf4c6165b8e4964c880839763fb2aa5abc00c6a0 Mon Sep 17 00:00:00 2001 From: Matthias Hocke Date: Sun, 20 Sep 2026 17:14:07 +0200 Subject: [PATCH 1/2] v39: Netze in der Weboberflaeche verwalten; eigenes WLAN mit Einrichtungsseite, wenn kein bekanntes Netz erreichbar ist (Issue #4) Co-Authored-By: Claude Fable 5.1 --- firmware/scanner/scanner.ino | 169 ++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 3 deletions(-) diff --git a/firmware/scanner/scanner.ino b/firmware/scanner/scanner.ino index d2eb72c..ba7e53a 100644 --- a/firmware/scanner/scanner.ino +++ b/firmware/scanner/scanner.ino @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -360,7 +361,7 @@ static volatile uint32_t g_bytesGeschrieben = 0; // seit dem letzten Verarbeit static uint32_t g_naechsterVersuch = 0; // 0 = sofort faellig static int g_versuche = 0; -#define FW_VERSION "v38" +#define FW_VERSION "v39" String cfgEndpoint; // Bekannte WLAN-Netze - mehrere, damit derselbe Stick an verschiedenen Standorten @@ -1756,6 +1757,63 @@ static bool wifiVerbinden() return WiFi.status() == WL_CONNECTED; } +// ---- Einrichtung ohne Karte im Lesegeraet ---- +// Kennt der Stick kein Netz oder erreicht er keins, spannt er ein eigenes WLAN +// auf (Name = Geraetename, offen) und beantwortet jede Namensanfrage mit sich +// selbst - Handy oder Rechner oeffnen dann von selbst die Einrichtungsseite, +// wie im Hotel. Sobald ein bekanntes Netz erreichbar ist, geht das WLAN wieder +// aus. Der Scanpfad ist davon unberuehrt: USB laeuft, Uploads warten nur. +static DNSServer g_dns; +static bool g_einrichtung = false; +static bool g_einrichtungGezeichnet = false; + +static void zeigeEinrichtung() +{ + if (g_einrichtungGezeichnet) return; + g_einrichtungGezeichnet = true; + g_screen = -2; + uint32_t rgb = 0xFF6600; + ledColor(rgb >> 16, (rgb >> 8) & 0xFF, rgb & 0xFF); + g_gfx->fillScreen(rgb565(rgb)); + g_gfx->setTextColor(COL_BLACK); + g_gfx->setTextSize(2); + g_gfx->setCursor(4, 4); + g_gfx->print("WLAN EINRICHTEN"); + g_gfx->setTextSize(1); + g_gfx->setCursor(4, 30); + g_gfx->print("Mit diesem Netz verbinden:"); + g_gfx->setTextSize(2); + g_gfx->setCursor(4, 42); + g_gfx->print(geraeteName()); + g_gfx->setTextSize(1); + g_gfx->setCursor(4, 66); + g_gfx->print("dann http://192.168.4.1/"); +} + +static void einrichtungStarten(const char *grund) +{ + if (g_einrichtung) return; + WiFi.mode(WIFI_AP_STA); + WiFi.softAP(geraeteName().c_str()); + delay(100); + g_dns.start(53, "*", WiFi.softAPIP()); + g_einrichtung = true; + g_einrichtungGezeichnet = false; + logZeile(String("[einrichtung] ") + grund + " - eigenes WLAN \"" + geraeteName() + "\" offen, Seite http://" + + WiFi.softAPIP().toString() + "/"); +} + +static void einrichtungBeenden() +{ + if (!g_einrichtung) return; + g_dns.stop(); + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_STA); + g_einrichtung = false; + g_screen = -1; + logZeile("[einrichtung] Netz gefunden - eigenes WLAN wieder aus"); +} + // http://host[:port]/pfad zerlegen static bool urlTeile(const String &url, String &host, uint16_t &port, String &pfad) { @@ -2351,6 +2409,23 @@ static void webEinstellungen() if (g_web.method() == HTTP_POST) { if (!herkunftPruefen()) return; if (g_web.hasArg("endpoint")) cfgEndpoint = g_web.arg("endpoint"); + // Bekannte Netze: entfernen per Kaestchen, hinzufuegen per Feld. Wirkt beim + // naechsten Suchlauf (Neustart oder zwei Minuten ohne Netz). + for (int i = MAX_NETZE - 1; i >= 0; i--) { + char k[12]; + snprintf(k, sizeof k, "netzweg%d", i); + if (g_web.hasArg(k) && i < cfgNetze) { + for (int j = i; j + 1 < cfgNetze; j++) { cfgNetzSsid[j] = cfgNetzSsid[j + 1]; cfgNetzPass[j] = cfgNetzPass[j + 1]; } + cfgNetze--; + } + } + if (g_web.hasArg("ssid_neu") && g_web.arg("ssid_neu").length()) { + String neuS = g_web.arg("ssid_neu"), neuP = g_web.arg("pass_neu"); + int vorhanden = -1; + for (int i = 0; i < cfgNetze; i++) if (cfgNetzSsid[i] == neuS) vorhanden = i; + if (vorhanden >= 0) cfgNetzPass[vorhanden] = neuP; // Passwort erneuern + else if (cfgNetze < MAX_NETZE) { cfgNetzSsid[cfgNetze] = neuS; cfgNetzPass[cfgNetze] = neuP; cfgNetze++; } + } if (g_web.hasArg("aufraeum")) { uint32_t m = g_web.arg("aufraeum").toInt(); if (m >= 1 && m <= 1440) g_aufraeumMin = m; @@ -2406,6 +2481,17 @@ static void webEinstellungen() ""; h += "Upload-Ziel"; + { + String netze; + for (int i = 0; i < cfgNetze; i++) + netze += String("
"; + if (!cfgNetze) netze = "keins hinterlegt
"; + netze += "neu: " + ""; + h += "Bekannte WLAN-Netze
bis zu " + String(MAX_NETZE) + "; der staerkste erreichbare " + "Zugangspunkt gewinnt. Aenderungen gelten ab dem naechsten Suchlauf (Neustart)." + netze + ""; + } h += "Namensanfang der Dateien
ergibt z.B. " + cfgPraefix + "-20260919-143205.pdf; bei mehreren Sticks den Standort hier eintragen" ""; @@ -2440,6 +2526,67 @@ static void webEinstellungen() g_web.send(200, "text/html; charset=utf-8", h + htmlFuss()); } +// Einrichtungsseite: Netz waehlen oder eintragen, Ziel, Schluessel, Passwort. +// Erreichbar immer, im eigenen WLAN als Startseite. Speichert in den Flash und +// startet neu - danach sucht der Stick das eingetragene Netz. +static void webEinrichten() +{ + if (!webAuth()) return; + if (g_web.method() == HTTP_POST) { + if (!herkunftPruefen()) return; + String ssid = g_web.arg("ssid"), pass = g_web.arg("pass"); + if (ssid.isEmpty()) ssid = g_web.arg("ssid_liste"); + if (ssid.length()) { + int vorhanden = -1; + for (int i = 0; i < cfgNetze; i++) if (cfgNetzSsid[i] == ssid) vorhanden = i; + if (vorhanden >= 0) cfgNetzPass[vorhanden] = pass; + else { + if (cfgNetze >= MAX_NETZE) cfgNetze = MAX_NETZE - 1; // aeltestes faellt raus + cfgNetzSsid[cfgNetze] = ssid; cfgNetzPass[cfgNetze] = pass; cfgNetze++; + } + } + if (g_web.hasArg("endpoint") && g_web.arg("endpoint").length()) cfgEndpoint = g_web.arg("endpoint"); + if (g_web.hasArg("schluessel") && g_web.arg("schluessel").length()) cfgSchluessel = g_web.arg("schluessel"); + if (g_web.hasArg("webpass") && g_web.arg("webpass").length()) cfgWebPass = g_web.arg("webpass"); + cfgNachNvs(); + logZeile("[einrichtung] gespeichert: Netz " + ssid + ", Ziel " + cfgEndpoint + " - Neustart"); + g_web.send(200, "text/html; charset=utf-8", htmlKopf("Gespeichert") + + "

Der Stick startet neu und verbindet sich mit " + ssid + ". " + "Das eigene WLAN verschwindet dabei. Danach ist er im Heimnetz unter http://" + geraeteName() + + ".local/ erreichbar; die Adresse steht auch im Display.

" + htmlFuss()); + delay(1500); + sanftNeustarten(); + return; + } + // Netze in der Naehe anbieten - im eigenen WLAN kann man nicht tippen, was man nicht sieht + String liste; + int n = WiFi.scanNetworks(); + for (int i = 0; i < n && i < 15; i++) { + String ss = WiFi.SSID(i); + if (ss.isEmpty()) continue; + liste += ""; + } + String h = htmlKopf("Scan-Stick einrichten"); + h += "

Netz waehlen oder eintragen, Ziel angeben, speichern. Der Stick startet dann neu.

"; + h += "
"; + h += ""; + h += ""; + h += ""; + h += ""; + h += String(""; + h += String(""; + h += "
Gefundene Netze
oder Netzname von Hand
WLAN-Passwort
Upload-Ziel
Adresse des Empfaengers
Geraeteschluessel
optional, wie SCAN_KEY beim Empfaenger
Passwort der Weboberflaeche
Benutzer scan

"; + if (cfgNetze) { + h += "

Bekannt: "; + for (int i = 0; i < cfgNetze; i++) h += (i ? ", " : "") + cfgNetzSsid[i]; + h += "

"; + } + g_web.send(200, "text/html; charset=utf-8", h + htmlFuss()); +} + static void webStarten() { g_web.on("/", webStatus); @@ -2508,7 +2655,14 @@ static void webStarten() else logZeile("[update] Abschluss fehlgeschlagen"); } }); - g_web.onNotFound([]() { g_web.sendHeader("Location", "/"); g_web.send(303, "text/plain", ""); }); + g_web.on("/einrichten", HTTP_GET, webEinrichten); + g_web.on("/einrichten", HTTP_POST, webEinrichten); + // Im eigenen WLAN landet jede fremde Adresse (Hotspot-Erkennung von iOS, + // Android, Windows) auf der Einrichtungsseite; sonst auf dem Status. + g_web.onNotFound([]() { + g_web.sendHeader("Location", g_einrichtung ? "http://" + WiFi.softAPIP().toString() + "/einrichten" : String("/")); + g_web.send(302, "text/plain", ""); + }); // Der WebServer behaelt nur angeforderte Kopfzeilen; Host merkt er sich immer. const char *kopf[] = { "Origin", "Referer" }; g_web.collectHeaders(kopf, 2); @@ -2910,6 +3064,10 @@ void setup() // WLAN erst jetzt: der Suchlauf blockiert einige Sekunden, und der Drucker // soll den Stick sofort als Laufwerk sehen, nicht erst nach dem WLAN. wlanStarten(); + if (WiFi.status() != WL_CONNECTED) { + einrichtungStarten(cfgNetze ? "kein bekanntes Netz erreichbar" : "kein Netz hinterlegt"); + webStarten(); + } if (g_screen != Z_SDFEHL) zeigeScreen(Z_BEREIT); } @@ -3004,7 +3162,9 @@ void loop() } } } + if (WiFi.status() == WL_CONNECTED && g_einrichtung) einrichtungBeenden(); if (WiFi.status() == WL_CONNECTED && !g_webAn) { webStarten(); zeitHolen(); } + if (g_einrichtung) g_dns.processNextRequest(); if (g_webAn) g_web.handleClient(); if (g_usbFehlerNeu) { @@ -3092,7 +3252,10 @@ void loop() // Nur die WARTE-Anzeige ist geschuetzt. Frueher stand hier !g_dirty - das // fror JEDE Anzeige ein, sobald ein Schreibvorgang offen war, also ueber die // gesamte Ruhefrist hinweg. - if (g_screen != Z_SDFEHL && !g_warteAnzeige) { + if (g_einrichtung && !g_dirty && g_screen != Z_SDFEHL && !g_warteAnzeige) { + zeigeEinrichtung(); + } else if (g_screen != Z_SDFEHL && !g_warteAnzeige) { + if (g_einrichtungGezeichnet) { g_einrichtungGezeichnet = false; g_screen = -1; } uint32_t seitWrite = nun - g_lastWrite; // Noch nicht roh nachgesehen seit dem letzten Schreibzugriff, oder es // liegt wirklich etwas Ungesendetes: dann "SCAN ERKANNT". Nur die From 73c394594bc1ad3ac2c12f651825b5cdfcd73d25 Mon Sep 17 00:00:00 2001 From: Matthias Hocke Date: Sun, 20 Sep 2026 17:26:04 +0200 Subject: [PATCH 2/2] v39: Geraetename aus dem Chip (esp_read_mac) statt vom Treiber - hiess im Einrichtungs-WLAN sonst scanstick-0000; README: Einrichtung ueber das eigene WLAN dokumentiert, Offene-Punkte-Eintrag erledigt Co-Authored-By: Claude Fable 5.1 --- README.md | 17 ++++++++++------- firmware/scanner/scanner.ino | 8 +++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 866b368..f0e0c96 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,16 @@ liegen unter [Releases](../../releases). ### 3. WLAN-Zugangsdaten eintragen -Dafür ist **kein Kartenleser nötig** — der Stick ist ja selbst ein USB-Laufwerk: +**Der einfache Weg: über das eigene WLAN des Sticks.** Kennt der Stick kein Netz oder +erreicht er keins, spannt er ein eigenes WLAN namens `scanstick-XXXX` auf (offen; XXXX steht +auch im Display). Handy oder Rechner damit verbinden — die Einrichtungsseite öffnet sich von +selbst, wie bei einem Hotel-WLAN, sonst `http://192.168.4.1/` aufrufen. Dort Netz aus der +Liste wählen, Passwort, Empfängeradresse, optional Geräteschlüssel und Web-Passwort eintragen, +speichern. Der Stick startet neu und ist danach im Heimnetz unter `http://scanstick-XXXX.local/` +erreichbar. Weitere Netze lassen sich später in den Einstellungen hinzufügen oder entfernen. + +**Der zweite Weg: eine Datei auf der Karte.** Auch dafür ist **kein Kartenleser nötig** — der +Stick ist ja selbst ein USB-Laufwerk: 1. Stick in den Computer stecken, es erscheint ein Laufwerk namens **SCANS** 2. darauf eine Textdatei **`wifi.cfg`** anlegen (Vorlage: `wifi.cfg.beispiel`): @@ -134,9 +143,6 @@ braucht. Die Datei bleibt liegen. Übernommen wird sie nur, wenn sich ihr Inhalt letzten Mal **geändert** hat; sonst gelten die Werte aus dem Flash, also auch alles, was in der Weboberfläche eingestellt wurde. Zum Ändern des Netzes genügt es, sie zu überschreiben. -> Einen Einrichtungsassistenten über ein eigenes WLAN des Sticks gibt es noch nicht, -> siehe [Offene Punkte](#offene-punkte). - ### 4. In den Drucker Stick in den USB-Anschluss, etwa zehn Sekunden warten, bis das Display **BEREIT** zeigt, @@ -436,9 +442,6 @@ sudo systemctl enable --now scan-receiver ## Offene Punkte -- **Einrichtungsassistent**: Sind keine Zugangsdaten hinterlegt, soll der Stick ein - eigenes WLAN aufspannen, in dem man Netz und Ziel im Browser einträgt — dann braucht - es gar keine Datei mehr - Selbsttätiger Wechsel des Zugangspunkts, wenn der Empfang längere Zeit schwach bleibt (bei komplettem Ausfall sucht er nach zwei Minuten neu; bei nur schwachem Empfang noch nicht) - Auch andere Dateitypen als PDF auf Vollständigkeit prüfen diff --git a/firmware/scanner/scanner.ino b/firmware/scanner/scanner.ino index ba7e53a..cd7f01b 100644 --- a/firmware/scanner/scanner.ino +++ b/firmware/scanner/scanner.ino @@ -31,6 +31,7 @@ #include #include "mbedtls/md.h" #include "tusb.h" +#include "esp_mac.h" #define SD_D0 14 #define SD_D1 17 @@ -377,7 +378,7 @@ static WiFiMulti g_wifiMulti; static String geraeteName() { uint8_t mac[6]; - WiFi.macAddress(mac); + esp_read_mac(mac, ESP_MAC_WIFI_STA); // aus dem Chip, nicht vom Treiber: der liefert vor dem WLAN-Start nur Nullen char t[24]; snprintf(t, sizeof t, "scanstick-%02x%02x", mac[4], mac[5]); return String(t); @@ -1650,7 +1651,7 @@ static void cfgAusNvs() // Netze als Zeilen "ssidpass". Aeltere Staende kennen nur ssid/pass. String netze = g_nvs.getString("netze", ""); cfgNetze = 0; - if (netze.isEmpty()) { + if (netze.isEmpty() && !g_nvs.isKey("netze")) { // nur wenn es die Liste noch nie gab: alte Einzelwerte String s = g_nvs.getString("ssid", ""); if (s.length()) { cfgNetzSsid[0] = s; cfgNetzPass[0] = g_nvs.getString("pass", ""); cfgNetze = 1; } } else { @@ -1693,6 +1694,7 @@ static void cfgNachNvs() String netze; for (int i = 0; i < cfgNetze; i++) netze += cfgNetzSsid[i] + "\t" + cfgNetzPass[i] + "\n"; g_nvs.putString("netze", netze); + g_nvs.remove("ssid"); g_nvs.remove("pass"); // alte Einzelwerte: sonst kehrt ein entferntes Netz zurueck g_nvs.putString("endpoint", cfgEndpoint); g_nvs.putString("webpass", cfgWebPass); g_nvs.putString("praefix", cfgPraefix); @@ -3005,7 +3007,7 @@ void setup() delay(300); Serial.println("\n=== Scan-Stick " FW_VERSION " ==="); g_startGrund = resetGrund(); - logZeile(String("[boot] " FW_VERSION ", Grund: ") + g_startGrund); + logZeile(String("[boot] " FW_VERSION ", Grund: ") + g_startGrund + ", Geraet " + geraeteName()); ledInit(); ledColor(60, 60, 60); // WEISS = Strom da, bootet