#!/bin/bash # RaspiCar 环境设置脚本 # 下载并设置运行环境(I2C、摄像头、音频等权限) set -e # 遇到错误时退出 # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # 配置变量 DOWNLOAD_URL="https://240318.xyz/download/RaspiCar/RaspiCar.zip" INSTALL_DIR="$HOME/RaspiCar" NTP_SYNC_URL="https://240318.xyz/download/ntp-sync.sh" NODE_MODULES_URL="https://240318.xyz/download/node_modules.tar.xz" # 打印带颜色的消息 print_info() { echo -e "${GREEN}[INFO]${NC} $1" } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } print_header() { echo "" echo "===================================" echo "$1" echo "===================================" } # 检查是否以 root 权限运行 if [[ $EUID -eq 0 ]]; then print_error "请不要以 root 用户身份运行此脚本" exit 1 fi print_header "RaspiCar 环境设置脚本" # 显示当前时间 CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S %Z") print_info "当前时间: ${CURRENT_TIME}" # ========================================== # 第一步:交互询问所有选项 # ========================================== print_header "配置选项" read -p "是否同步系统时间(阿里云时钟源)? (y/N): " -n 1 -r echo "" SYNC_TIME=$([[ $REPLY =~ ^[Yy]$ ]] && echo "true" || echo "false") read -p "是否更新系统软件源与已安装软件包? (y/N): " -n 1 -r echo "" UPDATE_SYSTEM=$([[ $REPLY =~ ^[Yy]$ ]] && echo "true" || echo "false") read -p "是否下载 node_modules 离线包? (y/N): " -n 1 -r echo "" DOWNLOAD_NODE_MODULES=$([[ $REPLY =~ ^[Yy]$ ]] && echo "true" || echo "false") read -p "是否使用 PulseAudio 模式(与 network-rc 一致,Trixie 建议选 Y)? (Y/n): " -n 1 -r echo "" USE_PULSE=$([[ $REPLY =~ ^[Nn]$ ]] && echo "false" || echo "true") # ========================================== # 第二步:时间同步(必须先执行,否则无法更新软件源) # ========================================== if [ "$SYNC_TIME" = "true" ]; then print_header "同步系统时间" # 检查并安装必要的工具 if ! command -v wget &> /dev/null; then print_info "安装 wget..." sudo apt-get update || true sudo apt-get install -y wget || true fi NTP_SYNC_FILE="/tmp/ntp-sync.sh" if wget --no-check-certificate "$NTP_SYNC_URL" -O "$NTP_SYNC_FILE" 2>/dev/null; then chmod +x "$NTP_SYNC_FILE" print_info "执行时间同步..." sudo "$NTP_SYNC_FILE" || print_warn "时间同步失败,但继续执行" rm -f "$NTP_SYNC_FILE" # 显示同步后的时间 NEW_TIME=$(date "+%Y-%m-%d %H:%M:%S %Z") print_info "同步后时间: ${NEW_TIME}" else print_warn "无法下载 ntp-sync.sh,跳过时间同步" fi else print_info "跳过时间同步" fi # ========================================== # 第三步:更新系统软件源和软件包 # ========================================== if [ "$UPDATE_SYSTEM" = "true" ]; then print_header "更新系统软件源和软件包" print_info "更新软件源..." sudo apt-get update || { print_error "更新软件源失败,请检查网络连接或时间设置" exit 1 } print_info "升级已安装的软件包..." sudo apt-get upgrade -y || print_warn "软件包升级过程中出现错误,但继续执行" else print_info "跳过系统更新" fi # ========================================== # 第四步:下载项目 # ========================================== print_header "下载 RaspiCar 项目" cd "$HOME" if [ -d "RaspiCar" ]; then print_info "发现现有的 RaspiCar 项目目录,跳过下载步骤" elif [ -f "RaspiCar.zip" ]; then print_info "发现现有的 RaspiCar.zip 压缩包" if ! command -v unzip &> /dev/null; then print_info "安装 unzip..." sudo apt-get update sudo apt-get install -y unzip fi if [ ! -d "RaspiCar" ]; then print_info "解压 RaspiCar.zip..." unzip -o RaspiCar.zip || { print_error "解压失败" exit 1 } rm -f RaspiCar.zip print_info "已删除压缩包" fi else print_info "下载 RaspiCar 项目..." if ! command -v wget &> /dev/null; then print_info "安装 wget..." sudo apt-get update sudo apt-get install -y wget fi if ! command -v unzip &> /dev/null; then print_info "安装 unzip..." sudo apt-get install -y unzip fi if wget --no-check-certificate "$DOWNLOAD_URL" -O RaspiCar.zip; then print_info "下载成功!" print_info "解压 RaspiCar.zip..." unzip -o RaspiCar.zip || { print_error "解压失败" exit 1 } rm -f RaspiCar.zip print_info "已删除压缩包" else print_error "下载失败,请检查网络连接或下载地址" exit 1 fi fi # 确认项目目录存在 if [ ! -d "$INSTALL_DIR" ]; then print_error "无法找到安装目录 $INSTALL_DIR" exit 1 fi cd "$INSTALL_DIR" # ========================================== # 第五步:安装系统依赖 # ========================================== print_header "安装系统依赖" # 检查并安装系统包 check_and_install_package() { local package=$1 if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "install ok installed"; then print_info "$package 已安装,跳过..." return 0 else print_info "正在安装 $package..." sudo apt-get install -y "$package" fi } REQUIRED_PACKAGES=( "build-essential" "python3-dev" "libffi-dev" "libssl-dev" "libuv1-dev" "curl" "i2c-tools" "ffmpeg" "alsa-utils" "v4l-utils" "xz-utils" "pv" "pulseaudio" "pulseaudio-utils" "alsa-tools" ) for package in "${REQUIRED_PACKAGES[@]}"; do check_and_install_package "$package" done # ========================================== # 第六步:配置硬件接口 # ========================================== print_header "配置硬件接口" print_info "启用 I2C 接口..." sudo raspi-config nonint do_i2c 0 || print_warn "I2C 配置失败(可能不是树莓派)" print_info "启用摄像头接口..." sudo raspi-config nonint do_camera 0 || print_warn "摄像头配置失败(可能不是树莓派)" print_info "启用串口接口..." sudo raspi-config nonint do_serial_hw 0 || print_warn "串口配置失败(可能不是树莓派)" # ========================================== # 第七步:安装 Node.js # ========================================== print_header "安装 Node.js" if command -v node &> /dev/null && command -v npm &> /dev/null; then NODE_VERSION=$(node --version) NPM_VERSION=$(npm --version) print_info "Node.js 已安装: $NODE_VERSION" print_info "NPM 已安装: $NPM_VERSION" if [[ ! $NODE_VERSION == v18.* ]]; then print_info "Node.js 版本不是 18.x,正在安装 Node.js 18..." curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs fi else print_info "安装 Node.js 18..." curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs fi print_info "验证 Node.js 和 npm 安装..." node --version npm --version # ========================================== # 第八步:处理 node_modules # ========================================== print_header "处理 node_modules" NM_DIR="$INSTALL_DIR/node_modules" NM_TAR="$INSTALL_DIR/node_modules.tar.xz" NODE_MODULES_READY=false if [ -d "$NM_DIR" ] && [ "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then print_info "检测到 node_modules,跳过解压" NODE_MODULES_READY=true else if [ "$DOWNLOAD_NODE_MODULES" = "true" ]; then if [ ! -f "$NM_TAR" ]; then print_info "下载 node_modules 离线包..." if wget --progress=bar:force --no-check-certificate "$NODE_MODULES_URL" -O "$NM_TAR"; then print_info "下载完成" else print_warn "下载失败,将使用 npm install" rm -f "$NM_TAR" fi fi if [ -f "$NM_TAR" ]; then print_info "解压 node_modules..." mkdir -p "$NM_DIR" # 检测压缩包结构 NM_TAR_FIRST=$(tar -tf "$NM_TAR" 2>/dev/null | head -n 1 || echo "") STRIP_COMPONENTS=0 if echo "$NM_TAR_FIRST" | grep -q '^node_modules/'; then EXTRACT_DIR="$INSTALL_DIR" else # 查找 node_modules 路径 NM_TAR_NODE_PATH=$(tar -tf "$NM_TAR" 2>/dev/null | grep -m1 -E '(^|/)(node_modules/)' || echo "") if [ -n "$NM_TAR_NODE_PATH" ]; then PREFIX="${NM_TAR_NODE_PATH%%/node_modules/*}" if [ -n "$PREFIX" ] && [ "$PREFIX" != "$NM_TAR_NODE_PATH" ]; then STRIP_COMPONENTS=$(echo "$PREFIX" | awk -F/ '{print NF}') EXTRACT_DIR="$INSTALL_DIR" else EXTRACT_DIR="$NM_DIR" fi else EXTRACT_DIR="$NM_DIR" fi fi # 解压 if command -v pv &> /dev/null; then if [ "$STRIP_COMPONENTS" -gt 0 ]; then pv -pte -s "$(stat -c%s "$NM_TAR")" "$NM_TAR" | tar -xJf - -C "$EXTRACT_DIR" --strip-components="$STRIP_COMPONENTS" || { print_warn "解压失败,将使用 npm install" rm -rf "$NM_DIR" } else pv -pte -s "$(stat -c%s "$NM_TAR")" "$NM_TAR" | tar -xJf - -C "$EXTRACT_DIR" || { print_warn "解压失败,将使用 npm install" rm -rf "$NM_DIR" } fi else if [ "$STRIP_COMPONENTS" -gt 0 ]; then tar -xJf "$NM_TAR" -C "$EXTRACT_DIR" --strip-components="$STRIP_COMPONENTS" || { print_warn "解压失败,将使用 npm install" rm -rf "$NM_DIR" } else tar -xJf "$NM_TAR" -C "$EXTRACT_DIR" || { print_warn "解压失败,将使用 npm install" rm -rf "$NM_DIR" } fi fi # 验证解压结果 if [ -d "$NM_DIR" ] && [ -n "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then print_info "node_modules 解压完成" NODE_MODULES_READY=true rm -f "$NM_TAR" else print_warn "node_modules 为空或解压失败,将使用 npm install" rm -rf "$NM_DIR" fi fi fi # 如果 node_modules 未准备好,使用 npm install if [ "$NODE_MODULES_READY" != "true" ]; then print_info "使用 npm install 安装依赖..." npm install || { print_error "npm install 失败" exit 1 } fi fi # ========================================== # 第九步:配置硬件访问权限 # ========================================== print_header "配置硬件访问权限" print_info "添加用户到硬件组..." sudo usermod -a -G gpio,i2c,audio,video,dialout "$USER" || print_warn "添加用户组失败" # ========================================== # 第十步:配置音频设备(与程序匹配) # ========================================== print_header "配置音频设备" # 检测录音设备(优先 USB 麦克风) MIC_LINE=$(arecord -l 2>/dev/null | grep -Ei "card [0-9]+:.*(USB|usb)" | head -n1) if [ -z "$MIC_LINE" ]; then MIC_LINE=$(arecord -l 2>/dev/null | grep -E "card [0-9]+:" | head -n1) fi MIC_CARD=$(echo "$MIC_LINE" | sed -n 's/card \([0-9]\+\):.*/\1/p') MIC_DEVICE=$(echo "$MIC_LINE" | sed -n 's/.*device \([0-9]\+\):.*/\1/p') if [ -z "$MIC_DEVICE" ]; then MIC_DEVICE=0 fi # 检测播放设备(优先耳机口) PLAYBACK_CARD=$(aplay -l 2>/dev/null | grep -E "Headphones|bcm2835 Headphones" | head -n1 | sed -n 's/card \([0-9]\+\):.*/\1/p') if [ -z "$PLAYBACK_CARD" ]; then PLAYBACK_CARD=$(aplay -l 2>/dev/null | grep -E "card [0-9]+:" | head -n1 | sed -n 's/card \([0-9]\+\):.*/\1/p') fi if [ -z "$PLAYBACK_CARD" ]; then PLAYBACK_CARD=0 fi # 创建 ALSA 配置 ALSA_CONFIG="$HOME/.asoundrc" if [ -n "$MIC_CARD" ]; then print_info "检测到录音设备: card $MIC_CARD, device $MIC_DEVICE" print_info "检测到播放设备卡号: $PLAYBACK_CARD" cat > "$ALSA_CONFIG" << EOF pcm.usbmic { type hw card $MIC_CARD device $MIC_DEVICE } pcm.!default { type asym playback.pcm { type plug slave.pcm "hw:$PLAYBACK_CARD,0" } capture.pcm { type plug slave.pcm "hw:$MIC_CARD,$MIC_DEVICE" } } ctl.!default { type hw card $PLAYBACK_CARD } EOF print_info "ALSA 配置完成: $ALSA_CONFIG" else print_warn "未检测到录音设备,创建默认配置" cat > "$ALSA_CONFIG" << EOF pcm.!default { type asym playback.pcm "plug:hw:0,0" capture.pcm "plug:hw:0,0" } ctl.!default { type hw; card 0; } EOF fi # 创建环境变量文件(与程序匹配) ENV_RASPICAR="$INSTALL_DIR/.env.raspicar" if [ -n "$MIC_CARD" ]; then # 程序使用 ffmpeg -f alsa -i device,所以使用 plughw 格式 cat > "$ENV_RASPICAR" << EOF # 由 setup-raspicar.sh 生成,供麦克风采集使用 # 程序使用 ffmpeg -f alsa 采集,设备格式为 plughw:card,device export NRC_MIC_DEVICE=plughw:$MIC_CARD,$MIC_DEVICE EOF if [ "$USE_PULSE" = "true" ]; then # 如果使用 PulseAudio,程序启动时会 kill PulseAudio,需要保留它 cat >> "$ENV_RASPICAR" << EOF # 保留 PulseAudio(程序启动时会尝试 kill,设置此变量可避免) export NRC_AUDIO_KEEP_PULSE=1 # 使用 PulseAudio 采集(如果程序支持) export NRC_MIC_CAPTURE=pulse EOF fi else cat > "$ENV_RASPICAR" << EOF # 由 setup-raspicar.sh 生成(未检测到麦克风时使用默认) export NRC_MIC_DEVICE=default EOF if [ "$USE_PULSE" = "true" ]; then cat >> "$ENV_RASPICAR" << EOF export NRC_AUDIO_KEEP_PULSE=1 export NRC_MIC_CAPTURE=pulse EOF fi fi print_info "已写入麦克风环境变量: $ENV_RASPICAR" print_info "环境变量内容:" cat "$ENV_RASPICAR" # ========================================== # 第十一步:配置音频服务 # ========================================== print_header "配置音频服务" if [ "$USE_PULSE" = "true" ]; then # 检测 PipeWire USE_PIPEWIRE=false if systemctl --user is-active --quiet pipewire 2>/dev/null || \ systemctl --user is-active --quiet pipewire-pulse 2>/dev/null || \ pgrep -x pipewire >/dev/null 2>&1; then USE_PIPEWIRE=true fi if [ "$USE_PIPEWIRE" = "true" ]; then print_info "检测到 PipeWire,保留并使用其 Pulse 兼容层..." systemctl --user unmask pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user unmask pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user enable pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user start pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user start pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user stop pulseaudio pulseaudio.socket 2>/dev/null || true systemctl --user disable pulseaudio pulseaudio.socket 2>/dev/null || true systemctl --user mask pulseaudio pulseaudio.socket 2>/dev/null || true else print_info "使用 PulseAudio 模式..." systemctl --user stop pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user stop pipewire.socket pipewire-pulse.socket 2>/dev/null || true pkill -f pipewire 2>/dev/null || true pkill -f wireplumber 2>/dev/null || true systemctl --user unmask pulseaudio pulseaudio.socket 2>/dev/null || true systemctl --user enable pulseaudio.socket 2>/dev/null || true systemctl --user start pulseaudio.socket 2>/dev/null || true systemctl --user start pulseaudio 2>/dev/null || true systemctl --user disable pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user disable pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user mask pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user mask pipewire.socket pipewire-pulse.socket 2>/dev/null || true fi else print_info "禁用音频服务(PipeWire / PulseAudio)..." pulseaudio -k 2>/dev/null || true systemctl --user stop pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user stop pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user stop pulseaudio pulseaudio.socket 2>/dev/null || true pkill -f pipewire 2>/dev/null || true pkill -f wireplumber 2>/dev/null || true sleep 1 systemctl --user disable pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user disable pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user disable pulseaudio pulseaudio.socket 2>/dev/null || true systemctl --user mask pipewire pipewire-pulse wireplumber 2>/dev/null || true systemctl --user mask pipewire.socket pipewire-pulse.socket 2>/dev/null || true systemctl --user mask pulseaudio pulseaudio.socket 2>/dev/null || true fi # 配置音频权限 print_info "配置音频设备权限..." sudo chmod 666 /dev/snd/* 2>/dev/null || print_warn "无法设置音频设备权限" # 创建 udev 规则 print_info "创建 USB 设备 udev 规则..." sudo tee /etc/udev/rules.d/99-usb-permissions.rules > /dev/null << EOF # USB 音频设备 SUBSYSTEM=="sound", GROUP="audio", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="046d", GROUP="video", MODE="0666" SUBSYSTEM=="video4linux", GROUP="video", MODE="0666" EOF sudo udevadm control --reload-rules sudo udevadm trigger # ========================================== # 第十二步:创建启动脚本 # ========================================== print_header "创建启动脚本" if [ ! -f "$INSTALL_DIR/run.sh" ]; then cat > "$INSTALL_DIR/run.sh" << 'RUNEOF' #!/bin/bash # RaspiCar 启动脚本:自动加载麦克风环境变量后启动 cd "$(dirname "$0")" [ -f .env.raspicar ] && source .env.raspicar exec node index.js RUNEOF chmod +x "$INSTALL_DIR/run.sh" print_info "已创建启动脚本: $INSTALL_DIR/run.sh" fi # ========================================== # 第十三步:安装 systemd 服务(直接写入系统) # ========================================== print_header "安装 systemd 服务" SERVICE_FILE="/etc/systemd/system/raspicar.service" print_info "写入服务文件到 $SERVICE_FILE ..." sudo tee "$SERVICE_FILE" > /dev/null << EOF [Unit] Description=RaspiCar Service After=network.target [Service] Type=simple User=$USER WorkingDirectory=$INSTALL_DIR Environment="NODE_ENV=production" EnvironmentFile=-$INSTALL_DIR/.env.raspicar ExecStart=/usr/bin/node $INSTALL_DIR/index.js Restart=always RestartSec=10 StandardOutput=journal StandardError=journal NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target EOF print_info "重新加载 systemd..." sudo systemctl daemon-reload print_info "启用服务(开机自启)..." sudo systemctl enable raspicar.service # 配置 sudo 免密(用于前端重启服务功能) print_info "配置 sudo 免密(用于重启服务功能)..." SUDOERS_FILE="/etc/sudoers.d/raspicar" if [ ! -f "$SUDOERS_FILE" ]; then echo "$USER ALL=(ALL) NOPASSWD: /bin/systemctl restart raspicar.service" | sudo tee "$SUDOERS_FILE" > /dev/null sudo chmod 0440 "$SUDOERS_FILE" print_info "已配置 sudo 免密" else print_info "sudo 配置已存在,跳过" fi print_info "服务已安装并启用" print_info "服务管理命令:" echo " sudo systemctl start raspicar # 启动服务" echo " sudo systemctl stop raspicar # 停止服务" echo " sudo systemctl restart raspicar # 重启服务" echo " sudo systemctl status raspicar # 查看状态" echo " sudo systemctl disable raspicar # 禁用开机自启" echo " sudo journalctl -u raspicar -f # 查看实时日志" # ========================================== # 完成 # ========================================== print_header "环境设置完成!" print_info "项目位于: $INSTALL_DIR" if [ -f "$SERVICE_FILE" ]; then print_info "systemd 服务已安装" print_info "启动服务:" echo " sudo systemctl start raspicar" echo "" print_info "服务将在开机时自动启动" else print_warn "请先重启树莓派" print_info "运行程序命令:" echo " cd $INSTALL_DIR && ./run.sh" echo "或手动: cd $INSTALL_DIR && ( [ -f .env.raspicar ] && source .env.raspicar; node index.js )" fi echo "" print_info "音频配置说明:" echo " - 程序使用 ffmpeg -f alsa 采集音频" echo " - 设备格式:plughw:card,device(已自动检测并配置)" echo " - 环境变量文件:$ENV_RASPICAR" if [ "$USE_PULSE" = "true" ]; then echo " - 已启用 PulseAudio 模式(程序启动时会尝试 kill,但设置了 NRC_AUDIO_KEEP_PULSE=1)" fi echo "" print_info "音频问题排查:" echo " 1. 是否已重启树莓派" echo " 2. 运行前是否加载环境变量: source $INSTALL_DIR/.env.raspicar 或直接 ./run.sh" echo " 3. Debian Trixie 默认使用 PipeWire,建议选择「使用 PulseAudio」" echo "" echo "检查音频状态:" echo " - aplay -l # 列出播放设备" echo " - arecord -l # 列出录音设备" echo " - alsamixer # 音频混音器" echo " - sudo fuser -v /dev/snd/* # 查看占用音频设备的进程"