Onshape → ROS 2 simulation package generator.
Generate complete ROS 2 robot description packages directly from an Onshape assembly — including URDF, xacro, Gazebo integration, RViz configuration, and launch files.
Full ROS 2 simulation package, generated directly from your Onshape assembly.
Supports wheeled robots (2/3/4-wheel) · legged robots (beta) · arm robots (beta)
meshflow takes an Onshape assembly URL and outputs a complete, simulation-ready ROS 2 description package: URDF, xacro, Gazebo plugins, and launch files. Robot type is inferred entirely from URDF geometry; wheeled robots get drive plugins and odometry, legged and arm robots get ros2_control wired up automatically. No joint or link naming conventions required.
My bot — built from scratch in Onshape:
| RViz | Gazebo |
|---|---|
![]() |
![]() |
Public Onshape assemblies rendered with meshflow:
| RViz | Gazebo |
|---|---|
![]() |
![]() |
| RViz | Gazebo |
|---|---|
![]() |
![]() |
Given one Onshape URL and a robot name, meshflow generates a fully structured ROS 2 package:
<robot>_description/
├── launch/
│ ├── display.launch.py # RViz preview — no colcon needed
│ └── gazebo.launch.py # full Gazebo Classic 11 simulation
├── models/
│ ├── urdf/
│ │ ├── <robot>.urdf # flat URDF (used by display.launch)
│ │ └── <robot>.urdf.xacro # xacro with Gazebo plugins wired in
│ └── meshes/ # STL files pulled from Onshape
├── gazebo/
│ └── <robot>.gazebo # drive + sensor + friction plugins
├── config/ # legged / arm only
│ └── controllers.yaml
├── media/
│ └── materials/scripts/
│ └── <robot>.material # OGRE per-link colors for Gazebo Classic
└── rviz/
└── robot.rviz # pre-configured RViz layout
Plugins generated per detected geometry:
| Robot kind | Detected when | Gazebo plugin |
|---|---|---|
| Differential drive | 2–3 continuous wheels, axis ≈ Y |
libgazebo_ros_diff_drive.so |
| Skid steer | 4 continuous wheels, axis ≈ Y |
libgazebo_ros_skid_steer_drive.so |
| Legged (beta) | 3+ top-level branches with movable joints, no drive wheels | libgazebo_ros2_control.so (if installed) |
| Arm (beta) | 1–2 top-level movable branches, no drive wheels | libgazebo_ros2_control.so (if installed) |
| Lidar (revolute) | revolute joint, axis not Y, z > 5 cm; or name contains lidar/laser/scan/ray/hokuyo/velodyne/rplidar/sick/ust |
libgazebo_ros_ray_sensor.so |
| Lidar (fixed) | fixed leaf, disc geometry; or name keywords above |
libgazebo_ros_ray_sensor.so |
| Camera | fixed leaf, thin box geometry; or name contains cam/camera/rgb/mono/stereo/lens/vision |
libgazebo_ros_camera.so |
| Depth / TOF / Range | fixed leaf, wide thin box; or name contains depth/realsense/kinect/tof/range/sonar/ultrasonic/proximity |
libgazebo_ros_camera.so (depth type) |
| IMU | fixed leaf, near-cubic geometry < 4 cm; or name contains imu/gyro/accel/mpu/bno/ahrs/inertial |
libgazebo_ros_imu_sensor.so |
Per-link OGRE colors are extracted from URDF <material> tags and written to media/materials/scripts/<robot>.material.
Note: Legged and arm robot support is in beta. The robot will spawn and render correctly in Gazebo and RViz; controller integration (
joint_trajectory_controller) requiresros-humble-ros2-controland is actively being tested across robot configurations.
| Dependency | Version | Notes |
|---|---|---|
| Python | 3.10+ | |
| uv | any | package manager (auto-installed by install.sh) |
| ROS 2 | Humble or later | must be sourced before launching |
| Gazebo Classic | 11 | gazebo_ros bridge |
ros-$ROS_DISTRO-gazebo-ros-pkgs |
— | Gazebo ↔ ROS bridge |
ros-$ROS_DISTRO-robot-state-publisher |
— | TF broadcast |
ros-$ROS_DISTRO-joint-state-publisher-gui |
— | manual joint control in RViz |
ros-$ROS_DISTRO-xacro |
— | xacro processing |
pyyaml |
any | sensor axis config persistence (meshflow.yaml); installed automatically |
ros-$ROS_DISTRO-ros2-control |
optional | legged/arm controller spawners |
ros-$ROS_DISTRO-gazebo-ros2-control |
optional | Gazebo ros2_control plugin |
Install the ROS 2 packages (replace humble with your distro):
sudo apt install \
ros-humble-gazebo-ros-pkgs \
ros-humble-robot-state-publisher \
ros-humble-joint-state-publisher-gui \
ros-humble-xacroFor legged / arm robots, also install:
sudo apt install \
ros-humble-ros2-control \
ros-humble-gazebo-ros2-controlIf these are not installed, meshflow still generates the package and the robot spawns in Gazebo; controller spawners are skipped with an install hint printed to stderr.
git clone https://github.com/prtmxio/meshflow
cd meshflow
bash install.shThe script:
- Checks Python 3.10+
- Installs
uvif not present - Installs all Python dependencies (
onshape-to-robot,numpy,trimesh,python-dotenv,pyyaml) - Installs the
meshflowCLI globally viauv tool install→ available from anywhere asmeshflow
If meshflow is not found after install, add ~/.local/bin to your PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc # or ~/.bashrc
source ~/.zshrcmeshflow reads your CAD via the Onshape REST API. You need a free API key pair.
Get your keys:
- Log in to Onshape
- Click your profile avatar (top right) → My Account
- Go to the Developer tab
- Click the API Keys link
- Click Create new API key
- Copy both the Access Key and Secret Key before closing; the secret is only shown once
Save them:
meshflow initThis creates ~/.config/meshflow/.env and opens it in your $EDITOR. Fill in:
ONSHAPE_ACCESS_KEY=<your access key>
ONSHAPE_SECRET_KEY=<your secret key>
Save and close. Run meshflow init again at any time to edit the keys.
meshflowYou will be prompted for:
| Prompt | Example |
|---|---|
| Onshape Assembly URL | https://cad.onshape.com/documents/abc.../w/def.../e/ghi... — copy from the Assembly tab, not Part Studio |
| Robot name | my_robot |
| Output format | urdf |
The generated package lands in output/<robot>_description/.
Step 1: copy the package into your ROS 2 workspace:
cp -r output/<robot>_description ~/ros2_ws/src/Step 2: build:
cd ~/ros2_ws
colcon build --packages-select <robot>_description
source install/setup.zsh # or setup.bashStep 3: launch:
ros2 launch <robot>_description gazebo.launch.pyGazebo, RViz, and the robot spawn automatically. To drive the robot:
ros2 run teleop_twist_keyboard teleop_twist_keyboardTo verify the lidar:
ros2 topic hz /scan # expect ~20 HzTo inspect the URDF in RViz without building a workspace:
cd output/<robot>_description/launch
ros2 launch display.launch.pySensor link frames exported from Onshape can face any direction depending on how mate connectors were placed. sensor_config lets you dial in the correct orientation interactively — no math required.
meshflow sensor_config <path-to-description-package>It scans the package URDF for directional sensors (camera, depth, TOF/range), then prompts for two axes per sensor: which link-frame axis points forward, and which points skyward (or up in the image for cameras):
tof_sensor (TOF_joint, type: range_sensor)
Which axis of [tof_sensor] should face forward? [+z]: -y
Which axis of [tof_sensor] should point skyward in the scan plane? [+y]: -x
camera (camera_jt, type: camera)
Which axis of [camera] should face forward? [+z]: -z
Which axis of [camera] should be up in the camera image? [+y]: +y
How to find the right axes: open RViz with display.launch.py, enable Displays → TF → Show Axes. Each sensor link gets colored arrows: Red = +X, Green = +Y, Blue = +Z. Pick the arrow pointing forward and the one pointing up.
The tool then:
- computes the correct
<pose>rotation and patches it directly in the.gazebofile - saves the chosen axes to
meshflow.yamlinside the package directory - reads
meshflow.yamlautomatically on the nextmeshflowrun, so regenerating the package preserves the fix
meshflow sensor_config requires <preserveFixedJoint>true</preserveFixedJoint> on the sensor joint — meshflow now emits this automatically for every fixed joint during generation.
meshflow --help show usage summary
meshflow init create/edit API key config
meshflow run the converter
meshflow sensor_config <path> fix directional sensor orientations in an existing package
| Module | Role |
|---|---|
cli.py |
Entry point; handles prompts, subcommand dispatch, and sensor_config |
onshape.py |
API auth, URL parsing, onshape-to-robot subprocess |
restructure.py |
File layout, package:// URI patching, boilerplate |
detector.py |
KinematicDAG and URDFTraits; geometry-only robot classification |
generator.py |
.gazebo and xacro generation from URDFTraits |
templates.py |
All string templates as named constants |
KinematicDAG parses the URDF <joint> tree and computes global 4×4 transforms for every link. URDFTraits then classifies each node using spatial and kinematic properties:
Robot kind (checked in order):
| Robot kind | Rule |
|---|---|
| Wheeled | 2+ continuous joints, axis ≈ Y, z_min < 20 cm |
| Legged | 3+ top-level branches each containing at least one movable joint |
| Arm | 1–2 top-level movable branches, no drive wheels |
Node classification:
| Classification | Rule |
|---|---|
| Drive wheel | continuous joint, axis ≈ Y-axis, z_min < 20 cm |
| Passive contact (caster) | fixed joint, z_min < 1 cm |
| Revolute sensor | revolute joint, axis not Y, z_min > 5 cm |
| Fixed sensor | fixed leaf node, z_min > 5 cm, no co-located drive wheel sibling |
Sensor type falls back to link/joint name keywords (lidar, cam, imu, depth, realsense, …) when mesh geometry is not available (zero-volume or massless visual-only links).
Wheel diameter and separation are measured directly from mesh geometry via trimesh, so plugin values are physically accurate without any manual input.
uv run pytest tests/ -v





