Examples
Each example below is generated directly from the repository's examples/ directory during the documentation build. Use the copy button on a code block or download the original file.
01_differential_drive_rover.py
"""
Example 01: Differential Drive Rover
=====================================
Demonstrates basic wheel velocity control using the Husky robot from pybullet_data.
What this shows:
- Loading a robot by name using pybullet_data
- Setting joint velocities by name
- Smooth camera that follows the robot (CameraFollow)
- Joint/link highlighting on hover (RobotHighlighter)
- Monitoring base telemetry (position, speed, roll)
- Logging data to CSV
- Autopilot toggle with manual joystick override
- BulletLabUI joystick widget for gamepad-style control
Run::
python examples/01_differential_drive_rover.py
"""
import math
import time
import sys
from pathlib import Path
# Ensure bulletlab is importable from the repo root
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot, CameraFollow, RobotHighlighter
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.logging import DataLogger
from bulletlab.utils.urdf_utils import find_urdf
def main() -> None:
print("=== BulletLab Example 01: Differential Drive Rover ===\n")
# ──────────────────────────────────────────────
# 1. Create simulation
# ──────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81), timestep=1.0 / 240.0)
sim.start()
# Initial camera — CameraFollow will take over once the robot is loaded
sim.set_camera(distance=4.0, yaw=50.0, pitch=-30.0, target=(0, 0, 0.5))
# ──────────────────────────────────────────────
# 2. Build world
# ──────────────────────────────────────────────
world = World(sim)
world.load_plane()
# ──────────────────────────────────────────────
# 3. Load robot
# ──────────────────────────────────────────────
# husky.urdf is bundled with pybullet_data
try:
urdf_path = find_urdf("husky/husky.urdf")
except FileNotFoundError:
# Fallback: use racecar
try:
urdf_path = find_urdf("racecar/racecar.urdf")
except FileNotFoundError:
print("Could not find husky or racecar URDF. Trying r2d2...")
urdf_path = find_urdf("r2d2.urdf")
robot = Robot.load(str(urdf_path), sim=sim, position=(0, 0, 0.3), name="Rover")
print(f"Loaded robot: {robot}")
print(f" Joints: {list(robot.joints.keys())[:6]}{'...' if len(robot.joints) > 6 else ''}")
print(f" Links: {list(robot.links.keys())[:6]}{'...' if len(robot.links) > 6 else ''}")
print(f" State dim: {len(robot.get_state())}\n")
# ──────────────────────────────────────────────
# 4. Smooth camera follow
# ──────────────────────────────────────────────
cam = CameraFollow(
robot, sim,
mode="smooth",
distance=4.0,
pitch=-30.0,
yaw=50.0,
lerp=0.06, # gentle glide — lower = slower/smoother
height_offset=0.3,
)
# ──────────────────────────────────────────────
# 5. Joint / link hover highlighting
# ──────────────────────────────────────────────
hl = RobotHighlighter(robot, sim)
# ──────────────────────────────────────────────
# 6. Set up telemetry
# ──────────────────────────────────────────────
telemetry = TelemetryManager()
telemetry.watch("x", lambda: robot.base_position[0], unit="m")
telemetry.watch("y", lambda: robot.base_position[1], unit="m")
telemetry.watch("z", lambda: robot.base_position[2], unit="m")
telemetry.watch("speed", lambda: robot.speed, unit="m/s")
telemetry.watch("roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("pitch", lambda: math.degrees(robot.pitch), unit="°")
# ──────────────────────────────────────────────
# 7. Set up data logger
# ──────────────────────────────────────────────
logger = DataLogger()
logger.watch("speed", lambda: robot.speed)
logger.watch("x", lambda: robot.base_position[0])
logger.watch("y", lambda: robot.base_position[1])
logger.watch("roll", lambda: math.degrees(robot.roll))
logger.start("rover_run.csv")
print("Logging to: rover_run.csv\n")
# ──────────────────────────────────────────────
# 8. Try to launch UI (optional)
# ──────────────────────────────────────────────
ui = None
try:
from bulletlab.ui import BulletLabUI, widgets as ui_widgets, imgui
ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry, camera=cam, highlighter=hl)
ui.start()
# ── Shared state (mutable containers so lambdas can write to them) ──
autopilot_on = [True] # Autopilot active by default
joystick_snap = [True] # Snap-to-center on release
# Store current manual velocities so the joystick callbacks can
# write them from inside the UI frame without a direct joint reference.
manual_vel = [0.0, 0.0] # [left_side_vel, right_side_vel]
def on_joy_y(v):
"""Y axis: +1 = forward, -1 = backward."""
spd = v * 10.0
manual_vel[0] += spd # additive so X-axis turn is preserved
manual_vel[1] += spd
def on_joy_x(v):
"""X axis: +1 = turn right, -1 = turn left."""
turn = v * 5.0
manual_vel[0] += turn # left wheels speed up → turns right
manual_vel[1] -= turn # right wheels slow down → turns right
@ui.custom_panel("Drive")
def drive_panel() -> None:
# ── Autopilot toggle switch ───────────────────────────────────────
prev = autopilot_on[0]
ui_widgets.toggle_switch(
"Autopilot",
getter=lambda: autopilot_on[0],
setter=lambda v: autopilot_on.__setitem__(0, v),
color_on=(0.2, 0.75, 1.0, 1.0),
)
# When switching back ON, zero manual velocity so robot stops
if not prev and autopilot_on[0]:
manual_vel[0] = 0.0
manual_vel[1] = 0.0
imgui.separator()
# ── Manual controls (grayed out when autopilot is ON) ────────────
if autopilot_on[0]:
imgui.push_style_var(imgui.StyleVar_.alpha, 0.35)
# Snap toggle switch
ui_widgets.toggle_switch(
"Snap joystick to zero",
getter=lambda: joystick_snap[0],
setter=lambda v: (None if autopilot_on[0]
else joystick_snap.__setitem__(0, v)),
color_on=(0.2, 0.9, 0.4, 1.0),
)
# Reset manual_vel to zero every frame so joystick callbacks
# accumulate cleanly from the base speed on each frame
if not autopilot_on[0]:
manual_vel[0] = 0.0
manual_vel[1] = 0.0
# Joystick (callbacks only do work when autopilot is off)
ui_widgets.joystick(
"Rover Drive",
on_y=on_joy_y if not autopilot_on[0] else None,
on_x=on_joy_x if not autopilot_on[0] else None,
snap=joystick_snap[0],
size=75,
handle_color=(0.2, 0.75, 1.0, 1.0),
)
if autopilot_on[0]:
imgui.pop_style_var()
print("BulletLab control window opened.\n")
except Exception as exc:
autopilot_on = [True]
joystick_snap = [True]
manual_vel = [0.0, 0.0]
print(f"UI not available ({exc}). Running headless.\n")
# ──────────────────────────────────────────────
# 9. Simulation loop
# ──────────────────────────────────────────────
print("Running simulation. Press Ctrl+C to stop.\n")
# Identify wheel joints (try common naming patterns)
wheel_joints = []
for name, joint in robot.joints.items():
if any(kw in name.lower() for kw in ("wheel", "drive", "motor", "rear", "front")):
if not joint.is_fixed:
wheel_joints.append(joint)
if not wheel_joints:
# Fall back to all controllable joints
wheel_joints = robot.controllable_joints[:4]
print(f"Controlling joints: {[j.name for j in wheel_joints]}\n")
# Set max force for all wheel joints
for joint in wheel_joints:
joint.max_force = 100.0
phase = 0
step = 0
try:
while sim.is_connected:
if autopilot_on[0]:
# ── Autopilot phase-based motion ──────────────────────────────
phase_steps = 480 # 2 seconds at 240Hz
t = step % (phase_steps * 4)
if t < phase_steps:
left_vel, right_vel = 8.0, 8.0 # forward
elif t < phase_steps * 2:
left_vel, right_vel = 8.0, -2.0 # turn right
elif t < phase_steps * 3:
left_vel, right_vel = 8.0, 8.0 # forward again
else:
left_vel, right_vel = -2.0, 8.0 # turn left
for i, joint in enumerate(wheel_joints):
if i % 2 == 0:
joint.velocity = left_vel
else:
joint.velocity = right_vel
else:
# ── Manual / joystick control ─────────────────────────────────
# manual_vel is written by the joystick callbacks inside ui.step()
for i, joint in enumerate(wheel_joints):
if i % 2 == 0:
joint.velocity = manual_vel[0]
else:
joint.velocity = manual_vel[1]
sim.step()
cam.update() # smooth camera follows the rover
telemetry.update(t=sim.elapsed_time)
logger.step(t=sim.elapsed_time)
if ui is not None:
ui.step()
if ui.should_close:
break
step += 1
# Print telemetry every 2 seconds
if step % 480 == 0:
snap = telemetry.snapshot()
print(
f" t={sim.elapsed_time:.1f}s | "
f"speed={snap.get('speed', 0):.2f} m/s | "
f"pos=({snap.get('x', 0):.2f}, {snap.get('y', 0):.2f}) | "
f"roll={snap.get('roll', 0):.1f}°"
)
except KeyboardInterrupt:
print("\nSimulation stopped by user.")
except Exception as e:
# Catches pybullet.error when the 3D window is closed externally
if "isConnected" in str(e) or "Joint index" in str(e) or "connect" in str(e).lower():
print("\nSimulation window closed.")
else:
raise
finally:
logger.stop()
print(f"\nLogged {logger.step_count} steps to rover_run.csv")
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
02_robotic_arm.py
"""
Example 02: Robotic Arm (Kuka iiwa)
=====================================
Demonstrates joint position control on the Kuka iiwa arm with an ImGui
slider control panel for each joint.
What this shows:
- Loading the Kuka iiwa arm from pybullet_data
- Individual joint position control
- Reading joint limits and building dynamic UI sliders
- Joint/link highlighting on hover (RobotHighlighter)
- Monitoring joint state in real-time
Run::
python examples/02_robotic_arm.py
"""
import math
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot, RobotHighlighter
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.utils.urdf_utils import find_urdf
def main() -> None:
print("=== BulletLab Example 02: Robotic Arm (Kuka iiwa) ===\n")
# ──────────────────────────────────────────────
# Simulation setup
# ──────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81))
sim.start()
sim.set_camera(distance=2.0, yaw=45.0, pitch=-20.0, target=(0, 0, 0.5))
world = World(sim)
world.load_plane()
# ──────────────────────────────────────────────
# Load Kuka arm
# ──────────────────────────────────────────────
try:
urdf_path = find_urdf("kuka_iiwa/model.urdf")
except FileNotFoundError:
try:
urdf_path = find_urdf("kuka_lwr/kuka.urdf")
except FileNotFoundError:
urdf_path = find_urdf("franka_panda/panda.urdf")
robot = Robot.load(
str(urdf_path),
sim=sim,
position=(0, 0, 0),
fixed_base=True,
name="KukaArm",
)
print(f"Loaded: {robot}")
# ──────────────────────────────────────────────
# Hover highlighting
# ──────────────────────────────────────────────
hl = RobotHighlighter(robot, sim)
controllable = robot.controllable_joints
print(f"Controllable joints ({len(controllable)}):")
for j in controllable:
lo, hi = j.limits
print(f" {j.name}: [{lo:.2f}, {hi:.2f}] rad")
print()
# ──────────────────────────────────────────────
# Initial pose (all zeros)
# ──────────────────────────────────────────────
for joint in controllable:
joint.set_position(0.0)
# ──────────────────────────────────────────────
# Telemetry
# ──────────────────────────────────────────────
telemetry = TelemetryManager()
for joint in controllable:
telemetry.watch(
f"{joint.name}_pos",
(lambda j: lambda: j.position)(joint),
unit="rad",
)
telemetry.watch(
f"{joint.name}_vel",
(lambda j: lambda: j.velocity)(joint),
unit="rad/s",
)
# ──────────────────────────────────────────────
# UI setup
# ──────────────────────────────────────────────
ui = None
try:
from bulletlab.ui import BulletLabUI, imgui
ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry, highlighter=hl)
ui.start()
# Custom panel: arm joint sliders
target_positions = {j.name: 0.0 for j in controllable}
@ui.custom_panel("Arm Control")
def arm_control_panel() -> None:
for joint in controllable:
lo, hi = joint.limits
lo2 = lo if lo != 0 or hi != 0 else -math.pi
hi2 = hi if lo != 0 or hi != 0 else math.pi
changed, new_val = imgui.slider_float(
f"{joint.name}##arm_{joint.index}",
joint.position,
lo2, hi2,
)
# Highlight this joint's 3D link when its slider is hovered
if imgui.is_item_hovered():
hl.set_hover(joint)
if changed:
joint.set_position(float(new_val))
target_positions[joint.name] = new_val
print("BulletLab arm control window opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running automated demo.\n")
# ──────────────────────────────────────────────
# Simulation loop
# ──────────────────────────────────────────────
print("Running simulation. Press Ctrl+C to stop.\n")
step = 0
period = 480 # 2 seconds per motion cycle
try:
while sim.is_connected:
if ui is None:
# Automated sinusoidal joint motion for headless mode
t = step / 240.0
for i, joint in enumerate(controllable):
lo, hi = joint.limits
lo2 = lo if lo != 0 or hi != 0 else -math.pi
hi2 = hi if lo != 0 or hi != 0 else math.pi
mid = (lo2 + hi2) / 2.0
amp = (hi2 - lo2) * 0.3
target = mid + amp * math.sin(t * 0.5 + i * 0.8)
joint.set_position(target)
sim.step()
telemetry.update(t=sim.elapsed_time)
if ui is not None:
ui.step()
if ui.should_close:
break
step += 1
if step % 240 == 0:
print(f" t={sim.elapsed_time:.1f}s | joints: " +
", ".join(f"{j.name}={j.position:.2f}" for j in controllable[:3]))
except KeyboardInterrupt:
print("\nStopped.")
finally:
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
03_self_balancing_robot.py
"""
Example 03: Self-Balancing Robot
=================================
Demonstrates a PD controller keeping a two-wheeled robot upright.
Uses R2D2 as a stand-in for demonstration (roll/pitch balancing).
What this shows:
- Reading roll and pitch from base_orientation
- Implementing a simple PD control loop manually
- Monitoring control effort in real-time via telemetry
- Logging controller data
Run::
python examples/03_self_balancing_robot.py
"""
import math
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.logging import DataLogger
from bulletlab.utils.urdf_utils import find_urdf
from bulletlab.utils.math_utils import clamp
def main() -> None:
print("=== BulletLab Example 03: Self-Balancing Robot ===\n")
# ──────────────────────────────────────────────
# Simulation
# ──────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81))
sim.start()
sim.set_camera(distance=2.5, yaw=45.0, pitch=-15.0, target=(0, 0, 0.5))
world = World(sim)
world.load_plane()
# ──────────────────────────────────────────────
# Load robot – try r2d2 or cartpole-like URDF
# ──────────────────────────────────────────────
try:
urdf_path = find_urdf("cartpole.urdf")
robot = Robot.load(str(urdf_path), sim=sim, position=(0, 0, 0.1), name="CartPole")
is_cartpole = True
except FileNotFoundError:
urdf_path = find_urdf("r2d2.urdf")
robot = Robot.load(str(urdf_path), sim=sim, position=(0, 0, 0.3), name="R2D2")
is_cartpole = False
print(f"Loaded: {robot}")
print(f"Controllable joints: {[j.name for j in robot.controllable_joints]}\n")
# ──────────────────────────────────────────────
# PD Controller parameters
# ──────────────────────────────────────────────
Kp = 50.0 # proportional gain (roll error → wheel torque)
Kd = 5.0 # derivative gain (angular velocity damping)
target_angle = 0.0 # desired pitch/roll angle (upright)
prev_error = 0.0
dt = sim.timestep
print(f"PD Controller: Kp={Kp}, Kd={Kd}")
print(f"Timestep: {dt:.4f}s\n")
# ──────────────────────────────────────────────
# Telemetry
# ──────────────────────────────────────────────
control_effort = [0.0]
telemetry = TelemetryManager()
telemetry.watch("pitch", lambda: math.degrees(robot.pitch), unit="°")
telemetry.watch("roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("control", lambda: control_effort[0], unit="N·m")
telemetry.watch("speed", lambda: robot.speed, unit="m/s")
# ──────────────────────────────────────────────
# Logger
# ──────────────────────────────────────────────
logger = DataLogger()
logger.watch("pitch", lambda: math.degrees(robot.pitch))
logger.watch("roll", lambda: math.degrees(robot.roll))
logger.watch("control", lambda: control_effort[0])
logger.start("balancing_run.csv")
# ──────────────────────────────────────────────
# UI (optional)
# ──────────────────────────────────────────────
ui = None
try:
from bulletlab.ui import BulletLabUI
import bulletlab.ui.widgets as ui_widgets
ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry)
ui.start()
gains = [Kp, Kd]
@ui.custom_panel("PD Controller")
def controller_panel() -> None:
ui_widgets.text("Controller", "PD Balance")
gains[0] = ui_widgets.drag_float(
"Kp (Proportional)", lambda: gains[0], setter=lambda v: gains.__setitem__(0, v),
speed=0.5, min_val=0.0, max_val=500.0,
)
gains[1] = ui_widgets.drag_float(
"Kd (Derivative)", lambda: gains[1], setter=lambda v: gains.__setitem__(1, v),
speed=0.1, min_val=0.0, max_val=50.0,
)
ui_widgets.separator()
ui_widgets.text("Control effort", f"{control_effort[0]:.2f} N·m")
ui_widgets.text("Pitch", f"{math.degrees(robot.pitch):.2f}°")
ui_widgets.text("Roll", f"{math.degrees(robot.roll):.2f}°")
ui_widgets.separator()
if ui_widgets.button("Reset Robot"):
robot.reset()
print("BulletLab control window opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running headless.\n")
gains = [Kp, Kd]
# ──────────────────────────────────────────────
# Simulation loop
# ──────────────────────────────────────────────
print("Running. Press Ctrl+C to stop.\n")
step = 0
try:
while True:
# Read current angle
if is_cartpole:
# Cartpole: use first joint angle as pole angle
controllable = robot.controllable_joints
if controllable:
pole_angle = controllable[-1].position
error = target_angle - pole_angle
d_error = -controllable[-1].velocity
else:
error, d_error = 0.0, 0.0
else:
# General: use pitch for balance
error = target_angle - robot.pitch
d_error = (error - prev_error) / dt
# PD control
u = clamp(gains[0] * error + gains[1] * d_error, -100.0, 100.0)
control_effort[0] = u
prev_error = error
# Apply control to wheel/drive joints
controllable_joints = robot.controllable_joints
if is_cartpole and controllable_joints:
# Apply force to cart (prismatic joint)
controllable_joints[0].torque = u
else:
# Apply to drive wheels
for joint in controllable_joints[:4]:
joint.velocity = clamp(u * 0.5, -20.0, 20.0)
sim.step()
telemetry.update(t=sim.elapsed_time)
logger.step(t=sim.elapsed_time)
if ui is not None:
ui.step()
if ui.should_close:
break
step += 1
if step % 480 == 0:
print(
f" t={sim.elapsed_time:.1f}s | "
f"pitch={math.degrees(robot.pitch):.2f}° | "
f"roll={math.degrees(robot.roll):.2f}° | "
f"u={control_effort[0]:.2f}"
)
except KeyboardInterrupt:
print("\nStopped.")
finally:
logger.stop()
print(f"\nLogged {logger.step_count} steps to balancing_run.csv")
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
04_drone_parameter_tuning.py
"""
Example 04: Drone Parameter Tuning
=====================================
Demonstrates live physics parameter tuning on a quadrotor.
Uses the drone URDF from pybullet_data if available, otherwise uses a
simple box as a stand-in.
What this shows:
- Modifying link mass at runtime via robot.links["body"].mass
- Modifying friction at runtime
- Applying upward forces to simulate thrust (via joint torques)
- Exposing parameter sliders in BulletLabUI
- Real-time observation of how parameter changes affect flight
Run::
python examples/04_drone_parameter_tuning.py
"""
import math
import sys
from pathlib import Path
from bulletlab import Simulation, Robot
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.logging import DataLogger
from bulletlab.utils.urdf_utils import find_urdf
def create_drone_urdf(output_path: Path) -> None:
"""Generate a minimal quadrotor URDF on disk if none exists."""
urdf_content = """<?xml version="1.0"?>
<robot name="quadrotor">
<link name="base_link">
<visual>
<geometry><box size="0.4 0.4 0.1"/></geometry>
</visual>
<collision>
<geometry><box size="0.4 0.4 0.1"/></geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.01" iyy="0.01" izz="0.02" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<link name="rotor_fl">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_fl_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_fl"/>
<origin xyz="0.15 0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_fr">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_fr_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_fr"/>
<origin xyz="0.15 -0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_rl">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_rl_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_rl"/>
<origin xyz="-0.15 0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_rr">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_rr_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_rr"/>
<origin xyz="-0.15 -0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
"""
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(urdf_content)
def main() -> None:
print("=== BulletLab Example 04: Drone Parameter Tuning ===\n")
# ──────────────────────────────────────────────
# Simulation
# ──────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81))
sim.start()
sim.set_camera(distance=3.0, yaw=45.0, pitch=-20.0, target=(0, 0, 1.0))
world = World(sim)
world.load_plane()
# ──────────────────────────────────────────────
# Load drone URDF
# ──────────────────────────────────────────────
drone_urdf = Path(__file__).parent / "assets" / "quadrotor.urdf"
if not drone_urdf.exists():
print("Generating quadrotor URDF...")
create_drone_urdf(drone_urdf)
robot = Robot.load(str(drone_urdf), sim=sim, position=(0, 0, 0.5), name="Drone")
print(f"Loaded: {robot}")
print(f"Joints: {list(robot.joints.keys())}")
print(f"Links: {list(robot.links.keys())}\n")
# ──────────────────────────────────────────────
# Parameters (mutable via UI)
# ──────────────────────────────────────────────
params = {
"thrust": 12.0, # total thrust force (N)
"mass": 1.0, # body mass (kg)
"drag": 0.1, # artificial drag coefficient
"rotor_speed": 100.0, # rotor spin speed (rad/s)
}
# ──────────────────────────────────────────────
# Telemetry
# ──────────────────────────────────────────────
telemetry = TelemetryManager()
telemetry.watch("height", lambda: robot.base_position[2], unit="m")
telemetry.watch("vz", lambda: robot.base_velocity[2], unit="m/s")
telemetry.watch("roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("pitch", lambda: math.degrees(robot.pitch), unit="°")
# ──────────────────────────────────────────────
# Logger
# ──────────────────────────────────────────────
logger = DataLogger()
logger.watch("height", lambda: robot.base_position[2])
logger.watch("thrust", lambda: params["thrust"])
logger.watch("mass", lambda: params["mass"])
logger.start("drone_tuning.csv")
# ──────────────────────────────────────────────
# UI
# ──────────────────────────────────────────────
ui = None
try:
from bulletlab.ui import BulletLabUI
import bulletlab.ui.widgets as ui_widgets
ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry)
ui.start()
@ui.custom_panel("Drone Parameters")
def drone_panel() -> None:
ui_widgets.text("Drone", "Quadrotor Tuner")
ui_widgets.separator()
params["thrust"] = ui_widgets.slider(
"Total Thrust (N)", lambda: params["thrust"], 0.0, 50.0,
setter=lambda v: params.update({"thrust": v}),
)
params["mass"] = ui_widgets.slider(
"Body Mass (kg)", lambda: params["mass"], 0.1, 10.0,
setter=lambda v: (params.update({"mass": v}),
robot.set_dynamics("base_link", mass=v)),
)
params["drag"] = ui_widgets.slider(
"Drag Coefficient", lambda: params["drag"], 0.0, 2.0,
setter=lambda v: params.update({"drag": v}),
)
params["rotor_speed"] = ui_widgets.slider(
"Rotor Speed (rad/s)", lambda: params["rotor_speed"], 0.0, 500.0,
setter=lambda v: params.update({"rotor_speed": v}),
)
ui_widgets.separator()
ui_widgets.text("Height", f"{robot.base_position[2]:.3f} m")
ui_widgets.text("Vertical Vel", f"{robot.base_velocity[2]:.3f} m/s")
if ui_widgets.button("Reset Drone"):
robot.reset(position=(0, 0, 0.5))
print("BulletLab drone tuning window opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running headless.\n")
# ──────────────────────────────────────────────
# Simulation loop
# ──────────────────────────────────────────────
print("Running. Press Ctrl+C to stop.\n")
step = 0
rotor_joints = list(robot.joints.values())
try:
while True:
# Apply upward thrust force (world frame, at the robot's centre)
robot.apply_force((0, 0, params["thrust"]))
# Apply simple drag (opposite to velocity, world frame)
vel = robot.base_velocity
drag_force = tuple(-params["drag"] * v for v in vel)
robot.apply_force(drag_force)
# Spin rotors
for joint in rotor_joints:
if not joint.is_fixed:
joint.velocity = params["rotor_speed"]
sim.step()
telemetry.update(t=sim.elapsed_time)
logger.step(t=sim.elapsed_time)
if ui is not None:
ui.step()
if ui.should_close:
break
step += 1
if step % 240 == 0:
h = robot.base_position[2]
vz = robot.base_velocity[2]
print(f" t={sim.elapsed_time:.1f}s | height={h:.2f}m | vz={vz:.2f}m/s | thrust={params['thrust']:.1f}N")
except KeyboardInterrupt:
print("\nStopped.")
finally:
logger.stop()
print(f"\nLogged {logger.step_count} steps to drone_tuning.csv")
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
05_generic_robot_inspector.py
"""
Example 05: Generic Robot Inspector
=====================================
Load any URDF by name or path and automatically inspect all joints,
links, and telemetry in the BulletLab UI.
This is the most versatile example — it works with any robot.
Usage::
python examples/05_generic_robot_inspector.py
python examples/05_generic_robot_inspector.py kuka_iiwa/model.urdf
python examples/05_generic_robot_inspector.py r2d2.urdf
python examples/05_generic_robot_inspector.py /absolute/path/to/robot.urdf
What this shows:
- Auto-discovery of all joints and links
- Dynamically building a full inspector UI
- RL-compatible state inspection
- Generic telemetry without knowing the robot type in advance
"""
import math
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.logging import DataLogger
from bulletlab.utils.urdf_utils import find_urdf, list_available_urdfs
def pick_robot(arg: str | None) -> str:
"""Determine which URDF to load based on CLI argument."""
if arg is not None:
try:
return str(find_urdf(arg))
except FileNotFoundError:
print(f"Could not find: {arg}")
print("Available URDFs in pybullet_data:")
for u in list_available_urdfs(30):
print(f" {u}")
sys.exit(1)
# Default priority list
defaults = [
"kuka_iiwa/model.urdf",
"husky/husky.urdf",
"franka_panda/panda.urdf",
"r2d2.urdf",
"cartpole.urdf",
]
for d in defaults:
try:
return str(find_urdf(d))
except FileNotFoundError:
continue
print("No default URDF found. Available URDFs:")
for u in list_available_urdfs(20):
print(f" {u}")
sys.exit(1)
def main() -> None:
print("=== BulletLab Example 05: Generic Robot Inspector ===\n")
urdf_path = pick_robot(sys.argv[1] if len(sys.argv) > 1 else None)
print(f"Loading: {urdf_path}\n")
# ──────────────────────────────────────────────
# Simulation
# ──────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81))
sim.start()
sim.set_camera(distance=3.0, yaw=45.0, pitch=-20.0, target=(0, 0, 0.3))
world = World(sim)
world.load_plane()
robot = Robot.load(urdf_path, sim=sim, position=(0, 0, 0.2), name="InspectedRobot")
print(f"Robot: {robot}")
print(f" Joints ({len(robot.joints)}):")
for name, j in robot.joints.items():
lo, hi = j.limits
jtype = j.joint_type.name if hasattr(j.joint_type, "name") else str(j.joint_type)
print(f" [{name}] type={jtype} limits=[{lo:.2f}, {hi:.2f}]")
print(f" Links ({len(robot.links)}):")
for name, l in robot.links.items():
print(f" [{name}] mass={l.mass:.3f}kg")
print(f" State dim: {len(robot.get_state())}\n")
# ──────────────────────────────────────────────
# Telemetry – auto-discover
# ──────────────────────────────────────────────
telemetry = TelemetryManager()
telemetry.watch("x", lambda: robot.base_position[0], unit="m")
telemetry.watch("y", lambda: robot.base_position[1], unit="m")
telemetry.watch("z", lambda: robot.base_position[2], unit="m")
telemetry.watch("speed", lambda: robot.speed, unit="m/s")
telemetry.watch("roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("pitch", lambda: math.degrees(robot.pitch), unit="°")
telemetry.watch("yaw", lambda: math.degrees(robot.yaw), unit="°")
# Add joint positions for first 4 controllable joints
for joint in robot.controllable_joints[:4]:
telemetry.watch(
f"j_{joint.name[:12]}",
(lambda j: lambda: j.position)(joint),
unit="rad",
)
# ──────────────────────────────────────────────
# Logger
# ──────────────────────────────────────────────
logger = DataLogger()
logger.watch("x", lambda: robot.base_position[0])
logger.watch("y", lambda: robot.base_position[1])
logger.watch("z", lambda: robot.base_position[2])
logger.watch("speed", lambda: robot.speed)
logger.start("inspector_run.csv")
# ──────────────────────────────────────────────
# UI
# ──────────────────────────────────────────────
ui = None
try:
from bulletlab.ui import BulletLabUI
import bulletlab.ui.widgets as ui_widgets
ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry)
ui.start()
@ui.custom_panel("Joint Inspector")
def joint_inspector() -> None:
ui_widgets.text("Robot", robot.name)
ui_widgets.text("State dim", str(len(robot.get_state())))
ui_widgets.separator("Controllable Joints")
for joint in robot.controllable_joints[:8]:
lo, hi = joint.limits
lo2 = lo if lo != 0 or hi != 0 else -math.pi
hi2 = hi if lo != 0 or hi != 0 else math.pi
ui_widgets.slider(
joint.name[:20],
lambda j=joint: j.position,
lo2, hi2,
setter=lambda v, j=joint: j.set_position(v),
)
@ui.custom_panel("Link Inspector")
def link_inspector() -> None:
ui_widgets.text("Link count", str(len(robot.links)))
ui_widgets.separator("Editable Properties")
for link in list(robot.links.values())[:6]:
if ui_widgets.collapsing_header(link.name[:20], default_open=False):
ui_widgets.drag_float(
f"mass##{link.name}", lambda l=link: l.mass,
setter=lambda v, l=link: setattr(l, "mass", v),
speed=0.01, min_val=0.0001, max_val=100.0,
)
ui_widgets.drag_float(
f"friction##{link.name}", lambda l=link: l.friction,
setter=lambda v, l=link: setattr(l, "friction", v),
speed=0.01, min_val=0.0, max_val=10.0,
)
print("BulletLab inspector window opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running headless.\n")
# ──────────────────────────────────────────────
# Simulation loop
# ──────────────────────────────────────────────
print("Running inspector. Press Ctrl+C to stop.\n")
step = 0
try:
while sim.is_connected:
sim.step()
telemetry.update(t=sim.elapsed_time)
logger.step(t=sim.elapsed_time)
if ui is not None:
ui.step()
if ui.should_close:
break
step += 1
if step % 480 == 0:
snap = telemetry.snapshot()
pos = (snap.get("x", 0), snap.get("y", 0), snap.get("z", 0))
print(
f" t={sim.elapsed_time:.1f}s | "
f"pos=({pos[0]:.2f},{pos[1]:.2f},{pos[2]:.2f}) | "
f"speed={snap.get('speed', 0):.2f}m/s"
)
except KeyboardInterrupt:
print("\nStopped.")
finally:
logger.stop()
print(f"\nLogged {logger.step_count} steps to inspector_run.csv")
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
06_irregular_terrain.py
"""
Example 06: Husky Rover on Irregular Terrain
=============================================
Demonstrates rover navigation over procedurally generated terrain with
real-time throttle and steering control via an ImGui control panel.
What this shows:
- Procedural heightfield terrain using multi-octave sine noise
- Scattering static rock/box obstacles across the environment
- Loading the Husky rover and driving it with differential steering
- Real-time throttle, steering, and max-speed control via UI sliders
- Live telemetry (speed, roll, pitch, position)
Run::
python examples/06_irregular_terrain.py
"""
import math
import numpy as np
from bulletlab import Simulation, Robot
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
from bulletlab.ui import BulletLabUI
from bulletlab.ui import widgets as ui
# ─────────────────────────────────────────────────────────────────────────────
# 1. Simulation
# ─────────────────────────────────────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81), timestep=1/240).start()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Irregular terrain via World.load_heightfield()
# ─────────────────────────────────────────────────────────────────────────────
world = World(sim)
TERRAIN_SIZE = 256
np.random.seed(42)
# Multi-octave noise for natural-looking hills
def make_heightfield(n):
h = np.zeros((n, n))
for octave, amp, freq in [(1, 0.6, 0.05), (2, 0.3, 0.12), (4, 0.1, 0.3)]:
for i in range(n):
for j in range(n):
h[i, j] += amp * math.sin(freq * i) * math.cos(freq * j)
h += np.random.uniform(-0.15, 0.15, (n, n)) # fine grain noise
# Flatten a spawn-safe zone at the origin
cx, cy = n // 2, n // 2
h[cx-8:cx+8, cy-8:cy+8] = 0.0
return h
heights = make_heightfield(TERRAIN_SIZE)
world.load_heightfield(
heights,
xy_scale=0.1,
z_scale=0.25,
color=(0.55, 0.45, 0.35, 1.0),
)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Scatter rock/box obstacles via World.scatter_obstacles()
# ─────────────────────────────────────────────────────────────────────────────
world.scatter_obstacles(
count=30,
kind="box",
size_range=(0.2, 0.6),
region=(-10.0, -10.0, 8.0, 8.0),
color=(0.4, 0.4, 0.4, 1.0),
seed=7,
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Load Husky rover
# ─────────────────────────────────────────────────────────────────────────────
robot = Robot.load(
"husky/husky.urdf",
sim = sim,
position = (0, 0, 0.4),
name = "Husky",
)
sim.set_camera(distance=5.0, yaw=50, pitch=-30, target=(0, 0, 0.2))
# Wheel joint names in Husky URDF
WHEEL_JOINTS = [
"front_left_wheel",
"front_right_wheel",
"rear_left_wheel",
"rear_right_wheel",
]
def get_wheel(name):
"""Return joint by name, gracefully."""
return robot.joints.get(name)
wheels = {n: get_wheel(n) for n in WHEEL_JOINTS}
wheels = {k: v for k, v in wheels.items() if v is not None}
print("Wheel joints found:", list(wheels.keys()))
# ─────────────────────────────────────────────────────────────────────────────
# 5. Drive state (controlled by UI sliders)
# ─────────────────────────────────────────────────────────────────────────────
drive = {
"throttle": 0.0, # -1 → 1 (backward → forward)
"steer": 0.0, # -1 → 1 (left → right)
"max_vel": 15.0, # rad/s cap
"stopped": False,
}
def apply_drive():
if drive["stopped"]:
for w in wheels.values():
w.velocity = 0.0
return
cap = drive["max_vel"]
fwd = drive["throttle"] * cap
turn = drive["steer"] * cap * 0.5
left_vel = fwd - turn
right_vel = fwd + turn
for name, joint in wheels.items():
if "left" in name:
joint.velocity = left_vel
else:
joint.velocity = right_vel
# ─────────────────────────────────────────────────────────────────────────────
# 6. Telemetry
# ─────────────────────────────────────────────────────────────────────────────
telemetry = TelemetryManager()
telemetry.watch("Speed", lambda: robot.speed, unit="m/s")
telemetry.watch("Height", lambda: robot.base_position[2], unit="m")
telemetry.watch("Roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("Pitch", lambda: math.degrees(robot.pitch), unit="°")
# ─────────────────────────────────────────────────────────────────────────────
# 7. UI
# ─────────────────────────────────────────────────────────────────────────────
app = BulletLabUI(
sim=sim, robots=[robot], telemetry=telemetry,
title="BulletLab — Husky on Terrain",
width=480, height=820,
)
@app.custom_panel("Drive Controls")
def drive_panel():
pos = robot.base_position
ui.text("Speed", f"{robot.speed:.2f} m/s")
ui.text("Height", f"{pos[2]:.3f} m")
ui.text("Roll", f"{math.degrees(robot.roll):.1f} °")
ui.text("Pitch", f"{math.degrees(robot.pitch):.1f} °")
ui.separator("Controls")
drive["throttle"] = ui.slider(
"Throttle ↑↓",
lambda: drive["throttle"],
-1.0, 1.0,
setter=lambda v: drive.__setitem__("throttle", v),
fmt="%.2f",
)
drive["steer"] = ui.slider(
"Steer ←→",
lambda: drive["steer"],
-1.0, 1.0,
setter=lambda v: drive.__setitem__("steer", v),
fmt="%.2f",
)
drive["max_vel"] = ui.slider(
"Max Speed",
lambda: drive["max_vel"],
1.0, 40.0,
setter=lambda v: drive.__setitem__("max_vel", v),
fmt="%.1f rad/s",
)
ui.separator("Actions")
if ui.button(" ⏹ STOP "):
drive["throttle"] = 0.0
drive["steer"] = 0.0
ui.same_line()
if ui.button(" Reset Rover "):
robot.reset()
drive["throttle"] = 0.0
drive["steer"] = 0.0
app.start()
# ─────────────────────────────────────────────────────────────────────────────
# 8. Main loop
# ─────────────────────────────────────────────────────────────────────────────
step = 0
while sim.is_connected and not app.should_close:
apply_drive()
sim.step()
step += 1
if step % 10 == 0:
telemetry.update(t=sim.elapsed_time)
app.step()
app.stop()
sim.stop()
07_arsenal_loading.py
"""
Example 07: BulletLab Arsenal — Direct Loading Demo
=====================================================
Loads the BLem1 rover directly from the BulletLab Arsenal registry into a
temporary session cache. No installation or manual download is required —
the URDF and meshes are fetched on-the-fly and cleaned up on exit.
After loading, the full BulletLabUI opens with:
- A joystick panel to drive the rover with all four wheel pairs
- Live telemetry (position, speed, orientation)
- Camera follow in smooth mode
- Joint / link hover highlighting
Internet access is required on the first run to fetch assets from the registry.
Run::
python examples/07_arsenal_loading.py
"""
import math
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot, CameraFollow, RobotHighlighter
from bulletlab.core.world import World
from bulletlab.telemetry import TelemetryManager
def main() -> None:
print("=" * 60)
print("BulletLab Arsenal — BLem1 Rover")
print("=" * 60)
# ------------------------------------------------------------------
# 1. Simulation
# ------------------------------------------------------------------
sim = Simulation(mode="gui", gravity=(0, 0, -9.81), timestep=1.0 / 240.0)
# ------------------------------------------------------------------
# 2. Load BLem1 directly from Arsenal (session cache, no install)
# ------------------------------------------------------------------
print("\nFetching 'reference_bot/BLem1' from Arsenal registry...")
robot = Robot.load(
"arsenal:reference_bot/BLem1",
sim=sim,
position=(0, 0, 0.3),
name="BLem1",
)
print(f"Loaded: {robot}")
print(f" Joints: {list(robot.joints.keys())}")
print()
# ------------------------------------------------------------------
# 3. World and Camera setup
# ------------------------------------------------------------------
sim.set_camera(distance=3.5, yaw=45.0, pitch=-25.0, target=(0, 0, 0.3))
world = World(sim)
world.load_plane()
# ------------------------------------------------------------------
# 4. Camera follow
# ------------------------------------------------------------------
cam = CameraFollow(
robot, sim,
mode="smooth",
distance=3.5,
pitch=-25.0,
yaw=45.0,
lerp=0.06,
height_offset=0.3,
)
# ------------------------------------------------------------------
# 5. Hover highlighting
# ------------------------------------------------------------------
hl = RobotHighlighter(robot, sim)
# ------------------------------------------------------------------
# 6. Telemetry
# ------------------------------------------------------------------
telemetry = TelemetryManager()
telemetry.watch("x", lambda: robot.base_position[0], unit="m")
telemetry.watch("y", lambda: robot.base_position[1], unit="m")
telemetry.watch("z", lambda: robot.base_position[2], unit="m")
telemetry.watch("speed", lambda: robot.speed, unit="m/s")
telemetry.watch("roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("pitch", lambda: math.degrees(robot.pitch), unit="°")
telemetry.watch("yaw", lambda: math.degrees(robot.yaw), unit="°")
# ------------------------------------------------------------------
# 7. Identify wheel joints
# BLem1 wheels: rear_right_wheel, front_right_wheel,
# rear_left_wheel, front_left_wheel
# Left-side wheels spin one direction; right-side the other for steering.
# ------------------------------------------------------------------
left_wheels = [j for name, j in robot.joints.items() if "left_wheel" in name]
right_wheels = [j for name, j in robot.joints.items() if "right_wheel" in name]
# Fallback: use all wheel joints if naming differs
if not left_wheels or not right_wheels:
all_wheels = [j for name, j in robot.joints.items() if "wheel" in name]
half = len(all_wheels) // 2
left_wheels = all_wheels[:half]
right_wheels = all_wheels[half:]
for j in left_wheels + right_wheels:
j.max_force = 150.0
print(f" Left wheels: {[j.name for j in left_wheels]}")
print(f" Right wheels: {[j.name for j in right_wheels]}")
print()
# ------------------------------------------------------------------
# 8. UI
# ------------------------------------------------------------------
ui = None
manual_vel = [0.0, 0.0] # [left_vel, right_vel]
try:
from bulletlab.ui import BulletLabUI, widgets as ui_widgets, imgui
ui = BulletLabUI(
sim=sim,
robots=[robot],
telemetry=telemetry,
camera=cam,
highlighter=hl,
)
ui.start()
snap_on = [True]
def on_joy_y(v: float) -> None:
"""Forward (+) / backward (-)."""
spd = v * 12.0
manual_vel[0] += spd
manual_vel[1] += spd
def on_joy_x(v: float) -> None:
"""Steer right (+) / left (-)."""
turn = v * 6.0
manual_vel[0] += turn
manual_vel[1] -= turn
@ui.custom_panel("Drive — BLem1")
def drive_panel() -> None:
# Reset each frame so callbacks accumulate cleanly
manual_vel[0] = 0.0
manual_vel[1] = 0.0
ui_widgets.toggle_switch(
"Snap joystick to zero",
getter=lambda: snap_on[0],
setter=lambda v: snap_on.__setitem__(0, v),
color_on=(0.2, 0.9, 0.4, 1.0),
)
imgui.separator()
ui_widgets.joystick(
"BLem1 Drive",
on_y=on_joy_y,
on_x=on_joy_x,
snap=snap_on[0],
size=80,
handle_color=(0.2, 0.75, 1.0, 1.0),
)
imgui.separator()
ui_widgets.text("Left vel", f"{manual_vel[0]:+.1f} rad/s")
ui_widgets.text("Right vel", f"{manual_vel[1]:+.1f} rad/s")
# Leg controls — continuous revolute joints, no position limits
leg_joints = [j for name, j in robot.joints.items() if "leg" in name]
if leg_joints:
leg_vel = [0.0]
@ui.custom_panel("Legs")
def legs_panel() -> None:
changed, v = imgui.slider_float(
"Leg fold speed", leg_vel[0], -3.0, 3.0
)
if changed:
leg_vel[0] = v
for j in leg_joints:
j.velocity = leg_vel[0]
print("BulletLab UI opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running headless.\n")
# ------------------------------------------------------------------
# 9. Simulation loop
# ------------------------------------------------------------------
print("Running simulation. Close the UI window or press Ctrl+C to stop.\n")
try:
while sim.is_connected:
# Apply wheel velocities (updated by joystick callbacks)
for j in left_wheels:
j.velocity = manual_vel[0]
for j in right_wheels:
j.velocity = manual_vel[1]
sim.step()
telemetry.update(t=sim.elapsed_time)
cam.update()
if ui is not None:
ui.step()
if ui.should_close:
break
except KeyboardInterrupt:
print("\nStopped.")
finally:
if ui is not None:
ui.stop()
sim.stop()
print("Done. Session cache cleaned up automatically.")
if __name__ == "__main__":
main()
08_loading_humanoid.py
"""
Example 08: Loading Humanoid from Arsenal
=========================================
Loads the Unitree G1 humanoid from BulletLab Arsenal and auto-generates
a UI panel with sliders for all its controllable joints.
Run::
python examples/08_loading_humanoid.py
"""
import math
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from bulletlab import Simulation, Robot, CameraFollow
from bulletlab.core.world import World
def main() -> None:
print("=== BulletLab Example 08: Loading Humanoid ===\n")
# 1. Setup Simulation
# Note: We do not call sim.start() immediately. This ensures the PyBullet
# GUI window only pops up *after* the heavy model download completes.
sim = Simulation(mode="gui", gravity=(0, 0, -9.81), timestep=1.0 / 240.0)
# 2. Load Humanoid from Arsenal
print("Fetching 'unitree/g1_description/g1_29dof' from Arsenal registry...")
try:
robot = Robot.load(
"arsenal:unitree/g1_description/g1_29dof",
sim=sim,
position=(0, 0, 0.8), # Spawn slightly above ground
name="UnitreeG1",
)
except Exception as exc:
print(f"Failed to load robot from arsenal: {exc}")
sys.exit(1)
print(f"Loaded: {robot.name} with {len(robot.controllable_joints)} controllable joints.")
# 3. World and Camera setup
sim.set_camera(distance=2.5, yaw=45.0, pitch=-20.0, target=(0, 0, 0.5))
world = World(sim)
world.load_plane()
# 4. Camera Follow
cam = CameraFollow(
robot, sim,
mode="smooth",
distance=2.5,
pitch=-20.0,
yaw=45.0,
lerp=0.1,
height_offset=0.5,
)
# 5. UI Setup
ui = None
try:
from bulletlab.ui import BulletLabUI
import bulletlab.ui.widgets as ui_widgets
ui = BulletLabUI(sim=sim, robots=[robot], camera=cam)
ui.start()
@ui.custom_panel("Joint Controls")
def joint_controls() -> None:
ui_widgets.text("Robot", robot.name)
ui_widgets.text("Total Joints", str(len(robot.controllable_joints)))
ui_widgets.separator()
for joint in robot.controllable_joints:
lo, hi = joint.limits
# If limits are not defined, provide a reasonable default range
lo2 = lo if lo != 0 or hi != 0 else -math.pi
hi2 = hi if lo != 0 or hi != 0 else math.pi
ui_widgets.slider(
joint.name[:25],
lambda j=joint: j.position,
lo2, hi2,
setter=lambda v, j=joint: j.set_position(v),
)
print("BulletLab UI opened.\n")
except Exception as exc:
print(f"UI not available ({exc}). Running headless.\n")
# 6. Simulation loop
print("Running simulation. Close the UI window or press Ctrl+C to stop.\n")
try:
while sim.is_connected:
sim.step()
cam.update()
if ui is not None:
ui.step()
if ui.should_close:
break
except KeyboardInterrupt:
print("\nStopped.")
finally:
if ui is not None:
ui.stop()
sim.stop()
print("Done.")
if __name__ == "__main__":
main()
09_quick_launch.py
"""
Example 09: Instant Robot Deployment with quickLaunch()
======================================================
Demonstrates BulletLab's signature one-liner deployment feature.
Pass any model path (built-in URDF, local file, or Arsenal package URI)
and quickLaunch immediately spins up:
- Physics world with ground plane and realistic gravity
- Auto-generated Joint Control UI with Position / Velocity / Torque modes
- Dynamic Camera Tracking with smooth follow mode and capsule toggle switch
- Live Telemetry tracking base pose, speed, roll/pitch/yaw, and joint angles
- Interactive Python Console for live scripting
Run with default (R2D2):
python examples/09_quick_launch.py
Run with an Arsenal package:
python examples/09_quick_launch.py arsenal:reference_bot
Run with a local/built-in URDF:
python examples/09_quick_launch.py kuka_iiwa/model.urdf
"""
import sys
from pathlib import Path
# Allow importing bulletlab from source checkout
sys.path.insert(0, str(Path(__file__).parent.parent))
import bulletlab
def main() -> None:
# Accept model path from command-line argument, or default to Arsenal model
model_path = sys.argv[1] if len(sys.argv) > 1 else "arsenal:reference_bot"
print("=" * 60)
print(" BulletLab — Signature One-Line Deployment")
print(f" Target Model: {model_path}")
print("=" * 60)
# ⚡ ONE LINE AND YOUR MODEL IS DEPLOYED!
bulletlab.quickLaunch(model_path)
if __name__ == "__main__":
main()
assets/quadrotor.urdf
<?xml version="1.0"?>
<robot name="quadrotor">
<link name="base_link">
<visual>
<geometry><box size="0.4 0.4 0.1"/></geometry>
</visual>
<collision>
<geometry><box size="0.4 0.4 0.1"/></geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.01" iyy="0.01" izz="0.02" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<link name="rotor_fl">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_fl_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_fl"/>
<origin xyz="0.15 0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_fr">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_fr_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_fr"/>
<origin xyz="0.15 -0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_rl">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_rl_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_rl"/>
<origin xyz="-0.15 0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
<link name="rotor_rr">
<visual><geometry><cylinder radius="0.08" length="0.02"/></geometry></visual>
<collision><geometry><cylinder radius="0.08" length="0.02"/></geometry></collision>
<inertial><mass value="0.05"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="rotor_rr_joint" type="continuous">
<parent link="base_link"/>
<child link="rotor_rr"/>
<origin xyz="-0.15 -0.15 0.06"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
demo_ui_control.py
"""
BulletLab Interactive Demo
==========================
Loads a robot model and lets you control every joint via the UI window.
Two windows open:
• PyBullet window — 3D physics simulation
• BulletLab window — sliders, telemetry, console, live plots
Usage:
python examples/demo_ui_control.py # auto-picks best model
python examples/demo_ui_control.py r2d2 # R2D2 (velocity wheels)
python examples/demo_ui_control.py kuka # Kuka iiwa arm (position)
python examples/demo_ui_control.py husky # Husky rover (velocity)
"""
from __future__ import annotations
import math
import sys
import time
import pybullet_data
from bulletlab import Robot, Simulation
from bulletlab.telemetry import TelemetryManager
from bulletlab.ui import BulletLabUI
from bulletlab.ui import widgets as ui
# ──────────────────────────────────────────────────────────────────────────────
# 1. Model catalogue
# ──────────────────────────────────────────────────────────────────────────────
MODELS = {
"kuka": {
"urdf": "kuka_iiwa/model.urdf",
"name": "Kuka iiwa 14",
"position": (0, 0, 0),
"fixed_base": True,
"mode": "position", # joint control mode
"description": "7-DOF robotic arm — drag sliders to pose each joint",
},
"r2d2": {
"urdf": "r2d2.urdf",
"name": "R2D2",
"position": (0, 0, 0.3),
"fixed_base": False,
"mode": "velocity",
"description": "Wheeled robot — set wheel velocities to drive around",
},
"husky": {
"urdf": "husky/husky.urdf",
"name": "Husky Rover",
"position": (0, 0, 0.2),
"fixed_base": False,
"mode": "velocity",
"description": "4-wheel-drive rover — set wheel velocities to steer",
},
"ant": {
"urdf": "ant.urdf",
"name": "Ant Robot",
"position": (0, 0, 0.5),
"fixed_base": False,
"mode": "torque",
"description": "Multi-legged ant — apply torques to each leg joint",
},
}
# Default preference order
DEFAULT_ORDER = ["kuka", "r2d2", "husky", "ant"]
def pick_model(arg: str | None) -> dict:
"""Return model config; fall back gracefully if URDF not found."""
import os
data = pybullet_data.getDataPath()
if arg and arg.lower() in MODELS:
cfg = MODELS[arg.lower()]
if os.path.exists(os.path.join(data, cfg["urdf"])):
return cfg
print(f"[demo] '{arg}' URDF not found, trying defaults…")
for key in DEFAULT_ORDER:
cfg = MODELS[key]
if os.path.exists(os.path.join(data, cfg["urdf"])):
return cfg
raise RuntimeError("No bundled URDF found. Check pybullet_data installation.")
# ──────────────────────────────────────────────────────────────────────────────
# 2. Per-joint slider helpers
# ──────────────────────────────────────────────────────────────────────────────
class JointState:
"""Holds the current target value for one joint (position, velocity, or torque)."""
def __init__(self, default: float = 0.0):
self.target: float = default
# We store targets in a dict so the UI closures can capture live references.
_joint_targets: dict[str, JointState] = {}
def build_joint_panel(robot: Robot, mode: str):
"""Return an ImGui render function that draws sliders for each joint."""
controllable = robot.controllable_joints
for j in controllable:
_joint_targets[j.name] = JointState(0.0)
def render():
ui.text("Model", robot.name)
ui.text("Mode", mode.capitalize() + " control")
ui.text("Joints", str(len(controllable)))
ui.separator()
for j in controllable:
state = _joint_targets[j.name]
lo, hi = j.limits if j.limits != (0.0, 0.0) else (-3.14, 3.14)
if mode == "velocity":
lo, hi = -20.0, 20.0 # rad/s
elif mode == "torque":
lo, hi = -50.0, 50.0 # N·m
# Slider — short label so it fits
label = j.name[:22] if len(j.name) > 22 else j.name
state.target = ui.slider(
label,
lambda s=state: s.target,
lo, hi,
setter=lambda v, s=state: setattr(s, "target", v),
)
ui.separator()
if ui.button(" Zero All "):
for s in _joint_targets.values():
s.target = 0.0
if mode == "position":
ui.same_line()
if ui.button(" Wave "):
# Animate a sine wave across all joints
t = time.monotonic()
for i, j in enumerate(controllable):
_joint_targets[j.name].target = math.sin(t + i * 0.5) * 1.0
return render
def apply_targets(robot: Robot, mode: str):
"""Send the current slider targets to PyBullet."""
for joint in robot.controllable_joints:
target = _joint_targets.get(joint.name)
if target is None:
continue
if mode == "position":
joint.set_position(target.target)
elif mode == "velocity":
joint.velocity = target.target
elif mode == "torque":
joint.torque = target.target
# ──────────────────────────────────────────────────────────────────────────────
# 3. Main
# ──────────────────────────────────────────────────────────────────────────────
def main():
arg = sys.argv[1] if len(sys.argv) > 1 else None
model_cfg = pick_model(arg)
print(f"\n{'─'*60}")
print(f" BulletLab Demo — {model_cfg['name']}")
print(f" {model_cfg['description']}")
print(f"{'─'*60}\n")
# ── Simulation ───────────────────────────────────────────────────────────
sim = Simulation(mode="gui", gravity=(0, 0, -9.81), timestep=1/240).start()
# Ground plane
from bulletlab.core.world import World
world = World(sim=sim)
world.load_plane()
# Load robot
robot = Robot.load(
model_cfg["urdf"],
sim=sim,
position=model_cfg["position"],
fixed_base=model_cfg["fixed_base"],
name=model_cfg["name"],
)
print(f" Joints : {list(robot.joints.keys())}")
print(f" Links : {list(robot.links.keys())}")
print(f" Controllable joints: {robot.num_controllable_joints}\n")
mode = model_cfg["mode"]
# ── Telemetry ─────────────────────────────────────────────────────────────
telemetry = TelemetryManager()
telemetry.watch("Speed", lambda: robot.speed, unit="m/s")
telemetry.watch("Height", lambda: robot.base_position[2], unit="m")
telemetry.watch("Roll", lambda: math.degrees(robot.roll), unit="°")
telemetry.watch("Pitch", lambda: math.degrees(robot.pitch), unit="°")
telemetry.watch("SimStep", lambda: sim.step_count)
for j in robot.controllable_joints[:4]:
telemetry.watch(
f"j:{j.name[:12]}",
(lambda jj: lambda: jj.position)(j),
unit="rad",
)
# ── UI ────────────────────────────────────────────────────────────────────
app = BulletLabUI(
sim=sim,
robots=[robot],
telemetry=telemetry,
title=f"BulletLab — {model_cfg['name']}",
width=660,
height=900,
)
# Joint control panel (custom)
joint_render_fn = build_joint_panel(robot, mode)
app.register_panel("🎮 Joint Control", joint_render_fn)
# Info panel
def info_panel():
ui.text("Model", robot.name)
ui.text("Mode", mode.capitalize())
ui.text("Joints", str(robot.num_controllable_joints))
ui.text("Links", str(len(robot.links)))
ui.separator("Base State")
pos = robot.base_position
ui.text("X", f"{pos[0]:.3f} m")
ui.text("Y", f"{pos[1]:.3f} m")
ui.text("Z", f"{pos[2]:.3f} m")
ui.text("Speed", f"{robot.speed:.3f} m/s")
ui.text("Roll", f"{math.degrees(robot.roll):.1f} °")
ui.text("Pitch", f"{math.degrees(robot.pitch):.1f} °")
ui.text("Yaw", f"{math.degrees(robot.yaw):.1f} °")
ui.separator("Actions")
if ui.button("Reset Robot"):
robot.reset()
for s in _joint_targets.values():
s.target = 0.0
app.register_panel("📊 Info", info_panel)
app.start()
# ── Simulation loop ───────────────────────────────────────────────────────
print(" Simulation running. Close the BulletLab window to exit.\n")
step = 0
while sim.is_connected and not app.should_close:
apply_targets(robot, mode)
sim.step()
step += 1
if step % 10 == 0:
telemetry.update(t=sim.elapsed_time)
app.step()
app.stop()
sim.stop()
print("\n Demo finished. Goodbye!\n")
if __name__ == "__main__":
main()