bambu-printer-mcp 1.1.18 → 1.1.20
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/CONTRIBUTORS.md +1 -0
- package/dist/safety/print-file.js +39 -11
- package/package.json +1 -1
- package/src/safety/print-file.ts +35 -11
package/CONTRIBUTORS.md
CHANGED
|
@@ -6,6 +6,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| [Izzy Mansurov (sapoepsilon)](https://github.com/sapoepsilon) | Fixes Bambu Studio 02.08 P2S extruder-variant metadata and airduct-command inspection in [#33](https://github.com/DMontgomery40/bambu-printer-mcp/pull/33). Supplies regression tests and offline inspection evidence from four real P2S 0.6 mm PLA/PETG jobs and unsafe mutated copies; this contribution does not claim a physical print. |
|
|
9
10
|
| [Boardy (@boardyai)](https://x.com/boardyai) | Raises the nozzle-verification question that informs the added printer safety checks. |
|
|
10
11
|
| Fable red-team review, shared by David Montgomery | Supplies archive-ambiguity reproductions and hardware-safety review findings used for plate binding, upload checks, temperature policy, and human preflight. Network/security suggestions remain outside this hardware-safety release. |
|
|
11
12
|
| [Sebastian (sebas1986)](https://github.com/sebas1986) | Isolates the multi-filament CLI crash with real BambuStudio bisection, contributes per-slot colours and multi-nozzle prime-tower placement, and verifies X2D identification, status, and slicing in [#18](https://github.com/DMontgomery40/bambu-printer-mcp/pull/18). Also isolates FTPS session-host identity and supplies physical X2D USB upload/size/deletion evidence in [#24](https://github.com/DMontgomery40/bambu-printer-mcp/pull/24); that test does not establish MQTT print dispatch. |
|
|
@@ -206,12 +206,13 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
206
206
|
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\ufffd]/.test(source))
|
|
207
207
|
return fail('binary or malformed G-code is not supported');
|
|
208
208
|
const lines = source.split(/\r\n|\n|\r/);
|
|
209
|
-
const
|
|
209
|
+
const headers = [];
|
|
210
210
|
for (const line of lines) {
|
|
211
211
|
const match = line.match(/^\s*;\s*([a-z_][a-z0-9_]*(?:\s+used\s+\[(?:g|mm)\])?)\s*(?:=|:)\s*(.*?)\s*$/i);
|
|
212
212
|
if (match)
|
|
213
|
-
|
|
213
|
+
headers.push({ [match[1].toLowerCase()]: match[2] });
|
|
214
214
|
}
|
|
215
|
+
const metadata = [project, ...headers];
|
|
215
216
|
validateFffJob([...metadata, plate]);
|
|
216
217
|
const declaredModel = consistent(metadataValues(metadata, ['printer_model']), normalizeModel, 'printer model');
|
|
217
218
|
if (declaredModel !== model)
|
|
@@ -234,6 +235,20 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
234
235
|
const entries = list(value).map(entry => entry.toLowerCase().replace(/[\s_-]+/g, '_'));
|
|
235
236
|
return entries.length && entries.every(Boolean) ? entries : undefined;
|
|
236
237
|
};
|
|
238
|
+
// Bambu Studio writes printer_extruder_id/printer_extruder_variant to
|
|
239
|
+
// project_settings.config as the full per-variant table, but to the plate
|
|
240
|
+
// G-code header as the per-physical-extruder selection it sliced with (for
|
|
241
|
+
// example ["1","1","1"] vs "1" on a single-nozzle P2S). Preserve the source:
|
|
242
|
+
// a plate header can never substitute a full table for its selected values.
|
|
243
|
+
const variantTable = (key, rows, convert) => {
|
|
244
|
+
const field = key.replace(/_/g, ' ');
|
|
245
|
+
const tableValues = metadataValues([project], [key]);
|
|
246
|
+
const selectedValues = metadataValues(headers, [key]);
|
|
247
|
+
if (tableValues.some(value => list(value).length !== rows) || selectedValues.some(value => list(value).length !== nozzleDiameters.length))
|
|
248
|
+
return fail(`unknown or malformed ${field} metadata`);
|
|
249
|
+
const table = consistent(tableValues, convert, field);
|
|
250
|
+
return { table, selected: selectedValues.length ? consistent(selectedValues, convert, `selected ${field}`) : undefined };
|
|
251
|
+
};
|
|
237
252
|
const nozzleMetadata = (keys, variantRows = false) => {
|
|
238
253
|
const values = metadataValues(metadata, keys);
|
|
239
254
|
return values.length ? consistent(values, value => {
|
|
@@ -247,24 +262,34 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
247
262
|
// Bambu GUI projects may store nozzle types per extruder variant, while
|
|
248
263
|
// diameters and selected flow types are per physical extruder. Never truncate
|
|
249
264
|
// that variant table or mistake its rows for filament/nozzle positions.
|
|
250
|
-
const ids =
|
|
265
|
+
const { table: ids, selected: selectedIds } = variantTable('printer_extruder_id', entries.length, value => {
|
|
251
266
|
const ids = list(value);
|
|
252
|
-
return ids.length
|
|
253
|
-
}
|
|
267
|
+
return ids.length > 0 && ids.every(id => /^[1-9]\d*$/.test(id) && Number(id) <= nozzleDiameters.length) ? ids.map(Number) : undefined;
|
|
268
|
+
});
|
|
269
|
+
// The selected form names each physical extruder once, in order.
|
|
270
|
+
if (selectedIds && selectedIds.some((id, index) => id !== index + 1))
|
|
271
|
+
return undefined;
|
|
272
|
+
const variants = metadataValues(metadata, ['printer_extruder_variant']).length
|
|
273
|
+
? variantTable('printer_extruder_variant', entries.length, normalizedEntries) : undefined;
|
|
274
|
+
const extruders = variants ? consistent(metadataValues(metadata, ['extruder_type']), normalizedEntries, 'extruder type') : undefined;
|
|
254
275
|
const resolved = [];
|
|
255
276
|
for (let index = 0; index < nozzleDiameters.length; index++) {
|
|
256
277
|
const rows = ids.flatMap((id, row) => id === index + 1 ? [row] : []);
|
|
257
278
|
if (!rows.length)
|
|
258
279
|
return undefined;
|
|
259
|
-
if (
|
|
280
|
+
if (!variants) {
|
|
281
|
+
if (!rows.every(row => entries[row] === entries[rows[0]]))
|
|
282
|
+
return undefined;
|
|
260
283
|
resolved.push(entries[rows[0]]);
|
|
261
284
|
continue;
|
|
262
285
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
286
|
+
// Validate declared variants even when all rows use the same material.
|
|
287
|
+
if (!extruders || extruders.length !== nozzleDiameters.length || !nozzleFlows)
|
|
288
|
+
return undefined;
|
|
289
|
+
const variant = `${extruders[index]}_${nozzleFlows[index]}`;
|
|
290
|
+
if (variants.selected && variants.selected[index] !== variant)
|
|
266
291
|
return undefined;
|
|
267
|
-
const selected = rows.filter(row => variants[row] ===
|
|
292
|
+
const selected = rows.filter(row => variants.table[row] === variant);
|
|
268
293
|
if (selected.length !== 1)
|
|
269
294
|
return undefined;
|
|
270
295
|
resolved.push(entries[selected[0]]);
|
|
@@ -487,8 +512,11 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
487
512
|
fail(`unsupported thermal-affecting filament command ${code}`);
|
|
488
513
|
}
|
|
489
514
|
else if (code === 'M145') {
|
|
515
|
+
// Airduct mode switch (P0 cooling, P1 heating) in the official H2-series,
|
|
516
|
+
// X2D and P2S start/end G-code; it sets no heater target. Other forms and
|
|
517
|
+
// models without a switchable airduct stay rejected.
|
|
490
518
|
const args = parameters(argumentsText);
|
|
491
|
-
if (args.size !== 1 || !args.has('P') || ![0, 1].includes(args.get('P')) || !['h2d', 'h2dpro', 'h2c', 'h2s', 'x2d'].includes(model))
|
|
519
|
+
if (args.size !== 1 || !args.has('P') || ![0, 1].includes(args.get('P')) || !['h2d', 'h2dpro', 'h2c', 'h2s', 'x2d', 'p2s'].includes(model))
|
|
492
520
|
fail('unsupported thermal-affecting M145 parameters');
|
|
493
521
|
}
|
|
494
522
|
else if (code === 'M142') {
|
package/package.json
CHANGED
package/src/safety/print-file.ts
CHANGED
|
@@ -154,11 +154,12 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
154
154
|
if(plateIndex!==0) return fail('raw G-code has only plate index 0');source=bytes.toString('utf8');
|
|
155
155
|
} else return fail('unsupported printable artifact; use a sliced .3mf or textual .gcode file');
|
|
156
156
|
if(/[\x00-\x08\x0b\x0c\x0e-\x1f\ufffd]/.test(source)) return fail('binary or malformed G-code is not supported');
|
|
157
|
-
const lines=source.split(/\r\n|\n|\r/);const
|
|
157
|
+
const lines=source.split(/\r\n|\n|\r/);const headers:Metadata[]=[];
|
|
158
158
|
for(const line of lines) {
|
|
159
159
|
const match=line.match(/^\s*;\s*([a-z_][a-z0-9_]*(?:\s+used\s+\[(?:g|mm)\])?)\s*(?:=|:)\s*(.*?)\s*$/i);
|
|
160
|
-
if(match)
|
|
160
|
+
if(match) headers.push({[match[1].toLowerCase()]:match[2]});
|
|
161
161
|
}
|
|
162
|
+
const metadata:Metadata[]=[project,...headers];
|
|
162
163
|
validateFffJob([...metadata,plate]);
|
|
163
164
|
const declaredModel=consistent(metadataValues(metadata,['printer_model']),normalizeModel,'printer model');
|
|
164
165
|
if(declaredModel!==model) return fail(`file model ${declaredModel} contradicts requested model ${model}`);
|
|
@@ -178,6 +179,17 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
178
179
|
const entries=list(value).map(entry=>entry.toLowerCase().replace(/[\s_-]+/g,'_'));
|
|
179
180
|
return entries.length && entries.every(Boolean)?entries:undefined;
|
|
180
181
|
};
|
|
182
|
+
// Bambu Studio writes printer_extruder_id/printer_extruder_variant to
|
|
183
|
+
// project_settings.config as the full per-variant table, but to the plate
|
|
184
|
+
// G-code header as the per-physical-extruder selection it sliced with (for
|
|
185
|
+
// example ["1","1","1"] vs "1" on a single-nozzle P2S). Preserve the source:
|
|
186
|
+
// a plate header can never substitute a full table for its selected values.
|
|
187
|
+
const variantTable=<T>(key:string,rows:number,convert:(value:unknown)=>T[]|undefined):{table:T[];selected?:T[]}=>{
|
|
188
|
+
const field=key.replace(/_/g,' ');const tableValues=metadataValues([project],[key]);const selectedValues=metadataValues(headers,[key]);
|
|
189
|
+
if(tableValues.some(value=>list(value).length!==rows) || selectedValues.some(value=>list(value).length!==nozzleDiameters.length)) return fail(`unknown or malformed ${field} metadata`);
|
|
190
|
+
const table=consistent(tableValues,convert,field);
|
|
191
|
+
return {table,selected:selectedValues.length?consistent(selectedValues,convert,`selected ${field}`):undefined};
|
|
192
|
+
};
|
|
181
193
|
const nozzleMetadata=(keys:string[],variantRows=false):string[]|undefined=>{
|
|
182
194
|
const values=metadataValues(metadata,keys);
|
|
183
195
|
return values.length ? consistent(values,value=>{
|
|
@@ -188,18 +200,27 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
188
200
|
// Bambu GUI projects may store nozzle types per extruder variant, while
|
|
189
201
|
// diameters and selected flow types are per physical extruder. Never truncate
|
|
190
202
|
// that variant table or mistake its rows for filament/nozzle positions.
|
|
191
|
-
const ids=
|
|
192
|
-
const ids=list(value);return ids.length
|
|
193
|
-
}
|
|
203
|
+
const {table:ids,selected:selectedIds}=variantTable('printer_extruder_id',entries.length,value=>{
|
|
204
|
+
const ids=list(value);return ids.length>0 && ids.every(id=>/^[1-9]\d*$/.test(id) && Number(id)<=nozzleDiameters.length)?ids.map(Number):undefined;
|
|
205
|
+
});
|
|
206
|
+
// The selected form names each physical extruder once, in order.
|
|
207
|
+
if(selectedIds && selectedIds.some((id,index)=>id!==index+1)) return undefined;
|
|
208
|
+
const variants=metadataValues(metadata,['printer_extruder_variant']).length
|
|
209
|
+
? variantTable('printer_extruder_variant',entries.length,normalizedEntries):undefined;
|
|
210
|
+
const extruders=variants ? consistent(metadataValues(metadata,['extruder_type']),normalizedEntries,'extruder type'):undefined;
|
|
194
211
|
const resolved:string[]=[];
|
|
195
212
|
for(let index=0;index<nozzleDiameters.length;index++) {
|
|
196
213
|
const rows=ids.flatMap((id,row)=>id===index+1?[row]:[]);
|
|
197
214
|
if(!rows.length) return undefined;
|
|
198
|
-
if(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
if(!variants) {
|
|
216
|
+
if(!rows.every(row=>entries[row]===entries[rows[0]])) return undefined;
|
|
217
|
+
resolved.push(entries[rows[0]]);continue;
|
|
218
|
+
}
|
|
219
|
+
// Validate declared variants even when all rows use the same material.
|
|
220
|
+
if(!extruders || extruders.length!==nozzleDiameters.length || !nozzleFlows) return undefined;
|
|
221
|
+
const variant=`${extruders[index]}_${nozzleFlows[index]}`;
|
|
222
|
+
if(variants.selected && variants.selected[index]!==variant) return undefined;
|
|
223
|
+
const selected=rows.filter(row=>variants.table[row]===variant);
|
|
203
224
|
if(selected.length!==1) return undefined;
|
|
204
225
|
resolved.push(entries[selected[0]]);
|
|
205
226
|
}
|
|
@@ -336,8 +357,11 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
336
357
|
} else if((/^M620\./.test(code) && !['M620.3','M620.6','M620.11'].includes(code)) || /^M621\./.test(code)) {
|
|
337
358
|
fail(`unsupported thermal-affecting filament command ${code}`);
|
|
338
359
|
} else if(code==='M145') {
|
|
360
|
+
// Airduct mode switch (P0 cooling, P1 heating) in the official H2-series,
|
|
361
|
+
// X2D and P2S start/end G-code; it sets no heater target. Other forms and
|
|
362
|
+
// models without a switchable airduct stay rejected.
|
|
339
363
|
const args=parameters(argumentsText);
|
|
340
|
-
if(args.size!==1 || !args.has('P') || ![0,1].includes(args.get('P')!) || !['h2d','h2dpro','h2c','h2s','x2d'].includes(model)) fail('unsupported thermal-affecting M145 parameters');
|
|
364
|
+
if(args.size!==1 || !args.has('P') || ![0,1].includes(args.get('P')!) || !['h2d','h2dpro','h2c','h2s','x2d','p2s'].includes(model)) fail('unsupported thermal-affecting M145 parameters');
|
|
341
365
|
} else if(code==='M142') {
|
|
342
366
|
// Bambu chamber fan regulation thresholds, not an active heater command.
|
|
343
367
|
const args=parameters(argumentsText);for(const key of ['S','R']) if(args.has(key) && (!Number.isFinite(args.get(key)) || args.get(key)!<0 || args.get(key)!>65)) fail('unsupported chamber fan temperature threshold');
|