project-foundry 1.0.6 → 1.0.8
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 +218 -0
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import mqtt from 'mqtt';
|
|
2
2
|
import { connect as connectTls } from 'node:tls';
|
|
3
3
|
|
|
4
|
+
// Cache em memória das últimas telemetrias da Bambu (serialNumber -> { timestamp, print })
|
|
5
|
+
const latestBambuReports = new Map();
|
|
6
|
+
|
|
4
7
|
function buildBambuCameraAuthPacket(accessCode) {
|
|
5
8
|
const packet = Buffer.alloc(80);
|
|
6
9
|
packet.writeUInt32LE(0x40, 0);
|
|
@@ -67,6 +70,45 @@ function captureBambuJpegTls(connection) {
|
|
|
67
70
|
});
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
function bambuColor(value) {
|
|
74
|
+
if (typeof value !== 'string') return undefined;
|
|
75
|
+
const normalized = value.replace(/^#/, '').slice(0, 6);
|
|
76
|
+
return /^[0-9a-f]{6}$/i.test(normalized) ? `#${normalized.toUpperCase()}` : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseBambuAmsState(print) {
|
|
80
|
+
const ams = print?.ams || {};
|
|
81
|
+
const units = Array.isArray(ams.ams) ? ams.ams : [];
|
|
82
|
+
const slots = units.flatMap((unit, unitIndex) => {
|
|
83
|
+
const amsId = (unit.id !== undefined && !Number.isNaN(Number(unit.id))) ? Number(unit.id) : unitIndex;
|
|
84
|
+
const trays = Array.isArray(unit.tray) ? unit.tray : [];
|
|
85
|
+
return trays.map((tray, slotIndex) => {
|
|
86
|
+
const slotId = (tray.id !== undefined && !Number.isNaN(Number(tray.id))) ? Number(tray.id) : slotIndex;
|
|
87
|
+
const trayId = amsId * 4 + slotId;
|
|
88
|
+
const filamentType = typeof tray.tray_type === 'string' && tray.tray_type ? tray.tray_type : undefined;
|
|
89
|
+
const hasMetadata = Boolean(filamentType || tray.tray_info_idx || tray.setting_id);
|
|
90
|
+
const remaining = Number(tray.remain);
|
|
91
|
+
return {
|
|
92
|
+
trayId,
|
|
93
|
+
amsId,
|
|
94
|
+
slotId,
|
|
95
|
+
label: `AMS ${String.fromCharCode(65 + amsId)} · slot ${slotId + 1}`,
|
|
96
|
+
filamentType,
|
|
97
|
+
color: bambuColor(tray.tray_color),
|
|
98
|
+
remainingPercent: Number.isFinite(remaining) && remaining >= 0 ? remaining : undefined,
|
|
99
|
+
empty: !hasMetadata,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
const currentTrayId = Number(ams.tray_now);
|
|
104
|
+
return {
|
|
105
|
+
available: slots.length > 0,
|
|
106
|
+
currentTrayId: Number.isFinite(currentTrayId) && currentTrayId >= 0 && currentTrayId < 255 ? currentTrayId : undefined,
|
|
107
|
+
slots,
|
|
108
|
+
readAt: new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
70
112
|
export async function executeLocalPrinterAction({ action, printerId, connectionConfig, payload }) {
|
|
71
113
|
if (!connectionConfig) {
|
|
72
114
|
throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor');
|
|
@@ -205,6 +247,11 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
205
247
|
try {
|
|
206
248
|
const data = JSON.parse(payloadBuffer.toString('utf8'));
|
|
207
249
|
if (data.print) {
|
|
250
|
+
latestBambuReports.set(serialNumber, {
|
|
251
|
+
timestamp: Date.now(),
|
|
252
|
+
print: data.print,
|
|
253
|
+
});
|
|
254
|
+
|
|
208
255
|
isDone = true;
|
|
209
256
|
clearTimeout(timer);
|
|
210
257
|
client.end(true);
|
|
@@ -312,6 +359,45 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
312
359
|
const res = await fetch(`${baseUrl}${endpoint}`, { method: 'POST', headers, signal: AbortSignal.timeout(8000) });
|
|
313
360
|
return { success: res.ok };
|
|
314
361
|
}
|
|
362
|
+
|
|
363
|
+
if (type === 'bambu') {
|
|
364
|
+
const host = connectionConfig.host;
|
|
365
|
+
const port = connectionConfig.mqttPort || 8883;
|
|
366
|
+
const username = connectionConfig.username || 'bblp';
|
|
367
|
+
const password = connectionConfig.accessCode;
|
|
368
|
+
const serialNumber = connectionConfig.serialNumber;
|
|
369
|
+
const bCmd = cmd === 'pause' ? 'pause' : cmd === 'resume' ? 'resume' : cmd === 'cancel' ? 'stop' : null;
|
|
370
|
+
|
|
371
|
+
if (!bCmd) throw new Error(`Comando "${cmd}" não suportado para Bambu`);
|
|
372
|
+
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
const client = mqtt.connect(`mqtts://${host}:${port}`, {
|
|
375
|
+
username, password, rejectUnauthorized: false, connectTimeout: 6000, clean: true,
|
|
376
|
+
});
|
|
377
|
+
const timer = setTimeout(() => {
|
|
378
|
+
try { client.end(true); } catch {}
|
|
379
|
+
reject(new Error(`Timeout ao enviar comando "${cmd}" para a Bambu`));
|
|
380
|
+
}, 7000);
|
|
381
|
+
|
|
382
|
+
client.on('connect', () => {
|
|
383
|
+
client.publish(
|
|
384
|
+
`device/${serialNumber}/request`,
|
|
385
|
+
JSON.stringify({ print: { sequence_id: String(Date.now()), command: bCmd, param: '' } }),
|
|
386
|
+
() => {
|
|
387
|
+
clearTimeout(timer);
|
|
388
|
+
client.end(true);
|
|
389
|
+
resolve({ success: true });
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
client.on('error', (err) => {
|
|
395
|
+
clearTimeout(timer);
|
|
396
|
+
try { client.end(true); } catch {}
|
|
397
|
+
reject(err);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
315
401
|
}
|
|
316
402
|
|
|
317
403
|
// =====================================================
|
|
@@ -331,6 +417,138 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
331
417
|
return { success: res.ok };
|
|
332
418
|
}
|
|
333
419
|
}
|
|
420
|
+
|
|
421
|
+
if (type === 'bambu') {
|
|
422
|
+
const host = connectionConfig.host;
|
|
423
|
+
const port = connectionConfig.mqttPort || 8883;
|
|
424
|
+
const username = connectionConfig.username || 'bblp';
|
|
425
|
+
const password = connectionConfig.accessCode;
|
|
426
|
+
const serialNumber = connectionConfig.serialNumber;
|
|
427
|
+
const bCmd = jobCmd === 'pause' ? 'pause' : jobCmd === 'resume' ? 'resume' : jobCmd === 'cancel' ? 'stop' : null;
|
|
428
|
+
|
|
429
|
+
if (bCmd) {
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const client = mqtt.connect(`mqtts://${host}:${port}`, {
|
|
432
|
+
username, password, rejectUnauthorized: false, connectTimeout: 6000, clean: true,
|
|
433
|
+
});
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
try { client.end(true); } catch {}
|
|
436
|
+
reject(new Error(`Timeout ao enviar comando "${jobCmd}" para a Bambu`));
|
|
437
|
+
}, 7000);
|
|
438
|
+
|
|
439
|
+
client.on('connect', () => {
|
|
440
|
+
client.publish(
|
|
441
|
+
`device/${serialNumber}/request`,
|
|
442
|
+
JSON.stringify({ print: { sequence_id: String(Date.now()), command: bCmd, param: '' } }),
|
|
443
|
+
() => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
client.end(true);
|
|
446
|
+
resolve({ success: true });
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
client.on('error', (err) => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
try { client.end(true); } catch {}
|
|
454
|
+
reject(err);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// =====================================================
|
|
462
|
+
// 5. Bambu AMS State (Leitura de slots e filamentos)
|
|
463
|
+
// =====================================================
|
|
464
|
+
if (action === 'ams') {
|
|
465
|
+
if (type !== 'bambu') {
|
|
466
|
+
return {
|
|
467
|
+
available: false,
|
|
468
|
+
slots: [],
|
|
469
|
+
readAt: new Date().toISOString(),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const host = connectionConfig.host;
|
|
474
|
+
const port = connectionConfig.mqttPort || 8883;
|
|
475
|
+
const username = connectionConfig.username || 'bblp';
|
|
476
|
+
const password = connectionConfig.accessCode;
|
|
477
|
+
const serialNumber = connectionConfig.serialNumber;
|
|
478
|
+
|
|
479
|
+
if (!host || !password || !serialNumber) {
|
|
480
|
+
throw new Error('Configuração da Bambu incompleta: host, accessCode e serialNumber são obrigatórios');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Cache recente (< 30s)
|
|
484
|
+
const cached = latestBambuReports.get(serialNumber);
|
|
485
|
+
if (cached && (Date.now() - cached.timestamp < 30000) && cached.print?.ams) {
|
|
486
|
+
return parseBambuAmsState(cached.print);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Consulta MQTT na LAN local
|
|
490
|
+
return new Promise((resolve) => {
|
|
491
|
+
let isDone = false;
|
|
492
|
+
const timer = setTimeout(() => {
|
|
493
|
+
if (isDone) return;
|
|
494
|
+
isDone = true;
|
|
495
|
+
try { client.end(true); } catch {}
|
|
496
|
+
if (cached?.print?.ams) {
|
|
497
|
+
resolve(parseBambuAmsState(cached.print));
|
|
498
|
+
} else {
|
|
499
|
+
resolve({ available: false, slots: [], readAt: new Date().toISOString() });
|
|
500
|
+
}
|
|
501
|
+
}, 7000);
|
|
502
|
+
|
|
503
|
+
const client = mqtt.connect(`mqtts://${host}:${port}`, {
|
|
504
|
+
clientId: `foundry_edge_ams_${Date.now()}`,
|
|
505
|
+
username,
|
|
506
|
+
password,
|
|
507
|
+
rejectUnauthorized: false,
|
|
508
|
+
connectTimeout: 6000,
|
|
509
|
+
clean: true,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
client.on('connect', () => {
|
|
513
|
+
client.subscribe(`device/${serialNumber}/report`, () => {
|
|
514
|
+
client.publish(
|
|
515
|
+
`device/${serialNumber}/request`,
|
|
516
|
+
JSON.stringify({ pushing: { sequence_id: '1', command: 'pushall', version: 1, push_target: 1 } })
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
client.on('message', (_topic, payloadBuffer) => {
|
|
522
|
+
if (isDone) return;
|
|
523
|
+
try {
|
|
524
|
+
const data = JSON.parse(payloadBuffer.toString('utf8'));
|
|
525
|
+
if (data.print) {
|
|
526
|
+
latestBambuReports.set(serialNumber, {
|
|
527
|
+
timestamp: Date.now(),
|
|
528
|
+
print: data.print,
|
|
529
|
+
});
|
|
530
|
+
if (data.print.ams) {
|
|
531
|
+
isDone = true;
|
|
532
|
+
clearTimeout(timer);
|
|
533
|
+
client.end(true);
|
|
534
|
+
resolve(parseBambuAmsState(data.print));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch {}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
client.on('error', () => {
|
|
541
|
+
if (isDone) return;
|
|
542
|
+
isDone = true;
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
try { client.end(true); } catch {}
|
|
545
|
+
if (cached?.print?.ams) {
|
|
546
|
+
resolve(parseBambuAmsState(cached.print));
|
|
547
|
+
} else {
|
|
548
|
+
resolve({ available: false, slots: [], readAt: new Date().toISOString() });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
334
552
|
}
|
|
335
553
|
|
|
336
554
|
throw new Error(`Ação "${action}" não suportada`);
|