bambu-printer-mcp 1.1.33 → 1.1.35

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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Bambu's public certificates from Bambu Studio v02.08.02.61 resources/cert,
3
+ * the folder Bambu Studio and FULU's GUI pass to the networking plug-in's
4
+ * set_cert_file. printer.cer holds the BBL CA roots that sign each printer's
5
+ * LAN MQTT certificate; slicer_base64.cer is the cloud certificate. FULU's
6
+ * standalone Linux host runtime ships only slicer_base64.cer, so its plug-in
7
+ * cannot verify a LAN printer without these. Exact bytes, pinned by SHA-256
8
+ * (the same pins as scripts/setup-bambu-native.mjs).
9
+ */
10
+ export const BAMBU_NETWORK_CERTIFICATES = Object.freeze({
11
+ "printer.cer": {
12
+ sha256: "36f2bcee347ec7adce719b5fd350099591a4d3d0ec4e039c7019890d78e152a0",
13
+ base64: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZmekNDQTJlZ0F3SUJBZ0lVWHR6UjZ0UmlML1JIQlJYT295RlUwK1hybGlv" +
14
+ "d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRUxNQWtHQTFVRUJoTUNRMDR4SVRBZkJnTlZCQW9NR0VKQ1RDQlVaV05vYm05c2IyZHBa" +
15
+ "WE1nUTI4dQpJRXgwWkRFVU1CSUdBMVVFQXd3TFFrSk1JRU5CTWlCU1UwRXdJQmNOTWpVd05qRTNNREV6T0RBNFdoZ1BNakExCk1E" +
16
+ "QTJNVGN3TVRNNE1EaGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXYKYkc5bmFXVnpJRU52" +
17
+ "TGlCTWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRwo5dzBCQVFFRkFBT0NBZzhBTUlJQ0Nn" +
18
+ "S0NBZ0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBCjY3OXJQYU9kV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRF" +
19
+ "K1NwR2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG8KYk93ZjUzRkp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlz" +
20
+ "aWRvY1djZ3JETi9TSUR5d2dQQjVDVgpiWWczVnZ6dWE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJi" +
21
+ "bE93TEJBeCtmSG9rCndxaDZ0a0k2S3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFq" +
22
+ "QkYKdUh0a2pyYjVLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbQpxOHQ0SUwv" +
23
+ "MzVoUDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxClkybk5rRU4wK3RqajNoNFdk" +
24
+ "enJXZ1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2UKSzIxeGd4SmR6TWJkenRmVHBpYmlYazBhYmZP" +
25
+ "cE4rMVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MQo1VnF6ajNTVnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6" +
26
+ "a1QwbE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLCnYvWnNYUGdYTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRq" +
27
+ "RGg3YnZyWC8vY3ovVm9wQVUKbHhVdFF0TUNBd0VBQWFOak1HRXdIUVlEVlIwT0JCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0x" +
28
+ "YW4xTUI4RwpBMVVkSXdRWU1CYUFGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3CkRn" +
29
+ "WURWUjBQQVFIL0JBUURBZ0VHTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFCRlpES01KZnAvTjRnQmVGSGgKTWlGZWhhVXlNUzZl" +
30
+ "OW16clRmTUxKTEpvajZKb3BhOVY5aklmY0NFQkdadVJUaHFGY0FUVitVZEZIU0lOcFVjSAp1cGNDWW5helRSQzRkbjFobnhuUTFv" +
31
+ "alFjSHhkR3A5eEd3L1ljbEFLRDk3ZDhiUFNoZkJNVDF0bzl6Yk1LN1Q1Ckw4emdxZzAxWUlPS2pRazBIY2QwKzBpVXI2bTh6UTVQ" +
32
+ "OFJsM1FYcUF5ZVdncW1ZUXJyalRXd1BzZ2RmSE5YS1gKdkRyeDcvY3FyeTVsS1U4MDJoVXBsS01CeGVsdjRXODQwN1l0ajFsZkpP" +
33
+ "d3Z4cXh4c0ZVNWpTd2NVRzN6bzJ2awpRdGpSczhtNUJLdXA1SzFPUFlra1B1N0xkODlYMFhwVTA3My9kTkRHMTF1eGIxZURLcnRO" +
34
+ "UDZ2WnVaak5FMlBxCjhIQ29JMUV0UCtJdHlxdFVNdkhpNloyenNtbEEyNWJyb1Zpb2VVS3hqbEllY3BROUpSL0ZoRHU5Q1dORi9u" +
35
+ "RFcKTFNPUk5hTU16Z3NNU3pJK0hDaVVocU4rcU1JdlZQNnJ6R1RKendxei9sYzVMZitaUENuR0E5V0pUVDR1UEloZgp1ZmJabW5V" +
36
+ "SjM1V3VXS0h4b3ZEc3FCaDg4elE5c1orZWk0SGk0dlZ6T2hVZ2ZHM2FMb1NRRVlxUm9xYWJvQU5oCndDd3p5dVcyUnY1NHU1UVNC" +
37
+ "YmQ2R3gxT3B2c1dtTFBXZDIvaUwya0lTbDV3Zm1MR1Z5ZHZTSmErcmJPZnVBeTcKeWNWUWFjVkRRQ0FuYmhvVnJReTcrNDU0UXNL" +
38
+ "U1czWlY2QmN5UnJvcmV3Q3lDWWdkN255eGZseEhaVEJFeWtYWApoYUdOZS9LRk52SkJNT0l1SVV6a25SUm1pUT09Ci0tLS0tRU5E" +
39
+ "IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUI4ekNDQVptZ0F3SUJBZ0lVZTYxakdRNFJ6" +
40
+ "SUM4aytzTnVxYkkvQ2FOcVBJd0NnWUlLb1pJemowRUF3SXcKUmpFTE1Ba0dBMVVFQmhNQ1EwNHhJVEFmQmdOVkJBb01HRUpDVENC" +
41
+ "VVpXTm9ibTlzYjJkcFpYTWdRMjh1SUV4MApaREVVTUJJR0ExVUVBd3dMUWtKTUlFTkJNaUJGUTBNd0lCY05NalV3TmpFM01ERXpP" +
42
+ "RE0xV2hnUE1qQTFNREEyCk1UY3dNVE00TXpWYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURWUVFLREJoQ1Frd2dWR1ZqYUc1" +
43
+ "dmJHOW4KYVdWeklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZSEtvWkl6ajBDQVFZSQpLb1pJ" +
44
+ "emowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2SmdiaXdZCkFQUEhLN2tWYlZtZXJr" +
45
+ "cWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3SFFZRFZSME9CQllFRkt1UnBzalkKUkVPeUlLSDdId09FNmpoR0JkNk5NQjhH" +
46
+ "QTFVZEl3UVlNQmFBRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2TgpNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFR" +
47
+ "SC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTUNBMGdBCk1FVUNJRXJCaVVtM1ZkdFAzcno0a2I4YUxwSTVwK0J6TDdNOXZFbEJHV1dK" +
48
+ "eHBITUFpRUEzcjV0SldWR3d1eGkKWUNyQjFjNDBLWUZSRnlhaEdyaE9KWkFqL1loUmRuVT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUt" +
49
+ "LS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRWVUQ0NBMkdnQXdJQkFnSVVPcStsTklhQzJ4c3N3a0ZxajVKUHlW" +
50
+ "Qmw0NWN3RFFZSktvWklodmNOQVFFTApCUUF3UWpFTE1Ba0dBMVVFQmhNQ1EwNHhJakFnQmdOVkJBb01HVUpDVENCVVpXTm9ibTlz" +
51
+ "YjJkcFpYTWdRMjh1CkxDQk1kR1F4RHpBTkJnTlZCQU1NQmtKQ1RDQkRRVEFlRncweU5UQTJNVGN3TWpBeE1qZGFGdzB6TlRBMk1U" +
52
+ "VXcKTWpBeE1qZGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXZiRzluYVdWegpJRU52TGlC" +
53
+ "TWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGCkFBT0NBZzhBTUlJQ0NnS0NB" +
54
+ "Z0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBNjc5clBhT2QKV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRFK1Nw" +
55
+ "R2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG9iT3dmNTNGSgp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlzaWRv" +
56
+ "Y1djZ3JETi9TSUR5d2dQQjVDVmJZZzNWdnp1CmE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJibE93" +
57
+ "TEJBeCtmSG9rd3FoNnRrSTYKS3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFqQkZ1" +
58
+ "SHRranJiNQpLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbXE4dDRJTC8zCjVo" +
59
+ "UDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxWTJuTmtFTjAKK3RqajNoNFdkenJX" +
60
+ "Z1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2VLMjF4Z3hKZAp6TWJkenRmVHBpYmlYazBhYmZPcE4r" +
61
+ "MVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MTVWcXpqM1NWCnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6a1Qw" +
62
+ "bE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLdi9ac1hQZ1gKTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRqRGg3" +
63
+ "YnZyWC8vY3ovVm9wQVVseFV0UXRNQwpBd0VBQWFOak1HRXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1D" +
64
+ "QVFZd0hRWURWUjBPCkJCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUI4R0ExVWRJd1FZTUJhQUZJODBRbWpjWjA2UHhD" +
65
+ "S2UKeFh4SjVhdmRSTDRlTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBdlM4dHlmYWdhR3NGZjlZbmNBMmtvL05hNQo5QlZGKzhU" +
66
+ "bFVvKzMyb3pud0lWcFMxQWhTZ0xQNnJOVmVrWE5GS2J1UDVodHVkTFExN1pSQkpJL1VNVnlZRURxCklON3h2N1pqK3pKd0Y2VzZo" +
67
+ "YVlyamIyVms4aWd3MVh2TlVMWmZ2Vk5OS0lrdkpVaVZxRXNsV3JDK2s3NGNyay8KV3Y4Q2hWZit6cXZmSU42TFYzZXNhR1JMMDJK" +
68
+ "M0FwclFHYjdERGhSMUVlZlFNU2NEa05wR0pNVW12Q21ma25ybAppSzhxZ3ZRTjFTV083SlJmNmZOS0hzTjFaUXZ5UDBwZ0xXeHBU" +
69
+ "M1YwLzAvV3R0cVgzY01HdUpGK2pWVXptL05oCnhZaEZld0c4dmMzS3pUam53UUFwTUE2Q1c1NTRGT0pXRnlPRDJqbjV5SkxUM1Z1" +
70
+ "ZSthWURRUnA0YktNeAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDcmpD" +
71
+ "Q0FaYWdBd0lCQWdJVU9xK2xOSWFDMnhzc3drRnFqNUpQeVZCbDQ1Z3dEUVlKS29aSWh2Y05BUUVMCkJRQXdRakVMTUFrR0ExVUVC" +
72
+ "aE1DUTA0eElqQWdCZ05WQkFvTUdVSkNUQ0JVWldOb2JtOXNiMmRwWlhNZ1EyOHUKTENCTWRHUXhEekFOQmdOVkJBTU1Ca0pDVENC" +
73
+ "RFFUQWVGdzB5TlRBMk1UY3dNakF4TkRkYUZ3MHpOVEEyTVRVdwpNakF4TkRkYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURW" +
74
+ "UVFLREJoQ1Frd2dWR1ZqYUc1dmJHOW5hV1Z6CklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZ" +
75
+ "SEtvWkl6ajBDQVFZSUtvWkkKemowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2Smdi" +
76
+ "aXdZQVBQSApLN2tWYlZtZXJrcWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFPCkJn" +
77
+ "TlZIUThCQWY4RUJBTUNBUVl3SFFZRFZSME9CQllFRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2Tk1COEcKQTFVZEl3UVlNQmFB" +
78
+ "Rkk4MFFtamNaMDZQeENLZXhYeEo1YXZkUkw0ZU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQgpBUUNnNlBqVVNTWlYrNGJ2ZWpjVk12" +
79
+ "Z1htS3pmRDk1b3NXbjBjdG5vTUJ4UERhK20rR2crQmNMVDJJbEZBZTNFCktZTXZ1NFQyOTVXUWM5MnJqS1lxVzZjaXJGcHBuZzl1" +
80
+ "RUZXMm1aTGlteGFTbXV0c1RmdEUzc2JNVk1KL1NMWU4KUFY3VEZ2Nm1jQlNJRldYd21CT0lwYmg0QlVjVmZPTlR2ZFNmSXFmeUFW" +
81
+ "eHNxNHh6YzJuYzZoUEJwQW0yMUF5agpUb0MxZXYvVGJESjhWbGxGWmlFVm1XV2xJUDNhTnpBbThTMm1PcHhQQjJXbmFuYVpIU3J2" +
82
+ "WExGaHN0eXp3cmpECnlPMS9pc09aN3d0cjdyY3VUSmRFdnZ2Q2ltT1psa2ZSaGFEb1Rldzl0UTBFMkZWcHp6U2ludzAycW1RMXhJ" +
83
+ "RTkKNS9INVp6SlNQa3BlQUhXRVBuS2t4ZzB2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FU" +
84
+ "RS0tLS0tCk1JSURaVENDQWsyZ0F3SUJBZ0lVVjFGY2t3WEVseWVrMW9uRm5ROWtMN0JrNE44d0RRWUpLb1pJaHZjTkFRRUwKQlFB" +
85
+ "d1FqRUxNQWtHQTFVRUJoTUNRMDR4SWpBZ0JnTlZCQW9NR1VKQ1RDQlVaV05vYm05c2IyZHBaWE1nUTI4dQpMQ0JNZEdReER6QU5C" +
86
+ "Z05WQkFNTUJrSkNUQ0JEUVRBZUZ3MHlNakEwTURRd016UXlNVEZhRncwek1qQTBNREV3Ck16UXlNVEZhTUVJeEN6QUpCZ05WQkFZ" +
87
+ "VEFrTk9NU0l3SUFZRFZRUUtEQmxDUWt3Z1ZHVmphRzV2Ykc5bmFXVnoKSUVOdkxpd2dUSFJrTVE4d0RRWURWUVFEREFaQ1Frd2dR" +
88
+ "MEV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQgpEd0F3Z2dFS0FvSUJBUURMM3BuRGR4R09rNVo2dnVnaVQ0ZHBNMGp1KzNY" +
89
+ "YXR4ejA5VVk3bWJqNHRrSWRieTRICm9lRWRpWVNaamM1TEpuZ0p1Q0h3dEViQkp0MUJyaVJkU1ZyRjZNOUQyVWFCRHlhbUVvMGR4" +
90
+ "d1NhVnhaaURWV0MKZWVDUGRFTHBGWmRFaFNOVGFUNE83emd2Y25Gc2ZITWEvMHZNQWt2RTdpMHFwM21qRXpZTGZ6NjBheGNEb0pM" +
91
+ "awpwN242eEtYSStjSmJBNElsVG9GanBTbGRQbUMreW5PbzdZQU9zWHQ3QVlLWTZHbHowQndVVnpTSnhVKy8rVkZ5Ci9Rcm1ZR053" +
92
+ "bHJRdGRSRUhlUmkwU05LMzJ4MStiT25kZkpQMHNvanVJckRqS3NkQ0x5ZTVDU1pJdnFuYm93d1cKMWpSd1pnVEJSMjlacDJuekNv" +
93
+ "eEpZY1U5VFNRcC80S1p1V05WQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUApORUpvM0dkT2o4UWluc1Y4U2VXcjNVUytIakFm" +
94
+ "QmdOVkhTTUVHREFXZ0JTUE5FSm8zR2RPajhRaW5zVjhTZVdyCjNVUytIakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJ" +
95
+ "YjNEUUVCQ3dVQUE0SUJBUUFCbEJJVDVaZUcKZmdjSzFMT2gxQ045c1R6eE1DTGJ0VFBGRjFOR0dBMTNtQXB1NmoxaDVZRUxiU0tj" +
96
+ "VXFmWHpNblZlQWIwNkh0dQozQ29Db2Urd2o3TE9OVEZPKyt2Qm0yL2lmNkp0L0RVdzFDQUVjTnlxZWg2RVMwTlg4TEpSVlNlMHFk" +
97
+ "VHhQSnVBCkJkT29vOTZpWDg5clJQb3hlZWQxY3BxNWhad2Jla2EzK0NKR1Y3Nml0V3AzNVVwNXJtbVVxcmx5UU9yL1dheDYKaXRv" +
98
+ "c0l6RzBNZmhnVXpVNTFBMlAvaFNuRDNORE1Ydit3VVkvQXZxZ0lMN3U3ZmJES25rdTFHekVLSWtmSDhobQpSczZkOFNDVTg5eHly" +
99
+ "d3pRMFBSODUzaXJIYXMzV3JIVnFhYjNQK3FOd1IwWWlyTDBRazdYdC9xM08xZ3JpTmcyCkJsYmpnM29icEhvOQotLS0tLUVORCBD" +
100
+ "RVJUSUZJQ0FURS0tLS0t",
101
+ },
102
+ "slicer_base64.cer": {
103
+ sha256: "f504dc1c37bc9741b7a95198914e67f3f6f1d917b1e45eaa14b9e84eb7b51ec8",
104
+ base64: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUY3ekNDQk5lZ0F3SUJBZ0lRQm5XSDBPTmY1dE1XNlZyNXhtWXhEREFOQmdr" +
105
+ "cWhraUc5dzBCQVFzRkFEQloKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVObGNuUWdTVzVqTVRNd01RWURW" +
106
+ "UVFERXlwUwpZWEJwWkZOVFRDQlVURk1nUkZZZ1VsTkJJRTFwZUdWa0lGTklRVEkxTmlBeU1ESXdJRU5CTFRFd0hoY05NakV3Ck56" +
107
+ "RXpNREF3TURBd1doY05Nakl3TnpFek1qTTFPVFU1V2pBWk1SY3dGUVlEVlFRRERBNHFMbUpoYldKMWJHRmkKTG1OdmJUQ0NBU0l3" +
108
+ "RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUt4c3pMZGpBS21TMkpscQpaNTI4WkVsM0VxWkd2MWp1UnZ2TTR4" +
109
+ "cCtvcGVNWENzMWpLc2IxV2ZpZUZBL0tqWEJ6NG51ZVpzRkxhYnB5aXBBCjlvUFBkOG9HajhzWjRoOENJYk1iejJHeG1VVktUb1Ra" +
110
+ "OWYzd1lwMHNXRFVQVHhoWEd3L0p6cXk2bWN3cWhvQ08KUWJWck5hUUNxUXVOcElLTWx2UFg1YUsxQTBRL0FzZE4ybXBWdFNHMENw" +
111
+ "cGNSZjVaRTQyelZGUGcxaWVReGdpSAp3NkxXTGlaM0NHcHRueVAxc0JsSUx6VTQ4cXJoMUQwN1ptdUVITDA1cEo3ZFFBc2hzK0Jr" +
112
+ "bFJ6eXpUZ2lVKzFlCitRZVBGYXExRG9zekZlMndMS3Z1UjhPL1JaOGZGS0pzTnlodE80b21jMklTR3ZSb3lrN3FLU3JGUU9jNG1l" +
113
+ "a1kKMDJzSUZHVUNBd0VBQWFPQ0F2RXdnZ0x0TUI4R0ExVWRJd1FZTUJhQUZLU041YjU4ZWVSd0kyMHVLVFN0STFqYwo5VEYvTUIw" +
114
+ "R0ExVWREZ1FXQkJUUDIrTDR0YjJhOGZsMXBPWSt6RmxBNEdOamtqQW5CZ05WSFJFRUlEQWVnZzRxCkxtSmhiV0oxYkdGaUxtTnZi" +
115
+ "WUlNWW1GdFluVnNZV0l1WTI5dE1BNEdBMVVkRHdFQi93UUVBd0lGb0RBZEJnTlYKSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZ" +
116
+ "QkJRVUhBd0l3UGdZRFZSMGdCRGN3TlRBekJnWm5nUXdCQWdFdwpLVEFuQmdnckJnRUZCUWNDQVJZYmFIUjBjRG92TDNkM2R5NWth" +
117
+ "V2RwWTJWeWRDNWpiMjB2UTFCVE1JR0ZCZ2dyCkJnRUZCUWNCQVFSNU1IY3dKQVlJS3dZQkJRVUhNQUdHR0doMGRIQTZMeTl2WTNO" +
118
+ "d0xtUnBaMmxqWlhKMExtTnYKYlRCUEJnZ3JCZ0VGQlFjd0FvWkRhSFIwY0RvdkwyTmhZMlZ5ZEhNdVpHbG5hV05sY25RdVkyOXRM" +
119
+ "MUpoY0dsawpVMU5NVkV4VFJGWlNVMEZOYVhobFpGTklRVEkxTmpJd01qQkRRUzB4TG1OeWREQUpCZ05WSFJNRUFqQUFNSUlCCmZn" +
120
+ "WUtLd1lCQkFIV2VRSUVBZ1NDQVc0RWdnRnFBV2dBZGdBcGViN3duams1SWZCV2M1OWpwWGZsdmxkOW5HQUsKK1BsTlhTWmNKVjNI" +
121
+ "aEFBQUFYcWVwZEJsQUFBRUF3QkhNRVVDSUcrQVVPdnNqV1VIMDdBK0FaTkNyVVJackxYLwo2MldSVXcrM2FTWlhxSG8vQWlFQW9r" +
122
+ "c1dpMm13bTZrNEZDZW45VThNeTNJbmVEU1lxbnY3K2JoODhLOExrdHdBCmRnQlJvN0QxL1FGNW5GWnR1RGQ0and5a2Vzd2JKOHYz" +
123
+ "bm9oQ21nMysxSXNGNVFBQUFYcWVwZERWQUFBRUF3QkgKTUVVQ0lRRDZLYk5YRWZaT1pnN1MvdURIRlRWY00xczByemw0ZWtQNFhW" +
124
+ "cHlRSHQ5MkFJZ0pVbndQMzlDY1NjTwppZ1dZSm1yeGpCQTMxR25XajE4THVLeURWMWpYSmNRQWRnQkJ5TXF4M3lKR1NoREdvVG9K" +
125
+ "UW9kZVRqR0xHd1ByCjYwdkhhUENRWXBZRzlnQUFBWHFlcGRCRUFBQUVBd0JITUVVQ0lRQ3Y3dWZlY21CVmhwbVF1aUpFV1R6dFkw" +
126
+ "ck4KbnBnWnZnQlVNL3ppVlo0d1VBSWdRQXVHbUM5KzZzbkQwUklud3R0Ly9WdmRZcERYT2RpVVN3ZG4zdXc0NXNjdwpEUVlKS29a" +
127
+ "SWh2Y05BUUVMQlFBRGdnRUJBTEJSMFhuSFNBRm0zODN3TDM0VGcvZnFjdWtwWUFFTGZ4MG9zczVpCnZGUWhERUZndVovUXhjaDcw" +
128
+ "UmZBTnYyK0hETFVKNUJIWmZFRnpPQVZFc2xlalBEL2xPZFZKZVozWGE3azhXZXAKZDJudi9hVmR1UmlUOUc4Q0JFa3MrVWxXKzlW" +
129
+ "NmwvWVB5YThJMWh4bWRLWm5YWlhrSm1kOUJia1A4SndieHNzcApxUEYzRzlkc1Y1RTBab0RTWkZuOXgzSkpIM1QzNUZkSXJ4b0sv" +
130
+ "K3MvK3ZvbzdPbkRPWkdqekZrRjUrWW00ajZ1Cks2dW9vMVRXSmFDOW1NQ2FiNElneXJ5alNiVlNvNDlROUdsdXRvaGR2aEZrNEhD" +
131
+ "SWRmejJ3M0JYM29XdFJ3LzIKdUtLTHlEamRYeWYzVFlqWWFTYmJhblNmRDEycGQwWUsyejAvK0daUHZoMUtiN1E9Ci0tLS0tRU5E" +
132
+ "IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURyekNDQXBlZ0F3SUJBZ0lRQ0R2Z1ZwQkNS" +
133
+ "ckdoZFdySldaSEhTakFOQmdrcWhraUc5dzBCQVFVRkFEQmgKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVO" +
134
+ "bGNuUWdTVzVqTVJrd0Z3WURWUVFMRXhCMwpkM2N1WkdsbmFXTmxjblF1WTI5dE1TQXdIZ1lEVlFRREV4ZEVhV2RwUTJWeWRDQkhi" +
135
+ "RzlpWVd3Z1VtOXZkQ0JEClFUQWVGdzB3TmpFeE1UQXdNREF3TURCYUZ3MHpNVEV4TVRBd01EQXdNREJhTUdFeEN6QUpCZ05WQkFZ" +
136
+ "VEFsVlQKTVJVd0V3WURWUVFLRXd4RWFXZHBRMlZ5ZENCSmJtTXhHVEFYQmdOVkJBc1RFSGQzZHk1a2FXZHBZMlZ5ZEM1agpiMjB4" +
137
+ "SURBZUJnTlZCQU1URjBScFoybERaWEowSUVkc2IySmhiQ0JTYjI5MElFTkJNSUlCSWpBTkJna3Foa2lHCjl3MEJBUUVGQUFPQ0FR" +
138
+ "OEFNSUlCQ2dLQ0FRRUE0anZoRVhMZXFLVFRvMWVxVUtLUEMzZVF5YUtsN2hMT2xsc0IKQ1NETUFaT25UakMzVS9kRHhHa0FWNTNp" +
139
+ "alNMZGh3WkFBSUVKenM0Ymc3L2Z6VHR4UnVMV1pzY0ZzM1luRm85NwpuaDZWZmU2M1NLTUkydGF2ZWd3NUJtVi9TbDBmdkJmNHE3" +
140
+ "N3VLTmQwZjNwNG1WbUZhRzVjSXpKTHYwN0E2RnB0CjQzQy9keEMvL0FIMmhkbW9SQkJZTXFsMUdOWFJvcjVINGlkcTlKb3orRWtJ" +
141
+ "WUl2VVg3UTZoTCtocWtwTWZUN1AKVDE5c2RsNmdTemVSbnR3aTVtM09GQnFPYXN2K3piTVVaQmZIV3ltZU1yL3k3dnJUQzBMVXE3" +
142
+ "ZEJNdG9NMU8vNApnZFc3alZnL3RSdm9TU2lpY05veEJOMzNzaGJ5VEFwT0I2anRTajFldFgramtNT3ZKd0lEQVFBQm8yTXdZVEFP" +
143
+ "CkJnTlZIUThCQWY4RUJBTUNBWVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVQTk1UU5WYlIKVEx0bThLUGlH" +
144
+ "eHZEbDdJOTBWVXdId1lEVlIwakJCZ3dGb0FVQTk1UU5WYlJUTHRtOEtQaUd4dkRsN0k5MFZVdwpEUVlKS29aSWh2Y05BUUVGQlFB" +
145
+ "RGdnRUJBTXVjTjZwSUV4SUsrdDFFbkU5U3NQVGZyZ1QxZVhrSW95UVkvRXNyCmhNQXR1ZFhIL3ZUQkgxakx1RzJjZW5Ubm1DbXJF" +
146
+ "YlhqY0tDaHpVeUltWk9Na1hEaXF3OGN2cE9wLzJQVjVBZGcKMDZPL25Wc0o4ZFdPNDFQMGptUDZQNmZidEdiZlltYlcwVzVCamZJ" +
147
+ "dHRlcDNTcCtkV09JcldjQkFJKzB0S0lKRgpQbmxVa2lhWTRJQklxRGZ2OE5aNVlCYmVyT2dPelc2c1JCYzRMMG5hNFVVK0tyazJV" +
148
+ "ODg2VUFiM0x1akVWMGxzCllTRVkxUVN0ZUR3c09vQnJwK3V2RlJUcDJJbkJ1VGhzNHBGc2l2OWt1WGNsVnpEQUd5U2o0ZHpwMzBk" +
149
+ "OHRiUWsKQ0FVdzdDMjlDNzlGdjFDNXFmUHJtQUVTcmNpSXhwZzBYNDBLUE1icDFaV1ZiZDQ9Ci0tLS0tRU5EIENFUlRJRklDQVRF" +
150
+ "LS0tLS0K",
151
+ },
152
+ });
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import path from "path";
10
10
  import { createServer as createHttpServer } from "node:http";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
