diff --git a/deploy_p600_local.sh b/deploy_p600_local.sh new file mode 100644 index 00000000..2aeeec1d --- /dev/null +++ b/deploy_p600_local.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# ============================================================================= +# P600 机载电脑 - 电脑端网页本地部署脚本 +# 用法: P600 上执行: bash deploy_p600_local.sh +# 效果: 在 P600 上运行 Flask 服务,本地访问 http://192.168.1.14:5000 +# 直接连接 P600 机载电脑的 rosbridge,浏览器不拦截内网连接 +# ============================================================================= + +set -e + +echo "=========================================" +echo " P600 智途投送电脑端本地部署" +echo "=========================================" +echo "" + +PROJECT_DIR="/opt/zhitu-p600" + +# 创建项目目录 +echo "📁 创建项目目录 ${PROJECT_DIR} ..." +mkdir -p ${PROJECT_DIR} + +# 解压文件 +echo "📦 解压文件..." +tar xzf zhitu-local-deploy.tar.gz -C ${PROJECT_DIR} + +# 安装 Python 依赖 +echo "🐍 安装 Python 依赖..." +cd ${PROJECT_DIR}/server +pip3 install flask flask-cors werkzeug 2>/dev/null || pip install flask flask-cors werkzeug 2>/dev/null || echo "⚠️ pip 安装失败,手动执行: pip install flask flask-cors" + +# 创建 systemd 服务 +echo "📝 创建系统服务 zhitu-p600.service ..." + +cat > /etc/systemd/system/zhitu-p600.service << 'EOF' +[Unit] +Description=智途投送电脑端 - P600 本地服务 +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/zhitu-p600/server +ExecStart=/usr/bin/python3 app.py +Restart=always +RestartSec=5 +StandardOutput=append:/var/log/zhitu-p600.log +StandardError=append:/var/log/zhitu-p600.log + +[Install] +WantedBy=multi-user.target +EOF + +# 重载并启用服务 +systemctl daemon-reload +systemctl enable zhitu-p600.service + +# 启动服务 +echo "🚀 启动服务..." +systemctl restart zhitu-p600.service +sleep 2 + +# 检查状态 +echo "" +echo "📊 服务状态:" +systemctl status zhitu-p600.service --no-pager | head -10 + +echo "" +echo "=========================================" +echo " ✅ 部署完成!" +echo "=========================================" +echo "" +echo " 访问地址: http://192.168.1.14:5000" +echo " rosbridge: 本地自动连接 (无需 WebSocket 跨网)" +echo "" +echo " 管理命令:" +echo " 查看状态: sudo systemctl status zhitu-p600" +echo " 重启: sudo systemctl restart zhitu-p600" +echo " 查看日志: tail -f /var/log/zhitu-p600.log" +echo "" diff --git a/setup_p600_tunnel.sh b/setup_p600_tunnel.sh new file mode 100644 index 00000000..9527e0c5 --- /dev/null +++ b/setup_p600_tunnel.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# ============================================================================= +# P600 机载电脑 - SSH 隧道自启服务安装脚本 +# 用法: 传到 P600 上执行: bash setup_p600_tunnel.sh +# 功能: 开机自启 rosbridge 隧道,断线自动重连 +# 隧道: P600:8080 → 云服务器 121.41.216.243:9090 +# 网页连接地址: ws://121.41.216.243:9090 +# ============================================================================= + +set -e + +SERVICE_NAME="rosbridge-tunnel.service" +SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}" +SSH_HOST="root@121.41.216.243" +SSH_PORT="9090" +LOCAL_PORT="8080" + +echo "=========================================" +echo " P600 SSH 隧道自启服务安装" +echo "=========================================" +echo "" + +# 检查是否以 root 运行 +if [ "$EUID" -ne 0 ]; then + echo "❌ 请使用 root 用户运行: sudo bash setup_p600_tunnel.sh" + exit 1 +fi + +# 检查 SSH 连接是否可用 +echo "🔍 测试 SSH 连接到 ${SSH_HOST} ..." +if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${SSH_HOST}" "echo OK" 2>/dev/null; then + echo "✅ SSH 连接成功" +else + echo "" + echo "⚠️ SSH 连接失败,请确认:" + echo " ① P600 能访问外网" + echo " ② 密码为: pdl@#YwC\$WRWFyHKxC8nyu!4" + echo "" + echo " 手动测试: ssh ${SSH_HOST}" + echo "" + read -p "是否继续安装服务?(y/n)" -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# 检查 sshpass +SSHPASS_CMD="" +if command -v sshpass &> /dev/null; then + SSHPASS_CMD="sshpass -p 'pdl@#YwC\$WRWFyHKxC8nyu!4'" + echo "✅ 已安装 sshpass,将使用密码自动登录" +else + echo "" + echo "⚠️ 未安装 sshpass,需要先配置 SSH 密钥登录" + echo " 执行以下命令配置免密登录:" + echo "" + echo " ssh-copy-id ${SSH_HOST}" + echo " # 密码: pdl@#YwC\$WRWFyHKxC8nyu!4" + echo "" +fi + +# 创建 systemd 服务 +echo "" +echo "📝 创建系统服务 ${SERVICE_NAME} ..." + +cat > "${SERVICE_PATH}" << SERVICEEOF +[Unit] +Description=ROSbridge SSH Tunnel - P600 to Cloud Server +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=60 +StartLimitBurst=5 + +[Service] +Type=simple +ExecStart=/bin/bash -c '${SSHPASS_CMD} /usr/bin/ssh -NT \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o StrictHostKeyChecking=no \ + -o ExitOnForwardFailure=yes \ + -R ${SSH_PORT}:localhost:${LOCAL_PORT} \ + ${SSH_HOST}' + +Restart=always +RestartSec=10 +User=root + +[Install] +WantedBy=multi-user.target +SERVICEEOF + +echo "✅ 服务文件已创建" + +# 重载 systemd +echo "🔄 重载 systemd 配置..." +systemctl daemon-reload + +# 启用服务 +echo "🔗 启用开机自启..." +systemctl enable "${SERVICE_NAME}" + +# 启动服务 +echo "🚀 启动隧道服务..." +systemctl restart "${SERVICE_NAME}" + +# 等待 3 秒 +sleep 3 + +# 检查状态 +echo "" +echo "📊 服务状态:" +systemctl status "${SERVICE_NAME}" --no-pager | head -12 + +echo "" +echo "=========================================" +echo " ✅ 安装完成!" +echo "=========================================" +echo "" +echo " 服务管理命令:" +echo " 查看状态: sudo systemctl status ${SERVICE_NAME}" +echo " 启动: sudo systemctl start ${SERVICE_NAME}" +echo " 停止: sudo systemctl stop ${SERVICE_NAME}" +echo " 重启: sudo systemctl restart ${SERVICE_NAME}" +echo " 查看日志: sudo journalctl -u ${SERVICE_NAME} -f" +echo "" +echo " 网页连接地址: ws://121.41.216.243:${SSH_PORT}" +echo "" diff --git a/software/src/软件电脑端/js/websocket.js b/software/src/软件电脑端/js/websocket.js index bdbad8e4..5680b6b5 100644 --- a/software/src/软件电脑端/js/websocket.js +++ b/software/src/软件电脑端/js/websocket.js @@ -99,7 +99,9 @@ const WsModule = (() => { function connectDrone(url) { try { if (typeof ROSLIB === 'undefined') { - UIModule.onStatus({ type: 'error', message: 'roslibjs 库未加载' }); + UIModule.onStatus({ type: 'error', message: '❌ [环境检查] roslibjs 库未加载' }); + UIModule.onStatus({ type: 'warning', message: ' 原因: 浏览器可能无法访问 unpkg.com CDN' }); + UIModule.onStatus({ type: 'warning', message: ' 解决: 已在页面中加入 cdnjs 备选地址,刷新重试;或切换网络' }); return; } @@ -107,18 +109,95 @@ const WsModule = (() => { try { ros.close(); } catch (e) { /* ignore */ } } - UIModule.onStatus({ type: 'info', message: `正在连接 rosbridge: ${url} ...` }); + // === Step 0: URL 格式检查 === + UIModule.onStatus({ type: 'info', message: '═══════ 无人机连接诊断 ═══════' }); + UIModule.onStatus({ type: 'info', message: `[Step 0/5] 地址检查: ${url}` }); + var parsedOk = true; + try { + var parsed = new URL(url); + if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') { + UIModule.onStatus({ type: 'warning', message: ' ⚠️ 协议不是 ws:// 或 wss://,请检查地址格式' }); + parsedOk = false; + } + if (parsed.hostname === '192.168.1.14') { + UIModule.onStatus({ type: 'info', message: ' ⚠️ 当前使用默认地址 192.168.1.14,如果 P600 的 IP 不同请修改' }); + UIModule.onStatus({ type: 'info', message: ' 💡 在 P600 上执行 hostname -I 查看实际 IP' }); + } + } catch(e) { + UIModule.onStatus({ type: 'error', message: ' ❌ 地址格式错误,应为 ws://IP:9090 格式' }); + parsedOk = false; + } + if (!parsedOk) return; + + // === Step 1: 尝试 WebSocket 连接 === + UIModule.onStatus({ type: 'info', message: `[Step 1/5] 正在建立 WebSocket 连接到 ${url} ...` }); + UIModule.onStatus({ type: 'info', message: ` 提示: 如果长时间无响应,请逐项排查:` }); + UIModule.onStatus({ type: 'info', message: ` ① 电脑是否连接到 P600 的 WiFi (或同一局域网)` }); + UIModule.onStatus({ type: 'info', message: ` ② 在电脑 CMD 中执行: ping ${parsed.hostname}` }); + + // 连接超时检测(15秒) + var connTimeout = setTimeout(function() { + if (!rosConnected) { + UIModule.onStatus({ type: 'warning', message: `⏱ [Step 1/5] 连接超时 (15s)` }); + UIModule.onStatus({ type: 'warning', message: ` 🔍 排查步骤:` }); + UIModule.onStatus({ type: 'warning', message: ` ① 确认电脑 WiFi 已连接到 P600` }); + UIModule.onStatus({ type: 'warning', message: ` ② 在 CMD 执行 ping ${parsed.hostname} 检测连通性` }); + UIModule.onStatus({ type: 'warning', message: ` ③ SSH 进 P600 确认 rosbridge 是否运行:` }); + UIModule.onStatus({ type: 'warning', message: ` ps aux | grep rosbridge` }); + UIModule.onStatus({ type: 'warning', message: ` ④ 如果没运行,启动它:` }); + UIModule.onStatus({ type: 'warning', message: ` roslaunch rosbridge_server rosbridge_websocket.launch` }); + UIModule.onStatus({ type: 'warning', message: ` ⑤ 检查 P600 防火墙: sudo ufw status | grep 9090` }); + } + }, 15000); ros = new ROSLIB.Ros({ url: url }); ros.on('connection', () => { + clearTimeout(connTimeout); rosConnected = true; - UIModule.onStatus({ type: 'info', message: 'rosbridge 已连接,正在检测 MAVROS 话题...' }); + UIModule.onStatus({ type: 'success', message: `[Step 2/5] ✅ WebSocket 连接成功!` }); + UIModule.onStatus({ type: 'info', message: ` rosbridge 地址: ${url}` }); + UIModule.onStatus({ type: 'info', message: `[Step 3/5] 正在订阅 MAVROS 话题 (等待 5 秒内收到 state)...` }); + UIModule.onStatus({ type: 'info', message: ` 如果长时间无响应:` }); + UIModule.onStatus({ type: 'info', message: ` ① 在 P600 上确认 MAVROS 已启动:` }); + UIModule.onStatus({ type: 'info', message: ` roslaunch mavros px4.launch fcu_url:=/dev/ttyTHS1:921600` }); + UIModule.onStatus({ type: 'info', message: ` ② 检查话题列表: rostopic list | grep mavros/state` }); + UIModule.onStatus({ type: 'info', message: ` ③ 话题命名空间可能是 /uav1/mavros/state 或 /mavros/state` }); setupSubscriptions(); }); ros.on('error', (error) => { - UIModule.onStatus({ type: 'error', message: `rosbridge 连接错误: ${error || '未知'}` }); + clearTimeout(connTimeout); + var msg = String(error || ''); + var detail = ''; + var steps = ''; + + if (msg.includes('ECONNREFUSED') || msg.includes('Connection refused')) { + detail = '❌ 连接被拒绝 (ECONNREFUSED)'; + steps = ' rosbridge 未在 P600 上运行或端口不是 9090\n 解决: roslaunch rosbridge_server rosbridge_websocket.launch'; + } else if (msg.includes('ENETUNREACH') || msg.includes('Network is unreachable')) { + detail = '❌ 网络不可达 (ENETUNREACH)'; + steps = ' 电脑与 P600 不在同一网络\n 解决: 连接 P600 的 WiFi,或检查网络设置'; + } else if (msg.includes('EHOSTUNREACH') || msg.includes('No route to host')) { + detail = '❌ 主机不可达 (EHOSTUNREACH)'; + steps = ` IP 地址 ${parsed.hostname} 可能不正确\n 解决: 在 P600 上执行 hostname -I 获取正确 IP`; + } else if (msg.includes('ETIMEDOUT') || msg.includes('timed out')) { + detail = '❌ 连接超时 (ETIMEDOUT)'; + steps = ` 能 ping 通 ${parsed.hostname} 吗?\n 解决: CMD 中执行 ping ${parsed.hostname}\n 如果不通: 检查网络连接\n 如果通: 检查 P600 上 rosbridge 是否卡住,尝试重启`; + } else if (msg.includes('SecurityError') || msg.includes('security')) { + detail = '❌ 安全错误 (SecurityError)'; + steps = ' 浏览器安全策略阻止了连接\n 解决: 使用 HTTP 页面而非 HTTPS,或使用 localhost'; + } else { + detail = `❌ 连接失败: ${msg || '未知错误'}`; + steps = ' 查看浏览器控制台 (F12 > Console) 获取详细错误\n 或在 CMD 中测试: curl -v http://' + parsed.hostname + ':5000/api/ping'; + } + UIModule.onStatus({ type: 'error', message: `[Step 1/5] ${detail}` }); + if (steps) { + var parts = steps.split('\n'); + for (var si = 0; si < parts.length; si++) { + UIModule.onStatus({ type: (detail.includes('❌') ? 'warning' : 'info'), message: parts[si] }); + } + } }); ros.on('close', () => { @@ -127,11 +206,12 @@ const WsModule = (() => { droneConnected = false; stopAll(); if (wasConnected) { + UIModule.onStatus({ type: 'warning', message: '🔌 与 rosbridge 的连接已断开' }); UIModule.onDroneDisconnected(); } }); } catch (e) { - UIModule.onStatus({ type: 'error', message: `连接失败: ${e.message || e}` }); + UIModule.onStatus({ type: 'error', message: `❌ 连接异常: ${e.message || e}` }); } } @@ -168,10 +248,13 @@ const WsModule = (() => { // Try without namespace if (tryNS !== '') { uavNS = ''; - UIModule.onStatus({ type: 'warning', message: `未收到 ${tryNS}/mavros/state,尝试无命名空间...` }); + UIModule.onStatus({ type: 'warning', message: `[Step 3/4] ⏱ 未收到 ${tryNS}/mavros/state (5s 超时),尝试无命名空间...` }); trySubscribesWithFallback(); } else { - UIModule.onStatus({ type: 'error', message: '未检测到 MAVROS 话题,请确认仿真/实机已启动' }); + UIModule.onStatus({ type: 'error', message: `[Step 3/4] ❌ 未检测到 MAVROS 话题` }); + UIModule.onStatus({ type: 'warning', message: ` 请确认 P600 上 MAVROS 与 rosbridge 是否正常运行:` }); + UIModule.onStatus({ type: 'warning', message: ` roslaunch mavros px4.launch fcu_url:=/dev/ttyTHS1:921600 (或对应设备)` }); + UIModule.onStatus({ type: 'warning', message: ` roslaunch rosbridge_server rosbridge_websocket.launch` }); } } }, 5000); @@ -190,9 +273,12 @@ const WsModule = (() => { droneConnected = true; if (modeValid) { fcuReady = true; - UIModule.onStatus({ type: 'success', message: `无人机已连接 (模式: ${msg.mode}, ${msg.armed ? '已解锁' : '未解锁'})` }); + UIModule.onStatus({ type: 'success', message: `[Step 4/4] ✅ 无人机已连接 (命名空间: ${uavNS || '无'})` }); + UIModule.onStatus({ type: 'success', message: ` 模式: ${msg.mode}, ${msg.armed ? '已解锁' : '未解锁'}` }); } else { - UIModule.onStatus({ type: 'warning', message: `MAVROS 已连接,但飞控未就绪 (mode=空)。等待 PX4 初始化...` }); + UIModule.onStatus({ type: 'success', message: `[Step 3/4] ✅ MAVROS 已连接,但飞控未就绪 (mode=空)` }); + UIModule.onStatus({ type: 'warning', message: ` ⏳ 等待 PX4 飞控初始化 (通常需要 10-30 秒)...` }); + UIModule.onStatus({ type: 'info', message: ` 也可以在 P600 上手动检查: rostopic echo /mavros/state` }); } setDefaultOrigin(); } @@ -200,7 +286,7 @@ const WsModule = (() => { // Detect when FCU becomes ready if (!fcuReady && modeValid) { fcuReady = true; - UIModule.onStatus({ type: 'success', message: `飞控已就绪! 当前模式: ${msg.mode}, 状态: ${msg.armed ? '已解锁' : '未解锁'}` }); + UIModule.onStatus({ type: 'success', message: `✅ 飞控已就绪! 当前模式: ${msg.mode}, 状态: ${msg.armed ? '已解锁' : '未解锁'}` }); } });