import { randomUUID } from "crypto"; import log4js from "log4js"; import NodeCache from "node-cache"; import { loadConfig } from "../config.js"; import { sendIPC } from "../electron/ipc.js"; const config = loadConfig(); const logger = log4js.getLogger("Printer Database"); logger.level = config.logLevel; export class PrinterDatabase { constructor(socketClient, printer) { this.socketClient = socketClient; this.printer = printer; this.id = this.printer._id; // Initialize cache with 30 second TTL this.printerCache = new NodeCache({ stdTTL: 30 }); logger.info("Initialized PrinterDatabase with socket manager"); } async getPrinter() { const populate = [ "currentFilamentStock", "currentJob", "currentSubJob", "queue", ]; const populateKey = populate.sort().join(","); const cacheKey = `printer_${this.printer._id}_${populateKey}`; // Check if printer is in cache const cachedPrinter = this.printerCache.get(cacheKey); if (cachedPrinter) { logger.debug(`Returning cached printer for ${this.id}`); this.printer = cachedPrinter; return cachedPrinter; } // Fetch from socketClient if not in cache const object = await this.socketClient.getObject({ _id: this.printer._id, objectType: "printer", populate: populate, }); this.printer = object; // Store in cache this.printerCache.set(cacheKey, object); logger.debug(`Cached printer for ${this.id}`); return object; } async editPrinter(updateData) { sendIPC("setPrinter", this.printer); const populate = [ "currentFilamentStock", "currentJob", "currentSubJob", "queue", ]; const object = await this.socketClient.editObject({ _id: this.printer._id, objectType: "printer", updateData: updateData, populate: populate, }); // Update cache with the edited printer (overwrite existing cache) const populateKey = populate.sort().join(","); const cacheKey = `printer_${this.printer._id}_${populateKey}`; this.printerCache.del(cacheKey); // Delete existing cache entry this.printerCache.set(cacheKey, { ...object, ...updateData }); // Set new cache entry this.printer = { ...object, ...updateData }; logger.debug(`Updated printer cache for ${this.id}`); return object; } async setPrinterConnectedAt() { const updatedPrinter = await this.editPrinter({ connectedAt: new Date(), }); this.printer = updatedPrinter; return updatedPrinter; } async removePrinterConnectedAt() { const updatedPrinter = await this.editPrinter({ connectedAt: null, }); this.printer = updatedPrinter; return updatedPrinter; } async getPrinterConfig() { try { logger.debug(`Getting printer config for ${this.id}`); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, populate: ["moonraker"], }); if (!printers || printers.length === 0) { logger.error( `Printer with ID ${this.id} not found when getting config`, ); return null; } const printer = printers[0]; logger.debug( `Retrieved printer config for ${this.id}:`, printer.moonraker, ); return printer.moonraker; } catch (error) { logger.error(`Failed to get printer config for ${this.id}:`, error); throw error; } } async updatePrinterState(state, online, connectedAt) { try { logger.debug(`Updating printer state for ${this.printer.name}:`, { state, online, }); if (state.type === "printing" && state.progress === undefined) { logger.debug( `Setting default progress for printing state on printer ${this.printer.name}`, ); state.progress = 0; } this.printer.state = state; this.printer.online = online; this.printer.connectedAt = connectedAt; const updatedPrinter = await this.editPrinter({ state, online, connectedAt, }); logger.info(`Updated printer ${this.printer.name} state:`, { type: state.type, progress: state.progress, online, previousState: updatedPrinter.state, }); return updatedPrinter; } catch (error) { logger.error( `Failed to update printer state for ${this.printer.name}:`, error, ); throw error; } } async updateSubJobState(subJobId, state) { try { const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", updateData: { state }, }); logger.info(`Updated subjob ${subJobId} state:`, { type: state.type, progress: state.progress, }); return updatedSubJob; } catch (error) { logger.error(`Failed to update sub job state for ${subJobId}:`, error); throw error; } } async updateJobState(jobId) { try { logger.debug(`Updating job state for ${jobId}`); const subJobs = await this.socketClient.listObjects({ objectType: "subJob", filter: { job: { _id: jobId } }, cached: false, }); const subJobStates = subJobs.map((subJob) => subJob.state); const stateCounts = { printing: 0, paused: 0, complete: 0, failed: 0, queued: 0, cancelled: 0, deploying: 0, }; let jobProgress = 0; subJobStates.forEach((state) => { stateCounts[state.type]++; var subJobProgress = state.progress || 0; if (state.type === "complete") { subJobProgress = 1; } jobProgress += subJobProgress; }); logger.debug(`Job ${jobId} state counts:`, stateCounts); const jobState = { type: this.determineJobState(stateCounts), progress: jobProgress / subJobs.length, }; logger.debug(`Calculated job state for ${jobId}:`, { type: jobState.type, progress: jobState.progress, subJobCount: subJobs.length, }); if (jobState.type == "complete") { await this.setJobFinishedAt(jobId, new Date()); } await this.socketClient.editObject({ _id: jobId, objectType: "job", updateData: { state: jobState }, }); logger.info(`Updated job ${jobId} state:`, { _id: jobId, state: jobState, subJobStats: stateCounts, }); return jobState; } catch (error) { logger.error(`Failed to update job state for ${jobId}:`, error); throw error; } } async setSubJobMoonrakerJobId(subJobId, moonrakerJobId) { try { logger.debug(`Setting subjob ${subJobId} moonraker job id:`, { moonrakerJobId, }); const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", updateData: { moonrakerJobId: moonrakerJobId }, }); logger.info(`Set subjob ${subJobId} moonraker job id:`, { moonrakerJobId, }); return updatedSubJob; } catch (error) { logger.error(`Failed to set subjob ${subJobId} moonraker job id:`, error); throw error; } } determineJobState(stateCounts) { logger.debug("Determining job state from counts:", stateCounts); // If any subjob is printing, the overall state should be printing if (stateCounts.deploying > 0) { logger.debug( "Job state determined as 'deploying' due to active deploying subjobs", ); return "deploying"; } // If any subjob is printing, the overall state should be printing if (stateCounts.printing > 0) { logger.debug( "Job state determined as 'printing' due to active printing subjobs", ); return "printing"; } if (stateCounts.failed > 0 || stateCounts.cancelled > 0) { logger.debug( "Job state determined as 'failed' due to failed or cancelled subjobs", ); return "failed"; } if (stateCounts.paused > 0) { logger.debug("Job state determined as 'paused'"); return "paused"; } if ( stateCounts.complete === Object.values(stateCounts).reduce((a, b) => a + b, 0) ) { logger.debug("Job state determined as 'complete'"); return "complete"; } logger.debug("Job state determined as 'queued'"); return "queued"; } async setCurrentJobAndSubJob(subJob, job) { try { logger.debug(`Setting current job and subjob for printer ${this.id}:`, { subJobId: subJob?._id || null, jobId: job?._id || null, }); const updatedPrinter = await this.editPrinter({ currentSubJob: subJob, currentJob: job, }); return updatedPrinter; } catch (error) { logger.error( `Failed to set current job and subjob for printer ${this.id}:`, error, ); throw error; } } async updateDisplayStatus(message) { try { logger.debug(`Updating display status for printer ${this.id}:`, { message, }); logger.info(`Updated display status for printer ${this.id}:`, { message, }); } catch (error) { logger.error( `Failed to update display status for printer ${this.id}:`, error, ); throw error; } } async updatePrinterFirmware(firmwareVersion) { try { logger.debug( `Updating firmware version for printer ${this.id}:`, firmwareVersion, ); this.printer.firmware = firmwareVersion; await this.editPrinter({ firmware: firmwareVersion }); logger.info( `Updated firmware version for printer ${this.id}:`, firmwareVersion, ); } catch (error) { logger.error( `Failed to update firmware version for printer ${this.id}:`, error, ); throw error; } } async addAlert(alert) { try { const alertWithId = { ...alert, _id: alert._id ?? randomUUID(), }; logger.debug(`Adding alert to printer ${this.id}:`, alertWithId); const printer = await this.getPrinter(); const updatedAlerts = [...printer.alerts, alertWithId]; const updatedPrinter = await this.editPrinter({ alerts: updatedAlerts }); logger.info(`Added new alert to printer ${this.id}:`, { type: alert.type, code: alert.code, priority: alert.priority, hasMessage: !!alert.message, }); return updatedPrinter; } catch (error) { logger.error(`Failed to add alert to printer ${this.id}:`, error); throw error; } } async removeAlertById(alertId) { try { logger.debug(`Removing alert by id ${alertId} for printer ${this.id}`); const printer = await this.getPrinter(); const filteredAlerts = printer.alerts.filter( (alert) => alert._id !== alertId, ); const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts }); logger.info(`Removed alert ${alertId} for printer ${this.id}:`, { alertId, alerts: updatedPrinter.alerts, }); return updatedPrinter; } catch (error) { logger.error( `Failed to remove alert ${alertId} for printer ${this.id}:`, error, ); throw error; } } async removeAlertsByCode(code) { try { logger.debug(`Removing alerts by code ${code} for printer ${this.id}`); const printer = await this.getPrinter(); const filteredAlerts = printer.alerts.filter( (alert) => alert.code !== code, ); const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts }); logger.info(`Removed alerts with code ${code} for printer ${this.id}:`, { code, alerts: updatedPrinter.alerts, }); return updatedPrinter; } catch (error) { logger.error( `Failed to remove alerts with code ${code} for printer ${this.id}:`, error, ); throw error; } } async getAlerts(options = {}) { try { logger.debug(`Getting alerts for printer ${this.id}:`, options); const printer = await this.getPrinter(); let alerts = printer.alerts; // Filter alerts based on options if (options.action) { alerts = alerts.filter((alert) => alert.action === options.action); } // Sort alerts by priority alerts.sort((a, b) => String(a.priority ?? "").localeCompare(String(b.priority ?? "")), ); logger.info(`Retrieved ${alerts.length} alerts for printer ${this.id}`); return alerts; } catch (error) { logger.error(`Failed to get alerts for printer ${this.id}:`, error); throw error; } } async clearAlerts() { try { logger.debug(`Clearing all alerts for printer ${this.id}`); const updatedPrinter = await this.editPrinter({ alerts: [] }); if (!updatedPrinter) { logger.error( `Printer with ID ${this.id} not found when clearing alerts`, ); return null; } logger.info(`Cleared all alerts for printer ${this.id}`); return updatedPrinter; } catch (error) { logger.error(`Failed to clear alerts for printer ${this.id}:`, error); throw error; } } async setCurrentFilamentStock(filamentStock) { try { logger.debug(`Setting current filament stock for printer ${this.id}:`, { filamentStock, }); const updatedPrinter = await this.editPrinter({ currentFilamentStock: filamentStock, }); logger.info( `Updated current filament stock for printer ${this.id}:`, filamentStock, ); return updatedPrinter.currentFilamentStock; } catch (error) { logger.error( `Failed to set current filament stock for printer ${this.id}:`, error, ); throw error; } } async updateFilamentStockWeight( filamentStock, weight, subJob = null, job = null, ) { const subJobId = subJob._id || subJob.id; const jobId = job._id || job.id; try { const filamentStockStockEvents = await this.socketClient.listObjects({ objectType: "stockEvent", filter: { "parent._id": filamentStock._id, parentType: "filamentStock", }, populate: ["owner"], }); const stockEvents = filamentStockStockEvents.filter( (event) => event.owner._id.toString() === subJobId.toString() && event.ownerType === "subJob", ); let stockEvent; if (stockEvents && stockEvents.length > 0) { const existingEvent = stockEvents[0]; logger.trace( `Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`, ); stockEvent = await this.socketClient.editObject({ _id: existingEvent._id, objectType: "stockEvent", updateData: { value: weight, updatedAt: new Date(), }, auditLog: false, }); } else { logger.debug( `Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`, ); stockEvent = await this.socketClient.newObject({ objectType: "stockEvent", newData: { filamentStock: filamentStock._id, value: weight, unit: "g", parentType: "filamentStock", parent: { _id: filamentStock._id }, owner: this.printer.currentSubJob, ownerType: "subJob", }, }); } logger.trace( `Updated stock event for filament stock ${filamentStock._id}:`, { value: weight, updatedExistingEvent: stockEvents.length > 0, }, ); return stockEvent; } catch (error) { logger.error( `Failed to update stock event for filament stock ${filamentStock._id}:`, error, ); throw error; } } async queueSubJob(subJob, moonrakerJobId) { try { logger.info( `Queueing subjob ${subJob._id} for printer ${this.id} with Moonraker ID ${moonrakerJobId}`, ); // 1. Update SubJob with Moonraker ID and State await this.socketClient.editObject({ _id: subJob._id, objectType: "subJob", updateData: { moonrakerJobId: moonrakerJobId, state: { type: "queued" }, }, }); // 2. Add to Printer's Queue const newQueue = [...this.printer.queue, subJob]; const updatedPrinter = await this.editPrinter({ queue: newQueue, }); this.printer.queue = newQueue; return updatedPrinter; } catch (error) { logger.error(`Failed to queue subjob for printer ${this.id}:`, error); throw error; } } async setQueuedSubJobs(subJobs) { try { logger.debug( `Setting queued subjobs for printer ${this.id}:`, subJobs.map((subJob) => subJob._id).join(", "), ); const updatedPrinter = await this.editPrinter({ queue: subJobs, }); return updatedPrinter; } catch (error) { logger.error( `Failed to set queued subjobs for printer ${this.id}:`, error, ); throw error; } } async getQueuedSubJobs() { try { logger.debug(`Getting queued subjobs for printer ${this.id}`); const printer = await this.getPrinter(); return printer.queue; } catch (error) { logger.error( `Failed to get queued subjobs for printer ${this.id}:`, error, ); throw error; } } async getJobById(jobId) { try { logger.debug(`Getting job by ID ${jobId} for printer ${this.id}`); const job = await this.socketClient.getObject({ objectType: "job", _id: jobId, }); return job; } catch (error) { logger.error( `Failed to get job by ID ${jobId} for printer ${this.id}:`, error, ); throw error; } } async postSubJobPartStockItems(subJobId) { try { logger.info( `Posting subjob part stock items for subjob ${subJobId} for printer ${this.id}`, ); logger.debug(`Getting subjob ${subJobId} for printer ${this.id}`); const subJob = await this.socketClient.getObject({ objectType: "subJob", _id: subJobId, }); logger.debug(`Subjob ${subJobId} for printer ${this.id}:`, subJob); logger.debug( `Getting gcode file for subjob ${subJobId} for printer ${this.id}`, ); const gcodeFile = await this.socketClient.getObject({ objectType: "gcodeFile", _id: subJob.gcodeFile._id, populate: ["parts.part", "parts.partSku"], }); const partItems = gcodeFile.parts; for (const partItem of partItems) { const partSkuId = partItem.partSku?._id || partItem.partSku; logger.info( `Posting part stock item for part SKU ${ partItem.partSku?.name || partSkuId } for subjob ${subJobId} for printer ${this.id}`, ); const quantity = partItem.quantity; const partStock = await this.socketClient.newObject({ objectType: "partStock", newData: { sourceType: "subJob", source: subJobId, partSku: partSkuId, currentQuantity: quantity, state: { type: "new", progress: 1, }, }, }); logger.info( `Creating stock event for part stock ${partStock._id} with quantity ${quantity} for subjob ${subJobId} for printer ${this.id}`, ); await this.socketClient.newObject({ objectType: "stockEvent", newData: { value: quantity, unit: "", parentType: "partStock", parent: partStock._id, owner: subJobId, ownerType: "subJob", }, }); } } catch (error) { logger.error( `Failed to post subjob part stock items for subjob ${subJobId} for printer ${this.id}:`, error, ); throw error; } } async setSubJobStartedAt(subJobId, date) { try { logger.debug( `Setting started at for subjob ${subJobId} for printer ${this.id}`, ); const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", updateData: { startedAt: date }, }); return updatedSubJob; } catch (error) { logger.error( `Failed to set started at for subjob ${subJobId} for printer ${this.id}:`, error, ); throw error; } } async setSubJobFinishedAt(subJobId, date) { try { logger.debug( `Setting finished at for subjob ${subJobId} for printer ${this.id}`, ); const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", updateData: { finishedAt: date }, }); return updatedSubJob; } catch (error) { logger.error( `Failed to set finished at for subjob ${subJobId} for printer ${this.id}:`, error, ); throw error; } } async setJobStartedAt(jobId, date) { try { logger.debug( `Setting started at for job ${jobId} for printer ${this.id}`, ); const updatedJob = await this.socketClient.editObject({ _id: jobId, objectType: "job", updateData: { startedAt: date }, }); return updatedJob; } catch (error) { logger.error( `Failed to set started at for job ${jobId} for printer ${this.id}:`, error, ); throw error; } } async setJobFinishedAt(jobId, date) { try { logger.debug( `Setting finished at for job ${jobId} for printer ${this.id}`, ); const updatedJob = await this.socketClient.editObject({ _id: jobId, objectType: "job", updateData: { finishedAt: date }, }); return updatedJob; } catch (error) { logger.error( `Failed to set finished at for job ${jobId} for printer ${this.id}:`, error, ); throw error; } } }