/** * DJI无人机监控前端应用 */ class DroneMonitorApp { constructor() { this.ws = null; // 控制/状态WebSocket this.videoWs = null; // 视频流WebSocket this.isConnected = false; this.isVideoConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = 3000; // 状态数据 this.droneStatus = { device_sn: '', is_online: false, is_live: false, battery_level: 0, flight_mode: 'unknown', coordinates: { latitude: 0, longitude: 0, altitude: 0, heading: 0, pitch: 0, roll: 0 }, gimbal_attitude: { // 云台姿态数据 - 参考DJI Demo pitch: 0, roll: 0, yaw: 0 } }; // 视频相关 this.videoStats = { fps: 0, frameCount: 0, lastFrameTime: 0, latency: 0 }; // DOM元素 this.elements = {}; this.init(); } init() { console.log('🚀 初始化DJI无人机监控系统'); this.initElements(); this.connectWebSocket(); this.connectVideoWebSocket(); // 连接视频流WebSocket this.setupEventListeners(); } initElements() { // 状态指示器 this.elements.wsStatus = document.getElementById('wsStatus'); this.elements.wsStatusText = document.getElementById('wsStatusText'); this.elements.mqttStatus = document.getElementById('mqttStatus'); this.elements.mqttStatusText = document.getElementById('mqttStatusText'); this.elements.videoStatus = document.getElementById('videoStatus'); this.elements.videoStatusText = document.getElementById('videoStatusText'); // 设备信息 this.elements.deviceSn = document.getElementById('deviceSn'); this.elements.battery = document.getElementById('battery'); this.elements.flightMode = document.getElementById('flightMode'); // 坐标数据 this.elements.latitude = document.getElementById('latitude'); this.elements.longitude = document.getElementById('longitude'); this.elements.altitude = document.getElementById('altitude'); this.elements.heading = document.getElementById('heading'); this.elements.pitch = document.getElementById('pitch'); this.elements.roll = document.getElementById('roll'); // 云台姿态数据 - 参考DJI Demo this.elements.gimbalPitch = document.getElementById('gimbalPitch'); this.elements.gimbalRoll = document.getElementById('gimbalRoll'); this.elements.gimbalYaw = document.getElementById('gimbalYaw'); this.elements.cameraStatus = document.getElementById('cameraStatus'); // 视频相关 this.elements.videoPlaceholder = document.getElementById('videoPlaceholder'); this.elements.videoDisplay = document.getElementById('videoDisplay'); this.elements.videoOverlay = document.getElementById('videoOverlay'); this.elements.fps = document.getElementById('fps'); this.elements.latency = document.getElementById('latency'); // 控制按钮 this.elements.startStreamBtn = document.getElementById('startStreamBtn'); this.elements.stopStreamBtn = document.getElementById('stopStreamBtn'); } setupEventListeners() { // 页面可见性变化 document.addEventListener('visibilitychange', () => { if (document.hidden) { console.log('页面隐藏,暂停某些功能'); } else { console.log('页面显示,恢复功能'); if (!this.isConnected) { this.connectWebSocket(); } if (!this.isVideoConnected) { this.connectVideoWebSocket(); } } }); // 窗口关闭前 window.addEventListener('beforeunload', () => { if (this.ws) { this.ws.close(); } if (this.videoWs) { this.videoWs.close(); } }); } connectWebSocket() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.log('控制WebSocket已连接,跳过重复连接'); return; } const wsUrl = `ws://${window.location.hostname}:8765`; console.log(`🔌 连接控制WebSocket: ${wsUrl}`); this.updateWSStatus('connecting', 'WebSocket连接中...'); try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('✅ 控制WebSocket连接成功'); this.isConnected = true; this.reconnectAttempts = 0; this.updateWSStatus('online', 'WebSocket已连接'); this.showMessage('控制WebSocket连接成功', 'success'); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log('📨 收到控制WebSocket消息:', message.type, '数据大小:', event.data.length); this.handleMessage(message); } catch (error) { console.error('解析控制WebSocket消息失败:', error); console.error('原始消息:', event.data.substring(0, 200) + '...'); } }; this.ws.onclose = (event) => { console.log('❌ 控制WebSocket连接关闭', event.code, event.reason); this.isConnected = false; this.updateWSStatus('offline', 'WebSocket已断开'); if (!event.wasClean) { this.attemptReconnect(); } }; this.ws.onerror = (error) => { console.error('控制WebSocket连接错误:', error); this.updateWSStatus('offline', 'WebSocket连接失败'); this.showMessage('控制WebSocket连接失败', 'error'); }; } catch (error) { console.error('创建控制WebSocket连接失败:', error); this.updateWSStatus('offline', 'WebSocket创建失败'); this.showMessage('控制WebSocket创建失败', 'error'); } } connectVideoWebSocket() { if (this.videoWs && this.videoWs.readyState === WebSocket.OPEN) { console.log('视频WebSocket已连接,跳过重复连接'); return; } const videoWsUrl = `ws://${window.location.hostname}:8766`; console.log(`🔌 连接视频WebSocket: ${videoWsUrl}`); try { this.videoWs = new WebSocket(videoWsUrl); this.videoWs.onopen = () => { console.log('✅ 视频WebSocket连接成功'); this.isVideoConnected = true; this.showMessage('视频WebSocket连接成功', 'success'); }; this.videoWs.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log('📨 收到视频WebSocket消息:', message.type); this.handleVideoMessage(message); } catch (error) { console.error('解析视频WebSocket消息失败:', error); } }; this.videoWs.onclose = (event) => { console.log('❌ 视频WebSocket连接关闭', event.code, event.reason); this.isVideoConnected = false; if (!event.wasClean) { this.attemptVideoReconnect(); } }; this.videoWs.onerror = (error) => { console.error('视频WebSocket连接错误:', error); this.showMessage('视频WebSocket连接失败', 'error'); }; } catch (error) { console.error('创建视频WebSocket连接失败:', error); this.showMessage('视频WebSocket创建失败', 'error'); } } attemptVideoReconnect() { console.log('🔄 尝试重连视频WebSocket'); setTimeout(() => { this.connectVideoWebSocket(); }, this.reconnectInterval); } attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('达到最大重连次数,停止重连'); this.showMessage('连接失败,请手动重新连接', 'error'); return; } this.reconnectAttempts++; console.log(`🔄 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => { this.connectWebSocket(); }, this.reconnectInterval); } handleMessage(message) { console.log('📨 处理控制消息:', message.type, message.data ? Object.keys(message.data) : 'no data'); switch (message.type) { case 'connection_status': this.handleConnectionStatus(message.data); break; case 'drone_status': this.handleDroneStatus(message.data); break; case 'coordinates': this.handleCoordinates(message.data); break; case 'gimbal_attitude': // 云台姿态消息处理 - 参考DJI Demo this.handleGimbalAttitude(message.data); break; case 'error': this.handleError(message.data); break; default: console.log('未知控制消息类型:', message.type); } } handleVideoMessage(message) { console.log('📨 处理视频消息:', message.type); switch (message.type) { case 'video_frame': console.log('🎥 收到视频帧,frame大小:', message.data.frame ? message.data.frame.length : 'no frame'); this.handleVideoFrame(message.data); break; case 'connection_established': console.log('✅ 视频服务器连接已建立'); break; case 'stats': console.log('📊 视频服务器统计:', message.data); break; default: console.log('未知视频消息类型:', message.type); } } handleConnectionStatus(data) { console.log('连接状态更新:', data); if (data.status === 'connected') { this.showMessage(data.message, 'success'); } } handleDroneStatus(data) { console.log('无人机状态更新:', data); this.droneStatus = { ...this.droneStatus, ...data }; this.updateDroneStatusDisplay(); } handleCoordinates(data) { if (data.coordinates) { this.droneStatus.coordinates = { ...this.droneStatus.coordinates, ...data.coordinates }; this.updateCoordinatesDisplay(); } } handleGimbalAttitude(data) { console.log('📹 云台姿态更新:', data); if (data.gimbal_attitude) { this.droneStatus.gimbal_attitude = { ...this.droneStatus.gimbal_attitude, ...data.gimbal_attitude }; this.updateGimbalAttitudeDisplay(); // 更新相机视锥投影 - 调用index.html中的函数 if (window.updateGimbalData) { window.updateGimbalData( this.droneStatus.gimbal_attitude.pitch, this.droneStatus.gimbal_attitude.roll, this.droneStatus.gimbal_attitude.yaw ); } } } handleVideoFrame(data) { console.log('🎬 处理视频帧:', { device_sn: data.device_sn, frameSize: data.frame ? data.frame.length : 'no frame', timestamp: data.timestamp, width: data.width, height: data.height }); if (!data.frame) { console.error('❌ 视频帧数据为空'); return; } // 更新视频帧 const frameData = `data:image/jpeg;base64,${data.frame}`; this.elements.videoDisplay.src = frameData; // 显示视频 this.elements.videoPlaceholder.style.display = 'none'; this.elements.videoDisplay.style.display = 'block'; this.elements.videoOverlay.style.display = 'block'; // 更新视频状态 this.updateVideoStatus('online', '视频正常'); // 计算FPS和延迟 this.updateVideoStats(data.timestamp); console.log('✅ 视频帧更新完成'); } handleError(data) { console.error('服务器错误:', data.error); this.showMessage(data.error, 'error'); } updateDroneStatusDisplay() { // 更新设备信息 this.elements.deviceSn.textContent = this.droneStatus.device_sn || '-'; this.elements.battery.textContent = this.droneStatus.battery_level ? `${this.droneStatus.battery_level}%` : '-'; this.elements.flightMode.textContent = this.droneStatus.flight_mode || '-'; // 更新MQTT状态 if (this.droneStatus.is_online) { this.updateMQTTStatus('online', 'MQTT在线'); } else { this.updateMQTTStatus('offline', 'MQTT离线'); } // 更新按钮状态 this.updateControlButtons(); } updateCoordinatesDisplay() { const coords = this.droneStatus.coordinates; this.elements.latitude.textContent = coords.latitude ? coords.latitude.toFixed(6) : '-'; this.elements.longitude.textContent = coords.longitude ? coords.longitude.toFixed(6) : '-'; this.elements.altitude.textContent = coords.altitude ? `${coords.altitude.toFixed(1)}m` : '-'; this.elements.heading.textContent = coords.heading ? `${coords.heading.toFixed(1)}°` : '-'; this.elements.pitch.textContent = coords.pitch ? `${coords.pitch.toFixed(1)}°` : '-'; this.elements.roll.textContent = coords.roll ? `${coords.roll.toFixed(1)}°` : '-'; } updateGimbalAttitudeDisplay() { // 更新云台姿态显示 - 参考DJI Demo实现 const gimbal = this.droneStatus.gimbal_attitude; if (this.elements.gimbalPitch) { this.elements.gimbalPitch.textContent = gimbal.pitch ? `${gimbal.pitch.toFixed(1)}°` : '-'; } if (this.elements.gimbalRoll) { this.elements.gimbalRoll.textContent = gimbal.roll ? `${gimbal.roll.toFixed(1)}°` : '-'; } if (this.elements.gimbalYaw) { this.elements.gimbalYaw.textContent = gimbal.yaw ? `${gimbal.yaw.toFixed(1)}°` : '-'; } } updateVideoStats(frameTimestamp) { const now = Date.now(); // 计算延迟 this.videoStats.latency = now - (frameTimestamp * 1000); // 计算FPS if (this.videoStats.lastFrameTime > 0) { const timeDiff = (now - this.videoStats.lastFrameTime) / 1000; this.videoStats.fps = 1 / timeDiff; } this.videoStats.lastFrameTime = now; this.videoStats.frameCount++; // 更新显示 this.elements.fps.textContent = this.videoStats.fps.toFixed(1); this.elements.latency.textContent = this.videoStats.latency.toFixed(0); } updateControlButtons() { const isOnline = this.droneStatus.is_online; const isLive = this.droneStatus.is_live; this.elements.startStreamBtn.disabled = !isOnline || isLive; this.elements.stopStreamBtn.disabled = !isOnline || !isLive; } updateWSStatus(status, text) { this.elements.wsStatus.className = `status-indicator status-${status}`; this.elements.wsStatusText.textContent = text; } updateMQTTStatus(status, text) { this.elements.mqttStatus.className = `status-indicator status-${status}`; this.elements.mqttStatusText.textContent = text; } updateVideoStatus(status, text) { this.elements.videoStatus.className = `status-indicator status-${status}`; this.elements.videoStatusText.textContent = text; } showMessage(text, type = 'info') { const messageDiv = document.createElement('div'); messageDiv.className = `message ${type}`; messageDiv.textContent = text; document.getElementById('messageContainer').appendChild(messageDiv); // 显示消息 setTimeout(() => messageDiv.classList.add('show'), 100); // 自动隐藏 setTimeout(() => { messageDiv.classList.remove('show'); setTimeout(() => messageDiv.remove(), 300); }, 3000); } sendMessage(type, data = {}) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const message = { type: type, data: data, timestamp: Date.now() / 1000 }; this.ws.send(JSON.stringify(message)); return true; } else { console.error('WebSocket未连接,无法发送消息'); this.showMessage('WebSocket未连接', 'error'); return false; } } } // 全局函数 let app; window.onload = function() { app = new DroneMonitorApp(); }; function reconnectWebSocket() { console.log('🔄 手动重新连接WebSocket'); app.reconnectAttempts = 0; app.connectWebSocket(); app.connectVideoWebSocket(); } function startStream() { console.log('▶️ 开始直播'); app.sendMessage('start_stream'); app.showMessage('正在启动直播...', 'info'); } function stopStream() { console.log('⏹️ 停止直播'); app.sendMessage('stop_stream'); app.showMessage('正在停止直播...', 'info'); // 隐藏视频 app.elements.videoDisplay.style.display = 'none'; app.elements.videoOverlay.style.display = 'none'; app.elements.videoPlaceholder.style.display = 'block'; app.updateVideoStatus('offline', '视频离线'); } function toggleFullscreen() { const videoContainer = document.querySelector('.video-container'); if (!document.fullscreenElement) { videoContainer.requestFullscreen().catch(err => { console.error('进入全屏失败:', err); app.showMessage('全屏功能不可用', 'error'); }); } else { document.exitFullscreen(); } }