Enhance document size management in DocumentPrinterClient and related interfaces
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- Introduced a `getDocumentSizeId` utility function to streamline document size ID retrieval. - Updated `DocumentPrinterClient` to manage subscriptions for current document sizes, ensuring accurate updates and handling of document size changes. - Enhanced `DocumentPrinterManager` to handle document size updates and propagate changes to connected clients. - Improved `CupsInterface` and `ReceiptInterface` to support document size resolution and default media application based on current document sizes. - Added detailed logging for document size updates and error handling to improve debugging and user feedback.
This commit is contained in:
parent
d04e722366
commit
e7bf650e82
@ -11,6 +11,18 @@ const config = loadConfig();
|
||||
const logger = log4js.getLogger("Document Printer Client");
|
||||
logger.level = config.logLevel;
|
||||
|
||||
function getDocumentSizeId(documentSize) {
|
||||
if (!documentSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof documentSize === "string") {
|
||||
return documentSize;
|
||||
}
|
||||
|
||||
return documentSize._id?.toString?.() ?? null;
|
||||
}
|
||||
|
||||
export class DocumentPrinterClient {
|
||||
constructor(
|
||||
documentPrinter = {
|
||||
@ -29,16 +41,18 @@ export class DocumentPrinterClient {
|
||||
this.socketClient = documentPrinterManager.socketClient;
|
||||
this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os
|
||||
this.state = { type: this.active == true ? "offline" : "inactive" };
|
||||
this.paperState = documentPrinter.paperState || { type: "unknown" };
|
||||
this.isOnline = documentPrinter.online || false;
|
||||
this.shouldReconnect = true;
|
||||
this.isConnecting = false;
|
||||
this.reconnectTimeout = null;
|
||||
this.isProcessingQueue = false;
|
||||
this.eventUpdateInterval = null;
|
||||
this.subscribedDocumentSizeId = null;
|
||||
this.initializeInterface();
|
||||
this.registerEventHandlers();
|
||||
this.subscribeToActions();
|
||||
this.subscribeToObjectUpdates();
|
||||
this.syncCurrentDocumentSizeSubscription();
|
||||
}
|
||||
|
||||
initializeInterface() {
|
||||
@ -68,6 +82,12 @@ export class DocumentPrinterClient {
|
||||
logger.debug(`Updating document printer ${this.id} with data...`);
|
||||
sendIPC("setDocumentPrinter", { _id: this.id, ...data });
|
||||
|
||||
const previousDocumentSizeId = getDocumentSizeId(
|
||||
this.documentPrinter?.currentDocumentSize,
|
||||
);
|
||||
const previousRotateOrientation =
|
||||
this.documentPrinter?.rotateOrientation == true;
|
||||
|
||||
// Check for connection changes before updating
|
||||
|
||||
if (data?.connection) {
|
||||
@ -83,9 +103,20 @@ export class DocumentPrinterClient {
|
||||
newConnection.interface != null &&
|
||||
newConnection.interface !== oldConnection.interface;
|
||||
const portChanged = newConnection?.port !== oldConnection?.port;
|
||||
const usernameChanged =
|
||||
newConnection.username != null &&
|
||||
newConnection.username !== oldConnection.username;
|
||||
const passwordChanged =
|
||||
newConnection.password != null &&
|
||||
newConnection.password !== oldConnection.password;
|
||||
|
||||
const connectionChanged =
|
||||
hostChanged || interfaceChanged || portChanged || protocolChanged;
|
||||
hostChanged ||
|
||||
interfaceChanged ||
|
||||
portChanged ||
|
||||
protocolChanged ||
|
||||
usernameChanged ||
|
||||
passwordChanged;
|
||||
|
||||
logger.debug(`Connection changed: ${connectionChanged}`);
|
||||
logger.debug(`Host changed: ${hostChanged}`);
|
||||
@ -107,11 +138,34 @@ export class DocumentPrinterClient {
|
||||
await this.printerInterface.disconnect();
|
||||
this.initializeInterface();
|
||||
await this.reconnect();
|
||||
} else if (data?.connection) {
|
||||
this.connection = { ...oldConnection, ...newConnection };
|
||||
}
|
||||
}
|
||||
|
||||
this.documentPrinter = { ...this.documentPrinter, ...data };
|
||||
|
||||
const nextDocumentSizeId = Object.hasOwn(data || {}, "currentDocumentSize")
|
||||
? getDocumentSizeId(data.currentDocumentSize)
|
||||
: previousDocumentSizeId;
|
||||
const nextRotateOrientation = Object.hasOwn(data || {}, "rotateOrientation")
|
||||
? data.rotateOrientation == true
|
||||
: previousRotateOrientation;
|
||||
const documentSizeChanged = nextDocumentSizeId !== previousDocumentSizeId;
|
||||
const rotateOrientationChanged =
|
||||
nextRotateOrientation !== previousRotateOrientation;
|
||||
|
||||
if (documentSizeChanged) {
|
||||
this.syncCurrentDocumentSizeSubscription();
|
||||
}
|
||||
|
||||
if (
|
||||
(documentSizeChanged || rotateOrientationChanged) &&
|
||||
this.printerInterface?.handleCurrentDocumentSizeChange
|
||||
) {
|
||||
await this.applyCurrentDocumentSizeDefaults();
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data || {}, "active") && data.active != this.active) {
|
||||
if (data.active == true) {
|
||||
await this.setActive();
|
||||
@ -121,6 +175,80 @@ export class DocumentPrinterClient {
|
||||
}
|
||||
}
|
||||
|
||||
syncCurrentDocumentSizeSubscription() {
|
||||
const nextDocumentSizeId = getDocumentSizeId(
|
||||
this.documentPrinter?.currentDocumentSize,
|
||||
);
|
||||
|
||||
if (this.subscribedDocumentSizeId === nextDocumentSizeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.subscribedDocumentSizeId) {
|
||||
this.socketClient.unsubscribeFromObjectUpdates({
|
||||
objectType: "documentSize",
|
||||
_id: this.subscribedDocumentSizeId,
|
||||
});
|
||||
}
|
||||
|
||||
this.subscribedDocumentSizeId = nextDocumentSizeId;
|
||||
|
||||
if (this.subscribedDocumentSizeId) {
|
||||
this.socketClient.subscribeToObjectUpdates({
|
||||
objectType: "documentSize",
|
||||
_id: this.subscribedDocumentSizeId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async applyCurrentDocumentSizeDefaults() {
|
||||
if (!this.printerInterface?.handleCurrentDocumentSizeChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaResult =
|
||||
await this.printerInterface.handleCurrentDocumentSizeChange(
|
||||
this.documentPrinter.currentDocumentSize,
|
||||
);
|
||||
if (mediaResult?.error) {
|
||||
logger.warn(
|
||||
`Failed to apply current document size for ${this.documentPrinter.name}: ${mediaResult.error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleCurrentDocumentSizeUpdate(documentSize) {
|
||||
const documentSizeId = getDocumentSizeId(documentSize);
|
||||
const currentDocumentSizeId = getDocumentSizeId(
|
||||
this.documentPrinter?.currentDocumentSize,
|
||||
);
|
||||
|
||||
if (
|
||||
!documentSizeId ||
|
||||
!currentDocumentSizeId ||
|
||||
documentSizeId !== currentDocumentSizeId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousDocumentSize =
|
||||
typeof this.documentPrinter.currentDocumentSize === "object" &&
|
||||
this.documentPrinter.currentDocumentSize
|
||||
? this.documentPrinter.currentDocumentSize
|
||||
: { _id: currentDocumentSizeId };
|
||||
|
||||
this.documentPrinter = {
|
||||
...this.documentPrinter,
|
||||
currentDocumentSize: {
|
||||
...previousDocumentSize,
|
||||
...documentSize,
|
||||
_id: documentSizeId,
|
||||
},
|
||||
};
|
||||
|
||||
await this.applyCurrentDocumentSizeDefaults();
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
// Register event handlers for document printer notifications
|
||||
// This can be extended as needed
|
||||
@ -140,6 +268,18 @@ export class DocumentPrinterClient {
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeFromCurrentDocumentSizeUpdates() {
|
||||
if (!this.subscribedDocumentSizeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socketClient.unsubscribeFromObjectUpdates({
|
||||
objectType: "documentSize",
|
||||
_id: this.subscribedDocumentSizeId,
|
||||
});
|
||||
this.subscribedDocumentSizeId = null;
|
||||
}
|
||||
|
||||
statesEqual(a, b) {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
@ -152,6 +292,44 @@ export class DocumentPrinterClient {
|
||||
);
|
||||
}
|
||||
|
||||
paperStatesEqual(a, b) {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
return a.type === b.type && a.message === b.message;
|
||||
}
|
||||
|
||||
shouldPreserveStateFromStatusPolling() {
|
||||
return ["printing", "initializing", "connecting"].includes(this.state?.type);
|
||||
}
|
||||
|
||||
applyPrinterStatus(status) {
|
||||
if (!status || status.error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
|
||||
if (status.state && !this.shouldPreserveStateFromStatusPolling()) {
|
||||
if (!this.statesEqual(this.state, status.state)) {
|
||||
this.state = status.state;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (status.paperState) {
|
||||
if (!this.paperStatesEqual(this.paperState, status.paperState)) {
|
||||
this.paperState = status.paperState;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
scheduleReconnect(delay = 30000) {
|
||||
if (!this.shouldReconnect || this.active === false) {
|
||||
return;
|
||||
@ -187,9 +365,7 @@ export class DocumentPrinterClient {
|
||||
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
// Always stop the event update interval when connecting
|
||||
clearInterval(this.eventUpdateInterval);
|
||||
this.eventUpdateInterval = null;
|
||||
await this.stopStatusMonitoring();
|
||||
this.state = { type: "connecting", message: null };
|
||||
this.isOnline = false;
|
||||
await this.updateDocumentPrinterState();
|
||||
@ -241,6 +417,8 @@ export class DocumentPrinterClient {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.syncCurrentDocumentSizeSubscription();
|
||||
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
|
||||
@ -295,10 +473,7 @@ export class DocumentPrinterClient {
|
||||
}
|
||||
this.state = { type: "standby", message: null };
|
||||
await this.updateDocumentPrinterState();
|
||||
this.eventUpdateInterval = setInterval(
|
||||
this.handleEventUpdate.bind(this),
|
||||
3000,
|
||||
);
|
||||
await this.startStatusMonitoring();
|
||||
await stopWaiting();
|
||||
return true;
|
||||
}
|
||||
@ -309,16 +484,30 @@ export class DocumentPrinterClient {
|
||||
}
|
||||
}
|
||||
|
||||
async handleEventUpdate() {
|
||||
if (this.printerInterface && this.printerInterface.retrieveStatus) {
|
||||
try {
|
||||
//await this.printerInterface.retrieveStatus();
|
||||
} catch (error) {
|
||||
async startStatusMonitoring() {
|
||||
if (this.printerInterface?.startStatusMonitoring) {
|
||||
await this.printerInterface.startStatusMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
async stopStatusMonitoring() {
|
||||
if (this.printerInterface?.stopStatusMonitoring) {
|
||||
await this.printerInterface.stopStatusMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
async handlePrinterStatusUpdate(status) {
|
||||
if (!status || status.error) {
|
||||
if (status?.error) {
|
||||
logger.error(
|
||||
`Error retrieving status for document printer ${this.documentPrinter.name}:`,
|
||||
error,
|
||||
`Error retrieving status for document printer ${this.documentPrinter.name}: ${status.error}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.applyPrinterStatus(status)) {
|
||||
await this.updateDocumentPrinterState();
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,6 +515,7 @@ export class DocumentPrinterClient {
|
||||
const updateData = {
|
||||
online: this.isOnline,
|
||||
state: this.state,
|
||||
paperState: this.paperState,
|
||||
connectedAt: this.connectedAt ?? null,
|
||||
};
|
||||
|
||||
@ -337,6 +527,10 @@ export class DocumentPrinterClient {
|
||||
if (
|
||||
this.documentPrinter?.online === updateData.online &&
|
||||
this.statesEqual(this.documentPrinter?.state, updateData.state) &&
|
||||
this.paperStatesEqual(
|
||||
this.documentPrinter?.paperState,
|
||||
updateData.paperState,
|
||||
) &&
|
||||
connectedAtUnchanged
|
||||
) {
|
||||
return;
|
||||
@ -590,7 +784,10 @@ export class DocumentPrinterClient {
|
||||
);
|
||||
await this.updateJobState(jobId, { type: "printing" });
|
||||
|
||||
await this.printerInterface.print(jobId, quantity);
|
||||
const result = await this.printerInterface.print(jobId, quantity);
|
||||
if (result?.success === false) {
|
||||
throw new Error(result.error || "Print failed");
|
||||
}
|
||||
logger.info(
|
||||
`Successfully printed job ${jobId} (${quantity} ${quantity === 1 ? "copy" : "copies"}) for ${this.documentPrinter.name}`,
|
||||
);
|
||||
@ -630,14 +827,14 @@ export class DocumentPrinterClient {
|
||||
this.shouldReconnect = false;
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
// Always stop the event update interval when disconnecting
|
||||
clearInterval(this.eventUpdateInterval);
|
||||
this.eventUpdateInterval = null;
|
||||
this.unsubscribeFromCurrentDocumentSizeUpdates();
|
||||
await this.stopStatusMonitoring();
|
||||
if (this.printerInterface && this.printerInterface.disconnect) {
|
||||
await this.printerInterface.disconnect();
|
||||
}
|
||||
this.isOnline = false;
|
||||
this.state = { type: this.active == false ? "inactive" : "offline" };
|
||||
this.paperState = { type: "unknown" };
|
||||
this.isProcessingQueue = false;
|
||||
this.queue = []; // Clear queue on disconnect
|
||||
this.jobQuantities.clear();
|
||||
@ -660,6 +857,7 @@ export class DocumentPrinterClient {
|
||||
this.documentPrinter.active = true;
|
||||
this.shouldReconnect = true;
|
||||
this.isOnline = false;
|
||||
this.syncCurrentDocumentSizeSubscription();
|
||||
await this.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ export class DocumentPrinterManager {
|
||||
this.documentPrinters = await this.socketClient.listObjects({
|
||||
objectType: "documentPrinter",
|
||||
filter: { host: this.socketClient.id },
|
||||
populate: ["currentDocumentSize"],
|
||||
});
|
||||
|
||||
sendIPC("setDocumentPrinters", this.documentPrinters);
|
||||
@ -93,6 +94,21 @@ export class DocumentPrinterManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleDocumentSizeUpdate(id, data) {
|
||||
logger.debug("Handling document size update for id:", id);
|
||||
const documentSizeId = id?.toString?.() ?? `${id || ""}`;
|
||||
if (!documentSizeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const documentPrinterClient of this.documentPrinterClients.values()) {
|
||||
await documentPrinterClient.handleCurrentDocumentSizeUpdate({
|
||||
_id: documentSizeId,
|
||||
...(data.object || data),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDocumentPrinterClient(documentPrinterId) {
|
||||
return this.documentPrinterClients.get(documentPrinterId);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,17 @@
|
||||
// receiptinterface.js - Thermal receipt printer interface implementation
|
||||
import net from "net";
|
||||
import log4js from "log4js";
|
||||
import { loadConfig } from "../../config.js";
|
||||
import { ThermalPrinter, PrinterTypes } from "node-thermal-printer";
|
||||
import {
|
||||
buildStatusError,
|
||||
mapReceiptPrinterStatus,
|
||||
printerStatusesEqual,
|
||||
} from "../printerstatus.js";
|
||||
|
||||
const STATUS_TIMEOUT_MS = 5000;
|
||||
const STATUS_POLL_INTERVAL_MS = 1000;
|
||||
const VALID_STATUS_MASK = 0x12;
|
||||
|
||||
const config = loadConfig();
|
||||
const logger = log4js.getLogger("Receipt Printer Interface");
|
||||
@ -18,7 +28,8 @@ export default class ReceiptInterface {
|
||||
this.protocol = documentPrinterClient.connection.protocol;
|
||||
this.isConnected = false;
|
||||
this.receiptPrinter = null;
|
||||
this.retrieveStatusInterval = null;
|
||||
this.statusMonitoringInterval = null;
|
||||
this.lastReportedStatus = null;
|
||||
this.images = new Map();
|
||||
}
|
||||
|
||||
@ -50,7 +61,7 @@ export default class ReceiptInterface {
|
||||
default:
|
||||
type = PrinterTypes.EPSON;
|
||||
logger.warn(
|
||||
`Unknown interface ${this.interface}, defaulting to EPSON`
|
||||
`Unknown interface ${this.interface}, defaulting to EPSON`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,7 +69,7 @@ export default class ReceiptInterface {
|
||||
const interfaceStr = this.buildPrinterUrl();
|
||||
|
||||
logger.info(
|
||||
`Connecting to receipt printer ${this.name} (${interfaceStr})`
|
||||
`Connecting to receipt printer ${this.name} (${interfaceStr})`,
|
||||
);
|
||||
|
||||
// Initialize thermal printer
|
||||
@ -91,6 +102,7 @@ export default class ReceiptInterface {
|
||||
|
||||
async disconnect() {
|
||||
logger.info(`Disconnecting from receipt printer ${this.name}`);
|
||||
await this.stopStatusMonitoring();
|
||||
this.isConnected = false;
|
||||
this.receiptPrinter = null;
|
||||
return { success: true };
|
||||
@ -105,7 +117,7 @@ export default class ReceiptInterface {
|
||||
const isConnected = await this.receiptPrinter.isPrinterConnected();
|
||||
if (!isConnected) {
|
||||
logger.error(
|
||||
`Printer not connected during initialization for receipt printer ${this.name}`
|
||||
`Printer not connected during initialization for receipt printer ${this.name}`,
|
||||
);
|
||||
return { error: "Printer not connected during initialization" };
|
||||
}
|
||||
@ -113,7 +125,7 @@ export default class ReceiptInterface {
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to initialize receipt printer ${this.name}:`,
|
||||
error
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -123,7 +135,7 @@ export default class ReceiptInterface {
|
||||
async print(jobId, quantity = 1) {
|
||||
const copies = Math.max(1, Math.floor(Number(quantity) || 1));
|
||||
logger.info(
|
||||
`Printing job ${jobId} (${copies} ${copies === 1 ? "copy" : "copies"}) to receipt printer ${this.name}`
|
||||
`Printing job ${jobId} (${copies} ${copies === 1 ? "copy" : "copies"}) to receipt printer ${this.name}`,
|
||||
);
|
||||
|
||||
if (!this.isConnected || !this.receiptPrinter) {
|
||||
@ -155,14 +167,14 @@ export default class ReceiptInterface {
|
||||
await this.receiptPrinter.execute({ waitForResponse: true });
|
||||
|
||||
logger.info(
|
||||
`Successfully printed job ${jobId} (${copies} ${copies === 1 ? "copy" : "copies"}) to receipt printer ${this.name}`
|
||||
`Successfully printed job ${jobId} (${copies} ${copies === 1 ? "copy" : "copies"}) to receipt printer ${this.name}`,
|
||||
);
|
||||
|
||||
return { success: true, copies };
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to print job ${jobId} to receipt printer ${this.name}:`,
|
||||
error
|
||||
error,
|
||||
);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
@ -170,7 +182,7 @@ export default class ReceiptInterface {
|
||||
|
||||
async deploy(documentJob, documentTemplate, object, onProgress) {
|
||||
logger.info(
|
||||
`Deploying job ${documentJob._id} to receipt printer ${this.name}`
|
||||
`Deploying job ${documentJob._id} to receipt printer ${this.name}`,
|
||||
);
|
||||
|
||||
const imageObj =
|
||||
@ -187,7 +199,7 @@ export default class ReceiptInterface {
|
||||
|
||||
if (!imageObj || !imageObj.images) {
|
||||
throw new Error(
|
||||
imageObj?.error || "Failed to render document template to image"
|
||||
imageObj?.error || "Failed to render document template to image",
|
||||
);
|
||||
}
|
||||
|
||||
@ -195,22 +207,180 @@ export default class ReceiptInterface {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async retrieveStatus() {
|
||||
logger.debug(`Getting status of receipt printer ${this.name}`);
|
||||
if (this.isOnline == false) {
|
||||
logger.error("Printer is not connected or not reachable");
|
||||
return { error: "Printer is not connected or not reachable." };
|
||||
}
|
||||
try {
|
||||
const status = await this.receiptPrinter.raw(
|
||||
Buffer.from([0x10, 0x04, 0x04])
|
||||
);
|
||||
logger.info(`Printer status: ${status}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to execute printer status:`, error);
|
||||
isValidStatusByte(byte) {
|
||||
return (byte & VALID_STATUS_MASK) === VALID_STATUS_MASK;
|
||||
}
|
||||
|
||||
logger.debug(`Receipt printer ${this.name} is connected.`);
|
||||
return true;
|
||||
parsePrinterStatus(byte) {
|
||||
return {
|
||||
raw: byte,
|
||||
drawerKickPin3High: Boolean(byte & 0x04),
|
||||
offline: Boolean(byte & 0x08),
|
||||
waitingForOnlineRecovery: Boolean(byte & 0x20),
|
||||
paperFeedButtonPressed: Boolean(byte & 0x40),
|
||||
};
|
||||
}
|
||||
|
||||
parseOfflineStatus(byte) {
|
||||
return {
|
||||
raw: byte,
|
||||
coverOpen: Boolean(byte & 0x04),
|
||||
paperFeedButtonActive: Boolean(byte & 0x08),
|
||||
paperEndStop: Boolean(byte & 0x20),
|
||||
errorOccurred: Boolean(byte & 0x40),
|
||||
};
|
||||
}
|
||||
|
||||
parseErrorStatus(byte) {
|
||||
return {
|
||||
raw: byte,
|
||||
recoverableError: Boolean(byte & 0x04),
|
||||
autocutterError: Boolean(byte & 0x08),
|
||||
unrecoverableError: Boolean(byte & 0x20),
|
||||
autoRecoverableError: Boolean(byte & 0x40),
|
||||
};
|
||||
}
|
||||
|
||||
parsePaperStatus(byte) {
|
||||
return {
|
||||
raw: byte,
|
||||
paperNearEnd: (byte & 0x0c) === 0x0c,
|
||||
paperEnd: (byte & 0x60) === 0x60,
|
||||
};
|
||||
}
|
||||
|
||||
sendDleEotStatus(requests) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new net.Socket();
|
||||
const command = Buffer.from(requests.flatMap((n) => [0x10, 0x04, n]));
|
||||
const expectedBytes = requests.length;
|
||||
let received = Buffer.alloc(0);
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
socket.removeAllListeners();
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
const finish = (callback) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
callback();
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
finish(() =>
|
||||
reject(new Error("Timed out waiting for printer status response")),
|
||||
);
|
||||
}, STATUS_TIMEOUT_MS);
|
||||
|
||||
socket.on("data", (data) => {
|
||||
received = Buffer.concat([received, data]);
|
||||
if (received.length < expectedBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
finish(() => resolve(received.subarray(0, expectedBytes)));
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
finish(() => reject(error));
|
||||
});
|
||||
|
||||
socket.connect(this.port, this.host, () => {
|
||||
socket.write(command);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveStatus() {
|
||||
logger.trace(`Getting status of receipt printer ${this.name}`);
|
||||
|
||||
if (!this.isConnected) {
|
||||
logger.error("Printer is not connected or not reachable");
|
||||
return buildStatusError("Printer is not connected or not reachable.");
|
||||
}
|
||||
|
||||
if (this.protocol && this.protocol !== "tcp") {
|
||||
return {
|
||||
paperState: {
|
||||
type: "unknown",
|
||||
message: "Real-time printer status is only supported over TCP.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.sendDleEotStatus([1, 2, 3, 4]);
|
||||
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
if (!this.isValidStatusByte(response[i])) {
|
||||
throw new Error(
|
||||
`Invalid status response byte at index ${i}: 0x${response[i].toString(16)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const printer = this.parsePrinterStatus(response[0]);
|
||||
const offline = this.parseOfflineStatus(response[1]);
|
||||
const error = this.parseErrorStatus(response[2]);
|
||||
const paper = this.parsePaperStatus(response[3]);
|
||||
|
||||
const status = mapReceiptPrinterStatus({
|
||||
printer,
|
||||
offline,
|
||||
error,
|
||||
paper,
|
||||
});
|
||||
|
||||
logger.trace(`Receipt printer ${this.name} status:`, status);
|
||||
return status;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to retrieve printer status:`, error);
|
||||
return buildStatusError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAndNotifyStatus() {
|
||||
const status = await this.retrieveStatus();
|
||||
if (status?.error) {
|
||||
logger.error(
|
||||
`Failed to refresh receipt printer status for ${this.name}: ${status.error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!printerStatusesEqual(this.lastReportedStatus, status)) {
|
||||
this.lastReportedStatus = status;
|
||||
await this.documentPrinterClient.handlePrinterStatusUpdate(status);
|
||||
}
|
||||
}
|
||||
|
||||
async startStatusMonitoring() {
|
||||
if (this.statusMonitoringInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.refreshAndNotifyStatus();
|
||||
this.statusMonitoringInterval = setInterval(() => {
|
||||
this.refreshAndNotifyStatus().catch((error) => {
|
||||
logger.error(
|
||||
`Error polling receipt printer status for ${this.name}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, STATUS_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async stopStatusMonitoring() {
|
||||
if (this.statusMonitoringInterval) {
|
||||
clearInterval(this.statusMonitoringInterval);
|
||||
this.statusMonitoringInterval = null;
|
||||
}
|
||||
this.lastReportedStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
170
src/documentprinter/printerstatus.js
Normal file
170
src/documentprinter/printerstatus.js
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Standard shape returned by printer interface retrieveStatus():
|
||||
* {
|
||||
* state: { type: string, message?: string },
|
||||
* paperState: { type: string, message?: string },
|
||||
* }
|
||||
*
|
||||
* On failure: { error: string }
|
||||
*/
|
||||
|
||||
export function buildPrinterStatus({ stateType, stateMessage, paperStateType, paperStateMessage }) {
|
||||
return {
|
||||
state: {
|
||||
type: stateType,
|
||||
...(stateMessage ? { message: stateMessage } : {}),
|
||||
},
|
||||
paperState: {
|
||||
type: paperStateType,
|
||||
...(paperStateMessage ? { message: paperStateMessage } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildStatusError(message) {
|
||||
return { error: message };
|
||||
}
|
||||
|
||||
export function printerStatusesEqual(a, b) {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
if (a.error || b.error) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
a.state?.type === b.state?.type &&
|
||||
a.state?.message === b.state?.message &&
|
||||
a.paperState?.type === b.paperState?.type &&
|
||||
a.paperState?.message === b.paperState?.message
|
||||
);
|
||||
}
|
||||
|
||||
export function mapCupsJobState(jobState, stateReasons = []) {
|
||||
const reasons = (Array.isArray(stateReasons) ? stateReasons : [stateReasons])
|
||||
.filter(Boolean)
|
||||
.map((reason) => reason.toLowerCase());
|
||||
|
||||
if (jobState === 5) {
|
||||
return { type: "printing" };
|
||||
}
|
||||
|
||||
if (jobState === 9) {
|
||||
return { type: "complete" };
|
||||
}
|
||||
|
||||
if (jobState === 3 || jobState === 4) {
|
||||
return { type: "queued" };
|
||||
}
|
||||
|
||||
if (jobState === 6 || jobState === 7 || jobState === 8) {
|
||||
const message =
|
||||
reasons.find((reason) => reason !== "none") ||
|
||||
(jobState === 7 ? "Job canceled" : "Print job failed");
|
||||
return { type: "failed", message };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mapCupsPrinterStatus(printerState, stateMessage, stateReasons = []) {
|
||||
const reasons = (Array.isArray(stateReasons) ? stateReasons : [stateReasons])
|
||||
.filter(Boolean)
|
||||
.map((reason) => reason.toLowerCase());
|
||||
|
||||
let stateType = "standby";
|
||||
let resolvedStateMessage = stateMessage || null;
|
||||
|
||||
if (printerState === 4) {
|
||||
stateType = "printing";
|
||||
} else if (printerState === 5) {
|
||||
stateType = "error";
|
||||
}
|
||||
|
||||
let paperStateType = "ok";
|
||||
let paperStateMessage = null;
|
||||
|
||||
if (reasons.some((reason) => reason.includes("media-empty"))) {
|
||||
paperStateType = "out";
|
||||
paperStateMessage = "Paper out";
|
||||
} else if (
|
||||
reasons.some(
|
||||
(reason) => reason.includes("media-low") || reason.includes("media-needed"),
|
||||
)
|
||||
) {
|
||||
paperStateType = "nearEnd";
|
||||
paperStateMessage = "Paper low";
|
||||
}
|
||||
|
||||
if (reasons.some((reason) => reason.includes("door-open"))) {
|
||||
stateType = "error";
|
||||
resolvedStateMessage = resolvedStateMessage || "Door open";
|
||||
} else if (
|
||||
reasons.some(
|
||||
(reason) =>
|
||||
reason.includes("offline") ||
|
||||
reason.includes("shutdown") ||
|
||||
reason.includes("stopped-partly"),
|
||||
)
|
||||
) {
|
||||
stateType = "offline";
|
||||
resolvedStateMessage = resolvedStateMessage || "Printer offline";
|
||||
} else if (
|
||||
reasons.some((reason) => reason.includes("media-jam") || reason.includes("jam"))
|
||||
) {
|
||||
stateType = "error";
|
||||
resolvedStateMessage = resolvedStateMessage || "Paper jam";
|
||||
}
|
||||
|
||||
return buildPrinterStatus({
|
||||
stateType,
|
||||
stateMessage: resolvedStateMessage,
|
||||
paperStateType,
|
||||
paperStateMessage,
|
||||
});
|
||||
}
|
||||
|
||||
export function mapReceiptPrinterStatus({ printer, offline, error, paper }) {
|
||||
let stateType = "standby";
|
||||
let stateMessage = null;
|
||||
let paperStateType = "ok";
|
||||
let paperStateMessage = null;
|
||||
|
||||
if (paper?.paperEnd) {
|
||||
paperStateType = "out";
|
||||
paperStateMessage = "Paper out";
|
||||
} else if (paper?.paperNearEnd) {
|
||||
paperStateType = "nearEnd";
|
||||
paperStateMessage = "Paper near end";
|
||||
}
|
||||
|
||||
if (offline?.coverOpen) {
|
||||
stateType = "error";
|
||||
stateMessage = "Cover open";
|
||||
} else if (error?.unrecoverableError) {
|
||||
stateType = "error";
|
||||
stateMessage = "Unrecoverable error";
|
||||
} else if (error?.recoverableError || error?.autoRecoverableError) {
|
||||
stateType = "error";
|
||||
stateMessage = "Recoverable error";
|
||||
} else if (error?.autocutterError) {
|
||||
stateType = "error";
|
||||
stateMessage = "Autocutter error";
|
||||
} else if (printer?.offline) {
|
||||
stateType = "offline";
|
||||
stateMessage = "Printer offline";
|
||||
} else if (offline?.errorOccurred) {
|
||||
stateType = "error";
|
||||
stateMessage = "Printer error";
|
||||
}
|
||||
|
||||
return buildPrinterStatus({
|
||||
stateType,
|
||||
stateMessage,
|
||||
paperStateType,
|
||||
paperStateMessage,
|
||||
});
|
||||
}
|
||||
577
src/documentprinter/rawippnotifications.js
Normal file
577
src/documentprinter/rawippnotifications.js
Normal file
@ -0,0 +1,577 @@
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
||||
const IPP_VERSION = 0x0200;
|
||||
|
||||
export const IPP_OPERATIONS = {
|
||||
CREATE_PRINTER_SUBSCRIPTIONS: 0x0016,
|
||||
CANCEL_SUBSCRIPTION: 0x001b,
|
||||
GET_NOTIFICATIONS: 0x001c,
|
||||
};
|
||||
|
||||
const IPP_TAG = {
|
||||
OPERATION: 0x01,
|
||||
SUBSCRIPTION: 0x06,
|
||||
EVENT_NOTIFICATION: 0x07,
|
||||
END: 0x03,
|
||||
};
|
||||
|
||||
const IPP_VALUE = {
|
||||
INTEGER: 0x21,
|
||||
BOOLEAN: 0x22,
|
||||
CHARSET: 0x47,
|
||||
NATURAL_LANGUAGE: 0x48,
|
||||
URI: 0x45,
|
||||
KEYWORD: 0x44,
|
||||
};
|
||||
|
||||
const TAG_NAMES = {
|
||||
[IPP_TAG.OPERATION]: "operation-attributes-tag",
|
||||
0x02: "job-attributes-tag",
|
||||
0x04: "printer-attributes-tag",
|
||||
0x05: "unsupported-attributes-tag",
|
||||
[IPP_TAG.SUBSCRIPTION]: "subscription-attributes-tag",
|
||||
0x07: "event-notification-attributes-tag",
|
||||
};
|
||||
|
||||
const STATUS_CODES = {
|
||||
0x0000: "successful-ok",
|
||||
0x0001: "successful-ok-ignored-or-substituted-attributes",
|
||||
0x0002: "successful-ok-conflicting-attributes",
|
||||
0x0003: "successful-ok-ignored-subscriptions",
|
||||
};
|
||||
|
||||
class IppBufferWriter {
|
||||
constructor() {
|
||||
this.chunks = [];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
append(buffer) {
|
||||
this.chunks.push(buffer);
|
||||
this.length += buffer.length;
|
||||
}
|
||||
|
||||
writeUInt8(value) {
|
||||
const buffer = Buffer.alloc(1);
|
||||
buffer.writeUInt8(value);
|
||||
this.append(buffer);
|
||||
}
|
||||
|
||||
writeUInt16BE(value) {
|
||||
const buffer = Buffer.alloc(2);
|
||||
buffer.writeUInt16BE(value);
|
||||
this.append(buffer);
|
||||
}
|
||||
|
||||
writeUInt32BE(value) {
|
||||
const buffer = Buffer.alloc(4);
|
||||
buffer.writeUInt32BE(value);
|
||||
this.append(buffer);
|
||||
}
|
||||
|
||||
writeString(value, encoding = "utf8") {
|
||||
const buffer = Buffer.from(value, encoding);
|
||||
this.writeUInt16BE(buffer.length);
|
||||
this.append(buffer);
|
||||
}
|
||||
|
||||
toBuffer() {
|
||||
return Buffer.concat(this.chunks, this.length);
|
||||
}
|
||||
}
|
||||
|
||||
function writeAttribute(
|
||||
writer,
|
||||
valueTag,
|
||||
name,
|
||||
value,
|
||||
isAdditionalValue = false,
|
||||
) {
|
||||
writer.writeUInt8(valueTag);
|
||||
if (isAdditionalValue) {
|
||||
writer.writeUInt16BE(0);
|
||||
} else {
|
||||
writer.writeString(name);
|
||||
}
|
||||
|
||||
switch (valueTag) {
|
||||
case IPP_VALUE.INTEGER:
|
||||
writer.writeUInt16BE(4);
|
||||
writer.writeUInt32BE(value);
|
||||
break;
|
||||
case IPP_VALUE.BOOLEAN:
|
||||
writer.writeUInt16BE(1);
|
||||
writer.writeUInt8(value ? 1 : 0);
|
||||
break;
|
||||
case IPP_VALUE.KEYWORD:
|
||||
case IPP_VALUE.URI:
|
||||
case IPP_VALUE.CHARSET:
|
||||
case IPP_VALUE.NATURAL_LANGUAGE:
|
||||
writer.writeString(value, "ascii");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported IPP value tag: 0x${valueTag.toString(16)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeAttributeSet(writer, valueTag, name, values) {
|
||||
const valueList = Array.isArray(values) ? values : [values];
|
||||
valueList.forEach((value, index) => {
|
||||
writeAttribute(writer, valueTag, name, value, index > 0);
|
||||
});
|
||||
}
|
||||
|
||||
function writeMessageHeader(writer, operationId, requestId = 1) {
|
||||
writer.writeUInt16BE(IPP_VERSION);
|
||||
writer.writeUInt16BE(operationId);
|
||||
writer.writeUInt32BE(requestId);
|
||||
}
|
||||
|
||||
function writeStandardOperationAttributes(writer, printerUri, extras = {}) {
|
||||
writer.writeUInt8(IPP_TAG.OPERATION);
|
||||
writeAttribute(writer, IPP_VALUE.CHARSET, "attributes-charset", "utf-8");
|
||||
writeAttribute(
|
||||
writer,
|
||||
IPP_VALUE.NATURAL_LANGUAGE,
|
||||
"attributes-natural-language",
|
||||
"en-us",
|
||||
);
|
||||
writeAttribute(writer, IPP_VALUE.URI, "printer-uri", printerUri);
|
||||
|
||||
for (const [name, spec] of Object.entries(extras)) {
|
||||
writeAttributeSet(writer, spec.tag, name, spec.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCreatePrinterSubscriptionRequest(
|
||||
printerUri,
|
||||
{ leaseDuration = 86400 } = {},
|
||||
) {
|
||||
const writer = new IppBufferWriter();
|
||||
writeMessageHeader(writer, IPP_OPERATIONS.CREATE_PRINTER_SUBSCRIPTIONS);
|
||||
writeStandardOperationAttributes(writer, printerUri, {
|
||||
"requested-attributes": {
|
||||
tag: IPP_VALUE.KEYWORD,
|
||||
value: ["notify-subscription-id"],
|
||||
},
|
||||
});
|
||||
|
||||
writer.writeUInt8(IPP_TAG.SUBSCRIPTION);
|
||||
writeAttribute(writer, IPP_VALUE.KEYWORD, "notify-pull-method", "ippget");
|
||||
writeAttributeSet(writer, IPP_VALUE.KEYWORD, "notify-events", [
|
||||
"printer-state-changed",
|
||||
"job-state-changed",
|
||||
]);
|
||||
writeAttribute(
|
||||
writer,
|
||||
IPP_VALUE.INTEGER,
|
||||
"notify-lease-duration",
|
||||
leaseDuration,
|
||||
);
|
||||
writer.writeUInt8(IPP_TAG.END);
|
||||
return writer.toBuffer();
|
||||
}
|
||||
|
||||
export function buildGetNotificationsRequest(
|
||||
printerUri,
|
||||
subscriptionId,
|
||||
sequenceNumber,
|
||||
wait,
|
||||
) {
|
||||
const writer = new IppBufferWriter();
|
||||
writeMessageHeader(writer, IPP_OPERATIONS.GET_NOTIFICATIONS);
|
||||
|
||||
const extras = {
|
||||
"notify-subscription-ids": {
|
||||
tag: IPP_VALUE.INTEGER,
|
||||
value: [subscriptionId],
|
||||
},
|
||||
"notify-sequence-numbers": {
|
||||
tag: IPP_VALUE.INTEGER,
|
||||
value: [sequenceNumber],
|
||||
},
|
||||
};
|
||||
|
||||
if (wait != null) {
|
||||
extras["notify-wait"] = {
|
||||
tag: IPP_VALUE.BOOLEAN,
|
||||
value: wait,
|
||||
};
|
||||
}
|
||||
|
||||
writeStandardOperationAttributes(writer, printerUri, extras);
|
||||
writer.writeUInt8(IPP_TAG.END);
|
||||
return writer.toBuffer();
|
||||
}
|
||||
|
||||
export function buildCancelSubscriptionRequest(printerUri, subscriptionId) {
|
||||
const writer = new IppBufferWriter();
|
||||
writeMessageHeader(writer, IPP_OPERATIONS.CANCEL_SUBSCRIPTION);
|
||||
writeStandardOperationAttributes(writer, printerUri, {
|
||||
"notify-subscription-id": {
|
||||
tag: IPP_VALUE.INTEGER,
|
||||
value: subscriptionId,
|
||||
},
|
||||
});
|
||||
writer.writeUInt8(IPP_TAG.END);
|
||||
return writer.toBuffer();
|
||||
}
|
||||
|
||||
export function parseIppResponse(buffer) {
|
||||
let position = 0;
|
||||
|
||||
const read1 = () => buffer[position++];
|
||||
const read2 = () => {
|
||||
const value = buffer.readUInt16BE(position);
|
||||
position += 2;
|
||||
return value;
|
||||
};
|
||||
const read4 = () => {
|
||||
const value = buffer.readUInt32BE(position);
|
||||
position += 4;
|
||||
return value;
|
||||
};
|
||||
const read = (length, encoding = "utf8") => {
|
||||
if (length === 0) {
|
||||
return "";
|
||||
}
|
||||
const value = buffer.toString(encoding, position, position + length);
|
||||
position += length;
|
||||
return value;
|
||||
};
|
||||
|
||||
const result = {
|
||||
version: `${read1()}.${read1()}`,
|
||||
statusCode: STATUS_CODES[read2()] || "unknown",
|
||||
id: read4(),
|
||||
};
|
||||
|
||||
const hasAdditionalValue = () => {
|
||||
const current = buffer[position];
|
||||
return (
|
||||
current !== 0x4a &&
|
||||
current !== 0x37 &&
|
||||
current !== 0x03 &&
|
||||
buffer[position + 1] === 0x00 &&
|
||||
buffer[position + 2] === 0x00
|
||||
);
|
||||
};
|
||||
|
||||
const readValue = (tag, length) => {
|
||||
switch (tag) {
|
||||
case IPP_VALUE.INTEGER:
|
||||
case 0x23:
|
||||
return read4();
|
||||
case IPP_VALUE.BOOLEAN:
|
||||
return read1() === 1;
|
||||
case IPP_VALUE.KEYWORD:
|
||||
case IPP_VALUE.URI:
|
||||
case IPP_VALUE.CHARSET:
|
||||
case IPP_VALUE.NATURAL_LANGUAGE:
|
||||
case 0x42:
|
||||
return read(length, "ascii");
|
||||
default:
|
||||
position += length;
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const readValues = (tag) => {
|
||||
const length = read2();
|
||||
let value = readValue(tag, length);
|
||||
if (hasAdditionalValue()) {
|
||||
value = [value];
|
||||
do {
|
||||
const nextTag = read1();
|
||||
read2();
|
||||
const nextLength = read2();
|
||||
value.push(readValue(nextTag, nextLength));
|
||||
} while (hasAdditionalValue());
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const readAttr = (group) => {
|
||||
const tag = read1();
|
||||
if (tag === 0x7f) {
|
||||
read4();
|
||||
}
|
||||
const nameLength = read2();
|
||||
const name = read(nameLength);
|
||||
const value = readValues(tag);
|
||||
|
||||
if (group[name] !== undefined) {
|
||||
if (!Array.isArray(group[name])) {
|
||||
group[name] = [group[name]];
|
||||
}
|
||||
group[name].push(value);
|
||||
} else {
|
||||
group[name] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const readGroup = (tagByte) => {
|
||||
const tagName = TAG_NAMES[tagByte] || `tag-0x${tagByte.toString(16)}`;
|
||||
const group = {};
|
||||
|
||||
while (position < buffer.length && buffer[position] >= 0x0f) {
|
||||
readAttr(group);
|
||||
}
|
||||
|
||||
if (result[tagName]) {
|
||||
if (!Array.isArray(result[tagName])) {
|
||||
result[tagName] = [result[tagName]];
|
||||
}
|
||||
result[tagName].push(group);
|
||||
} else {
|
||||
result[tagName] = group;
|
||||
}
|
||||
};
|
||||
|
||||
while (position < buffer.length) {
|
||||
const tag = read1();
|
||||
if (tag === IPP_TAG.END) {
|
||||
break;
|
||||
}
|
||||
readGroup(tag);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildAuthHeader(auth) {
|
||||
if (!auth?.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const credentials = Buffer.from(
|
||||
`${auth.username}:${auth.password || ""}`,
|
||||
).toString("base64");
|
||||
return `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
function sendRawIppRequestOnce(
|
||||
httpUrl,
|
||||
body,
|
||||
{ rejectUnauthorized = true, auth = null } = {},
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(httpUrl);
|
||||
const transport = url.protocol === "https:" ? https : http;
|
||||
const headers = {
|
||||
"Content-Type": "application/ipp",
|
||||
"Content-Length": body.length,
|
||||
};
|
||||
const authHeader = buildAuthHeader(auth);
|
||||
if (authHeader) {
|
||||
headers.Authorization = authHeader;
|
||||
}
|
||||
|
||||
const request = transport.request(
|
||||
{
|
||||
protocol: url.protocol,
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === "https:" ? 443 : 631),
|
||||
path: `${url.pathname}${url.search}`,
|
||||
method: "POST",
|
||||
headers,
|
||||
...(url.protocol === "https:" ? { rejectUnauthorized } : {}),
|
||||
},
|
||||
(response) => {
|
||||
const chunks = [];
|
||||
response.on("data", (chunk) => chunks.push(chunk));
|
||||
response.on("end", () => {
|
||||
const responseBody = Buffer.concat(chunks);
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
let errorMessage = `Received unexpected response status ${response.statusCode} from the printer`;
|
||||
|
||||
if (responseBody.length > 0) {
|
||||
try {
|
||||
const parsedResponse = parseIppResponse(responseBody);
|
||||
errorMessage =
|
||||
parsedResponse["operation-attributes-tag"]?.[
|
||||
"status-message"
|
||||
] || errorMessage;
|
||||
} catch {
|
||||
const responseText = responseBody.toString("utf8");
|
||||
if (responseText.trim()) {
|
||||
errorMessage = responseText.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const error = new Error(errorMessage);
|
||||
error.statusCode = response.statusCode;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(parseIppResponse(responseBody));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
request.on("error", reject);
|
||||
request.write(body);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendRawIppRequest(
|
||||
httpUrl,
|
||||
body,
|
||||
{ rejectUnauthorized = true, allowTlsUpgrade = true, auth = null } = {},
|
||||
) {
|
||||
try {
|
||||
return await sendRawIppRequestOnce(httpUrl, body, {
|
||||
rejectUnauthorized,
|
||||
auth,
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
allowTlsUpgrade &&
|
||||
error?.statusCode === 426 &&
|
||||
httpUrl.startsWith("http://")
|
||||
) {
|
||||
const httpsUrl = `https://${httpUrl.slice("http://".length)}`;
|
||||
return sendRawIppRequestOnce(httpsUrl, body, {
|
||||
rejectUnauthorized: false,
|
||||
auth,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isSuccessfulIppStatus(statusCode) {
|
||||
return typeof statusCode === "string" && statusCode.startsWith("successful");
|
||||
}
|
||||
|
||||
export function extractSubscriptionId(response) {
|
||||
const groups = [
|
||||
response["subscription-attributes-tag"],
|
||||
response["subscription-object-attributes-tag"],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.flatMap((group) => (Array.isArray(group) ? group : [group]));
|
||||
|
||||
for (const group of groups) {
|
||||
const subscriptionId = group?.["notify-subscription-id"];
|
||||
if (subscriptionId != null) {
|
||||
return Array.isArray(subscriptionId) ? subscriptionId[0] : subscriptionId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildHttpRequestUrl(
|
||||
host,
|
||||
port = 631,
|
||||
protocol = "ipp",
|
||||
useTls = false,
|
||||
) {
|
||||
let hostName = host;
|
||||
let path = "/";
|
||||
|
||||
if (host.includes("/")) {
|
||||
const parts = host.split("/");
|
||||
hostName = parts[0];
|
||||
path = `/${parts.slice(1).join("/")}`;
|
||||
}
|
||||
|
||||
if (hostName.includes(":")) {
|
||||
const [name, embeddedPort] = hostName.split(":");
|
||||
hostName = name;
|
||||
if (!port) {
|
||||
port = Number(embeddedPort);
|
||||
}
|
||||
}
|
||||
|
||||
const scheme =
|
||||
useTls || protocol === "https" || protocol === "ipps" ? "https" : "http";
|
||||
return `${scheme}://${hostName}:${port}${path}`;
|
||||
}
|
||||
|
||||
export function buildIppPrinterUri(host, port = 631, useTls = false) {
|
||||
const httpUrl = buildHttpRequestUrl(host, port, "ipp", useTls);
|
||||
return httpUrl.replace(/^https?:\/\//, "ipp://");
|
||||
}
|
||||
|
||||
export function buildCupsServerUrl(host, port = 631, useTls = false) {
|
||||
const printerUrl = buildHttpRequestUrl(host, port, "ipp", useTls);
|
||||
const url = new URL(printerUrl);
|
||||
return `${url.protocol}//${url.host}/`;
|
||||
}
|
||||
|
||||
export function buildCupsPpdUrl(host, port = 631, useTls = false) {
|
||||
const printerUrl = buildHttpRequestUrl(host, port, "ipp", useTls);
|
||||
if (printerUrl.endsWith(".ppd")) {
|
||||
return printerUrl;
|
||||
}
|
||||
return `${printerUrl.replace(/\/$/, "")}.ppd`;
|
||||
}
|
||||
|
||||
export async function fetchCupsResource(
|
||||
httpUrl,
|
||||
{ rejectUnauthorized = true, allowTlsUpgrade = true, auth = null } = {},
|
||||
) {
|
||||
const fetchOnce = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const transport = parsed.protocol === "https:" ? https : http;
|
||||
const headers = {};
|
||||
const authHeader = buildAuthHeader(auth);
|
||||
if (authHeader) {
|
||||
headers.Authorization = authHeader;
|
||||
}
|
||||
|
||||
const request = transport.request(
|
||||
{
|
||||
protocol: parsed.protocol,
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port || (parsed.protocol === "https:" ? 443 : 631),
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
method: "GET",
|
||||
headers,
|
||||
...(parsed.protocol === "https:" ? { rejectUnauthorized } : {}),
|
||||
},
|
||||
(response) => {
|
||||
const chunks = [];
|
||||
response.on("data", (chunk) => chunks.push(chunk));
|
||||
response.on("end", () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
if (response.statusCode !== 200) {
|
||||
const error = new Error(
|
||||
body.toString("utf8").trim() ||
|
||||
`Received unexpected response status ${response.statusCode} from CUPS`,
|
||||
);
|
||||
error.statusCode = response.statusCode;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(body.toString("utf8"));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
request.on("error", reject);
|
||||
request.end();
|
||||
});
|
||||
|
||||
try {
|
||||
return await fetchOnce(httpUrl);
|
||||
} catch (error) {
|
||||
if (
|
||||
allowTlsUpgrade &&
|
||||
error?.statusCode === 426 &&
|
||||
httpUrl.startsWith("http://")
|
||||
) {
|
||||
const httpsUrl = `https://${httpUrl.slice("http://".length)}`;
|
||||
return fetchOnce(httpsUrl);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -171,6 +171,9 @@ export class SocketClient {
|
||||
callback,
|
||||
);
|
||||
}
|
||||
if (data.objectType == "documentSize") {
|
||||
this.documentPrinterManager.handleDocumentSizeUpdate(data._id, data);
|
||||
}
|
||||
if (data.objectType == "printer") {
|
||||
this.printerManager.handlePrinterUpdate(data._id, data, callback);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user