diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 826ee19..b08d79c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "simulations", - "dockerComposeFile": ["../docker/docker-compose.yml", "../docker/docker-compose-dev.yml"], + "dockerComposeFile": "../docker/docker-compose.yml", "service": "sim", "runServices": ["sim"], @@ -8,7 +8,7 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, - "initializeCommand": "bash .devcontainer/detect-host-env.sh || true", + "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", @@ -19,34 +19,26 @@ "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/detect-host-env.sh b/.devcontainer/host-env.sh similarity index 77% rename from .devcontainer/detect-host-env.sh rename to .devcontainer/host-env.sh index bbd1726..0fc0504 100755 --- a/.devcontainer/detect-host-env.sh +++ b/.devcontainer/host-env.sh @@ -3,13 +3,15 @@ set -eu script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" docker_dir="$(cd "$script_dir/../docker" && pwd)" -env_out="$docker_dir/.env" -host_out="$docker_dir/.env.host" + 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 @@ -18,24 +20,9 @@ gpu_available() { } gpu_runtime="" -if gpu_available; then - gpu_runtime="nvidia" -fi - -{ - cat "$defaults" - if [ -f "$overrides" ]; then - echo "" - echo "# --- user overrides (docker/.env.local) ---" - cat "$overrides" - fi - echo "" - echo "# --- host-detected (generated, do not edit) ---" - echo "SIM_GPU_RUNTIME=$gpu_runtime" -} >"$env_out" +gpu_available && gpu_runtime="nvidia" -{ - echo "# generated by .devcontainer/detect-host-env.sh — do not edit or commit" +write_display_env() { case "$uname_s" in Darwin) echo "# host: macOS (XQuartz over TCP + VirtualGL)" @@ -54,16 +41,37 @@ fi echo "DISPLAY=${DISPLAY:-:0}" ;; *) - echo "# host: unknown ($uname_s) — falling back to local 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 GPU detected (runtime: nvidia)" 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 -} >"$host_out" +} + +write_env >"$env_out" -echo "[detect-host-env] host=$uname_s gpu=${gpu_runtime:-none} -> wrote docker/.env, docker/.env.host" +echo "[INFO] host=$uname_s gpu=${gpu_runtime:-none}" diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 6eab0e2..de5d28e 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/reference/docker-environment.mdx. set -eo pipefail trap '' HUP @@ -101,7 +97,7 @@ try_display_passthrough() { 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-dev.yml. + # 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" diff --git a/.gitignore b/.gitignore index 813f0e2..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,14 +28,8 @@ chrono/results/* # env **/*.env -# docker/.env is generated each `devcontainer up` by .devcontainer/detect-host-env.sh -# (from docker/.env.defaults + docker/.env.local + host detection). docker/.env.defaults -# is the committed source of truth; docker/.env.local holds per-machine overrides. docker/.env -docker/.env.host 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 2f804f6..ce7c8a2 100644 --- a/chrono/data/vsg/imgui.ini +++ b/chrono/data/vsg/imgui.ini @@ -8,6 +8,6 @@ Size=531,121 [Window][Simulation] Pos=5,5 -Size=349,458 +Size=238,362 Collapsed=1 diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index 204aa7f..9c99913 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -117,51 +117,26 @@ def _x11_port_for(display: str) -> int: return 6000 -def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: - """Build a specific, actionable explanation for why `display` couldn't be opened. +_DOCKER_DOCS = "https://docs.trickfirerobotics.com/simulations/setup/docker" + - Runs its own DNS/TCP checks (independent of xdpyinfo) so the error points at the - layer that's actually broken, instead of a bare "cannot connect". - """ +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}", ""] - is_local = display.startswith(":") - if is_local: + if display.startswith(":"): lines += [ - "This is a local display spec - the container expected a Wayland/X11 socket to", - "already be forwarded in (native Linux host, or WSL2/WSLg).", - "", - "Checks to run inside the container:", - " 1. grep X11 /tmp/start_x_server.log", - " Look for '[X11] Using host X11 display' or '[X11] Using Wayland socket'.", - " If you see Xvfb/vkms/dummy-driver/noVNC lines instead, passthrough failed", - " at container startup and it fell back to the internal VNC stack - connect", - " a VNC viewer to localhost:5900 (or http://localhost:6080/vnc.html), or fix", - " passthrough on the host and restart the container to retry it.", - " 2. ls -la /tmp/.X11-unix/", - " Empty means the host's X11 socket wasn't bind-mounted in, or nothing is", - " listening on it on the host.", + "Expected a Wayland/X11 socket forwarded in from the hosts", ] return "\n".join(lines) host = display.split(":", 1)[0] - lines += [ - f"This is a remote display spec (host '{host}') - used by the macOS/Windows", - "devcontainer configs to forward GUI windows to XQuartz/VcXsrv over TCP.", - "", - ] 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})", - "", - " Docker Desktop provides this name automatically to containers. If it's", - " missing, Docker Desktop may not be running, or this isn't actually", - " running inside the container (check your shell prompt).", - ] + lines += [f" [FAIL] DNS: '{host}' did not resolve ({e})", "", "Is Docker Desktop running?"] return "\n".join(lines) port = _x11_port_for(display) @@ -171,37 +146,14 @@ def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: except OSError as e: lines += [ f" [FAIL] TCP: could not connect to {host}:{port} ({e})", - "", - " macOS + XQuartz:", - " - Is XQuartz actually running? (`ps aux | grep -i xquartz` on the Mac)", - " - XQuartz > Settings > Security > 'Allow connections from network", - " clients' must be checked, then XQuartz fully restarted for it to", - " take effect.", - " Windows + VcXsrv/X410:", - " - Is the X server running? For VcXsrv, XLaunch must have 'Disable", - " access control' checked.", - " - Windows Defender Firewall may be silently blocking it - check for a", - " blocked-app prompt, or allow it manually for Private networks.", + 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)'}", - "", - " DNS and TCP are both fine, so this is an X11 access-control problem, not a", - " network problem:", - "", - " macOS:", - " Run on the Mac (not in the container): `DISPLAY=:0 xhost + 127.0.0.1`", - " Do NOT use `xhost -display :0 + ...` - this is a documented xhost bug:", - " '-display' is parsed as 'remove a host named display', not a real flag.", - " xhost always connects using your shell's $DISPLAY env var instead, so set", - " it as a one-off prefix like above. This resets every time XQuartz", - " restarts, so you'll need to re-run it after any XQuartz restart.", - " Windows:", - " Relaunch VcXsrv/X410 with 'Disable access control' checked - there's no", - " separate allow-list step needed once that's set.", + f"See {_DOCKER_DOCS} for X11 authorization (xhost) setup.", ] return "\n".join(lines) @@ -223,12 +175,7 @@ def _check_display() -> None: info("Checking for display...") display = os.environ.get("DISPLAY") if not display: - die( - "DISPLAY environment variable not set\n\n" - " This is normally set by the devcontainer's compose config. If you're\n" - " seeing this, something stripped it from your shell - try a fresh\n" - " terminal/container restart, or run `env | grep DISPLAY` to confirm." - ) + die("DISPLAY not set! Try restarting the container") result = subprocess.run( ["xdpyinfo", "-display", display], @@ -242,42 +189,19 @@ def _check_display() -> None: def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: - """Route GL rendering through VirtualGL when displaying on a remote X server. - - macOS (XQuartz) and Windows (VcXsrv/X410) can display X11 windows over TCP, but can't - hand back a usable OpenGL context: their indirect GLX is deprecated and broken, so OGRE2 - - which both Gazebo and RViz use - fails at glXMakeCurrent and never creates a renderer. - - VirtualGL splits the two concerns. GL rendering runs against a container-local headless X - server (Mesa llvmpipe, OpenGL 4.5) started by .devcontainer/x_server.sh, and only the - finished frames go to the host's X server as ordinary X11 images - which it handles fine. - - Returns the command prefix to launch under, or an empty prefix if VirtualGL isn't needed - or isn't usable (in which case the launch still proceeds, just without GL acceleration). - """ + """Route GL rendering through VirtualGL when displaying on a remote X server""" display = os.environ.get("DISPLAY", "") if display.startswith(":"): - return [] # local passthrough (Linux/WSLg) - the app's GL already works directly + return [] if not shutil.which("vglrun"): - warn( - "VirtualGL (vglrun) is not installed, so Gazebo/RViz have no way to get a\n" - " working GL context on this host - expect a blank Gazebo window and\n" - " rviz2 dying with 'Unable to create the rendering window'.\n" - " \n" - " Rebuild the container to pick it up, or set FORCE_VNC=1 in docker/.env.local\n" - " and recreate the container to render over VNC instead." - ) + 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, so Gazebo/RViz can't\n" - " get a working GL context - expect rendering to fail.\n" - " \n" - " Start it with: bash .devcontainer/x_server.sh\n" - " (it normally starts automatically when the container starts)" + f"VirtualGL's 3D X server on {vgl_display} isn't running - run .devcontainer/x_server.sh" ) return [] @@ -294,7 +218,7 @@ def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: 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) - no direct GPU access") + info("FORCE_VNC: forcing software rendering (llvmpipe)") env["LIBGL_ALWAYS_SOFTWARE"] = "1" return [] @@ -333,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") @@ -363,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" @@ -393,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.defaults b/docker/.env.defaults index c056411..c7c6c2b 100644 --- a/docker/.env.defaults +++ b/docker/.env.defaults @@ -1,14 +1,6 @@ -# Committed defaults for docker exposed ports (and optional flags). -# -# This file is the source of truth. On `devcontainer up`, .devcontainer/detect-host-env.sh -# generates the real (gitignored) docker/.env from: these defaults + your optional -# docker/.env.local overrides + host-detected values (DISPLAY/GPU). To change ports or -# force VNC on YOUR machine, create docker/.env.local with the keys you want to override -# — do not edit this file (it's shared) or docker/.env (it's regenerated each up). +# do not edit! +# create "docker/.env.local" to override VNC_PORT=5900 NOVNC_PORT=6080 ROSBRIDGE_PORT=9090 - -# set to 1 in docker/.env.local if you want to force vnc startup -# FORCE_VNC=1 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.yml b/docker/docker-compose.yml index 7159f67..bdbac3e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,22 +9,32 @@ services: 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 runtime: "${SIM_GPU_RUNTIME:-}" - env_file: - - path: .env.host - required: false - 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:-}" volumes: - ..:/home/trickfire/simulations @@ -34,6 +44,23 @@ services: 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 working_dir: /home/trickfire/simulations diff --git a/docker/vsg-chrono.Dockerfile b/docker/vsg-chrono.Dockerfile index da85277..03cc76f 100644 --- a/docker/vsg-chrono.Dockerfile +++ b/docker/vsg-chrono.Dockerfile @@ -102,19 +102,14 @@ RUN git clone --depth 1 https://github.com/projectchrono/chrono.git /home/trickf # fix upstream bug: # AddActiveDomain appends without clearing the null-body default domain -# added by SetupInitial, causing a crash when OnBindAssets iterates all domains. +# added by SetupInitial, causing a crash when OnBindAssets iterates all domains RUN sed -i \ 's| m_loader->m_active_domains.push_back(ad);| if (!m_loader->m_user_domains)\n m_loader->m_active_domains.clear();\n m_loader->m_active_domains.push_back(ad);|' \ /home/trickfire/chrono/src/chrono_vehicle/terrain/SCMTerrain.cpp # fix upstream portability bug: # FindSIMD.cmake unconditionally compiles with -march=native on any recent -# GCC/Clang, baking in whatever SIMD extensions the *build* machine has -# (e.g. SVE on GitHub's arm64 runners). Since this image is built once and -# distributed to arbitrary machines, that produces SIGILL on hosts without -# the exact same ISA (e.g. Apple Silicon, which has no SVE). Skip the -# native shortcut on aarch64 so it falls through to Chrono's NEON detection, -# which resolves to the portable "-march=armv8-a" baseline instead. +# GCC/Clang, baking in whatever SIMD extensions the build machine has RUN sed -i \ -e 's|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ -e 's|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ 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..6562703 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 unexpectable 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..28f6f70 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,28 @@ 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. + + + See [Robot Packages](../reference/robot-packages/) for details on what post-processing + happens and how to inspect the raw OnShape output. + + + 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 3f505ce..fcf9fde 100644 --- a/docs/gazebo/gazebo.mdx +++ b/docs/gazebo/gazebo.mdx @@ -3,56 +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`. - -**RViz crashes with "Unable to create the rendering window", or Gazebo shows a blank window (macOS/Windows):** -OGRE2 (used by both Gazebo and RViz) needs a real GL context, which XQuartz/VcXsrv can't provide over their indirect GLX - the X11 connection works, but rendering doesn't. The container routes GL through VirtualGL to work around this, and `sim gazebo` launches under `vglrun` automatically. Seeing this error means that path isn't active. Check for a warning from `sim gazebo` at startup, then verify GL directly with `vglrun /opt/VirtualGL/bin/glxspheres64` - if `vglrun` is missing, rebuild the container; if the 3D X server isn't running, start it with `bash .devcontainer/x_server.sh`. `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback - see [Docker setup](../../setup/docker/) for details. - -**Package not found errors after launch:** -Try running `sim gazebo clean` and building again. - -**"Package '``_bringup' not found" right after launch:** -`sim gazebo ` takes a robot name (`arm`, `chassis`, etc. - see `robots.json`), not a subcommand. `sim gazebo launch` gets parsed as robot name `"launch"`, which doesn't exist. Use `sim gazebo arm` or `sim gazebo chassis` instead. + + + 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 a836aeb..11b7359 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -1,169 +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 -There is **one devcontainer config, and it auto-detects your host** — no variant to pick. Before the container starts, its `initializeCommand` runs `.devcontainer/detect-host-env.sh`, which detects your OS and GPU and generates `docker/.env` + `docker/.env.host` (loaded by compose) so GUI forwarding *and* GPU acceleration are configured for you: +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: -- **Linux (Wayland or X11) and WSL2/WSLg** → host display socket, passed straight through. -- **macOS with XQuartz** → forwarded over TCP to XQuartz via `host.docker.internal`, rendered with VirtualGL. -- **Windows without WSL2/WSLg** (Hyper-V backend, or a native Windows path) → forwarded over TCP to VcXsrv/X410. -- **NVIDIA GPU** (Linux, with the [nvidia container toolkit](/setup/nvidia) installed) → detected automatically and handed to the container; otherwise the default runtime is used. +| 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 | -So on every platform you just use the single config below. + + + 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). + + -### VSCode + ```bash + devcontainer up + ``` -Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right — accept it, or run **Dev Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. + -### CLI + -```bash title="Terminal" -devcontainer up -``` - - - Ports and flags are defined in `docker/.env.defaults` (committed). To change them on your machine, put the keys you want in `docker/.env.local` (gitignored) — e.g. `FORCE_VNC=1`. The real `docker/.env` is regenerated from the defaults, your `.env.local`, and host detection on every `devcontainer up`, so don't edit it directly. - - - - 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. + + Ports and flags & other configuration is in `docker/.env.defaults`. To change any, add it to + `docker/.env.local`. ## 2. Check if display works -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. - -### 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. - -### Linux host with X11 (no VNC needed) - -If your host runs Xorg instead of Wayland, the container forwards X11 directly through the host's `/tmp/.X11-unix` socket, which is bind-mounted in automatically. The log prints: - -``` -[X11] Using host X11 display at :0 -``` - -GUI windows appear on your desktop natively — you can skip the VNC steps below. - -### WSL2 with WSLg (no VNC needed) - -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. - -### macOS with XQuartz (no VNC needed) - -On macOS the container is auto-configured for XQuartz (see [step 1](#1-build-the-devcontainer)). GUI windows are forwarded over the X11 protocol to [XQuartz](https://www.xquartz.org) instead of VNC: - -1. Install XQuartz if you don't already have it: `brew install --cask xquartz`, or download it from [xquartz.org](https://www.xquartz.org). -2. Open XQuartz, go to **XQuartz > Settings > Security**, and check **"Allow connections from network clients"**. -3. Fully quit and reopen XQuartz (log out and back in, or restart) for that setting to take effect. -4. In a Mac Terminal, run `DISPLAY=:0 xhost + 127.0.0.1` to authorize the connection from Docker Desktop's VM. You'll need to run this again every time XQuartz restarts, since it isn't a persistent setting. - - - It's tempting to write `xhost -display :0 + 127.0.0.1`, but that doesn't do what it looks like. `xhost`'s own man page documents this as a bug: `-display` is parsed as "remove a host named `display`", not as a flag - `xhost` always connects using your shell's `$DISPLAY` env var instead, which may be stale (pointing at a dead XQuartz session) if you launched XQuartz manually rather than by opening the app normally. Set `DISPLAY` as an env var prefix on the command instead, as in step 4. - +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: -Once the container starts, the log should print: - -``` -[X11] Using host X11 display at host.docker.internal:0 +```bash title="Container" +vglrun /opt/VirtualGL/bin/glxspheres64 ``` -If you instead see the container falling back to starting Xvfb + noVNC, double check steps 2-4 above, then check `/tmp/start_x_server.log` inside the container. - -If the display *is* reachable but `sim gazebo launch` (or similar) still fails with "Cannot connect to display", it now prints a diagnostic breakdown - DNS resolution, TCP reachability, and X11 authorization are checked separately, with specific fix suggestions for whichever layer is actually broken. - - - A working X11 connection isn't enough on its own to render Gazebo/RViz. XQuartz can display windows, but it can't give them a usable OpenGL context - its indirect GLX (IGLX) is deprecated and [unsupported upstream](https://github.com/XQuartz/XQuartz/issues/144), so OGRE2 (used by both Gazebo and RViz) fails to create a renderer at all. - - The container works around this with [VirtualGL](https://virtualgl.org), which is set up automatically - there is nothing to install on your Mac beyond XQuartz itself, and no extra ports to open. GL rendering happens against a headless X server *inside* the container (Mesa llvmpipe, OpenGL 4.5), and only the finished frames are sent to XQuartz as ordinary X11 images, which it handles reliably. `sim gazebo` wraps the launch in `vglrun` for you. +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`. - You do **not** need to enable XQuartz's "Enable IGLX" / `enable_iglx` preference - VirtualGL deliberately avoids that path, and leaving it off is fine. - - Rendering is done in software (llvmpipe), so expect modest frame rates rather than GPU performance - macOS has no GPU passthrough into Docker. If rendering still fails, `sim gazebo` prints a warning explaining which piece is missing; setting `FORCE_VNC=1` in `docker/.env` and recreating the container is the fallback. + + `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. -### Windows without WSL2/WSLg (no VNC needed) - -On native Windows the container is auto-configured for a TCP X server (see [step 1](#1-build-the-devcontainer)). If you're using WSL2 with Docker Desktop's WSL2 backend, none of this is needed - detection uses WSLg's socket automatically (see above). Native Windows detection requires bash on PATH (Git for Windows) so the host script can run. - -1. Install an X server that supports TCP connections: [VcXsrv](https://sourceforge.net/projects/vcxsrv) (free) or X410 (Microsoft Store, paid). -2. Launch it with network access enabled. For VcXsrv's XLaunch: choose "Multiple windows", display number `0`, then on the "Extra settings" page check **"Disable access control"**. -3. If Windows Defender Firewall prompts you, allow the X server on Private networks. +## 3. Launch the sim -Once the container starts, the log should print: +Once the display works, head to [Running Gazebo](../gazebo/gazebo) to launch your first sim. -``` -[X11] Using host X11 display at host.docker.internal:0 -``` +--- -If it falls back to Xvfb + noVNC instead, confirm the X server is running with access control disabled and that the firewall isn't blocking it. +# NVIDIA Acceleration - - Same as the macOS/XQuartz note above: VcXsrv/X410 can display windows but can't provide Gazebo/RViz (OGRE2) with a usable OpenGL context over indirect GLX. The container routes GL through [VirtualGL](https://virtualgl.org) automatically - rendering happens inside the container with Mesa llvmpipe and only finished frames are sent to your X server, so there's nothing extra to install on Windows and no extra ports to open. Rendering is in software, so expect modest frame rates. If it still doesn't render, `sim gazebo` warns upfront - `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback. + + See the [official install + guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + for other distros. -### All other cases (headless servers, or the above not configured) +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: -On hosts without a reachable Wayland or X11 display, the script starts a VNC stack. Depending on what GPU or virtual display driver is available, you will see one of the following: +```bash +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ + | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg -``` -[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 -``` +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 -followed by: - -``` -[noVNC] Desktop available at: http://localhost:6080/vnc.html -[MAIN] All services started +sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker ``` -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) - - - - 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. - - -If the script fails, it automatically dumps `/tmp/start_x_server.log` to the terminal to help you diagnose the problem. - -### Verify the display works - -Before launching the sim, confirm both display paths are functioning: +Verify Docker can see the GPU: -- **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. -- **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. - -On macOS/Windows, `xeyes` only proves the X11 connection works - it doesn't use OpenGL, so it can succeed while Gazebo/RViz still fail to render. To check the GL path as well, run this inside the container: - -```bash title="Container" -vglrun /opt/VirtualGL/bin/glxspheres64 +```bash +docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nvidia-smi ``` - -A window of spinning spheres should appear on your desktop, and the terminal should report a frame rate and `OpenGL Renderer: llvmpipe`. If instead you get `vglrun: command not found`, rebuild the container; if it reports that it can't open the 3D X server, run `bash .devcontainer/x_server.sh` to start it. - - - `sim gazebo` adds the `vglrun` prefix for you, but anything you launch yourself that renders with OpenGL needs it too - `vglrun rviz2`, `vglrun gz sim`, and so on. Without it, those apps go straight to XQuartz/VcXsrv's broken indirect GLX and fail to create a renderer. Plain X11 tools like `xeyes` don't need it. This only applies on macOS/Windows; on Linux and WSLg, run them normally. - - -## 3. Launch the sim - -Once the display is running, head to [Running Gazebo](../../guides/gazebo/) to launch your first sim. diff --git a/docs/setup/nvidia.mdx b/docs/setup/nvidia.mdx deleted file mode 100644 index 388c6fb..0000000 --- a/docs/setup/nvidia.mdx +++ /dev/null @@ -1,62 +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 devcontainer - -There is no separate GPU config to pick. Once the toolkit above is installed, just open the **default** devcontainer the [normal way](../docker/#1-build-the-devcontainer) — on `devcontainer up` (or VS Code "Reopen in Container"), `.devcontainer/detect-host-env.sh` probes the host and, when it finds a working NVIDIA setup (`nvidia-smi` plus the `nvidia` runtime registered with Docker), enables `runtime: nvidia` and hands the GPU to the container. If the toolkit isn't installed, it silently stays on the default runtime — so this same config works with or without a GPU. - -```bash title="Terminal" -devcontainer up -``` - -You can confirm detection fired in the `devcontainer up` output — look for `[detect-host-env] host=Linux gpu=nvidia`. - -## 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..20cce26 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`.