Hardware Integration

The Ghost, The Anchor, and The Exploding Robot

A full engineering log of integrating the LeKiwi 3-wheeled omni-directional robot into Isaac Lab โ€” covering USD preparation, physics debugging, and friction modeling.

Why LeKiwi?

The official NavDP framework ships with support for the Clearpath Dingo, a simple differential-drive robot. While a solid baseline, differential-drive kinematics are limited in real-world applications โ€” the robot cannot strafe, must rotate to change direction, and cannot easily execute complex maneuvers in tight spaces.

LeKiwi is a compact, 3-wheeled omni-directional robot from the LeRobot project. It features an integrated 6-DOF robotic arm and three omni-wheels arranged in a triangular configuration, allowing holonomic motion (translation in any direction + rotation simultaneously). This makes it an ideal candidate for next-generation embodied navigation research.

โš ๏ธ
Integration Status: In Progress The LeKiwi integration reached a stable state (no NaN crashes, correct physical spawning) but the final sim-to-real friction model requires further tuning. The traction force issue documented in Chapter 5 is the current open problem.

LeKiwi Configuration

The full IsaacLab ArticulationCfg is defined in configs/robots/lekiwi_config.py. Key parameters:

ParameterValueNotes
Wheel Radius0.0325 m5% reduced for collision shape (interpenetration prevention)
Wheel Base0.18 mFront-wheel-to-front-wheel spacing
Wheel JointsST3215_Servo_Motor_v1_Revolute_64, _62Front two wheels are actuated
Wheel Damping5.0Conservative (vs Dingo's 1.0) for stability
Effort Limit20.0 NmMatched to Dingo for fair comparison
Arm Joints6ร— STS3215_03a.*6-DOF arm, stiffness=40, damping=10
Camera Pos[0, 0, 0.2]Front camera on base plate
Self-CollisionsDisabledRequired to prevent NaN explosions
# configs/robots/lekiwi_config.py (key excerpt)
LEKIWI_CFG = ArticulationCfg(
    prim_path = "{ENV_REGEX_NS}/Robot",
    spawn=sim_utils.UsdFileCfg(
        usd_path="assets/robots/lekiwi/lekiwi_final.usd",
        activate_contact_sensors=True,
    ),
    actuators={
        "wheels": ImplicitActuatorCfg(
            joint_names_expr=["ST3215_Servo_Motor_v1_Revolute_64",
                               "ST3215_Servo_Motor_v1_1_Revolute_62",
                               "ST3215_Servo_Motor_v1_2_Revolute_60"],
            effort_limit=20.0,
            velocity_limit=100.0,
            stiffness=0.0,    # Pure velocity control
            damping=5.0,      # Conservative for stability
        ),
        "arm": ImplicitActuatorCfg(
            joint_names_expr=["STS3215_03a.*"],
            effort_limit=100.0, stiffness=40.0, damping=10.0,
        ),
    },
)

USD Processing Pipeline

Raw USD files exported from CAD tools are rarely simulation-ready. LeKiwi required a three-stage pipeline before it could be used in Isaac Lab:

1
lekiwi.usd โ†’ lekiwi_floating.usd โ€” via fix_lekiwi_usd.py: deactivates root_joint, applies ArticulationRootAPI
2
lekiwi_floating.usd โ†’ lekiwi_final.usd โ€” via add_wheel_collisions.py: injects Cylinder (drive wheels) + Sphere (caster) collision geometry
โœ“
lekiwi_final.usd โ€” Production asset: used by lekiwi_config.py, physics-validated with check_wheel_physics.py

The Integration Log: Chapter by Chapter

Integrating a new robot into Isaac Lab is rarely plug-and-play. Below is the complete debugging chronicle, with each chapter representing a distinct failure mode discovered and resolved during the integration process.

Chapter 1

The Immovable Object

Fixed

The Issue: The robot spawned in Isaac Sim but was completely static โ€” it refused to fall due to gravity, respond to forces, or move at all. It was essentially a static prop glued to the origin.

The Diagnosis: The USD file exported from the CAD pipeline contained a fixed root_joint connecting the robot's base to the world frame. This is standard in URDF exports intended for static analysis (e.g., ROS visualization) but completely fatal for dynamic mobile robot simulation. The physics engine treated the robot as a world-fixed object.

The Fix: fix_lekiwi_usd.py opens the USD file, programmatically calls prim.SetActive(False) on the root_joint prim, and applies UsdPhysics.ArticulationRootAPI to the actual chassis body (/LeKiwi/base_plate_layer1_v5). The result is saved as lekiwi_floating.usd โ€” a true floating-base articulation.

Lesson: Always inspect the articulation tree with inspect_joints.py before attempting to simulate a new USD robot. Look for any FixedJoint connecting the robot base to the world.

Chapter 2

The Ghost in the Machine

Fixed

The Issue: With the articulation root fixed, the robot could now fall. But the wheels fell straight through the floor. When motors were commanded at 20 rad/s, logs confirmed the joints were spinning at the requested velocity โ€” yet the robot didn't translate by a single millimeter.

The Diagnosis: A deep USD inspection via inspect_usd.py revealed that the visual meshes had zero collision geometry. The physics engine's broad-phase collision detection found no physics shapes, so the wheels were "ghost" objects โ€” they could rotate, but had no surface to push against the ground.

The Fix: add_wheel_collisions.py procedurally injects collision primitives:

  • For each of the three wheel bodies: creates a child prim (/wheel_collision) under the wheel's prim path
  • Front two drive wheels โ†’ UsdGeom.Cylinder (radius = wheel radius ร— 0.95, axis = Y)
  • Applies UsdPhysics.CollisionAPI + PhysxSchema.PhysxCollisionAPI
  • Sets contactOffset = 0.002, restOffset = 0.001 to prevent interpenetration
  • Marks colliders as invisible so they don't affect rendering

Lesson: Run find_collisions.py on any new robot USD. If the count of UsdPhysics.CollisionAPI prims is zero, the robot is a ghost.

Chapter 3

The Exploding Robot (NaNs)

Fixed

The Issue: As soon as the collision shapes were added, the simulation exploded. The console was flooded with NaN (Not a Number) errors. The robot's world position would become [NaN, NaN, NaN] within milliseconds of spawning, crashing the entire simulation loop.

The Diagnosis: Two compounding failure modes:

  • Self-Collisions: The injected collision cylinders overlapped with the body's existing geometry. The physics solver attempted to resolve two rigid bodies occupying the same space, generating effectively infinite separation forces which propagated as NaN through all downstream calculations.
  • Actuator Instability: The default PID gains (copied from Dingo) were too aggressive for LeKiwi's smaller, lighter body. The wheels spasmed violently, causing the observation processing code to fail โ€” specifically, a torch.inverse() call on a nearly-singular matrix.

The Fix (multi-pronged):

  • PhysxArticulationAPI.CreateEnabledSelfCollisionsAttr().Set(False) โ€” disabled self-collisions at the articulation level
  • Reduced wheel collision cylinder radius by 5% (WHEEL_RADIUS * 0.95) to prevent ground interpenetration at spawn
  • Replaced torch.inverse with torch.linalg.pinv (pseudo-inverse) for numerical robustness
  • Wrapped MPC solver and visualization in try/except blocks to catch NaN before it propagates

Lesson: Always disable self-collisions when procedurally adding collision shapes. Any shape that overlaps with existing geometry at spawn will cause immediate NaN. Prefer torch.linalg.pinv over torch.inverse in robotics code.

Chapter 4

The Anchor

Fixed

The Issue: The robot was finally stable โ€” no NaN, correct physics, correct spawn height. But it refused to move correctly. The front wheels would spin and the robot would inch forward 2โ€“3 cm before stopping, as if dragging an invisible anchor. Turning in place was completely impossible.

The Diagnosis: LeKiwi is kinematically an omni-directional robot. The third wheel at the rear is designed as a caster โ€” it should roll freely in any direction to allow lateral motion. However, we had assigned all three wheels Cylinder collision shapes. A cylinder naturally resists motion perpendicular to its rolling axis. With the rear caster acting as a fixed-axis cylinder, any attempt to turn or strafe was blocked by an enormous lateral friction force.

The Fix: Changed the rear caster's collision shape from Cylinder to Sphere. A sphere contacts the ground at a single point and has no preferred rolling axis, offering near-zero resistance to lateral motion. The collision script uses is_caster = (i == 2) to select the appropriate shape per wheel index.

# add_wheel_collisions.py
for i, wheel_path in enumerate(WHEEL_BODIES):
    is_caster = (i == 2)   # 3rd wheel is the passive caster
    add_cylinder_collision(stage, wheel_path, WHEEL_RADIUS, WHEEL_WIDTH, is_caster=is_caster)
# is_caster=True โ†’ Sphere; is_caster=False โ†’ Cylinder

Lesson: Understand the kinematic role of each wheel before assigning collision geometry. Casters require shapes that allow omnidirectional contact (sphere, disk with no axle friction), not shapes that enforce a preferred rolling direction.

Chapter 5

Current Status โ€” The Traction Problem

Open

Current State: The robot spawns correctly, sits at the correct ground height (z โ‰ˆ 0.033m = wheel radius), does not explode, and the drive wheels physically rotate. However, the robot's base velocity remains zero โ€” it is effectively doing a burnout on frictionless ice.

Evidence:

  • Root position locked at: [-6.64, 14.05, 0.033] โ€” correct height, zero translation
  • MPC generates valid commands: Action: [14.0, 16.0]
  • Joint velocities confirm wheels are spinning physically
  • Base velocity: [0, 0, 0]

Hypothesis: The procedurally-injected collision cylinders are using Isaac Sim's default material, which appears to have near-zero friction coefficients. In PhysX, a collision shape without an explicit PhysicsMaterial binding sometimes defaults to frictionless behavior โ€” the equivalent of polished ice. The wheels are spinning, but generating no traction force.

Next Steps:

  • Create a PhysicsMaterial with staticFriction=0.8, dynamicFriction=0.6 and bind it to the drive wheel collision shapes
  • Use UsdShade.MaterialBindingAPI to programmatically apply the material in the collision injection script
  • Alternatively, investigate if PhysxSchema.PhysxMaterialAPI can be applied directly to the collision prim
๐Ÿ”ฌ
Contributions welcome! If you have experience with PhysX material binding in Isaac Lab, see the repository and check research_tools/add_wheel_collisions.py.

Camera Configuration

LeKiwi uses four cameras for different aspects of the evaluation pipeline:

CameraVariableResolutionPurpose
Front CameraLEKIWI_CameraCfg640ร—360Primary RGB+Depth observation input to the policy
Image Goal CameraLEKIWI_ImageGoal_CameraCfg640ร—360Captures goal image at target location (RGB only)
Metric CameraLEKIWI_MetricCameraCfg160ร—90Low-res overhead camera for BEV metric tracking

All cameras use a Pinhole model with focal length 1.4, clipping range (0.01, 100.0) m. Camera positions are defined relative to the base link prim path.

Comparing with Dingo

The evaluation scripts dynamically select robot configuration based on the --robot argument. The config objects are imported via configs/robots/__init__.py which re-exports both DINGO_CFG and LEKIWI_CFG along with all associated constants.

# Any evaluation script
python eval_pointgoal_wheeled.py \
  --robot lekiwi \        # Switch from dingo to lekiwi
  --port 8888 \
  --scene_dir ./assets/scenes/cluttered_easy \
  --num_episodes 5
โœ…
Real-World Extension baselines/logoplanner/lekiwi_logoplanner_host.py provides a real-world client that connects a physical LeKiwi robot (with Intel RealSense D435) to the LoGoPlanner HTTP server. This enables true sim-to-real transfer using the same navigation policy.
Explore Research Tools โ†’ Read LinkedIn Article โ†’