farmcontrol-server/src/documentprinter/documentprinterclient.js
Tom Butcher ffa6d84251
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit
Update dependencies, improve document printer management, and add workspace configuration
2026-06-15 00:53:28 +01:00

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();
}
}