bambu-printer-mcp 1.0.5 → 1.0.8
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 +485 -26
- package/dist/bambu-network-bridge.d.ts +63 -0
- package/dist/bambu-network-bridge.js +393 -0
- package/dist/index.js +562 -23
- package/dist/printers/bambu.js +6 -8
- package/dist/stl/stl-manipulator.d.ts +8 -4
- package/dist/stl/stl-manipulator.js +119 -30
- package/package.json +1 -1
- package/src/bambu-network-bridge.ts +526 -0
- package/src/index.ts +671 -26
- package/src/printers/bambu.ts +6 -8
- package/src/stl/stl-manipulator.ts +143 -33
package/src/printers/bambu.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import JSZip from "jszip";
|
|
5
5
|
import { Client as FTPClient } from "basic-ftp";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
6
7
|
import { BambuPrinter } from "bambu-js";
|
|
7
8
|
import {
|
|
8
9
|
BambuClient,
|
|
@@ -251,7 +252,7 @@ export class BambuImplementation {
|
|
|
251
252
|
print: {
|
|
252
253
|
command: "project_file",
|
|
253
254
|
param: `Metadata/${projectMetadata.plateFileName}`,
|
|
254
|
-
url: `
|
|
255
|
+
url: `ftp://${remoteFileName}`,
|
|
255
256
|
subtask_name: options.projectName,
|
|
256
257
|
md5,
|
|
257
258
|
flow_cali: options.flowCalibration ?? true,
|
|
@@ -275,7 +276,7 @@ export class BambuImplementation {
|
|
|
275
276
|
|
|
276
277
|
return {
|
|
277
278
|
status: "success",
|
|
278
|
-
message: `Uploaded and
|
|
279
|
+
message: `Uploaded 3MF and sent project_file print command: ${options.projectName}`,
|
|
279
280
|
remoteProjectPath,
|
|
280
281
|
plateFile: projectMetadata.plateFileName,
|
|
281
282
|
platePath: projectMetadata.plateInternalPath,
|
|
@@ -454,7 +455,7 @@ export class BambuImplementation {
|
|
|
454
455
|
localPath: string,
|
|
455
456
|
remotePath: string
|
|
456
457
|
): Promise<void> {
|
|
457
|
-
const client = new FTPClient(
|
|
458
|
+
const client = new FTPClient(30_000);
|
|
458
459
|
try {
|
|
459
460
|
await client.access({
|
|
460
461
|
host,
|
|
@@ -464,12 +465,9 @@ export class BambuImplementation {
|
|
|
464
465
|
secure: "implicit",
|
|
465
466
|
secureOptions: { rejectUnauthorized: false },
|
|
466
467
|
});
|
|
467
|
-
// Use absolute path to avoid CWD side-effects
|
|
468
468
|
const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
|
|
469
|
-
const
|
|
470
|
-
await client.
|
|
471
|
-
// uploadFrom with just the basename since we're already in the right dir
|
|
472
|
-
await client.uploadFrom(localPath, path.posix.basename(absoluteRemote));
|
|
469
|
+
const fileData = await fs.readFile(localPath);
|
|
470
|
+
await client.uploadFrom(Readable.from(fileData), absoluteRemote);
|
|
473
471
|
} finally {
|
|
474
472
|
client.close();
|
|
475
473
|
}
|
|
@@ -3,6 +3,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
|
3
3
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
|
|
4
4
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { promisify } from 'util';
|
|
8
9
|
import { EventEmitter } from 'events';
|
|
@@ -40,7 +41,124 @@ export type TransformationParams = {
|
|
|
40
41
|
selectionBounds?: THREE.Box3;
|
|
41
42
|
};
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
export const SLICER_TYPES = [
|
|
45
|
+
'bambustudio',
|
|
46
|
+
'orcaslicer',
|
|
47
|
+
'orcaslicer-bambulab',
|
|
48
|
+
'prusaslicer',
|
|
49
|
+
'cura',
|
|
50
|
+
'slic3r',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export type SlicerType = typeof SLICER_TYPES[number];
|
|
54
|
+
|
|
55
|
+
const SLICER_TYPE_ALIASES: Record<string, SlicerType> = {
|
|
56
|
+
bambustudio: 'bambustudio',
|
|
57
|
+
'bambu-studio': 'bambustudio',
|
|
58
|
+
bblstudio: 'bambustudio',
|
|
59
|
+
orcaslicer: 'orcaslicer',
|
|
60
|
+
'orca-slicer': 'orcaslicer',
|
|
61
|
+
orca: 'orcaslicer',
|
|
62
|
+
'orcaslicer-bambulab': 'orcaslicer-bambulab',
|
|
63
|
+
'orca-slicer-bambulab': 'orcaslicer-bambulab',
|
|
64
|
+
'orca-bambulab': 'orcaslicer-bambulab',
|
|
65
|
+
'fulu-orca': 'orcaslicer-bambulab',
|
|
66
|
+
'fulu-orcaslicer': 'orcaslicer-bambulab',
|
|
67
|
+
'fulu-orca-slicer': 'orcaslicer-bambulab',
|
|
68
|
+
'orca-studio': 'orcaslicer-bambulab',
|
|
69
|
+
orcastudio: 'orcaslicer-bambulab',
|
|
70
|
+
'orcaslicer-bmcu': 'orcaslicer-bambulab',
|
|
71
|
+
'orca-bmcu': 'orcaslicer-bambulab',
|
|
72
|
+
prusaslicer: 'prusaslicer',
|
|
73
|
+
'prusa-slicer': 'prusaslicer',
|
|
74
|
+
cura: 'cura',
|
|
75
|
+
curaengine: 'cura',
|
|
76
|
+
slic3r: 'slic3r',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function normalizeSlicerType(rawSlicerType: string): SlicerType {
|
|
80
|
+
const key = rawSlicerType.trim().toLowerCase().replace(/_/g, '-');
|
|
81
|
+
const normalized = SLICER_TYPE_ALIASES[key];
|
|
82
|
+
|
|
83
|
+
if (!normalized) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Unsupported slicer_type: "${rawSlicerType}". Valid values: ${SLICER_TYPES.join(', ')}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
|
|
93
|
+
return (
|
|
94
|
+
slicerType === 'bambustudio' ||
|
|
95
|
+
slicerType === 'orcaslicer' ||
|
|
96
|
+
slicerType === 'orcaslicer-bambulab'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPathLikeExecutable(executable: string): boolean {
|
|
101
|
+
return path.isAbsolute(executable) || /[\\/]/.test(executable);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function configuredBambuProfileDirs(): string[] {
|
|
105
|
+
const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
|
|
106
|
+
if (override !== undefined) {
|
|
107
|
+
return override.split(path.delimiter).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const home = os.homedir();
|
|
111
|
+
const macAppSupport = path.join(home, 'Library', 'Application Support');
|
|
112
|
+
const linuxConfig = path.join(home, '.config');
|
|
113
|
+
const appData = process.env.APPDATA;
|
|
114
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
115
|
+
const appNames = [
|
|
116
|
+
'BambuStudio',
|
|
117
|
+
'Bambu Studio',
|
|
118
|
+
'OrcaSlicer',
|
|
119
|
+
'Orca Slicer',
|
|
120
|
+
'OrcaStudio',
|
|
121
|
+
'Orca Studio',
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const roots = [
|
|
125
|
+
macAppSupport,
|
|
126
|
+
linuxConfig,
|
|
127
|
+
...(appData ? [appData] : []),
|
|
128
|
+
...(localAppData ? [localAppData] : []),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return roots.flatMap((root) =>
|
|
132
|
+
appNames.map((appName) => path.join(root, appName, 'system', 'BBL', 'machine'))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveBambuPrinterProfile(printerPreset: string): string | undefined {
|
|
137
|
+
if (printerPreset.includes('/') || printerPreset.includes('\\')) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const profileDir of configuredBambuProfileDirs()) {
|
|
142
|
+
const profilePath = path.join(profileDir, `${printerPreset}.json`);
|
|
143
|
+
if (fs.existsSync(profilePath)) {
|
|
144
|
+
return profilePath;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function generatedBambuPrinterSettings(printerPreset: string): Record<string, string> {
|
|
152
|
+
return {
|
|
153
|
+
type: 'machine',
|
|
154
|
+
from: 'system',
|
|
155
|
+
name: printerPreset,
|
|
156
|
+
inherits: printerPreset,
|
|
157
|
+
printer_settings_id: printerPreset,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Bambu-compatible slicer CLI flags exposed via the slice_stl tool
|
|
44
162
|
export interface BambuSliceOptions {
|
|
45
163
|
uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
|
|
46
164
|
repetitions?: number; // --repetitions N: print N copies
|
|
@@ -983,17 +1101,17 @@ export class STLManipulator extends EventEmitter {
|
|
|
983
1101
|
/**
|
|
984
1102
|
* Slice an STL or 3MF file using the specified slicer
|
|
985
1103
|
* @param stlFilePath Path to the input STL or 3MF file
|
|
986
|
-
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
|
|
1104
|
+
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
|
|
987
1105
|
* @param slicerPath Path to the slicer executable
|
|
988
1106
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
989
1107
|
* @param progressCallback Optional callback for progress updates
|
|
990
|
-
* @param printerPreset Optional
|
|
991
|
-
* @param bambuOptions Optional
|
|
1108
|
+
* @param printerPreset Optional Bambu-compatible printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
|
|
1109
|
+
* @param bambuOptions Optional Bambu-compatible CLI flags
|
|
992
1110
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
993
1111
|
*/
|
|
994
1112
|
async sliceSTL(
|
|
995
1113
|
stlFilePath: string,
|
|
996
|
-
slicerType:
|
|
1114
|
+
slicerType: SlicerType,
|
|
997
1115
|
slicerPath: string,
|
|
998
1116
|
slicerProfile?: string,
|
|
999
1117
|
progressCallback?: ProgressCallback,
|
|
@@ -1003,7 +1121,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1003
1121
|
const operationId = this.generateOperationId();
|
|
1004
1122
|
this.activeOperations.set(operationId, true);
|
|
1005
1123
|
|
|
1006
|
-
if (!fs.existsSync(slicerPath)) {
|
|
1124
|
+
if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
|
|
1007
1125
|
throw new Error(`Slicer executable not found at: ${slicerPath}`);
|
|
1008
1126
|
}
|
|
1009
1127
|
if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
@@ -1032,25 +1150,6 @@ export class STLManipulator extends EventEmitter {
|
|
|
1032
1150
|
args = args.filter(arg => arg !== '');
|
|
1033
1151
|
break;
|
|
1034
1152
|
|
|
1035
|
-
case 'orcaslicer': // Add OrcaSlicer case
|
|
1036
|
-
args = [
|
|
1037
|
-
// OrcaSlicer might use --load-settings like Bambu, or --load like Prusa
|
|
1038
|
-
// Assuming --load-settings based on discussion, adjust if needed.
|
|
1039
|
-
// Also assuming profile path points to the main .ini or .json config.
|
|
1040
|
-
'--load-settings', slicerProfile || '',
|
|
1041
|
-
'--output', outputFilePath, // Assuming --output works like PrusaSlicer
|
|
1042
|
-
stlFilePath
|
|
1043
|
-
];
|
|
1044
|
-
// Alternative if --output doesn't specify filename:
|
|
1045
|
-
// args = [
|
|
1046
|
-
// '--load-settings', slicerProfile || '',
|
|
1047
|
-
// '--outputdir', this.tempDir,
|
|
1048
|
-
// stlFilePath
|
|
1049
|
-
// ];
|
|
1050
|
-
// Remove empty profile arg if not provided
|
|
1051
|
-
args = args.filter(arg => arg !== '');
|
|
1052
|
-
break;
|
|
1053
|
-
|
|
1054
1153
|
case 'cura':
|
|
1055
1154
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
1056
1155
|
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
|
@@ -1067,7 +1166,12 @@ export class STLManipulator extends EventEmitter {
|
|
|
1067
1166
|
break;
|
|
1068
1167
|
|
|
1069
1168
|
case 'bambustudio':
|
|
1070
|
-
|
|
1169
|
+
case 'orcaslicer':
|
|
1170
|
+
case 'orcaslicer-bambulab':
|
|
1171
|
+
// Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
|
|
1172
|
+
// FULU's OrcaSlicer-bambulab fork keeps OrcaSlicer's Bambu-style
|
|
1173
|
+
// --slice/--export-3mf path, so it must not use the generic Prusa
|
|
1174
|
+
// --output G-code flow.
|
|
1071
1175
|
// For 3MF input: slice in place and export sliced 3MF
|
|
1072
1176
|
// For STL input: slice and export as 3MF
|
|
1073
1177
|
{
|
|
@@ -1085,15 +1189,21 @@ export class STLManipulator extends EventEmitter {
|
|
|
1085
1189
|
args.push('--allow-newer-file');
|
|
1086
1190
|
|
|
1087
1191
|
// If a printer preset name is given AND no explicit profile was provided,
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1192
|
+
// prefer the slicer's installed machine profile. Real BambuStudio rejects
|
|
1193
|
+
// skeletal JSON without profile metadata, so only fall back to generated
|
|
1194
|
+
// settings when no installed profile can be found.
|
|
1090
1195
|
if (printerPreset && !slicerProfile) {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1196
|
+
const installedProfile = resolveBambuPrinterProfile(printerPreset);
|
|
1197
|
+
if (installedProfile) {
|
|
1198
|
+
args.push('--load-settings', installedProfile);
|
|
1199
|
+
} else {
|
|
1200
|
+
const presetJson = path.join(this.tempDir, '_printer_preset.json');
|
|
1201
|
+
fs.writeFileSync(presetJson, JSON.stringify(generatedBambuPrinterSettings(printerPreset), null, 2));
|
|
1202
|
+
args.push('--load-settings', presetJson);
|
|
1203
|
+
}
|
|
1094
1204
|
}
|
|
1095
1205
|
|
|
1096
|
-
//
|
|
1206
|
+
// Bambu-compatible slicer flags
|
|
1097
1207
|
if (bambuOptions?.uptodate) args.push('--uptodate');
|
|
1098
1208
|
if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
|
|
1099
1209
|
if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
|
|
@@ -1113,7 +1223,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1113
1223
|
if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
|
|
1114
1224
|
|
|
1115
1225
|
args.push(stlFilePath);
|
|
1116
|
-
// Override outputFilePath
|
|
1226
|
+
// Override outputFilePath because Bambu-compatible slicers produce 3MF, not G-code.
|
|
1117
1227
|
outputFilePath = bambuOutputPath;
|
|
1118
1228
|
}
|
|
1119
1229
|
break;
|