从实战解决方案学习Shell
·
从实战解决方案学习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
不同点:
-
无 roscore,环境变量 ROS_MASTER_URI → ROS_DOMAIN_ID
原因:ROS1 依赖 Master 中心节点;ROS2 是 DDS 分布式发现,无 Master,靠 DOMAIN_ID 做网络隔离。
-
命令替换
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
- 话题就绪检测:rostopic list → ros2 topic list
原因:命令行工具全部重构;用来阻塞等待 ROS 环境就绪。
-
工作空间 setup:devel/setup.bash → install/setup.bash
source ~/catkin_ws/devel/setup.bash -> source ~/colcon_ws/install/setup.bash
原因:构建系统不同;catkin 输出 devel;colcon 输出 install 目录。
- 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 进程内部托管。
- 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"
}
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)