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.

199 lines (198 loc) 8.47 kB
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