mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-10 14:02:47 +00:00
feat(automation): Python reference client
This commit is contained in:
111
tools/automation/orca_automation.py
Normal file
111
tools/automation/orca_automation.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Reference client for the OrcaSlicer UI automation JSON-RPC server.
|
||||
|
||||
Usage:
|
||||
from orca_automation import OrcaClient
|
||||
orca = OrcaClient(port=13619)
|
||||
print(orca.version())
|
||||
orca.click({"id": "btn_slice"})
|
||||
orca.wait_for({"id": "btn_export"}, state="enabled", timeout_ms=120000)
|
||||
png = orca.screenshot_3d(width=1024, height=768)
|
||||
open("preview.png", "wb").write(png)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import base64
|
||||
import json
|
||||
import urllib.request
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class OrcaError(RuntimeError):
|
||||
def __init__(self, code: int, message: str):
|
||||
super().__init__(f"[{code}] {message}")
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
class OrcaClient:
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 13619, timeout: float = 30.0):
|
||||
self._url = f"http://{host}:{port}/jsonrpc"
|
||||
self._timeout = timeout
|
||||
self._id = 0
|
||||
|
||||
def _call(self, method: str, params: Optional[dict] = None) -> Any:
|
||||
self._id += 1
|
||||
payload = {"jsonrpc": "2.0", "id": self._id, "method": method}
|
||||
if params is not None:
|
||||
payload["params"] = params
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
self._url, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
||||
body = json.loads(resp.read().decode("utf-8"))
|
||||
if "error" in body:
|
||||
err = body["error"]
|
||||
raise OrcaError(err.get("code", -1), err.get("message", "unknown error"))
|
||||
return body.get("result")
|
||||
|
||||
# --- protocol methods ---
|
||||
def version(self) -> dict:
|
||||
return self._call("automation.version")
|
||||
|
||||
def dump_tree(self, root: Optional[str] = None, max_depth: Optional[int] = None,
|
||||
visible_only: bool = False, include_imgui: bool = True) -> dict:
|
||||
params: dict = {"visible_only": visible_only, "include_imgui": include_imgui}
|
||||
if root is not None:
|
||||
params["root"] = root
|
||||
if max_depth is not None:
|
||||
params["max_depth"] = max_depth
|
||||
return self._call("tree.dump", params)
|
||||
|
||||
def find(self, **predicate) -> list:
|
||||
# predicate keys: name, class, label, value, backend
|
||||
return self._call("tree.find", predicate)
|
||||
|
||||
def get(self, target: dict) -> dict:
|
||||
return self._call("widget.get", {"target": target})
|
||||
|
||||
def click(self, target: dict, button: str = "left",
|
||||
double: bool = False, modifiers: Optional[list] = None) -> dict:
|
||||
params = {"target": target, "button": button, "double": double}
|
||||
if modifiers:
|
||||
params["modifiers"] = modifiers
|
||||
return self._call("input.click", params)
|
||||
|
||||
def type(self, text: str, target: Optional[dict] = None) -> dict:
|
||||
params: dict = {"text": text}
|
||||
if target is not None:
|
||||
params["target"] = target
|
||||
return self._call("input.type", params)
|
||||
|
||||
def key(self, keys) -> dict:
|
||||
# keys: "ctrl+s" or ["ctrl", "s"]
|
||||
return self._call("input.key", {"keys": keys})
|
||||
|
||||
def wait_for(self, target: dict, state: str = "visible",
|
||||
value: Optional[str] = None, timeout_ms: int = 5000,
|
||||
poll_ms: int = 100) -> dict:
|
||||
params = {"target": target, "state": state,
|
||||
"timeout_ms": timeout_ms, "poll_ms": poll_ms}
|
||||
if value is not None:
|
||||
params["value"] = value
|
||||
return self._call("sync.wait_for", params)
|
||||
|
||||
def app_state(self) -> dict:
|
||||
return self._call("app.state")
|
||||
|
||||
def screenshot(self, target: Optional[dict] = None) -> bytes:
|
||||
params = {"target": target} if target is not None else None
|
||||
result = self._call("screenshot.window", params)
|
||||
return base64.b64decode(result["png_base64"])
|
||||
|
||||
def screenshot_3d(self, plate: Optional[int] = None,
|
||||
width: Optional[int] = None, height: Optional[int] = None) -> bytes:
|
||||
params: dict = {}
|
||||
if plate is not None:
|
||||
params["plate"] = plate
|
||||
if width is not None:
|
||||
params["width"] = width
|
||||
if height is not None:
|
||||
params["height"] = height
|
||||
result = self._call("screenshot.viewport3d", params or None)
|
||||
return base64.b64decode(result["png_base64"])
|
||||
Reference in New Issue
Block a user