project-foundry 1.0.8 → 1.0.9
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 +2 -1
- package/src/local-printer-executor.js +164 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-foundry",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Project Foundry CLI & Local Node Daemon Manager",
|
|
5
5
|
"bin": {
|
|
6
6
|
"project-foundry": "bin/foundry.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@clack/prompts": "^0.9.1",
|
|
20
|
+
"basic-ftp": "^6.2.4",
|
|
20
21
|
"commander": "^12.1.0",
|
|
21
22
|
"mqtt": "^5.16.1",
|
|
22
23
|
"open": "^10.1.0",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import mqtt from 'mqtt';
|
|
2
2
|
import { connect as connectTls } from 'node:tls';
|
|
3
|
+
import { Client as FtpClient } from 'basic-ftp';
|
|
4
|
+
import { readConfig } from './config.js';
|
|
3
5
|
|
|
4
6
|
// Cache em memória das últimas telemetrias da Bambu (serialNumber -> { timestamp, print })
|
|
5
7
|
const latestBambuReports = new Map();
|
|
@@ -551,5 +553,167 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
551
553
|
});
|
|
552
554
|
}
|
|
553
555
|
|
|
556
|
+
// =====================================================
|
|
557
|
+
// 6. Início e Envio de Trabalho (Job Start via LAN)
|
|
558
|
+
// =====================================================
|
|
559
|
+
if (action === 'job/start') {
|
|
560
|
+
const cfg = readConfig();
|
|
561
|
+
const serverUrl = (cfg?.serverUrl || '').replace(/\/$/, '');
|
|
562
|
+
const pat = cfg?.apiKey;
|
|
563
|
+
|
|
564
|
+
if (!payload?.fileId) {
|
|
565
|
+
throw new Error('ID do arquivo não fornecido para início do trabalho');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 1. Baixa o arquivo da nuvem usando o PAT
|
|
569
|
+
const downloadUrl = `${serverUrl}/api/v1/files/${payload.fileId}/download`;
|
|
570
|
+
let fileRes;
|
|
571
|
+
try {
|
|
572
|
+
fileRes = await fetch(downloadUrl, {
|
|
573
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
574
|
+
signal: AbortSignal.timeout(60000),
|
|
575
|
+
});
|
|
576
|
+
} catch (err) {
|
|
577
|
+
throw new Error(`Falha ao baixar arquivo da nuvem para o nó local: ${err.message}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!fileRes.ok) {
|
|
581
|
+
throw new Error(`Nuvem retornou HTTP ${fileRes.status} ao baixar arquivo ${payload.fileId}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const fileBuffer = Buffer.from(await fileRes.arrayBuffer());
|
|
585
|
+
|
|
586
|
+
// KLIPPER / MOONRAKER (ex: K1 Max)
|
|
587
|
+
if (type === 'klipper') {
|
|
588
|
+
const remoteName = payload.remoteName || payload.originalName || 'print.gcode';
|
|
589
|
+
const form = new FormData();
|
|
590
|
+
form.append('file', new Blob([fileBuffer], { type: 'text/x-gcode' }), remoteName);
|
|
591
|
+
form.append('root', 'gcodes');
|
|
592
|
+
form.append('print', 'true');
|
|
593
|
+
|
|
594
|
+
let uploadRes;
|
|
595
|
+
try {
|
|
596
|
+
uploadRes = await fetch(`${baseUrl}/server/files/upload`, {
|
|
597
|
+
method: 'POST',
|
|
598
|
+
headers,
|
|
599
|
+
body: form,
|
|
600
|
+
signal: AbortSignal.timeout(120000),
|
|
601
|
+
});
|
|
602
|
+
} catch (err) {
|
|
603
|
+
throw new Error(`Não foi possível enviar o arquivo para a K1 Max em ${baseUrl}: ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!uploadRes.ok) {
|
|
607
|
+
const detail = await uploadRes.text().catch(() => '');
|
|
608
|
+
throw new Error(`Klipper recusou o início (${uploadRes.status}): ${detail.slice(0, 200)}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
online: true,
|
|
613
|
+
status: 'PRINTING',
|
|
614
|
+
message: 'Impressão iniciada com sucesso via Klipper',
|
|
615
|
+
filename: remoteName,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// BAMBU LAB (P1S, etc.)
|
|
620
|
+
if (type === 'bambu') {
|
|
621
|
+
const host = connectionConfig.host;
|
|
622
|
+
const port = connectionConfig.ftpPort || 990;
|
|
623
|
+
const accessCode = connectionConfig.accessCode;
|
|
624
|
+
const serialNumber = connectionConfig.serialNumber;
|
|
625
|
+
const remoteName = payload.remoteName || payload.originalName || 'print.gcode.3mf';
|
|
626
|
+
|
|
627
|
+
if (!host || !accessCode || !serialNumber) {
|
|
628
|
+
throw new Error('Configuração da Bambu incompleta para envio');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Envia via FTPS
|
|
632
|
+
const ftpClient = new FtpClient(60000);
|
|
633
|
+
try {
|
|
634
|
+
await ftpClient.access({
|
|
635
|
+
host,
|
|
636
|
+
port,
|
|
637
|
+
user: 'bblp',
|
|
638
|
+
password: accessCode,
|
|
639
|
+
secure: 'implicit',
|
|
640
|
+
secureOptions: { rejectUnauthorized: false },
|
|
641
|
+
});
|
|
642
|
+
await ftpClient.ensureDir('/');
|
|
643
|
+
const { Readable } = await import('node:stream');
|
|
644
|
+
await ftpClient.uploadFrom(Readable.from(fileBuffer), remoteName);
|
|
645
|
+
} catch (err) {
|
|
646
|
+
throw new Error(`Falha no FTPS local da Bambu: ${err.message}`);
|
|
647
|
+
} finally {
|
|
648
|
+
ftpClient.close();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Dispara comando MQTT
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const client = mqtt.connect(`mqtts://${host}:${connectionConfig.mqttPort || 8883}`, {
|
|
654
|
+
username: connectionConfig.username || 'bblp',
|
|
655
|
+
password: accessCode,
|
|
656
|
+
rejectUnauthorized: false,
|
|
657
|
+
connectTimeout: 8000,
|
|
658
|
+
clean: true,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const timer = setTimeout(() => {
|
|
662
|
+
try { client.end(true); } catch {}
|
|
663
|
+
reject(new Error('Timeout aguardando confirmação MQTT da Bambu'));
|
|
664
|
+
}, 12000);
|
|
665
|
+
|
|
666
|
+
const isP1orA1 = Boolean(
|
|
667
|
+
serialNumber.startsWith('01P') || serialNumber.startsWith('030') || serialNumber.startsWith('039')
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const printCmd = {
|
|
671
|
+
print: {
|
|
672
|
+
sequence_id: String(Date.now()),
|
|
673
|
+
command: 'project_file',
|
|
674
|
+
param: `Metadata/plate_${payload.plateNumber || 1}.gcode`,
|
|
675
|
+
project_id: '0',
|
|
676
|
+
profile_id: '0',
|
|
677
|
+
task_id: '0',
|
|
678
|
+
subtask_id: '0',
|
|
679
|
+
subtask_name: remoteName,
|
|
680
|
+
file: remoteName,
|
|
681
|
+
url: `ftp:///${remoteName}`,
|
|
682
|
+
md5: '',
|
|
683
|
+
timelapse: Boolean(payload.printSettings?.timelapse),
|
|
684
|
+
bed_type: isP1orA1 ? 'textured_plate' : 'auto',
|
|
685
|
+
bed_levelling: payload.printSettings?.bedLeveling !== false,
|
|
686
|
+
flow_cali: isP1orA1 ? false : Boolean(payload.printSettings?.flowCalibration),
|
|
687
|
+
vibration_cali: payload.printSettings?.vibrationCalibration !== false,
|
|
688
|
+
layer_inspect: !isP1orA1,
|
|
689
|
+
use_ams: Boolean(payload.printSettings?.useAms),
|
|
690
|
+
ams_mapping: Array.isArray(payload.printSettings?.amsMapping) ? payload.printSettings.amsMapping : [0],
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
client.on('connect', () => {
|
|
695
|
+
client.publish(`device/${serialNumber}/request`, JSON.stringify(printCmd), () => {
|
|
696
|
+
clearTimeout(timer);
|
|
697
|
+
client.end(true);
|
|
698
|
+
resolve({
|
|
699
|
+
online: true,
|
|
700
|
+
status: 'PRINTING',
|
|
701
|
+
message: 'Impressão iniciada via Bambu LAN',
|
|
702
|
+
filename: remoteName,
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
client.on('error', (err) => {
|
|
708
|
+
clearTimeout(timer);
|
|
709
|
+
try { client.end(true); } catch {}
|
|
710
|
+
reject(err);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
throw new Error(`Protocolo "${type}" não suporta envio automático de arquivos`);
|
|
716
|
+
}
|
|
717
|
+
|
|
554
718
|
throw new Error(`Ação "${action}" não suportada`);
|
|
555
719
|
}
|