// documentprinterclient.js - Handles connection to a single document printer import { loadConfig } from "../config.js"; import log4js from "log4js"; import CupsPrinterInterface from "./interfaces/cupsinterface.js"; import ReceiptInterface from "./interfaces/receiptinterface.js"; import { sendIPC } from "../electron/ipc.js"; // Load configuration const config = loadConfig(); const logger = log4js.getLogger("Document Printer Client"); logger.level = config.logLevel; export class DocumentPrinterClient { constructor( documentPrinter = { connection: { interface: "cups", host: "localhost", port: 9100 }, }, documentPrinterManager, ) { this.id = documentPrinter._id; this.documentPrinter = documentPrinter; this.connection = documentPrinter.connection; this.queue = []; this.documentPrinterManager = documentPrinterManager; this.currentJob = null; this.active = documentPrinter.active == true; this.socketClient = documentPrinterManager.socketClient; this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os this.state = { type: this.active == true ? "offline" : "inactive" }; this.isOnline = documentPrinter.online || false; this.shouldReconnect = true; this.isProcessingQueue = false; this.eventUpdateInterval = null; this.initializeInterface(); this.registerEventHandlers(); this.subscribeToActions(); this.subscribeToObjectUpdates(); } initializeInterface() { logger.info( `Initializing ${this.interface} interface for document printer ${this.id}`, ); switch (this.interface) { case "cups": this.printerInterface = new CupsPrinterInterface(this); logger.debug("Cups printer interface initialized"); break; case "epsonReceipt": this.printerInterface = new ReceiptInterface(this); logger.debug("Epson receipt printer interface initialized"); break; case "starReceipt": this.printerInterface = new ReceiptInterface(this); logger.debug("Star receipt printer interface initialized"); break; default: logger.error(`Unknown interface type: ${this.interface}`); this.printerInterface = null; } } async updateDocumentPrinter(data) { logger.debug(`Updating document printer ${this.id} with data...`); sendIPC("setDocumentPrinter", { _id: this.id, ...data }); // Check for connection changes before updating if (data?.connection) { const oldConnection = this.connection || {}; const newConnection = data?.connection || {}; const hostChanged = newConnection.host != null && newConnection.host !== oldConnection.host; const protocolChanged = newConnection.protocol != null && newConnection.protocol !== oldConnection.protocol; const interfaceChanged = newConnection.interface != null && newConnection.interface !== oldConnection.interface; const portChanged = newConnection?.port !== oldConnection?.port; const connectionChanged = hostChanged || interfaceChanged || portChanged || protocolChanged; logger.debug(`Connection changed: ${connectionChanged}`); logger.debug(`Host changed: ${hostChanged}`); logger.debug(`Interface changed: ${interfaceChanged}`); logger.debug(`Port changed: ${portChanged}`); logger.debug(`Protocol changed: ${protocolChanged}`); // Update document printer data this.documentPrinter = { ...this.documentPrinter, ...data }; // Update interface if it changed if (interfaceChanged) { this.interface = newConnection.interface; } // Re-initialize only if connection properties changed if (connectionChanged) { this.connection = newConnection; await this.printerInterface.disconnect(); this.initializeInterface(); await this.reconnect(); } } this.documentPrinter = { ...this.documentPrinter, ...data }; if (Object.hasOwn(data || {}, "active") && data.active != this.active) { if (data.active == true) { await this.setActive(); } else { await this.setInactive(); } } } registerEventHandlers() { // Register event handlers for document printer notifications // This can be extended as needed } subscribeToActions() { this.socketClient.subscribeToObjectActions({ objectType: "documentPrinter", _id: this.id, }); } subscribeToObjectUpdates() { this.socketClient.subscribeToObjectUpdates({ objectType: "documentPrinter", _id: this.id, }); } async connect() { if (this.active == false) { logger.info( `Document printer ${this.id} is not active, skipping connection`, ); this.shouldReconnect = false; this.isOnline = false; this.state = { type: "inactive" }; await this.updateDocumentPrinterState(); return false; } logger.info( `Connecting to document printer ${this.id} (${this.interface})`, ); clearTimeout(this.reconnectTimeout); // Always stop the event update interval when connecting clearInterval(this.eventUpdateInterval); this.eventUpdateInterval = null; this.state = { type: "connecting", message: null }; this.isOnline = false; await this.updateDocumentPrinterState(); if (!this.printerInterface) { logger.error( `Cannot connect: No interface initialized for ${this.interface}`, ); return false; } const result = await this.printerInterface.connect(); if (result.error) { logger.error( `Error connecting to document printer ${this.documentPrinter.name}:`, result.error, ); this.isOnline = false; this.state = { type: "offline", message: result.error }; await this.updateDocumentPrinterState(); return false; } logger.info( `Connected to document printer ${this.documentPrinter.name} (${this.interface})`, ); return true; } async reconnect() { if (this.active == false) { logger.info( `Document printer ${this.documentPrinter.name} is inactive, skipping reconnect`, ); this.shouldReconnect = false; this.isOnline = false; this.state = { type: "inactive" }; await this.updateDocumentPrinterState(); return false; } if (this.isOnline == true) { logger.info( `Disconnecting from document printer ${this.documentPrinter.name} before reconnecting...`, ); await this.disconnect(); } logger.info( `Reconnecting to document printer ${this.documentPrinter.name}`, ); this.shouldReconnect = true; const connectResult = await this.connect(); if (connectResult == false) { logger.error( `Error reconnecting to document printer ${this.documentPrinter.name}:`, connectResult.error, ); if (this.shouldReconnect) { // Attempt to reconnect after delay setTimeout(() => this.reconnect(), 30000); } return false; } const initializeResult = await this.initialize(); if (initializeResult == false) { logger.error( `Error initializing document printer ${this.documentPrinter.name}:`, initializeResult.error, ); if (this.shouldReconnect) { // Attempt to reconnect after delay setTimeout(() => this.reconnect(), 30000); } return false; } return true; } async initialize() { logger.info("Running document printer initialization..."); this.state = { type: "initializing", message: null }; this.isOnline = true; this.connectedAt = new Date(); await this.updateDocumentPrinterState(); if (this.printerInterface && this.printerInterface.initialize) { const result = await this.printerInterface.initialize(); if (result.error) { logger.error( `Error initializing document printer ${this.documentPrinter.name}:`, result.error, ); this.state = { type: "offline", message: result.error }; await this.updateDocumentPrinterState(); return false; } this.state = { type: "standby", message: null }; await this.updateDocumentPrinterState(); this.eventUpdateInterval = setInterval( this.handleEventUpdate.bind(this), 3000, ); return true; } } async handleEventUpdate() { if (this.printerInterface && this.printerInterface.retrieveStatus) { try { //await this.printerInterface.retrieveStatus(); } catch (error) { logger.error( `Error retrieving status for document printer ${this.documentPrinter.name}:`, error, ); } } } async updateDocumentPrinterState() { try { // Update state in database or via socket client // This can be implemented based on your database structure this.socketClient.editObject({ _id: this.id, objectType: "documentPrinter", updateData: { online: this.isOnline, state: this.state, connectedAt: this.connectedAt, }, }); } catch (error) { logger.error(`Failed to update document printer state:`, error); } } async updateJobState(jobId, state) { logger.info(`Updating job state for ${jobId}`); await this.socketClient.editObject({ _id: jobId, objectType: "documentJob", updateData: { state: state }, }); logger.info(`Updated job state for ${jobId}:`, state); } async deployDocumentJob(documentJob) { logger.info( `Deploying document job ${documentJob._id} to ${this.documentPrinter.name}`, ); if (!this.isOnline) { logger.error( `Cannot deploy job: Document printer not connected (${this.documentPrinter.name})`, ); return { error: "Document printer not connected" }; } if (!this.printerInterface) { logger.error( `Cannot deploy job: No interface initialized (${this.documentPrinter.name})`, ); return { error: "No interface initialized" }; } try { if (this.printerInterface.deploy) { await this.updateJobState(documentJob._id, { type: "deploying" }); const documentTemplate = await this.socketClient.getObject({ objectType: "documentTemplate", _id: documentJob.documentTemplate._id, }); await this.updateJobState(documentJob._id, { type: "deploying", progress: 0.25, }); const object = await this.socketClient.getObject({ objectType: documentJob.objectType, _id: documentJob.object._id, }); await this.updateJobState(documentJob._id, { type: "deploying", progress: 0.5, }); if (!documentTemplate) { logger.error( `Document template not found for job ${documentJob._id}`, ); return { error: "Document template not found" }; } const pdfObj = await this.socketClient.renderTemplatePDF({ _id: documentTemplate._id, content: documentTemplate.content, object: object, }); await this.updateJobState(documentJob._id, { type: "deploying", progress: 0.75, }); if (!pdfObj) { logger.error( `Failed to render document template for job ${documentJob._id}`, ); return { error: "Failed to render document template" }; } const result = await this.printerInterface.deploy( documentJob, pdfObj.pdf, ); await this.updateJobState(documentJob._id, { type: "deploying", progress: 1.0, }); logger.info( `Deployed document job ${documentJob._id} to ${this.documentPrinter.name}`, ); await this.updateJobState(documentJob._id, { type: "queued", progress: null, }); // Only add job to queue if it's not already there if (!this.queue.includes(documentJob._id)) { this.queue.push(documentJob._id); } else { logger.warn( `Job ${documentJob._id} is already in the queue for ${this.documentPrinter.name}`, ); } this.startQueue(); return result; } else { logger.error( `Interface ${this.interface} does not support deployDocumentJob`, ); return { error: "Interface does not support this operation" }; } } catch (error) { logger.error( `Error deploying document job to ${this.documentPrinter.name}:`, error, ); await this.updateJobState(documentJob._id, { type: "error", progress: null, message: error.message || "Failed to deploy document job", }); return { error: error.message || "Failed to deploy document job" }; } } async startQueue() { if (!this.isOnline) { logger.error( `Cannot start queue: Document printer not connected (${this.documentPrinter.name})`, ); return { error: "Document printer not connected" }; } if (!this.printerInterface) { logger.error( `Cannot start queue: No interface initialized (${this.documentPrinter.name})`, ); return { error: "No interface initialized" }; } // Prevent concurrent queue processing if (this.isProcessingQueue) { logger.debug( `Queue is already being processed for ${this.documentPrinter.name}`, ); return { info: "Queue is already being processed" }; } if (this.state.type == "standby") { logger.info(`Starting queue for ${this.documentPrinter.name}`); await this.runQueue(); } } async runQueue() { if (!this.isOnline) { logger.error( `Cannot print next job: Document printer not connected (${this.documentPrinter.name})`, ); return { error: "Document printer not connected" }; } if (!this.printerInterface) { logger.error( `Cannot print next job: No interface initialized (${this.documentPrinter.name})`, ); return { error: "No interface initialized" }; } if (this.state.type != "standby") { logger.error( `Cannot print next job: Document printer not in standby mode (${this.documentPrinter.name})`, ); return { error: "Document printer not in standby mode" }; } // Prevent concurrent queue processing if (this.isProcessingQueue) { logger.debug( `Queue is already being processed for ${this.documentPrinter.name}`, ); return { info: "Queue is already being processed" }; } this.isProcessingQueue = true; logger.info(`Starting to print jobs for ${this.documentPrinter.name}`); this.state = { type: "printing", message: null }; await this.updateDocumentPrinterState(); try { // Process all jobs in the queue using a loop instead of recursion while (this.queue.length > 0) { // Re-check connection status before each job if (!this.isOnline) { logger.error( `Printer went offline while printing (${this.documentPrinter.name})`, ); this.state = { type: "offline", message: "Connection lost during printing", }; await this.updateDocumentPrinterState(); return { error: "Document printer not connected" }; } if (!this.printerInterface) { logger.error( `Printer interface lost while printing (${this.documentPrinter.name})`, ); this.state = { type: "offline", message: "Interface lost during printing", }; await this.updateDocumentPrinterState(); return { error: "No interface initialized" }; } // Get the next job ID but don't remove it yet const jobId = this.queue[0]; if (!jobId) { break; } try { logger.info(`Printing job ${jobId} for ${this.documentPrinter.name}`); await this.updateJobState(jobId, { type: "printing" }); await this.printerInterface.print(jobId); logger.info( `Successfully printed job ${jobId} for ${this.documentPrinter.name}`, ); // Only remove job from queue after successful printing this.queue.shift(); await this.updateJobState(jobId, { type: "complete" }); } catch (error) { logger.error( `Error printing job ${jobId} for ${this.documentPrinter.name}:`, error, ); // Remove failed job from queue to prevent infinite retry loop // You may want to implement retry logic or error handling here this.queue.shift(); await this.updateJobState(jobId, { type: "failed" }); // Continue with next job even if one fails } } logger.info( `Finished printing all jobs for ${this.documentPrinter.name}`, ); this.state = { type: "standby", message: null }; await this.updateDocumentPrinterState(); return true; } finally { // Always reset the processing flag, even if there was an error this.isProcessingQueue = false; } } async disconnect() { logger.info(`Disconnecting from ${this.documentPrinter.name}`); this.shouldReconnect = false; // Always stop the event update interval when disconnecting clearInterval(this.eventUpdateInterval); this.eventUpdateInterval = null; if (this.printerInterface && this.printerInterface.disconnect) { await this.printerInterface.disconnect(); } this.isOnline = false; this.state = { type: this.active == false ? "inactive" : "offline" }; this.isProcessingQueue = false; this.queue = []; // Clear queue on disconnect await this.updateDocumentPrinterState(); clearTimeout(this.reconnectTimeout); logger.info(`Successfully disconnected from ${this.documentPrinter.name}`); return true; } async setInactive() { this.active = false; this.documentPrinter.active = false; this.isOnline = false; await this.disconnect(); this.state = { type: "inactive" }; await this.updateDocumentPrinterState(); } async setActive() { this.active = true; this.documentPrinter.active = true; this.shouldReconnect = true; this.isOnline = false; await this.reconnect(); } }