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.
Files changed (96) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/CONTRIBUTORS.md +5 -1
  3. package/README.md +64 -29
  4. package/dist/ams-mapping.d.ts +38 -0
  5. package/dist/ams-mapping.js +125 -0
  6. package/dist/bambu-network-bridge.d.ts +200 -0
  7. package/dist/bambu-network-bridge.js +881 -0
  8. package/dist/bambu-network-certs.d.ts +13 -0
  9. package/dist/bambu-network-certs.js +152 -0
  10. package/dist/control-reply.d.ts +21 -0
  11. package/dist/control-reply.js +44 -0
  12. package/dist/counterpart/camera-sources.d.ts +16 -0
  13. package/dist/counterpart/camera-sources.js +138 -0
  14. package/dist/counterpart/camera-stream.d.ts +66 -0
  15. package/dist/counterpart/camera-stream.js +246 -0
  16. package/dist/counterpart/camera-tools.d.ts +247 -0
  17. package/dist/counterpart/camera-tools.js +35 -0
  18. package/dist/counterpart/contract.d.ts +21 -5
  19. package/dist/counterpart/contract.js +8 -2
  20. package/dist/counterpart/route.d.ts +31 -0
  21. package/dist/counterpart/route.js +70 -0
  22. package/dist/counterpart/service.d.ts +60 -4
  23. package/dist/counterpart/service.js +142 -16
  24. package/dist/design-guidance.d.ts +26 -0
  25. package/dist/design-guidance.js +177 -0
  26. package/dist/fulu-orca.d.ts +7 -0
  27. package/dist/fulu-orca.js +18 -2
  28. package/dist/index.js +528 -72
  29. package/dist/printers/bambu-filaments.d.ts +68 -0
  30. package/dist/printers/bambu-filaments.js +262 -0
  31. package/dist/printers/bambu.d.ts +99 -6
  32. package/dist/printers/bambu.js +478 -72
  33. package/dist/printers/creality.d.ts +2 -1
  34. package/dist/printers/creality.js +5 -1
  35. package/dist/printers/generic-base.d.ts +11 -6
  36. package/dist/printers/generic-base.js +21 -25
  37. package/dist/printers/klipper.d.ts +2 -1
  38. package/dist/printers/klipper.js +10 -7
  39. package/dist/printers/octoprint.d.ts +2 -1
  40. package/dist/printers/octoprint.js +15 -8
  41. package/dist/printers/printer-factory.d.ts +1 -1
  42. package/dist/printers/printer-factory.js +3 -3
  43. package/dist/printers/prusa.js +0 -1
  44. package/dist/safety/confirmation.d.ts +11 -16
  45. package/dist/safety/confirmation.js +8 -32
  46. package/dist/safety/hidden-hms.d.ts +32 -0
  47. package/dist/safety/hidden-hms.js +405 -0
  48. package/dist/safety/limits.d.ts +8 -2
  49. package/dist/safety/limits.js +14 -5
  50. package/dist/safety/print-file.d.ts +5 -0
  51. package/dist/safety/print-file.js +156 -19
  52. package/dist/safety/printer-state.d.ts +59 -8
  53. package/dist/safety/printer-state.js +174 -39
  54. package/dist/slicer/bambu-cli.d.ts +6 -2
  55. package/dist/slicer/bambu-cli.js +19 -12
  56. package/dist/slicer/layer-pauses.d.ts +47 -0
  57. package/dist/slicer/layer-pauses.js +130 -0
  58. package/dist/slicer/slicer-error.d.ts +11 -1
  59. package/dist/slicer/slicer-error.js +16 -2
  60. package/dist/stl/stl-manipulator.d.ts +27 -1
  61. package/dist/stl/stl-manipulator.js +216 -76
  62. package/dist/studio-print-options.d.ts +57 -0
  63. package/dist/studio-print-options.js +179 -0
  64. package/package.json +25 -4
  65. package/server.json +2 -2
  66. package/src/ams-mapping.ts +132 -0
  67. package/src/bambu-network-bridge.ts +1075 -0
  68. package/src/bambu-network-certs.ts +154 -0
  69. package/src/control-reply.ts +41 -0
  70. package/src/counterpart/camera-sources.ts +102 -0
  71. package/src/counterpart/camera-stream.ts +195 -0
  72. package/src/counterpart/camera-tools.ts +35 -0
  73. package/src/counterpart/contract.ts +12 -4
  74. package/src/counterpart/route.ts +82 -0
  75. package/src/counterpart/service.ts +150 -16
  76. package/src/design-guidance.ts +188 -0
  77. package/src/fulu-orca.ts +18 -2
  78. package/src/index.ts +546 -87
  79. package/src/printers/bambu-filaments.ts +346 -0
  80. package/src/printers/bambu.ts +506 -81
  81. package/src/printers/creality.ts +8 -3
  82. package/src/printers/generic-base.ts +33 -40
  83. package/src/printers/klipper.ts +12 -8
  84. package/src/printers/octoprint.ts +16 -9
  85. package/src/printers/printer-factory.ts +3 -3
  86. package/src/printers/prusa.ts +0 -1
  87. package/src/safety/confirmation.ts +15 -38
  88. package/src/safety/hidden-hms.ts +406 -0
  89. package/src/safety/limits.ts +15 -5
  90. package/src/safety/print-file.ts +139 -17
  91. package/src/safety/printer-state.ts +179 -40
  92. package/src/slicer/bambu-cli.ts +22 -13
  93. package/src/slicer/layer-pauses.ts +143 -0
  94. package/src/slicer/slicer-error.ts +21 -2
  95. package/src/stl/stl-manipulator.ts +254 -84
  96. package/src/studio-print-options.ts +201 -0
@@ -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 CLI's own reason for a failed run: result.json in the run's fresh output
645
- * directory (written on Linux for handled errors), else the exit-code table.
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 bambuCliFailureReason(exitCode: number | null, outputDir: string): string | undefined {
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
- if (typeof result?.error_string === "string" && result.error_string.trim()) {
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
- // result.json is written only on Linux and only for handled errors.
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
- // OrcaSlicer rejects --repetitions for one STL; it copies the loaded model with --clone-objects.
687
- if (input.slicerType !== "bambustudio" && !input.is3mf) args.push("--clone-objects", String(options.repetitions));
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(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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
- super(message);
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
  }