Files
visitor-signin/scripts/printer-status.mjs
T
2026-09-16 10:05:57 +10:00

152 lines
5.0 KiB
JavaScript

/**
* 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]));
});
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(' Some firmware only answers when idle — make sure it is not mid-job,');
console.error(' and that nothing else is holding port 9100 open.\n');
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('');
});