diff --git a/document/vx.json b/document/vx.json
new file mode 100644
index 0000000..abf7fb5
--- /dev/null
+++ b/document/vx.json
@@ -0,0 +1,11 @@
+{
+ "created_time": "2026-04-20T02:11:45Z",
+ "files": [
+ ],
+ "folders": [
+ ],
+ "id": "1542",
+ "modified_time": "2026-04-20T02:11:45Z",
+ "signature": "4031563627892149089",
+ "version": 3
+}
diff --git a/src/软件前端/.claude/settings.local.json b/src/软件前端/.claude/settings.local.json
new file mode 100644
index 0000000..640f442
--- /dev/null
+++ b/src/软件前端/.claude/settings.local.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(curl -sL \"https://unpkg.com/roslib@2.1.0/dist/\")",
+ "Bash(curl -sL \"https://unpkg.com/roslib@1.3.0/build/roslib.min.js\" -o \"d:/29578/Documents/Study/computer/S_E/软件体系结构与设计/软件前端/js/roslib.min.js\" -w \"%{http_code} %{size_download}\")"
+ ]
+ }
+}
diff --git a/src/软件前端/css/style.css b/src/软件前端/css/style.css
new file mode 100644
index 0000000..b3e15fc
--- /dev/null
+++ b/src/软件前端/css/style.css
@@ -0,0 +1,383 @@
+/* ===== 全局样式 ===== */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ background: #f0f2f5;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ color: #333;
+}
+
+/* ===== 顶部状态栏 ===== */
+.top-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 48px;
+ padding: 0 16px;
+ background: #1a1a2e;
+ color: #e0e0e0;
+ flex-shrink: 0;
+ z-index: 1000;
+}
+
+.top-bar-left {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.logo {
+ font-size: 15px;
+ font-weight: 700;
+ color: #fff;
+ letter-spacing: 1px;
+}
+
+.status-item {
+ font-size: 13px;
+ color: #aaa;
+}
+
+.status-item strong {
+ color: #e0e0e0;
+}
+
+.top-bar-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.top-bar-right input {
+ width: 220px;
+ padding: 5px 10px;
+ border: 1px solid #444;
+ border-radius: 4px;
+ background: #2a2a3e;
+ color: #e0e0e0;
+ font-size: 13px;
+}
+
+/* ===== 连接模式切换 ===== */
+.conn-mode-group {
+ display: flex;
+ border-radius: 4px;
+ overflow: hidden;
+ border: 1px solid #444;
+}
+
+.mode-btn {
+ padding: 4px 12px;
+ background: #2a2a3e;
+ color: #aaa;
+ border: none;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 600;
+ transition: all 0.2s;
+}
+
+.mode-btn:hover { color: #ddd; }
+
+.mode-btn.active {
+ background: #007aff;
+ color: #fff;
+}
+
+.conn-mode-tag {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 3px;
+ background: #2a2a3e;
+ color: #888;
+ white-space: nowrap;
+}
+
+/* ===== 标签 ===== */
+.tag {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.tag-success { background: #f6ffed; color: #52c41a; }
+.tag-danger { background: #fff1f0; color: #ff4d4f; }
+.tag-warning { background: #fffbe6; color: #faad14; }
+.tag-info { background: #e6f7ff; color: #007aff; }
+
+/* ===== 按钮 ===== */
+.btn {
+ padding: 7px 14px;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 600;
+ transition: opacity 0.2s;
+}
+
+.btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.btn:not(:disabled):hover { opacity: 0.85; }
+
+.btn-primary { background: #007aff; color: #fff; }
+.btn-success { background: #52c41a; color: #fff; }
+.btn-warning { background: #faad14; color: #fff; }
+.btn-danger { background: #ff4d4f; color: #fff; }
+.btn-secondary { background: #e0e0e0; color: #333; }
+.btn.wide { width: 100%; }
+
+.btn-link {
+ background: none;
+ border: none;
+ color: #999;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.btn-link:hover { color: #007aff; }
+
+.btn-group {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.btn-group .btn { flex: 1; }
+
+/* ===== 主内容区 ===== */
+.main-content {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+/* ===== 地图 ===== */
+.map-container {
+ flex: 1;
+ min-width: 0;
+ z-index: 1;
+}
+
+/* ===== 右侧控制面板 ===== */
+.control-panel {
+ width: 300px;
+ flex-shrink: 0;
+ background: #fff;
+ border-left: 1px solid #e0e0e0;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-section {
+ padding: 12px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.panel-header {
+ font-size: 14px;
+ font-weight: 700;
+ color: #333;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.panel-header span:last-child {
+ font-weight: 400;
+ font-size: 12px;
+ color: #999;
+}
+
+/* ===== 航点列表 ===== */
+.waypoint-list {
+ max-height: 220px;
+ overflow-y: auto;
+ margin-bottom: 8px;
+}
+
+.empty-hint {
+ text-align: center;
+ color: #bbb;
+ font-size: 13px;
+ padding: 20px 0;
+}
+
+.wp-item {
+ display: flex;
+ align-items: center;
+ padding: 6px 8px;
+ border-radius: 6px;
+ margin-bottom: 4px;
+ background: #f8f9fa;
+ cursor: pointer;
+ font-size: 13px;
+ transition: background 0.15s;
+}
+
+.wp-item:hover { background: #e6f7ff; }
+
+.wp-item.active { background: #e6f7ff; border: 1px solid #91d5ff; }
+
+.wp-index {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: #007aff;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ font-weight: 700;
+ margin-right: 8px;
+ flex-shrink: 0;
+}
+
+.wp-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.wp-coord {
+ font-size: 11px;
+ color: #666;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.wp-alt {
+ font-size: 11px;
+ color: #999;
+}
+
+.wp-delete {
+ width: 20px;
+ height: 20px;
+ border: none;
+ background: none;
+ color: #ccc;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ flex-shrink: 0;
+}
+
+.wp-delete:hover { color: #ff4d4f; }
+
+.wp-params {
+ display: flex;
+ gap: 10px;
+ font-size: 12px;
+ color: #666;
+}
+
+.wp-params input {
+ width: 60px;
+ padding: 3px 6px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 12px;
+ text-align: center;
+}
+
+/* ===== 数据面板 ===== */
+.data-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.data-item {
+ background: #f8f9fa;
+ border-radius: 6px;
+ padding: 8px;
+ text-align: center;
+}
+
+.data-label {
+ display: block;
+ font-size: 11px;
+ color: #999;
+ margin-bottom: 2px;
+}
+
+.data-value {
+ font-size: 13px;
+ font-weight: 600;
+ color: #333;
+}
+
+/* ===== 底部日志栏 ===== */
+.log-bar {
+ height: 120px;
+ flex-shrink: 0;
+ background: #1e1e1e;
+ display: flex;
+ flex-direction: column;
+}
+
+.log-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 12px;
+ font-size: 12px;
+ color: #888;
+ background: #2a2a2a;
+ border-bottom: 1px solid #333;
+}
+
+.log-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 4px 12px;
+ font-family: 'Consolas', 'Monaco', monospace;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.log-entry {
+ white-space: nowrap;
+}
+
+.log-entry .time { color: #666; margin-right: 8px; }
+.log-entry.success { color: #52c41a; }
+.log-entry.error { color: #ff4d4f; }
+.log-entry.warning { color: #faad14; }
+.log-entry.info { color: #aaa; }
+
+/* ===== Leaflet 自定义 ===== */
+.drone-icon {
+ width: 24px;
+ height: 24px;
+ position: relative;
+}
+
+.drone-icon-inner {
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-bottom: 20px solid #007aff;
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ transform-origin: center 60%;
+}
diff --git a/src/软件前端/index.html b/src/软件前端/index.html
new file mode 100644
index 0000000..fde94cb
--- /dev/null
+++ b/src/软件前端/index.html
@@ -0,0 +1,118 @@
+
+
+
+
+
+ P600 无人机定点巡航控制系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/软件前端/js/map.js b/src/软件前端/js/map.js
new file mode 100644
index 0000000..4cb05c0
--- /dev/null
+++ b/src/软件前端/js/map.js
@@ -0,0 +1,202 @@
+/**
+ * 地图模块 - 基于 Leaflet.js
+ * 负责: 地图初始化、航点管理、航线绘制、无人机位置显示
+ */
+
+const MapModule = (() => {
+ let map = null;
+ let waypoints = []; // 航点数据数组
+ let markers = []; // 航点 Leaflet Marker 对象
+ let polyline = null; // 航线折线
+ let droneMarker = null; // 无人机位置标记
+ let trailLine = null; // 飞行轨迹
+ let trailCoords = []; // 轨迹坐标历史
+
+ const DEFAULT_LAT = 30.0;
+ const DEFAULT_LNG = 120.0;
+ const DEFAULT_ZOOM = 16;
+
+ function init() {
+ map = L.map('map', {
+ center: [DEFAULT_LAT, DEFAULT_LNG],
+ zoom: DEFAULT_ZOOM,
+ zoomControl: true
+ });
+
+ // OpenStreetMap 底图(免费,无需 Key)
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap',
+ maxZoom: 19
+ }).addTo(map);
+
+ // 点击地图添加航点
+ map.on('click', onMapClick);
+
+ // 初始化航线(空折线)
+ polyline = L.polyline([], {
+ color: '#007aff',
+ weight: 2,
+ opacity: 0.7,
+ dashArray: '8, 6'
+ }).addTo(map);
+
+ // 初始化飞行轨迹
+ trailLine = L.polyline([], {
+ color: '#ff4d4f',
+ weight: 2,
+ opacity: 0.6
+ }).addTo(map);
+
+ // 创建无人机图标
+ const droneIcon = L.divIcon({
+ className: 'drone-icon',
+ html: '',
+ iconSize: [24, 24],
+ iconAnchor: [12, 12]
+ });
+
+ droneMarker = L.marker([DEFAULT_LAT, DEFAULT_LNG], {
+ icon: droneIcon,
+ visible: false
+ }).addTo(map);
+ }
+
+ function onMapClick(e) {
+ addWaypoint(e.latlng.lat, e.latlng.lng);
+ }
+
+ function addWaypoint(lat, lng) {
+ const defaultAlt = parseInt(document.getElementById('default-alt').value) || 15;
+ const defaultHold = parseInt(document.getElementById('default-hold').value) || 0;
+ const index = waypoints.length + 1;
+
+ const wp = {
+ id: Date.now(),
+ lat: parseFloat(lat.toFixed(6)),
+ lng: parseFloat(lng.toFixed(6)),
+ alt: defaultAlt,
+ hold_time: defaultHold
+ };
+ waypoints.push(wp);
+
+ // 添加地图标记
+ const marker = L.marker([wp.lat, wp.lng], {
+ draggable: true
+ }).addTo(map);
+
+ marker.bindPopup(`航点 ${index}
高度: ${wp.alt}m
悬停: ${wp.hold_time}s`);
+
+ // 拖拽更新航点坐标
+ marker.on('dragend', function () {
+ const pos = marker.getLatLng();
+ const wpIdx = markers.indexOf(marker);
+ if (wpIdx >= 0) {
+ waypoints[wpIdx].lat = parseFloat(pos.lat.toFixed(6));
+ waypoints[wpIdx].lng = parseFloat(pos.lng.toFixed(6));
+ marker.setPopupContent(`航点 ${wpIdx + 1}
高度: ${waypoints[wpIdx].alt}m`);
+ updatePolyline();
+ UIModule.updateWaypointList();
+ }
+ });
+
+ markers.push(marker);
+ updatePolyline();
+ UIModule.updateWaypointList();
+ UIModule.updateButtons();
+ }
+
+ function removeWaypoint(index) {
+ if (index < 0 || index >= waypoints.length) return;
+
+ waypoints.splice(index, 1);
+ const marker = markers.splice(index, 1)[0];
+ map.removeLayer(marker);
+
+ // 重新编号弹窗
+ markers.forEach((m, i) => {
+ m.setPopupContent(`航点 ${i + 1}
高度: ${waypoints[i].alt}m`);
+ });
+
+ updatePolyline();
+ UIModule.updateWaypointList();
+ UIModule.updateButtons();
+ }
+
+ function clearWaypoints() {
+ markers.forEach(m => map.removeLayer(m));
+ waypoints = [];
+ markers = [];
+ updatePolyline();
+ UIModule.updateWaypointList();
+ UIModule.updateButtons();
+ }
+
+ function updatePolyline() {
+ const coords = waypoints.map(wp => [wp.lat, wp.lng]);
+ polyline.setLatLngs(coords);
+ }
+
+ function updateDronePosition(lat, lng, heading) {
+ if (!droneMarker) return;
+ droneMarker.setLatLng([lat, lng]);
+ if (!droneMarker.visible) {
+ droneMarker.visible = true;
+ }
+
+ // 旋转无人机图标(航向)
+ const arrow = document.getElementById('drone-arrow');
+ if (arrow) {
+ arrow.style.transform = `rotate(${heading}deg)`;
+ }
+
+ // 记录轨迹
+ trailCoords.push([lat, lng]);
+ if (trailCoords.length > 500) trailCoords.shift();
+ trailLine.setLatLngs(trailCoords);
+ }
+
+ function centerOnDrone(lat, lng) {
+ map.setView([lat, lng], map.getZoom());
+ }
+
+ function panTo(lat, lng) {
+ map.panTo([lat, lng]);
+ }
+
+ function getWaypoints() {
+ return waypoints.map(wp => ({
+ lat: wp.lat,
+ lng: wp.lng,
+ alt: wp.alt,
+ hold_time: wp.hold_time
+ }));
+ }
+
+ function getWaypointCount() {
+ return waypoints.length;
+ }
+
+ function updateWaypointAlt(index, alt) {
+ if (index >= 0 && index < waypoints.length) {
+ waypoints[index].alt = alt;
+ markers[index].setPopupContent(
+ `航点 ${index + 1}
高度: ${alt}m
悬停: ${waypoints[index].hold_time}s`
+ );
+ }
+ }
+
+ return {
+ init,
+ addWaypoint,
+ removeWaypoint,
+ clearWaypoints,
+ updateDronePosition,
+ centerOnDrone,
+ panTo,
+ getWaypoints,
+ getWaypointCount,
+ updateWaypointAlt,
+ DEFAULT_LAT,
+ DEFAULT_LNG
+ };
+})();
diff --git a/src/软件前端/js/roslib.min.js b/src/软件前端/js/roslib.min.js
new file mode 100644
index 0000000..506562b
--- /dev/null
+++ b/src/软件前端/js/roslib.min.js
@@ -0,0 +1 @@
+!function n(r,s,o){function a(t,e){if(!s[t]){if(!r[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(c)return c(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}i=s[t]={exports:{}},r[t][0].call(i.exports,function(e){return a(r[t][1][e]||e)},i,i.exports,n,r,s,o)}return s[t].exports}for(var c="function"==typeof require&&require,e=0;e>2),s=0;s>6):(s<55296?n.push(224|s>>12):(s=65536+((s=(1023&s)<<10)|1023&t.charCodeAt(++r)),n.push(240|s>>18),n.push(128|s>>12&63)),n.push(128|s>>6&63)),n.push(128|63&s))}return m(3,n.length),p(n);default:if(Array.isArray(t))for(m(4,o=t.length),r=0;r>5!==e)throw"Invalid indefinite length element";return i}function M(e,t){for(var i=0;i>10),e.push(56320|1023&n))}}"function"!=typeof d&&(d=function(e){return e}),"function"!=typeof y&&(y=function(){return A});var e=function e(){var t,i,n=x(),r=n>>5,n=31&n;if(7==r)switch(n){case 25:var s=new ArrayBuffer(4),s=new DataView(s),o=T(),a=31744&o,c=1023&o;if(31744===a)a=261120;else if(0!==a)a+=114688;else if(0!=c)return c*U;return s.setUint32(0,(32768&o)<<16|a<<13|c<<13),s.getFloat32(0);case 26:return w(g.getFloat32(b),4);case 27:return w(g.getFloat64(b),8)}if((t=S(n))<0&&(r<2||6this._maxListeners&&(l._listeners.warned=!0,f.call(this,l._listeners.length,c))):l._listeners=t,!0;return!0}.call(this,e,t,i):this._events[e]?("function"==typeof this._events[e]&&(this._events[e]=[this._events[e]]),i?this._events[e].unshift(t):this._events[e].push(t),!this._events[e].warned&&0this._maxListeners&&(this._events[e].warned=!0,f.call(this,this._events[e].length,e))):this._events[e]=t,r},r.prototype.off=function(e,t){if("function"!=typeof t)throw new Error("removeListener only takes instances of Function");var i=[];if(this.wildcard){var n="string"==typeof e?e.split(this.delimiter):e.slice();if(!(i=_.call(this,null,n,this.listenerTree,0)))return this}else{if(!this._events[e])return this;o=this._events[e],i.push({_listeners:o})}for(var r=0;rt.secs)&&(e.secs {
+ // 应用状态
+ let droneConnected = false;
+ let missionUploaded = false;
+ let missionRunning = false;
+ let missionPaused = false;
+ let connectionMode = 'sim'; // 'sim' 仿真 | 'real' 实机
+
+ // 连接预设 (rosbridge WebSocket 地址)
+ const PRESETS = {
+ sim: { url: 'ws://localhost:9090', label: '仿真 rosbridge', desc: 'Prometheus SITL 仿真 (本地 rosbridge 端口 9090)' },
+ real: { url: 'ws://192.168.1.31:9090', label: '实机 rosbridge', desc: 'P600 实机 WiFi 数传 (rosbridge 192.168.1.31:9090)' }
+ };
+
+ function init() {
+ // 初始化地图
+ MapModule.init();
+
+ // 初始化 WebSocket
+ WsModule.init();
+
+ // 绑定按钮事件
+ document.getElementById('btn-connect').addEventListener('click', onConnect);
+ document.getElementById('btn-upload').addEventListener('click', onUpload);
+ document.getElementById('btn-start').addEventListener('click', onStart);
+ document.getElementById('btn-pause').addEventListener('click', onPause);
+ document.getElementById('btn-resume').addEventListener('click', onResume);
+ document.getElementById('btn-rth').addEventListener('click', onReturnHome);
+ document.getElementById('btn-land').addEventListener('click', onLand);
+ document.getElementById('btn-clear').addEventListener('click', onClear);
+ document.getElementById('btn-clear-log').addEventListener('click', onClearLog);
+
+ // 绑定连接模式切换
+ document.getElementById('btn-mode-sim').addEventListener('click', () => switchMode('sim'));
+ document.getElementById('btn-mode-real').addEventListener('click', () => switchMode('real'));
+
+ // 连接地址输入框变化时更新标签
+ document.getElementById('drone-url').addEventListener('input', onUrlInput);
+
+ updateButtons();
+ addLog('info', '系统就绪,当前模式: 仿真 (rosbridge)');
+ addLog('info', '提示: 浏览器通过 roslibjs 直接连接 rosbridge_server,无需 MAVSDK 后端');
+ }
+
+ // ---------- 连接模式切换 ----------
+
+ function switchMode(mode) {
+ if (droneConnected) {
+ addLog('warning', '请先断开当前连接再切换模式');
+ return;
+ }
+ connectionMode = mode;
+ const preset = PRESETS[mode];
+
+ // 更新按钮状态
+ document.getElementById('btn-mode-sim').classList.toggle('active', mode === 'sim');
+ document.getElementById('btn-mode-real').classList.toggle('active', mode === 'real');
+
+ // 自动填入预设地址
+ document.getElementById('drone-url').value = preset.url;
+ document.getElementById('conn-mode-tag').textContent = preset.label;
+
+ addLog('info', `已切换到${preset.label}模式: ${preset.desc}`);
+
+ if (mode === 'sim') {
+ addLog('info', '仿真模式说明: 先启动 Prometheus SITL (roslaunch prometheus_gazebo sitl.launch),再启动 rosbridge (roslaunch rosbridge_server rosbridge_websocket.launch)');
+ } else {
+ addLog('info', '实机模式说明: 请确保电脑已连接 P600 的 WiFi 数传,rosbridge 需在机载电脑上运行');
+ }
+ }
+
+ function onUrlInput() {
+ const url = document.getElementById('drone-url').value.trim();
+ const tag = document.getElementById('conn-mode-tag');
+ if (url.startsWith('ws://')) {
+ tag.textContent = 'rosbridge';
+ } else if (url.startsWith('wss://')) {
+ tag.textContent = 'rosbridge (SSL)';
+ } else {
+ tag.textContent = '未知';
+ }
+ }
+
+ // ---------- 按钮事件处理 ----------
+
+ function onConnect() {
+ const url = document.getElementById('drone-url').value.trim();
+ if (!url) {
+ addLog('error', '请输入 rosbridge 连接地址');
+ return;
+ }
+ if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
+ addLog('error', '地址格式错误,应以 ws:// 开头 (例如 ws://localhost:9090)');
+ return;
+ }
+ WsModule.connectDrone(url);
+ }
+
+ function onUpload() {
+ const waypoints = MapModule.getWaypoints();
+ if (waypoints.length === 0) {
+ addLog('error', '请先在地图上添加航点');
+ return;
+ }
+ WsModule.uploadMission(waypoints);
+ }
+
+ function onStart() {
+ WsModule.startMission();
+ }
+
+ function onPause() {
+ WsModule.pauseMission();
+ }
+
+ function onResume() {
+ WsModule.resumeMission();
+ }
+
+ function onReturnHome() {
+ WsModule.returnHome();
+ }
+
+ function onLand() {
+ WsModule.land();
+ }
+
+ function onClear() {
+ MapModule.clearWaypoints();
+ missionUploaded = false;
+ updateButtons();
+ addLog('info', '已清除所有航点');
+ }
+
+ function onClearLog() {
+ document.getElementById('log-content').innerHTML = '';
+ }
+
+ // ---------- 状态回调(由 WsModule 触发) ----------
+
+ function onTelemetry(data) {
+ if (data.type === 'telemetry') {
+ // 更新顶栏数据
+ document.getElementById('battery').textContent = data.battery.toFixed(0) + '%';
+ document.getElementById('speed').textContent = data.speed.toFixed(1) + ' m/s';
+ document.getElementById('altitude').textContent = data.alt.toFixed(1) + ' m';
+ document.getElementById('gps-status').textContent = data.gps_status;
+ document.getElementById('flight-mode').textContent = data.flight_mode;
+
+ // 更新右侧数据面板
+ document.getElementById('data-lat').textContent = data.lat.toFixed(6);
+ document.getElementById('data-lng').textContent = data.lng.toFixed(6);
+ document.getElementById('data-heading').textContent = data.heading.toFixed(0) + '°';
+ document.getElementById('data-wp').textContent = data.current_wp;
+
+ // 更新无人机位置 — 只要坐标有效就更新
+ if (droneConnected && (data.lat !== 0 || data.alt > 0.1)) {
+ MapModule.updateDronePosition(data.lat, data.lng, data.heading);
+ }
+ }
+
+ if (data.type === 'mission_progress') {
+ document.getElementById('data-wp').textContent =
+ data.current + ' / ' + data.total;
+ }
+ }
+
+ function onStatus(data) {
+ addLog(data.type, data.message);
+
+ // 根据状态消息更新应用状态
+ const msg = data.message;
+
+ if (msg.includes('连接成功') || msg.includes('已连接')) {
+ droneConnected = true;
+ document.getElementById('conn-status').className = 'tag tag-success';
+ document.getElementById('conn-status').textContent = '已连接';
+ document.getElementById('btn-connect').textContent = '已连接';
+ document.getElementById('btn-connect').disabled = true;
+ updateButtons();
+ }
+
+ if (msg.includes('上传成功')) {
+ missionUploaded = true;
+ updateButtons();
+ }
+
+ if (msg.includes('任务已开始')) {
+ missionRunning = true;
+ missionPaused = false;
+ updateButtons();
+ }
+
+ if (msg.includes('已暂停')) {
+ missionRunning = false;
+ missionPaused = true;
+ updateButtons();
+ }
+
+ if (msg.includes('已继续')) {
+ missionRunning = true;
+ missionPaused = false;
+ updateButtons();
+ }
+
+ if (msg.includes('返航') || msg.includes('降落') || msg.includes('失败')) {
+ if (msg.includes('返航') || msg.includes('降落')) {
+ missionRunning = false;
+ missionPaused = false;
+ updateButtons();
+ }
+ }
+
+ if (msg.includes('任务完成')) {
+ missionRunning = false;
+ missionPaused = false;
+ missionUploaded = false;
+ updateButtons();
+ }
+ }
+
+ function onDroneDisconnected() {
+ droneConnected = false;
+ missionUploaded = false;
+ missionRunning = false;
+ missionPaused = false;
+ document.getElementById('conn-status').className = 'tag tag-danger';
+ document.getElementById('conn-status').textContent = '未连接';
+ document.getElementById('btn-connect').textContent = '连接无人机';
+ document.getElementById('btn-connect').disabled = false;
+ updateButtons();
+ addLog('error', '无人机连接断开');
+ }
+
+ // ---------- 按钮状态管理 ----------
+
+ function updateButtons() {
+ const wpCount = MapModule.getWaypointCount();
+
+ setBtn('btn-upload', droneConnected && wpCount > 0 && !missionRunning && !missionPaused);
+ setBtn('btn-start', droneConnected && missionUploaded && !missionRunning && !missionPaused);
+ setBtn('btn-pause', droneConnected && missionRunning);
+ setBtn('btn-resume', droneConnected && missionPaused);
+ setBtn('btn-rth', droneConnected);
+ setBtn('btn-land', droneConnected);
+ }
+
+ function setBtn(id, enabled) {
+ document.getElementById(id).disabled = !enabled;
+ }
+
+ // ---------- 航点列表渲染 ----------
+
+ function updateWaypointList() {
+ const container = document.getElementById('waypoint-list');
+ const waypoints = MapModule.getWaypoints();
+
+ document.getElementById('wp-count').textContent = waypoints.length + ' 个航点';
+
+ if (waypoints.length === 0) {
+ container.innerHTML = '在地图上点击以添加航点
';
+ return;
+ }
+
+ container.innerHTML = waypoints.map((wp, i) => `
+
+
${i + 1}
+
+
${wp.lat}, ${wp.lng}
+
${wp.alt}m | 悬停 ${wp.hold_time}s
+
+
+
+ `).join('');
+
+ // 绑定点击定位事件
+ container.querySelectorAll('.wp-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ if (e.target.classList.contains('wp-delete')) return;
+ const idx = parseInt(item.dataset.index);
+ const wps = MapModule.getWaypoints();
+ if (wps[idx]) {
+ MapModule.panTo(wps[idx].lat, wps[idx].lng);
+ }
+ });
+ });
+
+ // 绑定删除事件
+ container.querySelectorAll('.wp-delete').forEach(btn => {
+ btn.addEventListener('click', () => {
+ MapModule.removeWaypoint(parseInt(btn.dataset.index));
+ });
+ });
+ }
+
+ // ---------- 日志输出 ----------
+
+ function addLog(type, message) {
+ const container = document.getElementById('log-content');
+ const now = new Date();
+ const time = now.toTimeString().split(' ')[0];
+
+ const entry = document.createElement('div');
+ entry.className = 'log-entry ' + type;
+ entry.innerHTML = `[${time}]${message}`;
+ container.appendChild(entry);
+
+ // 自动滚动到底部
+ container.scrollTop = container.scrollHeight;
+
+ // 限制日志条数
+ while (container.children.length > 200) {
+ container.removeChild(container.firstChild);
+ }
+ }
+
+ return {
+ init,
+ onTelemetry,
+ onStatus,
+ onDroneDisconnected,
+ updateWaypointList,
+ updateButtons
+ };
+})();
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', () => {
+ UIModule.init();
+});
diff --git a/src/软件前端/js/websocket.js b/src/软件前端/js/websocket.js
new file mode 100644
index 0000000..1fc3151
--- /dev/null
+++ b/src/软件前端/js/websocket.js
@@ -0,0 +1,683 @@
+/**
+ * MAVROS 通信模块 - 基于 roslibjs
+ * 直接通过 MAVROS 话题和服务控制无人机
+ *
+ * 架构: 浏览器 ──roslibjs──► rosbridge_server ──ROS话题──► MAVROS ──MAVLink──► PX4
+ *
+ * 使用的话题:
+ * 订阅: /uav1/mavros/state (mavros_msgs/State)
+ * 订阅: /uav1/mavros/local_position/pose (geometry_msgs/PoseStamped)
+ * 订阅: /uav1/mavros/global_position/raw/fix (sensor_msgs/NavSatFix)
+ * 订阅: /uav1/mavros/battery (sensor_msgs/BatteryState)
+ * 发布: /uav1/mavros/setpoint_position/local (geometry_msgs/PoseStamped)
+ * 服务: /uav1/mavros/cmd/arming (mavros_msgs/CommandBool)
+ * 服务: /uav1/mavros/set_mode (mavros_msgs/SetMode)
+ */
+
+const WsModule = (() => {
+ let ros = null;
+ let rosConnected = false;
+ let droneConnected = false;
+ let fcuReady = false; // true when MAVROS reports valid mode (FCU connection established)
+
+ // ========== 配置: UAV 命名空间 ==========
+ // 仿真环境默认: '/uav1' 实机可能: '' 或 '/uav1'
+ // 如果话题不对,改这里
+ let uavNS = '/uav1';
+
+ function topicName(name) { return uavNS + name; }
+ function serviceName(name) { return uavNS + name; }
+
+ // ROS handles
+ let stateSub = null;
+ let localPosSub = null;
+ let gpsSub = null;
+ let batterySub = null;
+ let setpointPub = null;
+
+ // Telemetry state
+ let telemetry = {
+ lat: 0, lng: 0, alt: 0,
+ speed: 0, battery: 100, heading: 0,
+ flightMode: 'UNKNOWN', gpsStatus: 'No Fix',
+ armed: false,
+ localPos: { x: 0, y: 0, z: 0 } // ENU: x=East, y=North, z=Up
+ };
+
+ // Origin for lat/lng <-> ENU conversion
+ let origin = { lat: null, lng: null };
+ let originSet = false;
+ let originFromGps = false; // true if origin was set from real GPS
+
+ // Navigation state
+ let rawWaypoints = []; // Original lat/lng waypoints
+ let navWaypoints = []; // Converted ENU waypoints
+ let currentWPIndex = 0;
+ let navActive = false;
+ let navPaused = false;
+ let publishTimer = null;
+ let wpCheckTimer = null;
+ let telemetryTimer = null; // Throttled UI update timer
+
+ const WP_REACH_DIST = 2.0; // meters
+ const PUBLISH_INTERVAL = 100; // ms (10 Hz, OFFBOARD needs >= 2Hz)
+
+ // ---- Coordinate conversion ----
+
+ function latLngToENU(lat, lng, alt) {
+ if (!originSet) return { x: 0, y: 0, z: alt };
+ const latRad = origin.lat * Math.PI / 180;
+ const x = (lng - origin.lng) * Math.cos(latRad) * 111319.488; // East
+ const y = (lat - origin.lat) * 111319.488; // North
+ return { x, y, z: alt }; // Up
+ }
+
+ function enuToLatLng(x, y) {
+ if (!originSet) return { lat: 0, lng: 0 };
+ const latRad = origin.lat * Math.PI / 180;
+ const lat = origin.lat + y / 111319.488;
+ const lng = origin.lng + x / (111319.488 * Math.cos(latRad));
+ return { lat, lng };
+ }
+
+ // Quaternion to yaw (degrees, ENU: 0=East, CCW+)
+ function quatToYawDeg(qx, qy, qz, qw) {
+ const siny = 2 * (qw * qz + qx * qy);
+ const cosy = 1 - 2 * (qy * qy + qz * qz);
+ let yaw = Math.atan2(siny, cosy) * 180 / Math.PI;
+ // Convert to compass heading: 0=North, CW+
+ return (90 - yaw + 360) % 360;
+ }
+
+ // ---- Init & Connection ----
+
+ function init() {}
+
+ function isConnected() { return rosConnected; }
+
+ function connectDrone(url) {
+ try {
+ if (typeof ROSLIB === 'undefined') {
+ UIModule.onStatus({ type: 'error', message: 'roslibjs 库未加载' });
+ return;
+ }
+
+ if (ros) {
+ try { ros.close(); } catch (e) { /* ignore */ }
+ }
+
+ UIModule.onStatus({ type: 'info', message: `正在连接 rosbridge: ${url} ...` });
+
+ ros = new ROSLIB.Ros({ url: url });
+
+ ros.on('connection', () => {
+ rosConnected = true;
+ UIModule.onStatus({ type: 'info', message: 'rosbridge 已连接,正在检测 MAVROS 话题...' });
+ setupSubscriptions();
+ });
+
+ ros.on('error', (error) => {
+ UIModule.onStatus({ type: 'error', message: `rosbridge 连接错误: ${error || '未知'}` });
+ });
+
+ ros.on('close', () => {
+ const wasConnected = rosConnected;
+ rosConnected = false;
+ droneConnected = false;
+ stopAll();
+ if (wasConnected) {
+ UIModule.onDroneDisconnected();
+ }
+ });
+ } catch (e) {
+ UIModule.onStatus({ type: 'error', message: `连接失败: ${e.message || e}` });
+ }
+ }
+
+ // ---- Topic subscriptions ----
+
+ function setupSubscriptions() {
+ // Try to detect which namespace has MAVROS
+ trySubscribesWithFallback();
+ }
+
+ // Set a default origin for coordinate conversion (no GPS needed)
+ function setDefaultOrigin() {
+ if (originSet) return;
+ // Use map default center as origin - simulation doesn't need real GPS
+ origin.lat = MapModule.DEFAULT_LAT || 30.0;
+ origin.lng = MapModule.DEFAULT_LNG || 120.0;
+ originSet = true;
+ originFromGps = false;
+ UIModule.onStatus({ type: 'info', message: `仿真模式: 使用默认坐标原点 (${origin.lat}, ${origin.lng}),无需GPS` });
+ }
+
+ function trySubscribesWithFallback() {
+ let gotState = false;
+ const tryNS = uavNS;
+
+ // Subscribe to MAVROS state
+ stateSub = new ROSLIB.Topic({
+ ros: ros,
+ name: topicName('/mavros/state'),
+ messageType: 'mavros_msgs/State'
+ });
+
+ const stateTimeout = setTimeout(() => {
+ if (!gotState) {
+ // Try without namespace
+ if (tryNS !== '') {
+ uavNS = '';
+ UIModule.onStatus({ type: 'warning', message: `未收到 ${tryNS}/mavros/state,尝试无命名空间...` });
+ trySubscribesWithFallback();
+ } else {
+ UIModule.onStatus({ type: 'error', message: '未检测到 MAVROS 话题,请确认仿真/实机已启动' });
+ }
+ }
+ }, 5000);
+
+ stateSub.subscribe((msg) => {
+ clearTimeout(stateTimeout);
+ gotState = true;
+
+ telemetry.armed = msg.armed || false;
+ telemetry.flightMode = msg.mode || 'UNKNOWN';
+
+ // Check if FCU is truly connected (has valid mode)
+ const modeValid = msg.connected && msg.mode && msg.mode.length > 0;
+
+ if (!droneConnected) {
+ droneConnected = true;
+ if (modeValid) {
+ fcuReady = true;
+ UIModule.onStatus({ type: 'success', message: `无人机已连接 (模式: ${msg.mode}, ${msg.armed ? '已解锁' : '未解锁'})` });
+ } else {
+ UIModule.onStatus({ type: 'warning', message: `MAVROS 已连接,但飞控未就绪 (mode=空)。等待 PX4 初始化...` });
+ }
+ setDefaultOrigin();
+ }
+
+ // Detect when FCU becomes ready
+ if (!fcuReady && modeValid) {
+ fcuReady = true;
+ UIModule.onStatus({ type: 'success', message: `飞控已就绪! 当前模式: ${msg.mode}, 状态: ${msg.armed ? '已解锁' : '未解锁'}` });
+ }
+ });
+
+ // Local position (ENU: x=East, y=North, z=Up)
+ localPosSub = new ROSLIB.Topic({
+ ros: ros,
+ name: topicName('/mavros/local_position/pose'),
+ messageType: 'geometry_msgs/PoseStamped'
+ });
+
+ localPosSub.subscribe((msg) => {
+ if (msg.pose && msg.pose.position) {
+ telemetry.localPos = {
+ x: msg.pose.position.x, // East
+ y: msg.pose.position.y, // North
+ z: msg.pose.position.z // Up
+ };
+ }
+ if (msg.pose && msg.pose.orientation) {
+ const o = msg.pose.orientation;
+ telemetry.heading = quatToYawDeg(o.x, o.y, o.z, o.w);
+ }
+ });
+
+ // GPS (for lat/lng display and origin)
+ gpsSub = new ROSLIB.Topic({
+ ros: ros,
+ name: topicName('/mavros/global_position/raw/fix'),
+ messageType: 'sensor_msgs/NavSatFix'
+ });
+
+ gpsSub.subscribe((msg) => {
+ telemetry.lat = msg.latitude || 0;
+ telemetry.lng = msg.longitude || 0;
+
+ if (msg.status) {
+ telemetry.gpsStatus = msg.status.status >= 0 ? '3D Fix' : 'No Fix';
+ }
+
+ // Update origin from real GPS (overrides default origin)
+ if (msg.latitude !== 0 && Math.abs(msg.latitude) > 1) {
+ if (!originFromGps) {
+ origin.lat = msg.latitude;
+ origin.lng = msg.longitude;
+ originSet = true;
+ originFromGps = true;
+ UIModule.onStatus({ type: 'info', message: `GPS 坐标已更新: ${origin.lat.toFixed(6)}, ${origin.lng.toFixed(6)}` });
+ MapModule.centerOnDrone(msg.latitude, msg.longitude);
+ }
+ }
+ });
+
+ // Battery
+ batterySub = new ROSLIB.Topic({
+ ros: ros,
+ name: topicName('/mavros/battery'),
+ messageType: 'sensor_msgs/BatteryState'
+ });
+
+ batterySub.subscribe((msg) => {
+ telemetry.battery = (msg.percentage != null) ? msg.percentage * 100 : 100;
+ });
+
+ // Setpoint publisher — PoseStamped is the standard MAVROS position control interface
+ setpointPub = new ROSLIB.Topic({
+ ros: ros,
+ name: topicName('/mavros/setpoint_position/local'),
+ messageType: 'geometry_msgs/PoseStamped'
+ });
+
+ // Start throttled telemetry loop (5Hz, decoupled from subscription rate)
+ startTelemetryLoop();
+ }
+
+ function pushTelemetry() {
+ let displayLat = telemetry.lat;
+ let displayLng = telemetry.lng;
+ if ((!displayLat || displayLat === 0) && originSet) {
+ const ll = enuToLatLng(telemetry.localPos.x, telemetry.localPos.y);
+ displayLat = ll.lat;
+ displayLng = ll.lng;
+ }
+
+ UIModule.onTelemetry({
+ type: 'telemetry',
+ lat: displayLat,
+ lng: displayLng,
+ alt: telemetry.localPos.z,
+ speed: telemetry.speed,
+ battery: telemetry.battery,
+ heading: telemetry.heading,
+ flight_mode: telemetry.flightMode,
+ gps_status: telemetry.gpsStatus,
+ current_wp: (currentWPIndex + 1) + '/' + navWaypoints.length
+ });
+ }
+
+ function startTelemetryLoop() {
+ if (telemetryTimer) clearInterval(telemetryTimer);
+ pushTelemetry();
+ telemetryTimer = setInterval(pushTelemetry, 200); // 5Hz UI update
+ }
+
+ function stopTelemetryLoop() {
+ if (telemetryTimer) { clearInterval(telemetryTimer); telemetryTimer = null; }
+ }
+
+ // ---- Setpoint publishing ----
+
+ let spSeq = 0;
+
+ function publishSetpoint(east, north, up) {
+ if (!setpointPub || !rosConnected) return;
+
+ const msg = new ROSLIB.Message({
+ header: {
+ seq: spSeq++,
+ stamp: { secs: Math.floor(Date.now() / 1000), nsecs: 0 },
+ frame_id: 'map'
+ },
+ pose: {
+ position: { x: east, y: north, z: up },
+ orientation: { x: 0, y: 0, z: 0, w: 1 }
+ }
+ });
+
+ setpointPub.publish(msg);
+ }
+
+ function startPublishLoop(east, north, up) {
+ stopPublishing();
+ publishSetpoint(east, north, up);
+ publishTimer = setInterval(() => {
+ publishSetpoint(east, north, up);
+ }, PUBLISH_INTERVAL);
+ }
+
+ function stopPublishing() {
+ if (publishTimer) { clearInterval(publishTimer); publishTimer = null; }
+ if (wpCheckTimer) { clearInterval(wpCheckTimer); wpCheckTimer = null; }
+ }
+
+ function stopAll() {
+ stopPublishing();
+ stopTelemetryLoop();
+ }
+
+ // ---- ROS Service calls ----
+
+ // Simple one-shot versions for arm/disarm/land buttons
+
+ // ---- Mission operations ----
+
+ function uploadMission(waypoints) {
+ if (waypoints.length === 0) {
+ UIModule.onStatus({ type: 'error', message: '航点列表为空' });
+ return;
+ }
+
+ // Store raw waypoints
+ rawWaypoints = waypoints;
+ currentWPIndex = 0;
+
+ // Convert to ENU (origin should always be set by now)
+ if (originSet) {
+ convertWaypoints();
+ UIModule.onStatus({ type: 'success', message: `任务上传成功,共 ${navWaypoints.length} 个航点` });
+ } else {
+ // Fallback (shouldn't happen, but just in case)
+ UIModule.onStatus({ type: 'success', message: `已暂存 ${waypoints.length} 个航点` });
+ }
+ }
+
+ function convertWaypoints() {
+ if (!originSet || !rawWaypoints || rawWaypoints.length === 0) return false;
+ navWaypoints = rawWaypoints.map(wp => {
+ const enu = latLngToENU(wp.lat, wp.lng, wp.alt);
+ return { ...enu, hold_time: wp.hold_time || 0 };
+ });
+ return true;
+ }
+
+ function startMission() {
+ if (!rosConnected || !droneConnected) {
+ UIModule.onStatus({ type: 'error', message: '未连接无人机' });
+ return;
+ }
+ if (!fcuReady) {
+ UIModule.onStatus({ type: 'error', message: '飞控未就绪 (MAVROS 模式为空)。请等待 PX4 完全启动后重试。也可尝试在终端执行: rosservice call ' + serviceName('/mavros/set_mode') + ' "{base_mode: 0, custom_mode: \'OFFBOARD\'}" 验证模式切换是否正常' });
+ return;
+ }
+ if (rawWaypoints.length === 0) {
+ UIModule.onStatus({ type: 'error', message: '请先上传任务' });
+ return;
+ }
+
+ // Convert waypoints
+ if (navWaypoints.length === 0 && rawWaypoints.length > 0) {
+ if (!originSet) setDefaultOrigin();
+ convertWaypoints();
+ }
+ if (navWaypoints.length === 0) {
+ UIModule.onStatus({ type: 'error', message: '航点转换失败,请重新上传任务' });
+ return;
+ }
+
+ // Step 1: Start sending takeoff setpoints at target altitude
+ // PX4 requires setpoints BEFORE switching OFFBOARD, and altitude must be positive
+ const firstWP = navWaypoints[0];
+ const startX = telemetry.localPos.x || 0;
+ const startY = telemetry.localPos.y || 0;
+ const takeoffAlt = firstWP.z;
+ startPublishLoop(startX, startY, takeoffAlt);
+ UIModule.onStatus({ type: 'info', message: '正在发送位置设定值 (10Hz)...' });
+
+ // Step 2: Wait for setpoints to flow, then switch OFFBOARD
+ setTimeout(() => {
+ callSetModeWithRetry('OFFBOARD', 3, (ok) => {
+ if (!ok) {
+ stopPublishing();
+ UIModule.onStatus({ type: 'error', message: 'OFFBOARD 模式切换失败,任务中止。请在终端验证: rosservice call ' + serviceName('/mavros/set_mode') + ' "{base_mode: 0, custom_mode: \'OFFBOARD\'}"' });
+ return;
+ }
+
+ // Step 3: Arm
+ setTimeout(() => {
+ callArmingWithRetry(true, 3, (ok) => {
+ if (!ok) {
+ stopPublishing();
+ UIModule.onStatus({ type: 'error', message: '电机解锁失败,任务中止。请检查: 1) 传感器是否校准 2) GPS是否定位 3) 安全检查是否通过' });
+ return;
+ }
+
+ // Step 4: Wait for takeoff, then navigate
+ setTimeout(() => {
+ UIModule.onStatus({ type: 'info', message: '正在起飞...' });
+
+ // Check if drone reached takeoff altitude
+ let takeoffDone = false;
+ const takeoffCheck = setInterval(() => {
+ const curZ = telemetry.localPos.z || 0;
+ if (curZ > takeoffAlt - 1.0 && curZ > 0.5) {
+ clearInterval(takeoffCheck);
+ takeoffDone = true;
+ navActive = true;
+ navPaused = false;
+ UIModule.onStatus({ type: 'success', message: '起飞完成,开始巡航' });
+ navigateToWaypoint(0);
+ }
+ }, 500);
+
+ // Fallback: start navigation after 15s regardless
+ setTimeout(() => {
+ clearInterval(takeoffCheck);
+ if (!takeoffDone) {
+ navActive = true;
+ navPaused = false;
+ UIModule.onStatus({ type: 'info', message: '任务已开始执行' });
+ navigateToWaypoint(0);
+ }
+ }, 15000);
+ }, 1000);
+ });
+ }, 500);
+ });
+ }, 3000);
+ }
+
+ // Retry wrappers for service calls
+ function callSetModeWithRetry(mode, retries, callback) {
+ UIModule.onStatus({ type: 'info', message: `正在切换 ${mode} 模式...` });
+ const service = new ROSLIB.Service({
+ ros: ros,
+ name: serviceName('/mavros/set_mode'),
+ serviceType: 'mavros_msgs/SetMode'
+ });
+ service.callService(
+ new ROSLIB.ServiceRequest({ base_mode: 0, custom_mode: mode }),
+ (result) => {
+ if (result && result.mode_sent) {
+ UIModule.onStatus({ type: 'success', message: `模式已切换: ${mode}` });
+ callback(true);
+ } else if (retries > 0) {
+ UIModule.onStatus({ type: 'warning', message: `模式切换未成功,剩余重试 ${retries} 次...` });
+ setTimeout(() => callSetModeWithRetry(mode, retries - 1, callback), 2000);
+ } else {
+ UIModule.onStatus({ type: 'error', message: `模式切换失败: ${JSON.stringify(result)}` });
+ callback(false);
+ }
+ },
+ (error) => {
+ if (retries > 0) {
+ UIModule.onStatus({ type: 'warning', message: `模式切换服务错误,重试中...` });
+ setTimeout(() => callSetModeWithRetry(mode, retries - 1, callback), 2000);
+ } else {
+ UIModule.onStatus({ type: 'error', message: `模式切换服务失败: ${error || '不可用'}` });
+ callback(false);
+ }
+ }
+ );
+ }
+
+ function callArmingWithRetry(arm, retries, callback) {
+ UIModule.onStatus({ type: 'info', message: arm ? '正在解锁电机...' : '正在上锁电机...' });
+ const service = new ROSLIB.Service({
+ ros: ros,
+ name: serviceName('/mavros/cmd/arming'),
+ serviceType: 'mavros_msgs/CommandBool'
+ });
+ service.callService(
+ new ROSLIB.ServiceRequest({ value: arm }),
+ (result) => {
+ if (result && result.success) {
+ UIModule.onStatus({ type: 'success', message: arm ? '电机已解锁' : '电机已上锁' });
+ callback(true);
+ } else if (retries > 0) {
+ UIModule.onStatus({ type: 'warning', message: `${arm ? '解锁' : '上锁'}未成功 (result=${result.result}),剩余重试 ${retries} 次...` });
+ setTimeout(() => callArmingWithRetry(arm, retries - 1, callback), 2000);
+ } else {
+ UIModule.onStatus({ type: 'error', message: `${arm ? '解锁' : '上锁'}失败: result=${result ? result.result : 'N/A'}` });
+ callback(false);
+ }
+ },
+ (error) => {
+ if (retries > 0) {
+ UIModule.onStatus({ type: 'warning', message: `${arm ? '解锁' : '上锁'}服务错误,重试中...` });
+ setTimeout(() => callArmingWithRetry(arm, retries - 1, callback), 2000);
+ } else {
+ UIModule.onStatus({ type: 'error', message: `${arm ? '解锁' : '上锁'}服务失败` });
+ callback(false);
+ }
+ }
+ );
+ }
+
+ // Simple one-shot mode switch (no retry, used by returnHome/land)
+ function callSetMode(mode) {
+ callSetModeWithRetry(mode, 1, () => {});
+ }
+
+ function navigateToWaypoint(index) {
+ if (!navActive || navPaused) return;
+
+ if (index >= navWaypoints.length) {
+ UIModule.onStatus({ type: 'success', message: '所有航点已到达,任务完成' });
+ const p = telemetry.localPos;
+ startPublishLoop(p.x, p.y, p.z);
+ navActive = false;
+ return;
+ }
+
+ currentWPIndex = index;
+ const wp = navWaypoints[index];
+
+ UIModule.onStatus({ type: 'info', message: `飞往航点 ${index + 1}/${navWaypoints.length} (${wp.x.toFixed(1)}, ${wp.y.toFixed(1)}, ${wp.z.toFixed(1)}m)` });
+ startPublishLoop(wp.x, wp.y, wp.z);
+ checkWaypointReached(index);
+ }
+
+ function checkWaypointReached(index) {
+ if (wpCheckTimer) clearInterval(wpCheckTimer);
+
+ const wpTimeout = setTimeout(() => {
+ // Fallback: if waypoint not reached in 30s, advance anyway
+ if (navActive && !navPaused) {
+ UIModule.onStatus({ type: 'warning', message: `航点 ${index + 1} 到达超时,跳至下一航点` });
+ navigateToWaypoint(index + 1);
+ }
+ }, 30000);
+
+ wpCheckTimer = setInterval(() => {
+ if (!navActive || navPaused) {
+ clearInterval(wpCheckTimer);
+ clearTimeout(wpTimeout);
+ return;
+ }
+
+ const wp = navWaypoints[index];
+ if (!wp) { clearTimeout(wpTimeout); return; }
+
+ const p = telemetry.localPos;
+ const dx = p.x - wp.x;
+ const dy = p.y - wp.y;
+ const dz = p.z - wp.z;
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ if (dist < WP_REACH_DIST) {
+ clearInterval(wpCheckTimer);
+ clearTimeout(wpTimeout);
+ wpCheckTimer = null;
+
+ UIModule.onTelemetry({
+ type: 'mission_progress',
+ current: index + 1,
+ total: navWaypoints.length
+ });
+
+ const holdTime = wp.hold_time || 0;
+ if (holdTime > 0) {
+ UIModule.onStatus({ type: 'info', message: `航点 ${index + 1} 已到达,悬停 ${holdTime} 秒` });
+ setTimeout(() => {
+ if (navActive && !navPaused) navigateToWaypoint(index + 1);
+ }, holdTime * 1000);
+ } else {
+ navigateToWaypoint(index + 1);
+ }
+ }
+ }, 500);
+ }
+
+ function pauseMission() {
+ if (!navActive || navPaused) return;
+ navPaused = true;
+ if (wpCheckTimer) { clearInterval(wpCheckTimer); wpCheckTimer = null; }
+ const p = telemetry.localPos;
+ startPublishLoop(p.x, p.y, p.z);
+ UIModule.onStatus({ type: 'info', message: '任务已暂停(悬停中)' });
+ }
+
+ function resumeMission() {
+ if (!navActive || !navPaused) return;
+ navPaused = false;
+ navigateToWaypoint(currentWPIndex);
+ UIModule.onStatus({ type: 'info', message: '任务已继续' });
+ }
+
+ function returnHome() {
+ stopPublishing();
+ navActive = false;
+ navPaused = false;
+
+ UIModule.onStatus({ type: 'warning', message: '正在返航...' });
+
+ // Fly to origin at current altitude
+ startPublishLoop(0, 0, telemetry.localPos.z);
+
+ const homeCheck = setInterval(() => {
+ const p = telemetry.localPos;
+ const dist = Math.sqrt(p.x * p.x + p.y * p.y);
+ if (dist < WP_REACH_DIST) {
+ clearInterval(homeCheck);
+ // Switch to AUTO.LAND for safe landing
+ callSetMode('AUTO.LAND');
+ setTimeout(() => stopPublishing(), 3000);
+ UIModule.onStatus({ type: 'info', message: '已到达起飞点上方,正在降落...' });
+ }
+ }, 500);
+ }
+
+ function land() {
+ stopPublishing();
+ navActive = false;
+ navPaused = false;
+ // Use PX4 AUTO.LAND mode (safest)
+ callSetMode('AUTO.LAND');
+ UIModule.onStatus({ type: 'warning', message: '正在降落 (AUTO.LAND)...' });
+ }
+
+ function arm() {
+ callArmingWithRetry(true, 2, () => {});
+ }
+
+ function disarm() {
+ callArmingWithRetry(false, 2, () => {});
+ }
+
+ return {
+ init,
+ isConnected,
+ connectDrone,
+ uploadMission,
+ startMission,
+ pauseMission,
+ resumeMission,
+ returnHome,
+ land,
+ arm,
+ disarm
+ };
+})();
diff --git a/src/软件前端/server/__pycache__/drone_manager.cpython-313.pyc b/src/软件前端/server/__pycache__/drone_manager.cpython-313.pyc
new file mode 100644
index 0000000..96ce6fc
Binary files /dev/null and b/src/软件前端/server/__pycache__/drone_manager.cpython-313.pyc differ
diff --git a/src/软件前端/server/app.py b/src/软件前端/server/app.py
new file mode 100644
index 0000000..17d8d13
--- /dev/null
+++ b/src/软件前端/server/app.py
@@ -0,0 +1,39 @@
+"""
+Flask 静态文件服务器 - 托管前端页面
+浏览器通过 roslibjs 直接连接 rosbridge_server,无需 Python 后端处理无人机逻辑
+"""
+
+import os
+from flask import Flask, send_from_directory
+
+# 项目根目录(server 的上级目录)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+app = Flask(__name__, static_folder=BASE_DIR)
+
+
+@app.route("/")
+def index():
+ return send_from_directory(BASE_DIR, "index.html")
+
+
+@app.route("/css/")
+def css_files(filename):
+ return send_from_directory(os.path.join(BASE_DIR, "css"), filename)
+
+
+@app.route("/js/")
+def js_files(filename):
+ return send_from_directory(os.path.join(BASE_DIR, "js"), filename)
+
+
+if __name__ == "__main__":
+ print("=" * 50)
+ print(" P600 无人机定点巡航控制系统")
+ print(" 打开浏览器访问: http://localhost:5000")
+ print()
+ print(" 架构: 浏览器 ──roslibjs──► rosbridge_server ──ROS──► PX4")
+ print(" rosbridge 地址默认: ws://localhost:9090 (仿真)")
+ print(" ws://192.168.1.31:9090 (实机)")
+ print("=" * 50)
+ app.run(host="0.0.0.0", port=5000, debug=False)
diff --git a/src/软件前端/server/drone_manager.py b/src/软件前端/server/drone_manager.py
new file mode 100644
index 0000000..46db332
--- /dev/null
+++ b/src/软件前端/server/drone_manager.py
@@ -0,0 +1,7 @@
+# 此文件已弃用 - 新架构通过 roslibjs + rosbridge_server 直接在浏览器端控制无人机
+# 无需 Python 后端处理无人机逻辑
+#
+# 新架构:
+# 浏览器 (roslibjs) ──WebSocket──► rosbridge_server ──ROS话题──► Prometheus/MAVROS ──► PX4
+#
+# 如需参考旧版 MAVSDK 实现,请查看 git 历史
diff --git a/src/软件前端/server/requirements.txt b/src/软件前端/server/requirements.txt
new file mode 100644
index 0000000..5bd19d3
--- /dev/null
+++ b/src/软件前端/server/requirements.txt
@@ -0,0 +1 @@
+flask==3.0.0
diff --git a/src/软件前端/使用说明.md b/src/软件前端/使用说明.md
new file mode 100644
index 0000000..799d213
--- /dev/null
+++ b/src/软件前端/使用说明.md
@@ -0,0 +1,310 @@
+# P600 无人机定点巡航控制系统 - 使用说明
+
+## 系统简介
+
+本系统是一个基于 Web 的 P600 无人机定点巡航控制前端,支持在地图上选择航点并让无人机按指定路径飞行。前端通过 **roslibjs** 直接连接机载 **rosbridge_server**,利用 ROS 话题和服务控制无人机,兼容**仿真模式**(Prometheus SITL)和**实机模式**(P600 WiFi 数传)。
+
+## 系统架构
+
+```
+Windows 浏览器 ──WebSocket──► rosbridge_server ──ROS话题/服务──► PX4 飞控
+ (地图+控制面板) (机载/仿真 ROS) (Prometheus/MAVROS)
+```
+
+- 前端通过 Leaflet 地图进行航点规划,通过 **roslibjs** 与 rosbridge_server 实时通信
+- rosbridge_server 将 WebSocket 消息转换为 ROS 话题发布/订阅和服务调用
+- 控制指令通过 `/prometheus/control_command` 话题发送到 Prometheus 控制节点
+- 无人机状态通过 `/prometheus/drone_state` 话题接收
+- 解锁/模式切换通过 MAVROS 服务(`/mavros/cmd/arming`、`/mavros/set_mode`)实现
+- QGC 可同时连接同一无人机做监控,互不干扰
+
+## 项目文件结构
+
+```
+软件前端/
+├── index.html # 主页面
+├── css/
+│ └── style.css # 样式文件
+├── js/
+│ ├── map.js # 地图模块(Leaflet 航点管理)
+│ ├── websocket.js # 通信模块(roslibjs + rosbridge)
+│ └── ui.js # 交互逻辑(按钮状态、日志)
+└── server/
+ ├── app.py # Flask 静态文件服务器(仅托管页面)
+ └── requirements.txt # Python 依赖(仅 flask)
+```
+
+---
+
+## 一、环境准备
+
+### 1.1 安装 Python 依赖(仅用于启动静态文件服务器)
+
+```bash
+cd server
+pip install -r requirements.txt
+```
+
+依赖内容:
+
+| 包名 | 版本 | 作用 |
+|------|------|------|
+| flask | 3.0.0 | Web 框架,托管前端页面 |
+
+### 1.2 确认 Python 版本
+
+需要 **Python 3.8+**:
+
+```bash
+python --version
+```
+
+### 1.3 ROS 端依赖(在无人机机载电脑或仿真环境中)
+
+需要在 ROS 环境中安装并运行以下组件:
+
+| 组件 | 作用 |
+|------|------|
+| rosbridge_suite | WebSocket-ROS 桥接,允许浏览器通过 WebSocket 与 ROS 通信 |
+| prometheus_msgs | Prometheus 自定义消息类型(ControlCommand、DroneState) |
+| MAVROS | ROS-MAVLink 桥接,提供服务调用(解锁、模式切换等) |
+
+安装 rosbridge_suite:
+
+```bash
+sudo apt install ros--rosbridge-suite
+# 例如 Ubuntu 20.04 + ROS Noetic:
+sudo apt install ros-noetic-rosbridge-suite
+```
+
+---
+
+## 二、仿真模式使用
+
+> 适用场景:在电脑上调试,不需要真机,使用 Prometheus SITL 仿真
+
+### 2.1 启动仿真环境(在 Ubuntu 虚拟机中)
+
+```bash
+# 1. 启动 Prometheus SITL 仿真
+cd ~/Prometheus/...
+roslaunch prometheus_gazebo sitl.launch
+
+# 2. 在新终端中启动 rosbridge_server
+roslaunch rosbridge_server rosbridge_websocket.launch
+```
+
+等待 Gazebo 窗口出现、PX4 终端显示 `pxh>` 即表示仿真就绪。rosbridge 默认监听端口 **9090**。
+
+> **提示**:如果后端运行在 Windows 上,仿真在虚拟机中,需要确保虚拟机网络为**桥接模式**,并能从 Windows 访问虚拟机 IP。
+
+### 2.2 启动前端页面服务器
+
+```bash
+cd server
+python app.py
+```
+
+浏览器访问:`http://localhost:5000`
+
+### 2.3 前端操作
+
+1. 页面顶栏点击 **「仿真模式」**,地址栏自动填入 `ws://localhost:9090`
+ - 如果仿真在虚拟机中,地址改为 `ws://<虚拟机IP>:9090`
+2. 点击 **「连接无人机」**,等待连接成功提示
+3. 在左侧地图上**点击添加航点**(可拖拽调整位置)
+4. 在右侧面板设置每个航点的**高度**和**悬停时间**
+5. 点击 **「上传任务」** → **「开始任务」**
+6. 系统将自动:切换 OFFBOARD 模式 → 解锁电机 → 起飞 → 按航点顺序飞行
+7. 可随时 **「暂停」** / **「继续」** / **「返航」** / **「降落」**
+
+---
+
+## 三、实机模式使用
+
+> 适用场景:连接真实的 P600 无人机进行飞行
+
+### 3.1 前置准备
+
+1. P600 无人机**已上电**
+2. 电脑连接 P600 的 **WiFi 数传**(WiFi 名称和密码见无人机底部标签)
+3. 确认能 ping 通机载电脑 IP(默认 `192.168.1.31`)
+4. 确认机载电脑上 **rosbridge_server 已启动**:
+ ```bash
+ # SSH 登录机载电脑后执行
+ roslaunch rosbridge_server rosbridge_websocket.launch
+ ```
+ 如果 rosbridge 已集成在 Prometheus 启动脚本中则无需手动启动。
+
+### 3.2 启动前端页面服务器
+
+```bash
+cd server
+python app.py
+```
+
+浏览器访问:`http://localhost:5000`
+
+### 3.3 前端操作
+
+1. 页面顶栏点击 **「实机模式」**,地址栏自动填入 `ws://192.168.1.31:9090`
+2. 点击 **「连接无人机」**,等待连接成功
+3. 在地图上规划航点 → 上传任务 → 开始任务(操作流程与仿真相同)
+
+> **注意**:实机飞行时,QGC 可同时连接(MAVLink 协议支持多客户端),用于辅助监控无人机状态。
+
+---
+
+## 四、界面说明
+
+### 4.1 顶部状态栏
+
+| 区域 | 内容 |
+|------|------|
+| 左侧 | 系统名称、连接状态标签、飞行模式、电量、GPS 状态、速度、高度 |
+| 右侧 | 仿真/实机模式切换、rosbridge 地址输入框、连接按钮 |
+
+### 4.2 左侧地图区域
+
+- **左键点击**:添加航点(蓝色编号标记)
+- **蓝色虚线**:航点之间的飞行路径
+- **蓝色三角图标**:无人机实时位置(随航向旋转)
+- **红色实线**:无人机已飞过的航迹
+- **拖拽标记**:移动航点位置
+
+### 4.3 右侧控制面板
+
+| 模块 | 功能 |
+|------|------|
+| 航点列表 | 显示所有航点的经纬度、高度;点击可定位;可单独删除 |
+| 默认参数 | 设置新建航点的默认高度(米)和悬停时间(秒) |
+| 任务操作 | 上传任务、开始任务、暂停/继续、返航、降落、清除航点 |
+| 实时数据 | 纬度、经度、航向、当前航点进度 |
+
+### 4.4 底部日志栏
+
+- 实时显示操作记录和状态消息
+- 颜色区分:绿色=成功,红色=错误,黄色=警告,灰色=信息
+
+---
+
+## 五、工作原理
+
+### 5.1 通信流程
+
+```
+浏览器 (roslibjs) ──WebSocket──► rosbridge_server (端口 9090)
+ │
+ ┌────────────────┼────────────────┐
+ ▼ ▼ ▼
+ /prometheus/ /prometheus/ /mavros/
+ control_command drone_state cmd/arming
+ (发布控制指令) (订阅无人机状态) (服务调用)
+```
+
+### 5.2 航点导航机制
+
+本系统采用**前端控制的逐航点导航**模式:
+
+1. 用户在地图上添加航点(经纬度 + 高度)
+2. 点击「上传任务」时,前端将经纬度转换为 ENU 坐标(以东经北天为坐标轴)
+3. 点击「开始任务」后,系统按以下顺序执行:
+ - 先发布控制指令(OFFBOARD 模式需要先有指令流)
+ - 切换 OFFBOARD 模式(MAVROS 服务调用)
+ - 解锁电机(MAVROS 服务调用)
+ - 发送起飞指令(Prometheus TAKEOFF 命令)
+ - 逐个航点发送 MOVE 指令,以 10Hz 频率持续发布
+ - 检测到达航点(距离 < 2 米),自动切换到下一个航点
+4. 所有航点完成后自动悬停
+
+### 5.3 坐标转换
+
+- 地图使用 WGS84 经纬度坐标
+- ROS/Prometheus 使用 ENU(东-北-天)局部坐标系
+- 系统在首次获取 GPS 定位时自动设置坐标原点
+- 所有航点在「上传任务」时从经纬度转换为相对于原点的 ENU 坐标
+
+---
+
+## 六、按钮状态说明
+
+| 状态 | 上传任务 | 开始任务 | 暂停 | 继续 | 返航 | 降落 |
+|------|----------|----------|------|------|------|------|
+| 未连接 | 禁用 | 禁用 | 禁用 | 禁用 | 禁用 | 禁用 |
+| 已连接(有航点) | 启用 | 禁用 | 禁用 | 禁用 | 启用 | 启用 |
+| 任务已上传 | 禁用 | 启用 | 禁用 | 禁用 | 启用 | 启用 |
+| 任务执行中 | 禁用 | 禁用 | 启用 | 禁用 | 启用 | 启用 |
+| 任务已暂停 | 禁用 | 禁用 | 禁用 | 启用 | 启用 | 启用 |
+
+---
+
+## 七、完整操作流程
+
+```
+ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
+ │ 启动 ROS │────►│ 连接前端 │────►│ 地图添加航点│────►│ 上传任务 │
+ │ SITL/实机 │ │ 选择模式 │ │ 点击地图 │ │ 转换坐标 │
+ └──────────┘ └──────────┘ └──────────┘ └────┬─────┘
+ │
+ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+ │ 任务完成 │◄────│ 飞行监控 │◄────│ 开始任务 │◄─────────┘
+ │ 或返航降落 │ │ 实时遥测 │ │ 解锁→起飞 │
+ └──────────┘ └──────────┘ └──────────┘
+```
+
+---
+
+## 八、连接参数参考
+
+| 参数 | 仿真模式 | 实机模式 |
+|------|----------|----------|
+| 协议 | WebSocket | WebSocket |
+| rosbridge 地址 | `ws://localhost:9090` | `ws://192.168.1.31:9090` |
+| 连接对象 | 本地/虚拟机 rosbridge | P600 机载电脑 rosbridge |
+| 网络要求 | 本地或虚拟机网络 | P600 WiFi 数传 |
+| ROS 话题(控制) | `/prometheus/control_command` | `/prometheus/control_command` |
+| ROS 话题(状态) | `/prometheus/drone_state` | `/prometheus/drone_state` |
+| ROS 话题(GPS) | `/mavros/global_position/raw/fix` | `/mavros/global_position/raw/fix` |
+| ROS 服务(解锁) | `/mavros/cmd/arming` | `/mavros/cmd/arming` |
+| ROS 服务(模式) | `/mavros/set_mode` | `/mavros/set_mode` |
+
+---
+
+## 九、常见问题
+
+### 连接超时怎么办?
+
+**仿真模式:**
+- 确认 Prometheus SITL 已启动(`roslaunch prometheus_gazebo sitl.launch`)
+- 确认 rosbridge_server 已启动(`roslaunch rosbridge_server rosbridge_websocket.launch`)
+- 如果仿真在虚拟机中,确认 Windows 能 ping 通虚拟机 IP
+- 确认虚拟机网络为桥接模式
+
+**实机模式:**
+- 确认电脑已连接 P600 的 WiFi 数传
+- 在终端执行 `ping 192.168.1.31` 确认网络通畅
+- 确认无人机已上电
+- 确认机载电脑上 rosbridge_server 正在运行
+- 关闭 VPN/防火墙等可能干扰网络的软件
+
+### 航点任务执行失败?
+
+- 确认无人机 GPS 已定位(状态栏显示 "3D Fix")
+- 航点高度建议设置在 10 米以上
+- 确认无人机已通过安全检查(传感器校准、解锁检查等)
+- 检查日志中的 ROS 话题和服务调用是否成功
+
+### Prometheus 话题收不到数据?
+
+- 确认 Prometheus 控制节点正在运行
+- 在 ROS 端检查话题是否存在:`rostopic list | grep prometheus`
+- 检查话题数据:`rostopic echo /prometheus/drone_state`
+- 确认 `prometheus_msgs` 包已正确编译和 source
+
+### 如何同时使用 QGC 监控?
+
+MAVLink 协议支持多客户端同时连接。QGC 通过 MAVLink 连接(UDP 14550),本系统通过 rosbridge 连接(WebSocket 9090),两者互不干扰,监控的是同一架无人机。
+
+### 默认航点参数可以改吗?
+
+可以。右侧面板的「默认高度」和「默认悬停时间」会影响后续新建的航点。已创建的航点参数不受影响。
diff --git a/src/软件前端/前端设计.html b/src/软件前端/前端设计.html
new file mode 100644
index 0000000..1e1faa5
--- /dev/null
+++ b/src/软件前端/前端设计.html
@@ -0,0 +1,1026 @@
+
+
+
+
+
+ 最后一公里补给系统 - UI原型(带跳转)
+
+
+
+
+
📱 最后一公里补给系统 - UI原型
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📱 单兵终端APP - 首页
+
+
+
+
+ 📡 信号: 98%
+ 🔋 电池: 85%
+
+
+
+
👤 前线士兵:张三
+
所属:第3步兵师/1连
+
+
+
💊 紧急物资需求上报
+
+
+
📦 当前库存状态
+
+
+
+
+
+
+
+
+
+
+ 📍 当前位置:A区街角12号
+ 更新
+
+
+
+ 📋 当前任务:#001 运输中
+ 查看
+
+
+
+ 👤 个人中心
+
+
+
+
+
+
+
+
📝 单兵终端APP - 物资需求上报
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回首页
+ 📝 填写物资信息
+
+
+
+
+
+
+
+
+
+
+ 发
+
+
+
+
+
+
+
+
+
+
已标注:
+
B区道路(危险区域)
+
2号楼地下(安全区域)
+
+
+
+
+
+
+
+
+
+
📋 单兵终端APP - 任务监控
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回首页
+ 📋 当前任务
+
+
+
+
+
📋 任务ID
+
#2024-05-20-001
+
🟢 执行中
+
预计到达:12:30
+
+
+
+
━━━━━━━━━━━━━━━━━━ 60%
+
剩余时间:12分钟
+
+
+
📍 飞行路径
+
+
🚀 起点
+
后方阵地
+
📍 目标
+
A区街角12号背侧
+
+
+
+
+
+
🚨 紧急操作
+
+
+
+
+
+
+
+
+
+
+
+
🎯 单兵终端APP - 投放点选择
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回
+ 🎯 选择投放点
+
+
+
+
🎯 推荐投放点列表
+
+
+
📍 安全点 #1
+
安全系数: 92% | 距离: 5m
+
原因: 深掩体保护,视角盲区
+
+
+
+
+
📍 安全点 #2
+
安全系数: 85% | 距离: 12m
+
原因: 钢筋混凝土建筑内部
+
+
+
+
+
⚠️ 陷阱点 #3
+
安全系数: 35% | 距离: 20m
+
原因: 孤立断墙,成瞄准点
+
+
+
+
+
+
+
+
+
+
+
🚀 单兵终端APP - 无人机状态
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回首页
+ 🚀 无人机状态
+
+
+
+
+
无人机ID无人机-01
+
任务ID#001
+
状态🟢 飞行中
+
当前位置A区街角12号
+
剩余电量85%
+
预计到达12:30
+
+
+
📊 实时数据
+
+
速度8.5m/s
+
高度15m
+
距离目标500m
+
温度38°C
+
+
+
📋 最近动态
+
+
12:25:30 到达投放点
+
12:20:45 接收任务指令
+
12:10:00 任务分配
+
+
+
+
+
+
+
+
📍 单兵终端APP - 位置更新
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回首页
+ 📍 位置更新
+
+
+
+
+
请求位置A区街角12号
+
当前位置A区街角15号
+
偏移距离50m
+
偏移时间5分钟前
+
+
+
+
+
+
⚠️ 注意事项
+
• 无人机距离新位置:约50米
+
• 无人机剩余电量:85%(可支持返回)
+
+
+
+
+
+
+
+
+
+
+
+
+
👤 单兵终端APP - 个人中心
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回首页
+ 👤 个人中心
+
+
+
+
+
👤
+
前线士兵:张三
+
所属:第3步兵师/1连 | 职位:狙击手
+
+
+
📊 任务统计
+
+
+
📋 功能菜单
+
+
+
+
+
+
+
+
+
+
+
+
+
⚙️ 单兵终端APP - 系统设置
+
+
+
+
🔋 电池: 85%
+
+
+ ← 返回
+ ⚙️ 系统设置
+
+
+
+
📋 常规设置
+
+
+
+
+
+
+
+
🔒 安全设置
+
+
+
+
+
+
+
ℹ️ 关于
+
+
+
+
+
+
+
+
+
+
+
+
🖥️ 后勤保障系统 - 任务调度
+
+
待处理需求5
+
执行中任务3
+
已完成128
+
无人机状态3空闲, 2飞行中
+
+
+
📋 待处理需求列表
+
+
+
任务ID#001
+
人员张三
+
物资20发弹药
+
位置A区街角12号
+
紧急程度🔴 紧急
+
+
+
+
任务ID#002
+
人员李四
+
物资5份食物
+
位置B区巷道5号
+
+
+
+
+
+
+
+
📋 后勤保障系统 - 需求详情
+
+
+ ← 返回列表
+ 需求详情 #001
+
+
+
+
📋 任务ID#2024-05-20-001
+
提交时间12:00
+
人员张三 (第3步兵师/1连)
+
状态待调度
+
+
+
📦 物资信息
+
+
物品名称弹药
+
数量20发
+
紧急程度🔴 紧急
+
+
+
📍 位置信息
+
+
请求位置A区街角12号
+
当前位置A区街角12号
+
+
+
⚙️ 调度选项
+
+
路径策略最安全 ← 选中
+
无人机选择无人机-02
+
预计到达时间12:28
+
+
+
+
+
+
+
+
+
+
+
+
🚀 后勤保障系统 - 无人机状态监控
+
+
+ ← 返回列表
+ 🚀 无人机监控
+
+
+
+
+
无人机ID无人机-01
+
所属人员张三
+
任务ID#001
+
状态🟢 飞行中
+
当前位置A区街角12号背侧
+
剩余电量85%
+
预计到达12:30
+
+
+
📊 实时数据
+
+
速度8.5m/s
+
高度15m
+
距离目标500m
+
信号强度98%
+
+
+
⚙️ 控制面板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vx.json b/vx.json
new file mode 100644
index 0000000..890ee4f
--- /dev/null
+++ b/vx.json
@@ -0,0 +1,24 @@
+{
+ "created_time": "2026-04-20T02:11:45Z",
+ "files": [
+ {
+ "attachment_folder": "",
+ "created_time": "2026-04-20T02:11:45Z",
+ "id": "1543",
+ "modified_time": "2026-04-20T02:11:45Z",
+ "name": "README.md",
+ "signature": "1126905480722352993",
+ "tags": [
+ ]
+ }
+ ],
+ "folders": [
+ {
+ "name": "document"
+ }
+ ],
+ "id": "1541",
+ "modified_time": "2026-04-20T02:11:45Z",
+ "signature": "5155401718504459105",
+ "version": 3
+}