mcp-3d-printer-server 1.2.24 → 1.2.26
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 +71 -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 +200 -0
- package/dist/bambu-network-bridge.js +881 -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 +528 -72
- 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 +59 -8
- package/dist/safety/printer-state.js +174 -39
- package/dist/slicer/bambu-cli.d.ts +6 -2
- package/dist/slicer/bambu-cli.js +19 -12
- package/dist/slicer/layer-pauses.d.ts +47 -0
- package/dist/slicer/layer-pauses.js +130 -0
- package/dist/slicer/slicer-error.d.ts +11 -1
- package/dist/slicer/slicer-error.js +16 -2
- package/dist/stl/stl-manipulator.d.ts +27 -1
- package/dist/stl/stl-manipulator.js +216 -76
- package/dist/studio-print-options.d.ts +57 -0
- package/dist/studio-print-options.js +179 -0
- package/package.json +25 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1075 -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 +546 -87
- 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 +179 -40
- package/src/slicer/bambu-cli.ts +22 -13
- package/src/slicer/layer-pauses.ts +143 -0
- package/src/slicer/slicer-error.ts +21 -2
- package/src/stl/stl-manipulator.ts +254 -84
- package/src/studio-print-options.ts +201 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The print options Bambu Studio's Send dialog sends, resolved the way it
|
|
3
|
+
* resolves them, so every print route sends what Bambu's own Print button
|
|
4
|
+
* would. Source: Bambu Studio v02.08.02.61 (commit 926a7192574b), the version
|
|
5
|
+
* the hidden-HMS and stage tables are pinned to:
|
|
6
|
+
*
|
|
7
|
+
* - SelectMachine.cpp:3344-3356 `set_print_config(bed_type,
|
|
8
|
+
* bed_leveling == "on", flow_cali == "on", false, timelapse, true,
|
|
9
|
+
* ext_change_assist, bed_leveling.getValueInt(), flow_cali.getValueInt(),
|
|
10
|
+
* nozzle_offset_cali.getValueInt(), pa_value_switch ? 0 : 1)`. Vibration
|
|
11
|
+
* calibration is always false and layer inspection always true; the
|
|
12
|
+
* parameter order is PrintJob.hpp:98-116.
|
|
13
|
+
* - SelectMachine.cpp:7318-7329 `getValueInt`: off 0, on 1, auto 2.
|
|
14
|
+
* - SelectMachine.cpp:1117-1123 `update_select_layout`: update_option_opts
|
|
15
|
+
* (which choices each option offers on this printer) runs before
|
|
16
|
+
* load_option_vals.
|
|
17
|
+
* - SelectMachine.cpp:2993-3027 `load_option_vals`: the value saved in
|
|
18
|
+
* BambuStudio.conf under the printer type, if the option offers it; else
|
|
19
|
+
* "auto" if offered; else "on". Dual-nozzle printers then set nozzle offset
|
|
20
|
+
* calibration to "auto" for a two-nozzle job and, without a filament
|
|
21
|
+
* switcher, "off" for a one-nozzle job.
|
|
22
|
+
* - SelectMachine.cpp:2586-2615 `update_option_opts`: bed leveling offers
|
|
23
|
+
* auto/on/off when the printer's support_bed_leveling is 2 and on/off when it
|
|
24
|
+
* is 1; with 0 it is hidden and keeps the auto/on/off it was built with
|
|
25
|
+
* (SelectMachine.cpp:676-691), as does flow calibration when the printer has
|
|
26
|
+
* no PA calibration. DeviceManager.cpp:1100/4441 give P-series printers
|
|
27
|
+
* (series_p1p: P1P, P1S) no PA calibration, so their flow calibration is
|
|
28
|
+
* hidden and resolves to "auto". Nozzle offset calibration always offers auto.
|
|
29
|
+
* - SelectMachine.cpp:705-707: the PA value switch starts on and is not saved,
|
|
30
|
+
* so extruder_cali_manual_mode is 0.
|
|
31
|
+
*
|
|
32
|
+
* Approximations, outside the P1P/P1S: flow calibration on other printers is
|
|
33
|
+
* shown when the printer's push status reports PA calibration (assumed here),
|
|
34
|
+
* and offers auto when support_auto_flow_calibration is true and the mapped
|
|
35
|
+
* filaments allow it (can_support_pa_auto_cali, assumed true). A filament
|
|
36
|
+
* switcher on a dual-nozzle printer is assumed absent.
|
|
37
|
+
*/
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import os from "node:os";
|
|
40
|
+
import path from "node:path";
|
|
41
|
+
const AUTO_ON_OFF = ["auto", "on", "off"];
|
|
42
|
+
const ON_OFF = ["on", "off"];
|
|
43
|
+
/**
|
|
44
|
+
* Studio's printer types (resources/printers/<type>.json at the pinned commit:
|
|
45
|
+
* model_id/printer_type, printer_series, support_bed_leveling,
|
|
46
|
+
* support_auto_flow_calibration), keyed by this server's model names.
|
|
47
|
+
*/
|
|
48
|
+
const STUDIO_PRINTERS = {
|
|
49
|
+
p1p: { types: ["C11"], pSeries: true, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
50
|
+
p1s: { types: ["C12"], pSeries: true, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
51
|
+
x1c: { types: ["3DPrinter-X1-Carbon"], pSeries: false, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
52
|
+
x1: { types: ["3DPrinter-X1"], pSeries: false, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
53
|
+
x1e: { types: ["C13"], pSeries: false, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
54
|
+
a1mini: { types: ["N1"], pSeries: false, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
55
|
+
a1: { types: ["N2S"], pSeries: false, bedLeveling: 1, autoFlow: false, dualNozzle: false },
|
|
56
|
+
p2s: { types: ["N7"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: false },
|
|
57
|
+
x2d: { types: ["N6"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: true },
|
|
58
|
+
h2s: { types: ["O1S"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: false },
|
|
59
|
+
h2d: { types: ["O1D"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: true },
|
|
60
|
+
h2dpro: { types: ["O1E"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: true },
|
|
61
|
+
h2c: { types: ["O1C", "O1C2"], pSeries: false, bedLeveling: 2, autoFlow: true, dualNozzle: true },
|
|
62
|
+
};
|
|
63
|
+
/** Where Studio keeps BambuStudio.conf; BAMBU_STUDIO_CONFIG overrides it (empty: none). */
|
|
64
|
+
export function studioConfigPath() {
|
|
65
|
+
const override = process.env.BAMBU_STUDIO_CONFIG;
|
|
66
|
+
if (override !== undefined)
|
|
67
|
+
return override.trim() || undefined;
|
|
68
|
+
if (process.platform === "darwin")
|
|
69
|
+
return path.join(os.homedir(), "Library", "Application Support", "BambuStudio", "BambuStudio.conf");
|
|
70
|
+
if (process.platform === "win32")
|
|
71
|
+
return process.env.APPDATA ? path.join(process.env.APPDATA, "BambuStudio", "BambuStudio.conf") : undefined;
|
|
72
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "BambuStudio", "BambuStudio.conf");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The Send dialog options Studio saved for a printer type, and nothing else:
|
|
76
|
+
* BambuStudio.conf also holds access codes, so only that one section is kept,
|
|
77
|
+
* and a parse failure is reported without any of the file's text.
|
|
78
|
+
*/
|
|
79
|
+
export function readStudioSavedOptions(printerTypes, configPath) {
|
|
80
|
+
if (!configPath)
|
|
81
|
+
return undefined;
|
|
82
|
+
let text;
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(configPath);
|
|
85
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
|
|
86
|
+
return undefined;
|
|
87
|
+
text = fs.readFileSync(configPath, "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
let config;
|
|
93
|
+
// Studio appends "# MD5 checksum <hex>" after the JSON.
|
|
94
|
+
try {
|
|
95
|
+
config = JSON.parse(text.split("\n").filter(line => !line.trimStart().startsWith("#")).join("\n"));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (!config || typeof config !== "object")
|
|
101
|
+
return undefined;
|
|
102
|
+
for (const type of printerTypes) {
|
|
103
|
+
const section = config[type];
|
|
104
|
+
if (!section || typeof section !== "object" || Array.isArray(section))
|
|
105
|
+
continue;
|
|
106
|
+
const saved = {};
|
|
107
|
+
for (const key of ["bed_leveling", "flow_cali", "nozzle_offset_cali", "timelapse"]) {
|
|
108
|
+
const value = section[key];
|
|
109
|
+
if (typeof value === "string")
|
|
110
|
+
saved[key] = value;
|
|
111
|
+
}
|
|
112
|
+
return saved;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function fromInt(value, label) {
|
|
117
|
+
if (value === 0)
|
|
118
|
+
return "off";
|
|
119
|
+
if (value === 1)
|
|
120
|
+
return "on";
|
|
121
|
+
if (value === 2)
|
|
122
|
+
return "auto";
|
|
123
|
+
throw new Error(`${label} must be 0 (off), 1 (on) or 2 (auto).`);
|
|
124
|
+
}
|
|
125
|
+
const toInt = (value) => value === "off" ? 0 : value === "on" ? 1 : 2;
|
|
126
|
+
/** load_option_vals for one option: saved if offered, else auto if offered, else on. */
|
|
127
|
+
function loaded(saved, offers) {
|
|
128
|
+
if (saved !== undefined && offers.includes(saved))
|
|
129
|
+
return { value: saved, source: "studio-config" };
|
|
130
|
+
return { value: offers.includes("auto") ? "auto" : "on", source: "studio-default" };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* What Studio's Send dialog would send for this model, with the caller's
|
|
134
|
+
* explicit choices taking precedence. usedNozzles is the number of nozzles the
|
|
135
|
+
* job uses, for dual-nozzle printers.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveStudioPrintOptions(model, caller = {}, options = {}) {
|
|
138
|
+
const printer = STUDIO_PRINTERS[model];
|
|
139
|
+
const saved = printer ? readStudioSavedOptions(printer.types, "configPath" in options ? options.configPath : studioConfigPath()) ?? {} : {};
|
|
140
|
+
const printerType = printer?.types[0];
|
|
141
|
+
const bedOffers = !printer || printer.bedLeveling !== 1 ? AUTO_ON_OFF : ON_OFF;
|
|
142
|
+
const flowOffers = !printer || printer.pSeries || printer.autoFlow ? AUTO_ON_OFF : ON_OFF;
|
|
143
|
+
const bed = loaded(saved.bed_leveling, bedOffers);
|
|
144
|
+
const flow = loaded(saved.flow_cali, flowOffers);
|
|
145
|
+
let offset = loaded(saved.nozzle_offset_cali, AUTO_ON_OFF);
|
|
146
|
+
if (printer?.dualNozzle && options.usedNozzles !== undefined) {
|
|
147
|
+
if (options.usedNozzles >= 2)
|
|
148
|
+
offset = { value: "auto", source: "studio-default" };
|
|
149
|
+
else if (options.usedNozzles === 1)
|
|
150
|
+
offset = { value: "off", source: "studio-default" };
|
|
151
|
+
}
|
|
152
|
+
const choose = (current, auto, flag, label) => auto !== undefined ? { value: fromInt(auto, label), source: "caller" }
|
|
153
|
+
: flag !== undefined ? { value: flag ? "on" : "off", source: "caller" }
|
|
154
|
+
: current;
|
|
155
|
+
const bedLeveling = choose(bed, caller.autoBedLeveling, caller.bedLeveling, "auto_bed_leveling");
|
|
156
|
+
const flowCali = choose(flow, caller.autoFlowCali, caller.flowCalibration, "auto_flow_cali");
|
|
157
|
+
const nozzleOffset = choose(offset, caller.autoOffsetCali, undefined, "auto_offset_cali");
|
|
158
|
+
const manualMode = caller.extruderCaliManualMode ?? 0;
|
|
159
|
+
if (!Number.isInteger(manualMode) || manualMode < -1 || manualMode > 1)
|
|
160
|
+
throw new Error("extruder_cali_manual_mode must be -1, 0 or 1.");
|
|
161
|
+
return {
|
|
162
|
+
...(printerType ? { printerType } : {}),
|
|
163
|
+
values: { bed_leveling: bedLeveling.value, flow_cali: flowCali.value, nozzle_offset_cali: nozzleOffset.value },
|
|
164
|
+
sources: { bed_leveling: bedLeveling.source, flow_cali: flowCali.source, nozzle_offset_cali: nozzleOffset.source },
|
|
165
|
+
task_bed_leveling: bedLeveling.value === "on",
|
|
166
|
+
task_flow_cali: flowCali.value === "on",
|
|
167
|
+
task_vibration_cali: caller.vibrationCalibration ?? false,
|
|
168
|
+
task_layer_inspect: caller.layerInspect ?? true,
|
|
169
|
+
auto_bed_leveling: toInt(bedLeveling.value),
|
|
170
|
+
auto_flow_cali: toInt(flowCali.value),
|
|
171
|
+
auto_offset_cali: toInt(nozzleOffset.value),
|
|
172
|
+
extruder_cali_manual_mode: manualMode,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** The number of nozzles a job's used filaments print from, when the inspection knows. */
|
|
176
|
+
export function usedNozzleCount(inspection) {
|
|
177
|
+
const nozzles = inspection.usedFilamentPositions.map(position => inspection.filamentNozzleIndices?.[position]).filter((value) => Number.isInteger(value));
|
|
178
|
+
return nozzles.length === inspection.usedFilamentPositions.length && nozzles.length ? new Set(nozzles).size : undefined;
|
|
179
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.26",
|
|
5
5
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"start:http": "MCP_TRANSPORT=streamable-http node dist/index.js",
|
|
13
13
|
"dev": "nodemon src/index.ts --exec \"npm run build && npm start\"",
|
|
14
14
|
"build": "tsc",
|
|
15
|
-
"test": "npm run build && node dist/test/stl-manipulator-test.js && node --test tests/*.test.mjs tests/slicer/*.test.mjs tests/safety/*.test.mjs",
|
|
15
|
+
"test": "npm run build && node dist/test/stl-manipulator-test.js && node --test --import ./tests/support/isolate-resume-records.mjs tests/*.test.mjs tests/slicer/*.test.mjs tests/safety/*.test.mjs",
|
|
16
16
|
"inspect": "npm run build && npx @modelcontextprotocol/inspector dist/index.js",
|
|
17
17
|
"prepack": "npm run build",
|
|
18
18
|
"test:package": "node scripts/test-package.mjs",
|
|
@@ -68,9 +68,27 @@
|
|
|
68
68
|
"server.json",
|
|
69
69
|
"dist/counterpart",
|
|
70
70
|
"src/counterpart",
|
|
71
|
+
"dist/design-guidance.js",
|
|
72
|
+
"dist/design-guidance.d.ts",
|
|
73
|
+
"src/design-guidance.ts",
|
|
71
74
|
"dist/fusion-mcp-bridge.js",
|
|
72
75
|
"dist/fusion-mcp-bridge.d.ts",
|
|
73
|
-
"src/fusion-mcp-bridge.ts"
|
|
76
|
+
"src/fusion-mcp-bridge.ts",
|
|
77
|
+
"dist/ams-mapping.js",
|
|
78
|
+
"dist/ams-mapping.d.ts",
|
|
79
|
+
"src/ams-mapping.ts",
|
|
80
|
+
"dist/bambu-network-bridge.js",
|
|
81
|
+
"dist/bambu-network-bridge.d.ts",
|
|
82
|
+
"src/bambu-network-bridge.ts",
|
|
83
|
+
"dist/control-reply.js",
|
|
84
|
+
"dist/control-reply.d.ts",
|
|
85
|
+
"src/control-reply.ts",
|
|
86
|
+
"dist/bambu-network-certs.js",
|
|
87
|
+
"dist/bambu-network-certs.d.ts",
|
|
88
|
+
"src/bambu-network-certs.ts",
|
|
89
|
+
"dist/studio-print-options.js",
|
|
90
|
+
"dist/studio-print-options.d.ts",
|
|
91
|
+
"src/studio-print-options.ts"
|
|
74
92
|
],
|
|
75
93
|
"keywords": [
|
|
76
94
|
"mcp",
|
|
@@ -90,7 +108,7 @@
|
|
|
90
108
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
91
109
|
"axios": "^1.21.0",
|
|
92
110
|
"bambu-node": "3.22.21",
|
|
93
|
-
"basic-ftp": "^6.2.
|
|
111
|
+
"basic-ftp": "^6.2.5",
|
|
94
112
|
"dotenv": "^16.6.1",
|
|
95
113
|
"form-data": "^4.0.6",
|
|
96
114
|
"jszip": "^3.10.2",
|
|
@@ -108,5 +126,8 @@
|
|
|
108
126
|
},
|
|
109
127
|
"engines": {
|
|
110
128
|
"node": ">=18"
|
|
129
|
+
},
|
|
130
|
+
"optionalDependencies": {
|
|
131
|
+
"sharp": "0.35.5"
|
|
111
132
|
}
|
|
112
133
|
}
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.26",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.26",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Tray-value options. `dualNozzle` admits 255, the right/main nozzle external spool that
|
|
2
|
+
* dual-nozzle printers report beside the left/deputy external spool 254 (Bambu Studio
|
|
3
|
+
* VIRTUAL_TRAY_MAIN_ID / VIRTUAL_TRAY_DEPUTY_ID). Single-nozzle printers keep 254 only. */
|
|
4
|
+
export interface AmsTrayValueOptions { dualNozzle?: boolean }
|
|
5
|
+
|
|
6
|
+
export function describeAmsTrayRange(options: AmsTrayValueOptions = {}): string {
|
|
7
|
+
return options.dualNozzle
|
|
8
|
+
? "[-1, 15] (absolute tray), 128-253 (AMS-HT), 254 (left/deputy external) or 255 (right/main external)"
|
|
9
|
+
: "[-1, 15] (absolute tray) or 128-254 (HT/external)";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeBridgeAmsTrayValue(value: unknown, label: string, options: AmsTrayValueOptions = {}): number {
|
|
13
|
+
let numeric: number;
|
|
14
|
+
if (typeof value === "number") {
|
|
15
|
+
numeric = value;
|
|
16
|
+
} else if (typeof value === "string" && value.trim() !== "") {
|
|
17
|
+
numeric = Number(value);
|
|
18
|
+
} else {
|
|
19
|
+
throw new Error(`${label} must be an integer tray index; got ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
!Number.isInteger(numeric) ||
|
|
24
|
+
numeric < -1 ||
|
|
25
|
+
(numeric > 15 && numeric < 128) ||
|
|
26
|
+
numeric > (options.dualNozzle ? 255 : 254)
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`${label} must be an integer in ${describeAmsTrayRange(options)}; got ${JSON.stringify(value)}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return numeric;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isNumericMappingKey(key: string): boolean {
|
|
37
|
+
const numeric = Number(key);
|
|
38
|
+
return Number.isInteger(numeric) && numeric >= 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeAmsMappingObject(mapping: Record<string, unknown>, label = "ams_mapping", options: AmsTrayValueOptions = {}): number[] {
|
|
42
|
+
return Object.entries(mapping)
|
|
43
|
+
.sort(([leftKey], [rightKey]) => {
|
|
44
|
+
const leftNumeric = isNumericMappingKey(leftKey);
|
|
45
|
+
const rightNumeric = isNumericMappingKey(rightKey);
|
|
46
|
+
if (leftNumeric && rightNumeric) return Number(leftKey) - Number(rightKey);
|
|
47
|
+
if (leftNumeric) return -1;
|
|
48
|
+
if (rightNumeric) return 1;
|
|
49
|
+
return 0;
|
|
50
|
+
})
|
|
51
|
+
.map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]`, options));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasAmsMappingInput(value: unknown): boolean {
|
|
55
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
56
|
+
if (value && typeof value === "object") return Object.keys(value as Record<string, unknown>).length > 0;
|
|
57
|
+
return value !== undefined && value !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Flat wire entries use -1 for dual-nozzle virtual trays; ams_mapping2 identifies their spool. */
|
|
61
|
+
export function bridgeAmsMappingValue(value: number, dualNozzle = false): number {
|
|
62
|
+
return dualNozzle && (value === 254 || value === 255) ? -1 : value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Select an external spool only from a verified physical nozzle on dual-nozzle printers. */
|
|
66
|
+
export function externalTrayForNozzle(nozzle: number | undefined, dualNozzle = false): number {
|
|
67
|
+
if (!dualNozzle) return 254;
|
|
68
|
+
if (nozzle === 0) return 255;
|
|
69
|
+
if (nozzle === 1) return 254;
|
|
70
|
+
throw new Error("External-spool printing requires inspected filament_map and physical_extruder_map nozzle bindings.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Virtual trays use slot zero on dual-nozzle printers; retain the historical single-nozzle form. */
|
|
74
|
+
export function bridgeAmsMapping2Entry(value: number, dualNozzle = false): { ams_id: number; slot_id: number } {
|
|
75
|
+
if (value < 0) return { ams_id: 255, slot_id: 255 };
|
|
76
|
+
if (value === 255) return dualNozzle ? { ams_id: 255, slot_id: 0 } : { ams_id: 255, slot_id: 255 };
|
|
77
|
+
if (value === 254) return { ams_id: 254, slot_id: dualNozzle ? 0 : 254 };
|
|
78
|
+
if (value >= 128) return { ams_id: 128, slot_id: value - 128 };
|
|
79
|
+
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A project filament as the sliced 3MF's Metadata/project_settings.config declares it. */
|
|
83
|
+
export interface ProjectFilament { type: string; filamentId: string; colour: string; nozzle?: number }
|
|
84
|
+
|
|
85
|
+
/** Read every project filament (not just the plate's used ones) from project_settings.config. */
|
|
86
|
+
export function projectFilaments(projectSettings: Record<string, unknown>): ProjectFilament[] {
|
|
87
|
+
const list = (key: string): string[] => Array.isArray(projectSettings[key]) ? (projectSettings[key] as unknown[]).map(String) : [];
|
|
88
|
+
const types = list("filament_type"), ids = list("filament_ids"), colours = list("filament_colour"), nozzles = list("filament_map");
|
|
89
|
+
return types.map((type, index) => ({
|
|
90
|
+
type, filamentId: ids[index] ?? "", colour: colours[index] ?? "",
|
|
91
|
+
...(nozzles[index] !== undefined && Number.isInteger(Number(nozzles[index])) ? { nozzle: Number(nozzles[index]) } : {}),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The colour the printer reports for an absolute tray value (AMS, AMS-HT or external spool). */
|
|
96
|
+
export function trayColour(rawStatus: any, tray: number): string | undefined {
|
|
97
|
+
const units: any[] = Array.isArray(rawStatus?.ams?.ams) ? rawStatus.ams.ams : [];
|
|
98
|
+
let colour: unknown;
|
|
99
|
+
if (tray === 254 || tray === 255) {
|
|
100
|
+
const slots: any[] = Array.isArray(rawStatus?.vir_slot) ? rawStatus.vir_slot : [];
|
|
101
|
+
colour = slots.find(slot => Number(slot?.id) === tray)?.tray_color ?? (tray === 254 ? rawStatus?.vt_tray?.tray_color : undefined);
|
|
102
|
+
} else if (tray >= 128) {
|
|
103
|
+
colour = units.find(unit => Number(unit?.id) === tray)?.tray?.[0]?.tray_color;
|
|
104
|
+
} else if (tray >= 0) {
|
|
105
|
+
const unit = units.find(candidate => Number(candidate?.id) === Math.floor(tray / 4));
|
|
106
|
+
colour = (Array.isArray(unit?.tray) ? unit.tray : []).find((slot: any) => Number(slot?.id) === tray % 4)?.tray_color;
|
|
107
|
+
}
|
|
108
|
+
return typeof colour === "string" ? colour : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* PrintParams.ams_mapping_info exactly as Bambu Studio and FULU's slicer build it
|
|
113
|
+
* (SelectMachineDialog::get_ams_mapping_result) and send it with every print: one entry per
|
|
114
|
+
* project filament. A mapped filament names its tray, its filament id and type, its colour as
|
|
115
|
+
* #RRGGBBAA and the tray's colour as the printer reports it; an unmapped one carries ams -1.
|
|
116
|
+
* Filament-map nozzle 1 (left) is cloud nozzle 1 and 2 (right) is 0 (CloudTaskNozzleId).
|
|
117
|
+
*/
|
|
118
|
+
export function studioAmsMappingInfo(filaments: ProjectFilament[], mapping: number[], colourOfTray: (tray: number) => string | undefined): Array<Record<string, unknown>> {
|
|
119
|
+
return filaments.map((filament, position) => {
|
|
120
|
+
const tray = mapping[position];
|
|
121
|
+
if (tray === undefined || tray < 0) return { ams: -1, targetColor: "", filamentId: "", filamentType: "" };
|
|
122
|
+
const hex = filament.colour.replace(/^#/, "").toUpperCase();
|
|
123
|
+
return {
|
|
124
|
+
ams: tray,
|
|
125
|
+
targetColor: colourOfTray(tray) ?? "",
|
|
126
|
+
filamentId: filament.filamentId,
|
|
127
|
+
filamentType: filament.type,
|
|
128
|
+
...(filament.nozzle === 1 ? { nozzleId: 1 } : filament.nozzle === 2 ? { nozzleId: 0 } : {}),
|
|
129
|
+
sourceColor: /^[0-9A-F]{6}$/.test(hex) ? `#${hex}FF` : /^[0-9A-F]{8}$/.test(hex) ? `#${hex}` : "",
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|