From 193c232620e0365f68a2129363eff48ba97aa138 Mon Sep 17 00:00:00 2001 From: Arcod7 Date: Mon, 31 Aug 2026 20:05:26 +0200 Subject: [PATCH] Make the Gazebo GUI work on macOS and keep the robot upright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three separate reasons the simulator never appeared on macOS. 1. gz sim refuses to run server and GUI in one process. Cocoa requires the Qt GUI on the main thread, so the ruby entry point exits(-1) unless given -s or -g (gazebosim/gz-sim#44): On macOS `gz sim` currently only works with either the -s argument or the -g argument, you cannot run both server and gui in one terminal. The launch passed plain "-r ", so the server died instantly and the spawner sat in an endless "Requesting list of worlds" loop. On Darwin, run the server with -s and start `gz sim -g` as a second, delayed process. 2. The robot lay on the ground. true does not survive urdf->sdf conversion — sdformat honours on the model, not on a link — so the stand stayed dynamic, and the robot was spawned at -z 0.5, in midair. Gravity did the rest. Anchor base_node to a `world` link with a fixed joint and spawn at -z 0.0. Upper body joints stay dynamic. The file is included only under use_gazebo_sim:=true, so the real/mock TF tree is untouched (verified: the world link appears in the sim description and not in the mock one). 3. filename="libmock_sensor.so" is hardcoded, but the plugin builds as libmock_sensor.dylib on macOS. A bare "mock_sensor" lets Gazebo resolve the platform extension, which is the usual convention for gz plugins anyway. Verified end to end: server and GUI both alive, spawner loop gone (8+ retries to 0), and the model reports Pose XYZ [0 0 0] RPY [0 -0 0] with the GUI rendering the robot standing on its stand. --- description/gazebo/gazebo.xacro | 4 +-- .../gazebo/inmoov_gazebo_physics.xacro | 18 +++++++--- launch/gazebo.launch.py | 35 +++++++++++++++++-- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/description/gazebo/gazebo.xacro b/description/gazebo/gazebo.xacro index e0f6368..797fe9e 100644 --- a/description/gazebo/gazebo.xacro +++ b/description/gazebo/gazebo.xacro @@ -379,7 +379,7 @@ - + 10 sensors/left_arm @@ -390,7 +390,7 @@ - + 10 sensors/right_arm diff --git a/description/gazebo/inmoov_gazebo_physics.xacro b/description/gazebo/inmoov_gazebo_physics.xacro index cddca10..0a4635b 100644 --- a/description/gazebo/inmoov_gazebo_physics.xacro +++ b/description/gazebo/inmoov_gazebo_physics.xacro @@ -13,10 +13,20 @@ self_collide on adjacent body meshes (avoids jitter), `gazebo_hand_link` enables it with friction tuned for grasping. --> - - - true - + + + + + + + true diff --git a/launch/gazebo.launch.py b/launch/gazebo.launch.py index 4a50ec6..82b42d4 100644 --- a/launch/gazebo.launch.py +++ b/launch/gazebo.launch.py @@ -22,17 +22,20 @@ # frames without an X server — the ros_gz camera bridge stays functional. import os +import sys from pathlib import Path from ament_index_python.packages import get_package_prefix, get_package_share_directory from launch import LaunchDescription from launch.actions import ( DeclareLaunchArgument, + ExecuteProcess, IncludeLaunchDescription, OpaqueFunction, SetEnvironmentVariable, TimerAction, ) +from launch.conditions import UnlessCondition from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.substitutions import ( LaunchConfiguration, @@ -43,6 +46,9 @@ import yaml +_IS_DARWIN = sys.platform == "darwin" + + def _gz_ros2_control_plugin_path(): pkg_share = get_package_prefix("inmoov_urdf") plugin_path = os.path.join(pkg_share, "lib", "mock_sensor") @@ -270,6 +276,11 @@ def _camera_compressor(topic: str, compressed_topic: str) -> Node: # When headless: server-only (-s) with EGL rendering (--headless-rendering) # so OGRE2 still renders camera sensors without an X display. Otherwise: # normal GUI launch. + # macOS cannot run the gz server and the Qt GUI in one process: Cocoa needs + # the GUI on the main thread, so `gz sim` refuses and exits(-1) unless given + # -s or -g (gazebosim/gz-sim#44, enforced in the ruby entry point). Run the + # server with -s here and start `gz sim -g` as a second process below. + gui_server_args = "-s -r " if _IS_DARWIN else "-r " gz_args = PythonExpression( [ "'-s -r --headless-rendering ", @@ -278,7 +289,8 @@ def _camera_compressor(topic: str, compressed_topic: str) -> Node: " if '", LaunchConfiguration("headless"), "'.lower() in ('true', '1', 'yes') ", - "else '-r ", + "else '", + gui_server_args, default_world, "'", ] @@ -288,13 +300,31 @@ def _camera_compressor(topic: str, compressed_topic: str) -> Node: launch_arguments={"gz_args": gz_args}.items(), ) + # Second process for the Qt GUI on macOS (see gz_args). Delayed so the + # server's transport is advertising before the GUI connects to it. + gazebo_gui_actions = [] + if _IS_DARWIN: + gazebo_gui_actions.append( + TimerAction( + period=6.0, + actions=[ + ExecuteProcess( + cmd=["gz", "sim", "-g"], + name="gazebo_gui", + output="screen", + condition=UnlessCondition(LaunchConfiguration("headless")), + ) + ], + ) + ) + spawn_robot = TimerAction( period=8.0, actions=[ Node( package="ros_gz_sim", executable="create", - arguments=["-name", "lucy", "-topic", "robot_description", "-z", "0.5"], + arguments=["-name", "lucy", "-topic", "robot_description", "-z", "0.0"], output="screen", parameters=[{"use_sim_time": True}], ) @@ -354,6 +384,7 @@ def spawner_actions_from_yaml(context, *args, **kwargs): supervisor, OpaqueFunction(function=spawner_actions_from_yaml), gz_sim_launch, + *gazebo_gui_actions, spawn_robot, bridge, *camera_compressors,