#!/usr/bin/env bash # ────────────────────────────────────────────────────────── # hhb — Hermes-Houdini Bridge Client # Sends Python/hou commands to a running Houdini session # via SSH + hcommand. # ────────────────────────────────────────────────────────── set -euo pipefail HOST="${HHB_HOST:-niklas@192.168.178.39}" PORT="${HHB_PORT:-12345}" HCOMMAND="${HHB_HCOMMAND:-/opt/hfs21.0/bin/hcommand}" WORKSPACE="${HHB_WORKSPACE:-/opt/data/projects/hermes-houdini-bridge}" usage() { cat <<'EOF' Usage: hhb [args] Commands: eval Execute Python code in the running Houdini session. Example: hhb eval "print(hou.selectedNodes())" script Send a Python script file to Houdini and execute it. Example: hhb script ./examples/create_pyro_setup.py get-selected Print paths and types of selected nodes. get-param Get value of a parameter on the first selected node. set-param Set a parameter on the first selected node. get-inputs Print input connections of the first selected node. status Check if the bridge port is open. help Show this help. EOF exit 0 } # ── Helpers ────────────────────────────────────────────── run_remote_python() { # Write Python code to a temp file, execute via hcommand. # This avoids quoting hell with complex Python snippets. local python_code="$1" local tmpfile="/tmp/hhb_$$.py" # Upload script printf '%s' "$python_code" | ssh "$HOST" "cat > $tmpfile" # Execute via hscript's python command — reads file and execs local output output=$(ssh "$HOST" \ "$HCOMMAND $PORT \"python exec(open('$tmpfile').read())\"" 2>&1) || { local rc=$? ssh "$HOST" "rm -f $tmpfile" 2>/dev/null echo "hhb: hcommand failed (exit $rc)" >&2 echo "$output" >&2 return $rc } # Cleanup ssh "$HOST" "rm -f $tmpfile" 2>/dev/null # hcommand output has a prefix line like "-> foobar", strip it echo "$output" | sed 's/^->\s*//' } # ── Commands ────────────────────────────────────────────── cmd_status() { local output output=$(ssh "$HOST" "$HCOMMAND $PORT openport" 2>&1) || { echo "Bridge INACTIVE — Houdini not reachable or port $PORT closed." return 1 } echo "$output" | sed 's/^->\s*//' } cmd_eval() { [[ $# -ge 1 ]] || { echo "Usage: hhb eval "; exit 1; } run_remote_python "$*" } cmd_script() { [[ $# -ge 1 ]] || { echo "Usage: hhb script "; exit 1; } local script_path="$1" [[ -f "$script_path" ]] || { echo "File not found: $script_path"; exit 1; } scp -q "$script_path" "$HOST:/tmp/hhb_script_$$.py" local output output=$(ssh "$HOST" \ "$HCOMMAND $PORT \"python exec(open('/tmp/hhb_script_$$.py').read())\"" 2>&1) || { local rc=$? ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null echo "hhb: hcommand failed (exit $rc)" >&2 echo "$output" >&2 return $rc } ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null echo "$output" | sed 's/^->\s*//' } cmd_get_selected() { run_remote_python ' import hou nodes = hou.selectedNodes() if not nodes: print("No nodes selected.") else: for n in nodes: print(f"{n.path():50s} [{n.type().name()}]") ' } cmd_get_param() { [[ $# -ge 1 ]] || { echo "Usage: hhb get-param "; exit 1; } local parm="$1" run_remote_python " import hou nodes = hou.selectedNodes() if not nodes: print('No nodes selected.') else: p = nodes[0].parm('$parm') if p is None: print(f'Parameter \"$parm\" not found on {nodes[0].path()}') else: print(f'{nodes[0].path()}.{p.name()} = {p.eval()}') " } cmd_set_param() { [[ $# -ge 2 ]] || { echo "Usage: hhb set-param "; exit 1; } local parm="$1" local val="$2" run_remote_python " import hou nodes = hou.selectedNodes() if not nodes: print('No nodes selected.') else: p = nodes[0].parm('$parm') if p is None: print(f'Parameter \"$parm\" not found on {nodes[0].path()}') else: p.set('$val') print(f'{nodes[0].path()}.{p.name()} = {p.eval()} ✓') " } cmd_get_inputs() { run_remote_python ' import hou nodes = hou.selectedNodes() if not nodes: print("No nodes selected.") else: for n in nodes: inputs = n.inputConnectors() print(f"\n{n.path()} [{n.type().name()}]") for i, conn in enumerate(inputs): in_nodes = n.inputConnections(i) if hasattr(n, "inputConnections") else [] if in_nodes: for c in in_nodes: print(f" input {i} ← {c.inputNode().path()} [{c.inputNode().type().name()}]") else: print(f" input {i} ← (none)") ' } # ── Main ───────────────────────────────────────────────── [[ $# -ge 1 ]] || usage CMD="$1" shift || true case "$CMD" in eval) cmd_eval "$@" ;; script) cmd_script "$@" ;; get-selected) cmd_get_selected ;; get-param) cmd_get_param "$@" ;; set-param) cmd_set_param "$@" ;; get-inputs) cmd_get_inputs ;; status) cmd_status ;; help|--help|-h) usage ;; *) echo "Unknown command: $CMD" usage ;; esac