Public Access
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.
This commit is contained in:
+63
-30
@@ -1,7 +1,7 @@
|
|||||||
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
||||||
|
|
||||||
import { getMedia, buildJob, PIXEL_WIDTH } from './ql-raster.js';
|
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';
|
import { describeProblem, checkMediaMatches } from './ql-status.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,11 +9,14 @@ import { describeProblem, checkMediaMatches } from './ql-status.js';
|
|||||||
* printer's raster protocol directly.
|
* printer's raster protocol directly.
|
||||||
*
|
*
|
||||||
* This is the drop-in replacement for shelling out to the Python brother_ql
|
* 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
|
* CLI in printer.js. Same job, minus a Python runtime in the image and minus
|
||||||
* writing every badge to a temp file, and with the printer's actual status
|
* writing every badge to a temp file.
|
||||||
* 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
|
* What it does NOT buy us is reliable printer state. The QL-820NWB accepts
|
||||||
* afterwards.
|
* 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,
|
* Input is the PNG renderBadgePng() already produces, so the badge layout,
|
||||||
* rotation and vertical centring are untouched.
|
* 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 {string} [options.label='62'] The site's printer_label value
|
||||||
* @param {boolean} [options.cut=true]
|
* @param {boolean} [options.cut=true]
|
||||||
* @param {number} [options.threshold] Luminance cutover, 0-255; higher is bolder
|
* @param {number} [options.threshold] Luminance cutover, 0-255; higher is bolder
|
||||||
* @param {boolean} [options.checkMedia=true]
|
* @param {boolean} [options.checkMedia=true] Best effort; skipped if the printer is mute
|
||||||
* @returns {Promise<{ok: boolean, message?: string}>}
|
* @returns {Promise<{ok: boolean, confirmed?: boolean, message: string}>}
|
||||||
*/
|
*/
|
||||||
export async function printPng(png, options = {}) {
|
export async function printPng(png, options = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -131,9 +134,20 @@ export async function printPng(png, options = {}) {
|
|||||||
return enqueue(host, async () => {
|
return enqueue(host, async () => {
|
||||||
try {
|
try {
|
||||||
if (checkMedia) {
|
if (checkMedia) {
|
||||||
const current = await queryStatus(host, { port });
|
const probe = await probeStatus(host, { port });
|
||||||
const problem = describeProblem(current) || checkMediaMatches(current, media);
|
if (!probe.reachable) {
|
||||||
if (problem) return { ok: false, message: problem };
|
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 });
|
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 result = await sendJob(host, job, { port });
|
||||||
const last = result.frames[result.frames.length - 1];
|
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 (problem) return { ok: false, message: problem };
|
||||||
if (!result.completed && result.timedOut) {
|
|
||||||
return {
|
// Without status frames, "delivered" is as strong a claim as we can make.
|
||||||
ok: false,
|
// Don't dress that up as confirmation the label physically came out.
|
||||||
message: 'The printer took the label but never confirmed it finished.',
|
return {
|
||||||
};
|
ok: true,
|
||||||
}
|
confirmed: result.statusAvailable,
|
||||||
return { ok: true, message: `Printed to ${host}` };
|
message: result.statusAvailable
|
||||||
|
? `Printed to ${host}`
|
||||||
|
: `Sent to ${host} (this printer does not confirm over the network)`,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, message: err.message };
|
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
|
* Ask the printer how it is, without printing.
|
||||||
* console so reception sees "roll empty" before a visitor is at the desk.
|
*
|
||||||
|
* `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' }) {
|
export async function printerHealth({ host, port = DEFAULT_PORT, label = '62' }) {
|
||||||
if (!host) return { online: false, ready: false, message: 'No printer configured' };
|
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 mapping = LABEL_TO_MEDIA[label] || LABEL_TO_MEDIA['62'];
|
||||||
const media = getMedia(mapping.media);
|
const media = getMedia(mapping.media);
|
||||||
|
|
||||||
try {
|
const probe = await probeStatus(host, { port });
|
||||||
const current = await queryStatus(host, { port });
|
|
||||||
const problem = describeProblem(current) || checkMediaMatches(current, media);
|
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 {
|
return {
|
||||||
online: true,
|
online: true,
|
||||||
ready: !problem,
|
ready: null,
|
||||||
message: problem || 'Ready',
|
message:
|
||||||
mediaWidthMm: current.mediaWidthMm,
|
'Reachable. This printer does not report status over the network, so ' +
|
||||||
mediaType: current.mediaType,
|
'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 };
|
export { LABEL_TO_MEDIA };
|
||||||
|
|||||||
Reference in New Issue
Block a user