Multi-Robot Simulation Mode
Purpose
In Booster Studio multi-robot simulation mode, run programs for multiple robots at the same time.
Core Concepts
- Each robot identifies itself using
robot_name. - All Topics are isolated by
robot_name. - Each robot has its own launch script.
Activation Method
- After switching to the "Football Field" scene, click "Multi-Robot Mode"

- Click "+" to add multiple robots, up to 6 robots can be added


Topic Naming Rules
Simulation-side Topics (Provided by Booster Studio)
- In multi-robot simulation mode, Booster Studio automatically generates Topics with the robot name for each virtual robot. The naming rule is:
/{robot_name}/module_name/topic
-
{robot_name}is the name of the virtual robot in Booster Studio simulation, such asRobot1,Robot2, etc. -
For example, if the simulation has two robots,
Robot1andRobot2, the simulation side publishes the following Topics:Robot
Topic
Robot1
/robot1/head_poseRobot2
/robot2/head_pose -
Your program needs to use the
robot_namein the simulation to construct the correct Topic name and subscribe to the corresponding data.
User Node Topic
- When multiple nodes inside your program need to communicate with each other (for example, a vision processing node sends results to a decision node), these Topics also need to be isolated by robot name; otherwise, data from multiple robots will be mixed together.
- It is recommended to use
/{robot_name}/as the prefix:
/{robot_name}/your_module_name/topic_name
-
For example, suppose your program has a vision processing node that publishes detection results to a decision node after processing simulated camera data:
Robot
User Node Topic
Robot1
/robot1/vision/detectionRobot2
/robot2/vision/detection -
In code, use the
robot_nameparameter to concatenate the Topic:
self.pub = self.create_publisher(
DetectionMsg,
f'/{self.robot_name}/vision/detection',
10)
SDK Usage (Code Example)
#!/usr/bin/env python3
import sys
import time
import random
from pathlib import Path
import booster_robotics_sdk_python as b1
def print_help():
print(r"""
========== B1 CLI Commands ==========
# Basic
help Show this help
quit / exit Exit
# Mode
mp ChangeMode(kPrepare)
md ChangeMode(kDamping)
mw ChangeMode(kWalking)
mc ChangeMode(kCustom)
sg <gait> SwitchGait: gait = whole/half/halfv2
lie LieDown()
getup GetUp()
gum <mode> GetUpWithMode: mode = walking/soccer
# Walking / Move (vx, vy, yaw)
stop Move(0, 0, 0)
w / a / s / d / q / e Forward / left / backward / right / rotate left / rotate right
# Head
hd Look down
hu Look up
hr Look right
hl Look left
ho Reset head orientation
rhd Rotate head with direction: pitch +1, yaw 0
rht <pitch> <yaw> <ms> Rotate head to target pose within given time
# Hand end-effector
mhel Execute a two-stage motion sequence to move both end-effectors to the target pose via the MoveHandEndEffectorV2 interface
mhe <l/r> Move a single hand (MoveHandEndEffector)
mhedual Move both hands in one call (MoveDualHandEndEffector)
mheaux Example for MoveHandEndEffectorWithAux
hstop StopHandEndEffector()
# Gripper
gopenl Open the left gripper slightly (ControlGripper)
gclosel Close the left gripper slightly
# Hand control mode
hcm-start SwitchHandEndEffectorControlMode(True)
hcm-stop SwitchHandEndEffectorControlMode(False)
# Dexterous hand
dex-open <l/r> Open all fingers slightly
dex-close <l/r> Close all fingers slightly
# Dance / Whole-body motion
dance <id> Dance: id = newyear/nezha/tf/dab/ultra/respect/cheer/lucky/stop
wbd <id> WholeBodyDance: id = arbic/m1/m2/m3/boxkick/roundkick/shanhe/gaige
vk <on/off> [v1|v2] VisualKick(True/False, version), default = v1
# Sound
ps <path> PlaySound(path)
ss StopSound()
# Trajectory recording and replay
rtrec on/off RecordTrajectory(True/False)
rtreplay <path> ReplayTrajectory(path)
# Custom Trained Trajectory
load_traj <json_path> Load custom trajectory from a JSON file. Returns TID.
act_traj <tid> ActivateCustomTrainedTraj(tid)
unload_traj <tid> UnloadCustomTrainedTraj(tid)
# Zero torque / upper-body custom control
zt on/off ZeroTorqueDrag(True/False)
ubcc on/off UpperBodyCustomControl(True/False)
# Query / information
gm GetMode()
gs GetStatus()
gri GetRobotInfo()
gft GetFrameTransform(Body -> LeftHand)
# Low-level API
raw_change_mode <mode> ChangeMode via SendApiRequest + JSON
mode = prepare/damping/walking/custom
raw_vk <on/off> [v1|v2] VisualKick via SendApiRequest + JSON
raw_status GetStatus via SendApiRequestWithResponse
====================================
""")
def parse_lr(s: str):
if s.lower().startswith("l"):
return b1.HandIndex.kLeftHand
if s.lower().startswith("r"):
return b1.HandIndex.kRightHand
raise ValueError("hand must be l or r")
def parse_mode_name(name: str):
name = name.lower()
if name == "prepare":
return b1.RobotMode.kPrepare
if name == "damping":
return b1.RobotMode.kDamping
if name == "walking":
return b1.RobotMode.kWalking
if name == "soccer":
return b1.RobotMode.kSoccer
if name == "custom":
return b1.RobotMode.kCustom
raise ValueError("mode must be one of: prepare/damping/walking/soccer/custom")
def parse_gait_type(name: str):
name = name.lower()
if name in ("whole", "wb", "wholebody"):
return b1.GaitType.kWholeBodyHumanlikeGait
if name in ("half", "hb", "halfbody"):
return b1.GaitType.kHalfBodyHumanlikeGait
if name in ("halfv2", "hbv2", "halfbodyv2"):
return b1.GaitType.kHalfBodyHumanlikeGaitV2
raise ValueError("gait must be one of: whole/half/halfv2")
def parse_dance_id(name: str):
name = name.lower()
if name in ("newyear", "ny"):
return b1.DanceId.kNewYear
if name == "nezha":
return b1.DanceId.kNezha
if name in ("tf", "towardsfuture"):
return b1.DanceId.kTowardsFuture
if name in ("dab", "dabbing"):
return b1.DanceId.kDabbingGesture
if name in ("ultra", "ultraman"):
return b1.DanceId.kUltramanGesture
if name in ("respect", "rsp"):
return b1.DanceId.kRespectGesture
if name in ("cheer", "chr"):
return b1.DanceId.kCheeringGesture
if name in ("lucky", "cat", "luckycat"):
return b1.DanceId.kLuckyCatGesture
if name == "stop":
return b1.DanceId.kStop
raise ValueError("dance id unknown")
def parse_whole_body_dance_id(name: str):
name = name.lower()
if name in ("arbic", "arabic"):
return b1.WholeBodyDanceId.kArbicDance
if name in ("m1", "michael1"):
return b1.WholeBodyDanceId.kMichaelDance1
if name in ("m2", "michael2"):
return b1.WholeBodyDanceId.kMichaelDance2
if name in ("m3", "michael3"):
return b1.WholeBodyDanceId.kMichaelDance3
if name in ("boxkick", "bk"):
return b1.WholeBodyDanceId.kBoxingStyleKick
if name in ("roundkick", "rk"):
return b1.WholeBodyDanceId.kRoundhouseKick
if name in ("shanhe", "shanheguren"):
return b1.WholeBodyDanceId.kShanHeGuRenDance
if name in ("gaige", "gaigechunfeng"):
return b1.WholeBodyDanceId.kGaiGeChunFengDance
raise ValueError("whole-body dance id unknown")
def parse_visual_kick_version(name: str):
name = name.lower()
if name in ("v1", "1", "default"):
return b1.VisualKickVersion.kV1
if name in ("v2", "2"):
return b1.VisualKickVersion.kV2
raise ValueError("visual kick version must be one of: v1/v2")
# Mapping from Action enum value to name (only valid actions)
ACTION_VALUE2NAME = {
int(b1.Action.kUnknown): "kUnknown",
int(b1.Action.kHandShake): "kHandShake",
int(b1.Action.kHandWave): "kHandWave",
int(b1.Action.kHandControl): "kHandControl",
int(b1.Action.kDanceNewYear): "kDanceNewYear",
int(b1.Action.kDanceNezha): "kDanceNezha",
int(b1.Action.kDanceTowardsFuture): "kDanceTowardsFuture",
int(b1.Action.kGestureDabbing): "kGestureDabbing",
int(b1.Action.kGestureUltraman): "kGestureUltraman",
int(b1.Action.kGestureRespect): "kGestureRespect",
int(b1.Action.kGestureCheer): "kGestureCheer",
int(b1.Action.kGestureLuckyCat): "kGestureLuckyCat",
int(b1.Action.kGestureBoxing): "kGestureBoxing",
int(b1.Action.kZeroTorqueDrag): "kZeroTorqueDrag",
int(b1.Action.kRecordTraj): "kRecordTraj",
int(b1.Action.kRunRecordedTraj): "kRunRecordedTraj",
}
def format_actions(actions):
items = []
for a in actions:
try:
value = int(a)
except Exception:
# Fallback if conversion fails
items.append(str(a))
continue
name = ACTION_VALUE2NAME.get(value)
if name is None:
# Unknown or invalid enum value
items.append(f"UnknownAction(id={value})")
else:
items.append(f"{name} (id={value})")
return items
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} networkInterface [robotName]")
sys.exit(-1)
net_if = sys.argv[1]
robot_name = sys.argv[2] if len(sys.argv) >= 3 else None
# Initialize DDS
b1.ChannelFactory.Instance().Init(0, net_if)
client = b1.B1LocoClient()
if robot_name:
client.InitWithName(robot_name)
else:
client.Init()
x, y, yaw = 0.0, 0.0, 0.0
head_yaw, head_pitch = 0.0, 0.0
print("B1 CLI started.")
print_help()
while True:
try:
raw = input("> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nExit.")
break
if not raw:
continue
parts = raw.split()
cmd = parts[0]
args = parts[1:]
need_print = False
try:
# ===== Basic =====
if cmd in ("help", "?"):
print_help()
continue
if cmd in ("quit", "exit"):
print("Bye.")
break
# ===== Mode =====
if cmd == "mp":
client.ChangeMode(b1.RobotMode.kPrepare)
elif cmd == "md":
client.ChangeMode(b1.RobotMode.kDamping)
elif cmd == "mw":
client.ChangeMode(b1.RobotMode.kWalking)
elif cmd == "mc":
client.ChangeMode(b1.RobotMode.kCustom)
elif cmd == "sg":
if len(args) != 1:
print("Usage: sg <whole|half|halfv2>")
continue
gait_type = parse_gait_type(args[0])
client.SwitchGait(gait_type)
elif cmd == "lie":
client.LieDown()
elif cmd == "getup":
client.GetUp()
elif cmd == "gum":
if len(args) != 1:
print("Usage: gum <walking|soccer>")
continue
mode = parse_mode_name(args[0])
client.GetUpWithMode(mode)
# ===== Move =====
elif cmd == "stop":
x, y, yaw = 0.0, 0.0, 0.0
client.Move(x, y, yaw)
need_print = True
elif cmd == "w":
x, y, yaw = 0.8, 0.0, 0.0
client.Move(x, y, yaw)
need_print = True
elif cmd == "a":
x, y, yaw = 0.0, 0.6, 0.0
client.Move(x, y, yaw)
need_print = True
elif cmd == "s":
x, y, yaw = -0.6, 0.0, 0.0
client.Move(x, y, yaw)
need_print = True
elif cmd == "d":
x, y, yaw = 0.0, -0.6, 0.0
client.Move(x, y, yaw)
need_print = True
elif cmd == "q":
x, y, yaw = 0.0, 0.0, 0.6
client.Move(x, y, yaw)
need_print = True
elif cmd == "e":
x, y, yaw = 0.0, 0.0, -0.6
client.Move(x, y, yaw)
need_print = True
# ===== Head =====
elif cmd == "hd":
head_yaw, head_pitch = 0.0, 1.0
client.RotateHead(head_pitch, head_yaw)
need_print = True
elif cmd == "hu":
head_yaw, head_pitch = 0.0, -0.3
client.RotateHead(head_pitch, head_yaw)
need_print = True
elif cmd == "hr":
head_yaw, head_pitch = -0.785, 0.0
client.RotateHead(head_pitch, head_yaw)
need_print = True
elif cmd == "hl":
head_yaw, head_pitch = 0.785, 0.0
client.RotateHead(head_pitch, head_yaw)
need_print = True
elif cmd == "ho":
head_yaw, head_pitch = 0.0, 0.0
client.RotateHead(head_pitch, head_yaw)
need_print = True
elif cmd == "rhd":
# Example for RotateHeadWithDirection: pitch +1, yaw 0
client.RotateHeadWithDirection(1, 0)
elif cmd == "rht":
if len(args) != 3:
print("Usage: rht <pitch> <yaw> <time_millis>")
continue
head_pitch = float(args[0])
head_yaw = float(args[1])
time_millis = int(args[2])
client.RotateHeadWithTime(head_pitch, head_yaw, time_millis)
need_print = True
print(f"Head duration: {time_millis} ms")
# ===== Wave / Handshake =====
elif cmd == "wave":
# Use HandAction to open or close the hand
action = b1.HandAction.kHandOpen
if args and args[0].lower() == "close":
action = b1.HandAction.kHandClose
client.WaveHand(action)
elif cmd == "shake":
# Example for Handshake
action = b1.HandAction.kHandOpen
client.Handshake(action)
# ===== Hand end-effector =====
elif cmd == "mhel":
# Left hand
tar = b1.Posture()
tar.position = b1.Position(0.35, 0.25, 0.1)
tar.orientation = b1.Orientation(-1.57, -1.57, 0.0)
client.MoveHandEndEffectorV2(
tar, 2000, b1.HandIndex.kLeftHand
)
time.sleep(4)
# Right hand
tar = b1.Posture()
tar.position = b1.Position(0.35, -0.25, 0.1)
tar.orientation = b1.Orientation(1.57, -1.57, 0.0)
client.MoveHandEndEffectorV2(
tar, 2000, b1.HandIndex.kRightHand
)
elif cmd == "mhe":
# Single-hand MoveHandEndEffector example
if len(args) != 1:
print("Usage: mhe <l|r>")
continue
hand = parse_lr(args[0])
tar = b1.Posture()
if hand == b1.HandIndex.kLeftHand:
tar.position = b1.Position(0.30, 0.25, 0.20)
else:
tar.position = b1.Position(0.30, -0.25, 0.20)
tar.orientation = b1.Orientation(0.0, -1.0, 0.0)
client.MoveHandEndEffector(tar, 1500, hand)
elif cmd == "mhedual":
left_target = b1.Posture()
left_target.position = b1.Position(0.35, 0.25, 0.10)
left_target.orientation = b1.Orientation(-1.57, -1.57, 0.0)
right_target = b1.Posture()
right_target.position = b1.Position(0.35, -0.25, 0.10)
right_target.orientation = b1.Orientation(1.57, -1.57, 0.0)
client.MoveDualHandEndEffector(left_target, right_target, 2000)
elif cmd == "mheaux":
# Example for MoveHandEndEffectorWithAux
hand = b1.HandIndex.kRightHand
tar = b1.Posture()
tar.position = b1.Position(0.30, -0.25, 0.15)
tar.orientation = b1.Orientation(0.0, -0.5, 0.0)
aux = b1.Posture()
aux.position = b1.Position(0.25, -0.30, 0.10)
aux.orientation = b1.Orientation(0.0, 0.0, 0.0)
client.MoveHandEndEffectorWithAux(tar, aux, 1800, hand)
elif cmd == "hstop":
client.StopHandEndEffector()
# ===== Gripper =====
elif cmd == "gopenl":
mp = b1.GripperMotionParameter()
mp.position = 500
mp.force = 100
mp.speed = 100
client.ControlGripper(
mp,
b1.GripperControlMode.kPosition,
b1.HandIndex.kLeftHand,
)
elif cmd == "gclosel":
mp = b1.GripperMotionParameter()
mp.position = 100
mp.force = 300
mp.speed = 200
client.ControlGripper(
mp,
b1.GripperControlMode.kPosition,
b1.HandIndex.kLeftHand,
)
# ===== Hand end-effector control mode =====
elif cmd == "hcm-start":
client.SwitchHandEndEffectorControlMode(True)
elif cmd == "hcm-stop":
client.SwitchHandEndEffectorControlMode(False)
# ===== Dexterous hand =====
elif cmd in ("dex-open", "dex-close"):
if len(args) != 1:
print(f"Usage: {cmd} <l|r>")
continue
hand = parse_lr(args[0])
fingers = []
# Build parameters for five fingers
for seq in range(5):
if cmd == "dex-open":
angle = 800
else:
angle = 200
fp = b1.DexterousFingerParameter(
seq, angle, 500, 500
)
fingers.append(fp)
client.ControlDexterousHand(
fingers, hand, b1.BoosterHandType.kInspireHand
)
# ===== Dance / whole-body motion =====
elif cmd == "dance":
if len(args) != 1:
print("Usage: dance <newyear/nezha/tf/dab/ultra/respect/cheer/lucky/stop>")
continue
did = parse_dance_id(args[0])
client.Dance(did)
elif cmd == "wbd":
if len(args) != 1:
print("Usage: wbd <arbic/m1/m2/m3/boxkick/roundkick/shanhe/gaige>")
continue
wid = parse_whole_body_dance_id(args[0])
client.WholeBodyDance(wid)
elif cmd == "vk":
if len(args) not in (1, 2) or args[0] not in ("on", "off"):
print("Usage: vk <on|off> [v1|v2]")
continue
version = (
parse_visual_kick_version(args[1])
if len(args) == 2
else b1.VisualKickVersion.kV1
)
client.VisualKick(args[0] == "on", version)
# ===== Sound =====
elif cmd == "ps":
if len(args) != 1:
print("Usage: ps <sound_file_path>")
continue
path = args[0]
client.PlaySound(path)
elif cmd == "ss":
client.StopSound()
# ===== Light =====
elif cmd == "sl":
if len(args) != 3:
print("Usage: sl <r> <g> <b>")
continue
r, g, b = map(int, args)
client.SetLEDLightColor(r, g, b)
# ===== Odom =====
elif cmd == "ro":
client.ResetOdometry()
# ===== Trajectory =====
elif cmd == "rtrec":
if len(args) != 1 or args[0] not in ("on", "off"):
print("Usage: rtrec <on|off>")
continue
enable = args[0] == "on"
client.RecordTrajectory(enable)
elif cmd == "rtreplay":
if len(args) != 1:
print("Usage: rtreplay <traj_file_path>")
continue
path = args[0]
client.ReplayTrajectory(path)
# ===== Custom Trained Trajectory =====
elif cmd == "load_traj":
if len(args) != 1:
print("Usage: load_traj <json_file_path>")
continue
json_path = args[0]
# Check file existence
if not Path(json_path).is_file():
print(f"Error: file not found: {json_path}")
continue
# Read JSON content
with open(json_path, 'r', encoding='utf-8') as f:
content = f.read()
# Parse JSON string to C++ object
try:
traj_obj = b1.CustomTrainedTraj.from_json_str(content)
tid = client.LoadCustomTrainedTraj(traj_obj)
print(f"Trajectory loaded successfully. TID: {tid}")
except Exception as e:
print(f"Error loading trajectory: {e}")
elif cmd == "act_traj":
if len(args) != 1:
print("Usage: act_traj <tid>")
continue
tid = args[0]
client.ActivateCustomTrainedTraj(tid)
print(f"Activated trajectory: {tid}")
elif cmd == "unload_traj":
if len(args) != 1:
print("Usage: unload_traj <tid>")
continue
tid = args[0]
client.UnloadCustomTrainedTraj(tid)
print(f"Unloaded trajectory: {tid}")
# ===== Zero torque / upper-body custom control =====
elif cmd == "zt":
if len(args) != 1 or args[0] not in ("on", "off"):
print("Usage: zt <on|off>")
continue
client.ZeroTorqueDrag(args[0] == "on")
elif cmd == "ubcc":
if len(args) != 1 or args[0] not in ("on", "off"):
print("Usage: ubcc <on|off>")
continue
client.UpperBodyCustomControl(args[0] == "on")
# ===== Query / information =====
elif cmd == "gm":
gm = client.GetMode()
print(f"Current mode: {gm.mode}")
elif cmd == "gs":
st = client.GetStatus()
print(f"Current mode : {st.current_mode}")
print(f"Current body ctrl : {st.current_body_control}")
if not st.current_actions:
print("Current actions : []")
else:
print("Current actions :")
for line in format_actions(st.current_actions):
print(f" - {line}")
elif cmd == "gri":
info = client.GetRobotInfo()
print(f"Name : {info.name}")
print(f"Nickname: {info.nickname}")
print(f"Version : {info.version}")
print(f"Model : {info.model}")
elif cmd == "gft":
src = b1.Frame.kBody
dst = b1.Frame.kLeftHand
tf = client.GetFrameTransform(src, dst)
p = tf.position
q = tf.orientation
print(
f"Transform Body -> LeftHand: "
f"pos=({p.x:.3f}, {p.y:.3f}, {p.z:.3f}), "
f"quat=({q.x:.3f}, {q.y:.3f}, {q.z:.3f}, {q.w:.3f})"
)
# ===== Low-level SendApiRequest + JSON example (ChangeMode) =====
elif cmd == "raw_change_mode":
if len(args) != 1:
print("Usage: raw_change_mode <prepare|damping|walking|custom>")
continue
mode = parse_mode_name(args[0])
# Serialize ChangeModeParameter to JSON
param = b1.ChangeModeParameter(mode)
json_str = param.to_json_str()
client.SendApiRequest(
int(b1.LocoApiId.kChangeMode),
json_str,
)
print("raw ChangeMode sent")
elif cmd == "raw_vk":
if len(args) not in (1, 2) or args[0] not in ("on", "off"):
print("Usage: raw_vk <on|off> [v1|v2]")
continue
version = (
parse_visual_kick_version(args[1])
if len(args) == 2
else b1.VisualKickVersion.kV1
)
param = b1.VisualKickParameter(args[0] == "on", version)
client.SendApiRequest(
b1.LocoApiId.kVisualKick,
param.to_json_str(),
)
print("raw VisualKick sent")
elif cmd == "raw_status":
resp = client.SendApiRequestWithResponse(
b1.LocoApiId.kGetStatus,
"",
)
print("[OK] SendApiRequestWithResponse returned:", resp)
else:
print(f"Unknown command: {raw}")
print("Type 'help' to see all commands.")
continue
if need_print:
print(f"Move param: x={x} y={y} yaw={yaw}")
print(f"Head param: pitch={head_pitch} yaw={head_yaw}")
except Exception as e:
# Catch runtime_error thrown by pybind when the underlying API returns a non-zero code
print(f"Request failed: {e}")
if __name__ == "__main__":
main()