#!/usr/bin/env bash set -euo pipefail # Unified launcher for TurtleBot3 office workflow: # - Terminal A: simulation / bridge / (optional SLAM) / RViz # - Terminal B: Nav2 # - Optional C: explore_lite # - Optional websocket + gzweb-demo frontend # # Modes: # mapping : online SLAM mapping # localization : map-based localization/navigation # # Mapping mode interactive commands: # s : save map and switch to localization mode # q : stop all # # Examples: # ./run_all.sh mapping --explore --gzweb # ./run_all.sh localization --gzweb --map /abs/path/office_map.yaml ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODE="${1:-mapping}" shift || true ROBOT_MODEL="${ROBOT_MODEL:-waffle}" USE_SIM_TIME="${USE_SIM_TIME:-true}" GZ_PARTITION="${GZ_PARTITION:-tb3_office_gz}" WORLD_PATH_DEFAULT="${ROOT}/turtlebot3_simulations/turtlebot3_gazebo/worlds/office_gz_dartsim.sdf" MAP_YAML_DEFAULT="${ROOT}/turtlebot3_simulations/turtlebot3_gazebo/map/office_map.yaml" MAP_BASENAME_DEFAULT="${ROOT}/turtlebot3_simulations/turtlebot3_gazebo/map/office_map" SLEEP_NAV2="${SLEEP_NAV2:-18}" EXPLORE_WAIT_TIMEOUT="${EXPLORE_WAIT_TIMEOUT:-420}" WORLD_PATH="${WORLD_PATH:-$WORLD_PATH_DEFAULT}" MAP_YAML="${NAV2_MAP_PATH:-$MAP_YAML_DEFAULT}" MAP_BASENAME="${MAP_BASENAME:-$MAP_BASENAME_DEFAULT}" WITH_EXPLORE=false WITH_GZWEB=false NO_RVIZ=false # 与 --gzweb 同用:只起 gz websocket(9002) 等资源,不起 office 内 npm run dev;页面用镜像内 /opt/gzweb(foundation) NO_GZWEB_VITE=false while [[ $# -gt 0 ]]; do case "$1" in --explore) WITH_EXPLORE=true; shift ;; --gzweb) WITH_GZWEB=true; shift ;; --no-gzweb-vite) NO_GZWEB_VITE=true; shift ;; --no-rviz) NO_RVIZ=true; shift ;; --map) MAP_YAML="$2"; shift 2 ;; --world) WORLD_PATH="$2"; shift 2 ;; --help|-h) cat <<'EOF' Usage: ./run_all.sh mapping [--explore] [--gzweb] [--no-gzweb-vite] [--no-rviz] [--world PATH] ./run_all.sh localization [--gzweb] [--no-gzweb-vite] [--no-rviz] [--map PATH] [--world PATH] --no-gzweb-vite 需与 --gzweb 同时使用:不启动 workspace 内 gzweb-demo 的 Vite;浏览器用 /opt/gzweb(8000)。 EOF exit 0 ;; *) echo "[run_all] unknown argument: $1" >&2 exit 1 ;; esac done if [[ "$MODE" != "mapping" && "$MODE" != "localization" ]]; then echo "[run_all] mode must be mapping or localization" >&2 exit 1 fi if [[ ! -f "$WORLD_PATH" ]]; then echo "[run_all] world not found: $WORLD_PATH" >&2 exit 1 fi if [[ "$MODE" == "localization" && ! -f "$MAP_YAML" ]]; then echo "[run_all] map yaml not found: $MAP_YAML" >&2 exit 1 fi set +u source /opt/ros/humble/setup.bash source "${ROOT}/install/setup.bash" set -u export TURTLEBOT3_MODEL="$ROBOT_MODEL" LOG_DIR="${ROOT}/logs/run_all_$(date +%Y%m%d_%H%M%S)" mkdir -p "$LOG_DIR" mkdir -p "$(dirname "$MAP_BASENAME")" PIDS=() start_bg() { local name="$1"; shift local logfile="${LOG_DIR}/${name}.log" echo "[run_all] starting ${name} ..." "$@" >"$logfile" 2>&1 & local pid=$! PIDS+=("$pid") echo "[run_all] ${name} pid=${pid}, log=${logfile}" } cleanup() { echo "[run_all] stopping all processes..." for pid in "${PIDS[@]:-}"; do kill "$pid" 2>/dev/null || true done } trap cleanup EXIT INT TERM wait_for_topic() { local topic="$1" local timeout="${2:-60}" local i=0 while (( i < timeout )); do if ros2 topic list 2>/dev/null | grep -Fxq "${topic}"; then return 0 fi sleep 1 ((i+=1)) done return 1 } wait_for_port() { local port="$1" local timeout="${2:-30}" local i=0 while (( i < timeout )); do if ss -ltn 2>/dev/null | grep -q ":${port} "; then return 0 fi sleep 1 ((i+=1)) done return 1 } wait_for_explore_stop_log() { local logfile="$1" local timeout="${2:-900}" local i=0 while (( i < timeout )); do if [[ -f "$logfile" ]] && grep -Eq "All frontiers traversed/tried out, stopping\\.|Exploration stopped\\." "$logfile"; then return 0 fi sleep 1 ((i+=1)) done return 1 } wait_for_explore_stop_log_with_timer() { local logfile="$1" local timeout="${2:-900}" local start_ts start_ts="$(date +%s)" local i=0 while (( i < timeout )); do if [[ -f "$logfile" ]] && grep -Eq "All frontiers traversed/tried out, stopping\\.|Exploration stopped\\." "$logfile"; then return 0 fi if (( i % 10 == 0 )); then local now_ts elapsed now_ts="$(date +%s)" elapsed=$(( now_ts - start_ts )) echo "[run_all] explore elapsed: ${elapsed}s" fi sleep 1 ((i+=1)) done return 1 } kill_port_listeners() { local port="$1" local pids pids="$(ss -ltnp 2>/dev/null | grep ":${port} " | sed -E 's/.*pid=([0-9]+).*/\1/' | sort -u || true)" if [[ -n "${pids}" ]]; then echo "[run_all] port ${port} already in use, killing: ${pids}" kill ${pids} 2>/dev/null || true sleep 1 fi } show_rviz_arg="show_rviz:=true" if [[ "$NO_RVIZ" == true ]]; then show_rviz_arg="show_rviz:=false" fi if [[ "$MODE" == "mapping" ]]; then start_bg "A_sim" \ ros2 launch turtlebot3_gazebo turtlebot3_office_gz.launch.py \ robot_model:="$ROBOT_MODEL" \ use_sim_time:="$USE_SIM_TIME" \ start_slam:=true \ "$show_rviz_arg" \ gz_partition:="$GZ_PARTITION" \ gz_world_name:=default \ world:="$WORLD_PATH" else start_bg "A_sim" \ ros2 launch turtlebot3_gazebo turtlebot3_office_gz.launch.py \ robot_model:="$ROBOT_MODEL" \ use_sim_time:="$USE_SIM_TIME" \ start_slam:=false \ "$show_rviz_arg" \ gz_partition:="$GZ_PARTITION" \ gz_world_name:=default \ world:="$WORLD_PATH" fi # --gzweb:gz launch(9002) + 可选 office 内 gzweb-demo 的 Vite(5173)。 # --gzweb --no-gzweb-vite:不起 office Vite;页面用 foundation 已在 /opt/gzweb:8000 起的实例(见 run_all_fix.sh)。 if [[ "$WITH_GZWEB" == true ]]; then export GZ_PARTITION OFFICE_MODEL_ROOT="${ROOT}/turtlebot3_simulations/turtlebot3_gazebo/models/turtlebot3_office" RESOURCE_PATHS=( "${HOME}/.gazebo/models" "${ROOT}/turtlebot3_simulations/turtlebot3_gazebo/models" "${OFFICE_MODEL_ROOT}" ) for d in "${OFFICE_MODEL_ROOT}"/*/materials/scripts "${OFFICE_MODEL_ROOT}"/*/materials/textures; do [[ -d "${d}" ]] && RESOURCE_PATHS+=("${d}") done RESOURCE_PATH_JOINED="$(IFS=:; echo "${RESOURCE_PATHS[*]}")" export GZ_SIM_RESOURCE_PATH="${RESOURCE_PATH_JOINED}" # Keep compatibility with components resolving textures via legacy lookup vars. export GZ_FILE_PATH="${RESOURCE_PATH_JOINED}" export GAZEBO_RESOURCE_PATH="${RESOURCE_PATH_JOINED}" if [[ "$NO_GZWEB_VITE" != true ]]; then if [[ ! -d "${ROOT}/gzweb-demo/gzweb-demo/node_modules/gzweb" ]]; then echo "[run_all] gzweb demo dependencies missing, running npm install once..." bash -lc "cd '${ROOT}/gzweb-demo/gzweb-demo' && npm install" fi else echo "[run_all] --no-gzweb-vite: skipping office gzweb-demo npm install; use /opt/gzweb (port 8000) for UI." fi kill_port_listeners 9002 kill_port_listeners 5173 echo "[run_all] waiting for /clock before websocket..." if wait_for_topic "/clock" 25; then start_bg "D_websocket" gz launch "${ROOT}/websocket.sdf" if [[ "$NO_GZWEB_VITE" != true ]]; then start_bg "E_gzweb_frontend" bash -lc "cd '${ROOT}/gzweb-demo/gzweb-demo' && npm run dev:host" echo "[run_all] office gzweb-demo Vite: http://localhost:5173" fi else echo "[run_all] WARN: /clock not detected in time; starting websocket (and optional frontend) anyway." start_bg "D_websocket" gz launch "${ROOT}/websocket.sdf" if [[ "$NO_GZWEB_VITE" != true ]]; then start_bg "E_gzweb_frontend" bash -lc "cd '${ROOT}/gzweb-demo/gzweb-demo' && npm run dev:host" fi fi if wait_for_port 9002 12; then echo "[run_all] websocket server ready on ws://localhost:9002" else echo "[run_all] ERROR: websocket server failed on 9002; check ${LOG_DIR}/D_websocket.log" fi fi echo "[run_all] waiting ${SLEEP_NAV2}s before starting Nav2..." sleep "$SLEEP_NAV2" if [[ "$MODE" == "mapping" ]]; then start_bg "B_nav2" ros2 launch nav2_bringup navigation_launch.py use_sim_time:="$USE_SIM_TIME" else start_bg "B_nav2" \ ros2 launch nav2_bringup bringup_launch.py \ use_sim_time:="$USE_SIM_TIME" \ map:="$MAP_YAML" \ slam:=false fi if [[ "$WITH_EXPLORE" == true && "$MODE" == "mapping" ]]; then start_bg "C_explore" ros2 launch explore_lite explore.launch.py use_sim_time:="$USE_SIM_TIME" last_index=$(( ${#PIDS[@]} - 1 )) EXPLORE_PID="${PIDS[$last_index]}" EXPLORE_LOG="${LOG_DIR}/C_explore.log" fi echo echo "[run_all] started. Mode: ${MODE}" echo "[run_all] logs: ${LOG_DIR}" echo if [[ "$MODE" == "mapping" ]]; then if [[ "$WITH_GZWEB" == true ]]; then if [[ "$NO_GZWEB_VITE" == true ]]; then echo "[run_all] UI: foundation /opt/gzweb on port 8000 (no office Vite)." elif wait_for_port 5173 25; then echo "[run_all] office gzweb-demo ready: http://localhost:5173" else echo "[run_all] WARN: office gzweb frontend not ready yet, check ${LOG_DIR}/E_gzweb_frontend.log" fi fi if [[ "${WITH_EXPLORE}" == true && -n "${EXPLORE_PID:-}" ]]; then echo "[run_all] exploration is running (pid=${EXPLORE_PID}), waiting for stop signal (timeout=${EXPLORE_WAIT_TIMEOUT}s)..." if wait_for_explore_stop_log_with_timer "${EXPLORE_LOG}" "${EXPLORE_WAIT_TIMEOUT}"; then echo "[run_all] detected exploration stop from log, finalizing explore process..." else echo "[run_all] WARN: explore stop not detected within timeout; forcing stop and saving current map." fi kill "${EXPLORE_PID}" 2>/dev/null || true wait "${EXPLORE_PID}" 2>/dev/null || true echo "[run_all] explore_lite stopped, now saving map." fi if [[ "${WITH_EXPLORE}" == true ]]; then echo "[run_all] auto-saving map to ${MAP_BASENAME}.yaml/.pgm ..." ros2 run nav2_map_server map_saver_cli -f "$MAP_BASENAME" --ros-args -p use_sim_time:="$USE_SIM_TIME" echo "[run_all] map saved successfully:" echo " - ${MAP_BASENAME}.yaml" echo " - ${MAP_BASENAME}.pgm" echo "[run_all] mapping workflow completed, exiting now." exit 0 fi cat <