math
This commit is contained in:
parent
b17ff98e74
commit
fbdc1a9eb8
5 changed files with 371 additions and 34 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
from math import floor
|
from math import floor
|
||||||
from typing import Any
|
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
|
||||||
|
|
@ -65,34 +65,25 @@ class SLICENODE_InputObject(InputNode):
|
||||||
class SLICENODE_InputIntrinsics(SliceNode):
|
class SLICENODE_InputIntrinsics(SliceNode):
|
||||||
bl_label = "Printer Intrinsics"
|
bl_label = "Printer Intrinsics"
|
||||||
|
|
||||||
bed_x: FloatProperty(name="Bed X", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
bed_y: FloatProperty(name="Bed Y", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
feedrate: FloatProperty(name="Feedrate", default=1200.0) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
extrusion_rate: FloatProperty(name="Extrusion Rate", default=0.017, precision=4) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_output("SLICESOCK_Intrinsics")
|
self.add_output("SLICESOCK_Intrinsics")
|
||||||
|
self.add_input("SLICESOCK_Float", "Bed X").value = 250.0
|
||||||
def draw_buttons(self, context, layout):
|
self.add_input("SLICESOCK_Float", "Bed Y").value = 210.0
|
||||||
assert layout
|
self.add_input("SLICESOCK_Float", "Feedrate").value = 1200.0
|
||||||
layout.prop(self, "bed_x")
|
self.add_input("SLICESOCK_Float", "Extrusion Rate").value = 0.035
|
||||||
layout.prop(self, "bed_y")
|
|
||||||
layout.prop(self, "feedrate")
|
|
||||||
layout.prop(self, "extrusion_rate")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
||||||
return SliceIntrinsics(
|
return SliceIntrinsics(
|
||||||
bed_x=self.bed_x,
|
bed_x=self.resolve("Bed X"),
|
||||||
bed_y=self.bed_y,
|
bed_y=self.resolve("Bed Y"),
|
||||||
feedrate=self.feedrate,
|
feedrate=self.resolve("Feedrate"),
|
||||||
extrusion_rate=self.extrusion_rate
|
extrusion_rate=self.resolve("Extrusion Rate")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Math
|
# Math
|
||||||
|
|
||||||
@register_class
|
class MathNode(SliceNode):
|
||||||
class SLICENODE_IntMath(SliceNode):
|
bl_label = "Float Math"
|
||||||
bl_label = "Integer Math"
|
|
||||||
|
|
||||||
operation: bpy.props.EnumProperty(
|
operation: bpy.props.EnumProperty(
|
||||||
name="Operation",
|
name="Operation",
|
||||||
|
|
@ -100,7 +91,7 @@ class SLICENODE_IntMath(SliceNode):
|
||||||
('ADD', "Add", "Add the two values"),
|
('ADD', "Add", "Add the two values"),
|
||||||
('SUBTRACT', "Subtract", "Subtract the second value from the first"),
|
('SUBTRACT', "Subtract", "Subtract the second value from the first"),
|
||||||
('MULTIPLY', "Multiply", "Multiply the two values"),
|
('MULTIPLY', "Multiply", "Multiply the two values"),
|
||||||
('DIVIDE', "Divide", "Integer division"),
|
('DIVIDE', "Divide", "Division"),
|
||||||
('ABS', "Absolute", "Absolute value of the first value"),
|
('ABS', "Absolute", "Absolute value of the first value"),
|
||||||
('NEGATE', "Negate", "Negate the first value"),
|
('NEGATE', "Negate", "Negate the first value"),
|
||||||
('POWER', "Power", "First value raised to the power of the second"),
|
('POWER', "Power", "First value raised to the power of the second"),
|
||||||
|
|
@ -116,19 +107,24 @@ class SLICENODE_IntMath(SliceNode):
|
||||||
default='ADD'
|
default='ADD'
|
||||||
) # pyright: ignore[reportInvalidTypeForm]
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
def init(self, context):
|
math_type: TypeAlias = float
|
||||||
self.add_output("SLICESOCK_IntRo", "Value")
|
socket_label: str = "Float"
|
||||||
|
|
||||||
self.add_input("SLICESOCK_Int", "Value")
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Int", "Value")
|
self.add_output(f"SLICESOCK_{self.socket_label}Ro", "Value")
|
||||||
|
|
||||||
|
self.add_input(f"SLICESOCK_{self.socket_label}", "Value")
|
||||||
|
self.add_input(f"SLICESOCK_{self.socket_label}", "Value")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "operation", text="")
|
layout.prop(self, "operation", text="")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> int:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> math_type:
|
||||||
a: int = resolve_input(self.inputs[0])
|
t = self.math_type
|
||||||
b: int = resolve_input(self.inputs[1])
|
|
||||||
|
a: t = resolve_input(self.inputs[0])
|
||||||
|
b: t = resolve_input(self.inputs[1])
|
||||||
|
|
||||||
op = self.operation
|
op = self.operation
|
||||||
try:
|
try:
|
||||||
|
|
@ -138,19 +134,31 @@ class SLICENODE_IntMath(SliceNode):
|
||||||
if op == 'DIVIDE': return a // b if b != 0 else 0
|
if op == 'DIVIDE': return a // b if b != 0 else 0
|
||||||
if op == 'ABS': return abs(a)
|
if op == 'ABS': return abs(a)
|
||||||
if op == 'NEGATE': return -a
|
if op == 'NEGATE': return -a
|
||||||
if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0
|
if op == 'POWER': return t(a ** b) if not (a == 0 and b < 0) else 0
|
||||||
if op == 'MIN': return min(a, b)
|
if op == 'MIN': return min(a, b)
|
||||||
if op == 'MAX': return max(a, b)
|
if op == 'MAX': return max(a, b)
|
||||||
if op == 'SIGN': return (a > 0) - (a < 0)
|
if op == 'SIGN': return (a > 0) - (a < 0)
|
||||||
if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0
|
if op == 'DIVIDE_ROUND': return t(round(a / b)) if b != 0 else 0
|
||||||
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
|
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
|
||||||
if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0
|
if op == 'DIVIDE_TRUNCATE': return t(a / b) if b != 0 else 0
|
||||||
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
|
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
|
||||||
if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0
|
if op == 'MODULO_TRUNCATED': return a - (b * t(a / b)) if b != 0 else 0
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_FloatMath(MathNode):
|
||||||
|
bl_label = "Math"
|
||||||
|
math_type = float
|
||||||
|
socket_label: str = "Float"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_IntMath(MathNode):
|
||||||
|
bl_label = "Integer Math"
|
||||||
|
math_type = int
|
||||||
|
socket_label: str = "Int"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GetMesh(SliceNode):
|
class SLICENODE_GetMesh(SliceNode):
|
||||||
bl_label = "Get Mesh"
|
bl_label = "Get Mesh"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class SLICENODE_MT_Menu(Menu):
|
||||||
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
|
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
|
||||||
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
|
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
|
||||||
layout.operator("node.add_node", text="Rotation").type = "SLICENODE_InputVectorRotation"
|
layout.operator("node.add_node", text="Rotation").type = "SLICENODE_InputVectorRotation"
|
||||||
|
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 Intrinsics").type = "SLICENODE_InputIntrinsics"
|
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
G0 Z0.1 F720 ; Move to the desired physical Z clearance height
|
|
||||||
G92 Z0 ; Redefine this position as Z=0
|
|
||||||
|
|
@ -23,6 +23,9 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
||||||
class SliceNode(Node, AutoBlIdName):
|
class SliceNode(Node, AutoBlIdName):
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||||
|
|
||||||
|
def resolve(self, fieldname) -> Any:
|
||||||
|
return resolve_input(self.inputs.get(fieldname))
|
||||||
|
|
||||||
def add_output(self, bl_idname: str, name: str | None = None):
|
def add_output(self, bl_idname: str, name: str | None = None):
|
||||||
from ..registry import get_label_by_name
|
from ..registry import get_label_by_name
|
||||||
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||||
|
|
|
||||||
327
infra/printer_backends.py
Normal file
327
infra/printer_backends.py
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
def with_api_state(api_state: str) -> Callable[..., Any]:
|
||||||
|
def decorator(func: Callable[..., Any]) -> Any:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
self.api_state = api_state
|
||||||
|
try: return func(self, *args, **kwargs)
|
||||||
|
finally: self.api_state = None
|
||||||
|
return wrapped
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Protocol, Any, Optional
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PrinterStatus:
|
||||||
|
progress: float = 0.0
|
||||||
|
state: str = "OFFLINE"
|
||||||
|
job_name: str = ""
|
||||||
|
job_id: str = ""
|
||||||
|
nozzle_temperature: float = 0.0
|
||||||
|
bed_temperature: float = 0.0
|
||||||
|
nozzle_diameter: float = 0.0
|
||||||
|
|
||||||
|
class PrinterBackend(Protocol):
|
||||||
|
def query_status(self) -> PrinterStatus: ...
|
||||||
|
def pause(self) -> None: ...
|
||||||
|
def resume(self) -> None: ...
|
||||||
|
def stop(self) -> None: ...
|
||||||
|
def start_print(self, gcode: Path, name: str) -> None: ...
|
||||||
|
|
||||||
|
class HttpBackend:
|
||||||
|
def __init__(self, host: str, port: int, prefix: str, api_key: Optional[str] = None, timeout: float = 30.0):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.base = f"http://{host}:{port}{prefix}"
|
||||||
|
self.timeout = timeout
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.headers = {}
|
||||||
|
self.api_state: str | None = None
|
||||||
|
if api_key:
|
||||||
|
self.headers["X-Api-Key"] = api_key
|
||||||
|
|
||||||
|
def _get(self, path: str, **kw) -> requests.Response:
|
||||||
|
headers = {**self.headers, **kw.pop('headers', {})}
|
||||||
|
return self.session.get(self.base + path, headers=headers, timeout=self.timeout, **kw)
|
||||||
|
|
||||||
|
def _post(self, path: str, **kw) -> requests.Response:
|
||||||
|
headers = {**self.headers, **kw.pop('headers', {})}
|
||||||
|
return self.session.post(self.base + path, headers=headers, timeout=self.timeout, **kw)
|
||||||
|
|
||||||
|
def _put(self, path: str, **kw) -> requests.Response:
|
||||||
|
headers = {**self.headers, **kw.pop('headers', {})}
|
||||||
|
return self.session.put(self.base + path, headers=headers, timeout=self.timeout, **kw)
|
||||||
|
|
||||||
|
def _delete(self, path: str, **kw) -> requests.Response:
|
||||||
|
headers = {**self.headers, **kw.pop('headers', {})}
|
||||||
|
return self.session.delete(self.base + path, headers=headers, timeout=self.timeout, **kw)
|
||||||
|
|
||||||
|
def _json(self, resp, default: dict = {}, ok=(200, 201, 202, 204)) -> dict:
|
||||||
|
if resp.status_code not in ok:
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.status_code == 204 or not resp.content:
|
||||||
|
return default
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
class PrinterHttpBackend(HttpBackend, PrinterBackend):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PrusaLinkBackend(PrinterHttpBackend):
|
||||||
|
def query_status(self) -> PrinterStatus:
|
||||||
|
s = self._get("/api/v1/status").json()
|
||||||
|
info = self._get("/api/v1/info").json()
|
||||||
|
job = self._json(self._get("/api/v1/job"))
|
||||||
|
return PrinterStatus(
|
||||||
|
progress=float(job.get("progress", 0.0)),
|
||||||
|
state=str(s.get("printer", {}).get("state", "OFFLINE")),
|
||||||
|
job_name=str(job.get("file", {}).get("display_name", "")),
|
||||||
|
job_id=str(s.get("job", {}).get("id", "")),
|
||||||
|
nozzle_temperature=float(s.get("printer", {}).get("temp_nozzle", 0) or 0),
|
||||||
|
bed_temperature=float(s.get("printer", {}).get("temp_bed", 0) or 0),
|
||||||
|
nozzle_diameter=float(info.get('nozzle_diameter', 0.0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@with_api_state('PAUSING')
|
||||||
|
def pause(self) -> None:
|
||||||
|
jid = self.query_status().job_id
|
||||||
|
if not jid: raise RuntimeError("No job to pause")
|
||||||
|
self._put(f"/api/v1/job/{jid}/pause").raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('RESUMING')
|
||||||
|
def resume(self) -> None:
|
||||||
|
jid = self.query_status().job_id
|
||||||
|
if not jid: raise RuntimeError("No job to resume")
|
||||||
|
self._put(f"/api/v1/job/{jid}/resume").raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('STOPPING')
|
||||||
|
def stop(self) -> None:
|
||||||
|
jid = self.query_status().job_id
|
||||||
|
if not jid: raise RuntimeError("No job to stop")
|
||||||
|
self._delete(f"/api/v1/job/{jid}").raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state("STARTING")
|
||||||
|
def start_print(self, gcode: Path, name: str) -> None:
|
||||||
|
storage = self._get("/api/v1/storage").json()
|
||||||
|
writables = [s for s in storage.get("storage_list", []) if s.get("available") and not s.get("read_only")]
|
||||||
|
if not writables:
|
||||||
|
raise RuntimeError("No writable storage")
|
||||||
|
|
||||||
|
path = writables[0]["path"].lstrip("/")
|
||||||
|
file_size = gcode.stat().st_size
|
||||||
|
|
||||||
|
with open(gcode, "rb") as f:
|
||||||
|
self._put(
|
||||||
|
f"/api/v1/files/{path}/{name}",
|
||||||
|
headers={
|
||||||
|
"Overwrite": "?1",
|
||||||
|
"Print-After-Upload": "?1",
|
||||||
|
"Content-Type": "text/x.gcode" if Path(name).suffix == ".gcode" else "application/octet-stream",
|
||||||
|
"Content-Length": str(file_size),
|
||||||
|
},
|
||||||
|
data=f,
|
||||||
|
).raise_for_status()
|
||||||
|
|
||||||
|
try: # This block will ensure a start on mk3s printers, which don't seem to respond to Print-After-Upload
|
||||||
|
# wait until /transfer is gone (204)
|
||||||
|
for _ in range(int(self.timeout)):
|
||||||
|
r = self._get("/api/v1/transfer")
|
||||||
|
if r.status_code == 204: break
|
||||||
|
time.sleep(1)
|
||||||
|
else: raise TimeoutError("Transfer timed out")
|
||||||
|
|
||||||
|
# verify remote metadata size matches
|
||||||
|
for _ in range(int(self.timeout)):
|
||||||
|
r = self._get(f"/api/v1/files/{path}/{name}", headers={"Accept": "application/json"})
|
||||||
|
if r.status_code == 200 and (r.json().get("size") == file_size): break
|
||||||
|
time.sleep(1)
|
||||||
|
else: raise TimeoutError("Remote file size did not match expected size")
|
||||||
|
|
||||||
|
|
||||||
|
self._post(f"/api/v1/files/{path}/{name}").raise_for_status()
|
||||||
|
|
||||||
|
finally: return
|
||||||
|
|
||||||
|
class CrealityBackend(PrinterHttpBackend):
|
||||||
|
STATUS_EP = "/protocal.csp?fname=Info&opt=main&function=get"
|
||||||
|
STORAGE_PATH = "/mmcblk0p1/creality/gztemp/"
|
||||||
|
STORAGE_PATH_PRINT = "/media/mmcblk0p1/creality/gztemp/"
|
||||||
|
|
||||||
|
def query_status(self) -> PrinterStatus:
|
||||||
|
r = self._get(self.STATUS_EP)
|
||||||
|
r.raise_for_status()
|
||||||
|
d = r.json()
|
||||||
|
|
||||||
|
# Map numeric state to string
|
||||||
|
state_id = str(d.get("state", "-1"))
|
||||||
|
state: str = (
|
||||||
|
"OFFLINE" if state_id == "-1" else
|
||||||
|
"PAUSED" if state_id == "5" else
|
||||||
|
"IDLE" if state_id in {"0", "3", "4"} else
|
||||||
|
"PRINTING" if state_id == "1" else
|
||||||
|
"FINISHED" if state_id == "2" else
|
||||||
|
"UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Defensive casting
|
||||||
|
def f(x, default=0.0):
|
||||||
|
try:
|
||||||
|
return float(x)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def s(x, default=""):
|
||||||
|
return str(x) if x is not None else default
|
||||||
|
|
||||||
|
return PrinterStatus(
|
||||||
|
progress=round(f(d.get("printProgress"), 0.0), 1),
|
||||||
|
state=state,
|
||||||
|
job_name=s(d.get("print")),
|
||||||
|
job_id="", # Not exposed by this API
|
||||||
|
nozzle_temperature=f(d.get("nozzleTemp"), 0.0),
|
||||||
|
bed_temperature=f(d.get("bedTemp"), 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
@with_api_state('PAUSING')
|
||||||
|
def pause(self) -> None:
|
||||||
|
r = self._get("/protocal.csp?fname=net&opt=iot_conf&function=set&pause=1")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('RESUMING')
|
||||||
|
def resume(self) -> None:
|
||||||
|
r = self._get("/protocal.csp?fname=net&opt=iot_conf&function=set&pause=0")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('STOPPING')
|
||||||
|
def stop(self) -> None:
|
||||||
|
r = self._get("/protocal.csp?fname=net&opt=iot_conf&function=set&stop=1")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('STARTING')
|
||||||
|
def start_print(self, gcode: Path, name: str) -> None:
|
||||||
|
from ..infra.ftp import ftp_upload, ftp_wipe, ftp_get_filesize
|
||||||
|
|
||||||
|
file_size = gcode.stat().st_size
|
||||||
|
|
||||||
|
# Clean target dir and upload
|
||||||
|
ftp_wipe(self.host, self.STORAGE_PATH)
|
||||||
|
ftp_upload(self.host, gcode, self.STORAGE_PATH, filename=name, overwrite=True, timeout=self.timeout)
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
print(_)
|
||||||
|
uploaded_size = ftp_get_filesize(self.host, storage_path=self.STORAGE_PATH, filename=name, timeout=self.timeout)
|
||||||
|
if uploaded_size == file_size:
|
||||||
|
break
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
raise TimeoutError("Uploaded file did not reach expected size on printer")
|
||||||
|
|
||||||
|
# Trigger print
|
||||||
|
ep = (
|
||||||
|
"/protocal.csp?fname=net&opt=iot_conf&function=set"
|
||||||
|
f"&print={self.STORAGE_PATH_PRINT}{name}"
|
||||||
|
)
|
||||||
|
r = self._get(ep)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
class MoonrakerBackend(PrinterHttpBackend):
|
||||||
|
def query_status(self) -> PrinterStatus:
|
||||||
|
url = f"{self.base}/printer/objects/query?webhooks&virtual_sdcard&print_stats"
|
||||||
|
r = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json().get("result", {}).get("status", {})
|
||||||
|
|
||||||
|
sd = data.get("virtual_sdcard", {}) or {}
|
||||||
|
ps = data.get("print_stats", {}) or {}
|
||||||
|
|
||||||
|
state = str(ps.get("state", "OFFLINE")).upper()
|
||||||
|
if state == "STANDBY":
|
||||||
|
state = "IDLE"
|
||||||
|
|
||||||
|
progress = 0.0
|
||||||
|
try:
|
||||||
|
progress = float(sd.get("progress", 0.0)) * 100.0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
file_path = sd.get("file_path", "") or ""
|
||||||
|
|
||||||
|
return PrinterStatus(
|
||||||
|
progress=round(progress, 1),
|
||||||
|
state=state,
|
||||||
|
job_name=str(file_path),
|
||||||
|
job_id="",
|
||||||
|
nozzle_temperature=0.0,
|
||||||
|
bed_temperature=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@with_api_state('PAUSING')
|
||||||
|
def pause(self) -> None:
|
||||||
|
url = f"{self.base}/printer/print/pause"
|
||||||
|
r = self.session.post(url, headers=self.headers, timeout=self.timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('RESUMING')
|
||||||
|
def resume(self) -> None:
|
||||||
|
url = f"{self.base}/printer/print/resume"
|
||||||
|
r = self.session.post(url, headers=self.headers, timeout=self.timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('STOPPING')
|
||||||
|
def stop(self) -> None:
|
||||||
|
url = f"{self.base}/printer/print/cancel"
|
||||||
|
r = self.session.post(url, headers=self.headers, timeout=self.timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@with_api_state('STARTING')
|
||||||
|
def start_print(self, gcode: Path, name: str) -> None:
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
expected_size = gcode.stat().st_size
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(gcode, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
checksum = h.hexdigest()
|
||||||
|
|
||||||
|
upload_url = f"{self.base}/server/files/upload"
|
||||||
|
|
||||||
|
with open(gcode, "rb") as f:
|
||||||
|
files = {"file": (name, f, "application/octet-stream")}
|
||||||
|
form = {
|
||||||
|
"root": "gcodes",
|
||||||
|
"print": "true",
|
||||||
|
"checksum": checksum,
|
||||||
|
}
|
||||||
|
r = self.session.post(
|
||||||
|
upload_url,
|
||||||
|
headers=self.headers,
|
||||||
|
files=files,
|
||||||
|
data=form,
|
||||||
|
timeout=self.timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
result = r.json()
|
||||||
|
|
||||||
|
if not (result.get("print_started") or result.get("print_queued")):
|
||||||
|
raise RuntimeError(f"Upload succeeded but print did not start: {result}")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ManagedPrinter:
|
||||||
|
name: str
|
||||||
|
backend: PrinterHttpBackend
|
||||||
|
status: PrinterStatus = field(default_factory=PrinterStatus)
|
||||||
|
last_error: str | None = None
|
||||||
|
last_command_time: str | None = None
|
||||||
|
last_command_response: str = ""
|
||||||
|
|
||||||
|
def query_status(self):
|
||||||
|
self.status = self.backend.query_status()
|
||||||
Loading…
Add table
Reference in a new issue