/** * 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 };