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.
|
* Probe the printer.
|
||||||
* Safe to call on a schedule for a "printer offline" banner in the admin console.
|
|
||||||
*
|
*
|
||||||
* @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 {
|
const {
|
||||||
port = DEFAULT_PORT,
|
port = DEFAULT_PORT,
|
||||||
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||||
replyTimeout = 5000,
|
replyTimeout = 2000,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
try {
|
||||||
let received = Buffer.alloc(0);
|
return await withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||||
|
let received = Buffer.alloc(0);
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
// No reply is the expected outcome over the network, so this timer is the
|
||||||
finish(
|
// normal path rather than an error path. Keep it short.
|
||||||
new Error(
|
const timer = setTimeout(
|
||||||
`Printer at ${host}:${port} accepted the connection but sent no status. ` +
|
() => finish(null, { reachable: true, status: null, error: null }),
|
||||||
'It may be mid-job, or another host may be holding the port.'
|
replyTimeout
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}, replyTimeout);
|
|
||||||
|
|
||||||
socket.on('data', (chunk) => {
|
socket.on('data', (chunk) => {
|
||||||
received = Buffer.concat([received, chunk]);
|
received = Buffer.concat([received, chunk]);
|
||||||
if (received.length >= FRAME_LENGTH) {
|
if (received.length >= FRAME_LENGTH) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
const frames = decodeAll(received);
|
const frames = decodeAll(received);
|
||||||
if (frames.length === 0) {
|
finish(null, {
|
||||||
finish(new Error('Printer sent an unrecognisable status frame'));
|
reachable: true,
|
||||||
} else {
|
status: frames.length ? frames[frames.length - 1] : null,
|
||||||
finish(null, frames[frames.length - 1]);
|
error: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
socket.write(buildStatusRequest(), (err) => {
|
socket.write(buildStatusRequest(), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(err);
|
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
|
* Completion is signalled by the write flushing and the socket closing, NOT by
|
||||||
* as success — a job can print fine and still emit a notification frame.
|
* 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
|
* The write is followed by end(), not destroy(). destroy() can discard data
|
||||||
* @param {Buffer} job
|
* still sitting in the kernel send buffer, which for a ~190 KB label job means
|
||||||
* @returns {Promise<{frames: Object[], completed: boolean, timedOut?: boolean}>}
|
* a silently truncated print.
|
||||||
|
*
|
||||||
|
* @returns {Promise<{frames: Object[], completed: boolean, statusAvailable: boolean}>}
|
||||||
*/
|
*/
|
||||||
async function sendJob(host, job, options = {}) {
|
async function sendJob(host, job, options = {}) {
|
||||||
const {
|
const {
|
||||||
port = DEFAULT_PORT,
|
port = DEFAULT_PORT,
|
||||||
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||||
jobTimeout = DEFAULT_JOB_TIMEOUT,
|
jobTimeout = DEFAULT_JOB_TIMEOUT,
|
||||||
waitForCompletion = true,
|
graceMs = 3000,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||||
let received = Buffer.alloc(0);
|
let received = Buffer.alloc(0);
|
||||||
const frames = [];
|
const frames = [];
|
||||||
|
let sawCompleted = false;
|
||||||
|
let flushed = false;
|
||||||
|
|
||||||
|
const done = () =>
|
||||||
|
finish(null, {
|
||||||
|
frames,
|
||||||
|
completed: sawCompleted || flushed,
|
||||||
|
statusAvailable: frames.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (frames.length > 0) {
|
finish(
|
||||||
// We heard something; report what we know rather than a bare timeout.
|
new Error(
|
||||||
finish(null, { frames, completed: false, timedOut: true });
|
`Printer at ${host}:${port} did not accept the whole job within ${jobTimeout} ms`
|
||||||
} else {
|
)
|
||||||
finish(
|
);
|
||||||
new Error(
|
|
||||||
`Printer at ${host}:${port} did not acknowledge the job within ${jobTimeout} ms`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, jobTimeout);
|
}, jobTimeout);
|
||||||
|
|
||||||
socket.on('data', (chunk) => {
|
socket.on('data', (chunk) => {
|
||||||
@@ -133,30 +148,38 @@ async function sendJob(host, job, options = {}) {
|
|||||||
|
|
||||||
if (frame.hasError) {
|
if (frame.hasError) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(null, { frames, completed: false });
|
finish(null, { frames, completed: false, statusAvailable: true });
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame.statusType === 'printing_completed') {
|
|
||||||
clearTimeout(timer);
|
|
||||||
finish(null, { frames, completed: true });
|
|
||||||
return;
|
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) => {
|
socket.write(job, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(err);
|
finish(err);
|
||||||
return;
|
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);
|
clearTimeout(timer);
|
||||||
// Give the printer a beat to reject the job outright.
|
done();
|
||||||
setTimeout(() => finish(null, { frames, completed: false }), 250);
|
}, graceMs);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { DEFAULT_PORT, queryStatus, sendJob };
|
export { DEFAULT_PORT, probeStatus, sendJob };
|
||||||
|
|||||||
Reference in New Issue
Block a user