From 90544a2efb023219a4f3bcfeb9ac08470ca6910c Mon Sep 17 00:00:00 2001 From: yjhjkcl <1942571676@qq.com> Date: Thu, 17 Apr 2025 08:44:37 +0800 Subject: [PATCH] 1 --- 1.txt | 0 src/2.txt | 0 src/software/.env.example | 32 + src/software/README.md | 73 +++ src/software/__pycache__/main.cpython-39.pyc | Bin 0 -> 7845 bytes .../config/__pycache__/config.cpython-39.pyc | Bin 0 -> 2876 bytes src/software/config/config.py | 110 ++++ src/software/init_project.sh | 89 +++ src/software/main.py | 311 +++++++++ src/software/requirements.txt | 9 + src/software/simulation.py | 194 ++++++ .../communication_manager.cpython-39.pyc | Bin 0 -> 5216 bytes .../communication/communication_manager.py | 140 ++++ .../drone_controller.cpython-39.pyc | Bin 0 -> 3256 bytes .../medical_supplies.cpython-39.pyc | Bin 0 -> 5965 bytes .../mission_planner.cpython-39.pyc | Bin 0 -> 8293 bytes src/software/src/drone/drone_controller.py | 103 +++ src/software/src/drone/medical_supplies.py | 190 ++++++ src/software/src/drone/mission_planner.py | 271 ++++++++ .../position_manager.cpython-39.pyc | Bin 0 -> 4407 bytes .../src/positioning/position_manager.py | 131 ++++ .../__pycache__/web_interface.cpython-39.pyc | Bin 0 -> 7987 bytes src/software/src/ui/static/css/styles.css | 342 ++++++++++ src/software/src/ui/static/js/app.js | 451 +++++++++++++ src/software/src/ui/templates/index.html | 617 +++++++++++++++++ src/software/src/ui/web_interface.py | 191 ++++++ .../智能战场医疗后送系统_SDS_1.0.md | 619 ++++++++++++++++++ 27 files changed, 3873 insertions(+) create mode 100644 1.txt create mode 100644 src/2.txt create mode 100644 src/software/.env.example create mode 100644 src/software/README.md create mode 100644 src/software/__pycache__/main.cpython-39.pyc create mode 100644 src/software/config/__pycache__/config.cpython-39.pyc create mode 100644 src/software/config/config.py create mode 100644 src/software/init_project.sh create mode 100644 src/software/main.py create mode 100644 src/software/requirements.txt create mode 100644 src/software/simulation.py create mode 100644 src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc create mode 100644 src/software/src/communication/communication_manager.py create mode 100644 src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc create mode 100644 src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc create mode 100644 src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc create mode 100644 src/software/src/drone/drone_controller.py create mode 100644 src/software/src/drone/medical_supplies.py create mode 100644 src/software/src/drone/mission_planner.py create mode 100644 src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc create mode 100644 src/software/src/positioning/position_manager.py create mode 100644 src/software/src/ui/__pycache__/web_interface.cpython-39.pyc create mode 100644 src/software/src/ui/static/css/styles.css create mode 100644 src/software/src/ui/static/js/app.js create mode 100644 src/software/src/ui/templates/index.html create mode 100644 src/software/src/ui/web_interface.py create mode 100644 src/software/智能战场医疗后送系统_SDS_1.0.md 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 0000000000000000000000000000000000000000..31a8f879502882e277fb9393fd935a571fb6a114 GIT binary patch literal 7845 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..add70e35cc9fd1ba43b9919228dd6a2db5734a33 GIT binary patch literal 2876 zcmbVO>yHyv6uQR-ve6)=F0P`a~glXkjk%fkuDr1lQA(Cw^urV<-X zFpCKUL5&JNFl>#67!!>eL$ZkeANQ-4!e_r}Vm#+g#V(JS*yfzx-#Pbj?s?r-5{Vdr z&lm6iUMnOB`4z$7M+LAC$Y;PPp;Cj`;jd&%faQiftJsP}TxFg}WSUTgs;3B5=Ve<3 zq){D^?yB>OtpScu1F#{m4me6vl}Yz@Uz7$K4@; zz?_aeFWCu!;zBpv*A3Bx&@B_XVY&>~CEeu$CFydZThYfW=t_ZC_VEZ^CGbceucm7R zUL{tqrR%`1)g40DAD8Vl{W%-xMlok?A8(?Y1zsoCY+15qy}N;q(ygZm*uD|Q56}n2 zcoTgHoVbna8LxS?UTZA_A9XJ;_BtnTy>+g4{hQu**IJuFcH`s4oAcLioc^eLX|enE zwcdq`-FMI5I{9YrhikoSm)qLJ*!b8ukLC)~PR=}N=D3=*rV8zy+qPHyYEYkbQ~u}^ zquaJOE4POY_f@wsfM;Fbuhd{7Hx8NmofitFaytUMik*dMrcf-~>hAH~a8jm_&x<+z zVW`_RK0eOZOco24>0}C)WoF9Rg5{LT#jG{mUe%&=_2%A&SFJQ&_WWS)#Liv2#(83Y zx@0=JbU9l-Fln~c9ec+1@ZnHcu%<()HZk$^*bbhYG^f%Ba%Cr-`)B$D-uz#Awpf}m z&B?a11I4D5-k&p_X|v!=nO4T+YMHfMt_H|g=hKIs$!w{d28}aSEaV+LwXNedVD<8R z)&fbnY!+V(-3rDDMSt5;Hh%!^E?7$v9#Wj>Kp7zaQD6Z%CIQJn3Xlq$< z=t1O|f!dA?sUWvjz=E6SuXf+LaO2{a-Cr*D7O!+qf1T2}HfioZFwL-(c;ryJXn{|; zW)_QuA~!O{Y&n}r=TcD~X?V3-y;)PwWnBH zX;nlM0Tgkd`&S*uHqtXQj+M@vJaW)1mY_Vi?D<>=`{8ZiT5E33Wudv~B!;!Zm3lK^ z5rhV^C^CHdxONmCmVm`kfI(vL3=)Q!1_AOQCJdV9>EpME9qY-FM#Wo&UD``F!{Yltm?5U+L^&(6dL7ZAX?u zhS$As?AcSG%>nu7WVjoAh3tP@ot$A1et1*7=77wgb((m)CcK zqIid8D7(L9qoBRFKW&|Ac7*ES*9cFPT|aoQ zje%0au?pku0c1>|mEi9h+)!_!0pSzTz5agp%(?Jly-!Ys`C`XS>tGfh;k;>;?Ku56 z&3J6?cJ%B>5cFhDskzx8XG>Ksx7@96}1^(PAP`Cq86m2DjExjsUg$hjdu{epG zl=1dJGTRLp`kUArn;b+*{5xbafUwD7dJde1EBJ7=#Te8?fA4`~h2zE9#= Ux|soI4k@}aoJs2NjT(u+0a%LDeE 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 0000000000000000000000000000000000000000..0d868fce1a0cbdd36dd628d1c56f028ec41182d4 GIT binary patch literal 5216 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..87160ef63edb74cffa8bcd09ac34ee01b49055d0 GIT binary patch literal 5965 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b1c840fc42e7978edb5dd3231aae7027faf04226 GIT binary patch literal 4407 zcmb_g-ESOM6~A}Bz3~^0V-is564H`|G)f|bV3i;cMko@PNI@;EQbwb_V{6^r9rw;` zs8{1EkN`F%mzv>DopbFB3}hIt7ia%5H8{f9-|1odrQ+eM`1A8<1QWc>3UpV>yrL8oPO@57 zD_TK|WnR_``Zt--gnoqy{jyRp1V7Jm#v}A+6FDCBO?Z`3)d@-!JXlk%CtSZ&EYj!g zS32?c@YZDoE?7YkTqsvqK^3adaK|LVz+D%k!V-N~I3~-8eRyMveldW%B@T!|F@!ID zVpx0(Z!%&;?8kkdFtMz$Xy}~hmx7X4{kBterd(O%+g3p`E#&L?^ADp5*fI~46&^BM z4K!NLRg(1*Lu&{XLu$2yP;AWKY}|US@r&z?wKq2&T-{t-+5FW{8*lu!u)pGpQqd{f z?nS3)`?G%F!VrZ_&3C1Z1yqBHoEe#AZ)&Pkor?OV+~8Y~T^U)nU3Dt1ZAS)ub){4= zQEb8@j3}d#qT|<`axiPpwD#oR=8@*R<;mMj4nXRfck9k?@>w`q_CX^urF+qp zz8jg25YqMisP7xE6x|turL;&xhqpZ$seT}Hy6mS{Dwc}It43DYDDtG*TGN0Z0(_Qr0 z)BPQdE)=BLIYkJWf6^Eojjl1;NZM3!jRw%1j5MHzS|>Oa%cly)j901#CtLSZktIrg;8csQBr@v;6^N!>tf9MWxAi^= zHGdcl>r;Ru3+!MdXgb`DrrypIJ1|6eA{@nZaSlQVG}M?J2KllTP(!E?mO>&BNGpm^ zmhd#Kg#0vUc$(0ohM)@pO|(EBTH~fkDWQ=>3F*5a)*M;|N4vX6N#r$-tM zDi7hSB$b4Jv{H4_YctQjZHABsNN0>1%8%VhIRHYlQ}uS9c@CNhSS&zARXF`J|LQmE z&vb)j*ZxIHeC>;5f`1Gb;<9#bS)sav*L=K6P1z#j|JBv)Sly16#vg7pe)4`gQ%;G^ z7pWl6+e#dL^0Qbp=tz^-QP8s}1gd@jZ)45Kr0e81+lscIK%34Bp5iX#Pn|*>YtgNtc_TsqMtRxLm;EZ{O9qkz7)U5Xa=54p)aX(+RJ011}qr0zhcT13rUPMmo9BRyapF0 zph&-V@@+obc<}4S!qvnZPw?~(wVnQMjaG49Niz|Uv2>}idTXydnr{bh*9yBheL3UXw%nGDRVPnJC=NP=B2p_=#Q)1;qd+zvlR{o7X=nN=4xxF6tk zshTc6nd1aaN)vUH#VCb-t7D3$=b`9l(J)KnmNLRM{0weNF+h*N-ftexw^* zyA9WK03sTSfko+nXk?kMpi~F7QZ1i4w&nJPOU>d3P%CuOcFa|hvaS3aHC?nc zB;AU4u|rFB=p<~UXHdb7D)pzk0n^nZ$8n;c$n{G)7)BY}u6UwWrXy;VN*+Kc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..70021e1f688b2c5ec9e2477f4c5f7af03da1f276 GIT binary patch literal 7987 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 PMEM$95PPQOKdb*2BUi06 literal 0 HcmV?d00001 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