Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
177 changes: 171 additions & 6 deletions firmware/scanner/scanner.ino
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
#include <Arduino_GFX_Library.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <Update.h>
#include <functional>
#include "mbedtls/md.h"
#include "tusb.h"
#include "esp_mac.h"

#define SD_D0 14
#define SD_D1 17
Expand Down Expand Up @@ -360,7 +362,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
Expand All @@ -376,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);
Expand Down Expand Up @@ -1649,7 +1651,7 @@ static void cfgAusNvs()
// Netze als Zeilen "ssid<TAB>pass". 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 {
Expand Down Expand Up @@ -1692,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);
Expand Down Expand Up @@ -1756,6 +1759,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)
{
Expand Down Expand Up @@ -2351,6 +2411,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;
Expand Down Expand Up @@ -2406,6 +2483,17 @@ static void webEinstellungen()
"<label><input type=\"checkbox\" name=\"invers\" value=\"1\"") +
(g_invertiert ? " checked" : "") + "> umkehren</label></td></tr>";
h += "<tr><td>Upload-Ziel</td><td><input name=\"endpoint\" size=\"34\" value=\"" + cfgEndpoint + "\"></td></tr>";
{
String netze;
for (int i = 0; i < cfgNetze; i++)
netze += String("<label><input type=\"checkbox\" name=\"netzweg") + i + "\" value=\"1\"> " + cfgNetzSsid[i] +
" <small>entfernen</small></label><br>";
if (!cfgNetze) netze = "<span class=\"bad\">keins hinterlegt</span><br>";
netze += "<small>neu:</small> <input name=\"ssid_neu\" placeholder=\"Netzname\" size=\"14\"> "
"<input name=\"pass_neu\" type=\"password\" placeholder=\"Passwort\" size=\"14\">";
h += "<tr><td>Bekannte WLAN-Netze<br><small>bis zu " + String(MAX_NETZE) + "; der staerkste erreichbare "
"Zugangspunkt gewinnt. Aenderungen gelten ab dem naechsten Suchlauf (Neustart).</small></td><td>" + netze + "</td></tr>";
}
h += "<tr><td>Namensanfang der Dateien<br><small>ergibt z.B. <code>" + cfgPraefix +
"-20260919-143205.pdf</code>; bei mehreren Sticks den Standort hier eintragen</small></td>"
"<td><input name=\"praefix\" size=\"16\" value=\"" + cfgPraefix + "\"></td></tr>";
Expand Down Expand Up @@ -2440,6 +2528,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") +
"<p class=\"ok\">Der Stick startet neu und verbindet sich mit <b>" + ssid + "</b>. "
"Das eigene WLAN verschwindet dabei. Danach ist er im Heimnetz unter <b>http://" + geraeteName() +
".local/</b> erreichbar; die Adresse steht auch im Display.</p>" + 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 += "<option value=\"" + ss + "\">" + ss + " (" + WiFi.RSSI(i) + " dBm)</option>";
}
String h = htmlKopf("Scan-Stick einrichten");
h += "<p>Netz waehlen oder eintragen, Ziel angeben, speichern. Der Stick startet dann neu.</p>";
h += "<form method=\"post\" action=\"/einrichten\"><table>";
h += "<tr><td>Gefundene Netze</td><td><select name=\"ssid_liste\"><option value=\"\">- bitte waehlen -</option>" + liste + "</select></td></tr>";
h += "<tr><td>oder Netzname von Hand</td><td><input name=\"ssid\" size=\"24\"></td></tr>";
h += "<tr><td>WLAN-Passwort</td><td><input name=\"pass\" type=\"password\" size=\"24\"></td></tr>";
h += "<tr><td>Upload-Ziel<br><small>Adresse des Empfaengers</small></td><td><input name=\"endpoint\" size=\"34\" value=\"" +
(cfgEndpoint.length() ? cfgEndpoint : String("http://192.168.1.50:8080/scan")) + "\"></td></tr>";
h += String("<tr><td>Geraeteschluessel<br><small>optional, wie SCAN_KEY beim Empfaenger</small></td><td><input name=\"schluessel\" type=\"password\" size=\"24\"") +
(cfgSchluessel.length() ? " placeholder=\"gesetzt - leer = behalten\"" : "") + "></td></tr>";
h += String("<tr><td>Passwort der Weboberflaeche<br><small>Benutzer scan</small></td><td><input name=\"webpass\" type=\"password\" size=\"24\"") +
(cfgWebPass.length() ? " placeholder=\"gesetzt - leer = behalten\"" : "") + "></td></tr>";
h += "</table><p><button class=\"btn\" type=\"submit\">Speichern und neu starten</button></p></form>";
if (cfgNetze) {
h += "<p><small>Bekannt: ";
for (int i = 0; i < cfgNetze; i++) h += (i ? ", " : "") + cfgNetzSsid[i];
h += "</small></p>";
}
g_web.send(200, "text/html; charset=utf-8", h + htmlFuss());
}

static void webStarten()
{
g_web.on("/", webStatus);
Expand Down Expand Up @@ -2508,7 +2657,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);
Expand Down Expand Up @@ -2851,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
Expand Down Expand Up @@ -2910,6 +3066,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);
}

Expand Down Expand Up @@ -3004,7 +3164,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) {
Expand Down Expand Up @@ -3092,7 +3254,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
Expand Down
Loading