Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.idea/workspace.xml
.idea/tasks.xml
.idea/*
.junie/

**/.idea/
**/cmake-build-debug/
Expand Down
91 changes: 54 additions & 37 deletions pixi.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ python = "==3.12.1, <3.13"

# Patches
readline = ">=8.2.999" # This is needed because we want to install an empty readline package from our models channel that leads to the system version being used. Otherwise we might have a mismatch between bash (often accessed globally ignoring the env) and readline (provided by the env) breaking everything.
pixi-pycharm = ">=0.0.12,<0.0.13"

[feature.ros.dependencies]
# Misc dependencies for our ROS 2 packages
Expand Down
91 changes: 46 additions & 45 deletions src/bitbots_misc/bitbots_bringup/launch/mujoco_simulation.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import yaml
from ament_index_python.packages import get_package_share_directory
from bitbots_mujoco_sim.world import compute_game_settings, generate_world_xml, parse_num_robots
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
Expand Down Expand Up @@ -88,34 +89,13 @@ def generate_domain_bridge_config(robot_domain: int, output_dir: Path) -> Path:
return config_path


def generate_world_xml(num_robots: int, package_share: str, robot_type: str) -> Path:
"""Generate MuJoCo world XML with the correct number of robots."""
template_path = Path(package_share) / "xml" / "kid_field.xml"
output_path = Path(package_share) / "xml" / "generated_world.xml"
offset = 4 * (
1 / num_robots
) # this makes the offset be the default value when there are 4 robots and increse the less robots there are

with open(template_path) as f:
template = f.read()

# Replace placeholder with actual robot count
world_xml = (
template.replace("{{NUM_ROBOTS}}", str(num_robots))
.replace("{{OFFSET}}", str(offset))
.replace("{{ROBOT_TYPE}}", robot_type)
)

with open(output_path, "w") as f:
f.write(world_xml)
return output_path


def launch_setup(context):
"""Dynamically set up launches based on num_robots."""
num_robots = int(LaunchConfiguration("num_robots").perform(context))
num_robots_spec = LaunchConfiguration("num_robots").perform(context)
num_robots = sum(parse_num_robots(num_robots_spec)) # total robots across all teams
robot_type = str(LaunchConfiguration("robot_type").perform(context))
use_web = LaunchConfiguration("web").perform(context).lower() == "true"
start_teamplayer = LaunchConfiguration("teamplayer").perform(context).lower() == "true"
package_share = get_package_share_directory("bitbots_mujoco_sim")
bridge_config_dir = Path(package_share) / "config" / "domain_bridges"

Expand All @@ -126,7 +106,12 @@ def launch_setup(context):
if value: # Only pass if not empty string
teamplayer_args.append(f"{arg_name}:={value}")

world_file = generate_world_xml(num_robots, package_share, robot_type)
world_file = generate_world_xml(num_robots_spec, package_share, robot_type)

# Compute individual game settings for every robot so that each robot gets its
# own team affiliation, bot id and role instead of relying on hand-written
# per-domain config files. Ordered the same as the robot domains below.
game_settings = compute_game_settings(parse_num_robots(num_robots_spec))

actions = []

Expand All @@ -144,7 +129,8 @@ def launch_setup(context):
),
)

for robot_domain in range(11, num_robots + 11): # 11 is the standart starting id for our robots
# 11 is the standard starting id for our robots
for robot_index, robot_domain in enumerate(range(11, num_robots + 11)):
config_file = generate_domain_bridge_config(robot_domain, bridge_config_dir)
actions.append(
LogInfo(msg=f"Starting domain bridge for robot{robot_domain} (domain {robot_domain})"),
Expand All @@ -160,25 +146,32 @@ def launch_setup(context):
),
)

