You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Software_Architecture/distance-judgement/drone_control.html

1223 lines
43 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无人机控制 - 距离判断系统</title>
<!-- 内联Bootstrap CSS - 避免CDN依赖 -->
<style>
/* Bootstrap 5.1.3 基础样式 - 简化版 */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #f8f9fa;
}
.container-fluid {
width: 100%;
padding-right: 0.75rem;
padding-left: 0.75rem;
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -0.75rem;
margin-left: -0.75rem;
}
.col-12 {
flex: 0 0 auto;
width: 100%;
}
.col-lg-6 {
flex: 0 0 auto;
width: 50%;
}
@media (max-width: 991.98px) {
.col-lg-6 {
width: 100%;
}
}
.py-4 {
padding-top: 1.5rem !important;
padding-bottom: 1.5rem !important;
}
.mb-4 {
margin-bottom: 1.5rem !important;
}
.mb-3 {
margin-bottom: 1rem !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.me-2 {
margin-right: 0.5rem !important;
}
.me-3 {
margin-right: 1rem !important;
}
.ms-auto {
margin-left: auto !important;
}
.text-center {
text-align: center !important;
}
.text-muted {
color: #6c757d !important;
}
.d-flex {
display: flex !important;
}
.d-block {
display: block !important;
}
.align-items-center {
align-items: center !important;
}
.justify-content-center {
justify-content: center !important;
}
.btn {
display: inline-block;
padding: 0.375rem 0.75rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}
.btn:hover {
text-decoration: none;
}
.btn:disabled {
pointer-events: none;
opacity: 0.65;
}
.btn-primary {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
}
.btn-success {
color: #fff;
background-color: #198754;
border-color: #198754;
}
.btn-success:hover {
background-color: #157347;
border-color: #146c43;
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #bb2d3b;
border-color: #b02a37;
}
.btn-warning {
color: #000;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-warning:hover {
background-color: #ffca2c;
border-color: #ffc720;
}
.btn-info {
color: #000;
background-color: #0dcaf0;
border-color: #0dcaf0;
}
.btn-info:hover {
background-color: #31d2f2;
border-color: #25cff2;
}
.btn-light {
color: #000;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-light:hover {
background-color: #f9fafb;
border-color: #f9fafb;
}
.btn-outline-light {
color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-outline-light:hover {
color: #000;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-outline-warning {
color: #ffc107;
border-color: #ffc107;
}
.btn-outline-warning:hover {
color: #000;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-lg {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.bg-danger {
background-color: #dc3545 !important;
}
.bg-success {
background-color: #198754 !important;
}
.bg-warning {
background-color: #ffc107 !important;
color: #000 !important;
}
.fs-6 {
font-size: 1rem !important;
}
.fs-5 {
font-size: 1.25rem !important;
}
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
.alert-info {
color: #055160;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-success {
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
}
.alert-warning {
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-secondary {
color: #383d41;
background-color: #e2e3e5;
border-color: #d6d8db;
}
.form-range {
width: 100%;
height: 1.5rem;
padding: 0;
background-color: transparent;
appearance: none;
-webkit-appearance: none;
}
.form-range::-webkit-slider-track {
width: 100%;
height: 0.5rem;
color: transparent;
cursor: pointer;
background-color: #dee2e6;
border-color: transparent;
border-radius: 1rem;
}
.form-range::-webkit-slider-thumb {
width: 1rem;
height: 1rem;
margin-top: -0.25rem;
background-color: #0d6efd;
border: 0;
border-radius: 1rem;
-webkit-appearance: none;
appearance: none;
}
.form-label {
margin-bottom: 0.5rem;
font-weight: 500;
}
.img-fluid {
max-width: 100%;
height: auto;
}
.rounded {
border-radius: 0.25rem !important;
}
/* 自定义样式 */
.video-container {
background: #000;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
}
.btn-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
max-width: 300px;
margin: 0 auto;
}
.btn-grid .btn {
min-height: 50px;
font-size: 14px;
}
.status-badge {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.control-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
}
.video-panel {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border-radius: 15px;
padding: 20px;
}
.status-panel {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border-radius: 15px;
padding: 15px;
}
/* Font Awesome图标替换为文字符号 */
.fa-wifi::before {
content: "📶";
}
.fa-play::before {
content: "▶";
}
.fa-stop::before {
content: "⏹";
}
.fa-paper-plane::before {
content: "✈";
}
.fa-rocket::before {
content: "🚀";
}
.fa-arrow-up::before {
content: "↑";
}
.fa-arrow-down::before {
content: "↓";
}
.fa-arrow-left::before {
content: "←";
}
.fa-arrow-right::before {
content: "→";
}
.fa-undo::before {
content: "↶";
}
.fa-redo::before {
content: "↷";
}
.fa-video::before {
content: "📹";
}
.fa-camera::before {
content: "📷";
}
.fa-ruler::before {
content: "📏";
}
.fa-sync-alt::before {
content: "🔄";
}
.fa-tachometer-alt::before {
content: "⚡";
}
.fa-home::before {
content: "🏠";
}
.fa-cog::before {
content: "⚙";
}
.fa-info-circle::before {
content: "";
}
.fa-check-circle::before {
content: "✅";
}
.fa-exclamation-triangle::before {
content: "⚠";
}
.fa-times-circle::before {
content: "❌";
}
.fa-minus-circle::before {
content: "⊖";
}
.fa-circle::before {
content: "●";
}
.text-success {
color: #198754 !important;
}
.text-secondary {
color: #6c757d !important;
}
.text-dark {
color: #212529 !important;
}
.text-white {
color: #fff !important;
}
</style>
</head>
<body class="bg-light">
<div class="container-fluid py-4">
<!-- 状态指示器 -->
<div class="status-badge">
<span id="connectionBadge" class="badge bg-danger fs-6">未连接</span>
</div>
<!-- 页面标题 -->
<div class="row mb-4">
<div class="col-12 text-center">
<h1 class="display-5">🚁 RoboMaster TT 无人机控制系统</h1>
<p class="text-muted fs-5">基于距离判断系统的无人机实时视频传输控制</p>
</div>
</div>
<div class="row">
<!-- 左侧控制面板 -->
<div class="col-lg-6">
<!-- 连接控制 -->
<div class="control-panel">
<h5 class="mb-3"><i class="fa-wifi me-2"></i>连接控制</h5>
<div class="d-flex mb-3 align-items-center">
<button id="connectBtn" class="btn btn-light me-2">
<i class="fa-play me-1"></i>连接无人机
</button>
<button id="disconnectBtn" class="btn btn-danger me-2" disabled>
<i class="fa-stop me-1"></i>断开连接
</button>
<button id="diagnoseBtn" class="btn btn-info me-2">
🔍 诊断连接
</button>
<div class="ms-auto">
<span id="batteryStatus" class="badge bg-warning text-dark fs-6">电量: --</span>
</div>
</div>
<div class="alert alert-info mb-0" id="connectionStatus">
<i class="fa-info-circle me-2"></i>未连接到无人机请确保已连接到无人机WiFi (TELLO-xxxxxx)
</div>
</div>
<!-- 飞行控制 -->
<div class="control-panel">
<h5 class="mb-3"><i class="fa-paper-plane me-2"></i>飞行控制</h5>
<!-- 起飞降落 -->
<div class="d-flex justify-content-center mb-4">
<button id="takeoffBtn" class="btn btn-success btn-lg me-3" disabled>
<i class="fa-rocket me-1"></i>起飞
</button>
<button id="landBtn" class="btn btn-warning btn-lg" disabled>
<i class="fa-arrow-down me-1"></i>降落
</button>
</div>
<!-- 移动控制网格 -->
<div class="btn-grid mb-4">
<div></div>
<button id="upBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-up d-block mb-1"></i>上升
</button>
<div></div>
<button id="leftBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-left d-block mb-1"></i>左移
</button>
<button id="forwardBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-up d-block mb-1"></i>前进
</button>
<button id="rightBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-right d-block mb-1"></i>右移
</button>
<button id="rotateLeftBtn" class="btn btn-outline-warning" disabled>
<i class="fa-undo d-block mb-1"></i>左转
</button>
<button id="backBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-down d-block mb-1"></i>后退
</button>
<button id="rotateRightBtn" class="btn btn-outline-warning" disabled>
<i class="fa-redo d-block mb-1"></i>右转
</button>
<div></div>
<button id="downBtn" class="btn btn-outline-light" disabled>
<i class="fa-arrow-down d-block mb-1"></i>下降
</button>
<div></div>
</div>
<!-- 参数控制 -->
<div class="row">
<div class="col-md-6">
<label for="moveDistance" class="form-label">
<i class="fa-ruler me-1"></i>移动距离 (厘米)
</label>
<input type="range" class="form-range" id="moveDistance" min="20" max="100" step="10"
value="30">
<div class="text-center"><span id="distanceValue">30</span> 厘米</div>
</div>
<div class="col-md-6">
<label for="rotateAngle" class="form-label">
<i class="fa-sync-alt me-1"></i>旋转角度 (度)
</label>
<input type="range" class="form-range" id="rotateAngle" min="15" max="360" step="15"
value="90">
<div class="text-center"><span id="angleValue">90</span></div>
</div>
</div>
</div>
</div>
<!-- 右侧视频面板 -->
<div class="col-lg-6">
<!-- 视频流控制 -->
<div class="video-panel mb-3">
<h5 class="mb-3"><i class="fa-video me-2"></i>实时视频流</h5>
<div class="d-flex mb-3">
<button id="startVideoBtn" class="btn btn-light me-2" disabled>
<i class="fa-play me-1"></i>开始视频流
</button>
<button id="stopVideoBtn" class="btn btn-danger me-2" disabled>
<i class="fa-stop me-1"></i>停止视频流
</button>
<button id="captureBtn" class="btn btn-info" disabled>
<i class="fa-camera me-1"></i>捕获图像
</button>
</div>
<div class="video-container mb-3">
<img id="videoStream" class="img-fluid rounded" style="max-width: 100%; max-height: 350px;"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzMzIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuaXoOinhumikea1gTwvdGV4dD48L3N2Zz4="
alt="无视频流">
</div>
<div id="videoStatus" class="text-center">
<small><i class="fa-circle text-secondary me-1"></i>视频流状态: 未启动</small>
</div>
</div>
<!-- 无人机状态 -->
<div class="status-panel">
<h6 class="mb-3"><i class="fa-tachometer-alt me-2"></i>无人机状态</h6>
<div class="row text-center">
<div class="col-3">
<div class="h4 mb-0" id="batteryLevel">--</div>
<small>电量</small>
</div>
<div class="col-3">
<div class="h4 mb-0" id="heightLevel">--</div>
<small>高度</small>
</div>
<div class="col-3">
<div class="h4 mb-0" id="speedLevel">--</div>
<small>速度</small>
</div>
<div class="col-3">
<div class="h4 mb-0" id="signalLevel">--</div>
<small>信号</small>
</div>
</div>
</div>
</div>
</div>
<!-- 快速访问链接 -->
<div class="row mt-4">
<div class="col-12 text-center">
<div class="btn-group" role="group">
<a href="/" class="btn btn-outline-primary">
<i class="fa-home me-1"></i>返回主页
</a>
<a href="/test_device_selector.html" class="btn btn-outline-info">
<i class="fa-cog me-1"></i>设备测试
</a>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let isConnected = false;
let isVideoStreaming = false;
let videoUpdateInterval = null;
// DOM元素
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const diagnoseBtn = document.getElementById('diagnoseBtn');
const connectionStatus = document.getElementById('connectionStatus');
const connectionBadge = document.getElementById('connectionBadge');
const batteryStatus = document.getElementById('batteryStatus');
const takeoffBtn = document.getElementById('takeoffBtn');
const landBtn = document.getElementById('landBtn');
const upBtn = document.getElementById('upBtn');
const downBtn = document.getElementById('downBtn');
const leftBtn = document.getElementById('leftBtn');
const rightBtn = document.getElementById('rightBtn');
const forwardBtn = document.getElementById('forwardBtn');
const backBtn = document.getElementById('backBtn');
const rotateLeftBtn = document.getElementById('rotateLeftBtn');
const rotateRightBtn = document.getElementById('rotateRightBtn');
const startVideoBtn = document.getElementById('startVideoBtn');
const stopVideoBtn = document.getElementById('stopVideoBtn');
const captureBtn = document.getElementById('captureBtn');
const videoStream = document.getElementById('videoStream');
const videoStatus = document.getElementById('videoStatus');
const moveDistance = document.getElementById('moveDistance');
const rotateAngle = document.getElementById('rotateAngle');
const distanceValue = document.getElementById('distanceValue');
const angleValue = document.getElementById('angleValue');
// 初始化
document.addEventListener('DOMContentLoaded', function () {
setupEventListeners();
// 🔧 确保状态变量正确初始化
isConnected = false;
isVideoStreaming = false;
videoUpdateInterval = null;
// 更新界面状态
updateConnectionState(false);
updateStatus('未连接到无人机请确保已连接到无人机WiFi (TELLO-xxxxxx)', 'info');
console.log('🚁 无人机控制界面已加载');
console.log('📊 初始状态 - 连接:', isConnected, '视频流:', isVideoStreaming);
});
// 设置事件监听器
function setupEventListeners() {
connectBtn.addEventListener('click', connectDrone);
disconnectBtn.addEventListener('click', disconnectDrone);
diagnoseBtn.addEventListener('click', diagnoseDroneConnection);
takeoffBtn.addEventListener('click', () => sendCommand('takeoff'));
landBtn.addEventListener('click', () => sendCommand('land'));
upBtn.addEventListener('click', () => sendMoveCommand('up'));
downBtn.addEventListener('click', () => sendMoveCommand('down'));
leftBtn.addEventListener('click', () => sendMoveCommand('left'));
rightBtn.addEventListener('click', () => sendMoveCommand('right'));
forwardBtn.addEventListener('click', () => sendMoveCommand('forward'));
backBtn.addEventListener('click', () => sendMoveCommand('back'));
rotateLeftBtn.addEventListener('click', () => sendRotateCommand('ccw'));
rotateRightBtn.addEventListener('click', () => sendRotateCommand('cw'));
startVideoBtn.addEventListener('click', startVideo);
stopVideoBtn.addEventListener('click', stopVideo);
captureBtn.addEventListener('click', captureImage);
moveDistance.addEventListener('input', () => {
distanceValue.textContent = moveDistance.value;
});
rotateAngle.addEventListener('input', () => {
angleValue.textContent = rotateAngle.value;
});
}
// 连接无人机
async function connectDrone() {
try {
connectBtn.disabled = true;
updateStatus('正在连接无人机...', 'warning');
const response = await fetch('/api/drone/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
drone_ip: '192.168.10.1'
})
});
const result = await response.json();
if (result.status === 'success') {
isConnected = true;
updateStatus('无人机连接成功!', 'success');
updateConnectionState(true);
updateDroneStatus(result.drone_info);
showToast('✅ 无人机连接成功!', 'success');
} else {
updateStatus('连接失败: ' + result.message, 'danger');
showToast('❌ 连接失败: ' + result.message, 'error');
}
} catch (error) {
updateStatus('连接错误: ' + error.message, 'danger');
showToast('🔥 连接错误: ' + error.message, 'error');
} finally {
connectBtn.disabled = false;
}
}
// 断开无人机
async function disconnectDrone() {
try {
const response = await fetch('/api/drone/disconnect', {
method: 'POST'
});
const result = await response.json();
// 🔧 强制清理所有状态
isConnected = false;
isVideoStreaming = false;
// 停止视频帧更新
if (videoUpdateInterval) {
clearInterval(videoUpdateInterval);
videoUpdateInterval = null;
console.log('⏹️ 已停止视频帧更新定时器');
}
// 重置视频显示
videoStream.src = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzMzIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuaXoOinhumikea1gTwvdGV4dD48L3N2Zz4=";
videoStatus.innerHTML = '<small><i class="fa-circle text-secondary me-1"></i>视频流状态: 未启动</small>';
// 重置按钮状态
startVideoBtn.disabled = true;
stopVideoBtn.disabled = true;
captureBtn.disabled = true;
updateStatus('无人机已断开连接', 'secondary');
updateConnectionState(false);
showToast('📡 无人机已断开连接', 'info');
console.log('📊 断开后状态 - 连接:', isConnected, '视频流:', isVideoStreaming);
} catch (error) {
updateStatus('断开连接错误: ' + error.message, 'danger');
showToast('❌ 断开连接错误: ' + error.message, 'error');
// 即使出错也要清理状态
isConnected = false;
isVideoStreaming = false;
if (videoUpdateInterval) {
clearInterval(videoUpdateInterval);
videoUpdateInterval = null;
}
updateConnectionState(false);
}
}
// 发送控制命令
async function sendCommand(command, params = {}) {
if (!isConnected) {
showToast('⚠️ 请先连接无人机', 'warning');
return;
}
try {
showToast('📡 正在发送命令: ' + command, 'info');
const response = await fetch('/api/drone/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
command: command,
params: params
})
});
const result = await response.json();
if (result.status === 'success') {
showToast('✅ 命令执行成功: ' + command, 'success');
} else {
showToast('❌ 命令执行失败: ' + result.message, 'error');
}
} catch (error) {
showToast('🔥 命令发送错误: ' + error.message, 'error');
}
}
// 发送移动命令
function sendMoveCommand(direction) {
const distance = parseInt(moveDistance.value);
sendCommand('move', { direction: direction, distance: distance });
}
// 发送旋转命令
function sendRotateCommand(direction) {
const angle = parseInt(rotateAngle.value);
sendCommand('rotate', { direction: direction, angle: angle });
}
// 开始视频流
async function startVideo() {
if (!isConnected) {
showToast('⚠️ 请先连接无人机', 'warning');
return;
}
try {
const response = await fetch('/api/drone/start_video', {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
isVideoStreaming = true;
videoStatus.innerHTML = '<small><i class="fa-circle text-success me-1"></i>视频流状态: 正在接收</small>';
startVideoBtn.disabled = true;
stopVideoBtn.disabled = false;
captureBtn.disabled = false;
// 开始更新视频帧
videoUpdateInterval = setInterval(updateVideoFrame, 200); // 5 FPS
showToast('📹 视频流已启动', 'success');
} else {
showToast('❌ 视频流启动失败: ' + result.message, 'error');
}
} catch (error) {
showToast('🔥 视频流启动错误: ' + error.message, 'error');
}
}
// 停止视频流
async function stopVideo() {
try {
const response = await fetch('/api/drone/stop_video', {
method: 'POST'
});
isVideoStreaming = false;
videoStatus.innerHTML = '<small><i class="fa-circle text-secondary me-1"></i>视频流状态: 已停止</small>';
startVideoBtn.disabled = false;
stopVideoBtn.disabled = true;
captureBtn.disabled = true;
if (videoUpdateInterval) {
clearInterval(videoUpdateInterval);
videoUpdateInterval = null;
}
// 重置视频显示
videoStream.src = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMzMzIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuaXoOinhumikea1gTwvdGV4dD48L3N2Zz4=";
showToast('⏹️ 视频流已停止', 'info');
} catch (error) {
showToast('❌ 停止视频流错误: ' + error.message, 'error');
}
}
// 更新视频帧
async function updateVideoFrame() {
// 双重检查确保视频流已启动
if (!isVideoStreaming || !isConnected) {
console.log('⚠️ 视频流未启动或无人机未连接,跳过帧更新');
return;
}
try {
const response = await fetch('/api/drone/video_frame');
// 检查HTTP响应状态
if (!response.ok) {
if (response.status === 404) {
// 没有视频帧可用,这是正常的,不显示错误
console.log('📹 等待视频帧...');
return;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.status === 'success') {
videoStream.src = result.frame;
const fps = result.stats?.fps || 0;
videoStatus.innerHTML = `<small><i class="fa-circle text-success me-1"></i>视频流状态: 正在接收 (${fps.toFixed(1)} FPS)</small>`;
} else if (result.status === 'no_frame') {
// 暂时没有帧,这是正常的
console.log('📹 等待视频帧数据...');
} else {
console.warn('视频帧响应异常:', result.message);
}
} catch (error) {
// 只在非预期错误时显示日志
if (!error.message.includes('404')) {
console.log('获取视频帧失败:', error.message);
}
}
}
// 捕获图像
function captureImage() {
if (!isVideoStreaming) {
showToast('⚠️ 请先启动视频流', 'warning');
return;
}
showToast('📸 图像捕获功能开发中...', 'info');
}
// 🔍 诊断无人机连接
async function diagnoseDroneConnection() {
try {
diagnoseBtn.disabled = true;
diagnoseBtn.innerHTML = '🔄 诊断中...';
showToast('🔍 正在诊断无人机连接状态...', 'info');
const response = await fetch('/api/drone/diagnose');
const result = await response.json();
if (result.status === 'success') {
const diagnosis = result.diagnosis;
// 🔧 显示诊断结果
let diagnosticHtml = '<div class="diagnostic-results">';
diagnosticHtml += '<h6>🔍 诊断结果:</h6>';
// 显示建议
diagnosis.recommendations.forEach(rec => {
diagnosticHtml += `<div class="mb-1">${rec}</div>`;
});
// 显示详细信息
diagnosticHtml += '<hr>';
diagnosticHtml += '<h6>📊 详细信息:</h6>';
if (diagnosis.network.ping_success !== undefined) {
diagnosticHtml += `<div>网络连通性: ${diagnosis.network.ping_success ? '✅ 正常' : '❌ 失败'}</div>`;
}
if (diagnosis.network.port_11111_available !== undefined) {
diagnosticHtml += `<div>UDP端口11111: ${diagnosis.network.port_11111_available ? '✅ 可用' : '❌ 被占用'}</div>`;
}
if (diagnosis.tello.connected !== undefined) {
diagnosticHtml += `<div>Tello连接: ${diagnosis.tello.connected ? '✅ 已连接' : '❌ 未连接'}</div>`;
if (diagnosis.tello.battery) {
diagnosticHtml += `<div>电池电量: ${diagnosis.tello.battery}%</div>`;
}
}
if (diagnosis.video_stream.receiver_exists !== undefined) {
diagnosticHtml += `<div>视频接收器: ${diagnosis.video_stream.receiver_exists ? '✅ 已创建' : '❌ 未创建'}</div>`;
if (diagnosis.video_stream.running !== undefined) {
diagnosticHtml += `<div>视频流状态: ${diagnosis.video_stream.running ? '🟢 运行中' : '⚪ 已停止'}</div>`;
}
if (diagnosis.video_stream.has_frames !== undefined) {
diagnosticHtml += `<div>视频帧接收: ${diagnosis.video_stream.has_frames ? '✅ 正常' : '⚠️ 无数据'}</div>`;
}
}
diagnosticHtml += `<div>系统平台: ${diagnosis.system.platform}</div>`;
diagnosticHtml += `<div>OpenCV版本: ${diagnosis.system.opencv_version}</div>`;
diagnosticHtml += '</div>';
// 创建诊断结果弹窗
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-info alert-dismissible fade show position-fixed';
alertDiv.style.cssText = 'top: 100px; right: 20px; z-index: 1060; max-width: 450px; max-height: 400px; overflow-y: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.1);';
alertDiv.innerHTML = `
${diagnosticHtml}
<button type="button" class="btn-close" onclick="this.parentElement.remove()"></button>
`;
document.body.appendChild(alertDiv);
// 10秒后自动删除
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 10000);
showToast('✅ 诊断完成,请查看右侧详细信息', 'success');
} else {
showToast('❌ 诊断失败: ' + result.message, 'error');
}
} catch (error) {
showToast('🔥 诊断过程出错: ' + error.message, 'error');
} finally {
diagnoseBtn.disabled = false;
diagnoseBtn.innerHTML = '🔍 诊断连接';
}
}
// 更新连接状态
function updateConnectionState(connected) {
isConnected = connected;
connectBtn.disabled = connected;
disconnectBtn.disabled = !connected;
// 更新所有控制按钮
const controlButtons = [
takeoffBtn, landBtn, upBtn, downBtn, leftBtn, rightBtn,
forwardBtn, backBtn, rotateLeftBtn, rotateRightBtn, startVideoBtn
];
controlButtons.forEach(btn => {
btn.disabled = !connected;
});
connectionBadge.textContent = connected ? '已连接' : '未连接';
connectionBadge.className = connected ? 'badge bg-success fs-6' : 'badge bg-danger fs-6';
}
// 更新状态显示
function updateStatus(message, type = 'info') {
const iconMap = {
'info': 'fa-info-circle',
'success': 'fa-check-circle',
'warning': 'fa-exclamation-triangle',
'danger': 'fa-times-circle',
'secondary': 'fa-minus-circle'
};
connectionStatus.innerHTML = `<i class="${iconMap[type] || iconMap.info} me-2"></i>${message}`;
connectionStatus.className = `alert alert-${type} mb-0`;
}
// 更新无人机状态
function updateDroneStatus(droneInfo) {
if (droneInfo) {
const battery = droneInfo.battery || 0;
const height = droneInfo.height || 0;
const speed = droneInfo.speed || 0;
const signal = droneInfo.signal_strength || 0;
batteryStatus.textContent = `电量: ${battery}%`;
batteryStatus.className = battery > 30 ? 'badge bg-success text-white fs-6' :
battery > 15 ? 'badge bg-warning text-dark fs-6' :
'badge bg-danger text-white fs-6';
document.getElementById('batteryLevel').textContent = `${battery}%`;
document.getElementById('heightLevel').textContent = `${height}cm`;
document.getElementById('speedLevel').textContent = `${speed}km/h`;
document.getElementById('signalLevel').textContent = `${signal}%`;
}
}
// 显示提示信息
function showToast(message, type = 'info') {
const typeMap = {
'success': 'success',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${typeMap[type] || 'info'} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 80px; right: 20px; z-index: 1050; max-width: 350px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动删除
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 3000);
}
// 定期更新无人机状态
setInterval(async () => {
if (isConnected) {
try {
const response = await fetch('/api/drone/status');
const result = await response.json();
if (result.status === 'success') {
updateDroneStatus(result.drone_state);
}
} catch (error) {
console.log('获取无人机状态失败:', error);
}
}
}, 2000); // 每2秒更新一次状态
// 简单的Bootstrap替代功能
document.addEventListener('click', function (e) {
// 处理alert关闭按钮
if (e.target.classList.contains('btn-close')) {
const alert = e.target.closest('.alert');
if (alert) {
alert.remove();
}
}
});
</script>
</body>
</html>