diff --git a/1.txt b/1.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/2.txt b/src/2.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/software/.env.example b/src/software/.env.example new file mode 100644 index 00000000..e404fb3b --- /dev/null +++ b/src/software/.env.example @@ -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 \ No newline at end of file diff --git a/src/software/README.md b/src/software/README.md new file mode 100644 index 00000000..59555a21 --- /dev/null +++ b/src/software/README.md @@ -0,0 +1,73 @@ +# 智能战场医疗后送系统 + +基于无人机的智能战场医疗后送系统,用于快速、高效地运送医疗资源和伤员。 + +## 功能特点 + +- 实时伤员位置报告 +- 医疗资源运送 +- 无人机状态监控 +- 简单易用的操作界面 + +## 系统要求 + +- Python 3.8+ +- 无人机硬件(支持MAVLink协议) +- GPS模块 +- 通信模块(4G/5G或Wi-Fi) + +## 安装说明 + +1. 克隆项目 +```bash +git clone [项目地址] +``` + +2. 安装依赖 +```bash +pip install -r requirements.txt +``` + +3. 配置环境变量 +```bash +cp .env.example .env +# 编辑.env文件,填入必要的配置信息 +``` + +4. 运行系统 +```bash +python main.py +``` + +## 项目结构 + +``` +├── main.py # 主程序入口 +├── requirements.txt # 项目依赖 +├── config/ # 配置文件目录 +├── src/ # 源代码目录 +│ ├── drone/ # 无人机控制模块 +│ ├── communication/ # 通信模块 +│ ├── positioning/ # 定位模块 +│ └── ui/ # 用户界面模块 +└── tests/ # 测试文件目录 +``` + +## 使用说明 + +1. 启动系统后,通过Web界面登录 +2. 在地图上标记伤员位置 +3. 选择需要运送的医疗资源 +4. 发送任务指令给无人机 +5. 实时监控无人机状态和任务进度 + +## 注意事项 + +- 使用前请确保无人机电量充足 +- 确保GPS信号良好 +- 遵守相关法律法规 +- 注意战场环境安全 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/src/software/__pycache__/main.cpython-39.pyc b/src/software/__pycache__/main.cpython-39.pyc new file mode 100644 index 00000000..31a8f879 Binary files /dev/null and b/src/software/__pycache__/main.cpython-39.pyc differ diff --git a/src/software/config/__pycache__/config.cpython-39.pyc b/src/software/config/__pycache__/config.cpython-39.pyc new file mode 100644 index 00000000..add70e35 Binary files /dev/null and b/src/software/config/__pycache__/config.cpython-39.pyc differ diff --git a/src/software/config/config.py b/src/software/config/config.py new file mode 100644 index 00000000..d4ac60c7 --- /dev/null +++ b/src/software/config/config.py @@ -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() \ No newline at end of file diff --git a/src/software/init_project.sh b/src/software/init_project.sh new file mode 100644 index 00000000..5d06cf1e --- /dev/null +++ b/src/software/init_project.sh @@ -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" \ No newline at end of file diff --git a/src/software/main.py b/src/software/main.py new file mode 100644 index 00000000..11aa1154 --- /dev/null +++ b/src/software/main.py @@ -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) \ No newline at end of file diff --git a/src/software/requirements.txt b/src/software/requirements.txt new file mode 100644 index 00000000..3a2a298d --- /dev/null +++ b/src/software/requirements.txt @@ -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 \ No newline at end of file diff --git a/src/software/simulation.py b/src/software/simulation.py new file mode 100644 index 00000000..d6caf332 --- /dev/null +++ b/src/software/simulation.py @@ -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) \ No newline at end of file diff --git a/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc b/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc new file mode 100644 index 00000000..0d868fce Binary files /dev/null and b/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc differ diff --git a/src/software/src/communication/communication_manager.py b/src/software/src/communication/communication_manager.py new file mode 100644 index 00000000..f3b464bd --- /dev/null +++ b/src/software/src/communication/communication_manager.py @@ -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("通信服务器已关闭") \ No newline at end of file diff --git a/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc b/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc new file mode 100644 index 00000000..89e50aae Binary files /dev/null and b/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc differ diff --git a/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc b/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc new file mode 100644 index 00000000..87160ef6 Binary files /dev/null and b/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc differ diff --git a/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc b/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc new file mode 100644 index 00000000..22617fb4 Binary files /dev/null and b/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc differ diff --git a/src/software/src/drone/drone_controller.py b/src/software/src/drone/drone_controller.py new file mode 100644 index 00000000..e7a9ad1a --- /dev/null +++ b/src/software/src/drone/drone_controller.py @@ -0,0 +1,103 @@ +from dronekit import connect, VehicleMode, LocationGlobalRelative +import time +import logging +import math + +class DroneController: + def __init__(self, connection_string): + """ + 初始化无人机控制器 + :param connection_string: 无人机连接字符串(如:'udpin:localhost:14550') + """ + self.vehicle = None + self.connection_string = connection_string + self.logger = logging.getLogger(__name__) + + def connect(self): + """连接到无人机""" + try: + self.vehicle = connect(self.connection_string, wait_ready=True) + self.logger.info("成功连接到无人机") + return True + except Exception as e: + self.logger.error(f"连接无人机失败: {str(e)}") + return False + + def arm_and_takeoff(self, target_altitude): + """ + 起飞到指定高度 + :param target_altitude: 目标高度(米) + """ + self.logger.info("准备起飞...") + + # 等待无人机准备就绪 + while not self.vehicle.is_armable: + self.logger.info("等待无人机准备就绪...") + time.sleep(1) + + # 切换到GUIDED模式 + self.vehicle.mode = VehicleMode("GUIDED") + + # 解锁无人机 + self.vehicle.armed = True + + # 等待无人机解锁 + while not self.vehicle.armed: + self.logger.info("等待无人机解锁...") + time.sleep(1) + + # 起飞 + self.logger.info(f"起飞到高度: {target_altitude}米") + self.vehicle.simple_takeoff(target_altitude) + + # 等待到达目标高度 + while True: + current_altitude = self.vehicle.location.global_relative_frame.alt + if current_altitude >= target_altitude * 0.95: + self.logger.info("到达目标高度") + break + time.sleep(1) + + def goto_location(self, lat, lon, altitude): + """ + 飞往指定位置 + :param lat: 纬度 + :param lon: 经度 + :param altitude: 高度(米) + """ + target_location = LocationGlobalRelative(lat, lon, altitude) + self.vehicle.simple_goto(target_location) + + # 等待到达目标位置 + while True: + current_location = self.vehicle.location.global_relative_frame + distance = self._get_distance_metres(current_location, target_location) + if distance < 1.0: # 距离小于1米认为到达 + self.logger.info("到达目标位置") + break + time.sleep(1) + + def land(self): + """降落""" + self.logger.info("开始降落...") + self.vehicle.mode = VehicleMode("LAND") + + # 等待降落完成 + while self.vehicle.armed: + time.sleep(1) + + self.logger.info("降落完成") + + def close(self): + """关闭连接""" + if self.vehicle: + self.vehicle.close() + self.logger.info("关闭无人机连接") + + def _get_distance_metres(self, aLocation1, aLocation2): + """ + 计算两点之间的距离(米) + """ + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon + return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 \ No newline at end of file diff --git a/src/software/src/drone/medical_supplies.py b/src/software/src/drone/medical_supplies.py new file mode 100644 index 00000000..1fee4b83 --- /dev/null +++ b/src/software/src/drone/medical_supplies.py @@ -0,0 +1,190 @@ +import logging +import time +from dataclasses import dataclass +from typing import Dict, List, Optional + +@dataclass +class MedicalSupply: + """医疗物资数据类""" + id: str + name: str + description: str + weight: float # 重量(克) + quantity: int # 数量 + + +class MedicalSupplyManager: + """医疗物资管理器""" + + def __init__(self): + """初始化医疗物资管理器""" + self.logger = logging.getLogger(__name__) + self.supplies = {} # 存储可用的医疗物资 + self.supply_requests = {} # 存储物资请求记录 + + # 初始化默认物资 + self._initialize_default_supplies() + + def _initialize_default_supplies(self): + """初始化默认物资""" + default_supplies = [ + MedicalSupply( + id="first_aid_kit", + name="急救包", + description="基础急救包,包含绷带、消毒用品等", + weight=500, + quantity=10 + ), + MedicalSupply( + id="medicine_pack", + name="药品包", + description="包含止痛药、抗生素等常用药物", + weight=300, + quantity=20 + ), + MedicalSupply( + id="blood_plasma", + name="血浆", + description="应急血浆,通用型", + weight=450, + quantity=5 + ), + MedicalSupply( + id="surgical_kit", + name="手术包", + description="简易手术工具包", + weight=800, + quantity=3 + ), + ] + + for supply in default_supplies: + self.supplies[supply.id] = supply + + def get_all_supplies(self) -> Dict[str, MedicalSupply]: + """ + 获取所有可用的物资 + :return: 物资字典 + """ + return self.supplies.copy() + + def get_supply(self, supply_id: str) -> Optional[MedicalSupply]: + """ + 获取指定ID的物资 + :param supply_id: 物资ID + :return: 物资对象或None + """ + return self.supplies.get(supply_id) + + def add_supply(self, supply: MedicalSupply) -> bool: + """ + 添加新的物资 + :param supply: 物资对象 + :return: 是否添加成功 + """ + if supply.id in self.supplies: + self.logger.warning(f"物资ID已存在: {supply.id}") + return False + + self.supplies[supply.id] = supply + self.logger.info(f"添加新物资: {supply.name}") + return True + + def update_supply_quantity(self, supply_id: str, quantity: int) -> bool: + """ + 更新物资数量 + :param supply_id: 物资ID + :param quantity: 新数量 + :return: 是否更新成功 + """ + if supply_id not in self.supplies: + self.logger.warning(f"物资ID不存在: {supply_id}") + return False + + self.supplies[supply_id].quantity = quantity + self.logger.info(f"更新物资数量: {supply_id}, 数量={quantity}") + return True + + def create_supply_request(self, + target_id: str, + supply_id: str, + quantity: int) -> Optional[str]: + """ + 创建物资请求 + :param target_id: 目标ID(伤员ID) + :param supply_id: 物资ID + :param quantity: 请求数量 + :return: 请求ID或None + """ + if supply_id not in self.supplies: + self.logger.warning(f"物资ID不存在: {supply_id}") + return None + + if self.supplies[supply_id].quantity < quantity: + self.logger.warning(f"物资不足: 请求={quantity}, 可用={self.supplies[supply_id].quantity}") + return None + + # 创建请求ID + request_id = f"req_{int(time.time())}_{supply_id}" + + # 更新可用数量 + self.supplies[supply_id].quantity -= quantity + + # 存储请求 + self.supply_requests[request_id] = { + 'request_id': request_id, + 'target_id': target_id, + 'supply_id': supply_id, + 'quantity': quantity, + 'status': 'pending', + 'timestamp': time.time() + } + + self.logger.info(f"创建物资请求: ID={request_id}, 目标={target_id}, 物资={supply_id}, 数量={quantity}") + return request_id + + def update_request_status(self, request_id: str, status: str) -> bool: + """ + 更新请求状态 + :param request_id: 请求ID + :param status: 新状态(pending, in_progress, delivered, failed) + :return: 是否更新成功 + """ + if request_id not in self.supply_requests: + self.logger.warning(f"请求ID不存在: {request_id}") + return False + + self.supply_requests[request_id]['status'] = status + self.logger.info(f"更新请求状态: ID={request_id}, 状态={status}") + + # 如果请求失败,恢复物资数量 + if status == 'failed': + supply_id = self.supply_requests[request_id]['supply_id'] + quantity = self.supply_requests[request_id]['quantity'] + self.supplies[supply_id].quantity += quantity + self.logger.info(f"恢复物资数量: ID={supply_id}, 增加={quantity}") + + return True + + def get_request(self, request_id: str) -> Optional[Dict]: + """ + 获取请求信息 + :param request_id: 请求ID + :return: 请求信息字典或None + """ + return self.supply_requests.get(request_id) + + def get_requests_by_target(self, target_id: str) -> List[Dict]: + """ + 获取指定目标的所有请求 + :param target_id: 目标ID + :return: 请求信息列表 + """ + return [req for req in self.supply_requests.values() if req['target_id'] == target_id] + + def get_all_requests(self) -> Dict[str, Dict]: + """ + 获取所有请求 + :return: 请求字典 + """ + return self.supply_requests.copy() \ No newline at end of file diff --git a/src/software/src/drone/mission_planner.py b/src/software/src/drone/mission_planner.py new file mode 100644 index 00000000..f71530f6 --- /dev/null +++ b/src/software/src/drone/mission_planner.py @@ -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 \ No newline at end of file diff --git a/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc b/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc new file mode 100644 index 00000000..b1c840fc Binary files /dev/null and b/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc differ diff --git a/src/software/src/positioning/position_manager.py b/src/software/src/positioning/position_manager.py new file mode 100644 index 00000000..5a80b897 --- /dev/null +++ b/src/software/src/positioning/position_manager.py @@ -0,0 +1,131 @@ +import logging +from geopy.geocoders import Nominatim +from geopy.distance import geodesic +import time + +class PositionManager: + def __init__(self): + """初始化定位管理器""" + self.logger = logging.getLogger(__name__) + self.geocoder = Nominatim(user_agent="medical_evac_system") + self.casualty_positions = {} # 存储伤员位置信息 + + def get_location_from_coordinates(self, lat, lon): + """ + 根据经纬度获取位置描述 + :param lat: 纬度 + :param lon: 经度 + :return: 位置描述 + """ + try: + location = self.geocoder.reverse((lat, lon)) + return location.address + except Exception as e: + self.logger.error(f"获取位置描述失败: {str(e)}") + return None + + def calculate_distance(self, lat1, lon1, lat2, lon2): + """ + 计算两点之间的距离(米) + :param lat1: 起点纬度 + :param lon1: 起点经度 + :param lat2: 终点纬度 + :param lon2: 终点经度 + :return: 距离(米) + """ + try: + point1 = (lat1, lon1) + point2 = (lat2, lon2) + distance = geodesic(point1, point2).meters + return distance + except Exception as e: + self.logger.error(f"计算距离失败: {str(e)}") + return None + + def add_casualty_position(self, casualty_id, lat, lon, timestamp=None): + """ + 添加伤员位置信息 + :param casualty_id: 伤员ID + :param lat: 纬度 + :param lon: 经度 + :param timestamp: 时间戳 + """ + if timestamp is None: + timestamp = time.time() + + self.casualty_positions[casualty_id] = { + 'latitude': lat, + 'longitude': lon, + 'timestamp': timestamp, + 'location_description': self.get_location_from_coordinates(lat, lon) + } + + self.logger.info(f"添加伤员位置: ID={casualty_id}, 位置=({lat}, {lon})") + + def get_casualty_position(self, casualty_id): + """ + 获取伤员位置信息 + :param casualty_id: 伤员ID + :return: 位置信息字典 + """ + return self.casualty_positions.get(casualty_id) + + def update_casualty_position(self, casualty_id, lat, lon): + """ + 更新伤员位置信息 + :param casualty_id: 伤员ID + :param lat: 新的纬度 + :param lon: 新的经度 + """ + if casualty_id in self.casualty_positions: + self.add_casualty_position(casualty_id, lat, lon) + self.logger.info(f"更新伤员位置: ID={casualty_id}, 新位置=({lat}, {lon})") + else: + self.logger.warning(f"未找到伤员ID: {casualty_id}") + + def get_nearest_casualty(self, lat, lon, max_distance=None): + """ + 获取最近的伤员 + :param lat: 当前位置纬度 + :param lon: 当前位置经度 + :param max_distance: 最大距离(米) + :return: 最近的伤员信息 + """ + nearest = None + min_distance = float('inf') + + for casualty_id, position in self.casualty_positions.items(): + distance = self.calculate_distance( + lat, lon, + position['latitude'], + position['longitude'] + ) + + if distance is not None and distance < min_distance: + if max_distance is None or distance <= max_distance: + min_distance = distance + nearest = { + 'casualty_id': casualty_id, + 'distance': distance, + 'position': position + } + + return nearest + + def get_all_casualties(self): + """ + 获取所有伤员位置信息 + :return: 伤员位置信息字典 + """ + return self.casualty_positions.copy() + + def remove_casualty(self, casualty_id): + """ + 移除伤员位置信息 + :param casualty_id: 伤员ID + """ + if casualty_id in self.casualty_positions: + del self.casualty_positions[casualty_id] + self.logger.info(f"移除伤员位置: ID={casualty_id}") + else: + self.logger.warning(f"未找到伤员ID: {casualty_id}") \ No newline at end of file diff --git a/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc b/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc new file mode 100644 index 00000000..70021e1f Binary files /dev/null and b/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc differ diff --git a/src/software/src/ui/static/css/styles.css b/src/software/src/ui/static/css/styles.css new file mode 100644 index 00000000..127f30ec --- /dev/null +++ b/src/software/src/ui/static/css/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/software/src/ui/static/js/app.js b/src/software/src/ui/static/js/app.js new file mode 100644 index 00000000..f7f9c845 --- /dev/null +++ b/src/software/src/ui/static/js/app.js @@ -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(` +
+
伤员ID: ${id}
+

