从实战解决方案学习Shell

目录

工作空间的创建

需求:每次创建工作目录时都要 mkdir 一堆目录、手写像CMakeLists.txt、package.xml等等文件,既慢又容易漏,一键化生成目录脚本十分重要

接下来讲解将以注释的形式写到本文档的代码块里

#!/bin/bash 
#首先定义变量
PKG_NAME=${1:-my_robot} #这里表示PKG_NAME的值取决于第一个参数如果不是则默认为my_robot 示例:./init_workspace.sh robot_demo 这样PKG_NAME = robot_demo
WS_DIS="$HOME/catkin_ws"
SRC_DIR="WS_DIR/src"
echo "===初始化ROS工作空间==="
echo "包名:$PKG_NAME"
echo "路径:$SRC_DIR/$PKG_NAME"
echo " "
mkdir -p "$SRC_DIR" #mkdir -p 是递归创建常用于多级或多个目录
cd "$SRC_DIR"
PKG_DIR="$SRC_DIR/$PKG_NAME"
mkdir -p "$PKG_DIR"/{src.include/$PKG_NAME,launch,config,scripts} #这里是一键生成多个目录{}内根据,划分
#生成文件并在其中写入内容
cat > "$PKG_DIR/CMakeList.txt" << INNER   #这里并不是一定要写INNER也可以写其他的不过有个点"INNER"和INNER是有区别的带引号的里面的$PKG_NAME不会被替换成脚本里定义的变量
cmake_minimum_required(VERSION 3.0.2)
project($PKG_NAME)
find_package(catkin REQUIRED COMPONENTS
        roscpp
        rospy
        std_msgs
)

catkin_package()
INNER
if command -v tree > /dev/null 2>&1; then #获取是否有这个指令并将输出重定向并输入到系统自带的黑洞里 2>&1 是将错误信息重定向到正确输出里也丢进黑洞里
    tree "$PKG_DIR"
else
    echo "(装 tree 看结构: brew install tree / apt install tree)"
    ls -R "$PKG_DIR"
fi

文件归档

