144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python
|
||
|
|
"""blender_bridge.py -- terminal client for the TinqsBlenderBridge server.
|
||
|
|
|
||
|
|
Talks newline-delimited JSON over a localhost TCP socket to the
|
||
|
|
TinqsBlenderBridge server running inside an open Blender session
|
||
|
|
(tools/blender_bridge/tinqs_blender_bridge.py -- started via Text Editor >
|
||
|
|
Run Script, or auto-started from scripts/startup/ after running
|
||
|
|
tools/install_blender_bridge.ps1). Unlike the MD bridge, Blender's UI stays
|
||
|
|
fully interactive while the bridge serves.
|
||
|
|
|
||
|
|
Same wire protocol and `result` convention as tools/iclone_bridge.py (18800)
|
||
|
|
and tools/md_bridge.py (18900), but on port 19000 so all three can run at
|
||
|
|
once.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python tools/blender_bridge.py --ping
|
||
|
|
python tools/blender_bridge.py --exec "result = [o.name for o in bpy.data.objects]"
|
||
|
|
python tools/blender_bridge.py --file some_script.py
|
||
|
|
python tools/blender_bridge.py --stop
|
||
|
|
python tools/blender_bridge.py --port 19001 --timeout 120 --exec "..."
|
||
|
|
|
||
|
|
The exec namespace is persistent per Blender session and pre-seeded with
|
||
|
|
bpy, view3d_override() (a temp_override kwargs helper for viewport
|
||
|
|
operators), and a BRIDGE info dict.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import socket
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
DEFAULT_PORT = 19000
|
||
|
|
DEFAULT_TIMEOUT = 30.0
|
||
|
|
|
||
|
|
# Sent by --ping: bridge metadata seeded by the server at startup.
|
||
|
|
PING_CODE = "BRIDGE['blend_file'] = bpy.data.filepath or '<unsaved>'\nresult = BRIDGE\n"
|
||
|
|
|
||
|
|
|
||
|
|
def _send(sock, obj):
|
||
|
|
sock.sendall((json.dumps(obj) + "\n").encode("utf-8"))
|
||
|
|
|
||
|
|
|
||
|
|
def _recv_line(sock, timeout):
|
||
|
|
sock.settimeout(timeout)
|
||
|
|
buf = b""
|
||
|
|
while not buf.endswith(b"\n"):
|
||
|
|
chunk = sock.recv(4096)
|
||
|
|
if not chunk:
|
||
|
|
if buf:
|
||
|
|
break
|
||
|
|
raise ConnectionError(
|
||
|
|
"connection closed by the Blender bridge before a response was received")
|
||
|
|
buf += chunk
|
||
|
|
return buf.decode("utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def run(code, port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT):
|
||
|
|
"""Send `code` to be exec()'d inside the open Blender session.
|
||
|
|
|
||
|
|
Returns the parsed response dict. Raises OSError on transport failure;
|
||
|
|
the dict's "ok" key reports exec-level success/failure.
|
||
|
|
"""
|
||
|
|
req = {"id": int(time.time() * 1000), "code": code}
|
||
|
|
with socket.create_connection(("127.0.0.1", port), timeout=timeout) as sock:
|
||
|
|
_send(sock, req)
|
||
|
|
line = _recv_line(sock, timeout)
|
||
|
|
return json.loads(line)
|
||
|
|
|
||
|
|
|
||
|
|
def ping(port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT):
|
||
|
|
"""Convenience wrapper: run(PING_CODE, ...)."""
|
||
|
|
return run(PING_CODE, port=port, timeout=timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def _read_code(args):
|
||
|
|
if args.exec is not None:
|
||
|
|
return args.exec
|
||
|
|
with open(args.file, "r", encoding="utf-8") as f:
|
||
|
|
return f.read()
|
||
|
|
|
||
|
|
|
||
|
|
def _main(argv=None):
|
||
|
|
p = argparse.ArgumentParser(
|
||
|
|
description="Client for the TinqsBlenderBridge socket server inside Blender.")
|
||
|
|
p.add_argument("--port", type=int, default=DEFAULT_PORT,
|
||
|
|
help="bridge TCP port (default: %(default)s; must match "
|
||
|
|
"TINQS_BLENDER_BRIDGE_PORT inside Blender if overridden there)")
|
||
|
|
p.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT,
|
||
|
|
help="socket timeout in seconds (default: %(default)s; raise "
|
||
|
|
"for long bakes/exports)")
|
||
|
|
group = p.add_mutually_exclusive_group(required=True)
|
||
|
|
group.add_argument("--ping", action="store_true",
|
||
|
|
help="check the bridge is alive; reports blender version, "
|
||
|
|
"open .blend, embedded python")
|
||
|
|
group.add_argument("--stop", action="store_true",
|
||
|
|
help="stop the bridge server inside Blender")
|
||
|
|
group.add_argument("--exec", metavar="CODE",
|
||
|
|
help="python source to exec() inside the open Blender")
|
||
|
|
group.add_argument("--file", metavar="PATH",
|
||
|
|
help="path to a python file to exec() inside the open Blender")
|
||
|
|
args = p.parse_args(argv)
|
||
|
|
|
||
|
|
try:
|
||
|
|
if args.ping:
|
||
|
|
resp = ping(port=args.port, timeout=args.timeout)
|
||
|
|
elif args.stop:
|
||
|
|
resp = run("__STOP__", port=args.port, timeout=args.timeout)
|
||
|
|
else:
|
||
|
|
resp = run(_read_code(args), port=args.port, timeout=args.timeout)
|
||
|
|
except ConnectionRefusedError:
|
||
|
|
print(
|
||
|
|
"error: connection refused on 127.0.0.1:{} -- is Blender running? "
|
||
|
|
"was the bridge started (Run Script or scripts/startup install)?".format(args.port),
|
||
|
|
file=sys.stderr,
|
||
|
|
)
|
||
|
|
return 1
|
||
|
|
except OSError as exc:
|
||
|
|
print("error: could not reach the Blender bridge on 127.0.0.1:{}: {}".format(
|
||
|
|
args.port, exc), file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
if resp.get("stdout"):
|
||
|
|
text = resp["stdout"]
|
||
|
|
print(text, end="" if text.endswith("\n") else "\n")
|
||
|
|
|
||
|
|
if resp.get("ok"):
|
||
|
|
if args.ping:
|
||
|
|
info = resp.get("result") or {}
|
||
|
|
py = str(info.get("python", "?")).split()[0]
|
||
|
|
print("ok: Blender bridge v{} blender={} python {} file={}".format(
|
||
|
|
info.get("version", "?"), info.get("blender", "?"), py,
|
||
|
|
info.get("blend_file", "?")))
|
||
|
|
else:
|
||
|
|
print(json.dumps(resp.get("result"), indent=2))
|
||
|
|
return 0
|
||
|
|
|
||
|
|
print("error: {}".format(resp.get("error", "unknown error")), file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(_main())
|