Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion embodichain/lab/sim/sim_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ def get_robot_uid_list(self) -> List[str]:

def enable_gizmo(
self, uid: str, control_part: str | None = None, gizmo_cfg: object = None
) -> None:
) -> Gizmo:
"""Enable gizmo control for any simulation object (Robot, RigidObject, Camera, etc.).
Comment on lines 1131 to 1134
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type was changed to Gizmo, but this method still has code paths that return without a value (e.g., when the gizmo already exists or the UID isn't found). To keep the API consistent, either return the existing gizmo / raise, or change the annotation to Gizmo | None and return None explicitly on those paths.

Copilot uses AI. Check for mistakes.

Args:
Expand Down Expand Up @@ -1190,6 +1190,8 @@ def enable_gizmo(
f"Failed to create gizmo for {object_type} '{uid}' with control_part '{control_part}': {e}"
)

return gizmo

def disable_gizmo(self, uid: str, control_part: str | None = None) -> None:
"""Disable and remove gizmo for a robot.

Expand Down
177 changes: 177 additions & 0 deletions embodichain/lab/sim/utility/gizmo_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
"""

from typing import Callable
from typing import TYPE_CHECKING
from dexsim.types import TransformMask

if TYPE_CHECKING:
from embodichain.lab.sim.objects import Robot


def create_gizmo_callback() -> Callable:
"""Create a standard gizmo transform callback function.
Expand All @@ -44,3 +48,176 @@ def gizmo_transform_callback(node, translation, rotation, flag):
node.set_rotation_rpy(rotation)

return gizmo_transform_callback


def run_gizmo_robot_control_loop(
robot: "Robot", control_part: str = "arm", end_link_name: str | None = None
):
"""Run a control loop for testing gizmo controls on a robot.

This function implements a control loop that allows users to manipulate a robot
using gizmo controls with keyboard input for additional commands.

Args:
robot (Robot): The robot to control with the gizmo.
control_part (str, optional): The part of the robot to control. Defaults to "arm".
end_link_name (str | None, optional): The name of the end link for FK calculations. Defaults to None.

Keyboard Controls:
Q/ESC: Exit the control loop
P: Print current robot state (joint positions, end-effector pose)
G: Toggle gizmo visibility
R: Reset robot to initial pose
I: Print control information
"""
import select
import sys
import tty
import termios
import time
import numpy as np

np.set_printoptions(precision=5, suppress=True)

from embodichain.lab.sim import SimulationManager
from embodichain.lab.sim.objects import Robot
from embodichain.lab.sim.solvers import PinkSolverCfg

from embodichain.utils.logger import log_info, log_warning, log_error

sim = SimulationManager.get_instance()

# Enter auto-update mode.
sim.set_manual_update(False)

Comment on lines +90 to +92
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper switches the global simulation into auto-update mode via sim.set_manual_update(False) but never restores the previous setting on exit. That can unexpectedly change how the rest of the program advances the simulation after this loop ends. Consider saving the prior mode (if available) and restoring it in finally, or explicitly setting it back to the project default (manual update True).

Copilot uses AI. Check for mistakes.
# Replace robot's default solver with PinkSolver for gizmo control.
robot_solver = robot.get_solver(name=control_part)
control_part_link_names = robot.get_control_part_link_names(name=control_part)
end_link_name = (
control_part_link_names[-1] if end_link_name is None else end_link_name
)
pink_solver_cfg = PinkSolverCfg(
urdf_path=robot.cfg.fpath,
end_link_name=end_link_name,
root_link_name=robot_solver.root_link_name,
pos_eps=1e-2,
Comment on lines +94 to +103
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

robot.get_solver(name=control_part) can return None (e.g., if solvers aren’t configured), and get_control_part_link_names can return an empty list when control_part is invalid. This code dereferences robot_solver.root_link_name and indexes control_part_link_names[-1] unconditionally, which can crash. Add validation/error handling for missing solver / missing links before building PinkSolverCfg.

Copilot uses AI. Check for mistakes.
rot_eps=5e-2,
max_iterations=300,
dt=0.1,
)
robot.init_solver(cfg={control_part: pink_solver_cfg})

# Enable gizmo for the robot
gizmo = sim.enable_gizmo(uid=robot.uid, control_part=control_part)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable gizmo is not used.

Suggested change
gizmo = sim.enable_gizmo(uid=robot.uid, control_part=control_part)
sim.enable_gizmo(uid=robot.uid, control_part=control_part)

Copilot uses AI. Check for mistakes.

Comment on lines +82 to +112
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several imports/variables in this helper are unused (Robot imported inside the function, log_warning, log_error, and the gizmo local). Please remove unused imports/assignments to keep this utility lean and avoid implying side effects that don’t exist.

Copilot uses AI. Check for mistakes.
# Store initial robot configuration
initial_qpos = robot.get_qpos(name=control_part)

gizmo_visible = True

log_info("\n=== Gizmo Robot Control ===")
log_info("Gizmo Controls:")
log_info(" Use the 3D gizmo to drag and manipulate the robot")
log_info("\nKeyboard Controls:")
log_info(" Q/ESC: Exit control loop")
log_info(" P: Print current robot state")
log_info(" G: Toggle gizmo visibility")
log_info(" R: Reset robot to initial pose")
log_info(" I: Print this information again")

# Save terminal settings
old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
Comment on lines +129 to +130
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

termios.tcgetattr expects a file descriptor (int) but this passes sys.stdin (a file object). This will raise TypeError on most platforms. Use sys.stdin.fileno() (and consider handling non-TTY stdin cases) when saving terminal settings.

Suggested change
old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
stdin_fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(stdin_fd)
tty.setcbreak(stdin_fd)

Copilot uses AI. Check for mistakes.

def get_key():
"""Non-blocking keyboard input."""
if select.select([sys.stdin], [], [], 0)[0]:
return sys.stdin.read(1)
return None

try:
while True:
time.sleep(0.033) # ~30Hz
sim.update_gizmos()

# Check for keyboard input
key = get_key()

if key:
# Exit controls
if key in ["q", "Q", "\x1b"]: # Q or ESC
log_info("Exiting gizmo control loop...")
sim.disable_gizmo(uid=robot.uid, control_part=control_part)
if robot_solver:
robot.init_solver(
cfg={control_part: robot_solver.cfg}
) # Restore original solver
break

# Print robot state
elif key in ["p", "P"]:
current_qpos = robot.get_qpos(name=control_part)
eef_pose = robot.compute_fk(name=control_part, qpos=current_qpos)
log_info(f"\n=== Robot State ===")
log_info(f"Control part: {control_part}")
log_info(f"Joint positions: {current_qpos.squeeze().tolist()}")
log_info(f"End-effector pose:\n{eef_pose.squeeze().numpy()}")

if eef_pose is None:
Comment on lines +160 to +166
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eef_pose is logged via eef_pose.squeeze().numpy() before checking whether compute_fk returned None, and .numpy() is not valid on a torch tensor without moving to CPU/detaching. This will throw when eef_pose is None or a CUDA tensor. Only log the pose after the None check, and convert with eef_pose.detach().cpu().numpy() (or similar).

Copilot uses AI. Check for mistakes.
log_info(
"End-effector pose unavailable: compute_fk returned None "
f"for control part '{control_part}'."
)
else:
eef_pose_np = eef_pose.detach().cpu().numpy().squeeze()
log_info(f"End-effector pose:\n{eef_pose_np}")
elif key in ["g", "G"]:
if gizmo_visible:
sim.set_gizmo_visibility(
uid=robot.uid, control_part=control_part, visible=False
)
log_info("Gizmo hidden")
gizmo_visible = False
else:
sim.set_gizmo_visibility(
uid=robot.uid, control_part=control_part, visible=True
)
log_info("Gizmo shown")
gizmo_visible = True

# Reset to initial pose
elif key in ["r", "R"]:
# TODO: Workaround for reset. Gizmo pose should be fixed in the future.
sim.disable_gizmo(uid=robot.uid, control_part=control_part)
robot.clear_dynamics()
robot.set_qpos(qpos=initial_qpos, name=control_part, target=False)
sim.enable_gizmo(uid=robot.uid, control_part=control_part)
log_info("Robot reset to initial pose")

# Print info
elif key in ["i", "I"]:
log_info("\n=== Gizmo Robot Control ===")
log_info("Gizmo Controls:")
log_info(" Use the 3D gizmo to drag and manipulate the robot")
log_info("\nKeyboard Controls:")
log_info(" Q/ESC: Exit control loop")
log_info(" P: Print current robot state")
log_info(" G: Toggle gizmo visibility")
log_info(" R: Reset robot to initial pose")
log_info(" I: Print this information again")

except KeyboardInterrupt:
sim.disable_gizmo(uid=robot.uid, control_part=control_part)
if robot_solver:
robot.init_solver(
cfg={control_part: robot_solver.cfg}
) # Restore original solver
log_info("\nControl loop interrupted by user (Ctrl+C)")

finally:
try:
# Restore terminal settings
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
except:
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except:
except:
# Intentionally ignore errors while restoring terminal settings during shutdown

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except block directly handles BaseException.

Suggested change
except:
except Exception:

Copilot uses AI. Check for mistakes.
pass
log_info("Gizmo control loop terminated")
Loading