|
|
<!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> |