增强无人机连接诊断 + P600 部署脚本

- 连接诊断: 分5步显示具体错误信息和排查步骤
- 新增P600本地部署脚本 (deploy_p600_local.sh)
- 新增SSH隧道自启服务脚本 (setup_p600_tunnel.sh)
- WebSocket 连接超时时长改为15秒,带逐项排查指引

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
master
赵昌 2 weeks ago
parent 81619618ef
commit 5d1de01978

@ -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 ""

@ -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 ""

@ -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 ? '已解锁' : '未解锁'}` });
}
});

Loading…
Cancel
Save