bambu-printer-mcp 1.1.29 → 1.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTORS.md +3 -1
- package/README.md +1 -1
- package/dist/index.js +28 -4
- package/dist/printers/bambu.d.ts +3 -2
- package/dist/printers/bambu.js +41 -4
- package/package.json +2 -2
- package/src/index.ts +29 -5
- package/src/printers/bambu.ts +42 -4
package/CONTRIBUTORS.md
CHANGED
|
@@ -6,7 +6,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. |
|
|
9
|
+
| [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. |
|
|
10
10
|
| [patrikmadoran-cyber](https://github.com/patrikmadoran-cyber) | Adds explicit Standard/High Flow nozzle selection across slicing and print auto-slicing in [#54](https://github.com/DMontgomery40/bambu-printer-mcp/pull/54), with unsupported-variant regressions and real Windows BambuStudio 02.06.00.51 H2S 0.6 slicing evidence. No physical print is claimed. |
|
|
11
11
|
| [Patrick Juchli (@patrickjuchli)](https://github.com/patrickjuchli) | Maintains basic-ftp and supplies the 6.2.3 fixes for rejected-command recovery, asynchronous credential-error crashes, and password redaction used by direct FTPS transfers. |
|
|
12
12
|
| [Aleksei Popovich (alexpapay)](https://github.com/alexpapay) | Fixes cross-platform AMS profile resolution and packed P2S chamber status in [#43](https://github.com/DMontgomery40/bambu-printer-mcp/pull/43), contributes regressions, and supplies read-only Windows P2S/AMS 2 Pro status and local slicing evidence. No physical print is claimed for this contribution. |
|
|
@@ -46,3 +46,5 @@ See the [full contribution history](https://github.com/DMontgomery40/bambu-print
|
|
|
46
46
|
Special thanks to [FULU Foundation](https://www.fulu.org/), [Louis Rossmann](https://www.youtube.com/watch?v=1jhRqgHxEP8), and the [OrcaSlicer-bambulab contributors](https://github.com/FULU-Foundation/OrcaSlicer-bambulab) for their work supporting user control and interoperable tools. We support open-source software, repair rights, and a local print workflow that does not require Bambu Studio, Bambu Connect, or Bambu Cloud.
|
|
47
47
|
|
|
48
48
|
This is community recognition, separate from code authorship in this repository. The optional FULU bridge uses Bambu's networking runtime; see the [FULU setup guide](./docs/FULU.md) for the direct LAN alternative and current validation limits.
|
|
49
|
+
|
|
50
|
+
Thanks to the [MQTT.js contributors](https://github.com/mqttjs/MQTT.js/releases/tag/v5.16.1), [proxy-addr maintainers](https://github.com/jshttp/proxy-addr/releases/tag/v2.0.8), and [source-map-js maintainers](https://github.com/7rulnik/source-map-js) for the reconnect and security fixes incorporated in 1.1.31. This recognizes upstream dependency work separately from authorship in this repository.
|
package/README.md
CHANGED
|
@@ -966,7 +966,7 @@ Use the same package version as the server, for example `bambu-printer-mcp@1.2.3
|
|
|
966
966
|
|
|
967
967
|
`--check-update` asks Bambu's plug-in service for the newest Linux plug-in and compares the `PrintParams` layout in the matching Bambu Studio header with the helper. It reports only: the setup never installs an unpinned plug-in, because a changed layout would send wrong print parameters. A pin is raised by a package release after the check passes and a hardware test.
|
|
968
968
|
|
|
969
|
-
Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. A repeated stop or heater-off is harmless. macOS keeps its native-only route. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
|
|
969
|
+
Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited and the immediate MQTT attempt has settled, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. Each MQTT send gives up after 15 seconds (`BAMBU_EMERGENCY_MQTT_TIMEOUT_MS`), so an unreachable MQTT connection cannot hold back a stop the native route already sent. A connection completing after that deadline cannot dispatch its expired command; an already queued MQTT publish is discarded by force-closing and invalidating that client. A repeated stop or heater-off is harmless. macOS keeps its native-only route. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
|
|
970
970
|
|
|
971
971
|
#### x2d_native_control
|
|
972
972
|
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,21 @@ const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase(
|
|
|
52
52
|
const DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
53
53
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
54
54
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
55
|
+
/** Bound for each Linux X2D emergency MQTT send (BAMBU_EMERGENCY_MQTT_TIMEOUT_MS, 1–60000 ms). */
|
|
56
|
+
function withEmergencyMqttTimeout(send) {
|
|
57
|
+
const raw = Number(process.env.BAMBU_EMERGENCY_MQTT_TIMEOUT_MS?.trim() || 15000);
|
|
58
|
+
const ms = Number.isInteger(raw) && raw >= 1 && raw <= 60000 ? raw : 15000;
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
let timer;
|
|
61
|
+
const timeout = new Promise((_, reject) => {
|
|
62
|
+
timer = setTimeout(() => {
|
|
63
|
+
const error = new Error(`no MQTT confirmation within ${ms / 1000} s`);
|
|
64
|
+
controller.abort(error);
|
|
65
|
+
reject(error);
|
|
66
|
+
}, ms);
|
|
67
|
+
});
|
|
68
|
+
return Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).finally(() => clearTimeout(timer));
|
|
69
|
+
}
|
|
55
70
|
function requestedPrintNozzles(args) {
|
|
56
71
|
if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
|
|
57
72
|
throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
|
|
@@ -2676,10 +2691,19 @@ class BambuPrinterMCPServer {
|
|
|
2676
2691
|
const linuxEmergency = (native, mqtt) => async () => {
|
|
2677
2692
|
if (process.platform !== "linux")
|
|
2678
2693
|
return native();
|
|
2694
|
+
// An unreachable printer makes the MQTT connect retry forever; bound each
|
|
2695
|
+
// MQTT send so it cannot hold back a stop the native route already sent.
|
|
2696
|
+
const immediate = withEmergencyMqttTimeout(mqtt);
|
|
2679
2697
|
const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
|
|
2680
2698
|
native(),
|
|
2681
|
-
|
|
2682
|
-
|
|
2699
|
+
immediate,
|
|
2700
|
+
// Finish the first attempt before reusing its cached publisher: its
|
|
2701
|
+
// expiry must never close the later, counted send's transport.
|
|
2702
|
+
Promise.allSettled([interruptNativeOperations(bambuSerial), immediate]).then(results => {
|
|
2703
|
+
if (results[0].status === "rejected")
|
|
2704
|
+
throw results[0].reason;
|
|
2705
|
+
return withEmergencyMqttTimeout(mqtt);
|
|
2706
|
+
}),
|
|
2683
2707
|
]);
|
|
2684
2708
|
const reason = (result) => result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
|
|
2685
2709
|
if (nativeResult.status === "rejected" && mqttResult.status === "rejected") {
|
|
@@ -2896,7 +2920,7 @@ class BambuPrinterMCPServer {
|
|
|
2896
2920
|
case "cancel_print":
|
|
2897
2921
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls()
|
|
2898
2922
|
? linuxEmergency(() => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2899
|
-
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }),
|
|
2923
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }), signal => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken, signal))
|
|
2900
2924
|
: undefined);
|
|
2901
2925
|
break;
|
|
2902
2926
|
case "pause_print":
|
|
@@ -3036,7 +3060,7 @@ class BambuPrinterMCPServer {
|
|
|
3036
3060
|
const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3037
3061
|
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
|
|
3038
3062
|
return target === 0
|
|
3039
|
-
? linuxEmergency(native,
|
|
3063
|
+
? linuxEmergency(native, signal => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater, signal))()
|
|
3040
3064
|
: native();
|
|
3041
3065
|
}
|
|
3042
3066
|
: undefined);
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -60,10 +60,11 @@ export declare class BambuImplementation {
|
|
|
60
60
|
print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
|
|
61
61
|
private print3mfPrepared;
|
|
62
62
|
cancelJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
|
|
63
|
+
private invokeEmergencyCommand;
|
|
63
64
|
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
64
|
-
sendStopOverMqtt(host: string, serial: string, token: string): Promise<any>;
|
|
65
|
+
sendStopOverMqtt(host: string, serial: string, token: string, signal?: AbortSignal): Promise<any>;
|
|
65
66
|
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
66
|
-
sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle"): Promise<any>;
|
|
67
|
+
sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle", signal?: AbortSignal): Promise<any>;
|
|
67
68
|
pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
|
|
68
69
|
resumeJob(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
|
|
69
70
|
clearHmsErrors(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -356,6 +356,12 @@ class BambuClientStore {
|
|
|
356
356
|
throw error;
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
+
forgetPrinter(printer) {
|
|
360
|
+
for (const [key, cached] of this.printers) {
|
|
361
|
+
if (cached === printer)
|
|
362
|
+
this.clearPrinterState(key);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
359
365
|
async disconnectAll() {
|
|
360
366
|
const disconnectPromises = [];
|
|
361
367
|
for (const printer of this.printers.values()) {
|
|
@@ -836,17 +842,48 @@ export class BambuImplementation {
|
|
|
836
842
|
throw new Error(`Failed to cancel print: ${error.message}`);
|
|
837
843
|
}
|
|
838
844
|
}
|
|
845
|
+
async invokeEmergencyCommand(printer, command, signal) {
|
|
846
|
+
if (!signal)
|
|
847
|
+
return invokeWithoutAck(printer, command);
|
|
848
|
+
let onAbort;
|
|
849
|
+
try {
|
|
850
|
+
await new Promise((resolve, reject) => {
|
|
851
|
+
onAbort = () => {
|
|
852
|
+
// Force-close the actual transport: a raced publish may be queued
|
|
853
|
+
// inside MQTT.js and otherwise dispatch after a later reconnect.
|
|
854
|
+
this.printerStore.forgetPrinter(printer);
|
|
855
|
+
void printer.disconnect(true).catch(() => {
|
|
856
|
+
console.error("Failed to close an expired emergency MQTT client.");
|
|
857
|
+
});
|
|
858
|
+
reject(signal.reason);
|
|
859
|
+
};
|
|
860
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
861
|
+
if (signal.aborted)
|
|
862
|
+
onAbort();
|
|
863
|
+
else
|
|
864
|
+
command.invoke(printer).then(resolve, reject);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
signal.removeEventListener("abort", onAbort);
|
|
869
|
+
}
|
|
870
|
+
await sleep(COMMAND_SETTLE_MS);
|
|
871
|
+
}
|
|
839
872
|
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
840
|
-
async sendStopOverMqtt(host, serial, token) {
|
|
873
|
+
async sendStopOverMqtt(host, serial, token, signal) {
|
|
874
|
+
signal?.throwIfAborted();
|
|
841
875
|
const printer = await this.getPrinter(host, serial, token);
|
|
842
|
-
|
|
876
|
+
signal?.throwIfAborted();
|
|
877
|
+
await this.invokeEmergencyCommand(printer, new UpdateStateCommand({ state: "stop" }), signal);
|
|
843
878
|
return { status: "success", message: "Cancel command sent over MQTT." };
|
|
844
879
|
}
|
|
845
880
|
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
846
|
-
async sendHeaterOffOverMqtt(host, serial, token, heater) {
|
|
881
|
+
async sendHeaterOffOverMqtt(host, serial, token, heater, signal) {
|
|
882
|
+
signal?.throwIfAborted();
|
|
847
883
|
const printer = await this.getPrinter(host, serial, token);
|
|
884
|
+
signal?.throwIfAborted();
|
|
848
885
|
const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
|
|
849
|
-
await
|
|
886
|
+
await this.invokeEmergencyCommand(printer, new GCodeLineCommand({ gcodes: [gcode] }), signal);
|
|
850
887
|
return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
|
|
851
888
|
}
|
|
852
889
|
async pauseJob(host, serial, token, dispatch) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bambu-printer-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.31",
|
|
4
4
|
"description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"basic-ftp": "^6.2.3",
|
|
55
55
|
"dotenv": "^16.3.1",
|
|
56
56
|
"jszip": "^3.10.2",
|
|
57
|
-
"mqtt": "^5.16.
|
|
57
|
+
"mqtt": "^5.16.1",
|
|
58
58
|
"patch-package": "8.0.1",
|
|
59
59
|
"three": "^0.173.0",
|
|
60
60
|
"xml2js": "^0.6.2"
|
package/src/index.ts
CHANGED
|
@@ -85,6 +85,22 @@ const DEFAULT_BAMBU_CONNECTION_MODE =
|
|
|
85
85
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
86
86
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
87
87
|
|
|
88
|
+
/** Bound for each Linux X2D emergency MQTT send (BAMBU_EMERGENCY_MQTT_TIMEOUT_MS, 1–60000 ms). */
|
|
89
|
+
function withEmergencyMqttTimeout<T>(send: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
90
|
+
const raw = Number(process.env.BAMBU_EMERGENCY_MQTT_TIMEOUT_MS?.trim() || 15_000);
|
|
91
|
+
const ms = Number.isInteger(raw) && raw >= 1 && raw <= 60_000 ? raw : 15_000;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
94
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
const error = new Error(`no MQTT confirmation within ${ms / 1000} s`);
|
|
97
|
+
controller.abort(error);
|
|
98
|
+
reject(error);
|
|
99
|
+
}, ms);
|
|
100
|
+
});
|
|
101
|
+
return Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).finally(() => clearTimeout(timer));
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
function requestedPrintNozzles(args: Record<string, any>): number[] | undefined {
|
|
89
105
|
if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
|
|
90
106
|
throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
|
|
@@ -3208,12 +3224,20 @@ class BambuPrinterMCPServer {
|
|
|
3208
3224
|
// again once every older helper for this printer has exited, and only
|
|
3209
3225
|
// that send counts. Either counted route succeeding is success. macOS
|
|
3210
3226
|
// stays native-only.
|
|
3211
|
-
const linuxEmergency = (native: () => Promise<any>, mqtt: () => Promise<any>) => async () => {
|
|
3227
|
+
const linuxEmergency = (native: () => Promise<any>, mqtt: (signal: AbortSignal) => Promise<any>) => async () => {
|
|
3212
3228
|
if (process.platform !== "linux") return native();
|
|
3229
|
+
// An unreachable printer makes the MQTT connect retry forever; bound each
|
|
3230
|
+
// MQTT send so it cannot hold back a stop the native route already sent.
|
|
3231
|
+
const immediate = withEmergencyMqttTimeout(mqtt);
|
|
3213
3232
|
const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
|
|
3214
3233
|
native(),
|
|
3215
|
-
|
|
3216
|
-
|
|
3234
|
+
immediate,
|
|
3235
|
+
// Finish the first attempt before reusing its cached publisher: its
|
|
3236
|
+
// expiry must never close the later, counted send's transport.
|
|
3237
|
+
Promise.allSettled([interruptNativeOperations(bambuSerial), immediate]).then(results => {
|
|
3238
|
+
if (results[0].status === "rejected") throw results[0].reason;
|
|
3239
|
+
return withEmergencyMqttTimeout(mqtt);
|
|
3240
|
+
}),
|
|
3217
3241
|
]);
|
|
3218
3242
|
const reason = (result: PromiseSettledResult<any>) =>
|
|
3219
3243
|
result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
|
|
@@ -3487,7 +3511,7 @@ class BambuPrinterMCPServer {
|
|
|
3487
3511
|
? linuxEmergency(
|
|
3488
3512
|
() => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3489
3513
|
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }),
|
|
3490
|
-
|
|
3514
|
+
signal => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken, signal))
|
|
3491
3515
|
: undefined);
|
|
3492
3516
|
break;
|
|
3493
3517
|
|
|
@@ -3647,7 +3671,7 @@ class BambuPrinterMCPServer {
|
|
|
3647
3671
|
const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3648
3672
|
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
|
|
3649
3673
|
return target === 0
|
|
3650
|
-
? linuxEmergency(native,
|
|
3674
|
+
? linuxEmergency(native, signal => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater, signal))()
|
|
3651
3675
|
: native();
|
|
3652
3676
|
}
|
|
3653
3677
|
: undefined
|
package/src/printers/bambu.ts
CHANGED
|
@@ -438,6 +438,12 @@ class BambuClientStore {
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
forgetPrinter(printer: BambuClient): void {
|
|
442
|
+
for (const [key, cached] of this.printers) {
|
|
443
|
+
if (cached === printer) this.clearPrinterState(key);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
441
447
|
async disconnectAll(): Promise<void> {
|
|
442
448
|
const disconnectPromises: Promise<void>[] = [];
|
|
443
449
|
|
|
@@ -964,18 +970,50 @@ export class BambuImplementation {
|
|
|
964
970
|
}
|
|
965
971
|
}
|
|
966
972
|
|
|
973
|
+
private async invokeEmergencyCommand(
|
|
974
|
+
printer: BambuClient,
|
|
975
|
+
command: { invoke(client: BambuClient): Promise<void> },
|
|
976
|
+
signal?: AbortSignal
|
|
977
|
+
): Promise<void> {
|
|
978
|
+
if (!signal) return invokeWithoutAck(printer, command);
|
|
979
|
+
let onAbort: () => void;
|
|
980
|
+
try {
|
|
981
|
+
await new Promise<void>((resolve, reject) => {
|
|
982
|
+
onAbort = () => {
|
|
983
|
+
// Force-close the actual transport: a raced publish may be queued
|
|
984
|
+
// inside MQTT.js and otherwise dispatch after a later reconnect.
|
|
985
|
+
this.printerStore.forgetPrinter(printer);
|
|
986
|
+
void printer.disconnect(true).catch(() => {
|
|
987
|
+
console.error("Failed to close an expired emergency MQTT client.");
|
|
988
|
+
});
|
|
989
|
+
reject(signal.reason);
|
|
990
|
+
};
|
|
991
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
992
|
+
if (signal.aborted) onAbort();
|
|
993
|
+
else command.invoke(printer).then(resolve, reject);
|
|
994
|
+
});
|
|
995
|
+
} finally {
|
|
996
|
+
signal.removeEventListener("abort", onAbort!);
|
|
997
|
+
}
|
|
998
|
+
await sleep(COMMAND_SETTLE_MS);
|
|
999
|
+
}
|
|
1000
|
+
|
|
967
1001
|
/** Raw MQTT stop for routes that already ran the shared cancellation steps. */
|
|
968
|
-
async sendStopOverMqtt(host: string, serial: string, token: string): Promise<any> {
|
|
1002
|
+
async sendStopOverMqtt(host: string, serial: string, token: string, signal?: AbortSignal): Promise<any> {
|
|
1003
|
+
signal?.throwIfAborted();
|
|
969
1004
|
const printer = await this.getPrinter(host, serial, token);
|
|
970
|
-
|
|
1005
|
+
signal?.throwIfAborted();
|
|
1006
|
+
await this.invokeEmergencyCommand(printer, new UpdateStateCommand({ state: "stop" }), signal);
|
|
971
1007
|
return { status: "success", message: "Cancel command sent over MQTT." };
|
|
972
1008
|
}
|
|
973
1009
|
|
|
974
1010
|
/** Raw MQTT heater-off; switching off needs no material or model declaration. */
|
|
975
|
-
async sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle"): Promise<any> {
|
|
1011
|
+
async sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle", signal?: AbortSignal): Promise<any> {
|
|
1012
|
+
signal?.throwIfAborted();
|
|
976
1013
|
const printer = await this.getPrinter(host, serial, token);
|
|
1014
|
+
signal?.throwIfAborted();
|
|
977
1015
|
const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
|
|
978
|
-
await
|
|
1016
|
+
await this.invokeEmergencyCommand(printer, new GCodeLineCommand({ gcodes: [gcode] }), signal);
|
|
979
1017
|
return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
|
|
980
1018
|
}
|
|
981
1019
|
|