actions.append(
TimerAction(
period=3.0,
actions=[
LogInfo(msg=f"Launching teamplayer stack for robot{robot_domain} in domain {robot_domain}"),
ExecuteProcess(
cmd=[
"ros2",
"launch",
"bitbots_bringup",
"teamplayer.launch",
]
+ teamplayer_args,
output="screen",
additional_env={"ROS_DOMAIN_ID": str(robot_domain)},
),
],
# Pass this robot's individual game settings down to the teamplayer stack,
# where parameter_blackboard.launch.py applies them on top of the shared
# game_settings.yaml defaults.
robot_game_settings_args = [f"{key}:={value}" for key, value in game_settings[robot_index].items()]

if start_teamplayer:
actions.append(
TimerAction(
period=3.0,
actions=[
LogInfo(msg=f"Launching teamplayer stack for robot{robot_domain} in domain {robot_domain}"),
ExecuteProcess(
cmd=[
"ros2",
"launch",
"bitbots_bringup",
"teamplayer.launch",
]
+ teamplayer_args
+ robot_game_settings_args,
output="screen",
additional_env={"ROS_DOMAIN_ID": str(robot_domain)},
),
],
)
)
)

return actions

Expand All @@ -187,10 +180,18 @@ def generate_launch_description():
"""Launch MuJoCo simulation with domain bridge for multi-robot support."""

declared_args = [
DeclareLaunchArgument(
"teamplayer",
default_value="true",
description="Whether to launch the teamplayer software stack for the simulated robots",
),
DeclareLaunchArgument(
"num_robots",
default_value="1",
description="Number of robots in the simulation",
description=(
"Robot setup in the simulation. Either a single number for one team "
"(e.g. '3') or a colon separated team setup (e.g. '2:2' for a 2 vs 2)."
),
),
DeclareLaunchArgument(
"robot_type",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0"?>
<launch>
<arg name="teamplayer" default="true" description="Whether the teamplayer software stack should be started" />
<arg name="audio" default="false" description="Whether the audio system should be started" />
<arg name="behavior" default="true" description="Whether the behavior control system should be started" />
<arg name="behavior_dsd_file" default="main.dsd" description="The behavior dsd file that should be used" />
Expand All @@ -20,7 +21,7 @@
</include>

<!-- load teamplayer software stack -->
<include file="$(find-pkg-share bitbots_bringup)/launch/teamplayer.launch">
<include if="$(var teamplayer)" file="$(find-pkg-share bitbots_bringup)/launch/teamplayer.launch">
<arg name="audio" value="$(var audio)" />
<arg name="behavior" value="$(var behavior)" />
<arg name="behavior_dsd_file" value="$(var behavior_dsd_file)" />
Expand Down
12 changes: 12 additions & 0 deletions src/bitbots_misc/bitbots_bringup/launch/teamplayer.launch
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@
<arg unless="$(var sim)" name="fieldname" default="small_division_2026" description="Loads field settings" />
<arg if="$(var sim)" name="fieldname" default="hsl_kid" description="Loads field settings" />

<!-- per-robot game settings, empty means use the default from the config for the robot -->
<arg name="bot_id" default="" description="Per-robot override for the bot_id game setting" />
<arg name="team_id" default="" description="Per-robot override for the team_id game setting" />
<arg name="team_color" default="" description="Per-robot override for the team_color game setting" />
<arg name="role" default="" description="Per-robot override for the role game setting" />
<arg name="position_number" default="" description="Per-robot override for the position_number game setting" />


<!-- load the global parameters -->
<include file="$(find-pkg-share bitbots_parameter_blackboard)/launch/parameter_blackboard.launch.py">
<arg name="sim" value="$(var sim)" />
<arg name="fieldname" value="$(var fieldname)" />
<arg name="bot_id" value="$(var bot_id)" />
<arg name="team_id" value="$(var team_id)" />
<arg name="team_color" value="$(var team_color)" />
<arg name="role" value="$(var role)" />
<arg name="position_number" value="$(var position_number)" />
</include>

<!-- load the text to speech engine -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
parameter_blackboard:
/parameter_blackboard:
ros__parameters:
bot_id: 1
monitoring_host_ip: 0.0.0.0
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction
Expand All @@ -10,6 +8,18 @@
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterFile

# Per-robot game settings that may be provided as launch arguments (e.g. by the
# multi-robot simulation launch) to override the shared ``game_settings.yaml``
# defaults. Each entry maps the launch argument name to the type its value should
# be cast to before it is passed to the parameter blackboard.
GAME_SETTING_OVERRIDES = {
"bot_id": int,
"team_id": int,
"team_color": int,
"role": str,
"position_number": int,
}


def generate_launch_description() -> LaunchDescription:
package_name = "bitbots_parameter_blackboard"
Expand All @@ -28,13 +38,17 @@ def create_node(context, *args, **kwargs):
ParameterFile(PathJoinSubstitution([package_share, "config", "game_settings.yaml"])),
]

robot_domain = os.environ.get("ROS_DOMAIN_ID")
if in_sim and robot_domain is not None:
parameters.append(
ParameterFile(
PathJoinSubstitution([package_share, "config", f"sim_game_settings_{int(robot_domain)}.yaml"])
)
)
# Per-robot game settings can be injected as launch arguments. They are
# applied after ``game_settings.yaml`` so that explicitly provided values
# win over the shared defaults. Unset arguments (empty string) are ignored
# and keep the shared default.
overrides = {}
for arg_name, cast in GAME_SETTING_OVERRIDES.items():
value = LaunchConfiguration(arg_name).perform(context)
if value != "":
overrides[arg_name] = cast(value)
if overrides:
parameters.append(overrides)

return [
Node(
Expand All @@ -43,6 +57,7 @@ def create_node(context, *args, **kwargs):
name="parameter_blackboard",
arguments=["--ros-args", "--log-level", "WARN"],
parameters=parameters,
namespace="/",
)
]

Expand All @@ -55,6 +70,17 @@ def create_node(context, *args, **kwargs):
default_value=PythonExpression(["'hsl_kid' if '", sim, "' == 'true' else 'labor'"]),
description="Field name to load parameters for.",
),
*[
DeclareLaunchArgument(
arg_name,
default_value="",
description=(
f"Per-robot override for the '{arg_name}' game setting. "
"Leave empty to use the shared game_settings.yaml default."
),
)
for arg_name in GAME_SETTING_OVERRIDES
],
IncludeLaunchDescription(
AnyLaunchDescriptionSource(
PathJoinSubstitution([get_package_share_directory("bitbots_utils"), "launch", "welcome.launch"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
self.ros_name: str = ros_name
self.name: str = name if name is not None else ros_name
self.joint_instance: int = model.joint(self.name)
self.actuator_instance: int = model.actuator(self.name.replace("_joint_", "_"))
self.actuator_instance: int = model.actuator(self.name.replace("_joint", ""))

aid = self.actuator_instance.id
self._default_kp: float = float(model.actuator_gainprm[aid, 0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ def quaternion_noisy_sensor(base_name: str, ros_name: str) -> QuaternionNoisySen
)

def _get_name(self, base_name: str) -> str:
return f"robot_{base_name}_{self.index}"
# Each robot is attached with a unique "robot_<index>_" prefix in the world XML,
# so its element names are the prefix followed by the bare model element name.
return f"robot_{self.index}_{base_name}"

@property
def domain(self) -> int:
Expand Down
Loading
Loading