UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

239 lines (238 loc) 10.8 kB
import { printerEvents } from "../constants/event.constants.js"; import { KeyDiffCache } from "../utils/cache/key-diff.cache.js"; import { octoPrintEvent } from "../services/octoprint/octoprint-websocket.adapter.js"; import { moonrakerEvent } from "../services/moonraker/constants/moonraker.constants.js"; import { bambuEvent } from "../services/bambu/bambu-mqtt.adapter.js"; import { prusaLinkEvent } from "../services/prusa-link/constants/prusalink.constants.js"; import { messages } from "../services/octoprint/dto/octoprint-event.dto.js"; //#region src/state/printer-events.cache.ts var PrinterEventsCache = class PrinterEventsCache extends KeyDiffCache { logger; constructor(eventEmitter2, loggerFactory, printJobService, printerCache, printerThumbnailCache) { super(); this.eventEmitter2 = eventEmitter2; this.printJobService = printJobService; this.printerCache = printerCache; this.printerThumbnailCache = printerThumbnailCache; this.logger = loggerFactory(PrinterEventsCache.name); this.subscribeToEvents(); } async deletePrinterSocketEvents(id) { await this.deleteKeyValue(id, true); } async getPrinterSocketEvents(id) { return this.keyValueStore.get(id); } async getOrCreateEvents(printerId) { let ref = await this.getValue(printerId); if (!ref) { ref = { connected: null, reauthRequired: null, notify_status_update: null, current: null, history: null, API_STATE_UPDATED: null, WS_CLOSED: null, WS_ERROR: null, WS_OPENED: null, WS_STATE_UPDATED: null }; await this.setKeyValue(printerId, ref); } return ref; } async setEvent(printerId, label, payload) { const ref = await this.getOrCreateEvents(printerId); ref[label] = { payload, receivedAt: Date.now() }; await this.setKeyValue(printerId, ref); } async setEventPartial(printerId, label, payload, keysToUpdate) { const ref = await this.getOrCreateEvents(printerId); if (!ref[label]) ref[label] = { payload: {}, receivedAt: Date.now() }; for (const key of keysToUpdate) ref[label].payload[key] = payload[key]; ref[label].receivedAt = Date.now(); await this.setKeyValue(printerId, ref); } async handlePrintersDeleted(event) { await this.deleteKeysBatch(event.printerIds); } subscribeToEvents() { this.eventEmitter2.on(octoPrintEvent("*"), (e) => this.onOctoPrintSocketMessage(e)); this.eventEmitter2.on(moonrakerEvent("*"), (e) => this.onMoonrakerSocketMessage(e)); this.eventEmitter2.on(prusaLinkEvent("*"), (e) => this.onPrusaLinkPollMessage(e)); this.eventEmitter2.on(bambuEvent("*"), (e) => this.onBambuSocketMessage(e)); this.eventEmitter2.on(printerEvents.printersDeleted, this.handlePrintersDeleted.bind(this)); } async getPrinterName(printerId) { try { return (await this.printerCache.getValue(printerId))?.name; } catch (error) { this.logger.debug(`Could not get printer name for ${printerId}: ${error}`); return; } } async onOctoPrintSocketMessage(e) { const printerId = e.printerId; if (e.event === messages.current && e.payload) { const payload = e.payload; const keysToUpdate = [ "state", "job", "progress", "currentZ", "offsets", "resends" ]; if (Array.isArray(payload.temps) && payload.temps.length > 0) keysToUpdate.push("temps"); await this.setEventPartial(printerId, messages.current, payload, keysToUpdate); const flags = payload?.state?.flags; const filePath = payload?.job?.file?.path; const completion = payload?.progress?.completion; if (flags?.printing && filePath) { const printerName = await this.getPrinterName(printerId); const job = await this.printJobService.markStarted(printerId, filePath, printerName); if (job) await this.printerThumbnailCache.handleJobStarted(printerId, job.id); if (job && !job.fileStorageId && job.analysisState === "NOT_ANALYZED") { this.logger.log(`Job ${job.id} has no local file - triggering download and analysis`); await this.printJobService.triggerFileAnalysis(job.id); } if (job && !job.metadata?.gcodePrintTimeSeconds) { const estimatedTime = payload?.job?.estimatedPrintTime; const filament = payload?.job?.filament?.tool0; await this.printJobService.updateJobMetadata(printerId, filePath, { gcodePrintTimeSeconds: estimatedTime ? Math.round(estimatedTime) : null, nozzleDiameterMm: null, filamentDiameterMm: null, filamentDensityGramsCm3: null, filamentUsedMm: filament?.length ? Math.round(filament.length) : null, filamentUsedCm3: filament?.volume ? Math.round(filament.volume * 100) / 100 : null, filamentUsedGrams: null, totalFilamentUsedGrams: null }); } } if (typeof completion === "number" && filePath) await this.printJobService.markProgress(printerId, filePath, completion); if ((flags?.finishing || flags?.error || flags?.cancelling) && filePath) if (flags?.finishing) { const job = await this.printJobService.markFinished(printerId, filePath); if (job) await this.printerThumbnailCache.handleJobCompleted(printerId, job.id); } else if (flags?.cancelling) await this.printJobService.markFailed(printerId, filePath, "Cancelled"); else await this.printJobService.markFailed(printerId, filePath, "Error"); } } async onMoonrakerSocketMessage(e) { const printerId = e.printerId; const eventType = e.event; if ([messages.notify_status_update, messages.current].includes(eventType)) { await this.setEvent(printerId, eventType, e.payload); const payload = e.payload; const status = payload?.status ?? payload?.result ?? {}; const filename = status?.print_stats?.filename; const progress = status?.display_status?.progress ?? status?.print_stats?.progress; const state = status?.print_stats?.state; if (state === "printing" && filename) { const printerName = await this.getPrinterName(printerId); const job = await this.printJobService.markStarted(printerId, filename, printerName); if (job) await this.printerThumbnailCache.handleJobStarted(printerId, job.id); if (job && !job.fileStorageId && job.analysisState === "NOT_ANALYZED") { this.logger.log(`Job ${job.id} has no local file - triggering download and analysis`); await this.printJobService.triggerFileAnalysis(job.id); } } if (typeof progress === "number" && filename) await this.printJobService.markProgress(printerId, filename, Math.round(progress * 100)); if ([ "complete", "cancelled", "error" ].includes(state) && filename) if (state === "complete") { const job = await this.printJobService.markFinished(printerId, filename); if (job) await this.printerThumbnailCache.handleJobCompleted(printerId, job.id); } else await this.printJobService.markFailed(printerId, filename, state); } } async onPrusaLinkPollMessage(e) { const printerId = e.printerId; if (e.event === messages.current) { await this.setEvent(printerId, messages.current, e.payload); const payload = e.payload; const state = payload?.state; const filename = payload?.job?.file?.path ?? payload?.job?.file?.display; const completion = payload?.progress?.completion; if (state === "Printing" && filename) { const printerName = await this.getPrinterName(printerId); const job = await this.printJobService.markStarted(printerId, filename, printerName); if (job) await this.printerThumbnailCache.handleJobStarted(printerId, job.id); if (job && !job.fileStorageId && job.analysisState === "NOT_ANALYZED") { this.logger.log(`Job ${job.id} has no local file - triggering download and analysis`); await this.printJobService.triggerFileAnalysis(job.id); } } if (typeof completion === "number" && filename) await this.printJobService.markProgress(printerId, filename, completion); if (state === "Finished" && filename) { const job = await this.printJobService.markFinished(printerId, filename); if (job) await this.printerThumbnailCache.handleJobCompleted(printerId, job.id); } else if (["Stopped", "Error"].includes(state) && filename) await this.printJobService.markFailed(printerId, filename, state); } } async onBambuSocketMessage(e) { const printerId = e.printerId; if (!printerId) { this.logger.warn("Received Bambu event without printerId", e); return; } if (e.event === messages.current) { await this.setEvent(printerId, messages.current, e.payload); const print = e.payload?.print; const percent = print?.mc_percent; const filename = print?.gcode_file || print?.subtask_name; print?.mc_print_stage; const state = print?.gcode_state; if (state === "PRINTING" && filename) { const printerName = await this.getPrinterName(printerId); const job = await this.printJobService.markStarted(printerId, filename, printerName); if (job) await this.printerThumbnailCache.handleJobStarted(printerId, job.id); if (job && !job.fileStorageId && job.analysisState === "NOT_ANALYZED") { this.logger.log(`Job ${job.id} has no local file - triggering download and analysis`); await this.printJobService.triggerFileAnalysis(job.id); } if (job && !job.metadata?.gcodePrintTimeSeconds) { const remainingMinutes = print?.mc_remaining_time; print?.total_layer_num; const estimatedSeconds = remainingMinutes ? remainingMinutes * 60 : null; const tray = print?.ams?.tray_now ? print?.vt_tray : print?.ams?.ams?.[0]?.tray?.[0]; const filamentDiameter = tray?.tray_diameter ? parseFloat(tray.tray_diameter) : null; await this.printJobService.updateJobMetadata(printerId, filename, { gcodePrintTimeSeconds: estimatedSeconds, nozzleDiameterMm: null, filamentDiameterMm: filamentDiameter, filamentDensityGramsCm3: null, filamentUsedMm: null, filamentUsedCm3: null, filamentUsedGrams: null, totalFilamentUsedGrams: null }); } } if (typeof percent === "number" && filename) await this.printJobService.markProgress(printerId, filename, percent); if (state === "FINISHED" && filename) { const job = await this.printJobService.markFinished(printerId, filename); if (job) await this.printerThumbnailCache.handleJobCompleted(printerId, job.id); } else if (state === "IDLE") { const activeJob = await this.printJobService.getActivePrintJob(printerId); if (activeJob && activeJob.status === "PRINTING") { this.logger.log(`Print job ${activeJob.id} transitioned to IDLE - marking as cancelled`); await this.printJobService.handlePrintCancelled(printerId, "Print stopped"); } } else if (state === "ERROR" && filename) await this.printJobService.markFailed(printerId, filename, "Error"); } } }; //#endregion export { PrinterEventsCache }; //# sourceMappingURL=printer-events.cache.js.map