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");
|
const logger = log4js.getLogger("Document Printer Client");
|
||||||
logger.level = config.logLevel;
|
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 {
|
export class DocumentPrinterClient {
|
||||||
constructor(
|
constructor(
|
||||||
documentPrinter = {
|
documentPrinter = {
|
||||||
@ -29,16 +41,18 @@ export class DocumentPrinterClient {
|
|||||||
this.socketClient = documentPrinterManager.socketClient;
|
this.socketClient = documentPrinterManager.socketClient;
|
||||||
this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os
|
this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os
|
||||||
this.state = { type: this.active == true ? "offline" : "inactive" };
|
this.state = { type: this.active == true ? "offline" : "inactive" };
|
||||||
|
this.paperState = documentPrinter.paperState || { type: "unknown" };
|
||||||
this.isOnline = documentPrinter.online || false;
|
this.isOnline = documentPrinter.online || false;
|
||||||
this.shouldReconnect = true;
|
this.shouldReconnect = true;
|
||||||
this.isConnecting = false;
|
this.isConnecting = false;
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
this.isProcessingQueue = false;
|
this.isProcessingQueue = false;
|
||||||
this.eventUpdateInterval = null;
|
this.subscribedDocumentSizeId = null;
|
||||||
this.initializeInterface();
|
this.initializeInterface();
|
||||||
this.registerEventHandlers();
|
this.registerEventHandlers();
|
||||||
this.subscribeToActions();
|
this.subscribeToActions();
|
||||||
this.subscribeToObjectUpdates();
|
this.subscribeToObjectUpdates();
|
||||||
|
this.syncCurrentDocumentSizeSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeInterface() {
|
initializeInterface() {
|
||||||
@ -68,6 +82,12 @@ export class DocumentPrinterClient {
|
|||||||
logger.debug(`Updating document printer ${this.id} with data...`);
|
logger.debug(`Updating document printer ${this.id} with data...`);
|
||||||
sendIPC("setDocumentPrinter", { _id: this.id, ...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
|
// Check for connection changes before updating
|
||||||
|
|
||||||
if (data?.connection) {
|
if (data?.connection) {
|
||||||
@ -83,9 +103,20 @@ export class DocumentPrinterClient {
|
|||||||
newConnection.interface != null &&
|
newConnection.interface != null &&
|
||||||
newConnection.interface !== oldConnection.interface;
|
newConnection.interface !== oldConnection.interface;
|
||||||
const portChanged = newConnection?.port !== oldConnection?.port;
|
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 =
|
const connectionChanged =
|
||||||
hostChanged || interfaceChanged || portChanged || protocolChanged;
|
hostChanged ||
|
||||||
|
interfaceChanged ||
|
||||||
|
portChanged ||
|
||||||
|
protocolChanged ||
|
||||||
|
usernameChanged ||
|
||||||
|
passwordChanged;
|
||||||
|
|
||||||
logger.debug(`Connection changed: ${connectionChanged}`);
|
logger.debug(`Connection changed: ${connectionChanged}`);
|
||||||
logger.debug(`Host changed: ${hostChanged}`);
|
logger.debug(`Host changed: ${hostChanged}`);
|
||||||
@ -107,11 +138,34 @@ export class DocumentPrinterClient {
|
|||||||
await this.printerInterface.disconnect();
|
await this.printerInterface.disconnect();
|
||||||
this.initializeInterface();
|
this.initializeInterface();
|
||||||
await this.reconnect();
|
await this.reconnect();
|
||||||
|
} else if (data?.connection) {
|
||||||
|
this.connection = { ...oldConnection, ...newConnection };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.documentPrinter = { ...this.documentPrinter, ...data };
|
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 (Object.hasOwn(data || {}, "active") && data.active != this.active) {
|
||||||
if (data.active == true) {
|
if (data.active == true) {
|
||||||
await this.setActive();
|
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() {
|
registerEventHandlers() {
|
||||||
// Register event handlers for document printer notifications
|
// Register event handlers for document printer notifications
|
||||||
// This can be extended as needed
|
// 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) {
|
statesEqual(a, b) {
|
||||||
if (!a && !b) {
|
if (!a && !b) {
|
||||||
return true;
|
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) {
|
scheduleReconnect(delay = 30000) {
|
||||||
if (!this.shouldReconnect || this.active === false) {
|
if (!this.shouldReconnect || this.active === false) {
|
||||||
return;
|
return;
|
||||||
@ -187,9 +365,7 @@ export class DocumentPrinterClient {
|
|||||||
|
|
||||||
clearTimeout(this.reconnectTimeout);
|
clearTimeout(this.reconnectTimeout);
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
// Always stop the event update interval when connecting
|
await this.stopStatusMonitoring();
|
||||||
clearInterval(this.eventUpdateInterval);
|
|
||||||
this.eventUpdateInterval = null;
|
|
||||||
this.state = { type: "connecting", message: null };
|
this.state = { type: "connecting", message: null };
|
||||||
this.isOnline = false;
|
this.isOnline = false;
|
||||||
await this.updateDocumentPrinterState();
|
await this.updateDocumentPrinterState();
|
||||||
@ -241,6 +417,8 @@ export class DocumentPrinterClient {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncCurrentDocumentSizeSubscription();
|
||||||
|
|
||||||
clearTimeout(this.reconnectTimeout);
|
clearTimeout(this.reconnectTimeout);
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
|
|
||||||
@ -295,10 +473,7 @@ export class DocumentPrinterClient {
|
|||||||
}
|
}
|
||||||
this.state = { type: "standby", message: null };
|
this.state = { type: "standby", message: null };
|
||||||
await this.updateDocumentPrinterState();
|
await this.updateDocumentPrinterState();
|
||||||
this.eventUpdateInterval = setInterval(
|
await this.startStatusMonitoring();
|
||||||
this.handleEventUpdate.bind(this),
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
await stopWaiting();
|
await stopWaiting();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -309,16 +484,30 @@ export class DocumentPrinterClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEventUpdate() {
|
async startStatusMonitoring() {
|
||||||
if (this.printerInterface && this.printerInterface.retrieveStatus) {
|
if (this.printerInterface?.startStatusMonitoring) {
|
||||||
try {
|
await this.printerInterface.startStatusMonitoring();
|
||||||
//await this.printerInterface.retrieveStatus();
|
}
|
||||||
} catch (error) {
|
}
|
||||||
|
|
||||||
|
async stopStatusMonitoring() {
|
||||||
|
if (this.printerInterface?.stopStatusMonitoring) {
|
||||||
|
await this.printerInterface.stopStatusMonitoring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePrinterStatusUpdate(status) {
|
||||||
|
if (!status || status.error) {
|
||||||
|
if (status?.error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error retrieving status for document printer ${this.documentPrinter.name}:`,
|
`Error retrieving status for document printer ${this.documentPrinter.name}: ${status.error}`,
|
||||||
error,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.applyPrinterStatus(status)) {
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,6 +515,7 @@ export class DocumentPrinterClient {
|
|||||||
const updateData = {
|
const updateData = {
|
||||||
online: this.isOnline,
|
online: this.isOnline,
|
||||||
state: this.state,
|
state: this.state,
|
||||||
|
paperState: this.paperState,
|
||||||
connectedAt: this.connectedAt ?? null,
|
connectedAt: this.connectedAt ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -337,6 +527,10 @@ export class DocumentPrinterClient {
|
|||||||
if (
|
if (
|
||||||
this.documentPrinter?.online === updateData.online &&
|
this.documentPrinter?.online === updateData.online &&
|
||||||
this.statesEqual(this.documentPrinter?.state, updateData.state) &&
|
this.statesEqual(this.documentPrinter?.state, updateData.state) &&
|
||||||
|
this.paperStatesEqual(
|
||||||
|
this.documentPrinter?.paperState,
|
||||||
|
updateData.paperState,
|
||||||
|
) &&
|
||||||
connectedAtUnchanged
|
connectedAtUnchanged
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -590,7 +784,10 @@ export class DocumentPrinterClient {
|
|||||||
);
|
);
|
||||||
await this.updateJobState(jobId, { type: "printing" });
|
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(
|
logger.info(
|
||||||
`Successfully printed job ${jobId} (${quantity} ${quantity === 1 ? "copy" : "copies"}) for ${this.documentPrinter.name}`,
|
`Successfully printed job ${jobId} (${quantity} ${quantity === 1 ? "copy" : "copies"}) for ${this.documentPrinter.name}`,
|
||||||
);
|
);
|
||||||
@ -630,14 +827,14 @@ export class DocumentPrinterClient {
|
|||||||
this.shouldReconnect = false;
|
this.shouldReconnect = false;
|
||||||
clearTimeout(this.reconnectTimeout);
|
clearTimeout(this.reconnectTimeout);
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
// Always stop the event update interval when disconnecting
|
this.unsubscribeFromCurrentDocumentSizeUpdates();
|
||||||
clearInterval(this.eventUpdateInterval);
|
await this.stopStatusMonitoring();
|
||||||
this.eventUpdateInterval = null;
|
|
||||||
if (this.printerInterface && this.printerInterface.disconnect) {
|
if (this.printerInterface && this.printerInterface.disconnect) {
|
||||||
await this.printerInterface.disconnect();
|
await this.printerInterface.disconnect();
|
||||||
}
|
}
|
||||||
this.isOnline = false;
|
this.isOnline = false;
|
||||||
this.state = { type: this.active == false ? "inactive" : "offline" };
|
this.state = { type: this.active == false ? "inactive" : "offline" };
|
||||||
|
this.paperState = { type: "unknown" };
|
||||||
this.isProcessingQueue = false;
|
this.isProcessingQueue = false;
|
||||||
this.queue = []; // Clear queue on disconnect
|
this.queue = []; // Clear queue on disconnect
|
||||||
this.jobQuantities.clear();
|
this.jobQuantities.clear();
|
||||||
@ -660,6 +857,7 @@ export class DocumentPrinterClient {
|
|||||||
this.documentPrinter.active = true;
|
this.documentPrinter.active = true;
|
||||||
this.shouldReconnect = true;
|
this.shouldReconnect = true;
|
||||||
this.isOnline = false;
|
this.isOnline = false;
|
||||||
|
this.syncCurrentDocumentSizeSubscription();
|
||||||
await this.reconnect();
|
await this.reconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export class DocumentPrinterManager {
|
|||||||
this.documentPrinters = await this.socketClient.listObjects({
|
this.documentPrinters = await this.socketClient.listObjects({
|
||||||
objectType: "documentPrinter",
|
objectType: "documentPrinter",
|
||||||
filter: { host: this.socketClient.id },
|
filter: { host: this.socketClient.id },
|
||||||
|
populate: ["currentDocumentSize"],
|
||||||
});
|
});
|
||||||
|
|
||||||
sendIPC("setDocumentPrinters", this.documentPrinters);
|
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) {
|
getDocumentPrinterClient(documentPrinterId) {
|
||||||
return this.documentPrinterClients.get(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
|
// receiptinterface.js - Thermal receipt printer interface implementation
|
||||||
|
import net from "net";
|
||||||
import log4js from "log4js";
|
import log4js from "log4js";
|
||||||
import { loadConfig } from "../../config.js";
|
import { loadConfig } from "../../config.js";
|
||||||
import { ThermalPrinter, PrinterTypes } from "node-thermal-printer";
|
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 config = loadConfig();
|
||||||
const logger = log4js.getLogger("Receipt Printer Interface");
|
const logger = log4js.getLogger("Receipt Printer Interface");
|
||||||
@ -18,7 +28,8 @@ export default class ReceiptInterface {
|
|||||||
this.protocol = documentPrinterClient.connection.protocol;
|
this.protocol = documentPrinterClient.connection.protocol;
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.receiptPrinter = null;
|
this.receiptPrinter = null;
|
||||||
this.retrieveStatusInterval = null;
|
this.statusMonitoringInterval = null;
|
||||||
|
this.lastReportedStatus = null;
|
||||||
this.images = new Map();
|
this.images = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +61,7 @@ export default class ReceiptInterface {
|
|||||||
default:
|
default:
|
||||||
type = PrinterTypes.EPSON;
|
type = PrinterTypes.EPSON;
|
||||||
logger.warn(
|
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();
|
const interfaceStr = this.buildPrinterUrl();
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Connecting to receipt printer ${this.name} (${interfaceStr})`
|
`Connecting to receipt printer ${this.name} (${interfaceStr})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize thermal printer
|
// Initialize thermal printer
|
||||||
@ -91,6 +102,7 @@ export default class ReceiptInterface {
|
|||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
logger.info(`Disconnecting from receipt printer ${this.name}`);
|
logger.info(`Disconnecting from receipt printer ${this.name}`);
|
||||||
|
await this.stopStatusMonitoring();
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.receiptPrinter = null;
|
this.receiptPrinter = null;
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@ -105,7 +117,7 @@ export default class ReceiptInterface {
|
|||||||
const isConnected = await this.receiptPrinter.isPrinterConnected();
|
const isConnected = await this.receiptPrinter.isPrinterConnected();
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
logger.error(
|
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" };
|
return { error: "Printer not connected during initialization" };
|
||||||
}
|
}
|
||||||
@ -113,7 +125,7 @@ export default class ReceiptInterface {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to initialize receipt printer ${this.name}:`,
|
`Failed to initialize receipt printer ${this.name}:`,
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +135,7 @@ export default class ReceiptInterface {
|
|||||||
async print(jobId, quantity = 1) {
|
async print(jobId, quantity = 1) {
|
||||||
const copies = Math.max(1, Math.floor(Number(quantity) || 1));
|
const copies = Math.max(1, Math.floor(Number(quantity) || 1));
|
||||||
logger.info(
|
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) {
|
if (!this.isConnected || !this.receiptPrinter) {
|
||||||
@ -155,14 +167,14 @@ export default class ReceiptInterface {
|
|||||||
await this.receiptPrinter.execute({ waitForResponse: true });
|
await this.receiptPrinter.execute({ waitForResponse: true });
|
||||||
|
|
||||||
logger.info(
|
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 };
|
return { success: true, copies };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to print job ${jobId} to receipt printer ${this.name}:`,
|
`Failed to print job ${jobId} to receipt printer ${this.name}:`,
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
@ -170,7 +182,7 @@ export default class ReceiptInterface {
|
|||||||
|
|
||||||
async deploy(documentJob, documentTemplate, object, onProgress) {
|
async deploy(documentJob, documentTemplate, object, onProgress) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Deploying job ${documentJob._id} to receipt printer ${this.name}`
|
`Deploying job ${documentJob._id} to receipt printer ${this.name}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageObj =
|
const imageObj =
|
||||||
@ -187,7 +199,7 @@ export default class ReceiptInterface {
|
|||||||
|
|
||||||
if (!imageObj || !imageObj.images) {
|
if (!imageObj || !imageObj.images) {
|
||||||
throw new Error(
|
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 };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveStatus() {
|
isValidStatusByte(byte) {
|
||||||
logger.debug(`Getting status of receipt printer ${this.name}`);
|
return (byte & VALID_STATUS_MASK) === VALID_STATUS_MASK;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Receipt printer ${this.name} is connected.`);
|
parsePrinterStatus(byte) {
|
||||||
return true;
|
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,
|
callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.objectType == "documentSize") {
|
||||||
|
this.documentPrinterManager.handleDocumentSizeUpdate(data._id, data);
|
||||||
|
}
|
||||||
if (data.objectType == "printer") {
|
if (data.objectType == "printer") {
|
||||||
this.printerManager.handlePrinterUpdate(data._id, data, callback);
|
this.printerManager.handlePrinterUpdate(data._id, data, callback);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user