Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"service": "sim",
"runServices": ["sim"],
"workspaceFolder": "/home/trickfire/gazebo-simulations",
"postCreateCommand": "[ -d .venv ] || (python3 -m venv --copies .venv && . .venv/bin/activate && pip install -e .)",
"postStartCommand": "bash -c 'bash .devcontainer/x_server.sh &'",
"postCreateCommand": "[ -d .venv ] || (python3 -m venv --system-site-packages .venv && . .venv/bin/activate && pip install -e .)",
// "postStartCommand": "bash ./.devcontainer/x_server.sh",
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/trickfire/.ssh,type=bind,consistency=cached"
"source=${localEnv:HOME}/.ssh,target=/home/trickfire/.ssh,type=bind,consistency=cached",
"source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached"
],
"forwardPorts": [6080, 5900],
"customizations": {
Expand Down
10 changes: 10 additions & 0 deletions .devcontainer/pixi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

# Gazebo plugin search path
export GZ_SIM_SYSTEM_PLUGIN_PATH="${CONDA_PREFIX}/lib${GZ_SIM_SYSTEM_PLUGIN_PATH:+:$GZ_SIM_SYSTEM_PLUGIN_PATH}"

# Switch to Cyclone DDS
# Fast-DDS calls pthread_setaffinity_np which macOS doesn't support
# flooding the log with 'Protocol family not supported' errors on
# every DDS thread startup
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
30 changes: 22 additions & 8 deletions .devcontainer/x_server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,28 @@ fi
: "${VNC_PORT:?VNC_PORT is not set}"
: "${NOVNC_PORT:?NOVNC_PORT is not set}"

# Check if DISPLAY is already in use
if xdpyinfo -display "$DISPLAY" &>/dev/null; then
log "[ERROR] Display $DISPLAY is already in use!"
exit 1
# Check if display is already available:
# - pgrep covers local Xorg/Xvfb including root-owned (reads /proc, no permission issues)
# - socket-without-lockfile covers forwarded displays (host X server mounted into container)
# - socket+lockfile with no process = stale from a previous container run, clean up
_XDISPLAY="${DISPLAY%%.*}"
_DISPLAY_NUM="${_XDISPLAY#:}"
_SOCKET="/tmp/.X11-unix/X${_DISPLAY_NUM}"
_LOCK="/tmp/.X${_DISPLAY_NUM}-lock"
if pgrep -f "Xorg $_XDISPLAY" >/dev/null 2>&1 || pgrep -f "Xvfb $_XDISPLAY" >/dev/null 2>&1; then
log "[X11] X server already running on $DISPLAY, skipping startup"
exit 0
elif [ -S "$_SOCKET" ] && [ ! -f "$_LOCK" ]; then
log "[X11] Display $DISPLAY is forwarded from host, skipping startup"
exit 0
elif [ -f "$_LOCK" ]; then
log "[X11] Removing stale X lock file and socket from previous run"
rm -f "$_LOCK" "$_SOCKET"
fi
unset _XDISPLAY _DISPLAY_NUM _SOCKET _LOCK

# Kill all child processes on exit
trap cleanup SIGINT SIGTERM EXIT
trap cleanup SIGINT SIGTERM

# Start X server (Xorg or Xvfb determined above)
if [ "$BACKEND" = "xvfb" ]; then
Expand All @@ -116,6 +130,6 @@ log "[noVNC] Starting browser-based desktop on port ${NOVNC_PORT}"
start /usr/share/novnc/utils/launch.sh --vnc "localhost:${VNC_PORT}" --listen "${NOVNC_PORT}"
log "[noVNC] Desktop available at: http://localhost:${NOVNC_PORT}/vnc.html"

# Done
log "[MAIN] All services started. Press Ctrl+C to stop"
wait
# Detach all background services so they survive this script exiting
disown "${PIDS[@]}"
log "[MAIN] All services started"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Icon

### Devcontainer ###

### pixi ###
.pixi/

### Python ###
.mypy_cache
__pycache__
Expand Down
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
"docs/node_modules": true,
"docs/dist": true,

// MacOS native build files
".native/": true
// Pixi env
".pixi/": true
},

