diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh deleted file mode 100755 index a9f0869..0000000 --- a/.devcontainer/entrypoint.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# Added by codiumDevcontainer: entrypoint -set -euo pipefail - -# Start sshd in foreground (backgrounded here for supervision) -/usr/sbin/sshd -D & -SSHD_PID=$! - -# Stop file written by remote extension deactivate() -STOP_FILE="${CODIUM_WS:-/workspace}/.codium-devcontainer-stop" - -cleanup() { - if kill -0 "$SSHD_PID" 2>/dev/null; then - kill "$SSHD_PID" || true - wait "$SSHD_PID" 2>/dev/null || true - fi - exit 0 -} -trap cleanup TERM INT - -# Detect active SSH connections to port 22 -has_active_ssh() { - if command -v ss >/dev/null 2>&1; then - ss -tan 2>/dev/null | awk '$4 ~ /:22$/ && $1 ~ /ESTAB/ {found=1} END {exit !found}' - return $? - elif command -v netstat >/dev/null 2>&1; then - netstat -tan 2>/dev/null | awk '$4 ~ /:22$/ && $6 ~ /ESTABLISHED/ {found=1} END {exit !found}' - return $? - else - # No socket tools; assume active - return 0 - fi -} - -CHECK_INTERVAL=${CHECK_INTERVAL:-2} -IDLE_GRACE_SECONDS=${IDLE_GRACE_SECONDS:-60} -idle_elapsed=0 - -# Poll for stop file or idle ssh -while true; do - if [ -f "$STOP_FILE" ]; then - rm -f "$STOP_FILE" || true - cleanup - fi - - if has_active_ssh; then - idle_elapsed=0 - else - idle_elapsed=$((idle_elapsed + CHECK_INTERVAL)) - if [ "$idle_elapsed" -ge "$IDLE_GRACE_SECONDS" ]; then - echo "No active SSH sessions for ${IDLE_GRACE_SECONDS}s; stopping." >&2 - cleanup - fi - fi - - sleep "$CHECK_INTERVAL" - - # In case sshd died unexpectedly, exit - if ! kill -0 "$SSHD_PID" 2>/dev/null; then - exit 1 - fi -done diff --git a/.devcontainer/nvidia/devcontainer.json b/.devcontainer/nvidia/devcontainer.json index 937dbc3..7ed8ec6 100644 --- a/.devcontainer/nvidia/devcontainer.json +++ b/.devcontainer/nvidia/devcontainer.json @@ -6,9 +6,6 @@ ], "service": "sim", "runServices": ["sim"], - // Ports are published in docker/docker-compose.yml (sourced from docker/.env), - // so VS Code auto-forwards them without needing forwardPorts here. - "workspaceFolder": "/home/trickfire/simulations", "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" diff --git a/.devcontainer/pixi.sh b/.devcontainer/pixi.sh index bd4d116..36606e0 100644 --- a/.devcontainer/pixi.sh +++ b/.devcontainer/pixi.sh @@ -1,10 +1,4 @@ #!/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 diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 844e616..012a501 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -57,9 +57,6 @@ parse_args() { done } -# Exits the script early (code 0) if a host Wayland compositor socket is -# available, since Vulkan/GL clients can talk to it directly without any -# in-container X server. FORCE_VNC always skips this and forces the VNC path. try_wayland_passthrough() { if [ -n "$FORCE_VNC" ]; then DISPLAY="${FORCE_VNC_DISPLAY:-:77}" @@ -75,16 +72,10 @@ try_wayland_passthrough() { fi } -# Sets BACKEND ("xorg"/"xvfb") and XORG_CONF based on available hardware. detect_backend() { if [ -n "$FORCE_VNC" ]; then - # FORCE_VNC always runs alongside a real host session, so real Xorg here - # (dummy, vkms-targeted, or real GPU driver - it doesn't matter which) has - # repeatedly ended up touching real display hardware and hijacking the - # host's screen. Xvfb is the only backend that's structurally incapable of - # touching /dev/dri at all, so FORCE_VNC always uses it, no exceptions. BACKEND="xvfb" - XORG_CONF="/etc/X11/xorg.dummy.conf" # only used below to read screen resolution + XORG_CONF="/etc/X11/xorg.dummy.conf" log "[X11] FORCE_VNC: using Xvfb unconditionally (no direct GPU access)" elif [ -e /dev/nvmap ] && [ ! -d /dev/dri ]; then BACKEND="xvfb" @@ -99,8 +90,6 @@ detect_backend() { fi } -# No-GPU case: sets BACKEND and XORG_CONF, preferring a vkms virtual display -# over plain Xvfb when the kernel module is available. detect_vkms_backend() { BACKEND="xorg" log "[X11] No GPU detected - trying vkms for DRI3-capable headless display" @@ -120,7 +109,7 @@ detect_vkms_backend() { if [ -z "$VKMS_CARD" ]; then log "[X11] vkms unavailable, falling back to Xvfb (no real GPU access, no DRI3)" BACKEND="xvfb" - XORG_CONF="/etc/X11/xorg.dummy.conf" # only used below to read screen resolution + XORG_CONF="/etc/X11/xorg.dummy.conf" return fi @@ -129,7 +118,6 @@ detect_vkms_backend() { sed "s|__VKMS_CARD__|$VKMS_CARD|" /etc/X11/xorg.vkms.conf >"$XORG_CONF" } -# Reads SCREEN_WIDTH/SCREEN_HEIGHT/SCREEN_DEPTH out of $XORG_CONF. parse_screen_resolution() { SCREEN_MODE=$(grep -oP '(?<=Modes ")[^"]+' "$XORG_CONF" | head -1) SCREEN_WIDTH=$(echo "$SCREEN_MODE" | cut -dx -f1) @@ -144,7 +132,6 @@ parse_screen_resolution() { start_services() { trap cleanup SIGINT SIGTERM - # Start X server (Xorg or Xvfb determined above) if [ "$BACKEND" = "xvfb" ]; then log "[X11] Starting Xvfb on display ${DISPLAY} (${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH})" start Xvfb "$DISPLAY" -screen 0 "${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH}" @@ -153,22 +140,18 @@ start_services() { start sudo Xorg "$DISPLAY" -noreset -config "$XORG_CONF" fi sleep 1 - stty sane 2>/dev/null || true # Xorg alters terminal CR/LF settings + stty sane 2>/dev/null || true - # Start window manager log "[WM] Starting Openbox window manager" start openbox-session - # Start x11vnc server for VNC connection log "[VNC] Starting x11vnc on port ${VNC_PORT}" start x11vnc -display "$DISPLAY" -forever -shared -rfbport "$VNC_PORT" -nopw -xkb - # Start noVNC web client to serve the VNC desktop in a browser log "[noVNC] Starting browser-based desktop on port ${NOVNC_PORT}" start /usr/share/novnc/utils/novnc_proxy --vnc "localhost:${VNC_PORT}" --listen "${NOVNC_PORT}" log "[noVNC] Desktop available at: http://localhost:${NOVNC_PORT}/vnc.html" - # Detach all background services so they survive this script exiting disown "${PIDS[@]}" || true log "[MAIN] All services started" } diff --git a/cli/drpc.py b/cli/drpc.py index 885410b..b357634 100644 --- a/cli/drpc.py +++ b/cli/drpc.py @@ -8,7 +8,7 @@ def _find_discord_socket_dir() -> str | None: - """Return the directory containing a discord-ipc-N socket, or None if not found.""" + """Return the directory containing a discord-ipc-N socket, or None if not found""" candidates = [ os.environ.get("XDG_RUNTIME_DIR"), # standard Linux user session os.environ.get("TMPDIR"), diff --git a/cli/gazebo/create/__init__.py b/cli/gazebo/create/__init__.py index 3e94ec4..c73e7d8 100644 --- a/cli/gazebo/create/__init__.py +++ b/cli/gazebo/create/__init__.py @@ -1,4 +1,4 @@ -"""sim create/update - robot package generation from OnShape.""" +"""sim create/update - robot package generation from OnShape""" from __future__ import annotations diff --git a/cli/gazebo/create/onshape.py b/cli/gazebo/create/onshape.py index b2d479a..4434213 100644 --- a/cli/gazebo/create/onshape.py +++ b/cli/gazebo/create/onshape.py @@ -36,7 +36,7 @@ def parse_onshape_url(url: str) -> tuple: def _poll_job(dashboard_url: str, job_id: str) -> requests.Response: - """Poll until the job is done; returns the streaming archive response.""" + """Poll until the job is done; returns the streaming archive response""" deadline = time.monotonic() + _POLL_TIMEOUT while time.monotonic() < deadline: time.sleep(_POLL_INTERVAL) diff --git a/cli/gazebo/create/urdf.py b/cli/gazebo/create/urdf.py index f32d8a8..2495a80 100644 --- a/cli/gazebo/create/urdf.py +++ b/cli/gazebo/create/urdf.py @@ -1,4 +1,4 @@ -"""URDF post-processing: xacro injection, mesh path rewriting, control generation.""" +"""URDF post-processing: xacro injection, mesh path rewriting, control generation""" import re import xml.etree.ElementTree as ET diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index f676c0f..2af21fc 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -122,14 +122,7 @@ def _check_display() -> None: def _configure_force_vnc_rendering(env: dict[str, str]) -> list[str]: - """Force software GL rendering for Gazebo/OGRE2 when running under FORCE_VNC. - - FORCE_VNC intentionally never touches real GPU/render-node devices - every - attempt to give it direct GPU access (Xorg with a real or virtual KMS - device, VirtualGL through the render node) has ended up interfering with - the host's real display. Mesa's software rasterizer is unaccelerated but - it's the only backend that's actually safe here. - """ + """Force software GL rendering for Gazebo/OGRE2 when running under FORCE_VNC""" if not os.environ.get("FORCE_VNC"): return [] @@ -139,10 +132,10 @@ def _configure_force_vnc_rendering(env: dict[str, str]) -> list[str]: def _setup_pixi_env() -> None: - """Configure Gazebo plugin/resource paths and Qt platform for a pixi environment.""" + """Configure Gazebo plugin/resource paths and Qt platform for a pixi environment""" 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") + die(f"No pixi environment at {pixi_dir}\nInstall dependencies first: pixi install") conda_prefix = os.environ.get("CONDA_PREFIX", "") if conda_prefix: @@ -165,8 +158,6 @@ def _setup_pixi_env() -> None: if ogre2_media not in paths: os.environ["GZ_RENDERING_RESOURCE_PATH"] = ":".join([ogre2_media] + paths) - # The pixi Qt build ships only the xcb plugin; force it when the session - # sets QT_QPA_PLATFORM=wayland (common on Wayland desktops like Hyprland). if os.environ.get("QT_QPA_PLATFORM") == "wayland": os.environ["QT_QPA_PLATFORM"] = "xcb" diff --git a/docker/.env b/docker/.env index e32b75a..4b31831 100644 --- a/docker/.env +++ b/docker/.env @@ -7,5 +7,4 @@ ROSBRIDGE_PORT=9090 # set to one if you want # to force vnc startup -# (ignore wayland, existing xserver etc) # FORCE_VNC=1 diff --git a/gazebo/sim_common/sim_common/joint_gui.py b/gazebo/sim_common/sim_common/joint_gui.py index f2ef722..ba1b9da 100755 --- a/gazebo/sim_common/sim_common/joint_gui.py +++ b/gazebo/sim_common/sim_common/joint_gui.py @@ -91,7 +91,7 @@ def wait_for_joints(self, timeout: float = DISCOVERY_TIMEOUT_S) -> bool: return self._joints_ready.wait(timeout=timeout) def discover_trajectory_topics(self) -> list[str]: - """Return all currently advertised topics that carry JointTrajectory messages.""" + """Return all currently advertised topics that carry JointTrajectory messages""" return [ topic for topic, types in self.get_topic_names_and_types() @@ -177,7 +177,7 @@ def _poll_discovery(self, attempt: int = 0) -> None: self.root.after(self._POLL_MS, lambda: self._poll_discovery(attempt + 1)) def _refresh_topics(self) -> None: - """Re-query available trajectory topics and update the combobox values.""" + """Re-query available trajectory topics and update the combobox values""" topics = self.node.discover_trajectory_topics() if self._topics_combo is not None: self._topics_combo["values"] = topics @@ -275,11 +275,11 @@ def _build_controls(self) -> None: # ------------------------------------------------------------------ def _on_slider(self, joint: str, val: str) -> None: - """Update the numeric label next to a slider whenever its value changes.""" + """Update the numeric label next to a slider whenever its value changes""" self.value_labels[joint].config(text=f"{float(val):+.3f}") def _send(self) -> None: - """Read current slider positions and publish a trajectory command to the selected topic.""" + """Read current slider positions and publish a trajectory command to the selected topic""" joints = self.node.joint_names positions = [self.sliders[j].get() + self.node.possible_joints[j]["origin"] for j in joints] topic = self._topic_var.get().strip() @@ -290,7 +290,7 @@ def _send(self) -> None: self.status_var.set(f"Sent to {topic}: {[f'{p:.2f}' for p in positions]}") def _sync(self) -> None: - """Sync sliders to the current joint positions reported by /joint_states.""" + """Sync sliders to the current joint positions reported by /joint_states""" current = self.node.current_positions if not current: self.status_var.set("No joint state data available") @@ -303,7 +303,7 @@ def _sync(self) -> None: self.status_var.set("Synced sliders to current joint positions") def _reset(self) -> None: - """Zero all joint sliders and reset their display labels.""" + """Zero all joint sliders and reset their display labels""" for joint in self.node.joint_names: self.sliders[joint].set(0.0) self.value_labels[joint].config(text="0.0") @@ -312,7 +312,7 @@ def _reset(self) -> None: def main() -> None: """Take in the filename, - Initialise ROS2, spin the node on a background thread, then run the Tkinter event loop.""" + Initialise ROS2, spin the node on a background thread, then run the Tkinter event loop""" if len(sys.argv) < 2: print("Failed no path was provided") sys.exit(1) diff --git a/gazebo/sim_common/sim_common/launch_utils.py b/gazebo/sim_common/sim_common/launch_utils.py index 0fd963b..09e3e2e 100644 --- a/gazebo/sim_common/sim_common/launch_utils.py +++ b/gazebo/sim_common/sim_common/launch_utils.py @@ -1,4 +1,4 @@ -"""Shared launch utilities for simulation packages.""" +"""Shared launch utilities for simulation packages""" import os import shutil @@ -22,12 +22,12 @@ def log(msg): - """Green info log for launch files.""" + """Green info log for launch files""" print("\033[0;32m", "[INFO] [launch]: ", msg, "\x1b[0m", sep="") def err(msg): - """Red error log, exits with code 1.""" + """Red error log, exits with code 1""" print("\033[0;31m", "[ERROR] [launch]: ", msg, "\x1b[0m", sep="") sys.exit(1) @@ -45,7 +45,7 @@ def get_asset(package, *parts): def gz_supports_sim_command(): - """Return True when `gz` exists and exposes the `sim` subcommand.""" + """Return True when `gz` exists and exposes the `sim` subcommand""" if shutil.which("gz") is None: return False @@ -64,7 +64,7 @@ def gz_supports_sim_command(): def process_robot_description(urdf_file, controller_config): - """Run xacro over `urdf_file`, wiring in the controller config and platform plugin extension.""" + """Run xacro over `urdf_file`, wiring in the controller config and platform plugin extension""" return xacro.process_file( urdf_file, mappings={ @@ -106,7 +106,7 @@ def gazebo_launch_actions(world_file, gz_gui_config, gui_launch_arg="gui"): def spawn_robot_node(robot_name, robot_desc): - """Node that spawns `robot_desc` into the running gz sim world as `robot_name`.""" + """Node that spawns `robot_desc` into the running gz sim world as `robot_name`""" return Node( package="ros_gz_sim", executable="create", @@ -126,7 +126,7 @@ def robot_state_publisher_node(robot_desc): def clock_bridge_node(): - """ros_gz_bridge node that bridges the gz sim clock onto /clock.""" + """ros_gz_bridge node that bridges the gz sim clock onto /clock""" return Node( package="ros_gz_bridge", executable="parameter_bridge",