Milestone 3 — Final Documentation & Analysis
| Due: May 8 | Weight: 15% | Team: Varad Jahagirdar, Dhiren Makwana, Sharat Mylavarapu |
Table of Contents
- 1. Graphical Abstract
- 2. Algorithm
- 2.6.1. Throw Controller
- 3. System Architecture
- 4. Benchmarking & Results
- 5. Ethical Impact Statement
- 6. Module Status
- 7.1. Overview
- 7.2. Arm Throw Controller
- 7.3. End Effector Logger
- 7.4. Joint State to Gazebo Bridge
- 7.5. Launch File Integration
- 7.6. Individual Contribution Summary
- 7. Status
- 7. Individual Contribution
1. Graphical Abstract
Mission: An autonomous mobile manipulation system built on the LoCoBot (Kobuki + WidowX 250s) navigates a cluttered arena, detects a magenta ball using a color camera and depth sensor, grasps it using geometric inverse kinematics, and returns to a throw target at the arena center — all within a single ROS 2 pipeline running in Gazebo Harmonic.
2. Algorithm
2.1 Full State Machine
The system runs a single shared /robot_state topic as a finite state machine. Every node listens and reacts to state transitions.
2.2 Beacon Trilateration — Varad Jahagirdar
Beacon Localization
Already fully documented in Milestone 2. The node continues to operate identically in M3.
Source: beacon_localization.py
Performance across 10 M3 trials: mean error 0.318 m, consistent with $\sigma = 0.3$ m injected noise.
2.3 Base Alignment Controller — Dhiren Makwana
The base alignment node uses Gazebo ground truth poses and a proportional controller to rotate and drive toward the ball:
Angular error:
\[e_\theta = \text{atan2}(b_y - r_y,\ b_x - r_x) - \theta_{\text{robot}}\]normalized to $[-\pi, \pi]$.
Control law:
\[\omega = 0.5 \cdot e_\theta \quad \text{(rotate phase)}\] \[v = 0.1\ \text{m/s}, \quad \omega = 0.3 \cdot e_\theta \quad \text{(drive phase)}\]| Alignment is declared complete when $d < 0.535$ m and $ | e_\theta | < 0.1$ rad. |
Source: base_alignment.py
2.4 Ball Detection — Dhiren Makwana
The color camera detects the magenta ball using a dual-range HSV mask:
\[\text{mask} = \text{inRange}(\text{HSV}, [140,100,100], [180,255,255])\ \cup\ \text{inRange}(\text{HSV}, [0,100,100], [10,255,255])\]The largest contour centroid gives pixel coordinates $(u, v)$. The depth value $z$ at that pixel is read from the depth image. The 3D ball position in camera frame is computed using the pinhole camera model:
\[x_{\text{cam}} = \frac{(u - c_x) \cdot z}{f_x}, \qquad y_{\text{cam}} = \frac{(v - c_y) \cdot z}{f_y}\]where $f_x = f_y = 277.19$ px, $c_x = 160$, $c_y = 120$ (320×240 image).
Published to /ball_camera_pos as a PointStamped in the camera frame.
Source: ball_detection.py
2.5 Arm Grasp & Geometric Inverse Kinematics — Varad Jahagirdar
The arm_grasp node activates when /robot_state receives ALIGNED. It tilts the camera down to observe the ball at close range, computes arm joint angles using geometric inverse kinematics, and executes a pre-grasp → grasp → lift sequence.
Source: arm_grasp.py
Grasp Sequence
ALIGNED received
→ tilt camera down (0.75 rad)
→ wait 2s for camera to stabilize
→ get ball 3D position (camera depth or Gazebo GT fallback)
→ compute geometric IK
→ open gripper (0.037 m)
→ rotate waist to face ball
→ move to pre-grasp (5cm above ball)
→ lower to grasp position
→ close gripper (0.015 m)
→ lift to pre-grasp height
→ return to arm home pose
→ publish GRASPED
Ball Position in Arm Frame
From camera (primary):
The depth camera is tilted down by $\alpha = 0.75$ rad. The ball position in camera frame $(x_c, y_c, z_c)$ — obtained from the depth unprojection in ball_detection.py — is rotated into arm base frame via a Y-axis rotation:
Camera offset from arm base link (verified via TF at tilt=0): $x_{\text{cam,offset}} = 0.003$ m, $z_{\text{cam,offset}} = 0.302$ m.
From Gazebo GT (fallback):
If camera depth is unavailable, ball position is computed from live Gazebo ground truth poses. The world-frame vector from robot to ball is rotated into the robot local frame using the live yaw $\psi$, then converted to shoulder frame:
\[x_{\text{arm}} = (b_x - r_x)\cos(-\psi) - (b_y - r_y)\sin(-\psi) - d_{\text{arm}}\] \[y_{\text{arm}} = (b_x - r_x)\sin(-\psi) + (b_y - r_y)\cos(-\psi)\] \[z_{\text{arm}} = b_z - (h_{\text{base}} + h_{\text{shoulder}})\]where $\psi$ is robot yaw, $d_{\text{arm}} = 0.140$ m (arm base forward offset from robot center), $h_{\text{base}} = 0.108$ m, $h_{\text{shoulder}} = 0.354825$ m.
Geometric Inverse Kinematics
The WidowX 250s arm is treated as a 2-link planar manipulator in the sagittal plane (x-z). All link lengths are derived directly from the URDF:
\[L_1 = \sqrt{0.04975^2 + 0.25^2} = 0.2549\ \text{m} \quad \text{(shoulder to elbow)}\] \[L_2 = 0.175 + 0.075 = 0.250\ \text{m} \quad \text{(elbow to wrist)}\] \[L_3 = 0.065 + 0.043 + 0.023 + 0.027575 = 0.1586\ \text{m} \quad \text{(wrist to gripper tip)}\]Wrist target — subtract gripper length from horizontal reach so IK targets the wrist, not the tip:
\[w_x = x_{\text{arm}} - L_3, \qquad w_z = -z_{\text{arm}}\]Reach distance:
\[d = \sqrt{w_x^2 + w_z^2}, \qquad d \leq L_1 + L_2\]Elbow angle via law of cosines:
\[\theta_{\text{elbow}} = \cos^{-1}\!\left(\frac{d^2 - L_1^2 - L_2^2}{2 L_1 L_2}\right)\]Shoulder angle — angle to wrist target minus correction for elbow bend:
\[\alpha = \text{atan2}(w_z,\ w_x)\] \[\beta = \cos^{-1}\!\left(\frac{d^2 + L_1^2 - L_2^2}{2\, d\, L_1}\right)\] \[\theta_{\text{shoulder}} = \alpha - \beta\]Wrist compensation — keeps gripper roughly horizontal throughout motion:
\[\theta_{\text{wrist}} = -(\theta_{\text{shoulder}} + \theta_{\text{elbow}})\]Waist angle — horizontal rotation to align arm plane with ball:
\[\theta_{\text{waist}} = \text{atan2}(y_{\text{arm}},\ x_{\text{arm}}), \qquad \theta_{\text{waist}} \in \left[-\frac{\pi}{2},\ \frac{\pi}{2}\right]\]Pre-Grasp Strategy
To avoid pushing the ball on approach, the arm first moves to a pre-grasp position 5 cm above the ball, then lowers straight down:
\[z_{\text{pre}} = z_{\text{arm}} + 0.05\ \text{m}\]IK is solved independently for both the pre-grasp and grasp positions. The arm moves elbow → wrist → shoulder in that order to keep the center of mass balanced and prevent the robot from tipping.
Joint Control
All joints are commanded directly via Gazebo transport using subprocess calls — bypassing the ROS layer entirely for lower latency:
topic = f"/model/locobot/joint/{joint}/0/cmd_pos"
cmd = ["gz", "topic", "-t", topic, "-m", "gz.msgs.Double", "-p", f"data: {angle}"]
subprocess.run(cmd, capture_output=True)
Joints controlled: waist, shoulder, elbow, forearm_roll, wrist_angle, wrist_rotate, left_finger, tilt.
2.6 Throw Controller — Sharat Mylavarapu
2.6.1. Throw Controller
1.1 Overview
The arm_throw_node activates after the robot has navigated to the ball position. It spawns the ball at a precomputed reachable position, performs a slow calibrated pickup, and executes a fast throw sequence toward the target.
Source: ros2_ws/locobot_nodes/locobot_nodes/arm_throw_node.py
2.6.1.2 Throw Sequence
Ball spawns at EE position (X=0.3013m, Z=0.6932m from base_footprint)
→ Wait 5 seconds
→ Open gripper (0.037m)
→ Move arm to pickup pose (q1=0.6704, q2=0, q3=0) — slow, 30 steps over 4s
→ Close gripper tight (0.016m)
→ Lift arm to home
→ Wind-back shoulder to -1.2 rad
→ Throw — shoulder sweeps to 1.5 rad
→ Open gripper at peak velocity — ball released
→ Return arm to home
2.6.1.3 Throw Distance Mapping
The elbow angle during the throw is mapped linearly from target distance:
\[\theta_{\text{elbow}} = -1.8 + \frac{d - 0.5}{4.5} \times 1.5 \quad \text{(rad)}\]where $d$ is the target distance in meters. At the default target of 3.5 m, the elbow angle is $-0.80$ rad ($-45.8°$).
2.6.1.4 Status
Throw controller implementation is complete. Pickup angles were calibrated using joint_state_publisher_gui and ee_logger to find the exact joint configuration that places the gripper at the ball position. End-to-end validation with physical ball collision is in progress.
3. System Architecture
3.1 ROS 2 Node Graph
3.2 Full System Diagram
3.3 Active Topics
| Topic | Message Type | Publisher | Subscriber |
|---|---|---|---|
/odom | nav_msgs/msg/Odometry | gz_bridge | odom_tf_broadcaster, Nav2 |
/cmd_vel | geometry_msgs/msg/Twist | Nav2, base_alignment, arm_grasp | gz_bridge |
/scan | sensor_msgs/msg/LaserScan | depthimage_to_laserscan | Nav2 costmap |
/camera/depth | sensor_msgs/msg/Image | depth_bridge | ball_detection, arm_grasp |
/camera/color | sensor_msgs/msg/Image | depth_bridge | ball_detection |
/ball_global_pose | geometry_msgs/msg/PoseStamped | beacon_localization | auto_navigator |
/ball_detected | std_msgs/msg/Bool | ball_detection | auto_navigator |
/ball_camera_pos | geometry_msgs/msg/PointStamped | ball_detection | arm_grasp |
/ball_pixel_pos | geometry_msgs/msg/Point | ball_detection | — |
/robot_state | std_msgs/msg/String | auto_navigator, base_alignment, arm_grasp | all nodes |
/goal_pose | geometry_msgs/msg/PoseStamped | auto_navigator | Nav2 bt_navigator |
4. Benchmarking & Results
4.1 Navigation Success Rate (10 Trials)
The full pipeline was tested across 10 independent trials with the ball spawning at random safe positions. Each trial starts from robot spawn at $(0, -4.5)$.
| Trial | Ball Spawn | Navigation | Alignment | Grasp Attempt | Ball Lifted |
|---|---|---|---|---|---|
| 1 | (3.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| 2 | (3.5, -3.5) | ✅ | ✅ | ✅ | ❌ |
| 3 | (-3.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| 4 | (-3.5, -3.5) | ✅ | ✅ | ✅ | ❌ |
| 5 | (1.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| 6 | (-1.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| 7 | (1.5, -3.5) | ✅ | ✅ | ✅ | ❌ |
| 8 | (-1.5, -3.5) | ✅ | ✅ | ✅ | ❌ |
| 9 | (3.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| 10 | (1.5, 3.5) | ✅ | ✅ | ✅ | ❌ |
| Stage | Success Rate |
|---|---|
| Navigation to ball | 10/10 (100%) |
| Base alignment | 10/10 (100%) |
| Grasp attempt (gripper reaches ball) | 10/10 (100%) |
| Ball successfully lifted | 0/10 (0%) |
4.2 Beacon Localization Error (10 Trials) — Varad Jahagirdar
| Trial | True Position | Estimated Position | Error (m) |
|---|---|---|---|
| 1 | (3.5, 3.5) | (3.71, 3.28) | 0.30 |
| 2 | (3.5, -3.5) | (3.24, -3.71) | 0.34 |
| 3 | (-3.5, 3.5) | (-3.68, 3.29) | 0.28 |
| 4 | (-3.5, -3.5) | (-3.79, -3.34) | 0.33 |
| 5 | (1.5, 3.5) | (1.72, 3.28) | 0.31 |
| 6 | (-1.5, 3.5) | (-1.74, 3.26) | 0.35 |
| 7 | (1.5, -3.5) | (1.29, -3.71) | 0.30 |
| 8 | (-1.5, -3.5) | (-1.71, -3.28) | 0.32 |
| 9 | (3.5, 3.5) | (3.68, 3.73) | 0.29 |
| 10 | (1.5, 3.5) | (1.78, 3.24) | 0.36 |
Mean error: 0.318 m — consistent with injected noise $\sigma = 0.3$ m.
4.3 Base Alignment Error — Dhiren Makwana
After the robot declares ALIGNED, residual angular and distance errors were measured:
| Trial | Distance at ALIGNED (m) | Angle Error at ALIGNED (deg) |
|---|---|---|
| 1 | 0.534 | 0.7 |
| 2 | 0.531 | 1.2 |
| 3 | 0.536 | 0.9 |
| 4 | 0.533 | 1.4 |
| 5 | 0.535 | 0.8 |
Mean distance: 0.534 m (target 0.535 m) — alignment is consistent and repeatable.
4.4 Grasp Performance Analysis — Varad Jahagirdar
Overview
The arm_grasp node activates when the /robot_state topic receives ALIGNED. It tilts the camera down to see the ball at close range, computes arm joint angles using geometric inverse kinematics, and executes a pre-grasp → grasp → lift sequence.
Source: arm_grasp.py
IK Accuracy
The geometric IK solver computes joint angles from live Gazebo ground truth positions with no hardcoding — the ball and robot positions are read dynamically on every grasp attempt. Across 10 trials with the ball spawning at 8 different arena positions, the gripper successfully reached the ball and positioned the fingers around it in 10/10 trials.
The IK reach clamping logic ensures graceful degradation: if the computed wrist target exceeds $L_1 + L_2 = 0.5049$ m, the target is scaled to the maximum reachable distance rather than producing invalid joint angles.
Gripper Limitation
Force-closure is not achieved due to a known Gazebo Harmonic physics limitation — the mimic constraint for the right finger (right_finger mimics left_finger with multiplier -1) is not supported by the physics engine, so only the left finger closes. The ball sits correctly between the fingers but is not lifted.
In a real robot deployment using the Interbotix Python API with servo torque control, the gripper would achieve force-closure on the ball through compliant torque-controlled grasping.
Grasp Performance Summary
| Metric | Result |
|---|---|
| Trials | 10 |
| Gripper reached ball | 10/10 (100%) |
| Ball successfully lifted | 0/10 (0%) |
| Failure cause | Gazebo mimic joint constraint not supported |
| IK solver | Geometric 2-link (law of cosines) |
| Position source | Live Gazebo GT (dynamic, no hardcoding) |
| Pre-grasp offset | 5 cm above ball |
4.5 Demo Video
Full Mission Demo: YouTube Link
5. Ethical Impact Statement
5.1 Privacy
The system uses an onboard RGB-D camera for ball detection. In simulation, this raises no privacy concerns. In a real-world deployment, the camera would capture bystanders in the arena. Mitigation strategies include: limiting the camera field of view to the floor plane only (achieved by tilting the camera downward as implemented), processing images entirely onboard without transmission, and applying person-detection masks to blur any human presence before logging. Future iterations should implement GDPR-compliant data handling by discarding raw frames immediately after detection.
5.2 Safety
The WidowX 250s arm has a maximum reach of ~0.5 m and operates at low joint velocities in our implementation (1 joint command per 0.8–1.5 seconds). The kinetic energy at the end effector is minimal. However, the Kobuki base moves at up to 0.8 m/s, which poses a collision risk in human-occupied spaces. Our system uses Nav2 with a 0.15 m inflation radius around obstacles, but this does not account for dynamic obstacles such as people. Future iterations should integrate a LIDAR-based emergency stop and velocity scaling based on proximity to humans. The throw controller must also constrain release velocity to ensure the ball does not exceed safe kinetic energy thresholds in shared spaces.
5.3 Bias & Hardware Limitations
The ball detection system relies on a specific magenta HSV color range. This creates a systematic bias — any object with similar color in the environment will be falsely detected. In our arena, all obstacles were deliberately set to grey to avoid false positives, but this would not hold in an uncontrolled environment. The depth camera used for 3D ball localization has a minimum range of 0.2 m, meaning objects closer than 20 cm are invisible. This is a fundamental hardware limitation that affects grasping performance at close range. From a justice perspective, the system is designed for a specific arena configuration — it would not generalize to environments with different lighting conditions, obstacle layouts, or ball colors without retuning the HSV parameters and Nav2 costmap settings.
6. Module Status
| Module | Status | Notes |
|---|---|---|
| Beacon Localization | ✅ Complete | Gaussian noise + trilateration |
| Nav2 Navigation | ✅ Complete | MPPI controller, obstacle avoidance |
| Ball Detection | ✅ Complete | HSV + depth camera 3D position |
| Base Alignment | ✅ Complete | Gazebo GT, proportional controller |
| Arm Home Pose | ✅ Complete | Safe navigation configuration |
| Random Ball Spawn | ✅ Complete | 8 predefined safe positions |
| Auto Navigator | ✅ Complete | Full state machine integration |
| Arm Grasp | 🔄 Partial | Gripper reaches ball, no force-closure |
| Throw Controller | 🔄 Partial | Sharat’s module |
7. Sharat Mylavarapu Contribution
Module: Arm Controller, End Effector Logging, RViz Debugging
Table of Contents
- TOC
7.1. Overview
This covers the complete arm controller pipeline for the Locobot wx250s in Gazebo simulation. The work includes a Forward Kinematics (FK) based arm throw controller, an end effector logging node for debugging and calibration, and a GUI-to-Gazebo joint state bridge. Together these nodes enable the robot to autonomously pick up a ball and throw it toward a target.
Commit: 8244a94
7.2. Arm Throw Controller
7.2.1 Overview
The arm_throw_node.py implements the complete pick-and-throw sequence for the wx250s arm. It spawns a ball at a known reachable position, moves the arm to the pickup pose using hardcoded joint angles derived from a physical calibration experiment, closes the gripper, lifts the arm, winds back, and executes a fast throw.
Source: ros2_ws/locobot_nodes/locobot_nodes/arm_throw_node.py
7.2.2 wx250s Forward Kinematics
All joint angle computation is grounded in the exact wx250s URDF geometry. The full TF chain from base_footprint to the gripper tip:
| Joint | Parent Link | X Offset (m) | Z Offset (m) | Type |
|---|---|---|---|---|
| base_joint | base_footprint | 0 | 0.0102 | fixed |
| plate | base_link | 0 | 0.08823 | fixed |
| arm_base | plate_link | 0.097277 | 0.0095 | fixed |
| waist | arm_base_link | 0 | 0.066175 | revolute (Z) |
| shoulder (q1) | shoulder_link | 0 | 0.03865 | revolute (Y) |
| elbow (q2) | upper_arm_link | 0.04975 | 0.25 | revolute (Y) |
| forearm_roll | upper_forearm_link | 0.175 | 0 | revolute (X) |
| wrist_angle (q3) | lower_forearm_link | 0.075 | 0 | revolute (Y) |
| wrist_rotate | wrist_link | 0.065 | 0 | revolute (X) |
| ee_arm | gripper_link | 0.043 | 0 | fixed |
| ee_bar | gripper_bar_link | 0.023 | 0 | fixed |
| ee_gripper | fingers_link | 0.027575 | 0 | fixed |
Shoulder joint height from base_footprint:
SHOULDER_Z = 0.0102 + 0.08823 + 0.0095 + 0.066175 + 0.03865 = 0.212555 m
Link lengths used in FK:
| Parameter | Value (m) | Source |
|---|---|---|
| UPPER_ARM_X | 0.04975 | elbow joint x offset |
| UPPER_ARM_Z | 0.25 | elbow joint z offset |
| FOREARM_LEN | 0.25 (0.175 + 0.075) | forearm_roll + wrist_angle origins |
| WRIST_TO_TIP | 0.158575 | wrist_rotate + ee_arm + ee_bar + ee_gripper |
| SHOULDER_X | 0.097277 | arm_base_link x offset from plate |
| SHOULDER_Z | 0.212555 | full chain sum from base_footprint |
FK equations in the sagittal plane (X forward, Z up), origin at shoulder joint:
x_elbow = UPPER_ARM_X * cos(q1) - UPPER_ARM_Z * sin(q1)
z_elbow = UPPER_ARM_X * sin(q1) + UPPER_ARM_Z * cos(q1)
x_wrist = x_elbow + FOREARM_LEN * cos(q1 + q2)
z_wrist = z_elbow + FOREARM_LEN * sin(q1 + q2)
x_grip = x_wrist + WRIST_TO_TIP * cos(q1 + q2 + q3)
z_grip = z_wrist + WRIST_TO_TIP * sin(q1 + q2 + q3)
7.2.3 Calibrated Pickup Angles
Pickup joint angles were determined experimentally using joint_state_publisher_gui sliders with the ee_logger node running in parallel. The arm was jogged to the ball position visually in Gazebo and the exact angles were recorded:
| Joint | Angle (rad) | Angle (deg) | Method |
|---|---|---|---|
| shoulder (q1) | 0.6704 | 38.4° | GUI calibration |
| elbow (q2) | -0.0007 | ~0° | GUI calibration |
| wrist (q3) | -0.0001 | ~0° | GUI calibration |
End effector position at these angles (from ee_logger output):
| Position | Value | Frame |
|---|---|---|
| X (forward) | 0.3013 m | base_footprint |
| Y (lateral) | 0.0000 m | base_footprint |
| Z (height) | 0.6932 m | base_footprint |
| Gripper x (rel shoulder) | +0.2040 m | shoulder joint |
| Gripper z (rel shoulder) | +0.4804 m | shoulder joint |
| Reach from shoulder | 0.5219 m | shoulder joint |
The ball is spawned at exactly X=0.3013, Z=0.6932 from base_footprint so the gripper is already at the ball when the arm reaches the pickup pose.
7.2.4 Throw Physics
The elbow angle during the throw is computed from target distance using a linear mapping:
t = (distance - THROW_DIST_MIN) / (THROW_DIST_MAX - THROW_DIST_MIN)
throw_elbow = THROW_ELBOW_MIN + t * (THROW_ELBOW_MAX - THROW_ELBOW_MIN)
# THROW_ELBOW_MIN = -1.8 rad
# THROW_ELBOW_MAX = -0.3 rad
# Default target = 3.5 m → elbow = -0.80 rad (-45.8°)
Throw parameters:
| Parameter | Value | Description |
|---|---|---|
| SHOULDER_WINDUP | -1.2 rad | Arm pulled back for wind-up |
| SHOULDER_THROW | 1.5 rad | Arm swept forward for release |
| wind_back_time | 2.0 s | Hold wind-back position |
| throw_speed_time | 0.4 s | Time after throw before home |
| Gripper release | 0.15 s after throw cmd | Open at peak arm velocity |
| Default target | 3.5 m | Maps to elbow = -0.80 rad |
7.2.5 Complete Sequence
| Step | Action | Duration |
|---|---|---|
| 1 | Wait for Gazebo to settle | 2.0 s |
| 2 | Spawn ball at exact EE position + publish RViz marker | 0.5 s |
| 3 | Countdown wait | 5.0 s |
| 4 | Open gripper slowly (0.015 → 0.037 m) | 1.0 s |
| 5 | Move arm to pickup pose q1=0.6704, q2=0, q3=0 (30 interpolated steps) | 4.0 s |
| 6 | Close gripper tight (0.037 → 0.016 m) | 2.0 s |
| 7 | Lift arm to home position (slow) | 2.5 s |
| 8 | Wind-back shoulder to -1.2 rad | 2.0 s |
| 9 | THROW — shoulder sweeps to 1.5 rad | 0.15 s |
| 10 | Open gripper — release ball | 0.4 s |
| 11 | Return arm to home | instant |
Slow arm movements use linear interpolation over 30 steps:
for i in range(1, steps+1):
t = i / steps
q1_cmd = cur_q1 + t * (target_q1 - cur_q1)
q2_cmd = cur_q2 + t * (target_q2 - cur_q2)
q3_cmd = cur_q3 + t * (target_q3 - cur_q3)
7.2.6 RViz Ball Marker
The arm_throw_node publishes a red sphere marker to /visualization_marker_array at the ball spawn position for real-time visualization and debugging in RViz2. The marker refreshes every 2 seconds to stay visible throughout the full sequence.
| Marker | Type | Color | Topic |
|---|---|---|---|
| Ball sphere | SPHERE (r=0.02m) | Red (1.0, 0.2, 0.2) | /visualization_marker_array |
| Ball label | TEXT_VIEW_FACING | White | /visualization_marker_array |
7.3. End Effector Logger
7.3.1 Overview
The ee_logger node subscribes to /joint_states and continuously computes the end effector world position using the wx250s FK equations. It logs joint angles, intermediate link positions, and the gripper tip position whenever any joint changes by more than a configurable threshold (default 0.01 rad).
Source: ros2_ws/locobot_nodes/locobot_nodes/ee_logger.py
7.3.2 Purpose
This node was created specifically to support the arm calibration workflow. By running ee_logger alongside joint_state_publisher_gui, the engineer can move arm sliders visually in Gazebo and immediately read off exact EE coordinates — including copy-paste ready joint angle constants for arm_throw_node.py.
7.3.3 Sample Output
──────────────────────────────────────────────────────────────────
JOINT ANGLES:
shoulder(q1) = +0.6704 rad (+38.4°)
elbow(q2) = -0.0007 rad (-0.0°)
wrist(q3) = -0.0001 rad (-0.0°)
FK POSITIONS (relative to shoulder):
elbow: x=-0.1163m z=+0.2268m
wrist: x=+0.0797m z=+0.3820m
gripper: x=+0.2040m z=+0.4804m
reach from shoulder: 0.5219m
END EFFECTOR (from base_footprint, robot at origin):
X = +0.3013 m (forward)
Y = +0.0000 m (lateral)
Z = +0.6932 m (height)
COPY-PASTE FOR arm_throw_node.py:
pick_q1 = 0.6704 # shoulder 38.4°
pick_q2 = -0.0007 # elbow ~0°
pick_q3 = -0.0001 # wrist ~0°
──────────────────────────────────────────────────────────────────
7.3.4 Joint Name Handling
The node handles both namespaced (locobot/shoulder) and plain (shoulder) joint names from the JointState message, making it compatible with both joint_state_publisher_gui and Gazebo bridge outputs.
7.4. Joint State to Gazebo Bridge
7.4.1 Overview
The joint_state_to_gz node bridges ROS2 /joint_states messages from joint_state_publisher_gui to Gazebo’s gz transport joint position controller topics. This allows the GUI sliders to directly drive the arm joints in the Gazebo simulation.
Source: ros2_ws/locobot_nodes/locobot_nodes/joint_state_to_gz.py
7.4.2 Topic Mapping
| ROS Joint Name | Gazebo gz Transport Topic |
|---|---|
| waist | /model/locobot/joint/waist/0/cmd_pos |
| shoulder | /model/locobot/joint/shoulder/0/cmd_pos |
| elbow | /model/locobot/joint/elbow/0/cmd_pos |
| forearm_roll | /model/locobot/joint/forearm_roll/0/cmd_pos |
| wrist_angle | /model/locobot/joint/wrist_angle/0/cmd_pos |
| wrist_rotate | /model/locobot/joint/wrist_rotate/0/cmd_pos |
| left_finger | /model/locobot/joint/left_finger/0/cmd_pos |
| right_finger | /model/locobot/joint/right_finger/0/cmd_pos |
The node strips the locobot/ namespace prefix from joint names before lookup, making it compatible with both namespaced and plain joint name formats.
7.5. Launch File Integration
7.5.1 locobot_gazebo.launch.py
The main Gazebo launch file was updated to include joint_state_publisher_gui and joint_state_to_gz. Both start with a 10-second TimerAction delay to allow the robot to fully spawn before the GUI connects.
| Node | Delay | Purpose |
|---|---|---|
| robot_state_publisher | none | Publishes /robot_description and TF |
| ros_gz_sim (Gazebo) | none | Simulation environment |
| ros_gz_sim create | 5s | Spawns robot at (-4, -4, 0.15) |
| ros_gz_bridge | none | Bridges clock, cmd_vel, odom, tf |
| rviz2 | none | Visualization |
| joint_state_publisher_gui | 10s | Slider GUI for manual arm control |
| joint_state_to_gz | 10s | Bridges GUI sliders to Gazebo joints |
7.5.2 Calibration Workflow
- Launch Gazebo:
ros2 launch locobot_gazebo locobot_gazebo.launch.py - Run EE logger:
ros2 run locobot_nodes ee_logger - Move GUI sliders to desired arm position — arm moves in Gazebo
- Read
COPY-PASTEblock fromee_loggerterminal - Paste
pick_q1/q2/q3and ball position intoarm_throw_node.py
7.6. Individual Contribution Summary
| File | Role |
|---|---|
arm_throw_node.py | FK-based arm controller: ball spawn, slow pickup, throw sequence, RViz marker |
ee_logger.py | FK end effector logger: joint angle monitoring, copy-paste calibration output |
joint_state_to_gz.py | GUI-to-Gazebo bridge: forwards joint_state_publisher_gui to gz transport |
locobot_gazebo.launch.py | Updated launch: added GUI + bridge with 10s delay |
| Commit | Description |
|---|---|
8244a94 | Add arm_throw_node.py, ee_logger.py, joint_state_to_gz.py |
7. Status
| Item | Status | Notes |
|---|---|---|
| FK equations (wx250s) | ✅ Complete | Exact URDF geometry used |
| Ball spawn at EE position | ✅ Complete | X=0.3013m, Z=0.6932m |
| Calibrated pickup angles | ✅ Complete | q1=0.6704, q2=-0.0007, q3=-0.0001 |
| Slow arm interpolation | ✅ Complete | 30 steps over 4 seconds |
| Gripper control | ✅ Complete | 0.037m open, 0.016m tight grip |
| Throw sequence | ✅ Complete | Wind-back + fast sweep + release |
| EE logger node | ✅ Complete | Logs on any joint change >0.01 rad |
| GUI-to-Gazebo bridge | ✅ Complete | All 8 joints bridged |
| RViz ball marker | ✅ Complete | Refreshes every 2s |
| End-to-end pick+throw | 🔄 In Progress | Pending physical ball collision tuning |
7. Individual Contribution
| Team Member | Primary Technical Role | Key Commits | Files |
|---|---|---|---|
| Dhiren Makwana | Navigation, Detection, Alignment, Integration | 213c91f 7d5a0b8 c497bac f342caa 84983b5 | auto_navigator.py base_alignment.py ball_detection.py locobot_gazebo.launch.py |
| Varad Jahagirdar | Perception, Localization, Arm Grasp | cfed6b9 bab74f0 bc905a7 | beacon_localization.py arm_grasp.py |
| Sharat Mylavarapu | Throw Controller, End-Effector Logging, Joint State Publishing | 8244a94 | arm_throw_node.py ee_logger.py joint_state_publisher.py |