# Tutorial 4: Robot Arm — Joint Control & Pick/Drop **Source files:** - [`examples/arm/pick_drop_arm_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/pick_drop_arm_demo.py) — low-level callback approach - [`examples/arm/pick_drop_arm_action_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/pick_drop_arm_action_demo.py) — action-queue approach This tutorial demonstrates **fixed-base robot arm** simulation: controlling joints, picking up objects at the end-effector, and dropping them at target locations. **What you'll learn:** - Spawning a URDF arm with `use_fixed_base=True` - Controlling joints with `set_all_joints_targets()` and `JointAction` - Kinematic vs physics joint control (how `mass` / `physics` affect behaviour) - Link-level object attachment (`attach_link` parameter) - Building a pick/drop cycle with low-level callbacks **and** with the action queue If you haven't read [Tutorial 2 — Action System](action-system), do so first — this tutorial builds on the `PickAction` / `DropAction` concepts introduced there. --- ## 1. Fixed-Base Arm Setup ```python arm_agent = Agent.from_urdf( urdf_path="robots/arm_robot.urdf", pose=Pose.from_xyz(0, 0, 0), use_fixed_base=True, # pin base to world sim_core=sim_core, ) ``` `use_fixed_base=True` prevents the arm from falling or sliding. The base link is pinned to the world frame at the given pose. --- ## 2. Kinematic vs Physics Joint Control How joints are driven depends on two factors: the agent's `mass` and the simulation's `physics` setting. | `mass` | `physics` | Joint control method | Behaviour | |--------|-----------|---------------------|-----------| | `> 0` | `True` | PyBullet motor control (`setJointMotorControl2`) | Physics-accurate; torque/velocity limits from motor | | `0.0` | any | Kinematic interpolation (`resetJointState`) | Smooth movement respecting URDF `` | | `> 0` | `False` | Kinematic interpolation (auto-override) | Warning logged; kinematic used because `stepSimulation()` not called | **Kinematic mode** (`mass=0.0`) is recommended for arm demonstrations and fleet-scale scenarios where physics fidelity is not needed. Joints move smoothly at URDF-defined velocity limits without requiring `stepSimulation()`. > **Note:** When `physics=False`, even `mass > 0` agents use kinematic joint control > because motor commands have no effect without `stepSimulation()`. An info-level > message is logged once (per process) when this fallback activates. ### Performance Kinematic mode is **significantly faster** than physics mode for joint control because `stepSimulation()` is skipped entirely. Joint positions are cached internally (`_kinematic_joint_positions`), eliminating per-step `p.getJointState()` calls. See [Benchmark Results — Arm Joint Control](../benchmarking/results) for scaling data from a representative test environment. --- ## 3. Joint Control API ```python # Set all joints at once arm_agent.set_all_joints_targets([1.5, 1.5, 1.5, 0.0]) # Set a single joint by index arm_agent.set_joint_target(joint_index=0, target_position=1.5) # Set by joint name arm_agent.set_joint_target_by_name("base_to_shoulder", 1.5) # Check arrival if arm_agent.are_all_joints_at_targets([1.5, 1.5, 1.5, 0.0], tolerance=0.05): print("Arm is in position") ``` These methods work identically regardless of kinematic/physics mode — the underlying implementation switches transparently. --- ## 4. Link-Level Object Attachment For arm robots, objects are attached to a specific **link** (e.g., the end-effector) rather than the base: ```python PICK_LINK_INDEX = p.getNumJoints(arm_agent.body_id) - 1 # last link arm_agent.attach_object( box_sim, parent_link_index=PICK_LINK_INDEX, relative_pose=Pose.from_xyz(0, 0, 0.14), # offset from link frame ) ``` The attached object follows the link as joints move. Use `arm_agent.detach_object(box_sim)` to release. --- ## 5. Approach A — Low-Level Callback (`pick_drop_arm_demo.py`) The callback approach uses a state machine driven by `are_all_joints_at_targets()`: ```python def pick_drop_callback(sim_core, dt): if step_state == 0: arm_agent.set_all_joints_targets(joint_init) if arm_agent.are_all_joints_at_targets(joint_init, tolerance=0.05): advance_state() elif step_state == 1: arm_agent.set_all_joints_targets(pick_joints) if arm_agent.are_all_joints_at_targets(pick_joints, tolerance=0.05): arm_agent.attach_object(box_sim, parent_link_index=PICK_LINK_INDEX, ...) advance_state() # ... more states for drop, reverse cycle ``` This gives full control but requires manual state management. --- ## 6. Approach B — Action Queue (`pick_drop_arm_action_demo.py`) The action-queue approach expresses the same cycle declaratively: ```python from pybullet_fleet.action import JointAction, PickAction, DropAction, WaitAction actions = [ JointAction(target_joint_positions=joint_init, tolerance=0.05), JointAction(target_joint_positions=pick_joints, tolerance=0.05), PickAction( target_object_id=box_sim.body_id, use_approach=False, # arm is already positioned by JointAction attach_link=PICK_LINK_INDEX, attach_relative_pose=Pose.from_xyz(0, 0, 0.14), ), JointAction(target_joint_positions=place_joints, tolerance=0.05), DropAction(drop_pose=box_place_pose, use_approach=False), JointAction(target_joint_positions=joint_init, tolerance=0.05), WaitAction(duration=0.5, action_type="idle"), ] arm_agent.add_action_sequence(actions) ``` **Key differences from mobile-robot actions:** | Parameter | Mobile robot | Arm robot | |-----------|-------------|-----------| | `use_approach` | `True` (navigate to approach waypoint) | `False` (arm is pre-positioned by `JointAction`) | | `attach_link` | `-1` (base link) | End-effector link index | | Movement before pick/drop | `MoveAction` (path following) | `JointAction` (joint targets) | --- ## 7. `JointAction` Reference `JointAction` moves all joints to target positions and completes when every joint is within `tolerance` of its target: ```python JointAction( target_joint_positions=[1.5, 1.5, 1.5, 0.0], tolerance=0.05, # radians for revolute, metres for prismatic; default 0.01 max_force=500.0, # only used in physics mode ) ``` **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `target_joint_positions` | `list` or `dict` | required | Target positions — list by index or dict by joint name. Units: radians (revolute), metres (prismatic). | | `tolerance` | `float`, `dict`, or `None` | `None` | Completion threshold per joint. See [Tolerance](#tolerance) below. | | `max_force` | `float` | `500.0` | Motor force (N·m) — only used in physics mode | ### Tolerance `tolerance` controls how close each joint must be to its target before the action completes. It accepts several forms: | Form | Example | Behaviour | |------|---------|----------| | **Scalar** (`float`) | `0.05` | Same threshold for every joint | | **List** (`list`) | `[0.005, 0.05, 0.01, 0.01, 0.01]` | Per-joint by absolute joint index; out-of-range indices fall back to the class default (0.01) | | **Dict** (`{name: float}`) | `{"rail_joint": 0.005, "base_to_shoulder": 0.05}` | Per-joint by name; joints not listed use the class default (0.01) | | **`None`** (default) | — | Resolved from `agent.joint_tolerance` on the first tick (see below) | #### Agent-level fallback When `tolerance` is `None`, the action resolves it from the agent's `joint_tolerance` property on the first `execute()` call and **writes the resolved value back** to `action.tolerance` — making it inspectable after the action starts: ```python # Set agent-level tolerance at construction agent = Agent.from_urdf( urdf_path="robots/rail_arm_robot.urdf", ..., joint_tolerance={"rail_joint": 0.005, "base_to_shoulder": 0.05}, ) # Action with no explicit tolerance → inherits from agent action = JointAction(target_joint_positions=[0.3, 0.2, 0.0, 0.0, 0.0]) assert action.tolerance is None # before execute agent.add_action(action) # ... after first tick ... assert action.tolerance == {"rail_joint": 0.005, "base_to_shoulder": 0.05} ``` The full fallback chain is: 1. **Action-level** — `JointAction(tolerance=0.05)` takes priority 2. **Agent-level** — `agent.joint_tolerance` (set at construction or via property) 3. **Class default** — `0.01` (applied when both are `None`) This is especially useful for **mixed prismatic + revolute** arms where prismatic joints measure in metres and revolute joints in radians. A dict tolerance lets you tighten prismatic accuracy (e.g., ±5 mm) without over-constraining revolute joints. ### Prismatic (linear) joints `JointAction` works transparently with prismatic joints — target values are in **metres** instead of radians. See [`examples/arm/rail_arm_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/rail_arm_demo.py) for a complete example using a rail arm with 1 prismatic + 4 revolute joints. --- ## 8. Running the Demos ```bash # Low-level callback approach (1 arm) python examples/arm/pick_drop_arm_demo.py # Action-queue approach (1 arm) python examples/arm/pick_drop_arm_action_demo.py # 100 arms — AgentManager grid spawn + bulk action management python examples/scale/pick_drop_arm_100robots_demo.py # Rail arm (prismatic + revolute) with EE control and per-joint tolerance python examples/arm/rail_arm_demo.py # Mobile manipulator (base + arm) — kinematic IK pick/drop python examples/arm/mobile_manipulator_demo.py ``` Both single-arm demos use kinematic mode by default (`physics=False`). To switch to physics mode, change the `SimulationParams` line at the top of the file. ### Switching Robot Models All arm demos accept a `--robot` argument to swap the arm model. Pass an arm model name or a direct URDF path: ```bash python examples/arm/pick_drop_arm_demo.py --robot kuka_iiwa python examples/arm/pick_drop_arm_action_demo.py --robot arm_robot ``` | `--robot` default | Alternatives | |-------------------|-------------| | `panda` | `kuka_iiwa`, `arm_robot` | See [Tutorial 6 — Robot Models](robot-models) for the full model resolution system. The 100-arm demo combines `AgentManager` / `GridSpawnParams` (see [Tutorial 3](multi-robot-fleet)) with `JointAction` for fleet-scale arm control. --- ## See Also - [Tutorial 1 — Spawning Objects](spawning-objects): `from_urdf`, `set_all_joints_targets` basics - [Tutorial 2 — Action System](action-system): `PickAction`, `DropAction` for mobile robots - [Tutorial 5 — EE Control & IK](arm-ee-control): control the arm by EE position instead of joint angles - [Mobile Manipulator Demo](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/mobile_manipulator_demo.py): kinematic mobile base + arm with IK pick/drop - [Rail Arm Demo](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/rail_arm_demo.py): prismatic + revolute joint control with per-joint tolerance - [Architecture Overview](../architecture/overview): kinematic joint interpolation internals - [Benchmark Results](../benchmarking/results): arm joint control performance data