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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-foundry",
3
- "version": "1.0.8",
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
  }