位置: ${lat.toFixed(6)}, ${lon.toFixed(6)}

+ +
+ `); + 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(` +
+
无人机状态
+

状态: ${data.status}

+

电量: ${data.battery}%

+
+ `); + } +} + +// 更新物资状态 +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 = ` +
+
+ ${supply.name} (${supply.quantity}个可用) +
+ +
+ ${supply.description} + `; + supplyList.appendChild(supplyItem); + }); + } else { + supplyList.innerHTML = '

没有可用物资

'; + } +} + +// 更新任务状态 +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 = ` +
+
+ ${getMissionTypeLabel(data.currentMission.mission_type)} + + ${data.currentMission.status} + +
+
ID: ${data.currentMission.mission_id}
+
目标: (${data.currentMission.target_location[0].toFixed(6)}, + ${data.currentMission.target_location[1].toFixed(6)})
+
+ `; + } + } + + // 更新历史任务列表 + 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 = ` +
+ ${getMissionTypeLabel(mission.mission_type)} + + ${mission.status} + +
+
ID: ${mission.mission_id}
+ ${formatTime(mission.start_time)} + `; + missionList.appendChild(missionItem); + }); + } else { + missionList.innerHTML = '

没有历史任务

'; + } +} + +// 获取任务类型标签 +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 = ` +
+
+ 伤员ID: ${id} +
+ +
+
位置: ${position.lat.toFixed(6)}, ${position.lng.toFixed(6)}
+ `; + + casualtyList.appendChild(casualtyItem); + } + + // 如果没有伤员,显示提示信息 + if (Object.keys(casualtyMarkers).length === 0) { + casualtyList.innerHTML = '

没有伤员数据

'; + } +} + +// 加载医疗物资数据 +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('请求物资失败,请检查网络连接'); + }); +} \ No newline at end of file diff --git a/src/software/src/ui/templates/index.html b/src/software/src/ui/templates/index.html new file mode 100644 index 00000000..7f2049c5 --- /dev/null +++ b/src/software/src/ui/templates/index.html @@ -0,0 +1,617 @@ + + + + + + 智能战场医疗后送系统 + + + + + + + + + +
+
+ +
+
+
+ 战场地图 +
+ +
+
+
+
+
+
+ + +
+
+ 任务状态 +
+
+
+
+
+ 0 +

待处理任务

+
+
+
+
+ 0 +

进行中任务

+
+
+
+
+ 0 +

已完成任务

+
+
+
+
+ 0 +

失败任务

+
+
+
+
+
+
+ + +
+ +
+
+ 无人机状态 + 已连接 +
+
+
+

+ 状态: + 待机 +

+

+ 电量: + 100% +

+

+ 位置: + 未获取 +

+

+ 速度: + 0 m/s +

+
+
+
+ + +
+
+ 伤员信息 + 0 +
+
+
+ +
+ 尚无伤员信息 +
+
+ +
+
+ + +
+
+ 医疗物资 +
+
+
+ +
+ 正在加载物资信息... +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/software/src/ui/web_interface.py b/src/software/src/ui/web_interface.py new file mode 100644 index 00000000..6994e6c9 --- /dev/null +++ b/src/software/src/ui/web_interface.py @@ -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/') + 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)}") \ No newline at end of file diff --git a/src/software/智能战场医疗后送系统_SDS_1.0.md b/src/software/智能战场医疗后送系统_SDS_1.0.md new file mode 100644 index 00000000..d33d0422 --- /dev/null +++ b/src/software/智能战场医疗后送系统_SDS_1.0.md @@ -0,0 +1,619 @@ +文档编号:智能战场医疗后送系统 – SDS – 1.0 + + + + + + + + + + +# 智能战场医疗后送系统 +# 软件设计规格说明书 + + + + + + + + + + + + + + + + + + +日期:2023-06-15 + + + +## 文档变更历史记录 +| 序号 | 变更日期 | 变更人员 | 变更内容详情描述 | 版本 | +|------|----------|----------|------------------|------| +| 1 | 2023-06-15 | 系统架构师 | 创建文档初稿 | 1.0 | +| | | | | | +| | | | | | + +## 目录 +1. [引言](#1引言) + 1. [编写目的](#11-编写目的) + 2. [读者对象](#12-读者对象) + 3. [软件项目概述](#13-软件项目概述) + 4. [文档概述](#14-文档概述) + 5. [定义](#15-定义) + 6. [参考资料](#16-参考资料) +2. [软件设计约束](#2软件设计约束) + 1. [软件设计目标和原则](#21-软件设计目标和原则) + 2. [软件设计的约束和限制](#22-软件设计的约束和限制) +3. [软件设计](#3软件设计) + 1. [软件体系结构设计](#31-软件体系结构设计) + 2. [用户界面设计](#32-用户界面设计) + 3. [用例设计](#33-用例设计) + 4. [类设计](#34-类设计) + 5. [数据设计](#35-数据设计) + 6. [部署设计](#36-部署设计) + +## 1、引言 + +### 1.1 编写目的 + +本文档旨在详细说明智能战场医疗后送系统的软件设计规格,为开发团队提供系统实现的技术指导,同时为测试和维护团队提供系统结构的清晰描述。文档描述了系统的架构设计、模块功能、接口规范、数据结构等内容,确保系统的开发符合预期目标和质量要求。 + +### 1.2 读者对象 + +本文档的读者对象包括: +- 项目经理:了解系统总体设计和进度安排 +- 系统开发人员:理解系统架构和详细设计,指导编码实现 +- 测试人员:了解系统功能和接口,制定测试计划和用例 +- 维护人员:了解系统结构,为后期维护提供参考 +- 项目干系人:了解系统总体设计和主要功能 + +### 1.3 软件项目概述 + +**项目名称**:智能战场医疗后送系统 +**项目简称**:医疗后送系统 +**项目代号**:MES-UAV-1.0 + +**用户单位**:军事医疗部门 + +**开发单位**:军事医疗装备研发中心 + +**项目需求描述**: +- 功能需求:系统基于无人机平台,实现战场伤员位置标记、医疗物资运送、无人机状态监控等功能,通过Web界面实现人机交互,支持医疗后送任务的规划和执行。 +- 性能需求:系统需支持实时数据传输和处理,无人机响应时间不超过3秒,Web界面刷新率不低于5秒/次,支持同时处理至少5个伤员请求和3个物资运送任务。 + +### 1.4 文档概述 + +本文档包含以下主要内容: +- 第1章:介绍文档的编写目的、读者对象、项目概述等基本信息 +- 第2章:说明软件设计的目标、原则和约束条件 +- 第3章:详细描述软件设计,包括体系结构、用户界面、用例、类、数据和部署设计 + +### 1.5 定义 + +| 术语/缩写 | 定义 | +|-----------|------| +| MES | Medical Evacuation System,医疗后送系统 | +| UAV | Unmanned Aerial Vehicle,无人机 | +| MAVLink | Micro Air Vehicle Link,微型飞行器通信协议 | +| SITL | Software In The Loop,软件在环模拟 | +| GCS | Ground Control Station,地面控制站 | +| API | Application Programming Interface,应用程序接口 | +| GPS | Global Positioning System,全球定位系统 | +| WebSocket | 一种在单个TCP连接上进行全双工通信的协议 | + +### 1.6 参考资料 + +1. 《无人机作战应用技术规范》,军事医疗装备研究所,2022年 +2. 《战场医疗救援系统需求分析报告》,军事医疗部门,2022年 +3. DroneKit-Python API文档,https://dronekit-python.readthedocs.io/ +4. MAVLink协议规范,https://mavlink.io/en/ +5. Flask Web框架文档,https://flask.palletsprojects.com/ +6. Socket.IO实时应用框架文档,https://socket.io/docs/ + +## 2、软件设计约束 + +### 2.1 软件设计目标和原则 + +**设计目标**: +1. 实现用户需求:满足战场医疗后送的实际需求,提供伤员位置标记、医疗物资运送、任务规划等功能 +2. 系统可靠性:确保系统在战场环境中稳定运行,具备一定的容错和恢复能力 +3. 易用性:提供简单直观的用户界面,减少操作人员的培训成本 +4. 可扩展性:系统架构支持功能模块的扩展和新型无人机的接入 +5. 可维护性:采用模块化设计,便于系统维护和升级 + +**设计原则**: +1. 模块化原则:系统功能划分为独立模块,降低模块间耦合度 +2. 开闭原则:系统设计对扩展开放,对修改关闭 +3. 单一职责原则:每个类和模块职责单一,提高内聚性 +4. 接口隔离原则:定义清晰的模块接口,降低依赖 +5. 依赖倒置原则:高层模块不应依赖低层模块,都应依赖抽象 +6. 抽象设计原则:抽象出核心概念和通用接口,支持不同实现 + +### 2.2 软件设计的约束和限制 + +**运行环境要求**: +- 硬件平台:支持x86/64架构的服务器或边缘计算设备 +- 操作系统:Linux(推荐Ubuntu 20.04 LTS)或Windows 10/11 + +**开发语言**: +- 后端:Python 3.8+ +- 前端:HTML5, CSS3, JavaScript (Vue.js框架) + +**标准规范**: +- 代码规范:PEP 8 (Python) +- API设计:RESTful风格 +- 通信协议:MAVLink (无人机通信),WebSocket (实时数据传输) + +**开发工具**: +- 集成开发环境:PyCharm或Visual Studio Code +- 版本控制:Git +- 测试工具:Pytest +- 文档工具:Sphinx + +**容量和性能要求**: +- 支持同时处理不少于5个无人机连接 +- Web界面响应时间不超过1秒 +- 无人机命令响应时间不超过3秒 +- 系统连续运行时间不少于72小时 + +**灵活性和配置要求**: +- 通过配置文件支持不同环境(开发、测试、生产)配置 +- 支持不同类型无人机的适配 +- 支持模拟和实际操作模式切换 +- 支持本地部署和云部署方式 + +## 3、软件设计 + +### 3.1 软件体系结构设计 + +智能战场医疗后送系统采用分层模块化架构,从逻辑上分为以下几个主要层次: + +**1. 表示层** +- Web界面模块:提供基于Web的用户交互界面 +- 实时数据可视化模块:显示无人机状态和任务进度 + +**2. 应用层** +- 任务管理模块:处理任务创建、分配和监控 +- 通信管理模块:处理外部系统通信请求 + +**3. 领域层** +- 无人机控制模块:负责无人机的基本操作控制 +- 任务规划模块:制定无人机执行任务的路径规划 +- 位置管理模块:管理伤员和基地位置信息 +- 医疗物资模块:管理医疗物资库存和请求 + +**4. 基础设施层** +- 配置管理模块:处理系统配置和环境设置 +- 数据持久化模块:存储系统运行数据 +- 日志模块:记录系统运行日志 + +**架构图**: + +``` ++-------------------+ +-------------------+ +| Web界面模块 | | 实时数据可视化模块 | ++-------------------+ +-------------------+ + | | + v v ++-------------------+ +-------------------+ +| 任务管理模块 | | 通信管理模块 | ++-------------------+ +-------------------+ + | | | + v v v ++-------------------+ +-------------------+ +-------------------+ +| 无人机控制模块 | | 任务规划模块 | | 位置管理模块 | ++-------------------+ +-------------------+ +-------------------+ + | | | + v v v ++-------------------+ +-------------------+ +-------------------+ +| 医疗物资模块 | | 配置管理模块 | | 日志模块 | ++-------------------+ +-------------------+ +-------------------+ + | + v + +-------------------+ + | 数据持久化模块 | + +-------------------+ +``` + +**模块说明**: + +1. **Web界面模块(WebInterface)** + - 负责提供基于Web的用户交互界面 + - 使用Flask框架和Socket.IO实现 + - 支持伤员位置标记、物资请求、无人机状态监控等功能 + +2. **任务管理模块(MissionPlanner)** + - 负责创建、安排和监控任务 + - 定义任务类型(伤员转运、物资运送) + - 管理任务队列和优先级 + +3. **无人机控制模块(DroneController)** + - 负责与无人机建立连接和通信 + - 执行基本飞行控制(起飞、降落、导航等) + - 监控无人机状态(位置、电量等) + +4. **位置管理模块(PositionManager)** + - 管理伤员位置信息 + - 提供位置查询和距离计算功能 + - 支持地理编码功能 + +5. **医疗物资模块(MedicalSupplyManager)** + - 管理医疗物资库存 + - 处理物资请求和分配 + - 跟踪物资使用情况 + +6. **通信管理模块(CommunicationManager)** + - 处理与外部系统的通信 + - 接收外部伤员信息和物资请求 + - 支持多种通信协议 + +7. **配置管理模块(Config)** + - 管理系统配置参数 + - 支持不同环境下的配置切换 + - 提供配置加载和验证功能 + +### 3.2 用户界面设计 + +系统采用Web界面设计,主要包含以下界面组件: + +**1. 主控制面板** +- 显示系统概览和无人机状态 +- 包含任务列表和执行状态 +- 提供快速操作按钮 + +**2. 地图视图** +- 显示无人机、伤员和基地位置 +- 支持地图缩放和平移 +- 允许直接在地图上标记伤员位置 + +**3. 物资管理界面** +- 显示可用医疗物资列表 +- 物资库存状态和使用记录 +- 创建物资运送请求 + +**4. 任务规划界面** +- 创建和编辑任务 +- 显示任务执行路径 +- 任务优先级设置 + +**5. 系统设置界面** +- 系统参数配置 +- 无人机连接设置 +- 用户权限管理 + +**界面跳转关系**: + +``` ++----------------+ +| 登录界面 | ++----------------+ + | + v ++----------------+ +| 主控制面板 | ++----------------+ + | | | | + v v v v ++------+ +------+ +------+ +------+ +|地图 | |物资 | |任务 | |系统 | +|视图 | |管理 | |规划 | |设置 | ++------+ +------+ +------+ +------+ +``` + +### 3.3 用例设计 + +系统主要用例设计如下: + +**1. 标记伤员位置** + +顺序图: +``` +操作员 -> Web界面: 在地图上标记伤员位置 +Web界面 -> PositionManager: 添加伤员位置(casualty_id, lat, lon) +PositionManager -> PositionManager: 存储伤员位置信息 +PositionManager -> Web界面: 返回成功状态 +Web界面 -> 操作员: 显示伤员标记成功 +``` + +**2. 请求医疗物资运送** + +顺序图: +``` +操作员 -> Web界面: 创建物资运送请求 +Web界面 -> MedicalSupplyManager: 创建物资请求(target_id, supply_id, quantity) +MedicalSupplyManager -> MedicalSupplyManager: 检查库存可用性 +MedicalSupplyManager -> PositionManager: 获取目标位置(target_id) +PositionManager -> MedicalSupplyManager: 返回目标位置 +MedicalSupplyManager -> MissionPlanner: 创建物资运送任务(mission_id, target_location) +MissionPlanner -> MissionPlanner: 将任务添加到队列 +MissionPlanner -> MedicalSupplyManager: 返回任务创建状态 +MedicalSupplyManager -> Web界面: 返回请求状态 +Web界面 -> 操作员: 显示请求结果 +``` + +**3. 执行无人机任务** + +顺序图: +``` +MissionPlanner -> MissionPlanner: 从队列获取下一个任务 +MissionPlanner -> DroneController: 执行起飞(altitude) +DroneController -> DroneController: 控制无人机起飞 +DroneController -> MissionPlanner: 返回起飞状态 +MissionPlanner -> DroneController: 导航到目标位置(lat, lon, altitude) +DroneController -> DroneController: 控制无人机飞往目标位置 +DroneController -> MissionPlanner: 返回导航状态 +MissionPlanner -> DroneController: 执行降落 +DroneController -> DroneController: 控制无人机降落 +DroneController -> MissionPlanner: 返回降落状态 +MissionPlanner -> MissionPlanner: 更新任务状态为完成 +``` + +**4. 监控无人机状态** + +顺序图: +``` +操作员 -> Web界面: 请求无人机状态 +Web界面 -> DroneController: 获取无人机状态 +DroneController -> Web界面: 返回状态数据(位置,电量,任务) +Web界面 -> 操作员: 显示无人机状态 +``` + +### 3.4 类设计 + +系统的主要类设计如下: + +**1. MedicalEvacuationSystem(主系统类)** + - 属性: + - drone_controller: DroneController + - mission_planner: MissionPlanner + - position_manager: PositionManager + - medical_supply_manager: MedicalSupplyManager + - communication_manager: CommunicationManager + - web_interface: WebInterface + - 方法: + - __init__(): 初始化系统 + - start(): 启动系统 + - stop(): 停止系统 + - _handle_casualty_request(data): 处理伤员请求 + - _handle_supply_request(data): 处理物资请求 + - _handle_drone_status_request(): 处理无人机状态请求 + +**2. DroneController(无人机控制类)** + - 属性: + - vehicle: Vehicle + - connection_string: string + - logger: Logger + - 方法: + - connect(): 连接到无人机 + - arm_and_takeoff(target_altitude): 解锁并起飞 + - goto_location(lat, lon, altitude): 飞往指定位置 + - land(): 降落 + - close(): 关闭连接 + +**3. Mission(任务类)** + - 属性: + - mission_id: string + - mission_type: MissionType + - target_location: tuple(lat, lon) + - altitude: float + - payload: dict + - status: MissionStatus + - start_time: datetime + - end_time: datetime + - 方法: + - to_dict(): 转换为字典表示 + +**4. MissionPlanner(任务规划类)** + - 属性: + - drone_controller: DroneController + - mission_queue: Queue + - current_mission: Mission + - mission_history: list + - running: bool + - 方法: + - start(): 启动任务规划器 + - stop(): 停止任务规划器 + - add_mission(mission): 添加任务 + - get_current_mission(): 获取当前任务 + - get_mission_history(): 获取任务历史 + +**5. PositionManager(位置管理类)** + - 属性: + - casualty_positions: dict + - geocoder: Geocoder + - 方法: + - add_casualty_position(casualty_id, lat, lon): 添加伤员位置 + - get_casualty_position(casualty_id): 获取伤员位置 + - calculate_distance(lat1, lon1, lat2, lon2): 计算距离 + - get_nearest_casualty(lat, lon): 获取最近伤员 + +**6. MedicalSupply(医疗物资类)** + - 属性: + - id: string + - name: string + - description: string + - weight: float + - quantity: int + +**7. MedicalSupplyManager(医疗物资管理类)** + - 属性: + - supplies: dict + - supply_requests: dict + - 方法: + - get_all_supplies(): 获取所有物资 + - create_supply_request(target_id, supply_id, quantity): 创建物资请求 + - update_request_status(request_id, status): 更新请求状态 + +**8. WebInterface(Web界面类)** + - 属性: + - app: Flask + - socketio: SocketIO + - casualty_handler: function + - supply_handler: function + - drone_status_handler: function + - 方法: + - start(): 启动Web服务 + - broadcast_casualty_update(data): 广播伤员更新 + - broadcast_drone_update(data): 广播无人机状态更新 + +**9. CommunicationManager(通信管理类)** + - 属性: + - server_socket: Socket + - clients: list + - casualty_handler: function + - supply_handler: function + - 方法: + - start_server(): 启动通信服务器 + - close(): 关闭服务器 + - send_message(client, message): 发送消息 + +### 3.5 数据设计 + +系统主要使用内存数据结构存储运行时数据,同时提供数据持久化功能。 + +**1. 内存数据结构** + +- **伤员位置数据** + ```python + casualty_positions = { + 'casualty_id': { + 'latitude': float, + 'longitude': float, + 'timestamp': float, + 'location_description': string + } + } + ``` + +- **医疗物资数据** + ```python + supplies = { + 'supply_id': MedicalSupply( + id='supply_id', + name='name', + description='description', + weight=float, + quantity=int + ) + } + ``` + +- **物资请求数据** + ```python + supply_requests = { + 'request_id': { + 'request_id': string, + 'target_id': string, + 'supply_id': string, + 'quantity': int, + 'status': string, + 'timestamp': float + } + } + ``` + +- **任务数据** + ```python + mission = { + 'mission_id': string, + 'mission_type': string, + 'target_location': (lat, lon), + 'altitude': float, + 'payload': dict, + 'status': string, + 'start_time': datetime, + 'end_time': datetime, + 'error_message': string + } + ``` + +**2. 数据持久化** + +系统将支持以下数据持久化机制: + +- **日志文件**:记录系统运行日志和关键事件 +- **配置文件**:存储系统配置参数 +- **数据导出**:支持将任务历史、伤员位置等数据导出为JSON或CSV格式 + +### 3.6 部署设计 + +系统支持以下部署方式: + +**1. 单机部署** + +``` ++-----------------+ +| 部署服务器 | ++-----------------+ +| Web服务 (Flask)| +| 通信服务 | +| 应用逻辑 | ++-----------------+ + | + v ++-----------------+ +| 无人机 | ++-----------------+ +``` + +- 所有组件部署在同一台服务器上 +- 适合小规模应用场景 +- 硬件要求:4核CPU,8GB内存,50GB存储空间 + +**2. 分布式部署** + +``` ++-----------------+ +-----------------+ +| Web服务器 | | 应用服务器 | ++-----------------+ +-----------------+ +| Web界面 | | 任务规划 | +| Socket.IO |<-->| 无人机控制 | ++-----------------+ | 位置管理 | + | 物资管理 | + +-----------------+ + | + v + +-----------------+ + | 无人机 | + +-----------------+ +``` + +- Web服务与应用逻辑分离部署 +- 适合中大规模应用或高负载场景 +- 支持水平扩展,提高系统可用性 + +**3. 边缘计算部署** + +``` ++----------------+ +----------------+ +| 云端服务器 | | 边缘服务器 | ++----------------+ +----------------+ +| Web界面 | | 任务规划 | +| 数据存储 |<-->| 无人机控制 | +| 分析报表 | | 通信服务 | ++----------------+ +----------------+ + | + v + +----------------+ + | 无人机 | + +----------------+ +``` + +- 核心控制逻辑部署在靠近作战区域的边缘服务器 +- Web界面和数据分析功能部署在云端 +- 提高控制响应速度,降低网络依赖 +- 适合网络条件受限的战场环境 + +**部署要求**: +- 操作系统:Ubuntu 20.04 LTS +- Python 3.8+及相关依赖包 +- 网络:支持TCP/IP通信,提供稳定的网络连接 +- 安全:配置防火墙规则,保护关键服务端口 +- 存储:提供足够的日志和数据存储空间 \ No newline at end of file