13
- import { BambuNetworkBridge } from "./bambu-network-bridge.js";
13
+ import { BambuNetworkBridge, describeBambuNetworkPrintError } from "./bambu-network-bridge.js";
14
14
  import { assertBambuNativeAvailable, bambuNativeControlsAvailable, bambuNativeUnavailableReason, interruptNativeOperations, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
15
15
  import { importFileViaBambuConnect } from "./bambu-connect.js";
16
16
  import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
@@ -1229,12 +1229,17 @@ class BambuPrinterMCPServer {
1229
1229
  this.bambu.assertBedClearance(confirmedStatus, bedClearance);
1230
1230
  const amsMapping = stringifyBridgeJson(mapping?.map(value => bridgeAmsMappingValue(value, isDualNozzleModel(printModel))));
1231
1231
  const remoteFileName = uniquePrintName(threeMFPath);
1232
+ const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
1232
1233
  const params = {
1233
1234
  dev_id: devId,
1234
1235
  // Firmware can report the task name instead of the uploaded filename.
1235
1236
  // Bind either report to this inspected artifact for a later safe resume.
1236
- task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
1237
- project_name: projectName,
1237
+ task_name: checkedName,
1238
+ // The plug-in names the uploaded file and the printer's job from
1239
+ // project_name, so it carries the same unique checked name, as the
1240
+ // native route already passes; a reused name would overwrite a stored
1241
+ // job and break the resume binding.
1242
+ project_name: checkedName,
1238
1243
  preset_name: presetName,
1239
1244
  filename: snapshot,
1240
1245
  config_filename: snapshot,
@@ -1309,7 +1314,7 @@ class BambuPrinterMCPServer {
1309
1314
  this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
1310
1315
  return { ...nativeResult, autoSliced, projectName, plateIndex, useAMS, amsMapping: mapping };
1311
1316
  }
1312
- const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, {
1317
+ const bridgeOptions = {
1313
1318
  ...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
1314
1319
  beforeDispatch: async (method) => {
1315
1320
  if (method !== bridgeMethod)
@@ -1323,7 +1328,15 @@ class BambuPrinterMCPServer {
1323
1328
  assertActive(); },
1324
1329
  onDispatched: method => { if (method === bridgeMethod)
1325
1330
  retainForDispatchedUpload(); },
1326
- });
1331
+ };
1332
+ let jobUpdates = [];
1333
+ let bridgeResult;
1334
+ if (isLocalBridgePrint) {
1335
+ ({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint({ devId, devIp, username: String(params.username), password, useSsl: params.use_ssl_for_mqtt }, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive));
1336
+ }
1337
+ else {
1338
+ bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions);
1339
+ }
1327
1340
  if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
1328
1341
  throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
1329
1342
  }
@@ -1332,7 +1345,10 @@ class BambuPrinterMCPServer {
1332
1345
  typeof bridgeResult.value === "number" &&
1333
1346
  bridgeResult.value !== 0) {
1334
1347
  const value = bridgeResult.value;
1335
- throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
1348
+ const reported = jobUpdates.filter(update => update.code !== 0 || update.msg).slice(-3)
1349
+ .map(update => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
1350
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
1351
+ (reported ? ` Plug-in reported: ${reported}.` : ""));
1336
1352
  }
1337
1353
  assertActive();
1338
1354
  this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
@@ -1346,6 +1362,7 @@ class BambuPrinterMCPServer {
1346
1362
  projectName,
1347
1363
  plateIndex,
1348
1364
  bridgePlateIndex: plateIndex + 1,
1365
+ printerJobName: checkedName,
1349
1366
  useAMS,
1350
1367
  amsMapping: mapping,
1351
1368
  params: redactPrintParams(params),
@@ -1972,11 +1989,11 @@ class BambuPrinterMCPServer {
1972
1989
  },
1973
1990
  use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile. Template 3MF process settings can still be used at the same time." },
1974
1991
  uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
1975
- repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
1992
+ repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies. OrcaSlicer copies an STL with --clone-objects; do not combine with clone_objects." },
1976
1993
  orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
1977
- arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
1994
+ arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing; STL input is arranged by default. Set false only to keep a 3MF's existing layout: an STL that is not already on the plate then fails to slice." },
1978
1995
  ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
1979
- clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
1996
+ clone_objects: { type: "string", description: "Clone the loaded model: total copies as a single integer, e.g. '3'. The Bambu-style CLIs count copies per input file and this tool loads one file, so a comma-separated list is rejected by the slicer." },
1980
1997
  skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
1981
1998
  load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
1982
1999
  filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
@@ -2078,7 +2095,7 @@ class BambuPrinterMCPServer {
2078
2095
  nozzle_type: { type: "string", enum: [...VALID_NOZZLE_TYPES], description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)." },
2079
2096
  nozzle_volume_type: { type: "string", enum: [...VALID_NOZZLE_VOLUME_TYPES], description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)." },
2080
2097
  plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
2081
- project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
2098
+ project_name: { type: "string", description: "Optional project name returned in the result; defaults to the 3MF filename without extension. The printer job and stored file use the server's unique checked name so resume_print can bind to the inspected file." },
2082
2099
  preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
2083
2100
  task_name: { type: "string", description: "Optional project label when project_name is omitted. The submitted task name uses a unique inspected-job identity for safe resume." },
2084
2101
  config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
@@ -2122,7 +2139,7 @@ class BambuPrinterMCPServer {
2122
2139
  type: "object",
2123
2140
  properties: {
2124
2141
  save_path: { type: "string", description: "Optional absolute path to write the JPEG to disk. If omitted, only the base64 payload is returned." },
2125
- timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
2142
+ timeout_ms: { type: "number", description: "Max ms to wait for a live frame (default 8000). A1/P1 cameras first replay a buffered frame, which is skipped, so a live frame takes about 3 s, longer on cold start." },
2126
2143
  bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
2127
2144
  experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
2128
2145
  ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series, X2D)." },
@@ -33,6 +33,11 @@ export type PrintPreflight = {
33
33
  model: string;
34
34
  notices: string[];
35
35
  };
36
+ /**
37
+ * Split the A1/P1 camera stream on port 6000 into JPEG frames. Each frame is
38
+ * a 16-byte header (payload size LE, itrack, flags, reserved) and the JPEG.
39
+ */
40
+ export declare function cameraFrameParser(onFrame: (jpeg: Buffer) => void, onError: (error: Error) => void): (chunk: Buffer) => void;
36
41
  export declare class BambuImplementation {
37
42
  private readonly confirm?;
38
43
  private printerStore;
@@ -108,10 +113,6 @@ export declare class BambuImplementation {
108
113
  files: string[];
109
114
  directories: Record<string, string[]>;
110
115
  }>;
111
- getFile(host: string, serial: string, token: string, filename: string): Promise<{
112
- name: string;
113
- exists: boolean;
114
- }>;
115
116
  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<{
116
117
  status: string;
117
118
  uploaded: boolean;
@@ -218,8 +219,13 @@ export declare class BambuImplementation {
218
219
  */
219
220
  private fetchRtspCameraFrame;
220
221
  /**
221
- * Open the TLS-on-6000 socket, send the 80-byte auth packet, and read
222
- * a single complete JPEG frame. Returns the JPEG bytes.
222
+ * Open the TLS-on-6000 socket, send the 80-byte auth packet, and return
223
+ * a live JPEG frame. The printer first replays the last frame it buffered,
224
+ * which after an idle period can show the chamber as it was long ago (a
225
+ * finished part still on the bed), so that frame and any byte-identical
226
+ * repeat of it are discarded and the next frame returned; live frames always
227
+ * differ by sensor noise. If no live frame arrives in time, this fails rather
228
+ * than return a possibly stale image.
223
229
  */
224
230
  private fetchTcpCameraFrame;
225
231
  /**
@@ -9,10 +9,9 @@ import { readSafe3mfArchive } from "../safety/archive.js";
9
9
  import { inspectPrintFile } from "../safety/print-file.js";
10
10
  import { isDualNozzleModel, normalizeModel, validateTemperature } from "../safety/limits.js";
11
11
  import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, describeAmsTrayRange } from "../ams-mapping.js";
12
- import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
12
+ import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, unchippedNozzles, validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
13
13
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
14
14
  import { Client as FTPClient } from "basic-ftp";
15
- import { BambuPrinter } from "bambu-js";
16
15
  import * as mqtt from "mqtt";
17
16
  import { BambuClient, GCodeFileCommand, GCodeLineCommand, PushAllCommand, UpdateFanCommand, UpdateLightCommand, UpdateStateCommand, } from "bambu-node";
18
17
  /**
@@ -277,6 +276,7 @@ class BambuClientStore {
277
276
  }
278
277
  async waitForInitialReport(host, serial, token, timeoutMs = 4000) {
279
278
  const key = getPrinterKey(host, serial, token);
279
+ const deadline = Date.now() + timeoutMs + 500;
280
280
  const existing = this.reportSnapshots.get(key);
281
281
  if (!existing || Object.keys(existing).length === 0) {
282
282
  // No MQTT data yet — wait for the first push.
@@ -298,8 +298,12 @@ class BambuClientStore {
298
298
  // (gcode_state, ams, hms, temperatures, fans, etc.) arrives afterward
299
299
  // and gets merged into reportSnapshots via incremental update.
300
300
  // This settle runs regardless of whether data arrived before or during
301
- // the promise race above, so the merge has time to complete.
301
+ // the promise race above, so the merge has time to complete. A busy host
302
+ // can take longer, which returned the sparse report as status UNKNOWN,
303
+ // so keep waiting for the full status within the same bound.
302
304
  await sleep(500);
305
+ while (typeof this.reportSnapshots.get(key)?.gcode_state !== "string" && Date.now() < deadline)
306
+ await sleep(50);
303
307
  return this.reportSnapshots.get(key) || null;
304
308
  }
305
309
  async getPrinter(host, serial, token) {
@@ -382,6 +386,37 @@ class BambuClientStore {
382
386
  this.initialReportResolvers.clear();
383
387
  }
384
388
  }
389
+ /**
390
+ * Split the A1/P1 camera stream on port 6000 into JPEG frames. Each frame is
391
+ * a 16-byte header (payload size LE, itrack, flags, reserved) and the JPEG.
392
+ */
393
+ export function cameraFrameParser(onFrame, onError) {
394
+ let buffered = Buffer.alloc(0);
395
+ let failed = false;
396
+ return chunk => {
397
+ if (failed)
398
+ return;
399
+ buffered = Buffer.concat([buffered, chunk]);
400
+ while (buffered.length >= 16) {
401
+ const payloadSize = buffered.readUInt32LE(0);
402
+ if (payloadSize <= 0 || payloadSize > 5000000) {
403
+ failed = true;
404
+ onError(new Error(`camera_snapshot: unreasonable payload size ${payloadSize} from header; auth likely failed.`));
405
+ return;
406
+ }
407
+ if (buffered.length < 16 + payloadSize)
408
+ return;
409
+ const jpeg = buffered.subarray(16, 16 + payloadSize);
410
+ buffered = buffered.subarray(16 + payloadSize);
411
+ if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
412
+ failed = true;
413
+ onError(new Error(`camera_snapshot: payload does not start with JPEG SOI (FF D8); got ${jpeg[0].toString(16)} ${jpeg[1].toString(16)}.`));
414
+ return;
415
+ }
416
+ onFrame(Buffer.from(jpeg));
417
+ }
418
+ };
419
+ }
385
420
  export class BambuImplementation {
386
421
  constructor(confirm) {
387
422
  this.confirm = confirm;
@@ -392,8 +427,14 @@ export class BambuImplementation {
392
427
  async confirmHardwareAction(message, physicalCheck = false) {
393
428
  if (!physicalCheck && process.env.BAMBU_REQUIRE_CONFIRMATION === "0")
394
429
  return;
395
- if (!this.confirm || !(await this.confirm(message))) {
396
- throw new Error("Hardware safety confirmation was declined or unavailable. Use an MCP client with elicitation support. No command was sent.");
430
+ if (!this.confirm) {
431
+ throw new Error("Hardware safety confirmation is unavailable. Use an MCP client with elicitation support. No command was sent.");
432
+ }
433
+ if (!(await this.confirm(message))) {
434
+ throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
435
+ ? "This confirmation covers a physical check at the printer and always needs a person to accept it."
436
+ : "Some clients decline prompts automatically, for example in a non-interactive permission mode. " +
437
+ "A deliberately headless setup that approves jobs another way can set BAMBU_REQUIRE_CONFIRMATION=0 to skip ordinary print and heating prompts."));
397
438
  }
398
439
  }
399
440
  finishedJobIdentity(status) {
@@ -408,13 +449,16 @@ export class BambuImplementation {
408
449
  const finishedJob = this.finishedJobIdentity(status);
409
450
  const cleared = this.clearedErrors.get(serial);
410
451
  const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
452
+ const unchipped = unchippedNozzles(status?.raw);
411
453
  await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
412
454
  `Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
413
455
  `File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
414
456
  (finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
415
457
  (cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
416
458
  (notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
417
- `${notices.length === 1 ? "it does" : "they do"} not block printing.` : ""), finishedJob !== undefined || !!cleared);
459
+ `${notices.length === 1 ? "it does" : "they do"} not block printing.` : "") +
460
+ (unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
461
+ `so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : ""), finishedJob !== undefined || !!cleared);
418
462
  return { finishedJob, serial, model: inspection.model, notices };
419
463
  }
420
464
  /** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
@@ -1211,21 +1255,6 @@ export class BambuImplementation {
1211
1255
  directories: filesByDirectory,
1212
1256
  };
1213
1257
  }
1214
- async getFile(host, serial, token, filename) {
1215
- const printer = new BambuPrinter(host, serial, token);
1216
- const normalized = filename.replace(/^\/+/, "");
1217
- const directory = path.posix.dirname(normalized) === "." ? "cache" : path.posix.dirname(normalized);
1218
- const baseName = path.posix.basename(normalized);
1219
- let exists = false;
1220
- await printer.manipulateFiles(async (context) => {
1221
- const entries = await context.readDir(directory);
1222
- exists = entries.includes(baseName);
1223
- });
1224
- return {
1225
- name: `${directory}/${baseName}`,
1226
- exists,
1227
- };
1228
- }
1229
1258
  async uploadFile(host, serial, token, filePath, filename, print, bambuModel, upload) {
1230
1259
  const remotePath = normalizedRemotePath(filename);
1231
1260
  if (!print) {
@@ -1609,10 +1638,15 @@ export class BambuImplementation {
1609
1638
  });
1610
1639
  }
1611
1640
  /**
1612
- * Open the TLS-on-6000 socket, send the 80-byte auth packet, and read
1613
- * a single complete JPEG frame. Returns the JPEG bytes.
1641
+ * Open the TLS-on-6000 socket, send the 80-byte auth packet, and return
1642
+ * a live JPEG frame. The printer first replays the last frame it buffered,
1643
+ * which after an idle period can show the chamber as it was long ago (a
1644
+ * finished part still on the bed), so that frame and any byte-identical
1645
+ * repeat of it are discarded and the next frame returned; live frames always
1646
+ * differ by sensor noise. If no live frame arrives in time, this fails rather
1647
+ * than return a possibly stale image.
1614
1648
  */
1615
- async fetchTcpCameraFrame(host, token, timeoutMs) {
1649
+ async fetchTcpCameraFrame(host, token, timeoutMs, port = 6000) {
1616
1650
  const tls = await import("node:tls");
1617
1651
  const auth = Buffer.alloc(80, 0);
1618
1652
  auth.writeUInt32LE(0x40, 0); // payload size
@@ -1621,11 +1655,9 @@ export class BambuImplementation {
1621
1655
  auth.write("bblp", 16, 4, "ascii");
1622
1656
  auth.write(token, 48, Math.min(32, Buffer.byteLength(token, "ascii")), "ascii");
1623
1657
  return new Promise((resolve, reject) => {
1624
- const chunks = [];
1625
- let totalLen = 0;
1626
- const FRAME_HEADER_BYTES = 16;
1627
- let payloadSize = null;
1628
1658
  let settled = false;
1659
+ let frames = 0;
1660
+ let buffered;
1629
1661
  const finish = (err, jpeg) => {
1630
1662
  if (settled)
1631
1663
  return;
@@ -1639,44 +1671,29 @@ export class BambuImplementation {
1639
1671
  else
1640
1672
  reject(new Error("camera_snapshot: ended without jpeg payload"));
1641
1673
  };
1642
- const timer = setTimeout(() => finish(new Error(`camera_snapshot: timed out after ${timeoutMs}ms`)), timeoutMs);
1674
+ const unfinished = (reason) => new Error(frames
1675
+ ? `camera_snapshot: ${reason} after only the printer's buffered frame, which may be stale; no live frame arrived. Retry, or raise timeout_ms.`
1676
+ : `camera_snapshot: ${reason} before a full JPEG frame arrived`);
1677
+ const timer = setTimeout(() => finish(unfinished(`timed out after ${timeoutMs}ms`)), timeoutMs);
1678
+ const parse = cameraFrameParser(jpeg => {
1679
+ frames++;
1680
+ if (!buffered)
1681
+ buffered = jpeg;
1682
+ else if (!jpeg.equals(buffered))
1683
+ finish(null, jpeg);
1684
+ }, error => finish(error));
1643
1685
  const socket = tls.connect({
1644
1686
  host,
1645
- port: 6000,
1687
+ port,
1646
1688
  rejectUnauthorized: false,
1647
1689
  // The printer uses TLS for confidentiality but presents a self-signed cert.
1648
1690
  // Same trust posture as the FTPS path (basic-ftp with rejectUnauthorized: false).
1649
1691
  }, () => {
1650
1692
  socket.write(auth);
1651
1693
  });
1652
- socket.on("data", (data) => {
1653
- chunks.push(data);
1654
- totalLen += data.length;
1655
- if (payloadSize === null && totalLen >= FRAME_HEADER_BYTES) {
1656
- const merged = Buffer.concat(chunks, totalLen);
1657
- payloadSize = merged.readUInt32LE(0);
1658
- if (payloadSize <= 0 || payloadSize > 5000000) {
1659
- finish(new Error(`camera_snapshot: unreasonable payload size ${payloadSize} from header; auth likely failed.`));
1660
- return;
1661
- }
1662
- // Reset chunk list to remaining bytes after the header.
1663
- const remainder = merged.subarray(FRAME_HEADER_BYTES);
1664
- chunks.length = 0;
1665
- chunks.push(remainder);
1666
- totalLen = remainder.length;
1667
- }
1668
- if (payloadSize !== null && totalLen >= payloadSize) {
1669
- const merged = Buffer.concat(chunks, totalLen);
1670
- const jpeg = merged.subarray(0, payloadSize);
1671
- if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
1672
- finish(new Error(`camera_snapshot: payload does not start with JPEG SOI (FF D8); got ${jpeg[0].toString(16)} ${jpeg[1].toString(16)}.`));
1673
- return;
1674
- }
1675
- finish(null, jpeg);
1676
- }
1677
- });
1694
+ socket.on("data", parse);
1678
1695
  socket.on("error", (err) => finish(err));
1679
- socket.on("end", () => finish(new Error("camera_snapshot: connection ended before a full JPEG frame arrived")));
1696
+ socket.on("end", () => finish(unfinished("connection ended")));
1680
1697
  });
1681
1698
  }
1682
1699
  /**
@@ -39,6 +39,8 @@ export interface ValidatedPrinterState {
39
39
  reportedNozzles: ReportedNozzle[];
40
40
  filaments: ReportedFilament[];
41
41
  }
42
+ /** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
43
+ export declare function unchippedNozzles(raw: unknown): number[];
42
44
  /** The one key format for a reported entry, shared by the gate and clear_hms_errors. */
43
45
  export declare function reportedHmsKey(entry: unknown): string;
44
46
  /** Bambu's display form of a cleared-code key such as "hms:83887616:131184"; other keys unchanged. */
@@ -128,6 +128,41 @@ function nozzleTypeAndFlow(value) {
128
128
  const type = token(value);
129
129
  return { type: ["stainlesssteel", "hardenedsteel", "tungstencarbide", "brass"].includes(type) ? type : undefined };
130
130
  }
131
+ /**
132
+ * A nozzle without an identity chip reports serial "N/A" and keeps the
133
+ * unreliable bit set permanently (stock H2D HS01 nozzles), so its diameter
134
+ * and type are configured settings, as on single-nozzle printers.
135
+ */
136
+ function hasNoIdentityChip(entry) {
137
+ return typeof entry?.sn === "string" && entry.sn.trim().toUpperCase() === "N/A";
138
+ }
139
+ /** Bit 0 (information unreliable) is accepted only from a nozzle without an identity chip. */
140
+ function unreliableWithoutChip(entry, stat) {
141
+ return (stat & 1) !== 0 && hasNoIdentityChip(entry);
142
+ }
143
+ /** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
144
+ export function unchippedNozzles(raw) {
145
+ const info = raw?.device?.nozzle?.info;
146
+ if (!Array.isArray(info))
147
+ return [];
148
+ return info.flatMap(entry => {
149
+ const index = finiteNumber(entry?.id);
150
+ const stat = finiteNumber(entry?.stat);
151
+ return index !== undefined && Number.isInteger(index) && index >= 0 && index < 16 &&
152
+ stat !== undefined && Number.isInteger(stat) && unreliableWithoutChip(entry, stat) ? [index] : [];
153
+ });
154
+ }
155
+ const loggedUnchippedNozzles = new Set();
156
+ /** Name each unchipped nozzle a check accepted once per printer on stderr, so headless runs still record it. */
157
+ function logUnchippedNozzles(raw, serial) {
158
+ for (const index of unchippedNozzles(raw)) {
159
+ const key = `${serial}:${index}`;
160
+ if (loggedUnchippedNozzles.has(key))
161
+ continue;
162
+ loggedUnchippedNozzles.add(key);
163
+ console.error(`Printer ${serial || "(unknown serial)"} nozzle ${index} reports no identity chip; its diameter and type are the printer's configured settings.`);
164
+ }
165
+ }
131
166
  function reportedNozzles(raw) {
132
167
  const info = raw.device?.nozzle?.info;
133
168
  const output = [];
@@ -141,9 +176,13 @@ function reportedNozzles(raw) {
141
176
  if (index === undefined || !Number.isInteger(index) || index < 0 || diameter === undefined || diameter <= 0)
142
177
  throw new Error("Printer reported malformed nozzle identity or diameter.");
143
178
  if (entry.stat !== undefined) {
179
+ // BambuStudio DevNozzle: bit 0 set means the information is unreliable;
180
+ // bits 1-2 are 0 normal, 1 abnormal, 2 unknown.
144
181
  const stat = finiteNumber(entry.stat);
145
- if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 7) !== 0)
182
+ if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 6) !== 0)
146
183
  throw new Error(`Printer nozzle ${index} information is unreliable, abnormal or unknown.`);
184
+ if ((stat & 1) !== 0 && !unreliableWithoutChip(entry, stat))
185
+ throw new Error(`Printer nozzle ${index} information is unreliable; refresh the nozzle information on the printer.`);
147
186
  }
148
187
  if (output.some(nozzle => nozzle.index === index))
149
188
  throw new Error("Printer reported ambiguous duplicate nozzle indices.");
@@ -420,6 +459,8 @@ export function validatePrinterState(status, requirements) {
420
459
  filaments.push({ position, material, trayIndex, nozzleMin, nozzleMax });
421
460
  }
422
461
  }
462
+ if (nozzles.length)
463
+ logUnchippedNozzles(raw, identity.observedSerial ?? String(status.serial ?? ""));
423
464
  return { model, reportedNozzles: nozzles, filaments };
424
465
  }
425
466
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
@@ -114,6 +114,14 @@ export declare class STLManipulator extends EventEmitter {
114
114
  * Scale an STL model uniformly or along specific axes
115
115
  */
116
116
  scaleSTL(stlFilePath: string, scaleFactors: number | [number, number, number], progressCallback?: ProgressCallback): Promise<string>;
117
+ /**
118
+ * Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
119
+ * CLI would, about the model's centre. Orca adds each --rotate* to the
120
+ * object's Euler angles (ModelVolume::rotate), so the result rotates about X,
121
+ * then Y, then Z whatever the argument order, after scaling. The slicer then
122
+ * places the result on the plate.
123
+ */
124
+ private transformMeshForCli;
117
125
  /**
118
126
  * Rotate an STL model around specific axes
119
127
  */