Public Access
Treat a silent printer as normal, and flush before closing
The QL-820NWB does not report status over TCP 9100 — brother_ql documents the same limitation for its network backend. queryStatus therefore became probeStatus, which reports reachable-with-unknown-state instead of failing. Also fixes a real bug: the socket was destroyed straight after write, which can discard data still queued in the kernel send buffer and truncate a ~190 KB job. Now half-closes with end() and waits for flush, with a grace window for units that never send FIN.
This commit is contained in:
@@ -42,85 +42,100 @@ function withSocket(host, port, connectTimeout, handler) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the printer what state it's in without sending a print job.
|
||||
* Safe to call on a schedule for a "printer offline" banner in the admin console.
|
||||
* Probe the printer.
|
||||
*
|
||||
* @returns {Promise<Object>} decoded status frame
|
||||
* Never throws for the "connected but silent" case, because that is the normal
|
||||
* result on this hardware: the QL-820NWB accepts jobs on port 9100 but does not
|
||||
* report state back over TCP — status frames only come back over USB. brother_ql
|
||||
* documents the same limitation for its network backend. A silent printer is
|
||||
* therefore reachable-and-probably-fine, not broken, and callers must treat a
|
||||
* null status as "unknown" rather than "bad".
|
||||
*
|
||||
* @returns {Promise<{reachable: boolean, status: Object|null, error: string|null}>}
|
||||
*/
|
||||
async function queryStatus(host, options = {}) {
|
||||
async function probeStatus(host, options = {}) {
|
||||
const {
|
||||
port = DEFAULT_PORT,
|
||||
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||
replyTimeout = 5000,
|
||||
replyTimeout = 2000,
|
||||
} = options;
|
||||
|
||||
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||
let received = Buffer.alloc(0);
|
||||
try {
|
||||
return await withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||
let received = Buffer.alloc(0);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
finish(
|
||||
new Error(
|
||||
`Printer at ${host}:${port} accepted the connection but sent no status. ` +
|
||||
'It may be mid-job, or another host may be holding the port.'
|
||||
)
|
||||
// No reply is the expected outcome over the network, so this timer is the
|
||||
// normal path rather than an error path. Keep it short.
|
||||
const timer = setTimeout(
|
||||
() => finish(null, { reachable: true, status: null, error: null }),
|
||||
replyTimeout
|
||||
);
|
||||
}, replyTimeout);
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
received = Buffer.concat([received, chunk]);
|
||||
if (received.length >= FRAME_LENGTH) {
|
||||
clearTimeout(timer);
|
||||
const frames = decodeAll(received);
|
||||
if (frames.length === 0) {
|
||||
finish(new Error('Printer sent an unrecognisable status frame'));
|
||||
} else {
|
||||
finish(null, frames[frames.length - 1]);
|
||||
socket.on('data', (chunk) => {
|
||||
received = Buffer.concat([received, chunk]);
|
||||
if (received.length >= FRAME_LENGTH) {
|
||||
clearTimeout(timer);
|
||||
const frames = decodeAll(received);
|
||||
finish(null, {
|
||||
reachable: true,
|
||||
status: frames.length ? frames[frames.length - 1] : null,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.write(buildStatusRequest(), (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timer);
|
||||
finish(err);
|
||||
}
|
||||
socket.write(buildStatusRequest(), (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timer);
|
||||
finish(null, { reachable: false, status: null, error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
return { reachable: false, status: null, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prepared job and wait for the printer to report completion.
|
||||
* Send a prepared job.
|
||||
*
|
||||
* Resolves with every status frame received. The caller decides what counts
|
||||
* as success — a job can print fine and still emit a notification frame.
|
||||
* Completion is signalled by the write flushing and the socket closing, NOT by
|
||||
* a status frame — see probeStatus. If the printer does happen to answer, the
|
||||
* frames come back as a bonus and a reported error is treated as authoritative.
|
||||
*
|
||||
* @param {string} host
|
||||
* @param {Buffer} job
|
||||
* @returns {Promise<{frames: Object[], completed: boolean, timedOut?: boolean}>}
|
||||
* The write is followed by end(), not destroy(). destroy() can discard data
|
||||
* still sitting in the kernel send buffer, which for a ~190 KB label job means
|
||||
* a silently truncated print.
|
||||
*
|
||||
* @returns {Promise<{frames: Object[], completed: boolean, statusAvailable: boolean}>}
|
||||
*/
|
||||
async function sendJob(host, job, options = {}) {
|
||||
const {
|
||||
port = DEFAULT_PORT,
|
||||
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||
jobTimeout = DEFAULT_JOB_TIMEOUT,
|
||||
waitForCompletion = true,
|
||||
graceMs = 3000,
|
||||
} = options;
|
||||
|
||||
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||
let received = Buffer.alloc(0);
|
||||
const frames = [];
|
||||
let sawCompleted = false;
|
||||
let flushed = false;
|
||||
|
||||
const done = () =>
|
||||
finish(null, {
|
||||
frames,
|
||||
completed: sawCompleted || flushed,
|
||||
statusAvailable: frames.length > 0,
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (frames.length > 0) {
|
||||
// We heard something; report what we know rather than a bare timeout.
|
||||
finish(null, { frames, completed: false, timedOut: true });
|
||||
} else {
|
||||
finish(
|
||||
new Error(
|
||||
`Printer at ${host}:${port} did not acknowledge the job within ${jobTimeout} ms`
|
||||
)
|
||||
);
|
||||
}
|
||||
finish(
|
||||
new Error(
|
||||
`Printer at ${host}:${port} did not accept the whole job within ${jobTimeout} ms`
|
||||
)
|
||||
);
|
||||
}, jobTimeout);
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
@@ -133,30 +148,38 @@ async function sendJob(host, job, options = {}) {
|
||||
|
||||
if (frame.hasError) {
|
||||
clearTimeout(timer);
|
||||
finish(null, { frames, completed: false });
|
||||
return;
|
||||
}
|
||||
if (frame.statusType === 'printing_completed') {
|
||||
clearTimeout(timer);
|
||||
finish(null, { frames, completed: true });
|
||||
finish(null, { frames, completed: false, statusAvailable: true });
|
||||
return;
|
||||
}
|
||||
if (frame.statusType === 'printing_completed') sawCompleted = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Peer closed and our data has gone out: the job is delivered.
|
||||
socket.once('close', () => {
|
||||
clearTimeout(timer);
|
||||
done();
|
||||
});
|
||||
|
||||
socket.write(job, (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timer);
|
||||
finish(err);
|
||||
return;
|
||||
}
|
||||
if (!waitForCompletion) {
|
||||
flushed = true;
|
||||
// Half-close: flushes everything queued, then sends FIN.
|
||||
socket.end();
|
||||
|
||||
// Some units never close their half of the connection. Once our data is
|
||||
// out the job is delivered, so don't hold the queue open waiting on a FIN
|
||||
// that may never arrive.
|
||||
setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
// Give the printer a beat to reject the job outright.
|
||||
setTimeout(() => finish(null, { frames, completed: false }), 250);
|
||||
}
|
||||
done();
|
||||
}, graceMs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { DEFAULT_PORT, queryStatus, sendJob };
|
||||
export { DEFAULT_PORT, probeStatus, sendJob };
|
||||
|
||||
Reference in New Issue
Block a user