@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
JavaScript
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