diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 979fcd5..638fd20 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -1,6 +1,7 @@ from __future__ import annotations import os import subprocess +from subprocess import CompletedProcess import tempfile import shlex import shutil @@ -11,6 +12,7 @@ import bpy import numpy as np from mathutils import Euler + from .bpy_ns_types import NfIntProperty, NfStringProperty, NfEnumProperty, NfBoolProperty, notify_tree_changed from ...registry import register_class @@ -19,6 +21,7 @@ from ..slice import Pather, slice_at_range, SlicePaths from ...infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup from ...infra.blender.blender_bridge import get_all_children from ...infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input +from ...infra.prusaslicer_profiles import ConfigWriter from ...infra.prusaslicer_profiles.cache import LocalCache from ...infra.prusaslicer_profiles.models import Profile from ...infra.networking.airtable_backend import AirtableRecord @@ -968,7 +971,6 @@ class SLICENODE_PrinterList(SliceNode): @register_class class SLICENODE_GcodeToFile(ExecSliceNode): bl_label = "Gcode To File" - button_label = "Generate File" def init(self, context): @@ -976,17 +978,49 @@ class SLICENODE_GcodeToFile(ExecSliceNode): self.add_input("SLICESOCK_Gcode") self.add_input("SLICESOCK_FilePath", "Filename") + def draw_buttons(self, context, layout): + super().draw_buttons(context, layout) + def evaluate(self, operator=None): + from ...infra.networking.bgcode import convert_to_bgcode + from pathlib import Path + gcode: str = resolve_input(self.inputs.get("Gcode")) filename: str = resolve_input(self.inputs.get("Filename")) + if not gcode: + if operator: operator.report({'ERROR'}, "No Gcode to save") + return 'error' + if not filename: + if operator: operator.report({'ERROR'}, "No Filename provided") + return 'error' - filepath = bpy.path.abspath(filename) - os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True) + # User's requested logic: .split('.')[0] + filepath_abs = bpy.path.abspath(filename) + dirname = os.path.dirname(filepath_abs) + basename = os.path.basename(filepath_abs) + name_part = basename.split('.')[0] + os.makedirs(dirname, exist_ok=True) - with open(filepath, "w", encoding="utf-8", newline="\n") as f: - f.write(gcode) + # Detect binary trigger in gcode comments + # Looking for "; binary_gcode = 1" + is_binary = "; binary_gcode = 1" in gcode - return 'ok' + try: + if is_binary: + filepath = os.path.join(dirname, name_part + ".bgcode") + content = convert_to_bgcode(gcode, name_part, True) + with open(filepath, "wb") as f: + f.write(content) + if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}") + else: + filepath = os.path.join(dirname, name_part + ".gcode") + with open(filepath, "w", encoding="utf-8", newline="\n") as f: + f.write(gcode) + if operator: operator.report({'INFO'}, f"G-code saved to {filepath}") + return 'ok' + except Exception as e: + if operator: operator.report({'ERROR'}, str(e)) + return 'error' @register_class class SLICENODE_GcodeToTextBlock(ExecSliceNode): @@ -1147,12 +1181,19 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode): return gcode, filename, fields def evaluate(self, operator=None): + from ...infra.networking.bgcode import convert_to_bgcode import tempfile import os from pathlib import Path gcode, filename, fields = self._prepare_submission(operator) if not gcode: return 'error' + + # Detect binary trigger in gcode comments + is_binary = "; binary_gcode = 1" in gcode + + name_part = filename.split('.')[0] + filename = name_part + ".bgcode" if is_binary else ".gcode" token_path = getattr(self, self._token_prop_name, "") token = "" @@ -1172,10 +1213,16 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode): temp_dir = tempfile.TemporaryDirectory() temp_path = Path(temp_dir.name) / filename - with open(temp_path, 'w', encoding='utf-8') as f: - f.write(gcode) try: + if is_binary: + content = convert_to_bgcode(gcode, name_part, True) + with open(temp_path, "wb") as f: + f.write(content) + else: + with open(temp_path, 'w', encoding='utf-8') as f: + f.write(gcode) + record_id = self._submit_to_db(token, fields, temp_path) if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})") return 'ok' @@ -1340,16 +1387,18 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): # Prepare overrides for generate_conf_writer # The format expected by generate_conf_writer is overrides={'key': {'value': 'val'}} writer_overrides = {k: {'value': v} for k, v in overrides.items()} - writer_overrides['binary_gcode'] = {'value': '0'} # Generate the ConfigWriter - conf_writer = cache.generate_conf_writer( + conf_writer: ConfigWriter = cache.generate_conf_writer( printer_id, filament_ids, print_id, overrides=writer_overrides ) + binary_gcode_value = conf_writer.config_dict['binary_gcode'] + conf_writer.config_dict['binary_gcode'] = '0' + # Prepare 3mf path temp_dir = Path(tempfile.gettempdir()) output_path = temp_dir / f"tmp.3mf" @@ -1397,6 +1446,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): if gcode_path.exists(): with open(gcode_path, 'r', encoding='utf-8') as f: self.gcode_result = f.read() + self.gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n" output_path.unlink() diff --git a/doc/NODES.md b/doc/NODES.md index c77fcc6..9eab4a3 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -74,6 +74,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Header**: Toggles the generation of standard NodeSlicer G-code headers and units setup. - **Load Gcode**: Reads a G-code file from disk. - **Gcode to File**: Writes G-code string to a file on disk. + - **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression. - **Baserow Submit**: Sends G-code and metadata to a Baserow database. - **Host**: Baserow host URL. - **Token File**: Path to a file containing the Baserow API token. diff --git a/gcode_snippets/mk3s_header.bgcode b/gcode_snippets/mk3s_header.bgcode new file mode 100644 index 0000000..d4aeec0 Binary files /dev/null and b/gcode_snippets/mk3s_header.bgcode differ diff --git a/infra/bin/bgcode b/infra/bin/bgcode new file mode 100755 index 0000000..17efe52 Binary files /dev/null and b/infra/bin/bgcode differ diff --git a/infra/networking/bgcode.py b/infra/networking/bgcode.py new file mode 100644 index 0000000..463e1bd --- /dev/null +++ b/infra/networking/bgcode.py @@ -0,0 +1,41 @@ +import os +import subprocess +import tempfile +import shutil +from pathlib import Path + +def convert_to_bgcode(gcode: str, name_part: str, compress: bool = True) -> bytes: + """ + Converts a G-code string to binary BGCode using the internal bgcode tool. + Returns the binary content of the converted file. + """ + addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + bgcode_bin = os.path.join(addon_dir, "infra", "bin", "bgcode") + + if not os.path.exists(bgcode_bin): + raise FileNotFoundError(f"bgcode binary not found at {bgcode_bin}") + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_gcode = Path(tmpdir) / (name_part + ".gcode") + tmp_gcode.write_text(gcode, encoding="utf-8") + + # Run conversion: bgcode + cmd = [bgcode_bin, str(tmp_gcode)] + if compress: + # Recommended settings for true binary bgcode + cmd.extend([ + "--slicer_metadata_compression=1", + "--gcode_compression=3", + "--gcode_encoding=2" + ]) + + res = subprocess.run(cmd, capture_output=True, text=True) + if res.returncode != 0: + err_msg = res.stderr.strip() or res.stdout.strip() or "Unknown error" + raise RuntimeError(f"bgcode conversion failed: {err_msg}") + + tmp_bgcode = tmp_gcode.with_suffix(".bgcode") + if tmp_bgcode.exists(): + return tmp_bgcode.read_bytes() + else: + raise FileNotFoundError("Failed to find generated .bgcode file") diff --git a/test.bgcode b/test.bgcode new file mode 100644 index 0000000..e69de29 diff --git a/test.gcode b/test.gcode new file mode 100644 index 0000000..a58dc90 --- /dev/null +++ b/test.gcode @@ -0,0 +1,4 @@ +; printer_model = MK3S +; printer_notes = +; total filament used [g] = 0 +G1 X10