mcp-3d-printer-server 1.0.2 → 1.2.1
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/README.md +105 -29
- package/dist/http-server.d.ts +16 -0
- package/dist/http-server.js +268 -0
- package/dist/index.js +462 -37
- package/dist/printers/bambu.d.ts +23 -12
- package/dist/printers/bambu.js +319 -232
- package/dist/printers/printer-factory.d.ts +0 -1
- package/dist/printers/printer-factory.js +6 -4
- package/dist/printers/prusa.d.ts +5 -0
- package/dist/printers/prusa.js +160 -59
- package/dist/sse-server.d.ts +20 -0
- package/dist/sse-server.js +318 -0
- package/dist/stl/stl-manipulator.d.ts +5 -5
- package/dist/stl/stl-manipulator.js +32 -7
- package/dist/types.d.ts +0 -6
- package/dist/types.js +0 -20
- package/package.json +10 -4
- package/src/http-server.ts +328 -0
- package/src/index.ts +582 -50
- package/src/printers/bambu.ts +452 -299
- package/src/printers/printer-factory.ts +7 -4
- package/src/printers/prusa.ts +218 -73
- package/src/sse-server.ts +409 -0
- package/src/stl/stl-manipulator.ts +38 -8
- package/src/test/Tape+Card+55mm+Ring.stl +0 -0
- package/src/types.ts +0 -19
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import express, { Request, Response } from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import { Readable } from 'stream';
|
|
9
|
+
import {
|
|
10
|
+
ListResourcesRequestSchema,
|
|
11
|
+
ReadResourceRequestSchema,
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ErrorCode,
|
|
15
|
+
McpError
|
|
16
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import axios from "axios";
|
|
18
|
+
import dotenv from "dotenv";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import * as THREE from 'three';
|
|
22
|
+
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
23
|
+
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
24
|
+
import { parse3MF, ThreeMFData } from './3mf_parser.js';
|
|
25
|
+
import { BambuImplementation } from "./printers/bambu.js";
|
|
26
|
+
|
|
27
|
+
// Load environment variables from .env file
|
|
28
|
+
dotenv.config();
|
|
29
|
+
|
|
30
|
+
// Default values
|
|
31
|
+
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
32
|
+
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
33
|
+
const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
34
|
+
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint";
|
|
35
|
+
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
36
|
+
|
|
37
|
+
// SSE Server configuration
|
|
38
|
+
const SSE_PORT = parseInt(process.env.SSE_PORT || "3000");
|
|
39
|
+
|
|
40
|
+
// Slicer configuration
|
|
41
|
+
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "prusaslicer";
|
|
42
|
+
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "";
|
|
43
|
+
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
44
|
+
|
|
45
|
+
// Bambu-specific default values
|
|
46
|
+
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
47
|
+
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
48
|
+
|
|
49
|
+
// Ensure temp directory exists
|
|
50
|
+
if (!fs.existsSync(TEMP_DIR)) {
|
|
51
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ThreeDPrinterMCPServerSSE {
|
|
55
|
+
private server: Server;
|
|
56
|
+
private printerFactory: PrinterFactory;
|
|
57
|
+
private stlManipulator: STLManipulator;
|
|
58
|
+
private app: express.Application;
|
|
59
|
+
private sessions: Map<string, { transport: SSEServerTransport, lastActivity: number }> = new Map();
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this.server = new Server(
|
|
63
|
+
{
|
|
64
|
+
name: "mcp-3d-printer-server",
|
|
65
|
+
version: "1.0.0"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
capabilities: {
|
|
69
|
+
resources: {},
|
|
70
|
+
tools: {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
this.printerFactory = new PrinterFactory();
|
|
76
|
+
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
77
|
+
|
|
78
|
+
this.app = express();
|
|
79
|
+
this.setupExpressApp();
|
|
80
|
+
this.setupHandlers();
|
|
81
|
+
this.setupErrorHandling();
|
|
82
|
+
this.startSessionCleanup();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private setupExpressApp() {
|
|
86
|
+
// Enable CORS for all origins
|
|
87
|
+
this.app.use(cors({
|
|
88
|
+
origin: '*',
|
|
89
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
90
|
+
allowedHeaders: ['Content-Type', 'Accept', 'Last-Event-ID'],
|
|
91
|
+
credentials: true
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// Parse JSON bodies
|
|
95
|
+
this.app.use(express.json({ limit: '50mb' }));
|
|
96
|
+
|
|
97
|
+
// SSE endpoint - establishes the event stream
|
|
98
|
+
this.app.get('/sse', async (req: Request, res: Response) => {
|
|
99
|
+
const sessionId = uuidv4();
|
|
100
|
+
const messagesPath = `/messages?session_id=${sessionId}`;
|
|
101
|
+
|
|
102
|
+
// Set SSE headers
|
|
103
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
104
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
105
|
+
res.setHeader('Connection', 'keep-alive');
|
|
106
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
107
|
+
|
|
108
|
+
// Send initial endpoint event
|
|
109
|
+
res.write(`event: endpoint\ndata: ${messagesPath}\n\n`);
|
|
110
|
+
|
|
111
|
+
// Create transport for this session
|
|
112
|
+
const transport = new SSEServerTransport(messagesPath);
|
|
113
|
+
this.sessions.set(sessionId, { transport, lastActivity: Date.now() });
|
|
114
|
+
|
|
115
|
+
// Send periodic ping events to keep connection alive
|
|
116
|
+
const pingInterval = setInterval(() => {
|
|
117
|
+
if (!res.writableEnded) {
|
|
118
|
+
res.write(`event: ping\ndata: ${new Date().toISOString()}\n\n`);
|
|
119
|
+
}
|
|
120
|
+
}, 30000);
|
|
121
|
+
|
|
122
|
+
// Handle client disconnect
|
|
123
|
+
req.on('close', () => {
|
|
124
|
+
clearInterval(pingInterval);
|
|
125
|
+
this.sessions.delete(sessionId);
|
|
126
|
+
res.end();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Handle transport errors
|
|
130
|
+
transport.onerror = (error) => {
|
|
131
|
+
console.error(`SSE Transport error for session ${sessionId}:`, error);
|
|
132
|
+
clearInterval(pingInterval);
|
|
133
|
+
this.sessions.delete(sessionId);
|
|
134
|
+
res.end();
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Messages endpoint - handles JSON-RPC messages
|
|
139
|
+
this.app.post('/messages', async (req: Request, res: Response) => {
|
|
140
|
+
const sessionId = req.query.session_id as string;
|
|
141
|
+
|
|
142
|
+
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
143
|
+
res.status(400).json({
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
error: {
|
|
146
|
+
code: -32600,
|
|
147
|
+
message: "Invalid or missing session ID"
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const session = this.sessions.get(sessionId)!;
|
|
154
|
+
session.lastActivity = Date.now();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Handle the message through the transport
|
|
158
|
+
const result = await session.transport.handleMessage(req.body);
|
|
159
|
+
|
|
160
|
+
if (result) {
|
|
161
|
+
res.json(result);
|
|
162
|
+
} else {
|
|
163
|
+
res.status(202).send(); // Accepted, no response needed
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Error handling message:', error);
|
|
167
|
+
res.status(500).json({
|
|
168
|
+
jsonrpc: "2.0",
|
|
169
|
+
error: {
|
|
170
|
+
code: -32603,
|
|
171
|
+
message: "Internal error",
|
|
172
|
+
data: error instanceof Error ? error.message : String(error)
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Health check endpoint
|
|
179
|
+
this.app.get('/health', (req: Request, res: Response) => {
|
|
180
|
+
res.json({
|
|
181
|
+
status: 'ok',
|
|
182
|
+
server: 'mcp-3d-printer-server',
|
|
183
|
+
transport: 'sse',
|
|
184
|
+
sessions: this.sessions.size
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Root endpoint
|
|
189
|
+
this.app.get('/', (req: Request, res: Response) => {
|
|
190
|
+
res.json({
|
|
191
|
+
name: 'MCP 3D Printer Server (SSE)',
|
|
192
|
+
version: '1.0.0',
|
|
193
|
+
endpoints: {
|
|
194
|
+
sse: '/sse',
|
|
195
|
+
messages: '/messages',
|
|
196
|
+
health: '/health'
|
|
197
|
+
},
|
|
198
|
+
transport: 'sse',
|
|
199
|
+
documentation: 'https://github.com/DMontgomery40/mcp-3D-printer-server'
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private startSessionCleanup() {
|
|
205
|
+
// Clean up inactive sessions every 5 minutes
|
|
206
|
+
setInterval(() => {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const timeout = 10 * 60 * 1000; // 10 minutes
|
|
209
|
+
|
|
210
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
211
|
+
if (now - session.lastActivity > timeout) {
|
|
212
|
+
console.log(`Cleaning up inactive session: ${sessionId}`);
|
|
213
|
+
this.sessions.delete(sessionId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}, 5 * 60 * 1000);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setupErrorHandling() {
|
|
220
|
+
this.server.onerror = (error) => {
|
|
221
|
+
console.error("[MCP Error]", error);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
process.on("SIGINT", async () => {
|
|
225
|
+
console.log("\nShutting down SSE server...");
|
|
226
|
+
// Disconnect all printers
|
|
227
|
+
await this.printerFactory.disconnectAll();
|
|
228
|
+
// Close all sessions
|
|
229
|
+
for (const [sessionId, _] of this.sessions.entries()) {
|
|
230
|
+
this.sessions.delete(sessionId);
|
|
231
|
+
}
|
|
232
|
+
await this.server.close();
|
|
233
|
+
process.exit(0);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setupHandlers() {
|
|
238
|
+
this.setupResourceHandlers();
|
|
239
|
+
this.setupToolHandlers();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setupResourceHandlers() {
|
|
243
|
+
// List available resources
|
|
244
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
245
|
+
return {
|
|
246
|
+
resources: [
|
|
247
|
+
{
|
|
248
|
+
uri: `printer://${DEFAULT_HOST}/status`,
|
|
249
|
+
name: "3D Printer Status",
|
|
250
|
+
mimeType: "application/json",
|
|
251
|
+
description: "Current status of the 3D printer including temperatures, print progress, and more"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
uri: `printer://${DEFAULT_HOST}/files`,
|
|
255
|
+
name: "3D Printer Files",
|
|
256
|
+
mimeType: "application/json",
|
|
257
|
+
description: "List of files available on the 3D printer"
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
templates: [
|
|
261
|
+
{
|
|
262
|
+
uriTemplate: "printer://{host}/status",
|
|
263
|
+
name: "3D Printer Status",
|
|
264
|
+
mimeType: "application/json"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
uriTemplate: "printer://{host}/files",
|
|
268
|
+
name: "3D Printer Files",
|
|
269
|
+
mimeType: "application/json"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
uriTemplate: "printer://{host}/file/{filename}",
|
|
273
|
+
name: "3D Printer File Content",
|
|
274
|
+
mimeType: "application/gcode"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Read resource
|
|
281
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
282
|
+
const uri = request.params.uri;
|
|
283
|
+
const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/);
|
|
284
|
+
|
|
285
|
+
if (!match) {
|
|
286
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI: ${uri}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const [, host, resource] = match;
|
|
290
|
+
let content;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
if (resource === "status") {
|
|
294
|
+
content = await this.getPrinterStatus(host);
|
|
295
|
+
} else if (resource === "files") {
|
|
296
|
+
content = await this.getPrinterFiles(host);
|
|
297
|
+
} else if (resource.startsWith("file/")) {
|
|
298
|
+
const filename = resource.substring(5);
|
|
299
|
+
content = await this.getPrinterFile(host, filename);
|
|
300
|
+
} else {
|
|
301
|
+
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
contents: [
|
|
306
|
+
{
|
|
307
|
+
uri,
|
|
308
|
+
mimeType: resource.startsWith("file/") ? "application/gcode" : "application/json",
|
|
309
|
+
text: typeof content === "string" ? content : JSON.stringify(content, null, 2)
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (axios.isAxiosError(error)) {
|
|
315
|
+
throw new McpError(
|
|
316
|
+
ErrorCode.InternalError,
|
|
317
|
+
`API error: ${error.response?.data?.error || error.message}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setupToolHandlers() {
|
|
326
|
+
// Tool handlers remain the same as in the stdio version
|
|
327
|
+
// ... (copy all tool handlers from the original index.ts)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Delegating methods to printer implementations (same as original)
|
|
331
|
+
|
|
332
|
+
async getPrinterStatus(
|
|
333
|
+
host: string,
|
|
334
|
+
port = DEFAULT_PORT,
|
|
335
|
+
type = DEFAULT_TYPE,
|
|
336
|
+
apiKey = DEFAULT_API_KEY,
|
|
337
|
+
bambuSerial = DEFAULT_BAMBU_SERIAL,
|
|
338
|
+
bambuToken = DEFAULT_BAMBU_TOKEN
|
|
339
|
+
) {
|
|
340
|
+
const implementation = this.printerFactory.getImplementation(type);
|
|
341
|
+
|
|
342
|
+
if (type.toLowerCase() === "bambu") {
|
|
343
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
344
|
+
return implementation.getStatus(host, port, bambuApiKey);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return implementation.getStatus(host, port, apiKey);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async getPrinterFiles(
|
|
351
|
+
host: string,
|
|
352
|
+
port = DEFAULT_PORT,
|
|
353
|
+
type = DEFAULT_TYPE,
|
|
354
|
+
apiKey = DEFAULT_API_KEY,
|
|
355
|
+
bambuSerial = DEFAULT_BAMBU_SERIAL,
|
|
356
|
+
bambuToken = DEFAULT_BAMBU_TOKEN
|
|
357
|
+
) {
|
|
358
|
+
const implementation = this.printerFactory.getImplementation(type);
|
|
359
|
+
|
|
360
|
+
if (type.toLowerCase() === "bambu") {
|
|
361
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
362
|
+
return implementation.getFiles(host, port, bambuApiKey);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return implementation.getFiles(host, port, apiKey);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async getPrinterFile(
|
|
369
|
+
host: string,
|
|
370
|
+
filename: string,
|
|
371
|
+
port = DEFAULT_PORT,
|
|
372
|
+
type = DEFAULT_TYPE,
|
|
373
|
+
apiKey = DEFAULT_API_KEY,
|
|
374
|
+
bambuSerial = DEFAULT_BAMBU_SERIAL,
|
|
375
|
+
bambuToken = DEFAULT_BAMBU_TOKEN
|
|
376
|
+
) {
|
|
377
|
+
const implementation = this.printerFactory.getImplementation(type);
|
|
378
|
+
|
|
379
|
+
if (type.toLowerCase() === "bambu") {
|
|
380
|
+
return (implementation as any).getFile(host, port, apiKey, bambuSerial, bambuToken, filename);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return implementation.getFile(host, port, apiKey, filename);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ... (copy all other delegating methods from original index.ts)
|
|
387
|
+
|
|
388
|
+
async run() {
|
|
389
|
+
// Connect SSE transports to the server
|
|
390
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
391
|
+
await this.server.connect(session.transport);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Start Express server
|
|
395
|
+
this.app.listen(SSE_PORT, () => {
|
|
396
|
+
console.log(`🚀 MCP 3D Printer Server (SSE) running on http://localhost:${SSE_PORT}`);
|
|
397
|
+
console.log(`📡 SSE endpoint: http://localhost:${SSE_PORT}/sse`);
|
|
398
|
+
console.log(`💬 Messages endpoint: http://localhost:${SSE_PORT}/messages`);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check if running as main module
|
|
404
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
405
|
+
const server = new ThreeDPrinterMCPServerSSE();
|
|
406
|
+
server.run().catch(console.error);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export default ThreeDPrinterMCPServerSSE;
|
|
@@ -79,7 +79,11 @@ export class STLManipulator extends EventEmitter {
|
|
|
79
79
|
|
|
80
80
|
// Load the STL data into a Three.js geometry
|
|
81
81
|
const loader = new STLLoader();
|
|
82
|
-
const
|
|
82
|
+
const rawArrayBuffer = stlData.buffer.slice(
|
|
83
|
+
stlData.byteOffset,
|
|
84
|
+
stlData.byteOffset + stlData.byteLength
|
|
85
|
+
) as ArrayBuffer;
|
|
86
|
+
const geometry = loader.parse(rawArrayBuffer);
|
|
83
87
|
|
|
84
88
|
// Create a mesh from the geometry
|
|
85
89
|
const material = new THREE.MeshStandardMaterial();
|
|
@@ -839,20 +843,21 @@ export class STLManipulator extends EventEmitter {
|
|
|
839
843
|
}
|
|
840
844
|
|
|
841
845
|
/**
|
|
842
|
-
* Slice an STL file using the specified slicer
|
|
843
|
-
* @param stlFilePath Path to the input STL file
|
|
844
|
-
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer)
|
|
846
|
+
* Slice an STL or 3MF file using the specified slicer
|
|
847
|
+
* @param stlFilePath Path to the input STL or 3MF file
|
|
848
|
+
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
|
|
845
849
|
* @param slicerPath Path to the slicer executable
|
|
846
850
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
847
851
|
* @param progressCallback Optional callback for progress updates
|
|
848
|
-
* @returns Path to the generated G-code file
|
|
852
|
+
* @returns Path to the generated G-code or sliced 3MF file
|
|
849
853
|
*/
|
|
850
854
|
async sliceSTL(
|
|
851
855
|
stlFilePath: string,
|
|
852
|
-
slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer',
|
|
856
|
+
slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio',
|
|
853
857
|
slicerPath: string,
|
|
854
858
|
slicerProfile?: string,
|
|
855
|
-
progressCallback?: ProgressCallback
|
|
859
|
+
progressCallback?: ProgressCallback,
|
|
860
|
+
printerPreset?: string
|
|
856
861
|
): Promise<string> {
|
|
857
862
|
const operationId = this.generateOperationId();
|
|
858
863
|
this.activeOperations.set(operationId, true);
|
|
@@ -866,7 +871,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
866
871
|
}
|
|
867
872
|
|
|
868
873
|
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
869
|
-
|
|
874
|
+
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
870
875
|
|
|
871
876
|
let args: string[] = [];
|
|
872
877
|
|
|
@@ -920,6 +925,31 @@ export class STLManipulator extends EventEmitter {
|
|
|
920
925
|
}
|
|
921
926
|
break;
|
|
922
927
|
|
|
928
|
+
case 'bambustudio':
|
|
929
|
+
// Bambu Studio CLI: slice and export as 3MF with embedded gcode
|
|
930
|
+
// For 3MF input: slice in place and export sliced 3MF
|
|
931
|
+
// For STL input: slice and export as 3MF
|
|
932
|
+
{
|
|
933
|
+
const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
|
|
934
|
+
const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
|
|
935
|
+
const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
|
|
936
|
+
args = [
|
|
937
|
+
'--slice', '0', // Slice all plates
|
|
938
|
+
'--export-3mf', bambuOutputPath,
|
|
939
|
+
];
|
|
940
|
+
// --load-machine ensures correct G-code for the specific printer model
|
|
941
|
+
if (printerPreset && !slicerProfile) {
|
|
942
|
+
args.push('--load-machine', printerPreset);
|
|
943
|
+
}
|
|
944
|
+
if (slicerProfile) {
|
|
945
|
+
args.push('--load-settings', slicerProfile);
|
|
946
|
+
}
|
|
947
|
+
args.push(stlFilePath);
|
|
948
|
+
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
949
|
+
outputFilePath = bambuOutputPath;
|
|
950
|
+
}
|
|
951
|
+
break;
|
|
952
|
+
|
|
923
953
|
default:
|
|
924
954
|
throw new Error(`Unsupported slicer type: ${slicerType}`);
|
|
925
955
|
}
|
|
Binary file
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import { BambuPrinter } from "bambu-js";
|
|
3
2
|
|
|
4
3
|
// Define shared types for the printer implementations
|
|
5
4
|
export type BambuFTP = {
|
|
@@ -25,25 +24,7 @@ export abstract class PrinterImplementation {
|
|
|
25
24
|
abstract setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Store for Bambu printers
|
|
29
|
-
export class BambuPrinterStore {
|
|
30
|
-
private printers: Map<string, InstanceType<typeof BambuPrinter>> = new Map();
|
|
31
27
|
|
|
32
|
-
get(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter> {
|
|
33
|
-
const key = `${host}-${serial}`;
|
|
34
|
-
if (!this.printers.has(key)) {
|
|
35
|
-
const printer = new BambuPrinter(host, serial, token);
|
|
36
|
-
this.printers.set(key, printer);
|
|
37
|
-
}
|
|
38
|
-
return this.printers.get(key)!;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async disconnectAll(): Promise<void> {
|
|
42
|
-
for (const printer of this.printers.values()) {
|
|
43
|
-
await printer.disconnect();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
28
|
|
|
48
29
|
export interface SectionBounds {
|
|
49
30
|
minX: number; minY: number; minZ: number;
|