chore: import upstream snapshot with attribution
Integration Tests / melodic (push) Has been cancelled
Integration Tests / noetic (push) Has been cancelled
Integration Tests / humble (push) Has been cancelled
Integration Tests / jazzy (push) Has been cancelled
Ruff Lint & Format / ruff (push) Has been cancelled
Sync main to develop / Check if sync is needed (push) Has been cancelled
Sync main to develop / Sync main to develop (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:23 +08:00
commit db1d565b64
209 changed files with 22046 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
FROM osrf/ros:humble-desktop
# Install dev tools, ROS2 extras, git, Python and pip
RUN apt-get update && apt-get install -y \
python3 \
python3-colcon-common-extensions \
python3-rosdep \
python3-vcstool \
python3-pip \
git \
build-essential \
ros-humble-rosbridge-server \
# GUI + X11/WSL2 compatibility
x11-apps \
libx11-xcb1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 \
mesa-utils \
libgl1-mesa-glx \
libglu1-mesa \
libxext6 \
libxrender1 \
libxrandr2 \
libxi6 \
&& rm -rf /var/lib/apt/lists/*
# Ensure pip is accessible as `pip`
RUN ln -sf /usr/bin/pip3 /usr/bin/pip
# Initialize rosdep
RUN rosdep init || true && rosdep update
# Install Ruff, pre-commit, uv globally
RUN pip install --no-cache-dir ruff pre-commit uv
# Add non-root user for VSCode devcontainer
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
# Auto-source ROS2 for all users
RUN echo "source /opt/ros/humble/setup.bash" >> /etc/bash.bashrc
# Set locale
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
# Ensure Qt apps can run
ENV QT_X11_NO_MITSHM=1
ENV QT_QPA_PLATFORM=xcb
ENV LIBGL_ALWAYS_SOFTWARE=1
ENV QT_XCB_GL_INTEGRATION=none
# Create a workspace for the non-root user
RUN mkdir -p /home/$USERNAME/workspace \
&& chown -R $USERNAME:$USERNAME /home/$USERNAME/workspace
# Set workspace directory
WORKDIR /home/$USERNAME/workspace
# Switch to non-root user by default in devcontainer
USER $USERNAME
# Allow pre-commit to run even if no config exists
ENV PRE_COMMIT_ALLOW_NO_CONFIG=1
RUN git init . \
&& pre-commit install --allow-missing-config || true
+47
View File
@@ -0,0 +1,47 @@
{
"name": "ROS2 Humble Dev",
"build": { "dockerfile": "Dockerfile" },
"workspaceFolder": "/home/vscode/workspace",
"mounts": [
"source=${localWorkspaceFolder},target=/home/vscode/workspace,type=bind"
],
"runArgs": [
"-v", "${env:SSH_AUTH_SOCK}:/ssh-agent",
"-e", "SSH_AUTH_SOCK=/ssh-agent",
"-e", "DISPLAY=${localEnv:DISPLAY}",
"-e", "LIBGL_ALWAYS_SOFTWARE=1",
"-e", "QT_XCB_GL_INTEGRATION=none",
"-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw"
],
"containerEnv": {
"QT_X11_NO_MITSHM": "1",
"QT_QPA_PLATFORM": "xcb",
"DISPLAY": "${localEnv:DISPLAY}",
"SSH_AUTH_SOCK": "/ssh-agent",
"LIBGL_ALWAYS_SOFTWARE": "1",
"QT_XCB_GL_INTEGRATION": "none",
"TERM": "xterm-256color"
},
"remoteUser": "vscode",
"postStartCommand": "xhost +local:vscode || true",
"forwardPorts": [5000, 9000, 9090],
"portsAttributes": {
"9000": { "label": "MCP Server", "onAutoForward": "ignore" },
"9090": { "label": "ROSBridge", "onAutoForward": "ignore" }
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-azuretools.vscode-docker",
"twxs.cmake",
"ms-vscode.cpptools",
"ms-python.vscode-pylance",
"astral-sh.ruff"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}
+151
View File
@@ -0,0 +1,151 @@
name: Track Clone Metrics
on:
workflow_dispatch:
schedule:
- cron: '0 8 * * *' # Run every day at 8am
jobs:
clone-stats:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper branch operations
- name: Generate GitHub App token
id: generate_token
uses: tibdex/github-app-token@v2.1.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Switch to metrics branch
run: |
# Checkout or create metrics branch
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
echo "📋 Checking out existing metrics branch..."
git checkout -b metrics origin/metrics || git checkout metrics
else
echo "🆕 Creating new metrics branch..."
git checkout -b metrics
fi
- name: Fetch clone data
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
mkdir -p .metrics
# Fetch clone metrics (contains both daily breakdown and 14-day totals)
curl -s -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/${{ github.repository }}/traffic/clones \
> .metrics/clone_stats.json
echo "Clone metrics:"
cat .metrics/clone_stats.json
- name: Update daily metrics
run: |
# Process each day from the clones array
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Create daily CSV with header if it doesn't exist
if [ ! -f .metrics/daily_clone_metrics.csv ]; then
echo "date,total_clones,unique_cloners,last_updated" > .metrics/daily_clone_metrics.csv
fi
echo "📊 Processing daily metrics..."
jq -r '.clones[] | "\(.timestamp | split("T")[0]),\(.count),\(.uniques)"' .metrics/clone_stats.json | while IFS=',' read -r day_date count uniques; do
echo "Processing $day_date: $count clones, $uniques unique"
# Check if this date already exists in the CSV
if grep -q "^$day_date" .metrics/daily_clone_metrics.csv; then
echo "📝 Updating existing entry for $day_date..."
# Update existing entry
awk -v date="$day_date" -v count="$count" -v uniques="$uniques" -v last_updated="$LAST_UPDATED" '
BEGIN { FS=","; OFS="," }
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
print $1, count, uniques, last_updated;
updated=1;
next
}
{ print }
' .metrics/daily_clone_metrics.csv > .metrics/daily_clone_metrics_temp.csv
mv .metrics/daily_clone_metrics_temp.csv .metrics/daily_clone_metrics.csv
else
echo " Adding new daily entry for $day_date..."
echo "$day_date,$count,$uniques,$LAST_UPDATED" >> .metrics/daily_clone_metrics.csv
fi
done
echo "Daily metrics:"
tail -n 5 .metrics/daily_clone_metrics.csv
- name: Update 14-day rolling metrics
run: |
# Process 14-day metrics
COUNT_14D=$(jq '.count' .metrics/clone_stats.json)
UNIQUES_14D=$(jq '.uniques' .metrics/clone_stats.json)
DATE_ONLY=$(date -u +"%Y-%m-%d")
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
echo "📊 Processing 14-day metrics... for date: $DATE_ONLY"
echo "Processing values: $COUNT_14D clones, $UNIQUES_14D unique"
# Create 14-day CSV with header if it doesn't exist
if [ ! -f .metrics/rolling_14d_clone_metrics.csv ]; then
echo "date,total_clones_14d,unique_cloners_14d,last_updated" > .metrics/rolling_14d_clone_metrics.csv
echo "📄 Created new 14-day rolling CSV file"
fi
# Check if today's date already exists in the 14-day CSV
if grep -q "^$DATE_ONLY" .metrics/rolling_14d_clone_metrics.csv; then
echo "📝 Updating existing 14-day rolling entry for $DATE_ONLY..."
# Update existing entry
awk -v date="$DATE_ONLY" -v count="$COUNT_14D" -v uniques="$UNIQUES_14D" -v last_updated="$LAST_UPDATED" '
BEGIN { FS=","; OFS=","; updated=0 }
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
print $1, count, uniques, last_updated;
updated=1;
next
}
{ print }
END { if (!updated) print date, count, uniques, last_updated }
' .metrics/rolling_14d_clone_metrics.csv > .metrics/rolling_14d_clone_metrics_temp.csv
mv .metrics/rolling_14d_clone_metrics_temp.csv .metrics/rolling_14d_clone_metrics.csv
echo "✅ Updated existing entry"
else
echo " Adding new 14-day rolling entry for $DATE_ONLY..."
echo "$DATE_ONLY,$COUNT_14D,$UNIQUES_14D,$LAST_UPDATED" >> .metrics/rolling_14d_clone_metrics.csv
echo "✅ Added new entry"
fi
echo "14-day rolling metrics:"
tail -n 5 .metrics/rolling_14d_clone_metrics.csv
- name: Commit and push results
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
git config user.name "CloneMetricsBot[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Add both CSV files
git add .metrics/daily_clone_metrics.csv .metrics/rolling_14d_clone_metrics.csv
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "️ No changes to commit - CSV data is up to date"
else
echo "📝 Committing changes..."
git commit -m "Automated update: repository clone metrics $(date)"
echo "🚀 Pushing to metrics branch..."
git push --force-with-lease origin metrics
fi
+109
View File
@@ -0,0 +1,109 @@
name: Track Download Metrics
on:
workflow_dispatch:
workflow_run:
workflows: ["Track View Metrics"] # exact name of PyPI workflow
types: [completed]
jobs:
download-stats:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper branch operations
- name: Generate GitHub App token
id: generate_token
uses: tibdex/github-app-token@v2.1.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Switch to metrics branch
run: |
# Checkout or create metrics branch
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
echo "📋 Checking out existing metrics branch..."
git checkout -b metrics origin/metrics || git checkout metrics
else
echo "🆕 Creating new metrics branch..."
git checkout -b metrics
fi
- name: Fetch download data
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
mkdir -p .metrics
# Fetch download metrics from releases
curl -s -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/releases?per_page=100" \
> .metrics/releases.json
echo "Download metrics:"
jq '[.[] | {tag: .tag_name, date: .published_at, total_downloads: ([.assets[].download_count] | add), assets_count: (.assets | length)}]' .metrics/releases.json
- name: Update total download metrics
run: |
# Process each release from the releases array
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Create total download CSV with header if it doesn't exist
if [ ! -f .metrics/total_download_metrics.csv ]; then
echo "date,release_tag,total_downloads,last_updated" > .metrics/total_download_metrics.csv
fi
echo "📊 Processing total download metrics..."
jq -r '.[] | "\(.published_at[0:10]),\(.tag_name),\([.assets[].download_count] | add)"' .metrics/releases.json | while IFS=',' read -r release_date tag_name total_downloads; do
echo "Processing $release_date: $tag_name - $total_downloads downloads"
# Check if this release already exists in the CSV
if grep -q ",$tag_name," .metrics/total_download_metrics.csv; then
echo "📝 Updating existing entry for $tag_name..."
# Update existing entry
awk -v tag="$tag_name" -v downloads="$total_downloads" -v last_updated="$LAST_UPDATED" '
BEGIN { FS=","; OFS="," }
$2 == tag {
print $1, $2, downloads, last_updated;
updated=1;
next
}
{ print }
' .metrics/total_download_metrics.csv > .metrics/total_download_metrics_temp.csv
mv .metrics/total_download_metrics_temp.csv .metrics/total_download_metrics.csv
else
echo " Adding new download entry for $tag_name..."
echo "$release_date,$tag_name,$total_downloads,$LAST_UPDATED" >> .metrics/total_download_metrics.csv
fi
done
echo "Total download metrics:"
tail -n 5 .metrics/total_download_metrics.csv
- name: Commit and push results
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
git config user.name "DownloadMetricsBot[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Add CSV file
git add .metrics/total_download_metrics.csv
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "️ No changes to commit - CSV data is up to date"
else
echo "📝 Committing changes..."
git commit -m "Automated update: repository download metrics $(date)"
echo "🚀 Pushing to metrics branch..."
git push --force-with-lease origin metrics
fi
+68
View File
@@ -0,0 +1,68 @@
name: Integration Tests
on:
pull_request:
push:
branches: [develop, main]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
integration:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ros-distro: [melodic, noetic, humble, jazzy]
include:
- ros-distro: melodic
dockerfile: Dockerfile.ros1-melodic
container-name: integration-ros-melodic
- ros-distro: noetic
dockerfile: Dockerfile.ros1-noetic
container-name: integration-ros-noetic
- ros-distro: humble
dockerfile: Dockerfile.ros2-humble
container-name: integration-ros2-humble
- ros-distro: jazzy
dockerfile: Dockerfile.ros2-jazzy
container-name: integration-ros2-jazzy
name: ${{ matrix.ros-distro }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --extra dev
- name: Start ROS container
run: |
ROS_DOCKERFILE=${{ matrix.dockerfile }} \
ROS_CONTAINER_NAME=${{ matrix.container-name }} \
docker compose -f tests/integration/docker-compose.yml up --build -d --wait
timeout-minutes: 10
- name: Detect ROS version
run: uv run python tests/integration/test_quick_detect.py
- name: Run integration tests
run: |
uv run pytest tests/integration/ -v \
--ros-distro ${{ matrix.ros-distro }} --skip-compose
- name: Tear down
if: always()
run: |
ROS_DOCKERFILE=${{ matrix.dockerfile }} \
ROS_CONTAINER_NAME=${{ matrix.container-name }} \
docker compose -f tests/integration/docker-compose.yml down --volumes --remove-orphans
+154
View File
@@ -0,0 +1,154 @@
name: Merge branch into target
# Manual trigger only
on:
workflow_dispatch:
inputs:
source_branch:
description: "Branch to merge from (default: the branch this workflow is run on)"
type: string
required: false
default: ""
target_branch:
description: "Branch to merge into"
type: string
required: true
default: develop
delete_source_branch:
description: "Delete the source branch after a successful merge"
type: boolean
required: false
default: false
check_only:
description: "Verify the merge succeeds without pushing"
type: boolean
required: false
default: false
permissions:
contents: write
pull-requests: write
jobs:
merge-into-branch:
runs-on: ubuntu-latest
name: Merge '${{ inputs.source_branch || github.ref_name }}' into '${{ inputs.target_branch }}'
env:
SRC_BRANCH: ${{ inputs.source_branch || github.ref_name }}
TARGET_BRANCH: ${{ inputs.target_branch }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up git user
run: |
git config user.name "RobotMCP Bot"
git config user.email "admin@robotmcp.ai"
# Make sure we have the source branch and a common ancestor to merge.
git fetch origin "$SRC_BRANCH"
if ! git merge-base "$TARGET_BRANCH" "origin/$SRC_BRANCH"; then
echo "No common ancestor found for merging!"
exit 1
fi
- name: Abort if conflict-resolution PR already open
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SAFE_SRC="${SRC_BRANCH//\//_}"
SAFE_TARGET="${TARGET_BRANCH//\//_}"
EXISTING_PR=$(gh pr list \
--base "$TARGET_BRANCH" \
--state open \
--json url,headRefName \
--jq ".[] | select(.headRefName | startswith(\"${SAFE_SRC}-\") and endswith(\"/merge-into-${SAFE_TARGET}\")) | .url" \
2>/dev/null | head -n1 || true)
if [ -n "$EXISTING_PR" ]; then
echo "::error::An open conflict-resolution PR already exists for '$SRC_BRANCH' -> '$TARGET_BRANCH': $EXISTING_PR"
echo "::error::Resolve and merge that PR before re-running this workflow."
exit 1
fi
- name: Attempt merge
id: merge
run: |
set +e
git merge \
--no-ff \
--no-edit \
--commit \
--message "Merged '$SRC_BRANCH' into '$TARGET_BRANCH'" \
"origin/$SRC_BRANCH"
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "Merge conflicts detected; aborting in-runner merge."
git merge --abort || true
echo "conflict=true" >> "$GITHUB_OUTPUT"
else
echo "conflict=false" >> "$GITHUB_OUTPUT"
fi
exit 0
- name: Push merged branch
if: ${{ steps.merge.outputs.conflict == 'false' && !inputs.check_only }}
run: git push origin "$TARGET_BRANCH"
- name: Delete source branch
if: ${{ inputs.delete_source_branch && steps.merge.outputs.conflict == 'false' && !inputs.check_only }}
run: git push origin --delete "$SRC_BRANCH"
- name: Create sync branch for conflict resolution
id: sync-branch
if: ${{ steps.merge.outputs.conflict == 'true' && !inputs.check_only }}
run: |
SAFE_SRC="${SRC_BRANCH//\//_}"
SAFE_TARGET="${TARGET_BRANCH//\//_}"
SHA_SHORT=$(git rev-parse --short "origin/$SRC_BRANCH")
SYNC_BRANCH="${SAFE_SRC}-${SHA_SHORT}/merge-into-${SAFE_TARGET}"
echo "Creating sync branch: $SYNC_BRANCH"
# If a stale sync branch exists from a previously closed-but-unmerged PR, drop it.
git push origin --delete "$SYNC_BRANCH" 2>/dev/null || true
git branch -f "$SYNC_BRANCH" "origin/$SRC_BRANCH"
git push origin "$SYNC_BRANCH"
echo "SYNC_BRANCH=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
- name: Open conflict-resolution PR
if: ${{ steps.merge.outputs.conflict == 'true' && !inputs.check_only }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SYNC_BRANCH: ${{ steps.sync-branch.outputs.SYNC_BRANCH }}
run: |
gh pr create \
--base "$TARGET_BRANCH" \
--head "$SYNC_BRANCH" \
--title "Conflict-resolution: merge '$SRC_BRANCH' into '$TARGET_BRANCH'" \
--draft \
--body "Automated merge of \`$SRC_BRANCH\` into \`$TARGET_BRANCH\` failed due to conflicts.
A temporary sync branch \`$SYNC_BRANCH\` was created off \`$SRC_BRANCH\` so conflicts can be resolved even when both branches are protected.
**Do not merge this PR through the GitHub UI.** Squash-merging a sync branch rewrites its commits, which inflates the commit count of later merges into \`$TARGET_BRANCH\`. Instead, a maintainer should:
1. Check out the sync branch locally:
\`\`\`
git fetch origin
git checkout $SYNC_BRANCH
\`\`\`
2. Merge '$TARGET_BRANCH' into the sync branch and resolve conflicts:
\`\`\`
git merge origin/$TARGET_BRANCH
# resolve conflicts, then commit
\`\`\`
3. Push the resolved sync branch:
\`\`\`
git push origin $SYNC_BRANCH
\`\`\`
4. Re-run the 'Merge branch into target' workflow from this branch (\`$SYNC_BRANCH\`) with target branch '$TARGET_BRANCH'.
5. After the workflow completes, this PR will close automatically."
- name: Fail on conflict
if: ${{ steps.merge.outputs.conflict == 'true' }}
run: |
echo "::error::Merge of '$SRC_BRANCH' into '$TARGET_BRANCH' has conflicts."
exit 1
+110
View File
@@ -0,0 +1,110 @@
name: Publish (PyPI + MCP Registry)
on:
workflow_dispatch:
push:
tags:
- 'v*'
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: true
jobs:
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # REQUIRED for PyPI Trusted Publishing (OIDC)
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate version matches tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
run: |
set -euo pipefail
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
PYPROJECT_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml)
SERVER_VERSION=$(grep -oP '"version":\s*"\K[^"]+' server.json | head -1)
SERVER_PKG_VERSION=$(grep -oP '"version":\s*"\K[^"]+' server.json | tail -1)
ERRORS=0
[ "$PYPROJECT_VERSION" != "$TAG_VERSION" ] && echo "pyproject.toml: $PYPROJECT_VERSION != $TAG_VERSION" && ERRORS=1
[ "$SERVER_VERSION" != "$TAG_VERSION" ] && echo "server.json: $SERVER_VERSION != $TAG_VERSION" && ERRORS=1
[ "$SERVER_PKG_VERSION" != "$TAG_VERSION" ] && echo "server.json package: $SERVER_PKG_VERSION != $TAG_VERSION" && ERRORS=1
if [ $ERRORS -eq 1 ]; then
echo "Please update all version fields to $TAG_VERSION before creating the tag."
exit 1
fi
echo "All versions match: $TAG_VERSION"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install uv
run: pipx install uv
- name: Sync dependencies
run: uv sync --extra dev
- name: Run ruff lint
run: uvx ruff check .
- name: Run ruff format check
run: uvx ruff format --check .
- name: Run tests
run: |
if [ -d tests ]; then
uv run pytest -q -m "not slow and not integration" || [ $? -eq 5 ] # exit 5 = no tests collected
else
echo "No tests/ directory; skipping."
fi
- name: Build package
run: uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
registry:
name: Publish to MCP Registry
runs-on: ubuntu-latest
timeout-minutes: 10
needs: pypi
permissions:
contents: read
id-token: write # REQUIRED for MCP Registry GitHub OIDC login
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Wait for PyPI metadata propagation
run: sleep 20
- name: Install MCP Publisher
run: |
set -euo pipefail
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
echo "Get the latest release version"
LATEST_VERSION=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest | jq -r '.tag_name')
echo "Installing MCP Publisher version: $LATEST_VERSION"
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" \
| tar xz mcp-publisher
- name: Login to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish to MCP Registry
run: ./mcp-publisher publish
+37
View File
@@ -0,0 +1,37 @@
name: Publish MCP Registry
on:
workflow_dispatch:
concurrency:
group: publish-mcp-${{ github.ref }}
cancel-in-progress: true
jobs:
mcp-registry:
name: Publish to MCP Registry
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write # REQUIRED for MCP Registry GitHub OIDC login
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install MCP Publisher
run: |
set -euo pipefail
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
echo "Get the latest release version"
LATEST_VERSION=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest | jq -r '.tag_name')
echo "Installing MCP Publisher version: $LATEST_VERSION"
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" \
| tar xz mcp-publisher
- name: Login to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish to MCP Registry
run: ./mcp-publisher publish
+53
View File
@@ -0,0 +1,53 @@
name: Publish PyPI
on:
workflow_dispatch:
concurrency:
group: publish-pypi-${{ github.ref }}
cancel-in-progress: true
jobs:
pypi:
name: Publish PyPI
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install uv
run: pipx install uv
- name: Sync dependencies
run: uv sync --extra dev
- name: Run ruff lint
run: uvx ruff check .
- name: Run ruff format check
run: uvx ruff format --check .
- name: Run tests
run: |
if [ -d tests ]; then
uv run pytest -q -m "not slow and not integration" || [ $? -eq 5 ] # exit 5 = no tests collected
else
echo "No tests/ directory; skipping."
fi
- name: Build package
run: uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
+27
View File
@@ -0,0 +1,27 @@
name: Ruff Lint & Format
on:
pull_request:
push:
branches: [develop, main]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Ruff
run: pip install ruff
- name: Run Ruff check (lint)
run: ruff check .
- name: Run Ruff format (verify formatting)
run: ruff format --check .
+115
View File
@@ -0,0 +1,115 @@
name: Sync main to develop
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
name: Check if sync is needed
outputs:
skip: ${{ steps.check-sync.outputs.SKIP }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Check if sync is needed
id: check-sync
run: |
# Use merge-tree to check if merging main into develop would produce
# any content changes, without touching the working tree. This correctly
# handles cherry-picks where the SHAs differ but the content is identical.
if MERGE_TREE=$(git merge-tree --write-tree origin/develop origin/main 2>&1); then
DEVELOP_TREE=$(git rev-parse origin/develop^{tree})
echo "Merge result tree: $MERGE_TREE"
echo "Develop tree: $DEVELOP_TREE"
if [ "$MERGE_TREE" = "$DEVELOP_TREE" ]; then
echo "Merge would not change develop. Skipping sync."
echo "SKIP=true" >> "$GITHUB_OUTPUT"
else
echo "Merge would introduce changes. Sync needed."
echo "SKIP=false" >> "$GITHUB_OUTPUT"
fi
else
echo "Merge would have conflicts. Sync needed."
echo "SKIP=false" >> "$GITHUB_OUTPUT"
fi
sync:
needs: check
if: needs.check.outputs.skip != 'true'
runs-on: ubuntu-latest
name: Sync main to develop
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up git user
run: |
git config user.name "RobotMCP Bot"
git config user.email "admin@robotmcp.ai"
- name: Create syncing branch off of main
id: create-sync-branch
run: |
SYNC_BRANCH="main-$(git rev-parse --short HEAD)/sync"
echo "Creating syncing branch: $SYNC_BRANCH"
git switch --create "$SYNC_BRANCH"
git push origin "$SYNC_BRANCH"
echo "SYNC_BRANCH=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
- name: Create syncing PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SYNC_BRANCH: ${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}
run: |
gh pr create \
--base develop \
--head "$SYNC_BRANCH" \
--title "chore: sync main to develop" \
--draft \
--body "Auto-syncing 'main' to 'develop'
If this PR does not merge automatically, there are conflicts between 'main' and 'develop'.
**Do not merge this PR through GitHub.** A maintainer should:
1. Pull this temporary syncing branch locally:
\`\`\`
git fetch origin
git checkout $SYNC_BRANCH
\`\`\`
2. Merge 'develop' into this branch and resolve conflicts:
\`\`\`
git merge origin/develop
# resolve any conflicts, then commit
\`\`\`
3. Push the resolved branch:
\`\`\`
git push origin $SYNC_BRANCH
\`\`\`
4. Run the 'Merge branch into target' workflow from this branch with target branch 'develop'
5. After the workflow completes, this PR will close automatically"
- name: Merge and push to develop
run: |
git switch develop
git merge --no-ff --no-edit --commit --message "chore: sync main to develop" "${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}"
git push origin develop
- name: Cleanup sync branch
continue-on-error: true
env:
SYNC_BRANCH: ${{ steps.create-sync-branch.outputs.SYNC_BRANCH }}
run: git push origin :"$SYNC_BRANCH"
+151
View File
@@ -0,0 +1,151 @@
name: Track View Metrics
on:
workflow_dispatch:
workflow_run:
workflows: ["Track Clone Metrics"] # exact name of PyPI workflow
types: [completed]
jobs:
view-stats:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper branch operations
- name: Generate GitHub App token
id: generate_token
uses: tibdex/github-app-token@v2.1.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Switch to metrics branch
run: |
# Checkout or create metrics branch
if git show-ref --verify --quiet refs/remotes/origin/metrics; then
echo "📋 Checking out existing metrics branch..."
git checkout -b metrics origin/metrics || git checkout metrics
else
echo "🆕 Creating new metrics branch..."
git checkout -b metrics
fi
- name: Fetch view data
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
mkdir -p .metrics
# Fetch view metrics (contains both daily breakdown and 14-day totals)
curl -s -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/${{ github.repository }}/traffic/views \
> .metrics/view_stats.json
echo "View metrics:"
cat .metrics/view_stats.json
- name: Update daily metrics
run: |
# Process each day from the views array
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Create daily CSV with header if it doesn't exist
if [ ! -f .metrics/daily_view_metrics.csv ]; then
echo "date,total_views,unique_visitors,last_updated" > .metrics/daily_view_metrics.csv
fi
echo "📊 Processing daily metrics..."
jq -r '.views[] | "\(.timestamp | split("T")[0]),\(.count),\(.uniques)"' .metrics/view_stats.json | while IFS=',' read -r day_date count uniques; do
echo "Processing $day_date: $count views, $uniques unique"
# Check if this date already exists in the CSV
if grep -q "^$day_date" .metrics/daily_view_metrics.csv; then
echo "📝 Updating existing entry for $day_date..."
# Update existing entry
awk -v date="$day_date" -v count="$count" -v uniques="$uniques" -v last_updated="$LAST_UPDATED" '
BEGIN { FS=","; OFS="," }
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
print $1, count, uniques, last_updated;
updated=1;
next
}
{ print }
' .metrics/daily_view_metrics.csv > .metrics/daily_view_metrics_temp.csv
mv .metrics/daily_view_metrics_temp.csv .metrics/daily_view_metrics.csv
else
echo " Adding new daily entry for $day_date..."
echo "$day_date,$count,$uniques,$LAST_UPDATED" >> .metrics/daily_view_metrics.csv
fi
done
echo "Daily metrics:"
tail -n 5 .metrics/daily_view_metrics.csv
- name: Update 14-day rolling metrics
run: |
# Process 14-day metrics
COUNT_14D=$(jq '.count' .metrics/view_stats.json)
UNIQUES_14D=$(jq '.uniques' .metrics/view_stats.json)
DATE_ONLY=$(date -u +"%Y-%m-%d")
LAST_UPDATED=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
echo "📊 Processing 14-day metrics... for date: $DATE_ONLY"
echo "Processing values: $COUNT_14D views, $UNIQUES_14D unique"
# Create 14-day CSV with header if it doesn't exist
if [ ! -f .metrics/rolling_14d_view_metrics.csv ]; then
echo "date,total_views_14d,unique_visitors_14d,last_updated" > .metrics/rolling_14d_view_metrics.csv
echo "📄 Created new 14-day rolling CSV file"
fi
# Check if today's date already exists in the 14-day CSV
if grep -q "^$DATE_ONLY" .metrics/rolling_14d_view_metrics.csv; then
echo "📝 Updating existing 14-day rolling entry for $DATE_ONLY..."
# Update existing entry
awk -v date="$DATE_ONLY" -v count="$COUNT_14D" -v uniques="$UNIQUES_14D" -v last_updated="$LAST_UPDATED" '
BEGIN { FS=","; OFS=","; updated=0 }
/^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $1 == date {
print $1, count, uniques, last_updated;
updated=1;
next
}
{ print }
END { if (!updated) print date, count, uniques, last_updated }
' .metrics/rolling_14d_view_metrics.csv > .metrics/rolling_14d_view_metrics_temp.csv
mv .metrics/rolling_14d_view_metrics_temp.csv .metrics/rolling_14d_view_metrics.csv
echo "✅ Updated existing entry"
else
echo " Adding new 14-day rolling entry for $DATE_ONLY..."
echo "$DATE_ONLY,$COUNT_14D,$UNIQUES_14D,$LAST_UPDATED" >> .metrics/rolling_14d_view_metrics.csv
echo "✅ Added new entry"
fi
echo "14-day rolling metrics:"
tail -n 5 .metrics/rolling_14d_view_metrics.csv
- name: Commit and push results
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
git config user.name "ViewMetricsBot[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Add both CSV files
git add .metrics/daily_view_metrics.csv .metrics/rolling_14d_view_metrics.csv
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "️ No changes to commit - CSV data is up to date"
else
echo "📝 Committing changes..."
git commit -m "Automated update: repository view metrics $(date)"
echo "🚀 Pushing to metrics branch..."
git push --force-with-lease origin metrics
fi
+184
View File
@@ -0,0 +1,184 @@
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# DS Store
.DS_Store
# .vscode
.vscode/
.vs/
# camera
camera/*
# superpowers plans
docs/superpowers/
+1
View File
@@ -0,0 +1 @@
3.10
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2025] [Contoro Inc.]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+2
View File
@@ -0,0 +1,2 @@
include server.json
recursive-include robot_specifications *.yaml
+105
View File
@@ -0,0 +1,105 @@
# ROS MCP Server 🧠⇄🤖
![Static Badge](https://img.shields.io/badge/ROS-Available-green)
![Static Badge](https://img.shields.io/badge/ROS2-Available-green)
![Static Badge](https://img.shields.io/badge/License-Apache%202.0-blue)
![Python](https://img.shields.io/badge/python-3.10%2B-blue)
![pip](https://img.shields.io/badge/pip-23.0%2B-blue)
![Dev Container](https://img.shields.io/badge/Dev-Container%20Ready-blue)
![GitHub Repo stars](https://img.shields.io/github/stars/robotmcp/ros-mcp-server?style=social)
![GitHub last commit](https://img.shields.io/github/last-commit/robotmcp/ros-mcp-server)
<!-- mcp-name: io.github.robotmcp/ros-mcp-server -->
<p align="center">
<img src="https://github.com/robotmcp/ros-mcp-server/blob/main/docs/images/framework.png"/>
</p>
ROS-MCP-Server connects large language models (such as Claude, GPT, and Gemini) to robots, enabling bidirectional communication with no changes to existing robot source code.
### Why ROS-MCP?
- **No robot source code changes** → just add the `rosbridge` node to your existing ROS setup.
- **True two-way communication** → LLMs can both *control* robots and *observe* everything happening on the Robot.
- **Full context** → publish & subscribe to topics, call services & actions, set parameters, read sensor data, and monitor robot state in real time.
- **Deep ROS understanding** → guides the LLM to discover available topics, services, actions, and their types (including custom ones) — enabling it to use them with the right syntax without manual configuration.
- **Works with any MCP client** → built on the open [MCP standard](https://modelcontextprotocol.io/), supporting Claude Code, Codex CLI, Gemini CLI, Claude Desktop, ChatGPT, Cursor, and more.
- **Works across ROS versions** → compatible across ROS 2 (Jazzy, Humble, and others) and ROS 1 distros.
---
## 🎥 Examples in Action
<p align="center">
<a href="https://youtu.be/Yy1loJAn9sA">
<img src="https://github.com/robotmcp/ros-mcp-server/blob/main/docs/images/MCP%20Demos%20Slide%20-%207to12s.gif" alt="ROS MCP demos" />
</a>
</p>
---
🏭 **Example - AI Agent diagnosis of Industrial Robot End Effector** ([Video](https://youtu.be/EhZNFULz9P4))
- The MCP server connects Claude to a production industrial robot, with only the technician manuals as reference.
- Claude discovers the robot's custom topic and service types and their syntax on its own.
- From a single prompt to test the gripper, it reads the manuals, runs its own tests, finds an anomaly, and reports the root cause.
<p align="center">
<a href="https://youtu.be/EhZNFULz9P4">
<img src="https://github.com/robotmcp/ros-mcp-server/blob/main/docs/images/ROS%20MCP%20Gripper%20vacuum%20test.jpg" width="400" alt="Testing and debugging an industrial robot" />
</a>
</p>
---
🤖 **Example - Controlling "Wilson" with natural language** ([video](https://www.traceglarue.com/wilson))
From a single prompt — *"Grab a Coke from the fridge & go to the living room."* — Google Gemini uses the MCP server to navigate and manipulate autonomously. Built on ROS 2 with Nav2 (SLAM) for mapping and navigation, and MoveIt to command the manipulator.
<p align="center">
<a href="https://www.traceglarue.com/wilson">
<img src="https://github.com/robotmcp/ros-mcp-server/blob/main/docs/images/Wilson%20thumbnail.jpg" width="400" alt="Wilson robot controlled with natural language" />
</a>
</p>
---
🐕 **Example - Controlling Unitree Go2 in NVIDIA Isaac Sim** ([video](https://www.youtube.com/watch?v=9StFx4lnvmc))
The MCP server connects Claude to a simulated Unitree Go2 quadruped in NVIDIA Isaac Sim, interpreting natural language commands to navigate and control the robot.
<p align="center">
<a href="https://www.youtube.com/watch?v=9StFx4lnvmc">
<img src="https://img.youtube.com/vi/9StFx4lnvmc/maxresdefault.jpg" width="400" alt="Controlling Unitree Go2 in NVIDIA Isaac Sim" />
</a>
</p>
---
## 🛠 Getting Started
Follow the [installation guide](docs/install/installation.md) to get started.
ROS-MCP works with Claude Code, Codex CLI, Gemini CLI, Claude Desktop, ChatGPT, Cursor, or any MCP-compatible client.
<p align="center">
<img src="https://github.com/robotmcp/ros-mcp-server/blob/main/docs/images/MCP_topology.png"/>
</p>
---
## 📚 More Examples & Tutorials
Browse our [examples](examples) to see the server in action.
We welcome community PRs with new examples and integrations!
---
## 🤝 Contributing
We love contributions of all kinds:
- Bug fixes and documentation updates
- New features (e.g., Action support, permissions)
- Additional examples and tutorials
Check out the [contributing guidelines](docs/contributing.md) and see issues tagged **good first issue** to get started.
---
## 📜 License
This project is licensed under the [Apache License 2.0](LICENSE).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`robotmcp/ros-mcp-server`
- 原始仓库:https://github.com/robotmcp/ros-mcp-server
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+1
View File
@@ -0,0 +1 @@
# Config package for ROS MCP Server
+33
View File
@@ -0,0 +1,33 @@
{
"mcpServers": {
"ros-mcp-server-http": {
"name": "ROS-MCP Server (http)",
"transport": "http",
"url": "http://127.0.0.1:9000/mcp"
},
"ros-mcp-server-stdio-linux": {
"name": "ROS-MCP Server (stdio)",
"transport": "stdio",
"command": "uv",
"args": [
"--directory",
"/home/<YOUR_USER>/ros-mcp-server",
"run",
"server.py"
]
},
"ros-mcp-server-stdio-wsl": {
"name": "ROS-MCP Server (stdio)",
"transport": "stdio",
"command": "wsl",
"args": [
"-d", "Ubuntu",
"/home/<YOUR_USER>/.local/bin/uv",
"--directory",
"/home/<YOUR_USER>/ros-mcp-server",
"run",
"server.py"
]
}
}
}
+304
View File
@@ -0,0 +1,304 @@
# ROS MCP Server Architecture
This document describes the architecture of the ROS MCP Server, including its components, organization, and design patterns.
## Overview
The ROS MCP Server is a Model Context Protocol (MCP) server that provides tools, resources, and prompts for interacting with ROS (Robot Operating System) robots via the rosbridge WebSocket interface. The server is built using FastMCP and follows a modular, category-based architecture.
## Architecture Principles
1. **Modular Design**: Tools, resources, and prompts are organized by category into separate modules
2. **Separation of Concerns**: Clear boundaries between tools, resources, prompts, and utilities
3. **Reusability**: Shared utilities and WebSocket manager for consistent ROS communication
4. **Extensibility**: Easy to add new tools, resources, or prompts by following established patterns
5. **Library-First**: Designed to be importable as a library for integration into other projects
## Directory Structure
```
ros-mcp-server/
├── ros_mcp/ # Main package
│ ├── __init__.py # Package initialization
│ ├── main.py # MCP server instance and entry point
│ │
│ ├── tools/ # Tool implementations (31 tools)
│ │ ├── __init__.py # Main registration function (public API)
│ │ ├── actions.py # Action tools (7 tools)
│ │ ├── connection.py # Connection tools (2 tools)
│ │ ├── images.py # Image analysis tools (1 tool + helpers)
│ │ ├── nodes.py # Node tools (3 tools)
│ │ ├── parameters.py # Parameter tools (7 tools)
│ │ ├── robot_config.py # Robot configuration tools (3 tools)
│ │ ├── services.py # Service tools (6 tools)
│ │ └── topics.py # Topic tools (10 tools)
│ │
│ ├── resources/ # Resource implementations
│ │ ├── __init__.py # Resource registration function
│ │ ├── robot_specs.py # Robot specification resources
│ │ └── ros_metadata.py # ROS metadata resources (5 resources)
│ │
│ ├── prompts/ # Prompt templates
│ │ ├── __init__.py # Prompt registration function
│ │ ├── test_actions_tools.py
│ │ ├── test_connection_tools.py
│ │ ├── test_nodes_tools.py
│ │ ├── test_parameters_tools.py
│ │ ├── test_server_tools.py
│ │ ├── test_services_tools.py
│ │ └── test_topics_tools.py
│ │
│ └── utils/ # Utility modules
│ ├── __init__.py
│ ├── config_utils.py # Robot configuration utilities
│ ├── network_utils.py # Network connectivity utilities
│ └── websocket.py # WebSocket manager for ROS communication
├── server.py # Entry point script
├── robot_specifications/ # Robot specification YAML files
└── docs/ # Documentation
```
## Core Components
### 1. Main Entry Point (`ros_mcp/main.py`)
The main entry point initializes the MCP server and registers all components:
```python
# Initialize MCP server
mcp = FastMCP("ros-mcp-server")
# Initialize WebSocket manager
ws_manager = WebSocketManager(ROSBRIDGE_IP, ROSBRIDGE_PORT, default_timeout=5.0)
# Register all components
register_all_tools(mcp, ws_manager, rosbridge_ip=ROSBRIDGE_IP, rosbridge_port=ROSBRIDGE_PORT)
register_all_resources(mcp, ws_manager)
register_all_prompts(mcp)
```
### 2. Tools (`ros_mcp/tools/`)
Tools are the primary interface for interacting with ROS systems. They are organized by category and follow a consistent pattern.
#### Tool Categories: Total 31 tools
| Category | File | Count | Description |
|----------|------|-------|-------------|
| **Connection** | `connection.py` | 2 | Robot connection and connectivity testing |
| **Robot Config** | `robot_config.py` | 3 | Robot specification and ROS version detection |
| **Topics** | `topics.py` | 8 | Topic discovery, subscription, and publishing |
| **Services** | `services.py` | 4 | Service discovery and calling |
| **Nodes** | `nodes.py` | 2 | Node discovery and inspection |
| **Parameters** | `parameters.py` | 6 | Parameter management (ROS 2 only) |
| **Actions** | `actions.py` | 5 | Action discovery and execution (ROS 2 only) |
| **Images** | `images.py` | 1 | Image analysis and processing |
#### Tool Template
All tools follow a consistent pattern with decorator, description, and comprehensive docstring:
```python
def register_category_tools(mcp: FastMCP, ws_manager: WebSocketManager) -> None:
"""Register all category-related tools."""
@mcp.tool(
description=(
"Brief description of what the tool does.\n"
"Example:\n"
"tool_name(param1='value1', param2=123)"
)
)
def tool_name(param1: str, param2: int = 0) -> dict:
"""
Comprehensive description of the tool's functionality.
Args:
param1 (str): Description of parameter 1 (e.g., 'example value')
param2 (int): Description of parameter 2 (e.g., 123). Default is 0.
Returns:
dict: Contains result data with specific fields,
or an error message if operation fails.
"""
# Implementation logic here
# Use ws_manager for ROS communication
with ws_manager:
# Tool implementation
pass
return {"result": "data"}
```
**Key Components:**
1. **Decorator** (`@mcp.tool`):
- Contains `description` parameter with brief explanation
- Includes usage examples when helpful
- Description is visible to LLMs for tool selection
2. **Function Docstring**:
- Comprehensive description of functionality
- **Args section**: Documents all parameters with types, descriptions, and examples
- **Returns section**: Documents return value structure and possible error cases
- Docstring is for developer reference and IDE tooltips
3. **Implementation**:
- Inline implementation
- Uses `ws_manager` context manager for ROS communication when needed
- Returns structured dictionaries with consistent error handling
#### Public API
The main registration function in `tools/__init__.py`:
```python
def register_all_tools(
mcp: FastMCP,
ws_manager: WebSocketManager,
rosbridge_ip: str = "127.0.0.1",
rosbridge_port: int = 9090,
) -> None:
"""Register all ROS MCP tools with the provided FastMCP instance."""
register_action_tools(mcp, ws_manager)
register_connection_tools(mcp, ws_manager, rosbridge_ip, rosbridge_port)
# ... other categories
```
### 3. Resources (`ros_mcp/resources/`)
Resources provide comprehensive system information in JSON format. They are accessed via URIs and return structured data.
#### Resource Types
**ROS Metadata Resources:**
- `ros-mcp://ros-metadata/all` - Complete system overview
- `ros-mcp://ros-metadata/topics/all` - All topics with details
- `ros-mcp://ros-metadata/services/all` - All services with details
- `ros-mcp://ros-metadata/nodes/all` - All nodes with details
- `ros-mcp://ros-metadata/actions/all` - All actions with details (ROS 2 only)
**Robot Specification Resources:**
- `ros-mcp://robot-specs/get_verified_robots_list` - List of available robot specifications
#### Public API
The main registration function is found in `resources/__init__.py`:
```python
def register_all_resources(
mcp: FastMCP,
ws_manager: WebSocketManager,
) -> None:
"""Register all resources with the MCP server instance."""
register_robot_spec_resources(mcp)
register_ros_metadata_resources(mcp, ws_manager)
```
### 4. Prompts (`ros_mcp/prompts/`)
Prompts are interactive guides that help users test and understand the ROS MCP Server tools.
#### Prompt Categories
- `test-server-tools` - High-level overview
- `test-connection-tools` - Connection testing
- `test-topics-tools` - Topic tools testing
- `test-services-tools` - Service tools testing
- `test-nodes-tools` - Node tools testing
- `test-parameters-tools` - Parameter tools testing (ROS 2)
- `test-actions-tools` - Action tools testing (ROS 2)
### 5. Utilities (`ros_mcp/utils/`)
Utilities provide shared functionality used across tools and resources.
#### Utility Modules
**`websocket.py` - WebSocket Manager**
- Manages WebSocket connections to rosbridge
- Provides request/response interface for ROS communication
- Handles connection lifecycle and error handling
- Thread-safe context manager for connection management
**`network_utils.py` - Network Utilities**
- `ping_ip_and_port()` - Test network connectivity
- Platform-specific ping implementation
- Port availability checking
**`config_utils.py` - Configuration Utilities**
- `load_robot_config()` - Load robot specification YAML files
- `get_verified_robot_spec_util()` - Parse and validate robot configs
- `get_verified_robots_list_util()` - List available robot specifications
## Extension Points
### Adding a New Tool
1. Create implementation function in appropriate category file
2. Create tool wrapper with `@mcp.tool` decorator
3. Register in category's `register_*_tools()` function
4. Tool is automatically available after server restart
### Adding a New Resource
1. Create resource function in `resources/ros_metadata.py` or `resources/robot_specs.py`
2. Use `@mcp.resource` decorator with URI
3. Add to `register_all_resources()` in `resources/__init__.py`
4. Resource is automatically available after server restart
### Adding a New Prompt
1. Create prompt function in `prompts/test_*.py`
2. Use `@mcp.prompt` decorator with name
3. Add to `register_all_prompts()` in `prompts/__init__.py`
4. Prompt is automatically available after server restart
## Integration
The ROS MCP Server is designed to be importable as a library:
```python
from ros_mcp.tools import register_all_tools
from ros_mcp.resources import register_all_resources
from ros_mcp.prompts import register_all_prompts
from ros_mcp.utils.websocket import WebSocketManager
# In your MCP server
mcp = FastMCP("your-server")
ws_manager = WebSocketManager("127.0.0.1", 9090)
register_all_tools(mcp, ws_manager, rosbridge_ip="127.0.0.1", rosbridge_port=9090)
register_all_resources(mcp, ws_manager)
register_all_prompts(mcp)
```
## Dependencies
### Core Dependencies
- **FastMCP**: MCP server framework
- **websocket-client**: WebSocket communication
- **opencv-python**: Image processing
- **numpy**: Numerical operations
- **PyYAML**: Robot configuration parsing
### ROS Dependencies
- **rosbridge_server**: ROS WebSocket bridge (external, must be running)
## Testing
See `docs/testing.md` for detailed testing instructions.
## Related Documentation
- **Testing Guide**: `docs/testing.md` - How to test the server
- **Restructuring Plan**: `docs/restructuring_plan.md` - Migration history
- **Launch System**: `docs/launch_system.md` - ROS launch guide
- **Installation**: `docs/install/installation.md` - Setup instructions
+175
View File
@@ -0,0 +1,175 @@
# Contributing to ROS-MCP-Server
---
## 📌 Ways to Contribute
- **Report issues**: Bug reports, documentation gaps, or feature requests.
- **Improve documentation**: Clarify installation steps, add tutorials, or share demos.
- **Submit code**: Bug fixes, new features, refactors, or tests.
- **Share use cases**: Show how you use ROS-MCP with your robot.
- **Community support**: Answer GitHub Discussions or ROS Discourse questions.
---
## 🔀 How to Pull-request
1. **Fork the repository:** Click the "Fork" button in the top right corner of the GitHub page.
2. **Create a new branch:** Create a separate branch for your changes to keep them isolated from the main project.
```bash
git checkout -b "your_branch_name"
```
3. **Make your changes:** Edit files with your additions or corrections. Please follow the existing format and style guidelines. When adding a new function, make sure to include:
* The function name and format.
* A brief description of the function's functionality.
* Any parameters the function takes.
* The return type of the function.
4. **Commit your changes:** Commit your changes with a clear and concise message explaining what you've done.
```bash
git commit -m "your_commit_message"
```
5. **Push your branch:** Push your branch to your forked repository.
```bash
git push origin "your_branch_name"
```
6. **Create a pull request:** Navigate to the original repository and click the "New pull request" button. Select your fork and the branch you pushed. **Important:** Target the `develop` branch (not `main`) for your pull request. Provide a clear title and description of your changes.
7. **Review and merge:** Your pull request will be reviewed by the maintainers. They may suggest changes or ask for clarification. Once approved, your changes will be merged into the main repository.
Thank you for contributing!
---
## Style & CI
We use **Ruff** for both linting and formatting. CI requires:
- `ruff format --check .`
- `ruff check .`
**Local setup**
- Option A (recommended): `pre-commit install` (auto-fixes on commit)
- Option B (manual): `ruff format . && ruff check --fix .`
**Using Black locally?**
- That's fine. Please align with:
- `line-length = 100`, target `py310`
- Avoid `--preview`, `--skip-string-normalization`, `--skip-magic-trailing-comma`
- CI uses Ruff as the final arbiter; run `ruff format .` before pushing.
---
## Optional: Using Devcontainer
The devcontainer provides a stable testing platform with ROS2 humble pre-installed as well as an environment to test the MCP server in http transport. (stdio transport is not compatible with the devcontainer)
1. Install [VSCode](https://code.visualstudio.com/) and the **Remote - Containers** extension.
2. Open the `ros-mcp-server` repository in VSCode.
3. When prompted, **reopen in container**.
- The container includes ROS2 Humble, Python 3.10+, `ruff`, `pre-commit`, `uv`, and `git`.
- The repository is mounted at `/root/workspace`.
- **Note for GUI apps** (`turtlesim`, `rviz`, `Gazebo`):
Ensure the container can access your host X server by running the following command once on the host:
<details>
<summary> Ubuntu host </summary>
```bash
sudo apt install x11-xserver-utils # if xhost is not installed
xhost +local:root # allow container user access
```
</details>
<details>
<summary> Windows WSL2 host </summary>
```bash
export DISPLAY=$(grep nameserver /etc/resolv.conf | awk '{print $2}'):0
export QT_X11_NO_MITSHM=1
xhost +local:root
```
</details>
<details>
<summary> macOS host </summary>
Install [XQuartz](https://www.xquartz.org/) and enable **"Allow connections from network clients"** in XQuartz → Settings → Security. Log out and back in (or restart XQuartz) for the setting to take effect.
```bash
xhost + # allow connections (run on macOS host, not inside the container)
```
Inside the devcontainer terminal, override the display variable:
```bash
export DISPLAY=host.docker.internal:0 # Docker's built-in DNS to reach host XQuartz
```
**Note:** The `postStartCommand` (`xhost +local:vscode || true`) runs inside the container and only works on Linux hosts with a shared X11 socket. On macOS, you must run `xhost` on the host side as shown above.
</details>
<details>
<summary> NVIDIA GPU passthrough (Linux only) </summary>
If you have an NVIDIA GPU and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed, you can enable GPU passthrough by adding `"--gpus=all"` to `runArgs` in `.devcontainer/devcontainer.json`:
```json
"runArgs": [
"--gpus=all",
...
]
```
This flag is not included by default because it prevents the container from starting on macOS and Windows, where the NVIDIA Container Toolkit is not available.
</details>
4. You can now control the Turtlesim robot following the [Rosbridge setup](install/rosbridge.md) and [Connect to your robot](install/connect.md) guides.
5. Initialize pre-commit hooks (optional but recommended):
```bash
pre-commit install
pre-commit run --all-files
```
6. Check Python code formatting with `ruff`
```bash
ruff check .
ruff format --check .
```
<details>
<summary>SSH Agent Setup for Git (click to expand)</summary>
This should be run on the host side prior to building the devcontainer.
```bash
# Start the SSH agent
eval "$(ssh-agent -s)"
# List keys currently loaded
ssh-add -l
```
If it says “The agent has no identities”, you must load your key, for example:
```bash
ssh-add ~/.ssh/id_ed25519
ssh-add -l # confirm fingerprint shows up
```
</details>
**Note:** This setup has been tested and verified on Ubuntu and macOS (Apple Silicon).
## License
This project is licensed under the **Apache License 2.0**. By contributing to this project, you agree that your contributions will be licensed under the same license.
**Key points for contributors:**
- Your contributions will be licensed under Apache 2.0
- You retain copyright to your contributions
- You grant the project a perpetual, worldwide, non-exclusive license to use your contributions
- No additional legal agreements required for standard contributions
For the full license text, see [LICENSE](../LICENSE) in the project root.
+16
View File
@@ -0,0 +1,16 @@
# Governance
1. **Neutral Standards**
The technology specifications of ROS-MCP-Server will be openly discussed and decided through a Technical Steering Committee (TSC) that includes representatives from participating industrial and research partners.
2. **Open Development**
All core code, demos, and tutorials will be developed in public repositories under a permissive open-source license (Apache 2.0). Discussions and proposals will be tracked transparently.
3. **Fair Contribution Model**
Contributions will be accepted based on technical merit through the standard GitHub pull-request process, reviewed by maintainers.
4. **Safety & Responsibility**
Given the power of AI-robotics, we commit to developing safety guidelines and best practices, reviewed by both technical and ethics advisors.
5. **Commercial vs. Community Boundary**
ROS-MCP-Server core will always remain free and open. Contoro may offer commercial services (enterprise feature support, certified distributions, integrations, and others) on top, but the open standard will not be locked behind proprietary barriers.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

+86
View File
@@ -0,0 +1,86 @@
# Step 1: ChatGPT Setup
[ChatGPT Desktop](https://chatgpt.com/download) is OpenAI's desktop application. It supports MCP servers through its Connectors settings. Download it from [chatgpt.com/download](https://chatgpt.com/download) or the Microsoft Store.
> **Recommended alternative:** ChatGPT's MCP setup is more complex than other clients because it requires an ngrok tunnel. For a simpler experience, consider using [Codex CLI](codex-cli.md) or [Claude Code](claude-code.md) instead.
> **Note:** ChatGPT Desktop requires a public HTTPS URL to connect to MCP servers, so you'll need to run the MCP server in HTTP mode with an [ngrok](https://ngrok.com) tunnel. See the [HTTP transport](../http-transport.md) guide for details on the transport layer.
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Install and Configure ngrok
[ngrok](https://ngrok.com) creates a public HTTPS tunnel to your local MCP server.
1. Install ngrok from [ngrok.com/download](https://ngrok.com/download)
2. Create an account and add your authtoken:
```bash
ngrok config add-authtoken <YOUR_AUTHTOKEN>
```
3. Get a free static domain from the [ngrok dashboard](https://dashboard.ngrok.com/domains) (e.g., `abc123-xyz789.ngrok-free.app`)
## 3. Start the MCP Server and Tunnel
Start the MCP server in HTTP mode:
```bash
uvx ros-mcp --transport streamable-http --host 127.0.0.1 --port 9000
```
In a separate terminal, start the ngrok tunnel:
```bash
ngrok http --url=<your-domain>.ngrok-free.app 9000
```
Verify the tunnel is working:
```bash
curl https://<your-domain>.ngrok-free.app/mcp
```
## 4. Configure ChatGPT
1. Open ChatGPT Desktop
2. Go to **Settings** (bottom left) > **Connectors**
3. Create a new connector:
- **Name:** ROS-MCP Server
- **MCP Server URL:** `https://<your-domain>.ngrok-free.app/mcp`
- **Authentication:** No authentication
- Check **I trust this application**
4. In a new chat, click **+** > **Developer Mode** > **Add sources** > Activate **ROS-MCP Server**
## 5. Verify the Setup
Ask ChatGPT:
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
> **Tip:** If your AI assistant can't find the ros-mcp server, make sure both the MCP server and ngrok tunnel are still running, then restart ChatGPT Desktop.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+66
View File
@@ -0,0 +1,66 @@
# Step 1: Claude Code Setup
[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) is Anthropic's CLI tool for working with Claude. It supports MCP servers natively. If you don't have it yet, see the [installation instructions](https://docs.anthropic.com/en/docs/claude-code/overview#getting-started).
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Add the MCP Server
Add the ROS-MCP server to Claude Code:
```bash
claude mcp add ros-mcp -- uvx ros-mcp --transport=stdio
```
This registers the server so that Claude Code will launch it automatically when needed.
## 3. Verify the Setup
Verify the server was added:
```bash
claude mcp list
```
You should see `ros-mcp` in the output.
Now start Claude Code and ask it to connect to a robot:
```bash
claude
```
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
> **Tip:** If your AI assistant can't find the ros-mcp server, exit and restart Claude Code so it picks up the new configuration.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service using the MCP http transport instead of default stdio transport.
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+115
View File
@@ -0,0 +1,115 @@
# Step 1: Claude Desktop Setup
[Claude Desktop](https://claude.ai/download) is Anthropic's desktop application for Claude. It supports MCP servers through a JSON configuration file.
- **Linux**: Install via the community-supported [claude-desktop-debian](https://github.com/aaddrick/claude-desktop-debian)
- **macOS / Windows**: Download from [claude.ai/download](https://claude.ai/download)
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Add the MCP Server
> ⚠️ **Important**: Quit Claude Desktop **completely** before editing `claude_desktop_config.json`. If the app is running in the background when you save the file, your changes can be silently overwritten the moment you enter a chat. Terminate the process first:
>
> - **Linux:** `pkill -f claude-desktop`
> - **macOS:** Right-click the dock icon → Quit, or `pkill -f -i claude` in Terminal
> - **Windows:** End the `Claude` task in Task Manager (Ctrl+Shift+Esc → find *Claude* → End task)
Locate and edit the `claude_desktop_config.json` file (create it if it doesn't exist):
| OS | Config file path |
|----|-----------------|
| Linux | `~/.config/Claude/claude_desktop_config.json` |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
Add the following to the file:
**Linux:**
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "bash",
"args": ["-lc", "uvx ros-mcp --transport=stdio"]
}
}
}
```
**macOS:**
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "zsh",
"args": ["-lc", "uvx ros-mcp --transport=stdio"]
}
}
}
```
> **macOS: "Could not attach to MCP server"?** `zsh -lc` sources `~/.zprofile` but not `~/.zshrc`, where the `uv` installer puts `~/.local/bin` on your `PATH` — so the subprocess can't find `uvx`. Fix it by adding `~/.local/bin` to `~/.zprofile`, or by using the absolute path to `uvx`. See [Troubleshooting](../troubleshooting.md#macos-could-not-attach-to-mcp-server--uvx-not-found).
**Windows (WSL):**
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "wsl",
"args": [
"-d", "Ubuntu-22.04",
"bash", "-lc",
"uvx ros-mcp --transport=stdio"
]
}
}
}
```
> **WSL users**: Replace `"Ubuntu-22.04"` with your actual WSL distribution name. Check with `wsl --list --verbose`.
## 3. Verify the Setup
1. Completely restart Claude Desktop — the MCP server list is cached on startup. If it doesn't appear, fully terminate the app and relaunch:
- **Linux:** `pkill -f claude-desktop && claude-desktop`
- **macOS:** Right-click the dock icon > Quit, or use Activity Monitor to force quit
- **Windows:** End the task in Task Manager, then relaunch
2. Check that **ros-mcp-server** appears in your list of tools.
> **Tip:** If you're having trouble with the config file, ask Claude to help set it up. Tell it that the command to run the MCP server is `uvx ros-mcp` and it can generate the correct configuration for your OS.
Once the server appears, try asking Claude:
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service instead of stdio
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+65
View File
@@ -0,0 +1,65 @@
# Step 1: Codex CLI Setup
[Codex CLI](https://github.com/openai/codex) is OpenAI's CLI agent for software development. It supports MCP servers natively. If you don't have it yet, see the [installation instructions](https://github.com/openai/codex#getting-started).
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Add the MCP Server
Edit your Codex CLI configuration file at `~/.codex/config.json` and add the MCP server:
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "uvx",
"args": ["ros-mcp", "--transport=stdio"]
}
}
}
```
> **Tip:** If you're having trouble with the config file, ask Codex to help set it up. Tell it that the command to run the MCP server is `uvx ros-mcp` and it can generate the correct configuration.
## 3. Verify the Setup
Start Codex CLI and ask it to connect to a robot:
```bash
codex
```
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
> **Tip:** If your AI assistant can't find the ros-mcp server, exit and restart Codex CLI so it picks up the new configuration.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service instead of stdio
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+80
View File
@@ -0,0 +1,80 @@
# Step 1: Cursor Setup
[Cursor](https://cursor.com) is an AI-powered IDE. It supports MCP servers through its settings. If you don't have it yet, download it from [cursor.com/downloads](https://cursor.com/downloads).
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Add the MCP Server
In Cursor, go to **Settings > MCP > New MCP Server** and edit the `mcp.json` file:
**Linux / macOS:**
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "uvx",
"args": ["ros-mcp", "--transport=stdio"]
}
}
}
```
**Windows (WSL):**
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "wsl",
"args": [
"-d", "Ubuntu",
"/home/<YOUR_USER>/.local/bin/uvx",
"ros-mcp", "--transport=stdio"
]
}
}
}
```
> **WSL users**: Replace `<YOUR_USER>` with your WSL username and `"Ubuntu"` with your distribution name. Verify the path to uvx with `wsl -- which uvx`.
> **Tip:** If you're having trouble with the config file, ask Cursor to help set it up. Tell it that the command to run the MCP server is `uvx ros-mcp` and it can generate the correct configuration.
## 3. Verify the Setup
Open a new chat in Cursor and ask:
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
> **Tip:** If your AI assistant can't find the ros-mcp server, restart Cursor so it picks up the new configuration.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service instead of stdio
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+51
View File
@@ -0,0 +1,51 @@
# Step 1: Custom / Programmatic Client
You can use the ROS-MCP server directly in your Python code using the [MCP SDK](https://github.com/modelcontextprotocol/python-sdk).
## Example
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server_params = StdioServerParameters(
command="uvx",
args=["ros-mcp", "--transport=stdio"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the session
await session.initialize()
# List available tools
tools = await session.list_tools()
print(tools)
# Call a tool
result = await session.call_tool("get_topics", {})
print(result)
```
## Prerequisites
- **uv** installed — see [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/)
- **MCP Python SDK** — `pip install mcp`
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service instead of stdio
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+65
View File
@@ -0,0 +1,65 @@
# Step 1: Gemini CLI Setup
[Gemini CLI](https://github.com/google-gemini/gemini-cli) is Google's CLI for Gemini. It supports MCP servers through a JSON settings file. If you don't have it yet, see the [installation instructions](https://github.com/google-gemini/gemini-cli#installation).
## 1. Install uv
[uv](https://docs.astral.sh/uv/) is needed to run the MCP server via `uvx`.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 2. Add the MCP Server
Edit your Gemini CLI settings file at `~/.gemini/settings.json` and add the MCP server:
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "uvx",
"args": ["ros-mcp", "--transport=stdio"]
}
}
}
```
> **Tip:** If you're having trouble with the config file, ask Gemini to help set it up. Tell it that the command to run the MCP server is `uvx ros-mcp` and it can generate the correct configuration.
## 3. Verify the Setup
Start Gemini CLI and ask it to connect to a robot:
```bash
gemini
```
```
Connect to the robot on localhost using the ros-mcp server
```
The MCP server will attempt to reach a robot on the same machine. It should report that the IP is reachable but the rosbridge port is closed — this confirms the MCP server is set up correctly.
> **Tip:** If your AI assistant can't find the ros-mcp server, exit and restart Gemini CLI so it picks up the new configuration.
To complete the connection, [set up rosbridge](../rosbridge.md) on your robot.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
### Advanced
- [HTTP transport](../http-transport.md) — run the MCP server as a standalone HTTP service instead of stdio
- Alternate installation methods:
- [Install via pip](../pip.md) — traditional `pip install` or install from source with pip
- [Install from source](../from-source.md) — for developers who need to modify the server code
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+26
View File
@@ -0,0 +1,26 @@
# Step 1: Robot MCP Client Setup
[Robot MCP Client](https://github.com/robotmcp/robot-mcp-client) is a lightweight terminal-based client for interacting with the MCP server without desktop LLM applications.
It supports the following LLMs, running natively in the terminal:
- **Anthropic:** Claude Sonnet 4.5
- **OpenAI:** GPT-4.1
- **Google Gemini:** Gemini 2.5 Flash Lite
- **Groq (open-source models):**
- Llama 4 Scout 17B
- Llama 3.1 8B Instant
- Llama 3.3 70B Versatile
- OpenAI GPT-OSS 120B
## Setup
See the [Robot MCP Client repository](https://github.com/robotmcp/robot-mcp-client) for installation and usage instructions.
## Next Step
Set up rosbridge on the machine where ROS is running: [Step 2: Rosbridge Setup](../rosbridge.md)
---
[Back to Installation Guide](../installation.md) | [Troubleshooting](../troubleshooting.md)
+74
View File
@@ -0,0 +1,74 @@
# Step 3: Connect to Your Robot
Now that your AI client has the MCP server configured and rosbridge is running on the robot, you're ready to connect.
## 1. Connect
Open your AI client and tell it to connect to the robot:
```
Connect to the robot at <robot-ip>
```
Replace `<robot-ip>` with your robot's IP address on the local network (e.g., `192.168.1.42`). If the MCP server and ROS are on the same machine, use `localhost`.
The MCP server will report that the IP is reachable and the rosbridge port is open — this means you're connected.
> Make sure the rosbridge port (default 9090) is not blocked by a firewall on the robot's machine.
## 2. Explore
Once connected, try asking your AI client to explore the ROS system:
```
What topics and services are available on the robot?
```
```
What nodes are currently running?
```
The MCP server will query rosbridge and return the results from the robot's ROS environment.
## 3. Try It Out
You can interact with the robot using natural language:
```
Make the robot move forward
```
```
Subscribe to the /odom topic and show me the latest message
```
If you don't have a physical robot, turtlesim is the standard "hello world" for ROS and is a great option to explore and experiment. Launch it using:
**ROS 2:**
```bash
ros2 run turtlesim turtlesim_node
```
**ROS 1:**
```bash
rosrun turtlesim turtlesim_node
```
For a full walkthrough, see the [Turtlesim Tutorial](../../examples/1_turtlesim/README.md).
## More Examples
This repo includes several examples to try with different robots and setups:
- [Turtlesim](../../examples/1_turtlesim/README.md) — the "hello world" of ROS
- [Turtlesim with Docker](../../examples/5_docker_turtlesim/README.md) — no ROS install required
- [LIMO Mobile Robot](../../examples/3_limo_mobile_robot/real_robot/README.md)
- [Unitree Go2](../../examples/4_unitree_go2/real_robot/README.md)
- [TurtleBot3](../../examples/9_turtlebot3/README.md)
- [Image Topics](../../examples/8_images/README.md)
For more advanced demos with simulated robots in Gazebo, see the [ROS-MCP Demos repository](https://github.com/robotmcp/demos-ros-mcp-server) which includes a warehouse TugBot, Unitree Go2 quadruped, and drone control with PX4.
---
[Back to Installation Guide](installation.md) | [Troubleshooting](troubleshooting.md)
+72
View File
@@ -0,0 +1,72 @@
# Installation from Source
This guide is for developers who need to modify the ROS-MCP server source code. For most users, we recommend the standard [installation via uvx](clients/claude-code.md).
## 1. Clone the Repository
```bash
git clone https://github.com/robotmcp/ros-mcp-server.git
```
> **WSL Users**: Clone in your WSL home directory (e.g., `/home/username/`), not the Windows filesystem mount (e.g., `/mnt/c/Users/username/`). The native Linux filesystem provides better performance and avoids permission issues.
## 2. Install uv
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) for other platforms or troubleshooting.
## 3. Install Dependencies
```bash
cd ros-mcp-server
uv sync
```
## 4. Test the Server
```bash
uv run server.py --help
```
If this prints the help output, the server is installed correctly and ready to run.
## 5. Configure Your AI Client
When running from source, use `uv run server.py` with the path to the cloned repository instead of `uvx ros-mcp`.
The recommended approach is to use [HTTP transport](http-transport.md), which runs the server as a standalone process:
```bash
uv run server.py --transport streamable-http --host 127.0.0.1 --port 9000
```
Then point your AI client to `http://127.0.0.1:9000/mcp`. See the [HTTP transport](http-transport.md) page for client configuration details.
Alternatively, you can configure your client to launch the server directly via stdio. For example, with Claude Code:
```bash
claude mcp add ros-mcp -- uv --directory /<path-to>/ros-mcp-server run server.py --transport=stdio
```
For clients that use a JSON config file:
```json
{
"command": "uv",
"args": [
"--directory", "/<path-to>/ros-mcp-server",
"run", "server.py", "--transport=stdio"
]
}
```
## Next Steps
- [Set up your AI client](installation.md#step-1-set-up-your-ai-client)
- [Set up rosbridge on the robot](rosbridge.md)
---
[Back to Installation Guide](installation.md) | [Troubleshooting](troubleshooting.md)
+63
View File
@@ -0,0 +1,63 @@
# HTTP Transport
By default, AI clients launch the MCP server automatically using the **stdio** transport. This is the simplest setup and works for most users.
As an alternative, you can run the MCP server as a standalone HTTP service using the **streamable-http** transport. This is useful when the MCP server needs to be accessed by multiple clients, run on a different machine from the AI client (for example run on the robot), or when [installing the MCP server from source](from-source.md).
## When to Use HTTP Transport
| | STDIO (Default) | HTTP/Streamable-HTTP |
|---|---|---|
| **Best for** | Local development, single-user setups | Remote access, multiple clients, production deployments |
| **Pros** | Simple setup, no network configuration needed | Network accessible, multiple clients can connect |
| **Cons** | MCP server and AI client must be on the same machine | Requires network configuration, server must be started manually |
| **Use case** | Running MCP server directly with your AI client | Remote robots, team environments, web-based clients, development |
## Start the MCP Server in HTTP Mode
```bash
uvx ros-mcp --transport streamable-http --host 127.0.0.1 --port 9000
```
The server will start listening at `http://127.0.0.1:9000/mcp`.
To make it accessible from other machines on the network, use `--host 0.0.0.0`.
## Configure Your AI Client
Point your AI client to the MCP server's HTTP endpoint. The configuration varies by client, but the URL is the same:
```
http://<server-ip>:9000/mcp
```
Example JSON configuration (used by Claude Desktop, Cursor, and others):
```json
{
"mcpServers": {
"ros-mcp-server-http": {
"name": "ROS-MCP Server (http)",
"transport": "http",
"url": "http://127.0.0.1:9000/mcp"
}
}
}
```
## Environment Variables (Legacy)
The server can also be configured using environment variables instead of command-line arguments:
```bash
export MCP_TRANSPORT=streamable-http
export MCP_HOST=127.0.0.1
export MCP_PORT=9000
uvx ros-mcp
```
Command-line arguments take precedence over environment variables.
---
For the default stdio setup, see the [client setup guides](installation.md#step-1-set-up-your-ai-client).
+73
View File
@@ -0,0 +1,73 @@
# Installation Guide
The ROS-MCP server lets any [MCP-compatible](https://modelcontextprotocol.io/) AI assistant control a robot running ROS — even from a different machine on the network.
Setup spans two machines on the **same local network** (or one machine if your AI client runs alongside ROS on the same hardware). A VPN is a great option for connecting over the internet.
| Machine | What to install | Prerequisites | Purpose |
|---------|----------------|---------------|---------|
| **Your machine** (laptop/desktop) | An AI client + the ROS-MCP server | An account with an AI provider (e.g., Claude, Codex, Gemini) | Runs the language model and the MCP server |
| **The robot's machine** | rosbridge | ROS installed | Bridges ROS over WebSocket for the MCP server to connect to |
Follow the three steps below to get up and running. Each step includes quick inline commands and a link to a more detailed guide.
---
## Step 1: Set Up Your AI Client
Quick setup with Claude Code:
```bash
# On your machine:
# 1.1. Install uv (Python package runner)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 1.2. Add the MCP server to Claude Code
claude mcp add ros-mcp -- uvx ros-mcp --transport=stdio
```
For detailed instructions or to set up a different AI client, follow the guide for your client below.
| Client | Description | Guide |
|--------|-------------|-------|
| **Claude Code** (Recommended) | Anthropic's CLI for Claude | [Setup guide](clients/claude-code.md) |
| Codex CLI | OpenAI's CLI agent | [Setup guide](clients/codex-cli.md) |
| Gemini CLI | Google's CLI for Gemini | [Setup guide](clients/gemini-cli.md) |
| Claude Desktop | Anthropic's desktop app | [Setup guide](clients/claude-desktop.md) |
| ChatGPT | OpenAI's desktop app | [Setup guide](clients/chatgpt.md) |
| Cursor | AI-powered IDE | [Setup guide](clients/cursor.md) |
| Robot MCP Client | Lightweight terminal client | [Setup guide](clients/robot-mcp-client.md) |
| Custom / Programmatic | Python MCP SDK | [Setup guide](clients/custom-client.md) |
## Step 2: Set Up Rosbridge on the Robot
Install and launch rosbridge on the machine where ROS is running. See the [Step 2: Rosbridge setup guide](rosbridge.md) for detailed instructions. Quick setup below:
```bash
# On the robot:
# 2.1. Install Rosbridge
sudo apt update
sudo apt install ros-<your ros distro>-rosbridge-server
```
```bash
# 2.2. Launch Rosbridge
source /<path to ros WS>/install/setup.bash
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
## Step 3: Connect to Your Robot
See the [Step 3: Connect and explore](connect.md) guide for connecting to your robot and sample commands. For a quick start, launch your AI assistant and type:
```
Connect to the robot on <ip address> and tell me what topics and services you see.
```
---
## Additional Resources
- [Troubleshooting](troubleshooting.md) — common issues and debug commands
- [Examples](../../examples/) — tutorials for turtlesim, Unitree Go2, LIMO, TurtleBot3, and more
- [ROS-MCP Demos](https://github.com/robotmcp/demos-ros-mcp-server) — advanced demos with simulated robots in Gazebo
+51
View File
@@ -0,0 +1,51 @@
# Install via pip
For most users, we recommend installing via [uvx](clients/claude-code.md) instead. This page covers alternative pip-based installation methods.
## pip install
```bash
pip install ros-mcp
```
> **Requirements:** pip 23.0+ and Python 3.10+. Check your versions with `pip --version` and `python3 --version`. Upgrade pip if needed:
> ```bash
> python3 -m pip install --upgrade pip
> ```
## pip install from source
```bash
git clone https://github.com/robotmcp/ros-mcp-server.git
cd ros-mcp-server
pip install .
```
> **Requirements:** pip 23.0+ and Python 3.10+.
## Configuring Your AI Client
When using pip install, the `ros-mcp` command is installed directly into your environment. Use `ros-mcp` instead of `uvx ros-mcp` when configuring your AI client.
For example, with Claude Code:
```bash
claude mcp add ros-mcp -- ros-mcp --transport=stdio
```
For clients that use a JSON config file:
```json
{
"command": "ros-mcp",
"args": ["--transport=stdio"]
}
```
## Next Steps
- [Set up your AI client](installation.md#step-1-set-up-your-ai-client)
- [Set up rosbridge on the robot](rosbridge.md)
- [Troubleshooting](troubleshooting.md)
---
[Back to Installation Guide](installation.md) | [Troubleshooting](troubleshooting.md)
+94
View File
@@ -0,0 +1,94 @@
# Step 2: Setting Up Rosbridge
Rosbridge runs on the **robot's machine** (wherever ROS is running). It provides a WebSocket interface that the MCP server on your machine connects to over the network.
> **Prerequisite:** ROS must already be installed on the robot's machine. If you don't have ROS installed and want to try things out quickly, see the [Turtlesim Docker example](../../examples/5_docker_turtlesim/README.md) which runs ROS and rosbridge in a container.
> **Important — rosbridge *and* rosapi are both required.** ros-mcp-server needs `rosbridge_server` (the WebSocket interface) **and** `rosapi` (the introspection services behind `get_nodes`, `get_topics`, `get_services`, `detect_ros_version`, parameter tools, etc.). The **launch file** used below starts both automatically. Do **not** start rosbridge with `ros2 run rosbridge_server ...` / `rosrun rosbridge_server ...` — those bring up the WebSocket *without* `rosapi`, and every introspection tool will fail with "Service does not exist". The `ros-<distro>-rosbridge-server` package pulls `rosapi` in as a dependency; if in doubt, install the full `ros-<distro>-rosbridge-suite` metapackage.
## Install rosbridge_server
For ROS 2 Jazzy:
```bash
# Update package list
sudo apt update
# Install rosbridge for ROS2 Jazzy
sudo apt install ros-jazzy-rosbridge-server
```
For other ROS 2 distros:
```bash
# Install rosbridge for ROS2 Humble
sudo apt install ros-humble-rosbridge-server
```
```bash
# Install for other ROS distros
sudo apt install ros-<your-distro>-rosbridge-server
```
> Using ROS 1? See [ROS 1 instructions](#ros-1-end-of-life) at the bottom of this page.
## Launch rosbridge
`source` your ROS workspace first to ensure that the rosbridge has access to your ROS system.
```bash
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
> Don't forget to `source` your ROS workspace before launching, especially if you're using custom messages or services.
## Verify rosbridge is running
From the robot's machine:
```bash
curl -I http://localhost:9090
```
A successful response confirms rosbridge is listening on its default port (9090).
Also confirm `rosapi` is running — its services must be present for the MCP tools to work:
```bash
# ROS 2
ros2 service list | grep rosapi
# ROS 1
rosservice list | grep rosapi
```
If this prints nothing, you launched rosbridge without `rosapi` — use the `ros2 launch` / `roslaunch` command above rather than `ros2 run` / `rosrun`.
## Next Step
Connect to your robot and test it out: [Step 3: Connect](connect.md)
---
### Advanced
<details>
<summary><strong>ROS 1 (End of Life)</strong></summary>
#### Install rosbridge_server
For ROS Noetic:
```bash
sudo apt install ros-noetic-rosbridge-server
```
For other ROS 1 distros:
```bash
sudo apt install ros-<your-distro>-rosbridge-server
```
#### Launch rosbridge
```bash
roslaunch rosbridge_server rosbridge_websocket.launch
```
> Don't forget to `source` your ROS workspace before launching, especially if you're using custom messages or services.
</details>
---
[Back to Installation Guide](installation.md) | [Troubleshooting](troubleshooting.md)
+123
View File
@@ -0,0 +1,123 @@
# Troubleshooting
[Back to Installation Guide](installation.md)
## MCP Server Not Appearing in Client
**Symptoms:** The ros-mcp-server doesn't show up in your AI client's tool list.
**Solutions:**
1. **Restart your client completely** — some clients cache MCP server state on startup.
2. **Test the server manually** to check for install issues:
```bash
uvx ros-mcp --help
```
3. **Check your client's logs** for error messages related to MCP server initialization.
## macOS: "Could not attach to MCP server" / `uvx` Not Found
**Symptoms:** On macOS, Claude Desktop reports "Could not attach to MCP server ros-mcp-server", even though `uvx ros-mcp --help` works fine in your terminal.
**Cause:** The default macOS config launches the server with `zsh -lc`, which starts a **login, non-interactive** shell. That sources `~/.zprofile` but **not** `~/.zshrc`. The `uv` installer adds `~/.local/bin` to your `PATH` in `~/.zshrc` only, so the subprocess Claude Desktop spawns can't find `uvx`.
Reproduce the clean-subprocess environment Claude Desktop uses:
```bash
env -i HOME="$HOME" USER="$USER" zsh -lc 'which uvx'
# "uvx not found" → you are hitting this issue
```
**Solutions** (pick one):
1. **Make `~/.local/bin` available to login shells** — add it to `~/.zprofile` (which `zsh -lc` *does* source):
```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile
```
Then fully restart Claude Desktop.
2. **Use the absolute path to `uvx`** in `claude_desktop_config.json` instead of going through a shell. Find it with `which uvx` (usually `~/.local/bin/uvx`):
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "/Users/<you>/.local/bin/uvx",
"args": ["ros-mcp", "--transport=stdio"]
}
}
}
```
## Connection Refused / Cannot Reach Rosbridge
**Symptoms:** "Connection refused", "No valid session ID provided", or timeout errors when trying to interact with ROS.
**Solutions:**
1. **Verify rosbridge is running** on the robot's machine:
```bash
# ROS 2
ros2 topic list
# ROS 1
rostopic list
```
2. **Test rosbridge directly** from the robot's machine:
```bash
curl -I http://localhost:9090
```
3. **Check the IP address** — if the robot is on a different machine, make sure you're using the correct IP and that both machines are on the same network.
4. **Check firewall rules** — ensure port 9090 (rosbridge default) is open on the robot's machine.
## WSL-Specific Issues
**Symptoms:** Issues when running on Windows with WSL.
**Solutions:**
1. **Use the correct WSL distribution name** in your MCP config (e.g., `"Ubuntu-22.04"` not `"Ubuntu"`). Check with:
```bash
wsl --list --verbose
```
2. **Clone repos in the Linux filesystem** — use `/home/username/`, not `/mnt/c/Users/username/`. The Windows filesystem mount has poor performance and can cause permission issues.
3. **Test the server in WSL directly:**
```bash
uvx ros-mcp --help
```
## HTTP Transport Issues
**Symptoms:** HTTP transport not working or connection timeouts.
**Solutions:**
1. **Check the server is running** — HTTP transport requires starting the server manually:
```bash
uvx ros-mcp --transport streamable-http --host 127.0.0.1 --port 9000
```
2. **Verify port availability:**
```bash
netstat -tulpn | grep :9000
```
3. **Test the endpoint directly:**
```bash
curl http://localhost:9000/mcp
```
4. **Check firewall rules** if accessing from another machine.
## Debug Commands
| What to check | Command |
|---------------|---------|
| ROS 2 topics | `ros2 topic list` |
| ROS 1 topics | `rostopic list` |
| Rosbridge reachable | `curl -I http://localhost:9090` |
| MCP server works | `uvx ros-mcp --help` |
| Running processes | `ps aux \| grep rosbridge` |
| WSL distributions | `wsl --list --verbose` |
## Still Having Issues?
1. **Check logs** — look for error messages in your AI client and MCP server output. Running logs through an LLM can help with debugging.
2. **Test with turtlesim** — verify basic functionality with the [Turtlesim Tutorial](../../examples/1_turtlesim/README.md).
3. **Open an issue** on the [GitHub repository](https://github.com/robotmcp/ros-mcp-server/issues) with:
- Your operating system
- ROS version
- AI client being used
- Error messages
- Steps to reproduce
+142
View File
@@ -0,0 +1,142 @@
# ROS2 Launch System for Robot Integration
## Overview
This guide provides template launch files for developers to integrate their robots with the ROS-MCP Server. These templates demonstrate how to combine your robot's existing launch files with rosbridge for MCP communication.
## Template Launch Files
### `ros_mcp_rosbridge.launch.py` - Basic Rosbridge Template
**Purpose**: Minimal rosbridge WebSocket server for robot integration
**Use Case**: Add MCP communication to any existing robot setup
```bash
# Basic usage
ros2 launch ros_mcp_rosbridge.launch.py
# Custom port
ros2 launch ros_mcp_rosbridge.launch.py port:=9090
# Specific address
ros2 launch ros_mcp_rosbridge.launch.py address:=127.0.0.1
```
## Integration Patterns
### How to add Rosbridge to Existing Robot
#### Method 1: Include Rosbridge Launch File
```python
# Your existing robot launch file (e.g., my_robot.launch.py)
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
# Your existing robot nodes
robot_node = Node(
package='my_robot_pkg',
executable='robot_node',
name='my_robot'
)
sensor_node = Node(
package='my_robot_pkg',
executable='sensor_node',
name='sensor_node'
)
# Include rosbridge for MCP communication
rosbridge_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
'ros_mcp_server', '/launch/ros_mcp_rosbridge.launch.py'
]),
launch_arguments={
'port': '9090',
'address': '',
'log_level': 'info'
}.items()
)
return LaunchDescription([
robot_node,
sensor_node,
rosbridge_launch, # Add this line
])
```
#### Method 2: Add Rosbridge Node Directly
```python
# Add rosbridge node to your existing launch file
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch import LaunchDescription
def generate_launch_description():
"""Generate the launch description for rosbridge only."""
# Declare launch arguments
port_arg = DeclareLaunchArgument(
"port", default_value="9090", description="Port for rosbridge websocket server"
)
address_arg = DeclareLaunchArgument(
"address",
default_value="",
description="Address for rosbridge websocket server (empty for all interfaces)",
)
log_level_arg = DeclareLaunchArgument(
"log_level", default_value="info", description="Log level for rosbridge server"
)
# Rosbridge websocket server node
rosbridge_node = Node(
package="rosbridge_server",
executable="rosbridge_websocket",
name="rosbridge_websocket",
output="screen",
parameters=[
{
"port": LaunchConfiguration("port"),
"address": LaunchConfiguration("address"),
"use_compression": False,
"max_message_size": 10000000,
"send_action_goals_in_new_thread": True,
"call_services_in_new_thread": True,
"default_call_service_timeout": 5.0,
}
],
arguments=["--ros-args", "--log-level", LaunchConfiguration("log_level")],
)
return LaunchDescription(
[
port_arg,
address_arg,
log_level_arg,
rosbridge_node,
]
)
```
#### Method 3: Separate Launch Files (Recommended)
```bash
# Terminal 1: Launch your robot
ros2 launch my_robot_pkg my_robot.launch.py
# Terminal 2: Launch rosbridge for MCP
ros2 launch ros_mcp_server ros_mcp_rosbridge.launch.py port:=9090
```
| Argument | Default | Description |
|----------|---------|-------------|
| `port` | 9090 | WebSocket server port |
| `address` | "" | Bind address (empty = all interfaces) |
| `log_level` | info | Log level (debug, info, warn, error) |
+183
View File
@@ -0,0 +1,183 @@
# Repository Restructuring & Tool Migration Plan
> **Note**: This plan documents the refactoring process that migrated the monolithic server to a modular structure. The refactoring is now complete.
## Summary: Implementation vs Original Plan
**Status**: ✅ **Phase 1 Complete** - All 31 tools migrated to modular structure
**Key Differences from Original Plan:**
- ✅ Used `tools/__init__.py` instead of `tools.py` (better Python package convention)
- ✅ Used `main.py` instead of `server.py` (entry point naming)
- ⚠️ Function signature: `register_all_tools(mcp, ws_manager, ...)` takes `ws_manager` as parameter (more flexible than creating it internally)
- ✅ WebSocket manager renamed to `utils/websocket.py` (matches original plan structure)
- ✅ Helper functions in `tools/images.py` (co-located with usage, not separate `utils.py`)
**Public API**: `from ros_mcp.tools import register_all_tools` - imports from `ros_mcp/tools/__init__.py`
## Goal
Refactor **ros-mcp-server** to be importable as a library, enabling integration into **simple-mcp-ai** (proprietary) using a git submodule approach.
- **ros-mcp-server**: Apache 2.0 licensed, ROS MCP tools
- **simple-mcp-ai**: Proprietary, OAuth + Cloudflare tunnel infrastructure
## Overview
### Current State
- **Total tools**: 31
- **Status**: ✅ **All tools migrated** (31/31)
- **Structure**: Modular structure with tools organized by category in `ros_mcp/tools/`
### Tool Categories Overview
| Category | File | Count | Tools | Status |
|----------|------|-------|-------|--------|
| Connection | `tools/connection.py` | 2 | connect_to_robot, ping_robot | ✅ Done |
| Robot Config | `tools/robot_config.py` | 3 | get_verified_robot_spec, get_verified_robots_list, detect_ros_version | ✅ Done |
| Topics | `tools/topics.py` | 10 | get_topics, get_topic_type, get_message_details, get_topic_publishers, get_topic_subscribers, inspect_all_topics, subscribe_once, publish_once, subscribe_for_duration, publish_for_durations | ✅ Done|
| Services | `tools/services.py` | 6 | get_services, get_service_type, get_service_details, get_service_providers, inspect_all_services, call_service | ✅ Done |
| Nodes | `tools/nodes.py` | 3 | get_nodes, get_node_details, inspect_all_nodes | ✅ Done |
| Parameters | `tools/parameters.py` | 7 | get_parameter, set_parameter, has_parameter, delete_parameter, get_parameters, inspect_all_parameters, get_parameter_details | ✅ Done |
| Actions | `tools/actions.py` | 7 | get_actions, get_action_type, get_action_details, get_action_status, inspect_all_actions, send_action_goal, cancel_action_goal | ✅ Done |
| Images | `tools/images.py` | 1 | view_saved_image | ✅ Done |
| Utils | `tools/images.py` | - | convert_expects_image_hint, _encode_image_to_imagecontent (helper functions in images.py) | ✅ Done |
### Current Structure (Implemented)
```
ros-mcp-server/
├── ros_mcp/ # Package
│ ├── __init__.py
│ ├── main.py # MCP instance + main() ✅
│ ├── tools/ # Tool implementations by category ✅
│ │ ├── __init__.py # Main registration function (public API) ✅
│ │ ├── connection.py # 2 tools ✅
│ │ ├── robot_config.py # 3 tools ✅
│ │ ├── topics.py # 10 tools ✅
│ │ ├── services.py # 6 tools ✅
│ │ ├── nodes.py # 3 tools ✅
│ │ ├── parameters.py # 7 tools ✅
│ │ ├── actions.py # 7 tools ✅
│ │ └── images.py # 1 tool + helper functions ✅
│ └── utils/ # Utility modules ✅
│ ├── config_utils.py
│ ├── network_utils.py
│ └── websocket.py # WebSocket manager (renamed from websocket_manager.py)
├── server.py # Entry point: from ros_mcp.main import main ✅
└── pyproject.toml
```
**Public API**: `from ros_mcp.tools import register_all_tools` (imports from `ros_mcp/tools/__init__.py`)
## Phase 1: Refactor ros-mcp-server (Tool Migration) ✅ COMPLETE
### Migration Pattern
For each tool category:
1. **Extract implementation**: Create `tool_name_impl()` function in appropriate module
2. **Create registration function**: Each module exports `register_<category>_tools(mcp, ws_manager, ...)`
3. **Update main registration**: Import and call in `ros_mcp/tools/__init__.py`
4. **Remove from server.py**: Delete `@mcp.tool` decorated function
### Tool Categories (All Complete ✅)
- **Connection** (2 tools): `connect_to_robot`, `ping_robot`
- **Robot Config** (3 tools): `get_verified_robot_spec`, `get_verified_robots_list`, `detect_ros_version`
- **Topics** (10 tools): `get_topics`, `get_topic_type`, `get_message_details`, `get_topic_publishers`, `get_topic_subscribers`, `inspect_all_topics`, `subscribe_once`, `publish_once`, `subscribe_for_duration`, `publish_for_durations`
- **Services** (6 tools): `get_services`, `get_service_type`, `get_service_details`, `get_service_providers`, `inspect_all_services`, `call_service`
- **Nodes** (3 tools): `get_nodes`, `get_node_details`, `inspect_all_nodes`
- **Parameters** (7 tools): `get_parameter`, `set_parameter`, `has_parameter`, `delete_parameter`, `get_parameters`, `inspect_all_parameters`, `get_parameter_details`
- **Actions** (7 tools): `get_actions`, `get_action_type`, `get_action_details`, `get_action_status`, `inspect_all_actions`, `send_action_goal`, `cancel_action_goal`
- **Images** (1 tool): `view_saved_image` + helper functions (`convert_expects_image_hint`, `_encode_image_to_imagecontent`)
### Main Registration Function ✅
**File**: `ros_mcp/tools/__init__.py`
The public API function `register_all_tools()` registers all 31 tools:
```python
def register_all_tools(
mcp: FastMCP,
ws_manager: WebSocketManager,
rosbridge_ip: str = "127.0.0.1",
rosbridge_port: int = 9090,
) -> None:
"""Register all ROS MCP tools with the provided FastMCP instance."""
# Registers all tool categories...
```
**Note**: Function signature differs from original plan - takes `ws_manager` as parameter (more flexible than creating it internally).
## Phase 2: Integration into simple-mcp-ai ⏳ Pending
**Note**: This phase is for the **simple-mcp-ai** repository, not ros-mcp-server.
### Integration Steps
1. **Add git submodule**:
```bash
cd simple-mcp-ai
git submodule add https://github.com/robotmcp/ros-mcp-server.git
```
2. **Create `ros_integration.py`**:
```python
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'ros-mcp-server'))
from ros_mcp.tools import register_all_tools
from ros_mcp.utils.websocket import WebSocketManager
```
3. **Update `main.py`**:
```python
from fastmcp import FastMCP
from ros_integration import register_all_tools, WebSocketManager
mcp = FastMCP("simple-mcp-ai")
ws_manager = WebSocketManager("127.0.0.1", 9090, default_timeout=5.0)
register_all_tools(mcp, ws_manager, rosbridge_ip="127.0.0.1", rosbridge_port=9090)
```
4. **Update `requirements.txt`** with ros-mcp dependencies
5. **Delete old `tools.py`** (if exists)
## Migration Checklist
### Phase 1: Tool Migration ✅ COMPLETE
- [X] Create `ros_mcp/tools/` directory structure ✅
- [X] Move helper functions (in `tools/images.py`) ✅
- [X] Move all 31 tools across 8 categories ✅
- [X] Update `ros_mcp/tools/__init__.py` registration function ✅
- [X] Update `server.py` entry point ✅
## Verification Checklist
### Phase 1 (Tool Migration) ✅
- [X] All 31 tools registered in `register_all_tools()`
- [X] Each category has its own module file
- [X] Helper functions in `tools/images.py`
- [X] `ros-mcp-server` works standalone
### Phase 2 (Integration) ⏳ Pending
- [ ] Submodule added to simple-mcp-ai
- [ ] `ros_integration.py` created
- [ ] `main.py` updated to use `register_all_tools()`
- [ ] Dependencies updated
- [ ] Old `tools.py` removed
- [ ] Integration tested end-to-end
## Benefits
- ✅ Clean licensing separation (submodule stays Apache 2.0)
- ✅ Easy updates: `git submodule update --remote`
- ✅ Single MCP instance with all tools
- ✅ ros-mcp-server works standalone
- ✅ Well-organized, maintainable code structure
- ✅ Clear separation of concerns
- ✅ Easy to extend with new tools
+415
View File
@@ -0,0 +1,415 @@
# Testing Guide for ROS MCP Server
This guide explains how to test the ROS MCP Server using prompts, resources, and automated tests.
## Running Installation Tests
Installation tests verify that the package can be installed correctly using different methods (uvx, pip, uv). These tests use Docker to create clean Python environments and install from git.
### Prerequisites
- Docker installed and running
- pytest (`pip install pytest pytest-timeout`)
### Running Installation Tests
```bash
# Run all installation tests (uses current branch by default)
pytest tests/installation -v
# Test a specific branch
pytest tests/installation -v --branch=feat/new-feature
# Test a specific tag/release
pytest tests/installation -v --branch=v2.5.0
# Test from a different repository (e.g., a fork)
pytest tests/installation -v --repo-url=https://github.com/user/fork.git --branch=main
# Run only uvx installation tests
pytest tests/installation/test_uvx_install.py -v
# Run only pip installation tests
pytest tests/installation/test_pip_install.py -v
```
### Installation Test Categories
| Test File | What It Tests |
|-----------|---------------|
| `test_uvx_install.py` | `uvx --from git+URL@branch ros-mcp` installation |
| `test_pip_install.py` | `pip install git+URL@branch` and `pip install .` |
| `test_source_install.py` | `uv sync` development workflow |
### Multi-Python Version Testing
Installation tests run against Python 3.10, 3.11, and 3.12:
```bash
# These are parametrized tests that run automatically
pytest tests/installation/test_pip_install.py::test_pip_install_python_versions -v
pytest tests/installation/test_source_install.py::test_uv_source_python_versions -v
```
## Prerequisites
Before testing, ensure you have:
1. **ROS installed** (ROS 1 or ROS 2)
- ROS 2: `which ros2`
- ROS 1: TODO
2. **Turtlesim package installed**
- ROS 2: `ros2 pkg list | grep turtlesim`
- ROS 1: TODO
3. **Rosbridge server installed**
- ROS 2: `ros2 pkg list | grep rosbridge`
- ROS 1: TODO
4. **MCP Server running**
- The ROS MCP Server should be configured and running in your MCP client (e.g., Cursor, Claude)
## Setting Up the Test Environment
### Step 1: Start ROS Core (if not already running)
<!-- **For ROS 2:** -->
```bash
# Terminal 1: Start ROS 2 daemon
ros2 daemon start
```
<!-- **For ROS 1:** -->
<!-- ```bash
# Terminal 1: Start roscore
roscore
``` -->
### Step 2: Start Turtlesim
<!-- **For ROS 2:** -->
```bash
# Terminal 2: Start turtlesim node
ros2 run turtlesim turtlesim_node
```
<!-- **For ROS 1:**
```bash
# Terminal 2: Start turtlesim node
rosrun turtlesim turtlesim_node
``` -->
### Step 3: Start Rosbridge
<!-- **For ROS 2:** -->
```bash
# Terminal 3: Start rosbridge WebSocket server
ros2 run rosbridge_server rosbridge_websocket
# or
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
<!-- **For ROS 1:**
```bash
# Terminal 3: Start rosbridge WebSocket server
rosrun rosbridge_server rosbridge_websocket
``` -->
The rosbridge server will start on `ws://localhost:9090` by default.
## Using Prompts for Testing
Prompts are interactive guides that provide step-by-step instructions for testing specific tool categories. They are accessed through the MCP prompt interface.
### Available Test Prompts
1. **`test-server-tools`** - High-level overview of all ROS MCP Server tools
2. **`test-connection-tools`** - Connection and ROS version detection tools
3. **`test-topics-tools`** - Topic discovery, subscription, and publishing tools
4. **`test-services-tools`** - Service discovery and calling tools
5. **`test-nodes-tools`** - Node discovery and inspection tools
6. **`test-parameters-tools`** - Parameter tools (ROS 2 only)
7. **`test-actions-tools`** - Action tools (ROS 2 only)
### How to Use Prompts
1. **Access prompts through your MCP client:**
- Use the prompt selector or command palette (if available) or simply prompt `test server tools`
- Prompts are registered with names like `test-server-tools`, `test-topics-tools`, etc.
2. **Follow the prompt instructions:**
- Each prompt provides detailed testing steps
- Includes tool usage examples
- Contains troubleshooting information
3. **Example workflow with prompts:**
```
1. Start by prompting `test server tools` or selecting te template prompts for an overview
2. Use `test connection tools` to establish connection and test the setup
3. Use category-specific prompts (e.g., `test topics- tools`) for detailed testing
```
### Using Resources for Testing
Resources provide comprehensive system information in JSON format. They are accessed through the MCP resource interface.
### Available Resources
#### ROS Metadata Resources
1. **`ros-mcp://ros-metadata/all`**
- Complete system overview
- Includes topics, services, nodes, parameters, and ROS version
- Useful for getting a snapshot of the entire ROS system
2. **`ros-mcp://ros-metadata/topics/all`**
- All topics with their types, publishers, and subscribers
- Comprehensive topic connection information
3. **`ros-mcp://ros-metadata/services/all`**
- All services with their types and providers
- Complete service discovery information
4. **`ros-mcp://ros-metadata/nodes/all`**
- All nodes with their publishers, subscribers, and services
- Detailed node connection information
5. **`ros-mcp://ros-metadata/actions/all`** (ROS 2 only)
- All actions with their types
- Action discovery information
#### Robot Specification Resources
6. **`ros-mcp://robot-specs/get_verified_robots_list`**
- List of available robot specifications
- Returns JSON with robot names from `robot_specifications/` directory
### How to Use Resources
1. **Request resources through your AI assistant:**
- Simply ask: `get ros-metadata resources` or `get topics resource`, or `get services resource`,
- Ask to access specific resources: `access ros-mcp://ros-metadata/all` etc.
- The AI assistant will access the resource and return the JSON data
2. **Alternative: Access through MCP client interface:**
- Resources are accessed via URI (e.g., `ros-mcp://ros-metadata/all`)
- Your MCP client may provide a resource browser or URI access
- Check your MCP client's documentation for resource access methods
3. **Parse the JSON response:**
- Resources return JSON strings
- The AI assistant can parse and explain the data for you
### Example: Using Resources
**Get complete system overview:**
```
Access: ros-mcp://ros-metadata/all
Returns JSON with:
{
"topics": [...],
"services": [...],
"nodes": [...],
"parameters": [...],
"ros_version": "ROS 2"
}
```
**Get all topic details:**
```
Access: ros-mcp://ros-metadata/topics/all
Returns JSON with:
{
"total_topics": 5,
"topics": {
"/turtle1/cmd_vel": {
"type": "geometry_msgs/msg/Twist",
"publishers": [...],
"subscribers": [...]
},
...
}
}
```
## Complete Tools Testing Workflow
### 1. Initial Setup and Connection
```python
# Step 1: Connect to ROS system
connect_to_robot(ip='127.0.0.1', port=9090)
# Step 2: Verify connection and detect ROS version
detect_ros_version()
```
### 2. Discovery Phase
**Using Tools:**
```python
# Discover what's available
get_topics()
get_services()
get_nodes()
get_actions() # ROS 2 only
```
**Using Resources:**
```
Access: ros-mcp://ros-metadata/all
```
### 3. Detailed Testing by Category
**Topics:**
```python
# Get topic details
get_topic_details('/turtle1/cmd_vel')
# Subscribe to a topic
subscribe_once(topic='/turtle1/pose', msg_type='turtlesim/msg/Pose')
# Publish to a topic
publish_once(
topic='/turtle1/cmd_vel',
msg_type='geometry_msgs/msg/Twist',
msg={'linear': {'x': 2.0, 'y': 0.0, 'z': 0.0}}
)
```
**Services:**
```python
# Get service details
get_service_details('/turtle1/teleport_absolute')
# Call a service
call_service(
service_name='/turtle1/teleport_absolute',
service_type='turtlesim/srv/TeleportAbsolute',
request={'x': 5.5, 'y': 5.5, 'theta': 0.0}
)
```
**Nodes:**
```python
# Get node details
get_node_details('/turtlesim')
```
**Parameters (ROS 2 only):**
```python
# Get parameters
get_parameters('turtlesim')
# Set parameter
set_parameter('/turtlesim:background_r', '255')
```
**Actions (ROS 2 only):**
```python
# Get action details
get_action_details('/turtle1/rotate_absolute')
# Send action goal
send_action_goal(
action_name='/turtle1/rotate_absolute',
action_type='turtlesim/action/RotateAbsolute',
goal={'theta': 1.57}
)
```
### 4. Gather Resources
Access resources to get comprehensive system information:
```
1. Access ros-mcp://ros-metadata/topics/all
- Get all topics with types, publishers, and subscribers
2. Access ros-mcp://ros-metadata/services/all
- Get all services with types and providers
3. Access ros-mcp://ros-metadata/nodes/all
- Get all nodes with publishers, subscribers, and services
4. Access ros-mcp://ros-metadata/actions/all (ROS 2 only)
- Get all actions with their types
5. Access ros-mcp://ros-metadata/all
- Get complete system overview
```
## Testing Checklist
- [ ] Prerequisites installed and verified
- [ ] ROS core/daemon running
- [ ] Turtlesim node running
- [ ] Rosbridge server running
- [ ] MCP server connected successfully
- [ ] ROS version detected correctly
- [ ] All discovery tools working (topics, services, nodes, actions)
- [ ] Category-specific tools tested (topics, services, nodes, parameters, actions)
- [ ] Resources accessible and returning valid JSON
## Troubleshooting
### Connection Issues
- **Cannot connect to rosbridge:**
- Verify rosbridge is running: Check terminal output
- Check port: Default is 9090
- Verify IP address: Use `127.0.0.1` for localhost
- **ROS version detection fails:**
- Ensure ROS environment is sourced
- For ROS 2: `source /opt/ros/<distro>/setup.bash`
- For ROS 1: `source /opt/ros/<distro>/setup.bash`
### Tool Issues
- **Tools return empty results:**
- Verify `turtlesim` is running
- Check that topics/services/nodes exist
- Ensure rosbridge is connected
- **Service calls fail:**
- Verify service exists: `get_services()`
- Check service type: `get_service_details()`
- Verify request format matches service definition
### Resource Issues
- **Resources return errors:**
- Verify connection to ROS system
- Check that rosbridge is running
- Ensure required ROS services are available
## Next Steps
After completing basic testing:
1. **Explore category-specific prompts:**
- Use `test-topics-tools` for detailed topic testing
- Use `test-services-tools` for service testing
- Use other category prompts as needed
2. **Test with your own robot:**
- Replace turtlesim with your robot's nodes
- Test with your robot's topics, services, and actions
- Gather resources to understand your system structure
3. **Integrate with your workflow:**
- Use prompts for guided testing
- Use resources for system monitoring
- Combine tools and resources for comprehensive testing
## Additional Resources
- **Launch System Guide:** See `docs/launch_system.md` for integrating rosbridge with your robot
- **Installation Guide:** See `docs/install/installation.md` for setup instructions
- **Category-Specific Prompts:** Access detailed testing guides for each tool category
+349
View File
@@ -0,0 +1,349 @@
# Tutorial - Getting Started with ROS MCP Server and Turtlesim
Welcome to your first steps with the ROS MCP Server! This tutorial will guide you through using the ROS MCP Server with Turtlesim, the perfect "Hello World" robot for learning ROS integration.
Turtlesim is a lightweight simulator that demonstrates the fundamental concepts of ROS at the most basic level. It's ideal for understanding how the MCP server can interact with ROS systems before moving on to more complex robots.
## What You'll Learn
By the end of this tutorial, you'll be able to:
- Launch Turtlesim on your ROS system
- Explore ROS topics and services
- Control the turtle using natural language commands through the MCP server
- Understand the basic concepts of ROS-MCP integration
## Prerequisites
Before starting this tutorial, make sure you have:
**Any version of ROS installed** (ROS1 Noetic, ROS2 Humble, or ROS2 Jazzy)
**Basic familiarity with terminal/command line**
**The ROS MCP Server installed** (see [Installation Guide](../../docs/install/installation.md) for setup instructions)
> 💡 **Tip**: If you don't have ROS installed yet, you can use our [Docker Turtlesim example](../5_docker_turtlesim/). However, we recommend this option only for users who are familiar with Docker and X11 forwarding settings. (We would like you to spend time exploring the Robot MCP server, not figuring out X11 forwarding on your machine!)
## Step 1: Launch Turtlesim
First, let's get Turtlesim running on your system. The exact command depends on your ROS version:
<details>
<summary><strong>ROS1 (e.g., Noetic)</strong></summary>
### Launch Turtlesim
1. **Source your ROS environment:**
```bash
source /opt/ros/noetic/setup.bash # or /opt/ros/<ros_distro>/setup.bash
```
2. **Launch Turtlesim:**
```bash
rosrun turtlesim turtlesim_node
```
You should see a window appear with a turtle in the center of a blue background.
### Troubleshooting ROS1
- If you get "command not found", make sure you've sourced the ROS environment
- If the window doesn't appear, check your display settings (especially on WSL or remote connections)
</details>
<details>
<summary><strong>ROS2 (e.g., Humble, Jazzy)</strong></summary>
### Launch Turtlesim
1. **Source your ROS2 environment:**
```bash
source /opt/ros/humble/setup.bash # or /opt/ros/jazzy/setup.bash
```
2. **Launch Turtlesim:**
```bash
ros2 run turtlesim turtlesim_node
```
You should see a window appear with a turtle in the center of a blue background.
### Troubleshooting ROS2
- If you get "command not found", make sure you've sourced the ROS2 environment
- If the window doesn't appear, check your display settings (especially on WSL or remote connections)
</details>
## Step 2: Explore ROS Topics and Services
Now that Turtlesim is running, let's explore what's available in the ROS system. Open a new terminal and source your ROS environment, then try these commands:
### List Available Topics
<details>
<summary><strong>ROS1 (e.g., Noetic)</strong></summary>
```bash
# Source ROS environment
source /opt/ros/noetic/setup.bash
# List all topics
rostopic list
# Monitor turtle position
rostopic echo /turtle1/pose
# Monitor velocity commands
rostopic echo /turtle1/cmd_vel
```
</details>
<details>
<summary><strong>ROS2 (e.g., Humble, Jazzy)</strong></summary>
```bash
# Source ROS2 environment (adjust for your version)
source /opt/ros/humble/setup.bash # or /opt/ros/jazzy/setup.bash
# List all topics
ros2 topic list
# Monitor turtle position
ros2 topic echo /turtle1/pose
# Monitor velocity commands
ros2 topic echo /turtle1/cmd_vel
```
</details>
### List Available Services
<details>
<summary><strong>ROS1 (e.g., Noetic)</strong></summary>
```bash
# List all services
rosservice list
# Get information about a specific service
rosservice info /turtle1/set_pen
```
</details>
<details>
<summary><strong>ROS2 (e.g., Humble, Jazzy)</strong></summary>
```bash
# List all services
ros2 service list
# Get information about a specific service
ros2 service type /turtle1/set_pen
```
</details>
### Understanding Topics and Services
- **Topics** are like radio stations - nodes can publish data to topics and subscribe to receive data
- **Services** are like function calls - you can request a specific action and get a response
- The turtle's position is published on `/turtle1/pose`
- Movement commands are sent via `/turtle1/cmd_vel`
- Services like `/turtle1/set_pen` can change the turtle's drawing properties
## Step 3: Install and Configure the MCP Server
If you haven't already set up the ROS MCP Server, follow the detailed [Installation Guide](../../docs/install/installation.md). The MCP server can run on:
- **Same machine** as your ROS system (simplest setup)
- **Different machine** on the same local network (for remote control)
The installation guide covers:
- Installing the MCP server
- Configuring your language model client (Claude Desktop, etc.)
- Setting up rosbridge for communication
## Step 4: Hands-on Exploration with MCP Server
Now for the fun part! Once your MCP server is connected, you can control the turtle using natural language. Here are some commands to try:
### 🚀 Basic Movement Commands
Try these natural language commands with your AI assistant:
```
Move the turtle forward
```
```
Turn the turtle left
```
```
Make the turtle go backward
```
```
Stop the turtle
```
### 📊 Information Queries
Ask your AI assistant about the robot's state:
```
Tell me about this robot.
```
```
What topics and services are available on the robot?
```
```
What is the turtle's current position?
```
### 🎨 Setup Commands
```
Reset the turtle to the center
```
```
Change the turtle's pen color to red
```
```
Spawn a new turtle
```
```
Clear the background
```
### 🎯 Advanced Commands
Try more complex behaviors:
```
Draw a square with the turtle
```
```
Move the turtle to position (5, 5)
```
```
Make the turtle follow a circular path
```
```
Draw a spiral pattern
```
### 💡 Pro Tips
- **Be specific**: Instead of "move", try "move forward at 2 m/s"
- **Ask questions**: "What can this robot do?" or "How do I make the turtle draw?"
- **Experiment**: Try combining commands like "draw a square, then change the pen color to green"
- **Monitor**: Use `rostopic echo` in a separate terminal to see the commands being sent
## Troubleshooting
### Common Issues
<details>
<summary><strong>MCP Server Connection Issues</strong></summary>
**Problem**: AI assistant can't connect to the robot
**Solutions**:
- Verify rosbridge is running: `ros2 launch rosbridge_server rosbridge_websocket_launch.xml`
- Check if MCP server is running and connected
- Ensure firewall allows WebSocket connections (port 9090)
- For remote connections, verify the robot's IP address
</details>
<details>
<summary><strong>ROS Environment Issues</strong></summary>
**Problem**: "command not found" errors
**Solutions**:
- Always source your ROS environment: `source /opt/ros/[version]/setup.bash`
- Add sourcing to your `.bashrc` for automatic setup
- Verify ROS installation with `rosversion -d` (ROS1) or `ros2 doctor` (ROS2)
</details>
<details>
<summary><strong>Display Issues</strong></summary>
**Problem**: Turtlesim window doesn't appear
**Solutions**:
- **WSL users**: Install X11 forwarding: `sudo apt install x11-apps`
- **Remote connections**: Use X11 forwarding: `ssh -X username@hostname`
- **Docker users**: Check X11 forwarding configuration
</details>
<details>
<summary><strong>Permission Issues</strong></summary>
**Problem**: Can't control the turtle
**Solutions**:
- Ensure you're not running multiple turtlesim instances
- Check if another process is controlling the turtle
- Restart turtlesim if commands aren't working
</details>
### 💡 Still Stuck?
If you're still having issues, check the official documentation for your ROS version:
- **[ROS1 Noetic Installation](https://wiki.ros.org/noetic/Installation)** | **[Turtlesim Wiki](https://wiki.ros.org/turtlesim)**
- **[ROS2 Humble Installation](https://docs.ros.org/en/humble/Installation.html)** | **[Turtlesim Tutorial](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim.html)**
- **[ROS2 Jazzy Installation](https://docs.ros.org/en/jazzy/Installation.html)** | **[Turtlesim Tutorial](https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim.html)**
**Additional Resources:**
- **[ROS Answers](https://answers.ros.org/)** - Community Q&A for specific problems
- **ROS2 Doctor**: Run `ros2 doctor --report` to diagnose ROS2 installation issues
- **[ROS Troubleshooting Guide](https://wiki.ros.org/ROS/Troubleshooting)** (ROS1)
## Next Steps
Congratulations! You've successfully controlled a robot using natural language. Here's what you can explore next:
### 🎯 Immediate Next Steps
1. **Try more complex patterns**: Draw shapes, follow paths, create animations
2. **Experiment with services**: Change colors, spawn multiple turtles, modify the environment
3. **Monitor the system**: Use `rostopic echo` to see the data flowing through ROS
### 🚀 Advanced Exploration
1. **Explore other examples** in this repository:
- [Gemini Integration](../2_gemini/) - Advanced AI integration
- [Limo Mobile Robot](../3_limo_mobile_robot/) - Real robot control
- [Unitree Go2](../4_unitree_go2/) - Quadruped robot
2. **Connect to real robots**: Use the same MCP server with physical robots
3. **Integrate with other tools**: Combine with computer vision, planning algorithms, etc.
### 📚 Learning Resources
- [ROS Documentation](https://docs.ros.org/)
- [Turtlesim Tutorials](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim.html)
- [MCP Protocol Documentation](https://modelcontextprotocol.io/)
---
**Happy robot controlling!** 🤖✨
This tutorial has shown you the fundamentals of ROS-MCP integration. The same principles apply to more complex robots - you're now ready to explore the exciting world of natural language robot control!
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
ROS2 Launch file for ROS-MCP Server
Replaces the functionality of scripts/launch_ros.sh with proper ROS2 launch system.
This launch file starts:
- rosbridge_websocket server
- turtlesim node
- Provides proper process management and cleanup
"""
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch import LaunchDescription
def generate_launch_description():
"""Generate the launch description for ROS-MCP Server."""
# Declare launch arguments
rosbridge_port_arg = DeclareLaunchArgument(
"port", default_value="9090", description="Port for rosbridge websocket server"
)
rosbridge_address_arg = DeclareLaunchArgument(
"address",
default_value="",
description="Address for rosbridge websocket server (empty for all interfaces)",
)
turtlesim_name_arg = DeclareLaunchArgument(
"turtlesim_name", default_value="turtlesim", description="Name for the turtlesim node"
)
# Rosbridge websocket server node
rosbridge_node = Node(
package="rosbridge_server",
executable="rosbridge_websocket",
name="rosbridge_websocket",
output="screen",
parameters=[
{
"port": LaunchConfiguration("port"),
"address": LaunchConfiguration("address"),
"use_compression": False,
"max_message_size": 10000000,
"send_action_goals_in_new_thread": True,
"call_services_in_new_thread": True,
}
],
arguments=["--ros-args", "--log-level", "info"],
)
# Turtlesim node
turtlesim_node = Node(
package="turtlesim",
executable="turtlesim_node",
name=LaunchConfiguration("turtlesim_name"),
output="screen",
arguments=["--ros-args", "--log-level", "info"],
)
# Log info about what's being launched
log_info = LogInfo(
msg=[
"Starting ROS-MCP Server with:",
" - Rosbridge WebSocket on port: ",
LaunchConfiguration("port"),
" - Turtlesim node: ",
LaunchConfiguration("turtlesim_name"),
]
)
return LaunchDescription(
[
rosbridge_port_arg,
rosbridge_address_arg,
turtlesim_name_arg,
log_info,
rosbridge_node,
turtlesim_node,
]
)
+39
View File
@@ -0,0 +1,39 @@
# Demo, Gemini-CLI with ROS-MCP-Server
## Prerequisite
- Installation of Gemini-CLI. [https://github.com/google-gemini/gemini-cli]
- Installation of ROS or ROS2. Test if ROS is installed by running Turtlesim. If you are not sure, follow this tutorial [https://wiki.ros.org/ROS/Tutorials]
- Installation of ROS-MCP-Server except Section II (installation/settings of Claude desktop) [Installation Guide](../../docs/install/installation.md). For Gemini CLI-specific setup, see the [Gemini CLI setup guide](../../docs/install/clients/gemini-cli.md)
## Update Gemini CLI MCP settings
- Open `settings.json` file of Gemini CLI, located `~/.gemini/settings.json`
- Add "mcpServer" setting below in the `settings.json`
```json
{
"mcpServers": {
"ros-mcp-server": {
"command": "uv",
"args": [
"--directory",
"/<ABSOLUTE_PATH>/ros-mcp-server",
"run",
"server.py"
]
}
}
}
```
## Demo Environment
- Ubuntu 20.04
- ROS Noetic
> **Compatibility note**
> The demo environment below was validated on Ubuntu 20.04 + ROS Noetic.
> Although the prerequisites mention ROS or ROS2, users running ROS2 Jazzy / WSL may need a different setup path and should verify `turtlesim`, `rosbridge_websocket` (port 9090), and MCP server connectivity separately.
## Demo Video
[![Gemini Demo with ROS MCP Server](./image/gemini-demo-ros-mcp.jpeg)](https://youtu.be/UAEG0qTADDk)
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1,247 @@
# Example - LIMO (Isaac Sim)
![Static Badge](https://img.shields.io/badge/ROS2-Available-green)
Here is an introduction to the ROS MCP servers capabilities using the robot simulator Isaac Sim with the mobile robot LIMO!
This example includes the Isaac Sim Docker installation and execution, as well as a USD file with the LIMO robot equipped with a camera and LiDAR sensor. This setup allows you to easily configure the LIMO robot environment within Isaac Sim.
## System Requirements
This example requires a PC that meets the minimum specifications to run Isaac Sim. Please refer to the [System Requirements](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/requirements.html) to prepare the appropriate hardware.
## Prerequisites
**Note:** This example is designed to run on Linux with ROS2 installed and has been tested on the following versions:
- **OS**: Ubuntu 20.04, 22.04
- **ROS2**: Foxy, Humble
- **Isaac Sim**: Local versions 4.5.0, 5.0.0 and Docker versions 4.5.0, 5.0.0
This example is written based on **Docker Isaac Sim 5.0.0** to maximize accessibility and minimize dependencies.
⚠️ **Windows Users:** If you're using Windows with WSL2, Isaac Sim Docker will not work due to WSL2 GPU passthrough limitations. Instead, install Isaac Sim natively on Windows and follow the Windows-specific instructions in the Quick Start section below.
**Linux Users:**
Before starting this example, make sure you have the following installed:
- **ROS2**: [Install ROS2](https://docs.ros.org/en/dashing/Installation/Ubuntu-Install-Binary.html)
- **Docker**: [Install Docker](https://docs.docker.com/get-docker/)
**Windows Users:**
Before starting this example, make sure you have the following installed:
- **WSL2**: [Install WSL2](https://learn.microsoft.com/en-us/windows/wsl/install)
- **ROS2 in WSL2**: [Install ROS2](https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Binary.html)
- **NVIDIA GPU Drivers**: Latest drivers for your GPU
## Quick Start
### Setup
<details>
<summary><strong>For Linux: Docker-based Setup</strong></summary>
### 1. Isaac Sim Container Installation
Follow the instructions on the [Isaac Sim Container Installation](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/install_container.html) to install the Isaac Sim Docker container.
### 2. Download Isaac Sim WebRTC Streaming Client
Follow the instructions on the [Isaac Sim WebRTC Streaming Client](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/download.html#isaac-sim-latest-release) to download and install the WebRTC streaming client.
In this example, the WebRTC Streaming Client was downloaded and executed as follows.
<img src="../images/isaac_sim_webrtc_streaming_client_version.png" width="500">
#### 3. Check Installation
To verify that the Isaac Sim Container and WebRTC Streaming Client are properly installed, please follow the steps below.
In the terminal where the Isaac Sim Container is running, execute the following command to run Isaac Sim with native livestream mode.
```bash
./runheadless.sh -v
```
When the message `Isaac Sim Full Streaming App is loaded.` appears, it means the application has been launched successfully.
After that, move to the folder where you downloaded the Isaac Sim WebRTC Streaming Client in a new terminal and run the following command to execute the client.
```bash
./isaacsim-webrtc-streaming-client-1.1.4-linux-x64.AppImage
```
When the GUI is displayed as shown below, it means the application has been launched successfully.
<img src="../images/isaac_sim_webrtc_streaming_client.png" width="500">
If the above steps have been executed successfully, press the Connect button on the Isaac Sim WebRTC Streaming Client to check if the Isaac Sim screen appears.
**Note:** This example is based on installing and running the Isaac Sim container and the Isaac Sim WebRTC streaming client on the same local PC. Therefore, the server address for the Isaac Sim WebRTC Streaming Client is `127.0.0.1`. If the Isaac Sim container is running on another PC, you need to enter the IP address of that PC.
<img src="../images/isaac_sim_streaming.png" width="500">
### 4. Copy LIMO example USD file to Isaac Sim
Copy the USD file of the LIMO robot created for this example to Isaac Sim.
```bash
cd <YOUR_PATH>/ros-mcp-server/examples/3_limo_mobile_robot/isaac_sim/usd/
docker exec <container_name> mkdir -p /example # default container_name : isaac-sim
docker cp ./limo_example.usd <container_name>:/example/limo_example.usd
```
If successfully copied, you can verify the USD file in the Isaac Sim screen as shown below.
```bash
Path : My Computer > / > example > limo_example.usd
```
<img src="../images/limo_isaac_sim_usd_path.png" width="500">
### 5. Launch LIMO Example
Now, double-click the `limo_example.usd` file to run it in Isaac Sim. You should see the LIMO robot loading in the simulation environment as shown below.
<img src="../images/limo_isaac_sim.png" width="500">
### 3. Start rosbridge
If the LIMO robot is running properly in Isaac Sim and ROS2 topics have been created, run rosbridge in the terminal to enable communication with the ros-mcp-server. Execute rosbridge using the following command:
```bash
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
</details>
<details>
<summary><strong>For Windows: Native Setup with WSL2</strong></summary>
### 1. Install Isaac Sim for Windows
Download and install Isaac Sim for Windows:
1. Visit the [Isaac Sim Download Page](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/download.html)
2. Download the Windows version
3. Extract the archive to your desired location (e.g., `C:\isaac_sim`)
<img src="../images/isaac_sim_installation.png" width="500">
**Note:** Remember the installation path as you'll need it in the next steps.
### 2. Configure Windows Environment and Launch Isaac Sim
Open PowerShell and configure the environment variables to enable ROS2 bridge:
```powershell
# Set Isaac Sim installation path (modify this to match your installation)
$env:ISAAC_SIM_PATH = "C:\isaac_sim"
# Set ROS2 distribution (change to 'foxy' if using ROS2 Foxy)
$env:ROS_DISTRO = "humble"
# Set RMW implementation
$env:RMW_IMPLEMENTATION = "rmw_fastrtps_cpp"
# Add ROS2 bridge library to PATH
$env:PATH = "$env:ISAAC_SIM_PATH\exts\isaacsim.ros2.bridge\$env:ROS_DISTRO\lib;$env:PATH"
# Move to the isaac folder
cd $env:ISAAC_SIM_PATH
# Launch Isaac Sim with the ROS2 bridge extension
& "$env:ISAAC_SIM_PATH\isaac-sim.bat" --/isaac/startup/ros_bridge_extension=isaacsim.ros2.bridge
```
**Important:** Replace `C:\isaac_sim` with your actual Isaac Sim installation path, and adjust `ROS_DISTRO` to match your WSL2 ROS2 version.
Wait for Isaac Sim to fully launch. You should see the Isaac Sim GUI window.
**Verify ROS2 Bridge:**
- Go to: **Window → Extensions**
- Search for: `ros2 bridge`
- Confirm that **isaacsim.ros2.bridge** is ON(enabled).
If it is not, make sure you followed the previous instructions.
<img src="../images/isaac_sim_check_extension.png" width="500">
### 3. Start rosbridge
If the LIMO robot is running properly in Isaac Sim and ROS2 topics have been created, run rosbridge to enable communication with the **ros-mcp-server**.
```bash
# Install rosbridge server if not already installed
sudo apt update
sudo apt install ros-humble-rosbridge-server
# Source again to ensure rosbridge is available
source /opt/ros/humble/setup.bash
# Set RMW implementation to match Windows
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
# Allow ROS2 to communicate across network boundaries
export ROS_LOCALHOST_ONLY=0
# Start rosbridge
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
**Note:** If you're using ROS2 Foxy, replace `humble` with `foxy` in the commands above.
### 4. Load LIMO Example in Isaac Sim
1. **Load USD File**
- In Isaac Sim, go to: **File → Open**
- Navigate to your cloned repository location:
- If cloned in Windows: `<YOUR_PATH>\ros-mcp-server\examples\3_limo_mobile_robot\isaac_sim\usd\limo_example.usd`
- If cloned in WSL2: Access via `\\wsl$\<distro_name>\<YOUR_PATH>\ros-mcp-server\examples\3_limo_mobile_robot\isaac_sim\usd\limo_example.usd`
- Select and open `limo_example.usd`
**Alternative:** Drag and drop the USD file from the Content panel at the bottom into the Viewport
<img src="../images/limo_isaac_sim_usd_path.png" width="500">
You should see the LIMO robot loading in the simulation environment as shown below.
<img src="../images/limo_isaac_sim.png" width="500">
</details>
#### Start Simulation and Check Topics
Now, let's check if the LIMO robot is running properly in Isaac Sim.
1. **Start Simulation**
- Click the **▶ (Play)** button in the toolbar on the left
- The button will change to **|| (Pause)** when the physics engine starts running
2. **Check ROS2 Topics**
Once the simulation starts, ROS2 topics will be created through a predefined [Action Graph](https://docs.isaacsim.omniverse.nvidia.com/latest/omnigraph/omnigraph_tutorial.html) within the USD file.
Run the following command in your terminal (Linux) or WSL2 terminal (Windows):
```bash
ros2 topic list
```
<img src="../images/limo_isaac_sim_topic_list.png" width="500">
As shown in the figure, `/camera/image_raw`, `/cmd_vel`, and `/scan` must appear as essential topics.
Each topic serves the following functions:
- `/camera/image_raw`: Transmits raw image data collected from the LIMO robot's camera
- `/cmd_vel`: Transmits movement commands for the LIMO robot
- `/scan`: Transmits scan data collected from the LIMO robot's LiDAR sensor
## **Integration with MCP Server**
If rosbridge is running, you can connect the MCP server to control the robot. If you havent set up the MCP server yet, follow the [installation guide](https://github.com/robotmcp/ros-mcp-server/blob/main/docs/install/installation.md) .
Since The **ros-mcp-server** needs to recognize the robot, configure it to connect to the robots IP address.
## **Example Walkthrough**
Once all the above connections are completed, you can connect to and control the LIMO robot from the **ros-mcp-server**. Below is an example screen showing the connection to the LIMO robot from the **ros-mcp-server**.
### **Example 1** : Connect to robot
**For Linux:** By default, the Isaac Sim Container is run with the `--network=host` option, so it can access the LIMO robot on the same network as the user's local PC. Therefore, the IP address of the LIMO robot is the same as the user's local PC. Use the `ifconfig` command to find your IP address.
**For Windows:** Find your Windows IP address from WSL2 using: `ip route show | grep -i default | awk '{ print $3}'`
<img src="../images/limo_isaac_sim_connect.png" width="500">
### **Example 2** : ROS2 Topic Check
<img src="../images/limo_isaac_sim_check_topic.png" width="500">
### **Example 3** : Simple Movement
<img src="../images/limo_isaac_sim_simple_movement.gif" width="1000">
## **Next Steps**
The LIMO is equipped with various sensors, such as vision and LiDAR sensors. Let's make use of them.
@@ -0,0 +1,177 @@
# Example - LIMO (Real)
![Static Badge](https://img.shields.io/badge/ROS-Available-green)
This is an example using the real robot, AgileX Robotics LIMO, in the ROS ecosystem.
The LIMO is a ROS-based mobile robot platform for education and research, offering features like SLAM and navigation on various hardware configuration.
## Prerequisites
For this example, the LIMO Standard version with Ubuntu 18.04 and ROS1 Melodic is used. Since each LIMO variant comes with different vision sensors, LiDAR sensors, and ROS versions, make sure to install the corresponding packages required for your specific model.
### AgileX LIMO
For more details, please refer to the [LIMO Documentation](https://docs.trossenrobotics.com/agilex_limo_docs/).
<img src="../images/limo.png" width="300">
- **Specification**
- **OS** : Ubuntu 18.04
- **ROS** : ROS1 Melodic
- **IPC** : NVIDIA Jetson Nano (4G)
- **Vision Sensor** : Dabai U3
- **LiDAR** : EAI X2L
<br>
Before starting this tutorial, make sure you have the following installed. It is recommended to use a single workspace. If they are already installed, you may skip this step.
<br>
- **Limo SDK (ROS1)** : [LIMO SDK](https://github.com/agilexrobotics/limo_ros)
- **Ydlidar Ros Driver** : [Ydlidar Ros Driver](https://github.com/YDLIDAR/ydlidar_ros_driver)
- **Ros Astra Camera** : [ROS Astra Camera](https://github.com/orbbec/ros_astra_camera)
## **Framework**
The AI system communicates with the ROS-MCP via the MCP protocol, which connects through ROSbridge to LIMO's ROS nodes. ROSbridge converts ROS messages to JSON format over WebSocket, while the nodes handle topics to conrol hardware and exchange data.
<img src="../images/limo_real_framework.png" width="700">
## Quick Start
### 1. Network Setup
Since the LIMO is controlled via an ROS-MCP from the user PC, it is important to connect both the user PC and the LIMO to the same network.
- **Ping Test**
After connecting them to the same network, perform a ping test from the user PC to LIMO to verify that the connection is established correctly:
```bash
ping <LIMO_IP> # e.g., ping 192.168.0.20
```
- **ROS Network setup**
In ROS1, nodes must know which host runs the ROS master.
On the LIMO, set the `ROS_MASTER_URI` environment variable:
```bash
echo "export ROS_MASTER_URI=http://<LIMO_IP>:11311" >> ~/.bashrc
echo "export ROS_IP=<LIMO_IP>" >> ~/.bashrc
source ~/.bashrc
```
### 2. SSH Setup
For convenience in file transfer, SSH (Secure Shell) is enabled on the robot before setup.
- **SSH server installation on the LIMO:**
```bash
sudo apt update
sudo apt install openssh-server
```
- **Service activation:**
```bash
sudo systemctl enable ssh
sudo systemctl start ssh
sudo systemctl status ssh
```
If it shows `active (running)`, it means the service is running properly.
- **Connect to Robot on the user PC:**
`<ROBOT_NAME>` is the robot's username (default: agilex), and `<LIMO_IP>`is the robot's network IP address, which you can find using the `ifconfig` command on the LIMO.
```bash
ssh <ROBOT_NAME>@<LIMO_IP> # e.g., ssh agilex@192.168.0.20
### 3. File Upload
After connecting via SSH, the following commands are executed on the user PC.
- **Set helper node (`cmd_vel` repeat node):**
On the LIMO, `cmd_vel` is consumed and applied to the motors immediately. This makes it hard to execute ROS-MCP generated motion commands accurately, since the ROS-MCP may not publish at a steady high-frequency rate. To compensate, a helper node is added that re-publishes the last command at a fixed frequency.
To ensure the ROS-MCP can use the topic unambiguously, the motor's input is remapped from `cmd_vel` to `cmd_vel_to_motor`, and configure the repeater to subscribe to `cmd_vel` (from the ROS-MCP) and republish it to `cmd_vel_to_motor`.
```bash
cd /<ABSOLUTE_PATH>/ros-mcp-server
scp ./examples/3_limo_mobile_robot/real_robot/scripts \
<ROBOT_NAME>@<LIMO_IP>:~/catkin_ws/src/limo_base
ssh <ROBOT_NAME>@<LIMO_IP> \
"chmod +x ~/catkin_ws/src/limo_base/scripts/cmd_vel_repeat.py"
```
- **Replace launch file:**
```bash
cd /<ABSOLUTE_PATH>/ros-mcp-server
scp /examples/3_limo_mobile_robot/real_robot/launch/limo_base.launch \
<ROBOT_NAME>@<LIMO_IP>:~/catkin_ws/src/limo_base/launch/
scp ./examples/3_limo_mobile_robot/real_robot/launch/limo_start.launch \
<ROBOT_NAME>@<LIMO_IP>:~/catkin_ws/src/limo_bringup/launch/
```
### 4. Launch Node
To use the robot's topics in ROS-MCP, each node is launched on the robot.
- **Start the base driver:**
```bash
roslaunch limo_base limo_start.launch
```
- **Start the Camera (DaBai U3):**
The LIMO standard uses Astras DaBai U3 depth camera.
launch the cam node:
```bash
roslaunch astra_camera dabai_u3.launch
```
- **Start the `cmd_vel` repeater:**
```bash
rosrun limo_base cmd_vel_repeat.py
```
- **Start rosbridge:**
```bash
roslaunch rosbridge_server rosbridge_websocket.launch
```
## **Integration with MCP Server**
Once rosbridge is running on the LIMO and your PC is on the same network, you can connect the MCP server to control the robot. If you havent set up the MCP server yet, follow the [installation guide](https://github.com/robotmcp/ros-mcp-server/blob/main/docs/install/installation.md) .
Since The ROS-MCP to recognize the robot, configure it to connect to the robots IP address.
## **Example Walkthrough**
After connecting to the robot, you can inspect which topics the robot subscribes to and what it can do.
### **Example 1** : Connect to robot
<img src="../images/limo_real_connect.png" width="500">
### **Example 2** : What can you do?
<img src="../images/limo_real_whatcanyoudo1.png" width="500">
<img src="../images/limo_real_whatcanyoudo2.png" width="500">
### **Example 3** : Simple Movement
<img src="../images/limo_real_simple_movement.gif" width="800">
## **Next Steps**
1. **Try running the navigation demo**
With the SLAM and navigation fratures provided by LIMO, more complex tasks can be carride out.
2. **Try more complex commands**
The LIMO is equipped with various sensors, such as vision and LiDAR sensors. Let's make use of them.
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<launch>
<arg name="port_name" default="ttyTHS1" />
<arg name="odom_frame" default="odom" />
<arg name="base_frame" default="base_link" />
<arg name="use_mcnamu" default="false" />
<arg name="pub_odom_tf" default="" />
<node name="limo_base_node" pkg="limo_base" type="limo_base_node" output="screen" >
<param name="port_name" value="$(arg port_name)" />
<param name="odom_frame" value="$(arg odom_frame)" />
<param name="base_frame" value="$(arg base_frame)" />
<param name="use_mcnamu" value="$(arg use_mcnamu)" />
<param name="pub_odom_tf" value="$(arg pub_odom_tf)" />
<remap from="/cmd_vel" to="/cmd_vel_to_motor" />
</node>
</launch>
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<launch>
<!-- ttyTHS1 for NVIDIA nano serial port-->
<!-- ttyUSB0 for IPC USB serial port -->
<arg name="port_name" default="ttyTHS1" />
<arg name="use_mcnamu" default="false" />
<arg name="pub_odom_tf" default="" />
<include file="$(find limo_base)/launch/limo_base.launch">
<arg name="port_name" default="$(arg port_name)" />
<arg name="use_mcnamu" default="$(arg use_mcnamu)" />
<arg name="pub_odom_tf" default="$(arg pub_odom_tf)" />
</include>
<include file="$(find ydlidar_ros)/launch/X2L.launch" />
<node pkg="tf" type="static_transform_publisher" name="base_link_to_camera_link" args="0.105 0 0.1 0.0 0.0 0.0 /base_link /camera_link 10" />
<node pkg="tf" type="static_transform_publisher" name="base_link_to_imu_link" args="0.0 0.0 0.0 0.0 0.0 0.0 /base_link /imu_link 10" />
<node pkg="tf" type="static_transform_publisher" name="base_link_to_laser_link" args="0.105 0.0 0.08 0.0 0.0 0.0 /base_link /laser_link 10" />
</launch>
@@ -0,0 +1,40 @@
#!/usr/bin/env python
import rospy
from geometry_msgs.msg import Twist
class CmdVelLatchRepeater(object):
def __init__(self):
self.in_topic = rospy.get_param("~in_topic", "/cmd_vel")
self.out_topic = rospy.get_param("~out_topic", "/cmd_vel_to_motor")
self.rate_hz = rospy.get_param("~rate_hz", 10.0)
self.stop_on_shutdown = rospy.get_param("~stop_on_shutdown", True)
self.pub = rospy.Publisher(self.out_topic, Twist, queue_size=5)
self.sub = rospy.Subscriber(self.in_topic, Twist, self.cb, queue_size=5)
self.last_cmd = None
rospy.loginfo(
"latch repeater: %s -> %s @ %.1fHz", self.in_topic, self.out_topic, self.rate_hz
)
def cb(self, msg):
self.last_cmd = msg
def spin(self):
r = rospy.Rate(self.rate_hz)
while not rospy.is_shutdown():
if self.last_cmd is not None:
self.pub.publish(self.last_cmd)
r.sleep()
def shutdown(self):
if self.stop_on_shutdown:
self.pub.publish(Twist())
if __name__ == "__main__":
rospy.init_node("cmd_vel_latch_repeater")
node = CmdVelLatchRepeater()
rospy.on_shutdown(node.shutdown)
node.spin()
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

+146
View File
@@ -0,0 +1,146 @@
# Example - Unitree GO2 (Isaac Sim)
![Static Badge](https://img.shields.io/badge/ROS2-Available-green)
[![IsaacSim](https://img.shields.io/badge/IsaacSim-4.5.0-silver.svg)](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html)
[![Python](https://img.shields.io/badge/python-3.10-blue.svg)](https://docs.python.org/3/whatsnew/3.10.html)
This example is built on the [go2_omniverse](https://github.com/abizovnuralem/go2_omniverse/tree/added_copter?tab=readme-ov-file) repository by @abizovnuralem. The repo enables seamless integration with ROS 2, giving access to various sensors such as LiDAR, IMU, and cameras, and allows you to easily leverage the locomotion model trained in Isaac Lab.
Using this setup, an environment is built to test and demonstrate the capabilities of ROS-MCP in the Isaac Sim.
## System Requirements
This example requires a PC that meets the minimum specifications to run Isaac Sim. Please refer to the [System Requirements](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/installation/requirements.html) to prepare the appropriate hardware.
## Prerequisites
**Note:** This example is designed to run on Linux with ROS2 installed and has been tested on the following versions:
- **OS**: Ubuntu 22.04
- **ROS2**: Humble
- **Isaac Sim**: 4.5.0
- **Isaac Lab**: 2.1.1 (recommmended)
Before starting this example, make sure you have the following installed:
- **ROS2** : [Install ROS2](https://docs.ros.org/en/dashing/Installation/Ubuntu-Install-Binary.html)
## Quick Start
### 1. IsaacSim & IsaacLab install in conda environment
Follow the instructions on the [Isaac Sim & Isaac Lab installation](https://isaac-sim.github.io/IsaacLab/v2.1.1/source/setup/installation/pip_installation.html) to install the Isaac Sim and Isaac Lab.
To avoid potential conflicts when working locally, it is recommended to work within a **conda** environment.
### 2. Environment Setup
Isaac Sim simulation environment for running the robot.
**Note:** In this step, the process is based on the [go2_omniverse](https://github.com/abizovnuralem/go2_omniverse/tree/added_copter?tab=readme-ov-file) repository. See its README for more details.
- **Downloading the code**
This example uses the **added_copter** branch, which is built on Isaac Sim 4.5.0.
```bash
git clone -b added_copter https://github.com/abizovnuralem/go2_omniverse/ --recurse-submodules -j8 --depth=1
```
- **Setup the RTX Lidar**
1. First check the directory of the virtual environment where Isaac Sim was installed:
```bash
conda info --envs
```
2. Update the Isaac Sim extension definition file to include the Unitree L1 lidar:
```bash
cp /<ABSOLUTE_PATH>/ros-mcp-server/examples/4_unitree_go2/isaac_sim/scripts/extension.toml \
<YOUR_CONDA_ENV_DIR>/lib/python3.10/site-packages/isaacsim/exts/isaacsim.sensors.rtx/config/
```
3. Add the Unitree L1 lidar config file to Isaac Lab repo folder:
```bash
mkdir -p <YOUR_CONDA_ENV_DIR>/lib/python3.10/site-packages/isaacsim/exts/isaacsim.sensors.rtx/data/lidar_configs/Unitree
cp /<ABSOLUTE_PATH>/go2_omniverse/Isaac_sim/Unitree/Unitree_L1.json \
<YOUR_CONDA_ENV_DIR>/lib/python3.10/site-packages/isaacsim/exts/isaacsim.sensors.rtx/data/lidar_configs/Unitree/
```
- **Custom Environment setup**
To effectively showcase the capabilities of ROS-MCP, the small warehouse environment proivided by Isaac Sim was used, and the setup was configured to allow users to start using ROS-MCP right away.
<img src="../images/unitree_go2_isaac_sim_custom_env.png" width="600">
To set up the environment, several files in go2_omniverse need to be replaced with the files provided in this repository.
```bash
cd /<ABSOLUTE_PATH>/ros-mcp-server/examples/4_unitree_go2/isaac_sim/scripts
cp custom_rl_env.py omniverse_sim.py /<ABSOLUTE_PATH>/go2_omniverse/
```
Finally, in the `run_sim.sh` file of go2_omniverse, add `--custom_env small_warehouse` to the Python execution command.
### 3. Launch
To use the robot's topics in ROS-MCP, each node is launched.
1. First, launch the simulation node.
```bash
#Launch simulation node
cd /<ABSOLUTE_PATH>/go2_omniverse/
./run_sim.sh
```
2. After that, open a new terminal and start rosbridge.
```bash
# Launch rosbridge
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
3. You can verify the available topics for the Go2 robot by running the ros2 topic list command.
```bash
ros2 topic list
```
<img src="../images/unitree_go2_isaac_sim_topiclist.png" width="300">
The key topics serve the following functions:
- `/robot0/cmd_vel` : Transmits movement commands for the robot.
- `/robot0/front_cam/rgb` : Transmits raw image data collected from the robot's camera.
- `/robot0/point_cloud2_L1` : Publishes point cloud data from the Unitree L1 LiDAR.
- `/robot0/point_cloud2_extra` : Publishes point cloud data from the extra simulated LiDAR. (If the extra sensor is not needed, it is recommended to disable its publishing.)
## **Integration with MCP Server**
If rosbridge is running, you can connect the MCP server to control the robot. If you havent set up the MCP server yet, follow the [installation guide](https://github.com/robotmcp/ros-mcp-server/blob/main/docs/install/installation.md) .
## **Example Walkthrough**
Once all the above connections are completed, you can connect to and control the Go2 robot from the **ros-mcp-server**. Below is an example screen showing the connection to the Go2 robot from the **ros-mcp-server**.
### **Example 1** : Connect to robot
<img src="../images/unitree_go2_isaac_sim_connect.png" width="500">
### **Example 2** : Check available topics
<img src="../images/unitree_go2_isaac_sim_gettopics.png" width="500">
### **Example 3** : Move around and observe ([video](https://www.youtube.com/watch?v=9StFx4lnvmc))
<a href="https://www.youtube.com/watch?v=9StFx4lnvmc"><img src="../images/unitree_go2_isaac_sim_movearound.png" width="720"></a>
## **Troubleshooting**
- ### **CUDA Driver failures(RTX lidar issue)**
```bash
[Error] [omni.sensors.nv.lidar.lidar_core.plugin] CUDA Driver CALL FAILED at line 522: the provided PTX was compiled with an unsupported toolchain.
[Error] [omni.sensors.nv.lidar.lidar_core.plugin] CUDA Driver CALL FAILED at line 548: named symbol not found
```
1. First, refer to the [Isaac Sim known issues](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/overview/known_issues.html#:~:text=CUDA%20driver%20failures%20from%20the%20omni.sensors.nv.lidar.lidar_core.plugin%20(example%20below)%20on%20Ubuntu%20may%20be%20due%20to%20a%20system%2Dlevel%20CUDA%20installation%20mismatch%20with%20the%20omni.sensors%20runtime%2Dcompiled%20libraries.) and try applying those fixes first.
2. If the issue still persists, consider upgrading the nvidia driver version to 570 or later.
## **Next Steps**
The Unitree Go2 has various sensors and functionalities, so let's make use of them by adding ROS nodes such as Nav2.
@@ -0,0 +1,348 @@
# Copyright (c) 2024, RoboVerse community
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import math
from dataclasses import MISSING
from math import sqrt
from typing import Literal
import isaaclab.sim as sim_utils
import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp
import torch
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import ContactSensorCfg, RayCasterCfg, patterns
from isaaclab.terrains import TerrainImporterCfg
from isaaclab.utils import configclass
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG
from robots.g1.config import G1_CFG
base_command = {}
def constant_commands(env: ManagerBasedRLEnvCfg) -> torch.Tensor:
global base_command
"""The generated command from the command generator."""
# tensor_lst = torch.tensor([0, 0, 0], device=env.device).repeat(env.num_envs, 1)
tensor_lst = torch.tensor([0.0, 0.0, 0.0], dtype=torch.float32, device=env.device).repeat(
env.num_envs, 1
)
for i in range(env.num_envs):
tensor_lst[i] = torch.tensor(base_command[str(i)], device=env.device)
return tensor_lst
@configclass
class MySceneCfg(InteractiveSceneCfg):
"""Configuration for the terrain scene with a legged robot."""
# ground terrain
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="plane",
debug_vis=False,
)
# robots
robot: ArticulationCfg = MISSING
height_scanner = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
# attach_yaw_only=True,
ray_alignment="yaw",
pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
contact_forces = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True
)
# lights
light = AssetBaseCfg(
prim_path="/World/light",
spawn=sim_utils.DistantLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0),
)
sky_light = AssetBaseCfg(
prim_path="/World/skyLight",
spawn=sim_utils.DomeLightCfg(color=(0.13, 0.13, 0.13), intensity=1000.0),
)
@configclass
class ViewerCfg:
"""Configuration of the scene viewport camera."""
eye: tuple[float, float, float] = (-2.5, -7.8, 4.0)
lookat: tuple[float, float, float] = (3.0, 2.0, -2.5)
cam_prim_path: str = "/OmniverseKit_Persp"
resolution: tuple[int, int] = (1920, 1080)
origin_type: Literal["world", "env", "asset_root"] = "world"
env_index: int = 0
asset_name: str | None = None
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(func=mdp.base_lin_vel)
base_ang_vel = ObsTerm(func=mdp.base_ang_vel)
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
noise=Unoise(n_min=-0.05, n_max=0.05),
)
velocity_commands = ObsTerm(func=constant_commands)
joint_pos = ObsTerm(func=mdp.joint_pos_rel)
joint_vel = ObsTerm(func=mdp.joint_vel_rel)
actions = ObsTerm(func=mdp.last_action)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
clip=(-1.0, 1.0),
)
def __post_init__(self):
self.enable_corruption = True
self.concatenate_terms = True
# observation groups
policy: PolicyCfg = PolicyCfg()
@configclass
class ActionsCfg:
"""Action specifications for the MDP."""
joint_pos = mdp.JointPositionActionCfg(
asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True
)
@configclass
class CommandsCfg:
"""Command specifications for the MDP."""
base_velocity = mdp.UniformVelocityCommandCfg(
asset_name="robot",
resampling_time_range=(0.0, 0.0),
rel_standing_envs=0.02,
rel_heading_envs=1.0,
heading_command=True,
heading_control_stiffness=0.5,
debug_vis=True,
ranges=mdp.UniformVelocityCommandCfg.Ranges(
lin_vel_x=(0.0, 0.0),
lin_vel_y=(0.0, 0.0),
ang_vel_z=(0.0, 0.0),
heading=(0, 0),
),
)
@configclass
class RewardsCfg:
"""Reward terms for the MDP."""
# -- task
track_lin_vel_xy_exp = RewTerm(
func=mdp.track_lin_vel_xy_exp,
weight=1.0,
params={"command_name": "base_velocity", "std": math.sqrt(0.25)},
)
track_ang_vel_z_exp = RewTerm(
func=mdp.track_ang_vel_z_exp,
weight=0.5,
params={"command_name": "base_velocity", "std": math.sqrt(0.25)},
)
# -- penalties
lin_vel_z_l2 = RewTerm(func=mdp.lin_vel_z_l2, weight=-2.0)
ang_vel_xy_l2 = RewTerm(func=mdp.ang_vel_xy_l2, weight=-0.05)
dof_torques_l2 = RewTerm(func=mdp.joint_torques_l2, weight=-1.0e-5)
dof_acc_l2 = RewTerm(func=mdp.joint_acc_l2, weight=-2.5e-7)
action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-0.01)
feet_air_time = RewTerm(
func=mdp.feet_air_time,
weight=0.125,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*FOOT"),
"command_name": "base_velocity",
"threshold": 0.5,
},
)
undesired_contacts = RewTerm(
func=mdp.undesired_contacts,
weight=-1.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*THIGH"),
"threshold": 1.0,
},
)
# -- optional penalties
flat_orientation_l2 = RewTerm(func=mdp.flat_orientation_l2, weight=0.0)
dof_pos_limits = RewTerm(func=mdp.joint_pos_limits, weight=0.0)
@configclass
class TerminationsCfg:
"""Termination terms for the MDP."""
time_out = DoneTerm(func=mdp.time_out, time_out=True)
base_contact = DoneTerm(
func=mdp.illegal_contact,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names="base"),
"threshold": 1.0,
},
)
@configclass
class EventCfg:
"""Configuration for events."""
# startup
physics_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"static_friction_range": (0.8, 0.8),
"dynamic_friction_range": (0.6, 0.6),
"restitution_range": (0.0, 0.0),
"num_buckets": 64,
},
)
@configclass
class LocomotionVelocityRoughEnvCfg(ManagerBasedRLEnvCfg):
"""Configuration for the locomotion velocity-tracking environment."""
# Scene settings
scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5)
viewer: ViewerCfg = ViewerCfg()
# Basic settings
observations: ObservationsCfg = ObservationsCfg()
actions: ActionsCfg = ActionsCfg()
commands: CommandsCfg = CommandsCfg()
# MDP settings
rewards: RewardsCfg = RewardsCfg()
terminations: TerminationsCfg = TerminationsCfg()
events: EventCfg = EventCfg()
def __post_init__(self):
"""Post initialization."""
# general settings
self.decimation = 4
self.sim.render_interval = self.decimation
self.episode_length_s = 20.0
# simulation settings
self.sim.dt = 0.005
self.sim.disable_contact_processing = True
self.sim.physics_material = self.scene.terrain.physics_material
# update sensor update periods
# we tick all the sensors based on the smallest update period (physics update period)
if self.scene.height_scanner is not None:
self.scene.height_scanner.update_period = self.decimation * self.sim.dt
if self.scene.contact_forces is not None:
self.scene.contact_forces.update_period = self.sim.dt
# check if terrain levels curriculum is enabled - if so, enable curriculum for terrain generator
# this generates terrains with increasing difficulty and is useful for training
if getattr(self.curriculum, "terrain_levels", None) is not None:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = True
else:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = False
@configclass
class UnitreeGo2CustomEnvCfg(LocomotionVelocityRoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
self.scene.robot = UNITREE_GO2_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/base"
self.scene.robot.init_state.rot = (sqrt(0.5), 0.0, 0.0, -sqrt(0.5))
# reduce action scale
self.actions.joint_pos.scale = 0.25
# rewards
self.rewards.feet_air_time.params["sensor_cfg"].body_names = ".*_foot"
self.rewards.feet_air_time.weight = 0.01
self.rewards.undesired_contacts = None
self.rewards.dof_torques_l2.weight = -0.0002
self.rewards.track_lin_vel_xy_exp.weight = 1.5
self.rewards.track_ang_vel_z_exp.weight = 0.75
self.rewards.dof_acc_l2.weight = -2.5e-7
# terminations
self.terminations.base_contact.params["sensor_cfg"].body_names = "base"
@configclass
class G1RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
# Scene
G1_MINIMAL_CFG = G1_CFG.copy()
G1_MINIMAL_CFG.spawn.usd_path = "./robots/g1/g1.usd"
self.scene.robot = G1_MINIMAL_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/torso_link"
# rewards
self.rewards.feet_air_time.params["sensor_cfg"].body_names = ".*_ankle_roll_link"
self.rewards.undesired_contacts = None
# Terminations
self.terminations.base_contact.params["sensor_cfg"].body_names = ["torso_link"]
@@ -0,0 +1,133 @@
[core]
reloadable = true
order = 0
[package]
version = "13.6.3"
category = "Simulation"
title = "Isaac Sim Isaac Sensor Simulation"
description = "Provides APIs for RTX-based sensors, including RTX Lidar & RTX Radar."
authors = ["NVIDIA"]
repository = ""
keywords = ["isaac", "physics", "robotics"]
changelog = "docs/CHANGELOG.md"
readme = "docs/README.md"
preview_image = "data/preview.png"
icon = "data/icon.png"
writeTarget.kit = true
[dependencies]
"isaacsim.core.api" = {}
"isaacsim.core.deprecation_manager" = {}
"isaacsim.core.nodes" = {}
"isaacsim.gui.components" = {}
"isaacsim.util.debug_draw" = {}
"omni.graph.action" = {}
"omni.graph" = {}
"isaacsim.storage.native" = {}
"omni.kit.numpy.common" = {}
"omni.kit.pip_archive" = {} # pulls in numpy
"omni.replicator.core" = {}
"omni.sensors.nv.common" = {}
"omni.sensors.nv.ids" = {}
"omni.sensors.nv.lidar" = {}
"omni.sensors.nv.radar" = {}
"omni.sensors.tiled" = {}
"omni.syntheticdata" = {}
"omni.timeline" = {} # Needed for simulation to occur
"isaacsim.robot.schema" = {}
"omni.usd" = {} # needed for call to add aov
"usdrt.scenegraph" = {}
[[python.module]]
name = "isaacsim.sensors.rtx"
[[python.module]]
name = "isaacsim.sensors.rtx.tests"
public = false
[[native.plugin]]
path = "bin/*.plugin"
recursive = false
[settings]
app.sensors.nv.lidar.profileBaseFolder=[
"${omni.sensors.nv.common}/data/lidar/",
"${isaacsim.sensors.rtx}/data/lidar_configs/HESAI/",
"${isaacsim.sensors.rtx}/data/lidar_configs/NVIDIA/",
"${isaacsim.sensors.rtx}/data/lidar_configs/Ouster/OS0/",
"${isaacsim.sensors.rtx}/data/lidar_configs/Ouster/OS1/",
"${isaacsim.sensors.rtx}/data/lidar_configs/Ouster/OS2/",
"${isaacsim.sensors.rtx}/data/lidar_configs/SICK/",
"${isaacsim.sensors.rtx}/data/lidar_configs/SLAMTEC/",
"${isaacsim.sensors.rtx}/data/lidar_configs/Velodyne/",
"${isaacsim.sensors.rtx}/data/lidar_configs/ZVISION/",
"${isaacsim.sensors.rtx}/data/lidar_configs/Unitree/",
"${app}/../exts/omni.isaac.sensor/data/lidar_configs/Unitree/",
"${isaacsim.sensors.rtx}/data/lidar_configs/"]
# app.sensors.nv.lidar.outputBufferOnGPU = true
app.sensors.nv.lidar.auxDataOutput = "EXTRA"
app.sensors.nv.lidar.enableVelocity = true
app.sensors.nv.radar.auxDataOutput = "EXTRA"
app.sensors.nv.radar.runWithoutMBVH = false
rtx.rtxsensor.coordinateFrameQuaternion = "0.0,0.0,0.0,1.0"
# sensor material mapping is hard coded for now, and this is needed to enable sensor materials.
rtx.materialDb.rtSensorNameToIdMap="DefaultMaterial:0;AsphaltStandardMaterial:1;AsphaltWeatheredMaterial:2;VegetationGrassMaterial:3;WaterStandardMaterial:4;GlassStandardMaterial:5;FiberGlassMaterial:6;MetalAlloyMaterial:7;MetalAluminumMaterial:8;MetalAluminumOxidizedMaterial:9;PlasticStandardMaterial:10;RetroMarkingsMaterial:11;RetroSignMaterial:12;RubberStandardMaterial:13;SoilClayMaterial:14;ConcreteRoughMaterial:15;ConcreteSmoothMaterial:16;OakTreeBarkMaterial:17;FabricStandardMaterial:18;PlexiGlassStandardMaterial:19;MetalSilverMaterial:20"
renderer.raytracingMotion.enabled=true
[[test]]
timeout = 900
dependencies = [
"omni.hydra.rtx",
]
stdoutFailPatterns.exclude = [
# This is excluded in at least 3 kit tests.
"*Missing call to destroyResourceBindingSignature()*",
'*Error processing node attribute `outputs:pointInstanceDataType`: dataType attribute is empty*'
]
args = [
'--/persistent/isaac/asset_root/default = "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/4.5"',
"--/app/asyncRendering=0",
"--/app/asyncRenderingLowLatency=0",
"--/app/fastShutdown=1",
"--/app/file/ignoreUnsavedOnExit=1",
"--/app/hydraEngine/waitIdle=0",
"--/app/renderer/skipWhileMinimized=0",
"--/app/renderer/sleepMsOnFocus=0",
"--/app/renderer/sleepMsOutOfFocus=0",
"--/app/settings/fabricDefaultStageFrameHistoryCount=3",
"--/app/settings/persistent=0",
"--/app/viewport/createCameraModelRep=0",
"--/crashreporter/skipOldDumpUpload=1",
"--/omni/kit/plugin/syncUsdLoads=1",
"--/omni/replicator/asyncRendering=0",
'--/persistent/app/stage/upAxis="Z"',
"--/persistent/app/viewport/defaults/tickRate=120",
"--/persistent/app/viewport/displayOptions=31951",
"--/persistent/omni/replicator/captureOnPlay=1",
"--/persistent/omnigraph/updateToUsd=0",
"--/persistent/physics/visualizationDisplayJoints=0",
"--/persistent/renderer/startupMessageDisplayed=1",
"--/persistent/simulation/defaultMetersPerUnit=1.0",
"--/persistent/simulation/minFrameRate=15",
"--/renderer/multiGpu/autoEnable=0",
"--/renderer/multiGpu/enabled=0",
"--/rtx-transient/dlssg/enabled=0",
"--/'rtx-transient'/resourcemanager/enableTextureStreaming=1",
"--/rtx/descriptorSets=360000",
"--/rtx/hydra/enableSemanticSchema=1",
"--/rtx/hydra/materialSyncLoads=1",
"--/rtx/materialDb/syncLoads=1",
"--/rtx/newDenoiser/enabled=1",
"--/rtx/reservedDescriptors=900000",
'--/exts/omni.usd/locking/onClose=0',
"--vulkan",
]
[[test]]
name = "startup"
args = [
'--/app/settings/fabricDefaultStageFrameHistoryCount = 3',
]
@@ -0,0 +1,309 @@
# Copyright (c) 2024, RoboVerse community
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations
import argparse
import os
import threading
import carb
import cli_args
import custom_rl_env
import gymnasium as gym
import isaaclab.sim as sim_utils
import omni
import omni.appwindow
import rclpy
import torch
from agent_cfg import unitree_g1_agent_cfg, unitree_go2_agent_cfg
from custom_rl_env import G1RoughEnvCfg, UnitreeGo2CustomEnvCfg
from geometry_msgs.msg import Twist
from isaaclab.app import AppLauncher
from isaaclab_rl.rsl_rl import (
RslRlOnPolicyRunnerCfg,
RslRlVecEnvWrapper,
)
from isaaclab_tasks.utils import get_checkpoint_path
from isaacsim.storage.native import get_assets_root_path
from omnigraph import create_front_cam_omnigraph
from ros2 import RobotBaseNode, add_camera, add_rtx_lidar, pub_robo_data_ros2
from rsl_rl.runners import OnPolicyRunner
# add argparse arguments
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
# parser.add_argument("--device", type=str, default="cpu", help="Use CPU pipeline.")
parser.add_argument(
"--disable_fabric",
action="store_true",
default=False,
help="Disable fabric and use USD I/O operations.",
)
parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.")
parser.add_argument(
"--task",
type=str,
default="Isaac-Velocity-Rough-Unitree-Go2-v0",
help="Name of the task.",
)
parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment")
parser.add_argument("--custom_env", type=str, default="", help="Setup the environment")
parser.add_argument("--robot", type=str, default="go2", help="Setup the robot")
parser.add_argument("--robot_amount", type=int, default=1, help="Setup the robot amount")
# append RSL-RL cli arguments
cli_args.add_rsl_rl_args(parser)
# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
args_cli = parser.parse_args()
# launch omniverse app
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app
ext_manager = omni.kit.app.get_app().get_extension_manager()
ext_manager.set_extension_enabled_immediate("isaacsim.ros2.bridge", True)
# FOR VR SUPPORT
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.core", True)
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.system.steamvr", True)
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.system.simulatedxr", True)
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.system.openxr", True)
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.telemetry", True)
# ext_manager.set_extension_enabled_immediate("omni.kit.xr.profile.vr", True)
def sub_keyboard_event(event, *args, **kwargs) -> bool:
if len(custom_rl_env.base_command) > 0:
if event.type == carb.input.KeyboardEventType.KEY_PRESS:
if event.input.name == "W":
custom_rl_env.base_command["0"] = [1, 0, 0]
if event.input.name == "S":
custom_rl_env.base_command["0"] = [-1, 0, 0]
if event.input.name == "A":
custom_rl_env.base_command["0"] = [0, 1, 0]
if event.input.name == "D":
custom_rl_env.base_command["0"] = [0, -1, 0]
if event.input.name == "Q":
custom_rl_env.base_command["0"] = [0, 0, 1]
if event.input.name == "E":
custom_rl_env.base_command["0"] = [0, 0, -1]
if len(custom_rl_env.base_command) > 1:
if event.input.name == "I":
custom_rl_env.base_command["1"] = [1, 0, 0]
if event.input.name == "K":
custom_rl_env.base_command["1"] = [-1, 0, 0]
if event.input.name == "J":
custom_rl_env.base_command["1"] = [0, 1, 0]
if event.input.name == "L":
custom_rl_env.base_command["1"] = [0, -1, 0]
if event.input.name == "U":
custom_rl_env.base_command["1"] = [0, 0, 1]
if event.input.name == "O":
custom_rl_env.base_command["1"] = [0, 0, -1]
elif event.type == carb.input.KeyboardEventType.KEY_RELEASE:
for i in range(len(custom_rl_env.base_command)):
custom_rl_env.base_command[str(i)] = [0, 0, 0]
return True
def move_copter(copter):
# TODO tmp solution for test
if custom_rl_env.base_command["0"] == [0, 0, 0]:
copter_move_cmd = torch.tensor([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [1, 0, 0]:
copter_move_cmd = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [-1, 0, 0]:
copter_move_cmd = torch.tensor([[-1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [0, 1, 0]:
copter_move_cmd = torch.tensor([[0.0, 1.0, 0.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [0, -1, 0]:
copter_move_cmd = torch.tensor([[0.0, -1.0, 0.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [0, 0, 1]:
copter_move_cmd = torch.tensor([[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]], device="cuda:0")
if custom_rl_env.base_command["0"] == [0, 0, -1]:
copter_move_cmd = torch.tensor([[0.0, 0.0, -1.0, 0.0, 0.0, 0.0]], device="cuda:0")
copter.write_root_velocity_to_sim(copter_move_cmd)
copter.write_data_to_sim()
def setup_custom_env():
try:
if args_cli.custom_env == "warehouse":
cfg_scene = sim_utils.UsdFileCfg(usd_path="./envs/warehouse.usd")
cfg_scene.func("/World/warehouse", cfg_scene, translation=(0.0, 0.0, 0.0))
if args_cli.custom_env == "office":
cfg_scene = sim_utils.UsdFileCfg(usd_path="./envs/office.usd")
cfg_scene.func("/World/office", cfg_scene, translation=(0.0, 0.0, 0.0))
if args_cli.custom_env == "small_warehouse":
assets_root_path = get_assets_root_path()
cfg_scene = sim_utils.UsdFileCfg(
usd_path=assets_root_path
+ "/Isaac/Environments/Digital_Twin_Warehouse/small_warehouse_digital_twin.usd"
)
cfg_scene.func("/World/small_warehouse", cfg_scene, translation=(2.5, -1.5, 0.0))
if args_cli.custom_env == "hospital":
cfg_scene = sim_utils.UsdFileCfg(usd_path="./envs/hospital.usd")
cfg_scene.func("/World/hospital", cfg_scene, translation=(0.0, 0.0, 0.0))
except Exception as e:
print(
f"Error loading custom environment. You should download custom envs folder from: https://drive.google.com/drive/folders/1vVGuO1KIX1K6mD6mBHDZGm9nk2vaRyj3?usp=sharing : {e}"
)
def cmd_vel_cb(msg, num_robot):
x = msg.linear.x
y = msg.linear.y
z = msg.angular.z
custom_rl_env.base_command[str(num_robot)] = [x, y, z]
def add_cmd_sub(num_envs):
node_test = rclpy.create_node("position_velocity_publisher")
for i in range(num_envs):
node_test.create_subscription(
Twist, f"robot{i}/cmd_vel", lambda msg, i=i: cmd_vel_cb(msg, str(i)), 10
)
# Spin in a separate thread
thread = threading.Thread(target=rclpy.spin, args=(node_test,), daemon=True)
thread.start()
def specify_cmd_for_robots(numv_envs):
for i in range(numv_envs):
custom_rl_env.base_command[str(i)] = [0, 0, 0]
def run_sim():
# acquire input interface
_input = carb.input.acquire_input_interface()
_appwindow = omni.appwindow.get_default_app_window()
_keyboard = _appwindow.get_keyboard()
_sub_keyboard = _input.subscribe_to_keyboard_events(_keyboard, sub_keyboard_event)
"""Play with RSL-RL agent."""
# parse configuration
env_cfg = UnitreeGo2CustomEnvCfg()
if args_cli.robot == "g1":
env_cfg = G1RoughEnvCfg()
# TODO need to think about better copter integration.
# copter_cfg = CRAZYFLIE_CFG
# copter_cfg.spawn.func(
# "/World/Crazyflie/Robot_1", copter_cfg.spawn, translation=(1.5, 0.5, 2.42)
# )
# # create handles for the robots
# copter = Articulation(copter_cfg.replace(prim_path="/World/Crazyflie/Robot.*"))
# add N robots to env
env_cfg.scene.num_envs = args_cli.robot_amount
specify_cmd_for_robots(env_cfg.scene.num_envs)
agent_cfg: RslRlOnPolicyRunnerCfg = unitree_go2_agent_cfg
if args_cli.robot == "g1":
agent_cfg: RslRlOnPolicyRunnerCfg = unitree_g1_agent_cfg
# create isaac environment
env = gym.make(args_cli.task, cfg=env_cfg)
# wrap around environment for rsl-rl
env = RslRlVecEnvWrapper(env)
# specify directory for logging experiments
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg["experiment_name"])
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Loading experiment from directory: {log_root_path}")
resume_path = get_checkpoint_path(
log_root_path, agent_cfg["load_run"], agent_cfg["load_checkpoint"]
)
# load previously trained model
ppo_runner = OnPolicyRunner(env, agent_cfg, log_dir=None, device=agent_cfg["device"])
ppo_runner.load(resume_path)
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
# obtain the trained policy for inference
policy = ppo_runner.get_inference_policy(device=env.unwrapped.device)
# reset environment
obs = env.get_observations()
# initialize ROS2 node
rclpy.init()
base_node = RobotBaseNode(env_cfg.scene.num_envs)
add_cmd_sub(env_cfg.scene.num_envs)
UnitreeL1_annotator_lst = add_rtx_lidar(
env_cfg.scene.num_envs, args_cli.robot, "UnitreeL1", False
)
ExtraLidar_annotator_lst = add_rtx_lidar(env_cfg.scene.num_envs, args_cli.robot, "Extra", False)
annotator_lst = UnitreeL1_annotator_lst + ExtraLidar_annotator_lst
add_camera(env_cfg.scene.num_envs, args_cli.robot)
# add_copter_camera()
# create ros2 camera stream omnigraph
for i in range(env_cfg.scene.num_envs):
create_front_cam_omnigraph(i)
setup_custom_env()
# simulate environment
while simulation_app.is_running():
# run everything in inference mode
with torch.inference_mode():
# agent stepping
actions = policy(obs)
# env stepping
obs, _, _, _ = env.step(actions)
pub_robo_data_ros2(
args_cli.robot,
env_cfg.scene.num_envs,
base_node,
env,
annotator_lst,
)
# move_copter(copter)
env.close()
+249
View File
@@ -0,0 +1,249 @@
# Example - Unitree GO2 (Real)
![Static Badge](https://img.shields.io/badge/ROS-Available-green)
![Static Badge](https://img.shields.io/badge/ROS2-Available-green)
This is an example using the real robot, **Unitree GO2**, in the ROS2 ecosystem. If needed, it can also be run on ROS1.
## Prerequisites
In this example, the **Unitree GO2 Edu** model was used, and the setup was tested on **Ubuntu 20.04 with ROS2 Foxy**. If needed, it can also be run on **Ubuntu 18.04 with ROS1 Noetic**.
⚠️ **Note**: For the Unitree GO2 Air and Pro models, SSH access may be restricted, so using the Edu model is recommended whenever possible.
### Unitree GO2
For more details, please refer to the [Unitree GO2 Documentation](https://support.unitree.com/home/en/developer/about_Go2).
<img src="../images/unitree_go2_real.png" width="300">
## Quick Start
### 1. Network Setup
#### 1.1 Assigning static IP to the robot though router configuration:
The Unitree GO2 and the users PC can be connected via SSH through the [GO2 Robot Interface](https://www.docs.quadruped.de/projects/go2/html/go2_driver.html#go2-network-interface).
However, by default, the Unitree GO2 does not have internet access, so you need to configure the router as described below to allow the Unitree GO2 to connect to the internet.
#### Requirements
- A router with Internet access
- A computer connected to the router via Wi-Fi or Ethernet
- Unitree GO2 connected to the router via Ethernet cable
#### Steps
- **Access the Router Gateway**
- Make sure your PC is connected to the router.
- Run `ifconfig` in the terminal to check the assigned IP address. In this example, the IP is `192.168.50.13`.
<img src="../images/unitree_go2_real_router_ip.png" width="500">
- If the IP is in the form `192.168.x.x`, replace the last number with `1` and open it in a browser (e.g., `192.168.x.1`). In this case, it is `192.168.50.1`.
<img src="../images/unitree_go2_real_router_connect.png" width="500">
- **Login to the Router**
- Enter the default username and password of the router.
- If the credentials were previously changed, use the updated account information.
- After a successful login, the router configuration page will be displayed.
- **Change Router Gateway Address**
- Navigate to **Advanced Settings → LAN** (the menu name may vary depending on the manufacturer/model).
- Change the routers IP address to `192.168.123.1`.
<img src="../images/unitree_go2_real_router_ip_change.png" width="500">
- Reason: The Unitree Go2 robot has a fixed IP address of `192.168.123.18`, so the router must assign addresses in the same subnet `192.168.123.x` to enable communication.
- **Assign a Static IP**
- By default, the router uses DHCP to dynamically assign IP addresses.
- However, after disconnection/reconnection, the Unitree Go2s IP may change. To prevent this, configure a **static IP assignment**.
- Go to the **DHCP menu**.
- Enable **Manual Assignment**, enter the Unitree Go2s **MAC address**, and set the IP address to `192.168.123.18`.
<img src="../images/unitree_go2_real_router_dhcp.png" width="500">
- Note: Most routers can automatically detect the MAC address (depending on the model).
---
#### 1.2 Assigning Dynamic IP
**Note**: Use the following method when you **do not have** admin access to the router
**Steps**:
- **Connect your robot to the computer:**
- Connect the `Unitree GO2` to your computer via ethernet
- Go to your network settings, and the select the one where the robot is connected.
- Inside the `TCP/IP` section, change the IP Address to `192.168.123.100` with a subnet mask of `255.255.255.0`
- Use `ifconfig/ipconfig`. The robot should be connected to your computer at the IP address `192.168.123.18`
- Connect to the robot at:
```
ssh unitree@192.168.123.18
```
- **Connect your robot to the shared Wi-Fi:**
- Open the following file inside the terminal:
```
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
```
- Add the following:
```
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=IN
network={
ssid="Wifi Name"
psk="Password"
}
```
- Reset the connection to `wlan0` to connect to wifi:
```
sudo pkill wpa_supplicant
sudo rm -rf /var/run/wpa_supplicant/wlan0
sudo ip link set wlan0 down
sudo ip link set wlan0 up
sudo wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf
sudo dhclient -v wlan0
```
**Tip:** You add the following commands in an executable shell script to avoid running the commands everytime manually
- The robot should be assigned with a dynamic IP in the format `10.x.x.x`
- `SSH` into the robot using the assigned IP in a new terminal
- After a successful connection you can remove the ethernet from your computer and the robot
- **Check Connectivity**
- After completing the above steps, check the connectivity between your PC and the Unitree Go2.
- You can use the `ping` command in the terminal to verify the connection:
```
ping 192.168.123.18
```
- If you receive replies, the connection is successful.
- **SSH Access**
- You can access the Unitree Go2 via SSH using the following command:
```
ssh -X unitree@192.168.123.18
```
or
```
ssh -X unitree@10.x.x.x #USE YOUR ASSIGNED IP
```
- The default password is `123`.
- After logging in, you can select the ROS version (ROS1 Noetic or ROS2 Foxy). In this example, Select ROS2 Foxy.
<img src="../images/unitree_go2_real_ssh.png" width="500">
- **Check Internet Access**
- Verify that the Unitree Go2 has internet access by running:
```
ping google.com
```
- If you receive replies, the internet connection is working properly.
---
### 2. Install rosbridge (Unitree GO2 side)
On the Unitree GO2, install rosbridge to enable communication with the **ros-mcp-server**.
```bash
sudo apt install ros-foxy-rosbridge-server
```
### 3. Choose and Build Control Package
You have two options for controlling the robot.
### Option A: Using the `go2_ws` workspace
The `go2_ws` workspace exposes the different GO2 sports mode functions as ROS2 services, which the MCP can call to control the robot.
Examples of different actions as service nodes:
- **Posture**: `/go2/stand_up`, `/go2/sit`, `/go2/recovery`, `/go2/balance`
- **Movement**: `/go2/move` (Handles velocity *and* distance/duration automatically)
- **Tricks**: `/go2/dance1`, `/go2/front_flip`, `/go2/hello`, `/go2/stretch`
**[See detailed setup instructions in go2_ws/README.md](./go2_ws/README.md)**
**Setup Summary:**
1. Install dependencies (`unitree_sdk2py`).
2. Copy the `go2_ws` folder to the robot.
3. Build the workspace on the robot using `colcon build`.
#### Option B: Standard `unitree_go` Package
[`unitree_go`](https://github.com/unitreerobotics/unitree_ros2) is the official ROS2 package provided by Unitree Robotics. It provides low-level control and standard topics but fewer high-level "skills" out of the box.
To build the `unitree_go` package, follow these steps:
- **clone the repository** (User PC side)
```bash
git clone https://github.com/unitreerobotics/unitree_ros2.git
```
- **move the package to the Unitree GO2's workspace** (User PC side)
```bash
cd unitree_ros2/cyclonedds_ws/src/
scp -r unitree unitree@192.168.123.18:/home/unitree/cyclonedds_ws/src/
```
- **build the package** (Unitree GO2 side)
```bash
cd ~/cyclonedds_ws
colcon build --symlink-install
```
- **source the workspace** (Unitree GO2 side)
```bash
source ~/.bashrc
```
---
### 4. Setup Camera Script (User PC side)
By default, the Unitree GO2 publishes camera data through a custom topic called `/frontvideostream`. To convert this custom topic into a standard ROS2 topic, you can upload and run the following script on the Unitree GO2.
- **move the script to the Unitree GO2** (User PC side)
```bash
cd /<ABSOLUTE_PATH>/ros-mcp-server/examples/4_unitree_go2/real_robot/scripts
scp ./camera_bridge.py unitree@192.168.123.18:/home/unitree/cyclonedds_ws/src/image_process/
```
- **check the script** (Unitree GO2 side)
```bash
cd ~/cyclonedds_ws/src/image_process/
python3 camera_bridge.py
```
If the camera is working properly, you should see the camera feed from the `/camera/rgb/image_raw` topic.
---
### 5. Launch rosbridge and camera script (Unitree GO2 side)
To enable communication between the **Unitree GO2** and the **ros-mcp-server**, you need to launch the following commands:
On one terminal, run:
```bash
# Launch rosbridge
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
```
On a separate terminal, run:
```bash
# Launch camera script
cd ~/cyclonedds_ws/src/image_process/
python3 camera_bridge.py
```
## **Integration with MCP Server**
Once rosbridge is running on the Unitree GO2 and your PC is on the same network, you can connect the MCP server to control the robot. If you havent set up the MCP server yet, follow the [installation guide](https://github.com/robotmcp/ros-mcp-server/blob/main/docs/install/installation.md) .
Since The **ros-mcp-server** to recognize the robot, configure it to connect to the robots IP address.
### **Example 1** : Connect to robot
By default, the **ros-mcp-server** can access the Unitree GO2 robot on the same network as the user's local PC. Therefore, the IP address of the Unitree GO2 robot is `192.168.123.18`.
<img src="../images/unitree_go2_real_connect.png" width="500">
### **Example 2** : Check available topics
<img src="../images/unitree_go2_real_topics.png" width="500">
### **Example 3** : Receive camera data and analyze it
<img src="../images/unitree_go2_real_camera.png" width="500">
## **Next Steps**
The Unitree GO2 has various sensors and functionalities. Let's make use of them.
@@ -0,0 +1,333 @@
# Go2 ROS 2 Control Workspace
Complete ROS 2 workspace for controlling the Unitree Go2 quadruped robot via custom services.
## Workspace Structure
```
go2_ws/
├── src/
│ ├── go2_interfaces/ # Custom service definitions
│ │ ├── CMakeLists.txt
│ │ ├── package.xml
│ │ └── srv/
│ │ ├── Trigger.srv # Simple trigger (no input)
│ │ ├── SetBool.srv # Enable/disable features
│ │ ├── Move.srv # Movement with velocity + distance
│ │ ├── SetFloat.srv # Float parameter setting
│ │ └── SetInt.srv # Integer parameter setting
│ │
│ └── go2_control/ # Main control package
│ ├── package.xml
│ ├── setup.py
│ ├── setup.cfg
│ ├── resource/
│ │ └── go2_control
│ └── go2_control/
│ ├── __init__.py
│ ├── go2_backend.py # SDK communication (separate process)
│ └── go2_service_node.py # ROS 2 services
```
## Build Instructions
```bash
cd ~/go2_ws
colcon build --symlink-install
source install/setup.bash
```
## Running
```bash
source ~/go2_ws/install/setup.bash
ros2 run go2_control go2_services
```
---
## Complete Service Reference
### Service Types
| Type | Description | Request Fields |
|------|-------------|----------------|
| `Trigger` | Simple actions | (none) |
| `SetBool` | Toggle features | `enable: bool` |
| `Move` | Movement control | `vx, vy, vyaw, distance` |
| `SetFloat` | Float parameters | `value: float64` |
| `SetInt` | Integer parameters | `value: int32` |
---
### POSTURE SERVICES (Trigger)
| Service | Description |
|---------|-------------|
| `/go2/damp` | **Emergency stop** - Enter damping state (highest priority) |
| `/go2/balance` | Enter balance stand mode (maintains posture on uneven terrain) |
| `/go2/stop` | Stop current action, restore parameters to defaults |
| `/go2/stand_up` | Stand up to high stance (0.33m default height) |
| `/go2/stand_down` | Lie down to low stance |
| `/go2/recovery` | Recovery stand - use after robot falls or is in unknown state |
| `/go2/sit` | Sit down (special sitting posture) |
| `/go2/rise_sit` | Stand up from sitting position |
```bash
# Examples
ros2 service call /go2/stand_up go2_interfaces/srv/Trigger
ros2 service call /go2/sit go2_interfaces/srv/Trigger
ros2 service call /go2/recovery go2_interfaces/srv/Trigger
```
---
### MOVEMENT SERVICE (Move)
**Service:** `/go2/move`
**Request Fields:**
- `vx` (float64): Forward velocity in m/s (range: -2.5 to 3.8)
- `vy` (float64): Lateral velocity in m/s (range: -1.0 to 1.0)
- `vyaw` (float64): Angular velocity in rad/s (range: -4.0 to 4.0)
- `distance` (float64): Distance in meters OR angle in radians
**Response Fields:**
- `success` (bool): Command accepted
- `message` (string): Execution details
- `estimated_time` (float64): Estimated completion time in seconds
- `iterations` (int32): Number of control iterations
**Note:** The robot automatically enters walking mode when needed. No need to call a separate "free_walk" service.
```bash
# Move forward 1 meter at 0.3 m/s
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.3, vy: 0.0, vyaw: 0.0, distance: 1.0}"
# Move backward 0.5 meters
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: -0.3, vy: 0.0, vyaw: 0.0, distance: 0.5}"
# Strafe left 1 meter
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: 0.2, vyaw: 0.0, distance: 1.0}"
# Strafe right 0.5 meters
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: -0.2, vyaw: 0.0, distance: 0.5}"
# Turn left 90 degrees (π/2 ≈ 1.57 radians)
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: 0.0, vyaw: 0.5, distance: 1.57}"
# Turn right 180 degrees (π ≈ 3.14 radians)
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: 0.0, vyaw: -0.5, distance: 3.14}"
# Diagonal movement (forward-left)
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.2, vy: 0.2, vyaw: 0.0, distance: 1.0}"
```
---
### BODY CONFIGURATION SERVICES
#### `/go2/body_height` (SetFloat)
Set body height relative to default (0.33m).
- Range: -0.18 to 0.03 meters
- Example: `-0.1` sets height to 0.23m
```bash
ros2 service call /go2/body_height go2_interfaces/srv/SetFloat "{value: -0.1}"
```
#### `/go2/foot_raise_height` (SetFloat)
Set foot raise height relative to default (0.09m).
- Range: -0.06 to 0.03 meters
```bash
ros2 service call /go2/foot_raise_height go2_interfaces/srv/SetFloat "{value: 0.02}"
```
#### `/go2/speed_level` (SetInt)
Set speed range.
- `-1` = Slow
- `0` = Normal
- `1` = Fast
```bash
ros2 service call /go2/speed_level go2_interfaces/srv/SetInt "{value: 1}"
```
#### `/go2/switch_gait` (SetInt)
Switch gait mode.
- `0` = Idle
- `1` = Trot
- `2` = Trot running
- `3` = Forward climbing mode
- `4` = Reverse climbing mode
```bash
ros2 service call /go2/switch_gait go2_interfaces/srv/SetInt "{value: 1}"
```
---
### TOGGLE SERVICES (SetBool)
| Service | Description |
|---------|-------------|
| `/go2/switch_joystick` | Enable/disable native remote control response |
| `/go2/continuous_gait` | Enable/disable continuous gait (maintains walking even at zero velocity) |
| `/go2/handstand` | Enable/disable handstand mode |
| `/go2/walk_upright` | Enable/disable upright walking |
| `/go2/free_avoid` | Enable/disable obstacle avoidance |
| `/go2/free_bound` | Enable/disable bounding gait |
| `/go2/cross_step` | Enable/disable cross-step walking |
| `/go2/free_jump` | Enable/disable jumping capability |
```bash
# Enable obstacle avoidance
ros2 service call /go2/free_avoid go2_interfaces/srv/SetBool "{enable: true}"
# Disable remote control response
ros2 service call /go2/switch_joystick go2_interfaces/srv/SetBool "{enable: false}"
# Enable handstand
ros2 service call /go2/handstand go2_interfaces/srv/SetBool "{enable: true}"
```
---
### GESTURE / TRICK SERVICES (Trigger)
| Service | Description |
|---------|-------------|
| `/go2/hello` | Wave hello greeting |
| `/go2/stretch` | Stretch |
| `/go2/wallow` | Roll over |
| `/go2/pose_on` | Strike a pose |
| `/go2/pose_off` | Exit pose |
| `/go2/scrape` | New Year greeting gesture |
| `/go2/front_flip` | Front flip |
| `/go2/front_jump` | Jump forward |
| `/go2/front_pounce` | Pounce forward |
| `/go2/left_flip` | Left flip |
| `/go2/back_flip` | Back flip |
| `/go2/dance1` | Dance routine 1 |
| `/go2/dance2` | Dance routine 2 |
```bash
# Say hello
ros2 service call /go2/hello go2_interfaces/srv/Trigger
# Dance
ros2 service call /go2/dance1 go2_interfaces/srv/Trigger
# Front flip (caution!)
ros2 service call /go2/front_flip go2_interfaces/srv/Trigger
# Stretch
ros2 service call /go2/stretch go2_interfaces/srv/Trigger
```
---
## Distance Calculation
The Move service automatically calculates iterations:
```
iterations = ceil(distance / (velocity × 0.5s))
```
**Example:** Moving 2 meters at 0.3 m/s
- Distance per iteration: 0.3 × 0.5 = 0.15 meters
- Iterations needed: ceil(2.0 / 0.15) = 14
- Estimated time: 14 × 0.5 = 7.0 seconds
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ LLM / User │
└─────────────────────────────────────────────────────────────┘
│ ROS 2 Service Calls
┌─────────────────────────────────────────────────────────────┐
│ Go2ServiceNode (ROS 2) │
│ - Exposes /go2/* services │
│ - Validates inputs, calculates iterations │
│ - Sends commands via multiprocessing Queue │
└─────────────────────────────────────────────────────────────┘
│ Multiprocessing Queue
┌─────────────────────────────────────────────────────────────┐
│ Backend Process (SDK) │
│ - Runs Unitree SDK (separate DDS domain) │
│ - Executes SportClient commands │
│ - Handles mode transitions and timing │
│ - Automatically enters walking mode for Move commands │
└─────────────────────────────────────────────────────────────┘
│ Unitree SDK
┌─────────────────────────────────────────────────────────────┐
│ Go2 Robot │
└─────────────────────────────────────────────────────────────┘
```
---
## LLM Integration Examples
### Common Commands for LLM
**"Stand up and walk forward 3 meters":**
```bash
ros2 service call /go2/stand_up go2_interfaces/srv/Trigger
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.3, vy: 0.0, vyaw: 0.0, distance: 3.0}"
```
**"Turn around" (180°):**
```bash
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: 0.0, vyaw: 0.5, distance: 3.14}"
```
**"Say hello and do a dance":**
```bash
ros2 service call /go2/hello go2_interfaces/srv/Trigger
ros2 service call /go2/dance1 go2_interfaces/srv/Trigger
```
**"Sit down":**
```bash
ros2 service call /go2/sit go2_interfaces/srv/Trigger
```
---
## Troubleshooting
### "Service not found"
```bash
source ~/go2_ws/install/setup.bash
```
### "Module not found: go2_interfaces"
Rebuild and source:
```bash
cd ~/go2_ws
colcon build --packages-select go2_interfaces
source install/setup.bash
colcon build --packages-select go2_control
source install/setup.bash
```
### Clock skew warnings
```bash
sudo timedatectl set-ntp true
# or
sudo ntpdate ntp.ubuntu.com
```
These warnings don't affect functionality.
@@ -0,0 +1,2 @@
# go2_control package
# ROS 2 services for Unitree Go2 robot control
@@ -0,0 +1,322 @@
"""
Go2 Backend Module
==================
Handles direct communication with the Unitree Go2 robot via the SDK.
This module runs in a SEPARATE PROCESS from the ROS 2 node because:
1. Unitree SDK uses its own DDS implementation (CycloneDDS)
2. ROS 2 also uses DDS (FastDDS by default)
3. Running both in the same process causes conflicts
Architecture:
[ROS 2 Node] --Queue--> [Backend Process] --SDK--> [Go2 Robot]
"""
import time
from multiprocessing import Queue
from unitree_sdk2py.core.channel import ChannelFactoryInitialize
from unitree_sdk2py.go2.sport.sport_client import SportClient
# Time interval between Move() calls (robot requires this delay)
MOVE_INTERVAL = 0.5 # seconds
def backend_loop(cmd_queue: Queue):
"""
Main backend loop - runs in separate process.
Continuously reads commands from the queue and executes them
on the robot via the Unitree SDK.
Args:
cmd_queue: Multiprocessing queue receiving command dictionaries
"""
# ================================================================
# SDK INITIALIZATION
# ================================================================
ChannelFactoryInitialize(0)
sc = SportClient()
sc.SetTimeout(10.0)
sc.Init()
# ================================================================
# STATE TRACKING
# ================================================================
walking = False # Is robot currently in walking mode?
last_mode_time = 0.0 # Timestamp of last mode change
def mode_guard():
"""
Enforce minimum delay between mode-changing commands.
The robot needs time to transition between modes.
Minimum gap: 0.5 seconds
"""
nonlocal last_mode_time
elapsed = time.time() - last_mode_time
if elapsed < 0.5:
time.sleep(0.5 - elapsed)
last_mode_time = time.time()
print("=" * 60)
print("[Go2 Backend] Initialized and ready for commands")
print("=" * 60)
# ================================================================
# MAIN COMMAND LOOP
# ================================================================
while True:
cmd = cmd_queue.get()
if cmd is None:
print("[Go2 Backend] Received shutdown signal")
break
ctype = cmd.get("type", "")
try:
# --------------------------------------------------------
# POSTURE / STATE COMMANDS
# --------------------------------------------------------
if ctype == "damp":
mode_guard()
sc.Damp()
walking = False
print("[Backend] Executed: Damp (emergency stop)")
elif ctype == "balance":
mode_guard()
sc.BalanceStand()
walking = False
print("[Backend] Executed: BalanceStand")
elif ctype == "stop":
mode_guard()
sc.StopMove()
walking = False
print("[Backend] Executed: StopMove")
elif ctype == "stand_up":
mode_guard()
sc.StandUp()
walking = False
print("[Backend] Executed: StandUp")
elif ctype == "stand_down":
mode_guard()
sc.StandDown()
walking = False
print("[Backend] Executed: StandDown")
elif ctype == "recovery":
mode_guard()
sc.RecoveryStand()
walking = False
print("[Backend] Executed: RecoveryStand")
elif ctype == "sit":
mode_guard()
sc.Sit()
walking = False
print("[Backend] Executed: Sit")
elif ctype == "rise_sit":
mode_guard()
sc.RiseSit()
walking = False
print("[Backend] Executed: RiseSit")
# --------------------------------------------------------
# MOVEMENT COMMAND
# --------------------------------------------------------
elif ctype == "move":
vx = cmd.get("vx", 0.0)
vy = cmd.get("vy", 0.0)
wz = cmd.get("wz", 0.0)
iterations = cmd.get("iterations", 1)
# Internally enable walking mode if not already
if not walking:
mode_guard()
sc.FreeWalk()
walking = True
print(
f"[Backend] Moving: vx={vx:.2f}, vy={vy:.2f}, wz={wz:.2f} for {iterations} iterations"
)
for i in range(iterations):
sc.Move(vx, vy, wz)
time.sleep(MOVE_INTERVAL)
# Stop after completing movement
sc.Move(0.0, 0.0, 0.0)
print("[Backend] Move complete")
# --------------------------------------------------------
# BODY CONFIGURATION COMMANDS
# --------------------------------------------------------
elif ctype == "euler":
roll = cmd.get("roll", 0.0)
pitch = cmd.get("pitch", 0.0)
yaw = cmd.get("yaw", 0.0)
mode_guard()
sc.Euler(roll, pitch, yaw)
print(f"[Backend] Executed: Euler(roll={roll}, pitch={pitch}, yaw={yaw})")
elif ctype == "body_height":
height = cmd.get("height", 0.0)
mode_guard()
sc.BodyHeight(height)
print(f"[Backend] Executed: BodyHeight({height})")
elif ctype == "foot_raise_height":
height = cmd.get("height", 0.0)
mode_guard()
sc.FootRaiseHeight(height)
print(f"[Backend] Executed: FootRaiseHeight({height})")
elif ctype == "speed_level":
level = cmd.get("level", 0)
mode_guard()
sc.SpeedLevel(level)
print(f"[Backend] Executed: SpeedLevel({level})")
elif ctype == "switch_gait":
gait = cmd.get("gait", 0)
mode_guard()
sc.SwitchGait(gait)
print(f"[Backend] Executed: SwitchGait({gait})")
# --------------------------------------------------------
# TOGGLE COMMANDS (enable/disable)
# --------------------------------------------------------
elif ctype == "switch_joystick":
enable = cmd.get("enable", True)
mode_guard()
sc.SwitchJoystick(enable)
print(f"[Backend] Executed: SwitchJoystick({enable})")
elif ctype == "continuous_gait":
enable = cmd.get("enable", False)
mode_guard()
sc.ContinuousGait(enable)
print(f"[Backend] Executed: ContinuousGait({enable})")
elif ctype == "handstand":
enable = cmd.get("enable", False)
mode_guard()
sc.HandStand(enable)
print(f"[Backend] Executed: HandStand({enable})")
elif ctype == "walk_upright":
enable = cmd.get("enable", False)
mode_guard()
sc.WalkUpright(enable)
print(f"[Backend] Executed: WalkUpright({enable})")
elif ctype == "free_avoid":
enable = cmd.get("enable", False)
mode_guard()
sc.FreeAvoid(enable)
print(f"[Backend] Executed: FreeAvoid({enable})")
elif ctype == "free_bound":
enable = cmd.get("enable", False)
mode_guard()
sc.FreeBound(enable)
print(f"[Backend] Executed: FreeBound({enable})")
elif ctype == "cross_step":
enable = cmd.get("enable", False)
mode_guard()
sc.CrossStep(enable)
print(f"[Backend] Executed: CrossStep({enable})")
elif ctype == "free_jump":
enable = cmd.get("enable", False)
mode_guard()
sc.FreeJump(enable)
print(f"[Backend] Executed: FreeJump({enable})")
# --------------------------------------------------------
# GESTURE / TRICK COMMANDS (one-shot actions)
# --------------------------------------------------------
elif ctype == "hello":
mode_guard()
sc.Hello()
print("[Backend] Executed: Hello (greeting)")
elif ctype == "stretch":
mode_guard()
sc.Stretch()
print("[Backend] Executed: Stretch")
elif ctype == "wallow":
mode_guard()
sc.Wallow()
print("[Backend] Executed: Wallow (rolling)")
elif ctype == "pose":
enable = cmd.get("enable", True)
mode_guard()
sc.Pose(enable)
print(f"[Backend] Executed: Pose({enable})")
elif ctype == "scrape":
mode_guard()
sc.Scrape()
print("[Backend] Executed: Scrape (New Year greeting)")
elif ctype == "front_flip":
mode_guard()
sc.FrontFlip()
print("[Backend] Executed: FrontFlip")
elif ctype == "front_jump":
mode_guard()
sc.FrontJump()
print("[Backend] Executed: FrontJump")
elif ctype == "front_pounce":
mode_guard()
sc.FrontPounce()
print("[Backend] Executed: FrontPounce")
elif ctype == "left_flip":
mode_guard()
sc.LeftFlip()
print("[Backend] Executed: LeftFlip")
elif ctype == "back_flip":
mode_guard()
sc.BackFlip()
print("[Backend] Executed: BackFlip")
elif ctype == "dance1":
mode_guard()
sc.Dance1()
print("[Backend] Executed: Dance1")
elif ctype == "dance2":
mode_guard()
sc.Dance2()
print("[Backend] Executed: Dance2")
else:
print(f"[Backend] WARNING: Unknown command type: {ctype}")
except Exception as e:
print(f"[Backend] ERROR executing {ctype}: {e}")
print("[Go2 Backend] Shutdown complete")
@@ -0,0 +1,618 @@
"""
Go2 ROS 2 Service Node
======================
Exposes all Go2 robot capabilities as individual ROS 2 services.
Custom service types from go2_interfaces:
- Trigger : Simple actions (no input)
- SetBool : Enable/disable features
- Move : Movement with velocity + distance
- SetFloat : Float parameter setting
- SetInt : Integer parameter setting
SERVICES PROVIDED:
==================
POSTURE SERVICES (Trigger):
/go2/damp - Emergency stop, enter damping state (highest priority)
/go2/balance - Enter balance stand mode
/go2/stop - Stop current action, restore defaults
/go2/stand_up - Stand up (high stance, 0.33m)
/go2/stand_down - Lie down (low stance)
/go2/recovery - Recovery stand (use after falls)
/go2/sit - Sit down (special action)
/go2/rise_sit - Stand up from sitting
MOVEMENT SERVICE (Move):
/go2/move - Move robot with velocity and distance control
Input: vx (m/s), vy (m/s), vyaw (rad/s), distance (m or rad)
Note: Automatically enters walking mode if needed
BODY CONFIGURATION SERVICES:
/go2/euler - Set body posture (roll, pitch, yaw in radians)
/go2/body_height - Set body height relative to default (SetFloat, range: -0.18 to 0.03m)
/go2/foot_raise_height - Set foot raise height relative to default (SetFloat, range: -0.06 to 0.03m)
/go2/speed_level - Set speed range (SetInt: -1=slow, 0=normal, 1=fast)
/go2/switch_gait - Switch gait (SetInt: 0=idle, 1=trot, 2=trot running, 3=forward climb, 4=reverse climb)
TOGGLE SERVICES (SetBool):
/go2/switch_joystick - Enable/disable native remote control response
/go2/continuous_gait - Enable/disable continuous gait mode
/go2/handstand - Enable/disable handstand
/go2/walk_upright - Enable/disable upright walking
/go2/free_avoid - Enable/disable obstacle avoidance
/go2/free_bound - Enable/disable bounding gait
/go2/cross_step - Enable/disable cross-step walking
/go2/free_jump - Enable/disable jumping
GESTURE / TRICK SERVICES (Trigger):
/go2/hello - Say hello (wave)
/go2/stretch - Stretch
/go2/wallow - Roll over
/go2/pose_on - Strike a pose
/go2/pose_off - Exit pose
/go2/scrape - New Year greeting
/go2/front_flip - Front flip
/go2/front_jump - Jump forward
/go2/front_pounce - Pounce forward
/go2/left_flip - Left flip
/go2/back_flip - Back flip
/go2/dance1 - Dance routine 1
/go2/dance2 - Dance routine 2
EXAMPLE SERVICE CALLS (for LLM reference):
==========================================
# Stand up
ros2 service call /go2/stand_up go2_interfaces/srv/Trigger
# Move forward 2 meters at 0.3 m/s
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.3, vy: 0.0, vyaw: 0.0, distance: 2.0}"
# Turn left 90 degrees (1.57 radians) at 0.5 rad/s
ros2 service call /go2/move go2_interfaces/srv/Move "{vx: 0.0, vy: 0.0, vyaw: 0.5, distance: 1.57}"
# Say hello
ros2 service call /go2/hello go2_interfaces/srv/Trigger
# Do a dance
ros2 service call /go2/dance1 go2_interfaces/srv/Trigger
# Set body height lower by 0.1m
ros2 service call /go2/body_height go2_interfaces/srv/SetFloat "{value: -0.1}"
# Set speed to fast
ros2 service call /go2/speed_level go2_interfaces/srv/SetInt "{value: 1}"
"""
import math
from multiprocessing import Process, Queue
import rclpy
from go2_interfaces.srv import Move, SetBool, SetFloat, SetInt, Trigger
from rclpy.node import Node
from go2_control.go2_backend import backend_loop
# ============================================================
# CONFIGURATION CONSTANTS
# ============================================================
MOVE_INTERVAL = 0.5 # seconds between Move() calls
# Velocity limits from SDK documentation
MAX_VX = 2.5 # m/s (can go up to 3.8 forward)
MAX_VY = 1.0 # m/s
MAX_VYAW = 4.0 # rad/s
def calculate_iterations(distance: float, velocity: float, interval: float = MOVE_INTERVAL) -> int:
"""
Calculate the number of move iterations needed to cover a distance.
Formula:
distance_per_call = velocity × interval
iterations = ceil(distance / distance_per_call)
Args:
distance: Target distance in meters (or radians for rotation)
velocity: Movement velocity in m/s (or rad/s for rotation)
interval: Time between Move() calls (default 0.5s)
Returns:
Number of iterations (minimum 1)
"""
if abs(velocity) < 0.001:
return 1
distance_per_call = abs(velocity) * interval
iterations = math.ceil(abs(distance) / distance_per_call)
return max(1, iterations)
class Go2ServiceNode(Node):
"""
ROS 2 Node providing services for Go2 robot control.
"""
def __init__(self, cmd_queue: Queue):
super().__init__("go2_services")
self.queue = cmd_queue
# ============================================================
# POSTURE SERVICES (Trigger type)
# ============================================================
self.create_service(Trigger, "/go2/damp", self.cb_damp)
self.create_service(Trigger, "/go2/balance", self.cb_balance)
self.create_service(Trigger, "/go2/stop", self.cb_stop)
self.create_service(Trigger, "/go2/stand_up", self.cb_stand_up)
self.create_service(Trigger, "/go2/stand_down", self.cb_stand_down)
self.create_service(Trigger, "/go2/recovery", self.cb_recovery)
self.create_service(Trigger, "/go2/sit", self.cb_sit)
self.create_service(Trigger, "/go2/rise_sit", self.cb_rise_sit)
# ============================================================
# MOVEMENT SERVICE (Move type)
# ============================================================
self.create_service(Move, "/go2/move", self.cb_move)
# ============================================================
# BODY CONFIGURATION SERVICES
# ============================================================
self.create_service(SetFloat, "/go2/body_height", self.cb_body_height)
self.create_service(SetFloat, "/go2/foot_raise_height", self.cb_foot_raise_height)
self.create_service(SetInt, "/go2/speed_level", self.cb_speed_level)
self.create_service(SetInt, "/go2/switch_gait", self.cb_switch_gait)
# ============================================================
# TOGGLE SERVICES (SetBool type)
# ============================================================
self.create_service(SetBool, "/go2/switch_joystick", self.cb_switch_joystick)
self.create_service(SetBool, "/go2/continuous_gait", self.cb_continuous_gait)
self.create_service(SetBool, "/go2/handstand", self.cb_handstand)
self.create_service(SetBool, "/go2/walk_upright", self.cb_walk_upright)
self.create_service(SetBool, "/go2/free_avoid", self.cb_free_avoid)
self.create_service(SetBool, "/go2/free_bound", self.cb_free_bound)
self.create_service(SetBool, "/go2/cross_step", self.cb_cross_step)
self.create_service(SetBool, "/go2/free_jump", self.cb_free_jump)
# ============================================================
# GESTURE / TRICK SERVICES (Trigger type)
# ============================================================
self.create_service(Trigger, "/go2/hello", self.cb_hello)
self.create_service(Trigger, "/go2/stretch", self.cb_stretch)
self.create_service(Trigger, "/go2/wallow", self.cb_wallow)
self.create_service(Trigger, "/go2/pose_on", self.cb_pose_on)
self.create_service(Trigger, "/go2/pose_off", self.cb_pose_off)
self.create_service(Trigger, "/go2/scrape", self.cb_scrape)
self.create_service(Trigger, "/go2/front_flip", self.cb_front_flip)
self.create_service(Trigger, "/go2/front_jump", self.cb_front_jump)
self.create_service(Trigger, "/go2/front_pounce", self.cb_front_pounce)
self.create_service(Trigger, "/go2/left_flip", self.cb_left_flip)
self.create_service(Trigger, "/go2/back_flip", self.cb_back_flip)
self.create_service(Trigger, "/go2/dance1", self.cb_dance1)
self.create_service(Trigger, "/go2/dance2", self.cb_dance2)
# ============================================================
# LOG AVAILABLE SERVICES
# ============================================================
self.get_logger().info("=" * 65)
self.get_logger().info("Go2 ROS 2 Services Ready!")
self.get_logger().info("=" * 65)
self.get_logger().info("")
self.get_logger().info("POSTURE (Trigger):")
self.get_logger().info(" /go2/damp, /go2/balance, /go2/stop, /go2/stand_up")
self.get_logger().info(" /go2/stand_down, /go2/recovery, /go2/sit, /go2/rise_sit")
self.get_logger().info("")
self.get_logger().info("MOVEMENT (Move):")
self.get_logger().info(" /go2/move - Args: vx, vy, vyaw, distance")
self.get_logger().info("")
self.get_logger().info("BODY CONFIG:")
self.get_logger().info(" /go2/body_height (SetFloat), /go2/foot_raise_height (SetFloat)")
self.get_logger().info(" /go2/speed_level (SetInt), /go2/switch_gait (SetInt)")
self.get_logger().info("")
self.get_logger().info("TOGGLES (SetBool):")
self.get_logger().info(" /go2/switch_joystick, /go2/continuous_gait, /go2/handstand")
self.get_logger().info(" /go2/walk_upright, /go2/free_avoid, /go2/free_bound")
self.get_logger().info(" /go2/cross_step, /go2/free_jump")
self.get_logger().info("")
self.get_logger().info("GESTURES/TRICKS (Trigger):")
self.get_logger().info(
" /go2/hello, /go2/stretch, /go2/wallow, /go2/pose_on, /go2/pose_off"
)
self.get_logger().info(" /go2/scrape, /go2/front_flip, /go2/front_jump, /go2/front_pounce")
self.get_logger().info(" /go2/left_flip, /go2/back_flip, /go2/dance1, /go2/dance2")
self.get_logger().info("=" * 65)
# ================================================================
# POSTURE SERVICE CALLBACKS
# ================================================================
def cb_damp(self, request, response):
"""Emergency stop - enter damping state."""
self.queue.put({"type": "damp"})
response.success = True
response.message = "Damp command sent (emergency stop)"
self.get_logger().info("Service: /go2/damp")
return response
def cb_balance(self, request, response):
"""Enter balance stand mode."""
self.queue.put({"type": "balance"})
response.success = True
response.message = "Balance stand command sent"
self.get_logger().info("Service: /go2/balance")
return response
def cb_stop(self, request, response):
"""Stop current action and restore defaults."""
self.queue.put({"type": "stop"})
response.success = True
response.message = "Stop command sent"
self.get_logger().info("Service: /go2/stop")
return response
def cb_stand_up(self, request, response):
"""Stand up to high stance."""
self.queue.put({"type": "stand_up"})
response.success = True
response.message = "Stand up command sent"
self.get_logger().info("Service: /go2/stand_up")
return response
def cb_stand_down(self, request, response):
"""Lie down to low stance."""
self.queue.put({"type": "stand_down"})
response.success = True
response.message = "Stand down command sent"
self.get_logger().info("Service: /go2/stand_down")
return response
def cb_recovery(self, request, response):
"""Recovery stand from fallen state."""
self.queue.put({"type": "recovery"})
response.success = True
response.message = "Recovery stand command sent"
self.get_logger().info("Service: /go2/recovery")
return response
def cb_sit(self, request, response):
"""Sit down."""
self.queue.put({"type": "sit"})
response.success = True
response.message = "Sit command sent"
self.get_logger().info("Service: /go2/sit")
return response
def cb_rise_sit(self, request, response):
"""Stand up from sitting."""
self.queue.put({"type": "rise_sit"})
response.success = True
response.message = "Rise from sit command sent"
self.get_logger().info("Service: /go2/rise_sit")
return response
# ================================================================
# MOVEMENT SERVICE CALLBACK
# ================================================================
def cb_move(self, request, response):
"""
Execute a movement with specified velocities and distance.
Automatically enters walking mode if needed.
"""
vx = request.vx
vy = request.vy
vyaw = request.vyaw
distance = request.distance
# Validate inputs
if distance <= 0:
response.success = False
response.message = "Distance must be positive"
response.estimated_time = 0.0
response.iterations = 0
return response
# Clamp velocities to safe limits
vx = max(-MAX_VX, min(MAX_VX, vx))
vy = max(-MAX_VY, min(MAX_VY, vy))
vyaw = max(-MAX_VYAW, min(MAX_VYAW, vyaw))
# Calculate iterations based on dominant motion
abs_vx = abs(vx)
abs_vy = abs(vy)
abs_vyaw = abs(vyaw)
if abs_vx >= abs_vy and abs_vx >= abs_vyaw and abs_vx > 0.001:
iterations = calculate_iterations(distance, abs_vx)
motion_type = "forward" if vx > 0 else "backward"
elif abs_vy >= abs_vx and abs_vy >= abs_vyaw and abs_vy > 0.001:
iterations = calculate_iterations(distance, abs_vy)
motion_type = "left" if vy > 0 else "right"
elif abs_vyaw > 0.001:
iterations = calculate_iterations(distance, abs_vyaw)
degrees = math.degrees(distance)
motion_type = f"rotate {'left' if vyaw > 0 else 'right'} {degrees:.1f}°"
else:
response.success = False
response.message = "At least one velocity component must be non-zero"
response.estimated_time = 0.0
response.iterations = 0
return response
estimated_time = iterations * MOVE_INTERVAL
self.queue.put({"type": "move", "vx": vx, "vy": vy, "wz": vyaw, "iterations": iterations})
response.success = True
response.message = (
f"Moving {motion_type}: vx={vx:.2f}m/s, vy={vy:.2f}m/s, "
f"vyaw={vyaw:.2f}rad/s, distance={distance:.2f}, "
f"iterations={iterations}, time≈{estimated_time:.1f}s"
)
response.estimated_time = estimated_time
response.iterations = iterations
self.get_logger().info(f"Service: /go2/move - {response.message}")
return response
# ================================================================
# BODY CONFIGURATION CALLBACKS
# ================================================================
def cb_body_height(self, request, response):
"""Set body height relative to default (range: -0.18 to 0.03m)."""
height = max(-0.18, min(0.03, request.value))
self.queue.put({"type": "body_height", "height": height})
response.success = True
response.message = f"Body height set to {height:.3f}m (relative to default 0.33m)"
self.get_logger().info(f"Service: /go2/body_height value={height}")
return response
def cb_foot_raise_height(self, request, response):
"""Set foot raise height relative to default (range: -0.06 to 0.03m)."""
height = max(-0.06, min(0.03, request.value))
self.queue.put({"type": "foot_raise_height", "height": height})
response.success = True
response.message = f"Foot raise height set to {height:.3f}m (relative to default 0.09m)"
self.get_logger().info(f"Service: /go2/foot_raise_height value={height}")
return response
def cb_speed_level(self, request, response):
"""Set speed level (-1=slow, 0=normal, 1=fast)."""
level = max(-1, min(1, request.value))
self.queue.put({"type": "speed_level", "level": level})
level_names = {-1: "slow", 0: "normal", 1: "fast"}
response.success = True
response.message = f"Speed level set to {level} ({level_names.get(level, 'unknown')})"
self.get_logger().info(f"Service: /go2/speed_level value={level}")
return response
def cb_switch_gait(self, request, response):
"""Switch gait (0=idle, 1=trot, 2=trot running, 3=forward climb, 4=reverse climb)."""
gait = max(0, min(4, request.value))
self.queue.put({"type": "switch_gait", "gait": gait})
gait_names = {
0: "idle",
1: "trot",
2: "trot running",
3: "forward climb",
4: "reverse climb",
}
response.success = True
response.message = f"Gait switched to {gait} ({gait_names.get(gait, 'unknown')})"
self.get_logger().info(f"Service: /go2/switch_gait value={gait}")
return response
# ================================================================
# TOGGLE SERVICE CALLBACKS
# ================================================================
def cb_switch_joystick(self, request, response):
"""Enable/disable native remote control response."""
self.queue.put({"type": "switch_joystick", "enable": request.enable})
response.success = True
response.message = f"Joystick response {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/switch_joystick enable={request.enable}")
return response
def cb_continuous_gait(self, request, response):
"""Enable/disable continuous gait mode."""
self.queue.put({"type": "continuous_gait", "enable": request.enable})
response.success = True
response.message = f"Continuous gait {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/continuous_gait enable={request.enable}")
return response
def cb_handstand(self, request, response):
"""Enable/disable handstand mode."""
self.queue.put({"type": "handstand", "enable": request.enable})
response.success = True
response.message = f"Handstand {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/handstand enable={request.enable}")
return response
def cb_walk_upright(self, request, response):
"""Enable/disable upright walking mode."""
self.queue.put({"type": "walk_upright", "enable": request.enable})
response.success = True
response.message = f"Walk upright {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/walk_upright enable={request.enable}")
return response
def cb_free_avoid(self, request, response):
"""Enable/disable obstacle avoidance."""
self.queue.put({"type": "free_avoid", "enable": request.enable})
response.success = True
response.message = f"Obstacle avoidance {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/free_avoid enable={request.enable}")
return response
def cb_free_bound(self, request, response):
"""Enable/disable bounding gait."""
self.queue.put({"type": "free_bound", "enable": request.enable})
response.success = True
response.message = f"Bounding gait {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/free_bound enable={request.enable}")
return response
def cb_cross_step(self, request, response):
"""Enable/disable cross-step walking."""
self.queue.put({"type": "cross_step", "enable": request.enable})
response.success = True
response.message = f"Cross-step {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/cross_step enable={request.enable}")
return response
def cb_free_jump(self, request, response):
"""Enable/disable jumping capability."""
self.queue.put({"type": "free_jump", "enable": request.enable})
response.success = True
response.message = f"Jumping {'enabled' if request.enable else 'disabled'}"
self.get_logger().info(f"Service: /go2/free_jump enable={request.enable}")
return response
# ================================================================
# GESTURE / TRICK SERVICE CALLBACKS
# ================================================================
def cb_hello(self, request, response):
"""Say hello (wave gesture)."""
self.queue.put({"type": "hello"})
response.success = True
response.message = "Hello gesture command sent"
self.get_logger().info("Service: /go2/hello")
return response
def cb_stretch(self, request, response):
"""Perform stretch."""
self.queue.put({"type": "stretch"})
response.success = True
response.message = "Stretch command sent"
self.get_logger().info("Service: /go2/stretch")
return response
def cb_wallow(self, request, response):
"""Roll over."""
self.queue.put({"type": "wallow"})
response.success = True
response.message = "Wallow (roll) command sent"
self.get_logger().info("Service: /go2/wallow")
return response
def cb_pose_on(self, request, response):
"""Strike a pose."""
self.queue.put({"type": "pose", "enable": True})
response.success = True
response.message = "Pose on command sent"
self.get_logger().info("Service: /go2/pose_on")
return response
def cb_pose_off(self, request, response):
"""Exit pose."""
self.queue.put({"type": "pose", "enable": False})
response.success = True
response.message = "Pose off command sent"
self.get_logger().info("Service: /go2/pose_off")
return response
def cb_scrape(self, request, response):
"""New Year greeting gesture."""
self.queue.put({"type": "scrape"})
response.success = True
response.message = "Scrape (New Year greeting) command sent"
self.get_logger().info("Service: /go2/scrape")
return response
def cb_front_flip(self, request, response):
"""Perform front flip."""
self.queue.put({"type": "front_flip"})
response.success = True
response.message = "Front flip command sent"
self.get_logger().info("Service: /go2/front_flip")
return response
def cb_front_jump(self, request, response):
"""Jump forward."""
self.queue.put({"type": "front_jump"})
response.success = True
response.message = "Front jump command sent"
self.get_logger().info("Service: /go2/front_jump")
return response
def cb_front_pounce(self, request, response):
"""Pounce forward."""
self.queue.put({"type": "front_pounce"})
response.success = True
response.message = "Front pounce command sent"
self.get_logger().info("Service: /go2/front_pounce")
return response
def cb_left_flip(self, request, response):
"""Perform left flip."""
self.queue.put({"type": "left_flip"})
response.success = True
response.message = "Left flip command sent"
self.get_logger().info("Service: /go2/left_flip")
return response
def cb_back_flip(self, request, response):
"""Perform back flip."""
self.queue.put({"type": "back_flip"})
response.success = True
response.message = "Back flip command sent"
self.get_logger().info("Service: /go2/back_flip")
return response
def cb_dance1(self, request, response):
"""Perform dance routine 1."""
self.queue.put({"type": "dance1"})
response.success = True
response.message = "Dance 1 command sent"
self.get_logger().info("Service: /go2/dance1")
return response
def cb_dance2(self, request, response):
"""Perform dance routine 2."""
self.queue.put({"type": "dance2"})
response.success = True
response.message = "Dance 2 command sent"
self.get_logger().info("Service: /go2/dance2")
return response
def main(args=None):
"""
Main entry point.
Starts the backend process and ROS 2 service node.
"""
cmd_queue = Queue()
backend = Process(target=backend_loop, args=(cmd_queue,), daemon=True)
backend.start()
rclpy.init(args=args)
node = Go2ServiceNode(cmd_queue)
try:
rclpy.spin(node)
except KeyboardInterrupt:
node.get_logger().info("Shutting down...")
finally:
cmd_queue.put(None)
node.destroy_node()
rclpy.shutdown()
backend.join(timeout=2.0)
if backend.is_alive():
backend.terminate()
if __name__ == "__main__":
main()
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>go2_control</name>
<version>1.0.0</version>
<description>ROS 2 control node for Unitree Go2 quadruped robot</description>
<maintainer email="bharat.jain@plaksha.edu.in">Bharat Jain</maintainer>
<license>MIT</license>
<buildtool_depend>ament_python</buildtool_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>go2_interfaces</exec_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
@@ -0,0 +1,5 @@
[develop]
script_dir=$base/lib/go2_control
[install]
install_scripts=$base/lib/go2_control

Some files were not shown because too many files have changed in this diff Show More