Der Kunde soll mit einem Zettel zur Kasse gehen koennen, auf dem steht, was er zahlen muss. Drei Wege fuehren jetzt zu einem Bon: der Knopf "Bon" auf der Timer-Karte druckt den Zwischenstand und laesst den Timer weiterlaufen, "Beenden" druckt den Endbeleg, und in der Historie laesst sich jeder Beleg nachdrucken. Der Drucker haengt per USB an einem Raspberry Pi und ist damit vom Timer-Server aus nicht direkt erreichbar. Auf dem Pi laeuft deshalb eine kleine Bruecke (deploy/bondrucker/, nur Python-Standardbibliothek), die fertige ESC/POS-Daten per HTTP annimmt. Das Bon-Layout bleibt bewusst hier im Repo, damit Aenderungen daran nicht auf dem Pi angefasst werden muessen. Beim Beenden wird erst gespeichert und dann gedruckt. Eine leere Papierrolle ist damit ein Papierproblem und kein Datenproblem - der Vorgang steht in der Historie, die Oberflaeche sagt ausdruecklich, dass der Bon nicht gedruckt wurde, und nennt den Grund. Ohne PRINTER_HOST in der .env ist die Druckfunktion aus und die App verhaelt sich unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.1 KiB
JavaScript
120 lines
4.1 KiB
JavaScript
/**
|
|
* Anbindung des Bondruckers
|
|
*
|
|
* Der Drucker haengt per USB an einem Raspberry Pi im Laden, nicht an diesem
|
|
* 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:
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
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;
|
|
|
|
// Zuletzt gesehener Zustand - fuer /health und die Anzeige in den Einstellungen.
|
|
let lastError = null;
|
|
let lastPrintAt = null;
|
|
|
|
function isConfigured() {
|
|
return HOST.length > 0;
|
|
}
|
|
|
|
function config() {
|
|
return { host: HOST, port: PORT, width: WIDTH, timeoutMs: TIMEOUT_MS, tokenSet: !!TOKEN };
|
|
}
|
|
|
|
function state() {
|
|
return {
|
|
configured: isConfigured(),
|
|
host: HOST || null,
|
|
port: HOST ? PORT : null,
|
|
lastError,
|
|
lastPrintAt
|
|
};
|
|
}
|
|
|
|
/** Ein Aufruf gegen die Bruecke. Loest nie mit einem Fehler auf, der den
|
|
* 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).' });
|
|
}
|
|
|
|
const headers = {};
|
|
if (body) {
|
|
headers['Content-Type'] = 'application/octet-stream';
|
|
headers['Content-Length'] = body.length;
|
|
}
|
|
if (TOKEN) headers['X-Print-Token'] = 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 */ }
|
|
|
|
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.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.`
|
|
: err.code === 'EHOSTUNREACH' || err.code === 'ENETUNREACH'
|
|
? `Drucker-Pi ${HOST} ist im Netz nicht erreichbar.`
|
|
: (err.message || String(err));
|
|
resolve({ ok: false, error: meldung });
|
|
});
|
|
|
|
if (body) req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
/** Zustand der Bruecke abfragen (Geraet da? beschreibbar?). */
|
|
async function status() {
|
|
const res = await request('/status', 'GET');
|
|
if (!res.ok) {
|
|
lastError = res.error;
|
|
return { ok: false, error: res.error, ...state() };
|
|
}
|
|
return { ok: true, printer: res.data, ...state() };
|
|
}
|
|
|
|
/** Rohe ESC/POS-Daten an den Drucker geben. */
|
|
async function print(buffer) {
|
|
const res = await request('/print', 'POST', buffer);
|
|
if (res.ok) {
|
|
lastError = null;
|
|
lastPrintAt = Date.now();
|
|
} else {
|
|
lastError = res.error;
|
|
console.warn('[drucker] Druck fehlgeschlagen:', res.error);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
module.exports = { isConfigured, config, state, status, print, WIDTH };
|