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
package/src/printers/creality.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AxiosInstance } from "axios";
|
|
2
|
-
import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
|
|
2
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
|
|
3
3
|
import { KlipperImplementation, MACRO_TIMEOUT_MS } from "./klipper.js";
|
|
4
4
|
import type { ConfirmHardware } from "../safety/confirmation.js";
|
|
5
5
|
import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
|
|
@@ -73,6 +73,11 @@ export class CrealityImplementation extends GenericPrinterImplementation {
|
|
|
73
73
|
return moonraker ? this.moonraker.snapshotCandidates(host, moonraker, "") : [];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
async cameraCandidates(host: string): Promise<CameraCandidate[]> {
|
|
77
|
+
const moonraker = await this.moonrakerPort(host);
|
|
78
|
+
return moonraker ? this.moonraker.cameraCandidates(host, moonraker, "") : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
async counterpartFilaments(host: string, port: string, apiKey: string): Promise<FilamentSlot[]> {
|
|
77
82
|
const moonraker = await this.moonrakerPort(host);
|
|
78
83
|
return moonraker ? this.moonraker.counterpartFilaments(host, moonraker, "") : super.counterpartFilaments(host, port, apiKey);
|
|
@@ -111,7 +116,7 @@ export class CrealityImplementation extends GenericPrinterImplementation {
|
|
|
111
116
|
}
|
|
112
117
|
const state = raw.trim().toLowerCase();
|
|
113
118
|
if (IDLE_STATES.has(state)) return { state, ready: true };
|
|
114
|
-
if (FINISHED_STATES.has(state)) return { state, ready: true
|
|
119
|
+
if (FINISHED_STATES.has(state)) return { state, ready: true };
|
|
115
120
|
return { state, ready: false, reason: `state is ${state}` };
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -217,4 +222,4 @@ export class CrealityImplementation extends GenericPrinterImplementation {
|
|
|
217
222
|
|
|
218
223
|
return response.data;
|
|
219
224
|
}
|
|
220
|
-
}
|
|
225
|
+
}
|
|
@@ -26,6 +26,12 @@ export interface SnapshotCandidate {
|
|
|
26
26
|
/** True when the firmware's own settings name this URL; false for a conventional fallback. */
|
|
27
27
|
configured: boolean;
|
|
28
28
|
}
|
|
29
|
+
export interface CameraCandidate extends SnapshotCandidate {
|
|
30
|
+
transport: "http-mjpeg" | "http-snapshot";
|
|
31
|
+
/** Snapshot belonging to this same camera, never an unrelated later candidate. */
|
|
32
|
+
snapshotUrl?: string;
|
|
33
|
+
unsupportedStream?: boolean;
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
/** Normalized adapter state used by the print and heating preflight. */
|
|
31
37
|
export interface GenericPrinterState {
|
|
@@ -35,8 +41,6 @@ export interface GenericPrinterState {
|
|
|
35
41
|
ready: boolean;
|
|
36
42
|
/** Why the printer is not ready. */
|
|
37
43
|
reason?: string;
|
|
38
|
-
/** Set when the printer explicitly reports a finished or cancelled job that may still be on the bed. */
|
|
39
|
-
finishedJob?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export interface HeaterComponent {
|
|
@@ -51,8 +55,8 @@ export const REMOTE_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
|
51
55
|
/**
|
|
52
56
|
* Shared print and heating gate for non-Bambu adapters. Public mutations
|
|
53
57
|
* validate every input before connecting, inspect the exact bytes that are
|
|
54
|
-
* uploaded, check fresh adapter state,
|
|
55
|
-
*
|
|
58
|
+
* uploaded, check fresh adapter state, recheck it and only then dispatch.
|
|
59
|
+
* Heater-off and cancel are never gated.
|
|
56
60
|
*/
|
|
57
61
|
/** seenRunning: a report showed this job printing or paused, so a later ended report is about it, not about the previous job. */
|
|
58
62
|
type CheckedJob = { name: string; sha256: string; peaks: GenericPrintInspection["peaks"]; materials: string[]; seenRunning: boolean };
|
|
@@ -105,6 +109,11 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
105
109
|
return [];
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/** Existing snapshot discovery is the fallback for cameras without an MJPEG URL. */
|
|
113
|
+
async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
|
|
114
|
+
return (await this.snapshotCandidates(host, port, apiKey)).map(candidate => ({...candidate, transport: "http-snapshot"}));
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
/** The single extruder slot; adapters with a spool manager override this. */
|
|
109
118
|
async counterpartFilaments(_host: string, _port: string, _apiKey: string): Promise<FilamentSlot[]> {
|
|
110
119
|
return [{ id: "ext", source: "unknown" }];
|
|
@@ -135,9 +144,9 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
135
144
|
* Resume reheats and moves the printer, so it is gated like a print start:
|
|
136
145
|
* only the job this server instance inspected and started, only while the
|
|
137
146
|
* printer freshly reports that job paused, with its bytes re-read and
|
|
138
|
-
* compared where the firmware can download them
|
|
139
|
-
*
|
|
140
|
-
* in the meantime cancels it.
|
|
147
|
+
* compared where the firmware can download them. Only when the bytes cannot
|
|
148
|
+
* be re-read does a person confirm (MCP elicitation). A stop or heater-off
|
|
149
|
+
* request in the meantime cancels it.
|
|
141
150
|
*/
|
|
142
151
|
async resumeJob(host: string, port: string, apiKey: string): Promise<{ status: string; action: "resume"; message: string; jobName: string; bytesVerified: boolean }> {
|
|
143
152
|
if (!(await this.supportsPauseResume(host, port, apiKey)) || !this.rawResumeJob) {
|
|
@@ -165,21 +174,23 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
165
174
|
bytesVerified = true;
|
|
166
175
|
}
|
|
167
176
|
assertActive();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
if (!bytesVerified) {
|
|
178
|
+
// This firmware cannot hand the file back, so only a person can tell it is still the inspected job.
|
|
179
|
+
await requireHumanConfirmation(
|
|
180
|
+
this.confirm,
|
|
181
|
+
`Resume ${checked.name} on ${this.printerLabel} (${host}:${port})? This firmware cannot re-read the file, so it is matched by name only. ` +
|
|
182
|
+
`It reheats to its targets (peak nozzle ${checked.peaks.nozzle}°C, bed ${checked.peaks.bed}°C, chamber ${checked.peaks.chamber}°C) and moves. ` +
|
|
183
|
+
`Materials: ${checked.materials.length ? checked.materials.join(", ") : "none declared"}. Inspected file SHA-256: ${checked.sha256}. ` +
|
|
184
|
+
"Confirm the paused job is that file, the nozzle path is clear and the filament is still loaded."
|
|
185
|
+
);
|
|
186
|
+
assertActive();
|
|
187
|
+
}
|
|
188
|
+
// Someone may have resumed or stopped it at the printer meanwhile (for example while a prompt was open).
|
|
178
189
|
await this.assertPausedJob(host, port, apiKey, checked.name);
|
|
179
|
-
// A progress read
|
|
180
|
-
// could now be paused under the same name); only the record that was
|
|
190
|
+
// A progress read meanwhile may have retired this record (the job ended, and a new one
|
|
191
|
+
// could now be paused under the same name); only the record that was checked may be resumed.
|
|
181
192
|
if (this.checkedJobs.get(key) !== checked) {
|
|
182
|
-
throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel}
|
|
193
|
+
throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel} during the resume checks; nothing was sent.`);
|
|
183
194
|
}
|
|
184
195
|
assertActive();
|
|
185
196
|
await this.rawResumeJob!(host, port, apiKey);
|
|
@@ -276,12 +287,6 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
276
287
|
validateGenericTemperature(target.heater, value, material ? [material] : undefined);
|
|
277
288
|
return withPrinterOperation(host, key, async (assertActive) => {
|
|
278
289
|
this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
|
|
279
|
-
await requireHumanConfirmation(
|
|
280
|
-
this.confirm,
|
|
281
|
-
`Heat ${target.heater} on ${this.printerLabel} (${host}:${port}) to ${value}°C?` +
|
|
282
|
-
(material && target.heater === "nozzle" ? ` Declared material: ${material}. Confirm the loaded filament matches.` : ""),
|
|
283
|
-
"generic"
|
|
284
|
-
);
|
|
285
290
|
assertActive();
|
|
286
291
|
this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
|
|
287
292
|
assertActive();
|
|
@@ -393,10 +398,9 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
393
398
|
assertExpectedPeaks(options.expectedPeaks, { nozzle: inspection.peaks.nozzle, bed: inspection.peaks.bed });
|
|
394
399
|
const initial = await this.readPrinterState(host, port, apiKey);
|
|
395
400
|
this.assertReady(initial, "print");
|
|
396
|
-
await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic");
|
|
397
401
|
assertActive();
|
|
398
|
-
const
|
|
399
|
-
this.assertReady(
|
|
402
|
+
const rechecked = await this.readPrinterState(host, port, apiKey);
|
|
403
|
+
this.assertReady(rechecked, "print");
|
|
400
404
|
assertActive();
|
|
401
405
|
|
|
402
406
|
const summary = {
|
|
@@ -436,15 +440,4 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
436
440
|
this.checkedJobs.set(this.operationKey(host, port), checkedJob(remoteName, inspection));
|
|
437
441
|
return { ...summary, upload, response };
|
|
438
442
|
}
|
|
439
|
-
|
|
440
|
-
private printPrompt(host: string, port: string, remoteName: string, inspection: GenericPrintInspection, state: GenericPrinterState): string {
|
|
441
|
-
const materials = inspection.materials.length ? inspection.materials.join(", ") : "none declared (no positive nozzle target)";
|
|
442
|
-
return (
|
|
443
|
-
`Start a checked print of ${remoteName} on ${this.printerLabel} (${host}:${port})? ` +
|
|
444
|
-
`Materials: ${materials} (${inspection.materialSource}). Peak targets: nozzle ${inspection.peaks.nozzle}°C, bed ${inspection.peaks.bed}°C, chamber ${inspection.peaks.chamber}°C ` +
|
|
445
|
-
`(server ceilings ${inspection.ceilings.nozzle}/${inspection.ceilings.bed}/${inspection.ceilings.chamber}°C). File SHA-256: ${inspection.sha256}. ` +
|
|
446
|
-
"Confirm the loaded filament matches." +
|
|
447
|
-
""
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
443
|
}
|
package/src/printers/klipper.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import FormData from "form-data";
|
|
3
3
|
import { isAxiosError } from "axios";
|
|
4
|
-
import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
|
|
4
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
|
|
5
5
|
import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
|
|
6
6
|
import { finiteNumber, nonEmptyText } from "../counterpart/contract.js";
|
|
7
7
|
import { mapMoonraker } from "../counterpart/progress.js";
|
|
@@ -66,11 +66,19 @@ export class KlipperImplementation extends GenericPrinterImplementation {
|
|
|
66
66
|
* path relative to the host. https://moonraker.readthedocs.io/en/latest/external_api/webcams/
|
|
67
67
|
*/
|
|
68
68
|
async snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]> {
|
|
69
|
+
return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
|
|
69
73
|
const data = (await this.apiClient.get(`http://${host}:${port}/server/webcams/list`, this.authHeaders(apiKey))).data;
|
|
70
74
|
const webcams = Array.isArray(data?.result?.webcams) ? data.result.webcams : [];
|
|
71
75
|
return webcams
|
|
72
|
-
.filter((webcam: any) => webcam?.enabled !== false
|
|
73
|
-
.
|
|
76
|
+
.filter((webcam: any) => webcam?.enabled !== false)
|
|
77
|
+
.flatMap((webcam: any) => [
|
|
78
|
+
...(nonEmptyText(webcam?.stream_url)
|
|
79
|
+
? [{url: String(webcam.stream_url).trim(), snapshotUrl: nonEmptyText(webcam?.snapshot_url), unsupportedStream: /webrtc|hls/i.test(String(webcam?.service ?? "")), transport: "http-mjpeg" as const}] : []),
|
|
80
|
+
...(nonEmptyText(webcam?.snapshot_url) ? [{url: String(webcam.snapshot_url).trim(), transport: "http-snapshot" as const}] : []),
|
|
81
|
+
].map(candidate => ({...candidate, source: "moonraker-webcam", configured: true})));
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/**
|
|
@@ -175,11 +183,7 @@ export class KlipperImplementation extends GenericPrinterImplementation {
|
|
|
175
183
|
if (!["standby", "complete", "cancelled"].includes(job)) {
|
|
176
184
|
return { state, ready: false, reason: `print_stats.state is ${job}` };
|
|
177
185
|
}
|
|
178
|
-
return {
|
|
179
|
-
state,
|
|
180
|
-
ready: true,
|
|
181
|
-
finishedJob: job === "standby" ? undefined : JSON.stringify([job, status?.print_stats?.filename ?? null]),
|
|
182
|
-
};
|
|
186
|
+
return { state, ready: true };
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
protected async downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise<void> {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import FormData from "form-data";
|
|
3
3
|
import { isAxiosError } from "axios";
|
|
4
|
-
import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate } from "./generic-base.js";
|
|
4
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
|
|
5
5
|
import type { CounterpartProgress, PrinterIdentity } from "../counterpart/contract.js";
|
|
6
6
|
import { nonEmptyText } from "../counterpart/contract.js";
|
|
7
7
|
import { mapOctoPrint } from "../counterpart/progress.js";
|
|
@@ -55,20 +55,27 @@ export class OctoPrintImplementation extends GenericPrinterImplementation {
|
|
|
55
55
|
* src/octoprint/plugins/classicwebcam/__init__.py; checked on 1.8.7 and 1.11.2.
|
|
56
56
|
*/
|
|
57
57
|
async snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]> {
|
|
58
|
+
return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
|
|
58
62
|
const settings = (await this.apiClient.get(`http://${host}:${port}/api/settings`, { headers: { "X-Api-Key": apiKey } })).data;
|
|
59
|
-
const candidates:
|
|
63
|
+
const candidates: CameraCandidate[] = [];
|
|
60
64
|
if (settings?.webcam?.webcamEnabled !== false) {
|
|
65
|
+
// The settings API maps webcam.stream to streamUrl. Classic Webcam on
|
|
66
|
+
// 1.9+ exposes its own stream field; only configured URLs are used.
|
|
61
67
|
const configured = [
|
|
62
|
-
settings?.plugins?.classicwebcam
|
|
63
|
-
settings?.webcam?.snapshotUrl,
|
|
64
|
-
...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam: any) => webcam?.compat
|
|
68
|
+
settings?.plugins?.classicwebcam,
|
|
69
|
+
{stream: settings?.webcam?.streamUrl, snapshot: settings?.webcam?.snapshotUrl},
|
|
70
|
+
...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam: any) => webcam?.compat) : []),
|
|
65
71
|
];
|
|
66
|
-
for (const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
72
|
+
for (const camera of configured) {
|
|
73
|
+
const stream = nonEmptyText(camera?.stream), snapshot = nonEmptyText(camera?.snapshot);
|
|
74
|
+
if (stream) candidates.push({url: stream, snapshotUrl: snapshot, source: "octoprint-settings", configured: true, transport: "http-mjpeg"});
|
|
75
|
+
if (snapshot) candidates.push({url: snapshot, source: "octoprint-settings", configured: true, transport: "http-snapshot"});
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
|
-
candidates.push({ url: `http://${host}/webcam/?action=snapshot`, source: "octoprint-fallback", configured: false });
|
|
78
|
+
candidates.push({ url: `http://${host}/webcam/?action=snapshot`, source: "octoprint-fallback", configured: false, transport: "http-snapshot" });
|
|
72
79
|
return candidates;
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -3,7 +3,7 @@ import { OctoPrintImplementation } from "./octoprint.js";
|
|
|
3
3
|
import { KlipperImplementation } from "./klipper.js";
|
|
4
4
|
import { DuetImplementation } from "./duet.js";
|
|
5
5
|
import { RepetierImplementation } from "./repetier.js";
|
|
6
|
-
import { BambuImplementation } from "./bambu.js";
|
|
6
|
+
import { BambuImplementation, defaultResumeStateDir } from "./bambu.js";
|
|
7
7
|
import { PrusaImplementation } from "./prusa.js";
|
|
8
8
|
import { CrealityImplementation } from "./creality.js";
|
|
9
9
|
import type { ConfirmHardware } from "../safety/confirmation.js";
|
|
@@ -21,13 +21,13 @@ export class PrinterFactory {
|
|
|
21
21
|
private implementations: Map<string, PrinterImplementation> = new Map();
|
|
22
22
|
private apiClient = axios.create({ timeout: 10000 });
|
|
23
23
|
|
|
24
|
-
/** confirm is the human (MCP elicitation) confirmation
|
|
24
|
+
/** confirm is the human (MCP elicitation) confirmation for physical checks software cannot make, such as resuming a job that cannot be re-read. */
|
|
25
25
|
constructor(confirm?: ConfirmHardware) {
|
|
26
26
|
this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient, confirm));
|
|
27
27
|
this.implementations.set("klipper", new KlipperImplementation(this.apiClient, confirm));
|
|
28
28
|
this.implementations.set("duet", new DuetImplementation(this.apiClient, confirm));
|
|
29
29
|
this.implementations.set("repetier", new RepetierImplementation(this.apiClient, confirm));
|
|
30
|
-
this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm));
|
|
30
|
+
this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm, { resumeStateDir: defaultResumeStateDir() }));
|
|
31
31
|
this.implementations.set("prusa", new PrusaImplementation(this.apiClient, confirm));
|
|
32
32
|
this.implementations.set("creality", new CrealityImplementation(this.apiClient, confirm));
|
|
33
33
|
}
|
package/src/printers/prusa.ts
CHANGED
|
@@ -232,7 +232,6 @@ export class PrusaImplementation extends GenericPrinterImplementation {
|
|
|
232
232
|
state,
|
|
233
233
|
ready,
|
|
234
234
|
reason: ready ? undefined : `printer.state is ${state}`,
|
|
235
|
-
finishedJob: ready && (state === "FINISHED" || state === "STOPPED") ? JSON.stringify([state, data?.job?.id ?? null]) : undefined,
|
|
236
235
|
};
|
|
237
236
|
}
|
|
238
237
|
const flags = data?.state?.flags;
|
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
/** Human confirmation for physical printer mutations (print start, positive heating). */
|
|
2
|
-
export type ConfirmHardware = (message: string) => Promise<boolean>;
|
|
3
|
-
export type ConfirmationScope = "bambu" | "generic";
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* Human confirmation through MCP elicitation. Ordinary print starts, positive
|
|
3
|
+
* heating and resumes of a re-read job do not ask: the client's own action is
|
|
4
|
+
* the approval. Only a check that software cannot make asks a person, such as
|
|
5
|
+
* resuming a job whose file the printer cannot hand back for re-reading.
|
|
10
6
|
*/
|
|
11
|
-
export
|
|
12
|
-
const names = scope === "bambu"
|
|
13
|
-
? ["BAMBU_REQUIRE_CONFIRMATION", "PRINT_REQUIRE_CONFIRMATION"]
|
|
14
|
-
: ["PRINT_REQUIRE_CONFIRMATION"];
|
|
15
|
-
const values = names
|
|
16
|
-
.map((name) => process.env[name]?.trim())
|
|
17
|
-
.filter((value): value is string => value !== undefined && value !== "");
|
|
18
|
-
return values.length > 0 && values.every((value) => value === "0");
|
|
19
|
-
}
|
|
7
|
+
export type ConfirmHardware = (message: string) => Promise<boolean>;
|
|
20
8
|
|
|
21
9
|
/**
|
|
22
|
-
* How long a
|
|
23
|
-
*
|
|
10
|
+
* How long a person has to answer a prompt from the server (a resume of a job
|
|
11
|
+
* that could not be re-read, or the Bambu printer-model prompt). The MCP SDK's
|
|
12
|
+
* default request timeout (60 s) is too short for someone walking to the printer.
|
|
24
13
|
*/
|
|
25
14
|
export function confirmationTimeoutMs(): number {
|
|
26
15
|
const raw = process.env.PRINT_CONFIRMATION_TIMEOUT_MS?.trim();
|
|
@@ -35,27 +24,15 @@ export function confirmationTimedOut(timeoutMs: number): string {
|
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
export const CONFIRMATION_UNAVAILABLE =
|
|
38
|
-
"
|
|
39
|
-
"PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0 for Bambu printers) disables ordinary print/heat prompts. No command was sent.";
|
|
27
|
+
"This step needs a person to confirm a check at the printer, which requires an MCP client with elicitation support. No command was sent.";
|
|
40
28
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
* clearing hardware errors). Those are required even when prompts are opted out.
|
|
44
|
-
*/
|
|
45
|
-
export async function requireHumanConfirmation(
|
|
46
|
-
confirm: ConfirmHardware | undefined,
|
|
47
|
-
message: string,
|
|
48
|
-
scope: ConfirmationScope,
|
|
49
|
-
physicalCheck = false
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
if (!physicalCheck && confirmationOptOut(scope)) return;
|
|
29
|
+
/** Ask a person to accept a physical check that software cannot replace. */
|
|
30
|
+
export async function requireHumanConfirmation(confirm: ConfirmHardware | undefined, message: string): Promise<void> {
|
|
52
31
|
if (!confirm) throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
53
32
|
if (!(await confirm(message))) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
? "PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0)"
|
|
59
|
-
: "PRINT_REQUIRE_CONFIRMATION=0"} to skip ordinary print and heating prompts.`));
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " +
|
|
35
|
+
"This confirmation covers a physical check at the printer and always needs a person to accept it."
|
|
36
|
+
);
|
|
60
37
|
}
|
|
61
38
|
}
|