nodeslicer/core/nodeslicer/nodes/system.py
2026-10-14 13:54:04 +03:00

271 lines
9.7 KiB
Python

from __future__ import annotations
from typing import Any
from pathlib import Path
import os
import bpy
import tempfile
from ..bpy_ns_types import NfBoolProperty, NfStringProperty
from ....registry import register_class
from .base import ExecSliceNode, SliceNodeJoiner
@register_class
class SLICENODE_PrinterList(ExecSliceNode):
bl_label = "Printer List"
button_label = "Start Prints"
def _refresh(self):
import json
if not self.printers_json_path:
return
abs_path = bpy.path.abspath(self.printers_json_path)
if not os.path.exists(abs_path):
return
try:
with open(abs_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception:
return
printers = data.get("printers", [])
printer_names = set()
for p_data in printers:
name = p_data.get("name", "Unknown")
printer_names.add(name)
sock = self.inputs.get(name)
if not sock:
self.add_input("NodeSocketString", name)
# Remove stale input sockets
for sock in list(self.inputs):
if sock.name not in printer_names:
self.inputs.remove(sock)
# Remove stale output sockets (except Execution Link)
for sock in list(self.outputs):
if sock.name != "Execution Link":
self.outputs.remove(sock)
def update_config(self, context):
self._refresh()
printers_json_path: str = NfStringProperty(name="Config File", default="", subtype='FILE_PATH', update=update_config)
def init(self, context):
self._init_exec()
def draw_buttons(self, context, layout):
super().draw_buttons(context, layout)
layout.prop(self, "printers_json_path")
def evaluate(self, operator=None):
import json
from ....infra.networking.printer_backends import (
PrusaLinkBackend,
CrealityBackend,
MoonrakerBackend
)
from ....infra.networking.gcode_utils import prepare_gcode_file
self._refresh()
if not self.printers_json_path:
if operator: operator.report({'ERROR'}, "No Config File path provided")
return 'error'
abs_path = bpy.path.abspath(self.printers_json_path)
if not os.path.exists(abs_path):
if operator: operator.report({'ERROR'}, f"Config file not found: {abs_path}")
return 'error'
try:
with open(abs_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e:
if operator: operator.report({'ERROR'}, f"Failed to read config file: {str(e)}")
return 'error'
printers = data.get("printers", [])
printer_configs = {p.get("name", "Unknown"): p for p in printers}
any_printed = False
for sock in self.inputs:
if sock.name not in printer_configs:
continue
gcode = self.resolve(sock.name)
if not gcode:
continue
from ....infra.blender.list_utils import is_list_data
if is_list_data(gcode):
if operator: operator.report({'ERROR'}, f"Printer List does not accept a list of Gcode on socket {sock.name}")
continue
p_data = printer_configs[sock.name]
host = str(p_data.get("ip", "localhost"))
port = int(p_data.get("port", 80))
prefix = str(p_data.get("prefix", ""))
password = str(p_data.get("password", ""))
host_type = str(p_data.get("host_type", "PRUSALINK")).upper()
# Instantiate backend
backend = None
if host_type == 'PRUSALINK':
backend = PrusaLinkBackend(host, port, prefix, api_key=password)
elif host_type == 'CREALITY':
backend = CrealityBackend(host, port, prefix)
elif host_type == 'MOONRAKER':
backend = MoonrakerBackend(host, port, prefix)
if not backend:
if operator: operator.report({'ERROR'}, f"Unsupported host type {host_type} for printer {sock.name}")
continue
# Write to temp file to send
name_part = f"NodeSlicer_{sock.name}".replace(" ", "_")
try:
file_bytes, filename = prepare_gcode_file(gcode, name_part)
except Exception as e:
if operator: operator.report({'ERROR'}, f"Failed to prepare G-code for {sock.name}: {str(e)}")
continue
suffix = Path(filename).suffix
with tempfile.NamedTemporaryFile(mode='wb', suffix=suffix, delete=False) as tf:
tf.write(file_bytes)
temp_path = Path(tf.name)
try:
backend.start_print(temp_path, filename)
if operator: operator.report({'INFO'}, f"Print started on {sock.name}")
any_printed = True
except Exception as e:
if operator: operator.report({'ERROR'}, f"Failed to start print on {sock.name}: {str(e)}")
finally:
if temp_path.exists():
temp_path.unlink()
if not any_printed:
if operator: operator.report({'WARNING'}, "No Gcode was sent to any printer")
return 'no gcode'
return 'ok'
@register_class
class SLICENODE_JoinExecution(SliceNodeJoiner):
bl_label = "Join Execution"
socket_type = "SLICESOCK_ExecutionLink"
def evaluate_output(self, socket_name: str, **_kwargs) -> list[ExecSliceNode]:
from ....infra.blender.blender_slice_nodes_generic import resolve_input
nodes: list[ExecSliceNode] = []
for input in self.inputs:
res = resolve_input(input)
if isinstance(res, ExecSliceNode):
nodes.append(res)
elif isinstance(res, list):
nodes.extend(res)
return nodes
@register_class
class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
bl_label = "Executor"
button_label = "Execute All"
socket_type = "SLICESOCK_ExecutionLink"
auto_refresh: bool = NfBoolProperty(name="Auto-refresh", default=False)
log: bool = NfBoolProperty(name="Monitor Performance", default=False, description="Monitor performance statistics to open text editor")
def init(self, context):
self._init_exec()
self.add_multi_socket_group(self.socket_type, name="Links")
def draw_buttons(self, context, layout):
super().draw_buttons(context, layout)
layout.prop(self, "auto_refresh")
layout.prop(self, "log")
def evaluate(self, operator=None):
from ....infra.blender.blender_slice_nodes_generic import (
execution_cache,
profiler_context,
profile_node_execution,
show_in_text_editor,
resolve_input
)
profiling_enabled = getattr(self, "log", False)
def _run_jobs():
for i, job in enumerate(self.inputs):
# Resolve the input to get the connected node(s)
res = resolve_input(job)
if res is None:
continue
# Ensure we have a list of jobs to process (JoinExecution returns a list)
jobs_to_run = res if isinstance(res, list) else [res]
for upstream_node in jobs_to_run:
if not hasattr(upstream_node, "evaluate"):
continue
if operator:
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
try:
with execution_cache():
with profile_node_execution(upstream_node):
from ....infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
wrapped_op = NodeErrorReportingProxy(operator, upstream_node)
exec_res = upstream_node.evaluate(operator=wrapped_op)
if exec_res != 'ok' and exec_res is not None:
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
except Exception as e:
from ....infra.blender.blender_slice_nodes_generic import attach_node_failure_info
attach_node_failure_info(upstream_node, e)
if operator: operator.report({'ERROR'}, str(e))
return 'error'
return 'ok'
if profiling_enabled:
with profiler_context() as profiler:
result = _run_jobs()
table_str = profiler.format_table()
show_in_text_editor("Performance", table_str)
return result
else:
return _run_jobs()
@register_class
class SLICENODE_JoinSettings(SliceNodeJoiner):
bl_label = "Join Settings"
socket_type = "SLICESOCK_Settings"
output_socket_type = "SLICESOCK_SettingsRo"
def evaluate_output(self, socket_name: str, **_kwargs) -> dict[str, str]:
from ....infra.blender.blender_slice_nodes_generic import resolve_input
merged: dict[str, str] = {}
for input in self.inputs:
val = resolve_input(input)
if isinstance(val, dict):
merged.update(val)
return merged
@register_class
class SLICENODE_Test(ExecSliceNode):
bl_label = "Test"
button_label = "Test"
def init(self, context):
self._init_exec()
self.add_input("NodeSocketString")
def evaluate(self, operator=None):
_ = self.resolve("Gcode")
return 'ok'