bambu-printer-mcp 1.1.13 → 1.1.15

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.
@@ -46,14 +46,14 @@ export declare class BambuImplementation {
46
46
  getStatus(host: string, serial: string, token: string): Promise<any>;
47
47
  print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
48
48
  private print3mfPrepared;
49
- cancelJob(host: string, serial: string, token: string): Promise<any>;
50
- pauseJob(host: string, serial: string, token: string): Promise<any>;
51
- resumeJob(host: string, serial: string, token: string): Promise<any>;
52
- clearHmsErrors(host: string, serial: string, token: string): Promise<any>;
49
+ cancelJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
50
+ pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
51
+ resumeJob(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
52
+ clearHmsErrors(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
53
53
  setPrintSpeed(host: string, serial: string, token: string, speedMode: string | number): Promise<any>;
54
54
  setAirductMode(host: string, serial: string, token: string, mode: string): Promise<any>;
55
55
  rereadAmsRfid(host: string, serial: string, token: string, amsId: number, slotId: number): Promise<any>;
56
- setTemperature(host: string, serial: string, token: string, component: string, temperature: unknown, bambuModel?: string, material?: string, nozzleDiameter?: number): Promise<{
56
+ setTemperature(host: string, serial: string, token: string, component: string, temperature: unknown, bambuModel?: string, material?: string, nozzleDiameter?: number, dispatch?: (heater: "bed" | "nozzle", target: number, assertActive: () => void, beforeDispatch?: () => Promise<void>) => Promise<any>): Promise<{
57
57
  status: string;
58
58
  message: string;
59
59
  command: string;
@@ -84,7 +84,7 @@ export declare class BambuImplementation {
84
84
  name: string;
85
85
  exists: boolean;
86
86
  }>;
87
- uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string): Promise<{
87
+ uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string, upload?: (snapshot: string, destination: string, assertActive: () => void) => Promise<void>): Promise<{
88
88
  status: string;
89
89
  uploaded: boolean;
90
90
  remotePath: string;
@@ -128,8 +128,8 @@ export declare class BambuImplementation {
128
128
  * [16..16+payloadSize] JPEG (FF D8 ... FF D9)
129
129
  *
130
130
  * Verified models per upstream docs: A1, A1 mini, P1S, P1P. X1/X1C/X1E
131
- * and P2S use RTSP on port 322 instead. H2/H2S/H2D/H2C
132
- * are not documented; we fail fast rather than guess at the protocol.
131
+ * and P2S use RTSP on port 322 instead. H2/H2S/H2D/H2C/X2D
132
+ * use the same RTSP path.
133
133
  *
134
134
  * Read-only; no confirm gate. Default 8s timeout for cold-start latency.
135
135
  */
@@ -163,18 +163,21 @@ export declare class BambuImplementation {
163
163
  }>;
164
164
  /**
165
165
  * Pull a single JPEG frame from the printer's RTSP/RTSPS stream using
166
- * ffmpeg. Used for X1, P2S, and H2 series.
166
+ * ffmpeg. Used for X1, P2S, H2-family printers, and X2D.
167
167
  *
168
168
  * URL pattern verified against HA bambulab's models.py example:
169
169
  * rtsps://bblp:<access_code>@<host>:322/streaming/live/1
170
170
  *
171
171
  * ffmpeg invocation:
172
- * ffmpeg -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
172
+ * ffmpeg -tls_verify 0 -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
173
173
  *
174
- * -rtsp_transport tcp avoids UDP NAT/firewall issues. -frames:v 1
175
- * makes ffmpeg exit as soon as one frame lands. -y overwrites the temp
176
- * file. The Bambu printer presents a self-signed cert; ffmpeg's TLS
177
- * layer accepts that by default (no host verification).
174
+ * -tls_verify 0 accepts the printer's self-signed certificate. This is
175
+ * consistent with the existing local-device TLS/FTPS paths, which do not
176
+ * have a public CA chain or hostname that ffmpeg can validate. The
177
+ * connection is still encrypted and remains scoped to the configured
178
+ * printer host. -rtsp_transport tcp avoids UDP NAT/firewall issues.
179
+ * -frames:v 1 makes ffmpeg exit as soon as one frame lands. -y overwrites
180
+ * the temp file.
178
181
  */
179
182
  private fetchRtspCameraFrame;
180
183
  /**
@@ -51,11 +51,11 @@ const MODEL_ID_TO_NAME = {
51
51
  "BL-P002": "X1",
52
52
  C13: "X1E",
53
53
  };
54
- const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"]);
54
+ const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s", "x2d"]);
55
55
  export function assertDirectPrintSupported(model, serial) {
56
56
  if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
57
- throw new Error("X2D direct printing is not supported pending native eMMC transport support. " +
58
- "X2D status and slicing remain available; use a supported slicer to print.");
57
+ throw new Error("X2D direct printing is not supported through legacy MQTT/FTPS. Use the macOS native eMMC route via print_3mf. " +
58
+ "X2D status and slicing remain available on all supported platforms.");
59
59
  }
60
60
  }
61
61
  function isH2ModelName(model) {
@@ -196,9 +196,18 @@ class TolerantBambuClient extends BambuClient {
196
196
  return [undefined];
197
197
  }
198
198
  }
199
- /** Build FTPS secureOptions that include the client cert+key when available. */
200
- function ftpsSecureOptions() {
201
- const opts = { rejectUnauthorized: false };
199
+ /**
200
+ * Build FTPS secureOptions that include the client cert+key when available.
201
+ *
202
+ * `host` must be included: basic-ftp opens each data connection by wrapping
203
+ * a plain socket with tls.connect(tlsOptions), and without a host Node can bind
204
+ * the resumable session to "localhost" instead of the printer host. Printers
205
+ * that require TLS session reuse (vsftpd require_ssl_reuse, e.g. X2D) refuse
206
+ * to resume the session and reply "522 SSL connection failed: session reuse
207
+ * required", failing every LIST, STOR and RETR.
208
+ */
209
+ function ftpsSecureOptions(host) {
210
+ const opts = { rejectUnauthorized: false, host };
202
211
  if (CLIENT_CREDS) {
203
212
  opts.cert = CLIENT_CREDS.cert;
204
213
  opts.key = CLIENT_CREDS.key;
@@ -751,12 +760,15 @@ export class BambuImplementation {
751
760
  amsMapping,
752
761
  };
753
762
  }
754
- async cancelJob(host, serial, token) {
763
+ async cancelJob(host, serial, token, dispatch) {
755
764
  cancelPendingPrinterOperations(host, serial);
756
765
  this.checkedJobs.delete(`${host}\n${serial}`);
757
- const printer = await this.getPrinter(host, serial, token);
766
+ const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
758
767
  try {
759
- await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
768
+ if (dispatch)
769
+ return await dispatch();
770
+ else
771
+ await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
760
772
  this.checkedJobs.delete(`${host}\n${serial}`);
761
773
  return { status: "success", message: "Cancel command sent successfully." };
762
774
  }
@@ -764,36 +776,47 @@ export class BambuImplementation {
764
776
  throw new Error(`Failed to cancel print: ${error.message}`);
765
777
  }
766
778
  }
767
- async pauseJob(host, serial, token) {
768
- const printer = await this.getPrinter(host, serial, token);
779
+ async pauseJob(host, serial, token, dispatch) {
780
+ const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
769
781
  try {
770
- await invokeWithoutAck(printer, new UpdateStateCommand({ state: "pause" }));
782
+ if (dispatch)
783
+ return await dispatch();
784
+ else
785
+ await invokeWithoutAck(printer, new UpdateStateCommand({ state: "pause" }));
771
786
  return { status: "success", message: "Pause command sent successfully." };
772
787
  }
773
788
  catch (error) {
774
789
  throw new Error(`Failed to pause print: ${error.message}`);
775
790
  }
776
791
  }
777
- async resumeJob(host, serial, token) {
792
+ async resumeJob(host, serial, token, dispatch) {
778
793
  return withPrinterOperation(host, serial, async (assertActive) => {
779
794
  const checked = this.checkedJobs.get(`${host}\n${serial}`);
780
795
  if (!checked)
781
796
  throw new Error("Resume requires a job inspected and started by this server instance. Verify other jobs on the printer before resuming them there.");
782
- const status = await this.getSafetyStatus(host, serial, token);
783
- validatePrinterState(status, { ...checked.requirements, requireIdle: false });
784
- if (status.raw.gcode_state !== "PAUSE")
785
- throw new Error("Resume requires a freshly reported paused job.");
786
- const stem = (value) => path.posix.basename(value).replace(/(?:\.gcode)?\.3mf$|\.gcode$/i, "");
787
- const names = [status.raw.gcode_file, status.raw.subtask_name].filter(value => typeof value === "string");
788
- if (!names.some(value => stem(value) === stem(checked.remotePath)))
789
- throw new Error("Paused job identity does not match this server's inspected artifact.");
790
- const printer = await this.getPrinter(host, serial, token);
797
+ const beforeDispatch = async () => {
798
+ assertActive();
799
+ const status = await this.getSafetyStatus(host, serial, token);
800
+ validatePrinterState(status, { ...checked.requirements, requireIdle: false });
801
+ if (status.raw.gcode_state !== "PAUSE")
802
+ throw new Error("Resume requires a freshly reported paused job.");
803
+ const stem = (value) => path.posix.basename(value).replace(/(?:\.gcode)?\.3mf$|\.gcode$/i, "");
804
+ const names = [status.raw.gcode_file, status.raw.subtask_name].filter(value => typeof value === "string");
805
+ if (!names.some(value => stem(value) === stem(checked.remotePath)))
806
+ throw new Error("Paused job identity does not match this server's inspected artifact.");
807
+ assertActive();
808
+ };
809
+ await beforeDispatch();
810
+ const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
791
811
  assertActive();
792
- await invokeWithoutAck(printer, new UpdateStateCommand({ state: "resume" }));
812
+ if (dispatch)
813
+ await dispatch(assertActive, beforeDispatch);
814
+ else
815
+ await invokeWithoutAck(printer, new UpdateStateCommand({ state: "resume" }));
793
816
  return { status: "success", message: "Resume command sent successfully." };
794
817
  });
795
818
  }
796
- async clearHmsErrors(host, serial, token) {
819
+ async clearHmsErrors(host, serial, token, dispatch) {
797
820
  return withPrinterOperation(host, serial, async (assertActive) => {
798
821
  const codesFor = (status) => {
799
822
  if (!Array.isArray(status.raw?.hms) || status.raw?.print_error === undefined)
@@ -802,12 +825,20 @@ export class BambuImplementation {
802
825
  };
803
826
  const codes = codesFor(await this.getSafetyStatus(host, serial, token));
804
827
  await this.confirmHardwareAction(`Clear reported hardware errors on ${serial}: ${codes.join(", ")}? Inspect the printer and resolve the physical cause first. Confirm only after removing obstructions and correcting the fault.`, true);
805
- const current = codesFor(await this.getSafetyStatus(host, serial, token));
806
- if (JSON.stringify(current) !== JSON.stringify(codes))
807
- throw new Error("Hardware errors changed during confirmation. Inspect the new report before retrying.");
808
- const printer = await this.getPrinter(host, serial, token);
828
+ const beforeDispatch = async () => {
829
+ assertActive();
830
+ const current = codesFor(await this.getSafetyStatus(host, serial, token));
831
+ if (JSON.stringify(current) !== JSON.stringify(codes))
832
+ throw new Error("Hardware errors changed before clearing. Inspect the new report before retrying.");
833
+ assertActive();
834
+ };
835
+ await beforeDispatch();
836
+ const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
809
837
  assertActive();
810
- await printer.publish({ print: { command: "clean_print_error", sequence_id: "0" } });
838
+ if (dispatch)
839
+ await dispatch(assertActive, beforeDispatch);
840
+ else
841
+ await printer.publish({ print: { command: "clean_print_error", sequence_id: "0" } });
811
842
  this.clearedErrors.set(serial, codes);
812
843
  await sleep(COMMAND_SETTLE_MS);
813
844
  return { status: "success", message: "Confirmed HMS clear command sent. The next print requires acknowledgment of these cleared codes.", cleared_codes: codes };
@@ -862,15 +893,15 @@ export class BambuImplementation {
862
893
  };
863
894
  }
864
895
  async rereadAmsRfid(host, serial, token, amsId, slotId) {
865
- const printer = await this.getPrinter(host, serial, token);
866
- const normalizedAmsId = Math.trunc(amsId);
867
- const normalizedSlotId = Math.trunc(slotId);
896
+ const normalizedAmsId = amsId;
897
+ const normalizedSlotId = slotId;
868
898
  if (!Number.isInteger(normalizedAmsId) || normalizedAmsId < 0 || normalizedAmsId > 3) {
869
899
  throw new Error("ams_id must be an integer from 0 to 3.");
870
900
  }
871
901
  if (!Number.isInteger(normalizedSlotId) || normalizedSlotId < 0 || normalizedSlotId > 3) {
872
902
  throw new Error("slot_id must be an integer from 0 to 3.");
873
903
  }
904
+ const printer = await this.getPrinter(host, serial, token);
874
905
  await printer.publish({
875
906
  print: {
876
907
  command: "ams_get_rfid",
@@ -887,7 +918,7 @@ export class BambuImplementation {
887
918
  slot_id: normalizedSlotId,
888
919
  };
889
920
  }
890
- async setTemperature(host, serial, token, component, temperature, bambuModel, material, nozzleDiameter = 0.4) {
921
+ async setTemperature(host, serial, token, component, temperature, bambuModel, material, nozzleDiameter = 0.4, dispatch) {
891
922
  const normalizedComponent = component.toLowerCase();
892
923
  const heater = normalizedComponent === "bed" ? "bed" :
893
924
  ["extruder", "nozzle", "tool", "tool0"].includes(normalizedComponent) ? "nozzle" : undefined;
@@ -906,20 +937,28 @@ export class BambuImplementation {
906
937
  const targetTemperature = temperature === 0 ? 0 : validateTemperature(heater, temperature, model, material ? [material] : undefined);
907
938
  const gcode = `${heater === "bed" ? "M140" : "M104"}${heater === "nozzle" && targetTemperature > 0 ? " T0" : ""} S${targetTemperature}`;
908
939
  const send = async (assertActive) => {
940
+ let beforeDispatch;
909
941
  if (targetTemperature > 0) {
910
942
  const status = await this.getSafetyStatus(host, serial, token);
911
- validatePrinterState(status, heater === "nozzle"
912
- ? manualHeatingRequirements(status, model, nozzleDiameter, material)
943
+ const validateHeatingStatus = (current) => validatePrinterState(current, heater === "nozzle"
944
+ ? manualHeatingRequirements(current, model, nozzleDiameter, material)
913
945
  : { model: model, nozzleDiameters: [] });
946
+ validateHeatingStatus(status);
914
947
  await this.confirmHardwareAction(`Heat ${heater} on ${model.toUpperCase()} (${serial}) to ${targetTemperature}°C?${material ? ` Declared material: ${material}. Confirm the physical spool label.` : ""}`);
915
- const confirmedStatus = await this.getSafetyStatus(host, serial, token);
916
- validatePrinterState(confirmedStatus, heater === "nozzle"
917
- ? manualHeatingRequirements(confirmedStatus, model, nozzleDiameter, material)
918
- : { model: model, nozzleDiameters: [] });
948
+ beforeDispatch = async () => {
949
+ assertActive();
950
+ const confirmedStatus = await this.getSafetyStatus(host, serial, token);
951
+ validateHeatingStatus(confirmedStatus);
952
+ assertActive();
953
+ };
954
+ await beforeDispatch();
919
955
  }
920
- const printer = await this.getPrinter(host, serial, token);
956
+ const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
921
957
  assertActive();
922
- await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
958
+ if (dispatch)
959
+ await dispatch(heater, targetTemperature, assertActive, beforeDispatch);
960
+ else
961
+ await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
923
962
  return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode };
924
963
  };
925
964
  if (targetTemperature === 0) {
@@ -976,16 +1015,16 @@ export class BambuImplementation {
976
1015
  };
977
1016
  }
978
1017
  async setAmsDrying(host, serial, token, action, amsId) {
979
- const printer = await this.getPrinter(host, serial, token);
980
1018
  const normalizedAction = action.trim().toLowerCase();
981
1019
  if (normalizedAction !== "start" && normalizedAction !== "stop") {
982
1020
  throw new Error("AMS drying action must be one of: start, stop.");
983
1021
  }
984
- const normalizedAmsId = Math.trunc(amsId);
1022
+ const normalizedAmsId = amsId;
985
1023
  if (!Number.isInteger(normalizedAmsId) || normalizedAmsId < 0 || normalizedAmsId > 3) {
986
1024
  throw new Error("ams_id must be an integer from 0 to 3.");
987
1025
  }
988
1026
  const param = normalizedAction === "start" ? "start_drying" : "stop_drying";
1027
+ const printer = await this.getPrinter(host, serial, token);
989
1028
  await printer.publish({
990
1029
  print: {
991
1030
  command: "ams_control",
@@ -1026,19 +1065,32 @@ export class BambuImplementation {
1026
1065
  };
1027
1066
  }
1028
1067
  async getFiles(host, serial, token) {
1029
- const printer = new BambuPrinter(host, serial, token);
1068
+ const client = new FTPClient(15000);
1030
1069
  const directories = ["cache", "timelapse", "logs"];
1031
1070
  const filesByDirectory = {};
1032
- await printer.manipulateFiles(async (context) => {
1071
+ try {
1072
+ await client.access({
1073
+ host,
1074
+ port: 990,
1075
+ user: "bblp",
1076
+ password: token,
1077
+ secure: "implicit",
1078
+ secureOptions: ftpsSecureOptions(host),
1079
+ });
1080
+ await this.waitForTlsSession(client);
1081
+ // Discover absent optional directories without creating them or treating
1082
+ // ambiguous FTP 550 responses (including permission errors) as emptiness.
1083
+ const root = await client.list("/");
1033
1084
  for (const directory of directories) {
1034
- try {
1035
- filesByDirectory[directory] = await context.readDir(directory);
1036
- }
1037
- catch {
1038
- filesByDirectory[directory] = [];
1039
- }
1085
+ const present = root.some(entry => entry.name === directory && (entry.isDirectory || entry.isSymbolicLink));
1086
+ filesByDirectory[directory] = present
1087
+ ? (await client.list(`/${directory}`)).map(entry => entry.name)
1088
+ : [];
1040
1089
  }
1041
- });
1090
+ }
1091
+ finally {
1092
+ client.close();
1093
+ }
1042
1094
  const files = Object.entries(filesByDirectory).flatMap(([directory, names]) => names.map((name) => `${directory}/${name}`));
1043
1095
  return {
1044
1096
  files,
@@ -1060,7 +1112,7 @@ export class BambuImplementation {
1060
1112
  exists,
1061
1113
  };
1062
1114
  }
1063
- async uploadFile(host, serial, token, filePath, filename, print, bambuModel) {
1115
+ async uploadFile(host, serial, token, filePath, filename, print, bambuModel, upload) {
1064
1116
  const remotePath = normalizedRemotePath(filename);
1065
1117
  if (!print) {
1066
1118
  const sourceExtension = path.extname(filePath).toLowerCase();
@@ -1104,7 +1156,10 @@ export class BambuImplementation {
1104
1156
  }
1105
1157
  assertActive();
1106
1158
  const destination = path.posix.join(path.posix.dirname(remotePath), uniquePrintName(remotePath));
1107
- await this.ftpUpload(host, token, snapshot, `/${destination}`);
1159
+ if (upload)
1160
+ await upload(snapshot, destination, assertActive);
1161
+ else
1162
+ await this.ftpUpload(host, token, snapshot, `/${destination}`);
1108
1163
  return { status: "success", uploaded: true, remotePath: destination, printRequested: false, inspected: printable };
1109
1164
  }));
1110
1165
  }
@@ -1164,7 +1219,7 @@ export class BambuImplementation {
1164
1219
  async ftpDownload(host, token, remotePath, localPath) {
1165
1220
  const client = new FTPClient(15000);
1166
1221
  try {
1167
- await client.access({ host, port: 990, user: "bblp", password: token, secure: "implicit", secureOptions: ftpsSecureOptions() });
1222
+ await client.access({ host, port: 990, user: "bblp", password: token, secure: "implicit", secureOptions: ftpsSecureOptions(host) });
1168
1223
  await this.waitForTlsSession(client);
1169
1224
  const size = await client.size(`/${remotePath}`);
1170
1225
  if (!Number.isFinite(size) || size <= 0 || size > 512 * 1024 * 1024)
@@ -1197,8 +1252,8 @@ export class BambuImplementation {
1197
1252
  * [16..16+payloadSize] JPEG (FF D8 ... FF D9)
1198
1253
  *
1199
1254
  * Verified models per upstream docs: A1, A1 mini, P1S, P1P. X1/X1C/X1E
1200
- * and P2S use RTSP on port 322 instead. H2/H2S/H2D/H2C
1201
- * are not documented; we fail fast rather than guess at the protocol.
1255
+ * and P2S use RTSP on port 322 instead. H2/H2S/H2D/H2C/X2D
1256
+ * use the same RTSP path.
1202
1257
  *
1203
1258
  * Read-only; no confirm gate. Default 8s timeout for cold-start latency.
1204
1259
  */
@@ -1257,18 +1312,21 @@ export class BambuImplementation {
1257
1312
  }
1258
1313
  /**
1259
1314
  * Pull a single JPEG frame from the printer's RTSP/RTSPS stream using
1260
- * ffmpeg. Used for X1, P2S, and H2 series.
1315
+ * ffmpeg. Used for X1, P2S, H2-family printers, and X2D.
1261
1316
  *
1262
1317
  * URL pattern verified against HA bambulab's models.py example:
1263
1318
  * rtsps://bblp:<access_code>@<host>:322/streaming/live/1
1264
1319
  *
1265
1320
  * ffmpeg invocation:
1266
- * ffmpeg -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
1321
+ * ffmpeg -tls_verify 0 -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
1267
1322
  *
1268
- * -rtsp_transport tcp avoids UDP NAT/firewall issues. -frames:v 1
1269
- * makes ffmpeg exit as soon as one frame lands. -y overwrites the temp
1270
- * file. The Bambu printer presents a self-signed cert; ffmpeg's TLS
1271
- * layer accepts that by default (no host verification).
1323
+ * -tls_verify 0 accepts the printer's self-signed certificate. This is
1324
+ * consistent with the existing local-device TLS/FTPS paths, which do not
1325
+ * have a public CA chain or hostname that ffmpeg can validate. The
1326
+ * connection is still encrypted and remains scoped to the configured
1327
+ * printer host. -rtsp_transport tcp avoids UDP NAT/firewall issues.
1328
+ * -frames:v 1 makes ffmpeg exit as soon as one frame lands. -y overwrites
1329
+ * the temp file.
1272
1330
  */
1273
1331
  async fetchRtspCameraFrame(host, token, timeoutMs, ffmpegPath) {
1274
1332
  const fsSync = await import("node:fs");
@@ -1283,6 +1341,7 @@ export class BambuImplementation {
1283
1341
  // have to detect ffmpeg version. -rtsp_transport tcp avoids UDP NAT
1284
1342
  // headaches; -frames:v 1 makes ffmpeg exit on first frame.
1285
1343
  const args = [
1344
+ "-tls_verify", "0",
1286
1345
  "-rtsp_transport", "tcp",
1287
1346
  "-i", url,
1288
1347
  "-frames:v", "1",
@@ -1466,7 +1525,7 @@ export class BambuImplementation {
1466
1525
  user: "bblp",
1467
1526
  password: token,
1468
1527
  secure: "implicit",
1469
- secureOptions: ftpsSecureOptions(),
1528
+ secureOptions: ftpsSecureOptions(host),
1470
1529
  });
1471
1530
  await this.waitForTlsSession(client);
1472
1531
  const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
@@ -1491,7 +1550,7 @@ export class BambuImplementation {
1491
1550
  user: "bblp",
1492
1551
  password: token,
1493
1552
  secure: "implicit",
1494
- secureOptions: ftpsSecureOptions(),
1553
+ secureOptions: ftpsSecureOptions(host),
1495
1554
  });
1496
1555
  // With TLS 1.3 the session ticket arrives asynchronously; basic-ftp calls
1497
1556
  // getSession() when opening the data channel and gets undefined if the