bambu-printer-mcp 1.1.13 → 1.1.14
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 +2 -2
- package/README.md +18 -4
- package/dist/bambu-connect.d.ts +7 -0
- package/dist/bambu-connect.js +77 -0
- package/dist/bambu-native.d.ts +66 -0
- package/dist/bambu-native.js +609 -0
- package/dist/index.js +457 -33
- package/dist/printers/bambu.d.ts +17 -14
- package/dist/printers/bambu.js +102 -56
- package/native/bambu-native-print.cpp +758 -0
- package/package.json +7 -3
- package/scripts/build-bambu-native.zsh +33 -0
- package/src/bambu-connect.ts +100 -0
- package/src/bambu-native.ts +722 -0
- package/src/index.ts +527 -66
- package/src/printers/bambu.ts +89 -53
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
/**
|
package/dist/printers/bambu.js
CHANGED
|
@@ -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
|
|
58
|
-
"X2D status and slicing remain available
|
|
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
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
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
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
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
|
|
866
|
-
const
|
|
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(
|
|
912
|
-
? manualHeatingRequirements(
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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 =
|
|
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",
|
|
@@ -1060,7 +1099,7 @@ export class BambuImplementation {
|
|
|
1060
1099
|
exists,
|
|
1061
1100
|
};
|
|
1062
1101
|
}
|
|
1063
|
-
async uploadFile(host, serial, token, filePath, filename, print, bambuModel) {
|
|
1102
|
+
async uploadFile(host, serial, token, filePath, filename, print, bambuModel, upload) {
|
|
1064
1103
|
const remotePath = normalizedRemotePath(filename);
|
|
1065
1104
|
if (!print) {
|
|
1066
1105
|
const sourceExtension = path.extname(filePath).toLowerCase();
|
|
@@ -1104,7 +1143,10 @@ export class BambuImplementation {
|
|
|
1104
1143
|
}
|
|
1105
1144
|
assertActive();
|
|
1106
1145
|
const destination = path.posix.join(path.posix.dirname(remotePath), uniquePrintName(remotePath));
|
|
1107
|
-
|
|
1146
|
+
if (upload)
|
|
1147
|
+
await upload(snapshot, destination, assertActive);
|
|
1148
|
+
else
|
|
1149
|
+
await this.ftpUpload(host, token, snapshot, `/${destination}`);
|
|
1108
1150
|
return { status: "success", uploaded: true, remotePath: destination, printRequested: false, inspected: printable };
|
|
1109
1151
|
}));
|
|
1110
1152
|
}
|
|
@@ -1164,7 +1206,7 @@ export class BambuImplementation {
|
|
|
1164
1206
|
async ftpDownload(host, token, remotePath, localPath) {
|
|
1165
1207
|
const client = new FTPClient(15000);
|
|
1166
1208
|
try {
|
|
1167
|
-
await client.access({ host, port: 990, user: "bblp", password: token, secure: "implicit", secureOptions: ftpsSecureOptions() });
|
|
1209
|
+
await client.access({ host, port: 990, user: "bblp", password: token, secure: "implicit", secureOptions: ftpsSecureOptions(host) });
|
|
1168
1210
|
await this.waitForTlsSession(client);
|
|
1169
1211
|
const size = await client.size(`/${remotePath}`);
|
|
1170
1212
|
if (!Number.isFinite(size) || size <= 0 || size > 512 * 1024 * 1024)
|
|
@@ -1197,8 +1239,8 @@ export class BambuImplementation {
|
|
|
1197
1239
|
* [16..16+payloadSize] JPEG (FF D8 ... FF D9)
|
|
1198
1240
|
*
|
|
1199
1241
|
* 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
|
-
*
|
|
1242
|
+
* and P2S use RTSP on port 322 instead. H2/H2S/H2D/H2C/X2D
|
|
1243
|
+
* use the same RTSP path.
|
|
1202
1244
|
*
|
|
1203
1245
|
* Read-only; no confirm gate. Default 8s timeout for cold-start latency.
|
|
1204
1246
|
*/
|
|
@@ -1257,18 +1299,21 @@ export class BambuImplementation {
|
|
|
1257
1299
|
}
|
|
1258
1300
|
/**
|
|
1259
1301
|
* Pull a single JPEG frame from the printer's RTSP/RTSPS stream using
|
|
1260
|
-
* ffmpeg. Used for X1, P2S, and
|
|
1302
|
+
* ffmpeg. Used for X1, P2S, H2-family printers, and X2D.
|
|
1261
1303
|
*
|
|
1262
1304
|
* URL pattern verified against HA bambulab's models.py example:
|
|
1263
1305
|
* rtsps://bblp:<access_code>@<host>:322/streaming/live/1
|
|
1264
1306
|
*
|
|
1265
1307
|
* ffmpeg invocation:
|
|
1266
|
-
* ffmpeg -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
|
|
1308
|
+
* ffmpeg -tls_verify 0 -rtsp_transport tcp -i <url> -frames:v 1 -f image2 -c:v mjpeg -y <out>
|
|
1267
1309
|
*
|
|
1268
|
-
* -
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1310
|
+
* -tls_verify 0 accepts the printer's self-signed certificate. This is
|
|
1311
|
+
* consistent with the existing local-device TLS/FTPS paths, which do not
|
|
1312
|
+
* have a public CA chain or hostname that ffmpeg can validate. The
|
|
1313
|
+
* connection is still encrypted and remains scoped to the configured
|
|
1314
|
+
* printer host. -rtsp_transport tcp avoids UDP NAT/firewall issues.
|
|
1315
|
+
* -frames:v 1 makes ffmpeg exit as soon as one frame lands. -y overwrites
|
|
1316
|
+
* the temp file.
|
|
1272
1317
|
*/
|
|
1273
1318
|
async fetchRtspCameraFrame(host, token, timeoutMs, ffmpegPath) {
|
|
1274
1319
|
const fsSync = await import("node:fs");
|
|
@@ -1283,6 +1328,7 @@ export class BambuImplementation {
|
|
|
1283
1328
|
// have to detect ffmpeg version. -rtsp_transport tcp avoids UDP NAT
|
|
1284
1329
|
// headaches; -frames:v 1 makes ffmpeg exit on first frame.
|
|
1285
1330
|
const args = [
|
|
1331
|
+
"-tls_verify", "0",
|
|
1286
1332
|
"-rtsp_transport", "tcp",
|
|
1287
1333
|
"-i", url,
|
|
1288
1334
|
"-frames:v", "1",
|
|
@@ -1466,7 +1512,7 @@ export class BambuImplementation {
|
|
|
1466
1512
|
user: "bblp",
|
|
1467
1513
|
password: token,
|
|
1468
1514
|
secure: "implicit",
|
|
1469
|
-
secureOptions: ftpsSecureOptions(),
|
|
1515
|
+
secureOptions: ftpsSecureOptions(host),
|
|
1470
1516
|
});
|
|
1471
1517
|
await this.waitForTlsSession(client);
|
|
1472
1518
|
const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
|
|
@@ -1491,7 +1537,7 @@ export class BambuImplementation {
|
|
|
1491
1537
|
user: "bblp",
|
|
1492
1538
|
password: token,
|
|
1493
1539
|
secure: "implicit",
|
|
1494
|
-
secureOptions: ftpsSecureOptions(),
|
|
1540
|
+
secureOptions: ftpsSecureOptions(host),
|
|
1495
1541
|
});
|
|
1496
1542
|
// With TLS 1.3 the session ticket arrives asynchronously; basic-ftp calls
|
|
1497
1543
|
// getSession() when opening the data channel and gets undefined if the
|