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