parent
a96095e481
commit
b6cab3c218
@ -0,0 +1,17 @@
|
||||
"""
|
||||
RoboMaster无人机控制系统
|
||||
一个集成了无人机控制和YOLO实时目标检测的智能控制系统
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "AI Assistant"
|
||||
__description__ = "RoboMaster无人机和无人车智能控制系统"
|
||||
|
||||
# 导入主要模块
|
||||
from .drone_controller import RoboMasterController
|
||||
from .yolo_stream import YOLOStream
|
||||
|
||||
__all__ = [
|
||||
"RoboMasterController",
|
||||
"YOLOStream"
|
||||
]
|
@ -0,0 +1,248 @@
|
||||
# robomaster_control/api.py
|
||||
|
||||
"""
|
||||
RoboMaster无人机和无人车控制API接口
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, WebSocket, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from robomaster_control.drone_controller import RoboMasterController
|
||||
from robomaster_control.yolo_stream import YOLOStream
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
app = FastAPI(title="RoboMaster控制系统", version="1.0.0")
|
||||
|
||||
# 允许跨域,方便前端本地调试
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 挂载静态文件
|
||||
app.mount("/static", StaticFiles(directory="robomaster_control/static"), name="static")
|
||||
|
||||
# 控制器实例
|
||||
drone = RoboMasterController()
|
||||
|
||||
# 多路YOLO流实例
|
||||
yolo_streams: Dict[str, YOLOStream] = {
|
||||
"drone": YOLOStream(model_path='yolov5s.pt', source=0, stream_id="drone"),
|
||||
}
|
||||
|
||||
# ==================== 无人机控制接口 ====================
|
||||
|
||||
@app.post("/drone/connect")
|
||||
def drone_connect(ip_address: str = "192.168.10.1", port: int = 8889):
|
||||
"""连接无人机"""
|
||||
return {"msg": drone.connect(ip_address, port)}
|
||||
|
||||
@app.post("/drone/disconnect")
|
||||
def drone_disconnect():
|
||||
"""断开无人机"""
|
||||
return {"msg": drone.disconnect()}
|
||||
|
||||
@app.post("/drone/takeoff")
|
||||
def drone_takeoff():
|
||||
"""无人机起飞"""
|
||||
return {"msg": drone.takeoff()}
|
||||
|
||||
@app.post("/drone/land")
|
||||
def drone_land():
|
||||
"""无人机降落"""
|
||||
return {"msg": drone.land()}
|
||||
|
||||
@app.post("/drone/emergency_land")
|
||||
def drone_emergency_land():
|
||||
"""无人机紧急降落"""
|
||||
return {"msg": drone.emergency_land()}
|
||||
|
||||
@app.post("/drone/move_forward")
|
||||
def drone_move_forward(distance: float = 1.0):
|
||||
"""无人机前进"""
|
||||
return {"msg": drone.move_forward(distance)}
|
||||
|
||||
@app.post("/drone/move_backward")
|
||||
def drone_move_backward(distance: float = 1.0):
|
||||
"""无人机后退"""
|
||||
return {"msg": drone.move_backward(distance)}
|
||||
|
||||
@app.post("/drone/move_left")
|
||||
def drone_move_left(distance: float = 1.0):
|
||||
"""无人机左移"""
|
||||
return {"msg": drone.move_left(distance)}
|
||||
|
||||
@app.post("/drone/move_right")
|
||||
def drone_move_right(distance: float = 1.0):
|
||||
"""无人机右移"""
|
||||
return {"msg": drone.move_right(distance)}
|
||||
|
||||
@app.post("/drone/move_up")
|
||||
def drone_move_up(distance: float = 1.0):
|
||||
"""无人机上升"""
|
||||
return {"msg": drone.move_up(distance)}
|
||||
|
||||
@app.post("/drone/move_down")
|
||||
def drone_move_down(distance: float = 1.0):
|
||||
"""无人机下降"""
|
||||
return {"msg": drone.move_down(distance)}
|
||||
|
||||
@app.post("/drone/rotate_left")
|
||||
def drone_rotate_left(angle: float = 90.0):
|
||||
"""无人机左转"""
|
||||
return {"msg": drone.rotate_left(angle)}
|
||||
|
||||
@app.post("/drone/rotate_right")
|
||||
def drone_rotate_right(angle: float = 90.0):
|
||||
"""无人机右转"""
|
||||
return {"msg": drone.rotate_right(angle)}
|
||||
|
||||
@app.post("/drone/reconnaissance/right")
|
||||
def drone_reconnaissance_right():
|
||||
"""无人机侦察-右转"""
|
||||
return {"msg": drone.reconnaissance_right()}
|
||||
|
||||
@app.post("/drone/reconnaissance/stop")
|
||||
def drone_reconnaissance_stop():
|
||||
"""无人机侦察-停止"""
|
||||
return {"msg": drone.reconnaissance_stop()}
|
||||
|
||||
@app.post("/drone/hover")
|
||||
def drone_hover():
|
||||
"""无人机悬停"""
|
||||
return {"msg": drone.hover()}
|
||||
|
||||
@app.post("/drone/cruise")
|
||||
def drone_cruise():
|
||||
"""无人机巡航"""
|
||||
return {"msg": drone.cruise()}
|
||||
|
||||
@app.post("/drone/stop_mission")
|
||||
def drone_stop_mission():
|
||||
"""停止无人机任务"""
|
||||
return {"msg": drone.stop_mission()}
|
||||
|
||||
@app.post("/drone/set_flight_mode")
|
||||
def drone_set_flight_mode(mode: str):
|
||||
"""设置无人机飞行模式"""
|
||||
return {"msg": drone.set_flight_mode(mode)}
|
||||
|
||||
@app.get("/drone/battery")
|
||||
def drone_battery():
|
||||
"""获取无人机电池状态"""
|
||||
battery = drone.get_battery_status()
|
||||
return {"battery": battery}
|
||||
|
||||
@app.get("/drone/attitude")
|
||||
def drone_attitude():
|
||||
"""获取无人机姿态信息"""
|
||||
attitude = drone.get_attitude()
|
||||
return {"attitude": attitude}
|
||||
|
||||
@app.get("/drone/status")
|
||||
def drone_status():
|
||||
"""获取无人机状态"""
|
||||
return drone.get_status()
|
||||
|
||||
|
||||
|
||||
# ==================== YOLO视频流接口 ====================
|
||||
|
||||
@app.websocket("/ws/yolo/{stream_name}")
|
||||
async def websocket_yolo(websocket: WebSocket, stream_name: str):
|
||||
"""
|
||||
YOLO识别视频流WebSocket接口
|
||||
Args:
|
||||
stream_name: 流名称 (drone/car)
|
||||
"""
|
||||
if stream_name not in yolo_streams:
|
||||
await websocket.close(code=4004, reason="Invalid stream name")
|
||||
return
|
||||
|
||||
await yolo_streams[stream_name].stream(websocket)
|
||||
|
||||
@app.post("/yolo/set_source/{stream_name}")
|
||||
async def set_yolo_source(stream_name: str, request: Request):
|
||||
"""
|
||||
设置YOLO视频流源和模型权重
|
||||
Args:
|
||||
stream_name: 流名称 (drone/car)
|
||||
"""
|
||||
if stream_name not in yolo_streams:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid stream name: {stream_name}")
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
source = data.get("source", 0)
|
||||
model_path = data.get("model_path", "yolov5s.pt")
|
||||
|
||||
# 释放旧流
|
||||
yolo_streams[stream_name].release()
|
||||
|
||||
# 创建新流
|
||||
yolo_streams[stream_name] = YOLOStream(
|
||||
model_path=model_path,
|
||||
source=source,
|
||||
stream_id=stream_name
|
||||
)
|
||||
|
||||
return {"msg": f"{stream_name}流已切换视频源为{source},模型为{model_path}"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"设置失败: {str(e)}")
|
||||
|
||||
@app.get("/yolo/streams")
|
||||
def get_yolo_streams():
|
||||
"""获取所有YOLO流信息"""
|
||||
streams_info = {}
|
||||
for name, stream in yolo_streams.items():
|
||||
streams_info[name] = stream.get_stream_info()
|
||||
return streams_info
|
||||
|
||||
@app.get("/yolo/detection_summary/{stream_name}")
|
||||
def get_detection_summary(stream_name: str):
|
||||
"""获取检测结果摘要"""
|
||||
if stream_name not in yolo_streams:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid stream name: {stream_name}")
|
||||
|
||||
return yolo_streams[stream_name].get_detection_summary()
|
||||
|
||||
# ==================== 系统状态接口 ====================
|
||||
|
||||
@app.get("/system/status")
|
||||
def system_status():
|
||||
"""获取系统整体状态"""
|
||||
return {
|
||||
"timestamp": time.time(),
|
||||
"drone": drone.get_status(),
|
||||
"yolo_streams": {name: stream.get_stream_info() for name, stream in yolo_streams.items()}
|
||||
}
|
||||
|
||||
@app.get("/system/health")
|
||||
def system_health():
|
||||
"""系统健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": time.time(),
|
||||
"drone_connected": drone.connected,
|
||||
"yolo_streams_count": len(yolo_streams)
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
"""根路径,返回系统信息"""
|
||||
return {
|
||||
"message": "RoboMaster无人机控制系统",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"drone": "/drone/*",
|
||||
"yolo": "/yolo/*",
|
||||
"websocket": "/ws/yolo/{stream_name}",
|
||||
"static": "/static/index.html",
|
||||
"docs": "/docs"
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
# robomaster_control/config.py
|
||||
|
||||
"""
|
||||
RoboMaster控制系统配置文件
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
class Config:
|
||||
"""配置管理类"""
|
||||
|
||||
# 无人机配置
|
||||
DRONE_CONFIG = {
|
||||
"default_ip": "192.168.10.1",
|
||||
"default_port": 8889,
|
||||
"connection_timeout": 10,
|
||||
"sn": "0TQZJ6B0050H87", # 无人机序列号
|
||||
"conn_type": "sta", # 连接类型: sta/ap
|
||||
"max_altitude": 120, # 最大飞行高度(米)
|
||||
"max_speed": 15, # 最大飞行速度(m/s)
|
||||
"hover_height": 1.5, # 悬停高度(米)
|
||||
"landing_speed": 0.5, # 降落速度(m/s)
|
||||
"takeoff_speed": 1.0 # 起飞速度(m/s)
|
||||
}
|
||||
|
||||
|
||||
|
||||
# YOLO配置
|
||||
YOLO_CONFIG = {
|
||||
"default_model": "yolov5s.pt",
|
||||
"confidence_threshold": 0.5,
|
||||
"nms_threshold": 0.4,
|
||||
"max_detections": 100,
|
||||
"device": "auto", # auto/cpu/cuda
|
||||
"stream_fps": 10,
|
||||
"detection_fps": 30
|
||||
}
|
||||
|
||||
# 视频流配置
|
||||
STREAM_CONFIG = {
|
||||
"drone_source": 0, # 无人机视频源
|
||||
"car_source": 1, # 无人车视频源
|
||||
"frame_width": 640,
|
||||
"frame_height": 480,
|
||||
"jpeg_quality": 85,
|
||||
"buffer_size": 100
|
||||
}
|
||||
|
||||
# 系统配置
|
||||
SYSTEM_CONFIG = {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8000,
|
||||
"debug": True,
|
||||
"log_level": "INFO",
|
||||
"cors_origins": ["*"],
|
||||
"static_dir": "robomaster_control/static"
|
||||
}
|
||||
|
||||
# 日志配置
|
||||
LOG_CONFIG = {
|
||||
"level": "INFO",
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
"file": "robomaster_control.log",
|
||||
"max_size": 10 * 1024 * 1024, # 10MB
|
||||
"backup_count": 5
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_drone_config(cls) -> Dict[str, Any]:
|
||||
"""获取无人机配置"""
|
||||
return cls.DRONE_CONFIG.copy()
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_yolo_config(cls) -> Dict[str, Any]:
|
||||
"""获取YOLO配置"""
|
||||
return cls.YOLO_CONFIG.copy()
|
||||
|
||||
@classmethod
|
||||
def get_stream_config(cls) -> Dict[str, Any]:
|
||||
"""获取视频流配置"""
|
||||
return cls.STREAM_CONFIG.copy()
|
||||
|
||||
@classmethod
|
||||
def get_system_config(cls) -> Dict[str, Any]:
|
||||
"""获取系统配置"""
|
||||
return cls.SYSTEM_CONFIG.copy()
|
||||
|
||||
@classmethod
|
||||
def get_log_config(cls) -> Dict[str, Any]:
|
||||
"""获取日志配置"""
|
||||
return cls.LOG_CONFIG.copy()
|
||||
|
||||
@classmethod
|
||||
def load_from_env(cls):
|
||||
"""从环境变量加载配置"""
|
||||
# 无人机配置
|
||||
if os.getenv("DRONE_IP"):
|
||||
cls.DRONE_CONFIG["default_ip"] = os.getenv("DRONE_IP")
|
||||
if os.getenv("DRONE_PORT"):
|
||||
cls.DRONE_CONFIG["default_port"] = int(os.getenv("DRONE_PORT"))
|
||||
if os.getenv("DRONE_SN"):
|
||||
cls.DRONE_CONFIG["sn"] = os.getenv("DRONE_SN")
|
||||
|
||||
|
||||
|
||||
# YOLO配置
|
||||
if os.getenv("YOLO_MODEL"):
|
||||
cls.YOLO_CONFIG["default_model"] = os.getenv("YOLO_MODEL")
|
||||
if os.getenv("YOLO_CONFIDENCE"):
|
||||
cls.YOLO_CONFIG["confidence_threshold"] = float(os.getenv("YOLO_CONFIDENCE"))
|
||||
|
||||
# 系统配置
|
||||
if os.getenv("HOST"):
|
||||
cls.SYSTEM_CONFIG["host"] = os.getenv("HOST")
|
||||
if os.getenv("PORT"):
|
||||
cls.SYSTEM_CONFIG["port"] = int(os.getenv("PORT"))
|
||||
if os.getenv("DEBUG"):
|
||||
cls.SYSTEM_CONFIG["debug"] = os.getenv("DEBUG").lower() == "true"
|
||||
if os.getenv("LOG_LEVEL"):
|
||||
cls.SYSTEM_CONFIG["log_level"] = os.getenv("LOG_LEVEL")
|
||||
|
||||
@classmethod
|
||||
def get_connection_info(cls, device_type: str = "drone") -> Dict[str, Any]:
|
||||
"""获取连接信息"""
|
||||
if device_type.lower() == "drone":
|
||||
return {
|
||||
"ip": cls.DRONE_CONFIG["default_ip"],
|
||||
"port": cls.DRONE_CONFIG["default_port"],
|
||||
"sn": cls.DRONE_CONFIG["sn"],
|
||||
"conn_type": cls.DRONE_CONFIG["conn_type"]
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unknown device type: {device_type}")
|
||||
|
||||
# 加载环境变量配置
|
||||
Config.load_from_env()
|
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RoboMaster控制系统启动脚本
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
"""
|
||||
主启动函数
|
||||
"""
|
||||
print("🚁 RoboMaster控制系统启动中...")
|
||||
print("=" * 50)
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies()
|
||||
|
||||
# 启动服务
|
||||
start_server()
|
||||
|
||||
def check_dependencies():
|
||||
"""
|
||||
检查项目依赖
|
||||
"""
|
||||
print("📦 检查项目依赖...")
|
||||
|
||||
required_packages = [
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'opencv-python',
|
||||
'torch',
|
||||
'ultralytics'
|
||||
]
|
||||
|
||||
missing_packages = []
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
__import__(package.replace('-', '_'))
|
||||
print(f"✅ {package}")
|
||||
except ImportError:
|
||||
missing_packages.append(package)
|
||||
print(f"❌ {package}")
|
||||
|
||||
if missing_packages:
|
||||
print(f"\n⚠️ 缺少依赖包: {', '.join(missing_packages)}")
|
||||
print("请运行: pip install -r requirements.txt")
|
||||
sys.exit(1)
|
||||
|
||||
print("✅ 所有依赖检查完成\n")
|
||||
|
||||
def start_server():
|
||||
"""
|
||||
启动FastAPI服务器
|
||||
"""
|
||||
print("🚀 启动服务器...")
|
||||
print("📍 访问地址: http://localhost:8000")
|
||||
print("🎮 控制界面: http://localhost:8000/static/index.html")
|
||||
print("📚 API文档: http://localhost:8000/docs")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
uvicorn.run(
|
||||
"robomaster_control.api:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"❌ 启动失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,547 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RoboMaster控制系统</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.control-panel h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(86, 171, 47, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(255, 65, 108, 0.4);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(240, 147, 251, 0.4);
|
||||
}
|
||||
|
||||
.status-display {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.video-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.video-container h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.video-stream {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
height: 480px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.detection-info {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
border-left: 4px solid #56ab2f;
|
||||
}
|
||||
|
||||
.detection-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: #56ab2f;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: #ff416c;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background: #f093fb;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.video-stream {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚁 RoboMaster无人机控制系统</h1>
|
||||
<p>无人机智能控制平台</p>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<h2>🎮 设备控制</h2>
|
||||
|
||||
<!-- 无人机控制 -->
|
||||
<div class="device-section">
|
||||
<h3>🚁 无人机控制</h3>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="send('/drone/connect')">连接</button>
|
||||
<button class="btn btn-danger" onclick="send('/drone/disconnect')">断开</button>
|
||||
<button class="btn btn-success" onclick="send('/drone/takeoff')">起飞</button>
|
||||
<button class="btn btn-warning" onclick="send('/drone/land')">降落</button>
|
||||
<button class="btn btn-danger" onclick="send('/drone/emergency_land')">紧急降落</button>
|
||||
</div>
|
||||
|
||||
<h4>移动控制</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_forward')">前进</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_backward')">后退</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_left')">左移</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_right')">右移</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_up')">上升</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/move_down')">下降</button>
|
||||
</div>
|
||||
|
||||
<h4>旋转控制</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="send('/drone/rotate_left')">左转</button>
|
||||
<button class="btn btn-primary" onclick="send('/drone/rotate_right')">右转</button>
|
||||
<button class="btn btn-warning" onclick="send('/drone/hover')">悬停</button>
|
||||
</div>
|
||||
|
||||
<h4>任务控制</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" onclick="send('/drone/reconnaissance/right')">侦察</button>
|
||||
<button class="btn btn-warning" onclick="send('/drone/reconnaissance/stop')">停止侦察</button>
|
||||
<button class="btn btn-success" onclick="send('/drone/cruise')">巡航</button>
|
||||
<button class="btn btn-danger" onclick="send('/drone/stop_mission')">停止任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 状态显示 -->
|
||||
<div class="status-display">
|
||||
<h4>📊 设备状态</h4>
|
||||
<div id="device-status">
|
||||
<div class="status-item">
|
||||
<span class="status-label">无人机:</span>
|
||||
<span class="status-value" id="drone-status">未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频流面板 -->
|
||||
<div class="video-section">
|
||||
<h2>📹 YOLO识别视频流</h2>
|
||||
|
||||
<!-- 无人机视频流 -->
|
||||
<div class="video-container">
|
||||
<h3>🚁 无人机识别流</h3>
|
||||
<img id="drone-video" class="video-stream" alt="无人机视频流">
|
||||
<div class="video-controls">
|
||||
<button class="btn btn-primary" onclick="startYoloStream('drone')">连接</button>
|
||||
<button class="btn btn-danger" onclick="stopYoloStream('drone')">断开</button>
|
||||
<span class="connection-status" id="drone-stream-status"></span>
|
||||
<span id="drone-stream-text">未连接</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>视频源:</label>
|
||||
<input type="text" id="drone-source" value="0" placeholder="摄像头设备号或RTSP地址">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>模型权重:</label>
|
||||
<input type="text" id="drone-model" value="yolov5s.pt" placeholder="模型文件路径">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="setYoloConfig('drone')">切换配置</button>
|
||||
<div class="detection-info" id="drone-detection-info">
|
||||
<h4>检测信息</h4>
|
||||
<div id="drone-detection-details">等待连接...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// WebSocket连接管理
|
||||
const wsConnections = {};
|
||||
|
||||
// API基础URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// 发送API请求
|
||||
async function send(endpoint, method = 'POST', data = null) {
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(API_BASE + endpoint, options);
|
||||
const result = await response.json();
|
||||
|
||||
// 显示结果
|
||||
showMessage(result.msg || result.message || '操作完成');
|
||||
|
||||
// 更新状态
|
||||
updateDeviceStatus();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('API请求失败:', error);
|
||||
showMessage('请求失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
// 这里可以添加更好的消息提示UI
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// 更新设备状态
|
||||
async function updateDeviceStatus() {
|
||||
try {
|
||||
const droneStatus = await fetch(API_BASE + '/drone/status').then(r => r.json());
|
||||
|
||||
document.getElementById('drone-status').textContent =
|
||||
droneStatus.connected ? '已连接' : '未连接';
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// YOLO流控制
|
||||
function startYoloStream(streamName) {
|
||||
if (wsConnections[streamName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:8000/ws/yolo/${streamName}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
updateStreamStatus(streamName, 'connected', '已连接');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
updateStreamStatus(streamName, 'disconnected', '已断开');
|
||||
wsConnections[streamName] = null;
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
updateStreamStatus(streamName, 'disconnected', '连接出错');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'frame') {
|
||||
// 更新视频流
|
||||
const videoElement = document.getElementById(`${streamName}-video`);
|
||||
videoElement.src = 'data:image/jpeg;base64,' + data.data;
|
||||
} else if (data.type === 'detections') {
|
||||
// 更新检测信息
|
||||
updateDetectionInfo(streamName, data.data, data.summary);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理WebSocket消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
wsConnections[streamName] = ws;
|
||||
}
|
||||
|
||||
function stopYoloStream(streamName) {
|
||||
if (wsConnections[streamName]) {
|
||||
wsConnections[streamName].close();
|
||||
wsConnections[streamName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStreamStatus(streamName, status, text) {
|
||||
const statusElement = document.getElementById(`${streamName}-stream-status`);
|
||||
const textElement = document.getElementById(`${streamName}-stream-text`);
|
||||
|
||||
statusElement.className = `connection-status status-${status}`;
|
||||
textElement.textContent = text;
|
||||
}
|
||||
|
||||
function updateDetectionInfo(streamName, detections, summary) {
|
||||
const detailsElement = document.getElementById(`${streamName}-detection-details`);
|
||||
|
||||
if (!detections || detections.length === 0) {
|
||||
detailsElement.innerHTML = '<p>未检测到目标</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<p>检测到 ${detections.length} 个目标</p>`;
|
||||
|
||||
// 按类别统计
|
||||
const classCounts = {};
|
||||
detections.forEach(det => {
|
||||
classCounts[det.class_name] = (classCounts[det.class_name] || 0) + 1;
|
||||
});
|
||||
|
||||
html += '<ul>';
|
||||
Object.entries(classCounts).forEach(([className, count]) => {
|
||||
html += `<li>${className}: ${count}个</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
if (summary) {
|
||||
html += `<p>平均置信度: ${(summary.average_confidence * 100).toFixed(1)}%</p>`;
|
||||
html += `<p>FPS: ${summary.fps}</p>`;
|
||||
}
|
||||
|
||||
detailsElement.innerHTML = html;
|
||||
}
|
||||
|
||||
async function setYoloConfig(streamName) {
|
||||
const source = document.getElementById(`${streamName}-source`).value;
|
||||
const model = document.getElementById(`${streamName}-model`).value;
|
||||
|
||||
try {
|
||||
await send(`/yolo/set_source/${streamName}`, 'POST', {
|
||||
source: source,
|
||||
model_path: model
|
||||
});
|
||||
|
||||
// 重新连接流
|
||||
stopYoloStream(streamName);
|
||||
setTimeout(() => startYoloStream(streamName), 500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('设置配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 更新设备状态
|
||||
updateDeviceStatus();
|
||||
|
||||
// 定期更新状态
|
||||
setInterval(updateDeviceStatus, 5000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 476 KiB |
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -0,0 +1,63 @@
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
import av
|
||||
import cv2
|
||||
import base64
|
||||
from flask import Flask
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
# Tello配置
|
||||
TELLO_IP = '192.168.10.1'
|
||||
TELLO_PORT = 8889
|
||||
LOCAL_PORT = 9000
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(('', LOCAL_PORT))
|
||||
|
||||
def send_command(cmd):
|
||||
sock.sendto(cmd.encode('utf-8'), (TELLO_IP, TELLO_PORT))
|
||||
try:
|
||||
sock.settimeout(2)
|
||||
response, _ = sock.recvfrom(1024)
|
||||
print(f"Command: {cmd}, Response: {response.decode()}")
|
||||
return True
|
||||
except socket.timeout:
|
||||
print(f"Timeout for command: {cmd}")
|
||||
return False
|
||||
|
||||
# 初始化Tello
|
||||
send_command('command')
|
||||
time.sleep(1)
|
||||
send_command('streamon')
|
||||
time.sleep(1)
|
||||
|
||||
app = Flask(__name__)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
@socketio.on('drone_command')
|
||||
def handle_drone_command(data):
|
||||
command = data.get('command')
|
||||
if command:
|
||||
success = send_command(command)
|
||||
socketio.emit('command_response', {'success': success, 'command': command})
|
||||
|
||||
def video_stream():
|
||||
container = av.open('udp://0.0.0.0:11111', format='h264')
|
||||
try:
|
||||
for frame in container.decode(video=0):
|
||||
img = frame.to_ndarray(format='bgr24')
|
||||
img = cv2.resize(img, (640, 480))
|
||||
_, buffer = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 70])
|
||||
img_base64 = base64.b64encode(buffer).decode('utf-8')
|
||||
socketio.emit('video_frame', {'image': img_base64})
|
||||
except Exception as e:
|
||||
print(f"视频流错误: {e}")
|
||||
finally:
|
||||
container.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
video_thread = threading.Thread(target=video_stream)
|
||||
video_thread.daemon = True
|
||||
video_thread.start()
|
||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue