From 851e021fc2d4255c4d48bdebc43db47a92c8039a Mon Sep 17 00:00:00 2001 From: jessikitty Date: Mon, 7 Sep 2026 14:17:47 +1000 Subject: [PATCH] Make printer status best effort, not a precondition A silent printer no longer blocks printing or reports as unhealthy. printPng returns confirmed:false when no status came back, so callers don't claim a label came out when all we know is the bytes were delivered. --- src/printing/ql-print.js | 93 +++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/src/printing/ql-print.js b/src/printing/ql-print.js index 4c608cf..74161fa 100644 --- a/src/printing/ql-print.js +++ b/src/printing/ql-print.js @@ -1,7 +1,7 @@ import { createCanvas, loadImage } from '@napi-rs/canvas'; import { getMedia, buildJob, PIXEL_WIDTH } from './ql-raster.js'; -import { queryStatus, sendJob, DEFAULT_PORT } from './ql-transport.js'; +import { probeStatus, sendJob, DEFAULT_PORT } from './ql-transport.js'; import { describeProblem, checkMediaMatches } from './ql-status.js'; /** @@ -9,11 +9,14 @@ import { describeProblem, checkMediaMatches } from './ql-status.js'; * printer's raster protocol directly. * * This is the drop-in replacement for shelling out to the Python brother_ql - * CLI in printer.js. Same job, minus a Python runtime in the image, minus - * writing every badge to a temp file, and with the printer's actual status - * frames instead of a parsed traceback — so "cover open" and "wrong roll" are - * reported before a label is wasted rather than guessed at from stderr - * afterwards. + * CLI in printer.js. Same job, minus a Python runtime in the image and minus + * writing every badge to a temp file. + * + * What it does NOT buy us is reliable printer state. The QL-820NWB accepts + * jobs on port 9100 but stays silent over TCP; status frames only come back + * over USB. So roll type, roll level and cover state are unknowable from here, + * exactly as they were with brother_ql. Status handling below is best effort: + * used when offered, never required. * * Input is the PNG renderBadgePng() already produces, so the badge layout, * rotation and vertical centring are untouched. @@ -109,8 +112,8 @@ async function pngToPlanes(png, media, { red, threshold = 180 }) { * @param {string} [options.label='62'] The site's printer_label value * @param {boolean} [options.cut=true] * @param {number} [options.threshold] Luminance cutover, 0-255; higher is bolder - * @param {boolean} [options.checkMedia=true] - * @returns {Promise<{ok: boolean, message?: string}>} + * @param {boolean} [options.checkMedia=true] Best effort; skipped if the printer is mute + * @returns {Promise<{ok: boolean, confirmed?: boolean, message: string}>} */ export async function printPng(png, options = {}) { const { @@ -131,9 +134,20 @@ export async function printPng(png, options = {}) { return enqueue(host, async () => { try { if (checkMedia) { - const current = await queryStatus(host, { port }); - const problem = describeProblem(current) || checkMediaMatches(current, media); - if (problem) return { ok: false, message: problem }; + const probe = await probeStatus(host, { port }); + if (!probe.reachable) { + return { + ok: false, + message: probe.error || `Cannot reach printer at ${host}:${port}`, + }; + } + // A silent printer is the normal case over TCP, so absent status must + // never block the job — the visitor is already signed in and waiting. + if (probe.status) { + const problem = + describeProblem(probe.status) || checkMediaMatches(probe.status, media); + if (problem) return { ok: false, message: problem }; + } } const page = await pngToPlanes(png, media, { red: mapping.red, threshold }); @@ -141,16 +155,19 @@ export async function printPng(png, options = {}) { const result = await sendJob(host, job, { port }); const last = result.frames[result.frames.length - 1]; - const problem = describeProblem(last); + const problem = last ? describeProblem(last) : null; if (problem) return { ok: false, message: problem }; - if (!result.completed && result.timedOut) { - return { - ok: false, - message: 'The printer took the label but never confirmed it finished.', - }; - } - return { ok: true, message: `Printed to ${host}` }; + + // Without status frames, "delivered" is as strong a claim as we can make. + // Don't dress that up as confirmation the label physically came out. + return { + ok: true, + confirmed: result.statusAvailable, + message: result.statusAvailable + ? `Printed to ${host}` + : `Sent to ${host} (this printer does not confirm over the network)`, + }; } catch (err) { return { ok: false, message: err.message }; } @@ -158,8 +175,11 @@ export async function printPng(png, options = {}) { } /** - * Ask the printer how it is, without printing. Worth polling from the admin - * console so reception sees "roll empty" before a visitor is at the desk. + * Ask the printer how it is, without printing. + * + * `ready` is tri-state: true, false, or null for "reachable but it won't say", + * which is the normal answer over the network. Treat null as unknown in the + * admin console rather than colouring it green or red. */ export async function printerHealth({ host, port = DEFAULT_PORT, label = '62' }) { if (!host) return { online: false, ready: false, message: 'No printer configured' }; @@ -167,19 +187,32 @@ export async function printerHealth({ host, port = DEFAULT_PORT, label = '62' }) const mapping = LABEL_TO_MEDIA[label] || LABEL_TO_MEDIA['62']; const media = getMedia(mapping.media); - try { - const current = await queryStatus(host, { port }); - const problem = describeProblem(current) || checkMediaMatches(current, media); + const probe = await probeStatus(host, { port }); + + if (!probe.reachable) { + return { online: false, ready: false, message: probe.error || 'Printer did not answer' }; + } + + // The usual case on this hardware: reachable, but mute. Report that honestly + // rather than dressing silence up as either health or failure. + if (!probe.status) { return { online: true, - ready: !problem, - message: problem || 'Ready', - mediaWidthMm: current.mediaWidthMm, - mediaType: current.mediaType, + ready: null, + message: + 'Reachable. This printer does not report status over the network, so ' + + 'roll type, roll level and cover state cannot be checked from here.', }; - } catch (err) { - return { online: false, ready: false, message: err.message }; } + + const problem = describeProblem(probe.status) || checkMediaMatches(probe.status, media); + return { + online: true, + ready: !problem, + message: problem || 'Ready', + mediaWidthMm: probe.status.mediaWidthMm, + mediaType: probe.status.mediaType, + }; } export { LABEL_TO_MEDIA };