#!/bin/bash # ROS AGV 公共库 # 提供生产脚本共享的配置、清理与验证函数 set -euo pipefail # ============================================================================ # 配置(可通过环境变量覆盖) # ============================================================================ readonly AGV_PROJECT_DIR="${AGV_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" readonly AGV_APP_DIR="${AGV_APP_DIR:-$AGV_PROJECT_DIR/agv_app}" readonly AGV_ROS2_DIR="${AGV_ROS2_DIR:-$HOME/agv_pro_ros2}" readonly ROS_SETUP="${ROS_SETUP:-/opt/ros/humble/setup.bash}" readonly ROS_WORKSPACE_SETUP="${ROS_WORKSPACE_SETUP:-$AGV_ROS2_DIR/install/setup.bash}" readonly SCAN_FIXER_DIR="${SCAN_FIXER_DIR:-$AGV_PROJECT_DIR/scan_fixer}" readonly LOG_DIR="${LOG_DIR:-/tmp}" readonly FASTRTPS_SHM_DIR="${FASTRTPS_SHM_DIR:-/dev/shm}" readonly AGV_CONTROLLER_DEVICE="${AGV_CONTROLLER_DEVICE:-/dev/agvpro_controller}" readonly UV_BIN="${UV_BIN:-}" export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-1}" # 日志文件 readonly BRINGUP_LOG="$LOG_DIR/ros2_bringup.log" readonly NAV2_LOG="$LOG_DIR/ros2_nav2.log" readonly CLOCK_LOG="$LOG_DIR/clock_publisher.log" readonly SCAN_FIXER_LOG="$LOG_DIR/scan_fixer.log" readonly FLASK_LOG="$LOG_DIR/agv_flask.log" # ============================================================================ # 进程管理 # ============================================================================ # ROS 相关进程列表(用于清理) readonly ROS_PROCESSES=( "ros2 launch agv_pro_bringup" "ros2 launch agv_pro_navigation2" "agv_pro_node" "lslidar_driver_node" "component_container" "robot_state_publisher" "fix_scan_timestamp" "clock_publisher" "python.*app.py" "uv run .*python app.py" ) # 软杀所有进程 kill_all_soft() { echo " 软杀进程中..." for proc in "${ROS_PROCESSES[@]}"; do pkill -f "$proc" 2>/dev/null || true done sleep 2 } # 硬杀所有进程 kill_all_hard() { echo " 强制终止中..." for proc in "${ROS_PROCESSES[@]}"; do pkill -9 -f "$proc" 2>/dev/null || true done sleep 1 } # 统计匹配进程数 count_matching_processes() { local pattern=$1 local current_pid=$$ local shell_pid=$BASHPID local parent_pid=$PPID local count=0 local pid local args while read -r pid args; do if [ -z "${pid:-}" ]; then continue fi if [ "$pid" = "$current_pid" ] || [ "$pid" = "$shell_pid" ] || [ "$pid" = "$parent_pid" ]; then continue fi if [[ "${args:-}" =~ $pattern ]]; then count=$((count + 1)) fi done < <(ps -eo pid=,args=) echo "$count" } # 统计残留进程数 count_residual_processes() { count_matching_processes "agv_pro_node|lslidar_driver_node|component_container|fix_scan_timestamp|clock_publisher|app.py|ros2-daemon" } # ============================================================================ # FastRTPS 清理 # ============================================================================ # 清理 FastRTPS 共享内存 cleanup_fastrtps() { local count count=$(count_fastrtps_files) if [ "$count" -gt 0 ]; then rm -rf "$FASTRTPS_SHM_DIR"/fastrtps_* echo " 已清理 $count 个 FastRTPS 文件" else echo " 无 FastRTPS 文件残留" fi # 清理锁文件 rm -f /tmp/scan_fixer.lock /tmp/clock_publisher.lock } # 统计 FastRTPS 文件数 count_fastrtps_files() ( shopt -s nullglob local files=("$FASTRTPS_SHM_DIR"/fastrtps_*) echo "${#files[@]}" ) # 载入 ROS2 环境后执行命令 ros2_exec() { bash -c ' source "$1" || exit 1 if [ -f "$2" ]; then source "$2" || exit 1 fi export ROS_DOMAIN_ID="$3" shift 3 "$@" ' _ "$ROS_SETUP" "$ROS_WORKSPACE_SETUP" "$ROS_DOMAIN_ID" "$@" } resolve_uv_bin() { if [ -n "$UV_BIN" ]; then if [ -x "$UV_BIN" ]; then echo "$UV_BIN" return 0 fi return 1 fi local candidate candidate=$(command -v uv 2>/dev/null || true) if [ -n "$candidate" ]; then echo "$candidate" return 0 fi for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do if [ -x "$candidate" ]; then echo "$candidate" return 0 fi done return 1 } # ============================================================================ # ROS2 环境操作 # ============================================================================ ros2_topic_list() { ros2_exec ros2 topic list 2>/dev/null || true } ros2_topic_count() { local topics topics=$(ros2_exec timeout "${1:-5}" ros2 topic list 2>/dev/null || true) if [ -z "$topics" ]; then echo 0 else printf '%s\n' "$topics" | sed '/^$/d' | wc -l fi } topic_exists() { ros2_topic_list | grep -Fxq "$1" } # 启动 ROS2 daemon start_ros2_daemon() { echo " 启动 ros2 daemon..." rm -rf "$FASTRTPS_SHM_DIR"/fastrtps_* 2>/dev/null || true nohup bash -c ' source "$1" || exit 1 export ROS_DOMAIN_ID="$2" ros2 daemon start ' _ "$ROS_SETUP" "$ROS_DOMAIN_ID" >/dev/null 2>&1 & sleep 4 # 等待 daemon 就绪 for _ in $(seq 1 5); do if ros2_exec timeout 3 ros2 topic list &>/dev/null; then echo " [OK] ros2 daemon 已就绪" return 0 fi sleep 2 done echo " [WARN] ros2 daemon 可能有问题" return 1 } # 停止 ROS2 daemon stop_ros2_daemon() { echo " 重置 ros2 daemon..." pkill -f "ros2-daemon" 2>/dev/null || true pkill -9 -f "ros2-daemon" 2>/dev/null || true sleep 2 bash -c ' source "$1" || exit 0 export ROS_DOMAIN_ID="$2" ros2 daemon stop ' _ "$ROS_SETUP" "$ROS_DOMAIN_ID" 2>/dev/null || true echo " [OK] ros2 daemon 已重置" } # ============================================================================ # 等待/验证函数 # ============================================================================ # 等待话题出现 # 用法: wait_for_topic <话题名> <最大等待秒数> wait_for_topic() { local topic=$1 local max_wait=${2:-30} local elapsed=0 while [ "$elapsed" -lt "$max_wait" ]; do if topic_exists "$topic"; then echo " [OK] $topic 已上线" return 0 fi sleep 2 elapsed=$((elapsed + 2)) done echo " [WARN] $topic 未在 $max_wait 秒内上线" return 1 } # 等待节点出现(匹配数量) # 用法: wait_for_nodes <节点模式> <期望数量> <最大等待秒数> wait_for_nodes() { local pattern=$1 local expected=$2 local max_wait=${3:-30} local elapsed=0 local count=0 while [ "$elapsed" -lt "$max_wait" ]; do local nodes nodes=$(ros2_exec timeout 8 ros2 node list --no-daemon --spin-time 3 2>/dev/null || true) count=$(printf '%s\n' "$nodes" | grep -cE "$pattern" || true) if [ "$count" -ge "$expected" ]; then echo " [OK] 已检测到 $count 个节点" return 0 fi sleep 2 elapsed=$((elapsed + 2)) done echo " [WARN] 仅检测到 $count 个节点(期望 $expected 个)" return 1 } # ============================================================================ # 日志/输出辅助 # ============================================================================ # 打印分节标题 section() { echo "" echo "==========================================" echo " $1" echo "==========================================" } # 打印步骤 step() { echo "" echo "[$1] $2" } # 打印带状态的信息 info() { local status=$1 local msg=$2 if [ "$status" = "ok" ]; then echo " [OK] $msg" elif [ "$status" = "warn" ]; then echo " [WARN] $msg" elif [ "$status" = "err" ]; then echo " [ERROR] $msg" else echo " $msg" fi } # 显示日志尾部 show_log_tail() { local log_file=$1 local lines=${2:-5} if [ -f "$log_file" ]; then echo " --- 日志尾部 ($log_file) ---" tail -"$lines" "$log_file" 2>/dev/null | sed 's/^/ /' || true fi } # ============================================================================ # 完整清理流程 # ============================================================================ # 执行完整清理(供 stop_all.sh 使用) full_cleanup() { section "Robot AGV 全量停止" step "1/5" "软杀所有相关进程" kill_all_soft step "2/5" "强制终止残留进程" kill_all_hard step "3/5" "重置 ros2 daemon" stop_ros2_daemon step "4/5" "清理 FastRTPS 共享内存" cleanup_fastrtps step "5/5" "验证清理结果" local proc_count=$(count_residual_processes) local fastrtps_left=$(count_fastrtps_files) echo " 残留进程数: $proc_count" echo " FastRTPS 文件数: $fastrtps_left" if [ "$proc_count" -eq 0 ] && [ "$fastrtps_left" -eq 0 ]; then section "[OK] 停止完成 - 系统已完全清理" else section "[WARN] 停止完成 - 部分残留可能需要手动清理" echo "" echo " 手动清理命令(如需要):" echo " pkill -9 -f 'agv_pro_node|lslidar|component_container'" echo " pkill -9 -f 'fix_scan_timestamp|app.py'" echo " pkill -9 -f 'ros2-daemon'" echo " rm -rf \"$FASTRTPS_SHM_DIR\"/fastrtps_*" fi echo "" echo " 现在可以安全运行 ./scripts/prod-backend.sh" echo "" } # ============================================================================ # 初始化(确保目录存在) # ============================================================================ mkdir -p "$LOG_DIR"