@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.
199 lines (198 loc) • 8.47 kB
JavaScript
import { BgCodeBlockParameterSizes, BgCodeBlockTypes, BgCodeChecksumTypeSize, BgCodeCompressionInfo, BgCodeCompressionName } from "./bgcode.types.js";
import { BGCODE_EXPECTED_HEADERS, BGCODE_HEADER_MARKER } from "./bgcode.constants.js";
import { HeatshrinkDecoder } from "./heatshrink-decoder.js";
import { inflateSync } from "node:zlib";
//#region src/utils/bgcode/bgcode.utils.ts
async function parseFileHeader(fileHandle) {
const buffer = Buffer.alloc(10);
await fileHandle.read(buffer);
if (buffer.length < 10) throw new Error("File too small to be a valid BGCode file");
const magic = buffer.toString("ascii", 0, 4);
if (magic !== "GCDE") throw new Error(`Invalid BGCode file: magic number not found (got "${magic}", expected ${BGCODE_HEADER_MARKER})`);
return {
magic,
version: buffer.readUInt32LE(4),
checksumType: buffer.readUInt16LE(8)
};
}
async function parseBlockHeaders(fileHandle, fileSize, checksumType, skipGcode) {
let offset = 10;
const blockHeaders = [];
let currentExpectedHeaderIndex = 0;
while (offset < fileSize) {
const blockHeader = await parseBlockHeader(fileHandle, fileSize, offset, checksumType);
if (skipGcode && blockHeader.type == BgCodeBlockTypes.GCode) break;
const expectedType = BGCODE_EXPECTED_HEADERS[currentExpectedHeaderIndex];
if (blockHeader.type !== expectedType) {
const nextExpectedType = BGCODE_EXPECTED_HEADERS[currentExpectedHeaderIndex + 1];
if (blockHeader.type !== nextExpectedType) throw new Error(`Unexpected header header type ${blockHeader.type}, expected either ${expectedType} or next ${nextExpectedType}`);
currentExpectedHeaderIndex++;
}
if (Number.isNaN(blockHeader.blockSize)) throw new Error(`Unexpected blockHeader.blockSize ${blockHeader.blockSize}`);
offset += blockHeader.blockSize;
blockHeaders.push(blockHeader);
}
return blockHeaders;
}
async function parseBlockHeader(fileHandle, fileSize, blockStartOffset, checksumType) {
if (blockStartOffset + 12 > fileSize) throw new Error("Not enough data for block header");
const buffer = Buffer.alloc(12);
await fileHandle.read({
buffer,
offset: 0,
length: 12,
position: blockStartOffset
});
const type = buffer.readUInt16LE(0);
if (type > 5) throw new Error(`Block header type ${type} exceeds 5, cant parse block`);
const compression = buffer.readUInt16LE(2);
if (compression > 3) throw new Error(`Block header compression ${compression} exceeds 3, cant parse block`);
const uncompressedSize = buffer.readUInt32LE(4);
if (uncompressedSize === 0) throw new Error(`Uncompressed Size is 0`);
let compressedSize = 0;
let headerSize = 8;
if (compression > 0) {
compressedSize = buffer.readUInt32LE(8);
if (compressedSize === 0) throw new Error(`Compression is ${BgCodeCompressionName[compression]} but compressed size is ${compressedSize}}`);
headerSize = 12;
}
const checksumSize = calculateChecksumSize(checksumType);
const blockSize = calculateBlockSize(checksumSize, type, compression, uncompressedSize, compressedSize);
const parameterOffset = blockStartOffset + headerSize;
const parametersSize = calculateParameterSize(type);
const parameters = parseBlockParameters(type, await getBlockParsedParameters(fileHandle, parameterOffset, parametersSize));
const dataOffset = parameterOffset + parametersSize;
const dataSize = blockSize - parametersSize - checksumSize;
const checksumOffset = dataOffset + dataSize;
return {
type,
compression,
uncompressedSize,
compressedSize,
blockSize,
blockStartOffset,
headerSize,
parameterOffset,
parameters,
parametersSize,
dataOffset,
dataSize,
checksumOffset,
checksumSize,
checksumType
};
}
async function getBlockParsedParameters(fileHandle, parameterOffset, parametersSize) {
const parameters = Buffer.alloc(parametersSize);
if (parametersSize > 0) await fileHandle.read({
buffer: parameters,
offset: 0,
length: parametersSize,
position: parameterOffset
});
return parameters;
}
async function getBlockData(fileHandle, blockHeader) {
if (blockHeader.blockSize === 0) throw new Error("Cant get block data for block with size 0");
if (blockHeader.blockSize === 1e3) throw new Error(`Cant get block data for block with size ${blockHeader.blockSize} > 1000`);
const data = Buffer.alloc(blockHeader.dataSize);
await fileHandle.read({
buffer: data,
offset: 0,
length: blockHeader.dataSize,
position: blockHeader.dataOffset
});
return data;
}
function decompressBlock(compression, data) {
const info = BgCodeCompressionInfo[compression];
switch (info.kind) {
case "none": return data;
case "deflate": return inflateSync(data);
case "heatshrink": return new HeatshrinkDecoder(info.window, info.lookahead).decompress(data);
default: throw new Error(`Unknown compression type: ${compression}`);
}
}
function parseBlockParameters(blockType, parameters) {
const parameterSize = BgCodeBlockParameterSizes[blockType];
if (parameters.length !== parameterSize) throw new Error(`Block Parameters should be exactly ${parameterSize} bytes long`);
if (blockType === BgCodeBlockTypes.Thumbnail) return {
format: calculateThumbnailFormat(parameters.readUInt16LE(0)),
width: parameters.readUInt16LE(2),
height: parameters.readUInt16LE(4)
};
return { encoding: parameters.readUInt16LE(0) };
}
function calculateBlockSize(checksumSize, blockType, compression, uncompressedSize, compressedSize) {
const headerSize = compression > 0 ? 12 : 8;
const dataSize = compression === 0 ? uncompressedSize : compressedSize;
return headerSize + (blockType === BgCodeBlockTypes.Thumbnail ? 6 : 2) + dataSize + checksumSize;
}
function calculateChecksumSize(checksumType) {
if (checksumType > 1) throw new Error(`Checksum type ${checksumType} exceeds max 1`);
return BgCodeChecksumTypeSize[checksumType];
}
function calculateParameterSize(blockType) {
if (blockType > 5) throw new Error(`Checksum type ${blockType} exceeds max 5`);
return BgCodeBlockParameterSizes[blockType];
}
function calculateThumbnailFormat(formatParameter) {
if (formatParameter > 2) throw new Error(`Thumbnail format type ${formatParameter} exceeds max 2`);
return formatParameter;
}
async function extractMetadataFromBlocks(fileHandle, blockHeaders) {
const metadata = {};
const metadataBlocks = blockHeaders.filter((b) => b.type === BgCodeBlockTypes.FileMetadata || b.type === BgCodeBlockTypes.SlicerMetadata || b.type === BgCodeBlockTypes.PrinterMetadata || b.type === BgCodeBlockTypes.PrintMetadata);
for (const header of metadataBlocks) {
const blockData = await getBlockData(fileHandle, header);
extractMetadataFromText(decompressBlock(header.compression, blockData).toString("utf8"), metadata);
}
return metadata;
}
const METADATA_KEY_NORMALIZATION = {
producer: "producer",
"produced on": "produced_on",
"print time": "print_time",
"estimated printing time (silent mode)": "estimated_printing_time_silent_mode",
"estimated printing time (normal mode)": "estimated_printing_time_normal_mode",
"layer height": "layer_height",
"first layer height": "first_layer_height",
"initial layer height": "initial_layer_height",
"nozzle diameter": "nozzle_diameter",
"filament diameter": "filament_diameter",
"filament density": "filament_density",
"filament used [mm]": "filament_used_mm",
"filament used [cm3]": "filament_used_cm3",
"filament used [g]": "filament_used_g",
"bed temperature": "bed_temperature",
temperature: "temperature",
"fill density": "fill_density",
"filament type": "filament_type",
"printer model": "printer_model",
"max layer z": "max_layer_z",
"total layers": "total_layers",
"layer count": "layer_count"
};
function extractMetadataFromText(text, metadata) {
const lines = text.split("\n");
for (const line of lines) {
const keyValuePair = parseMetadataLine(line);
if (!keyValuePair) continue;
const { key, value } = keyValuePair;
metadata[key] = value;
}
}
function parseMetadataLine(line) {
const equalIndex = line.indexOf("=");
if (equalIndex === -1) return null;
const rawKey = line.substring(0, equalIndex).trim().toLowerCase();
const value = line.substring(equalIndex + 1).trim();
if (!rawKey || !value) return null;
return {
key: METADATA_KEY_NORMALIZATION[rawKey] || rawKey.replace(/\s+/g, "_"),
value
};
}
//#endregion
export { decompressBlock, extractMetadataFromBlocks, getBlockData, getBlockParsedParameters, parseBlockHeader, parseBlockHeaders, parseBlockParameters, parseFileHeader };
//# sourceMappingURL=bgcode.utils.js.map