-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add mobile platform support and example notebook * arm_index is now optional in MobilePlatformWithSingleArmScene Because it's possible to want to do motion planning without the arm. Another solution would be to make the arm static, which would also check for collisions based on the arm itself. * Implement functionality required for MOBI integration in airo-planner * Update toppra.py (acc. limits) * Update mobile_platform.py Avoid collisions with parts of MOBI * Update scene.py * Update scene.py * Run pre-commit * Export animate_mobile_platform_trajectory
- Loading branch information
1 parent
0a03845
commit 1668e66
Showing
8 changed files
with
468 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
from typing import List, Tuple | ||
|
||
import airo_models | ||
import numpy as np | ||
from airo_typing import Vector2DType | ||
from pydrake.math import RigidTransform, RollPitchYaw | ||
from pydrake.multibody.tree import ModelInstanceIndex, PlanarJoint | ||
from pydrake.planning import RobotDiagramBuilder | ||
|
||
# X_URBASE_ROSBASE is the 180 rotation between ROS URDF base and the UR control box base | ||
X_URBASE_ROSBASE = RigidTransform(rpy=RollPitchYaw([0, 0, np.pi]), p=[0, 0, 0]) # type: ignore | ||
|
||
# 180 degree rotation and 1 cm (estimate) offset for the coupling / flange | ||
X_URTOOL0_ROBOTIQ = RigidTransform(rpy=RollPitchYaw([0, 0, np.pi / 2]), p=[0, 0, 0.01]) # type: ignore | ||
|
||
|
||
def add_mobile_platform( | ||
robot_diagram_builder: RobotDiagramBuilder, | ||
drive_positions: Tuple[Vector2DType] = ( | ||
np.array([1, -0.5]), | ||
np.array([1, 0.5]), | ||
np.array([-1, -0.5]), | ||
np.array([-1, 0.5]), | ||
), | ||
battery_position: Vector2DType = np.array([0, 0.5]), | ||
cpu_position: Vector2DType = np.array([0, -0.5]), | ||
side_height: float = 0.43, | ||
side_length: float = 0.69, | ||
roof_width: float = 0.525, | ||
roof_thickness: float = 0.03, | ||
brick_size: float = 0.233, | ||
) -> Tuple[ModelInstanceIndex, List[ModelInstanceIndex]]: | ||
"""Add a mobile platform on wheels to the robot diagram builder. | ||
Currently, this method specifically loads the KELO Robile platform bricks. | ||
It has default values that correspond to the configuration of the platform at AIRO. | ||
In the future, this may change. | ||
Looks up the URDF files for the bricks and welds them together. Also adds the robot's frame, approximated with | ||
boxes. This method returns the model instance index of a frame, which can be used to weld the robot to the | ||
world frame, or to add a PlanarJoint to make it possible to move the robot around. | ||
Args: | ||
robot_diagram_builder: The robot diagram builder to which the platform will be added. | ||
drive_positions: A tuple of 2D positions of the drive bricks, relative to the brick size and root frame. | ||
battery_position: A 2D position of the battery brick, relative to the brick size and root frame. | ||
cpu_position: A 2D position of the CPU brick, relative to the brick size and root frame. | ||
side_height: The height of the mobile robot's side. | ||
side_length: The length (along X) of the mobile robot. | ||
roof_width: The width (along Y) of the mobile robot. | ||
roof_thickness: The thickness of the roof. | ||
brick_size: The size (width and length) of a KELO brick. | ||
Returns: | ||
The mobile platform index and the indices of the parts that make up the mobile platform. | ||
""" | ||
|
||
plant = robot_diagram_builder.plant() | ||
parser = robot_diagram_builder.parser() | ||
parser.SetAutoRenaming(True) | ||
|
||
mobi_urdf_path = airo_models.get_urdf_path("kelo_robile") | ||
|
||
drive_transforms = [ | ||
RigidTransform(p=[brick_size * p[0], brick_size * p[1], 0], rpy=RollPitchYaw([0, 0, 0])) | ||
for p in drive_positions | ||
] | ||
|
||
mobi_model_index = parser.AddModels(mobi_urdf_path)[0] | ||
robot_root_frame = plant.GetFrameByName("base_link", mobi_model_index) | ||
|
||
mobi_part_indices = [] | ||
|
||
# robot_transform: relative to world | ||
# drive_transforms: relative to robot_transform | ||
for drive_index, drive_transform in enumerate(drive_transforms): | ||
brick_index = parser.AddModels(airo_models.get_urdf_path("kelo_robile_wheel"))[0] | ||
brick_frame = plant.GetFrameByName("base_link", brick_index) | ||
plant.WeldFrames(robot_root_frame, brick_frame, drive_transform) | ||
mobi_part_indices.append(brick_index) | ||
|
||
battery_transform = RigidTransform( | ||
p=[brick_size * battery_position[0], brick_size * battery_position[1], 0], rpy=RollPitchYaw([0, 0, 0]) | ||
) | ||
battery_index = parser.AddModels(airo_models.get_urdf_path("kelo_robile_battery"))[0] | ||
battery_frame = plant.GetFrameByName("base_link", battery_index) | ||
plant.WeldFrames(robot_root_frame, battery_frame, battery_transform) | ||
mobi_part_indices.append(battery_index) | ||
|
||
cpu_transform = RigidTransform( | ||
p=[brick_size * cpu_position[0], brick_size * cpu_position[1], 0], rpy=RollPitchYaw([0, 0, 0]) | ||
) | ||
cpu_index = parser.AddModels(airo_models.get_urdf_path("kelo_robile_cpu"))[0] | ||
cpu_frame = plant.GetFrameByName("base_link", cpu_index) | ||
plant.WeldFrames(robot_root_frame, cpu_frame, cpu_transform) | ||
mobi_part_indices.append(cpu_index) | ||
|
||
front_brick_position = np.max([brick_size * p[0] for p in drive_positions]) + brick_size / 2 | ||
|
||
side_height_half = 0.5 * side_height | ||
side_length_half = 0.5 * side_length | ||
side_transforms = [ | ||
RigidTransform(p=[front_brick_position - side_length_half, -brick_size, side_height_half]), | ||
RigidTransform(p=[front_brick_position - side_length_half, brick_size, side_height_half]), | ||
] | ||
|
||
for side_transform in side_transforms: | ||
side_urdf_path = airo_models.box_urdf_path([side_length, 0.03, side_height], "side") | ||
side_index = parser.AddModels(side_urdf_path)[0] | ||
side_frame = plant.GetFrameByName("base_link", side_index) | ||
plant.WeldFrames(robot_root_frame, side_frame, side_transform) | ||
mobi_part_indices.append(side_index) | ||
|
||
roof_length = side_length | ||
roof_length_half = 0.5 * roof_length | ||
roof_thickness_half = 0.5 * roof_thickness | ||
roof_transform = RigidTransform(p=[front_brick_position - roof_length_half, 0, side_height + roof_thickness_half]) | ||
|
||
roof_urdf_path = airo_models.box_urdf_path([roof_length, roof_width, 0.03], "roof") | ||
roof_index = parser.AddModels(roof_urdf_path)[0] | ||
roof_frame = plant.GetFrameByName("base_link", roof_index) | ||
plant.WeldFrames(robot_root_frame, roof_frame, roof_transform) | ||
mobi_part_indices.append(roof_index) | ||
|
||
return mobi_model_index, mobi_part_indices | ||
|
||
|
||
def attach_mobile_platform_to_world( | ||
robot_diagram_builder: RobotDiagramBuilder, mobi_model_index: ModelInstanceIndex, static_platform: bool | ||
): | ||
"""Attach the mobile platform to the world frame with a planar joint. This allows the mobile platform to | ||
move around by setting the (x, y, theta) values of the joint. | ||
Args: | ||
robot_diagram_builder: The robot diagram builder to which the platform will be added. | ||
mobi_model_index: The mobile platform index. | ||
static_platform: If true, will weld. If False, will make a joint.""" | ||
plant = robot_diagram_builder.plant() | ||
|
||
mobi_frame = plant.GetFrameByName("base_link", mobi_model_index) | ||
|
||
if not static_platform: | ||
platform_planar_joint = PlanarJoint("mobi_joint", plant.world_frame(), mobi_frame) | ||
plant.AddJoint(platform_planar_joint) | ||
else: | ||
plant.WeldFrames(plant.world_frame(), mobi_frame) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import numpy as np | ||
from loguru import logger | ||
from pydrake.geometry import Meshcat | ||
from pydrake.multibody.tree import ModelInstanceIndex | ||
from pydrake.planning import RobotDiagram | ||
from pydrake.systems.framework import Context | ||
from pydrake.trajectories import Trajectory | ||
|
||
|
||
def animate_mobile_platform_trajectory( | ||
meshcat: Meshcat, | ||
robot_diagram: RobotDiagram, | ||
mobile_platform_index: ModelInstanceIndex, | ||
pose_trajectory: Trajectory, | ||
context: Context | None = None, | ||
) -> None: | ||
"""Publish a recorded animation to meshcat where the mobile platform follows the provided pose trajectory. | ||
Args: | ||
meshcat: The MeshCat instance to add the visualization to. | ||
robot_diagram: The robot diagram. | ||
mobile_platform_index: The index of the platform to animate. | ||
pose_trajectory: The joint trajectory to animate. | ||
context: The context to use for the animation. If None, a new default context will be used. | ||
""" | ||
if pose_trajectory is None: | ||
logger.warning("Asked to animate pose trajectory, but none provided.") | ||
|
||
assert isinstance(pose_trajectory, Trajectory) # For mypy, but extra check we have a Drake Trajectory now | ||
|
||
if context is None: | ||
# Create a new context just for the recording | ||
context = robot_diagram.CreateDefaultContext() | ||
plant = robot_diagram.plant() | ||
plant_context = plant.GetMyContextFromRoot(context) | ||
|
||
fps = 60.0 | ||
|
||
duration = pose_trajectory.end_time() | ||
|
||
# TODO can support trajectories that don't start at 0? | ||
if not np.isclose(pose_trajectory.start_time(), 0.0): | ||
logger.warning("Pose trajectory does not start at time 0.0, this is not officially supported.") | ||
|
||
n_frames = int(max(duration * fps, 1)) | ||
frame_times = np.linspace(0.0, duration, n_frames) | ||
|
||
meshcat.StartRecording(set_visualizations_while_recording=False, frames_per_second=fps) | ||
|
||
for t in frame_times: | ||
context.SetTime(t) | ||
plant.SetPositions(plant_context, mobile_platform_index, pose_trajectory.value(t).squeeze()) | ||
robot_diagram.ForcedPublish(context) | ||
|
||
meshcat.StopRecording() | ||
meshcat.PublishRecording() |
Oops, something went wrong.