mcp-3d-printer-server 1.2.25 → 1.2.26
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/CHANGELOG.md +55 -0
- package/CONTRIBUTORS.md +5 -1
- package/README.md +64 -29
- package/dist/ams-mapping.d.ts +38 -0
- package/dist/ams-mapping.js +125 -0
- package/dist/bambu-network-bridge.d.ts +200 -0
- package/dist/bambu-network-bridge.js +881 -0
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/control-reply.d.ts +21 -0
- package/dist/control-reply.js +44 -0
- package/dist/counterpart/camera-sources.d.ts +16 -0
- package/dist/counterpart/camera-sources.js +138 -0
- package/dist/counterpart/camera-stream.d.ts +66 -0
- package/dist/counterpart/camera-stream.js +246 -0
- package/dist/counterpart/camera-tools.d.ts +247 -0
- package/dist/counterpart/camera-tools.js +35 -0
- package/dist/counterpart/contract.d.ts +21 -5
- package/dist/counterpart/contract.js +8 -2
- package/dist/counterpart/route.d.ts +31 -0
- package/dist/counterpart/route.js +70 -0
- package/dist/counterpart/service.d.ts +60 -4
- package/dist/counterpart/service.js +142 -16
- package/dist/design-guidance.d.ts +26 -0
- package/dist/design-guidance.js +177 -0
- package/dist/fulu-orca.d.ts +7 -0
- package/dist/fulu-orca.js +18 -2
- package/dist/index.js +512 -62
- package/dist/printers/bambu-filaments.d.ts +68 -0
- package/dist/printers/bambu-filaments.js +262 -0
- package/dist/printers/bambu.d.ts +99 -6
- package/dist/printers/bambu.js +478 -72
- package/dist/printers/creality.d.ts +2 -1
- package/dist/printers/creality.js +5 -1
- package/dist/printers/generic-base.d.ts +11 -6
- package/dist/printers/generic-base.js +21 -25
- package/dist/printers/klipper.d.ts +2 -1
- package/dist/printers/klipper.js +10 -7
- package/dist/printers/octoprint.d.ts +2 -1
- package/dist/printers/octoprint.js +15 -8
- package/dist/printers/printer-factory.d.ts +1 -1
- package/dist/printers/printer-factory.js +3 -3
- package/dist/printers/prusa.js +0 -1
- package/dist/safety/confirmation.d.ts +11 -16
- package/dist/safety/confirmation.js +8 -32
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +8 -2
- package/dist/safety/limits.js +14 -5
- package/dist/safety/print-file.d.ts +5 -0
- package/dist/safety/print-file.js +156 -19
- package/dist/safety/printer-state.d.ts +59 -8
- package/dist/safety/printer-state.js +174 -39
- package/dist/slicer/bambu-cli.d.ts +2 -0
- package/dist/slicer/bambu-cli.js +7 -5
- package/dist/slicer/layer-pauses.d.ts +47 -0
- package/dist/slicer/layer-pauses.js +130 -0
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +40 -0
- package/dist/studio-print-options.d.ts +57 -0
- package/dist/studio-print-options.js +179 -0
- package/package.json +25 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1075 -0
- package/src/bambu-network-certs.ts +154 -0
- package/src/control-reply.ts +41 -0
- package/src/counterpart/camera-sources.ts +102 -0
- package/src/counterpart/camera-stream.ts +195 -0
- package/src/counterpart/camera-tools.ts +35 -0
- package/src/counterpart/contract.ts +12 -4
- package/src/counterpart/route.ts +82 -0
- package/src/counterpart/service.ts +150 -16
- package/src/design-guidance.ts +188 -0
- package/src/fulu-orca.ts +18 -2
- package/src/index.ts +526 -75
- package/src/printers/bambu-filaments.ts +346 -0
- package/src/printers/bambu.ts +506 -81
- package/src/printers/creality.ts +8 -3
- package/src/printers/generic-base.ts +33 -40
- package/src/printers/klipper.ts +12 -8
- package/src/printers/octoprint.ts +16 -9
- package/src/printers/printer-factory.ts +3 -3
- package/src/printers/prusa.ts +0 -1
- package/src/safety/confirmation.ts +15 -38
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +15 -5
- package/src/safety/print-file.ts +139 -17
- package/src/safety/printer-state.ts +179 -40
- package/src/slicer/bambu-cli.ts +8 -5
- package/src/slicer/layer-pauses.ts +143 -0
- package/src/stl/stl-manipulator.ts +45 -0
- package/src/studio-print-options.ts +201 -0
|
@@ -0,0 +1,154 @@
|
|
|
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: Readonly<Record<"printer.cer" | "slicer_base64.cer", { sha256: string; base64: string }>> = Object.freeze({
|
|
11
|
+
"printer.cer": {
|
|
12
|
+
sha256: "36f2bcee347ec7adce719b5fd350099591a4d3d0ec4e039c7019890d78e152a0",
|
|
13
|
+
base64:
|
|
14
|
+
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZmekNDQTJlZ0F3SUJBZ0lVWHR6UjZ0UmlML1JIQlJYT295RlUwK1hybGlv" +
|
|
15
|
+
"d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRUxNQWtHQTFVRUJoTUNRMDR4SVRBZkJnTlZCQW9NR0VKQ1RDQlVaV05vYm05c2IyZHBa" +
|
|
16
|
+
"WE1nUTI4dQpJRXgwWkRFVU1CSUdBMVVFQXd3TFFrSk1JRU5CTWlCU1UwRXdJQmNOTWpVd05qRTNNREV6T0RBNFdoZ1BNakExCk1E" +
|
|
17
|
+
"QTJNVGN3TVRNNE1EaGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXYKYkc5bmFXVnpJRU52" +
|
|
18
|
+
"TGlCTWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRwo5dzBCQVFFRkFBT0NBZzhBTUlJQ0Nn" +
|
|
19
|
+
"S0NBZ0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBCjY3OXJQYU9kV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRF" +
|
|
20
|
+
"K1NwR2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG8KYk93ZjUzRkp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlz" +
|
|
21
|
+
"aWRvY1djZ3JETi9TSUR5d2dQQjVDVgpiWWczVnZ6dWE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJi" +
|
|
22
|
+
"bE93TEJBeCtmSG9rCndxaDZ0a0k2S3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFq" +
|
|
23
|
+
"QkYKdUh0a2pyYjVLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbQpxOHQ0SUwv" +
|
|
24
|
+
"MzVoUDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxClkybk5rRU4wK3RqajNoNFdk" +
|
|
25
|
+
"enJXZ1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2UKSzIxeGd4SmR6TWJkenRmVHBpYmlYazBhYmZP" +
|
|
26
|
+
"cE4rMVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MQo1VnF6ajNTVnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6" +
|
|
27
|
+
"a1QwbE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLCnYvWnNYUGdYTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRq" +
|
|
28
|
+
"RGg3YnZyWC8vY3ovVm9wQVUKbHhVdFF0TUNBd0VBQWFOak1HRXdIUVlEVlIwT0JCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0x" +
|
|
29
|
+
"YW4xTUI4RwpBMVVkSXdRWU1CYUFGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3CkRn" +
|
|
30
|
+
"WURWUjBQQVFIL0JBUURBZ0VHTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFCRlpES01KZnAvTjRnQmVGSGgKTWlGZWhhVXlNUzZl" +
|
|
31
|
+
"OW16clRmTUxKTEpvajZKb3BhOVY5aklmY0NFQkdadVJUaHFGY0FUVitVZEZIU0lOcFVjSAp1cGNDWW5helRSQzRkbjFobnhuUTFv" +
|
|
32
|
+
"alFjSHhkR3A5eEd3L1ljbEFLRDk3ZDhiUFNoZkJNVDF0bzl6Yk1LN1Q1Ckw4emdxZzAxWUlPS2pRazBIY2QwKzBpVXI2bTh6UTVQ" +
|
|
33
|
+
"OFJsM1FYcUF5ZVdncW1ZUXJyalRXd1BzZ2RmSE5YS1gKdkRyeDcvY3FyeTVsS1U4MDJoVXBsS01CeGVsdjRXODQwN1l0ajFsZkpP" +
|
|
34
|
+
"d3Z4cXh4c0ZVNWpTd2NVRzN6bzJ2awpRdGpSczhtNUJLdXA1SzFPUFlra1B1N0xkODlYMFhwVTA3My9kTkRHMTF1eGIxZURLcnRO" +
|
|
35
|
+
"UDZ2WnVaak5FMlBxCjhIQ29JMUV0UCtJdHlxdFVNdkhpNloyenNtbEEyNWJyb1Zpb2VVS3hqbEllY3BROUpSL0ZoRHU5Q1dORi9u" +
|
|
36
|
+
"RFcKTFNPUk5hTU16Z3NNU3pJK0hDaVVocU4rcU1JdlZQNnJ6R1RKendxei9sYzVMZitaUENuR0E5V0pUVDR1UEloZgp1ZmJabW5V" +
|
|
37
|
+
"SjM1V3VXS0h4b3ZEc3FCaDg4elE5c1orZWk0SGk0dlZ6T2hVZ2ZHM2FMb1NRRVlxUm9xYWJvQU5oCndDd3p5dVcyUnY1NHU1UVNC" +
|
|
38
|
+
"YmQ2R3gxT3B2c1dtTFBXZDIvaUwya0lTbDV3Zm1MR1Z5ZHZTSmErcmJPZnVBeTcKeWNWUWFjVkRRQ0FuYmhvVnJReTcrNDU0UXNL" +
|
|
39
|
+
"U1czWlY2QmN5UnJvcmV3Q3lDWWdkN255eGZseEhaVEJFeWtYWApoYUdOZS9LRk52SkJNT0l1SVV6a25SUm1pUT09Ci0tLS0tRU5E" +
|
|
40
|
+
"IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUI4ekNDQVptZ0F3SUJBZ0lVZTYxakdRNFJ6" +
|
|
41
|
+
"SUM4aytzTnVxYkkvQ2FOcVBJd0NnWUlLb1pJemowRUF3SXcKUmpFTE1Ba0dBMVVFQmhNQ1EwNHhJVEFmQmdOVkJBb01HRUpDVENC" +
|
|
42
|
+
"VVpXTm9ibTlzYjJkcFpYTWdRMjh1SUV4MApaREVVTUJJR0ExVUVBd3dMUWtKTUlFTkJNaUJGUTBNd0lCY05NalV3TmpFM01ERXpP" +
|
|
43
|
+
"RE0xV2hnUE1qQTFNREEyCk1UY3dNVE00TXpWYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURWUVFLREJoQ1Frd2dWR1ZqYUc1" +
|
|
44
|
+
"dmJHOW4KYVdWeklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZSEtvWkl6ajBDQVFZSQpLb1pJ" +
|
|
45
|
+
"emowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2SmdiaXdZCkFQUEhLN2tWYlZtZXJr" +
|
|
46
|
+
"cWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3SFFZRFZSME9CQllFRkt1UnBzalkKUkVPeUlLSDdId09FNmpoR0JkNk5NQjhH" +
|
|
47
|
+
"QTFVZEl3UVlNQmFBRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2TgpNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFR" +
|
|
48
|
+
"SC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTUNBMGdBCk1FVUNJRXJCaVVtM1ZkdFAzcno0a2I4YUxwSTVwK0J6TDdNOXZFbEJHV1dK" +
|
|
49
|
+
"eHBITUFpRUEzcjV0SldWR3d1eGkKWUNyQjFjNDBLWUZSRnlhaEdyaE9KWkFqL1loUmRuVT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUt" +
|
|
50
|
+
"LS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRWVUQ0NBMkdnQXdJQkFnSVVPcStsTklhQzJ4c3N3a0ZxajVKUHlW" +
|
|
51
|
+
"Qmw0NWN3RFFZSktvWklodmNOQVFFTApCUUF3UWpFTE1Ba0dBMVVFQmhNQ1EwNHhJakFnQmdOVkJBb01HVUpDVENCVVpXTm9ibTlz" +
|
|
52
|
+
"YjJkcFpYTWdRMjh1CkxDQk1kR1F4RHpBTkJnTlZCQU1NQmtKQ1RDQkRRVEFlRncweU5UQTJNVGN3TWpBeE1qZGFGdzB6TlRBMk1U" +
|
|
53
|
+
"VXcKTWpBeE1qZGFNRVl4Q3pBSkJnTlZCQVlUQWtOT01TRXdId1lEVlFRS0RCaENRa3dnVkdWamFHNXZiRzluYVdWegpJRU52TGlC" +
|
|
54
|
+
"TWRHUXhGREFTQmdOVkJBTU1DMEpDVENCRFFUSWdVbE5CTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGCkFBT0NBZzhBTUlJQ0NnS0NB" +
|
|
55
|
+
"Z0VBbzQ1NTBHNGM0MmdUS3pRcWl4d0tUMDg5Uml6SWRacHlPY0dBNjc5clBhT2QKV3NNcVZ3bllQUDJGcE1xWEtrakZiZWRFK1Nw" +
|
|
56
|
+
"R2xvaTJOS0N1aVBOVlJicTlQSE9PWndUczdZTG9iT3dmNTNGSgp1TzZ2UkZwekZmWDF0bGM5emxGcUp2Wm5ZTzlOZ0hwTXlzaWRv" +
|
|
57
|
+
"Y1djZ3JETi9TSUR5d2dQQjVDVmJZZzNWdnp1CmE5ZndaeDllNUtUOXhkNUlwVHFkVHJXUzQ3alFPVktMaGRRQ2JKRklsTXJibE93" +
|
|
58
|
+
"TEJBeCtmSG9rd3FoNnRrSTYKS3R1eXlqdzhEeXNlYmkxbmRXakt0WjJtVzQ3cjh4Wi9KK3ozRVpxY3lKTVk2TVJ0eC96YjFqQkZ1" +
|
|
59
|
+
"SHRranJiNQpLdjFETXpTS2xrYU5KSWJ2QytNaytoSTk3VytTakxTUnVJZEM3K29KVXpXYVN6Z3U5Y2pYQ1ZmbXE4dDRJTC8zCjVo" +
|
|
60
|
+
"UDY5UEs5NUxnTGVjdElyUDk2Q1lBVC9hVk1HMTlGckZXMFFXRXlmVCtrekc0amt1bWZQYkhxWTJuTmtFTjAKK3RqajNoNFdkenJX" +
|
|
61
|
+
"Z1FFb2pLL2xoZmNSRlZrdHM3NCthWm9NcFFQK3ZtTDE3Q0ttU3pYazVvL2VLMjF4Z3hKZAp6TWJkenRmVHBpYmlYazBhYmZPcE4r" +
|
|
62
|
+
"MVZSKzNOWWErYlJPQUtOeUdhUmVFR3N5VzJiamNqTng1MTVWcXpqM1NWCnhoTVNwNXZmRjlFNEExakU5OU0vbDlqUURNNlJ6a1Qw" +
|
|
63
|
+
"bE1jY0dBZDV0VVNkTnZEbHJxdFFhUWlLdi9ac1hQZ1gKTFRXZk9wdmFMTkVnd2RNZ1pNdWhqcGt3dkFaeW9ZZmVGOWt5eWRqRGg3" +
|
|
64
|
+
"YnZyWC8vY3ovVm9wQVVseFV0UXRNQwpBd0VBQWFOak1HRXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1D" +
|
|
65
|
+
"QVFZd0hRWURWUjBPCkJCWUVGTlZKZ1FhZDFzTlROMGp4Vmt3YkovWE0xYW4xTUI4R0ExVWRJd1FZTUJhQUZJODBRbWpjWjA2UHhD" +
|
|
66
|
+
"S2UKeFh4SjVhdmRSTDRlTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBdlM4dHlmYWdhR3NGZjlZbmNBMmtvL05hNQo5QlZGKzhU" +
|
|
67
|
+
"bFVvKzMyb3pud0lWcFMxQWhTZ0xQNnJOVmVrWE5GS2J1UDVodHVkTFExN1pSQkpJL1VNVnlZRURxCklON3h2N1pqK3pKd0Y2VzZo" +
|
|
68
|
+
"YVlyamIyVms4aWd3MVh2TlVMWmZ2Vk5OS0lrdkpVaVZxRXNsV3JDK2s3NGNyay8KV3Y4Q2hWZit6cXZmSU42TFYzZXNhR1JMMDJK" +
|
|
69
|
+
"M0FwclFHYjdERGhSMUVlZlFNU2NEa05wR0pNVW12Q21ma25ybAppSzhxZ3ZRTjFTV083SlJmNmZOS0hzTjFaUXZ5UDBwZ0xXeHBU" +
|
|
70
|
+
"M1YwLzAvV3R0cVgzY01HdUpGK2pWVXptL05oCnhZaEZld0c4dmMzS3pUam53UUFwTUE2Q1c1NTRGT0pXRnlPRDJqbjV5SkxUM1Z1" +
|
|
71
|
+
"ZSthWURRUnA0YktNeAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDcmpD" +
|
|
72
|
+
"Q0FaYWdBd0lCQWdJVU9xK2xOSWFDMnhzc3drRnFqNUpQeVZCbDQ1Z3dEUVlKS29aSWh2Y05BUUVMCkJRQXdRakVMTUFrR0ExVUVC" +
|
|
73
|
+
"aE1DUTA0eElqQWdCZ05WQkFvTUdVSkNUQ0JVWldOb2JtOXNiMmRwWlhNZ1EyOHUKTENCTWRHUXhEekFOQmdOVkJBTU1Ca0pDVENC" +
|
|
74
|
+
"RFFUQWVGdzB5TlRBMk1UY3dNakF4TkRkYUZ3MHpOVEEyTVRVdwpNakF4TkRkYU1FWXhDekFKQmdOVkJBWVRBa05PTVNFd0h3WURW" +
|
|
75
|
+
"UVFLREJoQ1Frd2dWR1ZqYUc1dmJHOW5hV1Z6CklFTnZMaUJNZEdReEZEQVNCZ05WQkFNTUMwSkNUQ0JEUVRJZ1JVTkRNRmt3RXdZ" +
|
|
76
|
+
"SEtvWkl6ajBDQVFZSUtvWkkKemowREFRY0RRZ0FFcEtURjd3UlN0eTREWHBHSnpnQ1B3Umg4Z2hMbHhVQzNxSmJ5RWdMcVR2Smdi" +
|
|
77
|
+
"aXdZQVBQSApLN2tWYlZtZXJrcWhIT1Q0UWVXUmxURzNkT1FHTEEyVnBhTmpNR0V3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFPCkJn" +
|
|
78
|
+
"TlZIUThCQWY4RUJBTUNBUVl3SFFZRFZSME9CQllFRkt1UnBzallSRU95SUtIN0h3T0U2amhHQmQ2Tk1COEcKQTFVZEl3UVlNQmFB" +
|
|
79
|
+
"Rkk4MFFtamNaMDZQeENLZXhYeEo1YXZkUkw0ZU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQgpBUUNnNlBqVVNTWlYrNGJ2ZWpjVk12" +
|
|
80
|
+
"Z1htS3pmRDk1b3NXbjBjdG5vTUJ4UERhK20rR2crQmNMVDJJbEZBZTNFCktZTXZ1NFQyOTVXUWM5MnJqS1lxVzZjaXJGcHBuZzl1" +
|
|
81
|
+
"RUZXMm1aTGlteGFTbXV0c1RmdEUzc2JNVk1KL1NMWU4KUFY3VEZ2Nm1jQlNJRldYd21CT0lwYmg0QlVjVmZPTlR2ZFNmSXFmeUFW" +
|
|
82
|
+
"eHNxNHh6YzJuYzZoUEJwQW0yMUF5agpUb0MxZXYvVGJESjhWbGxGWmlFVm1XV2xJUDNhTnpBbThTMm1PcHhQQjJXbmFuYVpIU3J2" +
|
|
83
|
+
"WExGaHN0eXp3cmpECnlPMS9pc09aN3d0cjdyY3VUSmRFdnZ2Q2ltT1psa2ZSaGFEb1Rldzl0UTBFMkZWcHp6U2ludzAycW1RMXhJ" +
|
|
84
|
+
"RTkKNS9INVp6SlNQa3BlQUhXRVBuS2t4ZzB2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FU" +
|
|
85
|
+
"RS0tLS0tCk1JSURaVENDQWsyZ0F3SUJBZ0lVVjFGY2t3WEVseWVrMW9uRm5ROWtMN0JrNE44d0RRWUpLb1pJaHZjTkFRRUwKQlFB" +
|
|
86
|
+
"d1FqRUxNQWtHQTFVRUJoTUNRMDR4SWpBZ0JnTlZCQW9NR1VKQ1RDQlVaV05vYm05c2IyZHBaWE1nUTI4dQpMQ0JNZEdReER6QU5C" +
|
|
87
|
+
"Z05WQkFNTUJrSkNUQ0JEUVRBZUZ3MHlNakEwTURRd016UXlNVEZhRncwek1qQTBNREV3Ck16UXlNVEZhTUVJeEN6QUpCZ05WQkFZ" +
|
|
88
|
+
"VEFrTk9NU0l3SUFZRFZRUUtEQmxDUWt3Z1ZHVmphRzV2Ykc5bmFXVnoKSUVOdkxpd2dUSFJrTVE4d0RRWURWUVFEREFaQ1Frd2dR" +
|
|
89
|
+
"MEV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQgpEd0F3Z2dFS0FvSUJBUURMM3BuRGR4R09rNVo2dnVnaVQ0ZHBNMGp1KzNY" +
|
|
90
|
+
"YXR4ejA5VVk3bWJqNHRrSWRieTRICm9lRWRpWVNaamM1TEpuZ0p1Q0h3dEViQkp0MUJyaVJkU1ZyRjZNOUQyVWFCRHlhbUVvMGR4" +
|
|
91
|
+
"d1NhVnhaaURWV0MKZWVDUGRFTHBGWmRFaFNOVGFUNE83emd2Y25Gc2ZITWEvMHZNQWt2RTdpMHFwM21qRXpZTGZ6NjBheGNEb0pM" +
|
|
92
|
+
"awpwN242eEtYSStjSmJBNElsVG9GanBTbGRQbUMreW5PbzdZQU9zWHQ3QVlLWTZHbHowQndVVnpTSnhVKy8rVkZ5Ci9Rcm1ZR053" +
|
|
93
|
+
"bHJRdGRSRUhlUmkwU05LMzJ4MStiT25kZkpQMHNvanVJckRqS3NkQ0x5ZTVDU1pJdnFuYm93d1cKMWpSd1pnVEJSMjlacDJuekNv" +
|
|
94
|
+
"eEpZY1U5VFNRcC80S1p1V05WQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUApORUpvM0dkT2o4UWluc1Y4U2VXcjNVUytIakFm" +
|
|
95
|
+
"QmdOVkhTTUVHREFXZ0JTUE5FSm8zR2RPajhRaW5zVjhTZVdyCjNVUytIakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJ" +
|
|
96
|
+
"YjNEUUVCQ3dVQUE0SUJBUUFCbEJJVDVaZUcKZmdjSzFMT2gxQ045c1R6eE1DTGJ0VFBGRjFOR0dBMTNtQXB1NmoxaDVZRUxiU0tj" +
|
|
97
|
+
"VXFmWHpNblZlQWIwNkh0dQozQ29Db2Urd2o3TE9OVEZPKyt2Qm0yL2lmNkp0L0RVdzFDQUVjTnlxZWg2RVMwTlg4TEpSVlNlMHFk" +
|
|
98
|
+
"VHhQSnVBCkJkT29vOTZpWDg5clJQb3hlZWQxY3BxNWhad2Jla2EzK0NKR1Y3Nml0V3AzNVVwNXJtbVVxcmx5UU9yL1dheDYKaXRv" +
|
|
99
|
+
"c0l6RzBNZmhnVXpVNTFBMlAvaFNuRDNORE1Ydit3VVkvQXZxZ0lMN3U3ZmJES25rdTFHekVLSWtmSDhobQpSczZkOFNDVTg5eHly" +
|
|
100
|
+
"d3pRMFBSODUzaXJIYXMzV3JIVnFhYjNQK3FOd1IwWWlyTDBRazdYdC9xM08xZ3JpTmcyCkJsYmpnM29icEhvOQotLS0tLUVORCBD" +
|
|
101
|
+
"RVJUSUZJQ0FURS0tLS0t",
|
|
102
|
+
},
|
|
103
|
+
"slicer_base64.cer": {
|
|
104
|
+
sha256: "f504dc1c37bc9741b7a95198914e67f3f6f1d917b1e45eaa14b9e84eb7b51ec8",
|
|
105
|
+
base64:
|
|
106
|
+
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUY3ekNDQk5lZ0F3SUJBZ0lRQm5XSDBPTmY1dE1XNlZyNXhtWXhEREFOQmdr" +
|
|
107
|
+
"cWhraUc5dzBCQVFzRkFEQloKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVObGNuUWdTVzVqTVRNd01RWURW" +
|
|
108
|
+
"UVFERXlwUwpZWEJwWkZOVFRDQlVURk1nUkZZZ1VsTkJJRTFwZUdWa0lGTklRVEkxTmlBeU1ESXdJRU5CTFRFd0hoY05NakV3Ck56" +
|
|
109
|
+
"RXpNREF3TURBd1doY05Nakl3TnpFek1qTTFPVFU1V2pBWk1SY3dGUVlEVlFRRERBNHFMbUpoYldKMWJHRmkKTG1OdmJUQ0NBU0l3" +
|
|
110
|
+
"RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUt4c3pMZGpBS21TMkpscQpaNTI4WkVsM0VxWkd2MWp1UnZ2TTR4" +
|
|
111
|
+
"cCtvcGVNWENzMWpLc2IxV2ZpZUZBL0tqWEJ6NG51ZVpzRkxhYnB5aXBBCjlvUFBkOG9HajhzWjRoOENJYk1iejJHeG1VVktUb1Ra" +
|
|
112
|
+
"OWYzd1lwMHNXRFVQVHhoWEd3L0p6cXk2bWN3cWhvQ08KUWJWck5hUUNxUXVOcElLTWx2UFg1YUsxQTBRL0FzZE4ybXBWdFNHMENw" +
|
|
113
|
+
"cGNSZjVaRTQyelZGUGcxaWVReGdpSAp3NkxXTGlaM0NHcHRueVAxc0JsSUx6VTQ4cXJoMUQwN1ptdUVITDA1cEo3ZFFBc2hzK0Jr" +
|
|
114
|
+
"bFJ6eXpUZ2lVKzFlCitRZVBGYXExRG9zekZlMndMS3Z1UjhPL1JaOGZGS0pzTnlodE80b21jMklTR3ZSb3lrN3FLU3JGUU9jNG1l" +
|
|
115
|
+
"a1kKMDJzSUZHVUNBd0VBQWFPQ0F2RXdnZ0x0TUI4R0ExVWRJd1FZTUJhQUZLU041YjU4ZWVSd0kyMHVLVFN0STFqYwo5VEYvTUIw" +
|
|
116
|
+
"R0ExVWREZ1FXQkJUUDIrTDR0YjJhOGZsMXBPWSt6RmxBNEdOamtqQW5CZ05WSFJFRUlEQWVnZzRxCkxtSmhiV0oxYkdGaUxtTnZi" +
|
|
117
|
+
"WUlNWW1GdFluVnNZV0l1WTI5dE1BNEdBMVVkRHdFQi93UUVBd0lGb0RBZEJnTlYKSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZ" +
|
|
118
|
+
"QkJRVUhBd0l3UGdZRFZSMGdCRGN3TlRBekJnWm5nUXdCQWdFdwpLVEFuQmdnckJnRUZCUWNDQVJZYmFIUjBjRG92TDNkM2R5NWth" +
|
|
119
|
+
"V2RwWTJWeWRDNWpiMjB2UTFCVE1JR0ZCZ2dyCkJnRUZCUWNCQVFSNU1IY3dKQVlJS3dZQkJRVUhNQUdHR0doMGRIQTZMeTl2WTNO" +
|
|
120
|
+
"d0xtUnBaMmxqWlhKMExtTnYKYlRCUEJnZ3JCZ0VGQlFjd0FvWkRhSFIwY0RvdkwyTmhZMlZ5ZEhNdVpHbG5hV05sY25RdVkyOXRM" +
|
|
121
|
+
"MUpoY0dsawpVMU5NVkV4VFJGWlNVMEZOYVhobFpGTklRVEkxTmpJd01qQkRRUzB4TG1OeWREQUpCZ05WSFJNRUFqQUFNSUlCCmZn" +
|
|
122
|
+
"WUtLd1lCQkFIV2VRSUVBZ1NDQVc0RWdnRnFBV2dBZGdBcGViN3duams1SWZCV2M1OWpwWGZsdmxkOW5HQUsKK1BsTlhTWmNKVjNI" +
|
|
123
|
+
"aEFBQUFYcWVwZEJsQUFBRUF3QkhNRVVDSUcrQVVPdnNqV1VIMDdBK0FaTkNyVVJackxYLwo2MldSVXcrM2FTWlhxSG8vQWlFQW9r" +
|
|
124
|
+
"c1dpMm13bTZrNEZDZW45VThNeTNJbmVEU1lxbnY3K2JoODhLOExrdHdBCmRnQlJvN0QxL1FGNW5GWnR1RGQ0and5a2Vzd2JKOHYz" +
|
|
125
|
+
"bm9oQ21nMysxSXNGNVFBQUFYcWVwZERWQUFBRUF3QkgKTUVVQ0lRRDZLYk5YRWZaT1pnN1MvdURIRlRWY00xczByemw0ZWtQNFhW" +
|
|
126
|
+
"cHlRSHQ5MkFJZ0pVbndQMzlDY1NjTwppZ1dZSm1yeGpCQTMxR25XajE4THVLeURWMWpYSmNRQWRnQkJ5TXF4M3lKR1NoREdvVG9K" +
|
|
127
|
+
"UW9kZVRqR0xHd1ByCjYwdkhhUENRWXBZRzlnQUFBWHFlcGRCRUFBQUVBd0JITUVVQ0lRQ3Y3dWZlY21CVmhwbVF1aUpFV1R6dFkw" +
|
|
128
|
+
"ck4KbnBnWnZnQlVNL3ppVlo0d1VBSWdRQXVHbUM5KzZzbkQwUklud3R0Ly9WdmRZcERYT2RpVVN3ZG4zdXc0NXNjdwpEUVlKS29a" +
|
|
129
|
+
"SWh2Y05BUUVMQlFBRGdnRUJBTEJSMFhuSFNBRm0zODN3TDM0VGcvZnFjdWtwWUFFTGZ4MG9zczVpCnZGUWhERUZndVovUXhjaDcw" +
|
|
130
|
+
"UmZBTnYyK0hETFVKNUJIWmZFRnpPQVZFc2xlalBEL2xPZFZKZVozWGE3azhXZXAKZDJudi9hVmR1UmlUOUc4Q0JFa3MrVWxXKzlW" +
|
|
131
|
+
"NmwvWVB5YThJMWh4bWRLWm5YWlhrSm1kOUJia1A4SndieHNzcApxUEYzRzlkc1Y1RTBab0RTWkZuOXgzSkpIM1QzNUZkSXJ4b0sv" +
|
|
132
|
+
"K3MvK3ZvbzdPbkRPWkdqekZrRjUrWW00ajZ1Cks2dW9vMVRXSmFDOW1NQ2FiNElneXJ5alNiVlNvNDlROUdsdXRvaGR2aEZrNEhD" +
|
|
133
|
+
"SWRmejJ3M0JYM29XdFJ3LzIKdUtLTHlEamRYeWYzVFlqWWFTYmJhblNmRDEycGQwWUsyejAvK0daUHZoMUtiN1E9Ci0tLS0tRU5E" +
|
|
134
|
+
"IENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURyekNDQXBlZ0F3SUJBZ0lRQ0R2Z1ZwQkNS" +
|
|
135
|
+
"ckdoZFdySldaSEhTakFOQmdrcWhraUc5dzBCQVFVRkFEQmgKTVFzd0NRWURWUVFHRXdKVlV6RVZNQk1HQTFVRUNoTU1SR2xuYVVO" +
|
|
136
|
+
"bGNuUWdTVzVqTVJrd0Z3WURWUVFMRXhCMwpkM2N1WkdsbmFXTmxjblF1WTI5dE1TQXdIZ1lEVlFRREV4ZEVhV2RwUTJWeWRDQkhi" +
|
|
137
|
+
"RzlpWVd3Z1VtOXZkQ0JEClFUQWVGdzB3TmpFeE1UQXdNREF3TURCYUZ3MHpNVEV4TVRBd01EQXdNREJhTUdFeEN6QUpCZ05WQkFZ" +
|
|
138
|
+
"VEFsVlQKTVJVd0V3WURWUVFLRXd4RWFXZHBRMlZ5ZENCSmJtTXhHVEFYQmdOVkJBc1RFSGQzZHk1a2FXZHBZMlZ5ZEM1agpiMjB4" +
|
|
139
|
+
"SURBZUJnTlZCQU1URjBScFoybERaWEowSUVkc2IySmhiQ0JTYjI5MElFTkJNSUlCSWpBTkJna3Foa2lHCjl3MEJBUUVGQUFPQ0FR" +
|
|
140
|
+
"OEFNSUlCQ2dLQ0FRRUE0anZoRVhMZXFLVFRvMWVxVUtLUEMzZVF5YUtsN2hMT2xsc0IKQ1NETUFaT25UakMzVS9kRHhHa0FWNTNp" +
|
|
141
|
+
"alNMZGh3WkFBSUVKenM0Ymc3L2Z6VHR4UnVMV1pzY0ZzM1luRm85NwpuaDZWZmU2M1NLTUkydGF2ZWd3NUJtVi9TbDBmdkJmNHE3" +
|
|
142
|
+
"N3VLTmQwZjNwNG1WbUZhRzVjSXpKTHYwN0E2RnB0CjQzQy9keEMvL0FIMmhkbW9SQkJZTXFsMUdOWFJvcjVINGlkcTlKb3orRWtJ" +
|
|
143
|
+
"WUl2VVg3UTZoTCtocWtwTWZUN1AKVDE5c2RsNmdTemVSbnR3aTVtM09GQnFPYXN2K3piTVVaQmZIV3ltZU1yL3k3dnJUQzBMVXE3" +
|
|
144
|
+
"ZEJNdG9NMU8vNApnZFc3alZnL3RSdm9TU2lpY05veEJOMzNzaGJ5VEFwT0I2anRTajFldFgramtNT3ZKd0lEQVFBQm8yTXdZVEFP" +
|
|
145
|
+
"CkJnTlZIUThCQWY4RUJBTUNBWVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVQTk1UU5WYlIKVEx0bThLUGlH" +
|
|
146
|
+
"eHZEbDdJOTBWVXdId1lEVlIwakJCZ3dGb0FVQTk1UU5WYlJUTHRtOEtQaUd4dkRsN0k5MFZVdwpEUVlKS29aSWh2Y05BUUVGQlFB" +
|
|
147
|
+
"RGdnRUJBTXVjTjZwSUV4SUsrdDFFbkU5U3NQVGZyZ1QxZVhrSW95UVkvRXNyCmhNQXR1ZFhIL3ZUQkgxakx1RzJjZW5Ubm1DbXJF" +
|
|
148
|
+
"YlhqY0tDaHpVeUltWk9Na1hEaXF3OGN2cE9wLzJQVjVBZGcKMDZPL25Wc0o4ZFdPNDFQMGptUDZQNmZidEdiZlltYlcwVzVCamZJ" +
|
|
149
|
+
"dHRlcDNTcCtkV09JcldjQkFJKzB0S0lKRgpQbmxVa2lhWTRJQklxRGZ2OE5aNVlCYmVyT2dPelc2c1JCYzRMMG5hNFVVK0tyazJV" +
|
|
150
|
+
"ODg2VUFiM0x1akVWMGxzCllTRVkxUVN0ZUR3c09vQnJwK3V2RlJUcDJJbkJ1VGhzNHBGc2l2OWt1WGNsVnpEQUd5U2o0ZHpwMzBk" +
|
|
151
|
+
"OHRiUWsKQ0FVdzdDMjlDNzlGdjFDNXFmUHJtQUVTcmNpSXhwZzBYNDBLUE1icDFaV1ZiZDQ9Ci0tLS0tRU5EIENFUlRJRklDQVRF" +
|
|
152
|
+
"LS0tLS0K",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The printer's answer to Pause, Resume or Stop, shared by every route (FULU's local network and Bambu Cloud,
|
|
3
|
+
* and the direct local network a printer in LAN Only + Developer Mode takes).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ControlReply = { accepted: boolean; errCode: number };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The printer's answer to a control it was sent: it echoes command and sequence_id, with
|
|
10
|
+
* err_code set when it refuses. A P1S on authorization firmware signs it: {"header":{...},"print":{...}}.
|
|
11
|
+
*/
|
|
12
|
+
export function controlReply(msg: unknown, command: string, sequenceId: string): ControlReply | undefined {
|
|
13
|
+
if (typeof msg !== "string" || !msg.includes(sequenceId)) return undefined;
|
|
14
|
+
let print: any;
|
|
15
|
+
try { print = JSON.parse(msg)?.print; } catch { return undefined; }
|
|
16
|
+
if (print?.command !== command || String(print.sequence_id) !== sequenceId) return undefined;
|
|
17
|
+
const errCode = Number(print.err_code ?? 0) || 0;
|
|
18
|
+
const failed = typeof print.result === "string" && print.result.toLowerCase() !== "success";
|
|
19
|
+
return { accepted: errCode === 0 && !failed, errCode };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The printer's command verification refused the envelope. These codes do not
|
|
24
|
+
* establish that third-party controls are unsupported: P1S 01.10 also returns
|
|
25
|
+
* 0x0502400A for a valid signature with a sequence ID outside the Studio window.
|
|
26
|
+
*/
|
|
27
|
+
export function isAuthorizationRefusal(errCode: number): boolean {
|
|
28
|
+
const code = errCode >>> 0;
|
|
29
|
+
return code >= 0x05024007 && code <= 0x0502400B;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What to tell a person about a refused control. direct: the control went straight to the printer (Developer Mode connection). */
|
|
33
|
+
export function refusedControlMessage(command: string, route: string, errCode: number, direct = false): string {
|
|
34
|
+
const word = `${command[0]!.toUpperCase()}${command.slice(1)}`;
|
|
35
|
+
const code = (errCode >>> 0).toString(16).toUpperCase().padStart(8, "0");
|
|
36
|
+
if (!isAuthorizationRefusal(errCode)) return `The printer refused ${word} over ${route} (error ${code}). Nothing changed on the printer.`;
|
|
37
|
+
const message = `The printer refused ${word} (error ${code}): its command verification failed. `;
|
|
38
|
+
return message + (direct
|
|
39
|
+
? "Check LAN Only and Developer Mode for the direct connection, or choose an authenticated Local network or Bambu Cloud connection."
|
|
40
|
+
: "Check the authenticated connection and signing setup for this printer.");
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {Agent as HttpAgent, type IncomingMessage} from "node:http";
|
|
2
|
+
import {Agent as HttpsAgent} from "node:https";
|
|
3
|
+
import {isAxiosError, type AxiosInstance} from "axios";
|
|
4
|
+
import type {OpenCameraSource} from "./camera-stream.js";
|
|
5
|
+
|
|
6
|
+
type Handlers = Parameters<OpenCameraSource>[0];
|
|
7
|
+
export type CameraUrl = {url: string; headers: Record<string, string>};
|
|
8
|
+
const MAX_BYTES = 5_000_000;
|
|
9
|
+
|
|
10
|
+
/** Same bounded JPEG framing as the Bambu stream; native code decodes pixels. */
|
|
11
|
+
export function mjpegParser(onFrame: (bytes: Buffer) => void, onError: (error: Error) => void): (chunk: Buffer) => void {
|
|
12
|
+
let buffer = Buffer.alloc(0), failed = false;
|
|
13
|
+
return chunk => {
|
|
14
|
+
if (failed) return;
|
|
15
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
16
|
+
for (;;) {
|
|
17
|
+
const start = buffer.indexOf(Buffer.from([0xff, 0xd8]));
|
|
18
|
+
if (start < 0) { buffer = buffer.subarray(Math.max(0, buffer.length - 1)); return; }
|
|
19
|
+
if (start) buffer = buffer.subarray(start);
|
|
20
|
+
const end = buffer.indexOf(Buffer.from([0xff, 0xd9]), 2);
|
|
21
|
+
if ((end >= 0 ? end + 2 : buffer.length) > MAX_BYTES) {
|
|
22
|
+
failed = true; onError(new Error("Camera JPEG too large")); return;
|
|
23
|
+
}
|
|
24
|
+
if (end < 0) return;
|
|
25
|
+
onFrame(Buffer.from(buffer.subarray(0, end + 2))); buffer = buffer.subarray(end + 2);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One persistent MJPEG response, or serialized snapshot requests on one keep-alive socket. */
|
|
31
|
+
export function openHttpCameraSource(http: AxiosInstance, plan: {urls: CameraUrl[]; snapshots?: CameraUrl[]; mode: "http-mjpeg" | "http-snapshot"}, fps: number, handlers: Handlers): ReturnType<OpenCameraSource> {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const httpAgent = new HttpAgent({keepAlive: true, maxSockets: 1});
|
|
34
|
+
const httpsAgent = new HttpsAgent({keepAlive: true, maxSockets: 1});
|
|
35
|
+
let closed = false, response: IncomingMessage | undefined, selected: CameraUrl | undefined;
|
|
36
|
+
let mode = plan.mode;
|
|
37
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
const fail = (error: unknown) => {
|
|
39
|
+
if (!closed) handlers.onError(new Error(isAxiosError(error) && [401, 403].includes(error.response?.status ?? 0) ? "Camera authentication failed" : "Camera unavailable"));
|
|
40
|
+
};
|
|
41
|
+
const close = () => {
|
|
42
|
+
closed = true; clearTimeout(timer); controller.abort(); response?.destroy(); httpAgent.destroy(); httpsAgent.destroy();
|
|
43
|
+
};
|
|
44
|
+
const useSnapshot = () => { mode = "http-snapshot"; selected = undefined; };
|
|
45
|
+
const request = async (): Promise<IncomingMessage | undefined> => {
|
|
46
|
+
for (const candidate of selected ? [selected] : mode === "http-snapshot" && plan.snapshots?.length ? plan.snapshots : plan.urls) {
|
|
47
|
+
try {
|
|
48
|
+
const result = await http.get<IncomingMessage>(candidate.url, {
|
|
49
|
+
headers: candidate.headers, responseType: "stream", timeout: 8000,
|
|
50
|
+
maxRedirects: Object.keys(candidate.headers).length ? 0 : 3,
|
|
51
|
+
httpAgent, httpsAgent, signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
response = result.data;
|
|
54
|
+
if (closed) { response.destroy(); return undefined; }
|
|
55
|
+
const contentType = String(result.headers["content-type"] ?? "");
|
|
56
|
+
if (mode === "http-mjpeg" && !/^multipart\/x-mixed-replace\b/i.test(contentType)) {
|
|
57
|
+
response.destroy();
|
|
58
|
+
if (plan.snapshots?.length) { useSnapshot(); return request(); }
|
|
59
|
+
throw new Error("Camera did not return MJPEG");
|
|
60
|
+
}
|
|
61
|
+
selected = candidate;
|
|
62
|
+
return response;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Error responses are streams too; always release their socket before trying another URL.
|
|
65
|
+
if (isAxiosError(error)) error.response?.data?.destroy?.();
|
|
66
|
+
if (closed || (isAxiosError(error) && [401, 403].includes(error.response?.status ?? 0))) throw error;
|
|
67
|
+
if (mode === "http-mjpeg" && plan.snapshots?.length && isAxiosError(error) && [404, 405, 415].includes(error.response?.status ?? 0)) {
|
|
68
|
+
useSnapshot(); return request();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error("No camera URL answered");
|
|
73
|
+
};
|
|
74
|
+
const run = async () => {
|
|
75
|
+
const began = Date.now(), body = await request();
|
|
76
|
+
if (!body || closed) return;
|
|
77
|
+
if (mode === "http-mjpeg") {
|
|
78
|
+
const arm = () => { clearTimeout(timer); timer = setTimeout(() => fail(new Error("Camera timed out")), 15000); timer.unref(); };
|
|
79
|
+
arm();
|
|
80
|
+
const parse = mjpegParser(bytes => { arm(); if (!closed) handlers.onFrame(bytes); }, fail);
|
|
81
|
+
for await (const chunk of body) { if (closed) return; parse(Buffer.from(chunk)); }
|
|
82
|
+
if (!closed) handlers.onEnd();
|
|
83
|
+
} else {
|
|
84
|
+
let size = 0; const chunks: Buffer[] = [];
|
|
85
|
+
for await (const chunk of body) {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
const bytes = Buffer.from(chunk); size += bytes.length;
|
|
88
|
+
if (size > MAX_BYTES) { body.destroy(); throw new Error("Camera JPEG too large"); }
|
|
89
|
+
chunks.push(bytes);
|
|
90
|
+
}
|
|
91
|
+
if (closed) return;
|
|
92
|
+
const jpeg = Buffer.concat(chunks);
|
|
93
|
+
if (jpeg.length < 3 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8 || jpeg[2] !== 0xff) throw new Error("Camera did not return JPEG");
|
|
94
|
+
handlers.onFrame(jpeg);
|
|
95
|
+
if (closed) return;
|
|
96
|
+
timer = setTimeout(() => { void run().catch(fail); }, Math.max(0, 1000 / fps - (Date.now() - began)));
|
|
97
|
+
timer.unref();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
void run().catch(fail);
|
|
101
|
+
return {close};
|
|
102
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/** One camera source per printer; bounded frames and native thumbnail processing. */
|
|
2
|
+
// Keep the core server and existing snapshots usable when the optional native addon
|
|
3
|
+
// cannot load (for example early Node 18 or an unsupported CPU).
|
|
4
|
+
// This narrow optional boundary also lets a Node 18 checkout build after npm omits Sharp.
|
|
5
|
+
// Runtime/package tests exercise these methods against the pinned native module.
|
|
6
|
+
interface NativeImage {
|
|
7
|
+
metadata(): Promise<{width?: number; height?: number}>;
|
|
8
|
+
resize(width: number, height: number, options: {fit: "fill"}): NativeImage;
|
|
9
|
+
resize(options: {width: number; withoutEnlargement: boolean}): NativeImage;
|
|
10
|
+
greyscale(): NativeImage;
|
|
11
|
+
raw(): NativeImage;
|
|
12
|
+
jpeg(options: {quality: number}): NativeImage;
|
|
13
|
+
toBuffer(): Promise<Buffer>;
|
|
14
|
+
toBuffer(options: {resolveWithObject: true}): Promise<{data: Buffer; info: {width: number; height: number}}>;
|
|
15
|
+
}
|
|
16
|
+
type NativeProcessor = ((input: Buffer, options: {limitInputPixels: number}) => NativeImage) & {
|
|
17
|
+
cache(options: {memory: number; files: number; items: number}): void;
|
|
18
|
+
};
|
|
19
|
+
let native: Promise<NativeProcessor> | undefined;
|
|
20
|
+
async function nativeSharp() {
|
|
21
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
22
|
+
if (major < 20 || (major === 20 && minor < 9)) throw new Error("Native camera processing requires Node 20.9+");
|
|
23
|
+
const optionalPackage: string = "sharp";
|
|
24
|
+
native ??= import(optionalPackage).then(module => {
|
|
25
|
+
const processor = module.default as NativeProcessor;
|
|
26
|
+
processor.cache({memory: 8, files: 0, items: 32});
|
|
27
|
+
return processor;
|
|
28
|
+
});
|
|
29
|
+
return native;
|
|
30
|
+
}
|
|
31
|
+
export async function cameraNativeAvailable(): Promise<boolean> {
|
|
32
|
+
try { await nativeSharp(); return true; } catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CameraTransport = "http";
|
|
36
|
+
export type CameraSource = { close(): void };
|
|
37
|
+
export type OpenCameraSource = (handlers: { onFrame(bytes: Buffer): void; onError(error: Error): void; onEnd(): void }) => CameraSource;
|
|
38
|
+
export type FrameOptions = { sinceSeq?: number; waitMs?: number; maxWidth?: number; maxBytes?: number; changedOnly?: boolean; minChange?: number; signal?: AbortSignal };
|
|
39
|
+
export type StreamFrameResult = { status: "frame" | "unchanged" | "stopped" | "unavailable"; seq: number; capturedAt?: string; width?: number; height?: number; format?: "image/jpeg"; base64?: string; change?: number; reason?: "auth" | "unreachable" | "no-camera" };
|
|
40
|
+
type Frame = { seq: number; bytes: Buffer; thumb: Buffer; at: number; width: number; height: number; scaled: Map<string, Promise<{data: Buffer; info: {width: number; height: number}}>> };
|
|
41
|
+
export type StreamOptions = { transport: CameraTransport; fps: number; idleTimeoutMs: number; open: OpenCameraSource; skipFirstFrame: boolean };
|
|
42
|
+
|
|
43
|
+
export function thumbnailChange(a: Uint8Array, b: Uint8Array): number {
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (let i = 0; i < a.length; i++) total += Math.abs(a[i] - b[i]);
|
|
46
|
+
return total / (a.length * 255);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class CameraStream {
|
|
50
|
+
readonly startedAt = new Date().toISOString();
|
|
51
|
+
readonly transport: CameraTransport;
|
|
52
|
+
readonly fps: number;
|
|
53
|
+
#source?: CameraSource;
|
|
54
|
+
#timer?: ReturnType<typeof setTimeout>;
|
|
55
|
+
#generation = 0;
|
|
56
|
+
#processing = false;
|
|
57
|
+
#lastAccepted = -Infinity;
|
|
58
|
+
#latest?: Frame;
|
|
59
|
+
// Keep thumbnails of frames actually delivered, not every camera frame. A quiet view may keep
|
|
60
|
+
// comparing with its first frame for hours; a rolling JPEG history would repeatedly resend it.
|
|
61
|
+
#references = new Map<number, Buffer>();
|
|
62
|
+
#waiters = new Set<() => void>();
|
|
63
|
+
#stopped = false;
|
|
64
|
+
#reconnected = false;
|
|
65
|
+
#reason?: StreamFrameResult["reason"];
|
|
66
|
+
#seq = 0;
|
|
67
|
+
constructor(private options: StreamOptions, private nextSeq: () => number, private now: () => number = Date.now) {
|
|
68
|
+
this.transport = options.transport; this.fps = options.fps;
|
|
69
|
+
}
|
|
70
|
+
get stopped(): boolean { return this.#stopped; }
|
|
71
|
+
start(): void { this.touch(); this.#connect(); }
|
|
72
|
+
touch(idleTimeoutMs?: number): void {
|
|
73
|
+
if (this.#stopped) return;
|
|
74
|
+
if (idleTimeoutMs !== undefined) this.options.idleTimeoutMs = idleTimeoutMs;
|
|
75
|
+
clearTimeout(this.#timer);
|
|
76
|
+
this.#timer = setTimeout(() => this.stop(), this.options.idleTimeoutMs);
|
|
77
|
+
this.#timer.unref();
|
|
78
|
+
}
|
|
79
|
+
stop(): void {
|
|
80
|
+
if (this.#stopped) return;
|
|
81
|
+
this.#stopped = true; this.#generation++;
|
|
82
|
+
clearTimeout(this.#timer);
|
|
83
|
+
this.#source?.close(); this.#source = undefined;
|
|
84
|
+
this.#latest = undefined; this.#references.clear(); this.#wake();
|
|
85
|
+
}
|
|
86
|
+
#wake(): void { for (const wake of [...this.#waiters]) wake(); }
|
|
87
|
+
#connect(): void {
|
|
88
|
+
const generation = ++this.#generation;
|
|
89
|
+
let buffered: Buffer | undefined, live = !this.options.skipFirstFrame;
|
|
90
|
+
const current = () => !this.#stopped && generation === this.#generation;
|
|
91
|
+
try {
|
|
92
|
+
const source = this.options.open({
|
|
93
|
+
onFrame: bytes => {
|
|
94
|
+
if (!current()) return;
|
|
95
|
+
// P1/A1 replay their stale buffered frame, sometimes more than once.
|
|
96
|
+
if (!live) { if (!buffered) { buffered = bytes; return; } if (bytes.equals(buffered)) return; buffered = undefined; live = true; }
|
|
97
|
+
if (this.#processing || this.now() - this.#lastAccepted < 1000 / this.fps) return;
|
|
98
|
+
this.#lastAccepted = this.now(); this.#processing = true;
|
|
99
|
+
void this.#accept(bytes, generation).catch(() => { if (current()) this.#lost("unreachable"); })
|
|
100
|
+
.finally(() => { this.#processing = false; });
|
|
101
|
+
},
|
|
102
|
+
onError: error => { if (current()) this.#lost(/auth|payload size|401|403/i.test(error.message) ? "auth" : "unreachable"); },
|
|
103
|
+
onEnd: () => { if (current()) this.#lost("unreachable"); },
|
|
104
|
+
});
|
|
105
|
+
// A transport may fail synchronously. Never keep its abandoned source after reconnect/stop.
|
|
106
|
+
if (current()) this.#source = source; else source.close();
|
|
107
|
+
} catch { if (current()) this.#lost("unreachable"); }
|
|
108
|
+
}
|
|
109
|
+
#lost(reason: NonNullable<StreamFrameResult["reason"]>): void {
|
|
110
|
+
this.#generation++;
|
|
111
|
+
this.#source?.close(); this.#source = undefined;
|
|
112
|
+
if (!this.#reconnected && reason !== "auth") { this.#reconnected = true; this.#connect(); return; }
|
|
113
|
+
this.#reason = reason; this.stop();
|
|
114
|
+
}
|
|
115
|
+
async #accept(bytes: Buffer, generation: number): Promise<void> {
|
|
116
|
+
if (bytes.length > 5_000_000) throw Error("Camera JPEG too large");
|
|
117
|
+
const decoder = (await nativeSharp())(bytes, {limitInputPixels: 8_000_000});
|
|
118
|
+
const metadata = await decoder.metadata();
|
|
119
|
+
const thumb = await decoder.resize(32, 18, {fit: "fill"}).greyscale().raw().toBuffer();
|
|
120
|
+
if (this.#stopped || generation !== this.#generation) return;
|
|
121
|
+
this.#latest = {seq: this.#seq = this.nextSeq(), bytes, thumb, at: this.now(), width: metadata.width!, height: metadata.height!, scaled: new Map()};
|
|
122
|
+
this.#wake();
|
|
123
|
+
}
|
|
124
|
+
async frame(options: FrameOptions = {}): Promise<StreamFrameResult> {
|
|
125
|
+
const since = options.sinceSeq ?? 0, deadline = Date.now() + (options.waitMs ?? 0);
|
|
126
|
+
this.touch();
|
|
127
|
+
for (;;) {
|
|
128
|
+
options.signal?.throwIfAborted();
|
|
129
|
+
if (this.#reason) return {status: "unavailable", seq: this.#seq, reason: this.#reason};
|
|
130
|
+
if (this.#stopped) return {status: "stopped", seq: this.#seq};
|
|
131
|
+
const latest = this.#latest;
|
|
132
|
+
if (latest && latest.seq > since) {
|
|
133
|
+
const reference = this.#references.get(since);
|
|
134
|
+
const change = reference ? thumbnailChange(latest.thumb, reference) : 1;
|
|
135
|
+
if (options.changedOnly === false || change >= (options.minChange ?? 0.02)) return this.#result(latest, change, options.maxWidth, options.maxBytes);
|
|
136
|
+
}
|
|
137
|
+
const left = deadline - Date.now();
|
|
138
|
+
if (left <= 0) return {status: "unchanged", seq: since, ...(latest ? {capturedAt: new Date(latest.at).toISOString()} : {})};
|
|
139
|
+
await new Promise<void>(resolve => {
|
|
140
|
+
const done = () => { clearTimeout(timer); this.#waiters.delete(done); options.signal?.removeEventListener("abort", done); resolve(); };
|
|
141
|
+
const timer = setTimeout(done, left);
|
|
142
|
+
this.#waiters.add(done); options.signal?.addEventListener("abort", done, {once: true});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async latest(waitMs: number): Promise<Buffer | undefined> {
|
|
147
|
+
const result = await this.frame({waitMs, changedOnly: false});
|
|
148
|
+
return result.status === "frame" ? Buffer.from(result.base64!, "base64") : undefined;
|
|
149
|
+
}
|
|
150
|
+
async #encoded(frame: Frame, width: number, maxBytes?: number): Promise<{data: Buffer; info: {width: number; height: number}}> {
|
|
151
|
+
// Start near the byte target instead of always encoding a full-size frame that will be
|
|
152
|
+
// discarded. Source JPEG density is a cheap conservative estimate, not a size guarantee.
|
|
153
|
+
if (maxBytes && frame.bytes.length > maxBytes) width = Math.min(width, Math.max(1, Math.floor(frame.width * Math.sqrt(maxBytes / frame.bytes.length) * 0.8)));
|
|
154
|
+
// At most four native encodes. The final 1-pixel fallback guarantees a bounded result
|
|
155
|
+
// even for pathological entropy; quality stays 70 and aspect ratio is preserved.
|
|
156
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
157
|
+
const encoded = await (await nativeSharp())(frame.bytes, {limitInputPixels: 8_000_000}).resize({width, withoutEnlargement: true}).jpeg({quality: 70}).toBuffer({resolveWithObject: true});
|
|
158
|
+
if (!maxBytes || encoded.data.length <= maxBytes) return encoded;
|
|
159
|
+
width = attempt === 2 ? 1 : Math.max(1, Math.floor(width * Math.min(0.8, Math.sqrt(maxBytes / encoded.data.length) * 0.9)));
|
|
160
|
+
}
|
|
161
|
+
throw new Error("Camera frame could not fit max_bytes");
|
|
162
|
+
}
|
|
163
|
+
async #result(frame: Frame, change: number, maxWidth?: number, maxBytes?: number): Promise<StreamFrameResult> {
|
|
164
|
+
let bytes = frame.bytes, width = frame.width, height = frame.height;
|
|
165
|
+
if ((maxWidth && width > maxWidth) || (maxBytes && bytes.length > maxBytes)) {
|
|
166
|
+
const key = `${maxWidth ?? width}:${maxBytes ?? 0}`;
|
|
167
|
+
let scaled = frame.scaled.get(key);
|
|
168
|
+
if (!scaled) {
|
|
169
|
+
scaled = this.#encoded(frame, Math.min(width, maxWidth ?? width), maxBytes);
|
|
170
|
+
if (frame.scaled.size < 4) frame.scaled.set(key, scaled);
|
|
171
|
+
}
|
|
172
|
+
const resized = await scaled; bytes = resized.data; width = resized.info.width; height = resized.info.height;
|
|
173
|
+
}
|
|
174
|
+
if (this.#stopped) return this.#reason ? {status: "unavailable", seq: this.#seq, reason: this.#reason} : {status: "stopped", seq: this.#seq};
|
|
175
|
+
this.#references.set(frame.seq, frame.thumb);
|
|
176
|
+
if (this.#references.size > 256) this.#references.delete(this.#references.keys().next().value!);
|
|
177
|
+
return {status: "frame", seq: frame.seq, capturedAt: new Date(frame.at).toISOString(), width, height, format: "image/jpeg", base64: bytes.toString("base64"), change};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class CameraStreams {
|
|
182
|
+
#streams = new Map<string, CameraStream>();
|
|
183
|
+
#sequence = 0;
|
|
184
|
+
constructor(private now: () => number = Date.now) {}
|
|
185
|
+
get(key: string): CameraStream | undefined { return this.#streams.get(key); }
|
|
186
|
+
start(key: string, options: StreamOptions): {stream: CameraStream; status: "started" | "running"} {
|
|
187
|
+
const existing = this.get(key);
|
|
188
|
+
if (existing && !existing.stopped) { existing.touch(options.idleTimeoutMs); return {stream: existing, status: "running"}; }
|
|
189
|
+
const stream = new CameraStream(options, () => ++this.#sequence, this.now);
|
|
190
|
+
this.#streams.set(key, stream); stream.start();
|
|
191
|
+
return {stream, status: "started"};
|
|
192
|
+
}
|
|
193
|
+
stop(key: string): void { this.get(key)?.stop(); }
|
|
194
|
+
stopAll(): void { for (const stream of this.#streams.values()) stream.stop(); this.#streams.clear(); }
|
|
195
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Additive stream tools; discovery is the capability check for older clients. */
|
|
2
|
+
const routing = {
|
|
3
|
+
port: {type: "string", description: "Printer API port; defaults to PRINTER_PORT."},
|
|
4
|
+
type: {type: "string", description: "Printer backend; defaults to PRINTER_TYPE."},
|
|
5
|
+
api_key: {type: "string", description: "Printer API key; defaults to API_KEY."},
|
|
6
|
+
host: {type: "string", description: "Printer host or IP; defaults to PRINTER_HOST."},
|
|
7
|
+
bambu_serial: {type: "string", description: "Printer serial number; defaults to the configured serial and identifies its shared stream."},
|
|
8
|
+
bambu_token: {type: "string", description: "Printer LAN access code; defaults to the configured access code."},
|
|
9
|
+
};
|
|
10
|
+
const frameSchema = {
|
|
11
|
+
type: "object" as const, properties: {
|
|
12
|
+
status: {type: "string", enum: ["frame", "unchanged", "stopped", "unavailable"]}, seq: {type: "integer", minimum: 0},
|
|
13
|
+
capturedAt: {type: "string"}, width: {type: "integer"}, height: {type: "integer"},
|
|
14
|
+
format: {type: "string", const: "image/jpeg"}, base64: {type: "string"}, change: {type: "number"},
|
|
15
|
+
reason: {type: "string", enum: ["no-camera", "auth", "unreachable"]},
|
|
16
|
+
}, required: ["status", "seq"],
|
|
17
|
+
};
|
|
18
|
+
export const CAMERA_STREAM_TOOLS = [
|
|
19
|
+
{name: "camera_stream_start", description: "Start or renew one persistent camera connection for this printer. Call only while watched; it closes automatically after idle_timeout_ms without a viewer request. camera_snapshot then reuses its latest frame. No printer motion or print commands.",
|
|
20
|
+
inputSchema: {type: "object" as const, properties: {...routing, fps: {type: "number", minimum: 0.2, maximum: 2, default: 1, description: "Maximum processed frames per second. The first viewer sets the rate until the stream closes."}, idle_timeout_ms: {type: "integer", minimum: 5000, maximum: 120000, default: 30000, description: "Close the shared connection after this many milliseconds without a viewer request."}}, additionalProperties: false},
|
|
21
|
+
outputSchema: {type: "object" as const, properties: {status: {type: "string", enum: ["started", "running", "not-supported"]}, reason: {type: "string", enum: ["runtime"]}, transport: {type: "string", enum: ["http"]}, fps: {type: "number"}, startedAt: {type: "string"}}, required: ["status"], oneOf: [
|
|
22
|
+
{properties: {status: {enum: ["started", "running"]}}, required: ["transport", "fps", "startedAt"]},
|
|
23
|
+
{properties: {status: {const: "not-supported"}}, required: ["reason"]},
|
|
24
|
+
]}},
|
|
25
|
+
{name: "camera_frame", description: "Read the shared camera stream, optionally waiting up to 10 seconds. Returns JPEG only when newer than since_seq and visibly changed (32×18 grayscale difference). Retain the last delivered frame's seq on unchanged. max_width resizes JPEG to the viewer. Does not start a stopped stream.",
|
|
26
|
+
inputSchema: {type: "object" as const, properties: {...routing, since_seq: {type: "integer", minimum: 0, default: 0, description: "Sequence of the last delivered JPEG; retain it when a result is unchanged."}, wait_ms: {type: "integer", minimum: 0, maximum: 10000, default: 0, description: "Maximum time to await a newer changed frame; zero returns immediately."}, max_width: {type: "integer", minimum: 1, maximum: 4096, description: "Optional maximum JPEG width in pixels, preserving aspect ratio and never enlarging."}, max_bytes: {type: "integer", minimum: 4096, maximum: 5000000, description: "Optional maximum encoded JPEG bytes. Downscales further at quality 70 when needed; base64 adds about one third plus protocol overhead. Use 42000 for a two-second 30 KB/s viewer budget."}, changed_only: {type: "boolean", default: true, description: "Return JPEG only when its thumbnail differs enough from the last delivered frame."}, min_change: {type: "number", minimum: 0, maximum: 1, default: 0.02, description: "Minimum mean absolute grayscale thumbnail difference, normalized from zero to one."}}, additionalProperties: false}, outputSchema: frameSchema},
|
|
27
|
+
{name: "camera_stream_stop", description: "Close this printer's camera connection and free its cached frames. Other printers keep their own streams. Active viewers may restart it; normally let the idle timeout close a shared stream.",
|
|
28
|
+
inputSchema: {type: "object" as const, properties: routing, additionalProperties: false}, outputSchema: {type: "object" as const, properties: {status: {type: "string", const: "stopped"}}, required: ["status"]}},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function cameraNumber(value: unknown, fallback: number, min: number, max: number, name: string, integer = false): number {
|
|
32
|
+
const n = value === undefined ? fallback : value;
|
|
33
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < min || n > max || (integer && !Number.isSafeInteger(n))) throw new Error(`${name} must be ${integer ? "an integer" : "a number"} from ${min} to ${max}`);
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
@@ -56,6 +56,7 @@ export interface CounterpartCapabilities {
|
|
|
56
56
|
progress: boolean;
|
|
57
57
|
layers: boolean;
|
|
58
58
|
camera: CameraCapability;
|
|
59
|
+
cameraStream?: boolean;
|
|
59
60
|
filaments: FilamentCapability;
|
|
60
61
|
pause: boolean;
|
|
61
62
|
resume: boolean;
|
|
@@ -78,11 +79,12 @@ export interface CounterpartPrinterInfo {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export interface FilamentSlot {
|
|
81
|
-
|
|
82
|
+
/** "ext" for a single-spool printer; Bambu: the absolute AMS tray ("0"-"15", "128"+) or external spool ("254", "255"). */
|
|
83
|
+
id: string;
|
|
82
84
|
material?: string;
|
|
83
85
|
color?: string;
|
|
84
86
|
name?: string;
|
|
85
|
-
source: "spoolman" | "unknown";
|
|
87
|
+
source: "spoolman" | "printer" | "unknown";
|
|
86
88
|
/** Absolute path of the slicer filament profile for this material, as slice_stl's load_filaments takes it. */
|
|
87
89
|
profile?: string;
|
|
88
90
|
}
|
|
@@ -144,6 +146,7 @@ export const PRINTER_INFO_OUTPUT_SCHEMA = {
|
|
|
144
146
|
progress: { type: "boolean" },
|
|
145
147
|
layers: { type: "boolean" },
|
|
146
148
|
camera: { type: "string", enum: ["bambu-tcp", "rtsp", "http-snapshot", "none"] },
|
|
149
|
+
cameraStream: {type: "boolean"},
|
|
147
150
|
filaments: { type: "string", enum: ["ams", "single", "none"] },
|
|
148
151
|
pause: { type: "boolean" },
|
|
149
152
|
resume: { type: "boolean" },
|
|
@@ -204,11 +207,16 @@ export const CAMERA_SNAPSHOT_OUTPUT_SCHEMA = {
|
|
|
204
207
|
},
|
|
205
208
|
} as const;
|
|
206
209
|
|
|
207
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* outputSchema for get_printer_filaments: one external slot, or for Bambu every
|
|
212
|
+
* loaded AMS tray and external spool plus bambu-printer-mcp's inventory fields
|
|
213
|
+
* (trays, summary, recommended, load_filaments_all), which keep its nulls.
|
|
214
|
+
*/
|
|
208
215
|
export const FILAMENTS_OUTPUT_SCHEMA = {
|
|
209
216
|
type: "object",
|
|
210
217
|
required: ["slots"],
|
|
211
218
|
properties: {
|
|
219
|
+
trays: { type: "array", description: "Bambu only: bambu-printer-mcp's AMS tray list (slot = ams_id * 4 + tray_id), empty trays included.", items: { type: "object" } },
|
|
212
220
|
slots: {
|
|
213
221
|
type: "array",
|
|
214
222
|
items: {
|
|
@@ -219,7 +227,7 @@ export const FILAMENTS_OUTPUT_SCHEMA = {
|
|
|
219
227
|
material: { type: "string" },
|
|
220
228
|
color: { type: "string" },
|
|
221
229
|
name: { type: "string" },
|
|
222
|
-
source: { type: "string", enum: ["spoolman", "unknown"] },
|
|
230
|
+
source: { type: "string", enum: ["spoolman", "printer", "unknown"] },
|
|
223
231
|
profile: {
|
|
224
232
|
type: "string",
|
|
225
233
|
description: "Absolute path of the installed slicer filament profile for this slot's material, which slice_stl accepts as load_filaments. Omitted when none resolves.",
|