project-foundry 1.0.4 → 1.0.6
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/package.json +1 -1
- package/src/local-printer-executor.js +135 -15
package/package.json
CHANGED
|
@@ -1,4 +1,71 @@
|
|
|
1
1
|
import mqtt from 'mqtt';
|
|
2
|
+
import { connect as connectTls } from 'node:tls';
|
|
3
|
+
|
|
4
|
+
function buildBambuCameraAuthPacket(accessCode) {
|
|
5
|
+
const packet = Buffer.alloc(80);
|
|
6
|
+
packet.writeUInt32LE(0x40, 0);
|
|
7
|
+
packet.writeUInt32LE(0x3000, 4);
|
|
8
|
+
packet.write('bblp', 16, 31, 'utf8');
|
|
9
|
+
packet.write(accessCode, 48, 31, 'utf8');
|
|
10
|
+
return packet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function captureBambuJpegTls(connection) {
|
|
14
|
+
const host = (connection.host || '').trim().replace(/^\w+:\/\//, '').replace(/\/$/, '').split('/')[0];
|
|
15
|
+
const accessCode = connection.accessCode;
|
|
16
|
+
if (!host || !accessCode) throw new Error('Câmera Bambu requer IP e código de acesso LAN');
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let pending = Buffer.alloc(0);
|
|
20
|
+
let expectedSize;
|
|
21
|
+
let settled = false;
|
|
22
|
+
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
finish(new Error('A câmera Bambu não respondeu dentro do tempo limite (12s)'));
|
|
25
|
+
}, 12000);
|
|
26
|
+
|
|
27
|
+
const socket = connectTls({ host, port: 6000, rejectUnauthorized: false });
|
|
28
|
+
|
|
29
|
+
const finish = (error, frame) => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
try { socket.destroy(); } catch {}
|
|
34
|
+
if (error) reject(error);
|
|
35
|
+
else if (frame) resolve(frame);
|
|
36
|
+
else reject(new Error('A câmera Bambu encerrou sem entregar imagem'));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
socket.once('secureConnect', () => {
|
|
40
|
+
try {
|
|
41
|
+
socket.write(buildBambuCameraAuthPacket(accessCode));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
finish(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
socket.on('data', (chunk) => {
|
|
48
|
+
pending = Buffer.concat([pending, chunk]);
|
|
49
|
+
if (expectedSize === undefined && pending.length >= 16) {
|
|
50
|
+
expectedSize = pending.readUInt32LE(0);
|
|
51
|
+
if (expectedSize <= 0 || expectedSize > 12 * 1024 * 1024) {
|
|
52
|
+
finish(new Error('A câmera Bambu retornou um tamanho de quadro inválido'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (expectedSize !== undefined && pending.length >= expectedSize + 16) {
|
|
57
|
+
const frame = pending.subarray(16, expectedSize + 16);
|
|
58
|
+
if (frame[0] !== 0xff || frame[1] !== 0xd8) {
|
|
59
|
+
finish(new Error('A câmera Bambu não retornou uma imagem JPEG válida'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
finish(null, frame);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
socket.on('error', (err) => finish(err));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
2
69
|
|
|
3
70
|
export async function executeLocalPrinterAction({ action, printerId, connectionConfig, payload }) {
|
|
4
71
|
if (!connectionConfig) {
|
|
@@ -11,7 +78,7 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
11
78
|
const headers = apiKey ? { 'X-Api-Key': apiKey } : {};
|
|
12
79
|
|
|
13
80
|
// =====================================================
|
|
14
|
-
// 1. Verificação de Conexão
|
|
81
|
+
// 1. Verificação de Conexão e Telemetria
|
|
15
82
|
// =====================================================
|
|
16
83
|
if (action === 'connect') {
|
|
17
84
|
// KLIPPER / MOONRAKER
|
|
@@ -36,20 +103,40 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
36
103
|
let status = klippyState === 'ready' ? 'IDLE' : 'ERROR';
|
|
37
104
|
let message = `Klipper: ${klippyState}`;
|
|
38
105
|
let temperatures = {};
|
|
106
|
+
let progress = 0;
|
|
107
|
+
let currentLayer = 0;
|
|
108
|
+
let totalLayers = 0;
|
|
109
|
+
let filename;
|
|
110
|
+
let elapsedSeconds = 0;
|
|
39
111
|
|
|
40
112
|
try {
|
|
41
113
|
const telRes = await fetch(
|
|
42
|
-
`${baseUrl}/printer/objects/query?print_stats&extruder&heater_bed`,
|
|
43
|
-
{ headers, signal: AbortSignal.timeout(
|
|
114
|
+
`${baseUrl}/printer/objects/query?print_stats&display_status&virtual_sdcard&extruder&heater_bed`,
|
|
115
|
+
{ headers, signal: AbortSignal.timeout(6000) }
|
|
44
116
|
);
|
|
45
117
|
if (telRes.ok) {
|
|
46
118
|
const tel = await telRes.json();
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
119
|
+
const s = tel?.result?.status || {};
|
|
120
|
+
const printStats = s.print_stats || {};
|
|
121
|
+
const display = s.display_status || {};
|
|
122
|
+
const virtualSd = s.virtual_sdcard || {};
|
|
123
|
+
const ext = s.extruder || {};
|
|
124
|
+
const bed = s.heater_bed || {};
|
|
125
|
+
|
|
126
|
+
const st = String(printStats.state || '').toLowerCase();
|
|
51
127
|
if (st === 'printing') status = 'PRINTING';
|
|
52
128
|
else if (st === 'paused') status = 'PAUSED';
|
|
129
|
+
else if (st === 'complete') status = 'IDLE';
|
|
130
|
+
else if (st === 'error') status = 'ERROR';
|
|
131
|
+
|
|
132
|
+
const dispProg = Number(display.progress || 0);
|
|
133
|
+
const sdProg = Number(virtualSd.progress || 0);
|
|
134
|
+
progress = Math.round(Math.max(dispProg, sdProg) * 100);
|
|
135
|
+
|
|
136
|
+
currentLayer = Number(virtualSd.layer || printStats?.info?.current_layer || 0);
|
|
137
|
+
totalLayers = Number(virtualSd.layer_count || printStats?.info?.total_layer || 0);
|
|
138
|
+
filename = printStats.filename || undefined;
|
|
139
|
+
elapsedSeconds = Number(printStats.print_duration || printStats.total_duration || 0);
|
|
53
140
|
|
|
54
141
|
temperatures = {
|
|
55
142
|
hotend: ext.temperature,
|
|
@@ -57,7 +144,7 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
57
144
|
bed: bed.temperature,
|
|
58
145
|
bedTarget: bed.target,
|
|
59
146
|
};
|
|
60
|
-
if (
|
|
147
|
+
if (printStats.message) message = printStats.message;
|
|
61
148
|
}
|
|
62
149
|
} catch {}
|
|
63
150
|
|
|
@@ -65,6 +152,11 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
65
152
|
online: true,
|
|
66
153
|
status,
|
|
67
154
|
message,
|
|
155
|
+
progress,
|
|
156
|
+
currentLayer,
|
|
157
|
+
totalLayers,
|
|
158
|
+
filename,
|
|
159
|
+
elapsedSeconds,
|
|
68
160
|
temperatures,
|
|
69
161
|
};
|
|
70
162
|
}
|
|
@@ -116,18 +208,41 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
116
208
|
isDone = true;
|
|
117
209
|
clearTimeout(timer);
|
|
118
210
|
client.end(true);
|
|
211
|
+
|
|
119
212
|
const state = String(data.print.gcode_state || 'IDLE').toUpperCase();
|
|
120
213
|
const percent = Number(data.print.mc_percent || 0);
|
|
214
|
+
const isFinished = state === 'FINISH' || (state === 'IDLE' && percent === 100);
|
|
215
|
+
const isPrinting = (state === 'RUNNING' || state === 'PREPARE') && !isFinished;
|
|
216
|
+
const isPaused = state === 'PAUSE';
|
|
217
|
+
|
|
218
|
+
const filename =
|
|
219
|
+
typeof data.print.subtask_name === 'string' && data.print.subtask_name.trim()
|
|
220
|
+
? data.print.subtask_name.trim()
|
|
221
|
+
: typeof data.print.gcode_file === 'string' && data.print.gcode_file.trim()
|
|
222
|
+
? data.print.gcode_file.trim()
|
|
223
|
+
: undefined;
|
|
224
|
+
|
|
225
|
+
const remainingMins = Number(data.print.mc_remaining_time);
|
|
226
|
+
const remainingSeconds = Number.isFinite(remainingMins) ? Math.max(0, remainingMins * 60) : undefined;
|
|
227
|
+
|
|
121
228
|
resolve({
|
|
122
229
|
online: true,
|
|
123
|
-
status:
|
|
124
|
-
message:
|
|
230
|
+
status: isPrinting ? 'PRINTING' : (isPaused ? 'PAUSED' : 'IDLE'),
|
|
231
|
+
message: isFinished
|
|
232
|
+
? 'Impressão concluída com sucesso'
|
|
233
|
+
: `Bambu LAN conectada (${state})`,
|
|
125
234
|
progress: percent,
|
|
235
|
+
currentLayer: Number(data.print.layer_num || 0),
|
|
236
|
+
totalLayers: Number(data.print.total_layer_num || 0),
|
|
237
|
+
filename,
|
|
238
|
+
remainingSeconds,
|
|
239
|
+
jobFinished: isFinished,
|
|
126
240
|
temperatures: {
|
|
127
|
-
hotend: data.print.nozzle_temper,
|
|
128
|
-
hotendTarget: data.print.nozzle_target_temper,
|
|
129
|
-
bed: data.print.bed_temper,
|
|
130
|
-
bedTarget: data.print.bed_target_temper,
|
|
241
|
+
hotend: Number(data.print.nozzle_temper || 0),
|
|
242
|
+
hotendTarget: Number(data.print.nozzle_target_temper || 0),
|
|
243
|
+
bed: Number(data.print.bed_temper || 0),
|
|
244
|
+
bedTarget: Number(data.print.bed_target_temper || 0),
|
|
245
|
+
chamber: Number(data.print.chamber_temper || 0),
|
|
131
246
|
},
|
|
132
247
|
});
|
|
133
248
|
}
|
|
@@ -161,9 +276,14 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
// =====================================================
|
|
164
|
-
// 2. Snapshot de Câmera Local
|
|
279
|
+
// 2. Snapshot de Câmera Local (Bambu / Klipper / OctoPrint)
|
|
165
280
|
// =====================================================
|
|
166
281
|
if (action === 'camera/snapshot') {
|
|
282
|
+
if (type === 'bambu') {
|
|
283
|
+
const jpegBuffer = await captureBambuJpegTls(connectionConfig);
|
|
284
|
+
return { base64: Buffer.from(jpegBuffer).toString('base64') };
|
|
285
|
+
}
|
|
286
|
+
|
|
167
287
|
const camUrl = connectionConfig.cameraSnapshotUrl || connectionConfig.cameraUrl || `${baseUrl}/webcam/?action=snapshot`;
|
|
168
288
|
const camRes = await fetch(camUrl, { signal: AbortSignal.timeout(10000) });
|
|
169
289
|
if (!camRes.ok) {
|