bambu-printer-mcp 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTORS.md +3 -1
- package/README.md +5 -3
- package/dist/bambu-network-bridge.d.ts +78 -4
- package/dist/bambu-network-bridge.js +292 -47
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/index.js +28 -11
- package/dist/printers/bambu.d.ts +12 -6
- package/dist/printers/bambu.js +74 -57
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +119 -12
- package/package.json +1 -2
- package/src/bambu-network-bridge.ts +324 -55
- package/src/bambu-network-certs.ts +154 -0
- package/src/index.ts +43 -25
- package/src/printers/bambu.ts +74 -79
- package/src/safety/printer-state.ts +42 -1
- package/src/stl/stl-manipulator.ts +126 -7
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type BambuSliceOptions,
|
|
25
25
|
type SlicerType,
|
|
26
26
|
} from "./stl/stl-manipulator.js";
|
|
27
|
-
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
27
|
+
import { BambuNetworkBridge, describeBambuNetworkPrintError, type BambuNetworkBridgeOptions, type BridgeJobUpdate } from "./bambu-network-bridge.js";
|
|
28
28
|
import {
|
|
29
29
|
assertBambuNativeAvailable,
|
|
30
30
|
bambuNativeControlsAvailable,
|
|
@@ -1642,12 +1642,17 @@ class BambuPrinterMCPServer {
|
|
|
1642
1642
|
this.bambu.assertBedClearance(confirmedStatus, bedClearance);
|
|
1643
1643
|
const amsMapping = stringifyBridgeJson(mapping?.map(value => bridgeAmsMappingValue(value, isDualNozzleModel(printModel))));
|
|
1644
1644
|
const remoteFileName = uniquePrintName(threeMFPath);
|
|
1645
|
+
const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
|
|
1645
1646
|
const params: Record<string, unknown> = {
|
|
1646
1647
|
dev_id: devId,
|
|
1647
1648
|
// Firmware can report the task name instead of the uploaded filename.
|
|
1648
1649
|
// Bind either report to this inspected artifact for a later safe resume.
|
|
1649
|
-
task_name:
|
|
1650
|
-
|
|
1650
|
+
task_name: checkedName,
|
|
1651
|
+
// The plug-in names the uploaded file and the printer's job from
|
|
1652
|
+
// project_name, so it carries the same unique checked name, as the
|
|
1653
|
+
// native route already passes; a reused name would overwrite a stored
|
|
1654
|
+
// job and break the resume binding.
|
|
1655
|
+
project_name: checkedName,
|
|
1651
1656
|
preset_name: presetName,
|
|
1652
1657
|
filename: snapshot,
|
|
1653
1658
|
config_filename: snapshot,
|
|
@@ -1723,22 +1728,31 @@ class BambuPrinterMCPServer {
|
|
|
1723
1728
|
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1724
1729
|
return { ...nativeResult, autoSliced, projectName, plateIndex, useAMS, amsMapping: mapping };
|
|
1725
1730
|
}
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1731
|
+
const bridgeOptions: BambuNetworkBridgeOptions = {
|
|
1732
|
+
...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
|
|
1733
|
+
beforeDispatch: async method => {
|
|
1734
|
+
if (method !== bridgeMethod) return;
|
|
1735
|
+
assertActive();
|
|
1736
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1737
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1738
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1739
|
+
},
|
|
1740
|
+
assertDispatchAllowed: method => { if (method === bridgeMethod) assertActive(); },
|
|
1741
|
+
onDispatched: method => { if (method === bridgeMethod) retainForDispatchedUpload(); },
|
|
1742
|
+
};
|
|
1743
|
+
let jobUpdates: BridgeJobUpdate[] = [];
|
|
1744
|
+
let bridgeResult: unknown;
|
|
1745
|
+
if (isLocalBridgePrint) {
|
|
1746
|
+
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint(
|
|
1747
|
+
{ devId, devIp, username: String(params.username), password, useSsl: params.use_ssl_for_mqtt as boolean },
|
|
1748
|
+
bridgeMethod,
|
|
1749
|
+
{ client_job_id: clientJobId, params },
|
|
1750
|
+
bridgeOptions,
|
|
1751
|
+
assertActive
|
|
1752
|
+
));
|
|
1753
|
+
} else {
|
|
1754
|
+
bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions);
|
|
1755
|
+
}
|
|
1742
1756
|
|
|
1743
1757
|
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
1744
1758
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
@@ -1750,7 +1764,10 @@ class BambuPrinterMCPServer {
|
|
|
1750
1764
|
(bridgeResult as { value: number }).value !== 0
|
|
1751
1765
|
) {
|
|
1752
1766
|
const value = (bridgeResult as { value: number }).value;
|
|
1753
|
-
|
|
1767
|
+
const reported = jobUpdates.filter(update => update.code !== 0 || update.msg).slice(-3)
|
|
1768
|
+
.map(update => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
|
|
1769
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
|
|
1770
|
+
(reported ? ` Plug-in reported: ${reported}.` : ""));
|
|
1754
1771
|
}
|
|
1755
1772
|
|
|
1756
1773
|
assertActive();
|
|
@@ -1766,6 +1783,7 @@ class BambuPrinterMCPServer {
|
|
|
1766
1783
|
projectName,
|
|
1767
1784
|
plateIndex,
|
|
1768
1785
|
bridgePlateIndex: plateIndex + 1,
|
|
1786
|
+
printerJobName: checkedName,
|
|
1769
1787
|
useAMS,
|
|
1770
1788
|
amsMapping: mapping,
|
|
1771
1789
|
params: redactPrintParams(params),
|
|
@@ -2500,11 +2518,11 @@ class BambuPrinterMCPServer {
|
|
|
2500
2518
|
},
|
|
2501
2519
|
use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile. Template 3MF process settings can still be used at the same time." },
|
|
2502
2520
|
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
2503
|
-
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
2521
|
+
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies. OrcaSlicer copies an STL with --clone-objects; do not combine with clone_objects." },
|
|
2504
2522
|
orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
|
|
2505
|
-
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing
|
|
2523
|
+
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing; STL input is arranged by default. Set false only to keep a 3MF's existing layout: an STL that is not already on the plate then fails to slice." },
|
|
2506
2524
|
ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
|
|
2507
|
-
clone_objects: { type: "string", description: "
|
|
2525
|
+
clone_objects: { type: "string", description: "Clone the loaded model: total copies as a single integer, e.g. '3'. The Bambu-style CLIs count copies per input file and this tool loads one file, so a comma-separated list is rejected by the slicer." },
|
|
2508
2526
|
skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
|
|
2509
2527
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
2510
2528
|
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
@@ -2606,7 +2624,7 @@ class BambuPrinterMCPServer {
|
|
|
2606
2624
|
nozzle_type: { type: "string", enum: [...VALID_NOZZLE_TYPES], description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)." },
|
|
2607
2625
|
nozzle_volume_type: { type: "string", enum: [...VALID_NOZZLE_VOLUME_TYPES], description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)." },
|
|
2608
2626
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
2609
|
-
project_name: { type: "string", description: "Optional project name
|
|
2627
|
+
project_name: { type: "string", description: "Optional project name returned in the result; defaults to the 3MF filename without extension. The printer job and stored file use the server's unique checked name so resume_print can bind to the inspected file." },
|
|
2610
2628
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
2611
2629
|
task_name: { type: "string", description: "Optional project label when project_name is omitted. The submitted task name uses a unique inspected-job identity for safe resume." },
|
|
2612
2630
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
@@ -2650,7 +2668,7 @@ class BambuPrinterMCPServer {
|
|
|
2650
2668
|
type: "object",
|
|
2651
2669
|
properties: {
|
|
2652
2670
|
save_path: { type: "string", description: "Optional absolute path to write the JPEG to disk. If omitted, only the base64 payload is returned." },
|
|
2653
|
-
timeout_ms: { type: "number", description: "Max ms to wait for a
|
|
2671
|
+
timeout_ms: { type: "number", description: "Max ms to wait for a live frame (default 8000). A1/P1 cameras first replay a buffered frame, which is skipped, so a live frame takes about 3 s, longer on cold start." },
|
|
2654
2672
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
2655
2673
|
experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
|
|
2656
2674
|
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series, X2D)." },
|
package/src/printers/bambu.ts
CHANGED
|
@@ -9,10 +9,9 @@ import { readSafe3mfArchive } from "../safety/archive.js";
|
|
|
9
9
|
import { inspectPrintFile, type PrintFileInspection } from "../safety/print-file.js";
|
|
10
10
|
import { isDualNozzleModel, normalizeModel, validateTemperature } from "../safety/limits.js";
|
|
11
11
|
import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, describeAmsTrayRange } from "../ams-mapping.js";
|
|
12
|
-
import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements, type PrinterStateRequirements } from "../safety/printer-state.js";
|
|
12
|
+
import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, unchippedNozzles, validatePrinterState, manualHeatingRequirements, type PrinterStateRequirements } from "../safety/printer-state.js";
|
|
13
13
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
|
|
14
14
|
import { Client as FTPClient } from "basic-ftp";
|
|
15
|
-
import { BambuPrinter } from "bambu-js";
|
|
16
15
|
import * as mqtt from "mqtt";
|
|
17
16
|
import {
|
|
18
17
|
BambuClient,
|
|
@@ -348,6 +347,7 @@ class BambuClientStore {
|
|
|
348
347
|
timeoutMs = 4000
|
|
349
348
|
): Promise<Record<string, any> | null> {
|
|
350
349
|
const key = getPrinterKey(host, serial, token);
|
|
350
|
+
const deadline = Date.now() + timeoutMs + 500;
|
|
351
351
|
const existing = this.reportSnapshots.get(key);
|
|
352
352
|
|
|
353
353
|
if (!existing || Object.keys(existing).length === 0) {
|
|
@@ -370,8 +370,11 @@ class BambuClientStore {
|
|
|
370
370
|
// (gcode_state, ams, hms, temperatures, fans, etc.) arrives afterward
|
|
371
371
|
// and gets merged into reportSnapshots via incremental update.
|
|
372
372
|
// This settle runs regardless of whether data arrived before or during
|
|
373
|
-
// the promise race above, so the merge has time to complete.
|
|
373
|
+
// the promise race above, so the merge has time to complete. A busy host
|
|
374
|
+
// can take longer, which returned the sparse report as status UNKNOWN,
|
|
375
|
+
// so keep waiting for the full status within the same bound.
|
|
374
376
|
await sleep(500);
|
|
377
|
+
while (typeof this.reportSnapshots.get(key)?.gcode_state !== "string" && Date.now() < deadline) await sleep(50);
|
|
375
378
|
return this.reportSnapshots.get(key) || null;
|
|
376
379
|
}
|
|
377
380
|
|
|
@@ -471,6 +474,36 @@ class BambuClientStore {
|
|
|
471
474
|
/** What the human confirmed in confirmPrintPreflight, checked again before dispatch. */
|
|
472
475
|
export type PrintPreflight = { finishedJob?: string; serial: string; model: string; notices: string[] };
|
|
473
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Split the A1/P1 camera stream on port 6000 into JPEG frames. Each frame is
|
|
479
|
+
* a 16-byte header (payload size LE, itrack, flags, reserved) and the JPEG.
|
|
480
|
+
*/
|
|
481
|
+
export function cameraFrameParser(onFrame: (jpeg: Buffer) => void, onError: (error: Error) => void): (chunk: Buffer) => void {
|
|
482
|
+
let buffered = Buffer.alloc(0);
|
|
483
|
+
let failed = false;
|
|
484
|
+
return chunk => {
|
|
485
|
+
if (failed) return;
|
|
486
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
487
|
+
while (buffered.length >= 16) {
|
|
488
|
+
const payloadSize = buffered.readUInt32LE(0);
|
|
489
|
+
if (payloadSize <= 0 || payloadSize > 5_000_000) {
|
|
490
|
+
failed = true;
|
|
491
|
+
onError(new Error(`camera_snapshot: unreasonable payload size ${payloadSize} from header; auth likely failed.`));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (buffered.length < 16 + payloadSize) return;
|
|
495
|
+
const jpeg = buffered.subarray(16, 16 + payloadSize);
|
|
496
|
+
buffered = buffered.subarray(16 + payloadSize);
|
|
497
|
+
if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
|
|
498
|
+
failed = true;
|
|
499
|
+
onError(new Error(`camera_snapshot: payload does not start with JPEG SOI (FF D8); got ${jpeg[0].toString(16)} ${jpeg[1].toString(16)}.`));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
onFrame(Buffer.from(jpeg));
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
474
507
|
export class BambuImplementation {
|
|
475
508
|
private printerStore: BambuClientStore;
|
|
476
509
|
private checkedJobs = new Map<string, { remotePath: string; requirements: PrinterStateRequirements }>();
|
|
@@ -482,8 +515,14 @@ export class BambuImplementation {
|
|
|
482
515
|
|
|
483
516
|
async confirmHardwareAction(message: string, physicalCheck = false): Promise<void> {
|
|
484
517
|
if (!physicalCheck && process.env.BAMBU_REQUIRE_CONFIRMATION === "0") return;
|
|
485
|
-
if (!this.confirm
|
|
486
|
-
throw new Error("Hardware safety confirmation
|
|
518
|
+
if (!this.confirm) {
|
|
519
|
+
throw new Error("Hardware safety confirmation is unavailable. Use an MCP client with elicitation support. No command was sent.");
|
|
520
|
+
}
|
|
521
|
+
if (!(await this.confirm(message))) {
|
|
522
|
+
throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
|
|
523
|
+
? "This confirmation covers a physical check at the printer and always needs a person to accept it."
|
|
524
|
+
: "Some clients decline prompts automatically, for example in a non-interactive permission mode. " +
|
|
525
|
+
"A deliberately headless setup that approves jobs another way can set BAMBU_REQUIRE_CONFIRMATION=0 to skip ordinary print and heating prompts."));
|
|
487
526
|
}
|
|
488
527
|
}
|
|
489
528
|
|
|
@@ -500,6 +539,7 @@ export class BambuImplementation {
|
|
|
500
539
|
const finishedJob = this.finishedJobIdentity(status);
|
|
501
540
|
const cleared = this.clearedErrors.get(serial);
|
|
502
541
|
const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
|
|
542
|
+
const unchipped = unchippedNozzles(status?.raw);
|
|
503
543
|
await this.confirmHardwareAction(
|
|
504
544
|
`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
505
545
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
@@ -507,7 +547,9 @@ export class BambuImplementation {
|
|
|
507
547
|
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
508
548
|
(cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
|
|
509
549
|
(notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
|
|
510
|
-
`${notices.length === 1 ? "it does" : "they do"} not block printing.` : "")
|
|
550
|
+
`${notices.length === 1 ? "it does" : "they do"} not block printing.` : "") +
|
|
551
|
+
(unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
|
|
552
|
+
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : ""),
|
|
511
553
|
finishedJob !== undefined || !!cleared
|
|
512
554
|
);
|
|
513
555
|
return { finishedJob, serial, model: inspection.model, notices };
|
|
@@ -1410,26 +1452,6 @@ export class BambuImplementation {
|
|
|
1410
1452
|
};
|
|
1411
1453
|
}
|
|
1412
1454
|
|
|
1413
|
-
async getFile(host: string, serial: string, token: string, filename: string) {
|
|
1414
|
-
const printer = new BambuPrinter(host, serial, token);
|
|
1415
|
-
|
|
1416
|
-
const normalized = filename.replace(/^\/+/, "");
|
|
1417
|
-
const directory = path.posix.dirname(normalized) === "." ? "cache" : path.posix.dirname(normalized);
|
|
1418
|
-
const baseName = path.posix.basename(normalized);
|
|
1419
|
-
|
|
1420
|
-
let exists = false;
|
|
1421
|
-
|
|
1422
|
-
await printer.manipulateFiles(async (context) => {
|
|
1423
|
-
const entries = await context.readDir(directory);
|
|
1424
|
-
exists = entries.includes(baseName);
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
return {
|
|
1428
|
-
name: `${directory}/${baseName}`,
|
|
1429
|
-
exists,
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
1455
|
async uploadFile(
|
|
1434
1456
|
host: string, serial: string, token: string, filePath: string,
|
|
1435
1457
|
filename: string, print: boolean, bambuModel?: string,
|
|
@@ -1828,13 +1850,19 @@ export class BambuImplementation {
|
|
|
1828
1850
|
}
|
|
1829
1851
|
|
|
1830
1852
|
/**
|
|
1831
|
-
* Open the TLS-on-6000 socket, send the 80-byte auth packet, and
|
|
1832
|
-
* a
|
|
1853
|
+
* Open the TLS-on-6000 socket, send the 80-byte auth packet, and return
|
|
1854
|
+
* a live JPEG frame. The printer first replays the last frame it buffered,
|
|
1855
|
+
* which after an idle period can show the chamber as it was long ago (a
|
|
1856
|
+
* finished part still on the bed), so that frame and any byte-identical
|
|
1857
|
+
* repeat of it are discarded and the next frame returned; live frames always
|
|
1858
|
+
* differ by sensor noise. If no live frame arrives in time, this fails rather
|
|
1859
|
+
* than return a possibly stale image.
|
|
1833
1860
|
*/
|
|
1834
1861
|
private async fetchTcpCameraFrame(
|
|
1835
1862
|
host: string,
|
|
1836
1863
|
token: string,
|
|
1837
|
-
timeoutMs: number
|
|
1864
|
+
timeoutMs: number,
|
|
1865
|
+
port = 6000
|
|
1838
1866
|
): Promise<Buffer> {
|
|
1839
1867
|
const tls = await import("node:tls");
|
|
1840
1868
|
|
|
@@ -1846,11 +1874,9 @@ export class BambuImplementation {
|
|
|
1846
1874
|
auth.write(token, 48, Math.min(32, Buffer.byteLength(token, "ascii")), "ascii");
|
|
1847
1875
|
|
|
1848
1876
|
return new Promise((resolve, reject) => {
|
|
1849
|
-
const chunks: Buffer[] = [];
|
|
1850
|
-
let totalLen = 0;
|
|
1851
|
-
const FRAME_HEADER_BYTES = 16;
|
|
1852
|
-
let payloadSize: number | null = null;
|
|
1853
1877
|
let settled = false;
|
|
1878
|
+
let frames = 0;
|
|
1879
|
+
let buffered: Buffer | undefined;
|
|
1854
1880
|
|
|
1855
1881
|
const finish = (err: Error | null, jpeg?: Buffer) => {
|
|
1856
1882
|
if (settled) return;
|
|
@@ -1861,16 +1887,25 @@ export class BambuImplementation {
|
|
|
1861
1887
|
else if (jpeg) resolve(jpeg);
|
|
1862
1888
|
else reject(new Error("camera_snapshot: ended without jpeg payload"));
|
|
1863
1889
|
};
|
|
1890
|
+
const unfinished = (reason: string) => new Error(frames
|
|
1891
|
+
? `camera_snapshot: ${reason} after only the printer's buffered frame, which may be stale; no live frame arrived. Retry, or raise timeout_ms.`
|
|
1892
|
+
: `camera_snapshot: ${reason} before a full JPEG frame arrived`);
|
|
1893
|
+
|
|
1894
|
+
const timer = setTimeout(() => finish(unfinished(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
1864
1895
|
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1896
|
+
const parse = cameraFrameParser(
|
|
1897
|
+
jpeg => {
|
|
1898
|
+
frames++;
|
|
1899
|
+
if (!buffered) buffered = jpeg;
|
|
1900
|
+
else if (!jpeg.equals(buffered)) finish(null, jpeg);
|
|
1901
|
+
},
|
|
1902
|
+
error => finish(error)
|
|
1868
1903
|
);
|
|
1869
1904
|
|
|
1870
1905
|
const socket = tls.connect(
|
|
1871
1906
|
{
|
|
1872
1907
|
host,
|
|
1873
|
-
port
|
|
1908
|
+
port,
|
|
1874
1909
|
rejectUnauthorized: false,
|
|
1875
1910
|
// The printer uses TLS for confidentiality but presents a self-signed cert.
|
|
1876
1911
|
// Same trust posture as the FTPS path (basic-ftp with rejectUnauthorized: false).
|
|
@@ -1880,49 +1915,9 @@ export class BambuImplementation {
|
|
|
1880
1915
|
}
|
|
1881
1916
|
);
|
|
1882
1917
|
|
|
1883
|
-
socket.on("data",
|
|
1884
|
-
chunks.push(data);
|
|
1885
|
-
totalLen += data.length;
|
|
1886
|
-
|
|
1887
|
-
if (payloadSize === null && totalLen >= FRAME_HEADER_BYTES) {
|
|
1888
|
-
const merged = Buffer.concat(chunks, totalLen);
|
|
1889
|
-
payloadSize = merged.readUInt32LE(0);
|
|
1890
|
-
if (payloadSize <= 0 || payloadSize > 5_000_000) {
|
|
1891
|
-
finish(
|
|
1892
|
-
new Error(
|
|
1893
|
-
`camera_snapshot: unreasonable payload size ${payloadSize} from header; auth likely failed.`
|
|
1894
|
-
)
|
|
1895
|
-
);
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
// Reset chunk list to remaining bytes after the header.
|
|
1899
|
-
const remainder = merged.subarray(FRAME_HEADER_BYTES);
|
|
1900
|
-
chunks.length = 0;
|
|
1901
|
-
chunks.push(remainder);
|
|
1902
|
-
totalLen = remainder.length;
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
if (payloadSize !== null && totalLen >= payloadSize) {
|
|
1906
|
-
const merged = Buffer.concat(chunks, totalLen);
|
|
1907
|
-
const jpeg = merged.subarray(0, payloadSize);
|
|
1908
|
-
if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
|
|
1909
|
-
finish(
|
|
1910
|
-
new Error(
|
|
1911
|
-
`camera_snapshot: payload does not start with JPEG SOI (FF D8); got ${jpeg[0].toString(16)} ${jpeg[1].toString(16)}.`
|
|
1912
|
-
)
|
|
1913
|
-
);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
finish(null, jpeg);
|
|
1917
|
-
}
|
|
1918
|
-
});
|
|
1919
|
-
|
|
1918
|
+
socket.on("data", parse);
|
|
1920
1919
|
socket.on("error", (err) => finish(err));
|
|
1921
|
-
socket.on("end", () =>
|
|
1922
|
-
finish(
|
|
1923
|
-
new Error("camera_snapshot: connection ended before a full JPEG frame arrived")
|
|
1924
|
-
)
|
|
1925
|
-
);
|
|
1920
|
+
socket.on("end", () => finish(unfinished("connection ended")));
|
|
1926
1921
|
});
|
|
1927
1922
|
}
|
|
1928
1923
|
|
|
@@ -138,6 +138,43 @@ function nozzleTypeAndFlow(value: unknown): {type?: string; flow?: string} {
|
|
|
138
138
|
const type = token(value);
|
|
139
139
|
return {type: ["stainlesssteel","hardenedsteel","tungstencarbide","brass"].includes(type) ? type : undefined};
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* A nozzle without an identity chip reports serial "N/A" and keeps the
|
|
143
|
+
* unreliable bit set permanently (stock H2D HS01 nozzles), so its diameter
|
|
144
|
+
* and type are configured settings, as on single-nozzle printers.
|
|
145
|
+
*/
|
|
146
|
+
function hasNoIdentityChip(entry: any): boolean {
|
|
147
|
+
return typeof entry?.sn === "string" && entry.sn.trim().toUpperCase() === "N/A";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Bit 0 (information unreliable) is accepted only from a nozzle without an identity chip. */
|
|
151
|
+
function unreliableWithoutChip(entry: any, stat: number): boolean {
|
|
152
|
+
return (stat & 1) !== 0 && hasNoIdentityChip(entry);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
|
|
156
|
+
export function unchippedNozzles(raw: unknown): number[] {
|
|
157
|
+
const info = (raw as any)?.device?.nozzle?.info;
|
|
158
|
+
if (!Array.isArray(info)) return [];
|
|
159
|
+
return info.flatMap(entry => {
|
|
160
|
+
const index = finiteNumber(entry?.id);
|
|
161
|
+
const stat = finiteNumber(entry?.stat);
|
|
162
|
+
return index !== undefined && Number.isInteger(index) && index >= 0 && index < 16 &&
|
|
163
|
+
stat !== undefined && Number.isInteger(stat) && unreliableWithoutChip(entry, stat) ? [index] : [];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const loggedUnchippedNozzles = new Set<string>();
|
|
168
|
+
/** Name each unchipped nozzle a check accepted once per printer on stderr, so headless runs still record it. */
|
|
169
|
+
function logUnchippedNozzles(raw: Record<string, any>, serial: string): void {
|
|
170
|
+
for (const index of unchippedNozzles(raw)) {
|
|
171
|
+
const key = `${serial}:${index}`;
|
|
172
|
+
if (loggedUnchippedNozzles.has(key)) continue;
|
|
173
|
+
loggedUnchippedNozzles.add(key);
|
|
174
|
+
console.error(`Printer ${serial || "(unknown serial)"} nozzle ${index} reports no identity chip; its diameter and type are the printer's configured settings.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
function reportedNozzles(raw: Record<string,any>): ReportedNozzle[] {
|
|
142
179
|
const info = raw.device?.nozzle?.info;
|
|
143
180
|
const output: ReportedNozzle[] = [];
|
|
@@ -149,8 +186,11 @@ function reportedNozzles(raw: Record<string,any>): ReportedNozzle[] {
|
|
|
149
186
|
const diameter = finiteNumber(entry?.diameter);
|
|
150
187
|
if (index === undefined || !Number.isInteger(index) || index < 0 || diameter === undefined || diameter <= 0) throw new Error("Printer reported malformed nozzle identity or diameter.");
|
|
151
188
|
if (entry.stat !== undefined) {
|
|
189
|
+
// BambuStudio DevNozzle: bit 0 set means the information is unreliable;
|
|
190
|
+
// bits 1-2 are 0 normal, 1 abnormal, 2 unknown.
|
|
152
191
|
const stat = finiteNumber(entry.stat);
|
|
153
|
-
if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat &
|
|
192
|
+
if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 6) !== 0) throw new Error(`Printer nozzle ${index} information is unreliable, abnormal or unknown.`);
|
|
193
|
+
if ((stat & 1) !== 0 && !unreliableWithoutChip(entry, stat)) throw new Error(`Printer nozzle ${index} information is unreliable; refresh the nozzle information on the printer.`);
|
|
154
194
|
}
|
|
155
195
|
if (output.some(nozzle => nozzle.index === index)) throw new Error("Printer reported ambiguous duplicate nozzle indices.");
|
|
156
196
|
output.push({index,diameter,...nozzleTypeAndFlow(entry.type)});
|
|
@@ -398,6 +438,7 @@ export function validatePrinterState(status: any, requirements: PrinterStateRequ
|
|
|
398
438
|
filaments.push({position,material,trayIndex,nozzleMin,nozzleMax});
|
|
399
439
|
}
|
|
400
440
|
}
|
|
441
|
+
if (nozzles.length) logUnchippedNozzles(raw, identity.observedSerial ?? String(status.serial ?? ""));
|
|
401
442
|
return {model,reportedNozzles:nozzles,filaments};
|
|
402
443
|
}
|
|
403
444
|
|
|
@@ -201,6 +201,69 @@ const PROFILE_METADATA_KEYS = new Set([
|
|
|
201
201
|
'compatible_printers', 'compatible_printers_condition', 'compatible_prints', 'compatible_prints_condition',
|
|
202
202
|
]);
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Why the slicer process failed. execFile's message already carries stderr;
|
|
206
|
+
* a crash often leaves stderr empty (FULU OrcaSlicer-bambulab v1.0.0
|
|
207
|
+
* segfaults with no output), so name the signal or exit code and add the end
|
|
208
|
+
* of stdout instead.
|
|
209
|
+
*/
|
|
210
|
+
/** Where a Bambu-style CLI records its error on Linux, and when this run started. */
|
|
211
|
+
type CliResultSource = { resultPath: string; startedAt: number };
|
|
212
|
+
|
|
213
|
+
function describeSlicerFailure(
|
|
214
|
+
error: { message: string; code?: unknown; signal?: unknown },
|
|
215
|
+
stdout: string,
|
|
216
|
+
stderr: string,
|
|
217
|
+
cli?: CliResultSource
|
|
218
|
+
): string {
|
|
219
|
+
const status = typeof error.signal === "string" && error.signal
|
|
220
|
+
? `crashed with ${error.signal}`
|
|
221
|
+
: typeof error.code === "number" ? `exited with code ${error.code}` : "failed";
|
|
222
|
+
const reason = cli ? bambuCliFailureReason(error.code, cli) : undefined;
|
|
223
|
+
const output = stderr.trim() ? "" : stdout.trim().split("\n").slice(-20).join("\n");
|
|
224
|
+
return `Slicer failed: the slicer ${status}${reason ? ` (${reason})` : ""}. ${error.message.trim()}${output ? `\nSlicer output: ${output}` : ""}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
|
|
228
|
+
// exits with the low byte of the code; on Linux result.json names the error.
|
|
229
|
+
const BAMBU_CLI_ERRORS: Record<number, string> = {
|
|
230
|
+
[-2]: "invalid parameters to the slicer",
|
|
231
|
+
[-3]: "input files not found",
|
|
232
|
+
[-5]: "preset file cannot be parsed",
|
|
233
|
+
[-6]: "model file cannot be parsed",
|
|
234
|
+
[-9]: "copying objects failed",
|
|
235
|
+
[-17]: "printer is not compatible with the process preset",
|
|
236
|
+
[-21]: "auto-arranging objects failed",
|
|
237
|
+
[-22]: "auto-orienting objects failed",
|
|
238
|
+
[-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
|
|
239
|
+
[-51]: "incorrect slicing parameters",
|
|
240
|
+
[-52]: "objects extend over the bed boundary",
|
|
241
|
+
[-60]: "no printable objects remain after skipping",
|
|
242
|
+
[-61]: "filaments are not compatible with the plate type",
|
|
243
|
+
[-62]: "the filaments' temperatures differ too much",
|
|
244
|
+
[-63]: "objects collide in print-by-object mode",
|
|
245
|
+
[-64]: "objects collide",
|
|
246
|
+
[-100]: "slicing the model failed",
|
|
247
|
+
[-101]: "G-code conflicts after slicing",
|
|
248
|
+
[-102]: "G-code in an unprintable area",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
function bambuCliFailureReason(exitCode: unknown, cli: CliResultSource): string | undefined {
|
|
252
|
+
try {
|
|
253
|
+
if (fs.statSync(cli.resultPath).mtimeMs >= cli.startedAt) {
|
|
254
|
+
const result = JSON.parse(fs.readFileSync(cli.resultPath, "utf8"));
|
|
255
|
+
if (typeof result.error_string === "string" && result.error_string.trim()) {
|
|
256
|
+
return `${result.error_string.trim()} [${result.return_code}]`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// result.json is written only on Linux and only for handled errors.
|
|
261
|
+
}
|
|
262
|
+
if (typeof exitCode !== "number") return undefined;
|
|
263
|
+
const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find(value => (value & 0xff) === exitCode);
|
|
264
|
+
return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
|
|
265
|
+
}
|
|
266
|
+
|
|
204
267
|
export class STLManipulator extends EventEmitter {
|
|
205
268
|
private tempDir: string;
|
|
206
269
|
private activeOperations: Map<string, boolean> = new Map();
|
|
@@ -978,6 +1041,29 @@ export class STLManipulator extends EventEmitter {
|
|
|
978
1041
|
}
|
|
979
1042
|
}
|
|
980
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
|
|
1046
|
+
* CLI would, about the model's centre. Orca adds each --rotate* to the
|
|
1047
|
+
* object's Euler angles (ModelVolume::rotate), so the result rotates about X,
|
|
1048
|
+
* then Y, then Z whatever the argument order, after scaling. The slicer then
|
|
1049
|
+
* places the result on the plate.
|
|
1050
|
+
*/
|
|
1051
|
+
private async transformMeshForCli(stlFilePath: string, options: BambuSliceOptions): Promise<string> {
|
|
1052
|
+
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
1053
|
+
const center = new THREE.Vector3();
|
|
1054
|
+
boundingBox.getCenter(center);
|
|
1055
|
+
const radians = (degrees: number) => degrees * Math.PI / 180;
|
|
1056
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
|
|
1057
|
+
if (options.scale !== undefined) geometry.applyMatrix4(new THREE.Matrix4().makeScale(options.scale, options.scale, options.scale));
|
|
1058
|
+
if (options.rotateX) geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(radians(options.rotateX)));
|
|
1059
|
+
if (options.rotateY) geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(radians(options.rotateY)));
|
|
1060
|
+
if (options.rotate) geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(radians(options.rotate)));
|
|
1061
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(center.x, center.y, center.z));
|
|
1062
|
+
const outputFilePath = path.join(this.tempDir, `${path.basename(stlFilePath, path.extname(stlFilePath))}_transformed_${this.generateOperationId()}.stl`);
|
|
1063
|
+
await this.saveSTL(geometry, outputFilePath);
|
|
1064
|
+
return outputFilePath;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
981
1067
|
/**
|
|
982
1068
|
* Rotate an STL model around specific axes
|
|
983
1069
|
*/
|
|
@@ -1610,7 +1696,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
1610
1696
|
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
1611
1697
|
|
|
1612
1698
|
let args: string[] = [];
|
|
1699
|
+
let cliResult: CliResultSource | undefined;
|
|
1613
1700
|
const orcaTemporaryDirectories: string[] = [];
|
|
1701
|
+
const temporaryFiles: string[] = [];
|
|
1614
1702
|
|
|
1615
1703
|
try {
|
|
1616
1704
|
if (progressCallback) progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
@@ -1724,6 +1812,28 @@ export class STLManipulator extends EventEmitter {
|
|
|
1724
1812
|
});
|
|
1725
1813
|
break;
|
|
1726
1814
|
}
|
|
1815
|
+
// OrcaSlicer's CLI crashes (SIGSEGV in PartPlate::check_outside,
|
|
1816
|
+
// which needs the GUI's Plater) when --scale or --rotate-x/-y move an
|
|
1817
|
+
// object below the bed and it arranges, the default for STL; it also
|
|
1818
|
+
// rejects --repetitions for one STL. Apply STL transforms to the mesh
|
|
1819
|
+
// in the CLI's order instead, and refuse the 3MF case it cannot fix.
|
|
1820
|
+
const orcaCli = slicerType === 'orcaslicer' || slicerType === 'orcaslicer-bambulab';
|
|
1821
|
+
const tilts = !!bambuOptions?.rotateX || !!bambuOptions?.rotateY;
|
|
1822
|
+
const meshTransform = orcaCli && !is3mf && (bambuOptions?.scale !== undefined || tilts);
|
|
1823
|
+
if (orcaCli && is3mf && bambuOptions?.arrange === true && ((bambuOptions.scale ?? 1) > 1 || tilts)) {
|
|
1824
|
+
throw new Error(
|
|
1825
|
+
"OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
|
|
1826
|
+
"Set arrange to false, or apply the transform in the slicer and export the project."
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
if (orcaCli && !is3mf && bambuOptions?.repetitions !== undefined && bambuOptions.cloneObjects) {
|
|
1830
|
+
throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
|
|
1831
|
+
}
|
|
1832
|
+
const inputPath = meshTransform ? await this.transformMeshForCli(stlFilePath, bambuOptions!) : stlFilePath;
|
|
1833
|
+
if (inputPath !== stlFilePath) temporaryFiles.push(inputPath);
|
|
1834
|
+
cliResult = { resultPath: path.join(outputDir, 'result.json'), startedAt: Date.now() };
|
|
1835
|
+
// A result.json left by an earlier run must not explain this one.
|
|
1836
|
+
await fs.promises.rm(cliResult.resultPath, { force: true });
|
|
1727
1837
|
args = [
|
|
1728
1838
|
...orcaExtraArgs,
|
|
1729
1839
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
@@ -1745,11 +1855,17 @@ export class STLManipulator extends EventEmitter {
|
|
|
1745
1855
|
if (bambuOptions?.skipModifiedGcodes) args.push('--skip-modified-gcodes');
|
|
1746
1856
|
if (bambuOptions?.orient !== undefined) args.push('--orient', bambuOptions.orient ? '1' : '0');
|
|
1747
1857
|
if (bambuOptions?.arrange !== undefined) args.push('--arrange', bambuOptions.arrange ? '1' : '0');
|
|
1748
|
-
if (bambuOptions?.repetitions !== undefined)
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1858
|
+
if (bambuOptions?.repetitions !== undefined) {
|
|
1859
|
+
// OrcaSlicer counts copies of a single loaded STL with --clone-objects.
|
|
1860
|
+
if (orcaCli && !is3mf) args.push('--clone-objects', String(bambuOptions.repetitions));
|
|
1861
|
+
else args.push('--repetitions', String(bambuOptions.repetitions));
|
|
1862
|
+
}
|
|
1863
|
+
if (!meshTransform) {
|
|
1864
|
+
if (bambuOptions?.scale !== undefined) args.push('--scale', String(bambuOptions.scale));
|
|
1865
|
+
if (bambuOptions?.rotate !== undefined) args.push('--rotate', String(bambuOptions.rotate));
|
|
1866
|
+
if (bambuOptions?.rotateX !== undefined) args.push('--rotate-x', String(bambuOptions.rotateX));
|
|
1867
|
+
if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
|
|
1868
|
+
}
|
|
1753
1869
|
if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
|
|
1754
1870
|
if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
|
|
1755
1871
|
if (cliBundle.filamentPaths.length > 0) {
|
|
@@ -1758,7 +1874,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1758
1874
|
}
|
|
1759
1875
|
if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1760
1876
|
|
|
1761
|
-
args.push(
|
|
1877
|
+
args.push(inputPath);
|
|
1762
1878
|
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
1763
1879
|
outputFilePath = bambuOutputPath;
|
|
1764
1880
|
}
|
|
@@ -1778,7 +1894,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1778
1894
|
if (error) {
|
|
1779
1895
|
console.error(`Slicer Error: ${error.message}`);
|
|
1780
1896
|
console.error(`Slicer Stderr: ${stderr}`);
|
|
1781
|
-
reject(new Error(
|
|
1897
|
+
reject(new Error(describeSlicerFailure(error, stdout, stderr, cliResult)));
|
|
1782
1898
|
} else {
|
|
1783
1899
|
console.error(`Slicer Stdout: ${stdout}`);
|
|
1784
1900
|
if (stderr) {
|
|
@@ -1841,6 +1957,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
1841
1957
|
throw error;
|
|
1842
1958
|
} finally {
|
|
1843
1959
|
this.activeOperations.delete(operationId);
|
|
1960
|
+
for (const file of temporaryFiles) {
|
|
1961
|
+
await fs.promises.rm(file, { force: true }).catch(error => console.error('Could not remove a temporary mesh:', error));
|
|
1962
|
+
}
|
|
1844
1963
|
for (const directory of orcaTemporaryDirectories.reverse()) {
|
|
1845
1964
|
await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
|
|
1846
1965
|
console.error('Could not remove temporary Orca profiles:', error);
|