network print
This commit is contained in:
parent
fbdc1a9eb8
commit
c37f93d91f
5 changed files with 339 additions and 7 deletions
|
|
@ -5,7 +5,7 @@ from typing import Any, TypeAlias
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Euler
|
from mathutils import Euler
|
||||||
from bpy.props import StringProperty, FloatProperty, IntProperty
|
from bpy.props import StringProperty, FloatProperty, IntProperty, EnumProperty
|
||||||
|
|
||||||
from ...registry import register_class
|
from ...registry import register_class
|
||||||
from .. import slice
|
from .. import slice
|
||||||
|
|
@ -61,8 +61,54 @@ class SLICENODE_InputObject(InputNode):
|
||||||
input_type = "Object"
|
input_type = "Object"
|
||||||
bl_icon = "OBJECT_DATA"
|
bl_icon = "OBJECT_DATA"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputPrinter(SliceNode):
|
||||||
|
bl_label = "Printer"
|
||||||
|
bl_icon = "OUTPUT"
|
||||||
|
|
||||||
|
def update_label(self, context):
|
||||||
|
self.label = self.printer_label
|
||||||
|
|
||||||
|
printer_label: StringProperty(name="Label", default="Printer", update=update_label) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
host_type: EnumProperty(
|
||||||
|
name="Type",
|
||||||
|
items=[
|
||||||
|
('PRUSALINK', "PrusaLink", "PrusaLink API"),
|
||||||
|
('CREALITY', "Creality", "Creality Cloud API"),
|
||||||
|
('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"),
|
||||||
|
],
|
||||||
|
default='PRUSALINK'
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("SLICESOCK_Printer")
|
||||||
|
self.add_input("SLICESOCK_String", "Host").value = "localhost"
|
||||||
|
self.add_input("SLICESOCK_Int", "Port").value = 80
|
||||||
|
self.add_input("SLICESOCK_String", "Prefix")
|
||||||
|
self.add_input("SLICESOCK_String", "Username")
|
||||||
|
self.add_input("SLICESOCK_String", "Password")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
layout.prop(self, "printer_label") # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
layout.prop(self, "host_type") # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
from ...infra.printer_backends import PrinterConfig
|
||||||
|
return PrinterConfig(
|
||||||
|
host=self.resolve("Host"),
|
||||||
|
port=self.resolve("Port"),
|
||||||
|
prefix=self.resolve("Prefix"),
|
||||||
|
label=self.printer_label,
|
||||||
|
username=self.resolve("Username"),
|
||||||
|
password=self.resolve("Password"),
|
||||||
|
host_type=self.host_type
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_InputIntrinsics(SliceNode):
|
class SLICENODE_InputIntrinsics(SliceNode):
|
||||||
|
|
||||||
bl_label = "Printer Intrinsics"
|
bl_label = "Printer Intrinsics"
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
|
|
@ -657,6 +703,66 @@ class ExecSliceNode(SliceNode):
|
||||||
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
class SLICENODE_PrinterList(SliceNode):
|
||||||
|
bl_label = "Printer List"
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
import json
|
||||||
|
if not self.config_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
abs_path = bpy.path.abspath(self.config_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.outputs.get(name)
|
||||||
|
if not sock:
|
||||||
|
sock = self.add_output("SLICESOCK_Printer", name)
|
||||||
|
|
||||||
|
# Update properties
|
||||||
|
sock.label = name
|
||||||
|
sock.host = str(p_data.get("ip", "localhost"))
|
||||||
|
sock.port = int(p_data.get("port", 80))
|
||||||
|
sock.prefix = str(p_data.get("prefix", ""))
|
||||||
|
sock.username = str(p_data.get("username", ""))
|
||||||
|
sock.password = str(p_data.get("password", ""))
|
||||||
|
sock.host_type = str(p_data.get("host_type", "PRUSALINK")).upper()
|
||||||
|
|
||||||
|
# Remove stale sockets
|
||||||
|
for sock in list(self.outputs):
|
||||||
|
if sock.name not in printer_names:
|
||||||
|
self.outputs.remove(sock)
|
||||||
|
|
||||||
|
def update_config(self, context):
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
config_path: StringProperty(name="Config File", default="", subtype='FILE_PATH', update=update_config) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
layout.prop(self, "config_path") # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
self._refresh()
|
||||||
|
sock = self.outputs.get(socket_name)
|
||||||
|
if sock:
|
||||||
|
return sock.value
|
||||||
|
return None
|
||||||
|
@register_class
|
||||||
class SLICENODE_GcodeToFile(ExecSliceNode):
|
class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
bl_label = "Gcode To File"
|
bl_label = "Gcode To File"
|
||||||
|
|
||||||
|
|
@ -666,7 +772,7 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
self.add_input("SLICESOCK_Gcode")
|
self.add_input("SLICESOCK_Gcode")
|
||||||
self.add_input("SLICESOCK_FilePath", "Filename")
|
self.add_input("SLICESOCK_FilePath", "Filename")
|
||||||
|
|
||||||
def evaluate(self):
|
def evaluate(self, operator=None):
|
||||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||||
filename: str = resolve_input(self.inputs.get("Filename"))
|
filename: str = resolve_input(self.inputs.get("Filename"))
|
||||||
|
|
||||||
|
|
@ -678,6 +784,66 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
|
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_StartPrint(ExecSliceNode):
|
||||||
|
bl_label = "Start Print"
|
||||||
|
button_label = "Send to Printer"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
self.add_input("SLICESOCK_Printer")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from ...infra.printer_backends import (
|
||||||
|
PrinterConfig,
|
||||||
|
PrusaLinkBackend,
|
||||||
|
CrealityBackend,
|
||||||
|
MoonrakerBackend
|
||||||
|
)
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||||
|
config: PrinterConfig | None = resolve_input(self.inputs.get("Printer"))
|
||||||
|
|
||||||
|
if not gcode:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
||||||
|
return 'no gcode'
|
||||||
|
if not config:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Printer configuration")
|
||||||
|
return 'no printer'
|
||||||
|
|
||||||
|
# Instantiate correct backend
|
||||||
|
backend = None
|
||||||
|
if config.host_type == 'PRUSALINK':
|
||||||
|
backend = PrusaLinkBackend(config.host, config.port, config.prefix, api_key=config.password)
|
||||||
|
elif config.host_type == 'CREALITY':
|
||||||
|
backend = CrealityBackend(config.host, config.port, config.prefix)
|
||||||
|
elif config.host_type == 'MOONRAKER':
|
||||||
|
backend = MoonrakerBackend(config.host, config.port, config.prefix)
|
||||||
|
|
||||||
|
if not backend:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Unsupported host type: {config.host_type}")
|
||||||
|
return 'unsupported'
|
||||||
|
|
||||||
|
# Write to temp file to send
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.gcode', delete=False) as tf:
|
||||||
|
tf.write(gcode)
|
||||||
|
temp_path = Path(tf.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print_name = f"NodeSlicer_{config.label}.gcode".replace(" ", "_")
|
||||||
|
backend.start_print(temp_path, print_name)
|
||||||
|
if operator: operator.report({'INFO'}, f"Print started on {config.label}")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Failed to start print: {str(e)}")
|
||||||
|
return 'error'
|
||||||
|
finally:
|
||||||
|
if temp_path.exists():
|
||||||
|
temp_path.unlink()
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
bl_label = "Gcode to Mesh"
|
bl_label = "Gcode to Mesh"
|
||||||
|
|
@ -687,7 +853,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Gcode")
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
|
||||||
def evaluate(self):
|
def evaluate(self, operator=None):
|
||||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||||
if not gcode:
|
if not gcode:
|
||||||
return 'empty'
|
return 'empty'
|
||||||
|
|
@ -795,6 +961,6 @@ class SLICENODE_Test(ExecSliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Gcode")
|
self.add_input("SLICESOCK_Gcode")
|
||||||
|
|
||||||
def evaluate(self):
|
def evaluate(self, operator=None):
|
||||||
_ = resolve_input(self.inputs.get("Gcode"))
|
_ = resolve_input(self.inputs.get("Gcode"))
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty
|
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty
|
||||||
|
|
||||||
from ...registry import register_class
|
from ...registry import register_class
|
||||||
from ...infra.blender_slice_nodes_generic import SliceNodeSocket
|
from ...infra.blender_slice_nodes_generic import SliceNodeSocket
|
||||||
|
|
@ -114,3 +114,52 @@ class SLICESOCK_Intrinsics(LabelNodeSocket):
|
||||||
bl_label = "Intrinsics"
|
bl_label = "Intrinsics"
|
||||||
default = None
|
default = None
|
||||||
color = (0.2, 0.2, 0.2, 1.0)
|
color = (0.2, 0.2, 0.2, 1.0)
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICESOCK_Printer(InputNodeSocket):
|
||||||
|
bl_label = "Printer"
|
||||||
|
|
||||||
|
host: StringProperty(name="Host", default="localhost") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
port: IntProperty(name="Port", default=80, min=1, max=65535) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
prefix: StringProperty(name="Prefix", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
label: StringProperty(name="Label", default="Printer") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
username: StringProperty(name="Username", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
password: StringProperty(name="Password", default="", subtype='PASSWORD') # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
host_type: EnumProperty(
|
||||||
|
name="Type",
|
||||||
|
items=[
|
||||||
|
('PRUSALINK', "PrusaLink", "PrusaLink API"),
|
||||||
|
('CREALITY', "Creality", "Creality Cloud API"),
|
||||||
|
('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"),
|
||||||
|
],
|
||||||
|
default='PRUSALINK'
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
color = (1.0, 0.45, 0.1, 1.0) # Orange
|
||||||
|
|
||||||
|
def draw(self, context, layout, node, text):
|
||||||
|
if self.is_output or self.is_linked:
|
||||||
|
layout.label(text=text or self.name)
|
||||||
|
else:
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.prop(self, "label", text="")
|
||||||
|
col.prop(self, "host_type", text="")
|
||||||
|
col.prop(self, "host")
|
||||||
|
col.prop(self, "port")
|
||||||
|
col.prop(self, "prefix")
|
||||||
|
col.prop(self, "username")
|
||||||
|
col.prop(self, "password")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> Any:
|
||||||
|
from ...infra.printer_backends import PrinterConfig
|
||||||
|
return PrinterConfig(
|
||||||
|
host=self.host,
|
||||||
|
port=self.port,
|
||||||
|
prefix=self.prefix,
|
||||||
|
label=self.label,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
host_type=self.host_type
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,16 @@ class SLICENODE_OT_Execute(Operator):
|
||||||
def execute(self, context) -> set['OperatorReturnItems']:
|
def execute(self, context) -> set['OperatorReturnItems']:
|
||||||
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
||||||
node = node_tree.nodes[self.node_name]
|
node = node_tree.nodes[self.node_name]
|
||||||
_ = node.evaluate()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = node.evaluate(operator=self)
|
||||||
|
if res == 'ok':
|
||||||
self.report({"INFO"}, f"Execution Successful")
|
self.report({"INFO"}, f"Execution Successful")
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, f"Execution failed: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -45,6 +52,8 @@ class SLICENODE_MT_Menu(Menu):
|
||||||
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
||||||
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
||||||
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||||
|
layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter"
|
||||||
|
layout.operator("node.add_node", text="Printer List").type = "SLICENODE_PrinterList"
|
||||||
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
||||||
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
||||||
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
||||||
|
|
@ -66,6 +75,7 @@ class SLICENODE_MT_Menu(Menu):
|
||||||
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
|
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
||||||
|
layout.operator("node.add_node", text="Start Print").type = "SLICENODE_StartPrint"
|
||||||
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
|
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
|
||||||
|
|
||||||
def draw_add_menu(self, context):
|
def draw_add_menu(self, context):
|
||||||
|
|
|
||||||
97
infra/ftp.py
Normal file
97
infra/ftp.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def ftp_upload(
|
||||||
|
host: str,
|
||||||
|
filepath: Path,
|
||||||
|
storage_path: str,
|
||||||
|
filename: str,
|
||||||
|
overwrite: bool = False,
|
||||||
|
timeout: float = 30,
|
||||||
|
user: str = "",
|
||||||
|
passwd: str = "",
|
||||||
|
):
|
||||||
|
from ftplib import FTP, error_perm
|
||||||
|
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
raise FileNotFoundError(f"Local file not found: {filepath}")
|
||||||
|
|
||||||
|
remote_name = filename or os.path.basename(filepath)
|
||||||
|
|
||||||
|
ftp = FTP(host, timeout=timeout)
|
||||||
|
ftp.login(user=user, passwd=passwd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Change to target directory
|
||||||
|
ftp.cwd(storage_path)
|
||||||
|
|
||||||
|
# Check if file exists remotely
|
||||||
|
existing = ftp.nlst()
|
||||||
|
if remote_name in existing:
|
||||||
|
if not overwrite:
|
||||||
|
raise FileExistsError(f"Remote file already exists: {remote_name}")
|
||||||
|
# delete existing file
|
||||||
|
try:
|
||||||
|
ftp.delete(str(remote_name))
|
||||||
|
except error_perm as e:
|
||||||
|
raise RuntimeError(f"Failed to delete remote file: {e}")
|
||||||
|
|
||||||
|
# Upload new file
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
ftp.storbinary(f'STOR {remote_name}', f)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
ftp.quit()
|
||||||
|
|
||||||
|
def ftp_wipe(
|
||||||
|
host: str,
|
||||||
|
storage_path: str,
|
||||||
|
timeout: int = 30,
|
||||||
|
user: str = "",
|
||||||
|
passwd: str = "",
|
||||||
|
):
|
||||||
|
from ftplib import FTP, error_perm
|
||||||
|
|
||||||
|
ftp = FTP(host, timeout=timeout)
|
||||||
|
ftp.login(user=user, passwd=passwd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ftp.cwd(storage_path)
|
||||||
|
existing = ftp.nlst()
|
||||||
|
for f in existing:
|
||||||
|
try:
|
||||||
|
ftp.delete(filename=f)
|
||||||
|
except error_perm as e:
|
||||||
|
raise RuntimeError(f"Failed to delete remote file: {f}")
|
||||||
|
except error_perm as e:
|
||||||
|
raise RuntimeError("e")
|
||||||
|
finally:
|
||||||
|
ftp.quit()
|
||||||
|
|
||||||
|
def ftp_get_filesize(
|
||||||
|
host: str,
|
||||||
|
storage_path: str,
|
||||||
|
filename: str,
|
||||||
|
timeout: float = 30,
|
||||||
|
user: str = "",
|
||||||
|
passwd: str = "",
|
||||||
|
) -> int:
|
||||||
|
from ftplib import FTP, error_perm
|
||||||
|
|
||||||
|
ftp = FTP(host, timeout=timeout)
|
||||||
|
ftp.login(user=user, passwd=passwd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ftp.cwd(storage_path)
|
||||||
|
try:
|
||||||
|
size = ftp.size(filename)
|
||||||
|
except error_perm as e:
|
||||||
|
raise FileNotFoundError(f"Remote file not found or SIZE not supported: {filename}") from e
|
||||||
|
|
||||||
|
if size is None:
|
||||||
|
raise RuntimeError("FTP server returned no size information")
|
||||||
|
|
||||||
|
return size
|
||||||
|
|
||||||
|
finally:
|
||||||
|
ftp.quit()
|
||||||
|
|
@ -29,6 +29,16 @@ class PrinterStatus:
|
||||||
bed_temperature: float = 0.0
|
bed_temperature: float = 0.0
|
||||||
nozzle_diameter: float = 0.0
|
nozzle_diameter: float = 0.0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrinterConfig:
|
||||||
|
host: str = "localhost"
|
||||||
|
port: int = 80
|
||||||
|
prefix: str = ""
|
||||||
|
label: str = "Printer"
|
||||||
|
username: str = ""
|
||||||
|
password: str = ""
|
||||||
|
host_type: str = "PRUSALINK"
|
||||||
|
|
||||||
class PrinterBackend(Protocol):
|
class PrinterBackend(Protocol):
|
||||||
def query_status(self) -> PrinterStatus: ...
|
def query_status(self) -> PrinterStatus: ...
|
||||||
def pause(self) -> None: ...
|
def pause(self) -> None: ...
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue