Metal Rod Repackaging (Angled Tray)
This example demonstrates how to use Tzara, the Telekinesis Physical AI Agent, to build a repackaging pipeline. The source tray is tilted roughly 23 degrees around the robot's X axis, so Tzara accounts for the tilt when computing pick poses — keeping the TCP aligned with the tray surface for all grid slots. Parts are picked from the angled tray and dropped (unstructured) into a flat destination box grid.
Demo
UR10e picking aluminum rods from an angled tray and placing into a box grid
The Natural Language Instruction
I have an angled tray where aluminum cylindrical parts sorted in a grid structure. The tray has about 23 degrees rotation around X axis of robot base. First pick pose of the grid on the tray i have measured is [-0.3869, 1.3308, 0.1729, -180, -23, 90], note that this is starting pick on the angled tray (that is why -23 deg Y). I need to pick the objects and place in a box also in a grid format. You can drop the objects instead of placing them. Add logging at each step.The Generated Code
python
# Repackaging pipeline: UR10e + OnRobot RG6
# Picks aluminum rods from a tray tilted 23 degrees around the robot X axis.
# Row-to-row travel has both a Y component and a Z component due to the tilt.
# Parts are dropped (unstructured) into a flat destination box grid.
import math
from loguru import logger
from telekinesis.synapse.robots.manipulators.universal_robots import UniversalRobotsUR10E
from telekinesis.synapse.tools.parallel_grippers.onrobot import OnRobotRG6
# ============================================================
# Constants
# ============================================================
ROBOT_IP = "192.168.1.2"
GRIPPER_IP = "192.168.1.1"
GRIPPER_PROTOCOL = "MODBUS_TCP"
# Tray tilt
TRAY_TILT_DEG = 23.0
TRAY_TILT_RAD = math.radians(TRAY_TILT_DEG)
# Pick grid — tray is tilted, so rows move along both Y and Z in the base frame.
# Measured first pick pose: [x, y, z, rx, ry, rz]
PICK_ORIGIN = [-0.3869, 1.3308, 0.1729] # meters
PICK_RPY = [-180.0, -23.0, 90.0] # degrees; keeps TCP normal to tray surface
PICK_GRID_ROWS = 4
PICK_GRID_COLS = 5
ROW_PITCH = 0.065 # distance between rows measured along the tray surface (m)
COL_PITCH = 0.065 # distance between columns along X in the base frame (m)
# Row step projected into base frame (tray slopes away from robot, so Z decreases)
ROW_DY_BASE = ROW_PITCH * math.cos(TRAY_TILT_RAD) # positive Y per row
ROW_DZ_BASE = -ROW_PITCH * math.sin(TRAY_TILT_RAD) # negative Z per row (slopes down)
# Drop grid — flat box, no orientation constraint (drop, not structured place)
DROP_ORIGIN = [0.100, 0.820, 0.195] # meters, first drop slot
DROP_RPY = [180.0, 0.0, 90.0] # TCP pointing down, flat orientation
DROP_GRID_ROWS = 5
DROP_GRID_COLS = 4
DROP_ROW_PITCH = -0.070 # row spacing along Y (m)
DROP_COL_PITCH = 0.070 # column spacing along X (m)
# Z levels (meters)
Z_APPROACH = 0.350 # safe clearance height above both the tray and the box
# Gripper settings (mm / N)
GRIPPER_OPEN_MM = 65.0
GRIPPER_CLOSE_MM = 14.0
GRIPPER_FORCE_N = 45.0
# Motion parameters
CART_SPEED = 0.45 # m/s
CART_ACCEL = 0.65 # m/s^2
# Home joint pose (degrees)
HOME_JOINT_DEG = [90.0, -90.0, -90.0, -90.0, 90.0, 0.0]
JOINT_SPEED_DEG = 45.0
JOINT_ACCEL_DEG = 90.0
# ============================================================
# Helper functions
# ============================================================
def generate_pick_poses():
"""
Generate pick poses for the angled tray.
Column pitch moves along X only (tray width direction).
Row pitch moves along Y and Z in the base frame due to the 23-degree tilt.
"""
poses = []
for r in range(PICK_GRID_ROWS):
for c in range(PICK_GRID_COLS):
x = PICK_ORIGIN[0] + c * COL_PITCH
y = PICK_ORIGIN[1] + r * ROW_DY_BASE
z = PICK_ORIGIN[2] + r * ROW_DZ_BASE
poses.append([x, y, z] + list(PICK_RPY))
logger.debug(
f"Generated {len(poses)} pick poses "
f"(rows={PICK_GRID_ROWS}, cols={PICK_GRID_COLS}, "
f"dy_per_row={ROW_DY_BASE:.4f} m, dz_per_row={ROW_DZ_BASE:.4f} m)"
)
return poses
def generate_drop_poses():
"""Generate drop poses for the flat destination box grid (row-major)."""
poses = []
for r in range(DROP_GRID_ROWS):
for c in range(DROP_GRID_COLS):
x = DROP_ORIGIN[0] + c * DROP_COL_PITCH
y = DROP_ORIGIN[1] + r * DROP_ROW_PITCH
z = DROP_ORIGIN[2]
poses.append([x, y, z] + list(DROP_RPY))
logger.debug(f"Generated {len(poses)} drop poses ({DROP_GRID_ROWS} rows x {DROP_GRID_COLS} cols)")
return poses
def _approach(pose, z_safe):
"""Return a copy of pose with z replaced by z_safe (base-frame clearance height)."""
p = list(pose)
p[2] = z_safe
return p
def pick_part(robot, gripper, pick_pose, index):
"""Execute the pick sequence for a single rod on the angled tray."""
approach = _approach(pick_pose, Z_APPROACH)
logger.info(f"[Part {index}] Moving to pick approach")
logger.debug(f" approach = {approach}")
robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] At pick approach")
logger.info(f"[Part {index}] Opening gripper")
gripper.move(position=GRIPPER_OPEN_MM, force=GRIPPER_FORCE_N)
logger.success(f"[Part {index}] Gripper open")
logger.info(f"[Part {index}] Descending to pick pose on angled tray")
logger.debug(f" pick pose = {pick_pose}")
robot.set_cartesian_pose(pick_pose, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] At pick pose")
logger.info(f"[Part {index}] Closing gripper to grasp rod")
gripper.move(position=GRIPPER_CLOSE_MM, force=GRIPPER_FORCE_N)
logger.success(f"[Part {index}] Rod grasped")
logger.info(f"[Part {index}] Retreating from tray")
robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] Retreated from pick")
def drop_part(robot, gripper, drop_pose, index):
"""Move to the drop pose above the box and release the rod."""
approach = _approach(drop_pose, Z_APPROACH)
logger.info(f"[Part {index}] Moving to drop approach")
logger.debug(f" drop approach = {approach}")
robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] At drop approach")
logger.info(f"[Part {index}] Descending to drop pose")
logger.debug(f" drop pose = {drop_pose}")
robot.set_cartesian_pose(drop_pose, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] At drop pose")
logger.info(f"[Part {index}] Opening gripper to drop rod")
gripper.move(position=GRIPPER_OPEN_MM, force=GRIPPER_FORCE_N)
logger.success(f"[Part {index}] Rod dropped into box")
logger.info(f"[Part {index}] Retreating from drop pose")
robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
logger.success(f"[Part {index}] Retreated from drop")
def run_pipeline(robot, gripper):
"""Pick every rod from the angled tray and drop into the flat box grid."""
pick_poses = generate_pick_poses()
drop_poses = generate_drop_poses()
n_parts = min(len(pick_poses), len(drop_poses))
logger.info(f"Starting repackaging pipeline: {n_parts} rods to transfer")
logger.info("Moving to home pose before starting")
robot.set_joint_positions(HOME_JOINT_DEG, speed=JOINT_SPEED_DEG, acceleration=JOINT_ACCEL_DEG)
logger.success("At home pose")
for i in range(n_parts):
logger.info(f"===== Rod {i + 1} / {n_parts} =====")
pick_part(robot, gripper, pick_poses[i], i + 1)
drop_part(robot, gripper, drop_poses[i], i + 1)
logger.success(f"===== Rod {i + 1} complete =====")
logger.info("All rods transferred — returning to home pose")
robot.set_joint_positions(HOME_JOINT_DEG, speed=JOINT_SPEED_DEG, acceleration=JOINT_ACCEL_DEG)
logger.success("Pipeline complete")
# ============================================================
# Main
# ============================================================
def main():
robot = UniversalRobotsUR10E()
gripper = OnRobotRG6()
robot_connected = False
gripper_connected = False
try:
logger.info(f"Connecting to UR10e at {ROBOT_IP}")
robot.connect(ROBOT_IP)
robot_connected = True
logger.success("Robot connected")
logger.info(f"Connecting to OnRobot RG6 at {GRIPPER_IP} via {GRIPPER_PROTOCOL}")
gripper.connect(ip=GRIPPER_IP, protocol=GRIPPER_PROTOCOL)
gripper_connected = True
logger.success("Gripper connected")
run_pipeline(robot, gripper)
except Exception as e:
logger.exception(f"Pipeline failed: {type(e).__name__}: {e}")
finally:
if gripper_connected:
try:
logger.info("Disconnecting gripper")
gripper.disconnect()
except Exception as e:
logger.warning(f"Gripper disconnect failed: {e}")
if robot_connected:
try:
logger.info("Disconnecting robot")
robot.disconnect()
except Exception as e:
logger.warning(f"Robot disconnect failed: {e}")
logger.info("Cleanup complete")
if __name__ == "__main__":
main()
