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
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 .
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -0,0 +1 @@
|
||||
3.10
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
include server.json
|
||||
recursive-include robot_specifications *.yaml
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`robotmcp/ros-mcp-server`
|
||||
- 原始仓库:https://github.com/robotmcp/ros-mcp-server
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1 @@
|
||||
# Config package for ROS MCP Server
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 324 KiB |
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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) |
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
@@ -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
|
||||
|
||||
[](https://youtu.be/UAEG0qTADDk)
|
||||
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 16 MiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 47 MiB |
|
After Width: | Height: | Size: 5.6 MiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,247 @@
|
||||
# Example - LIMO (Isaac Sim)
|
||||

|
||||
|
||||
Here is an introduction to the ROS MCP server’s 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 haven’t 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 robot’s 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)
|
||||

|
||||
|
||||
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 Astra’s 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 haven’t 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 robot’s 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()
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 722 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 1019 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 300 KiB |
@@ -0,0 +1,146 @@
|
||||
# Example - Unitree GO2 (Isaac Sim)
|
||||

|
||||
[](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html)
|
||||
[](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 haven’t 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()
|
||||
@@ -0,0 +1,249 @@
|
||||
# Example - Unitree GO2 (Real)
|
||||

|
||||

|
||||
|
||||
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 user’s 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 router’s 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 Go2’s IP may change. To prevent this, configure a **static IP assignment**.
|
||||
- Go to the **DHCP menu**.
|
||||
- Enable **Manual Assignment**, enter the Unitree Go2’s **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 haven’t 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 robot’s 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
|
||||