diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff6a03b..b08d79c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,6 @@ { - "dockerComposeFile": ["../docker/docker-compose.yml", "../docker/docker-compose-dev.yml"], + "name": "simulations", + "dockerComposeFile": "../docker/docker-compose.yml", "service": "sim", "runServices": ["sim"], @@ -7,43 +8,37 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, + "initializeCommand": "bash .devcontainer/host-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", "customizations": { "vscode": { "settings": { + "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ - // --- Python --- "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "ms-python.mypy-type-checker", - - // --- C and C++ --- "ms-vscode.cmake-tools", "josetr.cmake-language-support-vscode", "llvm-vs-code-extensions.vscode-clangd", "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- "Ranch-Hand-Robotics.rde-ros-2", "smilerobotics.urdf", "morningfrog.urdf-visualizer", "misiekhardcore.stl-previewer", - - // --- Container --- "ms-azuretools.vscode-docker", - - // --- Formatters --- "esbenp.prettier-vscode", "yzhang.markdown-all-in-one", "DotJoshJohnson.xml", "tamasfe.even-better-toml", "github.vscode-github-actions", - "mkhl.shfmt" + "mkhl.shfmt", + "unifiedjs.vscode-mdx" ] } } diff --git a/.devcontainer/host-env.sh b/.devcontainer/host-env.sh new file mode 100755 index 0000000..0fc0504 --- /dev/null +++ b/.devcontainer/host-env.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -eu + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +docker_dir="$(cd "$script_dir/../docker" && pwd)" + +defaults="$docker_dir/.env.defaults" +overrides="$docker_dir/.env.local" +env_out="$docker_dir/.env" + +uname_s="$(uname -s 2>/dev/null || echo unknown)" + +log() { printf "\033[1;36m%s\033[0m\n" "$1"; } + +gpu_available() { + [ "$uname_s" = "Linux" ] || return 1 + command -v nvidia-smi >/dev/null 2>&1 || return 1 + nvidia-smi -L >/dev/null 2>&1 || return 1 + docker info --format '{{json .Runtimes}}' 2>/dev/null | grep -q '"nvidia"' || return 1 +} + +gpu_runtime="" +gpu_available && gpu_runtime="nvidia" + +write_display_env() { + case "$uname_s" in + Darwin) + echo "# host: macOS (XQuartz over TCP + VirtualGL)" + echo "DISPLAY=host.docker.internal:0" + echo "VGL_DISPLAY=:88" + echo "VGL_COMPRESS=proxy" + ;; + MINGW* | MSYS* | CYGWIN*) + echo "# host: Windows (VcXsrv/X410 over TCP + VirtualGL)" + echo "DISPLAY=host.docker.internal:0" + echo "VGL_DISPLAY=:88" + echo "VGL_COMPRESS=proxy" + ;; + Linux) + echo "# host: Linux/WSLg (direct socket passthrough)" + echo "DISPLAY=${DISPLAY:-:0}" + ;; + *) + echo "# host: unknown ($uname_s) - falling back to local display :0" + echo "DISPLAY=${DISPLAY:-:0}" + ;; + esac +} + +write_env() { + echo "# generated by .devcontainer/host-env.sh - do not edit or commit" + echo "# to change ports/flags, edit docker/.env.defaults (shared) or docker/.env.local (yours)" + echo "" + + cat "$defaults" + + if [ -f "$overrides" ]; then + echo "" + echo "# --- user overrides (docker/.env.local) ---" + cat "$overrides" + fi + + echo "" + echo "# --- host-detected (generated, do not edit) ---" + write_display_env + echo "SIM_GPU_RUNTIME=$gpu_runtime" + + if [ -n "$gpu_runtime" ]; then + echo "NVIDIA_VISIBLE_DEVICES=all" + echo "NVIDIA_DRIVER_CAPABILITIES=graphics,display,compute,utility" + echo "__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json" + fi +} + +write_env >"$env_out" + +echo "[INFO] host=$uname_s gpu=${gpu_runtime:-none}" diff --git a/.devcontainer/nvidia/devcontainer.json b/.devcontainer/nvidia/devcontainer.json deleted file mode 100644 index 7ed8ec6..0000000 --- a/.devcontainer/nvidia/devcontainer.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "dockerComposeFile": [ - "../../docker/docker-compose.yml", - "../../docker/docker-compose-dev.yml", - "../../docker/docker-compose-gpu.yml" - ], - "service": "sim", - "runServices": ["sim"], - "workspaceFolder": "/home/trickfire/simulations", - "remoteEnv": { - "HOST_WORKSPACE": "${localWorkspaceFolder}" - }, - "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli - "postStartCommand": "bash ./.devcontainer/x_server.sh || true", - - "customizations": { - "vscode": { - "settings": { - "shfmt.executablePath": "/usr/local/bin/shfmt" - }, - "extensions": [ - // --- Python --- - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - - // --- C and C++ --- - "ms-vscode.cmake-tools", - "josetr.cmake-language-support-vscode", - "llvm-vs-code-extensions.vscode-clangd", - "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- - "Ranch-Hand-Robotics.rde-ros-2", - "smilerobotics.urdf", - "morningfrog.urdf-visualizer", - "misiekhardcore.stl-previewer", - - // --- Container --- - "ms-azuretools.vscode-docker", - - // --- Formatters --- - "esbenp.prettier-vscode", - "yzhang.markdown-all-in-one", - "DotJoshJohnson.xml", - "tamasfe.even-better-toml", - "github.vscode-github-actions", - "mkhl.shfmt" - ] - } - } -} diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 012a501..411a7df 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -1,10 +1,6 @@ #!/usr/bin/env bash - -# -------------------------------------------------------------------------------------------- -# Starts a headless X11 desktop (Xorg/Xvfb + Openbox + x11vnc + noVNC) inside the container. -# Intended for running GUI apps (Gazebo) in Docker. -# Supports --verbose flag for debugging (prints all output to console instead of log file). -# -------------------------------------------------------------------------------------------- +# Sets up the container's display: Wayland/X11 passthrough when available, otherwise a +# headless Xorg/Xvfb + Openbox + x11vnc + noVNC stack. See docs/setup/docker.mdx. set -eo pipefail trap '' HUP @@ -57,7 +53,42 @@ parse_args() { done } -try_wayland_passthrough() { +start_vgl_3d_server() { + if [[ $DISPLAY == :* ]]; then + return 0 + fi + + if ! command -v vglrun >/dev/null 2>&1; then + log "[VGL] vglrun not found - skipping (Gazebo/RViz will not be able to render)" + return 0 + fi + + local vgl_display="${VGL_DISPLAY:-:88}" + if xdpyinfo -display "$vgl_display" &>/dev/null; then + log "[VGL] 3D X server already running on $vgl_display" + return 0 + fi + + log "[VGL] Starting VirtualGL 3D X server (Xvfb) on $vgl_display" + Xvfb "$vgl_display" -screen 0 2560x1440x24 >>"$LOG_FILE" 2>&1 & + local vgl_pid=$! + disown "$vgl_pid" 2>/dev/null || true + + # Xvfb takes a moment to start listening; without this the first `sim gazebo` after a + # container start could race it and silently fall back to unaccelerated rendering. + local i + for i in $(seq 1 20); do + if xdpyinfo -display "$vgl_display" &>/dev/null; then + log "[VGL] 3D X server ready on $vgl_display" + return 0 + fi + sleep 0.25 + done + + log "[VGL] WARNING: 3D X server on $vgl_display did not come up - see $LOG_FILE" +} + +try_display_passthrough() { if [ -n "$FORCE_VNC" ]; then DISPLAY="${FORCE_VNC_DISPLAY:-:77}" unset WAYLAND_DISPLAY @@ -65,11 +96,19 @@ try_wayland_passthrough() { return fi + # Case 1: Linux host with a Wayland compositor, or WSL2 with WSLg. The host socket is + # bind-mounted into /run/host-runtime by docker-compose.yml. local wayland_sock="/run/host-runtime/${WAYLAND_DISPLAY:-wayland-0}" if [ -S "$wayland_sock" ]; then log "[X11] Using Wayland socket at $wayland_sock" exit 0 fi + + if [ -n "$DISPLAY" ] && xdpyinfo -display "$DISPLAY" &>/dev/null; then + log "[X11] Using host X11 display at $DISPLAY" + start_vgl_3d_server + exit 0 + fi } detect_backend() { @@ -158,7 +197,12 @@ start_services() { main() { parse_args "$@" - try_wayland_passthrough + try_display_passthrough + + if [[ $DISPLAY != :* ]]; then + log "[X11] $DISPLAY unreachable; falling back to internal Xvfb/Xorg on :0" + DISPLAY=":0" + fi detect_backend parse_screen_resolution @@ -167,11 +211,6 @@ main() { : "${VNC_PORT:?VNC_PORT is not set}" : "${NOVNC_PORT:?NOVNC_PORT is not set}" - if xdpyinfo -display "$DISPLAY" &>/dev/null; then - log "[ERROR] Display $DISPLAY is already in use!" - exit 1 - fi - start_services } diff --git a/.gitignore b/.gitignore index aa2688e..f27adf1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,22 +7,19 @@ !.vscode/launch.json !.vscode/extensions.json -# pixi -.pixi/ - -# Python +# python .mypy_cache __pycache__ *.egg-info/ build/ **/.venv -# ROS2 +# ros gazebo/install gazebo/build gazebo/log -# sim create tests +# sim tests cli/gazebo/create/tests # chrono outputs @@ -31,9 +28,8 @@ chrono/results/* # env **/*.env -!docker/.env +docker/.env +docker/.env.local # trickfire-docs -.trickfire-docs/ dist/ -.cache/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 482d539..6a9767a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,10 @@ "editor.formatOnSave": true, "editor.formatOnPaste": true }, + "[mdx]": { + "editor.wordWrap": "on", + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, @@ -74,6 +78,5 @@ "${workspaceFolder}/gazebo/sim_common" ], "ROS2.distro": "jazzy", - "cmake.ignoreCMakeListsMissing": true, - "autoDevcontainer.enabled": true + "cmake.ignoreCMakeListsMissing": true } diff --git a/README.md b/README.md index 77cc424..12638b5 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Robot simulations for TrickFire Robotics - Gazebo Harmonic (ROS 2 Jazzy) for rob **Full documentation is at [docs.trickfirerobotics.com/simulations](https://docs.trickfirerobotics.com/simulations)** -- [Getting Started](https://docs.trickfirerobotics.com/simulations/getting-started) - native setup, Dev Container alternative, first launch +- [Getting Started](https://docs.trickfirerobotics.com/simulations) - native setup, Dev Container alternative, first launch - [Running Gazebo](https://docs.trickfirerobotics.com/simulations/gazebo/gazebo) - `sim gazebo` CLI usage and flags - [Running Chrono](https://docs.trickfirerobotics.com/simulations/chrono/chrono) - `sim chrono` CLI usage - [Adding a New Robot](https://docs.trickfirerobotics.com/simulations/gazebo/adding-robots) - OnShape to Gazebo with `sim gazebo create` -- [Environment](https://docs.trickfirerobotics.com/simulations/reference/docker-environment) - Docker environment in depth +- [Dev Notes](https://docs.trickfirerobotics.com/simulations/dev-notes) - architecture decisions, Docker environment internals, CLI tips diff --git a/chrono/data/vsg/imgui.ini b/chrono/data/vsg/imgui.ini index 1319453..ce7c8a2 100644 --- a/chrono/data/vsg/imgui.ini +++ b/chrono/data/vsg/imgui.ini @@ -9,4 +9,5 @@ Size=531,121 [Window][Simulation] Pos=5,5 Size=238,362 +Collapsed=1 diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index 2af21fc..9c99913 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -5,6 +5,7 @@ import os import re import shutil +import socket import subprocess from datetime import datetime from pathlib import Path @@ -107,28 +108,121 @@ def _run_logged_command( raise subprocess.CalledProcessError(return_code, command) +def _x11_port_for(display: str) -> int: + """The X11 protocol's TCP port for a display spec is 6000 + display number.""" + try: + num_part = display.rsplit(":", 1)[-1].split(".")[0] + return 6000 + int(num_part) + except (ValueError, IndexError): + return 6000 + + +_DOCKER_DOCS = "https://docs.trickfirerobotics.com/simulations/setup/docker" + + +def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: + """Pin down which layer (local socket, DNS, TCP, or X11 auth) is broken.""" + lines = [f"Cannot connect to display {display}", ""] + + if display.startswith(":"): + lines += [ + "Expected a Wayland/X11 socket forwarded in from the hosts", + ] + return "\n".join(lines) + + host = display.split(":", 1)[0] + + try: + ip = socket.gethostbyname(host) + lines.append(f" [OK] DNS: '{host}' resolves to {ip}") + except OSError as e: + lines += [f" [FAIL] DNS: '{host}' did not resolve ({e})", "", "Is Docker Desktop running?"] + return "\n".join(lines) + + port = _x11_port_for(display) + try: + with socket.create_connection((host, port), timeout=3): + lines.append(f" [OK] TCP: port {port} on {host} is reachable") + except OSError as e: + lines += [ + f" [FAIL] TCP: could not connect to {host}:{port} ({e})", + f"See {_DOCKER_DOCS} for XQuartz/VcXsrv setup.", + ] + return "\n".join(lines) + + lines += [ + " [FAIL] X11: connected over TCP, but the X server rejected the session:", + f" {xdpyinfo_stderr.strip() or '(no error output captured)'}", + f"See {_DOCKER_DOCS} for X11 authorization (xhost) setup.", + ] + return "\n".join(lines) + + +def _display_reachable(display: str) -> bool: + """Whether an X server is answering on `display`.""" + return ( + subprocess.run( + ["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode + == 0 + ) + + def _check_display() -> None: info("Checking for display...") display = os.environ.get("DISPLAY") if not display: - die("DISPLAY environment variable not set") - if ( - subprocess.call( - ["xdpyinfo", "-display", display], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + die("DISPLAY not set! Try restarting the container") + + result = subprocess.run( + ["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if result.returncode != 0: + die(_diagnose_display(display, result.stderr)) + + +def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: + """Route GL rendering through VirtualGL when displaying on a remote X server""" + display = os.environ.get("DISPLAY", "") + if display.startswith(":"): + return [] + + if not shutil.which("vglrun"): + warn(f"vglrun not installed - GL rendering will fail. See {_DOCKER_DOCS}") + return [] + + vgl_display = os.environ.get("VGL_DISPLAY", ":88") + if not _display_reachable(vgl_display): + warn( + f"VirtualGL's 3D X server on {vgl_display} isn't running - run .devcontainer/x_server.sh" ) - != 0 - ): - die("Cannot connect to display " + display) + return [] + for stale in ("LIBGL_ALWAYS_INDIRECT", "MESA_LOADER_DRIVER_OVERRIDE"): + env.pop(stale, None) -def _configure_force_vnc_rendering(env: dict[str, str]) -> list[str]: - """Force software GL rendering for Gazebo/OGRE2 when running under FORCE_VNC""" - if not os.environ.get("FORCE_VNC"): + env["VGL_DISPLAY"] = vgl_display + env.setdefault("VGL_COMPRESS", "proxy") + + info(f"Rendering through VirtualGL ({vgl_display} -> {display})") + return ["vglrun"] + + +def _configure_rendering(env: dict[str, str]) -> list[str]: + """Pick how Gazebo/OGRE2 should get its GL context, based on where it's being displayed.""" + if os.environ.get("FORCE_VNC"): + info("FORCE_VNC: forcing software rendering (llvmpipe)") + env["LIBGL_ALWAYS_SOFTWARE"] = "1" return [] - info("FORCE_VNC: forcing software rendering (llvmpipe) - no direct GPU access") - env["LIBGL_ALWAYS_SOFTWARE"] = "1" - return [] + return _configure_virtualgl_rendering(env) def _setup_pixi_env() -> None: @@ -163,11 +257,7 @@ def _setup_pixi_env() -> None: def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: bool = False) -> None: - """Build the ROS 2 workspace and launch a robot simulation. - - Auto-detects the environment: configures pixi paths when running natively, - or checks the X display when running inside the Dev Container. - """ + """Build the ROS 2 workspace and launch a robot simulation.""" if build_only and no_build: die("Use either --build-only or --no-build, not both") @@ -179,7 +269,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo build = not no_build launch = not build_only env = os.environ.copy() - render_prefix = _configure_force_vnc_rendering(env) + render_prefix = _configure_rendering(env) bringup_pkg, description_pkg, launch_file_name = _validate_robot_layout(robot_name) if build: @@ -193,12 +283,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / f"{robot_name}-gazebo-{datetime.now():%Y-%m-%d_%H-%M}.log" # noqa: DTZ005 - print("--------------------------------------------------------------") - print(f"Robot: {robot_name}") - print("Simulator: gazebo") - print(f"Workspace: {WORKSPACE_DIR}") - print(f"Log: {log_path}") - print("--------------------------------------------------------------") + info(f"Launching {robot_name} - log: {log_path}") setup_bash = WORKSPACE_DIR / "install" / "setup.bash" @@ -223,10 +308,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo info("Build complete") if not setup_bash.is_file(): - die( - "Missing install/setup.bash.\n" - " Run without --no-build once to generate install artifacts." - ) + die("Missing install/setup.bash - run without --no-build once to generate it") if not launch: info("Build-only requested; skipping launch") diff --git a/docker/.env b/docker/.env deleted file mode 100644 index 4b31831..0000000 --- a/docker/.env +++ /dev/null @@ -1,10 +0,0 @@ -# main source of truth -# for docker exposed ports - -VNC_PORT=5900 -NOVNC_PORT=6080 -ROSBRIDGE_PORT=9090 - -# set to one if you want -# to force vnc startup -# FORCE_VNC=1 diff --git a/docker/.env.defaults b/docker/.env.defaults new file mode 100644 index 0000000..c7c6c2b --- /dev/null +++ b/docker/.env.defaults @@ -0,0 +1,6 @@ +# do not edit! +# create "docker/.env.local" to override + +VNC_PORT=5900 +NOVNC_PORT=6080 +ROSBRIDGE_PORT=9090 diff --git a/docker/Dockerfile b/docker/Dockerfile index 86f8354..935c22e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -134,6 +134,7 @@ RUN rosdep init || true # VNC / DISPLAY # # ---------------------------------------------------------------------------- # + RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ @@ -141,6 +142,22 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ mesa-utils libglx-mesa0 libgl1-mesa-dri x11-apps \ xserver-xorg-core xserver-xorg-video-dummy kmod +# ---------------------------------------------------------------------------- # +# VirtualGL # +# ---------------------------------------------------------------------------- # + +ARG VIRTUALGL_VERSION=3.1.4 +RUN ARCH="$(dpkg --print-architecture)" && \ + case "${ARCH}" in \ + arm64) VGL_SHA256="5583e08ee5694d14c6ea0f25d2cfd73fa6d5077869c6645c334a948265a777bb" ;; \ + amd64) VGL_SHA256="02edc6b599571c385389af1a006f07a70c298e1d97c580a9bfd4b39d835c51e6" ;; \ + *) echo "No VirtualGL build for architecture '${ARCH}'" >&2; exit 1 ;; \ + esac && \ + curl -L -s -f -o /tmp/virtualgl.deb \ + "https://github.com/VirtualGL/virtualgl/releases/download/${VIRTUALGL_VERSION}/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" && \ + echo "${VGL_SHA256} */tmp/virtualgl.deb" | sha256sum --strict --check && \ + dpkg -i /tmp/virtualgl.deb && \ + rm -f /tmp/virtualgl.deb # ---------------------------------------------------------------------------- # # DEV TOOLING # @@ -151,7 +168,9 @@ RUN echo "--------------- DEV TOOLING ----------------" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ - clangd openssh-server python3.12-venv wget zstd + clangd openssh-server python3.12-venv wget zstd \ + build-essential cmake ninja-build \ + libeigen3-dev libvulkan-dev libxcb1-dev pkg-config RUN --mount=type=cache,target=/root/.cache/pip \ pip3 install ruff pre-commit --break-system-packages --ignore-installed diff --git a/docker/bashrc.sh b/docker/bashrc.sh index 7db664d..a798cf6 100644 --- a/docker/bashrc.sh +++ b/docker/bashrc.sh @@ -73,11 +73,8 @@ fi # ---------- display ---------- if [ -n "${FORCE_VNC:-}" ]; then - # Point new shells at the virtual display x_server.sh set up for FORCE_VNC, - # not the host's real DISPLAY (which is still reachable via the bind-mounted - # X11 socket and would otherwise render GUI apps on the host instead of VNC). export DISPLAY="${FORCE_VNC_DISPLAY:-:77}" -elif [[ -n ${DISPLAY:-} && ${DISPLAY} != :* ]]; then +elif [[ -n ${DISPLAY:-} && ${DISPLAY} =~ ^[0-9]+(\.[0-9]+)?$ ]]; then export DISPLAY=":${DISPLAY}" fi diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml deleted file mode 100644 index 1af67cf..0000000 --- a/docker/docker-compose-dev.yml +++ /dev/null @@ -1,31 +0,0 @@ -services: - sim: - environment: - WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}" - XDG_RUNTIME_DIR: /run/host-runtime - QT_SCALE_FACTOR: "${QT_SCALE_FACTOR:-}" - QT_ENABLE_HIGHDPI_SCALING: "${QT_ENABLE_HIGHDPI_SCALING:-}" - GDK_SCALE: "${GDK_SCALE:-}" - GDK_DPI_SCALE: "${GDK_DPI_SCALE:-}" - FORCE_VNC: "${FORCE_VNC:-}" - build: - args: - USER_UID: "${UID:-1000}" - volumes: - - ${XDG_RUNTIME_DIR:-/tmp}:/run/host-runtime:ro - - type: bind - source: /tmp/.X11-unix - target: /tmp/.X11-unix - bind: - create_host_path: true - - type: bind - source: ${HOME}/.config/trickfire - target: /home/trickfire/.config/trickfire - bind: - create_host_path: true - - type: bind - source: ${HOME}/.ssh - target: /home/trickfire/.ssh - read_only: true - bind: - create_host_path: true diff --git a/docker/docker-compose-gpu.yml b/docker/docker-compose-gpu.yml deleted file mode 100644 index 5876f48..0000000 --- a/docker/docker-compose-gpu.yml +++ /dev/null @@ -1,18 +0,0 @@ -# nvidia gpu acceleration, addon compose for base compose - -services: - sim: - runtime: nvidia - - environment: - NVIDIA_VISIBLE_DEVICES: all - NVIDIA_DRIVER_CAPABILITIES: graphics,display,compute,utility - __EGL_VENDOR_LIBRARY_FILENAMES: /usr/share/glvnd/egl_vendor.d/10_nvidia.json - - volumes: - - /tmp/.X11-unix:/tmp/.X11-unix - - command: >- - bash -lc 'set -e; - python3 -m venv .venv && source .venv/bin/activate && pip install -e . && \ - exec sleep infinity' diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 36004ac..bdbac3e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,43 +1,75 @@ services: - sim: - build: - context: .. - dockerfile: docker/Dockerfile - target: sim - network: host - args: - VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" - NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" - ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" - image: simulations:latest - container_name: simulations - user: trickfire + sim: + build: + context: .. + dockerfile: docker/Dockerfile + target: sim + network: host + args: + VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" + NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" + ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" + USER_UID: "${UID:-1000}" + image: simulations:latest + container_name: simulations + user: trickfire - environment: - DISPLAY: "${DISPLAY:-:0}" - VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" - NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" - ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" - QT_X11_NO_MITSHM: "1" - TZ: ${TZ:-UTC} + runtime: "${SIM_GPU_RUNTIME:-}" - volumes: - - ..:/home/trickfire/simulations - - /lib/modules:/lib/modules:ro - - type: bind - source: ${XDG_RUNTIME_DIR:-/run/user/1000} - target: /run/user/1000 - bind: - create_host_path: true + environment: + VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" + NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" + ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" + QT_X11_NO_MITSHM: "1" + TZ: ${TZ:-UTC} + WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}" + XDG_RUNTIME_DIR: /run/host-runtime + QT_SCALE_FACTOR: "${QT_SCALE_FACTOR:-}" + QT_ENABLE_HIGHDPI_SCALING: "${QT_ENABLE_HIGHDPI_SCALING:-}" + GDK_SCALE: "${GDK_SCALE:-}" + GDK_DPI_SCALE: "${GDK_DPI_SCALE:-}" + FORCE_VNC: "${FORCE_VNC:-}" + DISPLAY: "${DISPLAY:-}" + VGL_DISPLAY: "${VGL_DISPLAY:-}" + VGL_COMPRESS: "${VGL_COMPRESS:-}" + NVIDIA_VISIBLE_DEVICES: "${NVIDIA_VISIBLE_DEVICES:-}" + NVIDIA_DRIVER_CAPABILITIES: "${NVIDIA_DRIVER_CAPABILITIES:-}" + __EGL_VENDOR_LIBRARY_FILENAMES: "${__EGL_VENDOR_LIBRARY_FILENAMES:-}" - working_dir: /home/trickfire/simulations + volumes: + - ..:/home/trickfire/simulations + - /lib/modules:/lib/modules:ro + - type: bind + source: ${XDG_RUNTIME_DIR:-/run/user/1000} + target: /run/user/1000 + bind: + create_host_path: true + - ${XDG_RUNTIME_DIR:-/tmp}:/run/host-runtime:ro + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + bind: + create_host_path: true + - type: bind + source: ${HOME}/.config/trickfire + target: /home/trickfire/.config/trickfire + bind: + create_host_path: true + - type: bind + source: ${HOME}/.ssh + target: /home/trickfire/.ssh + read_only: true + bind: + create_host_path: true - ports: - - "${VNC_PORT}:${VNC_PORT}" - - "${NOVNC_PORT}:${NOVNC_PORT}" - - "${ROSBRIDGE_PORT}:${ROSBRIDGE_PORT}" + working_dir: /home/trickfire/simulations - privileged: true + ports: + - "${VNC_PORT}:${VNC_PORT}" + - "${NOVNC_PORT}:${NOVNC_PORT}" + - "${ROSBRIDGE_PORT}:${ROSBRIDGE_PORT}" - stdin_open: true - tty: true + privileged: true + + stdin_open: true + tty: true diff --git a/docs.config.json b/docs.config.json index 973b6b6..421a11b 100644 --- a/docs.config.json +++ b/docs.config.json @@ -4,13 +4,12 @@ "description": "Multi-sim environment: Gazebo Harmonic (ROS 2 Jazzy) for robot subsystems, and Project Chrono for terrain/wheel physics", "icon": "MonitorCloud", "sidebar": [ - { "label": "Getting Started", "slug": "getting-started" }, + { "label": "Getting Started", "slug": "index" }, { "label": "Setup", "items": [ - { "label": "Native (pixi)", "slug": "setup/pixi" }, { "label": "Docker", "slug": "setup/docker" }, - { "label": "Docker + Nvidia", "slug": "setup/nvidia" } + { "label": "Native (pixi)", "slug": "setup/pixi" } ] }, { @@ -25,28 +24,13 @@ "label": "Adding a New Robot", "slug": "gazebo/adding-robots" }, - { "label": "ROS Workspace", "slug": "gazebo/ros-workspace" }, - { "label": "Launch System", "slug": "gazebo/launch-system" }, - { - "label": "Robot Packages", - "slug": "gazebo/robot-packages" - }, - { "label": "Joint GUI", "slug": "gazebo/joint-gui" } + { "label": "Architecture", "slug": "gazebo/architecture" } ] }, { "label": "Chrono", "items": [{ "label": "Running Simulations", "slug": "chrono/chrono" }] }, - { - "label": "Reference", - "items": [ - { "label": "Dev Notes", "slug": "reference/dev-notes" }, - { - "label": "Docker Environment", - "slug": "reference/docker-environment" - } - ] - } + { "label": "Dev Notes", "slug": "dev-notes" } ] } diff --git a/docs/chrono/chrono.mdx b/docs/chrono/chrono.mdx index 36e0c0f..81570cc 100644 --- a/docs/chrono/chrono.mdx +++ b/docs/chrono/chrono.mdx @@ -13,15 +13,23 @@ sim chrono run This builds the C++ simulation from `chrono/` (if not already built) and runs it. A Vulkan window opens in the VNC desktop showing the terrain simulation. -## Cleaning + -```bash title="Inside devcontainer" -sim chrono clean -``` + To force a clean rebuild (could help during any unexpected error) run: + + ```bash title="Inside devcontainer" + sim chrono clean + ``` -Deletes `chrono/build/`. Use this to force a clean rebuild. + This deletes `chrono/build/`. + + ## Troubleshooting -**Chrono window doesn't appear:** -Chrono uses Vulkan for rendering. Make sure the VNC desktop is running (`localhost:5900`) and that the container has Wayland/display access. If the window still doesn't appear, check the terminal output for Vulkan errors. + + + Chrono uses [Vulkan](https://en.wikipedia.org/wiki/Vulkan) for rendering. Make sure you followed all the steps for your platform [during setup](../setup/docker) + + + diff --git a/docs/dev-notes.mdx b/docs/dev-notes.mdx new file mode 100644 index 0000000..7f6f326 --- /dev/null +++ b/docs/dev-notes.mdx @@ -0,0 +1,230 @@ +--- +title: Dev Notes +description: Internal notes, tips, and architectural decisions for contributors. +--- + +## Architecture decisions + +### Docker vs. pixi + +Both workflows produce the same ROS 2 Jazzy + Gazebo Harmonic environment. Docker is the primary, +recommended workflow; pixi is a supporting option for a faster Gazebo-only loop. + + + + ROS 2 + Gazebo + Chrono are notoriously painful to install and keep in sync natively, + especially across different OS versions. The Dev Container pins every dependency (ROS 2 + Jazzy, Gazebo Harmonic, VSG, Chrono, VirtualGL) inside a pre-built image, so it behaves the + same on Linux, macOS, and Windows, with or without a GPU. The display/GPU quirks that used + to make Docker painful - X11 forwarding, VirtualGL, NVIDIA runtime detection - are now + handled automatically by `.devcontainer/host-env.sh` and `.devcontainer/x_server.sh` (see + [below](#docker-environment)), so the container works out of the box. It's also the only + workflow that supports Chrono. + + + [pixi](https://pixi.sh) installs ROS 2 Jazzy and Gazebo Harmonic into a self-contained conda + environment inside the repo's `.pixi/` directory - no system-level ROS install, and deleting + the repo removes the environment cleanly. It's a faster loop than Docker since there's no + container layer, but it only supports Gazebo (not Chrono) and leaves you to work around any + OS-specific quirks yourself. Reach for it when you want the fastest Gazebo-only iteration; + use Docker for everything else, especially Chrono. + + + + + + [Gazebo Harmonic](https://gazebosim.org/docs/harmonic/) is the current LTS release of the new-generation Gazebo and + pairs with ROS 2 Jazzy which is used on the Apollo rover this sim is mainly used to test components of and for. It + replaces the old [Gazebo Fortress](https://gazebosim.org/docs/fortress/) that was LTS for ROS Humble Viator uses. + + + The `_description` / `_bringup` split is a ROS convention: + + - **`_description`** contains the robot model (URDF + meshes) - things that change when the CAD changes + - **`_bringup`** contains runtime config (launch files, controller YAML, RViz config) - things you tweak during development + + This separation means `sim gazebo update` can safely replace the description without + touching your launch customizations. See [Architecture](./gazebo/architecture) for the + full package layout. + + + + +## Docker environment + +The [Dev Container](./setup/docker) is built from two Dockerfiles that work together, plus a pair +of host-side scripts that configure display and GPU access before the container starts. + +### Image build + +`docker/vsg-chrono.Dockerfile` builds VSG and Chrono from source - a slow, rarely-changing C++ +build - and publishes it as `ghcr.io/trickfirerobotics/simulations-vsg-chrono` via +[CI](https://github.com/TrickfireRobotics/simulations/blob/main/.github/workflows/vsg-chrono.yml) +on every push to `main` that touches it (or on manual dispatch), for both `amd64` and `arm64`. +`docker/Dockerfile`'s `sim` stage then copies `/opt/vsg` and `/home/trickfire/chrono` out of that +image and builds everything else on top as `simulations:latest` - changing the app Dockerfile +never triggers a Chrono rebuild. + +| Section | Contents | +| ------------- | ------------------------------------------------------------------------ | +| BASE | Locale, `trickfire` user, a libdrm stub needed for Jetson Mesa rendering | +| VSG + CHRONO | Copied in from the `vsg-chrono` base image | +| GAZEBO | ROS 2 Jazzy + Gazebo Harmonic, `ros-gz`, controllers, RViz, xacro | +| VNC / DISPLAY | Headless X stack: Xorg/Xvfb, Openbox, x11vnc, noVNC | +| VirtualGL | GL proxying for remote (XQuartz/VcXsrv) displays | +| DEV TOOLING | SSH server, ruff, pre-commit, shfmt, clangd | + + + A `BUILD_JOBS` build arg controls C++ build parallelism (defaults to `nproc`). Override it in + `docker/.env.local` if a full-parallel build overwhelms your machine. (my Macbook Air reaches + 50°C normally doing a all 8-core build 😭) + + +### Host detection + +`.devcontainer/host-env.sh` runs as the devcontainer's `initializeCommand`, before the container +starts. It writes `docker/.env` (loaded by compose) from three sources, in order: + + + `docker/.env.defaults` - committed defaults (ports, etc.) + `docker/.env.local` - your machine-only overrides, gitignored + + Host/GPU detection - `DISPLAY`, `VGL_DISPLAY`, and `SIM_GPU_RUNTIME` based on `uname` and + whether `nvidia-smi` + the `nvidia` Docker runtime are both present + + + +| Variable | Set by | Purpose | +| ------------------------------------ | --------------------------- | ----------------------------------------------- | +| `DISPLAY` | host-env.sh | Host display to forward (local or TCP) | +| `VGL_DISPLAY` | host-env.sh (macOS/Windows) | VirtualGL's in-container 3D X server | +| `SIM_GPU_RUNTIME` | host-env.sh | `nvidia` or empty - feeds `runtime:` in compose | +| `WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR` | docker-compose.yml | Wayland socket passthrough for Vulkan (Chrono) | + +### Display architecture + +`.devcontainer/x_server.sh` runs as `postStartCommand`, after the container is up, and picks the +cheapest option that works: + +| Situation | What happens | +| --------------------------------------------------- | -------------------------------------------------------------------------- | +| Host Wayland socket (`/run/host-runtime`) reachable | Used directly - no extra services started | +| Host X11 socket (`/tmp/.X11-unix`) reachable | Used directly - no extra services started | +| Neither reachable | Falls back to a self-contained stack: Xorg/Xvfb + Openbox + x11vnc + noVNC | + + + Over TCP it additionally starts a headless VirtualGL 3D X server on `:88` - that's what `vglrun` + (used automatically by `sim gazebo`) renders against, so Gazebo/RViz get a real GL context even + though the host X server can't provide one. See [Docker + setup](./setup/docker#2-check-if-display-works) for the per-OS walkthrough. + + +When falling back to VNC, the backend depends on available hardware: + +- Xorg with the NVIDIA driver on desktop GPUs +- `vkms` for a virtual DRI3-capable display when no GPU is present +- Xvfb + EGL on Jetson/Tegra (no `/dev/dri`) + +Chrono's Vulkan rendering uses the Wayland socket when present, independent of this X11 path. + +### Container user + +Runs as non-root `trickfire` (uid 1000) with passwordless sudo, so files created in the +bind-mounted repo stay owned by your host user. + +### Extending the container + + + + Edit `docker/Dockerfile` (or `docker/vsg-chrono.Dockerfile` for the VSG/Chrono base) to add + system packages. + + Rebuild with **Dev Containers: Rebuild Container**. + + + + Python packages for simulation-time use go in the `pip3 install` line in the GAZEBO section; + dev-only tools go in DEV TOOLING. + + +## Useful CLI commands + +### Gazebo camera position + +To set the camera position in the Gazebo viewer: + +```bash +gz service -s /gui/move_to/pose \ + --reqtype gz.msgs.GUICamera \ + --reptype gz.msgs.Boolean \ + --timeout 2000 \ + --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}" +``` + +To read the current camera position: + +```bash +gz topic -e -t /gui/camera/pose +``` + +### ROS 2 inspection + +```bash +# List all topics +ros2 topic list + +# See joint states in real time +ros2 topic echo /joint_states + +# List active controllers +ros2 control list_controllers + +# Check controller manager status +ros2 control list_hardware_interfaces +``` + +### Building a single package + +```bash +cd gazebo +colcon build --packages-select arm_description +source install/setup.bash +``` + +## Community `apt` repository (Dev Container) + +Some ROS packages live in the `universe` repository rather than `main`. If `apt` can't find a package inside the container: + +```bash title="Inside devcontainer" +apt-get update +apt-get install -y software-properties-common +add-apt-repository -y universe +apt-get update +``` + + + The Dockerfile already enables `universe` for the packages that need it. + + +## Common pitfalls + + + + ROS 2 can't find packages until you run `source gazebo/install/setup.bash`. The launch + script does this automatically, but if you're running ROS commands manually, you need to + source first. + + + If something breaks for no obvious reason, run `sim gazebo clean` and rebuild. Colcon's + incremental builds can get confused after certain types of changes. + + + Gazebo and RViz will fail silently or crash if the X server isn't started. The Dev Container + starts it automatically, but if you need to restart it manually, run + `.devcontainer/x_server.sh`. + + + If port 6080 or 5900 is already in use, the X server script will fail. Make sure no other + VNC sessions or containers are using those ports. + + diff --git a/docs/gazebo/adding-robots.mdx b/docs/gazebo/adding-robots.mdx index 16a6c2b..b1e2e31 100644 --- a/docs/gazebo/adding-robots.mdx +++ b/docs/gazebo/adding-robots.mdx @@ -3,42 +3,18 @@ title: Adding a New Robot description: How to generate ROS 2 packages from an OnShape CAD model and get a new robot running in Gazebo. --- -`sim gazebo create` takes a robot from OnShape and produces two ready-to-build ROS 2 packages - no manual URDF editing or package scaffolding required. +The command `sim gazebo create` takes a robot from OnShape and produces two ROS 2 packages. To make sure we do not hit the low OnShape API limits, requests to OnShape are routed through the [dashboard](https://github.com/TrickfireRobotics/dashboard.git) that authenticates you and caches common request long-term. Authentication is required. You can do it with the following command: -## Via GitHub Actions (recommended) - -You do not need any credentials or local setup. The **Create new robot** workflow runs on CI and opens a PR with the generated packages. - -1. Go to **Actions** and then to **Create new robot** -2. Click **Run workflow** and fill in the inputs: - - **Mode:** `create` - - **Robot name:** unique, lowercase, no spaces (e.g. `arm`) - - **OnShape URL:** the full document URL from OnShape - - **Fix robot to world:** yes for stationary robots (arms, turrets), no for mobile -3. Wait for the workflow to finish, then review and merge the generated PR - -## Via CLI (developers only) - -Running `sim gazebo create` locally requires authentication via the TrickFire dashboard: - - - This method uses your GitHub `ssh` key. Make sure it is set up. In the Dev Container, your SSH keys are mounted automatically from the host. - - -```bash title="Terminal" +```bash sim gazebo auth ``` Then you can create the robot using: -```bash title="Terminal" +```bash sim gazebo create ``` - - This sim will work with any Onshape API key and secret as long as you put them in the `cli/onshape.env` file under `ONSHAPE_API_KEY` and `ONSHAPE_API_SECRET` (in standard env format). The above is just for TrickFire specific robots. - - ## Flags | Flag | Description | @@ -50,24 +26,29 @@ sim gazebo create ## Updating an existing robot -When the OnShape CAD changes, pull in the new geometry without touching your bringup config. - -**Via CI (recommended):** Actions → **Create new robot** → mode: `update` - -**Locally:** +When the OnShape CAD changes, pull in the new geometry without touching your bringup config using this command: -```bash title="Terminal" +```bash sim gazebo update arm ``` -This replaces only `arm_description/urdf/arm.urdf` and `arm_description/meshes/`. Everything else - your control xacro edits, launch file, controller YAML, RViz config - is left untouched. +This replaces only `arm_description/urdf/arm.urdf` and `arm_description/meshes/`. Everything else, your control xacro edits, launch file, controller YAML, RViz config is left untouched. ## Troubleshooting -**Missing joints:** Joints must be defined as mates in the OnShape assembly. Check the OnShape model if joints don't appear in the generated controller YAML. - -**URDF errors after generation:** See [Robot Packages](../../reference/robot-packages/) for details on what post-processing happens and how to inspect the raw OnShape output. - -**Large mesh files:** `sim gazebo create` decimates STL triangle counts automatically. If meshes are still too large, simplify the geometry in OnShape before re-running. - -**`sim gazebo auth` fails with "no access":** Your API key may be invalid or expired. Generate a new one at [dashboard.trickfirerobotics.com](https://dashboard.trickfirerobotics.com) → Settings → CLI access. + + + Joints must be defined as mates in the OnShape assembly. Check the OnShape model if joints + don't appear in the generated controller YAML. + + + `sim gazebo create` post-processes the raw OnShape URDF (mesh paths, control xacro + generation, indentation). Check the OnShape assembly for unexpected geometry or joint + definitions if the generated URDF still looks wrong afterwards. + + + Your API key may be invalid or expired. Generate a new one at + [dashboard.trickfirerobotics.com](https://dashboard.trickfirerobotics.com) → Settings → CLI + access. + + diff --git a/docs/gazebo/architecture.mdx b/docs/gazebo/architecture.mdx new file mode 100644 index 0000000..095d0b5 --- /dev/null +++ b/docs/gazebo/architecture.mdx @@ -0,0 +1,274 @@ +--- +title: Architecture +description: How the gazebo/ colcon workspace is laid out, how sim gazebo builds it, and how the launch files orchestrate simulation startup. +--- + +`gazebo/` at the repo root is a standard [ROS 2](https://docs.ros.org/en/jazzy/index.html) [colcon](https://colcon.readthedocs.io/) workspace. Every robot, plus a couple of shared packages, lives directly under it as a colcon package. + +``` +gazebo/ +├── _description/ # URDF + meshes from OnShape +├── _bringup/ # arm launch file + configs +├── sim_common/ # shared Python nodes & launch helpers +├── sim_worlds/ # world SDFs + Gazebo GUI config +├── build/ ┐ +├── install/ ├─ generated by colcon, gitignored +└── log/ ┘ +``` + + + Every robot splits into a `_description` package containing the [URDF](https://en.wikipedia.org/wiki/URDF) and meshes. + Then a `_bringup` package with the launch file and configs. + + +## Package types + +| Package | Build type | Contains | +| --------------------- | -------------- | --------------------------------------------------- | +| `_description` | `ament_cmake` | `urdf/`, `meshes/` | +| `_bringup` | `ament_cmake` | `launch/`, `config/` (controller YAML, RViz config) | +| `sim_worlds` | `ament_cmake` | `worlds/` (SDF files), `gui/` (Gazebo GUI config) | +| `sim_common` | `ament_python` | Shared launch helpers and standalone ROS 2 nodes | + + + The difference between `ament_cmake` and `ament_python` packages is in its project managers. + `ament_cmake` uses + [CMake](https://cmake.org/cmake/help/book/mastering-cmake/chapter/Writing%20CMakeLists%20Files.html) + with the `CMakeLists.txt` file while `ament_python` uses [Setup + Tools](https://setuptools.pypa.io/en/latest/userguide/declarative_config.html) with the + `setup.cfg` file. + + +## `sim_common` + +This is a shared package containing cross-package repetitive code & also independent ROS nodes like the joint GUI. Its `setup.py` registers three console scripts: + +| Command | Source | Purpose | +| ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `move_joints` | `sim_common/move_joints.py` | Publishes a single `JointTrajectory` and exits. See [Moving Joints](./moving-joints#move_joints-node-cli). | +| `joint_gui` | `sim_common/joint_gui.py` | Tkinter joint slider GUI. See [Moving Joints](./moving-joints#joint-gui) for usage, or [Joint GUI internals](#joint-gui-internals) below for how it's implemented. | +| `drivebase` | `sim_common/drivebase.py` | Sim equivalent of the real rover's drivebase node - converts joystick deflection messages into wheel velocity commands, mirroring `urc-2023/src/drivebase`. | + +It also exports `sim_common/launch_utils.py`, a library (not a node) of helpers every `.launch.py` imports - `get_asset()` for resolving files in a package's share directory, plus node/launch-action builders for the Gazebo server, spawning, controllers, and the ROS-Gazebo bridge. See [launch_utils.py](#launch_utilspy) below for what each helper does. + +## `sim_worlds` + +Holds the environment the robot spawns into: + +- `worlds/empty.world.sdf` - the default world, passed to `gz_sim.launch.py` as `-r ` +- `gui/gui.config` and `gz_gui.xml` - the Gazebo GUI layout (camera view, panels) + +Its `share/` install path is added to `GZ_SIM_RESOURCE_PATH` at launch time so Gazebo can find world assets - see the `SIM_WORLDS_SHARE` handling in `cli/gazebo/launch.py`. + +## `robots.json` + +The repo-root `robots.json` is the CLI's registry of available robots. For each one the object includes these fields: + +| Field | Description | +| ----------------- | --------------------------------------------------------------------------------- | +| `name` | Robot name, must match the `_description` / `_bringup` package prefix | +| `url` | OnShape assembly URL the packages were generated from | +| `world_base_link` | Whether `base_link` is fixed to the world | + +New entries are added automatically by `sim gazebo create` - see [Adding a New Robot](./adding-robots). + +## Building + +`sim gazebo ` (see [Running Gazebo](./gazebo)) drives colcon for you, under the hood it uses a command like this: + +```bash +colcon build --packages-up-to _bringup _description sim_worlds sim_common \ + --cmake-args -DBUILD_TESTING=OFF +``` + +`--packages-up-to` builds only the requested robot's packages plus their dependencies (not every robot in the workspace), so switching robots doesn't force a full rebuild. By default it also deletes `build/`, `install/`, and `log/` first - equivalent to running `sim gazebo clean` - for a clean rebuild every launch; pass `--no-build` to skip building entirely and reuse the existing `install/`. + + + ```bash + cd gazebo + colcon build --packages-select arm_description + source install/setup.bash + ``` + + Useful when iterating on one package without going through the CLI. Remember to `source install/setup.bash` afterwards - ROS 2 won't find the package otherwise. See [Dev Notes](../dev-notes#building-a-single-package) for more. + + + + + All three are gitignored. Don't edit anything inside them - they're wiped on every build. If + colcon behaves strangely after unrelated changes, delete them and rebuild with `sim gazebo + clean`. + + +## Launch system + +Each robot has a launch file at `gazebo/_bringup/launch/.launch.py` that orchestrates the full simulation startup. These are the commands the sim CLI does under the hood. + +### Flags + +| Argument | Default | Description | +| -------- | ------- | -------------------------------------- | +| `rviz` | `true` | Open RViz with the robot's config file | +| `gui` | `true` | Open the Joint GUI | + +These are set via command line: + +```bash +ros2 launch arm_bringup arm.launch.py gui:=false rviz:=false +``` + +### Startup sequence + +The launch file orchestrates several components with specific ordering dependencies: + + + **Gazebo Simulator** - starts `gz_sim` with the world file and GUI config + + **Spawn Robot + Robot State Publisher** (parallel) - spawns the URDF model into Gazebo and + starts `robot_state_publisher` with the URDF + + + **Joint State Broadcaster** - starts reading joint states from Gazebo once the spawn + finishes. + + + **Joint Trajectory Controller** - accepts trajectory commands and drives joints, once the + broadcaster is ready. + + + **ROS-Gazebo Bridge, RViz, Joint GUI** (all in parallel) - bridges `/clock` between Gazebo + and ROS, opens RViz with the saved config, and opens the Joint GUI with the URDF file. + + + +The robot spawn uses a `TimerAction` with a 5-second delay to give Gazebo time to fully initialize before the model is spawned. Steps 3 and 4 use `RegisterEventHandler` with `OnProcessExit` to enforce ordering -- the trajectory controller can't start until the state broadcaster is ready, and the broadcaster can't start until the robot is spawned. + +### Components + +#### Gazebo simulation + +```python +gz_sim = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(get_package_share_directory("ros_gz_sim"), "launch", "gz_sim.launch.py") + ), + launch_arguments={ + "gz_args": " ".join(["-r", world_file, "--gui-config", gz_gui_config]) + }.items(), +) +``` + +Launches Gazebo Harmonic with: + +- `-r` -- start running immediately (not paused) +- The world SDF file from `sim_worlds` (default: `empty.world.sdf`) +- A custom GUI config from `sim_worlds/gui/gui.config` + +#### URDF processing + +```python +robot_desc = xacro.process_file( + urdf_file, + mappings={"controller_config": controller_config}, +).toxml() +``` + +The URDF is processed through xacro at launch time. The `controller_config` mapping passes the path to the controller YAML so the control xacro can reference it. The result is a fully resolved URDF string used for both spawning and state publishing. + +#### Robot spawn + +```python +spawn_robot = Node( + package="ros_gz_sim", + executable="create", + arguments=["-name", "arm", "-string", robot_desc, "-x", "0", "-y", "0", "-z", "0.1"], +) +``` + +Spawns the robot into the Gazebo world from the processed URDF string. The `-z 0.1` offset prevents the robot from spawning inside the ground plane. + +#### Robot state publisher + +```python +robot_state_publisher = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + parameters=[{"robot_description": robot_desc}], +) +``` + +Publishes the robot's TF tree and makes the URDF available on the `/robot_description` topic. RViz uses this to render the robot model. + +#### Controllers + +Two controllers are spawned in sequence: + +1. **`joint_state_broadcaster`** -- reads joint states from Gazebo hardware interfaces and publishes them to `/joint_states` +2. **`joint_trajectory_controller`** -- listens for `JointTrajectory` messages on `/joint_trajectory_controller/joint_trajectory` and commands Gazebo to move the joints + +The controller configuration lives in `config/.controller.yaml`: + +```yaml +controller_manager: + ros__parameters: + use_sim_time: true + update_rate: 60 # Hz + +joint_trajectory_controller: + ros__parameters: + joints: + - shoulder_1 + - elbow_1 + - wrist_1 + - wrist_2 + command_interfaces: + - position + state_interfaces: + - position + - velocity +``` + +#### ROS-Gazebo bridge + +```python +bridge = Node( + package="ros_gz_bridge", + executable="parameter_bridge", + arguments=["/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"], +) +``` + +Bridges the `/clock` topic from Gazebo to ROS 2. This is essential for `use_sim_time: true` -- without it, ROS nodes won't have synchronized time with the simulation. + +#### RViz + +Launches RViz2 with a saved config file. Conditional on the `rviz` launch argument. + +#### Joint GUI + +Launches the Tkinter joint control GUI with the URDF file path as an argument. Conditional on the `gui` launch argument. See [Moving Joints](./moving-joints#joint-gui) for how to use it, or [Joint GUI internals](#joint-gui-internals) below for how it's implemented. + +### launch_utils.py + +The `sim_common` package provides a `get_asset()` helper used throughout launch files: + +```python +from sim_common.launch_utils import get_asset + +controller_config = get_asset("arm_bringup", "config", "arm.controller.yaml") +``` + +It resolves a file path inside a ROS 2 package's share directory and exits with an error if the file doesn't exist. This catches missing files early rather than failing mid-launch. + + + + If you need to customize a robot's launch beyond what `sim gazebo create` generates, edit `gazebo/_bringup/launch/.launch.py`. The file is standard ROS 2 launch Python - you can add nodes, change parameters, or modify the startup sequence. + + Common customizations: + + - Adding sensor bridges (cameras, lidar) + - Changing spawn position + - Adding additional ROS 2 nodes + - Modifying controller parameters + + diff --git a/docs/gazebo/gazebo.mdx b/docs/gazebo/gazebo.mdx index 9496e2e..fcf9fde 100644 --- a/docs/gazebo/gazebo.mdx +++ b/docs/gazebo/gazebo.mdx @@ -3,50 +3,52 @@ title: Running Gazebo description: How to build and launch robot simulations with the Gazebo sim CLI. --- -Gazebo simulations are launched through the `sim gazebo` CLI. It auto-detects whether you are running natively (pixi) or inside the Dev Container and applies the right setup automatically. To launch the sim use: +Gazebo simulations are launched through the `sim gazebo` CLI: ```bash sim gazebo ``` -Of course, if you are using pixi, it is going to be: +With **pixi**, prefix it all of the `sim ...` commands here with `pixi run` -```bash -pixi run sim gazebo -``` - -## Options - -| Option | Description | -| -------------- | --------------------------------------------------------------------------------------------------- | -| `--no-build` | Skip the `colcon build` step. Use this when you haven't changed any code and want a faster startup. | -| `--build-only` | Build the workspace but don't launch the simulation. Useful for checking if your changes compile. | - -```bash -# Skip building (already built) -pixi run sim gazebo arm --no-build +## Flags -# Only build, don't launch -pixi run sim gazebo arm --build-only -``` +| Option | Description | +| -------------- | ---------------------------------------------------- | +| `--no-build` | Skip the `colcon build` step. | +| `--build-only` | Build the workspace but don't launch the simulation. | ## Cleaning the workspace To remove build artifacts and do a clean rebuild: -```bash title="Terminal" -pixi run sim gazebo clean +```bash +sim gazebo clean ``` This deletes `gazebo/build/`, `gazebo/install/`, and `gazebo/log/`. Use this when you encounter unexplained build failures. ## Troubleshooting -**Build fails with cryptic errors:** -Run `sim gazebo clean` to delete stale build artifacts, then try again. Stale artifacts are the most common cause of unexplained build failures. - -**Gazebo window doesn't appear (Dev Container):** -Connect via your VNC viewer at `localhost:5900` and verify it works with `xeyes`. If the display isn't running, restart it with `.devcontainer/x_server.sh`. - -**Package not found errors after launch:** -Try running `sim gazebo clean` and building again. + + + Run `sim gazebo clean` to delete stale build artifacts, then try again. Stale artifacts + are the most common cause of unexplained build failures. + + + Connect via your VNC viewer at `localhost:5900` and verify it works with `xeyes`. If the + display isn't running, restart it with `.devcontainer/x_server.sh`. + + + The `VirtualGL` path that gives OGRE2 a working GL context isn't active. Check for a + warning from `sim gazebo` at startup, then see [Docker setup](../../setup/docker/) for + how to verify and fix it. + + + Try running `sim gazebo clean` and building again. + + + `sim gazebo ` takes a robot name (`arm`, `chassis` ...) from configured robots in `robots.json`, + not a subcommand. Check the `robots.json` file for list of available robots or [create a new one](./adding-robots.mdx) + + diff --git a/docs/gazebo/joint-gui.mdx b/docs/gazebo/joint-gui.mdx deleted file mode 100644 index 8331798..0000000 --- a/docs/gazebo/joint-gui.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Joint GUI -description: Reference for the Tkinter-based joint control GUI that launches with the simulation. ---- - -The Joint GUI is a Tkinter application that provides interactive slider-based control of robot joints during simulation. It launches automatically as part of the ROS 2 launch file. - -## How it works - -### Startup sequence - -``` -1. Parse the URDF for all revolute joints - → Extract joint names, lower/upper limits - -2. Subscribe to /joint_states - → Wait for first message to discover active joints - → Record origin angles for each joint - -3. Build the GUI - → Create sliders with limits from the URDF - → Auto-discover available JointTrajectory topics - -4. Run two threads - → Main thread: Tkinter event loop (UI updates) - → Background thread: ROS 2 spin (message callbacks) -``` - -### Joint discovery - -The GUI doesn't hardcode joint names. Instead: - -1. It parses the robot's URDF file (passed as an argument) for all joints of type `revolute` -2. It subscribes to `/joint_states` and waits for the first message to arrive -3. The joints reported in that message become the active joints shown in the GUI - -This means the GUI works with any robot -- it adapts to whatever joints exist. - -### Origin angles - -When joints are first discovered, their current positions (from `/joint_states`) are saved as "origin angles." Slider values are offsets from these origins. When you hit **Send**, the published position is `slider_value + origin_angle`. - -This matters because joints may not start at position 0 -- the origin angle ensures the slider's zero position matches the robot's natural resting state. - -## Controls - -| Control | Description | -| --- | --- | -| **Topic dropdown** | Select which `JointTrajectory` topic to publish on. Auto-populated from available topics. | -| **Refresh** | Re-scan for available trajectory topics | -| **Joint sliders** | One per joint. Range is set by the URDF's `` values. | -| **Duration** | Time in seconds for the trajectory to complete (default: 2.0s) | -| **Send** | Publish current slider positions as a trajectory command | -| **Sync** | Read current positions from `/joint_states` and update sliders to match | -| **Reset** | Zero all sliders (back to origin position) | - -## ROS 2 interface - -**Subscribes to:** -- `/joint_states` (`sensor_msgs/msg/JointState`) -- reads current joint positions - -**Publishes to:** -- Configurable trajectory topic (default: `/joint_trajectory_controller/joint_trajectory`) -- Message type: `trajectory_msgs/msg/JointTrajectory` - -## Running standalone - -The GUI is registered as a console script entry point. You can run it outside of the launch file: - -```bash title="Inside devcontainer" -ros2 run sim_common joint_gui -``` - -For example: - -```bash title="Inside devcontainer" -ros2 run sim_common joint_gui \ - $(ros2 pkg prefix arm_description)/share/arm_description/urdf/arm.urdf -``` - -The simulation must already be running with controllers active for the GUI to discover joints. - -## Source - -The implementation is in `gazebo/sim_common/sim_common/joint_gui.py`. Key classes: - -- **`JointPublisher`** (ROS 2 Node) -- handles URDF parsing, joint state subscription, topic discovery, and trajectory publishing -- **`JointGui`** (Tkinter) -- builds the UI, manages sliders, and dispatches commands to `JointPublisher` diff --git a/docs/gazebo/launch-system.mdx b/docs/gazebo/launch-system.mdx deleted file mode 100644 index aeca8fa..0000000 --- a/docs/gazebo/launch-system.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: Launch System -description: How the ROS 2 launch files orchestrate Gazebo, controllers, RViz, and the Joint GUI. ---- - -Each robot has a launch file at `gazebo/_bringup/launch/.launch.py` that orchestrates the full simulation startup. This page explains the architecture using the arm as an example. - -## Launch arguments - -| Argument | Default | Description | -| --- | --- | --- | -| `rviz` | `true` | Open RViz with the robot's config file | -| `gui` | `true` | Open the Joint GUI | - -These are set via command line: - -```bash title="Terminal" -ros2 launch arm_bringup arm.launch.py gui:=false rviz:=false -``` - -## Startup sequence - -The launch file orchestrates several components with specific ordering dependencies: - -``` -1. Gazebo Simulator - Start gz_sim with the world file and GUI config - ↓ -2. Spawn Robot + Robot State Publisher (parallel) - - Spawn the URDF model into Gazebo - - Start robot_state_publisher with the URDF - ↓ (waits for spawn to finish) -3. Joint State Broadcaster - Starts reading joint states from Gazebo - ↓ (waits for broadcaster to finish) -4. Joint Trajectory Controller - Accepts trajectory commands and drives joints - -5. ROS-Gazebo Bridge, RViz, Joint GUI (parallel) - - Bridge /clock between Gazebo and ROS - - Open RViz with saved config - - Open Joint GUI with URDF file -``` - -The robot spawn uses a `TimerAction` with a 5-second delay to give Gazebo time to fully initialize before the model is spawned. Steps 3 and 4 use `RegisterEventHandler` with `OnProcessExit` to enforce ordering -- the trajectory controller can't start until the state broadcaster is ready, and the broadcaster can't start until the robot is spawned. - -## Components - -### Gazebo simulation - -```python -gz_sim = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(get_package_share_directory("ros_gz_sim"), "launch", "gz_sim.launch.py") - ), - launch_arguments={ - "gz_args": " ".join(["-r", world_file, "--gui-config", gz_gui_config]) - }.items(), -) -``` - -Launches Gazebo Harmonic with: -- `-r` -- start running immediately (not paused) -- The world SDF file from `sim_worlds` (default: `empty.world.sdf`) -- A custom GUI config from `sim_worlds/gui/gui.config` - -### URDF processing - -```python -robot_desc = xacro.process_file( - urdf_file, - mappings={"controller_config": controller_config}, -).toxml() -``` - -The URDF is processed through xacro at launch time. The `controller_config` mapping passes the path to the controller YAML so the control xacro can reference it. The result is a fully resolved URDF string used for both spawning and state publishing. - -### Robot spawn - -```python -spawn_robot = Node( - package="ros_gz_sim", - executable="create", - arguments=["-name", "arm", "-string", robot_desc, "-x", "0", "-y", "0", "-z", "0.1"], -) -``` - -Spawns the robot into the Gazebo world from the processed URDF string. The `-z 0.1` offset prevents the robot from spawning inside the ground plane. - -### Robot state publisher - -```python -robot_state_publisher = Node( - package="robot_state_publisher", - executable="robot_state_publisher", - parameters=[{"robot_description": robot_desc}], -) -``` - -Publishes the robot's TF tree and makes the URDF available on the `/robot_description` topic. RViz uses this to render the robot model. - -### Controllers - -Two controllers are spawned in sequence: - -1. **`joint_state_broadcaster`** -- reads joint states from Gazebo hardware interfaces and publishes them to `/joint_states` -2. **`joint_trajectory_controller`** -- listens for `JointTrajectory` messages on `/joint_trajectory_controller/joint_trajectory` and commands Gazebo to move the joints - -The controller configuration lives in `config/.controller.yaml`: - -```yaml -controller_manager: - ros__parameters: - use_sim_time: true - update_rate: 60 # Hz - -joint_trajectory_controller: - ros__parameters: - joints: - - shoulder_1 - - elbow_1 - - wrist_1 - - wrist_2 - command_interfaces: - - position - state_interfaces: - - position - - velocity -``` - -### ROS-Gazebo bridge - -```python -bridge = Node( - package="ros_gz_bridge", - executable="parameter_bridge", - arguments=["/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"], -) -``` - -Bridges the `/clock` topic from Gazebo to ROS 2. This is essential for `use_sim_time: true` -- without it, ROS nodes won't have synchronized time with the simulation. - -### RViz - -Launches RViz2 with a saved config file. Conditional on the `rviz` launch argument. - -### Joint GUI - -Launches the Tkinter joint control GUI with the URDF file path as an argument. Conditional on the `gui` launch argument. - -## launch_utils.py - -The `sim_common` package provides a `get_asset()` helper used throughout launch files: - -```python -from sim_common.launch_utils import get_asset - -controller_config = get_asset("arm_bringup", "config", "arm.controller.yaml") -``` - -It resolves a file path inside a ROS 2 package's share directory and exits with an error if the file doesn't exist. This catches missing files early rather than failing mid-launch. - -## Writing a custom launch file - -If you need to customize a robot's launch beyond what `sim gazebo create` generates: - -1. Edit `gazebo/_bringup/launch/.launch.py` -2. The file is standard ROS 2 launch Python -- you can add nodes, change parameters, or modify the startup sequence -3. `sim gazebo update` will **not** overwrite your launch file changes - -Common customizations: -- Adding sensor bridges (cameras, lidar) -- Changing spawn position -- Adding additional ROS 2 nodes -- Modifying controller parameters diff --git a/docs/gazebo/moving-joints.mdx b/docs/gazebo/moving-joints.mdx index 454a3ef..62d8e29 100644 --- a/docs/gazebo/moving-joints.mdx +++ b/docs/gazebo/moving-joints.mdx @@ -1,13 +1,11 @@ --- title: Moving Joints -description: Control robot joints using the Joint GUI or the move_joints CLI node. +description: Control robot joints using the GUI or CLI --- -There are two ways to move joints in the simulation: the **Joint GUI** (graphical, interactive) and the **`move_joints` CLI node** (scriptable, one-shot commands). +There are two ways to move joints in the simulation: the **Joint GUI** and the **`move_joints` CLI node**. Both publish `JointTrajectory` messages to the `joint_trajectory_controller`. -Both publish `JointTrajectory` messages to the `joint_trajectory_controller`. - -## Joint GUI (interactive) +## Joint GUI The Joint GUI launches automatically with the simulation. It provides sliders for each joint discovered from the robot's URDF. @@ -25,65 +23,69 @@ You can also: - Select which **Topic** to publish on (auto-discovered from available `JointTrajectory` topics) - Click **Refresh** to re-scan for topics -### How it works + -1. On launch, the GUI parses the URDF file for all `revolute` joints, reading their `lower` and `upper` limits -2. It subscribes to `/joint_states` to discover which joints are actually active and their starting positions -3. When you hit **Send**, it publishes a `JointTrajectory` message to the selected topic with positions offset by each joint's origin angle + 1. On launch, the GUI parses the URDF file for all `revolute` joints, reading their `lower` and `upper` limits + 2. It subscribes to `/joint_states` to discover which joints are actually active and their starting positions + 3. When you hit **Send**, it publishes a `JointTrajectory` message to the selected topic with positions offset by each joint's origin angle - - If the GUI shows "Discovering joints from /joint_states..." for a long time, the joint controllers may not have started yet. Wait for Gazebo to fully load. -## move_joints node (CLI) +## `move_joints` node (CLI) -The `move_joints` node sends a single trajectory command and exits. It's useful for scripting or quick tests. +The `move_joints` node sends a single trajectory command and exits. It's useful for deterministic move sequences and scripting. ### Usage -```bash title="Terminal" +```bash ros2 run sim_common move_joints --ros-args \ - -p joints:="['shoulder_1', 'elbow_1', 'wrist_1', 'wrist_2']" \ - -p positions:="[0.5, 0.5, 0.2, 0.0]" \ + -p joints:="['joint1', 'joint2', 'joint3', 'joint4']" \ + -p positions:="[0.1, 0.5, 0.2, 0.0]" \ -p duration:=2.0 ``` ### Parameters -| Parameter | Required | Default | Description | -| ----------- | -------- | --------- | ----------------------------------------- | -| `joints` | Yes | -- | List of joint names to move | -| `positions` | Yes | -- | Target positions (radians), one per joint | -| `duration` | No | `2.0` | Seconds to reach the target | -| `topic` | No | See below | Trajectory topic to publish on | - -Default topic: `/joint_trajectory_controller/joint_trajectory` - -### Behavior +| Parameter | Required | Default | Description | +| ----------- | -------- | ----------------------------------------------- | ----------------------------------------- | +| `joints` | Yes | -- | List of joint names to move | +| `positions` | Yes | -- | Target positions (radians), one per joint | +| `duration` | No | `2.0` | Seconds to reach the target | +| `topic` | No | `/joint_trajectory_controller/joint_trajectory` | Trajectory topic to publish on | -The node: + 1. Creates a publisher on the trajectory topic -2. Waits (polling every 0.5s) until at least one subscriber is connected +2. Waits until at least one subscriber is connected 3. Publishes the trajectory message 4. Logs the command and exits This means the joint trajectory controller must be running before `move_joints` can send its command. If you run it right after launching the simulation, it will wait automatically until the controller is ready. -### Example: move two joints + -```bash title="Terminal" -ros2 run sim_common move_joints --ros-args \ - -p joints:="['shoulder_1', 'elbow_1']" \ - -p positions:="[1.0, -0.5]" \ - -p duration:=3.0 -``` +## Examples -### Example: move a single joint slowly + + -```bash title="Terminal" -ros2 run sim_common move_joints --ros-args \ - -p joints:="['wrist_1']" \ - -p positions:="[0.8]" \ - -p duration:=5.0 -``` + ```bash + ros2 run sim_common move_joints --ros-args \ + -p joints:="['shoulder_1', 'elbow_1']" \ + -p positions:="[1.0, -0.5]" \ + -p duration:=3.0 + ``` + + + + + ```bash + ros2 run sim_common move_joints --ros-args \ + -p joints:="['wrist_1']" \ + -p positions:="[0.8]" \ + -p duration:=5.0 + ``` + + + + diff --git a/docs/gazebo/robot-packages.mdx b/docs/gazebo/robot-packages.mdx deleted file mode 100644 index 0526bd6..0000000 --- a/docs/gazebo/robot-packages.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Robot Packages -description: What sim gazebo create generates, how URDF post-processing works, and how the robot registry is structured. ---- - -Reference for `sim gazebo create` and `sim gazebo update` - what they produce and how the underlying pipeline works. - -## Generated package layout - -`sim gazebo create` writes two packages into `gazebo/`: - -``` -_description/ - urdf/.urdf ← Post-processed geometry URDF - urdf/_control.urdf.xacro ← ros2_control hardware interface - meshes/ ← Decimated STL files - CMakeLists.txt - package.xml - -_bringup/ - launch/.launch.py ← Launch orchestration - config/.controller.yaml ← Joint controller definitions - config/.rviz ← RViz config - CMakeLists.txt - package.xml -``` - -The `_description` / `_bringup` split is a ROS convention: description holds the robot model (changes when CAD changes), bringup holds runtime config (things you tune during development). `sim gazebo update` can safely replace the description without touching bringup. - -## URDF post-processing - -`sim gazebo create` downloads a raw URDF from OnShape via `onshape-to-robot`, then runs it through several transforms before writing the final files: - -1. **Xacro namespace** - adds `xmlns:xacro` to the `` tag so the file can use xacro macros -2. **Mesh path rewriting** - rewrites `filename="package://assets/foo.stl"` to `filename="${mesh_path}/foo.stl"` so paths resolve correctly in ROS -3. **World base link** - if `--attach-to-world`, inserts a `world` link and a fixed `world_to_base_link` joint before the first `` -4. **Joint extraction** - finds all `revolute` joints with their limits -5. **Control xacro generation** - produces `_control.urdf.xacro` with a `ros2_control` block containing `position` command interfaces and `position`/`velocity`/`effort` state interfaces for every joint -6. **Control include injection** - appends `` for the control xacro at the end of the geometry URDF -7. **Reindent** - normalises indentation from 2-space (OnShape default) to 4-space - -## What `sim gazebo update` touches - -Only geometry - everything authored by hand is preserved: - -| File | `sim gazebo update` | -| ----------------------------------------------------- | ---------------------------------- | -| `_description/urdf/.urdf` | Replaced | -| `_description/meshes/` | Replaced (old files deleted first) | -| `_description/urdf/_control.urdf.xacro` | **Untouched** | -| `_bringup/` (all files) | **Untouched** | - -## robots.json - -`robots.json` at the repo root is the robot registry. `sim gazebo create` writes to it; `sim gazebo update` reads from it to find the OnShape URL. - -```json -[ - { - "name": "arm", - "url": "https://cad.onshape.com/documents/...", - "world_base_link": true - } -] -``` - -## Debugging raw OnShape output - -If the generated packages look wrong, inspect what OnShape actually produced before post-processing: - -```bash title="Inside devcontainer" -sim gazebo create --raw -``` - -This downloads the raw URDF and assets into `cli/gazebo/create/tests//` (gitignored) without running any post-processing. Inspect `robot.urdf` there to see what OnShape produced. - -Once you've identified a fix, re-run the full generation on those local files without hitting OnShape again: - -```bash title="Inside devcontainer" -sim gazebo create --local -``` - -## Code structure - -| File | Responsibility | -| ----------------------------------- | -------------------------------------------------------- | -| `cli/gazebo/create/__init__.py` | `create()` / `update()` entry points, credential loading | -| `cli/gazebo/create/commands.py` | `cmd_create`, `cmd_update`, `cmd_local`, `cmd_raw` | -| `cli/gazebo/create/onshape.py` | URL parsing, `onshape-to-robot` invocation | -| `cli/gazebo/create/urdf.py` | All URDF transforms (steps 1–7 above) | -| `cli/gazebo/create/ros_packages.py` | File scaffolding from templates | -| `cli/gazebo/create/template.py` | `__ROBOT__` token replacement | -| `cli/gazebo/create/reduce_stl.py` | STL decimation via `open3d` | -| `cli/gazebo/create/registry.py` | `robots.json` read/write | -| `cli/gazebo/create/templates/` | Template files for generated packages | -| `cli/auth.py` | Dashboard API key management (`sim gazebo auth`) | diff --git a/docs/gazebo/ros-workspace.mdx b/docs/gazebo/ros-workspace.mdx deleted file mode 100644 index cf72d15..0000000 --- a/docs/gazebo/ros-workspace.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: ROS Workspace -description: Overview of the ROS 2 package structure, build system, and conventions used in this project. ---- - -## Package structure - -All ROS 2 packages live in the `gazebo/` directory. Every directory containing a `package.xml` is a package. - -### Robot packages - -Each robot has two packages, generated by [sim gazebo create](../../guides/adding-robots/): - -**`_description`** -- the robot model -- `urdf/.urdf` -- geometry URDF (positions, links, joints, mesh references) -- `urdf/_control.urdf.xacro` -- `ros2_control` hardware interface block (joints, command/state interfaces) -- `meshes/` -- STL mesh files from OnShape -- Built with `ament_cmake` -- `CMakeLists.txt` installs URDFs and meshes to the share directory - -**`_bringup`** -- launch configuration -- `launch/.launch.py` -- orchestrates the full simulation startup -- `config/.controller.yaml` -- controller manager config (joints, update rate, controller types) -- `config/.rviz` -- RViz visualization layout -- Built with `ament_cmake` - -### Shared packages - -**`sim_common`** -- shared Python utilities -- `launch_utils.py` -- helper functions for launch files (`get_asset()` resolves paths in package share directories) -- `move_joints.py` -- ROS 2 node for sending one-shot joint trajectory commands -- `joint_gui.py` -- Tkinter GUI for interactive joint control -- Built with `ament_python` (`setup.py` + `setup.cfg`) -- Registers `move_joints` and `joint_gui` as console script entry points - -**`sim_worlds`** -- Gazebo world files -- `worlds/empty.world.sdf` -- default world with physics, lighting, and a ground plane -- `gui/gui.config` -- Gazebo GUI plugin layout -- Built with `ament_cmake` - -## Building - -The `sim` CLI handles building automatically. To build manually: - -```bash title="Inside devcontainer" -cd gazebo -colcon build --packages-up-to arm_bringup arm_description sim_worlds sim_common \ - --cmake-args -DBUILD_TESTING=OFF -source install/setup.bash -``` - -`--packages-up-to` ensures all dependencies are resolved and built in the correct order. The `install/setup.bash` overlay lets ROS 2 find the built packages. - -### Build outputs - -After building, `gazebo/` contains three generated directories: - -| Directory | Contents | -| --- | --- | -| `build/` | Intermediate build artifacts | -| `install/` | Installed packages (what ROS 2 uses at runtime) | -| `log/` | Build logs | - -## Adding or modifying packages - -When you add or change a package: - -1. **`package.xml`** -- declares the package name, version, description, and dependencies. `colcon` uses the ``, ``, and `` tags to determine build order. - -2. **`CMakeLists.txt`** (CMake packages) -- defines what gets installed to the share directory. Typically installs URDFs, meshes, configs, and launch files. - -3. **`setup.py`** (Python packages) -- lists Python modules and console script entry points. - - - If colcon can't find a new package, make sure `package.xml` exists and the package name matches the directory name. - - -## System flowchart - -Here is a flowchart showing how the arm simulation works end-to-end: - -![Arm system flowchart](../assets/arm-flowchart.excalidraw.png) - -## Troubleshooting - -**Unexplained build errors:** Run `sim gazebo clean` and rebuild. This fixes the majority of mysterious failures. - -**Package not found at runtime:** Make sure you sourced the workspace (`source install/setup.bash`). Opening a new terminal in VS Code doesn't automatically source it. - -**Dependency order issues:** Check that all `` tags in `package.xml` are correct. Missing dependencies cause packages to build before their dependencies are ready. diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx deleted file mode 100644 index e6203aa..0000000 --- a/docs/getting-started.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Simulations -description: Clone the repository and pick your simulation environment. ---- - -This repository contains robot simulations for TrickFire Robotics. It provides two complementary environments: [**Gazebo Harmonic**](https://gazebosim.org/docs/harmonic/install/) (with [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/index.html)) for simulating robot subsystems and joint control, and [**Project Chrono**](https://projectchrono.org/) for high-fidelity terrain and wheel-soil physics. Both are driven by a unified Python `sim` CLI that handles building and launching. - -## Clone the repository - -```bash -git clone https://github.com/TrickfireRobotics/simulations.git -cd simulations -``` - -## Choose your workflow - -| Environment | Platforms | Pros & Cons | Requirements | -| --------------------------------------- | --------------------- | -------------------------------- | ----------------------------------------------------------------------------- | -| [Native (pixi)](../setup/pixi/) | Linux, macOS, WSL2 | Performant, Gazebo sim only | [pixi](https://pixi.prefix.dev/latest/) | -| [Dev Container](../setup/devcontainer/) | Any OS with Docker | Consistent, everything will work | [Docker](https://www.docker.com/) & [Devcontainers](https://containers.dev/) | -| [Docker + Nvidia GPU](../setup/nvidia/) | Linux with Nvidia GPU | Docker but GPU-accelerated | [Linux + Docker + Nvidia GPU](https://docs.docker.com/engine/containers/gpu/) | diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..1c50223 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,20 @@ +--- +title: Simulations +description: Simulation systems for TrickFire Robotics +--- + +This repository contains robot simulations for TrickFire Robotics. It provides two complementary environments: [**Gazebo Harmonic**](https://gazebosim.org/docs/harmonic/install/) (with [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/index.html)) for simulating robot subsystems and joint control, and [**Project Chrono**](https://projectchrono.org/) for high-fidelity terrain and wheel-soil physics. Both are driven by a unified Python `sim` CLI that handles building and launching. + +## Clone the repository + +```bash +git clone https://github.com/TrickfireRobotics/simulations.git +cd simulations +``` + +## Choose your workflow + +| Environment | Platforms | Notes | Requirements | +| -------------------------------------------- | ------------------ | ----------------------------------- | ---------------------------------------------------------------------------- | +| [Docker & Devcontainers](./setup/docker.mdx) | Any OS with Docker | Slower first build; Gazebo + Chrono | [Docker](https://www.docker.com/) & [Devcontainers](https://containers.dev/) | +| [Pixi Native](./setup/pixi.mdx) | Linux, macOS, WSL2 | Fastest; Gazebo only, no Chrono | [pixi](https://pixi.prefix.dev/latest/) | diff --git a/docs/reference/dev-notes.mdx b/docs/reference/dev-notes.mdx deleted file mode 100644 index 533f8c9..0000000 --- a/docs/reference/dev-notes.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Dev Notes -description: Internal notes, tips, and architectural decisions for contributors. ---- - -## Architecture decisions - -### Why pixi for the native workflow? - -ROS 2 + Gazebo + their dependencies are notoriously painful to install natively, especially across different OS versions. [pixi](https://pixi.sh) solves this by storing the entire environment (ROS 2 Jazzy, Gazebo Harmonic, all dependencies) inside the repo's `.pixi/` directory via conda. Deleting the repo also deletes the environment - your system stays completely clean. The same `pixi install` works on Linux, macOS, and WSL2 without any OS-level ROS installation. - -### Why a Dev Container (alternative)? - -The Docker Dev Container is an alternative for users who prefer a containerized environment or already have Docker set up. It provides the same ROS 2 Jazzy + Gazebo Harmonic environment inside a container. The Dev Container is useful for Windows users (via Docker Desktop), CI systems, or anyone who wants strong isolation from their host system. - -### Why Gazebo Harmonic? - -Gazebo Harmonic (gz-sim 8.x) is the current LTS release of the new-generation Gazebo and pairs with ROS 2 Jazzy. It replaces the old Gazebo Fortress (ignition-fortress). The package prefix changed from `ign` to `gz` in this generation - use `gz topic`, `gz service`, etc. - -### Why two packages per robot? - -The `_description` / `_bringup` split is a ROS convention: -- **`_description`** contains the robot model (URDF + meshes) -- things that change when the CAD changes -- **`_bringup`** contains runtime config (launch files, controller YAML, RViz config) -- things you tweak during development - -This separation means `sim gazebo update` can safely replace the description without touching your launch customizations. - -## Useful CLI commands - -### Gazebo camera position - -To set the camera position in the Gazebo viewer: - -```bash title="Terminal" -gz service -s /gui/move_to/pose \ - --reqtype gz.msgs.GUICamera \ - --reptype gz.msgs.Boolean \ - --timeout 2000 \ - --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}" -``` - -To read the current camera position: - -```bash title="Terminal" -gz topic -e -t /gui/camera/pose -``` - -### ROS 2 inspection - -```bash title="Terminal" -# List all topics -ros2 topic list - -# See joint states in real time -ros2 topic echo /joint_states - -# List active controllers -ros2 control list_controllers - -# Check controller manager status -ros2 control list_hardware_interfaces -``` - -### Building a single package - -```bash title="Terminal" -cd gazebo -colcon build --packages-select arm_description -source install/setup.bash -``` - -## Community `apt` repository (Dev Container) - -Some ROS packages live in the `universe` repository rather than `main`. If `apt` can't find a package inside the container: - -```bash title="Inside devcontainer" -apt-get update -apt-get install -y software-properties-common -add-apt-repository -y universe -apt-get update -``` - - - The Dockerfile already enables `universe` for the packages that need it. - - -## Common pitfalls - -**Forgetting to source after build:** ROS 2 can't find packages until you run `source gazebo/install/setup.bash`. The launch script does this automatically, but if you're running ROS commands manually, you need to source first. - -**Stale build artifacts:** If something breaks for no obvious reason, run `sim gazebo clean` and rebuild. Colcon's incremental builds can get confused after certain types of changes. - -**X server not running (Dev Container only):** Gazebo and RViz will fail silently or crash if the X server isn't started. The Dev Container starts it automatically, but if you need to restart it manually, run `.devcontainer/x_server.sh`. - -**Port conflicts (Dev Container only):** If port 6080 or 5900 is already in use, the X server script will fail. Make sure no other VNC sessions or containers are using those ports. diff --git a/docs/reference/docker-environment.mdx b/docs/reference/docker-environment.mdx deleted file mode 100644 index dc234dd..0000000 --- a/docs/reference/docker-environment.mdx +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: Docker Environment -description: How the container is configured, the Dockerfile sections, standalone docker-compose usage, and how the headless display works. ---- - -The Docker container is the alternative to the [native pixi workflow](../setup/macos/). There are two ways to use it: - -1. **Dev Container** (VS Code) -- Open the repo in VS Code and choose **Reopen in Container**. -2. **Standalone container** (docker-compose) -- Run simulations on any host without VS Code. NVIDIA GPU passthrough is enabled automatically when hardware is detected. - -Both methods use the same `sim` image built from `docker/Dockerfile`. - -## Dockerfile - -The Dockerfile at `docker/Dockerfile` produces a single stage named `sim` built on Ubuntu 24.04. It is divided into labelled sections: - -``` -ubuntu:24.04 AS sim - ├── BASE - locale, user setup, libdrm stub - ├── VSG - VulkanSceneGraph stack (built from source, /opt/vsg) - ├── CHRONO - Project Chrono (built from source, /home/trickfire/chrono) - ├── GAZEBO - ROS 2 Jazzy + Gazebo Harmonic - ├── VNC/DISPLAY - headless GUI stack - └── DEV TOOLING - SSH server, ruff, pre-commit, shfmt -``` - -A `BUILD_JOBS` build arg controls parallelism for the C++ builds (defaults to `nproc`). - -### VSG section - -VulkanSceneGraph and its dependencies are built from source and installed to `/opt/vsg`: - -| Library | Version | Purpose | -| -------------------------- | -------- | -------------------------------------------- | -| glslang | 16.1.0 | GLSL shader compiler (runtime library) | -| KTX-Software | v4.4.2 | Texture format support | -| draco | 1.5.7 | Mesh compression | -| assimp | v6.0.5 | 3D asset import | -| VulkanSceneGraph (vsg) | v1.1.15 | Vulkan-based scene graph rendering | -| vsgXchange | v1.1.12 | Asset format bridge for vsg (uses assimp) | -| vsgImGui | v0.7.0 | ImGui integration for vsg overlays | - -`LD_LIBRARY_PATH` is set to `/opt/vsg/lib` so the shared libraries are found at runtime. - -### Chrono section - -Project Chrono is cloned from source and built with the Vehicle and VSG modules enabled: - -``` -CH_ENABLE_MODULE_VEHICLE - wheel/terrain dynamics -CH_ENABLE_MODULE_VSG - Vulkan-based visualizer -``` - -The build target is `demo_VEH_SCMTerrain_RigidTire`. An upstream bug in `SCMTerrain.cpp` (domain list not cleared before `AddActiveDomain`) is patched at image build time. - -The Chrono source and build live at `/home/trickfire/chrono` and are owned by the `trickfire` user. - -### Gazebo section - -**Simulation stack:** - -| Package | Purpose | -| -------------------------------------- | ----------------------------------------------- | -| Gazebo Harmonic | Physics simulation engine | -| `ros-jazzy-ros-gz` | ROS 2 / Gazebo integration + bridge | -| `ros-jazzy-ros2-controllers` | Joint state broadcaster + trajectory controller | -| `ros-jazzy-gz-ros2-control` | Hardware interface for Gazebo | -| `ros-jazzy-rviz2` | Robot visualization | -| `ros-jazzy-xacro` | URDF macro processing | -| `ros-jazzy-joint-state-publisher-gui` | Joint state publishing GUI | - -**Python packages:** `pypresence`, `trimesh`, `pyfqmr` - -### VNC / display section - -**Display stack (headless GUI):** - -| Package | Purpose | -| ------------------------------------------------ | --------------------------------------------- | -| `xserver-xorg-core` + `xserver-xorg-video-dummy` | Virtual X server with dummy driver (no GPU) | -| `openbox` | Lightweight window manager | -| `x11vnc` | VNC server for remote X11 access | -| `novnc` + `websockify` | Browser-based VNC client | -| `xvfb` | Virtual framebuffer for Jetson/Tegra (no DRI) | -| `mesa-utils`, `libgl1-mesa-dri` | Software OpenGL rendering | -| `kmod` | Kernel module tools (for `/lib/modules` mount)| -| `x11-apps` | X11 utilities including `xeyes` | - -### Dev tooling section - -| Package/tool | Purpose | -| -------------- | -------------------------------------------------------- | -| `openssh-server` | SSH server (used by VS Code remote connection) | -| `ruff` | Python linter/formatter | -| `pre-commit` | Git hook framework | -| `shfmt` | Shell script formatter | - -## Container user - -The container runs as a non-root user `trickfire` with passwordless sudo: - -```dockerfile -RUN useradd trickfire --uid 1000 --shell /bin/bash --create-home --no-log-init -RUN echo "trickfire ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/user -``` - -## Dev Container configuration - -The `devcontainer.json` configures how VS Code opens the container: - -**Build target:** `sim` stage of `docker/Dockerfile` - -**Workspace mount:** The repo is bind-mounted into the container at `/home/trickfire/simulations`. - -**Compose config:** `.devcontainer/devcontainer.json` uses `docker/docker-compose.yml` plus `docker/docker-compose-dev.yml`. The override adds Wayland and display environment variables for Vulkan rendering. - -**Port forwarding:** Ports `6080` (noVNC) and `5900` (VNC) are forwarded to the host. - -**Privileged mode:** The container runs with `--privileged` and device access for hardware interaction (USB, CAN bus). - -**VS Code extensions:** Pre-installs Python, C++ (clangd, CMake, Makefile), ROS, URDF, Docker, Prettier, and other formatting extensions. - -## X-server Display Architecture - -``` -Xorg/Xvfb → x11vnc → websockify → Browser - ↑ ↑ - Openbox WM noVNC client - ↑ (port 6080) - Gazebo / RViz / Chrono VSG -``` - -### Why this approach? - -Alternatives like Xvfb don't support GLX properly, which means Gazebo's 3D rendering fails. Using Xorg with a dummy driver + Mesa software rendering gives us full OpenGL support without needing a real GPU. The VNC + noVNC layer makes it accessible from any browser. On Jetsons, we fall back to Xvfb because there's no `/dev/dri`, but GPU rendering still works via EGL through injected Tegra libs. - -Chrono uses Vulkan for rendering via the VSG module. Wayland socket passthrough (`WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR`) is exposed to the container so Vulkan can reach the host compositor when available. - -### Environment variables - -| Variable | Value | Set in | -| ------------------ | ---------------------- | ----------------------------------- | -| `DISPLAY` | Host-dependent | `docker-compose.yml` (standalone) | -| `WAYLAND_DISPLAY` | Host-dependent | `docker-compose-dev.yml` | -| `XDG_RUNTIME_DIR` | `/run/host-runtime` | `docker-compose-dev.yml` | -| `VNC_PORT` | `5900` | Dockerfile | -| `NOVNC_PORT` | `6080` | Dockerfile | -| `VSG_INSTALL_DIR` | `/opt/vsg` | Dockerfile | -| `LD_LIBRARY_PATH` | `/opt/vsg/lib` | Dockerfile | - -## Extending the container - -To add system packages, edit `docker/Dockerfile` and rebuild. For Python packages, add them to the `pip3 install` line in the Gazebo section (simulation-time) or the dev tooling section (development-only tools). - -After changing the Dockerfile, use **Dev Containers: Rebuild Container** in VS Code's Command Palette. diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index 6889bc4..2e4c95a 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -1,75 +1,212 @@ --- -title: Docker -description: Run the simulation inside a container. +title: Docker & Devcontainers +description: Run the simulation inside a Docker container. --- -With this approcach everything runs inside a Docker container with everything deterministically installed. The native approach is faster (except on Linux system, Docker runs with native speeds there) but you can have issues installing or running the sim using it. This project uses [devcontainers](https://containers.dev/) to build the Docker environment. You can build it either using the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or the [CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli) if you do not want to/cannot use VSCode. +Everything runs inside a Docker container with all dependencies pinned and pre-built. It's slower to start than [native (pixi)](../setup/pixi) on most systems (not Linux, Docker there works at close-to-native speeds) but works on any OS, including Chrono, without a local ROS/Gazebo install. This project uses [devcontainers](https://containers.dev/), built with the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or the [CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli). ## 1. Build the devcontainer -If you are using VSCode, open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. +Before the container starts, `initializeCommand` runs `.devcontainer/host-env.sh`, which detects your OS and GPU and writes `docker/.env`. These are all possible options: -If you are using the CLI, navigate to the folder in your terminal and then run `devcontainer up`. +| Host | Display path | +| ------------------------------------- | ------------------------------------------------------------------------------------------------- | +| **Linux Wayland/X11** & **WSL2/WSLg** | Host socket, passed straight through | +| **macOS** + **XQuartz** | Forwarded over TCP via `host.docker.internal` & rendered with VirtualGL | +| **Vanilla Windows** | Forwarded over TCP to VcXsrv & rendered with VirtualGL | +| **Linux** + **NVIDIA GPU** | Uses the [container toolkit](#nvidia-acceleration) and NVIDIA Docker runtime for faster rendering | - - This will take a long time if it is your first time (30 mins)! Docker builds Chrono and its dependencies (many lines of C++) from source, and then the same for Gazebo and ROS! Subsequent launches will be very fast though. - - -## 2. Check if display works + + + Open the repo in VS Code and accept the **Reopen in Container** prompt, or run **Dev + Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). + Requires the [Dev Containers + extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). + + -Gazebo and RViz need a display to render their GUIs. When the Dev Container starts, a script detects your host setup and picks the best display path automatically. Check the terminal you launched the container from — the log line it prints tells you which case applies to you. + ```bash + devcontainer up + ``` -### Linux host with Wayland (no VNC needed) + -If your host runs a Wayland compositor (GNOME, KDE, Hyprland, etc.), the log prints: - -``` -[X11] Using Wayland socket at /run/host-runtime/wayland-1 -``` + -The container forwards Wayland and X11 directly to your host compositor. GUI windows appear on your desktop natively — you can skip the VNC steps below. - -### WSL2 with WSLg (no VNC needed) + + Ports and flags & other configuration is in `docker/.env.defaults`. To change any, add it to + `docker/.env.local`. + -WSL2 includes WSLg, a built-in Wayland compositor with X11 forwarding. Gazebo and RViz windows appear via WSLg automatically, the same as on native Linux with Wayland above. +## 2. Check if display works -### All other cases (macOS, headless servers, Windows without WSLg) +Gazebo and RViz need a display. On container start, `.devcontainer/x_server.sh` picks the best +path for your host and prints which one it picked. Check the terminal you launched the +container from. + + + + Wayland or X11 is forwarded straight through - no VNC needed. The log prints one of: + + ``` + [X11] Using Wayland socket at /run/host-runtime/wayland-1 + [X11] Using host X11 display at :0 + ``` + + WSL2 uses the same path automatically via WSLg's built-in compositor. + + + GUI windows forward over X11 to [XQuartz](https://www.xquartz.org) instead of VNC. + + + Install XQuartz: `brew install --cask xquartz`, or from [xquartz.org](https://www.xquartz.org). + + Open **XQuartz > Settings > Security** and check **"Allow connections from + network clients"**, then fully quit and reopen XQuartz for it to take effect. + + + In a Mac terminal, run `DISPLAY=:0 xhost + 127.0.0.1` to authorize Docker + Desktop's VM. This resets every time XQuartz restarts, so re-run it after each + restart. + + + + + The log should print `[X11] Using host X11 display at host.docker.internal:0`. If it + falls back to Xvfb + noVNC instead, recheck steps 2-3, then check + `/tmp/start_x_server.log` inside the container. + + + + `XQuartz` can display `X11` windows but can't give them a working `OpenGL` context cause its + indirect `GLX` is deprecated and [unsupported + upstream](https://github.com/XQuartz/XQuartz/issues/144), so `OGRE2` (used by both + Gazebo and RViz) can't create a renderer over it. The container works around this + with [`VirtualGL`](https://virtualgl.org), where `GL` rendering happens against a headless X + server *inside* the container (Mesa `llvmpipe`), and only finished frames go to + `XQuartz` as plain `X11` images. + + + + Only needed without WSL2/WSLg, which uses the Linux path above instead. Requires [bash on + `PATH` (Git for Windows)](https://stackoverflow.com/questions/26620312/git-installing-git-in-path-with-github-client-for-windows) so the host script can run. + + + Install [VcXsrv](https://github.com/marchaesen/vcxsrv/releases/latest) + + Launch it with **network access enabled** - for VcXsrv's XLaunch: "Multiple + windows", display `0`, then check **"Disable access control"** on the Extra + Settings page. + + If Windows Defender Firewall prompts you, allow it on Private networks. + + + The log should print `[X11] Using host X11 display at host.docker.internal:0`. If it + falls back to Xvfb + noVNC, confirm the X server is running with access control disabled + and the firewall isn't blocking it. + + + `VcXsrv` can display `X11` windows but can't give them a working `OpenGL` context cause its + indirect `GLX` is deprecated, so `OGRE2` (used by both + Gazebo and RViz) can't create a renderer over it. The container works around this + with [`VirtualGL`](https://virtualgl.org), where `GL` rendering happens against a headless X + server *inside* the container (Mesa `llvmpipe`), and only finished frames go to + `XQuartz` as plain `X11` images. + + + + Used for headless servers, or when nothing above applies. `x_server.sh` starts a VNC + stack, picking a backend based on available hardware: + + ``` + [X11] Using vkms virtual display (/dev/dri/card1) + [X11] vkms unavailable, falling back to dummy driver (no DRI3) + [X11] Jetson/Tegra detected (no DRI), using Xvfb + EGL + ``` + + Once you see `[MAIN] All services started`, connect a VNC viewer + ([TigerVNC](https://tigervnc.org/), [RealVNC + Viewer](https://www.realvnc.com/en/connect/download/viewer/)) to `localhost:5900`, or + open `http://localhost:6080/vnc.html` for a quick browser check - noVNC doesn't forward + modifier keys correctly, so use a real client for regular use. + + If the script fails, it dumps `/tmp/start_x_server.log` to the terminal. + + + If you are using an NVIDIA GPU follow the [directions below](#nvidia-acceleration). If you have set up everything correctly you should see the following somewhere in the `x-server` output: + + ``` + [X11] Desktop NVIDIA GPU detected, using Xorg NVIDIA driver + ``` + + + This log line specifically is only printed in the VNC/headless fallback path, since it comes from the + container's own Xorg server picking a driver. It does **not** mean the NVIDIA runtime only matters for + VNC: `runtime: nvidia` is applied to the container in every display mode, and Vulkan/Chrono rendering + (`vkcube`, vsg-chrono) always benefits from it regardless of which display path is active. Wayland + passthrough in particular still renders client-side inside the container, so it also needs the runtime + for real GPU acceleration - only the X11 passthrough case can lean on the host's own GPU for rendering. + + + + + + +### Verify it works + +- **X11 (Gazebo/RViz):** run `xeyes` inside the container - a pair of animated eyes should + appear. +- **Vulkan (Chrono):** run `vkcube` inside the container - a spinning textured cube should + render. + +On macOS/Windows, `xeyes` only proves the X11 connection, not GL - check that separately: + +```bash title="Container" +vglrun /opt/VirtualGL/bin/glxspheres64 +``` -On hosts without a Wayland socket, the script starts a VNC stack. Depending on what GPU or virtual display driver is available, you will see one of the following: +A window of spinning spheres should appear, and the terminal should report a frame rate with +`OpenGL Renderer: llvmpipe`. `vglrun: command not found` means rebuild the container; "can't open +the 3D X server" means run `bash .devcontainer/x_server.sh`. -``` -[X11] Desktop NVIDIA GPU detected, using Xorg nvidia driver -[X11] Using vkms virtual display (/dev/dri/card1) -[X11] vkms unavailable, falling back to dummy driver (no DRI3) -[X11] Jetson/Tegra detected (no DRI), using Xvfb + EGL -``` + + `sim gazebo` prefixes `vglrun` for you automatically. Anything else you launch yourself that + uses OpenGL needs it too - `vglrun rviz2`, `vglrun gz sim`, etc. Plain X11 tools like `xeyes` + don't need it. This only applies on macOS/Windows; run normally on Linux/WSLg. + -followed by: +## 3. Launch the sim -``` -[noVNC] Desktop available at: http://localhost:6080/vnc.html -[MAIN] All services started -``` +Once the display works, head to [Running Gazebo](../gazebo/gazebo) to launch your first sim. -Once you see `[MAIN] All services started`, connect with a VNC viewer at `localhost:5900`. You should see a blank desktop. +--- - - Good VNC viewers I would reccomend are [TigerVNC](https://tigervnc.org/) or [VNCViewer](https://www.realvnc.com/en/connect/download/viewer/?lai_sr=0-4&lai_sl=l) - +# NVIDIA Acceleration - - For a quick check without a VNC client, open `http://localhost:6080/vnc.html` in your browser. Note that noVNC doesn't forward modifier keys (Alt, Super, etc.) correctly, so a native VNC viewer is better for regular use. + + See the [official install + guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + for other distros. -If the script fails, it automatically dumps `/tmp/start_x_server.log` to the terminal to help you diagnose the problem. +If you have a Linux system with a NVIDIA GPU, you can use a special Docker runtime (`nvidia-container-toolkit`) to give the container hardware access to it - this speeds up both GL rendering (Gazebo/RViz) and Vulkan (Chrono) in every display mode, not just VNC. `host-env.sh` detects it automatically and enables it for you, but the runtime has to be installed and registered with Docker first. On a Debian-based distro, run this on your host: -### Verify the display works +```bash +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ + | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg -Before launching the sim, confirm both display paths are functioning: +distribution=$(. /etc/os-release; echo $ID$VERSION_ID) +curl -fsSL https://nvidia.github.io/libnvidia-container/libnvidia-container.list \ + | sed 's|^deb |deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] |' \ + | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null -- **X11 (Gazebo / RViz):** Run `xeyes` inside the container. If a pair of animated eyes appears (in your VNC viewer or on your local desktop), X11 is working and Gazebo will render. -- **Vulkan (Chrono):** Run `vkcube` inside the container. If a spinning textured cube renders without errors, Vulkan is working and Chrono's VSG visualizer will function. +sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker +``` -## 3. Launch the sim +Verify Docker can see the GPU: -Once the display is running, head to [Running Gazebo](../../guides/gazebo/) to launch your first sim. +```bash +docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nvidia-smi +``` diff --git a/docs/setup/nvidia.mdx b/docs/setup/nvidia.mdx deleted file mode 100644 index d683386..0000000 --- a/docs/setup/nvidia.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Docker + Nvidia -description: Run the simulation with NVIDIA GPU passthrough in Docker. ---- - -This guide adds GPU passthrough to the standard Dev Container setup, giving Gazebo and Chrono direct access to your NVIDIA GPU. It works on any Linux machine with an NVIDIA GPU. - -## 1. Install NVIDIA Container Toolkit - - - The toolkit lets Docker access the NVIDIA driver on the host. The commands below are for **Ubuntu/Debian**. For other distros see the [official install guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). - - -Run this on your host machine: - -```bash -curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ - | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg - -distribution=$(. /etc/os-release; echo $ID$VERSION_ID) -curl -fsSL https://nvidia.github.io/libnvidia-container/libnvidia-container.list \ - | sed 's|^deb |deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] |' \ - | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null - -sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit -sudo nvidia-ctk runtime configure --runtime=docker -sudo systemctl restart docker -``` - -Verify the toolkit can see your GPU: - -```bash title="Terminal" -docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nvidia-smi -``` - -You should see your GPU listed in the output. - -## 2. Open the NVIDIA devcontainer - -There is a dedicated devcontainer configuration at `.devcontainer/nvidia/` that layers GPU support on top of the base setup. Use it instead of the default one. - -### VSCode - -Open the repository folder in VSCode. When prompted to **Reopen in Container**, click the prompt and select **NVIDIA** from the configuration picker. If the prompt doesn't appear, open the Command Palette (`Ctrl+Shift+P`) and run **Dev Containers: Reopen in Container**, then select **NVIDIA**. - -### CLI - -```bash title="Terminal" -devcontainer up --config .devcontainer/nvidia/devcontainer.json -``` - -## 3. Launch the simulation - -Once inside the container, follow the normal [Docker guide](../docker/) from the display check onward. The GPU is available automatically — no extra flags needed when running `sim`. - ---- - -## Jetson-specific setup - -Jetson boards ship with NVIDIA drivers pre-installed via L4T. The Container Toolkit installation above still applies. - -After installing, configure the board for best simulation performance: - -- **Power mode:** Switch to MAXN (maximum performance) in `nvpmodel` or the power mode GUI -- **Fan speed:** Set fans to full speed with `jetson_clocks --fan` or the fan control utility -- **WiFi power management:** Disable with `sudo iw dev wlan0 set power_save off` to reduce latency if using the sim over SSH diff --git a/docs/setup/pixi.mdx b/docs/setup/pixi.mdx index 42c9e75..a0704fb 100644 --- a/docs/setup/pixi.mdx +++ b/docs/setup/pixi.mdx @@ -1,14 +1,20 @@ --- -title: Native Setup +title: Pixi Native description: Run the simulation natively using pixi. --- The native workflow installs ROS 2 Jazzy and Gazebo Harmonic into a self-contained environment inside the repository using [pixi](https://pixi.sh), not requiring Docker and using your system directly without another layer, producing better performance. -> **Supported platforms:** Linux (x86-64, ARM64), macOS (Intel and Apple Silicon), WSL2. + + + 1. This way of running the project does not support the Chrono sim, **only Gazebo**. See + [Docker](../../setup/docker). + + 2. Only the following platforms are supported: + - **Linux** (x86_64, ARM64) + - **MacOS** (Intel, Silicon) + - **WSL2** - - This way of running the project does not support the Chrono sim, only Gazebo! See [Docker](../../setup/docker) ## 1. Install pixi @@ -27,18 +33,16 @@ From the repo root, install all project dependencies using the following command pixi install ``` -This downloads ~3–5 GB on first run, and takes a while. Subsequent runs are instant. +This downloads ~3-5 GB on first run, and takes a while. Subsequent runs are instant as everything is already downloaded. All of the pixi environment data is located in the `.pixi` folder in the project root. ## 3. Launch a simulation +You can now launch the sim. It is the same as you would [normally would](../gazebo/gazebo) but all the commands are always prefixed by `pixi run` to use the pixi environment like so: + ```bash -pixi run sim gazebo native arm +pixi run sim gazebo ``` -This builds the ROS 2 workspace and launches 3 windows, Gazebo, RViz, and the Joint GUI. - -See [Running Gazebo](../../guides/gazebo/) for full CLI options and flags. - - - Do not use `pixi shell` as your regular working shell. It modifies library paths in a way that can break system tools like `git` on macOS. Use `pixi run ` instead. For example: `pixi run sim gazebo native arm`. + + Do not use `pixi shell` as your regular working shell. It modifies library paths in a way that can break system tools like `git`. Use `pixi run ` instead. For example: `pixi run sim gazebo native arm`.