// ================================
Expand Down
10 changes: 6 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ gazebo-simulations/
├── robot-sim/ # ROS 2 workspace - simulation packages
├── cli/ # Python CLI for building and launching simulations
├── docs/ # Documentation site (Astro / Starlight)
├── pixi.toml # Native environment definition (ROS 2 Jazzy + Gazebo Harmonic)
├── robots.json # Robot configuration registry
└── pyproject.toml # Python tooling config
```
Expand All @@ -17,7 +18,7 @@ gazebo-simulations/

### `robot-sim/` - Simulation

The ROS 2 workspace containing the Gazebo Fortress simulation packages. Built with `colcon` inside the Dev Container.
The ROS 2 workspace containing the Gazebo Harmonic simulation packages. Built with `colcon` via the native pixi environment or the Dev Container.

**Packages:**
- `<robot>_bringup` - launch files
Expand Down Expand Up @@ -45,16 +46,17 @@ Content pages live in `docs/content/docs/` and follow the existing directory str
Python package providing the `sim` CLI command:

- `cli.py` - Main entry point and argument parsing
- `docker.py` - `sim docker` build and launch logic
- `native.py` - `sim native` build and launch logic (pixi environment)
- `docker.py` - `sim docker` build and launch logic (Dev Container)
- `create/` - `sim create` / `sim update` implementation (OnShape download, URDF processing, package scaffolding)
- `paths.py` - Workspace path utilities
- `output.py` - Terminal output helpers (`info`, `warn`, `die`)

## Dev setup

The simulation runs inside a Docker Dev Container. Follow the [Getting Started](https://trickfirerobotics.github.io/gazebo-simulations/setup/getting-started/) guide to set up your environment before making changes to `robot-sim/`.
The primary workflow uses pixi for a self-contained native environment. Follow the [Getting Started](https://trickfirerobotics.github.io/gazebo-simulations/setup/getting-started/) guide.

For `docs/` changes, only Node.js is required, no container needed.
For `docs/` changes, only Node.js is required.

## Formatting

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Gazebo Simulations

Gazebo Fortress simulations for TrickFire Robotics robot subsystems, running on ROS 2 Humble inside a Docker Dev Container.
Gazebo Harmonic simulations for TrickFire Robotics robot subsystems, running on ROS 2 Jazzy.

## Documentation

**Full documentation is at [trickfirerobotics.com/gazebo-simulations](https://trickfirerobotics.com/gazebo-simulations)**

- [Getting Started](https://trickfirerobotics.com/gazebo-simulations/setup/getting-started/) - setup, Dev Container, display, first launch
- [Getting Started](https://trickfirerobotics.com/gazebo-simulations/setup/getting-started/) - native setup, Dev Container alternative, first launch
- [Running Simulations](https://trickfirerobotics.com/gazebo-simulations/guides/running-simulations/) - `sim` CLI usage and flags
- [Moving Joints](https://trickfirerobotics.com/gazebo-simulations/guides/moving-joints/) - Joint GUI and `move_joints` CLI node
- [Adding a New Robot](https://trickfirerobotics.com/gazebo-simulations/guides/adding-robots/) - OnShape to Gazebo with `sim create`
Expand Down
20 changes: 20 additions & 0 deletions cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys

from .docker import build_and_launch
from .native import build_and_launch_native
from .create import create as robot_create, update as robot_update, PACKAGE_DIR, REPO_ROOT
from .create.commands import cmd_local, cmd_raw
from .auth import auth as cmd_auth
Expand Down Expand Up @@ -37,6 +38,23 @@ def main() -> None:
help="Skip build, use existing install/",
)

# --- native ---
native_parser = subparsers.add_parser(
"native",
help="Build and launch a robot simulation using the native pixi environment",
)
native_parser.add_argument("robot", help="Robot name (e.g., arm, gripper)")
native_parser.add_argument(
"--build-only",
action="store_true",
help="Build only, don't launch simulation",
)
native_parser.add_argument(
"--no-build",
action="store_true",
help="Skip build, use existing install/",
)

# --- clean ---
subparsers.add_parser(
"clean",
Expand Down Expand Up @@ -117,6 +135,8 @@ def main() -> None:
try:
if args.command == "docker":
build_and_launch(args.robot, build_only=args.build_only, no_build=args.no_build)
elif args.command == "native":
build_and_launch_native(args.robot, build_only=args.build_only, no_build=args.no_build)
elif args.command == "clean":
for name in ("build", "install", "log"):
path = WORKSPACE_DIR / name
Expand Down
2 changes: 1 addition & 1 deletion cli/create/templates/bringup/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.8)
cmake_minimum_required(VERSION 3.14)
project(__ROBOT___bringup)

find_package(ament_cmake REQUIRED)
Expand Down
2 changes: 1 addition & 1 deletion cli/create/templates/description/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.8)
cmake_minimum_required(VERSION 3.14)
project(__ROBOT___description)

find_package(ament_cmake REQUIRED)
Expand Down
47 changes: 47 additions & 0 deletions cli/native.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Native simulation launcher for the `sim` CLI"""

from __future__ import annotations

import os

from .docker import build_and_launch as _build_and_launch
from .output import die
from .paths import REPO_DIR


def _check_pixi_env() -> None:
in_env = bool(os.environ.get("PIXI_PROJECT_ROOT") or os.environ.get("CONDA_PREFIX"))
if not in_env:
die(
"Not inside a pixi environment.\n"
" Activate with: pixi shell\n"
" Then re-run: sim native <robot>"
)

pixi_dir = REPO_DIR / ".pixi"
if not pixi_dir.is_dir():
die(
f"No pixi environment at {pixi_dir}\n"
" Install dependencies first: pixi install"
)


def _setup_plugin_paths() -> None:
conda_prefix = os.environ.get("CONDA_PREFIX", "")
if not conda_prefix:
return
plugin_lib = f"{conda_prefix}/lib"
for var in ("GZ_SIM_SYSTEM_PLUGIN_PATH", "GZ_SIM_RESOURCE_PATH"):
existing = os.environ.get(var, "")
paths = [p for p in existing.split(":") if p]
if plugin_lib not in paths:
os.environ[var] = ":".join([plugin_lib] + paths)


def build_and_launch_native(
robot_name: str, *, build_only: bool = False, no_build: bool = False
) -> None:
"""Build the workspace and launch a robot simulation using the native pixi environment."""
_check_pixi_env()
_setup_plugin_paths()
_build_and_launch(robot_name, build_only=build_only, no_build=no_build)
46 changes: 22 additions & 24 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# --------------------------------------------------------------------------------------------
# Multi-stage build: Ubuntu 22.04 (Jammy) + ROS 2 Humble + Gazebo Fortress
# Multi-stage build: Ubuntu 24.04 (Noble) + ROS 2 Jazzy + Gazebo Harmonic
#
# Targets:
# runtime simulation environment + VNC/headless display stack (docker-compose)
Expand All @@ -10,7 +10,7 @@
# --------------------------------------------------------------------------------------------
# RUNTIME

FROM ros:humble-ros-base AS runtime
FROM ros:jazzy-ros-base AS runtime

ARG DEBIAN_FRONTEND=noninteractive
ARG TZ="America/Los_Angeles"
Expand All @@ -30,7 +30,7 @@ RUN ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime && \
echo "${TZ}" > /etc/timezone

# --------------------------------------
# Gazebo Fortress repo
# Gazebo Harmonic repo

RUN curl https://packages.osrfoundation.org/gazebo.gpg \
--output /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg && \
Expand All @@ -43,41 +43,40 @@ RUN curl https://packages.osrfoundation.org/gazebo.gpg \
# APT packages

RUN apt-get update && apt-get install -y --no-install-recommends \
git python3-pip python3.10-venv wget zstd \
git openssh-client python3-pip python3.12-venv wget zstd \
python3-rosdep \
python3-colcon-common-extensions \
ros-humble-ros-gz-sim \
ros-humble-ros-ign-bridge \
ros-humble-ros2-controllers \
ros-jazzy-ros-gz \
ros-jazzy-ros2-controllers \
python3-tk \
age \
&& rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y --no-install-recommends \
ignition-fortress \
gz-harmonic \
&& rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common \
&& add-apt-repository universe \
&& apt-get update && apt-get install -y --no-install-recommends \
libgz-cmake3-dev \
libgz-gui6 \
libgz-plugin2-dev \
libgz-common5-dev \
libgz-sim7-dev \
ros-humble-rviz2 \
ros-humble-xacro \
ros-humble-joint-state-publisher-gui \
ros-humble-gz-ros2-control \
libgz-cmake4-dev \
libgz-gui8 \
libgz-plugin3-dev \
libgz-common6-dev \
libgz-sim8-dev \
ros-jazzy-rviz2 \
ros-jazzy-xacro \
ros-jazzy-joint-state-publisher-gui \
ros-jazzy-gz-ros2-control \
&& rm -rf /var/lib/apt/lists/*

# --------------------------------------
# VNC / headless display stack

RUN apt-get update && apt-get install -y --no-install-recommends \
xvfb novnc x11vnc websockify openbox xterm python3-xdg x11-utils \
mesa-utils libglx-mesa0 libgl1-mesa-glx libgl1-mesa-dri \
mesa-utils libglx-mesa0 libgl1-mesa-dri \
xserver-xorg-core xserver-xorg-video-dummy \
&& rm -rf /var/lib/apt/lists/*

Expand All @@ -103,21 +102,20 @@ RUN printf '#include <stdlib.h>\nint drmCloseBufferHandle(int fd, unsigned int h
# --------------------------------------
# Python packages

RUN pip3 install --upgrade pip "setuptools<66.0.0"

RUN rosdep init || true

# --------------------------------------
# User

RUN useradd trickfire --shell /bin/bash --create-home --no-log-init && \
RUN userdel ubuntu 2>/dev/null || true && \
useradd trickfire --uid 1000 --shell /bin/bash --create-home --no-log-init && \
usermod -aG video trickfire
COPY docker/container.bashrc /home/trickfire/.bashrc
RUN echo "trickfire ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/user

RUN rosdep fix-permissions && \
chown -R trickfire:trickfire /usr/local/lib/python3.10/dist-packages && \
chmod -R a+rX /usr/local/lib/python3.10/dist-packages
chown -R trickfire:trickfire /usr/local/lib/python3.12/dist-packages && \
chmod -R a+rX /usr/local/lib/python3.12/dist-packages

USER trickfire

Expand All @@ -129,7 +127,7 @@ FROM runtime AS dev

USER root

RUN pip3 install ruff
RUN pip3 install ruff --break-system-packages

RUN curl -sSL "https://github.com/mvdan/sh/releases/latest/download/shfmt_$(curl -sSL https://api.github.com/repos/mvdan/sh/releases/latest | grep -oP '(?<="tag_name": ")v[^"]+')_linux_amd64" \
-o /usr/local/bin/shfmt && \
Expand Down
8 changes: 4 additions & 4 deletions docker/container.bashrc
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ fi

# ---------- ROS/Gazebo environment ----------
tf_source_env() {
if [ -f /opt/ros/humble/setup.bash ]; then
if [ -f /opt/ros/jazzy/setup.bash ]; then
# shellcheck source=/dev/null
source /opt/ros/humble/setup.bash
source /opt/ros/jazzy/setup.bash
fi

if [ -n "${TF_ROBOT_WS:-}" ] && [ -f "$TF_ROBOT_WS/install/setup.bash" ]; then
Expand Down Expand Up @@ -113,8 +113,8 @@ alias ...='cd ../..'

alias rtl='ros2 topic list'
alias rte='ros2 topic echo'
alias gtl='ign topic -l'
alias gte='ign topic -e -t'
alias gtl='gz topic -l'
alias gte='gz topic -e -t'

alias simup='sim docker'
alias simclean='sim clean'
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ services:
sim:
build:
target: dev
args:
USER_UID: "${UID:-1000}"
Loading
Loading