# iClone bridge A terminal-to-iClone bridge: drive iClone 8 from the shell (query the scene, run RLPy code, trigger exports) without clicking through the UI. iClone 8's Python API (`RLPy`) only runs *inside* iClone -- there is no external/network API. TinqsBridge is a small auto-load iClone plugin that opens a localhost TCP socket server inside iClone and executes whatever Python source is sent to it, on iClone's own Qt main thread. ## Pieces - `tools/iclone_bridge/TinqsBridge/main.py` -- the plugin source of truth (lives in this repo, gets copied into iClone). - `tools/install_iclone_bridge.ps1` -- copies the plugin into iClone's auto-load folder. - `tools/iclone_bridge.py` -- the terminal client (Python 3.12, stdlib only). ## Install / update ```powershell powershell -File tools\install_iclone_bridge.ps1 ``` This copies `tools/iclone_bridge/TinqsBridge/` to `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\`, overwriting any existing copy. It is idempotent and only ever touches the `TinqsBridge` subfolder -- other plugins under `OpenPlugin\` (AIStudio, MotionLIVE, VideoMocap) are never touched. **iClone must be restarted** after every install/update to load the new plugin code -- there is no live-reload of `main.py` on disk. (You *can* hot-swap runtime *behavior* once the bridge is up, by `--exec`-ing new code through it -- see "Persistent namespace" below -- but that only affects the running session, not what gets loaded on the next restart.) If iClone prompts with a Windows Firewall dialog for a `127.0.0.1` bind, allow it -- the bind is localhost-only either way. ## CLI usage ``` python tools/iclone_bridge.py --ping python tools/iclone_bridge.py --exec "result = 1 + 1" python tools/iclone_bridge.py --file some_script.py python tools/iclone_bridge.py --port 18801 --timeout 120 --exec "..." ``` - `--ping` checks the bridge is alive and prints the product name, iClone version, and the embedded Python version (via `RApplication` + `sys.version`). - `--exec CODE` runs a snippet of Python source inside iClone. - `--file PATH` runs a `.py` file's contents inside iClone. - `--port` (default `18800`) must match `TINQS_ICLONE_BRIDGE_PORT` if you've overridden it for the iClone process's environment. - `--timeout` (default `30` seconds) is the client socket timeout. On a connection failure the client exits `1` with a hint to check that iClone is running and was restarted after install. On an exec-level error (an exception raised by your code inside iClone) it also exits `1`, printing the traceback captured from iClone. ### Example: list scene objects ``` python tools/iclone_bridge.py --exec "result = [o.GetName() for o in RLPy.RScene.GetAvatars()] + [o.GetName() for o in RLPy.RScene.GetProps()]" ``` ## The `result` convention Whatever code you send is `exec()`'d inside iClone. If it sets a variable named `result`, that becomes the JSON `result` field of the response (falling back to `repr()` if it isn't JSON-serializable -- RLPy objects generally aren't). If your code doesn't set `result`, the response's `result` is `null`. `stdout`/`stderr` printed during execution are captured and returned as the response's `stdout` field. ## Persistent namespace All code you send runs against **one shared namespace dict** that persists for the life of the iClone session (until iClone is restarted or the plugin reloaded). This means: ``` python tools/iclone_bridge.py --exec "x = 41" python tools/iclone_bridge.py --exec "result = x + 1" # -> 42 ``` variables and imports from one `--exec`/`--file` call are visible to the next one. `RLPy` is pre-imported into the namespace. Each new client connection is a fresh TCP connection, but the *iClone-side* namespace is not reset per-connection. ## Error resilience An exception in your code (e.g. `1/0`) produces `ok: false` with the full traceback in `error` -- the bridge itself keeps running and will serve the next request normally; you don't need to restart iClone after a bad `--exec`. ## Log file `%TEMP%\tinqs_iclone_bridge.log` -- append-only, timestamped lines for: plugin startup, the bind result (and port), each request's id + ok/error status, and shutdown. If `--ping` can't connect, or the plugin doesn't seem to be running after a restart, check this file first. If it's not being created at all, the plugin likely failed to load -- check iClone's **Script > Console Log** window too. ## Threading rule (read this before extending the plugin) `RLPy` is **not thread-safe** and must only be touched on iClone's Qt main thread. In `main.py`: - The socket-accept thread and per-connection handler threads (`_accept_loop`, `_handle_conn`) only move bytes and put/get on a `queue.Queue`. They never call `RLPy` or touch Qt objects. - A `QTimer` created in `initialize_plugin()` (which itself runs on the main thread) polls that queue every ~50 ms and does all the actual `exec()` / `RLPy` work in `_drain_queue()` / `_execute()`. If you add new functionality to the plugin, keep any RLPy-touching code inside that QTimer callback path. Calling RLPy from a socket thread can crash or corrupt iClone. ## Long-running code freezes iClone's UI Submitted code runs **synchronously on the Qt main thread**. iClone's UI (and the bridge itself, since the same timer that answers other requests is blocked) will freeze for the duration of your `--exec`/`--file` call. This is fine for anything that takes a few seconds (an FBX export blocks the UI anyway) but avoid submitting code with long sleeps or unbounded loops. ## Environment variable - `TINQS_ICLONE_BRIDGE_PORT` -- set in iClone's process environment before launch to override the default port `18800`. Pass the matching `--port` to the client. ## Security note The bridge executes arbitrary Python with full `RLPy` access. It only binds to `127.0.0.1` -- there is no authentication beyond "only processes on this machine can connect." Do not change the bind address.