Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit
573 lines
18 KiB
JavaScript
573 lines
18 KiB
JavaScript
// 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();
|
|
}
|
|
}
|