随着系统使用时间的增加日志,配置,脚本等等文件会越来越多堆起来手动归档真的很麻烦这里介绍一个自动按日期和类型归档的脚本(仅限于以日期开头命名的如20260804nav.log)
#!/bin/bash
SRC_DIR=${1:-raw_data}
ARChIVE_DIR="archived"
mkdir -p "$ARCHIVE_DIR"
echo "将 $SRC_DIR 目录下的文件归档到 $ARCHIVE_DIR 中"
archive_by_date() {  #将按日期归档写成函数之后在按类型归档里循环调用即可
    for date_prefix in $(ls "$1" | cut -c1-8 | sort -u); do #截取文件名的前8个字符
        target_dir="$1/$date_prefix" #在同级目录下创建以日期命名的目录
        mkdir -p "$target_dir"
        mv "$1"/${date_prefix}* "$target_dir/" 2>/dev/null #根据日期归档
        echo "已归档 $date_prefix 的文件到 $target_dir"
    done
}
for date_type in $(ls "$SRC_DIR" | cut -d'.' -f2-| sort -u); do #-d '.' -f- 是表示从第一个.之后到最后一个字符
    target_dir="$ARCHIVE_DIR/$date_type"
    mkdir -p "$target_dir"
    mv "$SRC_DIR"/*."$date_type" "$target_dir/" 2>/dev/null
    echo "已归档 .$date_type 的文件到 $target_dir"
    archive_by_date "$target_dir"
done

echo "归档完成!"

日志健康报告

一键分析日志
#!/bin/bash
exec > >(tee report.txt) 2>&1 #同时将输出和错误写入终端和report.txt(生成在同目录)里
DAY=$(date|cut -c1-19|tr ' ' '_') #date是系统指令(获取当前时间) 截取前面日期部分并删除空格和下划线(可能具体不是(中文系统的年月日也要占而且占2个字符)-c1-c19可以自己输出尝试几次获得具体截取切片范围)
LOG_FILE=${1:-logs/ros_${DAY}.log} 
LOG_FILE2=${2:-logs/ros_${DAY}.log} #日志文件以及对比文件
if [ ! -f "$LOG_FILE" ]; then
	echo "错误:日志文件$LOG_FILE不存在"
	exit 1
fi

if [ ! -f "$LOG_FILE2" ]; then
	echo "错误:日志文件$LOG_FILE2不存在"
	exit 1
fi
echo "=========================="
echo "ROS 日志健康报告"
echo "文件: $LOG_FILE"
echo "对比文件: $LOG_FILE2 "
echo "========================="
echo ""

#统计各级别数量
echo "【1】日志级别统计:"
echo "-----$LOG_FILE-----"
grep -oE '\[(INFO|WARN|ERROR)\]' $@ | awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn|echo " "
echo "-----$LOG_FILE2-----"
grep -oE '\[(INFO|WARN|ERROR)\]' "$LOG_FILE2" | awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn
#grep -oE 使用正则表达式只输出符合的部分   
#awk '{count[$0]++} END {for(i in count) print count[i], i}'这个的含义是遍历所有输入 将每行信息填入count[$0]中代替$0每有一个一样的行对对应的count[i]++; 最后遍历count并确定输出格式 键分别是INFO WARN ERROR
echo ""
#之后就是类似操作不加以赘述了
#统计ERROR涉及的节点
echo "【2】ERROR 涉及的节点(按次数排序)"
echo "-----------$LOG_FILE---------------------"
grep "\[ERROR\]" "$LOG_FILE" | grep -oE '\[[a-z_]+_node\]' | awk '{count[$0]++} END {for(i in count) print count[i], i}'  | sort -rn
echo "-----$LOG_FILE2-----"
grep "\[ERROR\]" "$LOG_FILE2" | grep -oE '\[[a-z_]+_node\]' | awk '{count[$0]++} END {for(i in count) print count[i], i}'  | sort -rn
echo ""

# 统计每个节点的总日志数
echo "【3】各节点日志数"
echo "--------------$LOG_FILE--------------------"
grep -oE '\[[a-z_]+_node\]' "$LOG_FILE" | awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn
echo "-----$LOG_FILE2-----"
grep -oE '\[[a-z_]+_node\]' "$LOG_FILE2" | awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn
echo ""

#统计所以错误信息
echo "所有错误信息:"
echo "----$LOG_FILE----"
grep -E '\[(ERROR)\]' "$LOG_FILE" 
echo "----$LOG_FILE2---"
grep -E '\[(ERROR)\]' "$LOG_FILE2" 
#关键词警告
echo "关键词告警"
grep -iE '\[(temperature|timeout|failed)\]' "$LOG_FILE"
grep -iE '\[(temperature|timeout|failed)\]' "$LOG_FILE2"
echo "---------"
#小时错误
echo "统计小时内出现的ERROR"
echo "----$LOG_FILE----"
grep -E '\[(ERROR)\]' "$LOG_FILE" | cut -c2-14 |awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn
echo "----$LOG_FILE2---"
grep -E '\[(ERROR)\]' "$LOG_FILE2"| cut -c2-14 |awk '{count[$0]++} END {for(i in count) print count[i], i}' | sort -rn 

echo "========================================"
echo "  分析完成"
echo "========================================"

日志文件如下

[2026-07-15 09:00:01] [INFO]  [nav_node] Navigation started, goal=(1.2, 3.4)
[2026-07-15 09:00:02] [INFO]  [slam_node] Map loaded: 50x50, resolution=0.05
[2026-07-15 09:01:15] [WARN]  [lidar_node] Point cloud rate dropped to 8Hz
[2026-07-15 11:02:30] [ERROR] [nav_node] Planning failed: no path found
[2026-07-15 09:02:31] [ERROR] [amcl_node] Pose covariance too high
[2026-07-15 09:03:00] [INFO]  [nav_node] Recovery behavior: rotate
[2026-07-15 09:03:45] [ERROR] [nav_node] Planning failed: no path found
[2026-07-15 09:04:00] [WARN]  [lidar_node] Point cloud rate dropped to 5Hz
[2026-07-15 10:05:00] [ERROR] [motor_node] Motor temperature: 85C
[2026-07-15 09:06:00] [INFO]  [nav_node] Goal reached

ROS环境初始化脚本

对于新设备或切换连接的设备而言一键配置好所有的环境变量对开发很重要
#!/bin/bash

if [ "$1" == "--help" ]; then
    echo "使用说明:"
    echo "1.本脚本要使用source执行"
    echo "2.要在脚本所在目录下创建robots.conf文件按robot1 192.168.0.0 humble的格式写入" #这里也可以将参数改为手动录入然后定位不同路径的配置文件
    echo "3.读取完机器人后请按编号选择"
    echo "4.查看历史: cat ~/.ros_env_history"
    return 0
fi
#检查开头是不是source开头的
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    echo "错误:请使用source执行本脚本"
    echo "正确输入: source $0 $@"
    return 0
fi

#读取配置文件
CONFIG_FILE="robots.conf"
HISTORY_FILE="$HOME/.ros_env_history"

#检查robots.conf文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
    echo "错误: 配置文件 $CONFIG_FILE 不存在"
    echo "请创建格式如下的配置文件:"
    echo "  scout_01 192.168.91.128 humble"
    echo "  scout_02 192.168.91.129 humble"
    return 1 2>/dev/null || exit 1
fi

#读取robots.conf文件里的所有机器人信息
#每次执行时要清空可能已经写入的内容
unset ROBOT_NAMES ROBOT_IPS ROBOT_DISTROS
#创建3个数组存储
declare -a ROBOT_NAMES
declare -a ROBOT_IPS
declare -a ROBOT_DISTROS
#按行读取并将信息加入数组
while IFS= read -r line || [ -n "$line" ]; do
    name=$(echo "$line" | awk '{print $1}')
    ip=$(echo "$line" | awk '{print $2}')
    distro=$(echo "$line" | awk '{print $3}')
    #当三个值都存在时继续
    if [ -n "$name" ] && [ -n "$ip" ] && [ -n "$distro" ]; then
        ROBOT_NAMES+=("$name")
        ROBOT_IPS+=("$ip")
        ROBOT_DISTROS+=("$distro")
    fi
done < "$CONFIG_FILE"

if [ ${#ROBOT_NAMES[@]} -eq 0 ]; then
    echo "没有从配置文件$CONFIG_FILE里读取到机器人的配置"
    return 1 2>/dev/null || exit 1
fi

#显示读取到的所有机器人
CURR_NAME=""
CURR_IP=""
CURR_DISTRO=""
#若参数存在查找其是否在配置文件了
if [ -n "$1" ]; then
    # 命令行指定了机器人名称
    for i in "${!ROBOT_NAMES[@]}"; do
        if [ "${ROBOT_NAMES[$i]}" = "$1" ]; then
            CURR_NAME="${ROBOT_NAMES[$i]}"
            CURR_IP="${ROBOT_IPS[$i]}"
            CURR_DISTRO="${ROBOT_DISTROS[$i]}"
            break
        fi
    done
    if [ -z "$CURR_NAME" ]; then
        echo "错误: 未找到机器人 '$1'"
        echo "可用机器人:"
        for i in "${!ROBOT_NAMES[@]}"; do
            echo "  ${ROBOT_NAMES[$i]}  ->  ${ROBOT_IPS[$i]}  (${ROBOT_DISTROS[$i]})"
        done
        return 1 2>/dev/null || exit 1
    fi
else
    # 不带参数时的交互性选择
    echo "======选择ROS机器人环境========"
    for i in "${!ROBOT_NAMES[@]}"; do
        echo "$((i+1)). name: ${ROBOT_NAMES[i]}, IP: ${ROBOT_IPS[i]}, DISTROS: ${ROBOT_DISTROS[i]}"
    done
    echo "输入对应编号的机器人进行选择"
    read -r INPUT
    #按编号查询(INPUT大于1小于总数)
    if [[ "$INPUT" =~ ^[0-9]+$ ]] && [ "$INPUT" -ge 1 ] && [ "$INPUT" -le ${#ROBOT_NAMES[@]} ]; then
        idx=$((INPUT - 1))
        CURR_NAME="${ROBOT_NAMES[$idx]}"
        CURR_IP="${ROBOT_IPS[$idx]}"
        CURR_DISTRO="${ROBOT_DISTROS[$idx]}"
    else
        echo "错误: 无效输入,请输入 1 到 ${#ROBOT_NAMES[@]} 之间的数字"
        return 1 2>/dev/null || exit 1
    fi
fi
#根据选择的机器人获取所需信息
ROBOT_NAME="$CURR_NAME"
MASTER_IP="$CURR_IP"
ROS_DISTRO="$CURR_DISTRO"
ROS_ROOT="/opt/ros/$ROS_DISTRO"

# ========== 加载 ROS 环境 ==========
# 1. 加载 ROS 全局环境
if [ -f "$ROS_ROOT/setup.bash" ]; then
    source "$ROS_ROOT/setup.bash"
    echo "[OK] 已加载 ROS $ROS_DISTRO 环境"
else
    echo "[警告] 未找到 $ROS_ROOT/setup.bash,请确认 ROS $ROS_DISTRO 已安装"
fi

# 2. 加载工作空间
WORKSPACE="$HOME/catkin_ws"
if [ -f "$WORKSPACE/devel/setup.bash" ]; then
    source "$WORKSPACE/devel/setup.bash"
    echo "[OK] 已加载工作空间 $WORKSPACE"
fi

# ========== 导出环境变量 ==========
export ROS_MASTER_URI="http://$MASTER_IP:11311"
export ROS_IP=$(hostname -I | awk '{print $1}')
export ROS_HOSTNAME=$ROS_IP

export ROBOT_NAME=$ROBOT_NAME
export ROS_LOG_DIR="$HOME/ros_logs/$ROBOT_NAME"
export BAG_STORAGE="$HOME/bags/$ROBOT_NAME"

# 创建目录
mkdir -p "$ROS_LOG_DIR" "$BAG_STORAGE"

# ========== 显示信息 ==========
echo ""
echo "=== 环境配置 ==="
echo "[OK] ROBOT_NAME     = $ROBOT_NAME"
echo "[OK] ROS_MASTER_URI = $ROS_MASTER_URI"
echo "[OK] ROS_IP         = $ROS_IP"
echo "[OK] ROS_LOG_DIR    = $ROS_LOG_DIR"
echo "[OK] BAG_STORAGE    = $BAG_STORAGE"
echo ""

# ========== 记录历史 ==========
HISTORY_ENTRY="$(date) | $ROBOT_NAME | $MASTER_IP | $ROS_DISTRO"

# 追加到历史文件
mkdir -p "$(dirname "$HISTORY_FILE")"
echo "$HISTORY_ENTRY" >> "$HISTORY_FILE"

# 只保留最近 500 条记录(防止文件过大)
if [ -f "$HISTORY_FILE" ] && [ $(wc -l < "$HISTORY_FILE") -gt 500 ]; then
    tail -n 500 "$HISTORY_FILE" > "$HISTORY_FILE.tmp"
    mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
fi

echo "[OK] 已记录环境切换历史 → $HISTORY_FILE"
echo ""
echo "=== 环境初始化完成 ==="
echo "提示: 可用 rosnode list / rostopic list 验证"
echo "查看历史: cat ~/.ros_env_history"

配置文件如下

scout_01 192.168.1.101 noetic
scout_02 192.168.1.102 melodic

机器人健康自检

#!/bin/bash

MASTER_IP=${1:-localhost}
#定义三个变量统计
PASS=0
FAIL=0
WARN=0
#写统计函数输出对应信息的同时让对应统计量加1
check() {
    local name=$1
    local result=$2
    if [ "$result" = "pass" ]; then
        echo "[PASS] $name"
        PASS=$((PASS+1))
    elif [ "$result" = "warn" ]; then
        echo "[WARN] $name"
        WARN=$((WARN+1))
    else
        echo "[FAIL] $name"
        FAIL=$((FAIL+1))
    fi
}

echo "========================================"
echo "  机器人健康自检"
echo "  目标 Master: $MASTER_IP"
echo "  时间: $(date)"
echo "========================================"
echo ""

# 1. 检查 ROS 环境
if [ -z "$ROS_DISTRO" ]; then
    check "ROS 环境" fail
else
    check "ROS 环境 (distro=$ROS_DISTRO)" pass
fi

# 2. 检查工作空间
if [ -d "$HOME/catkin_ws/src" ]; then
    check "工作空间存在" pass
else
    check "工作空间存在" warn
fi

# 3. 检查节点是否运行
LIST="nodes.list"
if [ ! -f "$LIST" ]; then
	echo "节点记录文件不存在"
	return 1 2>/dev/null || exit 1
fi
declare -a NODES
while IFS= read -r line || [ -n "$line" ]; do
	node=$(echo "$line")
	NODES+=("$node")
done <"$LIST"
if [ ${#NODES[@]} -eq 0 ]; then
	echo "没有从$LIST中读到节点信息"
	return 1 2>/dev/null || exit 1
fi
for (( i = 0; i < ${#NODES[@]}; i++)); do
	if pgrep -x "${NODES[$i]}" > /dev/null 2>&1 || pgrep -x rosmaster > /dev/null 2>&1; then #检查名为${NODES[$i]}的进程是否在运行
    	check "${NODES[$i]}运行中" pass
	else
    	check "${NODES[$i]}运行中" fail
	fi
done
#网络联通性
if ping -c 1 -W 2 "$MASTER_IP" > /dev/null 2>&1; then #发送一个ICMP数据包并让超时时间设为2秒
    check "Master 网络可达" pass
else
    check "Master 网络可达" fail
fi

# 5. 检查磁盘空间
DISK_USAGE=$(df -h "$HOME" | awk 'NR==2{print $5}' | tr -d '%') #截取df -h 的第五个字段
if [ "$DISK_USAGE" -lt 80 ]; then
    check "磁盘空间 ($DISK_USAGE% 已用)" pass
elif [ "$DISK_USAGE" -lt 90 ]; then
    check "磁盘空间 ($DISK_USAGE% 已用)" warn
else
    check "磁盘空间 ($DISK_USAGE% 已用)" fail
fi

# 6. 检查日志目录
if [ -d "$HOME/ros_logs" ]; then
    LOG_COUNT=$(find "$HOME/ros_logs" -name "*.log" | wc -l | tr -d ' ')
    check "日志目录 ($LOG_COUNT 个日志)" pass
else
    check "日志目录不存在" warn
fi
# 温度检查 - 只检查存在的路径  
TEMP_FILE="/sys/class/thermal/thermal_zone0/temp"
if [ -f "$TEMP_FILE" ]; then
    temp_raw=$(cat "$TEMP_FILE" 2>/dev/null)
    if [ -n "$temp_raw" ]; then
        temp=$((temp_raw / 1000))
        if [ $temp -gt 70 ]; then
            check "CPU温度${temp}°C" fail
        else
            check "CPU温度${temp}°C" pass
        fi
    else
        check "CPU温度读取失败" fail
    fi
else
    # 跳过温度检查(传感器不存在是正常情况)
    check "温度检查" skip
fi

# 时间检查
if command -v ntpq > /dev/null 2>&1; then
    ntp_stat=$(ntpq -p 2>/dev/null | grep -E '^\*' | awk '{print $9}')
    if [ -n "$ntp_stat" ]; then
        diff=$(echo "$ntp_stat" | awk '{print ($1<0)?-$1:$1}' | cut -d. -f1)
        if [ "$diff" -gt 1 ] 2>/dev/null; then
            check "NTP同步偏差${diff}秒" fail
        else
            check "NTP同步正常" pass
        fi
    else
        check "NTP未同步" skip
    fi
fi

echo ""
echo "========================================"
echo "  检查结果: PASS=$PASS  WARN=$WARN  FAIL=$FAIL"
echo "========================================"

# 根据结果决定退出码
if [ "$FAIL" -gt 0 ]; then
    echo "有 $FAIL 项检查失败,建议修复后再运行"
    exit 1
elif [ "$WARN" -gt 0 ]; then
    echo "有 $WARN 项警告,建议关注"
    exit 0
else
    echo "所有检查通过"
    exit 0
fi

一键启动/停止/监测节点

批量处理以及多任务管理也很重要

这里使用函数封装多个功能

#!/bin/bash

get_bag_files(){
	local dir="$1"
	if [ ! -d "$dir" ]; then
		echo -e "[ERROR] 目录"$dir" 不存在" >&2 #将错误信息输入到标准错误
		return 1
	fi
	find "$dir" -name "*.bag" -type f | sort #对目录里后缀为.bag的文件打印并排序
}

get_file_size(){
	du -h "$1" | cut -f1 #du -h 读取磁盘使用情况然后以人类可读的情况输出然后用cut提取第一个字段
}

get_file_size_bytes() {
    stat -c%s "$1" 2>/dev/null|| echo "0" #stat显示文件状态信息 -c%s 指定s代表的文件大小输出  丢失错误当所以都失败返回 0
}

get_bag_duration(){
	if command -v rosbag &> /dev/null; then #command -v rosbag检查rosbag命令是否存在
		rosbag info "$1" 2>/dev/null | grep -E "^duration:" | awk '{print $2}'
		#使用rosbag命令info获取bag的文件信息获取duration行后的第二个字符就是获取bag时长
	else
		echo "N/A"
	fi
}

get_bag_topics(){
	if command -v rosbag &> /dev/null; then
		rosbag info "$1" 2>/dev/null | grep -E "^topics:" -A 100 | tail -n +2 | awk '{print $1}' #获取话题链表正好将标题和之后的空格去掉 然后输出第一个字段
	else
		echo ""
	fi
}

#打印分隔线
print_separator(){
	echo "================================================================="
}
#列出所有bag信息
cmd_info(){
	local dir="$1"
	if [ -z "$dir" ]; then
		echo -e "用法:$0 info (目录)"
		return 1
	fi
	
	local bags=$(get_bag_files "$dir")
	if [ -z "$bags" ]; then
		echo -e "目录'$dir'中没有找到.bag文件"
		return 0
	fi
	echo -e "===ROSBAG信息列表==="
	echo -e "目录: $dir"
	print_separator
	local count=0
	for bag in $bags;do
		count=$((count + 1))
		printf "[%2d] %-40s 大小: %8s 时长: %s\n" \
       "$count" \
       "$(basename "$bag")" \
       "$(get_file_size "$bag")" \
       "$(get_bag_duration "$bag")"
	done
	print_separator
	echo -e "总计:$count个bag文件"
}
#批量压缩bag
cmd_compress(){
	local dir="$1"
	if [ -z "$dir" ]; then
		echo -e "用法:$0 compress (目录)"
		return 1
	fi
	local bags=$(get_bag_files "$dir")
       	if [ -z "$bags" ]; then
       		echo -e "目录:'$dir'中没有找到.bag文件"
       		return 0
       	fi
	local total_count=$(echo "$bags" | wc -l | tr -d ' ')
       	
       	echo -e "===批量压缩ROSBAG==="
       	echo -e "目录: $dir"
       	print_separator
       	echo -e "确认压缩?(y/N); \c"
       	read -r confirm
       	if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
       		echo -e "操作已取消"
       		return 0
       	fi
       	print_separator
       	local count=0
       	local success=0
       	for bag in $bags;do
       		count=$((count + 1))
       		local filename=$(basename "$bag")
       		local original_size=$(get_file_size "$bag")
       		echo -e "[$count/$total_count] 压缩:$filename" 
       		echo -e "原始大小:$original_size"
       		print_separator
       		if bzip2 -k "$bag" 2>/dev/null; then
            	success=$((success + 1))
            	# 压缩后的文件为 ${bag}.bz2
            	local compressed_file="${bag}.bz2"
            	if [ -f "$compressed_file" ]; then
                	local compressed_size=$(get_file_size "$compressed_file")
                	echo -e "[SUCCESS]压缩后大小: $compressed_size"
            	fi
        	else
            	echo -e "[ERROR]压缩失败" >&2
        	fi
        	echo ""
    	done
    
    	print_separator
    	echo -e "压缩完成!成功: $success / $total_count"
}
#批量过滤话题
cmd_filter() {
    local dir="$1"
    local topic="$2"
    
    if [ -z "$dir" ] || [ -z "$topic" ]; then
        echo -e "用法: $0 filter(目录)(话题)"
        return 1
    fi
    
    if [ ! -d "$dir" ]; then
        echo -e "错误: 目录 '$dir' 不存在"
        return 1
    fi
    
    # 检查 rosbag 命令是否存在
    if ! command -v rosbag &> /dev/null; then
        echo -e "错误: 未找到 rosbag 命令,请确保已安装 ROS"
        return 1
    fi
    
    local bags=$(find "$dir" -maxdepth 1 -name "*.bag" -type f | sort)
    if [ -z "$bags" ]; then
        echo -e "目录 '$dir' 中没有找到 .bag 文件"
        return 0
    fi
    
    local total_count=$(echo "$bags" | wc -l | tr -d ' ')
    
    echo -e "=== 批量过滤话题 ==="
    echo -e "目录: $dir"
    echo -e "话题: $topic"
    echo -e "文件数: $total_count"
    
    echo -e "确认过滤?(y/N): \c" 
    read -r confirm
    if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
        echo -e "操作已取消"
        return 0
    fi
    
    print_separator
    
    local count=0
    local success=0
    local failed=0
    
    for bag in $bags; do
        count=$((count + 1))
        local filename=$(basename "$bag")
        local base_name=$(basename "$bag" .bag)
        local output_file="${dir}/${base_name}_filtered.bag"
        local original_size=$(get_file_size "$bag")
        
        echo -e "[$count/$total_count] $filename"
        echo -e "    原始大小: $original_size"
        echo -e "    输出文件: $(basename "$output_file")"
        echo -e "过滤中..."
        if rosbag filter "$bag" "$output_file" "topic == '$topic'" 2>&1; then
            success=$((success + 1))
            if [ -f "$output_file" ]; then
                local filtered_size=$(get_file_size "$output_file")
                echo -e "[SUCCESS]过滤完成-> $filtered_size"
            fi
        else
            failed=$((failed + 1))
            echo -e "[ERROR]过滤失败"
        fi
        echo ""
    done
    
    print_separator
    echo -e "过滤完成"
    echo -e "  成功: $success 个"
    echo -e "  失败: $failed 个"
    echo -e "  总计: $total_count 个"
}

#清理旧bag
cmd_clean(){
	local dir="$1"
	local keep_count=3
	shift
	while [ $# -gt 0 ]; do 
		case "$1" in
            --keep)
                keep_count="$2"
                shift 2
                ;;
            *)
                break
                ;;
        	esac
    	done
    	
    	if [ -z "$dir" ]; then
    		echo -e "用法:$0 clean (目录) --keep (保留数量)"
    		return 1
    	fi
    	
    	if [ ! -d "$dir" ]; then
    		echo -e "[ERROR]错误:目录"$dir"不存在"
    		return 1
    	fi
    	
    	local all_bags=$(cd "$dir" && ls -t *.bag 2>/dev/null || true)
    	if [ -z "$all_bags" ]; then
    		echo -e "目录'$dir'中没有找到.bag文件"
    		return 0
    	fi
    	
    	local total_count=$(echo "$all_bags" | wc -l | tr -d ' ') 
    	
    	local to_delete=$(echo "$all_bags" | tail -n +$((keep_count + 1)))  
    	local delete_count=$(echo "$to_delete" | grep -v '^$' | wc -l | tr -d ' ')  # 修复:变量名和语法
    	
    	echo -e "===清理旧ROSBAG文件==="
    	echo -e "目录: $dir"
    	echo -e "保留最新: $keep_count个"
    	echo -e "现有文件: $total_count个"
    	echo -e "将删除: $delete_count个"
    	print_separator
    	
    	if [ $delete_count -eq 0 ]; then
    		echo -e "无需清理,文件数量不超过保留数量"
    		return 0
    	fi
    	
    	echo -e "将保留的文件(最新$keep_count个):"
    	echo "$all_bags" | head -n "$keep_count" | while read -r file; do
    		if [ -n "$file" ]; then
    			local size=$(cd "$dir" && get_file_size "$file" )  # 修复:使用 get_file_size 函数
    			echo "[保留] $file ($size)"
    		fi
    	done
    	print_separator
    	
    	echo -e "将删除的文件:"
    	echo "$to_delete" | while read -r file; do
    		if [ -n "$file" ]; then
    			local size=$(cd "$dir" && get_file_size "$file")
    			echo "[删除] $file ($size)"
    		fi
    	done
    	print_separator
    	
    	echo -e "确认删除$delete_count个文件?"
    	echo -e "输入 y/Y 确认,其他取消: \c" 
    	read -r confirm  
    	if [[ ! "$confirm" =~ ^[Yy]$ ]]; then  
    		echo -e "操作已取消"
    		return 0
    	fi
    	
    	echo -e "开始删除....."
    	local deleted=0
    	echo "$to_delete" | while read -r file; do
    		if [ -n "$file" ]; then
    			rm -v "$dir/$file"
    			deleted=$((deleted + 1))
    		fi
    	done
    	
    	echo -e "已删除 $delete_count 个文件"
    	
}

# 命令:stats - 统计信息
cmd_stats() {
    local dir="$1"
    if [ -z "$dir" ]; then
        echo -e "用法: $0 stats <dir>"
        return 1
    fi
    
    if [ ! -d "$dir" ]; then
        echo -e "[ERROR] 目录 \"$dir\" 不存在"  
        return 1
    fi
    
    local bags=$(get_bag_files "$dir")
    if [ -z "$bags" ]; then
        echo -e "目录 '$dir' 中没有找到 .bag 文件"
        return 0
    fi
    
    echo -e "=== ROSBAG 统计分析 ==="
    echo -e "目录: $dir"
    print_separator
    
    local total_size=0
    local total_duration=0
    local count=0
    declare -A topic_counts  
    
    # 收集统计信息
    for bag in $bags; do
        count=$((count + 1))
        
        # 大小统计
        local size_bytes=$(get_file_size_bytes "$bag")
        total_size=$((total_size + size_bytes))
        
        # 时长统计
        local duration=$(get_bag_duration "$bag")
        if [ "$duration" != "N/A" ] && [ -n "$duration" ]; then
            # 将时长转换为秒(假设格式为 MM:SS 或 HH:MM:SS)
            if [[ "$duration" == *:* ]]; then
                IFS=':' read -ra parts <<< "$duration"
                if [ ${#parts[@]} -eq 2 ]; then
                    local mins=${parts[0]}
                    local secs=${parts[1]}
                    duration_secs=$((mins * 60 + secs))
                elif [ ${#parts[@]} -eq 3 ]; then
                    local hours=${parts[0]}
                    local mins=${parts[1]}
                    local secs=${parts[2]}
                    duration_secs=$((hours * 3600 + mins * 60 + secs))
                else
                    duration_secs=0
                fi
                total_duration=$((total_duration + duration_secs))
            fi
        fi
        
        # 话题统计
        if command -v rosbag &> /dev/null; then
            local topics=$(get_bag_topics "$bag")
            for topic in $topics; do
                topic_counts["$topic"]=$((topic_counts["$topic"] + 1))
            done
        fi
    done
    
    # 显示统计结果
    echo -e "文件数量: $count"
    
    # 总大小
    if [ $total_size -gt 1073741824 ]; then
        local total_size_str=$(echo "scale=2; $total_size / 1073741824" | bc 2>/dev/null || echo "$total_size")
        echo -e "总大小: ${total_size_str} GB"
    elif [ $total_size -gt 1048576 ]; then
        local total_size_str=$(echo "scale=2; $total_size / 1048576" | bc 2>/dev/null || echo "$total_size")
        echo -e "总大小: ${total_size_str} MB"
    elif [ $total_size -gt 1024 ]; then
        local total_size_str=$(echo "scale=2; $total_size / 1024" | bc 2>/dev/null || echo "$total_size")
        echo -e "总大小:${total_size_str} KB"
    else
        echo -e "总大小:$total_size B"
    fi
    
    # 平均大小
    if [ $count -gt 0 ]; then
        local avg_size=$((total_size / count))
        echo -e "平均大小: $(numfmt --to=iec $avg_size 2>/dev/null || echo "$avg_size")"
    fi
    
    # 平均时长
    if [ $count -gt 0 ] && [ $total_duration -gt 0 ]; then
        local avg_duration=$((total_duration / count))
        local hours=$((avg_duration / 3600))
        local mins=$(((avg_duration % 3600) / 60))
        local secs=$((avg_duration % 60))
        if [ $hours -gt 0 ]; then
            echo -e "平均时长: ${hours}h ${mins}m ${secs}s"
        else
            echo -e "平均时长: ${mins}m ${secs}s"
        fi
    fi
    
    # 话题分布
    if [ ${#topic_counts[@]} -gt 0 ]; then
        echo -e "\n话题分布:"
        for topic in "${!topic_counts[@]}"; do
            local freq="${topic_counts[$topic]}"
            local percent=$(echo "scale=1; $freq * 100 / $count" | bc 2>/dev/null || echo "0")
            printf "  %-30s %3d 个文件 (%5.1f%%)\n" "$topic" "$freq" "$percent"
        done | sort -t: -k2 -rn
    fi
    
    print_separator
}

# 显示帮助
show_help() {
    cat << EOF
ROSBAG 批处理工具

用法: $0 <command> [options]

命令:
  info <dir>        列出目录下所有 bag 的信息(名、大小、时长)
  compress <dir>    批量压缩 bag(使用 bzip2,模拟)
  filter <dir> <topic> 批量过滤话题(模拟)
  clean <dir> --keep <N> 只保留最新 N 个,其余删掉(二次确认)
  stats <dir>       统计总大小、平均时长、话题分布
  -h, --help        显示此帮助信息

示例:
  $0 info /path/to/bags
  $0 compress /path/to/bags
  $0 filter /path/to/bags /odom
  $0 clean /path/to/bags --keep 3
  $0 stats /path/to/bags

EOF
}
main() {
    local cmd="$1"
    shift || true
    
    case "$cmd" in
        info)
            cmd_info "$@"
            ;;
        compress)
            cmd_compress "$@"
            ;;
        filter)
            cmd_filter "$@"
            ;;
        clean)
            cmd_clean "$@"
            ;;
        stats)
            cmd_stats "$@"
            ;;
        -h|--help|help)
            show_help
            ;;
        "")
            show_help
            ;;
        *)
            echo -e "[ERROR]错误: 未知命令 '$cmd'" >&2
            echo ""
            show_help
            exit 1
            ;;
    esac
}

# 执行主函数
main "$@"

工具库的建设

我们知道很多编程语言里都有头文件了父类了库了什么的在shell里也有类似的部分

目录结构如下:

├── deploy.sh
├── lib
│   ├── logger.sh
│   └── ros_common.sh
└── logs
    └── 20260804.log

具体实现:

logger.sh

#!/bin/bash
# 日志函数库(给不同类型信息使用不同颜色显示)
# 用法: source 后调用 log_info / log_warn / log_error

# 颜色定义
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'           # No Color,恢复默认

# 日志目录
LOGGER_DIR=${LOGGER_DIR:-./logs}
mkdir -p "$LOGGER_DIR"
LOGGER_FILE="$LOGGER_DIR/$(date +%Y%m%d).log"

# 内部:写日志(同时输出屏幕和文件)
_log() {
    local level=$1
    local color=$2
    shift 2
    local msg="$*"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local line="[$timestamp] [$level] $msg"
    
    # 屏幕输出(带颜色)
    echo -e "${color}${line}${NC}"
    
    # 文件输出(不带颜色)
    echo "$line" >> "$LOGGER_FILE"
}

log_info()  { _log "INFO"  "$BLUE"   "$@"; }
log_warn()  { _log "WARN"  "$YELLOW" "$@"; }
log_error() { _log "ERROR" "$RED"    "$@"; }
log_ok()    { _log "OK"    "$GREEN"  "$@"; }

# 调试用:打印变量
log_var() {
    local name=$1
    local value=$2
    log_info "$name = $value"
}

ros_common.sh 通用函数

#!/bin/bash
# ROS 通用函数库

# 检查命令是否存在
require_cmd() {
    local cmd=$1
    if ! command -v "$cmd" > /dev/null 2>&1; then
        echo "错误: 未安装 $cmd"
        return 1
    fi
    return 0
}

# 检查 ROS 是否可用
check_ros() {
    if [ -z "$ROS_DISTRO" ]; then
        return 1
    fi
    if ! command -v roscore > /dev/null 2>&1; then
        return 1
    fi
    return 0
}

# 获取本机 IP(优先非 loopback)
get_local_ip() {
    local ip
    ip=$(hostname -I 2>/dev/null | awk '{print $1}')
    if [ -z "$ip" ]; then
        ip=$(ifconfig 2>/dev/null | grep 'inet ' | grep -v 127 | awk '{print $2}' | head -1)
    fi
    echo "${ip:-127.0.0.1}"
}

# 等待 roscore 启动
wait_for_roscore() {
    local timeout=${1:-30}
    local elapsed=0
    while [ "$elapsed" -lt "$timeout" ]; do
        if rostopic list > /dev/null 2>&1; then
            return 0
        fi
        sleep 1
        elapsed=$((elapsed+1))
    done
    return 1
}

# 安全删除(带确认)
safe_rm() {
    local target=$1
    if [ ! -e "$target" ]; then
        echo "不存在: $target"
        return 1
    fi
    echo "将删除: $target ($(du -sh "$target" | cut -f1))"
    read -p "确认?(y/N) " confirm
    if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
        rm -rf "$target"
        echo "已删除"
    else
        echo "已取消"
    fi
}

# 进程是否运行
is_running() {
    local pattern=$1
    pgrep -f "$pattern" > /dev/null 2>&1
}

deploy.sh 具体调用

#!/bin/bash
# ============================================
#  ROS 一键部署工具
#  用法:
#    ./deploy.sh init      # 初始化工作空间
#    ./deploy.sh check     # 环境检查
#    ./deploy.sh start     # 启动所有节点 + 录制
#    ./deploy.sh stop      # 停止所有
#    ./deploy.sh status    # 查看状态
#    ./deploy.sh archive   # 归档数据
#    ./deploy.sh all       # check -> start -> 监控
# ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/logger.sh"
source "$SCRIPT_DIR/lib/ros_common.sh"

# ====== 配置 ======
PID_DIR="$SCRIPT_DIR/.pids"
PID_FILE="$PID_DIR/nodes.pid"
RECORD_PID_FILE="$PID_DIR/record.pid"
LOG_DIR="${ROS_LOG_DIR:-$SCRIPT_DIR/logs}"
BAG_DIR="${BAG_STORAGE:-$SCRIPT_DIR/bags}"
CONFIG_FILE="$SCRIPT_DIR/config.conf"

mkdir -p "$PID_DIR" "$LOG_DIR" "$BAG_DIR"

# ====== 清理函数 ======
cleanup() {
    log_warn "收到退出信号,开始清理..."
    stop_all
    log_ok "清理完成"
    exit 0
}
trap cleanup INT TERM

# ====== 初始化工作空间 ======
do_init() {
    log_info "=== 初始化工作空间 ==="
    
    # 用阶段 1 的逻辑创建骨架
    local ws="$HOME/catkin_ws/src/my_robot"
    mkdir -p "$ws"/{src,include/my_robot,launch,config,scripts,maps,worlds,urdf}
    
    # 创建 CMakeLists
    if [ ! -f "$ws/CMakeLists.txt" ]; then
        cat > "$ws/CMakeLists.txt" << 'INNER'
cmake_minimum_required(VERSION 3.0.2)
project(my_robot)
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs)
catkin_package()
INNER
        log_ok "创建 CMakeLists.txt"
    fi
    
    log_ok "工作空间就绪: $ws"
}

# ====== 环境检查 ======
do_check() {
    log_info "=== 环境检查 ==="
    local pass=0 fail=0
    
    if check_ros; then
        log_ok "ROS ($ROS_DISTRO)"; pass=$((pass+1))
    else
        log_error "ROS 未就绪"; fail=$((fail+1))
    fi
    
    if [ -d "$HOME/catkin_ws/src" ]; then
        log_ok "工作空间"; pass=$((pass+1))
    else
        log_warn "工作空间不存在 (运行 init)" 
    fi
    
    local disk=$(df -h "$HOME" | awk 'NR==2{print $5}' | tr -d '%')
    if [ "$disk" -lt 85 ]; then
        log_ok "磁盘 ${disk}%"; pass=$((pass+1))
    else
        log_error "磁盘 ${disk}% 过满"; fail=$((fail+1))
    fi
    
    log_info "结果: PASS=$pass FAIL=$fail"
    [ "$fail" -gt 0 ] && return 1 || return 0
}

# ====== 启动节点 ======
start_node() {
    local node=$1
    local node_name=${node#/}
    
    if is_running "$node_name"; then
        log_warn "$node 已在运行,跳过"
        return 0
    fi
    
    log_info "启动 $node ..."
    # 真实环境: rosrun my_robot "$node_name" > "$LOG_DIR/${node_name}.log" 2>&1 &
    # 模拟:
    sleep 3600 > "$LOG_DIR/${node_name}.log" 2>&1 &
    local pid=$!
    echo "$pid $node" >> "$PID_FILE"
    log_ok "$node (PID: $pid)"
}

do_start() {
    log_info "=== 启动节点 ==="
    > "$PID_FILE"
    
    # 节点列表:优先配置文件,否则默认
    local nodes
    if [ -f "$CONFIG_FILE" ] && grep -q "^NODES=" "$CONFIG_FILE"; then
        nodes=$(grep "^NODES=" "$CONFIG_FILE" | cut -d= -f2)
    else
        nodes="/nav_node /slam_node /lidar_node /amcl_node /motor_node"
    fi
    
    for node in $nodes; do
        start_node "$node"
    done
    
    # 启动录制
    log_info "启动 bag 录制..."
    # 真实: rosbag record -a -o "$BAG_DIR/$(date +%Y%m%d_%H%M%S)" &
    sleep 3600 > "$LOG_DIR/record.log" 2>&1 &
    echo $! > "$RECORD_PID_FILE"
    log_ok "录制中 (PID: $(cat "$RECORD_PID_FILE"))"
    
    log_ok "全部启动完成"
}

# ====== 停止所有 ======
stop_all() {
    log_info "=== 停止所有进程 ==="
    
    # 停录制
    if [ -f "$RECORD_PID_FILE" ]; then
        local rpid=$(cat "$RECORD_PID_FILE")
        kill "$rpid" 2>/dev/null && log_ok "停止录制 (PID: $rpid)"
        rm -f "$RECORD_PID_FILE"
    fi
    
    # 停节点
    if [ -f "$PID_FILE" ]; then
        while read pid node; do
            [ -z "$pid" ] && continue
            kill "$pid" 2>/dev/null && log_ok "停止 $node (PID: $pid)"
        done < "$PID_FILE"
        > "$PID_FILE"
    fi
}

# ====== 状态 ======
do_status() {
    log_info "=== 运行状态 ==="
    printf "%-20s %-10s %-8s\n" "进程" "状态" "PID"
    printf "%-20s %-10s %-8s\n" "----" "----" "---"
    
    if [ -f "$PID_FILE" ]; then
        while read pid node; do
            [ -z "$pid" ] && continue
            if kill -0 "$pid" 2>/dev/null; then
                printf "%-20s %-10s %-8s\n" "$node" "running" "$pid"
            else
                printf "%-20s %-10s %-8s\n" "$node" "dead" "$pid"
            fi
        done < "$PID_FILE"
    fi
    
    if [ -f "$RECORD_PID_FILE" ]; then
        local rpid=$(cat "$RECORD_PID_FILE")
        if kill -0 "$rpid" 2>/dev/null; then
            printf "%-20s %-10s %-8s\n" "rosbag_record" "running" "$rpid"
        fi
    fi
}

# ====== 归档 ======
do_archive() {
    log_info "=== 归档数据 ==="
    local date_str=$(date +%Y%m%d)
    local archive_dir="$SCRIPT_DIR/archived/$date_str"
    mkdir -p "$archive_dir"
    
    # 归档日志
    if ls "$LOG_DIR"/*.log > /dev/null 2>&1; then
        cp "$LOG_DIR"/*.log "$archive_dir/" 2>/dev/null
        log_ok "日志已归档到 $archive_dir"
    fi
    
    # 归档 bag
    if ls "$BAG_DIR"/*.bag > /dev/null 2>&1; then
        mv "$BAG_DIR"/*.bag "$archive_dir/" 2>/dev/null
        log_ok "bag 已归档到 $archive_dir"
    fi
    
    log_ok "归档完成: $archive_dir"
}

# ====== 全流程 ======
do_all() {
    do_check || { log_error "环境检查失败,终止"; exit 1; }
    do_start
    log_info "监控中... (Ctrl+C 停止并归档)"
    while true; do
        sleep 10
        do_status > /dev/null 2>&1
        log_info "运行中 | $(date '+%H:%M:%S')"
    done
}

# ====== 帮助 ======
show_help() {
    cat << 'EOF'
ROS 一键部署工具
用法: ./deploy.sh <命令>

命令:
  init      初始化工作空间
  check     环境检查
  start     启动所有节点 + 录制
  stop      停止所有进程
  status    查看运行状态
  archive   归档日志和 bag
  all       check + start + 监控(Ctrl+C 退出时自动归档)
  help      显示帮助
EOF
}

# ====== 主入口 ======
case "${1:-help}" in
    init)    do_init ;;
    check)   do_check ;;
    start)   do_start ;;
    stop)    stop_all ;;
    status)  do_status ;;
    archive) do_archive ;;
    all)     do_all ;;
    help|-h|--help) show_help ;;
    *)       echo "未知命令: $1"; show_help; exit 1 ;;
esac

一键式ROS开发工作流工具

注意:之前写的代码是ROS1版本的下面为ROS2

不同点:

  1. 无 roscore,环境变量 ROS_MASTER_URI → ROS_DOMAIN_ID

    原因:ROS1 依赖 Master 中心节点;ROS2 是 DDS 分布式发现,无 Master,靠 DOMAIN_ID 做网络隔离。

  2. 命令替换

rosrun my_pkg nav_node
roslaunch my_pkg robot.launch
rosbag record -a

改为

ros2 run my_pkg nav_node
ros2 launch my_pkg robot.launch.py
ros2 bag record -a
  1. 话题就绪检测:rostopic list → ros2 topic list

原因:命令行工具全部重构;用来阻塞等待 ROS 环境就绪。

  1. 工作空间 setup:devel/setup.bash → install/setup.bash

    source ~/catkin_ws/devel/setup.bash -> source ~/colcon_ws/install/setup.bash

原因:构建系统不同;catkin 输出 devel;colcon 输出 install 目录。

  1. roslaunch 进程模型差异
roslaunch my_pkg robot.launch
# 会fork出多个独立操作系统PID,每个节点一个PID
# shell脚本可以拿到每一个业务节点PID,挂哪个重启哪个
ros2 launch my_pkg robot.launch.py
# 只有1个Python主进程PID,业务节点是它内部子进程
# shell拿不到业务节点PID;内部节点崩溃外部脚本感知不到

原因:ros1 launch 是多进程 fork;ros2‑launch 是 python 调度器,节点在 launch 进程内部托管。

  1. ROS2 使用 ros2 run 才能保留原有 PID 监控自愈
ros2 run my_pkg nav_node &
echo $! >> nodes.pid
# 每个节点独立OS PID,脚本可以检测PID死亡,单独重启该节点

该部分将所有所学的部分进行总和

#!/bin/bash
# ============================================
#  ROS2 一键部署工具 (企业级增强版)
#  用法:
#    ./deploy.sh init                    # 初始化工作空间
#    ./deploy.sh check                   # 环境检查
#    ./deploy.sh start                   # 启动所有节点 + 录制
#    ./deploy.sh stop                    # 停止所有
#    ./deploy.sh status                  # 查看状态
#    ./deploy.sh archive                 # 归档数据
#    ./deploy.sh all                     # check -> start -> 监控
#    ./deploy.sh deploy <robot_ip>       # 远程部署到机器人
#    ./deploy.sh reload                  # 热加载配置
#    ./deploy.sh dashboard               # 生成可视化仪表盘
#    ./deploy.sh --robot <name> start    # 管理指定机器人
# ============================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# 检查依赖库
for lib in logger.sh ros_common.sh; do
    if [ ! -f "$SCRIPT_DIR/lib/$lib" ]; then
        echo "错误: 缺少依赖库 $lib"
        echo "请确保 $SCRIPT_DIR/lib/$lib 存在"
        exit 1
    fi
done

source "$SCRIPT_DIR/lib/logger.sh"
source "$SCRIPT_DIR/lib/ros_common.sh"

# ====== 配置 ======
PID_DIR="$SCRIPT_DIR/.pids"
PID_FILE="$PID_DIR/nodes.pid"
RECORD_PID_FILE="$PID_DIR/record.pid"
MONITOR_PID_FILE="$PID_DIR/monitor.pid"
LOG_DIR="${ROS_LOG_DIR:-$SCRIPT_DIR/logs}"
BAG_DIR="${BAG_STORAGE:-$SCRIPT_DIR/bags}"
CONFIG_FILE="$SCRIPT_DIR/config.conf"
DASHBOARD_DIR="$SCRIPT_DIR/dashboard"
WORKSPACE="${ROS_WORKSPACE:-$HOME/colcon_ws}"
ROBOT_CONFIG_DIR="$SCRIPT_DIR/robots"

mkdir -p "$PID_DIR" "$LOG_DIR" "$BAG_DIR" "$DASHBOARD_DIR" "$ROBOT_CONFIG_DIR"

# ====== 全局变量 ======
declare -A NODE_RESTART_COUNT
declare -A NODE_STATUS
CURRENT_ROBOT=""
ROBOT_MODE="single"  # single 或 multi

# ====== 清理函数 ======
cleanup() {
    log_warn "收到退出信号,开始清理..."
    stop_all
    [ -f "$MONITOR_PID_FILE" ] && rm -f "$MONITOR_PID_FILE"
    log_ok "清理完成"
    exit 0
}
trap cleanup INT TERM

# ====== 加载配置 ======
load_config() {
    local config_file="${1:-$CONFIG_FILE}"
    if [ -f "$config_file" ]; then
        local temp_config=$(mktemp)
        grep -v '^[[:space:]]*#' "$config_file" | grep -v '^[[:space:]]*$' > "$temp_config"
        source "$temp_config"
        rm -f "$temp_config"
        log_info "加载配置文件: $config_file"
    else
        # ROS2 默认配置
        ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-0}"
        NODES="${NODES:-"my_pkg nav_node my_pkg slam_node my_pkg lidar_node"}"
        BAG_TOPICS="${BAG_TOPICS:--a}"
        USE_LAUNCH="${USE_LAUNCH:-false}"
        LAUNCH_FILE="${LAUNCH_FILE:-my_robot all_nodes.launch.py}"
        ENABLE_RECORD="${ENABLE_RECORD:-true}"
        MAX_RESTART="${MAX_RESTART:-3}"
        RESTART_INTERVAL="${RESTART_INTERVAL:-5}"
        WEBHOOK_URL="${WEBHOOK_URL:-}"
        MONITOR_INTERVAL="${MONITOR_INTERVAL:-10}"
        export ROS_DOMAIN_ID
    fi
}

# ====== 机器人配置管理 ======
load_robot_config() {
    local robot_name=$1
    local robot_file="$ROBOT_CONFIG_DIR/${robot_name}.conf"

    if [ -f "$robot_file" ]; then
        source "$robot_file"
        CURRENT_ROBOT="$robot_name"
        ROBOT_MODE="multi"
        log_info "加载机器人配置: $robot_name"
        return 0
    else
        log_error "机器人配置不存在: $robot_name"
        return 1
    fi
}

list_robots() {
    echo "=== 已配置的机器人 ==="
    for conf in "$ROBOT_CONFIG_DIR"/*.conf; do
        if [ -f "$conf" ]; then
            local name=$(basename "$conf" .conf)
            local ip=$(grep "^ROBOT_IP=" "$conf" | cut -d= -f2)
            local status=$(get_robot_status "$name")
            printf "%-15s %-20s %-10s\n" "$name" "$ip" "$status"
        fi
    done
}

get_robot_status() {
    local robot_name=$1
    local robot_file="$ROBOT_CONFIG_DIR/${robot_name}.conf"
    source "$robot_file"

    if ping -c 1 -W 1 "$ROBOT_IP" >/dev/null 2>&1; then
        ssh -o ConnectTimeout=2 "$ROBOT_USER@$ROBOT_IP" "ps aux | grep -E 'deploy.sh (start|all)' | grep -v grep" >/dev/null 2>&1
        if [ $? -eq 0 ]; then
            echo "运行中"
        else
            echo "在线(空闲)"
        fi
    else
        echo "离线"
    fi
}

# ====== 远程部署 ======
do_deploy() {
    local robot_ip=$1
    local robot_user="${2:-$USER}"
    local robot_dir="${3:-~/ros2_shell_toolkit}"

    if [ -z "$robot_ip" ]; then
        log_error "请指定机器人 IP 地址"
        echo "用法: ./deploy.sh deploy <robot_ip> [user] [remote_dir]"
        return 1
    fi

    log_info "=== 开始远程部署到 $robot_ip ==="

    if ! ssh -o ConnectTimeout=5 "$robot_user@$robot_ip" "echo 'SSH OK'" >/dev/null 2>&1; then
        log_error "无法连接到 $robot_ip,请检查 SSH 配置"
        return 1
    fi
    log_ok "SSH 连接成功"

    ssh "$robot_user@$robot_ip" "mkdir -p $robot_dir"

    log_info "同步工具箱到远程..."
    rsync -avz --progress \
        --exclude='.git' \
        --exclude='.pids' \
        --exclude='logs' \
        --exclude='bags' \
        --exclude='dashboard' \
        --exclude='archived' \
        "$SCRIPT_DIR/" "$robot_user@$robot_ip:$robot_dir/"

    if [ $? -eq 0 ]; then
        log_ok "文件同步完成"
    else
        log_error "文件同步失败"
        return 1
    fi

    log_info "远程执行部署..."
    local remote_cmd="cd $robot_dir && ./deploy.sh start"
    if [ -n "$3" ]; then
        remote_cmd="cd $robot_dir && ./deploy.sh $3"
    fi

    ssh "$robot_user@$robot_ip" "chmod +x $robot_dir/*.sh && $remote_cmd" &
    local ssh_pid=$!
    sleep 2

    if ssh "$robot_user@$robot_ip" "ps aux | grep -E 'deploy.sh (start|all)' | grep -v grep" >/dev/null 2>&1; then
        log_ok "远程部署成功!"
        log_info "查看远程状态: ssh $robot_user@$robot_ip 'cd $robot_dir && ./deploy.sh status'"
    else
        log_warn "远程进程可能未启动,请检查日志"
    fi

    local robot_name="robot_$(echo $robot_ip | tr '.' '_')"
    cat > "$ROBOT_CONFIG_DIR/${robot_name}.conf" << EOF
# ROS2机器人配置
ROBOT_NAME="$robot_name"
ROBOT_IP="$robot_ip"
ROBOT_USER="$robot_user"
ROBOT_DIR="$robot_dir"
DEPLOY_TIME="$(date)"
EOF
    log_ok "机器人配置已保存: $robot_name"
}

# ====== 热加载配置 ======
do_reload() {
    log_info "=== 热加载配置 ==="
    if [ -f "$PID_FILE" ] && [ -s "$PID_FILE" ]; then
        local main_pid=$(head -1 "$PID_FILE" | awk '{print $1}')
        if kill -0 "$main_pid" 2>/dev/null; then
            kill -HUP "$main_pid"
            log_ok "发送重载信号到 PID: $main_pid"
            load_config
            log_ok "配置重新加载完成"
            if [ -f "$MONITOR_PID_FILE" ]; then
                local mon_pid=$(cat "$MONITOR_PID_FILE")
                kill "$mon_pid" 2>/dev/null
                log_info "重启监控进程..."
                start_monitor &
                echo $! > "$MONITOR_PID_FILE"
            fi
        else
            log_error "主进程不存在,请重新启动"
        fi
    else
        log_warn "没有运行中的进程,直接加载配置"
        load_config
    fi
}

# ====== 崩溃自愈监控 ======
start_monitor() {
    log_info "启动崩溃自愈监控..."
    load_config

    while true; do
        if [ -f "$PID_FILE" ] && [ -s "$PID_FILE" ]; then
            while read -r pid node; do
                [ -z "$pid" ] && continue
                if ! kill -0 "$pid" 2>/dev/null; then
                    log_error "检测到节点崩溃: $node (PID: $pid)"
                    local restart_count=${NODE_RESTART_COUNT["$node"]:-0}
                    if [ "$restart_count" -lt "$MAX_RESTART" ]; then
                        log_warn "尝试重启 $node (第 $((restart_count+1))/$MAX_RESTART 次)"
                        if [ "$USE_LAUNCH" != "true" ]; then
                            start_node "$node"
                        else
                            log_warn "USE_LAUNCH=true,launch模式不支持单节点重启,请整体重启"
                        fi
                        NODE_RESTART_COUNT["$node"]=$((restart_count+1))
                        send_alert "warning" "节点 $node 已自动重启 (第 $((restart_count+1)) 次)" "$node"
                    else
                        log_error "节点 $node 超过最大重启次数 ($MAX_RESTART),停止尝试"
                        send_alert "critical" "节点 $node 超过最大重启次数,需要人工介入" "$node"
                        NODE_STATUS["$node"]="failed"
                    fi
                else
                    NODE_RESTART_COUNT["$node"]=0
                    NODE_STATUS["$node"]="running"
                fi
            done < "$PID_FILE"
        fi
        generate_dashboard "realtime" >/dev/null 2>&1
        sleep "$MONITOR_INTERVAL"
    done
}

# ====== 告警通知 ======
send_alert() {
    local severity=$1
    local message=$2
    local node=$3
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    local alert_msg="【ROS2部署告警】\n"
    alert_msg+="时间: $timestamp\n"
    alert_msg+="机器人: ${CURRENT_ROBOT:-local}\n"
    alert_msg+="节点: ${node:-未知}\n"
    alert_msg+="严重程度: $severity\n"
    alert_msg+="消息: $message\n"

    log_info "告警: $severity - $message"

    if [ -n "$WEBHOOK_URL" ]; then
        local color="info"
        case $severity in
            warning) color="warning" ;;
            critical) color="danger" ;;
        esac
        local json_data=$(cat <<EOF
{
    "msgtype": "text",
    "text": {
        "content": "$alert_msg"
    }
}
EOF
)
        curl -s -X POST -H "Content-Type: application/json" -d "$json_data" "$WEBHOOK_URL" >/dev/null 2>&1
        if [ $? -eq 0 ]; then
            log_ok "告警已发送到 Webhook"
        else
            log_error "发送 Webhook 失败"
        fi
    else
        log_warn "未配置 WEBHOOK_URL,跳过告警发送"
    fi
    echo "[$timestamp] [$severity] [$node] $message" >> "$LOG_DIR/alerts.log"
}

# ====== 生成可视化仪表盘 ======
generate_dashboard() {
    local mode="${1:-full}"
    local dashboard_file="$DASHBOARD_DIR/index.html"

    local cpu_usage=$(top -bn1 | head -n 5 | awk '/Cpu/ {print $2}' | cut -d'%' -f1)
    local mem_usage=$(free -m | awk '/Mem:/ {printf "%.1f", $3/$2 * 100}')
    local disk_usage=$(df -h "$HOME" | awk 'NR==2{print $5}' | tr -d '%')
    local uptime=$(uptime | awk -F'up |,' '{print $2}')

    local node_status_html=""
    if [ -f "$PID_FILE" ] && [ -s "$PID_FILE" ]; then
        while read -r pid node; do
            [ -z "$pid" ] && continue
            local status="运行中"
            local color="green"
            if ! kill -0 "$pid" 2>/dev/null; then
                status="已停止"
                color="red"
            fi
            node_status_html+="<tr><td>$node</td><td>$pid</td><td style=\"color:$color\">$status</td></tr>"
        done < "$PID_FILE"
    fi

    local record_status="未启动"
    local record_color="gray"
    if [ -f "$RECORD_PID_FILE" ]; then
        local rpid=$(cat "$RECORD_PID_FILE")
        if kill -0 "$rpid" 2>/dev/null; then
            record_status="录制中 (PID: $rpid)"
            record_color="green"
        fi
    fi

    local recent_alerts=""
    if [ -f "$LOG_DIR/alerts.log" ]; then
        recent_alerts=$(tail -5 "$LOG_DIR/alerts.log" | sed 's/^/<li>/' | sed 's/$/<\/li>/')
    fi

    cat > "$dashboard_file" <<HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="refresh" content="5">
    <title>ROS2 部署仪表盘</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            background: #f0f2f5;
            padding: 20px;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        .header {
            background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
            color: white;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 20px;
        }
        .header h1 { margin-bottom: 5px; }
        .header .subtitle { opacity: 0.8; font-size: 14px; }
        .grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-bottom: 20px;
        }
        .card {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .card h3 {
            color: #333;
            border-bottom: 2px solid #2563eb;
            padding-bottom: 10px;
            margin-bottom: 15px;
        }
        .metric {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid #eee;
        }
        .metric .label { color: #666; }
        .metric .value { font-weight: bold; }
        .status-running { color: #10b981; }
        .status-stopped { color: #ef4444; }
        .status-warning { color: #f59e0b; }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        table th {
            text-align: left;
            padding: 8px;
            background: #f8f9fa;
            border-bottom: 2px solid #dee2e6;
        }
        table td {
            padding: 8px;
            border-bottom: 1px solid #dee2e6;
        }
        .alert-list {
            list-style: none;
            padding: 0;
        }
        .alert-list li {
            padding: 8px;
            margin-bottom: 5px;
            background: #fef3c7;
            border-left: 3px solid #f59e0b;
            border-radius: 4px;
            font-size: 14px;
        }
        .footer {
            text-align: center;
            color: #999;
            padding: 20px;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🤖 ROS2 部署仪表盘</h1>
            <div class="subtitle">
                更新时间: $(date '+%Y-%m-%d %H:%M:%S')
                | 机器人: ${CURRENT_ROBOT:-local}
                | 模式: ${ROBOT_MODE}
                | DOMAIN_ID: ${ROS_DOMAIN_ID}
            </div>
        </div>
        <div class="grid">
            <div class="card">
                <h3>📊 系统状态</h3>
                <div class="metric"><span class="label">CPU 使用率</span><span class="value">${cpu_usage:-0}%</span></div>
                <div class="metric"><span class="label">内存使用率</span><span class="value">${mem_usage:-0}%</span></div>
                <div class="metric"><span class="label">磁盘使用率</span><span class="value">${disk_usage:-0}%</span></div>
                <div class="metric"><span class="label">运行时间</span><span class="value">${uptime:-未知}</span></div>
            </div>
            <div class="card">
                <h3>📼 录制状态</h3>
                <div class="metric"><span class="label">状态</span><span class="value">${record_status}</span></div>
                <div class="metric"><span class="label">Bag 目录</span><span class="value">${BAG_DIR}</span></div>
                <div class="metric"><span class="label">日志目录</span><span class="value">${LOG_DIR}</span></div>
            </div>
        </div>
        <div class="card">
            <h3>📋 节点状态</h3>
            <table>
                <thead><tr><th>节点</th><th>PID</th><th>状态</th></tr></thead>
                <tbody>${node_status_html}</tbody>
            </table>
        </div>
        <div class="grid">
            <div class="card">
                <h3>⚠️ 最近告警</h3>
                <ul class="alert-list">${recent_alerts}</ul>
            </div>
            <div class="card">
                <h3>🔄 重启统计</h3>
                <table>
                    <thead><tr><th>节点</th><th>重启次数</th><th>状态</th></tr></thead>
                    <tbody>
HTML
for node in "${!NODE_RESTART_COUNT[@]}"; do
    cat >> "$dashboard_file" <<SUB
<tr><td>$node</td><td>${NODE_RESTART_COUNT[$node]}</td><td>${NODE_STATUS[$node]:-运行中}</td></tr>
SUB
done

cat >> "$dashboard_file" <<HTML
                    </tbody>
                </table>
            </div>
        </div>
        <div class="footer">ROS2 部署工具 v2.0 | 自动刷新 (5秒)</div>
    </div>
</body>
</html>
HTML
    if [ "$mode" = "full" ]; then
        log_ok "仪表盘已生成: $dashboard_file"
        log_info "在浏览器中打开: file://$dashboard_file"
    fi
}

# ====== 多机器人编排 ======
do_robot_manage() {
    local robot_name=$1
    local command=$2
    if [ -z "$robot_name" ] || [ -z "$command" ]; then
        log_error "用法: ./deploy.sh --robot <机器人名> <命令>"
        echo "可用命令: start, stop, status, archive, reload, dashboard"
        echo "机器人列表:"
        list_robots
        return 1
    fi
    if ! load_robot_config "$robot_name"; then
        return 1
    fi
    log_info "管理机器人: $robot_name ($ROBOT_IP)"
    case "$command" in
        start|stop|status|archive|reload)
            ssh "$ROBOT_USER@$ROBOT_IP" "cd $ROBOT_DIR && ./deploy.sh $command"
            ;;
        deploy)
            do_deploy "$ROBOT_IP" "$ROBOT_USER" "$ROBOT_DIR"
            ;;
        dashboard)
            ssh "$ROBOT_USER@$ROBOT_IP" "cd $ROBOT_DIR && ./deploy.sh dashboard"
            scp "$ROBOT_USER@$ROBOT_IP:$ROBOT_DIR/dashboard/index.html" "$DASHBOARD_DIR/${robot_name}.html"
            log_ok "仪表盘已复制到本地: $DASHBOARD_DIR/${robot_name}.html"
            ;;
        *)
            log_error "未知命令: $command"
            return 1
            ;;
    esac
}

# ========== 补齐缺失函数 do_init do_check stop_all do_status do_archive ==========
do_init(){
    log_info "初始化ROS2工作空间 $WORKSPACE"
    mkdir -p "$WORKSPACE/src"
    if [ ! -d "$WORKSPACE/install" ]; then
        log_warn "未检测到install目录,请自行执行 colcon build"
    fi
    log_ok "初始化完成,请执行 colcon build && source $WORKSPACE/install/setup.bash"
}

do_check(){
    log_info "==== ROS2环境检查 ===="
    if ! check_ros;then
        log_error "ROS2环境未加载,请先 source install/setup.bash"
        return 1
    fi
    log_ok "ROS2环境 OK,ROS_DISTRO=$ROS_DISTRO DOMAIN_ID=$ROS_DOMAIN_ID"

    if [ ! -d "$WORKSPACE/install" ];then
        log_error "工作空间 $WORKSPACE/install 不存在,请colcon build编译"
        return 1
    fi
    log_ok "工作空间目录 OK"

    if ! wait_for_ros2 5;then
        log_warn "ROS2 DDS网络暂时无话题,不影响启动"
    fi
    log_ok "全部检查通过"
    return 0
}

stop_all(){
    log_info "停止所有进程..."
    if [ -f "$PID_FILE" ]; then
        while read -r pid node; do
            [ -z "$pid" ] && continue
            if kill -0 "$pid" 2>/dev/null;then
                kill "$pid" 2>/dev/null
                sleep 0.3
                kill -9 "$pid" 2>/dev/null
                log_info "终止进程 $pid [$node]"
            fi
        done < "$PID_FILE"
        rm -f "$PID_FILE"
    fi

    if [ -f "$RECORD_PID_FILE" ];then
        local rpid=$(cat "$RECORD_PID_FILE")
        kill "$rpid" 2>/dev/null
        sleep 0.2
        kill -9 "$rpid" 2>/dev/null
        rm -f "$RECORD_PID_FILE"
        log_info "停止ros2 bag录制"
    fi

    if [ -f "$MONITOR_PID_FILE" ];then
        local mpid=$(cat "$MONITOR_PID_FILE")
        kill "$mpid" 2>/dev/null
        rm -f "$MONITOR_PID_FILE"
    fi
    log_ok "全部进程已停止"
}

do_status(){
    log_info "==== 当前节点状态 ===="
    if [ ! -f "$PID_FILE" ] || [ ! -s "$PID_FILE" ];then
        log_warn "没有托管的节点进程"
        return 0
    fi
    printf "%-25s %8s %s\n" "NODE" "PID" "STATUS"
    printf "=============================================\n"
    while read -r pid node; do
        [ -z "$pid" ] && continue
        if kill -0 "$pid" 2>/dev/null;then
            stat="✅运行"
        else
            stat="❌已退出"
        fi
        printf "%-25s %8s %s\n" "$node" "$pid" "$stat"
    done < "$PID_FILE"
}

do_archive(){
    log_info "归档bags和日志"
    local archive_dir="$SCRIPT_DIR/archived/$(date +%Y%m%d_%H%M%S)"
    mkdir -p "$archive_dir"
    log_info "归档目录: $archive_dir"
    cp -r "$BAG_DIR" "$archive_dir/"
    cp -r "$LOG_DIR" "$archive_dir/"
    log_ok "归档完成 $archive_dir"
}

# 修改 do_start ROS2版本
do_start() {
    log_info "=== ROS2 启动节点 ==="
    if ! check_ros; then
        log_error "ROS2 环境未就绪,请source install/setup.bash"
        return 1
    fi
    if [ ! -d "$WORKSPACE/install" ]; then
        log_error "工作空间未编译,请先运行 init && colcon build"
        return 1
    fi

    load_config
    > "$PID_FILE"

    if [ "$USE_LAUNCH" = "true" ]; then
        log_info "使用 ros2 launch 启动: $LAUNCH_FILE"
        ros2 launch $LAUNCH_FILE > "$LOG_DIR/launch.log" 2>&1 &
        local pid=$!
        echo "$pid ros2_launch" >> "$PID_FILE"
    else
        for node in $NODES; do
            start_node "$node"
        done
    fi

    if [ "$ENABLE_RECORD" != "false" ]; then
        log_info "启动 ros2 bag 录制..."
        local bag_name="$BAG_DIR/$(date +%Y%m%d_%H%M%S)"
        ros2 bag record $BAG_TOPICS -o "$bag_name" > "$LOG_DIR/record.log" 2>&1 &
        echo $! > "$RECORD_PID_FILE"
        log_ok "录制中 (PID: $(cat "$RECORD_PID_FILE"))"
    fi

    start_monitor &
    echo $! > "$MONITOR_PID_FILE"
    log_ok "监控进程已启动 (PID: $(cat "$MONITOR_PID_FILE"))"
    log_ok "全部启动完成"
    log_info "查看日志: $LOG_DIR"
    log_info "查看仪表盘: ./deploy.sh dashboard"
}

# 修改 do_all ROS2版本
do_all() {
    log_info "=== ROS2完整流程启动 ==="
    do_check || { log_error "环境检查失败,终止"; return 1; }

    if [ -f "$PID_FILE" ] && [ -s "$PID_FILE" ]; then
        read -p "检测到运行中的进程,是否先停止并归档? (y/N): " -n 1 -r
        echo
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            stop_all
            do_archive
        fi
    fi

    do_start || { log_error "启动失败"; return 1; }
    log_info "监控中... (Ctrl+C 停止并归档)"

    while true; do
        sleep 30
        generate_dashboard "realtime" >/dev/null 2>&1
    done
}

# ====== 主入口 ======
ROBOT_NAME=""
COMMAND=""

while [[ $# -gt 0 ]]; do
    case $1 in
        --robot)
            ROBOT_NAME="$2"
            shift 2
            ;;
        --help|-h)
            show_help
            exit 0
            ;;
        *)
            COMMAND="$1"
            shift
            break
            ;;
    esac
done

if [ -n "$ROBOT_NAME" ]; then
    do_robot_manage "$ROBOT_NAME" "$COMMAND"
    exit $?
fi

case "${COMMAND:-help}" in
    init)       do_init ;;
    check)      do_check ;;
    start)      do_start ;;
    stop)       stop_all ;;
    status)     do_status ;;
    archive)    do_archive ;;
    reload)     do_reload ;;
    dashboard)  generate_dashboard "full" ;;
    deploy)
        if [ -n "$1" ]; then
            do_deploy "$1" "${2:-$USER}" "${3:-~/ros2_shell_toolkit}"
        else
            log_error "请指定机器人 IP"
            echo "用法: ./deploy.sh deploy <robot_ip> [user] [remote_dir]"
        fi
        ;;
    all)        do_all ;;
    robots)     list_robots ;;
    help|-h|--help) show_help ;;
    *)
        echo "未知命令: $COMMAND"
        show_help
        exit 1
        ;;
esac

# ====== 帮助信息 ======
show_help() {
    cat << 'EOF'
ROS2 一键部署工具 (企业级增强版)
用法: ./deploy.sh [--robot <机器人名>] <命令>

单机命令:
  init          初始化colcon工作空间
  check         ROS2环境检查
  start         启动所有节点 + 录制 + 监控
  stop          停止所有进程
  status        查看运行状态
  archive       归档日志和bag
  reload        热加载配置 (无需重启)
  dashboard     生成可视化HTML仪表盘
  all           完整流程 (check + start + 后台监控)
  deploy <ip>   远程部署到ROS2机器人

多机器人管理:
  --robot <name> start    启动指定机器人
  --robot <name> stop     停止指定机器人
  --robot <name> status   查看指定机器人状态
  --robot <name> deploy   重新部署
  --robot <name> dashboard 获取远程仪表盘
  robots                  列出所有配置的机器人

配置文件 config.conf:
  NODES="pkg1 exec1 pkg2 exec2"  # 节点列表,USE_LAUNCH=false生效
  LAUNCH_FILE="my_pkg main.launch.py"
  USE_LAUNCH=false
  ENABLE_RECORD=true
  MAX_RESTART=3
  MONITOR_INTERVAL=10
  ROS_DOMAIN_ID=0
  WEBHOOK_URL="钉钉/飞书webhook"

机器人配置 robots/*.conf
  ROBOT_NAME="robot01"
  ROBOT_IP="192.168.1.100"
  ROBOT_USER="ubuntu"
  ROBOT_DIR="/home/ubuntu/ros2_shell_toolkit"

⚠️注意:USE_LAUNCH=true模式,无法单独重启故障节点,只能整体重启。
EOF
}

# 创建示例机器人配置
create_example_robot_config() {
    cat > "$ROBOT_CONFIG_DIR/example_robot.conf" << 'EOF'
# ROS2示例机器人配置
ROBOT_NAME="robot_01"
ROBOT_IP="192.168.1.100"
ROBOT_USER="ubuntu"
ROBOT_DIR="/home/ubuntu/ros2_shell_toolkit"
EOF
    log_info "示例机器人配置已创建: $ROBOT_CONFIG_DIR/example_robot.conf"
}

# 创建默认配置 ROS2 config.conf
create_default_config() {
    if [ ! -f "$CONFIG_FILE" ]; then
        cat > "$CONFIG_FILE" << 'EOF'
# ROS2部署配置
# NODES格式:包名 可执行文件,空格分隔
NODES="my_pkg nav_node my_pkg slam_node my_pkg lidar_node"
LAUNCH_FILE="my_robot all_nodes.launch.py"
USE_LAUNCH=false
ENABLE_RECORD=true
BAG_TOPICS="-a"
MAX_RESTART=3
RESTART_INTERVAL=5
MONITOR_INTERVAL=10
ROS_DOMAIN_ID=0
WORKSPACE=$HOME/colcon_ws

# 告警 Webhook
# WEBHOOK_URL="https://oapi.dingtalk.com/robot/send?access_token=xxx"
EOF
        log_info "创建ROS2默认配置文件: $CONFIG_FILE"
    fi
}

if [ ! -d "$ROBOT_CONFIG_DIR" ]; then
    mkdir -p "$ROBOT_CONFIG_DIR"
    create_example_robot_config
fi

create_default_config

if [ $# -eq 0 ]; then
    show_help
    exit 0
fi

依赖库文件 ros_common.sh 需要修改

#!/bin/bash
# ROS2 通用函数库

# 检查命令是否存在
require_cmd() {
    local cmd=$1
    if ! command -v "$cmd" > /dev/null 2>&1; then
        echo "错误: 未安装 $cmd"
        return 1
    fi
    return 0
}

# ========== ROS2环境检查,替换ROS1 check_ros ==========
check_ros() {
    if [ -z "$ROS_DISTRO" ]; then
        return 1
    fi
    if ! command -v ros2 > /dev/null 2>&1; then
        return 1
    fi
    return 0
}

# 获取本机 IP(优先非 loopback)
get_local_ip() {
    local ip
    ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7}' | head -n1)
    if [ -z "$ip" ]; then
        ip=$(hostname -I 2>/dev/null | awk '{print $1}')
    fi
    if [ -z "$ip" ]; then
        ip=$(ifconfig 2>/dev/null | grep 'inet ' | grep -v 127 | awk '{print $2}' | head -1)
    fi
    echo "${ip:-127.0.0.1}"
}

# ========== ROS2:等待ROS2环境就绪(等待话题列表可调用,替代wait_for_roscore) ==========
wait_for_ros2() {
    local timeout=${1:-30}
    local elapsed=0
    while [ "$elapsed" -lt "$timeout" ]; do
        if ros2 topic list > /dev/null 2>&1; then
            return 0
        fi
        sleep 1
        elapsed=$((elapsed+1))
    done
    return 1
}

# 安全删除(带确认)
safe_rm() {
    local target=$1
    if [ ! -e "$target" ]; then
        echo "不存在: $target"
        return 1
    fi
    echo "将删除: $target ($(du -sh "$target" | cut -f1))"
    read -p "确认?(y/N) " confirm
    if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
        rm -rf "$target"
        echo "已删除"
    else
        echo "已取消"
    fi
}

# 进程是否运行
is_running() {
    local pattern=$1
    pgrep -f "$pattern" > /dev/null 2>&1
}

# ========= ROS2 新增:启动单个节点 ros2 run ,写入PID文件 =========
# 使用示例:start_node "package_name node_executable"
start_node() {
    local node_spec="$1"
    local logfile="$LOG_DIR/node_${node_spec// /_}.log"

    log_info "启动节点: ros2 run $node_spec"
    ros2 run $node_spec > "$logfile" 2>&1 &
    local pid=$!
    echo "$pid $node_spec" >> "$PID_FILE"
    log_ok "节点 $node_spec PID=$pid"
}
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