Public Access
Add TCP transport for the QL printer
Connects to port 9100, sends the job and reads back status frames until the printer reports completion or an error.
This commit is contained in:
@@ -0,0 +1,162 @@
|
|||||||
|
import net from 'node:net';
|
||||||
|
|
||||||
|
import { decodeAll, FRAME_LENGTH } from './ql-status.js';
|
||||||
|
import { buildStatusRequest } from './ql-raster.js';
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 9100;
|
||||||
|
const DEFAULT_CONNECT_TIMEOUT = 5000;
|
||||||
|
const DEFAULT_JOB_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a socket, run `handler`, and always clean up afterwards.
|
||||||
|
* The QL only accepts one connection at a time, so every helper here
|
||||||
|
* connects, does its work, and disconnects.
|
||||||
|
*/
|
||||||
|
function withSocket(host, port, connectTimeout, handler) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = new net.Socket();
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (err, value) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
socket.removeAllListeners();
|
||||||
|
socket.destroy();
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.setTimeout(connectTimeout);
|
||||||
|
socket.once('timeout', () =>
|
||||||
|
finish(new Error(`Timed out connecting to ${host}:${port}`))
|
||||||
|
);
|
||||||
|
socket.once('error', (err) =>
|
||||||
|
finish(new Error(`Cannot reach printer at ${host}:${port} — ${err.message}`))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.connect(port, host, () => {
|
||||||
|
socket.setTimeout(0);
|
||||||
|
handler(socket, finish);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} decoded status frame
|
||||||
|
*/
|
||||||
|
async function queryStatus(host, options = {}) {
|
||||||
|
const {
|
||||||
|
port = DEFAULT_PORT,
|
||||||
|
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||||
|
replyTimeout = 5000,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return 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.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 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.write(buildStatusRequest(), (err) => {
|
||||||
|
if (err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
finish(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a prepared job and wait for the printer to report completion.
|
||||||
|
*
|
||||||
|
* Resolves with every status frame received. The caller decides what counts
|
||||||
|
* as success — a job can print fine and still emit a notification frame.
|
||||||
|
*
|
||||||
|
* @param {string} host
|
||||||
|
* @param {Buffer} job
|
||||||
|
* @returns {Promise<{frames: Object[], completed: boolean, timedOut?: boolean}>}
|
||||||
|
*/
|
||||||
|
async function sendJob(host, job, options = {}) {
|
||||||
|
const {
|
||||||
|
port = DEFAULT_PORT,
|
||||||
|
connectTimeout = DEFAULT_CONNECT_TIMEOUT,
|
||||||
|
jobTimeout = DEFAULT_JOB_TIMEOUT,
|
||||||
|
waitForCompletion = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return withSocket(host, port, connectTimeout, (socket, finish) => {
|
||||||
|
let received = Buffer.alloc(0);
|
||||||
|
const frames = [];
|
||||||
|
|
||||||
|
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`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, jobTimeout);
|
||||||
|
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
received = Buffer.concat([received, chunk]);
|
||||||
|
while (received.length >= FRAME_LENGTH) {
|
||||||
|
const [frame] = decodeAll(received.subarray(0, FRAME_LENGTH));
|
||||||
|
received = received.subarray(FRAME_LENGTH);
|
||||||
|
if (!frame) continue;
|
||||||
|
frames.push(frame);
|
||||||
|
|
||||||
|
if (frame.hasError) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
finish(null, { frames, completed: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame.statusType === 'printing_completed') {
|
||||||
|
clearTimeout(timer);
|
||||||
|
finish(null, { frames, completed: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.write(job, (err) => {
|
||||||
|
if (err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
finish(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!waitForCompletion) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
// Give the printer a beat to reject the job outright.
|
||||||
|
setTimeout(() => finish(null, { frames, completed: false }), 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DEFAULT_PORT, queryStatus, sendJob };
|
||||||
Reference in New Issue
Block a user