mcp-3d-printer-server 1.2.25 → 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 +55 -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 +512 -62
- 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 +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/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 +526 -75
- 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 +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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { FilamentSlot } from "../counterpart/contract.js";
|
|
2
|
+
export declare const FILAMENT_MODEL_CODES: Record<string, string>;
|
|
3
|
+
type ProfileResolution = "exact-model-nozzle" | "model" | "generic" | "unresolved";
|
|
4
|
+
export type PrinterFilamentTray = {
|
|
5
|
+
ams_id: number | null;
|
|
6
|
+
tray_id: number | null;
|
|
7
|
+
slot: number | null;
|
|
8
|
+
state: number;
|
|
9
|
+
loaded: boolean;
|
|
10
|
+
tray_info_idx: string | null;
|
|
11
|
+
tray_type: string | null;
|
|
12
|
+
tray_sub_brands: string | null;
|
|
13
|
+
tray_color: string | null;
|
|
14
|
+
remain_percent: number | null;
|
|
15
|
+
nozzle_temp_min: number | null;
|
|
16
|
+
nozzle_temp_max: number | null;
|
|
17
|
+
resolved_base_profile_name: string | null;
|
|
18
|
+
resolved_profile_path: string | null;
|
|
19
|
+
profile_resolution: ProfileResolution;
|
|
20
|
+
match_confidence: "high" | "medium" | "low" | "none";
|
|
21
|
+
display_name: string;
|
|
22
|
+
profile_candidates: string[];
|
|
23
|
+
};
|
|
24
|
+
export type PrinterFilamentInventory = {
|
|
25
|
+
current_slot: number | null;
|
|
26
|
+
current_source: "ams" | "external" | null;
|
|
27
|
+
summary: {
|
|
28
|
+
loaded_slots: number;
|
|
29
|
+
resolved_profile_slots: number;
|
|
30
|
+
unresolved_loaded_slots: number;
|
|
31
|
+
empty_slots: number;
|
|
32
|
+
current_slot: number | null;
|
|
33
|
+
current_source: "ams" | "external" | null;
|
|
34
|
+
recommended_slot: number | null;
|
|
35
|
+
recommended_reason: string | null;
|
|
36
|
+
};
|
|
37
|
+
trays: PrinterFilamentTray[];
|
|
38
|
+
recommended: {
|
|
39
|
+
slot: number | null;
|
|
40
|
+
tray_info_idx: string | null;
|
|
41
|
+
tray_type: string | null;
|
|
42
|
+
resolved_profile_path: string | null;
|
|
43
|
+
load_filaments: string | null;
|
|
44
|
+
reason: string;
|
|
45
|
+
} | null;
|
|
46
|
+
load_filaments_all: string | null;
|
|
47
|
+
};
|
|
48
|
+
export interface FilamentProfileSource {
|
|
49
|
+
slicerPath?: string;
|
|
50
|
+
slicerType?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function clearBambuFilamentProfileCache(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Lowercase 6-character RGB, dropping a leading "#" and any alpha. Bambu mixes
|
|
55
|
+
* #RRGGBB (3MF project settings) and RRGGBBAA (AMS reports); RGB is the key.
|
|
56
|
+
*/
|
|
57
|
+
export declare function normalizeRgbColor(color: string | null | undefined): string | null;
|
|
58
|
+
/** bambu-printer-mcp's inventory: AMS trays (absolute slot = ams_id * 4 + tray_id), summary and recommendation. */
|
|
59
|
+
export declare function normalizePrinterFilamentInventory(status: any, source: FilamentProfileSource, bambuModel?: string, nozzleDiameter?: string): PrinterFilamentInventory;
|
|
60
|
+
/**
|
|
61
|
+
* The external spool(s) the printer reports: vir_slot entries 254/255 on
|
|
62
|
+
* dual-nozzle printers, else vt_tray as 254. A spool is loaded once the printer
|
|
63
|
+
* knows its type (tray_type), as Bambu Studio shows it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function externalSpools(status: any, source: FilamentProfileSource, bambuModel?: string, nozzleDiameter?: string): PrinterFilamentTray[];
|
|
66
|
+
/** counterpart/1 slots: every AMS tray (id "<absolute slot>") and external spool ("254"/"255"); no nulls. */
|
|
67
|
+
export declare function counterpartSlots(inventory: PrinterFilamentInventory, external: PrinterFilamentTray[]): FilamentSlot[];
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bambu AMS and external-spool inventory, ported from bambu-printer-mcp's
|
|
3
|
+
* get_printer_filaments (normalizePrinterFilamentInventory). `trays` and the
|
|
4
|
+
* fields beside it are bambu-printer-mcp's result, field for field, so a
|
|
5
|
+
* client reads either server alike; `slots` is the counterpart/1 contract,
|
|
6
|
+
* which also lists the external spool(s) the printer reports (vt_tray, and
|
|
7
|
+
* vir_slot on dual-nozzle printers).
|
|
8
|
+
*
|
|
9
|
+
* Profiles resolve by the tray's tray_info_idx (Bambu filament_id) against
|
|
10
|
+
* the slicer's BBL/filament profiles, preferring the model-and-nozzle preset.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { detectProfilesRoot } from "../slicer/profile-flatten.js";
|
|
15
|
+
export const FILAMENT_MODEL_CODES = {
|
|
16
|
+
p1s: "P1S",
|
|
17
|
+
p1p: "P1P",
|
|
18
|
+
p2s: "P2S",
|
|
19
|
+
x1c: "X1C",
|
|
20
|
+
x1e: "X1E",
|
|
21
|
+
a1: "A1",
|
|
22
|
+
a1mini: "A1M",
|
|
23
|
+
h2d: "H2D",
|
|
24
|
+
h2dpro: "H2DP",
|
|
25
|
+
h2s: "H2S",
|
|
26
|
+
h2c: "H2C",
|
|
27
|
+
x2d: "X2D",
|
|
28
|
+
};
|
|
29
|
+
const indexCache = new Map();
|
|
30
|
+
function buildFilamentProfileIndex(source) {
|
|
31
|
+
const byName = new Map();
|
|
32
|
+
const baseNameByFilamentId = new Map();
|
|
33
|
+
let filamentProfileDir;
|
|
34
|
+
try {
|
|
35
|
+
const slicerType = source.slicerType === "bambustudio" ? "bambustudio" : "orcaslicer";
|
|
36
|
+
filamentProfileDir = path.join(detectProfilesRoot(source.slicerPath, slicerType), "BBL", "filament");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Profile suggestions are optional; invalid slicer configuration must not hide AMS inventory.
|
|
40
|
+
return { byName, baseNameByFilamentId };
|
|
41
|
+
}
|
|
42
|
+
let profileEntries;
|
|
43
|
+
try {
|
|
44
|
+
profileEntries = fs.readdirSync(filamentProfileDir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { byName, baseNameByFilamentId };
|
|
48
|
+
}
|
|
49
|
+
for (const entry of profileEntries) {
|
|
50
|
+
if (!entry.endsWith(".json"))
|
|
51
|
+
continue;
|
|
52
|
+
const filePath = path.join(filamentProfileDir, entry);
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
55
|
+
const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
|
|
56
|
+
const filamentId = typeof parsed?.filament_id === "string" ? parsed.filament_id.trim() : "";
|
|
57
|
+
if (name)
|
|
58
|
+
byName.set(name, filePath);
|
|
59
|
+
if (name && filamentId)
|
|
60
|
+
baseNameByFilamentId.set(filamentId, name);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore malformed or non-profile JSON files.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { byName, baseNameByFilamentId };
|
|
67
|
+
}
|
|
68
|
+
function filamentProfileIndex(source) {
|
|
69
|
+
const key = `${process.env.BAMBU_PROFILES_ROOT ?? ""}\n${source.slicerType ?? ""}\n${source.slicerPath ?? ""}`;
|
|
70
|
+
let index = indexCache.get(key);
|
|
71
|
+
if (!index) {
|
|
72
|
+
index = buildFilamentProfileIndex(source);
|
|
73
|
+
indexCache.set(key, index);
|
|
74
|
+
}
|
|
75
|
+
return index;
|
|
76
|
+
}
|
|
77
|
+
export function clearBambuFilamentProfileCache() {
|
|
78
|
+
indexCache.clear();
|
|
79
|
+
}
|
|
80
|
+
function resolveFilamentProfileCandidates(trayInfoIdx, source, bambuModel, nozzleDiameter) {
|
|
81
|
+
const index = filamentProfileIndex(source);
|
|
82
|
+
const baseName = index.baseNameByFilamentId.get(trayInfoIdx) || null;
|
|
83
|
+
if (!baseName)
|
|
84
|
+
return { baseName: null, paths: [], resolution: "unresolved" };
|
|
85
|
+
const bareName = baseName.replace(/\s*@base$/, "");
|
|
86
|
+
const modelCode = bambuModel ? FILAMENT_MODEL_CODES[bambuModel] : undefined;
|
|
87
|
+
const candidateNames = [];
|
|
88
|
+
if (modelCode && nozzleDiameter)
|
|
89
|
+
candidateNames.push(`${bareName} @BBL ${modelCode} ${nozzleDiameter} nozzle`);
|
|
90
|
+
if (modelCode)
|
|
91
|
+
candidateNames.push(`${bareName} @BBL ${modelCode}`);
|
|
92
|
+
candidateNames.push(bareName, baseName);
|
|
93
|
+
const resolvedPaths = [];
|
|
94
|
+
let resolution = "unresolved";
|
|
95
|
+
for (const [candidateIndex, candidateName] of candidateNames.entries()) {
|
|
96
|
+
const candidatePath = index.byName.get(candidateName);
|
|
97
|
+
if (!candidatePath || resolvedPaths.includes(candidatePath))
|
|
98
|
+
continue;
|
|
99
|
+
resolvedPaths.push(candidatePath);
|
|
100
|
+
if (resolution === "unresolved") {
|
|
101
|
+
if (candidateIndex === 0 && modelCode && nozzleDiameter) {
|
|
102
|
+
resolution = "exact-model-nozzle";
|
|
103
|
+
}
|
|
104
|
+
else if ((candidateIndex === 0 && modelCode && !nozzleDiameter) ||
|
|
105
|
+
(candidateIndex === 1 && modelCode && nozzleDiameter)) {
|
|
106
|
+
resolution = "model";
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
resolution = "generic";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { baseName, paths: resolvedPaths, resolution };
|
|
114
|
+
}
|
|
115
|
+
function parseIntegerOrNull(value) {
|
|
116
|
+
if (value === undefined || value === null || value === "")
|
|
117
|
+
return null;
|
|
118
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
119
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Lowercase 6-character RGB, dropping a leading "#" and any alpha. Bambu mixes
|
|
123
|
+
* #RRGGBB (3MF project settings) and RRGGBBAA (AMS reports); RGB is the key.
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeRgbColor(color) {
|
|
126
|
+
if (!color)
|
|
127
|
+
return null;
|
|
128
|
+
const hex = color.replace(/^#/, "").toLowerCase();
|
|
129
|
+
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(hex))
|
|
130
|
+
return null;
|
|
131
|
+
return hex.slice(0, 6);
|
|
132
|
+
}
|
|
133
|
+
function trayEntry(tray, ids, source, bambuModel, nozzleDiameter) {
|
|
134
|
+
const trayInfoIdx = typeof tray?.tray_info_idx === "string" ? tray.tray_info_idx : null;
|
|
135
|
+
const profileResolution = trayInfoIdx
|
|
136
|
+
? resolveFilamentProfileCandidates(trayInfoIdx, source, bambuModel, nozzleDiameter)
|
|
137
|
+
: { baseName: null, paths: [], resolution: "unresolved" };
|
|
138
|
+
const matchConfidence = profileResolution.resolution === "exact-model-nozzle" ? "high" :
|
|
139
|
+
profileResolution.resolution === "model" ? "medium" :
|
|
140
|
+
profileResolution.resolution === "generic" ? "low" :
|
|
141
|
+
"none";
|
|
142
|
+
const trayType = typeof tray?.tray_type === "string" ? tray.tray_type : null;
|
|
143
|
+
const traySubBrands = typeof tray?.tray_sub_brands === "string" ? tray.tray_sub_brands : null;
|
|
144
|
+
const trayColor = typeof tray?.tray_color === "string" ? tray.tray_color : null;
|
|
145
|
+
const displayParts = [
|
|
146
|
+
traySubBrands && traySubBrands !== trayType ? traySubBrands : null,
|
|
147
|
+
trayType,
|
|
148
|
+
trayColor ? `#${normalizeRgbColor(trayColor) ?? trayColor.replace(/^#/, "").slice(0, 6)}` : null,
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
return {
|
|
151
|
+
ams_id: ids.amsId,
|
|
152
|
+
tray_id: ids.trayId,
|
|
153
|
+
slot: ids.slot,
|
|
154
|
+
state: ids.state,
|
|
155
|
+
loaded: ids.state !== 0 && Boolean(trayInfoIdx),
|
|
156
|
+
tray_info_idx: trayInfoIdx,
|
|
157
|
+
tray_type: trayType,
|
|
158
|
+
tray_sub_brands: traySubBrands,
|
|
159
|
+
tray_color: trayColor,
|
|
160
|
+
remain_percent: typeof tray?.remain === "number" && tray.remain >= 0 ? tray.remain : null,
|
|
161
|
+
nozzle_temp_min: parseIntegerOrNull(tray?.nozzle_temp_min),
|
|
162
|
+
nozzle_temp_max: parseIntegerOrNull(tray?.nozzle_temp_max),
|
|
163
|
+
resolved_base_profile_name: profileResolution.baseName,
|
|
164
|
+
resolved_profile_path: profileResolution.paths[0] || null,
|
|
165
|
+
profile_resolution: profileResolution.resolution,
|
|
166
|
+
match_confidence: matchConfidence,
|
|
167
|
+
display_name: displayParts.length > 0 ? displayParts.join(" ") : "empty/unknown",
|
|
168
|
+
profile_candidates: profileResolution.paths,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** bambu-printer-mcp's inventory: AMS trays (absolute slot = ams_id * 4 + tray_id), summary and recommendation. */
|
|
172
|
+
export function normalizePrinterFilamentInventory(status, source, bambuModel, nozzleDiameter) {
|
|
173
|
+
const rawAms = status?.raw?.ams || status?.ams || {};
|
|
174
|
+
const trayNow = parseIntegerOrNull(rawAms?.tray_now);
|
|
175
|
+
const trays = [];
|
|
176
|
+
const amsArray = Array.isArray(rawAms?.ams) ? rawAms.ams : [];
|
|
177
|
+
for (const [amsIndex, amsUnit] of amsArray.entries()) {
|
|
178
|
+
const trayArray = Array.isArray(amsUnit?.tray) ? amsUnit.tray : [];
|
|
179
|
+
for (const [trayIndex, tray] of trayArray.entries()) {
|
|
180
|
+
const amsId = parseIntegerOrNull(amsUnit?.id) ?? amsIndex;
|
|
181
|
+
const trayId = parseIntegerOrNull(tray?.id) ?? trayIndex;
|
|
182
|
+
const slot = amsId !== null && trayId !== null ? amsId * 4 + trayId : null;
|
|
183
|
+
const state = parseIntegerOrNull(tray?.state) ?? 0;
|
|
184
|
+
trays.push(trayEntry(tray, { amsId, trayId, slot, state }, source, bambuModel, nozzleDiameter));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const loadedTrays = trays.filter((tray) => tray.loaded);
|
|
188
|
+
const recommendedTray = loadedTrays.find((tray) => tray.slot === trayNow && tray.resolved_profile_path) ||
|
|
189
|
+
loadedTrays.find((tray) => tray.resolved_profile_path) ||
|
|
190
|
+
null;
|
|
191
|
+
const recommendedReason = recommendedTray?.slot === trayNow
|
|
192
|
+
? "current AMS slot with resolved slicer profile"
|
|
193
|
+
: recommendedTray
|
|
194
|
+
? "first loaded AMS slot with resolved slicer profile"
|
|
195
|
+
: null;
|
|
196
|
+
const allProfilePaths = Array.from(new Set(loadedTrays.map((tray) => tray.resolved_profile_path).filter((candidate) => Boolean(candidate))));
|
|
197
|
+
const emptySlots = trays.filter((tray) => !tray.loaded).length;
|
|
198
|
+
const resolvedProfileSlots = loadedTrays.filter((tray) => tray.resolved_profile_path).length;
|
|
199
|
+
const currentSlot = trayNow !== null && trayNow >= 0 && trayNow < 254 ? trayNow : null;
|
|
200
|
+
const currentSource = trayNow === 254 ? "external" : currentSlot !== null ? "ams" : null;
|
|
201
|
+
return {
|
|
202
|
+
current_slot: currentSlot,
|
|
203
|
+
current_source: currentSource,
|
|
204
|
+
summary: {
|
|
205
|
+
loaded_slots: loadedTrays.length,
|
|
206
|
+
resolved_profile_slots: resolvedProfileSlots,
|
|
207
|
+
unresolved_loaded_slots: loadedTrays.length - resolvedProfileSlots,
|
|
208
|
+
empty_slots: emptySlots,
|
|
209
|
+
current_slot: currentSlot,
|
|
210
|
+
current_source: currentSource,
|
|
211
|
+
recommended_slot: recommendedTray?.slot ?? null,
|
|
212
|
+
recommended_reason: recommendedReason,
|
|
213
|
+
},
|
|
214
|
+
trays,
|
|
215
|
+
recommended: recommendedTray
|
|
216
|
+
? {
|
|
217
|
+
slot: recommendedTray.slot,
|
|
218
|
+
tray_info_idx: recommendedTray.tray_info_idx,
|
|
219
|
+
tray_type: recommendedTray.tray_type,
|
|
220
|
+
resolved_profile_path: recommendedTray.resolved_profile_path,
|
|
221
|
+
load_filaments: recommendedTray.resolved_profile_path,
|
|
222
|
+
reason: recommendedReason ?? "resolved slicer profile",
|
|
223
|
+
}
|
|
224
|
+
: null,
|
|
225
|
+
load_filaments_all: allProfilePaths.length > 0 ? allProfilePaths.join(";") : null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The external spool(s) the printer reports: vir_slot entries 254/255 on
|
|
230
|
+
* dual-nozzle printers, else vt_tray as 254. A spool is loaded once the printer
|
|
231
|
+
* knows its type (tray_type), as Bambu Studio shows it.
|
|
232
|
+
*/
|
|
233
|
+
export function externalSpools(status, source, bambuModel, nozzleDiameter) {
|
|
234
|
+
const raw = status?.raw ?? status ?? {};
|
|
235
|
+
const virtual = Array.isArray(raw?.vir_slot) ? raw.vir_slot : [];
|
|
236
|
+
const spools = virtual
|
|
237
|
+
.filter((slot) => [254, 255].includes(Number(slot?.id)))
|
|
238
|
+
.map((slot) => ({ slot: Number(slot.id), tray: slot }));
|
|
239
|
+
if (!spools.length && raw?.vt_tray && typeof raw.vt_tray === "object")
|
|
240
|
+
spools.push({ slot: 254, tray: raw.vt_tray });
|
|
241
|
+
return spools.map(({ slot, tray }) => {
|
|
242
|
+
const known = typeof tray?.tray_type === "string" && tray.tray_type.trim() !== "";
|
|
243
|
+
const entry = trayEntry(tray, { amsId: slot, trayId: 0, slot, state: known ? 1 : 0 }, source, bambuModel, nozzleDiameter);
|
|
244
|
+
return { ...entry, loaded: known };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** counterpart/1 slots: every AMS tray (id "<absolute slot>") and external spool ("254"/"255"); no nulls. */
|
|
248
|
+
export function counterpartSlots(inventory, external) {
|
|
249
|
+
return [...inventory.trays, ...external].flatMap((tray) => {
|
|
250
|
+
if (tray.slot === null || !tray.loaded)
|
|
251
|
+
return [];
|
|
252
|
+
const rgb = normalizeRgbColor(tray.tray_color);
|
|
253
|
+
return [{
|
|
254
|
+
id: String(tray.slot),
|
|
255
|
+
...(tray.tray_type ? { material: tray.tray_type } : {}),
|
|
256
|
+
...(rgb ? { color: `#${rgb.toUpperCase()}` } : {}),
|
|
257
|
+
...(tray.display_name && tray.display_name !== "empty/unknown" ? { name: tray.display_name } : {}),
|
|
258
|
+
source: "printer",
|
|
259
|
+
...(tray.resolved_profile_path ? { profile: tray.resolved_profile_path } : {}),
|
|
260
|
+
}];
|
|
261
|
+
});
|
|
262
|
+
}
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PrinterImplementation } from "../types.js";
|
|
2
2
|
import { type PrintFileInspection } from "../safety/print-file.js";
|
|
3
|
+
import { type PrinterStateRequirements } from "../safety/printer-state.js";
|
|
3
4
|
import { type ConfirmHardware } from "../safety/confirmation.js";
|
|
4
5
|
import { type ExpectedPeakTemperatures, type PrintSafetyOptions } from "../safety/expected-peaks.js";
|
|
5
6
|
export interface BambuPrintOptionsInternal {
|
|
@@ -18,17 +19,62 @@ export interface BambuPrintOptionsInternal {
|
|
|
18
19
|
timelapse?: boolean;
|
|
19
20
|
/** Positional mapping: project filament position -> absolute AMS tray (254 = external spool). */
|
|
20
21
|
amsMapping?: number[];
|
|
22
|
+
/** One absolute tray per used filament in plate order; expanded to the project-level mapping. */
|
|
23
|
+
amsSlots?: number[];
|
|
21
24
|
md5?: string;
|
|
22
25
|
expectedPeaks?: ExpectedPeakTemperatures;
|
|
23
26
|
}
|
|
24
27
|
export type BambuSafetyOptions = PrintSafetyOptions;
|
|
28
|
+
/** The inspected bytes and limits of a started job, for the resume byte check and prompt. */
|
|
29
|
+
export type CheckedFile = {
|
|
30
|
+
sha256: string;
|
|
31
|
+
/** A project's printed plate: its number and the SHA-256 of its Metadata/plate_<index>.gcode. */
|
|
32
|
+
plate?: {
|
|
33
|
+
index: number;
|
|
34
|
+
sha256: string;
|
|
35
|
+
};
|
|
36
|
+
model: string;
|
|
37
|
+
maxNozzleTemperature: number;
|
|
38
|
+
maxBedTemperature: number;
|
|
39
|
+
maxChamberTemperature: number;
|
|
40
|
+
materials: string[];
|
|
41
|
+
};
|
|
42
|
+
/** A route's own dispatch for a print control (FULU's BambuNetwork); undefined sends over LAN MQTT. */
|
|
43
|
+
export type ControlDispatch = () => Promise<any>;
|
|
44
|
+
export type ResumeDispatch = (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>;
|
|
45
|
+
/**
|
|
46
|
+
* Where each printer's resume record is kept so a restart of this server (a deploy, a crash, a reboot) during a
|
|
47
|
+
* pause can still resume the job it started: BAMBU_RESUME_STATE_DIR, else mcp-3d-printer-server/resume in the
|
|
48
|
+
* config home (bambu-printer-mcp keeps its own). Resume runs every check on a reloaded record: a fresh paused
|
|
49
|
+
* report naming this exact job, the re-read bytes or its unique name, and a person for an unverified file.
|
|
50
|
+
*/
|
|
51
|
+
export declare function defaultResumeStateDir(env?: NodeJS.ProcessEnv): string;
|
|
52
|
+
/** What a resume needs to know about a started file (filePath, the inspected snapshot). */
|
|
53
|
+
export declare function checkedFile(inspection: PrintFileInspection, filePath: string): Promise<CheckedFile>;
|
|
25
54
|
export declare class BambuImplementation extends PrinterImplementation {
|
|
26
55
|
private readonly confirm?;
|
|
27
56
|
private printerStore;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The job each printer may resume, keyed host and serial. Cancel, a report
|
|
59
|
+
* that it ended after it was seen running, or another job seen in its place
|
|
60
|
+
* retires it, so a later job that reuses the name is never resumed.
|
|
61
|
+
*/
|
|
62
|
+
private readonly checkedJobs;
|
|
63
|
+
/** confirm: a person (MCP elicitation), asked only to resume a job whose file could not be re-read. */
|
|
64
|
+
constructor(apiClient: any, confirm?: ConfirmHardware | undefined, options?: {
|
|
65
|
+
resumeStateDir?: string;
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Internal handoff from a successful inspected transport; never an MCP tool.
|
|
69
|
+
* uploadedHere is false for FULU's BambuNetwork routes, where Bambu's plug-in
|
|
70
|
+
* put the file on the printer at a path this server has not verified: a
|
|
71
|
+
* resume then re-reads it if it can and otherwise asks a person.
|
|
72
|
+
*/
|
|
73
|
+
recordCheckedJob(host: string, serial: string, remotePath: string, requirements: PrinterStateRequirements, file: CheckedFile, uploadedHere?: boolean): void;
|
|
74
|
+
/** Retire a record that never started (the dispatch was refused or failed). */
|
|
75
|
+
forgetCheckedJob(host: string, serial: string, remotePath: string): void;
|
|
76
|
+
/** Feed one merged report to the resume record (the client store calls it for every report). */
|
|
77
|
+
observeReport(host: string, serial: string, raw: Record<string, any> | undefined): void;
|
|
32
78
|
private getPrinter;
|
|
33
79
|
/** Safety reads never use the display cache or configured-serial model inference. */
|
|
34
80
|
getSafetyStatus(host: string, serial: string, token: string): Promise<any>;
|
|
@@ -38,6 +84,17 @@ export declare class BambuImplementation extends PrinterImplementation {
|
|
|
38
84
|
getStatus(host: string, port: string, apiKey: string): Promise<any>;
|
|
39
85
|
print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
|
|
40
86
|
private print3mfPrepared;
|
|
87
|
+
/**
|
|
88
|
+
* The project-level tray mapping for a checked print, built as
|
|
89
|
+
* bambu-printer-mcp builds it: position = project filament, value = absolute
|
|
90
|
+
* tray (0-15 AMS, 128+ AMS-HT, 254 external; 255 the right/main external
|
|
91
|
+
* spool on dual-nozzle printers, -1 unused). ams_mapping is taken as given;
|
|
92
|
+
* ams_slots supplies one tray per used filament and is expanded against the
|
|
93
|
+
* plate's filament_ids; use_ams:false maps every used filament to the
|
|
94
|
+
* external spool of its nozzle. Every used position must end up mapped.
|
|
95
|
+
* The wire form pads to the historical five entries.
|
|
96
|
+
*/
|
|
97
|
+
private checkedAmsMapping;
|
|
41
98
|
/**
|
|
42
99
|
* A published print command is not proof the printer took it. Listen to the
|
|
43
100
|
* reports the printer pushes anyway (no extra pushall polling on weak P1
|
|
@@ -45,8 +102,44 @@ export declare class BambuImplementation extends PrinterImplementation {
|
|
|
45
102
|
* command; PREPARE/SLICING/RUNNING means it started. Attach before publishing.
|
|
46
103
|
*/
|
|
47
104
|
private watchDispatch;
|
|
48
|
-
/**
|
|
49
|
-
|
|
105
|
+
/** A refused or unconfirmed-then-failed dispatch leaves no job to resume. */
|
|
106
|
+
private settleRecorded;
|
|
107
|
+
/**
|
|
108
|
+
* Stop is never gated or queued behind pending checked operations, and it
|
|
109
|
+
* retires the resume record. dispatch sends it over the route that printed
|
|
110
|
+
* (FULU's BambuNetwork); without one it goes over LAN MQTT.
|
|
111
|
+
*/
|
|
112
|
+
cancelJob(host: string, port: string, apiKey: string, dispatch?: ControlDispatch): Promise<any>;
|
|
113
|
+
/** Pause is never gated. */
|
|
114
|
+
pauseJob(host: string, port: string, apiKey: string, dispatch?: ControlDispatch): Promise<{
|
|
115
|
+
status: string;
|
|
116
|
+
action: "pause";
|
|
117
|
+
message: string;
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Resume reheats and moves the printer, so it is gated like a print start:
|
|
122
|
+
* only the job this server inspected and started, while a fresh report shows
|
|
123
|
+
* it paused, with its bytes re-read over FTPS and compared. Only a file that
|
|
124
|
+
* could not be re-read, and that this server did not upload itself, asks a
|
|
125
|
+
* person. The record must still be current at every check.
|
|
126
|
+
*/
|
|
127
|
+
resumeJob(host: string, port: string, apiKey: string, dispatch?: ResumeDispatch): Promise<{
|
|
128
|
+
status: string;
|
|
129
|
+
action: "resume";
|
|
130
|
+
message: string;
|
|
131
|
+
jobName: string;
|
|
132
|
+
bytesVerified: boolean;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
}>;
|
|
135
|
+
/**
|
|
136
|
+
* Re-read a started job's bytes over FTPS: the uploaded file, else the plate
|
|
137
|
+
* G-code the printer extracted from the project to print it. The first file
|
|
138
|
+
* that can be read decides; a mismatch is reported as changed.
|
|
139
|
+
*/
|
|
140
|
+
private rereadJob;
|
|
141
|
+
/** SHA-256 of a printer file, streamed from an FTPS download into a private directory. */
|
|
142
|
+
private remoteSha256;
|
|
50
143
|
setTemperature(host: string, port: string, apiKey: string, component: string, temperature: unknown, options?: BambuSafetyOptions): Promise<{
|
|
51
144
|
status: string;
|
|
52
145
|
message: string;
|