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 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
- mqtt(),
2682
- interruptNativeOperations(bambuSerial).then(() => mqtt()),
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 }), () => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken))
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, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
3063
+ ? linuxEmergency(native, signal => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater, signal))()
3040
3064
  : native();
3041
3065
  }
3042
3066
  : undefined);
@@ -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>;
@@ -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
- await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
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 invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
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.29",
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.0",
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
- mqtt(),
3216
- interruptNativeOperations(bambuSerial).then(() => mqtt()),
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
- () => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken))
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, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
3674
+ ? linuxEmergency(native, signal => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater, signal))()
3651
3675
  : native();
3652
3676
  }
3653
3677
  : undefined
@@ -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
- await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
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 invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
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