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