/** * Asks the printer what it actually has loaded, and what it is complaining about. * * docker compose exec visitor-signin node scripts/printer-status.mjs * docker compose exec visitor-signin node scripts/printer-status.mjs --host 10.0.0.5 * * brother_ql's network backend only writes to the socket; it never reads, which is * why a refused job still looks like a success. The Brother raster protocol has a * status request that returns a 32 byte block describing the media in the machine * and any error, so we ask directly. */ import net from 'node:net'; import db from '../src/db.js'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); return i > -1 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : fallback; } const site = arg('site') ? db.prepare('SELECT * FROM sites WHERE id = ?').get(Number(arg('site'))) : db.prepare('SELECT * FROM sites WHERE printer_host IS NOT NULL ORDER BY id LIMIT 1').get(); const host = arg('host', site?.printer_host); const port = Number(arg('port', site?.printer_port || 9100)); if (!host) { console.error('No printer address. Set one in Admin -> Sites, or pass --host.'); process.exit(1); } /* ------------------------------------------------------------- decoding */ const MEDIA_TYPES = { 0x00: 'no media loaded', 0x0a: 'continuous roll', 0x0b: 'die-cut labels', 0x4a: 'continuous roll (cleaning)', 0x4b: 'die-cut labels (cleaning)', }; const ERRORS_1 = [ [0x01, 'no media loaded'], [0x02, 'end of media'], [0x04, 'cutter jam'], [0x08, 'weak batteries'], [0x10, 'printer in use'], [0x80, 'printer turned off'], ]; const ERRORS_2 = [ [0x01, 'wrong media — the job does not match the roll that is loaded'], [0x04, 'expansion buffer full'], [0x08, 'communication error'], [0x10, 'communication buffer full'], [0x20, 'cover is open'], [0x40, 'cancel key pressed'], [0x80, 'media cannot be fed'], ]; function decode(buf) { if (buf.length < 32) return { error: `Short reply (${buf.length} bytes).` }; const mediaWidth = buf[10]; const mediaType = buf[11]; const mediaLength = buf[17]; return { mediaWidthMm: mediaWidth, mediaLengthMm: mediaLength, mediaType: MEDIA_TYPES[mediaType] || `unknown (0x${mediaType.toString(16)})`, mediaTypeRaw: mediaType, errors: [ ...ERRORS_1.filter(([bit]) => buf[8] & bit).map(([, text]) => text), ...ERRORS_2.filter(([bit]) => buf[9] & bit).map(([, text]) => text), ], raw: buf.subarray(0, 32).toString('hex').replace(/(..)/g, '$1 ').trim(), }; } /** The label id brother_ql should be given, worked out from what is loaded. */ function suggestLabel(status) { if (status.mediaTypeRaw === 0x00) return null; const continuous = status.mediaTypeRaw === 0x0a || status.mediaTypeRaw === 0x4a; if (continuous) { return String(status.mediaWidthMm); // 62, 29, 12 ... } return status.mediaLengthMm ? `${status.mediaWidthMm}x${status.mediaLengthMm}` : `${status.mediaWidthMm} (die-cut, length unknown)`; } /* --------------------------------------------------------------- asking */ console.log(`\n Asking ${host}:${port} what it has loaded...\n`); const socket = net.createConnection({ host, port, timeout: 8000 }); const chunks = []; socket.on('connect', () => { // 200 null bytes clears any half-finished job, then initialise, then ask. socket.write(Buffer.alloc(200, 0x00)); socket.write(Buffer.from([0x1b, 0x40])); socket.write(Buffer.from([0x1b, 0x69, 0x53])); // Some firmware only answers once it is in raster mode, so ask again that way // before giving up. setTimeout(() => { if (!chunks.length && !socket.destroyed) { socket.write(Buffer.from([0x1b, 0x69, 0x61, 0x01])); socket.write(Buffer.from([0x1b, 0x69, 0x53])); } }, 1500); }); socket.on('data', (d) => { chunks.push(d); if (Buffer.concat(chunks).length >= 32) socket.end(); }); socket.on('timeout', () => { socket.destroy(); if (!chunks.length) { console.error(' The printer accepted the connection but sent nothing back.'); console.error(''); console.error(' Many Brother network print servers are write-only on port 9100: they accept'); console.error(' jobs but never report status, even though the same printer answers happily'); console.error(' over USB. If this is one of them, no amount of asking will help.'); console.error(''); console.error(' Read the printer instead from:'); console.error(' - the display on the machine itself'); console.error(' - Brother Status Monitor, on a PC with the printer installed'); console.error(' - the printer\'s own web page, at http://' + host + '/'); console.error(''); console.error(' Status Monitor in particular gives the exact reason a job was refused,'); console.error(' which is more than brother_ql can tell you — its network backend never'); console.error(' reads the socket, so it reports success whatever the printer does.'); console.error(''); process.exit(1); } }); socket.on('error', (err) => { console.error(` Could not reach it: ${err.message}\n`); process.exit(1); }); socket.on('close', () => { const buf = Buffer.concat(chunks); if (!buf.length) process.exit(1); const status = decode(buf); if (status.error) { console.error(` ${status.error}\n raw: ${buf.toString('hex')}\n`); process.exit(1); } console.log(` media loaded ${status.mediaWidthMm} mm ${status.mediaType}`); if (status.mediaLengthMm) console.log(` label length ${status.mediaLengthMm} mm`); console.log(` errors ${status.errors.length ? status.errors.join('; ') : 'none reported'}`); console.log(` raw status ${status.raw}`); const suggested = suggestLabel(status); console.log(''); if (!suggested) { console.log(' No media detected. Open and close the cover to make it re-read the roll.'); } else { console.log(` Use this roll id: --label ${suggested}`); console.log(` Try it with: node scripts/print-test.mjs --label ${suggested}`); } console.log(''); });