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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-foundry",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Project Foundry CLI & Local Node Daemon Manager",
5
5
  "bin": {
6
6
  "project-foundry": "bin/foundry.js",
@@ -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(5000) }
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 stats = tel?.result?.status?.print_stats || {};
48
- const ext = tel?.result?.status?.extruder || {};
49
- const bed = tel?.result?.status?.heater_bed || {};
50
- const st = String(stats.state || '').toLowerCase();
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 (stats.message) message = stats.message;
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: state === 'RUNNING' || state === 'PREPARE' ? 'PRINTING' : (state === 'PAUSE' ? 'PAUSED' : 'IDLE'),
124
- message: `Bambu LAN conectada (${state})`,
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) {