mcp-3d-printer-server 1.2.25 → 1.2.27
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 +67 -0
- package/CONTRIBUTORS.md +5 -1
- package/README.md +64 -29
- package/dist/ams-mapping.d.ts +38 -0
- package/dist/ams-mapping.js +125 -0
- package/dist/bambu-network-bridge.d.ts +211 -0
- package/dist/bambu-network-bridge.js +918 -0
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/control-reply.d.ts +21 -0
- package/dist/control-reply.js +44 -0
- package/dist/counterpart/camera-sources.d.ts +16 -0
- package/dist/counterpart/camera-sources.js +138 -0
- package/dist/counterpart/camera-stream.d.ts +66 -0
- package/dist/counterpart/camera-stream.js +246 -0
- package/dist/counterpart/camera-tools.d.ts +247 -0
- package/dist/counterpart/camera-tools.js +35 -0
- package/dist/counterpart/contract.d.ts +21 -5
- package/dist/counterpart/contract.js +8 -2
- package/dist/counterpart/route.d.ts +31 -0
- package/dist/counterpart/route.js +70 -0
- package/dist/counterpart/service.d.ts +60 -4
- package/dist/counterpart/service.js +142 -16
- package/dist/design-guidance.d.ts +26 -0
- package/dist/design-guidance.js +177 -0
- package/dist/fulu-orca.d.ts +7 -0
- package/dist/fulu-orca.js +18 -2
- package/dist/index.js +514 -70
- package/dist/printers/bambu-filaments.d.ts +68 -0
- package/dist/printers/bambu-filaments.js +262 -0
- package/dist/printers/bambu.d.ts +99 -6
- package/dist/printers/bambu.js +478 -72
- package/dist/printers/creality.d.ts +2 -1
- package/dist/printers/creality.js +5 -1
- package/dist/printers/generic-base.d.ts +11 -6
- package/dist/printers/generic-base.js +21 -25
- package/dist/printers/klipper.d.ts +2 -1
- package/dist/printers/klipper.js +10 -7
- package/dist/printers/octoprint.d.ts +2 -1
- package/dist/printers/octoprint.js +15 -8
- package/dist/printers/printer-factory.d.ts +1 -1
- package/dist/printers/printer-factory.js +3 -3
- package/dist/printers/prusa.js +0 -1
- package/dist/safety/confirmation.d.ts +11 -16
- package/dist/safety/confirmation.js +8 -32
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +8 -2
- package/dist/safety/limits.js +14 -5
- package/dist/safety/print-file.d.ts +5 -0
- package/dist/safety/print-file.js +156 -19
- package/dist/safety/printer-state.d.ts +68 -8
- package/dist/safety/printer-state.js +187 -40
- package/dist/slicer/bambu-cli.d.ts +2 -0
- package/dist/slicer/bambu-cli.js +7 -5
- package/dist/slicer/layer-pauses.d.ts +47 -0
- package/dist/slicer/layer-pauses.js +130 -0
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +40 -0
- package/dist/studio-print-options.d.ts +57 -0
- package/dist/studio-print-options.js +179 -0
- package/dist/tool-annotations.d.ts +25 -0
- package/dist/tool-annotations.js +58 -0
- package/package.json +27 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1106 -0
- package/src/bambu-network-certs.ts +154 -0
- package/src/control-reply.ts +41 -0
- package/src/counterpart/camera-sources.ts +102 -0
- package/src/counterpart/camera-stream.ts +195 -0
- package/src/counterpart/camera-tools.ts +35 -0
- package/src/counterpart/contract.ts +12 -4
- package/src/counterpart/route.ts +82 -0
- package/src/counterpart/service.ts +150 -16
- package/src/design-guidance.ts +188 -0
- package/src/fulu-orca.ts +18 -2
- package/src/index.ts +528 -83
- package/src/printers/bambu-filaments.ts +346 -0
- package/src/printers/bambu.ts +506 -81
- package/src/printers/creality.ts +8 -3
- package/src/printers/generic-base.ts +33 -40
- package/src/printers/klipper.ts +12 -8
- package/src/printers/octoprint.ts +16 -9
- package/src/printers/printer-factory.ts +3 -3
- package/src/printers/prusa.ts +0 -1
- package/src/safety/confirmation.ts +15 -38
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +15 -5
- package/src/safety/print-file.ts +139 -17
- package/src/safety/printer-state.ts +192 -41
- package/src/slicer/bambu-cli.ts +8 -5
- package/src/slicer/layer-pauses.ts +143 -0
- package/src/stl/stl-manipulator.ts +45 -0
- package/src/studio-print-options.ts +201 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readBoundedPrintFile, loadSafe3mfArchive } from './archive.js';
|
|
3
|
-
import {
|
|
3
|
+
import { projectFilaments } from "../ams-mapping.js";
|
|
4
|
+
import { MACHINE_LIMITS, isDualNozzleModel, normalizeMaterial, normalizeModel, validateStartupPurgeTemperature, validateTemperature } from './limits.js';
|
|
4
5
|
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
5
6
|
const fail = (message) => { throw new Error(`Print safety: ${message}`); };
|
|
6
7
|
function list(value) {
|
|
@@ -35,6 +36,67 @@ function consistent(values, convert, field) {
|
|
|
35
36
|
return fail(`contradictory ${field} metadata`);
|
|
36
37
|
return converted[0];
|
|
37
38
|
}
|
|
39
|
+
/** Bambu Studio keeps filament options per variant in the project, but writes
|
|
40
|
+
* one selected row per filament in the plate header (Print::full_print_config).
|
|
41
|
+
* Match the declared 1-based self index and variant, never truncate the table
|
|
42
|
+
* or borrow the slicer's row-zero fallback for a missing selection.
|
|
43
|
+
*/
|
|
44
|
+
function firstLayerTemperatures(project, headers, count) {
|
|
45
|
+
const key = 'nozzle_temperature_initial_layer';
|
|
46
|
+
const field = 'first-layer nozzle temperature';
|
|
47
|
+
const temperatures = (value) => {
|
|
48
|
+
const entries = list(value);
|
|
49
|
+
return entries.length && entries.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? entries.map(Number) : undefined;
|
|
50
|
+
};
|
|
51
|
+
const perFilament = (value) => {
|
|
52
|
+
const values = temperatures(value);
|
|
53
|
+
return values?.length === count ? values : undefined;
|
|
54
|
+
};
|
|
55
|
+
const selectedValues = metadataValues(headers, [key]);
|
|
56
|
+
if (project[key] === undefined)
|
|
57
|
+
return consistent(selectedValues, perFilament, field);
|
|
58
|
+
if (project.filament_extruder_variant === undefined && project.filament_self_index === undefined)
|
|
59
|
+
return consistent([project[key], ...selectedValues], perFilament, field);
|
|
60
|
+
const table = temperatures(project[key]);
|
|
61
|
+
const variants = list(project.filament_extruder_variant);
|
|
62
|
+
const ids = list(project.filament_self_index);
|
|
63
|
+
const validVariant = (value) => /^(Direct Drive|Bowden) (Standard|High Flow|Hybrid|TPU High Flow|E3D High Flow)$/.test(value);
|
|
64
|
+
if (!table || variants.length !== table.length || ids.length !== table.length ||
|
|
65
|
+
variants.some(v => !validVariant(v)) || ids.some(id => !/^[1-9]\d*$/.test(id) || Number(id) > count))
|
|
66
|
+
return fail(`unknown or malformed ${field} variant table`);
|
|
67
|
+
const selectedVariants = consistent(metadataValues(headers, ['filament_extruder_variant']), value => {
|
|
68
|
+
const values = list(value);
|
|
69
|
+
return values.length === count && values.every(validVariant) ? values : undefined;
|
|
70
|
+
}, 'selected filament extruder variant');
|
|
71
|
+
const selectedIds = consistent(metadataValues(headers, ['filament_self_index']), value => {
|
|
72
|
+
const values = list(value);
|
|
73
|
+
return values.length === count && values.every((id, index) => id === String(index + 1)) ? values : undefined;
|
|
74
|
+
}, 'selected filament self index');
|
|
75
|
+
// These are logical extruders: physical_extruder_map is used for spool
|
|
76
|
+
// routing, not this variant lookup. Hybrid selects the Standard variant.
|
|
77
|
+
const mappingKeys = ['filament_map', 'extruder_type', 'filament_volume_map'];
|
|
78
|
+
if (mappingKeys.every(key => metadataValues(headers, [key]).length)) {
|
|
79
|
+
const selection = (key) => consistent(metadataValues(headers, [key]), value => {
|
|
80
|
+
const entries = list(value);
|
|
81
|
+
return entries.length ? entries : undefined;
|
|
82
|
+
}, `selected ${key.replace(/_/g, ' ')}`);
|
|
83
|
+
const map = selection('filament_map');
|
|
84
|
+
const types = selection('extruder_type');
|
|
85
|
+
const volumes = selection('filament_volume_map');
|
|
86
|
+
const flows = { '0': 'Standard', '1': 'High Flow', '2': 'Standard', '3': 'TPU High Flow', '5': 'E3D High Flow' };
|
|
87
|
+
if (map.length !== count || volumes.length !== count || map.some(id => !/^[1-9]\d*$/.test(id) || Number(id) > types.length) ||
|
|
88
|
+
volumes.some(v => !Object.prototype.hasOwnProperty.call(flows, v)) || types.some(v => !['Direct Drive', 'Bowden'].includes(v)) ||
|
|
89
|
+
selectedVariants.some((variant, i) => variant !== `${types[Number(map[i]) - 1]} ${flows[volumes[i]]}`))
|
|
90
|
+
return fail('contradictory selected filament extruder variant mapping');
|
|
91
|
+
}
|
|
92
|
+
const resolved = selectedIds.map((id, index) => {
|
|
93
|
+
const rows = ids.flatMap((candidate, row) => candidate === id && variants[row] === selectedVariants[index] ? [row] : []);
|
|
94
|
+
if (rows.length !== 1)
|
|
95
|
+
return fail(`ambiguous or missing ${field} variant for filament ${id}`);
|
|
96
|
+
return table[rows[0]];
|
|
97
|
+
});
|
|
98
|
+
return consistent([resolved, ...selectedValues], perFilament, field);
|
|
99
|
+
}
|
|
38
100
|
function stripComments(line) {
|
|
39
101
|
let clean = '';
|
|
40
102
|
let depth = 0;
|
|
@@ -62,6 +124,11 @@ function stripComments(line) {
|
|
|
62
124
|
/** A deliberately narrow parameter lexer for commands affecting heat/material selection.
|
|
63
125
|
* Unknown nonthermal vendor commands do not require a global G-code allowlist.
|
|
64
126
|
*/
|
|
127
|
+
/** True when the parameter is present, finite and within [low, high]. */
|
|
128
|
+
function inRange(args, key, low, high) {
|
|
129
|
+
const value = args.get(key);
|
|
130
|
+
return value !== undefined && Number.isFinite(value) && value >= low && value <= high;
|
|
131
|
+
}
|
|
65
132
|
function parameters(text, flags = '') {
|
|
66
133
|
const result = new Map();
|
|
67
134
|
let rest = text.trim();
|
|
@@ -163,6 +230,7 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
163
230
|
const model = normalizeModel(options.model);
|
|
164
231
|
if (!model)
|
|
165
232
|
return fail(`unknown or missing printer model '${options.model}'`);
|
|
233
|
+
const dualNozzle = isDualNozzleModel(model);
|
|
166
234
|
const plateIndex = options.plateIndex ?? 0;
|
|
167
235
|
if (!Number.isInteger(plateIndex) || plateIndex < 0)
|
|
168
236
|
return fail('selected plate index must be a nonnegative integer');
|
|
@@ -303,6 +371,24 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
303
371
|
const values = list(value);
|
|
304
372
|
return values.length && values.every(v => !!normalizeMaterial(v)) ? values.map(v => normalizeMaterial(v)) : undefined;
|
|
305
373
|
}, 'filament material');
|
|
374
|
+
// GCode.cpp uses 1-based filament_map logical extruders and then indexes
|
|
375
|
+
// physical_extruder_map to select the physical nozzle. Only this selected
|
|
376
|
+
// plate's headers establish that binding; unsliced project defaults do not.
|
|
377
|
+
const headerMap = (key, count, low, high) => {
|
|
378
|
+
const values = metadataValues(headers, [key]);
|
|
379
|
+
if (!values.length)
|
|
380
|
+
return undefined;
|
|
381
|
+
return consistent(values, value => {
|
|
382
|
+
const entries = list(value);
|
|
383
|
+
return entries.length === count && entries.every(v => NUMBER.test(v) && Number.isInteger(Number(v)) && Number(v) >= low && Number(v) <= high)
|
|
384
|
+
? entries.map(Number) : undefined;
|
|
385
|
+
}, key.replace(/_/g, ' '));
|
|
386
|
+
};
|
|
387
|
+
const physicalExtruderMap = headerMap('physical_extruder_map', nozzleDiameters.length, 0, dualNozzle ? Math.min(nozzleDiameters.length, 2) - 1 : nozzleDiameters.length - 1);
|
|
388
|
+
if (physicalExtruderMap && new Set(physicalExtruderMap).size !== physicalExtruderMap.length)
|
|
389
|
+
fail('physical extruder map must be a complete permutation');
|
|
390
|
+
const filamentMap = headerMap('filament_map', materials.length, 1, nozzleDiameters.length);
|
|
391
|
+
const filamentNozzleIndices = filamentMap && physicalExtruderMap ? filamentMap.map(logical => physicalExtruderMap[logical - 1]) : undefined;
|
|
306
392
|
const bedValues = metadataValues([...metadata, plate], ['curr_bed_type', 'bed_type']);
|
|
307
393
|
const bedType = bedValues.length ? consistent(bedValues, normalizeBed, 'bed type') : undefined;
|
|
308
394
|
if (options.bedType && (!bedType || normalizeBed(options.bedType) !== bedType))
|
|
@@ -365,17 +451,25 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
365
451
|
let line = stripComments(lines[index]);
|
|
366
452
|
if (!line || line === '%')
|
|
367
453
|
continue;
|
|
368
|
-
//
|
|
454
|
+
// Accept only the exact vendor banners in their documented model/scope.
|
|
455
|
+
// X2D emits its filament-change banner throughout a print; H2C's record
|
|
456
|
+
// separator stays inside its explicitly bounded startup block.
|
|
457
|
+
if (line === '======== X2D filament_change gcode ==========' && model === 'x2d')
|
|
458
|
+
continue;
|
|
369
459
|
if (line === '========== record data ==========' && h2cStartup(index))
|
|
370
460
|
continue;
|
|
371
461
|
line = line.replace(/^N\d+\s*/i, '').replace(/\*\d+\s*$/, '');
|
|
372
462
|
if (/[{}\[\]#]/.test(line))
|
|
373
463
|
fail('unresolved dynamic command syntax');
|
|
374
464
|
if (/^SYNC(?=$|\s)/i.test(line)) {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// vendor template establishes no independent maximum duration.
|
|
465
|
+
// Vendor SYNC T is a duration, not a heater target. X2D and P2S
|
|
466
|
+
// startup templates additionally emit R0; no other R form is supported.
|
|
378
467
|
const args = parameters(line.slice(4));
|
|
468
|
+
if (args.has('R')) {
|
|
469
|
+
if (!['x2d', 'p2s'].includes(model) || args.get('R') !== 0)
|
|
470
|
+
fail('unsupported SYNC reset parameters');
|
|
471
|
+
args.delete('R');
|
|
472
|
+
}
|
|
379
473
|
if (args.size !== 1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T') < 0)
|
|
380
474
|
fail('unsupported SYNC duration parameters');
|
|
381
475
|
commandCount++;
|
|
@@ -388,6 +482,33 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
388
482
|
const code = command[1].toUpperCase() + String(Number(base)) + (subcode === undefined ? '' : '.' + subcode);
|
|
389
483
|
const argumentsText = line.slice(command[0].length).trim();
|
|
390
484
|
commandCount++;
|
|
485
|
+
if (code === 'M9711') {
|
|
486
|
+
// Official X2D, H2D and H2C time-lapse capture (Bambu Studio and OrcaSlicer
|
|
487
|
+
// templates), emitted on every layer: M is the time-lapse mode and E the
|
|
488
|
+
// physical extruder, not a second command; T is a duration, not a heater.
|
|
489
|
+
// It sets no temperature. Positions stay inside the machine volume.
|
|
490
|
+
const invalid = () => fail('unsupported M9711 time-lapse parameters');
|
|
491
|
+
if (!['x2d', 'h2d', 'h2c'].includes(model))
|
|
492
|
+
invalid();
|
|
493
|
+
let args;
|
|
494
|
+
try {
|
|
495
|
+
args = parameters(argumentsText);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return invalid();
|
|
499
|
+
}
|
|
500
|
+
const [width, depth, height] = MACHINE_LIMITS[model].volume;
|
|
501
|
+
const position = ['X', 'Y', 'U', 'V'].filter(key => args.has(key)).join('');
|
|
502
|
+
const limit = { X: width, U: width, Y: depth, V: depth };
|
|
503
|
+
if ([...args.keys()].some(key => !'MEXYUVZSCOT'.includes(key)) ||
|
|
504
|
+
// These models have two physical extruders, whatever the file declares.
|
|
505
|
+
![0, 1].includes(args.get('M')) || !Number.isInteger(args.get('E')) || !inRange(args, 'E', 0, Math.min(nozzleDiameters.length, 2) - 1) ||
|
|
506
|
+
args.get('S') !== 11 || args.get('C') !== 10 || args.get('O') !== 0 || !inRange(args, 'T', 0, 600000) ||
|
|
507
|
+
!inRange(args, 'Z', 0, height) ||
|
|
508
|
+
!['', 'XY', 'UV'].includes(position) || [...position].some(key => !inRange(args, key, 0, limit[key])))
|
|
509
|
+
invalid();
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
391
512
|
if (!['M117', 'M118', 'M1002', 'M1006', 'M900', 'M970', 'M983.1', 'G383'].includes(code) && /[GM]\s*\d/i.test(argumentsText))
|
|
392
513
|
fail('multiple commands on one line are unsupported');
|
|
393
514
|
// Coordinated extrusion closes the startup-only allowance even when the
|
|
@@ -411,7 +532,15 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
411
532
|
continue;
|
|
412
533
|
}
|
|
413
534
|
if (['M104', 'M109', 'M140', 'M190', 'M141', 'M191'].includes(code)) {
|
|
414
|
-
const args = parameters(argumentsText, 'A');
|
|
535
|
+
const args = parameters(argumentsText, code === 'M104' ? 'AM' : 'A');
|
|
536
|
+
// Studio 2.9's GCodeWriter marks layer-temperature changes with bare M.
|
|
537
|
+
// Accept only its exact S-target / optional T form; targets and tool
|
|
538
|
+
// selectors still pass through the independent thermal checks below.
|
|
539
|
+
if (args.has('M')) {
|
|
540
|
+
if (!Number.isNaN(args.get('M')) || !args.has('S') || [...args.keys()].some(key => !'MST'.includes(key)))
|
|
541
|
+
fail('unsupported M104 layer-change temperature parameters');
|
|
542
|
+
args.delete('M');
|
|
543
|
+
}
|
|
415
544
|
// Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
|
|
416
545
|
// (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
|
|
417
546
|
// checked target, so H is accepted only within those vendor bounds.
|
|
@@ -421,10 +550,7 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
421
550
|
// editable maxima or an unchecked offset as thermal evidence.
|
|
422
551
|
if (code !== 'M104' || !h2cStartup(index) || args.size !== 2 || args.get('O') !== -80 || !Number.isNaN(args.get('A')))
|
|
423
552
|
fail('unsupported H2C startup offset parameters');
|
|
424
|
-
const targets =
|
|
425
|
-
const values = list(value);
|
|
426
|
-
return values.length === materials.length && values.every(v => NUMBER.test(v) && Number.isFinite(Number(v))) ? values.map(Number) : undefined;
|
|
427
|
-
}, 'first-layer nozzle temperature');
|
|
553
|
+
const targets = firstLayerTemperatures(project, headers, materials.length);
|
|
428
554
|
for (let position = 0; position < targets.length; position++) {
|
|
429
555
|
validateTemperature('nozzle', targets[position], model, [materials[position]]);
|
|
430
556
|
const preheat = Math.max(0, targets[position] - 80);
|
|
@@ -441,9 +567,16 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
441
567
|
continue;
|
|
442
568
|
}
|
|
443
569
|
const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
|
|
570
|
+
// Bambu Studio addresses a physical nozzle on dual-nozzle machines with N<index>
|
|
571
|
+
// (X2D `M104 T1 S25 N0` multi-extruder pre-cooling and wipe-tower reheats).
|
|
572
|
+
// N selects a nozzle, never a target, so it is accepted only on those models,
|
|
573
|
+
// only for nozzle heaters, and only within the declared nozzle count.
|
|
574
|
+
const nozzleIndex = ['M104', 'M109'].includes(code) && dualNozzle;
|
|
444
575
|
for (const key of args.keys())
|
|
445
|
-
if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
|
|
576
|
+
if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait) && !(key === 'N' && nozzleIndex))
|
|
446
577
|
fail(`unsupported ${code} temperature parameter ${key}`);
|
|
578
|
+
if (args.has('N') && (!Number.isInteger(args.get('N')) || args.get('N') < 0 || args.get('N') >= Math.min(nozzleDiameters.length, 2)))
|
|
579
|
+
fail(`unsupported ${code} nozzle index N`);
|
|
447
580
|
if (args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H') < 0 || args.get('H') > 300))
|
|
448
581
|
fail(`unsupported ${code} wait parameter H`);
|
|
449
582
|
if (!args.has('S') && !args.has('R'))
|
|
@@ -453,13 +586,13 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
453
586
|
// Without a verified per-filament nozzle map, apply all possible materials.
|
|
454
587
|
if (args.has('T') && (!Number.isInteger(args.get('T')) || args.get('T') < 0 || args.get('T') >= nozzleDiameters.length))
|
|
455
588
|
fail('physical heater target has no declared nozzle');
|
|
456
|
-
const position = args.has('T') ? undefined : active;
|
|
589
|
+
const position = args.has('T') || args.has('N') ? undefined : active;
|
|
457
590
|
const commonFlush = code === 'M109' && args.size === 1 && args.get('S') === 290 && model === 'x1e' && !depositionStarted && !commonFlushUsed && boundedX1ePurge(lines, index);
|
|
458
591
|
if (commonFlush)
|
|
459
592
|
commonFlushUsed = true;
|
|
460
593
|
for (const key of ['S', 'R'])
|
|
461
594
|
if (args.has(key))
|
|
462
|
-
heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
|
|
595
|
+
heat(component, args.get(key), position, args.has('T') || args.has('N') || args.has('A'), commonFlush);
|
|
463
596
|
}
|
|
464
597
|
else if (code === 'M620' || code === 'M621') {
|
|
465
598
|
const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : model === 'h2c' ? 'MAN' : 'MA');
|
|
@@ -519,9 +652,10 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
519
652
|
}
|
|
520
653
|
else if (code === 'M620.14') {
|
|
521
654
|
const args = parameters(argumentsText);
|
|
522
|
-
// H2C
|
|
523
|
-
|
|
524
|
-
|
|
655
|
+
// H2C uses these coordinates during startup and a selected filament change.
|
|
656
|
+
// The rear wipe area extends outside object bounds (official fallback Y336).
|
|
657
|
+
if (!(h2cStartup(index) || (model === 'h2c' && pending !== undefined)) || args.size !== 2 || !['X', 'Y'].every(key => args.has(key) && Number.isFinite(args.get(key)) && args.get(key) >= 0 && args.get(key) <= 350))
|
|
658
|
+
fail('unsupported H2C tower parameters');
|
|
525
659
|
}
|
|
526
660
|
else if (model === 'h2c' && ['G383', 'G383.3', 'G383.7'].includes(code) && /U/i.test(argumentsText)) {
|
|
527
661
|
const args = parameters(argumentsText);
|
|
@@ -563,9 +697,12 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
563
697
|
// H2D change_filament_gcode supplies the incoming filament's cooling
|
|
564
698
|
// temperature as C{new_filament_temp - filament_cooling_before_tower}.
|
|
565
699
|
const args = parameters(argumentsText);
|
|
566
|
-
|
|
700
|
+
// H2C also supplies P{filament_pre_cooling_temperature_nc[next_filament_id]}
|
|
701
|
+
// before switching hotends. P and C both constrain the incoming material.
|
|
702
|
+
const target = model === 'h2c' && args.has('P') ? 'P' : 'C';
|
|
703
|
+
if (args.size !== 1 || !args.has(target))
|
|
567
704
|
fail('unsupported M620.15 cooling temperature parameters');
|
|
568
|
-
heat('nozzle', args.get(
|
|
705
|
+
heat('nozzle', args.get(target), pending ?? active);
|
|
569
706
|
}
|
|
570
707
|
else if (code === 'M620.17') {
|
|
571
708
|
const args = parameters(argumentsText);
|
|
@@ -620,5 +757,5 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
620
757
|
return fail('selected G-code contains no printable commands');
|
|
621
758
|
if (!used.size)
|
|
622
759
|
materials.forEach((_, index) => used.add(index));
|
|
623
|
-
return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows };
|
|
760
|
+
return { model, nozzleDiameters, materials, usedFilamentPositions: [...used].sort((a, b) => a - b), plateInternalPath, sha256, maxNozzleTemperature, maxBedTemperature, maxChamberTemperature, bedType, selectsAms, nozzleTypes, nozzleFlows, filamentNozzleIndices, ...(Array.isArray(project.filament_type) ? { projectFilaments: projectFilaments(project) } : {}) };
|
|
624
761
|
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { BambuClient } from "bambu-node";
|
|
2
|
-
|
|
3
|
-
* Publish one MQTT command with a string sequence id and no ACK wait.
|
|
4
|
-
* Unpatched bambu-node 3.22.21 command classes send numeric sequence ids;
|
|
5
|
-
* bambu-printer-mcp patches them to strings for H2-series firmware. This
|
|
6
|
-
* server does not install that patch, so safety-relevant commands are
|
|
7
|
-
* published here with the patched wire shape: {category:{sequence_id,command,...}}.
|
|
8
|
-
*/
|
|
9
|
-
export declare function publishBambuCommand(printer: Pick<BambuClient, "publish">, category: "print" | "pushing" | "info", command: string, extra?: Record<string, unknown>): Promise<void>;
|
|
2
|
+
export declare const SERIAL_MODELS: Record<string, string>;
|
|
10
3
|
/** Only raw reports received after this request are evidence. Never read printer.data here. */
|
|
11
4
|
export declare function readFreshPrinterStatus(printer: BambuClient, serial: string, timeoutMs?: number): Promise<any>;
|
|
12
5
|
export interface PrinterStateRequirements {
|
|
@@ -14,6 +7,8 @@ export interface PrinterStateRequirements {
|
|
|
14
7
|
nozzleDiameters: number[];
|
|
15
8
|
materials?: string[];
|
|
16
9
|
usedFilamentPositions?: number[];
|
|
10
|
+
/** Verified physical nozzle index for each project filament, composed from the inspected plate mappings. */
|
|
11
|
+
filamentNozzleIndices?: number[];
|
|
17
12
|
amsMapping?: number[];
|
|
18
13
|
useAMS?: boolean;
|
|
19
14
|
requireIdle?: boolean;
|
|
@@ -44,10 +39,75 @@ export interface ValidatedPrinterState {
|
|
|
44
39
|
reportedNozzles: ReportedNozzle[];
|
|
45
40
|
filaments: ReportedFilament[];
|
|
46
41
|
}
|
|
42
|
+
export declare function finiteNumber(value: unknown): number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* X2D retains FAILED after a canceled job even with no active print error.
|
|
45
|
+
* Bambu Studio classifies FAILED as a finished job; its v02.08.02.61
|
|
46
|
+
* hms_en_20P table (926a7192574bcb9b3a732e1ec59a46d79cb45466)
|
|
47
|
+
* calls 0300400C "The task was canceled."
|
|
48
|
+
* Only this observed terminal condition qualifies, never an arbitrary failure.
|
|
49
|
+
* Callers still validate HMS, identity, hardware and material configuration,
|
|
50
|
+
* and print dispatch rechecks the identity of the observed job.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isCancelledX2DJob(raw: Record<string, any>, model: string): boolean;
|
|
53
|
+
/** Bambu Studio 2.8.2.61 marks FAILED as finished. Limit P1S recovery to
|
|
54
|
+
* a named prior job explicitly reporting idle, no print error and no HMS.
|
|
55
|
+
* Dispatch rechecks the observed job identity. */
|
|
56
|
+
export declare function isFinishedP1SJob(raw: Record<string, any>, model: string): boolean;
|
|
47
57
|
/** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
|
|
48
58
|
export declare function unchippedNozzles(raw: unknown): number[];
|
|
59
|
+
export type HmsEntry = {
|
|
60
|
+
attr: number;
|
|
61
|
+
code: number;
|
|
62
|
+
severity: number;
|
|
63
|
+
hex: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function hmsCode(entry: any): HmsEntry | undefined;
|
|
66
|
+
/** The one key format for a reported entry, shared by the gate and clear_hms_errors. */
|
|
67
|
+
export declare function reportedHmsKey(entry: unknown): string;
|
|
68
|
+
/** Bambu's display form of a cleared-code key such as "hms:83887616:131184"; other keys unchanged. */
|
|
69
|
+
export declare function describeHmsKey(key: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* A serious or common entry that Bambu lists for this serial prefix with empty
|
|
72
|
+
* text: Bambu Studio, Bambu Handy and the printer screen never show it, and
|
|
73
|
+
* the printer keeps printing with it (an X2D raises 0500-0600-0002-0070 at
|
|
74
|
+
* every boot). Bambu Studio selects the table by serial prefix, so this does
|
|
75
|
+
* too, but only when that prefix belongs to the verified model: a sparse report
|
|
76
|
+
* without a serial must not let a misconfigured serial pick another model's
|
|
77
|
+
* table. Fatal, invalid-level and unlisted entries never qualify.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isHiddenNotice(serial: string, model: string, entry: HmsEntry): boolean;
|
|
80
|
+
/** Labels of reported entries that pass only because Bambu hides them for this printer. */
|
|
81
|
+
export declare function hiddenHmsNotices(raw: unknown, serial: string, model: string): string[];
|
|
82
|
+
/** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
|
|
83
|
+
export declare function hiddenHmsKeys(raw: unknown, serial: string, model: string): string[];
|
|
84
|
+
/**
|
|
85
|
+
* 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
|
|
86
|
+
* printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
|
|
87
|
+
* which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
|
|
88
|
+
* a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
|
|
89
|
+
* Ported from bambu-printer-mcp src/safety/printer-state.ts.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PAUSE_NOTICE = 50364435;
|
|
92
|
+
export declare function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): boolean;
|
|
49
93
|
/** Verifies reported configuration, not a physical nozzle/spool inspection. */
|
|
50
94
|
export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
|
|
51
95
|
/** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
|
|
52
96
|
export declare function manualHeatingRequirements(status: any, model: string, nozzleDiameter: number, material: string): PrinterStateRequirements;
|
|
97
|
+
export type ExternalStorageState = "none" | "normal" | "abnormal" | "readonly";
|
|
98
|
+
/**
|
|
99
|
+
* The SD card or USB stick state, read as Bambu Studio does: the `sdcard`
|
|
100
|
+
* flag, then `home_flag` bits 8-9, then on firmware that reports cfg/fun/aux/stat
|
|
101
|
+
* the `aux` bits 12-13, each later source overriding the earlier one
|
|
102
|
+
* (DevStorage.cpp, DeviceManager.cpp parse_home_flag and parse_new_info).
|
|
103
|
+
*/
|
|
104
|
+
export declare function externalStorageState(raw: unknown): ExternalStorageState;
|
|
105
|
+
/**
|
|
106
|
+
* Publish one MQTT command with a string sequence id and no ACK wait.
|
|
107
|
+
* Unpatched bambu-node 3.22.21 command classes send numeric sequence ids;
|
|
108
|
+
* bambu-printer-mcp patches them to strings for H2-series firmware. This
|
|
109
|
+
* server does not install that patch, so safety-relevant commands are
|
|
110
|
+
* published here with the patched wire shape: {category:{sequence_id,command,...}}.
|
|
111
|
+
*/
|
|
112
|
+
export declare function publishBambuCommand(printer: Pick<BambuClient, "publish">, category: "print" | "pushing" | "info", command: string, extra?: Record<string, unknown>): Promise<void>;
|
|
53
113
|
export {};
|