/** * NexusPrinter — Web USB / Web Serial bridge for thermal printers. * * Connects to 58mm or 80mm ESC/POS printers via WebUSB or Web Serial APIs * and sends raw byte commands generated by the backend. */ window.NexusPrinter = (function () { 'use strict'; let _device = null; // WebUSB device let _endpoint = null; // USB bulk-out endpoint number let _serialPort = null; // Web Serial port let _writer = null; // Serial writable stream writer let _type = null; // 'usb' | 'serial' | null // ── Connection ───────────────────────────────── async function connect() { // Try WebUSB first if ('usb' in navigator) { try { _device = await navigator.usb.requestDevice({ filters: [] }); await _device.open(); // Select first configuration if needed if (_device.configuration === null) { await _device.selectConfiguration(1); } // Claim first interface and find bulk-out endpoint const iface = _device.configuration.interfaces[0]; await _device.claimInterface(iface.interfaceNumber); const alt = iface.alternates[0]; const ep = alt.endpoints.find(e => e.direction === 'out' && e.type === 'bulk'); if (ep) { _endpoint = ep.endpointNumber; _type = 'usb'; _save(); return { ok: true, type: 'usb', name: _device.productName || 'USB Printer' }; } } catch (e) { // User cancelled or no USB device available _device = null; } } // Fall back to Web Serial if ('serial' in navigator) { try { _serialPort = await navigator.serial.requestPort(); await _serialPort.open({ baudRate: 9600 }); _writer = _serialPort.writable.getWriter(); _type = 'serial'; _save(); return { ok: true, type: 'serial', name: 'Serial Printer' }; } catch (e) { _serialPort = null; _writer = null; } } return { ok: false, error: 'No printer connected. Use a browser that supports WebUSB or Web Serial (Chrome/Edge).' }; } async function disconnect() { try { if (_type === 'usb' && _device) { await _device.close(); } if (_type === 'serial') { if (_writer) { _writer.releaseLock(); _writer = null; } if (_serialPort) { await _serialPort.close(); _serialPort = null; } } } catch (e) { /* ignore */ } _device = null; _endpoint = null; _type = null; localStorage.removeItem('nexus_printer'); } function isConnected() { return _type !== null; } // ── Printing ─────────────────────────────────── async function sendRaw(bytes) { if (!_type) return false; try { if (_type === 'usb' && _device && _endpoint !== null) { // Send in chunks of 512 bytes (common USB max packet size) const chunk = 512; for (let i = 0; i < bytes.length; i += chunk) { await _device.transferOut(_endpoint, bytes.slice(i, i + chunk)); } return true; } if (_type === 'serial' && _writer) { await _writer.write(bytes); return true; } } catch (e) { console.error('[NexusPrinter] send error:', e); // Connection likely lost await disconnect(); } return false; } /** * Print a sale ticket by fetching ESC/POS bytes from the backend. * @param {number} saleId * @param {number} [width=80] — 58 or 80 mm * @returns {Promise} */ async function printSale(saleId, width) { width = width || 80; const token = localStorage.getItem('pos_token'); const resp = await fetch('/pos/api/sales/' + saleId + '/print', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ printer_type: 'escpos_raw', width: width }) }); if (!resp.ok) { console.error('[NexusPrinter] backend error', resp.status); return false; } const buf = await resp.arrayBuffer(); return sendRaw(new Uint8Array(buf)); } // ── Persistence helpers ──────────────────────── function _save() { try { localStorage.setItem('nexus_printer', _type || ''); } catch (e) {} } // ── Public API ───────────────────────────────── return { connect: connect, disconnect: disconnect, isConnected: isConnected, sendRaw: sendRaw, printSale: printSale }; })();