parent
fc6329bbaf
commit
90544a2efb
@ -0,0 +1,32 @@
|
||||
# 应用环境
|
||||
ENVIRONMENT=development # development, testing, production
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
# Web服务器配置
|
||||
WEB_HOST=0.0.0.0
|
||||
WEB_PORT=8080
|
||||
|
||||
# 通信服务器配置
|
||||
COMM_HOST=0.0.0.0
|
||||
COMM_PORT=5000
|
||||
|
||||
# 无人机连接配置
|
||||
DRONE_CONNECTION_STRING=udpin:localhost:14550
|
||||
|
||||
# 基地位置
|
||||
BASE_LATITUDE=39.9
|
||||
BASE_LONGITUDE=116.3
|
||||
|
||||
# 飞行参数
|
||||
DEFAULT_ALTITUDE=10.0
|
||||
DEFAULT_AIRSPEED=3.0
|
||||
|
||||
# 安全设置
|
||||
ENABLE_GEO_FENCE=True
|
||||
MAX_DISTANCE_FROM_BASE=5000.0
|
||||
MIN_BATTERY_LEVEL=30.0
|
||||
|
||||
# 其他配置
|
||||
ENABLE_SIMULATION=True
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,110 @@
|
||||
import os
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 基础配置
|
||||
class Config:
|
||||
"""基础配置类"""
|
||||
|
||||
# 应用配置
|
||||
APP_NAME = "智能战场医疗后送系统"
|
||||
VERSION = "1.0.0"
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
# Web服务器配置
|
||||
WEB_HOST = os.getenv("WEB_HOST", "0.0.0.0")
|
||||
WEB_PORT = int(os.getenv("WEB_PORT", "8080"))
|
||||
|
||||
# 通信服务器配置
|
||||
COMM_HOST = os.getenv("COMM_HOST", "0.0.0.0")
|
||||
COMM_PORT = int(os.getenv("COMM_PORT", "5000"))
|
||||
|
||||
# 无人机连接配置
|
||||
DRONE_CONNECTION_STRING = os.getenv("DRONE_CONNECTION_STRING", "udpin:localhost:14550")
|
||||
|
||||
# 基地位置(纬度、经度)
|
||||
BASE_LATITUDE = float(os.getenv("BASE_LATITUDE", "39.9"))
|
||||
BASE_LONGITUDE = float(os.getenv("BASE_LONGITUDE", "116.3"))
|
||||
|
||||
# 飞行参数
|
||||
DEFAULT_ALTITUDE = float(os.getenv("DEFAULT_ALTITUDE", "10.0")) # 默认飞行高度(米)
|
||||
DEFAULT_AIRSPEED = float(os.getenv("DEFAULT_AIRSPEED", "3.0")) # 默认空速(米/秒)
|
||||
|
||||
# 安全设置
|
||||
ENABLE_GEO_FENCE = os.getenv("ENABLE_GEO_FENCE", "True").lower() == "true"
|
||||
MAX_DISTANCE_FROM_BASE = float(os.getenv("MAX_DISTANCE_FROM_BASE", "5000.0")) # 最大飞行距离(米)
|
||||
MIN_BATTERY_LEVEL = float(os.getenv("MIN_BATTERY_LEVEL", "30.0")) # 最低电量百分比
|
||||
|
||||
# 其他配置
|
||||
ENABLE_SIMULATION = os.getenv("ENABLE_SIMULATION", "True").lower() == "true"
|
||||
|
||||
@staticmethod
|
||||
def get_log_level():
|
||||
"""获取日志级别"""
|
||||
level_map = {
|
||||
"DEBUG": logging.DEBUG,
|
||||
"INFO": logging.INFO,
|
||||
"WARNING": logging.WARNING,
|
||||
"ERROR": logging.ERROR,
|
||||
"CRITICAL": logging.CRITICAL
|
||||
}
|
||||
return level_map.get(Config.LOG_LEVEL, logging.INFO)
|
||||
|
||||
@staticmethod
|
||||
def setup_logging():
|
||||
"""设置日志"""
|
||||
logging.basicConfig(
|
||||
level=Config.get_log_level(),
|
||||
format=Config.LOG_FORMAT
|
||||
)
|
||||
|
||||
# 降低第三方库的日志级别
|
||||
logging.getLogger("werkzeug").setLevel(logging.WARNING)
|
||||
logging.getLogger("socketio").setLevel(logging.WARNING)
|
||||
logging.getLogger("engineio").setLevel(logging.WARNING)
|
||||
logging.getLogger("geventwebsocket").setLevel(logging.WARNING)
|
||||
|
||||
logging.info(f"日志系统已初始化: 级别={Config.LOG_LEVEL}")
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""开发环境配置"""
|
||||
ENABLE_SIMULATION = True
|
||||
LOG_LEVEL = "DEBUG"
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""生产环境配置"""
|
||||
ENABLE_SIMULATION = False
|
||||
LOG_LEVEL = "INFO"
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""测试环境配置"""
|
||||
ENABLE_SIMULATION = True
|
||||
LOG_LEVEL = "DEBUG"
|
||||
WEB_PORT = 8081
|
||||
COMM_PORT = 5001
|
||||
|
||||
|
||||
# 根据环境变量选择配置
|
||||
def get_config():
|
||||
"""获取当前环境的配置"""
|
||||
env = os.getenv("ENVIRONMENT", "development").lower()
|
||||
|
||||
if env == "production":
|
||||
return ProductionConfig
|
||||
elif env == "testing":
|
||||
return TestingConfig
|
||||
else:
|
||||
return DevelopmentConfig
|
||||
|
||||
|
||||
# 导出当前配置
|
||||
current_config = get_config()
|
||||
@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 确保目录结构存在
|
||||
mkdir -p src/drone/
|
||||
mkdir -p src/communication/
|
||||
mkdir -p src/positioning/
|
||||
mkdir -p src/ui/templates/
|
||||
mkdir -p src/ui/static/css/
|
||||
mkdir -p src/ui/static/js/
|
||||
mkdir -p src/ui/static/img/
|
||||
mkdir -p config/
|
||||
mkdir -p tests/
|
||||
|
||||
# 创建空的__init__.py文件确保模块可以被导入
|
||||
touch src/__init__.py
|
||||
touch src/drone/__init__.py
|
||||
touch src/communication/__init__.py
|
||||
touch src/positioning/__init__.py
|
||||
touch src/ui/__init__.py
|
||||
touch config/__init__.py
|
||||
touch tests/__init__.py
|
||||
|
||||
# 创建无人机和伤员图标
|
||||
echo "创建示例图标..."
|
||||
# 无人机图标
|
||||
cat > src/ui/static/img/drone.png << EOF
|
||||
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA
|
||||
AXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAA
|
||||
DsMAAA7DAcdvqGQAAAKDSURBVFhHxZdPSBRhGMZnZ/9o/9xd
|
||||
d1ddT0GHUiGDoEMQhEGHOhR0COoQdKhLHToEXYIuQYc8BR3q
|
||||
VNChQ9Ch6BAEHaJDRIeIVNBW27Vd/7XrzrwzO9O8M9/MfLM7
|
||||
s+sXPPDyvO/7vd+8O7PrjrFGnMP+vgZ0dnZmzs/Pb0aj0QBG
|
||||
e97QJyqRSOB8Po8WFxcPAZcYehD0H9GRy+WyiAcpM4BvuKn/
|
||||
qCiKKkVRphgacCrYcRFPOHWQxpPJ5HWn/lZQFYkHQYWC0mJU
|
||||
wkk8CAqgKvHyIMchCDGqkuPQoEQQJMehAWLUiSAYZJbfb5TN
|
||||
Zvc45zrQCfN+NiC0iG4RoQUQIYi7xMuDHIcgxKhKjkODEkGQ
|
||||
HIcGiFF1iECQWbqDZmZmWs+0w2P9osBJtBvQCQdxXRA4wE5g
|
||||
NZvNcpzjnMsTlHQA12q1E4CfAF47OTnp5TgvJCh6jO5Fh4eH
|
||||
Wcj7gPfAHx1APB7/heMxdH9lZeUaJz00CrgYCCGb8/PzF7PZ
|
||||
7KKqqsfGzMuHVAECyePj428qlconY+blQwbQBXg3nU4/xgXk
|
||||
G6/CILtgjGgX8AGOBYyJ14YxoxeZqrPwkN2CfUQDbI2yGRGx
|
||||
lI+FdAuwM1qtVhPbIxmPx28ZMxdEAXbGW1tbK81m81MqlRox
|
||||
Zi6ILoB3wpOTk0cAj8Hr7e3t58bMBa/nQAdxPB/GA/nU1dV1
|
||||
wZhZIP8GaJBMJge2t7efxWKxH9PT0/nBwcFOjnPCi4EQYYLZ
|
||||
WVb8FZIpE0D89MTERO/Ozs55o+TlQ7YLsJPvPEbrOA7X6/XL
|
||||
uCZrxk4P2QDojPgf0wlgEvB+aWlpbWho6J/REqA6Rv8B0U+G
|
||||
YRp/6oQAAAAASUVORK5CYII=
|
||||
EOF
|
||||
|
||||
# 伤员图标
|
||||
cat > src/ui/static/img/casualty.png << EOF
|
||||
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA
|
||||
AXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAA
|
||||
DsMAAA7DAcdvqGQAAAKDSURBVFhHxZdPSBRhGMZnZ/9o/9xd
|
||||
d1ddT0GHUiGDoEMQhEGHOhR0COoQdKhLHToEXYIuQYc8BR3q
|
||||
VNChQ9Ch6BAEHaJDRIeIVNBW27Vd/7XrzrwzO9O8M9/MfLM7
|
||||
s+sXPPDyvO/7vd+8O7PrjrFGnMP+vgZ0dnZmzs/Pb0aj0QBG
|
||||
e97QJyqRSOB8Po8WFxcPAZcYehD0H9GRy+WyiAcpM4BvuKn/
|
||||
qCiKKkVRphgacCrYcRFPOHWQxpPJ5HWn/lZQFYkHQYWC0mJU
|
||||
wkk8CAqgKvHyIMchCDGqkuPQoEQQJMehAWLUiSAYZJbfb5TN
|
||||
Zvc45zrQCfN+NiC0iG4RoQUQIYi7xMuDHIcgxKhKjkODEkGQ
|
||||
HIcGiFF1iECQWbqDZmZmWs+0w2P9osBJtBvQCQdxXRA4wE5g
|
||||
NZvNcpzjnMsTlHQA12q1E4CfAF47OTnp5TgvJCh6jO5Fh4eH
|
||||
Wcj7gPfAHx1APB7/heMxdH9lZeUaJz00CrgYCCGb8/PzF7PZ
|
||||
7KKqqsfGzMuHVAECyePj428qlconY+blQwbQBXg3nU4/xgXk
|
||||
G6/CILtgjGgX8AGOBYyJ14YxoxeZqrPwkN2CfUQDbI2yGRGx
|
||||
lI+FdAuwM1qtVhPbIxmPx28ZMxdEAXbGW1tbK81m81MqlRox
|
||||
Zi6ILoB3wpOTk0cAj8Hr7e3t58bMBa/nQAdxPB/GA/nU1dV1
|
||||
wZhZIP8GaJBMJge2t7efxWKxH9PT0/nBwcFOjnPCi4EQYYLZ
|
||||
WVb8FZIpE0D89MTERO/Ozs55o+TlQ7YLsJPvPEbrOA7X6/XL
|
||||
uCZrxk4P2QDojPgf0wlgEvB+aWlpbWho6J/REqA6Rv8B0U+G
|
||||
YRp/6oQAAAAASUVORK5CYII=
|
||||
EOF
|
||||
|
||||
# 创建环境变量文件
|
||||
cp .env.example .env
|
||||
|
||||
# 创建一个简单的运行脚本
|
||||
cat > run.sh << EOF
|
||||
#!/bin/bash
|
||||
python simulation.py "\$@"
|
||||
EOF
|
||||
chmod +x run.sh
|
||||
|
||||
echo "项目初始化完成!"
|
||||
echo "运行以下命令安装依赖:"
|
||||
echo " pip install -r requirements.txt"
|
||||
echo "运行系统:"
|
||||
echo " ./run.sh"
|
||||
@ -0,0 +1,311 @@
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from config.config import current_config as config
|
||||
from src.drone.drone_controller import DroneController
|
||||
from src.drone.mission_planner import MissionPlanner, Mission, MissionType
|
||||
from src.drone.medical_supplies import MedicalSupplyManager
|
||||
from src.communication.communication_manager import CommunicationManager
|
||||
from src.positioning.position_manager import PositionManager
|
||||
from src.ui.web_interface import WebInterface
|
||||
|
||||
# 设置日志
|
||||
config.setup_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MedicalEvacuationSystem:
|
||||
def __init__(self):
|
||||
"""初始化医疗后送系统"""
|
||||
logger.info(f"初始化{config.APP_NAME} v{config.VERSION}")
|
||||
|
||||
# 初始化各个模块
|
||||
self.drone_controller = DroneController(config.DRONE_CONNECTION_STRING)
|
||||
self.medical_supply_manager = MedicalSupplyManager()
|
||||
self.position_manager = PositionManager()
|
||||
self.communication_manager = CommunicationManager(
|
||||
host=config.COMM_HOST,
|
||||
port=config.COMM_PORT
|
||||
)
|
||||
self.web_interface = WebInterface(
|
||||
host=config.WEB_HOST,
|
||||
port=config.WEB_PORT
|
||||
)
|
||||
self.mission_planner = None
|
||||
|
||||
# 注册处理器
|
||||
self._register_handlers()
|
||||
|
||||
def _register_handlers(self):
|
||||
"""注册各种处理器"""
|
||||
# 注册Web界面处理器
|
||||
self.web_interface.set_casualty_handler(self._handle_casualty_request)
|
||||
self.web_interface.set_supply_handler(self._handle_supply_request)
|
||||
self.web_interface.set_drone_status_handler(self._handle_drone_status_request)
|
||||
|
||||
# 注册通信管理器处理器
|
||||
self.communication_manager.set_casualty_handler(self._handle_casualty_request)
|
||||
self.communication_manager.set_supply_handler(self._handle_supply_request)
|
||||
|
||||
def _handle_casualty_request(self, data):
|
||||
"""处理伤员请求"""
|
||||
try:
|
||||
casualty_id = data.get('casualty_id')
|
||||
latitude = data.get('latitude')
|
||||
longitude = data.get('longitude')
|
||||
|
||||
if casualty_id and latitude is not None and longitude is not None:
|
||||
# 添加或更新伤员位置
|
||||
self.position_manager.add_casualty_position(casualty_id, latitude, longitude)
|
||||
|
||||
# 广播伤员位置更新
|
||||
self.web_interface.broadcast_casualty_update(data)
|
||||
|
||||
logger.info(f"处理伤员请求: ID={casualty_id}, 位置=({latitude}, {longitude})")
|
||||
return True
|
||||
else:
|
||||
logger.warning("无效的伤员请求数据")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"处理伤员请求失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _handle_supply_request(self, data):
|
||||
"""处理物资请求"""
|
||||
try:
|
||||
supply_id = data.get('type')
|
||||
quantity = data.get('quantity', 1)
|
||||
target_id = data.get('target_id')
|
||||
|
||||
if not supply_id or not target_id:
|
||||
logger.warning("无效的物资请求数据")
|
||||
return False
|
||||
|
||||
# 获取伤员位置
|
||||
casualty_position = self.position_manager.get_casualty_position(target_id)
|
||||
if not casualty_position:
|
||||
logger.warning(f"未找到伤员位置: {target_id}")
|
||||
return False
|
||||
|
||||
# 创建物资请求
|
||||
request_id = self.medical_supply_manager.create_supply_request(
|
||||
target_id, supply_id, quantity
|
||||
)
|
||||
|
||||
if not request_id:
|
||||
logger.warning("创建物资请求失败")
|
||||
return False
|
||||
|
||||
# 创建物资运送任务
|
||||
supply = self.medical_supply_manager.get_supply(supply_id)
|
||||
mission = Mission(
|
||||
mission_id=f"mission_{request_id}",
|
||||
mission_type=MissionType.MEDICAL_SUPPLY,
|
||||
target_location=(casualty_position['latitude'], casualty_position['longitude']),
|
||||
altitude=config.DEFAULT_ALTITUDE,
|
||||
payload={
|
||||
'request_id': request_id,
|
||||
'supply_id': supply_id,
|
||||
'supply_name': supply.name if supply else "未知物资",
|
||||
'quantity': quantity,
|
||||
'target_id': target_id
|
||||
}
|
||||
)
|
||||
|
||||
# 添加任务到任务规划器
|
||||
self.mission_planner.add_mission(mission)
|
||||
|
||||
# 更新物资请求状态
|
||||
self.medical_supply_manager.update_request_status(request_id, 'in_progress')
|
||||
|
||||
# 广播物资状态更新
|
||||
self._broadcast_supply_status()
|
||||
|
||||
logger.info(f"处理物资请求: ID={request_id}, 物资={supply_id}, 目标={target_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"处理物资请求失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _handle_drone_status_request(self):
|
||||
"""处理无人机状态请求"""
|
||||
try:
|
||||
# 获取当前任务
|
||||
current_mission = self.mission_planner.get_current_mission()
|
||||
mission_status = "执行任务" if current_mission else "待机"
|
||||
|
||||
# 获取无人机位置
|
||||
if self.drone_controller.vehicle:
|
||||
location = self.drone_controller.vehicle.location.global_relative_frame
|
||||
battery = self.drone_controller.vehicle.battery.level if self.drone_controller.vehicle.battery else 0
|
||||
else:
|
||||
location = None
|
||||
battery = 0
|
||||
|
||||
# 构建状态数据
|
||||
if location:
|
||||
status_data = {
|
||||
'status': mission_status,
|
||||
'battery': battery,
|
||||
'latitude': location.lat,
|
||||
'longitude': location.lon,
|
||||
'altitude': location.alt if hasattr(location, 'alt') else 0,
|
||||
'current_mission': current_mission.to_dict() if current_mission else None
|
||||
}
|
||||
else:
|
||||
# 模拟数据
|
||||
status_data = {
|
||||
'status': '未连接',
|
||||
'battery': 0,
|
||||
'latitude': config.BASE_LATITUDE,
|
||||
'longitude': config.BASE_LONGITUDE,
|
||||
'altitude': 0,
|
||||
'current_mission': None
|
||||
}
|
||||
|
||||
# 广播无人机状态
|
||||
self.web_interface.broadcast_drone_update(status_data)
|
||||
|
||||
return status_data
|
||||
except Exception as e:
|
||||
logger.error(f"处理无人机状态请求失败: {str(e)}")
|
||||
return {
|
||||
'status': '错误',
|
||||
'battery': 0,
|
||||
'latitude': config.BASE_LATITUDE,
|
||||
'longitude': config.BASE_LONGITUDE,
|
||||
'altitude': 0,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _broadcast_supply_status(self):
|
||||
"""广播物资状态"""
|
||||
try:
|
||||
supplies = []
|
||||
for supply_id, supply in self.medical_supply_manager.get_all_supplies().items():
|
||||
supplies.append({
|
||||
'id': supply_id,
|
||||
'name': supply.name,
|
||||
'description': supply.description,
|
||||
'quantity': supply.quantity
|
||||
})
|
||||
|
||||
self.web_interface.broadcast_medical_supply_update({
|
||||
'supplies': supplies
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"广播物资状态失败: {str(e)}")
|
||||
|
||||
def _broadcast_mission_status(self):
|
||||
"""广播任务状态"""
|
||||
try:
|
||||
current_mission = self.mission_planner.get_current_mission()
|
||||
mission_history = self.mission_planner.get_mission_history()
|
||||
|
||||
self.web_interface.socketio.emit('mission_status_updated', {
|
||||
'currentMission': current_mission.to_dict() if current_mission else None,
|
||||
'missionHistory': mission_history
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"广播任务状态失败: {str(e)}")
|
||||
|
||||
def start(self):
|
||||
"""启动系统"""
|
||||
try:
|
||||
logger.info("启动系统...")
|
||||
|
||||
# 连接无人机
|
||||
if not config.ENABLE_SIMULATION:
|
||||
if not self.drone_controller.connect():
|
||||
logger.error("无法连接到无人机,请检查连接设置")
|
||||
return False
|
||||
else:
|
||||
logger.info("系统运行在模拟模式,跳过无人机连接")
|
||||
|
||||
# 初始化任务规划器
|
||||
self.mission_planner = MissionPlanner(self.drone_controller)
|
||||
self.mission_planner.start()
|
||||
|
||||
# 启动通信服务器
|
||||
communication_thread = threading.Thread(
|
||||
target=self.communication_manager.start_server,
|
||||
daemon=True
|
||||
)
|
||||
communication_thread.start()
|
||||
|
||||
# 启动任务状态广播定时器
|
||||
def broadcast_status():
|
||||
while True:
|
||||
self._handle_drone_status_request()
|
||||
self._broadcast_mission_status()
|
||||
self._broadcast_supply_status()
|
||||
threading.Event().wait(5) # 每5秒广播一次
|
||||
|
||||
status_thread = threading.Thread(target=broadcast_status, daemon=True)
|
||||
status_thread.start()
|
||||
|
||||
# 启动Web界面
|
||||
self.web_interface.start()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动系统失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""停止系统"""
|
||||
try:
|
||||
logger.info("正在停止系统...")
|
||||
|
||||
# 停止任务规划器
|
||||
if self.mission_planner:
|
||||
self.mission_planner.stop()
|
||||
|
||||
# 关闭无人机连接
|
||||
if not config.ENABLE_SIMULATION:
|
||||
self.drone_controller.close()
|
||||
|
||||
# 关闭通信服务器
|
||||
self.communication_manager.close()
|
||||
|
||||
logger.info("系统已停止")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"停止系统失败: {str(e)}")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
"""信号处理函数"""
|
||||
logger.info("接收到停止信号,正在关闭系统...")
|
||||
system.stop()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""主程序入口"""
|
||||
global system
|
||||
|
||||
# 注册信号处理器
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# 创建系统实例
|
||||
system = MedicalEvacuationSystem()
|
||||
|
||||
# 启动系统
|
||||
if system.start():
|
||||
logger.info("系统启动成功")
|
||||
else:
|
||||
logger.error("系统启动失败")
|
||||
system.stop()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
system = None
|
||||
exit_code = main()
|
||||
sys.exit(exit_code)
|
||||
@ -0,0 +1,9 @@
|
||||
pymavlink==2.4.35
|
||||
dronekit==2.9.2
|
||||
pyserial==3.5
|
||||
flask==2.3.3
|
||||
flask-socketio==5.3.6
|
||||
python-dotenv==1.0.0
|
||||
geopy==2.4.1
|
||||
numpy==1.24.3
|
||||
pandas==2.0.3
|
||||
@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import random
|
||||
import threading
|
||||
import signal
|
||||
import argparse
|
||||
from config.config import current_config as config
|
||||
|
||||
# 确保环境变量设置为启用模拟
|
||||
os.environ['ENABLE_SIMULATION'] = 'True'
|
||||
|
||||
# 设置日志
|
||||
config.setup_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DroneSimulator:
|
||||
"""无人机模拟器"""
|
||||
|
||||
def __init__(self, port=14550):
|
||||
"""初始化模拟器"""
|
||||
self.port = port
|
||||
self.running = False
|
||||
self.thread = None
|
||||
self.latitude = config.BASE_LATITUDE
|
||||
self.longitude = config.BASE_LONGITUDE
|
||||
self.altitude = 0.0
|
||||
self.battery = 100.0
|
||||
self.armed = False
|
||||
self.mode = "GUIDED"
|
||||
|
||||
def start(self):
|
||||
"""启动模拟器"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._simulation_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info(f"模拟器已启动在端口 {self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止模拟器"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2)
|
||||
logger.info("模拟器已停止")
|
||||
|
||||
def _simulation_loop(self):
|
||||
"""模拟循环"""
|
||||
while self.running:
|
||||
# 模拟电池消耗
|
||||
if self.armed and self.battery > 0:
|
||||
self.battery -= 0.01
|
||||
if self.battery < 0:
|
||||
self.battery = 0
|
||||
|
||||
# 模拟位置变化
|
||||
if self.armed and self.mode == "GUIDED" and self.altitude > 0:
|
||||
# 随机漂移
|
||||
self.latitude += random.uniform(-0.00001, 0.00001)
|
||||
self.longitude += random.uniform(-0.00001, 0.00001)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def arm(self):
|
||||
"""解锁无人机"""
|
||||
self.armed = True
|
||||
logger.info("模拟无人机已解锁")
|
||||
|
||||
def disarm(self):
|
||||
"""锁定无人机"""
|
||||
self.armed = False
|
||||
logger.info("模拟无人机已锁定")
|
||||
|
||||
def set_mode(self, mode):
|
||||
"""设置模式"""
|
||||
self.mode = mode
|
||||
logger.info(f"模拟无人机模式已设置为 {mode}")
|
||||
|
||||
def takeoff(self, altitude):
|
||||
"""起飞"""
|
||||
self.arm()
|
||||
self.altitude = altitude
|
||||
logger.info(f"模拟无人机起飞到高度 {altitude}m")
|
||||
|
||||
def land(self):
|
||||
"""降落"""
|
||||
self.altitude = 0
|
||||
self.disarm()
|
||||
logger.info("模拟无人机已降落")
|
||||
|
||||
def goto(self, lat, lon, alt):
|
||||
"""飞往指定位置"""
|
||||
logger.info(f"模拟无人机飞往 ({lat}, {lon}, {alt})")
|
||||
|
||||
# 模拟飞行时间
|
||||
distance = ((lat - self.latitude) ** 2 + (lon - self.longitude) ** 2) ** 0.5 * 111000
|
||||
speed = 5.0 # 假设速度为5m/s
|
||||
flight_time = distance / speed
|
||||
|
||||
# 模拟飞行过程
|
||||
start_lat = self.latitude
|
||||
start_lon = self.longitude
|
||||
start_alt = self.altitude
|
||||
|
||||
for i in range(10):
|
||||
progress = (i + 1) / 10
|
||||
self.latitude = start_lat + (lat - start_lat) * progress
|
||||
self.longitude = start_lon + (lon - start_lon) * progress
|
||||
self.altitude = start_alt + (alt - start_alt) * progress
|
||||
time.sleep(flight_time / 10)
|
||||
|
||||
self.latitude = lat
|
||||
self.longitude = lon
|
||||
self.altitude = alt
|
||||
logger.info(f"模拟无人机已到达 ({lat}, {lon}, {alt})")
|
||||
|
||||
|
||||
def setup_simulation():
|
||||
"""设置模拟环境"""
|
||||
# 启动模拟器
|
||||
simulator = DroneSimulator()
|
||||
simulator.start()
|
||||
|
||||
# 创建SITL连接字符串和覆盖环境变量
|
||||
os.environ['DRONE_CONNECTION_STRING'] = f'udpin:localhost:{simulator.port}'
|
||||
|
||||
return simulator
|
||||
|
||||
|
||||
def run_simulation():
|
||||
"""运行模拟"""
|
||||
# 设置参数解析
|
||||
parser = argparse.ArgumentParser(description='智能战场医疗后送系统模拟器')
|
||||
parser.add_argument('--web-port', type=int, default=8080, help='Web服务端口')
|
||||
parser.add_argument('--comm-port', type=int, default=5000, help='通信服务端口')
|
||||
parser.add_argument('--drone-port', type=int, default=14550, help='无人机模拟器端口')
|
||||
args = parser.parse_args()
|
||||
|
||||
# 设置环境变量
|
||||
os.environ['WEB_PORT'] = str(args.web_port)
|
||||
os.environ['COMM_PORT'] = str(args.comm_port)
|
||||
|
||||
# 启动模拟器
|
||||
simulator = DroneSimulator(port=args.drone_port)
|
||||
simulator.start()
|
||||
|
||||
# 导入主程序模块
|
||||
from main import MedicalEvacuationSystem
|
||||
|
||||
# 创建系统实例
|
||||
system = MedicalEvacuationSystem()
|
||||
|
||||
# 注册信号处理
|
||||
def signal_handler(sig, frame):
|
||||
logger.info("接收到停止信号,正在关闭模拟器...")
|
||||
system.stop()
|
||||
simulator.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# 启动系统
|
||||
try:
|
||||
if system.start():
|
||||
logger.info("系统启动成功,运行在模拟模式")
|
||||
|
||||
# 保持程序运行
|
||||
while True:
|
||||
time.sleep(1)
|
||||
else:
|
||||
logger.error("系统启动失败")
|
||||
simulator.stop()
|
||||
return 1
|
||||
except Exception as e:
|
||||
logger.error(f"模拟运行错误: {str(e)}")
|
||||
system.stop()
|
||||
simulator.stop()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = run_simulation()
|
||||
sys.exit(exit_code)
|
||||
Binary file not shown.
@ -0,0 +1,140 @@
|
||||
import socket
|
||||
import json
|
||||
import logging
|
||||
from threading import Thread, Lock
|
||||
|
||||
class CommunicationManager:
|
||||
def __init__(self, host='0.0.0.0', port=5000):
|
||||
"""
|
||||
初始化通信管理器
|
||||
:param host: 服务器主机地址
|
||||
:param port: 服务器端口
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_socket = None
|
||||
self.clients = []
|
||||
self.lock = Lock()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.casualty_handler = None
|
||||
self.supply_handler = None
|
||||
|
||||
def set_casualty_handler(self, handler):
|
||||
"""
|
||||
设置伤员处理回调函数
|
||||
:param handler: 处理函数
|
||||
"""
|
||||
self.casualty_handler = handler
|
||||
self.logger.info("已设置伤员处理回调函数")
|
||||
|
||||
def set_supply_handler(self, handler):
|
||||
"""
|
||||
设置物资处理回调函数
|
||||
:param handler: 处理函数
|
||||
"""
|
||||
self.supply_handler = handler
|
||||
self.logger.info("已设置物资处理回调函数")
|
||||
|
||||
def start_server(self):
|
||||
"""启动通信服务器"""
|
||||
try:
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.bind((self.host, self.port))
|
||||
self.server_socket.listen(5)
|
||||
self.logger.info(f"通信服务器启动在 {self.host}:{self.port}")
|
||||
|
||||
# 启动客户端接受线程
|
||||
Thread(target=self._accept_clients, daemon=True).start()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动服务器失败: {str(e)}")
|
||||
|
||||
def _accept_clients(self):
|
||||
"""接受新的客户端连接"""
|
||||
while True:
|
||||
try:
|
||||
client_socket, address = self.server_socket.accept()
|
||||
self.logger.info(f"新客户端连接: {address}")
|
||||
|
||||
with self.lock:
|
||||
self.clients.append(client_socket)
|
||||
|
||||
# 为每个客户端启动一个处理线程
|
||||
Thread(target=self._handle_client, args=(client_socket,), daemon=True).start()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"接受客户端连接失败: {str(e)}")
|
||||
|
||||
def _handle_client(self, client_socket):
|
||||
"""处理客户端消息"""
|
||||
while True:
|
||||
try:
|
||||
data = client_socket.recv(1024).decode('utf-8')
|
||||
if not data:
|
||||
break
|
||||
|
||||
message = json.loads(data)
|
||||
self._process_message(message)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理客户端消息失败: {str(e)}")
|
||||
break
|
||||
|
||||
# 客户端断开连接
|
||||
with self.lock:
|
||||
if client_socket in self.clients:
|
||||
self.clients.remove(client_socket)
|
||||
client_socket.close()
|
||||
|
||||
def _process_message(self, message):
|
||||
"""
|
||||
处理接收到的消息
|
||||
:param message: 消息内容
|
||||
"""
|
||||
message_type = message.get('type')
|
||||
if message_type == 'casualty_location':
|
||||
self._handle_casualty_location(message)
|
||||
elif message_type == 'medical_supply_request':
|
||||
self._handle_medical_supply_request(message)
|
||||
elif message_type == 'drone_status':
|
||||
self._handle_drone_status(message)
|
||||
|
||||
def _handle_casualty_location(self, message):
|
||||
"""处理伤员位置信息"""
|
||||
location = message.get('location')
|
||||
self.logger.info(f"收到伤员位置: {location}")
|
||||
if self.casualty_handler:
|
||||
self.casualty_handler(message)
|
||||
|
||||
def _handle_medical_supply_request(self, message):
|
||||
"""处理医疗物资请求"""
|
||||
supplies = message.get('supplies')
|
||||
location = message.get('location')
|
||||
self.logger.info(f"收到医疗物资请求: {supplies} 送往 {location}")
|
||||
if self.supply_handler:
|
||||
self.supply_handler(message)
|
||||
|
||||
def _handle_drone_status(self, message):
|
||||
"""处理无人机状态信息"""
|
||||
status = message.get('status')
|
||||
self.logger.info(f"收到无人机状态: {status}")
|
||||
# TODO: 实现无人机状态处理逻辑
|
||||
|
||||
def broadcast_message(self, message):
|
||||
"""
|
||||
广播消息给所有客户端
|
||||
:param message: 要广播的消息
|
||||
"""
|
||||
message_str = json.dumps(message)
|
||||
with self.lock:
|
||||
for client in self.clients:
|
||||
try:
|
||||
client.send(message_str.encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送消息失败: {str(e)}")
|
||||
|
||||
def close(self):
|
||||
"""关闭通信服务器"""
|
||||
if self.server_socket:
|
||||
self.server_socket.close()
|
||||
self.logger.info("通信服务器已关闭")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,271 @@
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from queue import Queue
|
||||
from threading import Thread, Lock
|
||||
|
||||
class MissionType(Enum):
|
||||
"""任务类型枚举"""
|
||||
IDLE = 0
|
||||
MEDICAL_SUPPLY = 1
|
||||
SURVEILLANCE = 2
|
||||
RETURN_TO_BASE = 3
|
||||
|
||||
class MissionStatus(Enum):
|
||||
"""任务状态枚举"""
|
||||
PENDING = 0
|
||||
IN_PROGRESS = 1
|
||||
COMPLETED = 2
|
||||
FAILED = 3
|
||||
|
||||
class Mission:
|
||||
"""任务类"""
|
||||
def __init__(self,
|
||||
mission_id: str,
|
||||
mission_type: MissionType,
|
||||
target_location: Tuple[float, float],
|
||||
altitude: float = 10.0,
|
||||
payload: Dict = None):
|
||||
"""
|
||||
初始化任务
|
||||
:param mission_id: 任务ID
|
||||
:param mission_type: 任务类型
|
||||
:param target_location: 目标位置(纬度, 经度)
|
||||
:param altitude: 飞行高度(米)
|
||||
:param payload: 任务负载(如物资信息)
|
||||
"""
|
||||
self.mission_id = mission_id
|
||||
self.mission_type = mission_type
|
||||
self.target_location = target_location
|
||||
self.altitude = altitude
|
||||
self.payload = payload or {}
|
||||
self.status = MissionStatus.PENDING
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.error_message = None
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换成字典"""
|
||||
return {
|
||||
'mission_id': self.mission_id,
|
||||
'mission_type': self.mission_type.name,
|
||||
'target_location': self.target_location,
|
||||
'altitude': self.altitude,
|
||||
'payload': self.payload,
|
||||
'status': self.status.name,
|
||||
'start_time': self.start_time,
|
||||
'end_time': self.end_time,
|
||||
'error_message': self.error_message
|
||||
}
|
||||
|
||||
|
||||
class MissionPlanner:
|
||||
"""任务规划器"""
|
||||
def __init__(self, drone_controller):
|
||||
"""
|
||||
初始化任务规划器
|
||||
:param drone_controller: 无人机控制器
|
||||
"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.drone_controller = drone_controller
|
||||
self.mission_queue = Queue()
|
||||
self.current_mission = None
|
||||
self.mission_history = []
|
||||
self.lock = Lock()
|
||||
self.running = False
|
||||
self.worker_thread = None
|
||||
|
||||
def start(self):
|
||||
"""启动任务规划器"""
|
||||
if self.running:
|
||||
self.logger.warning("任务规划器已经在运行")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.worker_thread = Thread(target=self._mission_worker, daemon=True)
|
||||
self.worker_thread.start()
|
||||
self.logger.info("任务规划器已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止任务规划器"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
if self.worker_thread:
|
||||
self.worker_thread.join(timeout=5)
|
||||
self.logger.info("任务规划器已停止")
|
||||
|
||||
def add_mission(self, mission: Mission) -> bool:
|
||||
"""
|
||||
添加任务到队列
|
||||
:param mission: 任务对象
|
||||
:return: 是否添加成功
|
||||
"""
|
||||
try:
|
||||
self.mission_queue.put(mission)
|
||||
self.logger.info(f"添加任务: ID={mission.mission_id}, 类型={mission.mission_type.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"添加任务失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_current_mission(self) -> Optional[Mission]:
|
||||
"""
|
||||
获取当前执行的任务
|
||||
:return: 当前任务或None
|
||||
"""
|
||||
with self.lock:
|
||||
return self.current_mission
|
||||
|
||||
def get_mission_queue_size(self) -> int:
|
||||
"""
|
||||
获取任务队列大小
|
||||
:return: 队列中的任务数
|
||||
"""
|
||||
return self.mission_queue.qsize()
|
||||
|
||||
def get_mission_history(self) -> List[Dict]:
|
||||
"""
|
||||
获取任务历史
|
||||
:return: 任务历史记录列表
|
||||
"""
|
||||
with self.lock:
|
||||
return [mission.to_dict() for mission in self.mission_history]
|
||||
|
||||
def _mission_worker(self):
|
||||
"""任务工作线程"""
|
||||
while self.running:
|
||||
try:
|
||||
if not self.mission_queue.empty():
|
||||
# 从队列中获取下一个任务
|
||||
mission = self.mission_queue.get()
|
||||
|
||||
with self.lock:
|
||||
self.current_mission = mission
|
||||
self.current_mission.status = MissionStatus.IN_PROGRESS
|
||||
self.current_mission.start_time = time.time()
|
||||
|
||||
self.logger.info(f"开始执行任务: ID={mission.mission_id}, 类型={mission.mission_type.name}")
|
||||
|
||||
# 根据任务类型执行不同操作
|
||||
success = self._execute_mission(mission)
|
||||
|
||||
with self.lock:
|
||||
if success:
|
||||
self.current_mission.status = MissionStatus.COMPLETED
|
||||
self.logger.info(f"任务完成: ID={mission.mission_id}")
|
||||
else:
|
||||
self.current_mission.status = MissionStatus.FAILED
|
||||
self.logger.error(f"任务失败: ID={mission.mission_id}")
|
||||
|
||||
self.current_mission.end_time = time.time()
|
||||
self.mission_history.append(self.current_mission)
|
||||
self.current_mission = None
|
||||
|
||||
self.mission_queue.task_done()
|
||||
else:
|
||||
# 无任务时等待
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"任务执行过程出错: {str(e)}")
|
||||
if self.current_mission:
|
||||
with self.lock:
|
||||
self.current_mission.status = MissionStatus.FAILED
|
||||
self.current_mission.error_message = str(e)
|
||||
self.current_mission.end_time = time.time()
|
||||
self.mission_history.append(self.current_mission)
|
||||
self.current_mission = None
|
||||
|
||||
def _execute_mission(self, mission: Mission) -> bool:
|
||||
"""
|
||||
执行任务
|
||||
:param mission: 任务对象
|
||||
:return: 是否执行成功
|
||||
"""
|
||||
try:
|
||||
if mission.mission_type == MissionType.MEDICAL_SUPPLY:
|
||||
return self._execute_medical_supply_mission(mission)
|
||||
elif mission.mission_type == MissionType.SURVEILLANCE:
|
||||
return self._execute_surveillance_mission(mission)
|
||||
elif mission.mission_type == MissionType.RETURN_TO_BASE:
|
||||
return self._execute_return_to_base_mission(mission)
|
||||
else:
|
||||
self.logger.warning(f"未知任务类型: {mission.mission_type}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行任务失败: {str(e)}")
|
||||
mission.error_message = str(e)
|
||||
return False
|
||||
|
||||
def _execute_medical_supply_mission(self, mission: Mission) -> bool:
|
||||
"""
|
||||
执行医疗物资运送任务
|
||||
:param mission: 任务对象
|
||||
:return: 是否执行成功
|
||||
"""
|
||||
try:
|
||||
# 飞往目标位置
|
||||
lat, lon = mission.target_location
|
||||
self.drone_controller.goto_location(lat, lon, mission.altitude)
|
||||
|
||||
# 模拟投放物资(实际应该有硬件操作)
|
||||
self.logger.info(f"投放医疗物资: {mission.payload.get('supply_name', '未知物资')}")
|
||||
time.sleep(2) # 模拟投放时间
|
||||
|
||||
# 返回基地
|
||||
return self._execute_return_to_base_mission(mission)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"医疗物资任务执行失败: {str(e)}")
|
||||
mission.error_message = str(e)
|
||||
return False
|
||||
|
||||
def _execute_surveillance_mission(self, mission: Mission) -> bool:
|
||||
"""
|
||||
执行侦察任务
|
||||
:param mission: 任务对象
|
||||
:return: 是否执行成功
|
||||
"""
|
||||
try:
|
||||
# 飞往目标位置
|
||||
lat, lon = mission.target_location
|
||||
self.drone_controller.goto_location(lat, lon, mission.altitude)
|
||||
|
||||
# 模拟侦察(实际应该有摄像头操作)
|
||||
self.logger.info(f"进行区域侦察: ({lat}, {lon})")
|
||||
time.sleep(5) # 模拟侦察时间
|
||||
|
||||
# 返回基地
|
||||
return self._execute_return_to_base_mission(mission)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"侦察任务执行失败: {str(e)}")
|
||||
mission.error_message = str(e)
|
||||
return False
|
||||
|
||||
def _execute_return_to_base_mission(self, mission: Mission) -> bool:
|
||||
"""
|
||||
执行返回基地任务
|
||||
:param mission: 任务对象
|
||||
:return: 是否执行成功
|
||||
"""
|
||||
try:
|
||||
# 获取基地位置(这里应该从配置中读取)
|
||||
base_lat, base_lon = 39.9, 116.3 # 默认位置,实际应该从配置获取
|
||||
|
||||
# 飞往基地
|
||||
self.logger.info("返回基地")
|
||||
self.drone_controller.goto_location(base_lat, base_lon, mission.altitude)
|
||||
|
||||
# 降落
|
||||
self.drone_controller.land()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"返回基地任务执行失败: {str(e)}")
|
||||
mission.error_message = str(e)
|
||||
return False
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,342 @@
|
||||
/* 全局样式 */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(90deg, #1a3a5f 0%, #2d5f8b 100%);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.5px;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 地图样式 */
|
||||
#map {
|
||||
height: 550px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 状态面板样式 */
|
||||
.status-panel {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.status-panel p {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
background-color: #f8fafc;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
margin-right: 8px;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #2d5f8b 0%, #3498db 100%);
|
||||
border: none;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 16px;
|
||||
border-radius: 5px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(45deg, #3498db 0%, #2d5f8b 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
border: 1px solid #ced4da;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #e2e6ea;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.casualty-item, .supply-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #e74c3c;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.supply-item {
|
||||
border-left-color: #2ecc71;
|
||||
}
|
||||
|
||||
.casualty-item:hover, .supply-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background-color: #95a5a6;
|
||||
}
|
||||
|
||||
.status-in-flight {
|
||||
background-color: #3498db;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #f39c12;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 任务列表样式 */
|
||||
.mission-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.mission-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.mission-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mission-list::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mission-item {
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.mission-pending {
|
||||
border-left: 4px solid #95a5a6;
|
||||
}
|
||||
|
||||
.mission-in-progress {
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.mission-completed {
|
||||
border-left: 4px solid #2ecc71;
|
||||
}
|
||||
|
||||
.mission-failed {
|
||||
border-left: 4px solid #e74c3c;
|
||||
}
|
||||
|
||||
/* 自定义地图标记样式 */
|
||||
.casualty-marker .leaflet-popup-content-wrapper {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #e74c3c;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.drone-marker .leaflet-popup-content-wrapper {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3498db;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 响应式布局调整 */
|
||||
@media (max-width: 768px) {
|
||||
#map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.col-md-4 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 地图标记样式 */
|
||||
.map-marker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.casualty-marker {
|
||||
background-color: rgba(231, 76, 60, 0.9);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.drone-marker {
|
||||
background-color: rgba(52, 152, 219, 0.9);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
border: 2px solid white;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
min-width: 200px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.popup-content h6 {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-counter {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-counter:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-counter span {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.status-counter p {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 警告提示 */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #e9f7fe;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
/* 高级表单控件 */
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.25);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 6px 10px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
}
|
||||
@ -0,0 +1,451 @@
|
||||
// 初始化应用
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化地图
|
||||
initMap();
|
||||
|
||||
// 初始化WebSocket连接
|
||||
initWebSocket();
|
||||
|
||||
// 加载伤员数据
|
||||
loadCasualties();
|
||||
|
||||
// 加载医疗物资数据
|
||||
loadMedicalSupplies();
|
||||
|
||||
// 加载无人机状态
|
||||
loadDroneStatus();
|
||||
});
|
||||
|
||||
// 全局变量
|
||||
let map = null;
|
||||
let socket = null;
|
||||
let casualtyMarkers = {};
|
||||
let droneMarker = null;
|
||||
let droneIcon = null;
|
||||
let casualtyIcon = null;
|
||||
|
||||
// 初始化地图
|
||||
function initMap() {
|
||||
// 创建地图
|
||||
map = L.map('map').setView([35.8617, 104.1954], 4); // 中国中心位置
|
||||
|
||||
// 添加OSM地图图层
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// 创建自定义图标
|
||||
droneIcon = L.icon({
|
||||
iconUrl: '/static/img/drone.png',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
|
||||
casualtyIcon = L.icon({
|
||||
iconUrl: '/static/img/casualty.png',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
|
||||
// 地图点击事件 - 可以用于直接在地图上添加伤员
|
||||
map.on('click', function(e) {
|
||||
console.log("地图点击位置:", e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
socket = io();
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
updateConnectionStatus('已连接');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', function() {
|
||||
console.log('WebSocket连接已断开');
|
||||
updateConnectionStatus('已断开');
|
||||
});
|
||||
|
||||
// 伤员位置更新
|
||||
socket.on('casualty_location_updated', function(data) {
|
||||
updateCasualtyMarker(data);
|
||||
updateCasualtyList();
|
||||
});
|
||||
|
||||
// 无人机状态更新
|
||||
socket.on('drone_status_updated', function(data) {
|
||||
updateDroneStatus(data);
|
||||
});
|
||||
|
||||
// 医疗物资更新
|
||||
socket.on('medical_supply_updated', function(data) {
|
||||
updateSupplyStatus(data);
|
||||
});
|
||||
|
||||
// 任务状态更新
|
||||
socket.on('mission_status_updated', function(data) {
|
||||
updateMissionStatus(data);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(status) {
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
if (connectionStatus) {
|
||||
connectionStatus.textContent = status;
|
||||
|
||||
if (status === '已连接') {
|
||||
connectionStatus.classList.remove('text-danger');
|
||||
connectionStatus.classList.add('text-success');
|
||||
} else {
|
||||
connectionStatus.classList.remove('text-success');
|
||||
connectionStatus.classList.add('text-danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新伤员标记
|
||||
function updateCasualtyMarker(data) {
|
||||
const lat = data.latitude;
|
||||
const lon = data.longitude;
|
||||
const id = data.casualty_id;
|
||||
|
||||
if (casualtyMarkers[id]) {
|
||||
casualtyMarkers[id].setLatLng([lat, lon]);
|
||||
} else {
|
||||
const marker = L.marker([lat, lon], {icon: casualtyIcon}).addTo(map);
|
||||
marker.bindPopup(`
|
||||
<div class="casualty-popup">
|
||||
<h5>伤员ID: ${id}</h5>
|
||||
<p>位置: ${lat.toFixed(6)}, ${lon.toFixed(6)}</p>
|
||||
<button class="btn btn-sm btn-primary" onclick="requestMedicalSupply('${id}')">请求医疗物资</button>
|
||||
</div>
|
||||
`);
|
||||
casualtyMarkers[id] = marker;
|
||||
}
|
||||
|
||||
// 更新伤员列表UI
|
||||
updateCasualtyList();
|
||||
}
|
||||
|
||||
// 更新无人机状态
|
||||
function updateDroneStatus(data) {
|
||||
// 更新状态面板
|
||||
document.getElementById('drone-status').textContent = data.status;
|
||||
document.getElementById('drone-battery').textContent = data.battery + '%';
|
||||
document.getElementById('drone-location').textContent =
|
||||
`纬度: ${data.latitude.toFixed(6)}, 经度: ${data.longitude.toFixed(6)}`;
|
||||
|
||||
// 根据电量变化状态颜色
|
||||
const batteryLevel = parseInt(data.battery);
|
||||
const batteryElement = document.getElementById('drone-battery');
|
||||
|
||||
if (batteryLevel < 20) {
|
||||
batteryElement.className = 'text-danger';
|
||||
} else if (batteryLevel < 50) {
|
||||
batteryElement.className = 'text-warning';
|
||||
} else {
|
||||
batteryElement.className = 'text-success';
|
||||
}
|
||||
|
||||
// 更新无人机标记
|
||||
if (droneMarker) {
|
||||
droneMarker.setLatLng([data.latitude, data.longitude]);
|
||||
} else {
|
||||
droneMarker = L.marker([data.latitude, data.longitude], {icon: droneIcon}).addTo(map);
|
||||
droneMarker.bindPopup(`
|
||||
<div class="drone-popup">
|
||||
<h5>无人机状态</h5>
|
||||
<p>状态: ${data.status}</p>
|
||||
<p>电量: ${data.battery}%</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新物资状态
|
||||
function updateSupplyStatus(data) {
|
||||
// 更新物资列表
|
||||
const supplyList = document.getElementById('supply-list');
|
||||
|
||||
if (!supplyList) return;
|
||||
|
||||
supplyList.innerHTML = '';
|
||||
|
||||
if (data.supplies && data.supplies.length > 0) {
|
||||
data.supplies.forEach(supply => {
|
||||
const supplyItem = document.createElement('div');
|
||||
supplyItem.className = 'supply-item';
|
||||
supplyItem.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${supply.name}</strong> (${supply.quantity}个可用)
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="requestSupply('${supply.id}')">
|
||||
请求
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">${supply.description}</small>
|
||||
`;
|
||||
supplyList.appendChild(supplyItem);
|
||||
});
|
||||
} else {
|
||||
supplyList.innerHTML = '<p class="text-muted">没有可用物资</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
function updateMissionStatus(data) {
|
||||
// 更新任务列表
|
||||
const missionList = document.getElementById('mission-list');
|
||||
|
||||
if (!missionList) return;
|
||||
|
||||
if (data.currentMission) {
|
||||
// 显示当前任务
|
||||
const currentMissionElement = document.getElementById('current-mission');
|
||||
if (currentMissionElement) {
|
||||
currentMissionElement.innerHTML = `
|
||||
<div class="mission-item mission-${data.currentMission.status.toLowerCase()}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong>${getMissionTypeLabel(data.currentMission.mission_type)}</strong>
|
||||
<span class="badge bg-${getMissionStatusBadge(data.currentMission.status)}">
|
||||
${data.currentMission.status}
|
||||
</span>
|
||||
</div>
|
||||
<div>ID: ${data.currentMission.mission_id}</div>
|
||||
<div>目标: (${data.currentMission.target_location[0].toFixed(6)},
|
||||
${data.currentMission.target_location[1].toFixed(6)})</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新历史任务列表
|
||||
if (data.missionHistory && data.missionHistory.length > 0) {
|
||||
missionList.innerHTML = '';
|
||||
|
||||
data.missionHistory.forEach(mission => {
|
||||
const missionItem = document.createElement('div');
|
||||
missionItem.className = `mission-item mission-${mission.status.toLowerCase()}`;
|
||||
missionItem.innerHTML = `
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong>${getMissionTypeLabel(mission.mission_type)}</strong>
|
||||
<span class="badge bg-${getMissionStatusBadge(mission.status)}">
|
||||
${mission.status}
|
||||
</span>
|
||||
</div>
|
||||
<div>ID: ${mission.mission_id}</div>
|
||||
<small class="text-muted">${formatTime(mission.start_time)}</small>
|
||||
`;
|
||||
missionList.appendChild(missionItem);
|
||||
});
|
||||
} else {
|
||||
missionList.innerHTML = '<p class="text-muted">没有历史任务</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务类型标签
|
||||
function getMissionTypeLabel(missionType) {
|
||||
switch (missionType) {
|
||||
case 'MEDICAL_SUPPLY': return '医疗物资运送';
|
||||
case 'SURVEILLANCE': return '区域侦察';
|
||||
case 'RETURN_TO_BASE': return '返回基地';
|
||||
default: return missionType;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务状态标签样式
|
||||
function getMissionStatusBadge(status) {
|
||||
switch (status) {
|
||||
case 'PENDING': return 'secondary';
|
||||
case 'IN_PROGRESS': return 'primary';
|
||||
case 'COMPLETED': return 'success';
|
||||
case 'FAILED': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// 加载伤员数据
|
||||
function loadCasualties() {
|
||||
fetch('/api/casualties')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.casualties && data.casualties.length > 0) {
|
||||
data.casualties.forEach(casualty => {
|
||||
updateCasualtyMarker(casualty);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('加载伤员数据失败:', error));
|
||||
}
|
||||
|
||||
// 更新伤员列表
|
||||
function updateCasualtyList() {
|
||||
const casualtyList = document.getElementById('casualty-list');
|
||||
|
||||
if (!casualtyList) return;
|
||||
|
||||
// 清空现有列表
|
||||
casualtyList.innerHTML = '';
|
||||
|
||||
// 遍历所有伤员标记
|
||||
for (const id in casualtyMarkers) {
|
||||
const marker = casualtyMarkers[id];
|
||||
const position = marker.getLatLng();
|
||||
|
||||
const casualtyItem = document.createElement('div');
|
||||
casualtyItem.className = 'casualty-item';
|
||||
casualtyItem.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>伤员ID: ${id}</strong>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="requestMedicalSupply('${id}')">
|
||||
请求物资
|
||||
</button>
|
||||
</div>
|
||||
<div>位置: ${position.lat.toFixed(6)}, ${position.lng.toFixed(6)}</div>
|
||||
`;
|
||||
|
||||
casualtyList.appendChild(casualtyItem);
|
||||
}
|
||||
|
||||
// 如果没有伤员,显示提示信息
|
||||
if (Object.keys(casualtyMarkers).length === 0) {
|
||||
casualtyList.innerHTML = '<p class="text-muted">没有伤员数据</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载医疗物资数据
|
||||
function loadMedicalSupplies() {
|
||||
fetch('/api/medical-supplies')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateSupplyStatus(data);
|
||||
})
|
||||
.catch(error => console.error('加载医疗物资失败:', error));
|
||||
}
|
||||
|
||||
// 加载无人机状态
|
||||
function loadDroneStatus() {
|
||||
fetch('/api/drone/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateDroneStatus(data);
|
||||
})
|
||||
.catch(error => console.error('加载无人机状态失败:', error));
|
||||
}
|
||||
|
||||
// 添加伤员
|
||||
function addCasualty() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('addCasualtyModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// 提交伤员信息
|
||||
function submitCasualty() {
|
||||
const id = document.getElementById('casualty-id').value;
|
||||
const lat = parseFloat(document.getElementById('casualty-lat').value);
|
||||
const lon = parseFloat(document.getElementById('casualty-lon').value);
|
||||
|
||||
if (!id || isNaN(lat) || isNaN(lon)) {
|
||||
alert('请填写完整的伤员信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
casualty_id: id,
|
||||
latitude: lat,
|
||||
longitude: lon
|
||||
};
|
||||
|
||||
// 通过API发送
|
||||
fetch('/api/casualties', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
// 关闭模态框
|
||||
bootstrap.Modal.getInstance(document.getElementById('addCasualtyModal')).hide();
|
||||
|
||||
// 通过WebSocket发送更新
|
||||
socket.emit('update_casualty_location', data);
|
||||
} else {
|
||||
alert('添加伤员失败: ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加伤员失败:', error);
|
||||
alert('添加伤员失败,请检查网络连接');
|
||||
});
|
||||
}
|
||||
|
||||
// 请求医疗物资
|
||||
function requestMedicalSupply(casualtyId) {
|
||||
// 打开请求物资模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('requestSuppliesModal'));
|
||||
document.getElementById('supply-target').value = casualtyId;
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// 提交医疗物资请求
|
||||
function submitSupplyRequest() {
|
||||
const type = document.getElementById('supply-type').value;
|
||||
const quantity = parseInt(document.getElementById('supply-quantity').value);
|
||||
const target = document.getElementById('supply-target').value;
|
||||
|
||||
if (!type || isNaN(quantity) || quantity <= 0 || !target) {
|
||||
alert('请填写完整的物资请求信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
type: type,
|
||||
quantity: quantity,
|
||||
target_id: target
|
||||
};
|
||||
|
||||
// 通过API发送
|
||||
fetch('/api/medical-supplies', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
// 关闭模态框
|
||||
bootstrap.Modal.getInstance(document.getElementById('requestSuppliesModal')).hide();
|
||||
|
||||
// 通过WebSocket发送更新
|
||||
socket.emit('request_medical_supplies', data);
|
||||
} else {
|
||||
alert('请求物资失败: ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求物资失败:', error);
|
||||
alert('请求物资失败,请检查网络连接');
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
from flask import Flask, render_template, jsonify, request, send_from_directory
|
||||
from flask_socketio import SocketIO, emit
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from functools import wraps
|
||||
|
||||
class WebInterface:
|
||||
def __init__(self, host='0.0.0.0', port=8080):
|
||||
"""
|
||||
初始化Web界面
|
||||
:param host: 服务器主机地址
|
||||
:param port: 服务器端口
|
||||
"""
|
||||
self.app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||
self.socketio = SocketIO(self.app, cors_allowed_origins="*")
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 处理器
|
||||
self.casualty_handler = None
|
||||
self.supply_handler = None
|
||||
self.drone_status_handler = None
|
||||
|
||||
# 注册路由
|
||||
self._register_routes()
|
||||
|
||||
def set_casualty_handler(self, handler):
|
||||
"""设置伤员处理器"""
|
||||
self.casualty_handler = handler
|
||||
|
||||
def set_supply_handler(self, handler):
|
||||
"""设置物资处理器"""
|
||||
self.supply_handler = handler
|
||||
|
||||
def set_drone_status_handler(self, handler):
|
||||
"""设置无人机状态处理器"""
|
||||
self.drone_status_handler = handler
|
||||
|
||||
def _register_routes(self):
|
||||
"""注册Web路由"""
|
||||
# 主页
|
||||
@self.app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
# 静态文件
|
||||
@self.app.route('/static/<path:path>')
|
||||
def serve_static(path):
|
||||
return send_from_directory(self.app.static_folder, path)
|
||||
|
||||
# 伤员API
|
||||
@self.app.route('/api/casualties', methods=['GET'])
|
||||
def get_casualties():
|
||||
try:
|
||||
if self.casualty_handler:
|
||||
casualties = []
|
||||
# 实际应该调用position_manager.get_all_casualties()
|
||||
return jsonify({'casualties': casualties, 'status': 'success'})
|
||||
return jsonify({'casualties': [], 'status': 'error', 'message': '伤员处理器未设置'})
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取伤员数据失败: {str(e)}")
|
||||
return jsonify({'casualties': [], 'status': 'error', 'message': str(e)})
|
||||
|
||||
@self.app.route('/api/casualties', methods=['POST'])
|
||||
def add_casualty():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if self.casualty_handler:
|
||||
result = self.casualty_handler(data)
|
||||
return jsonify({'status': 'success' if result else 'error'})
|
||||
return jsonify({'status': 'error', 'message': '伤员处理器未设置'})
|
||||
except Exception as e:
|
||||
self.logger.error(f"添加伤员失败: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
# 医疗物资API
|
||||
@self.app.route('/api/medical-supplies', methods=['GET'])
|
||||
def get_medical_supplies():
|
||||
try:
|
||||
# 实际应该调用medical_supply_manager.get_all_supplies()
|
||||
return jsonify({'supplies': [], 'status': 'success'})
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取医疗物资失败: {str(e)}")
|
||||
return jsonify({'supplies': [], 'status': 'error', 'message': str(e)})
|
||||
|
||||
@self.app.route('/api/medical-supplies', methods=['POST'])
|
||||
def request_medical_supplies():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if self.supply_handler:
|
||||
result = self.supply_handler(data)
|
||||
return jsonify({'status': 'success' if result else 'error'})
|
||||
return jsonify({'status': 'error', 'message': '物资处理器未设置'})
|
||||
except Exception as e:
|
||||
self.logger.error(f"请求医疗物资失败: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
# 无人机状态API
|
||||
@self.app.route('/api/drone/status', methods=['GET'])
|
||||
def get_drone_status():
|
||||
try:
|
||||
if self.drone_status_handler:
|
||||
status = self.drone_status_handler()
|
||||
return jsonify(status)
|
||||
return jsonify({'status': 'error', 'message': '无人机状态处理器未设置'})
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取无人机状态失败: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
# WebSocket事件处理
|
||||
@self.socketio.on('connect')
|
||||
def handle_connect():
|
||||
self.logger.info("客户端连接")
|
||||
|
||||
@self.socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
self.logger.info("客户端断开连接")
|
||||
|
||||
@self.socketio.on('update_casualty_location')
|
||||
def handle_casualty_location(data):
|
||||
try:
|
||||
if self.casualty_handler:
|
||||
result = self.casualty_handler(data)
|
||||
if not result:
|
||||
self.logger.warning("处理伤员位置更新失败")
|
||||
else:
|
||||
self.logger.warning("伤员处理器未设置")
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理伤员位置更新失败: {str(e)}")
|
||||
|
||||
@self.socketio.on('request_medical_supplies')
|
||||
def handle_medical_supplies(data):
|
||||
try:
|
||||
if self.supply_handler:
|
||||
result = self.supply_handler(data)
|
||||
if not result:
|
||||
self.logger.warning("处理医疗物资请求失败")
|
||||
else:
|
||||
self.logger.warning("物资处理器未设置")
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理医疗物资请求失败: {str(e)}")
|
||||
|
||||
@self.socketio.on('update_drone_status')
|
||||
def handle_drone_status():
|
||||
try:
|
||||
if self.drone_status_handler:
|
||||
self.drone_status_handler()
|
||||
else:
|
||||
self.logger.warning("无人机状态处理器未设置")
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理无人机状态更新失败: {str(e)}")
|
||||
|
||||
def start(self):
|
||||
"""启动Web服务器"""
|
||||
try:
|
||||
self.logger.info(f"启动Web服务器: {self.host}:{self.port}")
|
||||
self.socketio.run(self.app, host=self.host, port=self.port)
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动Web服务器失败: {str(e)}")
|
||||
|
||||
def broadcast_casualty_update(self, casualty_data):
|
||||
"""
|
||||
广播伤员信息更新
|
||||
:param casualty_data: 伤员数据
|
||||
"""
|
||||
try:
|
||||
self.socketio.emit('casualty_location_updated', casualty_data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"广播伤员更新失败: {str(e)}")
|
||||
|
||||
def broadcast_drone_update(self, drone_data):
|
||||
"""
|
||||
广播无人机状态更新
|
||||
:param drone_data: 无人机数据
|
||||
"""
|
||||
try:
|
||||
self.socketio.emit('drone_status_updated', drone_data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"广播无人机状态更新失败: {str(e)}")
|
||||
|
||||
def broadcast_medical_supply_update(self, supply_data):
|
||||
"""
|
||||
广播医疗物资状态更新
|
||||
:param supply_data: 医疗物资数据
|
||||
"""
|
||||
try:
|
||||
self.socketio.emit('medical_supply_updated', supply_data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"广播医疗物资更新失败: {str(e)}")
|
||||
Loading…
Reference in new issue