import log4js from "log4js"; 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; this.filamentStock = null; // Store current filament stock this.existingEvent = null; // Store existing stock event logger.info("Initialized PrinterDatabase with socket manager"); } async getPrinter() { const object = await this.socketClient.getObject({ _id: this.printer._id, objectType: "printer", populate: ["subJobs"], }); return object; } async updatePrinter() { sendIPC("setPrinter", this.printer); const object = await this.socketClient.editObject({ _id: this.printer._id, objectType: "printer", updateData: this.printer, }); return object; } 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) { 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; const updatedPrinter = await this.updatePrinter(); 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 clearCurrentJob() { try { const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { currentSubJob: null, currentJob: null, }, }); if (!updatedPrinter) { logger.error( `Printer with ID ${this.id} not found when clearing current job` ); return null; } logger.info(`Cleared current job for printer ${this.id}`); return { currentSubJob: null, currentJob: null, }; } catch (error) { logger.error( `Failed to clear current job for printer ${this.id}:`, error ); throw error; } } async setCurrentJobForPrinting(queuedJobIds) { try { const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, populate: ["subJobs"], }); if (!printers || printers.length === 0) { logger.error(`Printer with ID ${this.id} not found`); return null; } const printer = printers[0]; const subJobs = printer.subJobs; for (const subJob of subJobs) { if ( !queuedJobIds.includes(subJob.subJobId) && ["printing", "queued", "paused"].includes(subJob.state.type) ) { logger.info( `Setting current job and subjob for printer ${this.id} as printing starts`, { subJobId: subJob.id, jobId: subJob.job, } ); const now = new Date(); // Update printer with current job and startedAt const oldCurrentJob = printer.currentJob; const oldCurrentSubJob = printer.currentSubJob; const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { currentSubJob: subJob.id, currentJob: subJob.job, startedAt: now, }, }); if (!updatedPrinter) { logger.error( `Printer with ID ${this.id} not found when setting job and subjob` ); return null; } // Update subjob with startedAt const oldStartedAt = subJob.startedAt; await this.socketClient.editObject({ _id: subJob.id, objectType: "subJob", updateData: { startedAt: now }, }); // Get the full job object and update its startedAt if null const jobs = await this.socketClient.listObjects({ objectType: "job", filter: { _id: subJob.job }, populate: ["gcodeFile"], }); if (!jobs || jobs.length === 0) { logger.error(`Job with ID ${subJob.job} not found`); return null; } const job = jobs[0]; const oldJobStartedAt = job.startedAt; if (!job.startedAt) { await this.socketClient.editObject({ _id: subJob.job, objectType: "job", updateData: { startedAt: now }, }); job.startedAt = now; } logger.info(`Set current job for printer ${this.id}:`, { subJobId: subJob.id, jobId: subJob.job, }); return { currentSubJob: subJob, currentJob: job, }; } } return null; } catch (error) { logger.error(`Failed to set current job for printer ${this.id}:`, error); throw error; } } async updateSubJobState(subJobId, state) { try { if ( state.type == "standby" || state.type == "error" || state.type == "offline" ) { state.type = "failed"; logger.warn(`Updating subjob state for ${subJobId}:`, state); } else { logger.debug(`Updating subjob state for ${subJobId}:`, state); } // Set startedAt if the subjob is starting to print const updateData = { state }; if (state.type === "printing") { const subJobs = await this.socketClient.listObjects({ objectType: "subJob", filter: { _id: subJobId }, }); if (subJobs && subJobs.length > 0) { const subJob = subJobs[0]; if (!subJob.startedAt) { updateData.startedAt = new Date(); logger.info( `Setting startedAt for subjob ${subJobId} as printing begins` ); } } } // Set finishedAt if the subjob is complete, failed, or cancelled if (["complete", "failed", "cancelled"].includes(state.type)) { const subJobs = await this.socketClient.listObjects({ objectType: "subJob", filter: { _id: subJobId }, }); if (subJobs && subJobs.length > 0) { const subJob = subJobs[0]; if (!subJob.finishedAt) { updateData.finishedAt = new Date(); logger.info( `Setting finishedAt for subjob ${subJobId} as state is ${state.type}` ); } } } const subJobs = await this.socketClient.listObjects({ objectType: "subJob", filter: { _id: subJobId }, }); if (!subJobs || subJobs.length === 0) { logger.error(`Sub job with ID ${subJobId} not found`); return; } const subJob = subJobs[0]; const oldState = subJob.state; const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", updateData, }); logger.info(`Updated subjob ${subJobId} state:`, { type: state.type, progress: state.progress, previousState: updatedSubJob.state, job: updatedSubJob.job, startedAt: updatedSubJob.startedAt, finishedAt: updatedSubJob.finishedAt, }); // Update parent job state await this.updateJobState(updatedSubJob.job, subJob.printer); return updatedSubJob; } catch (error) { logger.error(`Failed to update sub job state for ${subJobId}:`, error); throw error; } } async updateJobState(jobId, printerId) { try { logger.debug(`Updating job state for ${jobId}`); const jobs = await this.socketClient.listObjects({ objectType: "job", filter: { _id: jobId }, populate: ["subJobs"], }); if (!jobs || jobs.length === 0) { logger.error(`Job with ID ${jobId} not found`); return; } const job = jobs[0]; const subJobStates = job.subJobs.map((subJob) => subJob.state); const stateCounts = { printing: 0, paused: 0, complete: 0, failed: 0, queued: 0, }; let progress = 0; subJobStates.forEach((state) => { stateCounts[state.type]++; progress += state.progress || 0; }); logger.debug(`Job ${jobId} state counts:`, stateCounts); const jobState = { type: this.determineJobState(stateCounts), progress: progress / job.subJobs.length, }; logger.debug(`Calculated job state for ${jobId}:`, { type: jobState.type, progress: jobState.progress, subJobCount: job.subJobs.length, }); var oldAuditLogValues = {}; var newAuditLogValues = {}; // Update finishedAt if all subjobs are complete and none are queued const oldFinishedAt = job.finishedAt; if ( stateCounts.complete + stateCounts.failed + stateCounts.cancelled === job.subJobs.length && stateCounts.queued === 0 && !job.finishedAt ) { logger.info( `Setting finishedAt for job ${jobId} as all subjobs are complete` ); job.finishedAt = new Date(); oldAuditLogValues = { ...oldAuditLogValues, finishedAt: oldFinishedAt, }; newAuditLogValues = { ...newAuditLogValues, finishedAt: job.finishedAt, }; } const oldState = job.state; if (oldState.type !== jobState.type) { oldAuditLogValues = { ...oldAuditLogValues, state: oldState }; newAuditLogValues = { ...newAuditLogValues, state: jobState }; } const updateData = { state: jobState, subJobStats: stateCounts, }; if (job.finishedAt) { updateData.finishedAt = job.finishedAt; } const updatedJob = await this.socketClient.editObject({ _id: jobId, objectType: "job", updateData, }); logger.info(`Updated job ${jobId} state:`, { _id: jobId, state: jobState, subJobStats: stateCounts, finishedAt: updatedJob.finishedAt, }); return updatedJob; } catch (error) { logger.error(`Failed to update job state for ${jobId}:`, 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.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 addSubJobToPrinter(subJobId) { try { logger.debug(`Adding subjob ${subJobId} to printer ${this.id}`); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, }); if (!printers || printers.length === 0) { logger.error(`Printer with ID ${this.id} not found`); return null; } const printer = printers[0]; const updatedSubJobs = [...printer.subJobs, subJobId]; const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { subJobs: updatedSubJobs }, }); logger.info(`Added subjob ${subJobId} to printer ${this.id}`, { currentSubJobs: updatedPrinter.subJobs.length, printerState: updatedPrinter.state, }); return updatedPrinter; } catch (error) { logger.error( `Failed to add subjob ${subJobId} to printer ${this.id}:`, error ); throw error; } } async removePrinterSubJob(subJobId) { try { logger.debug(`Removing subjob ${subJobId} from printer ${this.id}`); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, }); if (!printers || printers.length === 0) { logger.error(`Printer with ID ${this.id} not found`); return; } const printer = printers[0]; const updatedSubJobs = printer.subJobs.filter( (id) => id.toString() !== subJobId.toString() ); const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { subJobs: updatedSubJobs }, }); logger.info(`Removed subjob ${subJobId} from printer ${this.id}`, { remainingSubJobs: updatedPrinter.subJobs.length, printerState: updatedPrinter.state, }); return updatedPrinter; } catch (error) { logger.error( `Failed to remove subjob ${subJobId} from 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.updatePrinter(); 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 { logger.debug(`Adding alert to printer ${this.id}:`, alert); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, }); if (!printers || printers.length === 0) { logger.error(`Printer with ID ${this.id} not found`); return null; } const printer = printers[0]; const existingAlertIndex = printer.alerts.findIndex( (a) => a.type === alert.type ); if (existingAlertIndex !== -1) { // If we have a message to update, update the existing alert if (alert.message) { logger.debug( `Updating message for existing alert of type ${alert.type} on printer ${this.id}` ); const updatedAlerts = [...printer.alerts]; updatedAlerts[existingAlertIndex].message = alert.message; const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { alerts: updatedAlerts }, }); logger.info( `Updated message for existing alert on printer ${this.id}:`, { type: alert.type, message: alert.message, } ); return updatedPrinter; } logger.debug( `Alert of type ${alert.type} already exists for printer ${this.id}, skipping` ); return printer; } // No existing alert found, create a new one logger.debug(`Creating new alert for printer ${this.id}:`, { type: alert.type, priority: alert.priority, hasMessage: !!alert.message, }); const updatedAlerts = [...printer.alerts, alert]; const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { alerts: updatedAlerts }, }); logger.info(`Added new alert to printer ${this.id}:`, { type: alert.type, priority: alert.priority, hasMessage: !!alert.message, }); return updatedPrinter; } catch (error) { logger.error(`Failed to add alert to printer ${this.id}:`, error); throw error; } } async removeAlerts(options = {}) { try { logger.debug(`Clearing alerts for printer ${this.id}:`, options); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, }); if (!printers || printers.length === 0) { logger.error( `Printer with ID ${this.id} not found when clearing alerts` ); return null; } const printer = printers[0]; let filteredAlerts = printer.alerts; if (options.type) { filteredAlerts = filteredAlerts.filter( (alert) => alert.type !== options.type ); } if (options.priority) { filteredAlerts = filteredAlerts.filter( (alert) => alert.priority !== options.priority ); } const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { alerts: filteredAlerts }, }); logger.info(`Cleared alerts for printer ${this.id}:`, { options, remainingAlerts: updatedPrinter.alerts.length, }); return updatedPrinter; } catch (error) { logger.error(`Failed to clear alerts for printer ${this.id}:`, error); throw error; } } async getAlerts(options = {}) { try { logger.debug(`Getting alerts for printer ${this.id}:`, options); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, }); if (!printers || printers.length === 0) { logger.error( `Printer with ID ${this.id} not found when getting alerts` ); return null; } const printer = printers[0]; let alerts = printer.alerts; // Filter alerts based on options if (options.type) { alerts = alerts.filter((alert) => alert.type === options.type); } if (options.priority) { alerts = alerts.filter((alert) => alert.priority === options.priority); } // Sort alerts by priority alerts.sort((a, b) => a.priority.localeCompare(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.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { 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(filamentStockId) { try { logger.debug(`Setting current filament stock for printer ${this.id}:`, { filamentStockId, }); const printers = await this.socketClient.listObjects({ objectType: "printer", filter: { _id: this.id }, populate: [ { path: "currentFilamentStock", populate: { path: "filament", }, }, ], }); if (!printers || printers.length === 0) { logger.error( `Printer with ID ${this.id} not found when setting current filament stock` ); return null; } const printer = printers[0]; const oldFilamentStock = printer.currentFilamentStock; const updatedPrinter = await this.socketClient.editObject({ _id: this.id, objectType: "printer", updateData: { currentFilamentStock: filamentStockId }, populate: [ { path: "currentFilamentStock", populate: { path: "filament", }, }, ], }); if (!updatedPrinter) { logger.error( `Printer with ID ${this.id} not found when setting current filament stock` ); return null; } logger.info(`Updated current filament stock for printer ${this.id}:`, { filamentStock: updatedPrinter.currentFilamentStock, }); return updatedPrinter.currentFilamentStock; } catch (error) { logger.error( `Failed to set current filament stock for printer ${this.id}:`, error ); throw error; } } async updateFilamentStockWeight( filamentStockId, weight, subJobId = null, jobId = null ) { try { // Get or fetch filament stock if ( !this.filamentStock || this.filamentStock._id.toString() !== filamentStockId ) { const filamentStocks = await this.socketClient.listObjects({ objectType: "filamentStock", filter: { _id: filamentStockId }, populate: ["stockEvents"], }); if (!filamentStocks || filamentStocks.length === 0) { logger.error(`Filament stock with ID ${filamentStockId} not found`); return null; } this.filamentStock = filamentStocks[0]; } // Calculate new weights immediately const totalEventWeight = this.filamentStock.stockEvents.reduce((sum, event) => { // Skip the existing event if it exists if ( this.existingEvent && event._id.toString() === this.existingEvent._id.toString() ) { return sum; } return sum + event.value; }, 0) + weight; const newNetWeight = totalEventWeight; const newGrossWeight = totalEventWeight + (this.filamentStock.startingGrossWeight - this.filamentStock.startingNetWeight); const remainingPercent = newNetWeight / this.filamentStock.startingNetWeight; const state = { type: newNetWeight <= 0 ? "fullyconsumed" : "partiallyconsumed", percent: (1 - remainingPercent).toFixed(2), }; // Check if a stock event already exists for this subJobId and jobId const stockEvents = await this.socketClient.listObjects({ objectType: "stockEvent", filter: { filamentStock: filamentStockId, subJob: subJobId, job: jobId, }, }); let stockEvent; if (stockEvents && stockEvents.length > 0) { // Update existing event this.existingEvent = stockEvents[0]; logger.debug( `Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}` ); stockEvent = await this.socketClient.editObject({ _id: this.existingEvent._id, objectType: "stockEvent", updateData: { value: weight, updatedAt: new Date(), }, }); } else { // Create new stock event logger.debug( `Creating new stock event for subJobId ${subJobId} and jobId ${jobId}` ); stockEvent = await this.socketClient.editObject({ _id: null, // This will create a new object objectType: "stockEvent", updateData: { type: "subJob", value: weight, subJob: subJobId, job: jobId, filamentStock: filamentStockId, unit: "g", updatedAt: new Date(), createdAt: new Date(), }, }); // Add the new stock event to the filament stock const updatedStockEvents = [ ...this.filamentStock.stockEvents, stockEvent._id, ]; await this.socketClient.editObject({ _id: filamentStockId, objectType: "filamentStock", updateData: { stockEvents: updatedStockEvents }, }); } const oldState = this.filamentStock.state; const updatedFilamentStock = await this.socketClient.editObject({ _id: filamentStockId, objectType: "filamentStock", updateData: { currentNetWeight: newNetWeight, currentGrossWeight: newGrossWeight, state, }, populate: [ { path: "stockEvents", populate: [ { path: "subJob", select: "number", }, { path: "job", select: "startedAt", }, ], }, ], }); // Update the cached filament stock this.filamentStock = updatedFilamentStock; logger.info(`Updated filament stock ${filamentStockId}:`, { newGrossWeight: newGrossWeight, newNetWeight: newNetWeight, eventCount: updatedFilamentStock.stockEvents.length, updatedExistingEvent: !!this.existingEvent, remainingPercent: remainingPercent.toFixed(2), consumedPercent: state.percent, }); return updatedFilamentStock; } catch (error) { logger.error( `Failed to update filament stock weight for ${filamentStockId}:`, error ); throw error; } } }