mcp-3d-printer-server 1.2.9 → 1.2.12
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 +222 -0
- package/CONTRIBUTORS.md +56 -0
- package/README.md +528 -811
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +1 -0
- package/dist/blender-mcp-bridge.js +209 -21
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +614 -110
- package/dist/printers/bambu-node-guard.d.ts +2 -0
- package/dist/printers/bambu-node-guard.js +38 -0
- package/dist/printers/bambu.d.ts +53 -7
- package/dist/printers/bambu.js +420 -126
- package/dist/printers/creality.d.ts +15 -6
- package/dist/printers/creality.js +42 -9
- package/dist/printers/duet.d.ts +11 -6
- package/dist/printers/duet.js +57 -34
- package/dist/printers/generic-base.d.ts +69 -0
- package/dist/printers/generic-base.js +209 -0
- package/dist/printers/klipper.d.ts +17 -6
- package/dist/printers/klipper.js +63 -16
- package/dist/printers/octoprint.d.ts +18 -6
- package/dist/printers/octoprint.js +64 -9
- package/dist/printers/printer-factory.d.ts +11 -2
- package/dist/printers/printer-factory.js +9 -8
- package/dist/printers/prusa.d.ts +14 -6
- package/dist/printers/prusa.js +56 -9
- package/dist/printers/repetier.d.ts +18 -6
- package/dist/printers/repetier.js +48 -6
- package/dist/safety/archive.d.ts +11 -0
- package/dist/safety/archive.js +214 -0
- package/dist/safety/artifact.d.ts +6 -0
- package/dist/safety/artifact.js +66 -0
- package/dist/safety/confirmation.d.ts +22 -0
- package/dist/safety/confirmation.js +43 -0
- package/dist/safety/expected-peaks.d.ts +24 -0
- package/dist/safety/expected-peaks.js +24 -0
- package/dist/safety/gcode-thermal.d.ts +64 -0
- package/dist/safety/gcode-thermal.js +382 -0
- package/dist/safety/limits.d.ts +31 -0
- package/dist/safety/limits.js +105 -0
- package/dist/safety/print-file.d.ts +25 -0
- package/dist/safety/print-file.js +610 -0
- package/dist/safety/printer-state.d.ts +51 -0
- package/dist/safety/printer-state.js +371 -0
- package/dist/slicer/bambu-cli.d.ts +110 -0
- package/dist/slicer/bambu-cli.js +547 -0
- package/dist/slicer/profile-flatten.d.ts +104 -0
- package/dist/slicer/profile-flatten.js +676 -0
- package/dist/slicer/run-slicer.d.ts +18 -0
- package/dist/slicer/run-slicer.js +102 -0
- package/dist/slicer/slicer-error.d.ts +27 -0
- package/dist/slicer/slicer-error.js +53 -0
- package/dist/slicer/templates.d.ts +126 -0
- package/dist/slicer/templates.js +255 -0
- package/dist/stl/stl-manipulator.d.ts +25 -3
- package/dist/stl/stl-manipulator.js +214 -232
- package/package.json +16 -9
- package/server.json +56 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +200 -19
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +686 -137
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -164
- package/src/printers/creality.ts +40 -9
- package/src/printers/duet.ts +64 -53
- package/src/printers/generic-base.ts +286 -0
- package/src/printers/klipper.ts +83 -28
- package/src/printers/octoprint.ts +74 -21
- package/src/printers/printer-factory.ts +21 -11
- package/src/printers/prusa.ts +53 -10
- package/src/printers/repetier.ts +55 -17
- package/src/safety/archive.ts +181 -0
- package/src/safety/artifact.ts +58 -0
- package/src/safety/confirmation.ts +56 -0
- package/src/safety/expected-peaks.ts +47 -0
- package/src/safety/gcode-thermal.ts +404 -0
- package/src/safety/limits.ts +98 -0
- package/src/safety/print-file.ts +429 -0
- package/src/safety/printer-state.ts +344 -0
- package/src/slicer/bambu-cli.ts +633 -0
- package/src/slicer/profile-flatten.ts +830 -0
- package/src/slicer/run-slicer.ts +126 -0
- package/src/slicer/slicer-error.ts +72 -0
- package/src/slicer/templates.ts +288 -0
- package/src/stl/stl-manipulator.ts +247 -275
package/dist/printers/klipper.js
CHANGED
|
@@ -1,48 +1,95 @@
|
|
|
1
|
-
import { PrinterImplementation } from "../types.js";
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import FormData from "form-data";
|
|
4
|
-
|
|
3
|
+
import { GenericPrinterImplementation } from "./generic-base.js";
|
|
4
|
+
function encodePath(filename) {
|
|
5
|
+
return filename.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
6
|
+
}
|
|
7
|
+
export class KlipperImplementation extends GenericPrinterImplementation {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.printerLabel = "Klipper/Moonraker";
|
|
11
|
+
}
|
|
12
|
+
authHeaders(apiKey) {
|
|
13
|
+
return apiKey ? { headers: { "X-Api-Key": apiKey } } : {};
|
|
14
|
+
}
|
|
5
15
|
async getStatus(host, port, apiKey) {
|
|
6
16
|
const url = `http://${host}:${port}/printer/info`;
|
|
7
|
-
const response = await this.apiClient.get(url);
|
|
17
|
+
const response = await this.apiClient.get(url, this.authHeaders(apiKey));
|
|
8
18
|
return response.data;
|
|
9
19
|
}
|
|
10
20
|
async getFiles(host, port, apiKey) {
|
|
11
21
|
const url = `http://${host}:${port}/server/files/list`;
|
|
12
|
-
const response = await this.apiClient.get(url);
|
|
22
|
+
const response = await this.apiClient.get(url, this.authHeaders(apiKey));
|
|
13
23
|
return response.data;
|
|
14
24
|
}
|
|
15
25
|
async getFile(host, port, apiKey, filename) {
|
|
16
26
|
const url = `http://${host}:${port}/server/files/metadata?filename=${encodeURIComponent(filename)}`;
|
|
17
|
-
const response = await this.apiClient.get(url);
|
|
27
|
+
const response = await this.apiClient.get(url, this.authHeaders(apiKey));
|
|
18
28
|
return response.data;
|
|
19
29
|
}
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Moonraker object query: webhooks.state must be "ready" (not startup,
|
|
32
|
+
* shutdown or error) and print_stats.state standby/complete/cancelled.
|
|
33
|
+
* complete/cancelled can leave a part on the bed, so they need a human.
|
|
34
|
+
*/
|
|
35
|
+
async readPrinterState(host, port, apiKey) {
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = (await this.apiClient.get(`http://${host}:${port}/printer/objects/query?webhooks&print_stats`, this.authHeaders(apiKey))).data;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Cannot read Klipper/Moonraker printer state; nothing was sent: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
const status = data?.result?.status;
|
|
44
|
+
const klippy = status?.webhooks?.state;
|
|
45
|
+
const job = status?.print_stats?.state;
|
|
46
|
+
if (typeof klippy !== "string" || typeof job !== "string") {
|
|
47
|
+
throw new Error("Moonraker did not report webhooks.state and print_stats.state; cannot verify the printer is idle.");
|
|
48
|
+
}
|
|
49
|
+
const state = `klippy ${klippy}, print ${job}`;
|
|
50
|
+
if (klippy !== "ready") {
|
|
51
|
+
return { state, ready: false, reason: status?.webhooks?.state_message ? String(status.webhooks.state_message) : "Klipper is not ready" };
|
|
52
|
+
}
|
|
53
|
+
if (!["standby", "complete", "cancelled"].includes(job)) {
|
|
54
|
+
return { state, ready: false, reason: `print_stats.state is ${job}` };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
state,
|
|
58
|
+
ready: true,
|
|
59
|
+
finishedJob: job === "standby" ? undefined : JSON.stringify([job, status?.print_stats?.filename ?? null]),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async downloadRemoteFile(host, port, apiKey, filename, destination) {
|
|
63
|
+
await this.downloadToFile(`http://${host}:${port}/server/files/gcodes/${encodePath(filename)}`, apiKey ? { "X-Api-Key": apiKey } : {}, destination);
|
|
64
|
+
}
|
|
65
|
+
/** Current Moonraker returns {result:{item}}; older releases returned result "success". */
|
|
66
|
+
uploadSucceeded(data) {
|
|
67
|
+
return data?.result === "success" || !!data?.result?.item || !!data?.item;
|
|
68
|
+
}
|
|
69
|
+
async rawUploadFile(host, port, apiKey, filePath, filename, print) {
|
|
21
70
|
const url = `http://${host}:${port}/server/files/upload`;
|
|
22
71
|
const formData = new FormData();
|
|
23
72
|
formData.append("file", fs.createReadStream(filePath));
|
|
24
73
|
formData.append("filename", filename);
|
|
25
74
|
const response = await this.apiClient.post(url, formData, {
|
|
26
75
|
headers: {
|
|
27
|
-
...formData.getHeaders()
|
|
76
|
+
...formData.getHeaders(),
|
|
77
|
+
...(apiKey ? { "X-Api-Key": apiKey } : {})
|
|
28
78
|
}
|
|
29
79
|
});
|
|
30
|
-
if (print && response.data.result === "success") {
|
|
31
|
-
await this.startJob(host, port, apiKey, filename);
|
|
32
|
-
}
|
|
33
80
|
return response.data;
|
|
34
81
|
}
|
|
35
|
-
async
|
|
82
|
+
async rawStartJob(host, port, apiKey, filename) {
|
|
36
83
|
const url = `http://${host}:${port}/printer/print/start`;
|
|
37
|
-
const response = await this.apiClient.post(url, { filename });
|
|
84
|
+
const response = await this.apiClient.post(url, { filename }, this.authHeaders(apiKey));
|
|
38
85
|
return response.data;
|
|
39
86
|
}
|
|
40
|
-
async
|
|
87
|
+
async rawCancelJob(host, port, apiKey) {
|
|
41
88
|
const url = `http://${host}:${port}/printer/print/cancel`;
|
|
42
|
-
const response = await this.apiClient.post(url, null);
|
|
89
|
+
const response = await this.apiClient.post(url, null, this.authHeaders(apiKey));
|
|
43
90
|
return response.data;
|
|
44
91
|
}
|
|
45
|
-
async
|
|
92
|
+
async rawSetTemperature(host, port, apiKey, component, temperature) {
|
|
46
93
|
const url = `http://${host}:${port}/printer/gcode/script`;
|
|
47
94
|
let gcode;
|
|
48
95
|
if (component === "bed") {
|
|
@@ -56,7 +103,7 @@ export class KlipperImplementation extends PrinterImplementation {
|
|
|
56
103
|
}
|
|
57
104
|
const response = await this.apiClient.post(url, {
|
|
58
105
|
script: gcode
|
|
59
|
-
});
|
|
106
|
+
}, this.authHeaders(apiKey));
|
|
60
107
|
return response.data;
|
|
61
108
|
}
|
|
62
109
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare class OctoPrintImplementation extends
|
|
1
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent } from "./generic-base.js";
|
|
2
|
+
export declare class OctoPrintImplementation extends GenericPrinterImplementation {
|
|
3
|
+
protected readonly printerLabel = "OctoPrint";
|
|
4
|
+
protected readonly uploadStartsPrint = true;
|
|
3
5
|
getStatus(host: string, port: string, apiKey: string): Promise<any>;
|
|
4
6
|
getFiles(host: string, port: string, apiKey: string): Promise<any>;
|
|
5
7
|
getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/printer state.flags (OctoPrint REST API). A 409 means the printer
|
|
10
|
+
* is not operational. Informational flags such as sdReady never block.
|
|
11
|
+
*/
|
|
12
|
+
protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
|
|
13
|
+
/** OctoPrint reports effectivePrint=false when print=true had no effect. */
|
|
14
|
+
protected uploadStartedPrint(data: any): boolean;
|
|
15
|
+
protected downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise<void>;
|
|
16
|
+
protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
|
|
17
|
+
protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
18
|
+
protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
|
|
19
|
+
/** OctoPrint tool targets are keyed tool{n} (POST /api/printer/tool, command "target"). */
|
|
20
|
+
protected heaterComponent(component: string): HeaterComponent | undefined;
|
|
21
|
+
protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
|
|
10
22
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { PrinterImplementation } from "../types.js";
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import FormData from "form-data";
|
|
4
|
-
|
|
3
|
+
import { isAxiosError } from "axios";
|
|
4
|
+
import { GenericPrinterImplementation } from "./generic-base.js";
|
|
5
|
+
const BLOCKING_FLAGS = ["printing", "paused", "pausing", "cancelling", "resuming", "finishing", "error", "closedOrError"];
|
|
6
|
+
function encodePath(filename) {
|
|
7
|
+
return filename.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
8
|
+
}
|
|
9
|
+
export class OctoPrintImplementation extends GenericPrinterImplementation {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.printerLabel = "OctoPrint";
|
|
13
|
+
// POST /api/files/local with print=true uploads and starts the same bytes.
|
|
14
|
+
this.uploadStartsPrint = true;
|
|
15
|
+
}
|
|
5
16
|
async getStatus(host, port, apiKey) {
|
|
6
17
|
const url = `http://${host}:${port}/api/printer`;
|
|
7
18
|
const response = await this.apiClient.get(url, {
|
|
@@ -29,7 +40,40 @@ export class OctoPrintImplementation extends PrinterImplementation {
|
|
|
29
40
|
});
|
|
30
41
|
return response.data;
|
|
31
42
|
}
|
|
32
|
-
|
|
43
|
+
/**
|
|
44
|
+
* GET /api/printer state.flags (OctoPrint REST API). A 409 means the printer
|
|
45
|
+
* is not operational. Informational flags such as sdReady never block.
|
|
46
|
+
*/
|
|
47
|
+
async readPrinterState(host, port, apiKey) {
|
|
48
|
+
let data;
|
|
49
|
+
try {
|
|
50
|
+
data = (await this.apiClient.get(`http://${host}:${port}/api/printer`, { headers: { "X-Api-Key": apiKey } })).data;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isAxiosError(error) && error.response?.status === 409) {
|
|
54
|
+
return { state: "Offline", ready: false, reason: "OctoPrint reports the printer is not operational" };
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Cannot read OctoPrint printer state; nothing was sent: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
const flags = data?.state?.flags;
|
|
59
|
+
if (!flags || typeof flags !== "object") {
|
|
60
|
+
throw new Error("OctoPrint returned no printer state flags; cannot verify the printer is idle.");
|
|
61
|
+
}
|
|
62
|
+
const blocking = BLOCKING_FLAGS.filter((flag) => flags[flag] === true);
|
|
63
|
+
if (flags.operational !== true)
|
|
64
|
+
blocking.unshift("not operational");
|
|
65
|
+
if (flags.ready === false && !blocking.length)
|
|
66
|
+
blocking.push("not ready");
|
|
67
|
+
return { state: String(data?.state?.text ?? "unknown"), ready: blocking.length === 0, reason: blocking.join(", ") || undefined };
|
|
68
|
+
}
|
|
69
|
+
/** OctoPrint reports effectivePrint=false when print=true had no effect. */
|
|
70
|
+
uploadStartedPrint(data) {
|
|
71
|
+
return data?.effectivePrint !== false;
|
|
72
|
+
}
|
|
73
|
+
async downloadRemoteFile(host, port, apiKey, filename, destination) {
|
|
74
|
+
await this.downloadToFile(`http://${host}:${port}/downloads/files/local/${encodePath(filename)}`, { "X-Api-Key": apiKey }, destination);
|
|
75
|
+
}
|
|
76
|
+
async rawUploadFile(host, port, apiKey, filePath, filename, print) {
|
|
33
77
|
const url = `http://${host}:${port}/api/files/local`;
|
|
34
78
|
const formData = new FormData();
|
|
35
79
|
formData.append("file", fs.createReadStream(filePath));
|
|
@@ -45,7 +89,7 @@ export class OctoPrintImplementation extends PrinterImplementation {
|
|
|
45
89
|
});
|
|
46
90
|
return response.data;
|
|
47
91
|
}
|
|
48
|
-
async
|
|
92
|
+
async rawStartJob(host, port, apiKey, filename) {
|
|
49
93
|
const url = `http://${host}:${port}/api/files/local/${filename}`;
|
|
50
94
|
const response = await this.apiClient.post(url, {
|
|
51
95
|
command: "select",
|
|
@@ -58,7 +102,7 @@ export class OctoPrintImplementation extends PrinterImplementation {
|
|
|
58
102
|
});
|
|
59
103
|
return response.data;
|
|
60
104
|
}
|
|
61
|
-
async
|
|
105
|
+
async rawCancelJob(host, port, apiKey) {
|
|
62
106
|
const url = `http://${host}:${port}/api/job`;
|
|
63
107
|
const response = await this.apiClient.post(url, {
|
|
64
108
|
command: "cancel"
|
|
@@ -70,7 +114,19 @@ export class OctoPrintImplementation extends PrinterImplementation {
|
|
|
70
114
|
});
|
|
71
115
|
return response.data;
|
|
72
116
|
}
|
|
73
|
-
|
|
117
|
+
/** OctoPrint tool targets are keyed tool{n} (POST /api/printer/tool, command "target"). */
|
|
118
|
+
heaterComponent(component) {
|
|
119
|
+
const normalized = component.trim().toLowerCase();
|
|
120
|
+
if (normalized === "bed")
|
|
121
|
+
return { heater: "bed", raw: "bed" };
|
|
122
|
+
if (["extruder", "nozzle", "tool"].includes(normalized))
|
|
123
|
+
return { heater: "nozzle", raw: "tool0" };
|
|
124
|
+
const indexed = normalized.match(/^(?:extruder|tool)(\d+)$/);
|
|
125
|
+
if (indexed)
|
|
126
|
+
return { heater: "nozzle", raw: `tool${Number(indexed[1])}` };
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
async rawSetTemperature(host, port, apiKey, component, temperature) {
|
|
74
130
|
let url = `http://${host}:${port}/api/printer/tool`;
|
|
75
131
|
const data = {};
|
|
76
132
|
if (component === "bed") {
|
|
@@ -78,10 +134,9 @@ export class OctoPrintImplementation extends PrinterImplementation {
|
|
|
78
134
|
data.target = temperature;
|
|
79
135
|
url = `http://${host}:${port}/api/printer/bed`;
|
|
80
136
|
}
|
|
81
|
-
else if (component
|
|
137
|
+
else if (/^tool\d+$/.test(component)) {
|
|
82
138
|
data.command = "target";
|
|
83
|
-
data.targets = {};
|
|
84
|
-
data.targets[component] = temperature;
|
|
139
|
+
data.targets = { [component]: temperature };
|
|
85
140
|
}
|
|
86
141
|
else {
|
|
87
142
|
throw new Error(`Unsupported component: ${component}`);
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { PrinterImplementation } from "../types.js";
|
|
2
|
+
import type { ConfirmHardware } from "../safety/confirmation.js";
|
|
3
|
+
import type { PrintSafetyOptions } from "../safety/expected-peaks.js";
|
|
4
|
+
/** Every adapter accepts trailing safety options on its print and heating mutations. */
|
|
5
|
+
export type GuardedPrinterImplementation = PrinterImplementation & {
|
|
6
|
+
uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean, options?: PrintSafetyOptions): Promise<any>;
|
|
7
|
+
startJob(host: string, port: string, apiKey: string, filename: string, options?: PrintSafetyOptions): Promise<any>;
|
|
8
|
+
setTemperature(host: string, port: string, apiKey: string, component: string, temperature: unknown, options?: PrintSafetyOptions): Promise<any>;
|
|
9
|
+
};
|
|
2
10
|
export declare class PrinterFactory {
|
|
3
11
|
private implementations;
|
|
4
12
|
private apiClient;
|
|
5
|
-
|
|
6
|
-
|
|
13
|
+
/** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
|
|
14
|
+
constructor(confirm?: ConfirmHardware);
|
|
15
|
+
getImplementation(type: string): GuardedPrinterImplementation;
|
|
7
16
|
disconnectAll(): Promise<void>;
|
|
8
17
|
}
|
|
@@ -7,16 +7,17 @@ import { PrusaImplementation } from "./prusa.js";
|
|
|
7
7
|
import { CrealityImplementation } from "./creality.js";
|
|
8
8
|
import axios from "axios";
|
|
9
9
|
export class PrinterFactory {
|
|
10
|
-
|
|
10
|
+
/** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
|
|
11
|
+
constructor(confirm) {
|
|
11
12
|
this.implementations = new Map();
|
|
12
13
|
this.apiClient = axios.create({ timeout: 10000 });
|
|
13
|
-
this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient));
|
|
14
|
-
this.implementations.set("klipper", new KlipperImplementation(this.apiClient));
|
|
15
|
-
this.implementations.set("duet", new DuetImplementation(this.apiClient));
|
|
16
|
-
this.implementations.set("repetier", new RepetierImplementation(this.apiClient));
|
|
17
|
-
this.implementations.set("bambu", new BambuImplementation(this.apiClient));
|
|
18
|
-
this.implementations.set("prusa", new PrusaImplementation(this.apiClient));
|
|
19
|
-
this.implementations.set("creality", new CrealityImplementation(this.apiClient));
|
|
14
|
+
this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient, confirm));
|
|
15
|
+
this.implementations.set("klipper", new KlipperImplementation(this.apiClient, confirm));
|
|
16
|
+
this.implementations.set("duet", new DuetImplementation(this.apiClient, confirm));
|
|
17
|
+
this.implementations.set("repetier", new RepetierImplementation(this.apiClient, confirm));
|
|
18
|
+
this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm));
|
|
19
|
+
this.implementations.set("prusa", new PrusaImplementation(this.apiClient, confirm));
|
|
20
|
+
this.implementations.set("creality", new CrealityImplementation(this.apiClient, confirm));
|
|
20
21
|
}
|
|
21
22
|
getImplementation(type) {
|
|
22
23
|
const implementation = this.implementations.get(type.toLowerCase());
|
package/dist/printers/prusa.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare class PrusaImplementation extends
|
|
1
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent } from "./generic-base.js";
|
|
2
|
+
export declare class PrusaImplementation extends GenericPrinterImplementation {
|
|
3
|
+
protected readonly printerLabel = "Prusa";
|
|
3
4
|
private buildAuthHeaders;
|
|
4
5
|
private buildBaseUrl;
|
|
5
6
|
private defaultProtocolFor;
|
|
@@ -11,8 +12,15 @@ export declare class PrusaImplementation extends PrinterImplementation {
|
|
|
11
12
|
getStatus(host: string, port: string, apiKey: string): Promise<any>;
|
|
12
13
|
getFiles(host: string, port: string, apiKey: string): Promise<any>;
|
|
13
14
|
getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* PrusaLink /api/v1/status printer.state, falling back to the legacy
|
|
17
|
+
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle but
|
|
18
|
+
* may leave a part on the bed, so they require a human confirmation.
|
|
19
|
+
*/
|
|
20
|
+
protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
|
|
21
|
+
protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
|
|
22
|
+
protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
23
|
+
protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
|
|
24
|
+
protected heaterComponent(component: string): HeaterComponent | undefined;
|
|
25
|
+
protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
|
|
18
26
|
}
|
package/dist/printers/prusa.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { isAxiosError } from "axios";
|
|
2
|
-
import {
|
|
2
|
+
import { GenericPrinterImplementation } from "./generic-base.js";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import FormData from "form-data";
|
|
5
|
-
|
|
5
|
+
const PRUSALINK_READY_STATES = new Set(["IDLE", "READY", "FINISHED", "STOPPED"]);
|
|
6
|
+
const OCTOPRINT_BLOCKING_FLAGS = ["printing", "paused", "pausing", "cancelling", "resuming", "finishing", "error", "closedOrError"];
|
|
7
|
+
export class PrusaImplementation extends GenericPrinterImplementation {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.printerLabel = "Prusa";
|
|
11
|
+
}
|
|
6
12
|
buildAuthHeaders(apiKey) {
|
|
7
13
|
return {
|
|
8
14
|
Accept: "application/json",
|
|
@@ -114,7 +120,41 @@ export class PrusaImplementation extends PrinterImplementation {
|
|
|
114
120
|
]);
|
|
115
121
|
return response.data;
|
|
116
122
|
}
|
|
117
|
-
|
|
123
|
+
/**
|
|
124
|
+
* PrusaLink /api/v1/status printer.state, falling back to the legacy
|
|
125
|
+
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle but
|
|
126
|
+
* may leave a part on the bed, so they require a human confirmation.
|
|
127
|
+
*/
|
|
128
|
+
async readPrinterState(host, port, apiKey) {
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await this.getWithFallback(host, port, apiKey, ["/api/v1/status", "/api/printer"]);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new Error(`Cannot read Prusa printer state; nothing was sent: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
const data = response.data;
|
|
137
|
+
const v1State = data?.printer?.state;
|
|
138
|
+
if (typeof v1State === "string" && v1State) {
|
|
139
|
+
const state = v1State.toUpperCase();
|
|
140
|
+
const ready = PRUSALINK_READY_STATES.has(state);
|
|
141
|
+
return {
|
|
142
|
+
state,
|
|
143
|
+
ready,
|
|
144
|
+
reason: ready ? undefined : `printer.state is ${state}`,
|
|
145
|
+
finishedJob: ready && (state === "FINISHED" || state === "STOPPED") ? JSON.stringify([state, data?.job?.id ?? null]) : undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const flags = data?.state?.flags;
|
|
149
|
+
if (flags && typeof flags === "object") {
|
|
150
|
+
const blocking = OCTOPRINT_BLOCKING_FLAGS.filter((flag) => flags[flag] === true);
|
|
151
|
+
if (flags.operational !== true)
|
|
152
|
+
blocking.unshift("not operational");
|
|
153
|
+
return { state: String(data?.state?.text ?? "unknown"), ready: blocking.length === 0, reason: blocking.join(", ") || undefined };
|
|
154
|
+
}
|
|
155
|
+
throw new Error("Prusa did not report printer.state or state flags; cannot verify the printer is idle.");
|
|
156
|
+
}
|
|
157
|
+
async rawUploadFile(host, port, apiKey, filePath, filename, print) {
|
|
118
158
|
const formData = new FormData();
|
|
119
159
|
formData.append("file", fs.createReadStream(filePath));
|
|
120
160
|
formData.append("filename", filename);
|
|
@@ -126,12 +166,10 @@ export class PrusaImplementation extends PrinterImplementation {
|
|
|
126
166
|
...formData.getHeaders(),
|
|
127
167
|
},
|
|
128
168
|
});
|
|
129
|
-
|
|
130
|
-
await this.startJob(host, port, apiKey, filename);
|
|
131
|
-
}
|
|
169
|
+
// Printing is started by the shared gate only after a fresh state recheck.
|
|
132
170
|
return response.data;
|
|
133
171
|
}
|
|
134
|
-
async
|
|
172
|
+
async rawStartJob(host, port, apiKey, filename) {
|
|
135
173
|
const response = await this.postWithFallback(host, port, apiKey, [
|
|
136
174
|
{
|
|
137
175
|
route: "/api/v1/job",
|
|
@@ -164,7 +202,7 @@ export class PrusaImplementation extends PrinterImplementation {
|
|
|
164
202
|
]);
|
|
165
203
|
return response.data;
|
|
166
204
|
}
|
|
167
|
-
async
|
|
205
|
+
async rawCancelJob(host, port, apiKey) {
|
|
168
206
|
const response = await this.postWithFallback(host, port, apiKey, [
|
|
169
207
|
{
|
|
170
208
|
route: "/api/v1/job",
|
|
@@ -181,7 +219,16 @@ export class PrusaImplementation extends PrinterImplementation {
|
|
|
181
219
|
]);
|
|
182
220
|
return response.data;
|
|
183
221
|
}
|
|
184
|
-
|
|
222
|
+
heaterComponent(component) {
|
|
223
|
+
const normalized = component.trim().toLowerCase();
|
|
224
|
+
if (normalized === "bed")
|
|
225
|
+
return { heater: "bed", raw: "bed" };
|
|
226
|
+
if (normalized.startsWith("extruder") || normalized === "nozzle" || normalized === "tool0" || normalized === "tool") {
|
|
227
|
+
return { heater: "nozzle", raw: "nozzle" };
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
async rawSetTemperature(host, port, apiKey, component, temperature) {
|
|
185
232
|
const normalized = component.toLowerCase();
|
|
186
233
|
if (normalized === "bed") {
|
|
187
234
|
const response = await this.postWithFallback(host, port, apiKey, [
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { GenericPrinterImplementation, type GenericPrinterState } from "./generic-base.js";
|
|
2
|
+
/**
|
|
3
|
+
* Note: Repetier-Server documents printer commands at /printer/api/<slug>;
|
|
4
|
+
* this adapter's slug-less routes are unverified. Remote starts are refused
|
|
5
|
+
* because Repetier documents no complete file download for inspection.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RepetierImplementation extends GenericPrinterImplementation {
|
|
8
|
+
protected readonly printerLabel = "Repetier";
|
|
9
|
+
protected readonly uploadStartsPrint = true;
|
|
3
10
|
getStatus(host: string, port: string, apiKey: string): Promise<any>;
|
|
4
11
|
getFiles(host: string, port: string, apiKey: string): Promise<any>;
|
|
5
12
|
getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* listPrinter returns one entry per printer (online 0/1, active, job "none").
|
|
15
|
+
* Without a configured slug, more than one printer is ambiguous.
|
|
16
|
+
*/
|
|
17
|
+
protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
|
|
18
|
+
protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
|
|
19
|
+
protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
20
|
+
protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
|
|
21
|
+
protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
|
|
10
22
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { PrinterImplementation } from "../types.js";
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import FormData from "form-data";
|
|
4
|
-
|
|
3
|
+
import { GenericPrinterImplementation } from "./generic-base.js";
|
|
4
|
+
/**
|
|
5
|
+
* Note: Repetier-Server documents printer commands at /printer/api/<slug>;
|
|
6
|
+
* this adapter's slug-less routes are unverified. Remote starts are refused
|
|
7
|
+
* because Repetier documents no complete file download for inspection.
|
|
8
|
+
*/
|
|
9
|
+
export class RepetierImplementation extends GenericPrinterImplementation {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.printerLabel = "Repetier";
|
|
13
|
+
// The upload form's print=1 flag uploads and starts the same bytes.
|
|
14
|
+
this.uploadStartsPrint = true;
|
|
15
|
+
}
|
|
5
16
|
async getStatus(host, port, apiKey) {
|
|
6
17
|
const url = `http://${host}:${port}/printer/api/?a=getPrinterInfo&apikey=${apiKey}`;
|
|
7
18
|
const response = await this.apiClient.get(url);
|
|
@@ -17,7 +28,38 @@ export class RepetierImplementation extends PrinterImplementation {
|
|
|
17
28
|
const response = await this.apiClient.get(url);
|
|
18
29
|
return response.data;
|
|
19
30
|
}
|
|
20
|
-
|
|
31
|
+
/**
|
|
32
|
+
* listPrinter returns one entry per printer (online 0/1, active, job "none").
|
|
33
|
+
* Without a configured slug, more than one printer is ambiguous.
|
|
34
|
+
*/
|
|
35
|
+
async readPrinterState(host, port, apiKey) {
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = (await this.apiClient.get(`http://${host}:${port}/printer/api/?a=listPrinter&apikey=${encodeURIComponent(apiKey)}`)).data;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Cannot read Repetier printer state; nothing was sent: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
const printers = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : undefined;
|
|
44
|
+
if (!printers)
|
|
45
|
+
throw new Error("Repetier did not return a printer list; cannot verify the printer is idle.");
|
|
46
|
+
if (printers.length !== 1) {
|
|
47
|
+
throw new Error(`Repetier reports ${printers.length} printers; this adapter cannot tell which one would receive the job.`);
|
|
48
|
+
}
|
|
49
|
+
const [printer] = printers;
|
|
50
|
+
const online = printer?.online === 1 || printer?.online === true;
|
|
51
|
+
const job = typeof printer?.job === "string" ? printer.job : undefined;
|
|
52
|
+
if (job === undefined)
|
|
53
|
+
throw new Error("Repetier did not report the printer's job state; cannot verify it is idle.");
|
|
54
|
+
const reasons = [
|
|
55
|
+
...(online ? [] : ["offline"]),
|
|
56
|
+
...(printer?.active === false ? ["inactive"] : []),
|
|
57
|
+
...(job !== "none" && job !== "" ? [`job ${job} is active`] : []),
|
|
58
|
+
...(printer?.paused === true ? ["paused"] : []),
|
|
59
|
+
];
|
|
60
|
+
return { state: `${online ? "online" : "offline"}, job ${job || "none"}`, ready: reasons.length === 0, reason: reasons.join(", ") || undefined };
|
|
61
|
+
}
|
|
62
|
+
async rawUploadFile(host, port, apiKey, filePath, filename, print) {
|
|
21
63
|
const url = `http://${host}:${port}/printer/api/`;
|
|
22
64
|
const formData = new FormData();
|
|
23
65
|
formData.append("a", "upload");
|
|
@@ -32,17 +74,17 @@ export class RepetierImplementation extends PrinterImplementation {
|
|
|
32
74
|
});
|
|
33
75
|
return response.data;
|
|
34
76
|
}
|
|
35
|
-
async
|
|
77
|
+
async rawStartJob(host, port, apiKey, filename) {
|
|
36
78
|
const url = `http://${host}:${port}/printer/api/?a=startJob&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`;
|
|
37
79
|
const response = await this.apiClient.get(url);
|
|
38
80
|
return response.data;
|
|
39
81
|
}
|
|
40
|
-
async
|
|
82
|
+
async rawCancelJob(host, port, apiKey) {
|
|
41
83
|
const url = `http://${host}:${port}/printer/api/?a=stopJob&apikey=${apiKey}`;
|
|
42
84
|
const response = await this.apiClient.get(url);
|
|
43
85
|
return response.data;
|
|
44
86
|
}
|
|
45
|
-
async
|
|
87
|
+
async rawSetTemperature(host, port, apiKey, component, temperature) {
|
|
46
88
|
let url;
|
|
47
89
|
if (component === "bed") {
|
|
48
90
|
url = `http://${host}:${port}/printer/api/?a=setBedTemp&apikey=${apiKey}&temp=${temperature}`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
export declare const MAX_PRINT_FILE_BYTES: number;
|
|
3
|
+
/** Read no more than the fixed print-artifact limit, even if the file grows. */
|
|
4
|
+
export declare function readBoundedPrintFile(filePath: string): Promise<Buffer>;
|
|
5
|
+
/** Bounded native inflation prevents false size fields from making JSZip allocate
|
|
6
|
+
* unbounded output. Populate JSZip only with the verified, uniquely named bytes. */
|
|
7
|
+
export declare function loadSafe3mfArchive(bytes: Buffer): Promise<JSZip>;
|
|
8
|
+
export declare function readSafe3mfArchive(filePath: string): Promise<{
|
|
9
|
+
bytes: Buffer;
|
|
10
|
+
zip: JSZip;
|
|
11
|
+
}>;
|