/** * 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. * * 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. 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'); // 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 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: cfg.host, port: cfg.port, width: cfg.width, timeoutMs: cfg.timeoutMs, tokenSet: !!cfg.token }; } function state() { return { configured: isConfigured(), host: cfg.host || null, port: isConfigured() ? cfg.port : null, width: cfg.width, 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 (!cfg.host) { return resolve({ ok: false, error: 'Kein Drucker eingerichtet.' }); } const headers = {}; if (body) { headers['Content-Type'] = 'application/octet-stream'; headers['Content-Length'] = body.length; } if (cfg.token) headers['X-Print-Token'] = cfg.token; 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 }); }); } ); 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 ${cfg.host}:${cfg.port} nimmt keine Verbindung an.` : err.code === 'EHOSTUNREACH' || err.code === 'ENETUNREACH' ? `Drucker-Pi ${cfg.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 = { setConfig, isConfigured, width, config, state, status, print };