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.

202 lines (201 loc) 7.32 kB
import { convertQoiToPng } from "../bgcode/bgcode-thumbnail.parser.js"; import { createReadStream } from "node:fs"; import * as fs$1 from "node:fs/promises"; import * as readline from "node:readline"; //#region src/utils/parsers/gcode.parser.ts /** * G-code parser for extracting metadata from .gcode files * Reads first and last N lines to extract slicer metadata */ var GCodeParser = class { maxHeaderLinesToRead = 500; maxFooterLinesToRead = 500; async parse(filePath) { const stats = await fs$1.stat(filePath); const fileName = filePath.split(/[/\\]/).pop() || filePath; const metadata = await this.extractMetadata(filePath); const thumbnails = await this.extractThumbnails(filePath); const normalized = { fileName, fileFormat: "gcode", fileSize: stats.size, gcodePrintTimeSeconds: this.parseTime(metadata.estimated_printing_time_normal_mode || metadata.estimated_printing_time || metadata.print_time), nozzleDiameterMm: this.parseFloat(metadata.nozzle_diameter), filamentDiameterMm: this.parseFloat(metadata.filament_diameter), filamentDensityGramsCm3: this.parseFloat(metadata.filament_density), filamentUsedMm: this.parseFloat(metadata.filament_used_mm), filamentUsedCm3: this.parseFloat(metadata.filament_used_cm3), filamentUsedGrams: this.parseFloat(metadata.filament_used_g), totalFilamentUsedGrams: this.parseFloat(metadata.total_filament_used_g || metadata.filament_used_g), layerHeight: this.parseFloat(metadata.layer_height), firstLayerHeight: this.parseFloat(metadata.first_layer_height || metadata.initial_layer_height), bedTemperature: this.parseFloat(metadata.bed_temperature || metadata.first_layer_bed_temperature), nozzleTemperature: this.parseFloat(metadata.temperature || metadata.first_layer_temperature), fillDensity: metadata.fill_density || null, filamentType: metadata.filament_type || null, printerModel: metadata.printer_model || metadata.printer_name || null, slicerVersion: metadata.generated_by || metadata.slicer_version || null, maxLayerZ: this.parseFloat(metadata.max_layer_z), totalLayers: this.parseInt(metadata.total_layers) || this.parseInt(metadata.layer_count), generatedBy: metadata.generated_by, thumbnails: thumbnails.length > 0 ? thumbnails.map((t) => ({ width: t.width, height: t.height, format: t.format, dataLength: t.data?.length || 0 })) : void 0 }; return { raw: { _thumbnails: thumbnails, metadata }, normalized }; } async extractMetadata(filePath) { const metadata = {}; await this.extractMetadataFromStart(filePath, metadata); await this.extractMetadataFromEnd(filePath, metadata); return metadata; } async extractMetadataFromStart(filePath, metadata) { let linesRead = 0; const fileStream = createReadStream(filePath); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); for await (const line of rl) { if (linesRead >= this.maxHeaderLinesToRead) break; linesRead++; this.parseMetadataLine(line, metadata); } rl.close(); fileStream.close(); } async extractMetadataFromEnd(filePath, metadata) { const fileSize = (await fs$1.stat(filePath)).size; const estimatedBytes = this.maxFooterLinesToRead * 50; const startPosition = Math.max(0, fileSize - estimatedBytes); const fileHandle = await fs$1.open(filePath, "r"); try { const buffer = Buffer.alloc(estimatedBytes); const { bytesRead } = await fileHandle.read(buffer, 0, estimatedBytes, startPosition); const lines = buffer.toString("utf8", 0, bytesRead).split("\n"); for (const line of lines) this.parseMetadataLine(line, metadata); } finally { await fileHandle.close(); } } parseMetadataLine(line, metadata) { if (!line.startsWith(";")) return; const generatedByMatch = line.match(/^;\s*generated by\s+([^\s]+)/i); if (generatedByMatch && !metadata.generated_by) { metadata.generated_by = generatedByMatch[1]; return; } const prusaMatch = line.match(/^;\s*([^=]+?)\s*=\s*(.+)$/); if (prusaMatch) { let key = prusaMatch[1].trim().toLowerCase().replace(/\s+/g, "_"); let value = prusaMatch[2].trim(); key = key.replace(/\[([^\]]+)\]/g, (_, unit) => "_" + unit.replace(/\^/g, "")); key = key.replace(/\(([^)]+)\)/g, (_, content) => "_" + content.replace(/\s+/g, "_")); key = key.replace(/_+/g, "_"); if (!metadata[key]) metadata[key] = value.trim(); return; } const curaMatch = line.match(/^;([A-Z_]+):(.+)$/); if (curaMatch) { const [, key, value] = curaMatch; const normalizedKey = key.toLowerCase(); if (!metadata[normalizedKey]) metadata[normalizedKey] = value.trim(); return; } const s3dMatch = line.match(/^;\s*([^:]+?):\s*(.+)$/); if (s3dMatch) { const [, key, value] = s3dMatch; const normalizedKey = key.trim().toLowerCase().replace(/\s+/g, "_"); if (!metadata[normalizedKey]) metadata[normalizedKey] = value.trim(); } } async extractThumbnails(filePath) { const thumbnails = []; let linesRead = 0; let inThumbnail = false; let thumbnailData = []; let currentWidth = 0; let currentHeight = 0; let currentFormat = "PNG"; const fileStream = createReadStream(filePath); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); for await (const line of rl) { if (linesRead >= this.maxHeaderLinesToRead && !inThumbnail) break; linesRead++; const thumbnailStart = line.match(/;\s*thumbnail begin (\d+)x(\d+)\s*(\w+)?/i); if (thumbnailStart) { inThumbnail = true; currentWidth = parseInt(thumbnailStart[1]); currentHeight = parseInt(thumbnailStart[2]); const thirdParam = thumbnailStart[3]; if (thirdParam && /^(PNG|JPG|JPEG|QOI)$/i.test(thirdParam)) currentFormat = thirdParam.toUpperCase(); else currentFormat = "PNG"; thumbnailData = []; continue; } if (inThumbnail) { if (line.match(/;\s*thumbnail end/i)) { let base64Data = thumbnailData.join(""); let format = currentFormat.toUpperCase(); if (format === "QOI") try { base64Data = convertQoiToPng(Buffer.from(base64Data, "base64")).toString("base64"); format = "PNG"; } catch {} thumbnails.push({ width: currentWidth, height: currentHeight, format, data: base64Data }); inThumbnail = false; thumbnailData = []; } else if (line.startsWith(";")) { const data = line.substring(1).trim(); if (data) thumbnailData.push(data); } } } rl.close(); fileStream.close(); return thumbnails; } parseFloat(value) { if (!value) return null; const num = parseFloat(value); return isNaN(num) ? null : num; } parseInt(value) { if (!value) return null; const num = parseInt(value, 10); return isNaN(num) ? null : num; } parseTime(value) { if (!value) return null; const match = value.match(/(?:(\d+)h)?(?:\s*(\d+)m)?(?:\s*(\d+)s)?/); if (match && (match[1] || match[2] || match[3])) { const hours = parseInt(match[1] || "0"); const minutes = parseInt(match[2] || "0"); const secs = parseInt(match[3] || "0"); return hours * 3600 + minutes * 60 + secs; } const seconds = parseFloat(value); if (!isNaN(seconds)) return seconds; return null; } }; //#endregion export { GCodeParser }; //# sourceMappingURL=gcode.parser.js.map