mcp-3d-printer-server 1.2.24 → 1.2.26
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/CHANGELOG.md +71 -0
- package/CONTRIBUTORS.md +5 -1
- package/README.md +64 -29
- package/dist/ams-mapping.d.ts +38 -0
- package/dist/ams-mapping.js +125 -0
- package/dist/bambu-network-bridge.d.ts +200 -0
- package/dist/bambu-network-bridge.js +881 -0
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/control-reply.d.ts +21 -0
- package/dist/control-reply.js +44 -0
- package/dist/counterpart/camera-sources.d.ts +16 -0
- package/dist/counterpart/camera-sources.js +138 -0
- package/dist/counterpart/camera-stream.d.ts +66 -0
- package/dist/counterpart/camera-stream.js +246 -0
- package/dist/counterpart/camera-tools.d.ts +247 -0
- package/dist/counterpart/camera-tools.js +35 -0
- package/dist/counterpart/contract.d.ts +21 -5
- package/dist/counterpart/contract.js +8 -2
- package/dist/counterpart/route.d.ts +31 -0
- package/dist/counterpart/route.js +70 -0
- package/dist/counterpart/service.d.ts +60 -4
- package/dist/counterpart/service.js +142 -16
- package/dist/design-guidance.d.ts +26 -0
- package/dist/design-guidance.js +177 -0
- package/dist/fulu-orca.d.ts +7 -0
- package/dist/fulu-orca.js +18 -2
- package/dist/index.js +528 -72
- package/dist/printers/bambu-filaments.d.ts +68 -0
- package/dist/printers/bambu-filaments.js +262 -0
- package/dist/printers/bambu.d.ts +99 -6
- package/dist/printers/bambu.js +478 -72
- package/dist/printers/creality.d.ts +2 -1
- package/dist/printers/creality.js +5 -1
- package/dist/printers/generic-base.d.ts +11 -6
- package/dist/printers/generic-base.js +21 -25
- package/dist/printers/klipper.d.ts +2 -1
- package/dist/printers/klipper.js +10 -7
- package/dist/printers/octoprint.d.ts +2 -1
- package/dist/printers/octoprint.js +15 -8
- package/dist/printers/printer-factory.d.ts +1 -1
- package/dist/printers/printer-factory.js +3 -3
- package/dist/printers/prusa.js +0 -1
- package/dist/safety/confirmation.d.ts +11 -16
- package/dist/safety/confirmation.js +8 -32
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +8 -2
- package/dist/safety/limits.js +14 -5
- package/dist/safety/print-file.d.ts +5 -0
- package/dist/safety/print-file.js +156 -19
- package/dist/safety/printer-state.d.ts +59 -8
- package/dist/safety/printer-state.js +174 -39
- package/dist/slicer/bambu-cli.d.ts +6 -2
- package/dist/slicer/bambu-cli.js +19 -12
- package/dist/slicer/layer-pauses.d.ts +47 -0
- package/dist/slicer/layer-pauses.js +130 -0
- package/dist/slicer/slicer-error.d.ts +11 -1
- package/dist/slicer/slicer-error.js +16 -2
- package/dist/stl/stl-manipulator.d.ts +27 -1
- package/dist/stl/stl-manipulator.js +216 -76
- package/dist/studio-print-options.d.ts +57 -0
- package/dist/studio-print-options.js +179 -0
- package/package.json +25 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1075 -0
- package/src/bambu-network-certs.ts +154 -0
- package/src/control-reply.ts +41 -0
- package/src/counterpart/camera-sources.ts +102 -0
- package/src/counterpart/camera-stream.ts +195 -0
- package/src/counterpart/camera-tools.ts +35 -0
- package/src/counterpart/contract.ts +12 -4
- package/src/counterpart/route.ts +82 -0
- package/src/counterpart/service.ts +150 -16
- package/src/design-guidance.ts +188 -0
- package/src/fulu-orca.ts +18 -2
- package/src/index.ts +546 -87
- package/src/printers/bambu-filaments.ts +346 -0
- package/src/printers/bambu.ts +506 -81
- package/src/printers/creality.ts +8 -3
- package/src/printers/generic-base.ts +33 -40
- package/src/printers/klipper.ts +12 -8
- package/src/printers/octoprint.ts +16 -9
- package/src/printers/printer-factory.ts +3 -3
- package/src/printers/prusa.ts +0 -1
- package/src/safety/confirmation.ts +15 -38
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +15 -5
- package/src/safety/print-file.ts +139 -17
- package/src/safety/printer-state.ts +179 -40
- package/src/slicer/bambu-cli.ts +22 -13
- package/src/slicer/layer-pauses.ts +143 -0
- package/src/slicer/slicer-error.ts +21 -2
- package/src/stl/stl-manipulator.ts +254 -84
- package/src/studio-print-options.ts +201 -0
package/src/slicer/bambu-cli.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface BambuSliceOptions {
|
|
|
49
49
|
minSave?: boolean; // --min-save
|
|
50
50
|
skipModifiedGcodes?: boolean;// --skip-modified-gcodes
|
|
51
51
|
slicePlate?: number; // --slice N (0 = all plates)
|
|
52
|
+
pauseAtLayers?: number[]; // pause before each 1-based layer, through the slicer's own custom G-code
|
|
53
|
+
customGcodesFile?: string; // internal --load-custom-gcodes file, prepared by sliceSTLWithResult
|
|
52
54
|
/** Process keys written over the resolved process preset (layer_height, wall_loops, sparse_infill_density). */
|
|
53
55
|
processOverrides?: Record<string, string>;
|
|
54
56
|
}
|
|
@@ -597,6 +599,9 @@ export interface BambuCliInput {
|
|
|
597
599
|
*/
|
|
598
600
|
export function planBambuCliInput(slicerType: BambuCliSlicerType, is3mf: boolean, options: BambuSliceOptions): BambuCliInput {
|
|
599
601
|
const input = { slicerType, is3mf, meshTransformed: false };
|
|
602
|
+
if (!is3mf && options.repetitions !== undefined && options.cloneObjects) {
|
|
603
|
+
throw new Error("Use either repetitions or clone_objects for an STL, not both.");
|
|
604
|
+
}
|
|
600
605
|
if (slicerType === "bambustudio") return input;
|
|
601
606
|
const tilts = !!options.rotateX || !!options.rotateY;
|
|
602
607
|
if (is3mf) {
|
|
@@ -608,9 +613,6 @@ export function planBambuCliInput(slicerType: BambuCliSlicerType, is3mf: boolean
|
|
|
608
613
|
}
|
|
609
614
|
return input;
|
|
610
615
|
}
|
|
611
|
-
if (options.repetitions !== undefined && options.cloneObjects) {
|
|
612
|
-
throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
|
|
613
|
-
}
|
|
614
616
|
return { ...input, meshTransformed: options.scale !== undefined || tilts };
|
|
615
617
|
}
|
|
616
618
|
|
|
@@ -641,17 +643,23 @@ const BAMBU_CLI_ERRORS: Record<number, string> = {
|
|
|
641
643
|
};
|
|
642
644
|
|
|
643
645
|
/**
|
|
644
|
-
* The
|
|
645
|
-
*
|
|
646
|
+
* The result.json in the run's fresh output directory, parsed verbatim. Bambu
|
|
647
|
+
* Studio 02.08.02.61 writes it on macOS for successes and failures alike.
|
|
646
648
|
*/
|
|
647
|
-
export function
|
|
649
|
+
export function readCliResult(outputDir: string): Record<string, unknown> | undefined {
|
|
648
650
|
try {
|
|
649
|
-
const result = JSON.parse(fs.readFileSync(path.join(outputDir, "result.json"), "utf8"));
|
|
650
|
-
|
|
651
|
-
return `${result.error_string.trim()}${result.return_code !== undefined ? ` [${result.return_code}]` : ""}`;
|
|
652
|
-
}
|
|
651
|
+
const result: unknown = JSON.parse(fs.readFileSync(path.join(outputDir, "result.json"), "utf8"));
|
|
652
|
+
return result && typeof result === "object" && !Array.isArray(result) ? result as Record<string, unknown> : undefined;
|
|
653
653
|
} catch {
|
|
654
|
-
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** The CLI's own reason for a failed run: its result.json, else the exit-code table. */
|
|
659
|
+
export function bambuCliFailureReason(exitCode: number | null, outputDir: string): string | undefined {
|
|
660
|
+
const result = readCliResult(outputDir);
|
|
661
|
+
if (typeof result?.error_string === "string" && result.error_string.trim()) {
|
|
662
|
+
return `${result.error_string.trim()}${result.return_code !== undefined ? ` [${result.return_code}]` : ""}`;
|
|
655
663
|
}
|
|
656
664
|
if (exitCode === null) return undefined;
|
|
657
665
|
const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find((value) => value === exitCode || (value & 0xff) === exitCode);
|
|
@@ -683,8 +691,8 @@ export function buildBambuCliArgs(
|
|
|
683
691
|
if (options.orient !== undefined) args.push("--orient", options.orient ? "1" : "0");
|
|
684
692
|
if (options.arrange !== undefined) args.push("--arrange", options.arrange ? "1" : "0");
|
|
685
693
|
if (options.repetitions !== undefined) {
|
|
686
|
-
//
|
|
687
|
-
if (
|
|
694
|
+
// Both Bambu Studio and OrcaSlicer copy standalone STL objects with --clone-objects.
|
|
695
|
+
if (!input.is3mf) args.push("--clone-objects", String(options.repetitions));
|
|
688
696
|
else args.push("--repetitions", String(options.repetitions));
|
|
689
697
|
}
|
|
690
698
|
if (!input.meshTransformed) {
|
|
@@ -700,6 +708,7 @@ export function buildBambuCliArgs(
|
|
|
700
708
|
args.push("--load-defaultfila");
|
|
701
709
|
}
|
|
702
710
|
if (options.loadFilamentIds) args.push("--load-filament-ids", options.loadFilamentIds);
|
|
711
|
+
if (options.customGcodesFile) args.push("--load-custom-gcodes", path.resolve(options.customGcodesFile));
|
|
703
712
|
return args;
|
|
704
713
|
}
|
|
705
714
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import JSZip from "jszip";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pause-at-layer for Bambu-compatible slicer CLIs ("Add pause" in Bambu Studio
|
|
6
|
+
* and OrcaSlicer). The slicer writes the machine's own machine_pause_gcode
|
|
7
|
+
* (M400 U1 on Bambu printers) before the chosen layer, so the printer pauses
|
|
8
|
+
* by itself there and waits for Resume; no remote command is involved.
|
|
9
|
+
*
|
|
10
|
+
* The CLI takes these as --load-custom-gcodes <json> (CustomGCode::Info). It
|
|
11
|
+
* does not apply the pauses a project 3MF already carries in
|
|
12
|
+
* Metadata/custom_gcode_per_print_z.xml, so those are read here and passed the
|
|
13
|
+
* same way. A pause's print_z must be the exact top of its layer: a height
|
|
14
|
+
* between layers lands on the layer below.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** CustomGCode::Type, in the order bbs_3mf.cpp writes it as an integer. */
|
|
18
|
+
const CUSTOM_GCODE_TYPES = ["ColorChange", "PausePrint", "ToolChange", "Template", "Custom", "Unknown"] as const;
|
|
19
|
+
export type CustomGcodeType = (typeof CUSTOM_GCODE_TYPES)[number];
|
|
20
|
+
|
|
21
|
+
export interface CustomGcodeItem {
|
|
22
|
+
type: CustomGcodeType;
|
|
23
|
+
print_z: number;
|
|
24
|
+
color: string;
|
|
25
|
+
extruder: number;
|
|
26
|
+
extra: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CustomGcodeInfo {
|
|
30
|
+
mode: string;
|
|
31
|
+
gcodes: CustomGcodeItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 1-based layer numbers, as Bambu Studio's and OrcaSlicer's preview slider count them. */
|
|
35
|
+
export function parsePauseLayers(value: unknown): number[] | undefined {
|
|
36
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
37
|
+
const raw = Array.isArray(value) ? value : String(value).split(/[\s,;]+/).filter(Boolean);
|
|
38
|
+
const layers = raw.map(entry => Number(entry));
|
|
39
|
+
for (const layer of layers) {
|
|
40
|
+
if (!Number.isInteger(layer) || layer < 2) {
|
|
41
|
+
throw new Error(`pause_at_layers takes whole layer numbers of 2 or more (the printer cannot pause before layer 1), not ${JSON.stringify(value)}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const unique = [...new Set(layers)].sort((a, b) => a - b);
|
|
45
|
+
return unique.length ? unique : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function xmlAttr(tag: string, name: string): string | undefined {
|
|
49
|
+
const match = new RegExp(`\\b${name}="([^"]*)"`).exec(tag);
|
|
50
|
+
return match ? match[1].replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&") : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The custom G-codes (pauses, colour changes, ...) a project 3MF stores for
|
|
55
|
+
* one plate (1-based), as Bambu Studio and OrcaSlicer save them.
|
|
56
|
+
*/
|
|
57
|
+
export async function readProjectCustomGcodes(threeMfPath: string, plate = 1): Promise<CustomGcodeInfo | undefined> {
|
|
58
|
+
let zip: JSZip;
|
|
59
|
+
try {
|
|
60
|
+
zip = await JSZip.loadAsync(await fs.promises.readFile(threeMfPath));
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const file = zip.file(/^Metadata\/custom_gcode_per_print_z\.xml$/i)[0];
|
|
65
|
+
if (!file) return undefined;
|
|
66
|
+
const xml = await file.async("string");
|
|
67
|
+
for (const block of xml.match(/<plate>[\s\S]*?<\/plate>/g) ?? []) {
|
|
68
|
+
const info = /<plate_info\b[^>]*>/.exec(block)?.[0];
|
|
69
|
+
if (Number(info ? xmlAttr(info, "id") : NaN) !== plate) continue;
|
|
70
|
+
const gcodes: CustomGcodeItem[] = [];
|
|
71
|
+
for (const tag of block.match(/<layer\b[^>]*>/g) ?? []) {
|
|
72
|
+
const type = CUSTOM_GCODE_TYPES[Number(xmlAttr(tag, "type"))] ?? "Unknown";
|
|
73
|
+
const printZ = Number(xmlAttr(tag, "top_z"));
|
|
74
|
+
if (!Number.isFinite(printZ)) continue;
|
|
75
|
+
gcodes.push({
|
|
76
|
+
type,
|
|
77
|
+
print_z: printZ,
|
|
78
|
+
color: xmlAttr(tag, "color") ?? "",
|
|
79
|
+
extruder: Number(xmlAttr(tag, "extruder") ?? 1) || 1,
|
|
80
|
+
// Template and Custom keep their G-code text in "extra" (CustomGCode.hpp).
|
|
81
|
+
extra: xmlAttr(tag, "extra") ?? "",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const modeTag = /<mode\b[^>]*>/.exec(block)?.[0];
|
|
85
|
+
return { mode: (modeTag && xmlAttr(modeTag, "value")) || "SingleExtruder", gcodes };
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The plate's G-code text inside a sliced 3MF. */
|
|
91
|
+
export async function readPlateGcode(threeMfPath: string, plate = 1): Promise<string | undefined> {
|
|
92
|
+
const zip = await JSZip.loadAsync(await fs.promises.readFile(threeMfPath));
|
|
93
|
+
const exact = zip.file(`Metadata/plate_${plate}.gcode`);
|
|
94
|
+
const file = exact ?? zip.file(/^Metadata\/plate_\d+\.gcode$/)[0];
|
|
95
|
+
return file ? file.async("string") : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Each layer's top Z in order, from the slicer's "; Z_HEIGHT:" markers. */
|
|
99
|
+
export function layerTops(gcode: string): number[] {
|
|
100
|
+
const tops: number[] = [];
|
|
101
|
+
for (const match of gcode.matchAll(/^; Z_HEIGHT: ([0-9.]+)\s*$/gm)) tops.push(Number(match[1]));
|
|
102
|
+
return tops;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The 1-based layers whose block holds a pause: the slicer's "; PAUSE_PRINTING"
|
|
107
|
+
* marker or the machine's pause G-code on its own line.
|
|
108
|
+
*/
|
|
109
|
+
export function pausedLayers(gcode: string, pauseGcode = "M400 U1"): number[] {
|
|
110
|
+
const lines = gcode.split(/\r?\n/);
|
|
111
|
+
const paused = new Set<number>();
|
|
112
|
+
let layer = 0;
|
|
113
|
+
const pauseLine = pauseGcode.trim();
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (line.startsWith("; Z_HEIGHT:")) { layer += 1; continue; }
|
|
116
|
+
if (layer === 0) continue;
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
if (trimmed === "; PAUSE_PRINTING" || (pauseLine && trimmed === pauseLine)) paused.add(layer);
|
|
119
|
+
}
|
|
120
|
+
return [...paused].sort((a, b) => a - b);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The machine_pause_gcode a sliced file was made with (its config block), else Bambu's M400 U1. */
|
|
124
|
+
export function machinePauseGcode(gcode: string): string {
|
|
125
|
+
const match = /^; machine_pause_gcode = (.*)$/m.exec(gcode);
|
|
126
|
+
const value = match?.[1]?.split("\\n")[0]?.trim();
|
|
127
|
+
return value || "M400 U1";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Project custom G-codes plus a pause before each requested layer, at that layer's exact top. */
|
|
131
|
+
export function customGcodesWithPauses(tops: number[], layers: number[], project?: CustomGcodeInfo): CustomGcodeInfo {
|
|
132
|
+
const total = tops.length;
|
|
133
|
+
const beyond = layers.filter(layer => layer > total);
|
|
134
|
+
if (beyond.length) {
|
|
135
|
+
throw new Error(`This print has ${total} layers, so it cannot pause before layer ${beyond.join(", ")}.`);
|
|
136
|
+
}
|
|
137
|
+
const pauses: CustomGcodeItem[] = layers.map(layer => ({ type: "PausePrint", print_z: tops[layer - 1], color: "", extruder: 1, extra: "" }));
|
|
138
|
+
const kept = (project?.gcodes ?? []).filter(item => !(item.type === "PausePrint" && pauses.some(p => Math.abs(p.print_z - item.print_z) < 1e-4)));
|
|
139
|
+
return {
|
|
140
|
+
mode: project?.mode || "SingleExtruder",
|
|
141
|
+
gcodes: [...kept, ...pauses].sort((a, b) => a.print_z - b.print_z),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -41,16 +41,35 @@ export function slicerSuggestion(phase: SlicerFailurePhase, slicerType?: string)
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** Bambu Studio's or OrcaSlicer's own result.json, parsed verbatim. */
|
|
45
|
+
export type SlicerResult = Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The slicer's own words: its error_string when it reports a failing
|
|
49
|
+
* return_code (not "Success."), then each plate's warning_message.
|
|
50
|
+
*/
|
|
51
|
+
export function slicerResultText(result: SlicerResult): string {
|
|
52
|
+
const plates = Array.isArray(result.sliced_plates) ? result.sliced_plates : [];
|
|
53
|
+
return [
|
|
54
|
+
result.return_code === 0 ? undefined : result.error_string,
|
|
55
|
+
...plates.map((plate) => plate && typeof plate === "object" ? (plate as Record<string, unknown>).warning_message : undefined),
|
|
56
|
+
].filter((text): text is string => typeof text === "string" && text.trim() !== "").map((text) => text.trim()).join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
export class SlicerError extends Error {
|
|
45
60
|
readonly phase: SlicerFailurePhase;
|
|
46
61
|
readonly suggestion: string;
|
|
47
62
|
readonly details: SlicerFailureDetails;
|
|
63
|
+
/** The slicer's own words from `slicerResult`; the message starts with them. */
|
|
64
|
+
readonly slicerText: string;
|
|
48
65
|
|
|
49
|
-
constructor(phase: SlicerFailurePhase, message: string, details: SlicerFailureDetails = {}) {
|
|
50
|
-
|
|
66
|
+
constructor(phase: SlicerFailurePhase, message: string, details: SlicerFailureDetails = {}, readonly slicerResult?: SlicerResult) {
|
|
67
|
+
const slicerText = slicerResult ? slicerResultText(slicerResult) : "";
|
|
68
|
+
super(slicerText ? `${slicerText}\n${message}` : message);
|
|
51
69
|
this.name = "SlicerError";
|
|
52
70
|
this.phase = phase;
|
|
53
71
|
this.details = details;
|
|
72
|
+
this.slicerText = slicerText;
|
|
54
73
|
this.suggestion = slicerSuggestion(phase, details.slicerType);
|
|
55
74
|
}
|
|
56
75
|
}
|