Drucker-Pi aus der Oberflaeche heraus einrichten
Die Einrichtung war Handarbeit: Dateien per scp auf den Pi, dort install.sh mit sudo starten, Token abschreiben, in die .env eintragen, Dienst neu starten. Das ist genau die Sorte Arbeit, die beim naechsten Geraetetausch niemand mehr weiss. Jetzt macht das ein Knopf unter "Einstellungen": Adresse, Benutzer und Passwort eintragen, und die App meldet sich per SSH an, prueft Python/systemd/Druckergeraet, uebertraegt deploy/bondrucker/, startet install.sh mit einem selbst erzeugten Token, raeumt auf und prueft von aussen nach. Jeder Schritt steht mit Ergebnis im Protokoll. Damit das ueberhaupt Sinn ergibt, liegt der Druckerzugang jetzt in der Datenbank statt in der .env - sonst muesste hinterher doch wieder jemand auf den Server. Die PRINTER_*-Werte in der .env sind nur noch Startwerte beim allerersten Start, wie RATE_PER_10MIN auch. Zwei Fehler, die beim Testen der Einrichtung auffielen: install.sh startete den Dienst nicht neu, sondern nur "enable --now". Bei einer erneuten Einrichtung lief die Bruecke deshalb mit dem ALTEN Token weiter und lehnte jeden Bon mit HTTP 403 ab. Jetzt: restart. Die Gegenprobe fragte nur /status ab - und /status prueft kein Token. Ein Dienst mit altem Token galt damit faelschlich als bereit. Die Bruecke meldet in /status jetzt zusaetzlich, ob das mitgeschickte Token passt (Version 1.1), und die Einrichtung prueft das mit. Zugangsdaten werden einmal benutzt und danach vergessen; das erzeugte Token wird aus dem Protokoll entfernt, bevor es in den Browser oder ins Journal geht. Neu dabei: ssh2, reines JavaScript. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
70756f3762
commit
5e23ccdcbc
+63
-34
@@ -5,39 +5,65 @@
|
||||
* Server. Auf dem Pi laeuft die Bruecke aus deploy/bondrucker/, die fertige
|
||||
* ESC/POS-Daten per HTTP annimmt. Hier entsteht nur der Aufruf.
|
||||
*
|
||||
* Zwei Grundsaetze, die den Rest erklaeren:
|
||||
* Drei Grundsaetze, die den Rest erklaeren:
|
||||
*
|
||||
* 1. Drucken darf die Zeiterfassung nie aufhalten. Ein leerer Papierrollen-
|
||||
* halter ist ein Papierproblem, kein Datenproblem: der Vorgang ist da
|
||||
* bereits gespeichert, der Bon laesst sich aus der Historie nachdrucken.
|
||||
* 2. Kein Fremdpaket. Node bringt einen HTTP-Client mit.
|
||||
* 2. Die Zugangsdaten stehen in der Datenbank, nicht in der .env. Nur so kann
|
||||
* die Oberflaeche einen neuen Pi einrichten, ohne dass jemand auf den
|
||||
* Server muss. Die .env liefert weiterhin die Startwerte (siehe server.js).
|
||||
* 3. Kein Fremdpaket fuers Drucken. Node bringt einen HTTP-Client mit.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
|
||||
const HOST = (process.env.PRINTER_HOST || '').trim();
|
||||
const PORT = parseInt(process.env.PRINTER_PORT) || 9100;
|
||||
const TOKEN = (process.env.PRINTER_TOKEN || '').trim();
|
||||
const TIMEOUT_MS = parseInt(process.env.PRINTER_TIMEOUT_MS) || 6000;
|
||||
const WIDTH = parseInt(process.env.PRINTER_WIDTH) || 42;
|
||||
// Laufende Konfiguration. Wird beim Start und nach jeder Aenderung in den
|
||||
// Einstellungen aus der Datenbank gesetzt - siehe refreshPrinterConfig().
|
||||
let cfg = {
|
||||
host: '',
|
||||
port: 9100,
|
||||
token: '',
|
||||
width: 42,
|
||||
timeoutMs: parseInt(process.env.PRINTER_TIMEOUT_MS) || 6000
|
||||
};
|
||||
|
||||
// Zuletzt gesehener Zustand - fuer /health und die Anzeige in den Einstellungen.
|
||||
let lastError = null;
|
||||
let lastPrintAt = null;
|
||||
|
||||
function isConfigured() {
|
||||
return HOST.length > 0;
|
||||
function setConfig(next = {}) {
|
||||
const port = parseInt(next.port);
|
||||
const width = parseInt(next.width);
|
||||
cfg = {
|
||||
...cfg,
|
||||
host: String(next.host == null ? cfg.host : next.host).trim(),
|
||||
port: Number.isInteger(port) && port > 0 && port < 65536 ? port : cfg.port,
|
||||
token: next.token == null ? cfg.token : String(next.token).trim(),
|
||||
width: Number.isInteger(width) && width >= 24 && width <= 96 ? width : cfg.width
|
||||
};
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function isConfigured() {
|
||||
return cfg.host.length > 0;
|
||||
}
|
||||
|
||||
function width() {
|
||||
return cfg.width;
|
||||
}
|
||||
|
||||
/** Konfiguration OHNE das Token - dieser Wert verlaesst den Server nie. */
|
||||
function config() {
|
||||
return { host: HOST, port: PORT, width: WIDTH, timeoutMs: TIMEOUT_MS, tokenSet: !!TOKEN };
|
||||
return { host: cfg.host, port: cfg.port, width: cfg.width, timeoutMs: cfg.timeoutMs, tokenSet: !!cfg.token };
|
||||
}
|
||||
|
||||
function state() {
|
||||
return {
|
||||
configured: isConfigured(),
|
||||
host: HOST || null,
|
||||
port: HOST ? PORT : null,
|
||||
host: cfg.host || null,
|
||||
port: isConfigured() ? cfg.port : null,
|
||||
width: cfg.width,
|
||||
lastError,
|
||||
lastPrintAt
|
||||
};
|
||||
@@ -47,8 +73,8 @@ function state() {
|
||||
* Aufrufer zwingt, ihn zu behandeln - stattdessen {ok, error}. */
|
||||
function request(pfad, method, body) {
|
||||
return new Promise((resolve) => {
|
||||
if (!isConfigured()) {
|
||||
return resolve({ ok: false, error: 'Kein Drucker eingerichtet (PRINTER_HOST fehlt).' });
|
||||
if (!cfg.host) {
|
||||
return resolve({ ok: false, error: 'Kein Drucker eingerichtet.' });
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
@@ -56,34 +82,37 @@ function request(pfad, method, body) {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
headers['Content-Length'] = body.length;
|
||||
}
|
||||
if (TOKEN) headers['X-Print-Token'] = TOKEN;
|
||||
if (cfg.token) headers['X-Print-Token'] = cfg.token;
|
||||
|
||||
const req = http.request({ host: HOST, port: PORT, path: pfad, method, headers }, (res) => {
|
||||
const teile = [];
|
||||
res.on('data', d => teile.push(d));
|
||||
res.on('end', () => {
|
||||
const roh = Buffer.concat(teile).toString('utf8');
|
||||
let daten = null;
|
||||
try { daten = JSON.parse(roh); } catch (e) { /* Bruecke antwortet sonst nichts */ }
|
||||
const req = http.request(
|
||||
{ host: cfg.host, port: cfg.port, path: pfad, method, headers },
|
||||
(res) => {
|
||||
const teile = [];
|
||||
res.on('data', d => teile.push(d));
|
||||
res.on('end', () => {
|
||||
const roh = Buffer.concat(teile).toString('utf8');
|
||||
let daten = null;
|
||||
try { daten = JSON.parse(roh); } catch (e) { /* Bruecke antwortet sonst nichts */ }
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
return resolve({ ok: true, data: daten });
|
||||
}
|
||||
const meldung = (daten && daten.error) || `Drucker antwortet mit HTTP ${res.statusCode}`;
|
||||
resolve({ ok: false, error: meldung, data: daten });
|
||||
});
|
||||
});
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
return resolve({ ok: true, data: daten });
|
||||
}
|
||||
const meldung = (daten && daten.error) || `Drucker antwortet mit HTTP ${res.statusCode}`;
|
||||
resolve({ ok: false, error: meldung, data: daten });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.setTimeout(TIMEOUT_MS, () => {
|
||||
req.destroy(new Error(`Drucker antwortet nicht innerhalb von ${TIMEOUT_MS} ms`));
|
||||
req.setTimeout(cfg.timeoutMs, () => {
|
||||
req.destroy(new Error(`Drucker antwortet nicht innerhalb von ${cfg.timeoutMs} ms`));
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
// ECONNREFUSED/EHOSTUNREACH sind der Normalfall, wenn der Pi aus ist.
|
||||
const meldung = err.code === 'ECONNREFUSED'
|
||||
? `Druckdienst auf ${HOST}:${PORT} nimmt keine Verbindung an.`
|
||||
? `Druckdienst auf ${cfg.host}:${cfg.port} nimmt keine Verbindung an.`
|
||||
: err.code === 'EHOSTUNREACH' || err.code === 'ENETUNREACH'
|
||||
? `Drucker-Pi ${HOST} ist im Netz nicht erreichbar.`
|
||||
? `Drucker-Pi ${cfg.host} ist im Netz nicht erreichbar.`
|
||||
: (err.message || String(err));
|
||||
resolve({ ok: false, error: meldung });
|
||||
});
|
||||
@@ -116,4 +145,4 @@ async function print(buffer) {
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = { isConfigured, config, state, status, print, WIDTH };
|
||||
module.exports = { setConfig, isConfigured, width, config, state, status, print };
|
||||
|
||||
Reference in New Issue
Block a user