Files
Hermes 79aa35df6b v0.2: Working bridge — temp-file output, eval with stdout capture
- Changed from exec(open()) to temp-file output pattern (/tmp/hhb_out_*.txt)
- eval command now captures print() via io.StringIO redirect
- get-selected, get-param, get-inputs: working with temp-file output
- Verified live on Houdini 21.0.729 (Fedora, Wayland)
- Known limitation: python /tmp/script.py via hcommand does not capture print();
  workaround is writing to /tmp/hhb_out_*.txt
2026-07-12 12:28:53 +00:00

233 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────
# hhb — Hermes-Houdini Bridge Client
# Sends Python/hou commands to a running Houdini session
# via SSH + hcommand. Results returned via temp file.
# ──────────────────────────────────────────────────────────
set -euo pipefail
HOST="${HHB_HOST:-niklas@192.168.178.39}"
PORT="${HHB_PORT:-12345}"
HCOMMAND="${HHB_HCOMMAND:-/opt/hfs21.0/bin/hcommand}"
usage() {
cat <<'EOF'
Usage: hhb <command> [args]
Commands:
eval <python_code> Execute Python code in the running Houdini session.
script <file.py> Send a Python script file to Houdini and execute it.
get-selected Print paths and types of selected nodes.
get-param <parm> Get value of a parameter on the first selected node.
set-param <parm> <val> 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
}
# ── Core: write Python script, execute, read back output ─
run_houdini_python() {
# $1: Python code (as a string)
# Writes code to /tmp/hhb_script.py, executes via hcommand,
# reads output from /tmp/hhb_out.txt
local python_code="$1"
local sid="$$"
# Upload the script
printf '%s' "$python_code" | ssh "$HOST" "cat > /tmp/hhb_script_${sid}.py"
# Execute via hcommand
ssh "$HOST" "$HCOMMAND $PORT 'python /tmp/hhb_script_${sid}.py'" 2>/dev/null || true
# Read output file
ssh "$HOST" "cat /tmp/hhb_out_${sid}.txt 2>/dev/null" || echo ""
# Cleanup
ssh "$HOST" "rm -f /tmp/hhb_script_${sid}.py /tmp/hhb_out_${sid}.txt" 2>/dev/null || true
}
# Wrap user code to capture output via temp file
wrap_python() {
local code="$1"
local sid="$$"
cat <<PYEOF
import hou, json, sys
try:
${code}
except Exception as e:
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
f.write(json.dumps({"error": str(e)}))
PYEOF
}
# ── 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 <python_code>"; exit 1; }
local sid="$$"
local code
code=$(cat <<PYEOF
import hou, json, sys, io
_stdout = io.StringIO()
sys.stdout = _stdout
$*
sys.stdout = sys.__stdout__
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
f.write(_stdout.getvalue())
PYEOF
)
run_houdini_python "$code"
}
cmd_script() {
[[ $# -ge 1 ]] || { echo "Usage: hhb script <file.py>"; exit 1; }
local script_path="$1"
local sid="$$"
[[ -f "$script_path" ]] || { echo "File not found: $script_path"; exit 1; }
# Read the script, wrap in output capture
local code
code=$(cat "$script_path")
local wrapped
wrapped=$(cat <<PYEOF
import hou, json
try:
${code}
except Exception as e:
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
f.write(json.dumps({"error": str(e)}))
PYEOF
)
run_houdini_python "$wrapped"
}
cmd_get_selected() {
local sid="$$"
local code
code=$(cat <<'PYEOF'
nodes = hou.selectedNodes()
lines = []
if not nodes:
lines.append("No nodes selected.")
else:
for n in nodes:
lines.append(f"{n.path():50s} [{n.type().name()}]")
with open('/tmp/hhb_out_PLACEHOLDER.txt', 'w') as f:
f.write("\n".join(lines))
PYEOF
)
code="${code//PLACEHOLDER/$sid}"
run_houdini_python "$code"
}
cmd_get_param() {
[[ $# -ge 1 ]] || { echo "Usage: hhb get-param <parm_name>"; exit 1; }
local parm="$1"
local sid="$$"
local code
code=$(cat <<PYEOF
nodes = hou.selectedNodes()
if not nodes:
msg = "No nodes selected."
else:
p = nodes[0].parm("PARM_PLACEHOLDER")
if p is None:
msg = f'Parameter "PARM_PLACEHOLDER" not found on {nodes[0].path()}'
else:
msg = f'{nodes[0].path()}.{p.name()} = {p.eval()}'
with open('/tmp/hhb_out_PLACEHOLDER_SID.txt', 'w') as f:
f.write(msg)
PYEOF
)
code="${code//PARM_PLACEHOLDER/$parm}"
code="${code//PLACEHOLDER_SID/$sid}"
run_houdini_python "$code"
}
cmd_set_param() {
[[ $# -ge 2 ]] || { echo "Usage: hhb set-param <parm_name> <value>"; exit 1; }
local parm="$1"
local val="$2"
local sid="$$"
local code
code=$(cat <<PYEOF
nodes = hou.selectedNodes()
if not nodes:
msg = "No nodes selected."
else:
p = nodes[0].parm("PARM_PLACEHOLDER")
if p is None:
msg = f'Parameter "PARM_PLACEHOLDER" not found on {nodes[0].path()}'
else:
p.set("VAL_PLACEHOLDER")
msg = f'{nodes[0].path()}.{p.name()} = {p.eval()} OK'
with open('/tmp/hhb_out_PLACEHOLDER_SID.txt', 'w') as f:
f.write(msg)
PYEOF
)
code="${code//PARM_PLACEHOLDER/$parm}"
code="${code//VAL_PLACEHOLDER/$val}"
code="${code//PLACEHOLDER_SID/$sid}"
run_houdini_python "$code"
}
cmd_get_inputs() {
local sid="$$"
local code
code=$(cat <<'PYEOF'
lines = []
nodes = hou.selectedNodes()
if not nodes:
lines.append("No nodes selected.")
else:
for n in nodes:
lines.append(f"\n{n.path()} [{n.type().name()}]")
for i in range(len(n.inputConnectors())):
conns = n.inputConnections(i)
if conns:
for c in conns:
lines.append(f" input {i} <- {c.inputNode().path()} [{c.inputNode().type().name()}]")
else:
lines.append(f" input {i} <- (none)")
with open('/tmp/hhb_out_PLACEHOLDER.txt', 'w') as f:
f.write("\n".join(lines))
PYEOF
)
code="${code//PLACEHOLDER/$sid}"
run_houdini_python "$code"
}
# ── 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